mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Add experimental MPV player backend (#161)
Experimental MPV player backend Related to #14, #22, & #85 You can switch to MPV in advanced settings and toggle using hardware decoding or not. This uses code and buildscripts from https://github.com/mpv-android/mpv-android, plus other third party libraries to build MPV
This commit is contained in:
parent
d7b333e519
commit
6be2662d4e
84 changed files with 3987 additions and 289 deletions
79
.github/actions/native-build/action.yml
vendored
Normal file
79
.github/actions/native-build/action.yml
vendored
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
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@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
app/libs
|
||||||
|
key: ${{ runner.os }}-ffmpeg-${{ hashFiles('**/libs.versions.toml', '**/scripts/ffmpeg/build_ffmpeg_decoder.sh') }}
|
||||||
|
- 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@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
app/libs
|
||||||
|
key: ${{ steps.cache_ffmpeg_module.outputs.cache-primary-key }}
|
||||||
|
|
||||||
|
- name: Load libmpv module cache
|
||||||
|
id: cache_libmpv_module
|
||||||
|
if: ${{ inputs.cache }}
|
||||||
|
uses: actions/cache/restore@v4
|
||||||
|
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_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
|
||||||
|
- 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@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
app/src/main/libs
|
||||||
|
key: ${{ steps.cache_libmpv_module.outputs.cache-primary-key }}
|
||||||
31
.github/actions/setup/action.yml
vendored
Normal file
31
.github/actions/setup/action.yml
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
name: Setup
|
||||||
|
description: "Setup the environment"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
# Setup the SDKs
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
- name: Setup JDK
|
||||||
|
uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
distribution: zulu
|
||||||
|
java-version: '21'
|
||||||
|
cache: 'gradle'
|
||||||
|
- name: Set versions
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "BUILD_TOOLS_VERSION=36.0.0" >> $GITHUB_ENV
|
||||||
|
echo "NDK_VERSION=29.0.14206865" >> $GITHUB_ENV
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
with:
|
||||||
|
packages: "tools platform-tools build-tools;${{ env.BUILD_TOOLS_VERSION }} ndk;${{ env.NDK_VERSION }}"
|
||||||
|
- name: Add NDK to path
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "Adding to path: ${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.NDK_VERSION }}:${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.NDK_VERSION }}/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
||||||
|
echo "${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.NDK_VERSION }}:${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.NDK_VERSION }}/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_PATH
|
||||||
68
.github/workflows/main.yml
vendored
68
.github/workflows/main.yml
vendored
|
|
@ -15,11 +15,7 @@ concurrency:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
APK_SHORT_NAME: Wholphin-release.apk
|
|
||||||
APK_DEBUG_SHORT_NAME: Wholphin-debug.apk
|
|
||||||
BRANCH_NAME: ${{ github.ref_name }}
|
BRANCH_NAME: ${{ github.ref_name }}
|
||||||
BUILD_TOOLS_VERSION: 36.0.0
|
|
||||||
NDK_VERSION: "29.0.14206865"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
@ -31,17 +27,12 @@ jobs:
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Need the tags to build
|
fetch-depth: 0 # Need the tags to build
|
||||||
# Setup the SDKs
|
- name: Setup
|
||||||
- name: Setup JDK
|
uses: ./.github/actions/setup
|
||||||
uses: actions/setup-java@v5
|
- name: Native build
|
||||||
|
uses: ./.github/actions/native-build
|
||||||
with:
|
with:
|
||||||
distribution: zulu
|
cache: true
|
||||||
java-version: '21'
|
|
||||||
cache: 'gradle'
|
|
||||||
- name: Setup Android SDK
|
|
||||||
uses: android-actions/setup-android@v3
|
|
||||||
with:
|
|
||||||
packages: "tools platform-tools build-tools;${{ env.BUILD_TOOLS_VERSION }} ndk;${{ env.NDK_VERSION }}"
|
|
||||||
|
|
||||||
- name: Get version names
|
- name: Get version names
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -56,25 +47,6 @@ jobs:
|
||||||
echo "LATEST_TAG=$LATEST_TAG" >> "$GITHUB_ENV"
|
echo "LATEST_TAG=$LATEST_TAG" >> "$GITHUB_ENV"
|
||||||
echo "VERSION_NAME=$VERSION_NAME" >> "$GITHUB_ENV"
|
echo "VERSION_NAME=$VERSION_NAME" >> "$GITHUB_ENV"
|
||||||
echo "TAG_NAME=$TAG_NAME" >> "$GITHUB_ENV"
|
echo "TAG_NAME=$TAG_NAME" >> "$GITHUB_ENV"
|
||||||
- name: Load ffmpeg module cache
|
|
||||||
id: cache_ffmpeg_module
|
|
||||||
uses: actions/cache/restore@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
app/libs
|
|
||||||
key: ${{ runner.os }}-ffmpeg-${{ hashFiles('**/libs.versions.toml', '**/build_ffmpeg_decoder.sh') }}
|
|
||||||
- name: Build ffmpeg decoder
|
|
||||||
id: ffmpeg-decoder
|
|
||||||
if: steps.cache_ffmpeg_module.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
|
||||||
./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@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
app/libs
|
|
||||||
key: ${{ steps.cache_ffmpeg_module.outputs.cache-primary-key }}
|
|
||||||
- name: Build release app
|
- name: Build release app
|
||||||
id: build
|
id: build
|
||||||
env:
|
env:
|
||||||
|
|
@ -84,23 +56,27 @@ jobs:
|
||||||
SIGNING_KEY: "${{ secrets.SIGNING_KEY }}"
|
SIGNING_KEY: "${{ secrets.SIGNING_KEY }}"
|
||||||
run: |
|
run: |
|
||||||
./gradlew clean assembleRelease assembleDebug
|
./gradlew clean assembleRelease assembleDebug
|
||||||
echo "apk=$(ls app/build/outputs/apk/release/Wholphin-release*.apk)" >> "$GITHUB_OUTPUT"
|
- name: Verify signatures
|
||||||
echo "apk_debug=$(ls app/build/outputs/apk/debug/Wholphin-debug*.apk)" >> "$GITHUB_OUTPUT"
|
|
||||||
- name: Verify signature
|
|
||||||
run: |
|
run: |
|
||||||
echo "Verify release APK"
|
echo "Verify APK signatures"
|
||||||
${{env.ANDROID_SDK_ROOT}}/build-tools/${{ env.BUILD_TOOLS_VERSION }}/apksigner verify --verbose --print-certs "${{ steps.build.outputs.apk }}"
|
find app/build/outputs/apk -name '*.apk'
|
||||||
echo "Verify debug APK"
|
find app/build/outputs/apk -name '*.apk' -print0 | xargs -t -0 -n1 ${{env.ANDROID_SDK_ROOT}}/build-tools/${{ env.BUILD_TOOLS_VERSION }}/apksigner verify --verbose --print-certs
|
||||||
${{env.ANDROID_SDK_ROOT}}/build-tools/${{ env.BUILD_TOOLS_VERSION }}/apksigner verify --verbose --print-certs "${{ steps.build.outputs.apk_debug }}"
|
- name: Copy APK to shorter names
|
||||||
- name: Copy APK to ${{ env.APK_SHORT_NAME }}
|
id: apks
|
||||||
run: |
|
run: |
|
||||||
cp "${{ steps.build.outputs.apk }}" "${{ env.APK_SHORT_NAME }}"
|
find app/build/outputs/apk -name '*.apk' -print0 |
|
||||||
cp "${{ steps.build.outputs.apk_debug }}" "${{ env.APK_DEBUG_SHORT_NAME }}"
|
while IFS= read -r -d '' line; do
|
||||||
|
# Wholphin-release-0.2.9-62-g5c12e03-16-arm64-v8a.apk => Wholphin-arm64-v8a.apk
|
||||||
|
short_name="$(echo $line | sed -E 's/-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+-g[a-fA-F0-9]+-[0-9]+//')"
|
||||||
|
echo "$line => $short_name"
|
||||||
|
cp "$line" "$short_name"
|
||||||
|
done
|
||||||
|
apks=$(find app/build/outputs/apk -name '*.apk' -print0 | tr '\0' ',' | sed 's/,$//')
|
||||||
|
echo "apks=$apks" >> "$GITHUB_OUTPUT"
|
||||||
- name: Checksums
|
- name: Checksums
|
||||||
run: |
|
run: |
|
||||||
echo "SHA256 checksums:"
|
echo "SHA256 checksums:"
|
||||||
sha256sum "${{ steps.build.outputs.apk }}" "${{ env.APK_SHORT_NAME }}"
|
find app/build/outputs/apk -name '*.apk' -print0 | xargs -0 sha256sum
|
||||||
sha256sum "${{ steps.build.outputs.apk_debug }}" "${{ env.APK_DEBUG_SHORT_NAME }}"
|
|
||||||
- name: Delete ${{ env.TAG_NAME }} tag and release
|
- name: Delete ${{ env.TAG_NAME }} tag and release
|
||||||
uses: dev-drprasad/delete-tag-and-release@v1.1
|
uses: dev-drprasad/delete-tag-and-release@v1.1
|
||||||
with:
|
with:
|
||||||
|
|
@ -125,7 +101,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
artifactErrorsFailBuild: true
|
artifactErrorsFailBuild: true
|
||||||
artifacts: "${{ steps.build.outputs.apk_debug }},${{ steps.build.outputs.apk }},${{ env.APK_DEBUG_SHORT_NAME }},${{ env.APK_SHORT_NAME }}"
|
artifacts: "${{ steps.apks.outputs.apks }}"
|
||||||
draft: false
|
draft: false
|
||||||
generateReleaseNotes: false
|
generateReleaseNotes: false
|
||||||
makeLatest: false
|
makeLatest: false
|
||||||
|
|
|
||||||
49
.github/workflows/pr.yml
vendored
49
.github/workflows/pr.yml
vendored
|
|
@ -8,10 +8,7 @@ defaults:
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BUILD_TOOLS_VERSION: 36.0.0
|
|
||||||
BUILD_DIRS_ARTIFACT: build-dirs
|
BUILD_DIRS_ARTIFACT: build-dirs
|
||||||
NDK_VERSION: "29.0.14206865"
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre-commit:
|
pre-commit:
|
||||||
|
|
@ -36,45 +33,21 @@ jobs:
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Need the tags to build
|
fetch-depth: 0 # Need the tags to build
|
||||||
- name: Setup JDK
|
- name: Setup
|
||||||
uses: actions/setup-java@v5
|
uses: ./.github/actions/setup
|
||||||
|
- name: Native build
|
||||||
|
uses: ./.github/actions/native-build
|
||||||
with:
|
with:
|
||||||
distribution: zulu
|
cache: true
|
||||||
java-version: '21'
|
|
||||||
cache: 'gradle'
|
|
||||||
- name: Setup Android SDK
|
|
||||||
uses: android-actions/setup-android@v3
|
|
||||||
with:
|
|
||||||
packages: "tools platform-tools build-tools;${{ env.BUILD_TOOLS_VERSION }} ndk;${{ env.NDK_VERSION }}"
|
|
||||||
|
|
||||||
- name: Load ffmpeg module cache
|
|
||||||
id: cache_ffmpeg_module
|
|
||||||
uses: actions/cache/restore@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
app/libs
|
|
||||||
key: ${{ runner.os }}-ffmpeg-${{ hashFiles('**/libs.versions.toml', '**/build_ffmpeg_decoder.sh') }}
|
|
||||||
- name: Build ffmpeg decoder
|
|
||||||
id: ffmpeg-decoder
|
|
||||||
if: steps.cache_ffmpeg_module.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
|
||||||
./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@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
app/libs
|
|
||||||
key: ${{ steps.cache_ffmpeg_module.outputs.cache-primary-key }}
|
|
||||||
|
|
||||||
- name: Build app
|
- name: Build app
|
||||||
id: buildapp
|
id: buildapp
|
||||||
run: |
|
run: |
|
||||||
./gradlew clean build
|
./gradlew clean assembleDebug
|
||||||
echo "apk=$(ls app/build/outputs/apk/debug/Wholphin-debug*.apk)" >> "$GITHUB_OUTPUT"
|
apks=$(find app/build/outputs/apk -name '*.apk' -print0 | tr '\0' ',' | sed 's/,$//')
|
||||||
|
echo "apks=$apks" >> "$GITHUB_OUTPUT"
|
||||||
- name: Tar build dirs
|
- name: Tar build dirs
|
||||||
run: |
|
run: |
|
||||||
tar -czf build.tgz */build/.
|
tar -czf build.tgz ./app/.
|
||||||
- uses: actions/upload-artifact@v5
|
- uses: actions/upload-artifact@v5
|
||||||
id: upload-build-dirs
|
id: upload-build-dirs
|
||||||
with:
|
with:
|
||||||
|
|
@ -83,6 +56,6 @@ jobs:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
- uses: actions/upload-artifact@v5
|
- uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: Wholphin-debug.apk
|
name: APKs
|
||||||
path: "${{ steps.buildapp.outputs.apk }}"
|
path: "${{ steps.buildapp.outputs.apks }}"
|
||||||
compression-level: 0
|
compression-level: 0
|
||||||
|
|
|
||||||
48
.github/workflows/release.yml
vendored
48
.github/workflows/release.yml
vendored
|
|
@ -11,8 +11,6 @@ defaults:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
APK_SHORT_NAME: Wholphin.apk
|
APK_SHORT_NAME: Wholphin.apk
|
||||||
BUILD_TOOLS_VERSION: 36.0.0
|
|
||||||
NDK_VERSION: "29.0.14206865"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
|
|
@ -24,24 +22,12 @@ jobs:
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Need the tags to build
|
fetch-depth: 0 # Need the tags to build
|
||||||
- name: Setup Python
|
- name: Setup
|
||||||
uses: actions/setup-python@v5
|
uses: ./.github/actions/setup
|
||||||
|
- name: Native build
|
||||||
|
uses: ./.github/actions/native-build
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
cache: false
|
||||||
- name: Setup JDK
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: zulu
|
|
||||||
java-version: '21'
|
|
||||||
cache: 'gradle'
|
|
||||||
- name: Setup Android SDK
|
|
||||||
uses: android-actions/setup-android@v3
|
|
||||||
with:
|
|
||||||
packages: "tools platform-tools build-tools;${{ env.BUILD_TOOLS_VERSION }} ndk;${{ env.NDK_VERSION }}"
|
|
||||||
- name: Build ffmpeg decoder
|
|
||||||
id: ffmpeg-decoder
|
|
||||||
run: |
|
|
||||||
./build_ffmpeg_decoder.sh "${{ env.ANDROID_SDK_ROOT }}/ndk/${{ env.NDK_VERSION }}"
|
|
||||||
- name: Build app
|
- name: Build app
|
||||||
id: buildapp
|
id: buildapp
|
||||||
env:
|
env:
|
||||||
|
|
@ -51,21 +37,31 @@ jobs:
|
||||||
SIGNING_KEY: "${{ secrets.SIGNING_KEY }}"
|
SIGNING_KEY: "${{ secrets.SIGNING_KEY }}"
|
||||||
run: |
|
run: |
|
||||||
./gradlew clean assembleRelease
|
./gradlew clean assembleRelease
|
||||||
echo "apk=$(ls app/build/outputs/apk/release/Wholphin-release*.apk)" >> "$GITHUB_OUTPUT"
|
- name: Verify signatures
|
||||||
- name: Verify signature
|
|
||||||
run: |
|
run: |
|
||||||
${{env.ANDROID_SDK_ROOT}}/build-tools/${{ env.BUILD_TOOLS_VERSION }}/apksigner verify --verbose --print-certs "${{ steps.buildapp.outputs.apk }}"
|
echo "Verify APK signatures"
|
||||||
- name: Copy APK to ${{ env.APK_SHORT_NAME }}
|
find app/build/outputs/apk -name '*.apk'
|
||||||
|
find app/build/outputs/apk -name '*.apk' -print0 | xargs -0 -n1 ${{env.ANDROID_SDK_ROOT}}/build-tools/${{ env.BUILD_TOOLS_VERSION }}/apksigner verify --verbose --print-certs
|
||||||
|
- name: Copy APK to shorter names
|
||||||
|
id: apks
|
||||||
run: |
|
run: |
|
||||||
cp "${{ steps.buildapp.outputs.apk }}" "${{ env.APK_SHORT_NAME }}"
|
find app/build/outputs/apk -name '*.apk' -print0 |
|
||||||
|
while IFS= read -r -d '' line; do
|
||||||
|
# Wholphin-release-0.2.9-62-g5c12e03-16-arm64-v8a.apk => Wholphin-arm64-v8a.apk
|
||||||
|
short_name="$(echo $line | sed -E 's/-release-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+-g[a-fA-F0-9]+-[0-9]+//')"
|
||||||
|
echo "$line => $short_name"
|
||||||
|
#cp "$line" "$short_name"
|
||||||
|
done
|
||||||
|
apks=$(find app/build/outputs/apk -name '*.apk' -print0 | tr '\0' ',' | sed 's/,$//')
|
||||||
|
echo "apks=$apks" >> "$GITHUB_OUTPUT"
|
||||||
- name: Checksums
|
- name: Checksums
|
||||||
run: |
|
run: |
|
||||||
echo "SHA256 checksums:"
|
echo "SHA256 checksums:"
|
||||||
sha256sum "${{ steps.buildapp.outputs.apk }}" "${{ env.APK_SHORT_NAME }}"
|
find app/build/outputs/apk -name '*.apk' -print0 | xargs -0 sha256sum
|
||||||
- name: Create GitHub release
|
- name: Create GitHub release
|
||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
artifacts: "${{ steps.buildapp.outputs.apk }},${{ env.APK_SHORT_NAME }}"
|
artifacts: "${{ steps.apks.outputs.apks }}"
|
||||||
makeLatest: true
|
makeLatest: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
generateReleaseNotes: false
|
generateReleaseNotes: false
|
||||||
|
|
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -53,7 +53,12 @@ app/libs/
|
||||||
ffmpeg_decoder/
|
ffmpeg_decoder/
|
||||||
app/release/
|
app/release/
|
||||||
app/debug/
|
app/debug/
|
||||||
|
<<<<<<< HEAD
|
||||||
|
|
||||||
app/src/main/jnilibs/
|
# mpv
|
||||||
app/src/main/libs/
|
|
||||||
app/src/main/obj/
|
app/src/main/obj/
|
||||||
|
app/src/main/libs/
|
||||||
|
app/src/main/jnilibs
|
||||||
|
app/src/main/jnilibs/
|
||||||
|
scripts/mpv/deps/
|
||||||
|
scripts/mpv/prefix/
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,18 @@ 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
|
||||||
|
|
||||||
### FFmpeg decoder module
|
### Native components
|
||||||
|
|
||||||
The app ships with [media3 ffmpeg decoder module](https://github.com/androidx/media/blob/release/libraries/decoder_ffmpeg/README.md).
|
#### FFmpeg decoder module
|
||||||
|
|
||||||
|
Wholphin ships with [media3 ffmpeg decoder module](https://github.com/androidx/media/blob/release/libraries/decoder_ffmpeg/README.md).
|
||||||
|
|
||||||
It is not required to build the extension in order to build the app locally.
|
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`](./build_ffmpeg_decoder.sh) script. You must have the [Android NDK](https://developer.android.com/ndk) installed.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ This is not a fork of the [official client](https://github.com/jellyfin/jellyfin
|
||||||
- Other (subjective) enhancements:
|
- Other (subjective) enhancements:
|
||||||
- Subtly show playback position along the bottom of the screen while seeking w/ D-Pad
|
- Subtly show playback position along the bottom of the screen while seeking w/ D-Pad
|
||||||
- Force Continue Watching & Next Up TV episodes to use their Series posters
|
- Force Continue Watching & Next Up TV episodes to use their Series posters
|
||||||
|
- Different media playback engines, including:
|
||||||
|
- Default ExoPlayer/Media3
|
||||||
|
- Experimental MPV
|
||||||
|
|
||||||
### Roadmap
|
### Roadmap
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -139,12 +139,22 @@ android {
|
||||||
variant.outputs
|
variant.outputs
|
||||||
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
|
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
|
||||||
.forEach { output ->
|
.forEach { output ->
|
||||||
|
val abi = output.getFilter("ABI").let { if (it != null) "-$it" else "" }
|
||||||
val outputFileName =
|
val outputFileName =
|
||||||
"Wholphin-${variant.baseName}-${variant.versionName}-${variant.versionCode}.apk"
|
"Wholphin-${variant.baseName}-${variant.versionName}-${variant.versionCode}$abi.apk"
|
||||||
output.outputFileName = outputFileName
|
output.outputFileName = outputFileName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
isEnable = true
|
||||||
|
reset()
|
||||||
|
include("armeabi-v7a", "arm64-v8a")
|
||||||
|
isUniversalApk = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protobuf {
|
protobuf {
|
||||||
|
|
@ -175,6 +185,7 @@ aboutLibraries {
|
||||||
duplicationRule = DuplicateRule.SIMPLE
|
duplicationRule = DuplicateRule.SIMPLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
|
|
|
||||||
11
app/config/libraries/lib_dav1d.json
Normal file
11
app/config/libraries/lib_dav1d.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"uniqueId": "com.github.videolan.dav1d",
|
||||||
|
"developers": [],
|
||||||
|
"artifactVersion": "master",
|
||||||
|
"description": "dav1d is an AV1 cross-platform decoder, open-source, and focused on speed and correctness",
|
||||||
|
"name": "dav1d",
|
||||||
|
"licenses": [
|
||||||
|
"dav1d"
|
||||||
|
],
|
||||||
|
"website": "https://github.com/videolan/dav1d"
|
||||||
|
}
|
||||||
11
app/config/libraries/lib_fribidi.json
Normal file
11
app/config/libraries/lib_fribidi.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"uniqueId": "com.github.fribidi.fribidi",
|
||||||
|
"developers": [],
|
||||||
|
"artifactVersion": "1.0.16",
|
||||||
|
"description": "The Free Implementation of the Unicode Bidirectional Algorithm",
|
||||||
|
"name": "fribidi",
|
||||||
|
"licenses": [
|
||||||
|
"lgpl-2.1"
|
||||||
|
],
|
||||||
|
"website": "https://github.com/fribidi/fribidi"
|
||||||
|
}
|
||||||
11
app/config/libraries/lib_libass.json
Normal file
11
app/config/libraries/lib_libass.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"uniqueId": "com.github.libass.libass",
|
||||||
|
"developers": [],
|
||||||
|
"artifactVersion": "master",
|
||||||
|
"description": "libass is a portable subtitle renderer for the ASS/SSA (Advanced Substation Alpha/Substation Alpha) subtitle format",
|
||||||
|
"name": "libass",
|
||||||
|
"licenses": [
|
||||||
|
"libass"
|
||||||
|
],
|
||||||
|
"website": "https://github.com/libass/libass"
|
||||||
|
}
|
||||||
11
app/config/libraries/lib_libplacebo.json
Normal file
11
app/config/libraries/lib_libplacebo.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"uniqueId": "com.github.haasn.libplacebo",
|
||||||
|
"developers": [],
|
||||||
|
"artifactVersion": "master",
|
||||||
|
"description": "libplacebo is, in a nutshell, the core rendering algorithms and ideas of mpv rewritten as an independent library",
|
||||||
|
"name": "libplacebo",
|
||||||
|
"licenses": [
|
||||||
|
"lgpl-2.1"
|
||||||
|
],
|
||||||
|
"website": "https://github.com/haasn/libplacebo"
|
||||||
|
}
|
||||||
11
app/config/libraries/lib_libunibreak.json
Normal file
11
app/config/libraries/lib_libunibreak.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"uniqueId": "com.github.adah1972.libunibreak",
|
||||||
|
"developers": [],
|
||||||
|
"artifactVersion": "6.1",
|
||||||
|
"description": "an implementation of the line breaking and word/grapheme breaking algorithms as described in Unicode Standard Annex 14 (UAX #14) and Unicode Standard Annex 29 (UAX #29)",
|
||||||
|
"name": "libunibreak",
|
||||||
|
"licenses": [
|
||||||
|
"libunibreak"
|
||||||
|
],
|
||||||
|
"website": "https://github.com/adah1972/libunibreak"
|
||||||
|
}
|
||||||
11
app/config/libraries/lib_mbedtls.json
Normal file
11
app/config/libraries/lib_mbedtls.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"uniqueId": "com.github.Mbed-TLS.mbedtls",
|
||||||
|
"developers": [],
|
||||||
|
"artifactVersion": "v3.6.4",
|
||||||
|
"description": "Mbed TLS is a C library that implements X.509 certificate manipulation and the TLS and DTLS protocols",
|
||||||
|
"name": "mbedtls",
|
||||||
|
"licenses": [
|
||||||
|
"gpl-2.0"
|
||||||
|
],
|
||||||
|
"website": "https://github.com/Mbed-TLS/mbedtls"
|
||||||
|
}
|
||||||
11
app/config/libraries/lib_mpv.json
Normal file
11
app/config/libraries/lib_mpv.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"uniqueId": "com.github.mpv-player.mpv",
|
||||||
|
"developers": [],
|
||||||
|
"artifactVersion": "master",
|
||||||
|
"description": "mpv is a free (as in freedom) media player for the command line",
|
||||||
|
"name": "mpv",
|
||||||
|
"licenses": [
|
||||||
|
"gpl-2.0"
|
||||||
|
],
|
||||||
|
"website": "https://github.com/mpv-player/mpv"
|
||||||
|
}
|
||||||
11
app/config/libraries/lib_mpv_android.json
Normal file
11
app/config/libraries/lib_mpv_android.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"uniqueId": "com.github.mpv-android.mpv-android",
|
||||||
|
"developers": [],
|
||||||
|
"artifactVersion": "master",
|
||||||
|
"description": "mpv-android is a video player for Android based on libmpv",
|
||||||
|
"name": "mpv for Android",
|
||||||
|
"licenses": [
|
||||||
|
"mpv-android"
|
||||||
|
],
|
||||||
|
"website": "https://github.com/mpv-android/mpv-android"
|
||||||
|
}
|
||||||
11
app/config/libraries/lin_freetype.json
Normal file
11
app/config/libraries/lin_freetype.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"uniqueId": "org.freetype",
|
||||||
|
"developers": [],
|
||||||
|
"artifactVersion": "2.14.1",
|
||||||
|
"description": "FreeType is a freely available software library to render fonts",
|
||||||
|
"name": "freetype",
|
||||||
|
"licenses": [
|
||||||
|
"gpl-2.0"
|
||||||
|
],
|
||||||
|
"website": "https://freetype.org"
|
||||||
|
}
|
||||||
28
app/config/licenses/lic_dav1d.json
Normal file
28
app/config/licenses/lic_dav1d.json
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"content": "Copyright © 2018-2025, VideoLAN and dav1d authors
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.",
|
||||||
|
"hash": "dav1d",
|
||||||
|
"url": "https://github.com/videolan/dav1d?tab=BSD-2-Clause-1-ov-file",
|
||||||
|
"name": "BSD-2-Clause"
|
||||||
|
}
|
||||||
20
app/config/licenses/lic_libass.json
Normal file
20
app/config/licenses/lic_libass.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"content": "ISC License
|
||||||
|
|
||||||
|
Copyright (C) 2006-2016 libass contributors
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.",
|
||||||
|
"hash": "libass",
|
||||||
|
"url": "https://github.com/libass/libass?tab=ISC-1-ov-file",
|
||||||
|
"name": "ISC"
|
||||||
|
}
|
||||||
26
app/config/licenses/lic_libunibreak.json
Normal file
26
app/config/licenses/lic_libunibreak.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"content": "Copyright (C) Wu Yongwei <wuyongwei at gmail dot com>
|
||||||
|
Copyright (C) Tom Hacohen <tom at stosb dot com>
|
||||||
|
Copyright (C) Petr Filipsky <philodej at gmail dot com>
|
||||||
|
Copyright (C) Andreas Röver <roever at users dot sf dot net>
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the author be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgement in the product documentation would
|
||||||
|
be appreciated but is not required.
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not
|
||||||
|
be misrepresented as being the original software.
|
||||||
|
3. This notice may not be removed or altered from any source
|
||||||
|
distribution.",
|
||||||
|
"hash": "libunibreak",
|
||||||
|
"url": "https://github.com/adah1972/libunibreak?tab=Zlib-1-ov-file",
|
||||||
|
"name": "Zlib"
|
||||||
|
}
|
||||||
13
app/config/licenses/lic_mpv_android.json
Normal file
13
app/config/licenses/lic_mpv_android.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"content": "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.",
|
||||||
|
"hash": "mpv-android",
|
||||||
|
"url": "https://github.com/mpv-android/mpv-android?tab=MIT-1-ov-file",
|
||||||
|
"name": "MIT"
|
||||||
|
}
|
||||||
93
app/src/main/assets/font-roboto/OFL.txt
Normal file
93
app/src/main/assets/font-roboto/OFL.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
5
app/src/main/assets/font-roboto/README.md
Normal file
5
app/src/main/assets/font-roboto/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Roboto font
|
||||||
|
|
||||||
|
Sourced from https://fonts.google.com/specimen/Roboto
|
||||||
|
|
||||||
|
Licensed under SIL OPEN FONT LICENSE Version 1.1, see [OFL.txt](./OFL.txt).
|
||||||
BIN
app/src/main/assets/font-roboto/Roboto-Bold.ttf
Normal file
BIN
app/src/main/assets/font-roboto/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
app/src/main/assets/font-roboto/Roboto-Regular.ttf
Normal file
BIN
app/src/main/assets/font-roboto/Roboto-Regular.ttf
Normal file
Binary file not shown.
BIN
app/src/main/assets/subfont.ttf
Normal file
BIN
app/src/main/assets/subfont.ttf
Normal file
Binary file not shown.
|
|
@ -77,9 +77,12 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Timber.i("MainActivity.onCreate")
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
Timber.i("MainActivity.onCreate")
|
||||||
lifecycle.addObserver(playbackLifecycleObserver)
|
lifecycle.addObserver(playbackLifecycleObserver)
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
appUpgradeHandler.copySubfont(false)
|
||||||
|
}
|
||||||
setContent {
|
setContent {
|
||||||
CoilConfig(okHttpClient, false)
|
CoilConfig(okHttpClient, false)
|
||||||
val appPreferences by userPreferencesDataStore.data.collectAsState(null)
|
val appPreferences by userPreferencesDataStore.data.collectAsState(null)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import androidx.preference.PreferenceManager
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.WholphinApplication
|
import com.github.damontecres.wholphin.WholphinApplication
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.wholphin.ui.preferences.ConditionalPreferences
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup
|
import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
|
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferenceValidation
|
import com.github.damontecres.wholphin.ui.preferences.PreferenceValidation
|
||||||
|
|
@ -672,6 +673,30 @@ sealed interface AppPreference<T> {
|
||||||
title = R.string.subtitle_style,
|
title = R.string.subtitle_style,
|
||||||
destination = Destination.Settings(PreferenceScreenOption.SUBTITLES),
|
destination = Destination.Settings(PreferenceScreenOption.SUBTITLES),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val PlayerBackendPref =
|
||||||
|
AppChoicePreference<PlayerBackend>(
|
||||||
|
title = R.string.player_backend,
|
||||||
|
defaultValue = PlayerBackend.EXO_PLAYER,
|
||||||
|
getter = { it.playbackPreferences.playerBackend },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updatePlaybackPreferences { playerBackend = value }
|
||||||
|
},
|
||||||
|
displayValues = R.array.player_backend_options,
|
||||||
|
indexToValue = { PlayerBackend.forNumber(it) },
|
||||||
|
valueToIndex = { it.number },
|
||||||
|
)
|
||||||
|
|
||||||
|
val MpvHardwareDecoding =
|
||||||
|
AppSwitchPreference(
|
||||||
|
title = R.string.mpv_hardware_decoding,
|
||||||
|
defaultValue = true,
|
||||||
|
getter = { it.playbackPreferences.mpvOptions.enableHardwareDecoding },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updateMpvOptions { enableHardwareDecoding = value }
|
||||||
|
},
|
||||||
|
summary = R.string.disable_if_crash,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -752,6 +777,7 @@ val advancedPreferences =
|
||||||
preferences =
|
preferences =
|
||||||
listOf(
|
listOf(
|
||||||
AppPreference.OneClickPause,
|
AppPreference.OneClickPause,
|
||||||
|
AppPreference.GlobalContentScale,
|
||||||
AppPreference.SkipIntros,
|
AppPreference.SkipIntros,
|
||||||
AppPreference.SkipOutros,
|
AppPreference.SkipOutros,
|
||||||
AppPreference.SkipCommercials,
|
AppPreference.SkipCommercials,
|
||||||
|
|
@ -762,15 +788,24 @@ val advancedPreferences =
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PreferenceGroup(
|
PreferenceGroup(
|
||||||
title = R.string.playback_overrides,
|
title = R.string.player_backend,
|
||||||
preferences =
|
preferences = listOf(AppPreference.PlayerBackendPref),
|
||||||
|
conditionalPreferences =
|
||||||
listOf(
|
listOf(
|
||||||
AppPreference.GlobalContentScale,
|
ConditionalPreferences(
|
||||||
AppPreference.DownMixStereo,
|
{ it.playbackPreferences.playerBackend == PlayerBackend.EXO_PLAYER },
|
||||||
AppPreference.Ac3Supported,
|
listOf(
|
||||||
AppPreference.DirectPlayAss,
|
AppPreference.FfmpegPreference,
|
||||||
AppPreference.DirectPlayPgs,
|
AppPreference.DownMixStereo,
|
||||||
AppPreference.FfmpegPreference,
|
AppPreference.Ac3Supported,
|
||||||
|
AppPreference.DirectPlayAss,
|
||||||
|
AppPreference.DirectPlayPgs,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ConditionalPreferences(
|
||||||
|
{ it.playbackPreferences.playerBackend == PlayerBackend.MPV },
|
||||||
|
listOf(AppPreference.MpvHardwareDecoding),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PreferenceGroup(
|
PreferenceGroup(
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ class AppPreferencesSerializer
|
||||||
passOutProtectionMs =
|
passOutProtectionMs =
|
||||||
AppPreference.PassOutProtection.defaultValue.hours.inWholeMilliseconds
|
AppPreference.PassOutProtection.defaultValue.hours.inWholeMilliseconds
|
||||||
showNextUpWhen = AppPreference.ShowNextUpTiming.defaultValue
|
showNextUpWhen = AppPreference.ShowNextUpTiming.defaultValue
|
||||||
|
playerBackend = AppPreference.PlayerBackendPref.defaultValue
|
||||||
|
|
||||||
overrides =
|
overrides =
|
||||||
PlaybackOverrides
|
PlaybackOverrides
|
||||||
|
|
@ -58,6 +59,14 @@ class AppPreferencesSerializer
|
||||||
mediaExtensionsEnabled =
|
mediaExtensionsEnabled =
|
||||||
AppPreference.FfmpegPreference.defaultValue
|
AppPreference.FfmpegPreference.defaultValue
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
|
mpvOptions =
|
||||||
|
MpvOptions
|
||||||
|
.newBuilder()
|
||||||
|
.apply {
|
||||||
|
enableHardwareDecoding =
|
||||||
|
AppPreference.MpvHardwareDecoding.defaultValue
|
||||||
|
}.build()
|
||||||
}.build()
|
}.build()
|
||||||
homePagePreferences =
|
homePagePreferences =
|
||||||
HomePagePreferences
|
HomePagePreferences
|
||||||
|
|
@ -112,6 +121,11 @@ inline fun AppPreferences.updatePlaybackOverrides(block: PlaybackOverrides.Build
|
||||||
overrides = overrides.toBuilder().apply(block).build()
|
overrides = overrides.toBuilder().apply(block).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun AppPreferences.updateMpvOptions(block: MpvOptions.Builder.() -> Unit): AppPreferences =
|
||||||
|
updatePlaybackPreferences {
|
||||||
|
mpvOptions = mpvOptions.toBuilder().apply(block).build()
|
||||||
|
}
|
||||||
|
|
||||||
inline fun AppPreferences.updateHomePagePreferences(block: HomePagePreferences.Builder.() -> Unit): AppPreferences =
|
inline fun AppPreferences.updateHomePagePreferences(block: HomePagePreferences.Builder.() -> Unit): AppPreferences =
|
||||||
update {
|
update {
|
||||||
homePagePreferences = homePagePreferences.toBuilder().apply(block).build()
|
homePagePreferences = homePagePreferences.toBuilder().apply(block).build()
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||||
import com.github.damontecres.wholphin.util.Version
|
import com.github.damontecres.wholphin.util.Version
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
@ -48,6 +49,7 @@ class AppUpgradeHandler
|
||||||
putLong(VERSION_CODE_CURRENT_KEY, newVersionCode)
|
putLong(VERSION_CODE_CURRENT_KEY, newVersionCode)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
copySubfont(true)
|
||||||
upgradeApp(
|
upgradeApp(
|
||||||
context,
|
context,
|
||||||
Version.Companion.fromString(previousVersion ?: "0.0.0"),
|
Version.Companion.fromString(previousVersion ?: "0.0.0"),
|
||||||
|
|
@ -60,6 +62,27 @@ class AppUpgradeHandler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun copySubfont(overwrite: Boolean) {
|
||||||
|
try {
|
||||||
|
val fontFileName = "subfont.ttf"
|
||||||
|
val outputFile = File(context.filesDir, fontFileName)
|
||||||
|
if (!outputFile.exists() || overwrite) {
|
||||||
|
context.assets.open(fontFileName).use { input ->
|
||||||
|
outputFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timber.i("Wrote font %s to local", fontFileName)
|
||||||
|
}
|
||||||
|
// val oldFontDir = File(context.filesDir, "fonts")
|
||||||
|
// if (oldFontDir.exists()) {
|
||||||
|
// oldFontDir.deleteRecursively()
|
||||||
|
// }
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Exception copying subfont.tff")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val VERSION_NAME_PREVIOUS_KEY = "version.previous.name"
|
const val VERSION_NAME_PREVIOUS_KEY = "version.previous.name"
|
||||||
const val VERSION_CODE_PREVIOUS_KEY = "version.previous.code"
|
const val VERSION_CODE_PREVIOUS_KEY = "version.previous.code"
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,11 @@ import androidx.media3.common.Player
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.MediaExtensionStatus
|
import com.github.damontecres.wholphin.preferences.MediaExtensionStatus
|
||||||
|
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MpvPlayer
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
@ -38,27 +41,45 @@ class PlayerFactory
|
||||||
currentPlayer?.release()
|
currentPlayer?.release()
|
||||||
}
|
}
|
||||||
|
|
||||||
val extensions =
|
val prefs = runBlocking { appPreferences.data.firstOrNull()?.playbackPreferences }
|
||||||
runBlocking { appPreferences.data.firstOrNull() }?.playbackPreferences?.overrides?.mediaExtensionsEnabled
|
val backend = prefs?.playerBackend ?: AppPreference.PlayerBackendPref.defaultValue
|
||||||
Timber.v("extensions=$extensions")
|
|
||||||
val rendererMode =
|
|
||||||
when (extensions) {
|
|
||||||
MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
|
|
||||||
MediaExtensionStatus.MES_PREFERRED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
|
||||||
MediaExtensionStatus.MES_DISABLED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF
|
|
||||||
else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
|
|
||||||
}
|
|
||||||
val newPlayer =
|
val newPlayer =
|
||||||
ExoPlayer
|
when (backend) {
|
||||||
.Builder(context)
|
PlayerBackend.MPV -> {
|
||||||
.setRenderersFactory(
|
val enableHardwareDecoding =
|
||||||
DefaultRenderersFactory(context)
|
prefs?.mpvOptions?.enableHardwareDecoding
|
||||||
.setEnableDecoderFallback(true)
|
?: AppPreference.MpvHardwareDecoding.defaultValue
|
||||||
.setExtensionRendererMode(rendererMode),
|
MpvPlayer(context, enableHardwareDecoding)
|
||||||
).build()
|
.apply {
|
||||||
.apply {
|
playWhenReady = true
|
||||||
playWhenReady = true
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PlayerBackend.EXO_PLAYER,
|
||||||
|
PlayerBackend.UNRECOGNIZED,
|
||||||
|
-> {
|
||||||
|
val extensions =
|
||||||
|
runBlocking { appPreferences.data.firstOrNull() }?.playbackPreferences?.overrides?.mediaExtensionsEnabled
|
||||||
|
Timber.v("extensions=$extensions")
|
||||||
|
val rendererMode =
|
||||||
|
when (extensions) {
|
||||||
|
MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
|
||||||
|
MediaExtensionStatus.MES_PREFERRED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||||
|
MediaExtensionStatus.MES_DISABLED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF
|
||||||
|
else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
|
||||||
|
}
|
||||||
|
ExoPlayer
|
||||||
|
.Builder(context)
|
||||||
|
.setRenderersFactory(
|
||||||
|
DefaultRenderersFactory(context)
|
||||||
|
.setEnableDecoderFallback(true)
|
||||||
|
.setExtensionRendererMode(rendererMode),
|
||||||
|
).build()
|
||||||
|
.apply {
|
||||||
|
playWhenReady = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
currentPlayer = newPlayer
|
currentPlayer = newPlayer
|
||||||
return newPlayer
|
return newPlayer
|
||||||
}
|
}
|
||||||
|
|
@ -68,6 +89,7 @@ val Player.isReleased: Boolean
|
||||||
get() {
|
get() {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is ExoPlayer -> isReleased
|
is ExoPlayer -> isReleased
|
||||||
|
is MpvPlayer -> isReleased
|
||||||
else -> throw IllegalStateException("Unknown Player type: ${this::class.qualifiedName}")
|
else -> throw IllegalStateException("Unknown Player type: ${this::class.qualifiedName}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.contentOrNull
|
import kotlinx.serialization.json.contentOrNull
|
||||||
import kotlinx.serialization.json.jsonArray
|
import kotlinx.serialization.json.jsonArray
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
|
@ -51,9 +53,8 @@ class UpdateChecker
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
// TODO apk names
|
// TODO apk names
|
||||||
private const val ASSET_NAME = "Wholphin.apk"
|
private const val ASSET_NAME = "Wholphin"
|
||||||
private const val DEBUG_ASSET_NAME = "Wholphin-debug.apk"
|
private const val APK_NAME = "$ASSET_NAME.apk"
|
||||||
private const val RELEASE_ASSET_NAME = "Wholphin-release.apk"
|
|
||||||
|
|
||||||
private const val APK_MIME_TYPE = "application/vnd.android.package-archive"
|
private const val APK_MIME_TYPE = "application/vnd.android.package-archive"
|
||||||
|
|
||||||
|
|
@ -116,15 +117,10 @@ class UpdateChecker
|
||||||
|
|
||||||
suspend fun getLatestRelease(updateUrl: String): Release? {
|
suspend fun getLatestRelease(updateUrl: String): Release? {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val preferredAsset =
|
val preferRelease =
|
||||||
if (PreferenceManager
|
PreferenceManager
|
||||||
.getDefaultSharedPreferences(context)
|
.getDefaultSharedPreferences(context)
|
||||||
.getBoolean("updatePreferRelease", true)
|
.getBoolean("updatePreferRelease", true)
|
||||||
) {
|
|
||||||
RELEASE_ASSET_NAME
|
|
||||||
} else {
|
|
||||||
DEBUG_ASSET_NAME
|
|
||||||
}
|
|
||||||
|
|
||||||
val request =
|
val request =
|
||||||
Request
|
Request
|
||||||
|
|
@ -136,21 +132,15 @@ class UpdateChecker
|
||||||
if (it.isSuccessful && it.body != null) {
|
if (it.isSuccessful && it.body != null) {
|
||||||
val result = Json.parseToJsonElement(it.body!!.string())
|
val result = Json.parseToJsonElement(it.body!!.string())
|
||||||
val name = result.jsonObject["name"]?.jsonPrimitive?.contentOrNull
|
val name = result.jsonObject["name"]?.jsonPrimitive?.contentOrNull
|
||||||
val version = Version.Companion.tryFromString(name)
|
val version = Version.tryFromString(name)
|
||||||
val publishedAt =
|
val publishedAt =
|
||||||
result.jsonObject["published_at"]?.jsonPrimitive?.contentOrNull
|
result.jsonObject["published_at"]?.jsonPrimitive?.contentOrNull
|
||||||
val body = result.jsonObject["body"]?.jsonPrimitive?.contentOrNull
|
val body = result.jsonObject["body"]?.jsonPrimitive?.contentOrNull
|
||||||
val downloadUrl =
|
val downloadUrl =
|
||||||
result.jsonObject["assets"]
|
result.jsonObject["assets"]
|
||||||
?.jsonArray
|
?.jsonArray
|
||||||
?.firstOrNull { asset ->
|
?.let { assets -> getDownloadUrl(assets, preferRelease) }
|
||||||
val assetName =
|
Timber.v("version=$version, downloadUrl=$downloadUrl")
|
||||||
asset.jsonObject["name"]?.jsonPrimitive?.contentOrNull
|
|
||||||
assetName == ASSET_NAME || assetName == preferredAsset
|
|
||||||
}?.jsonObject
|
|
||||||
?.get("browser_download_url")
|
|
||||||
?.jsonPrimitive
|
|
||||||
?.contentOrNull
|
|
||||||
if (version != null) {
|
if (version != null) {
|
||||||
val notes =
|
val notes =
|
||||||
if (body.isNotNullOrBlank()) {
|
if (body.isNotNullOrBlank()) {
|
||||||
|
|
@ -174,6 +164,35 @@ class UpdateChecker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getDownloadUrl(
|
||||||
|
assets: JsonArray,
|
||||||
|
preferRelease: Boolean,
|
||||||
|
): String? {
|
||||||
|
val abiSuffix = Build.SUPPORTED_ABIS.firstOrNull().let { if (it != null) "-$it" else "" }
|
||||||
|
val releaseSuffix = if (preferRelease) "-release" else "-debug"
|
||||||
|
val preferredNames =
|
||||||
|
listOf(
|
||||||
|
"$ASSET_NAME${releaseSuffix}$abiSuffix.apk",
|
||||||
|
"$ASSET_NAME$releaseSuffix.apk",
|
||||||
|
"$ASSET_NAME.apk",
|
||||||
|
)
|
||||||
|
var preferredAsset: JsonObject? = null
|
||||||
|
outer@ for (name in preferredNames) {
|
||||||
|
for (asset in assets) {
|
||||||
|
val assetName =
|
||||||
|
asset.jsonObject["name"]?.jsonPrimitive?.contentOrNull
|
||||||
|
if (name == assetName) {
|
||||||
|
preferredAsset = asset.jsonObject
|
||||||
|
break@outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return preferredAsset
|
||||||
|
?.get("browser_download_url")
|
||||||
|
?.jsonPrimitive
|
||||||
|
?.contentOrNull
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun installRelease(
|
suspend fun installRelease(
|
||||||
release: Release,
|
release: Release,
|
||||||
callback: DownloadCallback,
|
callback: DownloadCallback,
|
||||||
|
|
@ -195,7 +214,7 @@ class UpdateChecker
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
val contentValues =
|
val contentValues =
|
||||||
ContentValues().apply {
|
ContentValues().apply {
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, ASSET_NAME)
|
put(MediaStore.MediaColumns.DISPLAY_NAME, APK_NAME)
|
||||||
put(MediaStore.MediaColumns.MIME_TYPE, APK_MIME_TYPE)
|
put(MediaStore.MediaColumns.MIME_TYPE, APK_MIME_TYPE)
|
||||||
put(
|
put(
|
||||||
MediaStore.MediaColumns.RELATIVE_PATH,
|
MediaStore.MediaColumns.RELATIVE_PATH,
|
||||||
|
|
@ -267,7 +286,7 @@ class UpdateChecker
|
||||||
val downloadDir =
|
val downloadDir =
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||||
downloadDir.mkdirs()
|
downloadDir.mkdirs()
|
||||||
val targetFile = File(downloadDir, ASSET_NAME)
|
val targetFile = File(downloadDir, APK_NAME)
|
||||||
targetFile.outputStream().use { output ->
|
targetFile.outputStream().use { output ->
|
||||||
response.body!!.byteStream().use { input ->
|
response.body!!.byteStream().use { input ->
|
||||||
copyTo(input, output, callback = callback)
|
copyTo(input, output, callback = callback)
|
||||||
|
|
@ -320,7 +339,7 @@ class UpdateChecker
|
||||||
} else {
|
} else {
|
||||||
val downloadDir =
|
val downloadDir =
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||||
val targetFile = File(downloadDir, ASSET_NAME)
|
val targetFile = File(downloadDir, APK_NAME)
|
||||||
if (targetFile.exists()) {
|
if (targetFile.exists()) {
|
||||||
targetFile.delete()
|
targetFile.delete()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,10 @@ fun CircularProgress(modifier: Modifier = Modifier) {
|
||||||
* Fill the space with a loading indicator and take focus
|
* Fill the space with a loading indicator and take focus
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun LoadingPage(modifier: Modifier = Modifier) {
|
fun LoadingPage(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
focusEnabled: Boolean = true,
|
||||||
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -39,7 +42,7 @@ fun LoadingPage(modifier: Modifier = Modifier) {
|
||||||
modifier
|
modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester)
|
||||||
.focusable(),
|
.focusable(focusEnabled),
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
color = MaterialTheme.colorScheme.border,
|
color = MaterialTheme.colorScheme.border,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail
|
package com.github.damontecres.wholphin.ui.detail
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
|
|
@ -224,6 +225,11 @@ fun DebugPage(
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
|
Text(
|
||||||
|
text = "ABIs: ${Build.SUPPORTED_ABIS.toList()}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
|
@ -237,12 +243,12 @@ fun DebugPage(
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Current server: ${viewModel.serverRepository.currentServer}",
|
text = "Current server: ${viewModel.serverRepository.currentServer.value}",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Current user: ${viewModel.serverRepository.currentUser}",
|
text = "Current user: ${viewModel.serverRepository.currentUser.value}",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,7 @@ fun SeekBar(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
enabled = isEnabled,
|
enabled = isEnabled,
|
||||||
durationMs = player.contentDuration,
|
durationMs = player.duration,
|
||||||
seekBack = seekBack,
|
seekBack = seekBack,
|
||||||
seekForward = seekForward,
|
seekForward = seekForward,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -407,6 +407,12 @@ fun PlaybackOverlay(
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.background(AppColors.TransparentBlack50),
|
.background(AppColors.TransparentBlack50),
|
||||||
) {
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Backend: ${currentPlayback?.backend}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "Play method: ${currentPlayback?.playMethod?.serialName}",
|
text = "Play method: ${currentPlayback?.playMethod?.serialName}",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.input.key.onKeyEvent
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.intl.Locale
|
import androidx.compose.ui.text.intl.Locale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -65,6 +66,7 @@ import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
import com.github.damontecres.wholphin.data.model.Playlist
|
import com.github.damontecres.wholphin.data.model.Playlist
|
||||||
|
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.skipBackOnResume
|
import com.github.damontecres.wholphin.preferences.skipBackOnResume
|
||||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||||
|
|
@ -72,12 +74,14 @@ import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.applyToMpv
|
||||||
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.toSubtitleStyle
|
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.toSubtitleStyle
|
||||||
import com.github.damontecres.wholphin.ui.seasonEpisode
|
import com.github.damontecres.wholphin.ui.seasonEpisode
|
||||||
import com.github.damontecres.wholphin.ui.stringRes
|
import com.github.damontecres.wholphin.ui.stringRes
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
|
|
@ -117,6 +121,7 @@ fun PlaybackPage(
|
||||||
LoadingState.Success -> {
|
LoadingState.Success -> {
|
||||||
val prefs = preferences.appPreferences.playbackPreferences
|
val prefs = preferences.appPreferences.playbackPreferences
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
val player = viewModel.player
|
val player = viewModel.player
|
||||||
val title by viewModel.title.observeAsState(null)
|
val title by viewModel.title.observeAsState(null)
|
||||||
|
|
@ -157,6 +162,13 @@ fun PlaybackPage(
|
||||||
|
|
||||||
OneTimeLaunchedEffect {
|
OneTimeLaunchedEffect {
|
||||||
player.addListener(cueListener)
|
player.addListener(cueListener)
|
||||||
|
if (prefs.playerBackend == PlayerBackend.MPV) {
|
||||||
|
scope.launch(Dispatchers.Main + ExceptionHandler()) {
|
||||||
|
preferences.appPreferences.interfacePreferences.subtitlesPreferences.applyToMpv(
|
||||||
|
density,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose { player.removeListener(cueListener) }
|
onDispose { player.removeListener(cueListener) }
|
||||||
|
|
@ -166,7 +178,7 @@ fun PlaybackPage(
|
||||||
var playbackSpeed by remember { mutableFloatStateOf(1.0f) }
|
var playbackSpeed by remember { mutableFloatStateOf(1.0f) }
|
||||||
LaunchedEffect(playbackSpeed) { player.setPlaybackSpeed(playbackSpeed) }
|
LaunchedEffect(playbackSpeed) { player.setPlaybackSpeed(playbackSpeed) }
|
||||||
|
|
||||||
val presentationState = rememberPresentationState(player)
|
val presentationState = rememberPresentationState(player, false)
|
||||||
val scaledModifier =
|
val scaledModifier =
|
||||||
Modifier.resizeWithContentScale(contentScale, presentationState.videoSizeDp)
|
Modifier.resizeWithContentScale(contentScale, presentationState.videoSizeDp)
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
@ -241,12 +253,15 @@ fun PlaybackPage(
|
||||||
modifier = scaledModifier,
|
modifier = scaledModifier,
|
||||||
)
|
)
|
||||||
if (presentationState.coverSurface) {
|
if (presentationState.coverSurface) {
|
||||||
|
val isLoading by rememberPlayerLoadingState(player)
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.matchParentSize()
|
.matchParentSize()
|
||||||
.background(Color.Black),
|
.background(Color.Black),
|
||||||
) {
|
) {
|
||||||
LoadingPage()
|
if (isLoading) {
|
||||||
|
LoadingPage(focusEnabled = false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -365,7 +380,7 @@ fun PlaybackPage(
|
||||||
|
|
||||||
PlaybackAction.Next -> {
|
PlaybackAction.Next -> {
|
||||||
// TODO focus is lost
|
// TODO focus is lost
|
||||||
viewModel.playUpNextUp()
|
viewModel.playNextUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackAction.Previous -> {
|
PlaybackAction.Previous -> {
|
||||||
|
|
@ -458,14 +473,14 @@ fun PlaybackPage(
|
||||||
if (autoPlayEnabled) {
|
if (autoPlayEnabled) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (timeLeft == 0L) {
|
if (timeLeft == 0L) {
|
||||||
viewModel.playUpNextUp()
|
viewModel.playNextUp()
|
||||||
} else {
|
} else {
|
||||||
while (timeLeft > 0) {
|
while (timeLeft > 0) {
|
||||||
delay(1.seconds)
|
delay(1.seconds)
|
||||||
timeLeft--
|
timeLeft--
|
||||||
}
|
}
|
||||||
if (timeLeft == 0L && autoPlayEnabled) {
|
if (timeLeft == 0L && autoPlayEnabled) {
|
||||||
viewModel.playUpNextUp()
|
viewModel.playNextUp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -483,7 +498,7 @@ fun PlaybackPage(
|
||||||
?: AspectRatios.WIDE,
|
?: AspectRatios.WIDE,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.reportInteraction()
|
viewModel.reportInteraction()
|
||||||
viewModel.playUpNextUp()
|
viewModel.playNextUp()
|
||||||
},
|
},
|
||||||
timeLeft = if (autoPlayEnabled) timeLeft.seconds else null,
|
timeLeft = if (autoPlayEnabled) timeLeft.seconds else null,
|
||||||
modifier =
|
modifier =
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import com.github.damontecres.wholphin.data.model.chooseSource
|
||||||
import com.github.damontecres.wholphin.data.model.chooseStream
|
import com.github.damontecres.wholphin.data.model.chooseStream
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
|
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||||
import com.github.damontecres.wholphin.preferences.ShowNextUpWhen
|
import com.github.damontecres.wholphin.preferences.ShowNextUpWhen
|
||||||
import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior
|
import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
|
@ -52,6 +53,7 @@ import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import com.github.damontecres.wholphin.util.TrackActivityPlaybackListener
|
import com.github.damontecres.wholphin.util.TrackActivityPlaybackListener
|
||||||
import com.github.damontecres.wholphin.util.TrackSupport
|
import com.github.damontecres.wholphin.util.TrackSupport
|
||||||
import com.github.damontecres.wholphin.util.checkForSupport
|
import com.github.damontecres.wholphin.util.checkForSupport
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.mpvDeviceProfile
|
||||||
import com.github.damontecres.wholphin.util.subtitleMimeTypes
|
import com.github.damontecres.wholphin.util.subtitleMimeTypes
|
||||||
import com.github.damontecres.wholphin.util.supportItemKinds
|
import com.github.damontecres.wholphin.util.supportItemKinds
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
|
@ -77,6 +79,8 @@ import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
import org.jellyfin.sdk.model.api.MediaSegmentDto
|
import org.jellyfin.sdk.model.api.MediaSegmentDto
|
||||||
import org.jellyfin.sdk.model.api.MediaSegmentType
|
import org.jellyfin.sdk.model.api.MediaSegmentType
|
||||||
|
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||||
|
import org.jellyfin.sdk.model.api.MediaStream
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||||
import org.jellyfin.sdk.model.api.PlayMethod
|
import org.jellyfin.sdk.model.api.PlayMethod
|
||||||
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
||||||
|
|
@ -209,7 +213,7 @@ class PlaybackViewModel
|
||||||
destination.forceTranscoding,
|
destination.forceTranscoding,
|
||||||
)
|
)
|
||||||
if (!played) {
|
if (!played) {
|
||||||
playUpNextUp()
|
playNextUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPlaylist && queriedItem.type == BaseItemKind.EPISODE) {
|
if (!isPlaylist && queriedItem.type == BaseItemKind.EPISODE) {
|
||||||
|
|
@ -364,6 +368,7 @@ class PlaybackViewModel
|
||||||
enableDirectStream = !forceTranscoding,
|
enableDirectStream = !forceTranscoding,
|
||||||
)
|
)
|
||||||
player.prepare()
|
player.prepare()
|
||||||
|
player.play()
|
||||||
|
|
||||||
this@PlaybackViewModel.chapters.value = Chapter.fromDto(base, api)
|
this@PlaybackViewModel.chapters.value = Chapter.fromDto(base, api)
|
||||||
Timber.v("chapters=${this@PlaybackViewModel.chapters.value?.size}")
|
Timber.v("chapters=${this@PlaybackViewModel.chapters.value?.size}")
|
||||||
|
|
@ -378,29 +383,74 @@ class PlaybackViewModel
|
||||||
currentItemPlayback: ItemPlayback = this@PlaybackViewModel.currentItemPlayback.value!!,
|
currentItemPlayback: ItemPlayback = this@PlaybackViewModel.currentItemPlayback.value!!,
|
||||||
audioIndex: Int?,
|
audioIndex: Int?,
|
||||||
subtitleIndex: Int?,
|
subtitleIndex: Int?,
|
||||||
positionMs: Long = C.TIME_UNSET,
|
positionMs: Long = 0,
|
||||||
userInitiated: Boolean,
|
userInitiated: Boolean,
|
||||||
enableDirectPlay: Boolean = true,
|
enableDirectPlay: Boolean = true,
|
||||||
enableDirectStream: Boolean = true,
|
enableDirectStream: Boolean = true,
|
||||||
) = withContext(Dispatchers.IO) {
|
) = withContext(Dispatchers.IO) {
|
||||||
val itemId = item.id
|
val itemId = item.id
|
||||||
|
val playerBackend = preferences.appPreferences.playbackPreferences.playerBackend
|
||||||
|
|
||||||
|
val currentPlayback = this@PlaybackViewModel.currentPlayback.value
|
||||||
|
if (currentPlayback != null && currentPlayback.item.id == item.id && currentPlayback.playMethod == PlayMethod.DIRECT_PLAY) {
|
||||||
|
// If direct playing, can try to switch tracks without playback restarting
|
||||||
|
// Except for external subtitles
|
||||||
|
// TODO there's probably no reason why we can't add external subtitles?
|
||||||
|
Timber.v("changeStreams direct play")
|
||||||
|
|
||||||
|
val source = currentPlayback.mediaSourceInfo
|
||||||
|
val externalSubtitle = source.findExternalSubtitle(subtitleIndex)
|
||||||
|
|
||||||
|
if (externalSubtitle == null) {
|
||||||
|
val result =
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
applyTrackSelections(
|
||||||
|
player,
|
||||||
|
playerBackend,
|
||||||
|
true,
|
||||||
|
audioIndex,
|
||||||
|
subtitleIndex,
|
||||||
|
source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (result.bothSelected) {
|
||||||
|
// TODO lots of duplicate code in this block
|
||||||
|
Timber.d("Changes tracks audio=$audioIndex, subtitle=$subtitleIndex")
|
||||||
|
val itemPlayback =
|
||||||
|
currentItemPlayback.copy(
|
||||||
|
sourceId = source.id?.toUUIDOrNull(),
|
||||||
|
audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED,
|
||||||
|
subtitleIndex = subtitleIndex ?: TrackIndex.DISABLED,
|
||||||
|
)
|
||||||
|
if (userInitiated) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
Timber.v("Saving user initiated item playback: %s", itemPlayback)
|
||||||
|
val updated = itemPlaybackRepository.saveItemPlayback(itemPlayback)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
this@PlaybackViewModel.currentItemPlayback.value = updated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
this@PlaybackViewModel.currentPlayback.value =
|
||||||
|
currentPlayback.copy(
|
||||||
|
tracks = checkForSupport(player.currentTracks),
|
||||||
|
)
|
||||||
|
this@PlaybackViewModel.currentItemPlayback.value = itemPlayback
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.v("changeStreams direct play, external subtitle was requested")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO
|
|
||||||
// if (currentItemPlayback.let {
|
|
||||||
// it.itemId == itemId &&
|
|
||||||
// it.audioIndex == audioIndex &&
|
|
||||||
// it.subtitleIndex == subtitleIndex
|
|
||||||
// } == true
|
|
||||||
// ) {
|
|
||||||
// Timber.i("No change in playback for changeStreams")
|
|
||||||
// return@withContext
|
|
||||||
// }
|
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"changeStreams: userInitiated=$userInitiated, audioIndex=$audioIndex, subtitleIndex=$subtitleIndex, " +
|
"changeStreams: userInitiated=$userInitiated, audioIndex=$audioIndex, subtitleIndex=$subtitleIndex, " +
|
||||||
"enableDirectPlay=$enableDirectPlay, enableDirectStream=$enableDirectStream",
|
"enableDirectPlay=$enableDirectPlay, enableDirectStream=$enableDirectStream, positionMs=$positionMs",
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO if the new audio or subtitle index is already in the streams (eg direct play), should toggle in the player instead
|
|
||||||
val maxBitrate =
|
val maxBitrate =
|
||||||
preferences.appPreferences.playbackPreferences.maxBitrate
|
preferences.appPreferences.playbackPreferences.maxBitrate
|
||||||
.takeIf { it > 0 } ?: AppPreference.DEFAULT_BITRATE
|
.takeIf { it > 0 } ?: AppPreference.DEFAULT_BITRATE
|
||||||
|
|
@ -410,7 +460,12 @@ class PlaybackViewModel
|
||||||
itemId,
|
itemId,
|
||||||
PlaybackInfoDto(
|
PlaybackInfoDto(
|
||||||
startTimeTicks = null,
|
startTimeTicks = null,
|
||||||
deviceProfile = deviceProfile,
|
deviceProfile =
|
||||||
|
if (playerBackend == PlayerBackend.EXO_PLAYER) {
|
||||||
|
deviceProfile
|
||||||
|
} else {
|
||||||
|
mpvDeviceProfile
|
||||||
|
},
|
||||||
maxAudioChannels = null,
|
maxAudioChannels = null,
|
||||||
audioStreamIndex = audioIndex,
|
audioStreamIndex = audioIndex,
|
||||||
subtitleStreamIndex = subtitleIndex,
|
subtitleStreamIndex = subtitleIndex,
|
||||||
|
|
@ -453,6 +508,7 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
val transcodeType =
|
val transcodeType =
|
||||||
when {
|
when {
|
||||||
|
// playerBackend == PlayerBackend.MPV -> PlayMethod.DIRECT_PLAY
|
||||||
source.supportsDirectPlay -> PlayMethod.DIRECT_PLAY
|
source.supportsDirectPlay -> PlayMethod.DIRECT_PLAY
|
||||||
source.supportsDirectStream -> PlayMethod.DIRECT_STREAM
|
source.supportsDirectStream -> PlayMethod.DIRECT_STREAM
|
||||||
source.supportsTranscoding -> PlayMethod.TRANSCODE
|
source.supportsTranscoding -> PlayMethod.TRANSCODE
|
||||||
|
|
@ -461,29 +517,27 @@ class PlaybackViewModel
|
||||||
val decision = StreamDecision(itemId, transcodeType, mediaUrl)
|
val decision = StreamDecision(itemId, transcodeType, mediaUrl)
|
||||||
Timber.v("Playback decision: $decision")
|
Timber.v("Playback decision: $decision")
|
||||||
|
|
||||||
val externalSubtitleCount =
|
val externalSubtitleCount = source.externalSubtitlesCount
|
||||||
source.mediaStreams
|
|
||||||
?.count { it.type == MediaStreamType.SUBTITLE && it.isExternal } ?: 0
|
|
||||||
|
|
||||||
val externalSubtitle =
|
val externalSubtitle =
|
||||||
source.mediaStreams
|
source.findExternalSubtitle(subtitleIndex)?.let {
|
||||||
?.firstOrNull { it.type == MediaStreamType.SUBTITLE && it.isExternal && it.index == subtitleIndex }
|
it.deliveryUrl?.let { deliveryUrl ->
|
||||||
?.let {
|
var flags = 0
|
||||||
it.deliveryUrl?.let { deliveryUrl ->
|
if (it.isForced) flags = flags.or(C.SELECTION_FLAG_FORCED)
|
||||||
var flags = 0
|
if (it.isDefault) flags = flags.or(C.SELECTION_FLAG_DEFAULT)
|
||||||
if (it.isForced) flags = flags.or(C.SELECTION_FLAG_FORCED)
|
MediaItem.SubtitleConfiguration
|
||||||
if (it.isDefault) flags = flags.or(C.SELECTION_FLAG_DEFAULT)
|
.Builder(
|
||||||
MediaItem.SubtitleConfiguration
|
api.createUrl(deliveryUrl).toUri(),
|
||||||
.Builder(
|
).setId("e:${it.index}")
|
||||||
api.createUrl(deliveryUrl).toUri(),
|
.setMimeType(subtitleMimeTypes[it.codec])
|
||||||
).setId("e:${it.index}")
|
.setLanguage(it.language)
|
||||||
.setMimeType(subtitleMimeTypes[it.codec])
|
.setLabel(it.title)
|
||||||
.setLanguage(it.language)
|
.setSelectionFlags(flags)
|
||||||
.setSelectionFlags(flags)
|
.build()
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Timber.v("externalSubtitleCount=$externalSubtitleCount, externalSubtitle=$externalSubtitle")
|
}
|
||||||
|
|
||||||
|
Timber.v("subtitleIndex=$subtitleIndex, externalSubtitleCount=$externalSubtitleCount, externalSubtitle=$externalSubtitle")
|
||||||
|
|
||||||
val mediaItem =
|
val mediaItem =
|
||||||
MediaItem
|
MediaItem
|
||||||
|
|
@ -497,9 +551,11 @@ class PlaybackViewModel
|
||||||
CurrentPlayback(
|
CurrentPlayback(
|
||||||
item = item,
|
item = item,
|
||||||
tracks = listOf(),
|
tracks = listOf(),
|
||||||
|
backend = preferences.appPreferences.playbackPreferences.playerBackend,
|
||||||
playMethod = transcodeType,
|
playMethod = transcodeType,
|
||||||
playSessionId = response.playSessionId,
|
playSessionId = response.playSessionId,
|
||||||
liveStreamId = source.liveStreamId,
|
liveStreamId = source.liveStreamId,
|
||||||
|
mediaSourceInfo = source,
|
||||||
)
|
)
|
||||||
val itemPlayback =
|
val itemPlayback =
|
||||||
currentItemPlayback.copy(
|
currentItemPlayback.copy(
|
||||||
|
|
@ -536,7 +592,7 @@ class PlaybackViewModel
|
||||||
|
|
||||||
duration.value = source.runTimeTicks?.ticks
|
duration.value = source.runTimeTicks?.ticks
|
||||||
loading.value = LoadingState.Success
|
loading.value = LoadingState.Success
|
||||||
currentPlayback.value = playback
|
this@PlaybackViewModel.currentPlayback.value = playback
|
||||||
this@PlaybackViewModel.currentItemPlayback.value = itemPlayback
|
this@PlaybackViewModel.currentItemPlayback.value = itemPlayback
|
||||||
player.setMediaItem(
|
player.setMediaItem(
|
||||||
mediaItem,
|
mediaItem,
|
||||||
|
|
@ -548,15 +604,18 @@ class PlaybackViewModel
|
||||||
override fun onTracksChanged(tracks: Tracks) {
|
override fun onTracksChanged(tracks: Tracks) {
|
||||||
Timber.v("onTracksChanged: $tracks")
|
Timber.v("onTracksChanged: $tracks")
|
||||||
if (tracks.groups.isNotEmpty()) {
|
if (tracks.groups.isNotEmpty()) {
|
||||||
applyTrackSelections(
|
val result =
|
||||||
player,
|
applyTrackSelections(
|
||||||
source.supportsDirectPlay,
|
player,
|
||||||
audioIndex,
|
playerBackend,
|
||||||
subtitleIndex,
|
source.supportsDirectPlay,
|
||||||
externalSubtitleCount,
|
audioIndex,
|
||||||
externalSubtitle != null,
|
subtitleIndex,
|
||||||
)
|
source,
|
||||||
player.removeListener(this)
|
)
|
||||||
|
if (result.bothSelected) {
|
||||||
|
player.removeListener(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -727,7 +786,7 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun playUpNextUp() {
|
fun playNextUp() {
|
||||||
playlist.value?.let {
|
playlist.value?.let {
|
||||||
if (it.hasNext()) {
|
if (it.hasNext()) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
|
|
@ -735,7 +794,7 @@ class PlaybackViewModel
|
||||||
val item = it.getAndAdvance()
|
val item = it.getAndAdvance()
|
||||||
val played = play(item, 0)
|
val played = play(item, 0)
|
||||||
if (!played) {
|
if (!played) {
|
||||||
playUpNextUp()
|
playNextUp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -768,7 +827,7 @@ class PlaybackViewModel
|
||||||
if (toPlay != null) {
|
if (toPlay != null) {
|
||||||
val played = play(toPlay, 0)
|
val played = play(toPlay, 0)
|
||||||
if (!played) {
|
if (!played) {
|
||||||
playUpNextUp()
|
playNextUp()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO
|
// TODO
|
||||||
|
|
@ -837,7 +896,7 @@ class PlaybackViewModel
|
||||||
|
|
||||||
PlaystateCommand.PAUSE -> player.pause()
|
PlaystateCommand.PAUSE -> player.pause()
|
||||||
PlaystateCommand.UNPAUSE -> player.play()
|
PlaystateCommand.UNPAUSE -> player.play()
|
||||||
PlaystateCommand.NEXT_TRACK -> playUpNextUp()
|
PlaystateCommand.NEXT_TRACK -> playNextUp()
|
||||||
PlaystateCommand.PREVIOUS_TRACK -> playPrevious()
|
PlaystateCommand.PREVIOUS_TRACK -> playPrevious()
|
||||||
PlaystateCommand.SEEK -> it.seekPositionTicks?.ticks?.let { player.seekTo(it.inWholeMilliseconds) }
|
PlaystateCommand.SEEK -> it.seekPositionTicks?.ticks?.let { player.seekTo(it.inWholeMilliseconds) }
|
||||||
PlaystateCommand.REWIND ->
|
PlaystateCommand.REWIND ->
|
||||||
|
|
@ -1013,9 +1072,11 @@ class PlaybackViewModel
|
||||||
data class CurrentPlayback(
|
data class CurrentPlayback(
|
||||||
val item: BaseItem,
|
val item: BaseItem,
|
||||||
val tracks: List<TrackSupport>,
|
val tracks: List<TrackSupport>,
|
||||||
|
val backend: PlayerBackend,
|
||||||
val playMethod: PlayMethod,
|
val playMethod: PlayMethod,
|
||||||
val playSessionId: String?,
|
val playSessionId: String?,
|
||||||
val liveStreamId: String?,
|
val liveStreamId: String?,
|
||||||
|
val mediaSourceInfo: MediaSourceInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed interface SubtitleSearch {
|
sealed interface SubtitleSearch {
|
||||||
|
|
@ -1044,81 +1105,205 @@ val Format.idAsInt: Int?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of external subtitle streams there are
|
||||||
|
*/
|
||||||
|
val MediaSourceInfo.externalSubtitlesCount: Int
|
||||||
|
get() =
|
||||||
|
mediaStreams
|
||||||
|
?.count { it.type == MediaStreamType.SUBTITLE && it.isExternal } ?: 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of embedded subtitle streams there are
|
||||||
|
*/
|
||||||
|
val MediaSourceInfo.embeddedSubtitleCount: Int
|
||||||
|
get() =
|
||||||
|
mediaStreams
|
||||||
|
?.count { it.type == MediaStreamType.SUBTITLE && !it.isExternal } ?: 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of video streams there are
|
||||||
|
*/
|
||||||
|
val MediaSourceInfo.videoStreamCount: Int
|
||||||
|
get() =
|
||||||
|
mediaStreams
|
||||||
|
?.count { it.type == MediaStreamType.VIDEO } ?: 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of audio streams there are
|
||||||
|
*/
|
||||||
|
val MediaSourceInfo.audioStreamCount: Int
|
||||||
|
get() =
|
||||||
|
mediaStreams
|
||||||
|
?.count { it.type == MediaStreamType.AUDIO } ?: 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the [MediaStream] for the given subtitle index iff it is external
|
||||||
|
*/
|
||||||
|
fun MediaSourceInfo.findExternalSubtitle(subtitleIndex: Int?): MediaStream? =
|
||||||
|
subtitleIndex?.let {
|
||||||
|
mediaStreams
|
||||||
|
?.firstOrNull { it.type == MediaStreamType.SUBTITLE && it.isExternal && it.index == subtitleIndex }
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun <T> onMain(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block)
|
suspend fun <T> onMain(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block)
|
||||||
|
|
||||||
|
data class TrackSelectionResult(
|
||||||
|
val audioSelected: Boolean,
|
||||||
|
val subtitleSelected: Boolean,
|
||||||
|
) {
|
||||||
|
val bothSelected: Boolean = audioSelected && subtitleSelected
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private fun applyTrackSelections(
|
private fun applyTrackSelections(
|
||||||
player: Player,
|
player: Player,
|
||||||
|
playerBackend: PlayerBackend,
|
||||||
supportsDirectPlay: Boolean,
|
supportsDirectPlay: Boolean,
|
||||||
audioIndex: Int?,
|
audioIndex: Int?,
|
||||||
subtitleIndex: Int?,
|
subtitleIndex: Int?,
|
||||||
externalSubtitleCount: Int,
|
source: MediaSourceInfo,
|
||||||
subtitleIsExternal: Boolean,
|
): TrackSelectionResult {
|
||||||
) {
|
val videoStreamCount = source.videoStreamCount
|
||||||
if (subtitleIndex != null && subtitleIndex >= 0 && (subtitleIsExternal || supportsDirectPlay)) {
|
val audioStreamCount = source.audioStreamCount
|
||||||
val chosenTrack =
|
val embeddedSubtitleCount = source.embeddedSubtitleCount
|
||||||
if (subtitleIsExternal) {
|
val externalSubtitleCount = source.externalSubtitlesCount
|
||||||
player.currentTracks.groups.firstOrNull { group ->
|
|
||||||
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
|
val paramsBuilder = player.trackSelectionParameters.buildUpon()
|
||||||
(0..<group.mediaTrackGroup.length)
|
val tracks = player.currentTracks.groups
|
||||||
.mapNotNull {
|
|
||||||
group.getTrackFormat(it).id
|
val subtitleSelected =
|
||||||
}.any { it.endsWith("e:$subtitleIndex") }
|
if (subtitleIndex != null && subtitleIndex >= 0) {
|
||||||
|
val subtitleIsExternal = source.findExternalSubtitle(subtitleIndex) != null
|
||||||
|
if (subtitleIsExternal || supportsDirectPlay) {
|
||||||
|
val chosenTrack =
|
||||||
|
if (subtitleIsExternal && playerBackend == PlayerBackend.EXO_PLAYER) {
|
||||||
|
tracks.firstOrNull { group ->
|
||||||
|
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
|
||||||
|
(0..<group.mediaTrackGroup.length)
|
||||||
|
.mapNotNull {
|
||||||
|
group.getTrackFormat(it).id
|
||||||
|
}.any { it.endsWith("e:$subtitleIndex") }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val indexToFind =
|
||||||
|
calculateIndexToFind(
|
||||||
|
subtitleIndex,
|
||||||
|
MediaStreamType.SUBTITLE,
|
||||||
|
playerBackend,
|
||||||
|
videoStreamCount,
|
||||||
|
audioStreamCount,
|
||||||
|
embeddedSubtitleCount,
|
||||||
|
externalSubtitleCount,
|
||||||
|
subtitleIsExternal,
|
||||||
|
)
|
||||||
|
Timber.v("Chosen subtitle ($subtitleIndex/$indexToFind) track")
|
||||||
|
// subtitleIndex - externalSubtitleCount + 1
|
||||||
|
tracks.firstOrNull { group ->
|
||||||
|
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
|
||||||
|
(0..<group.mediaTrackGroup.length)
|
||||||
|
.map {
|
||||||
|
group.getTrackFormat(it).idAsInt
|
||||||
|
}.contains(indexToFind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.v("Chosen subtitle ($subtitleIndex) track: $chosenTrack")
|
||||||
|
chosenTrack?.let {
|
||||||
|
paramsBuilder
|
||||||
|
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
|
||||||
|
.setOverrideForType(
|
||||||
|
TrackSelectionOverride(
|
||||||
|
chosenTrack.mediaTrackGroup,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
chosenTrack != null
|
||||||
} else {
|
} else {
|
||||||
val indexToFind = subtitleIndex - externalSubtitleCount + 1
|
false
|
||||||
player.currentTracks.groups.firstOrNull { group ->
|
}
|
||||||
group.type == C.TRACK_TYPE_TEXT && group.isSupported &&
|
} else {
|
||||||
|
paramsBuilder
|
||||||
|
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
val audioSelected =
|
||||||
|
if (audioIndex != null && supportsDirectPlay) {
|
||||||
|
val indexToFind =
|
||||||
|
calculateIndexToFind(
|
||||||
|
audioIndex,
|
||||||
|
MediaStreamType.AUDIO,
|
||||||
|
playerBackend,
|
||||||
|
videoStreamCount,
|
||||||
|
audioStreamCount,
|
||||||
|
embeddedSubtitleCount,
|
||||||
|
externalSubtitleCount,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
val chosenTrack =
|
||||||
|
tracks.firstOrNull { group ->
|
||||||
|
group.type == C.TRACK_TYPE_AUDIO && group.isSupported &&
|
||||||
(0..<group.mediaTrackGroup.length)
|
(0..<group.mediaTrackGroup.length)
|
||||||
.map {
|
.map {
|
||||||
group.getTrackFormat(it).idAsInt
|
group.getTrackFormat(it).idAsInt
|
||||||
}.contains(indexToFind)
|
}.contains(indexToFind)
|
||||||
}
|
}
|
||||||
}
|
Timber.v("Chosen audio ($audioIndex/$indexToFind) track: $chosenTrack")
|
||||||
|
chosenTrack?.let {
|
||||||
Timber.v("Chosen subtitle ($subtitleIndex) track: $chosenTrack")
|
paramsBuilder
|
||||||
chosenTrack?.let {
|
|
||||||
player.trackSelectionParameters =
|
|
||||||
player.trackSelectionParameters
|
|
||||||
.buildUpon()
|
|
||||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
|
|
||||||
.setOverrideForType(
|
|
||||||
TrackSelectionOverride(
|
|
||||||
chosenTrack.mediaTrackGroup,
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
).build()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
player.trackSelectionParameters =
|
|
||||||
player.trackSelectionParameters
|
|
||||||
.buildUpon()
|
|
||||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
if (audioIndex != null && supportsDirectPlay) {
|
|
||||||
val indexToFind =
|
|
||||||
audioIndex - externalSubtitleCount + 1
|
|
||||||
val chosenTrack =
|
|
||||||
player.currentTracks.groups.firstOrNull { group ->
|
|
||||||
group.type == C.TRACK_TYPE_AUDIO && group.isSupported &&
|
|
||||||
(0..<group.mediaTrackGroup.length)
|
|
||||||
.map {
|
|
||||||
group.getTrackFormat(it).idAsInt
|
|
||||||
}.contains(indexToFind)
|
|
||||||
}
|
|
||||||
Timber.v("Chosen audio ($audioIndex/$indexToFind) track: $chosenTrack")
|
|
||||||
chosenTrack?.let {
|
|
||||||
player.trackSelectionParameters =
|
|
||||||
player.trackSelectionParameters
|
|
||||||
.buildUpon()
|
|
||||||
.setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, false)
|
.setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, false)
|
||||||
.setOverrideForType(
|
.setOverrideForType(
|
||||||
TrackSelectionOverride(
|
TrackSelectionOverride(
|
||||||
chosenTrack.mediaTrackGroup,
|
chosenTrack.mediaTrackGroup,
|
||||||
0,
|
0,
|
||||||
),
|
),
|
||||||
).build()
|
)
|
||||||
|
}
|
||||||
|
chosenTrack != null
|
||||||
|
} else {
|
||||||
|
audioIndex == null
|
||||||
|
}
|
||||||
|
if (audioSelected && subtitleSelected) {
|
||||||
|
player.trackSelectionParameters = paramsBuilder.build()
|
||||||
|
}
|
||||||
|
return TrackSelectionResult(audioSelected, subtitleSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the server provided index to the track index based on the [PlayerBackend] and other stream information
|
||||||
|
*/
|
||||||
|
private fun calculateIndexToFind(
|
||||||
|
serverIndex: Int,
|
||||||
|
type: MediaStreamType,
|
||||||
|
playerBackend: PlayerBackend,
|
||||||
|
videoStreamCount: Int,
|
||||||
|
audioStreamCount: Int,
|
||||||
|
embeddedSubtitleCount: Int,
|
||||||
|
externalSubtitleCount: Int,
|
||||||
|
subtitleIsExternal: Boolean,
|
||||||
|
): Int =
|
||||||
|
when (playerBackend) {
|
||||||
|
PlayerBackend.EXO_PLAYER,
|
||||||
|
PlayerBackend.UNRECOGNIZED,
|
||||||
|
-> {
|
||||||
|
serverIndex - externalSubtitleCount + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO MPV could use literal indexes because they are stored in the track format ID
|
||||||
|
PlayerBackend.MPV -> {
|
||||||
|
when (type) {
|
||||||
|
MediaStreamType.VIDEO -> serverIndex - externalSubtitleCount + 1
|
||||||
|
MediaStreamType.AUDIO -> serverIndex - externalSubtitleCount - videoStreamCount + 1
|
||||||
|
MediaStreamType.SUBTITLE -> {
|
||||||
|
if (subtitleIsExternal) {
|
||||||
|
serverIndex + embeddedSubtitleCount + 1
|
||||||
|
} else {
|
||||||
|
serverIndex - externalSubtitleCount - videoStreamCount - audioStreamCount + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> throw UnsupportedOperationException("Cannot calculate index for $type")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.playback
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.listen
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
@Composable
|
||||||
|
fun rememberPlayerLoadingState(player: Player): PlayerLoadingState {
|
||||||
|
val state = remember(player) { PlayerLoadingState(player) }
|
||||||
|
LaunchedEffect(player) {
|
||||||
|
state.observe()
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
class PlayerLoadingState(
|
||||||
|
private val player: Player,
|
||||||
|
) : State<Boolean> {
|
||||||
|
override var value by mutableStateOf(player.isLoading)
|
||||||
|
private set
|
||||||
|
|
||||||
|
suspend fun observe() {
|
||||||
|
value = player.isLoading
|
||||||
|
player.listen {
|
||||||
|
if (it.contains(Player.EVENT_IS_LOADING_CHANGED)) {
|
||||||
|
value = player.isLoading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.ui.preferences
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -10,6 +11,12 @@ import kotlinx.serialization.Serializable
|
||||||
data class PreferenceGroup(
|
data class PreferenceGroup(
|
||||||
@param:StringRes val title: Int,
|
@param:StringRes val title: Int,
|
||||||
val preferences: List<AppPreference<out Any?>>,
|
val preferences: List<AppPreference<out Any?>>,
|
||||||
|
val conditionalPreferences: List<ConditionalPreferences> = listOf(),
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ConditionalPreferences(
|
||||||
|
val condition: (AppPreferences) -> Boolean,
|
||||||
|
val preferences: List<AppPreference<out Any?>>,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,11 @@ import coil3.SingletonImageLoader
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
|
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||||
import com.github.damontecres.wholphin.preferences.advancedPreferences
|
import com.github.damontecres.wholphin.preferences.advancedPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.basicPreferences
|
import com.github.damontecres.wholphin.preferences.basicPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.uiPreferences
|
import com.github.damontecres.wholphin.preferences.uiPreferences
|
||||||
|
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.playOnClickSound
|
import com.github.damontecres.wholphin.ui.playOnClickSound
|
||||||
|
|
@ -53,9 +55,11 @@ import com.github.damontecres.wholphin.ui.playSoundOnFocus
|
||||||
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings
|
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings
|
||||||
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleStylePage
|
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleStylePage
|
||||||
import com.github.damontecres.wholphin.ui.setup.UpdateViewModel
|
import com.github.damontecres.wholphin.ui.setup.UpdateViewModel
|
||||||
|
import com.github.damontecres.wholphin.ui.showToast
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PreferencesContent(
|
fun PreferencesContent(
|
||||||
|
|
@ -112,6 +116,22 @@ fun PreferencesContent(
|
||||||
visible = true
|
visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(preferences.playbackPreferences.playerBackend) {
|
||||||
|
if (preferences.playbackPreferences.playerBackend == PlayerBackend.MPV) {
|
||||||
|
Timber.d("Checking for libmpv")
|
||||||
|
try {
|
||||||
|
System.loadLibrary("mpv")
|
||||||
|
System.loadLibrary("player")
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.w(ex, "Could not load libmpv")
|
||||||
|
showToast(context, "MPV is not supported on this device")
|
||||||
|
viewModel.preferenceDataStore.updateData {
|
||||||
|
it.updatePlaybackPreferences { playerBackend = PlayerBackend.EXO_PLAYER }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn() + slideInHorizontally { it / 2 },
|
enter = fadeIn() + slideInHorizontally { it / 2 },
|
||||||
|
|
@ -179,7 +199,13 @@ fun PreferencesContent(
|
||||||
.padding(top = 8.dp, bottom = 4.dp),
|
.padding(top = 8.dp, bottom = 4.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
group.preferences.forEachIndexed { prefIndex, pref ->
|
val groupPreferences =
|
||||||
|
group.preferences +
|
||||||
|
group.conditionalPreferences
|
||||||
|
.filter { it.condition.invoke(preferences) }
|
||||||
|
.map { it.preferences }
|
||||||
|
.flatten()
|
||||||
|
groupPreferences.forEachIndexed { prefIndex, pref ->
|
||||||
pref as AppPreference<Any>
|
pref as AppPreference<Any>
|
||||||
item {
|
item {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import android.graphics.Typeface
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.ui.CaptionStyleCompat
|
import androidx.media3.ui.CaptionStyleCompat
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
|
|
@ -17,6 +19,8 @@ import com.github.damontecres.wholphin.preferences.SubtitlePreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
||||||
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup
|
import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MPVLib
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.setPropertyColor
|
||||||
|
|
||||||
object SubtitleSettings {
|
object SubtitleSettings {
|
||||||
val FontSize =
|
val FontSize =
|
||||||
|
|
@ -245,4 +249,56 @@ object SubtitleSettings {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun SubtitlePreferences.applyToMpv(density: Density) {
|
||||||
|
val fo = (fontOpacity / 100.0 * 255).toInt().shl(24)
|
||||||
|
val fc = Color(fo.or(fontColor))
|
||||||
|
val bg = Color((backgroundOpacity / 100.0 * 255).toInt().shl(24).or(backgroundColor))
|
||||||
|
val edge = Color(fo.or(edgeColor))
|
||||||
|
|
||||||
|
// TODO weird, but seems to get the size to be very close to matching sizes between renderers
|
||||||
|
val fontSizePx = with(density) { fontSize.sp.toPx() * .8 }.toInt()
|
||||||
|
MPVLib.setPropertyInt("sub-font-size", fontSizePx)
|
||||||
|
MPVLib.setPropertyColor("sub-color", fc)
|
||||||
|
MPVLib.setPropertyColor("sub-outline-color", edge)
|
||||||
|
|
||||||
|
when (edgeStyle) {
|
||||||
|
EdgeStyle.EDGE_NONE,
|
||||||
|
EdgeStyle.UNRECOGNIZED,
|
||||||
|
-> {
|
||||||
|
MPVLib.setPropertyInt("sub-shadow-offset", 0)
|
||||||
|
MPVLib.setPropertyDouble("sub-outline-size", 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
EdgeStyle.EDGE_SOLID -> {
|
||||||
|
MPVLib.setPropertyInt("sub-shadow-offset", 0)
|
||||||
|
MPVLib.setPropertyDouble("sub-outline-size", 1.15)
|
||||||
|
}
|
||||||
|
|
||||||
|
EdgeStyle.EDGE_SHADOW -> {
|
||||||
|
MPVLib.setPropertyInt("sub-shadow-offset", 4)
|
||||||
|
MPVLib.setPropertyDouble("sub-outline-size", 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (fontBold) {
|
||||||
|
// MPVLib.setPropertyString("sub-font", "Roboto Bold")
|
||||||
|
// } else {
|
||||||
|
// MPVLib.setPropertyString("sub-font", "Roboto Regular")
|
||||||
|
// }
|
||||||
|
MPVLib.setPropertyBoolean("sub-bold", fontBold)
|
||||||
|
MPVLib.setPropertyBoolean("sub-italic", fontItalic)
|
||||||
|
|
||||||
|
MPVLib.setPropertyColor("sub-back-color", bg)
|
||||||
|
val borderStyle =
|
||||||
|
when (backgroundStyle) {
|
||||||
|
BackgroundStyle.UNRECOGNIZED,
|
||||||
|
BackgroundStyle.BG_NONE,
|
||||||
|
-> "outline-and-shadow"
|
||||||
|
|
||||||
|
BackgroundStyle.BG_WRAP -> "opaque-box"
|
||||||
|
BackgroundStyle.BG_BOXED -> "background-box"
|
||||||
|
}
|
||||||
|
MPVLib.setPropertyString("sub-border-style", borderStyle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,334 @@
|
||||||
|
package com.github.damontecres.wholphin.util.mpv
|
||||||
|
|
||||||
|
/*
|
||||||
|
This file is copied from https://github.com/mpv-android/mpv-android/blob/master/app/src/main/java/is/xyz/mpv/MPVLib.kt
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.view.Surface
|
||||||
|
|
||||||
|
// Wrapper for native library
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
object MPVLib {
|
||||||
|
init {
|
||||||
|
val libs = arrayOf("mpv", "player")
|
||||||
|
for (lib in libs) {
|
||||||
|
System.loadLibrary(lib)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
external fun create(appctx: Context)
|
||||||
|
|
||||||
|
external fun init()
|
||||||
|
|
||||||
|
external fun destroy()
|
||||||
|
|
||||||
|
external fun attachSurface(surface: Surface)
|
||||||
|
|
||||||
|
external fun detachSurface()
|
||||||
|
|
||||||
|
external fun command(cmd: Array<out String>)
|
||||||
|
|
||||||
|
external fun setOptionString(
|
||||||
|
name: String,
|
||||||
|
value: String,
|
||||||
|
): Int
|
||||||
|
|
||||||
|
external fun grabThumbnail(dimension: Int): Bitmap?
|
||||||
|
|
||||||
|
external fun getPropertyInt(property: String): Int?
|
||||||
|
|
||||||
|
external fun setPropertyInt(
|
||||||
|
property: String,
|
||||||
|
value: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
external fun getPropertyDouble(property: String): Double?
|
||||||
|
|
||||||
|
external fun setPropertyDouble(
|
||||||
|
property: String,
|
||||||
|
value: Double,
|
||||||
|
)
|
||||||
|
|
||||||
|
external fun getPropertyBoolean(property: String): Boolean?
|
||||||
|
|
||||||
|
external fun setPropertyBoolean(
|
||||||
|
property: String,
|
||||||
|
value: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
external fun getPropertyString(property: String): String?
|
||||||
|
|
||||||
|
external fun setPropertyString(
|
||||||
|
property: String,
|
||||||
|
value: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
external fun observeProperty(
|
||||||
|
property: String,
|
||||||
|
format: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val observers = mutableListOf<EventObserver>()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun addObserver(o: EventObserver) {
|
||||||
|
synchronized(observers) {
|
||||||
|
observers.add(o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun removeObserver(o: EventObserver) {
|
||||||
|
synchronized(observers) {
|
||||||
|
observers.remove(o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun eventProperty(
|
||||||
|
property: String,
|
||||||
|
value: Long,
|
||||||
|
) {
|
||||||
|
synchronized(observers) {
|
||||||
|
for (o in observers) {
|
||||||
|
o.eventProperty(property, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun eventProperty(
|
||||||
|
property: String,
|
||||||
|
value: Boolean,
|
||||||
|
) {
|
||||||
|
synchronized(observers) {
|
||||||
|
for (o in observers) {
|
||||||
|
o.eventProperty(property, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun eventProperty(
|
||||||
|
property: String,
|
||||||
|
value: Double,
|
||||||
|
) {
|
||||||
|
synchronized(observers) {
|
||||||
|
for (o in observers) {
|
||||||
|
o.eventProperty(property, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun eventProperty(
|
||||||
|
property: String,
|
||||||
|
value: String,
|
||||||
|
) {
|
||||||
|
synchronized(observers) {
|
||||||
|
for (o in observers) {
|
||||||
|
o.eventProperty(property, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun eventProperty(property: String) {
|
||||||
|
synchronized(observers) {
|
||||||
|
for (o in observers) {
|
||||||
|
o.eventProperty(property)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun event(eventId: Int) {
|
||||||
|
synchronized(observers) {
|
||||||
|
for (o in observers) {
|
||||||
|
o.event(eventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun eventEndFile(
|
||||||
|
reason: Int,
|
||||||
|
error: Int,
|
||||||
|
) {
|
||||||
|
synchronized(observers) {
|
||||||
|
for (o in observers) {
|
||||||
|
o.eventEndFile(reason, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val log_observers = mutableListOf<LogObserver>()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun addLogObserver(o: LogObserver) {
|
||||||
|
synchronized(log_observers) {
|
||||||
|
log_observers.add(o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun removeLogObserver(o: LogObserver) {
|
||||||
|
synchronized(log_observers) {
|
||||||
|
log_observers.remove(o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun logMessage(
|
||||||
|
prefix: String,
|
||||||
|
level: Int,
|
||||||
|
text: String,
|
||||||
|
) {
|
||||||
|
synchronized(log_observers) {
|
||||||
|
for (o in log_observers) {
|
||||||
|
o.logMessage(prefix, level, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventObserver {
|
||||||
|
fun eventProperty(property: String)
|
||||||
|
|
||||||
|
fun eventProperty(
|
||||||
|
property: String,
|
||||||
|
value: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun eventProperty(
|
||||||
|
property: String,
|
||||||
|
value: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun eventProperty(
|
||||||
|
property: String,
|
||||||
|
value: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun eventProperty(
|
||||||
|
property: String,
|
||||||
|
value: Double,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun event(eventId: Int)
|
||||||
|
|
||||||
|
fun eventEndFile(
|
||||||
|
reason: Int,
|
||||||
|
error: Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogObserver {
|
||||||
|
fun logMessage(
|
||||||
|
prefix: String,
|
||||||
|
level: Int,
|
||||||
|
text: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
object MpvFormat {
|
||||||
|
const val MPV_FORMAT_NONE: Int = 0
|
||||||
|
const val MPV_FORMAT_STRING: Int = 1
|
||||||
|
const val MPV_FORMAT_OSD_STRING: Int = 2
|
||||||
|
const val MPV_FORMAT_FLAG: Int = 3
|
||||||
|
const val MPV_FORMAT_INT64: Int = 4
|
||||||
|
const val MPV_FORMAT_DOUBLE: Int = 5
|
||||||
|
const val MPV_FORMAT_NODE: Int = 6
|
||||||
|
const val MPV_FORMAT_NODE_ARRAY: Int = 7
|
||||||
|
const val MPV_FORMAT_NODE_MAP: Int = 8
|
||||||
|
const val MPV_FORMAT_BYTE_ARRAY: Int = 9
|
||||||
|
}
|
||||||
|
|
||||||
|
object MpvEvent {
|
||||||
|
const val MPV_EVENT_NONE: Int = 0
|
||||||
|
const val MPV_EVENT_SHUTDOWN: Int = 1
|
||||||
|
const val MPV_EVENT_LOG_MESSAGE: Int = 2
|
||||||
|
const val MPV_EVENT_GET_PROPERTY_REPLY: Int = 3
|
||||||
|
const val MPV_EVENT_SET_PROPERTY_REPLY: Int = 4
|
||||||
|
const val MPV_EVENT_COMMAND_REPLY: Int = 5
|
||||||
|
const val MPV_EVENT_START_FILE: Int = 6
|
||||||
|
const val MPV_EVENT_END_FILE: Int = 7
|
||||||
|
const val MPV_EVENT_FILE_LOADED: Int = 8
|
||||||
|
|
||||||
|
@Deprecated("")
|
||||||
|
const val MPV_EVENT_IDLE: Int = 11
|
||||||
|
|
||||||
|
@Deprecated("")
|
||||||
|
const val MPV_EVENT_TICK: Int = 14
|
||||||
|
const val MPV_EVENT_CLIENT_MESSAGE: Int = 16
|
||||||
|
const val MPV_EVENT_VIDEO_RECONFIG: Int = 17
|
||||||
|
const val MPV_EVENT_AUDIO_RECONFIG: Int = 18
|
||||||
|
const val MPV_EVENT_SEEK: Int = 20
|
||||||
|
const val MPV_EVENT_PLAYBACK_RESTART: Int = 21
|
||||||
|
const val MPV_EVENT_PROPERTY_CHANGE: Int = 22
|
||||||
|
const val MPV_EVENT_QUEUE_OVERFLOW: Int = 24
|
||||||
|
const val MPV_EVENT_HOOK: Int = 25
|
||||||
|
}
|
||||||
|
|
||||||
|
object MpvLogLevel {
|
||||||
|
const val MPV_LOG_LEVEL_NONE: Int = 0
|
||||||
|
const val MPV_LOG_LEVEL_FATAL: Int = 10
|
||||||
|
const val MPV_LOG_LEVEL_ERROR: Int = 20
|
||||||
|
const val MPV_LOG_LEVEL_WARN: Int = 30
|
||||||
|
const val MPV_LOG_LEVEL_INFO: Int = 40
|
||||||
|
const val MPV_LOG_LEVEL_V: Int = 50
|
||||||
|
const val MPV_LOG_LEVEL_DEBUG: Int = 60
|
||||||
|
const val MPV_LOG_LEVEL_TRACE: Int = 70
|
||||||
|
}
|
||||||
|
|
||||||
|
object MpvEndFileReason {
|
||||||
|
const val MPV_END_FILE_REASON_EOF: Int = 0
|
||||||
|
const val MPV_END_FILE_REASON_STOP: Int = 2
|
||||||
|
const val MPV_END_FILE_REASON_QUIT: Int = 3
|
||||||
|
const val MPV_END_FILE_REASON_ERROR: Int = 4
|
||||||
|
const val MPV_END_FILE_REASON_REDIRECT: Int = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
object MpvError {
|
||||||
|
const val MPV_ERROR_SUCCESS = 0
|
||||||
|
const val MPV_ERROR_EVENT_QUEUE_FULL = -1
|
||||||
|
const val MPV_ERROR_NOMEM = -2
|
||||||
|
const val MPV_ERROR_UNINITIALIZED = -3
|
||||||
|
const val MPV_ERROR_INVALID_PARAMETER = -4
|
||||||
|
const val MPV_ERROR_OPTION_NOT_FOUND = -5
|
||||||
|
const val MPV_ERROR_OPTION_FORMAT = -6
|
||||||
|
const val MPV_ERROR_OPTION_ERROR = -7
|
||||||
|
const val MPV_ERROR_PROPERTY_NOT_FOUND = -8
|
||||||
|
const val MPV_ERROR_PROPERTY_FORMAT = -9
|
||||||
|
const val MPV_ERROR_PROPERTY_UNAVAILABLE = -10
|
||||||
|
const val MPV_ERROR_PROPERTY_ERROR = -11
|
||||||
|
const val MPV_ERROR_COMMAND = -12
|
||||||
|
const val MPV_ERROR_LOADING_FAILED = -13
|
||||||
|
const val MPV_ERROR_AO_INIT_FAILED = -14
|
||||||
|
const val MPV_ERROR_VO_INIT_FAILED = -15
|
||||||
|
const val MPV_ERROR_NOTHING_TO_PLAY = -16
|
||||||
|
const val MPV_ERROR_UNKNOWN_FORMAT = -17
|
||||||
|
const val MPV_ERROR_UNSUPPORTED = -18
|
||||||
|
const val MPV_ERROR_NOT_IMPLEMENTED = -19
|
||||||
|
const val MPV_ERROR_GENERIC = -20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package com.github.damontecres.wholphin.util.mpv
|
||||||
|
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvFormat.MPV_FORMAT_DOUBLE
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvFormat.MPV_FORMAT_FLAG
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvFormat.MPV_FORMAT_INT64
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvFormat.MPV_FORMAT_NONE
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvFormat.MPV_FORMAT_STRING
|
||||||
|
|
||||||
|
object MPVProperty {
|
||||||
|
const val POSITION = "time-pos"
|
||||||
|
const val POSITION_FULL = "time-pos/full"
|
||||||
|
const val DURATION = "duration/full"
|
||||||
|
const val PAUSED = "pause"
|
||||||
|
const val PAUSED_FOR_CACHE = "paused-for-cache"
|
||||||
|
const val SPEED = "speed"
|
||||||
|
const val TRACK_LIST = "track-list"
|
||||||
|
const val ASPECT = "video-params/aspect"
|
||||||
|
const val ROTATE = "video-params/rotate"
|
||||||
|
const val TRACK_VIDEO = "current-tracks/video/image"
|
||||||
|
const val METADATA = "metadata"
|
||||||
|
const val HWDEC = "hwdec-current"
|
||||||
|
const val MUTE = "mute"
|
||||||
|
const val TRACK_AUDIO = "current-tracks/audio/selected"
|
||||||
|
const val SEEKABLE = "seekable"
|
||||||
|
const val SUBTITLE_TEXT = "sub-text"
|
||||||
|
|
||||||
|
val observedProperties =
|
||||||
|
mapOf(
|
||||||
|
POSITION to MPV_FORMAT_INT64,
|
||||||
|
// POSITION_FULL to MPV_FORMAT_DOUBLE,
|
||||||
|
DURATION to MPV_FORMAT_DOUBLE,
|
||||||
|
PAUSED to MPV_FORMAT_FLAG,
|
||||||
|
PAUSED_FOR_CACHE to MPV_FORMAT_FLAG,
|
||||||
|
SPEED to MPV_FORMAT_STRING,
|
||||||
|
TRACK_LIST to MPV_FORMAT_NONE,
|
||||||
|
ASPECT to MPV_FORMAT_DOUBLE,
|
||||||
|
ROTATE to MPV_FORMAT_DOUBLE,
|
||||||
|
TRACK_VIDEO to MPV_FORMAT_NONE,
|
||||||
|
METADATA to MPV_FORMAT_NONE,
|
||||||
|
HWDEC to MPV_FORMAT_NONE,
|
||||||
|
MUTE to MPV_FORMAT_FLAG,
|
||||||
|
TRACK_AUDIO to MPV_FORMAT_NONE,
|
||||||
|
SEEKABLE to MPV_FORMAT_FLAG,
|
||||||
|
SUBTITLE_TEXT to MPV_FORMAT_STRING,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
package com.github.damontecres.wholphin.util.mpv
|
||||||
|
|
||||||
|
import com.github.damontecres.wholphin.util.profile.Codec
|
||||||
|
import com.github.damontecres.wholphin.util.profile.subtitleProfile
|
||||||
|
import com.github.damontecres.wholphin.util.profile.supportedAudioCodecs
|
||||||
|
import org.jellyfin.sdk.model.api.DlnaProfileType
|
||||||
|
import org.jellyfin.sdk.model.api.EncodingContext
|
||||||
|
import org.jellyfin.sdk.model.api.MediaStreamProtocol
|
||||||
|
import org.jellyfin.sdk.model.deviceprofile.buildDeviceProfile
|
||||||
|
|
||||||
|
val mpvDeviceProfile =
|
||||||
|
buildDeviceProfile {
|
||||||
|
name = "mpv"
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// maxStaticBitrate = maxBitrate
|
||||||
|
// maxStreamingBitrate = maxBitrate
|
||||||
|
transcodingProfile {
|
||||||
|
type = DlnaProfileType.VIDEO
|
||||||
|
context = EncodingContext.STREAMING
|
||||||
|
|
||||||
|
container = Codec.Container.TS
|
||||||
|
protocol = MediaStreamProtocol.HLS
|
||||||
|
|
||||||
|
videoCodec(Codec.Video.HEVC)
|
||||||
|
videoCodec(Codec.Video.H264)
|
||||||
|
|
||||||
|
audioCodec(*supportedAudioCodecs)
|
||||||
|
|
||||||
|
copyTimestamps = false
|
||||||
|
enableSubtitlesInManifest = true
|
||||||
|
}
|
||||||
|
directPlayProfile {
|
||||||
|
type = DlnaProfileType.VIDEO
|
||||||
|
|
||||||
|
container(
|
||||||
|
Codec.Container.ASF,
|
||||||
|
Codec.Container.AVI,
|
||||||
|
Codec.Container.HLS,
|
||||||
|
Codec.Container.M4V,
|
||||||
|
Codec.Container.MPEG,
|
||||||
|
Codec.Container.MPEGTS,
|
||||||
|
Codec.Container.MKV,
|
||||||
|
Codec.Container.MOV,
|
||||||
|
Codec.Container.MP4,
|
||||||
|
Codec.Container.MPG,
|
||||||
|
Codec.Container.OGM,
|
||||||
|
Codec.Container.OGV,
|
||||||
|
Codec.Container.TS,
|
||||||
|
Codec.Container.VOB,
|
||||||
|
Codec.Container.WEBM,
|
||||||
|
Codec.Container.WMV,
|
||||||
|
Codec.Container.XVID,
|
||||||
|
)
|
||||||
|
|
||||||
|
videoCodec(
|
||||||
|
Codec.Video.AV1,
|
||||||
|
Codec.Video.H264,
|
||||||
|
Codec.Video.HEVC,
|
||||||
|
Codec.Video.MPEG,
|
||||||
|
Codec.Video.MPEG2VIDEO,
|
||||||
|
Codec.Video.VP8,
|
||||||
|
Codec.Video.VP9,
|
||||||
|
)
|
||||||
|
|
||||||
|
audioCodec(*supportedAudioCodecs, Codec.Audio.WAV, Codec.Audio.OGG)
|
||||||
|
}
|
||||||
|
|
||||||
|
subtitleProfile(Codec.Subtitle.VTT, embedded = true, hls = true, external = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.WEBVTT, embedded = true, hls = true, external = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.SRT, embedded = true, external = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.SUBRIP, embedded = true, external = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.TTML, embedded = true, external = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.DVBSUB, embedded = true, encode = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.DVDSUB, embedded = true, encode = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.IDX, embedded = true, encode = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.PGS, embedded = true, encode = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.PGSSUB, embedded = true, encode = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.ASS, encode = true, embedded = true, external = true)
|
||||||
|
subtitleProfile(Codec.Subtitle.SSA, encode = true, embedded = true, external = true)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,817 @@
|
||||||
|
package com.github.damontecres.wholphin.util.mpv
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.view.Surface
|
||||||
|
import android.view.SurfaceHolder
|
||||||
|
import android.view.SurfaceView
|
||||||
|
import android.view.TextureView
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.media3.common.AudioAttributes
|
||||||
|
import androidx.media3.common.BasePlayer
|
||||||
|
import androidx.media3.common.C
|
||||||
|
import androidx.media3.common.DeviceInfo
|
||||||
|
import androidx.media3.common.Format
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.MediaMetadata
|
||||||
|
import androidx.media3.common.MimeTypes
|
||||||
|
import androidx.media3.common.PlaybackException
|
||||||
|
import androidx.media3.common.PlaybackParameters
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.Timeline
|
||||||
|
import androidx.media3.common.TrackGroup
|
||||||
|
import androidx.media3.common.TrackSelectionParameters
|
||||||
|
import androidx.media3.common.Tracks
|
||||||
|
import androidx.media3.common.VideoSize
|
||||||
|
import androidx.media3.common.text.CueGroup
|
||||||
|
import androidx.media3.common.util.Clock
|
||||||
|
import androidx.media3.common.util.ListenerSet
|
||||||
|
import androidx.media3.common.util.Size
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.common.util.Util
|
||||||
|
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||||
|
import androidx.media3.exoplayer.trackselection.TrackSelector
|
||||||
|
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEndFileReason.MPV_END_FILE_REASON_EOF
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEndFileReason.MPV_END_FILE_REASON_ERROR
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEndFileReason.MPV_END_FILE_REASON_STOP
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_AUDIO_RECONFIG
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_END_FILE
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_FILE_LOADED
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_PLAYBACK_RESTART
|
||||||
|
import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_VIDEO_RECONFIG
|
||||||
|
import timber.log.Timber
|
||||||
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is barebones implementation of a [Player] which plays content using libmpv
|
||||||
|
*
|
||||||
|
* It doesn't support every feature or emit every event
|
||||||
|
*/
|
||||||
|
@kotlin.OptIn(ExperimentalAtomicApi::class)
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
class MpvPlayer(
|
||||||
|
private val context: Context,
|
||||||
|
enableHardwareDecoding: Boolean,
|
||||||
|
) : BasePlayer(),
|
||||||
|
MPVLib.EventObserver,
|
||||||
|
TrackSelector.InvalidationListener {
|
||||||
|
companion object {
|
||||||
|
private const val DEBUG = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isPaused: Boolean = true
|
||||||
|
private var surface: Surface? = null
|
||||||
|
|
||||||
|
private val looper = Util.getCurrentOrMainLooper()
|
||||||
|
private val handler = Handler(looper)
|
||||||
|
private val listeners =
|
||||||
|
ListenerSet<Player.Listener>(
|
||||||
|
looper,
|
||||||
|
Clock.DEFAULT,
|
||||||
|
) { listener, eventFlags ->
|
||||||
|
listener.onEvents(this@MpvPlayer, Player.Events(eventFlags))
|
||||||
|
}
|
||||||
|
private val availableCommands: Player.Commands
|
||||||
|
private val trackSelector = DefaultTrackSelector(context)
|
||||||
|
|
||||||
|
private var mediaItem: MediaItem? = null
|
||||||
|
private var startPositionMs: Long = 0L
|
||||||
|
private var durationMs: Long = 0L
|
||||||
|
private var positionMs: Long = -1L
|
||||||
|
private var playbackState: Int = STATE_READY
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var isReleased = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var isLoadingFile = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
Timber.v("config-dir=${context.filesDir.path}")
|
||||||
|
MPVLib.create(context)
|
||||||
|
MPVLib.setOptionString("config", "yes")
|
||||||
|
MPVLib.setOptionString("config-dir", context.filesDir.path)
|
||||||
|
|
||||||
|
if (enableHardwareDecoding) {
|
||||||
|
MPVLib.setOptionString("hwdec", "mediacodec,mediacodec-copy")
|
||||||
|
MPVLib.setOptionString("vo", "gpu")
|
||||||
|
} else {
|
||||||
|
MPVLib.setOptionString("hwdec", "no")
|
||||||
|
}
|
||||||
|
MPVLib.setOptionString("gpu-context", "android")
|
||||||
|
|
||||||
|
MPVLib.setOptionString("opengl-es", "yes")
|
||||||
|
MPVLib.setOptionString("hwdec-codecs", "h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1")
|
||||||
|
val cacheMegs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) 64 else 32
|
||||||
|
MPVLib.setOptionString("demuxer-max-bytes", "${cacheMegs * 1024 * 1024}")
|
||||||
|
MPVLib.setOptionString("demuxer-max-back-bytes", "${cacheMegs * 1024 * 1024}")
|
||||||
|
|
||||||
|
MPVLib.init()
|
||||||
|
|
||||||
|
MPVLib.setOptionString("force-window", "no")
|
||||||
|
MPVLib.setOptionString("idle", "yes")
|
||||||
|
// MPVLib.setOptionString("sub-fonts-dir", File(context.filesDir, "fonts").absolutePath)
|
||||||
|
|
||||||
|
MPVLib.addObserver(this)
|
||||||
|
MPVProperty.observedProperties.forEach(MPVLib::observeProperty)
|
||||||
|
|
||||||
|
availableCommands =
|
||||||
|
Player.Commands
|
||||||
|
.Builder()
|
||||||
|
.addAll(
|
||||||
|
COMMAND_PLAY_PAUSE,
|
||||||
|
COMMAND_PREPARE,
|
||||||
|
COMMAND_STOP,
|
||||||
|
COMMAND_SET_SPEED_AND_PITCH,
|
||||||
|
COMMAND_SET_SHUFFLE_MODE,
|
||||||
|
COMMAND_SET_REPEAT_MODE,
|
||||||
|
COMMAND_GET_CURRENT_MEDIA_ITEM,
|
||||||
|
COMMAND_GET_TIMELINE,
|
||||||
|
COMMAND_GET_METADATA,
|
||||||
|
// COMMAND_SET_PLAYLIST_METADATA,
|
||||||
|
COMMAND_SET_MEDIA_ITEM,
|
||||||
|
// COMMAND_CHANGE_MEDIA_ITEMS,
|
||||||
|
COMMAND_GET_TRACKS,
|
||||||
|
// COMMAND_GET_AUDIO_ATTRIBUTES,
|
||||||
|
// COMMAND_SET_AUDIO_ATTRIBUTES,
|
||||||
|
// COMMAND_GET_VOLUME,
|
||||||
|
// COMMAND_SET_VOLUME,
|
||||||
|
COMMAND_SET_VIDEO_SURFACE,
|
||||||
|
// COMMAND_GET_TEXT,
|
||||||
|
COMMAND_RELEASE,
|
||||||
|
).build()
|
||||||
|
trackSelector.init(this, DefaultBandwidthMeter.getSingletonInstance(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getApplicationLooper(): Looper = looper
|
||||||
|
|
||||||
|
override fun addListener(listener: Player.Listener) {
|
||||||
|
if (DEBUG) Timber.v("addListener")
|
||||||
|
listeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeListener(listener: Player.Listener) {
|
||||||
|
if (DEBUG) Timber.v("removeListener")
|
||||||
|
listeners.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMediaItems(
|
||||||
|
mediaItems: List<MediaItem>,
|
||||||
|
resetPosition: Boolean,
|
||||||
|
) {
|
||||||
|
throwIfReleased()
|
||||||
|
|
||||||
|
if (DEBUG) Timber.v("setMediaItems")
|
||||||
|
mediaItems.firstOrNull()?.let {
|
||||||
|
mediaItem = it
|
||||||
|
if (surface != null) {
|
||||||
|
loadFile(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMediaItems(
|
||||||
|
mediaItems: List<MediaItem>,
|
||||||
|
startIndex: Int,
|
||||||
|
startPositionMs: Long,
|
||||||
|
) {
|
||||||
|
if (DEBUG) Timber.v("setMediaItems")
|
||||||
|
this.startPositionMs = startPositionMs
|
||||||
|
setMediaItems(mediaItems.subList(startIndex, mediaItems.size), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addMediaItems(
|
||||||
|
index: Int,
|
||||||
|
mediaItems: List<MediaItem>,
|
||||||
|
): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun moveMediaItems(
|
||||||
|
fromIndex: Int,
|
||||||
|
toIndex: Int,
|
||||||
|
newIndex: Int,
|
||||||
|
): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun replaceMediaItems(
|
||||||
|
fromIndex: Int,
|
||||||
|
toIndex: Int,
|
||||||
|
mediaItems: List<MediaItem>,
|
||||||
|
): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun removeMediaItems(
|
||||||
|
fromIndex: Int,
|
||||||
|
toIndex: Int,
|
||||||
|
): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun getAvailableCommands(): Player.Commands = availableCommands
|
||||||
|
|
||||||
|
override fun prepare() {
|
||||||
|
if (DEBUG) Timber.v("prepare")
|
||||||
|
durationMs = 0L
|
||||||
|
positionMs = -1L
|
||||||
|
playbackState = STATE_READY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPlaybackState(): Int {
|
||||||
|
if (DEBUG) Timber.v("getPlaybackState")
|
||||||
|
return playbackState
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPlaybackSuppressionReason(): Int = Player.PLAYBACK_SUPPRESSION_REASON_NONE
|
||||||
|
|
||||||
|
override fun getPlayerError(): PlaybackException? {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setPlayWhenReady(playWhenReady: Boolean) {
|
||||||
|
if (isReleased) return
|
||||||
|
if (DEBUG) Timber.v("setPlayWhenReady")
|
||||||
|
if (playWhenReady) {
|
||||||
|
MPVLib.setPropertyBoolean("pause", false)
|
||||||
|
} else {
|
||||||
|
MPVLib.setPropertyBoolean("pause", true)
|
||||||
|
}
|
||||||
|
notifyListeners(EVENT_PLAY_WHEN_READY_CHANGED) {
|
||||||
|
onPlayWhenReadyChanged(
|
||||||
|
playWhenReady,
|
||||||
|
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPlayWhenReady(): Boolean {
|
||||||
|
if (DEBUG) Timber.v("getPlayWhenReady")
|
||||||
|
if (isReleased) return false
|
||||||
|
val isPaused = MPVLib.getPropertyBoolean("pause") ?: this.isPaused
|
||||||
|
return !isPaused
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setRepeatMode(repeatMode: Int) {
|
||||||
|
if (DEBUG) Timber.v("setRepeatMode")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRepeatMode(): Int = Player.REPEAT_MODE_OFF
|
||||||
|
|
||||||
|
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {
|
||||||
|
if (DEBUG) Timber.v("setShuffleModeEnabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getShuffleModeEnabled(): Boolean = false
|
||||||
|
|
||||||
|
override fun isLoading(): Boolean = isLoadingFile
|
||||||
|
|
||||||
|
override fun getSeekBackIncrement(): Long = 10_000
|
||||||
|
|
||||||
|
override fun getSeekForwardIncrement(): Long = 30_000
|
||||||
|
|
||||||
|
override fun getMaxSeekToPreviousPosition(): Long = 10_000
|
||||||
|
|
||||||
|
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
|
||||||
|
if (DEBUG) Timber.v("setPlaybackParameters")
|
||||||
|
MPVLib.setPropertyDouble("speed", playbackParameters.speed.toDouble())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPlaybackParameters(): PlaybackParameters {
|
||||||
|
if (DEBUG) Timber.v("getPlaybackParameters")
|
||||||
|
val speed = MPVLib.getPropertyDouble("speed")?.toFloat() ?: 1f
|
||||||
|
return PlaybackParameters(speed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
if (DEBUG) Timber.v("stop")
|
||||||
|
if (isReleased) return
|
||||||
|
pause()
|
||||||
|
mediaItem = null
|
||||||
|
positionMs = -1L
|
||||||
|
durationMs = 0L
|
||||||
|
playbackState = STATE_IDLE
|
||||||
|
notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(false) }
|
||||||
|
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) { onPlaybackStateChanged(STATE_IDLE) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun release() {
|
||||||
|
Timber.i("release")
|
||||||
|
if (!isReleased) {
|
||||||
|
MPVLib.removeObserver(this)
|
||||||
|
clearVideoSurfaceView(null)
|
||||||
|
MPVLib.destroy()
|
||||||
|
}
|
||||||
|
isReleased = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentTracks(): Tracks {
|
||||||
|
if (DEBUG) Timber.v("getCurrentTracks")
|
||||||
|
if (isReleased) return Tracks.EMPTY
|
||||||
|
return getTracks()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTrackSelectionParameters(): TrackSelectionParameters {
|
||||||
|
if (DEBUG) Timber.v("getTrackSelectionParameters")
|
||||||
|
|
||||||
|
return TrackSelectionParameters
|
||||||
|
.Builder()
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setTrackSelectionParameters(parameters: TrackSelectionParameters) {
|
||||||
|
Timber.v("TrackSelection: setTrackSelectionParameters %s", parameters)
|
||||||
|
if (isReleased) return
|
||||||
|
val tracks = getTracks()
|
||||||
|
if (C.TRACK_TYPE_TEXT in parameters.disabledTrackTypes) {
|
||||||
|
// Subtitles disabled
|
||||||
|
Timber.v("TrackSelection: disabling subtitles")
|
||||||
|
MPVLib.setPropertyString("sid", "no")
|
||||||
|
}
|
||||||
|
if (C.TRACK_TYPE_AUDIO in parameters.disabledTrackTypes) {
|
||||||
|
// Audio disabled
|
||||||
|
Timber.v("TrackSelection: disabling audio")
|
||||||
|
MPVLib.setPropertyString("aid", "no")
|
||||||
|
}
|
||||||
|
Timber.v("TrackSelection: Got ${parameters.overrides.size} overrides")
|
||||||
|
parameters.overrides.forEach { (trackGroup, trackSelectionOverride) ->
|
||||||
|
val result =
|
||||||
|
tracks.groups.firstOrNull { it.mediaTrackGroup == trackGroup }?.let {
|
||||||
|
val id = it.mediaTrackGroup.getFormat(0).id
|
||||||
|
val splits = id?.split(":")
|
||||||
|
val trackId = splits?.getOrNull(1)
|
||||||
|
val propertyName =
|
||||||
|
when (it.mediaTrackGroup.type) {
|
||||||
|
C.TRACK_TYPE_AUDIO -> "aid"
|
||||||
|
C.TRACK_TYPE_VIDEO -> "vid"
|
||||||
|
C.TRACK_TYPE_TEXT -> "sid"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
Timber.v("TrackSelection: activating %s %s '%s'", propertyName, trackId, id)
|
||||||
|
if (trackId != null && propertyName != null) {
|
||||||
|
MPVLib.setPropertyString(propertyName, trackId)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result != true) {
|
||||||
|
Timber.w(
|
||||||
|
"Did not find track to select for type=%s, id=%s",
|
||||||
|
trackGroup.type,
|
||||||
|
trackGroup.getFormat(0).id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMediaMetadata(): MediaMetadata {
|
||||||
|
if (DEBUG) Timber.v("getMediaMetadata")
|
||||||
|
return MediaMetadata.EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPlaylistMetadata(): MediaMetadata {
|
||||||
|
if (DEBUG) Timber.v("getPlaylistMetadata")
|
||||||
|
return MediaMetadata.EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setPlaylistMetadata(mediaMetadata: MediaMetadata): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun getCurrentTimeline(): Timeline {
|
||||||
|
if (DEBUG) Timber.v("getCurrentTimeline")
|
||||||
|
// TODO
|
||||||
|
return Timeline.EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentPeriodIndex(): Int {
|
||||||
|
if (DEBUG) Timber.v("getCurrentPeriodIndex")
|
||||||
|
// TODO
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentMediaItemIndex(): Int {
|
||||||
|
if (DEBUG) Timber.v("getCurrentMediaItemIndex")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDuration(): Long {
|
||||||
|
if (DEBUG) Timber.v("getDuration")
|
||||||
|
if (isReleased) {
|
||||||
|
return durationMs
|
||||||
|
}
|
||||||
|
val duration =
|
||||||
|
MPVLib.getPropertyDouble("duration/full")?.seconds?.inWholeMilliseconds
|
||||||
|
?: durationMs
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentPosition(): Long {
|
||||||
|
if (DEBUG) Timber.v("getCurrentPosition")
|
||||||
|
if (isReleased) {
|
||||||
|
return positionMs
|
||||||
|
}
|
||||||
|
val position =
|
||||||
|
MPVLib.getPropertyDouble("time-pos/full")?.seconds?.inWholeMilliseconds
|
||||||
|
?: positionMs
|
||||||
|
return position
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBufferedPosition(): Long {
|
||||||
|
if (DEBUG) Timber.v("getBufferedPosition")
|
||||||
|
return currentPosition + totalBufferedDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTotalBufferedDuration(): Long {
|
||||||
|
if (DEBUG) Timber.v("getTotalBufferedDuration")
|
||||||
|
if (isReleased) return 0
|
||||||
|
return MPVLib.getPropertyDouble("demuxer-cache-duration")?.seconds?.inWholeMilliseconds ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isPlayingAd(): Boolean {
|
||||||
|
if (DEBUG) Timber.v("isPlayingAd")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentAdGroupIndex(): Int = C.INDEX_UNSET
|
||||||
|
|
||||||
|
override fun getCurrentAdIndexInAdGroup(): Int = C.INDEX_UNSET
|
||||||
|
|
||||||
|
override fun getContentPosition(): Long = currentPosition
|
||||||
|
|
||||||
|
override fun getContentBufferedPosition(): Long = bufferedPosition
|
||||||
|
|
||||||
|
override fun getAudioAttributes(): AudioAttributes = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun setVolume(volume: Float): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun getVolume(): Float = 1f
|
||||||
|
|
||||||
|
override fun clearVideoSurface(): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun clearVideoSurface(surface: Surface?): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun setVideoSurface(surface: Surface?): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun setVideoSurfaceHolder(surfaceHolder: SurfaceHolder?): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun setVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||||
|
throwIfReleased()
|
||||||
|
if (DEBUG) Timber.v("setVideoSurfaceView")
|
||||||
|
val surface = surfaceView?.holder?.surface
|
||||||
|
if (surface != null) {
|
||||||
|
this.surface = surface
|
||||||
|
Timber.v("Queued attach")
|
||||||
|
MPVLib.attachSurface(surface)
|
||||||
|
MPVLib.setOptionString("force-window", "yes")
|
||||||
|
Timber.d("Attached surface")
|
||||||
|
mediaItem?.let(::loadFile)
|
||||||
|
if (mediaItem == null) {
|
||||||
|
Timber.w("mediaItem is null in setVideoSurfaceView")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clearVideoSurfaceView(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
|
||||||
|
Timber.d("clearVideoSurfaceView")
|
||||||
|
MPVLib.detachSurface()
|
||||||
|
MPVLib.setPropertyString("vo", "null")
|
||||||
|
MPVLib.setPropertyString("force-window", "no")
|
||||||
|
mediaItem = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setVideoTextureView(textureView: TextureView?): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun clearVideoTextureView(textureView: TextureView?): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun getVideoSize(): VideoSize {
|
||||||
|
if (DEBUG) Timber.v("getVideoSize")
|
||||||
|
if (isReleased) return VideoSize.UNKNOWN
|
||||||
|
val width = MPVLib.getPropertyInt("width")
|
||||||
|
val height = MPVLib.getPropertyInt("height")
|
||||||
|
return if (width != null && height != null) {
|
||||||
|
VideoSize(width, height)
|
||||||
|
} else {
|
||||||
|
VideoSize.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSurfaceSize(): Size = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun getCurrentCues(): CueGroup = CueGroup.EMPTY_TIME_ZERO
|
||||||
|
|
||||||
|
override fun getDeviceInfo(): DeviceInfo {
|
||||||
|
if (DEBUG) Timber.v("getDeviceInfo")
|
||||||
|
return DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDeviceVolume(): Int = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun isDeviceMuted(): Boolean = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun setDeviceVolume(volume: Int): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun setDeviceVolume(
|
||||||
|
volume: Int,
|
||||||
|
flags: Int,
|
||||||
|
): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun increaseDeviceVolume(): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun increaseDeviceVolume(flags: Int): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun decreaseDeviceVolume(): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun decreaseDeviceVolume(flags: Int): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun setDeviceMuted(muted: Boolean): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun setDeviceMuted(
|
||||||
|
muted: Boolean,
|
||||||
|
flags: Int,
|
||||||
|
): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun setAudioAttributes(
|
||||||
|
audioAttributes: AudioAttributes,
|
||||||
|
handleAudioFocus: Boolean,
|
||||||
|
): Unit = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun seekTo(
|
||||||
|
mediaItemIndex: Int,
|
||||||
|
positionMs: Long,
|
||||||
|
seekCommand: Int,
|
||||||
|
isRepeatingCurrentItem: Boolean,
|
||||||
|
) {
|
||||||
|
if (DEBUG) Timber.v("seekTo")
|
||||||
|
if (isReleased) {
|
||||||
|
Timber.w("seekTo called after release")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (mediaItemIndex == C.INDEX_UNSET) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
MPVLib.setPropertyDouble("time-pos", positionMs / 1000.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun eventProperty(property: String) {
|
||||||
|
if (DEBUG) Timber.v("eventProperty: $property")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun eventProperty(
|
||||||
|
property: String,
|
||||||
|
value: Long,
|
||||||
|
) {
|
||||||
|
if (DEBUG) Timber.v("eventPropertyLong: $property=$value")
|
||||||
|
when (property) {
|
||||||
|
MPVProperty.POSITION -> positionMs = value.seconds.inWholeMilliseconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun eventProperty(
|
||||||
|
property: String,
|
||||||
|
value: Boolean,
|
||||||
|
) {
|
||||||
|
if (DEBUG) Timber.v("eventPropertyBoolean: $property=$value")
|
||||||
|
when (property) {
|
||||||
|
MPVProperty.PAUSED -> {
|
||||||
|
isPaused = value
|
||||||
|
notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(!value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun eventProperty(
|
||||||
|
property: String,
|
||||||
|
value: String,
|
||||||
|
) {
|
||||||
|
if (DEBUG) Timber.v("eventPropertyString: $property=$value")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun eventProperty(
|
||||||
|
property: String,
|
||||||
|
value: Double,
|
||||||
|
) {
|
||||||
|
Timber.v("eventPropertyDouble: $property=$value")
|
||||||
|
when (property) {
|
||||||
|
MPVProperty.DURATION -> durationMs = value.seconds.inWholeMilliseconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun event(eventId: Int) {
|
||||||
|
when (eventId) {
|
||||||
|
// MPV_EVENT_START_FILE -> {
|
||||||
|
// }
|
||||||
|
MPV_EVENT_FILE_LOADED -> {
|
||||||
|
isLoadingFile = false
|
||||||
|
notifyListeners(EVENT_IS_LOADING_CHANGED) { onIsLoadingChanged(false) }
|
||||||
|
Timber.d("event: MPV_EVENT_FILE_LOADED")
|
||||||
|
mediaItem?.let {
|
||||||
|
it.localConfiguration?.subtitleConfigurations?.forEach {
|
||||||
|
val url = it.uri.toString()
|
||||||
|
val title = it.label ?: "External Subtitles"
|
||||||
|
Timber.v("Adding external subtitle track '$title'")
|
||||||
|
MPVLib.command(arrayOf("sub-add", url, "auto", title))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyListeners(EVENT_RENDERED_FIRST_FRAME) { onRenderedFirstFrame() }
|
||||||
|
notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(true) }
|
||||||
|
getTracks().let {
|
||||||
|
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MPV_EVENT_PLAYBACK_RESTART -> {
|
||||||
|
Timber.d("event: MPV_EVENT_PLAYBACK_RESTART")
|
||||||
|
getTracks().let {
|
||||||
|
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MPV_EVENT_AUDIO_RECONFIG -> {
|
||||||
|
Timber.d("event: MPV_EVENT_AUDIO_RECONFIG")
|
||||||
|
getTracks().let {
|
||||||
|
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MPV_EVENT_VIDEO_RECONFIG -> {
|
||||||
|
Timber.d("event: MPV_EVENT_VIDEO_RECONFIG")
|
||||||
|
getTracks().let {
|
||||||
|
notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MPV_EVENT_END_FILE -> {
|
||||||
|
Timber.d("event: MPV_EVENT_END_FILE")
|
||||||
|
// Handled by eventEndFile
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Timber.v("event: $eventId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun eventEndFile(
|
||||||
|
reason: Int,
|
||||||
|
error: Int,
|
||||||
|
) {
|
||||||
|
Timber.d("MPV_EVENT_END_FILE: %s %s", reason, error)
|
||||||
|
notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(false) }
|
||||||
|
when (reason) {
|
||||||
|
MPV_END_FILE_REASON_EOF -> {
|
||||||
|
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) {
|
||||||
|
onPlaybackStateChanged(STATE_ENDED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MPV_END_FILE_REASON_STOP -> {
|
||||||
|
// User initiated (eg stop, play next, etc)
|
||||||
|
}
|
||||||
|
|
||||||
|
MPV_END_FILE_REASON_ERROR -> {
|
||||||
|
Timber.e("libmpv error, error=%s", error)
|
||||||
|
notifyListeners(EVENT_PLAYER_ERROR) {
|
||||||
|
onPlayerError(
|
||||||
|
PlaybackException(
|
||||||
|
"libmpv error",
|
||||||
|
null,
|
||||||
|
error,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadFile(mediaItem: MediaItem) {
|
||||||
|
isLoadingFile = true
|
||||||
|
notifyListeners(EVENT_IS_LOADING_CHANGED) { onIsLoadingChanged(true) }
|
||||||
|
val url = mediaItem.localConfiguration?.uri.toString()
|
||||||
|
if (startPositionMs > 0) {
|
||||||
|
MPVLib.command(
|
||||||
|
arrayOf(
|
||||||
|
"loadfile",
|
||||||
|
url,
|
||||||
|
"replace",
|
||||||
|
"-1",
|
||||||
|
"start=${startPositionMs / 1000.0}",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
MPVLib.command(arrayOf("loadfile", url, "replace", "-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
MPVLib.setPropertyString("vo", "gpu")
|
||||||
|
Timber.d("Called loadfile")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun throwIfReleased() {
|
||||||
|
if (isReleased) {
|
||||||
|
throw IllegalStateException("Cannot access MpvPlayer after it is released")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyListeners(
|
||||||
|
eventId: Int,
|
||||||
|
block: Player.Listener.() -> Unit,
|
||||||
|
) {
|
||||||
|
handler.post {
|
||||||
|
listeners.queueEvent(eventId) {
|
||||||
|
block.invoke(it)
|
||||||
|
}
|
||||||
|
listeners.flushEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTracks(): Tracks {
|
||||||
|
val trackCount = MPVLib.getPropertyInt("track-list/count") ?: return Tracks.EMPTY
|
||||||
|
val groups =
|
||||||
|
(0..<trackCount).mapNotNull { idx ->
|
||||||
|
val type = MPVLib.getPropertyString("track-list/$idx/type")
|
||||||
|
val id = MPVLib.getPropertyInt("track-list/$idx/id")
|
||||||
|
val lang = MPVLib.getPropertyString("track-list/$idx/lang")
|
||||||
|
val codec = MPVLib.getPropertyString("track-list/$idx/codec")
|
||||||
|
val codecDescription = MPVLib.getPropertyString("track-list/$idx/codec-desc")
|
||||||
|
val isDefault = MPVLib.getPropertyBoolean("track-list/$idx/default") ?: false
|
||||||
|
val isForced = MPVLib.getPropertyBoolean("track-list/$idx/forced") ?: false
|
||||||
|
val isExternal = MPVLib.getPropertyBoolean("track-list/$idx/external") ?: false
|
||||||
|
val isSelected = MPVLib.getPropertyBoolean("track-list/$idx/selected") ?: false
|
||||||
|
val channelCount = MPVLib.getPropertyInt("track-list/$idx/demux-channel-count")
|
||||||
|
val title = MPVLib.getPropertyString("track-list/$idx/title")
|
||||||
|
|
||||||
|
if (type != null && id != null) {
|
||||||
|
// TODO do we need the real mimetypes?
|
||||||
|
val mimeType =
|
||||||
|
when (type) {
|
||||||
|
"video" -> MimeTypes.BASE_TYPE_VIDEO + "/todo"
|
||||||
|
"audio" -> MimeTypes.BASE_TYPE_AUDIO + "/todo"
|
||||||
|
"sub" -> MimeTypes.BASE_TYPE_TEXT + "/todo"
|
||||||
|
else -> "unknown/todo"
|
||||||
|
}
|
||||||
|
var flags = 0
|
||||||
|
if (isDefault) flags = flags or C.SELECTION_FLAG_DEFAULT
|
||||||
|
if (isForced) flags = flags or C.SELECTION_FLAG_FORCED
|
||||||
|
val builder =
|
||||||
|
Format
|
||||||
|
.Builder()
|
||||||
|
.setId("$idx:$id")
|
||||||
|
.setCodecs(codec)
|
||||||
|
.setSampleMimeType(mimeType)
|
||||||
|
.setLanguage(lang)
|
||||||
|
.setLabel(listOfNotNull(title, codecDescription).joinToString(","))
|
||||||
|
.setSelectionFlags(flags)
|
||||||
|
if (type == "video" && isSelected) {
|
||||||
|
builder.setWidth(MPVLib.getPropertyInt("width") ?: -1)
|
||||||
|
builder.setHeight(MPVLib.getPropertyInt("height") ?: -1)
|
||||||
|
}
|
||||||
|
channelCount?.let(builder::setChannelCount)
|
||||||
|
val format = builder.build()
|
||||||
|
|
||||||
|
val trackGroup = TrackGroup(format)
|
||||||
|
val group =
|
||||||
|
Tracks.Group(
|
||||||
|
trackGroup,
|
||||||
|
false,
|
||||||
|
intArrayOf(C.FORMAT_HANDLED),
|
||||||
|
booleanArrayOf(isSelected),
|
||||||
|
)
|
||||||
|
group
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Tracks(groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTrackSelectionsInvalidated() {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
var subtitleDelay: Double
|
||||||
|
get() {
|
||||||
|
if (isReleased) return 0.0
|
||||||
|
return MPVLib.getPropertyDouble("sub-delay") ?: 0.0
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
if (isReleased) return
|
||||||
|
MPVLib.setPropertyDouble("sub-delay", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MPVLib.setPropertyColor(
|
||||||
|
property: String,
|
||||||
|
color: Color,
|
||||||
|
) = MPVLib.setPropertyString(property, color.mpvFormat)
|
||||||
|
|
||||||
|
private val Color.mpvFormat: String get() = "$red/$green/$blue/$alpha"
|
||||||
|
|
@ -22,7 +22,7 @@ private val downmixSupportedAudioCodecs =
|
||||||
Codec.Audio.MP3,
|
Codec.Audio.MP3,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val supportedAudioCodecs =
|
val supportedAudioCodecs =
|
||||||
arrayOf(
|
arrayOf(
|
||||||
Codec.Audio.AAC,
|
Codec.Audio.AAC,
|
||||||
Codec.Audio.AAC_LATM,
|
Codec.Audio.AAC_LATM,
|
||||||
|
|
@ -495,7 +495,7 @@ fun createDeviceProfile(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Little helper function to more easily define subtitle profiles
|
// Little helper function to more easily define subtitle profiles
|
||||||
private fun DeviceProfileBuilder.subtitleProfile(
|
fun DeviceProfileBuilder.subtitleProfile(
|
||||||
format: String,
|
format: String,
|
||||||
embedded: Boolean = false,
|
embedded: Boolean = false,
|
||||||
external: Boolean = false,
|
external: Boolean = false,
|
||||||
|
|
|
||||||
82
app/src/main/jni/Android.mk
Normal file
82
app/src/main/jni/Android.mk
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
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)
|
||||||
17
app/src/main/jni/Application.mk
Normal file
17
app/src/main/jni/Application.mk
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
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
|
||||||
16
app/src/main/jni/README.md
Normal file
16
app/src/main/jni/README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# 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.
|
||||||
120
app/src/main/jni/event.cpp
Normal file
120
app/src/main/jni/event.cpp
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
#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);
|
||||||
|
default:
|
||||||
|
ALOGV("event: %s\n", mpv_event_name(mp_event->event_id));
|
||||||
|
sendEventToJava(env, mp_event->event_id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_vm->DetachCurrentThread();
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
3
app/src/main/jni/event.h
Normal file
3
app/src/main/jni/event.h
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
void *event_thread(void *arg);
|
||||||
7
app/src/main/jni/globals.h
Normal file
7
app/src/main/jni/globals.h
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
extern JavaVM *g_vm;
|
||||||
|
extern mpv_handle *g_mpv;
|
||||||
|
extern std::atomic<bool> g_event_thread_request_exit;
|
||||||
52
app/src/main/jni/jni_utils.cpp
Normal file
52
app/src/main/jni/jni_utils.cpp
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
#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;
|
||||||
|
}
|
||||||
30
app/src/main/jni/jni_utils.h
Normal file
30
app/src/main/jni/jni_utils.h
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
#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;
|
||||||
9
app/src/main/jni/log.cpp
Normal file
9
app/src/main/jni/log.cpp
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
void die(const char *msg)
|
||||||
|
{
|
||||||
|
ALOGE("%s", msg);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
20
app/src/main/jni/log.h
Normal file
20
app/src/main/jni/log.h
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <android/log.h>
|
||||||
|
|
||||||
|
#define DEBUG 1
|
||||||
|
|
||||||
|
#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)
|
||||||
108
app/src/main/jni/main.cpp
Normal file
108
app/src/main/jni/main.cpp
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
#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]);
|
||||||
|
}
|
||||||
125
app/src/main/jni/property.cpp
Normal file
125
app/src/main/jni/property.cpp
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
#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);
|
||||||
|
}
|
||||||
38
app/src/main/jni/render.cpp
Normal file
38
app/src/main/jni/render.cpp
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#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;
|
||||||
|
}
|
||||||
133
app/src/main/jni/thumbnail.cpp
Normal file
133
app/src/main/jni/thumbnail.cpp
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,15 @@ enum MediaExtensionStatus{
|
||||||
MES_DISABLED = 2;
|
MES_DISABLED = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PlayerBackend{
|
||||||
|
EXO_PLAYER = 0;
|
||||||
|
MPV = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MpvOptions{
|
||||||
|
bool enable_hardware_decoding = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message PlaybackOverrides{
|
message PlaybackOverrides{
|
||||||
bool ac3_supported = 1;
|
bool ac3_supported = 1;
|
||||||
bool downmix_stereo = 2;
|
bool downmix_stereo = 2;
|
||||||
|
|
@ -59,6 +68,8 @@ message PlaybackPreferences {
|
||||||
PrefContentScale global_content_scale = 17;
|
PrefContentScale global_content_scale = 17;
|
||||||
ShowNextUpWhen show_next_up_when = 18;
|
ShowNextUpWhen show_next_up_when = 18;
|
||||||
bool one_click_pause = 19;
|
bool one_click_pause = 19;
|
||||||
|
PlayerBackend player_backend = 20;
|
||||||
|
MpvOptions mpv_options = 21;
|
||||||
}
|
}
|
||||||
|
|
||||||
message HomePagePreferences{
|
message HomePagePreferences{
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,12 @@
|
||||||
<string name="reset">Reset</string>
|
<string name="reset">Reset</string>
|
||||||
<string name="bold_font">Bold font</string>
|
<string name="bold_font">Bold font</string>
|
||||||
|
|
||||||
|
<string name="player_backend">Playback Backend</string>
|
||||||
|
<string name="mpv_hardware_decoding">MPV: Use hardware decoding</string>
|
||||||
|
<string name="disable_if_crash">Disable if you experience crashes</string>
|
||||||
|
<string name="mpv_options">MPV Options</string>
|
||||||
|
<string name="exoplayer_options">ExoPlayer Options</string>
|
||||||
|
|
||||||
<string-array name="theme_song_volume">
|
<string-array name="theme_song_volume">
|
||||||
<item>Disabled</item>
|
<item>Disabled</item>
|
||||||
<item>Lowest</item>
|
<item>Lowest</item>
|
||||||
|
|
@ -126,4 +132,8 @@
|
||||||
<item>Boxed</item>
|
<item>Boxed</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="player_backend_options">
|
||||||
|
<item>ExoPlayer (default)</item>
|
||||||
|
<item>MPV (Experimental)</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
3
scripts/ffmpeg/README.md
Normal file
3
scripts/ffmpeg/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# FFmpeg ExoPlayer decoder extension module
|
||||||
|
|
||||||
|
Builds the ffmpeg decoder extension module for `ExoPlayer` which supports extra codecs during playback.
|
||||||
|
|
@ -7,6 +7,10 @@ if [ -z "$1" ]; then
|
||||||
fi
|
fi
|
||||||
NDK_PATH="$1"
|
NDK_PATH="$1"
|
||||||
|
|
||||||
|
SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"
|
||||||
|
SCRIPT_DIR="$(dirname "${SCRIPT_PATH}")"
|
||||||
|
PROJECT_ROOT="$(realpath "${SCRIPT_DIR}/../../")"
|
||||||
|
|
||||||
# Config
|
# Config
|
||||||
ANDROID_ABI=21
|
ANDROID_ABI=21
|
||||||
ENABLED_DECODERS=(dca ac3 eac3 mlp truehd)
|
ENABLED_DECODERS=(dca ac3 eac3 mlp truehd)
|
||||||
|
|
@ -14,8 +18,8 @@ FFMPEG_BRANCH="release/6.0"
|
||||||
|
|
||||||
# Path configs
|
# Path configs
|
||||||
DIR_PATH="$(pwd)"
|
DIR_PATH="$(pwd)"
|
||||||
TARGET_PATH="$DIR_PATH/app/libs"
|
TARGET_PATH="$PROJECT_ROOT/app/libs"
|
||||||
MEDIA_PATH="$DIR_PATH//ffmpeg_decoder/media"
|
MEDIA_PATH="$DIR_PATH/ffmpeg_decoder/media"
|
||||||
FFMPEG_MODULE_PATH="$MEDIA_PATH/libraries/decoder_ffmpeg/src/main"
|
FFMPEG_MODULE_PATH="$MEDIA_PATH/libraries/decoder_ffmpeg/src/main"
|
||||||
FFMPEG_PATH="$DIR_PATH/ffmpeg_decoder/ffmpeg"
|
FFMPEG_PATH="$DIR_PATH/ffmpeg_decoder/ffmpeg"
|
||||||
HOST="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
HOST="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||||
|
|
@ -24,7 +28,9 @@ HOST_PLATFORM="$HOST-x86_64"
|
||||||
mkdir -p "$TARGET_PATH"
|
mkdir -p "$TARGET_PATH"
|
||||||
mkdir -p ffmpeg_decoder
|
mkdir -p ffmpeg_decoder
|
||||||
|
|
||||||
media_version="$(grep "androidx-media3 = " gradle/libs.versions.toml | awk -F'"' '{print $2}')"
|
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
|
pushd ffmpeg_decoder || exit
|
||||||
|
|
||||||
7
scripts/mpv/README.md
Normal file
7
scripts/mpv/README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# MPV build scripts
|
||||||
|
|
||||||
|
This scripts are adapted from https://github.com/mpv-android/mpv-android/tree/ae0d956c5a98ab8bf25af7e2c73bcb59e19c15b7/buildscripts licensed MIT.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
TODO
|
||||||
180
scripts/mpv/buildall.sh
Executable file
180
scripts/mpv/buildall.sh
Executable file
|
|
@ -0,0 +1,180 @@
|
||||||
|
#!/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
|
||||||
70
scripts/mpv/get_dependencies.sh
Executable file
70
scripts/mpv/get_dependencies.sh
Executable file
|
|
@ -0,0 +1,70 @@
|
||||||
|
#!/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 checkout --force "$branch"
|
||||||
|
popd || exit
|
||||||
|
else
|
||||||
|
git clone "$repo" --depth 1 --single-branch -b "$branch" "$dir" "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
clone "https://github.com/videolan/dav1d" "1.5.2" 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" "master" libass
|
||||||
|
|
||||||
|
clone "https://github.com/haasn/libplacebo" "master" libplacebo --recurse-submodules
|
||||||
|
|
||||||
|
clone "https://github.com/mpv-player/mpv" "master" 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
|
||||||
91
scripts/mpv/include/ci.sh
Executable file
91
scripts/mpv/include/ci.sh
Executable file
|
|
@ -0,0 +1,91 @@
|
||||||
|
#!/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
|
||||||
43
scripts/mpv/include/depinfo.sh
Executable file
43
scripts/mpv/include/depinfo.sh
Executable file
|
|
@ -0,0 +1,43 @@
|
||||||
|
#!/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"
|
||||||
32
scripts/mpv/include/path.sh
Executable file
32
scripts/mpv/include/path.sh
Executable file
|
|
@ -0,0 +1,32 @@
|
||||||
|
#!/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
|
||||||
22
scripts/mpv/scripts/dav1d.sh
Executable file
22
scripts/mpv/scripts/dav1d.sh
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/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
|
||||||
48
scripts/mpv/scripts/ffmpeg.sh
Executable file
48
scripts/mpv/scripts/ffmpeg.sh
Executable file
|
|
@ -0,0 +1,48 @@
|
||||||
|
#!/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
|
||||||
21
scripts/mpv/scripts/freetype2.sh
Executable file
21
scripts/mpv/scripts/freetype2.sh
Executable file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#!/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
|
||||||
22
scripts/mpv/scripts/fribidi.sh
Executable file
22
scripts/mpv/scripts/fribidi.sh
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/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
|
||||||
22
scripts/mpv/scripts/harfbuzz.sh
Executable file
22
scripts/mpv/scripts/harfbuzz.sh
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/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
|
||||||
25
scripts/mpv/scripts/libass.sh
Executable file
25
scripts/mpv/scripts/libass.sh
Executable file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#!/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
|
||||||
25
scripts/mpv/scripts/libplacebo.sh
Executable file
25
scripts/mpv/scripts/libplacebo.sh
Executable file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#!/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
|
||||||
44
scripts/mpv/scripts/lua.sh
Executable file
44
scripts/mpv/scripts/lua.sh
Executable file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#!/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
|
||||||
22
scripts/mpv/scripts/mbedtls.sh
Executable file
22
scripts/mpv/scripts/mbedtls.sh
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/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
|
||||||
77
scripts/mpv/scripts/mpv-android.sh
Executable file
77
scripts/mpv/scripts/mpv-android.sh
Executable file
|
|
@ -0,0 +1,77 @@
|
||||||
|
#!/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
|
||||||
30
scripts/mpv/scripts/mpv.sh
Executable file
30
scripts/mpv/scripts/mpv.sh
Executable file
|
|
@ -0,0 +1,30 @@
|
||||||
|
#!/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
|
||||||
24
scripts/mpv/scripts/unibreak.sh
Executable file
24
scripts/mpv/scripts/unibreak.sh
Executable file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue