Move native builds to wholphin-extensions (#1172)

## Description
This PR moves the native components (ffmpeg decoder, av1 decoder, &
`libmpv`) to https://github.com/damontecres/wholphin-extensions. They
can be retrieved as gradle dependencies now.

This allows for contributors to more easily include these extensions
when building Wholphin locally.

There are no user facing changes in this PR.

Note: the kotlin code for interfacing with `libmpv`, ie `MPVLib` &
`MpvPlayer`, is still in this repo for now.

### Related issues
Related to #828

### Testing
Emulator

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Ray 2026-03-31 18:10:40 -04:00 committed by GitHub
parent ca43baa531
commit 4ea21be430
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 48 additions and 1806 deletions

View file

@ -1,82 +0,0 @@
name: "Native build"
description: "Performs native builds"
inputs:
cache:
description: "Whether to use the cache or not"
required: true
runs:
using: "composite"
steps:
- name: Load ffmpeg module cache
id: cache_ffmpeg_module
if: ${{ inputs.cache }}
uses: actions/cache/restore@v5
with:
path: |
app/libs
key: ${{ runner.os }}-ffmpeg-${{ hashFiles('**/libs.versions.toml', '**/scripts/ffmpeg/build_ffmpeg_decoder.sh') }}
- name: Load libmpv module cache
id: cache_libmpv_module
if: ${{ inputs.cache }}
uses: actions/cache/restore@v5
with:
path: |
app/src/main/libs
key: ${{ runner.os }}-libmpv-${{ hashFiles('**/scripts/mpv/*.sh', '**/scripts/mpv/include/*', '**/scripts/mpv/scripts/*', 'app/src/main/jni/*') }}
- name: Install dependencies
if: steps.cache_ffmpeg_module.outputs.cache-hit != 'true' || steps.cache_libmpv_module.outputs.cache-hit != 'true'
shell: bash
run: |
python -m pip install --upgrade pip
pip install jsonschema jinja2
sudo apt update
sudo apt install -y build-essential autoconf pkg-config libtool ninja-build unzip wget meson nasm
# ffmpeg
- name: Build ffmpeg decoder
id: ffmpeg-decoder
if: steps.cache_ffmpeg_module.outputs.cache-hit != 'true'
shell: bash
run: |
cd scripts/ffmpeg
./build_ffmpeg_decoder.sh "${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.NDK_VERSION }}"
- name: Save ffmpeg module cache
id: cache_ffmpeg_module_save
uses: actions/cache/save@v5
with:
path: |
app/libs
key: ${{ steps.cache_ffmpeg_module.outputs.cache-primary-key }}
# libmpv
- name: Get libmpv dependencies
if: steps.cache_libmpv_module.outputs.cache-hit != 'true'
shell: bash
run: |
cd scripts/mpv
./get_dependencies.sh
- name: Build libmpv dependencies
if: steps.cache_libmpv_module.outputs.cache-hit != 'true'
shell: bash
run: |
cd scripts/mpv
./buildall.sh --clean --arch arm64 mpv
./buildall.sh mpv
cd ../..
env PREFIX32="$(realpath scripts/mpv/prefix/armv7l)" PREFIX64="$(realpath scripts/mpv/prefix/arm64)" ndk-build -C app/src/main -j
- name: Setup jniLibs
shell: bash
run: |
cd app/src/main
mkdir -p jniLibs
cp -r libs/* jniLibs
#ln -s libs jniLibs
- name: Save libmpv module cache
id: cache_libmpv_module_save
uses: actions/cache/save@v5
with:
path: |
app/src/main/libs
key: ${{ steps.cache_libmpv_module.outputs.cache-primary-key }}

View file

@ -30,10 +30,6 @@ jobs:
fetch-depth: 0 # Need the tags to build fetch-depth: 0 # Need the tags to build
- name: Setup - name: Setup
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: Native build
uses: ./.github/actions/native-build
with:
cache: true
- name: Get version names - name: Get version names
run: | run: |
@ -55,8 +51,11 @@ jobs:
KEY_PASSWORD: "${{ secrets.KEY_PASSWORD }}" KEY_PASSWORD: "${{ secrets.KEY_PASSWORD }}"
KEY_STORE_PASSWORD: "${{ secrets.KEY_STORE_PASSWORD }}" KEY_STORE_PASSWORD: "${{ secrets.KEY_STORE_PASSWORD }}"
SIGNING_KEY: "${{ secrets.SIGNING_KEY }}" SIGNING_KEY: "${{ secrets.SIGNING_KEY }}"
ORG_GRADLE_PROJECT_WholphinExtensionsUsername: "${{ secrets.EXTENSIONS_USERNAME }}"
ORG_GRADLE_PROJECT_WholphinExtensionsPassword: "${{ secrets.EXTENSIONS_PASSWORD }}"
run: | run: |
./gradlew clean assembleRelease assembleDebug --no-daemon ./gradlew clean assembleRelease assembleDebug --no-daemon
- name: Verify signatures - name: Verify signatures
run: | run: |
echo "Verify APK signatures" echo "Verify APK signatures"

View file

@ -36,10 +36,6 @@ jobs:
fetch-depth: 0 # Need the tags to build fetch-depth: 0 # Need the tags to build
- name: Setup - name: Setup
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: Native build
uses: ./.github/actions/native-build
with:
cache: true
- name: Build app - name: Build app
id: buildapp id: buildapp
run: | run: |

View file

@ -26,10 +26,6 @@ jobs:
fetch-depth: 0 # Need the tags to build fetch-depth: 0 # Need the tags to build
- name: Setup - name: Setup
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: Native build
uses: ./.github/actions/native-build
with:
cache: false
- name: Build app - name: Build app
id: buildapp id: buildapp
env: env:
@ -37,6 +33,8 @@ jobs:
KEY_PASSWORD: "${{ secrets.KEY_PASSWORD }}" KEY_PASSWORD: "${{ secrets.KEY_PASSWORD }}"
KEY_STORE_PASSWORD: "${{ secrets.KEY_STORE_PASSWORD }}" KEY_STORE_PASSWORD: "${{ secrets.KEY_STORE_PASSWORD }}"
SIGNING_KEY: "${{ secrets.SIGNING_KEY }}" SIGNING_KEY: "${{ secrets.SIGNING_KEY }}"
ORG_GRADLE_PROJECT_WholphinExtensionsUsername: "${{ secrets.EXTENSIONS_USERNAME }}"
ORG_GRADLE_PROJECT_WholphinExtensionsPassword: "${{ secrets.EXTENSIONS_PASSWORD }}"
run: | run: |
./gradlew clean assembleRelease --no-daemon ./gradlew clean assembleRelease --no-daemon
@ -47,6 +45,8 @@ jobs:
KEY_PASSWORD: "${{ secrets.KEY_PASSWORD }}" KEY_PASSWORD: "${{ secrets.KEY_PASSWORD }}"
KEY_STORE_PASSWORD: "${{ secrets.KEY_STORE_PASSWORD }}" KEY_STORE_PASSWORD: "${{ secrets.KEY_STORE_PASSWORD }}"
SIGNING_KEY: "${{ secrets.SIGNING_KEY }}" SIGNING_KEY: "${{ secrets.SIGNING_KEY }}"
ORG_GRADLE_PROJECT_WholphinExtensionsUsername: "${{ secrets.EXTENSIONS_USERNAME }}"
ORG_GRADLE_PROJECT_WholphinExtensionsPassword: "${{ secrets.EXTENSIONS_PASSWORD }}"
run: | run: |
git apply app/src/patches/play_store.patch git apply app/src/patches/play_store.patch
./gradlew bundleRelease --no-daemon ./gradlew bundleRelease --no-daemon

View file

@ -43,21 +43,11 @@ Code is split into several packages:
- `ui` - User interface code and ViewModels - `ui` - User interface code and ViewModels
- `util` - Utility classes and functions - `util` - Utility classes and functions
### Native components ## Extensions
#### FFmpeg decoder module Wholphin uses several native components for extra playback compatibility. This includes Media3 ffmpeg/av1 decoders and `libmpv`. These extensions are not required to build the app, but without them some functionality will not work.
Wholphin ships with [media3 ffmpeg decoder module](https://github.com/androidx/media/blob/release/libraries/decoder_ffmpeg/README.md). If you want to include these in a local build, see the [instructions here](https://github.com/damontecres/wholphin-extensions?tab=readme-ov-file#usage) for configuring the repository.
It is not required to build the extension in order to build the app locally.
You can build the module on MacOS or Linux with the [`build_ffmpeg_decoder.sh`](./scripts/ffmpeg/build_ffmpeg_decoder.sh) script.
#### MPV player backend
Wholphin has a playback engine that uses [`libmpv`](https://github.com/mpv-player/mpv). The app uses JNI code from [`mpv-android`](https://github.com/mpv-android/mpv-android) and has an implementation of `androidx.media3.common.Player` to swap out for `ExoPlayer`.
See the [build scripts](scripts/mpv/) for details on building this component.
### App settings ### App settings

View file

@ -21,6 +21,8 @@ val isCI = if (System.getenv("CI") != null) System.getenv("CI").toBoolean() else
val shouldSign = isCI && System.getenv("KEY_ALIAS") != null val shouldSign = isCI && System.getenv("KEY_ALIAS") != null
val ffmpegModuleExists = project.file("libs/lib-decoder-ffmpeg-release.aar").exists() val ffmpegModuleExists = project.file("libs/lib-decoder-ffmpeg-release.aar").exists()
val av1ModuleExists = project.file("libs/lib-decoder-av1-release.aar").exists() val av1ModuleExists = project.file("libs/lib-decoder-av1-release.aar").exists()
val mpvModuleExists = project.file("libs/wholphin-mpv-release.aar").exists()
val extensionsRepoActive = project.hasProperty("WholphinExtensionsUsername")
val gitTags = val gitTags =
providers providers
@ -288,11 +290,33 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.test.manifest)
coreLibraryDesugaring(libs.desugar.jdk.libs) coreLibraryDesugaring(libs.desugar.jdk.libs)
if (ffmpegModuleExists || isCI) {
if (ffmpegModuleExists) {
logger.info("Using local ffmpeg decoder")
implementation(files("libs/lib-decoder-ffmpeg-release.aar")) implementation(files("libs/lib-decoder-ffmpeg-release.aar"))
} else if (extensionsRepoActive) {
logger.info("Using prebuilt ffmpeg decoder")
implementation(libs.wholphin.extensions.ffmpeg)
} else {
logger.warn("Media3 ffmpeg decoder was NOT found")
} }
if (av1ModuleExists || isCI) { if (av1ModuleExists) {
logger.info("Using local av1 decoder")
implementation(files("libs/lib-decoder-av1-release.aar")) implementation(files("libs/lib-decoder-av1-release.aar"))
} else if (extensionsRepoActive) {
logger.info("Using prebuilt av1 decoder")
implementation(libs.wholphin.extensions.av1)
} else {
logger.warn("Media3 av1 decoder was NOT found")
}
if (mpvModuleExists) {
logger.info("Using local libMPV build")
implementation(files("libs/wholphin-mpv-release.aar"))
} else if (extensionsRepoActive) {
logger.info("Using prebuilt libMPV")
implementation(libs.wholphin.extensions.mpv)
} else {
logger.warn("libMPV was NOT found")
} }
testImplementation(libs.mockk.android) testImplementation(libs.mockk.android)

View file

@ -1,82 +0,0 @@
LOCAL_PATH:= $(call my-dir)
ifeq ($(TARGET_ARCH_ABI),armeabi-v7a)
PREFIX = $(PREFIX32)
endif
ifeq ($(TARGET_ARCH_ABI),arm64-v8a)
PREFIX = $(PREFIX64)
endif
ifeq ($(TARGET_ARCH_ABI),x86_64)
PREFIX = $(PREFIX_X64)
endif
ifeq ($(TARGET_ARCH_ABI),x86)
PREFIX = $(PREFIX_X86)
endif
include $(CLEAR_VARS)
LOCAL_MODULE := libswresample
LOCAL_SRC_FILES := $(PREFIX)/lib/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libpostproc
LOCAL_SRC_FILES := $(PREFIX)/lib/$(LOCAL_MODULE).so
# only include if library file exists
ifneq (,$(wildcard $(LOCAL_SRC_FILES)))
include $(PREBUILT_SHARED_LIBRARY)
endif
include $(CLEAR_VARS)
LOCAL_MODULE := libavutil
LOCAL_SRC_FILES := $(PREFIX)/lib/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libavcodec
LOCAL_SRC_FILES := $(PREFIX)/lib/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libavformat
LOCAL_SRC_FILES := $(PREFIX)/lib/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libswscale
LOCAL_SRC_FILES := $(PREFIX)/lib/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libavfilter
LOCAL_SRC_FILES := $(PREFIX)/lib/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libavdevice
LOCAL_SRC_FILES := $(PREFIX)/lib/$(LOCAL_MODULE).so
LOCAL_EXPORT_C_INCLUDES := $(PREFIX)/include
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libmpv
LOCAL_SRC_FILES := $(PREFIX)/lib/libmpv.so
LOCAL_EXPORT_C_INCLUDES := $(PREFIX)/include
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libplayer
LOCAL_CFLAGS := -Werror
LOCAL_CPPFLAGS += -std=c++11
LOCAL_SRC_FILES := \
main.cpp \
render.cpp \
log.cpp \
jni_utils.cpp \
property.cpp \
event.cpp \
thumbnail.cpp
LOCAL_LDLIBS := -llog -lGLESv3 -lEGL -latomic
LOCAL_SHARED_LIBRARIES := swscale avcodec mpv
include $(BUILD_SHARED_LIBRARY)

View file

@ -1,17 +0,0 @@
APP_ABI :=
ifneq ($(PREFIX32),)
APP_ABI += armeabi-v7a
endif
ifneq ($(PREFIX64),)
APP_ABI += arm64-v8a
endif
ifneq ($(PREFIX_X64),)
APP_ABI += x86_64
endif
ifneq ($(PREFIX_X86),)
APP_ABI += x86
endif
APP_PLATFORM := android-21
APP_STL := c++_shared
APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true

View file

@ -1,16 +0,0 @@
# JNI bindings for libmpv
The files in this directory are copied from https://github.com/mpv-android/mpv-android/tree/master/app/src/main/jni
## License & copyright
The files are copyrighted and used under MIT license below:
Copyright (c) 2016 Ilya Zhuravlev
Copyright (c) 2016 sfan5 <sfan5@live.de>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,121 +0,0 @@
#include <jni.h>
#include <mpv/client.h>
#include "globals.h"
#include "jni_utils.h"
#include "log.h"
static void sendPropertyUpdateToJava(JNIEnv *env, mpv_event_property *prop)
{
jstring jprop = env->NewStringUTF(prop->name);
jstring jvalue = NULL;
switch (prop->format) {
case MPV_FORMAT_NONE:
env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_eventProperty_S, jprop);
break;
case MPV_FORMAT_FLAG:
env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_eventProperty_Sb, jprop,
(jboolean) (*(int*)prop->data != 0));
break;
case MPV_FORMAT_INT64:
env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_eventProperty_Sl, jprop,
(jlong) *(int64_t*)prop->data);
break;
case MPV_FORMAT_DOUBLE:
env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_eventProperty_Sd, jprop,
(jdouble) *(double*)prop->data);
break;
case MPV_FORMAT_STRING:
jvalue = env->NewStringUTF(*(const char**)prop->data);
env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_eventProperty_SS, jprop, jvalue);
break;
default:
ALOGV("sendPropertyUpdateToJava: Unknown property update format received in callback: %d!", prop->format);
break;
}
if (jprop)
env->DeleteLocalRef(jprop);
if (jvalue)
env->DeleteLocalRef(jvalue);
}
static void sendEventToJava(JNIEnv *env, int event)
{
env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_event, event);
}
static void sendEndFileEventToJava(JNIEnv *env, mpv_event_end_file *event)
{
env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_end_file_event, event->reason, event->error);
}
static void sendLogMessageToJava(JNIEnv *env, mpv_event_log_message *msg)
{
// filter the most obvious cases of invalid utf-8, since Java would choke on it
const auto invalid_utf8 = [] (unsigned char c) {
return c == 0xc0 || c == 0xc1 || c >= 0xf5;
};
for (int i = 0; msg->text[i]; i++) {
if (invalid_utf8(static_cast<unsigned char>(msg->text[i])))
return;
}
jstring jprefix = env->NewStringUTF(msg->prefix);
jstring jtext = env->NewStringUTF(msg->text);
env->CallStaticVoidMethod(mpv_MPVLib, mpv_MPVLib_logMessage_SiS,
jprefix, (jint) msg->log_level, jtext);
if (jprefix)
env->DeleteLocalRef(jprefix);
if (jtext)
env->DeleteLocalRef(jtext);
}
void *event_thread(void *arg)
{
JNIEnv *env = NULL;
acquire_jni_env(g_vm, &env);
if (!env)
die("failed to acquire java env");
while (1) {
mpv_event *mp_event;
mpv_event_property *mp_property = NULL;
mpv_event_log_message *msg = NULL;
mpv_event_end_file *mp_end_file = NULL;
mp_event = mpv_wait_event(g_mpv, -1.0);
if (g_event_thread_request_exit)
break;
if (mp_event->event_id == MPV_EVENT_NONE)
continue;
switch (mp_event->event_id) {
case MPV_EVENT_LOG_MESSAGE:
msg = (mpv_event_log_message*)mp_event->data;
ALOGV("[%s:%s] %s", msg->prefix, msg->level, msg->text);
sendLogMessageToJava(env, msg);
break;
case MPV_EVENT_PROPERTY_CHANGE:
mp_property = (mpv_event_property*)mp_event->data;
sendPropertyUpdateToJava(env, mp_property);
break;
case MPV_EVENT_END_FILE:
mp_end_file = (mpv_event_end_file*)mp_event->data;
sendEndFileEventToJava(env, mp_end_file);
break;
default:
ALOGV("event: %s\n", mpv_event_name(mp_event->event_id));
sendEventToJava(env, mp_event->event_id);
break;
}
}
g_vm->DetachCurrentThread();
return NULL;
}

View file

@ -1,3 +0,0 @@
#pragma once
void *event_thread(void *arg);

View file

@ -1,7 +0,0 @@
#pragma once
#include <atomic>
extern JavaVM *g_vm;
extern mpv_handle *g_mpv;
extern std::atomic<bool> g_event_thread_request_exit;

View file

@ -1,52 +0,0 @@
#define UTIL_EXTERN
#include "jni_utils.h"
#include <jni.h>
#include <stdlib.h>
bool acquire_jni_env(JavaVM *vm, JNIEnv **env)
{
int ret = vm->GetEnv((void**) env, JNI_VERSION_1_6);
if (ret == JNI_EDETACHED)
return vm->AttachCurrentThread(env, NULL) == 0;
else
return ret == JNI_OK;
}
// Apparently it's considered slow to FindClass and GetMethodID every time we need them,
// so let's have a nice cache here.
void init_methods_cache(JNIEnv *env)
{
static bool methods_initialized = false;
if (methods_initialized)
return;
#define FIND_CLASS(name) reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass(name)))
java_Integer = FIND_CLASS("java/lang/Integer");
java_Integer_init = env->GetMethodID(java_Integer, "<init>", "(I)V");
java_Double = FIND_CLASS("java/lang/Double");
java_Double_init = env->GetMethodID(java_Double, "<init>", "(D)V");
java_Boolean = FIND_CLASS("java/lang/Boolean");
java_Boolean_init = env->GetMethodID(java_Boolean, "<init>", "(Z)V");
android_graphics_Bitmap = FIND_CLASS("android/graphics/Bitmap");
// createBitmap(int[], int, int, android.graphics.Bitmap$Config)
android_graphics_Bitmap_createBitmap = env->GetStaticMethodID(android_graphics_Bitmap, "createBitmap", "([IIILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
android_graphics_Bitmap_Config = FIND_CLASS("android/graphics/Bitmap$Config");
// static final android.graphics.Bitmap$Config ARGB_8888
android_graphics_Bitmap_Config_ARGB_8888 = env->GetStaticFieldID(android_graphics_Bitmap_Config, "ARGB_8888", "Landroid/graphics/Bitmap$Config;");
mpv_MPVLib = FIND_CLASS("com/github/damontecres/wholphin/util/mpv/MPVLib");
mpv_MPVLib_eventProperty_S = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;)V"); // eventProperty(String)
mpv_MPVLib_eventProperty_Sb = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;Z)V"); // eventProperty(String, boolean)
mpv_MPVLib_eventProperty_Sl = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;J)V"); // eventProperty(String, long)
mpv_MPVLib_eventProperty_Sd = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;D)V"); // eventProperty(String, double)
mpv_MPVLib_eventProperty_SS = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;Ljava/lang/String;)V"); // eventProperty(String, String)
mpv_MPVLib_event = env->GetStaticMethodID(mpv_MPVLib, "event", "(I)V"); // event(int)
mpv_MPVLib_end_file_event = env->GetStaticMethodID(mpv_MPVLib, "eventEndFile", "(II)V"); // eventEndFile(int, int)
mpv_MPVLib_logMessage_SiS = env->GetStaticMethodID(mpv_MPVLib, "logMessage", "(Ljava/lang/String;ILjava/lang/String;)V"); // logMessage(String, int, String)
#undef FIND_CLASS
methods_initialized = true;
}

View file

@ -1,30 +0,0 @@
#pragma once
#include <jni.h>
#define jni_func_name(name) Java_com_github_damontecres_wholphin_util_mpv_MPVLib_##name
#define jni_func(return_type, name, ...) JNIEXPORT return_type JNICALL jni_func_name(name) (JNIEnv *env, jobject obj, ##__VA_ARGS__)
bool acquire_jni_env(JavaVM *vm, JNIEnv **env);
void init_methods_cache(JNIEnv *env);
#ifndef UTIL_EXTERN
#define UTIL_EXTERN extern
#endif
UTIL_EXTERN jclass java_Integer, java_Double, java_Boolean;
UTIL_EXTERN jmethodID java_Integer_init, java_Double_init, java_Boolean_init;
UTIL_EXTERN jclass android_graphics_Bitmap, android_graphics_Bitmap_Config;
UTIL_EXTERN jmethodID android_graphics_Bitmap_createBitmap;
UTIL_EXTERN jfieldID android_graphics_Bitmap_Config_ARGB_8888;
UTIL_EXTERN jclass mpv_MPVLib;
UTIL_EXTERN jmethodID mpv_MPVLib_eventProperty_S,
mpv_MPVLib_eventProperty_Sb,
mpv_MPVLib_eventProperty_Sl,
mpv_MPVLib_eventProperty_Sd,
mpv_MPVLib_eventProperty_SS,
mpv_MPVLib_event,
mpv_MPVLib_end_file_event,
mpv_MPVLib_logMessage_SiS;

View file

@ -1,9 +0,0 @@
#include "log.h"
#include <stdlib.h>
void die(const char *msg)
{
ALOGE("%s", msg);
exit(1);
}

View file

@ -1,20 +0,0 @@
#pragma once
#include <android/log.h>
#define DEBUG 0
#define LOG_TAG "mpv"
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#if DEBUG
#define ALOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#else
#define ALOGV(...) (void)0
#endif
__attribute__((noreturn)) void die(const char *msg);
#define CHECK_MPV_INIT() do { \
if (__builtin_expect(!g_mpv, 0)) \
die("libmpv is not initialized"); \
} while (0)

View file

@ -1,108 +0,0 @@
#include <jni.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <locale.h>
#include <atomic>
#include <mpv/client.h>
#include <pthread.h>
extern "C" {
#include <libavcodec/jni.h>
}
#include "log.h"
#include "jni_utils.h"
#include "event.h"
#define ARRAYLEN(a) (sizeof(a)/sizeof(a[0]))
extern "C" {
jni_func(void, create, jobject appctx);
jni_func(void, init);
jni_func(void, destroy);
jni_func(void, command, jobjectArray jarray);
};
JavaVM *g_vm;
mpv_handle *g_mpv;
std::atomic<bool> g_event_thread_request_exit(false);
static pthread_t event_thread_id;
static void prepare_environment(JNIEnv *env, jobject appctx) {
setlocale(LC_NUMERIC, "C");
if (!env->GetJavaVM(&g_vm) && g_vm)
av_jni_set_java_vm(g_vm, NULL);
jobject global_appctx = env->NewGlobalRef(appctx);
if (global_appctx)
av_jni_set_android_app_ctx(global_appctx, NULL);
init_methods_cache(env);
}
jni_func(void, create, jobject appctx) {
prepare_environment(env, appctx);
if (g_mpv)
die("mpv is already initialized");
g_mpv = mpv_create();
if (!g_mpv)
die("context init failed");
// use terminal log level but request verbose messages
// this way --msg-level can be used to adjust later
mpv_request_log_messages(g_mpv, "terminal-default");
mpv_set_option_string(g_mpv, "msg-level", "all=v");
}
jni_func(void, init) {
if (!g_mpv)
die("mpv is not created");
if (mpv_initialize(g_mpv) < 0)
die("mpv init failed");
g_event_thread_request_exit = false;
if (pthread_create(&event_thread_id, NULL, event_thread, NULL) != 0)
die("thread create failed");
pthread_setname_np(event_thread_id, "event_thread");
}
jni_func(void, destroy) {
if (!g_mpv) {
ALOGV("mpv destroy called but it's already destroyed");
return;
}
// poke event thread and wait for it to exit
g_event_thread_request_exit = true;
mpv_wakeup(g_mpv);
pthread_join(event_thread_id, NULL);
mpv_terminate_destroy(g_mpv);
g_mpv = NULL;
}
jni_func(void, command, jobjectArray jarray) {
CHECK_MPV_INIT();
const char *arguments[128] = {0};
int len = env->GetArrayLength(jarray);
if (len >= ARRAYLEN(arguments))
die("too many command arguments");
for (int i = 0; i < len; ++i)
arguments[i] = env->GetStringUTFChars((jstring)env->GetObjectArrayElement(jarray, i), NULL);
mpv_command(g_mpv, arguments);
for (int i = 0; i < len; ++i)
env->ReleaseStringUTFChars((jstring)env->GetObjectArrayElement(jarray, i), arguments[i]);
}

View file

@ -1,125 +0,0 @@
#include <jni.h>
#include <stdlib.h>
#include <mpv/client.h>
#include "jni_utils.h"
#include "log.h"
#include "globals.h"
extern "C" {
jni_func(jint, setOptionString, jstring option, jstring value);
jni_func(jobject, getPropertyInt, jstring property);
jni_func(void, setPropertyInt, jstring property, jint value);
jni_func(jobject, getPropertyDouble, jstring property);
jni_func(void, setPropertyDouble, jstring property, jdouble value);
jni_func(jobject, getPropertyBoolean, jstring property);
jni_func(void, setPropertyBoolean, jstring property, jboolean value);
jni_func(jstring, getPropertyString, jstring jproperty);
jni_func(void, setPropertyString, jstring jproperty, jstring jvalue);
jni_func(void, observeProperty, jstring property, jint format);
}
jni_func(jint, setOptionString, jstring joption, jstring jvalue) {
CHECK_MPV_INIT();
const char *option = env->GetStringUTFChars(joption, NULL);
const char *value = env->GetStringUTFChars(jvalue, NULL);
int result = mpv_set_option_string(g_mpv, option, value);
env->ReleaseStringUTFChars(joption, option);
env->ReleaseStringUTFChars(jvalue, value);
return result;
}
static int common_get_property(JNIEnv *env, jstring jproperty, mpv_format format, void *output)
{
CHECK_MPV_INIT();
const char *prop = env->GetStringUTFChars(jproperty, NULL);
int result = mpv_get_property(g_mpv, prop, format, output);
if (result == MPV_ERROR_PROPERTY_UNAVAILABLE)
ALOGV("mpv_get_property(%s) format %d was unavailable", prop, format);
else if (result < 0)
ALOGE("mpv_get_property(%s) format %d returned error %s", prop, format, mpv_error_string(result));
env->ReleaseStringUTFChars(jproperty, prop);
return result;
}
static int common_set_property(JNIEnv *env, jstring jproperty, mpv_format format, void *value)
{
CHECK_MPV_INIT();
const char *prop = env->GetStringUTFChars(jproperty, NULL);
int result = mpv_set_property(g_mpv, prop, format, value);
if (result < 0)
ALOGE("mpv_set_property(%s, %p) format %d returned error %s", prop, value, format, mpv_error_string(result));
env->ReleaseStringUTFChars(jproperty, prop);
return result;
}
jni_func(jobject, getPropertyInt, jstring jproperty) {
int64_t value = 0;
if (common_get_property(env, jproperty, MPV_FORMAT_INT64, &value) < 0)
return NULL;
return env->NewObject(java_Integer, java_Integer_init, (jint)value);
}
jni_func(jobject, getPropertyDouble, jstring jproperty) {
double value = 0;
if (common_get_property(env, jproperty, MPV_FORMAT_DOUBLE, &value) < 0)
return NULL;
return env->NewObject(java_Double, java_Double_init, (jdouble)value);
}
jni_func(jobject, getPropertyBoolean, jstring jproperty) {
int value = 0;
if (common_get_property(env, jproperty, MPV_FORMAT_FLAG, &value) < 0)
return NULL;
return env->NewObject(java_Boolean, java_Boolean_init, (jboolean)value);
}
jni_func(jstring, getPropertyString, jstring jproperty) {
char *value;
if (common_get_property(env, jproperty, MPV_FORMAT_STRING, &value) < 0)
return NULL;
jstring jvalue = env->NewStringUTF(value);
mpv_free(value);
return jvalue;
}
jni_func(void, setPropertyInt, jstring jproperty, jint jvalue) {
int64_t value = static_cast<int64_t>(jvalue);
common_set_property(env, jproperty, MPV_FORMAT_INT64, &value);
}
jni_func(void, setPropertyDouble, jstring jproperty, jdouble jvalue) {
double value = static_cast<double>(jvalue);
common_set_property(env, jproperty, MPV_FORMAT_DOUBLE, &value);
}
jni_func(void, setPropertyBoolean, jstring jproperty, jboolean jvalue) {
int value = jvalue == JNI_TRUE ? 1 : 0;
common_set_property(env, jproperty, MPV_FORMAT_FLAG, &value);
}
jni_func(void, setPropertyString, jstring jproperty, jstring jvalue) {
const char *value = env->GetStringUTFChars(jvalue, NULL);
common_set_property(env, jproperty, MPV_FORMAT_STRING, &value);
env->ReleaseStringUTFChars(jvalue, value);
}
jni_func(void, observeProperty, jstring property, jint format) {
CHECK_MPV_INIT();
const char *prop = env->GetStringUTFChars(property, NULL);
int result = mpv_observe_property(g_mpv, 0, prop, (mpv_format)format);
if (result < 0)
ALOGE("mpv_observe_property(%s) format %d returned error %s", prop, format, mpv_error_string(result));
env->ReleaseStringUTFChars(property, prop);
}

View file

@ -1,38 +0,0 @@
#include <jni.h>
#include <mpv/client.h>
#include "jni_utils.h"
#include "log.h"
#include "globals.h"
extern "C" {
jni_func(void, attachSurface, jobject surface_);
jni_func(void, detachSurface);
};
static jobject surface;
jni_func(void, attachSurface, jobject surface_) {
CHECK_MPV_INIT();
surface = env->NewGlobalRef(surface_);
if (!surface)
die("invalid surface provided");
int64_t wid = reinterpret_cast<intptr_t>(surface);
int result = mpv_set_option(g_mpv, "wid", MPV_FORMAT_INT64, &wid);
if (result < 0)
ALOGE("mpv_set_option(wid) returned error %s", mpv_error_string(result));
}
jni_func(void, detachSurface) {
CHECK_MPV_INIT();
int64_t wid = 0;
int result = mpv_set_option(g_mpv, "wid", MPV_FORMAT_INT64, &wid);
if (result < 0)
ALOGE("mpv_set_option(wid) returned error %s", mpv_error_string(result));
env->DeleteGlobalRef(surface);
surface = NULL;
}

View file

@ -1,133 +0,0 @@
#include <stdlib.h>
#include <string>
#include <jni.h>
#include <android/bitmap.h>
#include <mpv/client.h>
extern "C" {
#include <libswscale/swscale.h>
};
#include "jni_utils.h"
#include "globals.h"
#include "log.h"
extern "C" {
jni_func(jobject, grabThumbnail, jint dimension);
};
static inline mpv_node make_node_str(const char *s)
{
mpv_node r{};
r.format = MPV_FORMAT_STRING;
r.u.string = const_cast<char*>(s);
return r;
}
jni_func(jobject, grabThumbnail, jint dimension) {
CHECK_MPV_INIT();
mpv_node result{};
{
mpv_node c{}, c_args[2];
mpv_node_list c_array{};
c_args[0] = make_node_str("screenshot-raw");
c_args[1] = make_node_str("video");
c_array.num = 2;
c_array.values = c_args;
c.format = MPV_FORMAT_NODE_ARRAY;
c.u.list = &c_array;
if (mpv_command_node(g_mpv, &c, &result) < 0) {
ALOGE("screenshot-raw command failed");
return NULL;
}
}
// extract relevant property data from the node map mpv returns
int w = 0, h = 0, stride = 0;
bool format_ok = false;
struct mpv_byte_array *data = NULL;
do {
if (result.format != MPV_FORMAT_NODE_MAP)
break;
for (int i = 0; i < result.u.list->num; i++) {
std::string key(result.u.list->keys[i]);
const mpv_node *val = &result.u.list->values[i];
if (key == "w" || key == "h" || key == "stride") {
if (val->format != MPV_FORMAT_INT64)
break;
if (key == "w")
w = val->u.int64;
else if (key == "h")
h = val->u.int64;
else
stride = val->u.int64;
} else if (key == "format") {
if (val->format != MPV_FORMAT_STRING)
break;
format_ok = !strcmp(val->u.string, "bgr0");
} else if (key == "data") {
if (val->format != MPV_FORMAT_BYTE_ARRAY)
break;
data = val->u.ba;
}
}
} while (0);
if (!w || !h || !stride || !format_ok || !data) {
ALOGE("extracting data failed");
mpv_free_node_contents(&result);
return NULL;
}
ALOGV("screenshot w:%d h:%d stride:%d", w, h, stride);
// crop to square
int crop_left = 0, crop_top = 0;
int new_w = w, new_h = h;
if (w > h) {
crop_left = (w - h) / 2;
new_w = h;
} else {
crop_top = (h - w) / 2;
new_h = w;
}
ALOGV("cropped w:%u h:%u", new_w, new_h);
uint8_t *new_data = reinterpret_cast<uint8_t*>(data->data);
new_data += crop_left * sizeof(uint32_t); // move begin rightwards
new_data += stride * crop_top; // move begin downwards
// convert & scale to appropriate size
struct SwsContext *ctx = sws_getContext(
new_w, new_h, AV_PIX_FMT_BGR0,
dimension, dimension, AV_PIX_FMT_RGB32,
SWS_BICUBIC, NULL, NULL, NULL);
if (!ctx) {
mpv_free_node_contents(&result);
return NULL;
}
jintArray arr = env->NewIntArray(dimension * dimension);
jint *scaled = env->GetIntArrayElements(arr, NULL);
uint8_t *src_p[4] = { new_data }, *dst_p[4] = { (uint8_t*) scaled };
int src_stride[4] = { stride },
dst_stride[4] = { (int) sizeof(jint) * dimension };
sws_scale(ctx, src_p, src_stride, 0, new_h, dst_p, dst_stride);
sws_freeContext(ctx);
mpv_free_node_contents(&result); // frees data->data
// create android.graphics.Bitmap
env->ReleaseIntArrayElements(arr, scaled, 0);
jobject bitmap_config =
env->GetStaticObjectField(android_graphics_Bitmap_Config, android_graphics_Bitmap_Config_ARGB_8888);
jobject bitmap =
env->CallStaticObjectMethod(android_graphics_Bitmap, android_graphics_Bitmap_createBitmap,
arr, dimension, dimension, bitmap_config);
env->DeleteLocalRef(arr);
env->DeleteLocalRef(bitmap_config);
return bitmap;
}

View file

@ -44,8 +44,12 @@ kotlinxCoroutinesTest = "1.10.2"
coreTesting = "2.2.0" coreTesting = "2.2.0"
openapi-generator = "7.21.0" openapi-generator = "7.21.0"
runner = "1.7.0" runner = "1.7.0"
wholphin-extensions = "0.1.0"
[libraries] [libraries]
wholphin-extensions-mpv = { module = "com.github.damontecres.wholphin.mpv:wholphin-mpv", version.ref = "wholphin-extensions" }
wholphin-extensions-ffmpeg = { module = "com.github.damontecres.wholphin.media3:decoder-ffmpeg", version.ref = "wholphin-extensions" }
wholphin-extensions-av1 = { module = "com.github.damontecres.wholphin.media3:decoder-av1", version.ref = "wholphin-extensions" }
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" } aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" }
aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutLibraries" } aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutLibraries" }
acra-dialog = { module = "ch.acra:acra-dialog", version.ref = "acra" } acra-dialog = { module = "ch.acra:acra-dialog", version.ref = "acra" }

View file

@ -1,14 +0,0 @@
# FFmpeg ExoPlayer decoder extension module
Builds the ffmpeg decoder extension module for `ExoPlayer` which supports extra codecs during playback.
## Usage
```sh
./build_ffmpeg_decoder.sh /path/to/android/ndk
```
Or clean build:
```sh
./build_ffmpeg_decoder.sh /path/to/android/ndk --clean
```

View file

@ -1,97 +0,0 @@
#!/usr/bin/env bash
set -ex
if [ -z "$1" ]; then
echo "Error: Must provide NDK path"
exit 1
fi
NDK_PATH="$1"
SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"
SCRIPT_DIR="$(dirname "${SCRIPT_PATH}")"
PROJECT_ROOT="$(realpath "${SCRIPT_DIR}/../../")"
# Config
ANDROID_ABI=21
ENABLED_DECODERS=(dca ac3 eac3 mlp truehd flac alac pcm_mulaw pcm_alaw mp3)
FFMPEG_BRANCH="release/6.0"
DAV1D_BRANCH="1.5.3"
# Path configs
DIR_PATH="$(pwd)"
TARGET_PATH="$PROJECT_ROOT/app/libs"
MEDIA_PATH="$DIR_PATH/ffmpeg_decoder/media"
FFMPEG_MODULE_PATH="$MEDIA_PATH/libraries/decoder_ffmpeg/src/main"
FFMPEG_PATH="$DIR_PATH/ffmpeg_decoder/ffmpeg"
AV1_MODULE_PATH="$MEDIA_PATH/libraries/decoder_av1/src/main"
HOST="$(uname -s | tr '[:upper:]' '[:lower:]')"
HOST_PLATFORM="$HOST-x86_64"
if [[ "$2" == "--clean" ]]; then
rm -rf ffmpeg_decoder
rm -f "$TARGET_PATH/lib-decoder-ffmpeg-release.aar"
rm -f "$TARGET_PATH/lib-decoder-av1-release.aar"
fi
mkdir -p "$TARGET_PATH"
mkdir -p ffmpeg_decoder
echo "$PROJECT_ROOT/gradle/libs.versions.toml"
media_version="$(grep "androidx-media3 = " "$PROJECT_ROOT/gradle/libs.versions.toml" | awk -F'"' '{print $2}')"
pushd ffmpeg_decoder || exit
if [[ -d media ]]; then
pushd media || exit
git fetch origin "$media_version" --depth 1
git checkout --force FETCH_HEAD
else
git clone https://github.com/androidx/media.git --depth 1 --single-branch -b "$media_version" media
fi
if [[ -d ffmpeg ]]; then
pushd ffmpeg || exit
git fetch origin "$FFMPEG_BRANCH" --depth 1
git checkout --force FETCH_HEAD
else
git clone https://github.com/FFmpeg/FFmpeg --depth 1 --single-branch -b "$FFMPEG_BRANCH" ffmpeg
fi
[[ ! -d "${FFMPEG_MODULE_PATH}/jni/ffmpeg" ]] && ln -s "$FFMPEG_PATH" "${FFMPEG_MODULE_PATH}/jni/ffmpeg"
pushd "${FFMPEG_MODULE_PATH}/jni" || exit
./build_ffmpeg.sh "${FFMPEG_MODULE_PATH}" "${NDK_PATH}" "${HOST_PLATFORM}" "${ANDROID_ABI}" "${ENABLED_DECODERS[@]}"
# av1 module
pushd "$AV1_MODULE_PATH/jni" || exit
if [[ ! -d cpu_features ]]; then
git clone https://github.com/google/cpu_features --depth 1 --single-branch cpu_features
fi
pushd "$AV1_MODULE_PATH/jni" || exit
if [[ -d dav1d ]]; then
pushd dav1d || exit
git fetch origin "$DAV1D_BRANCH" --depth 1
git checkout --force FETCH_HEAD
else
git clone https://code.videolan.org/videolan/dav1d --depth 1 --single-branch -b "$DAV1D_BRANCH" dav1d
fi
pushd "$AV1_MODULE_PATH/jni" || exit
/usr/bin/env bash ./build_dav1d.sh "${AV1_MODULE_PATH}" "${NDK_PATH}" "${HOST_PLATFORM}"
pushd "$MEDIA_PATH" || exit
./gradlew :lib-decoder-ffmpeg:assemble :lib-decoder-av1:assemble
popd || exit
popd || exit
cp "$MEDIA_PATH/libraries/decoder_ffmpeg/buildout/outputs/aar/lib-decoder-ffmpeg-release.aar" "$TARGET_PATH/"
cp "$MEDIA_PATH/libraries/decoder_av1/buildout/outputs/aar/lib-decoder-av1-release.aar" "$TARGET_PATH/"
popd || exit

View file

@ -1,22 +0,0 @@
# MPV build scripts
This scripts are adapted from https://github.com/mpv-android/mpv-android/tree/ae0d956c5a98ab8bf25af7e2c73bcb59e19c15b7/buildscripts licensed MIT.
## Instructions
```bash
cd scripts/mpv
./get_dependencies.sh
# Install build dependencies
pip install meson jsonschema
export NDK_PATH=... # Such as ~/Library/Android/sdk/ndk/29.0.14206865
# Build arm64
PATH="$PATH:$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64/bin" ./buildall.sh --clean --arch arm64 mpv
# Build arm32
PATH="$PATH:$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64/bin" ./buildall.sh mpv
cd ../..
env PREFIX32="$(realpath scripts/mpv/prefix/armv7l)" PREFIX64="$(realpath scripts/mpv/prefix/arm64)" "$NDK_PATH/ndk-build" -C app/src/main -j && cp -fr app/src/main/libs/ app/src/main/jnilibs/
```

View file

@ -1,180 +0,0 @@
#!/bin/bash -e
cd "$( dirname "${BASH_SOURCE[0]}" )"
. ./include/depinfo.sh
cleanbuild=0
nodeps=0
clang=1
target=mpv-android
arch=armv7l
getdeps () {
varname="dep_${1//-/_}[*]"
echo ${!varname}
}
loadarch () {
unset CC CXX CPATH LIBRARY_PATH C_INCLUDE_PATH CPLUS_INCLUDE_PATH
unset CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
local apilvl=21
# ndk_triple: what the toolchain actually is
# cc_triple: what Google pretends the toolchain is
if [ "$1" == "armv7l" ]; then
export ndk_suffix=
export ndk_triple=arm-linux-androideabi
cc_triple=armv7a-linux-androideabi$apilvl
prefix_name=armv7l
elif [ "$1" == "arm64" ]; then
export ndk_suffix=-arm64
export ndk_triple=aarch64-linux-android
cc_triple=$ndk_triple$apilvl
prefix_name=arm64
elif [ "$1" == "x86" ]; then
export ndk_suffix=-x86
export ndk_triple=i686-linux-android
cc_triple=$ndk_triple$apilvl
prefix_name=x86
elif [ "$1" == "x86_64" ]; then
export ndk_suffix=-x64
export ndk_triple=x86_64-linux-android
cc_triple=$ndk_triple$apilvl
prefix_name=x86_64
else
echo "Invalid architecture" >&2
exit 1
fi
export prefix_dir="$PWD/prefix/$prefix_name"
if [ $clang -eq 1 ]; then
export CC=$cc_triple-clang
export CXX=$cc_triple-clang++
else
export CC=$cc_triple-gcc
export CXX=$cc_triple-g++
fi
export LDFLAGS="-Wl,-O1,--icf=safe -Wl,-z,max-page-size=16384"
export AR=llvm-ar
export RANLIB=llvm-ranlib
}
setup_prefix () {
if [ ! -d "$prefix_dir" ]; then
mkdir -p "$prefix_dir"
# enforce flat structure (/usr/local -> /)
ln -s . "$prefix_dir/usr"
ln -s . "$prefix_dir/local"
fi
local cpu_family=${ndk_triple%%-*}
[ "$cpu_family" == "i686" ] && cpu_family=x86
if ! command -v pkg-config >/dev/null; then
echo "pkg-config not provided!"
return 1
fi
# meson wants to be spoonfed this file, so create it ahead of time
# also define: release build, static libs and no source downloads at runtime(!!!)
cat >"$prefix_dir/crossfile.tmp" <<CROSSFILE
[built-in options]
buildtype = 'release'
default_library = 'static'
wrap_mode = 'nodownload'
prefix = '/usr/local'
[binaries]
c = '$CC'
cpp = '$CXX'
ar = 'llvm-ar'
nm = 'llvm-nm'
strip = 'llvm-strip'
pkgconfig = 'pkg-config'
pkg-config = 'pkg-config'
[host_machine]
system = 'android'
cpu_family = '$cpu_family'
cpu = '${CC%%-*}'
endian = 'little'
CROSSFILE
# also avoid rewriting it needlessly
if cmp -s "$prefix_dir"/crossfile.{tmp,txt}; then
rm "$prefix_dir/crossfile.tmp"
else
mv "$prefix_dir"/crossfile.{tmp,txt}
fi
}
build () {
if [ $1 != "mpv-android" ] && [ ! -d deps/$1 ]; then
printf >&2 '\e[1;31m%s\e[m\n' "Target $1 not found"
return 1
fi
if [ $nodeps -eq 0 ]; then
printf >&2 '\e[1;34m%s\e[m\n' "Preparing $1..."
local deps=$(getdeps $1)
echo >&2 "Dependencies: $deps"
for dep in $deps; do
build $dep
done
fi
printf >&2 '\e[1;34m%s\e[m\n' "Building $1..."
if [ "$1" == "mpv-android" ]; then
pushd ..
BUILDSCRIPT=buildscripts/scripts/$1.sh
else
pushd deps/$1
BUILDSCRIPT=../../scripts/$1.sh
fi
[ $cleanbuild -eq 1 ] && $BUILDSCRIPT clean
$BUILDSCRIPT build
popd
}
usage () {
printf '%s\n' \
"Usage: buildall.sh [options] [target]" \
"Builds the specified target (default: $target)" \
"-n Do not build dependencies" \
"--clean Clean build dirs before compiling" \
"--gcc Use gcc compiler (unsupported!)" \
"--arch <arch> Build for specified architecture (default: $arch; supported: armv7l, arm64, x86, x86_64)"
exit 0
}
while [ $# -gt 0 ]; do
case "$1" in
--clean)
cleanbuild=1
;;
-n|--no-deps)
nodeps=1
;;
--gcc)
clang=0
;;
--arch)
shift
arch=$1
;;
-h|--help)
usage
;;
-*)
echo "Unknown flag $1" >&2
exit 1
;;
*)
target=$1
;;
esac
shift
done
loadarch $arch
setup_prefix
build $target
[ "$target" == "mpv-android" ] && \
ls -lh ../app/build/outputs/apk/{default,api29}/*/*.apk
exit 0

View file

@ -1,75 +0,0 @@
#!/usr/bin/env bash
set -exou pipefail
BUILD_PATH="./deps"
mkdir -p "$BUILD_PATH"
pushd "$BUILD_PATH" || exit
function clone(){
repo=$1
branch=$2
dir=$3
shift 3
if [[ -d "$dir" ]]; then
pushd "$dir" || exit
git fetch origin "$branch" --depth 1
git checkout --force FETCH_HEAD
popd || exit
else
git clone "$repo" --depth 1 --single-branch -b "$branch" "$dir" "$@"
fi
}
clone "https://github.com/videolan/dav1d" "1.5.3" dav1d
clone "https://github.com/FFmpeg/FFmpeg" "n8.0" ffmpeg
clone "https://gitlab.freedesktop.org/freetype/freetype.git" "VER-2-14-1" freetype2 --recurse-submodules
clone "https://github.com/libass/libass" "0.17.4" libass
rm -rf libplacebo
git clone "https://github.com/haasn/libplacebo" --single-branch -b "master" --recurse-submodules libplacebo
pushd libplacebo || exit
git checkout 1bd8d2d6a715bc870bdffa6759e62c419000dd51
popd || exit
clone "https://github.com/mpv-player/mpv" "v0.41.0" mpv
if [[ ! -d mbedtls ]]; then
mkdir mbedtls
wget https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-3.6.4/mbedtls-3.6.4.tar.bz2 -O - | \
tar -xj -C mbedtls --strip-components=1
fi
if [[ ! -d fribidi ]]; then
mkdir fribidi
wget https://github.com/fribidi/fribidi/releases/download/v1.0.16/fribidi-1.0.16.tar.xz -O - | \
tar -xJ -C fribidi --strip-components=1
fi
if [[ ! -d harfbuzz ]]; then
mkdir harfbuzz
wget https://github.com/harfbuzz/harfbuzz/releases/download/12.1.0/harfbuzz-12.1.0.tar.xz -O - | \
tar -xJ -C harfbuzz --strip-components=1
fi
version_unibreak="6.1"
if [[ ! -d unibreak ]]; then
mkdir unibreak
wget https://github.com/adah1972/libunibreak/releases/download/libunibreak_${version_unibreak//./_}/libunibreak-${version_unibreak}.tar.gz -O - | \
tar -xz -C unibreak --strip-components=1
fi
if [[ ! -d lua ]]; then
mkdir lua
wget https://www.lua.org/ftp/lua-5.2.4.tar.gz -O - | \
tar -xz -C lua --strip-components=1
fi
# python packages: jsonschema jinja2 meson
popd || exit

View file

@ -1,91 +0,0 @@
#!/bin/bash -e
# go to buildscripts root folder
cd "$( dirname "${BASH_SOURCE[0]}" )/.."
. ./include/depinfo.sh
msg() {
printf '==> %s\n' "$1"
}
fetch_prefix() {
if [[ "$CACHE_MODE" == folder ]]; then
local text=
if [ -f "$CACHE_FOLDER/id.txt" ]; then
text=$(cat "$CACHE_FOLDER/id.txt")
else
echo "Cache seems to be empty"
fi
printf 'Expecting "%s",\nfound "%s".\n' "$ci_tarball" "$text"
if [[ "$text" == "$ci_tarball" ]]; then
tar -xzf "$CACHE_FOLDER/data.tgz" -C prefix && return 0
fi
fi
return 1
}
build_prefix() {
msg "Building the prefix ($ci_tarball)..."
msg "Fetching deps"
IN_CI=1 ./include/download-deps.sh
# build everything mpv depends on (but not mpv itself)
for x in ${dep_mpv[@]}; do
msg "Building $x"
./buildall.sh $x
done
if [[ "$CACHE_MODE" == folder && -w "$CACHE_FOLDER" ]]; then
msg "Compressing the prefix"
tar -cvzf "$CACHE_FOLDER/data.tgz" -C prefix .
echo "$ci_tarball" >"$CACHE_FOLDER/id.txt"
fi
}
export WGET="wget --progress=bar:force"
if [ "$1" = "export" ]; then
# export variable with unique cache identifier
echo "CACHE_IDENTIFIER=$ci_tarball"
exit 0
elif [ "$1" = "install" ]; then
# install deps
if [[ -n "$ANDROID_HOME" && -d "$ANDROID_HOME" ]]; then
msg "Linking existing SDK"
mkdir -p sdk
ln -sv "$ANDROID_HOME" sdk/android-sdk-linux
fi
msg "Fetching SDK + NDK"
IN_CI=1 ./include/download-sdk.sh
msg "Fetching mpv"
mkdir -p deps/mpv
$WGET https://github.com/mpv-player/mpv/archive/master.tar.gz -O master.tgz
tar -xzf master.tgz -C deps/mpv --strip-components=1
rm master.tgz
msg "Trying to fetch existing prefix"
mkdir -p prefix
fetch_prefix || build_prefix
exit 0
elif [ "$1" = "build" ]; then
# run build
:
else
exit 1
fi
msg "Building mpv"
./buildall.sh -n mpv || {
# show logfile if configure failed
[ ! -f deps/mpv/_build/config.h ] && cat deps/mpv/_build/meson-logs/meson-log.txt
exit 1
}
msg "Building mpv-android"
./buildall.sh -n
exit 0

View file

@ -1,43 +0,0 @@
#!/bin/bash -e
## Dependency versions
# Make sure to keep v_ndk and v_ndk_n in sync, both are listed on the NDK download page
v_sdk=11076708_latest
v_ndk=r29
v_ndk_n=29.0.14206865
v_sdk_platform=35
v_sdk_build_tools=35.0.0
v_lua=5.2.4
v_unibreak=6.1
v_harfbuzz=12.1.0
v_fribidi=1.0.16
v_freetype=2.14.1
v_mbedtls=3.6.4
## Dependency tree
# I would've used a dict but putting arrays in a dict is not a thing
dep_mbedtls=()
dep_dav1d=()
dep_ffmpeg=(mbedtls dav1d)
dep_freetype2=()
dep_fribidi=()
dep_harfbuzz=()
dep_unibreak=()
dep_libass=(freetype2 fribidi harfbuzz unibreak)
dep_lua=()
dep_libplacebo=()
dep_mpv=(ffmpeg libass lua libplacebo)
dep_mpv_android=(mpv)
## for CI workflow
# pinned ffmpeg revision
v_ci_ffmpeg=n8.0
# filename used to uniquely identify a build prefix
ci_tarball="prefix-ndk-${v_ndk}-lua-${v_lua}-unibreak-${v_unibreak}-harfbuzz-${v_harfbuzz}-fribidi-${v_fribidi}-freetype-${v_freetype}-mbedtls-${v_mbedtls}-ffmpeg-${v_ci_ffmpeg}.tgz"

View file

@ -1,32 +0,0 @@
#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
. "$DIR/include/depinfo.sh"
os=linux
[[ "$OSTYPE" == "darwin"* ]] && os=mac
export os
if [ "$os" == "mac" ]; then
[ -z "$cores" ] && cores=$(sysctl -n hw.ncpu)
# various things rely on GNU behaviour
export INSTALL=`which ginstall`
export SED=gsed
else
[ -z "$cores" ] && cores=$(grep -c ^processor /proc/cpuinfo)
fi
cores=${cores:-4}
# configure pkg-config paths if inside buildscripts
if [ -n "$ndk_triple" ]; then
export PKG_CONFIG_SYSROOT_DIR="$prefix_dir"
export PKG_CONFIG_LIBDIR="$PKG_CONFIG_SYSROOT_DIR/lib/pkgconfig"
unset PKG_CONFIG_PATH
fi
toolchain=$(echo "$DIR/sdk/android-ndk-${v_ndk}/toolchains/llvm/prebuilt/"*)
[ -d "$toolchain" ] && \
export PATH="$toolchain/bin:$DIR/sdk/android-ndk-${v_ndk}:$DIR/sdk/bin:$PATH"
export ANDROID_HOME="$DIR/sdk/android-sdk-$os"
unset ANDROID_SDK_ROOT ANDROID_NDK_ROOT

View file

@ -1,22 +0,0 @@
#!/bin/bash -e
. ../../include/path.sh
build=_build$ndk_suffix
if [ "$1" == "build" ]; then
true
elif [ "$1" == "clean" ]; then
rm -rf $build
exit 0
else
exit 255
fi
unset CC CXX # meson wants these unset
meson setup $build --cross-file "$prefix_dir"/crossfile.txt \
-Denable_tests=false -Db_lto=true -Dstack_alignment=16
ninja -C $build -j$cores
DESTDIR="$prefix_dir" ninja -C $build install

View file

@ -1,48 +0,0 @@
#!/bin/bash -e
. ../../include/path.sh
if [ "$1" == "build" ]; then
true
elif [ "$1" == "clean" ]; then
rm -rf _build$ndk_suffix
exit 0
else
exit 255
fi
mkdir -p _build$ndk_suffix
cd _build$ndk_suffix
cpu=armv7-a
[[ "$ndk_triple" == "aarch64"* ]] && cpu=armv8-a
[[ "$ndk_triple" == "x86_64"* ]] && cpu=generic
[[ "$ndk_triple" == "i686"* ]] && cpu="i686 --disable-asm"
cpuflags=
[[ "$ndk_triple" == "arm"* ]] && cpuflags="$cpuflags -mfpu=neon -mcpu=cortex-a8"
args=(
--target-os=android --enable-cross-compile
--cross-prefix=$ndk_triple- --cc=$CC --pkg-config=pkg-config --nm=llvm-nm
--arch=${ndk_triple%%-*} --cpu=$cpu
--extra-cflags="-I$prefix_dir/include $cpuflags" --extra-ldflags="-L$prefix_dir/lib"
--enable-{jni,mediacodec,mbedtls,libdav1d} --disable-vulkan
--disable-static --enable-shared --enable-{gpl,version3}
# disable unneeded parts
--disable-{stripping,doc,programs}
# to keep the build lean we disable some feature quite aggressively:
# - muxers, encoders: mpv-android does not have any way to use these
# - devices: no practical use on Android
--disable-{muxers,encoders,devices}
# useful to taking screenshots
--enable-encoder=mjpeg,png
# useful for the `dump-cache` command
--enable-muxer=mov,matroska,mpegts
)
../configure "${args[@]}"
make -j$cores
make DESTDIR="$prefix_dir" install

View file

@ -1,21 +0,0 @@
#!/bin/bash -e
. ../../include/path.sh
build=_build$ndk_suffix
if [ "$1" == "build" ]; then
true
elif [ "$1" == "clean" ]; then
rm -rf $build
exit 0
else
exit 255
fi
unset CC CXX # meson wants these unset
meson setup $build --cross-file "$prefix_dir"/crossfile.txt
ninja -C $build -j$cores
DESTDIR="$prefix_dir" ninja -C $build install

View file

@ -1,22 +0,0 @@
#!/bin/bash -e
. ../../include/path.sh
build=_build$ndk_suffix
if [ "$1" == "build" ]; then
true
elif [ "$1" == "clean" ]; then
rm -rf $build
exit 0
else
exit 255
fi
unset CC CXX # meson wants these unset
meson setup $build --cross-file "$prefix_dir"/crossfile.txt \
-D{tests,docs}=false
ninja -C $build -j$cores
DESTDIR="$prefix_dir" ninja -C $build install

View file

@ -1,22 +0,0 @@
#!/bin/bash -e
. ../../include/path.sh
build=_build$ndk_suffix
if [ "$1" == "build" ]; then
true
elif [ "$1" == "clean" ]; then
rm -rf $build
exit 0
else
exit 255
fi
unset CC CXX # meson wants these unset
meson setup $build --cross-file "$prefix_dir"/crossfile.txt \
-Dtests=disabled -Ddocs=disabled
ninja -C $build -j$cores
DESTDIR="$prefix_dir" ninja -C $build install

View file

@ -1,25 +0,0 @@
#!/bin/bash -e
. ../../include/path.sh
if [ "$1" == "build" ]; then
true
elif [ "$1" == "clean" ]; then
rm -rf _build$ndk_suffix
exit 0
else
exit 255
fi
[ -f configure ] || ./autogen.sh
mkdir -p _build$ndk_suffix
cd _build$ndk_suffix
../configure \
--host=$ndk_triple --with-pic \
--enable-static --disable-shared \
--enable-libunibreak --disable-require-system-font-provider
make -j$cores
make DESTDIR="$prefix_dir" install

View file

@ -1,25 +0,0 @@
#!/bin/bash -e
. ../../include/path.sh
build=_build$ndk_suffix
if [ "$1" == "build" ]; then
true
elif [ "$1" == "clean" ]; then
rm -rf $build
exit 0
else
exit 255
fi
unset CC CXX
meson setup $build --cross-file "$prefix_dir"/crossfile.txt \
-Dvulkan=disabled -Ddemos=false
ninja -C $build -j$cores
DESTDIR="$prefix_dir" ninja -C $build install
# add missing library for static linking
# this isn't "-lstdc++" due to a meson bug: https://github.com/mesonbuild/meson/issues/11300
${SED:-sed} '/^Libs:/ s|$| -lc++|' "$prefix_dir/lib/pkgconfig/libplacebo.pc" -i

View file

@ -1,44 +0,0 @@
#!/bin/bash -e
. ../../include/path.sh
if [ "$1" == "build" ]; then
true
elif [ "$1" == "clean" ]; then
make clean
exit 0
else
exit 255
fi
# Building seperately from source tree is not supported, this means we are forced to always clean
$0 clean
mycflags=(
# ensures correct linking into libmpv.so
-fPIC
# bionic is missing decimal_point in localeconv [src/llex.c]
-Dgetlocaledecpoint\\\(\\\)=\\\(46\\\)
# force fallback as ftello/fseeko are not defined [src/liolib.c]
-Dlua_fseek
)
# LUA_T= and LUAC_T= to disable building lua & luac
# -Dgetlocaledecpoint()=('.') fixes bionic missing decimal_point in localeconv
make CC="$CC" AR="$AR rc" RANLIB="$RANLIB" \
MYCFLAGS="${mycflags[*]}" \
PLAT=linux LUA_T= LUAC_T= -j$cores
# TO_BIN=/dev/null disables installing lua & luac
make INSTALL=${INSTALL:-install} INSTALL_TOP="$prefix_dir" TO_BIN=/dev/null install
# make pc only generates a partial pkg-config file because ????
mkdir -p $prefix_dir/lib/pkgconfig
make pc >$prefix_dir/lib/pkgconfig/lua.pc
cat >>$prefix_dir/lib/pkgconfig/lua.pc <<'EOF'
Name: Lua
Description:
Version: ${version}
Libs: -L${libdir} -llua
Cflags: -I${includedir}
EOF

View file

@ -1,22 +0,0 @@
#!/bin/bash -e
. ../../include/path.sh
if [ "$1" == "build" ]; then
true
elif [ "$1" == "clean" ]; then
make clean
exit 0
else
exit 255
fi
$0 clean # separate building not supported, always clean
if [[ "$ndk_triple" == "i686"* ]]; then
./scripts/config.py unset MBEDTLS_AESNI_C
else
./scripts/config.py set MBEDTLS_AESNI_C
fi
make -j$cores no_test
make DESTDIR="$prefix_dir" install

View file

@ -1,77 +0,0 @@
#!/bin/bash -e
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BUILD="$DIR/.."
MPV_ANDROID="$DIR/../.."
. $BUILD/include/path.sh
. $BUILD/include/depinfo.sh
if [ "$1" == "build" ]; then
true
elif [ "$1" == "clean" ]; then
rm -rf $MPV_ANDROID/{app,.}/build $MPV_ANDROID/app/src/main/{libs,obj}
exit 0
else
exit 255
fi
[ -n "$ANDROID_SIGNING_KEY" ] && BUNDLE=1
nativeprefix () {
if [ -f $BUILD/prefix/$1/lib/libmpv.so ]; then
echo $BUILD/prefix/$1
else
echo >&2 "Warning: libmpv.so not found in native prefix for $1, support will be omitted"
fi
}
prefix32=$(nativeprefix "armv7l")
prefix64=$(nativeprefix "arm64")
prefix_x64=$(nativeprefix "x86_64")
prefix_x86=$(nativeprefix "x86")
if [[ -z "$prefix32" && -z "$prefix64" && -z "$prefix_x64" && -z "$prefix_x86" ]]; then
echo >&2 "Error: no mpv library detected."
exit 255
fi
PREFIX32=$prefix32 PREFIX64=$prefix64 PREFIX_X64=$prefix_x64 PREFIX_X86=$prefix_x86 \
ndk-build -C app/src/main -j$cores
targets=(assembleDebug)
if [ -z "$DONT_BUILD_RELEASE" ]; then
targets+=(assembleRelease)
[ -n "$BUNDLE" ] && targets+=(bundleRelease)
fi
./gradlew "${targets[@]}"
if [ -n "$ANDROID_SIGNING_KEY" ]; then
cd "${MPV_ANDROID}/app/build/outputs/apk"
apksigner=${ANDROID_HOME}/build-tools/${v_sdk_build_tools}/apksigner
for v in default api29; do
pushd $v
# sign the universal debug APK
"$apksigner" sign --ks "${ANDROID_SIGNING_KEY}" \
--in debug/app-$v-universal-debug.apk --out debug/app-$v-universal-debug-signed.apk
# but all of the release APKs
for apk in release/*-unsigned.apk; do
"$apksigner" sign --ks "${ANDROID_SIGNING_KEY}" \
--in $apk --out ${apk/-unsigned/-signed}
done
popd
done
# and the bundle
cd ../bundle
if [ -n "$BUNDLE" ]; then
if [ -z "$ANDROID_SIGNING_ALIAS" ]; then
echo >&2 "Error: ANDROID_SIGNING_ALIAS must be set to use jarsigner"
exit 1
fi
pushd defaultRelease
jarsigner -keystore "${ANDROID_SIGNING_KEY}" -signedjar \
app-default-release-signed.aab app-default-release.aab \
"${ANDROID_SIGNING_ALIAS}"
popd
fi
fi

View file

@ -1,30 +0,0 @@
#!/bin/bash -e
. ../../include/path.sh
build=_build$ndk_suffix
if [ "$1" == "build" ]; then
true
elif [ "$1" == "clean" ]; then
rm -rf $build
exit 0
else
exit 255
fi
unset CC CXX # meson wants these unset
meson setup $build --cross-file "$prefix_dir"/crossfile.txt \
--default-library shared \
-Diconv=disabled -Dlua=enabled \
-Dlibmpv=true -Dcplayer=false \
-Dmanpage-build=disabled
ninja -C $build -j$cores
if [ -f $build/libmpv.a ]; then
echo >&2 "Meson fucked up, forcing rebuild."
$0 clean
exec $0 build
fi
DESTDIR="$prefix_dir" ninja -C $build install

View file

@ -1,24 +0,0 @@
#!/bin/bash -e
. ../../include/path.sh
build=_build$ndk_suffix
if [ "$1" == "build" ]; then
true
elif [ "$1" == "clean" ]; then
rm -rf $build
exit 0
else
exit 255
fi
mkdir -p $build
cd $build
../configure \
--host=$ndk_triple --with-pic \
--enable-static --disable-shared
make -j$cores
make DESTDIR="$prefix_dir" install

View file

@ -16,6 +16,14 @@ dependencyResolutionManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
@Suppress("ktlint:standard:property-naming")
val WholphinExtensionsUsername: String? by settings
if (!WholphinExtensionsUsername.isNullOrBlank()) {
maven("https://maven.pkg.github.com/damontecres/wholphin-extensions") {
name = "WholphinExtensions"
credentials(PasswordCredentials::class)
}
}
} }
} }