diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml index 39f9057b..b300c04d 100644 --- a/.github/workflows/build-apps.yml +++ b/.github/workflows/build-apps.yml @@ -33,7 +33,7 @@ jobs: swap-storage: false - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -41,12 +41,12 @@ jobs: show-progress: false - name: ๐Ÿž Setup Bun - uses: oven-sh/setup-bun@b7a1c7ccf290d58743029c4f6903da283811b979 # v2.1.0 + uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 with: bun-version: latest - name: ๐Ÿ’พ Cache Bun dependencies - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.bun/install/cache key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }} @@ -60,7 +60,7 @@ jobs: bun run submodule-reload - name: ๐Ÿ’พ Cache Gradle global - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: | ~/.gradle/caches @@ -73,7 +73,7 @@ jobs: run: bun run prebuild - name: ๐Ÿ’พ Cache project Gradle (.gradle) - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: android/.gradle key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }} @@ -116,7 +116,7 @@ jobs: swap-storage: false - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -124,12 +124,12 @@ jobs: show-progress: false - name: ๐Ÿž Setup Bun - uses: oven-sh/setup-bun@b7a1c7ccf290d58743029c4f6903da283811b979 # v2.1.0 + uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 with: bun-version: latest - name: ๐Ÿ’พ Cache Bun dependencies - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.bun/install/cache key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }} @@ -143,7 +143,7 @@ jobs: bun run submodule-reload - name: ๐Ÿ’พ Cache Gradle global - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: | ~/.gradle/caches @@ -156,7 +156,7 @@ jobs: run: bun run prebuild:tv - name: ๐Ÿ’พ Cache project Gradle (.gradle) - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: android/.gradle key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }} @@ -187,7 +187,7 @@ jobs: steps: - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -195,12 +195,12 @@ jobs: show-progress: false - name: ๐Ÿž Setup Bun - uses: oven-sh/setup-bun@b7a1c7ccf290d58743029c4f6903da283811b979 # v2.1.0 + uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 with: bun-version: latest - name: ๐Ÿ’พ Cache Bun dependencies - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.bun/install/cache key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} @@ -251,7 +251,7 @@ jobs: steps: - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -259,12 +259,12 @@ jobs: show-progress: false - name: ๐Ÿž Setup Bun - uses: oven-sh/setup-bun@b7a1c7ccf290d58743029c4f6903da283811b979 # v2.1.0 + uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 with: bun-version: latest - name: ๐Ÿ’พ Cache Bun dependencies - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: ~/.bun/install/cache key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} diff --git a/.github/workflows/check-lockfile.yml b/.github/workflows/check-lockfile.yml index 2173ccfc..ad189f7f 100644 --- a/.github/workflows/check-lockfile.yml +++ b/.github/workflows/check-lockfile.yml @@ -19,7 +19,7 @@ jobs: steps: - name: ๐Ÿ“ฅ Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} show-progress: false @@ -27,12 +27,12 @@ jobs: fetch-depth: 0 - name: ๐Ÿž Setup Bun - uses: oven-sh/setup-bun@b7a1c7ccf290d58743029c4f6903da283811b979 # v2.1.0 + uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 with: bun-version: latest - name: ๐Ÿ’พ Cache Bun dependencies - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 with: path: | ~/.bun/install/cache diff --git a/.github/workflows/ci-codeql.yml b/.github/workflows/ci-codeql.yml index 58d806b5..29330ac7 100644 --- a/.github/workflows/ci-codeql.yml +++ b/.github/workflows/ci-codeql.yml @@ -24,16 +24,16 @@ jobs: steps: - name: ๐Ÿ“ฅ Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: ๐Ÿ Initialize CodeQL - uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 + uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 with: languages: ${{ matrix.language }} queries: +security-extended,security-and-quality - name: ๐Ÿ› ๏ธ Autobuild - uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 + uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 - name: ๐Ÿงช Perform CodeQL Analysis - uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 + uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index 69a3c86d..c73cad70 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -23,12 +23,12 @@ jobs: steps: - name: ๐Ÿ“ฅ Checkout Repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: ๐ŸŒ Sync Translations with Crowdin - uses: crowdin/github-action@60debf382ee245b21794321190ad0501db89d8c1 # v2.13.0 + uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2.14.0 with: upload_sources: true upload_translations: true diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index af981cd1..08e0a884 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -51,7 +51,7 @@ jobs: contents: read steps: - name: Checkout Repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -69,14 +69,14 @@ jobs: runs-on: ubuntu-24.04 steps: - name: ๐Ÿ›’ Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} submodules: recursive fetch-depth: 0 - name: ๐Ÿž Setup Bun - uses: oven-sh/setup-bun@b7a1c7ccf290d58743029c4f6903da283811b979 # v2.1.0 + uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 with: bun-version: latest @@ -100,7 +100,7 @@ jobs: steps: - name: "๐Ÿ“ฅ Checkout PR code" - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} submodules: recursive @@ -112,7 +112,7 @@ jobs: node-version: '24.x' - name: "๐Ÿž Setup Bun" - uses: oven-sh/setup-bun@b7a1c7ccf290d58743029c4f6903da283811b979 # v2.1.0 + uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 with: bun-version: latest diff --git a/.github/workflows/update-issue-form.yml b/.github/workflows/update-issue-form.yml index 7a7b0763..25fa3319 100644 --- a/.github/workflows/update-issue-form.yml +++ b/.github/workflows/update-issue-form.yml @@ -18,7 +18,7 @@ jobs: steps: - name: ๐Ÿ“ฅ Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: "๐ŸŸข Setup Node.js" uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 @@ -54,7 +54,7 @@ jobs: dry_run: no-push - name: ๐Ÿ“ฌ Commit and create pull request - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: add-paths: .github/ISSUE_TEMPLATE/bug_report.yml branch: ci-update-bug-report diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index efa854e0..b5dcac73 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -449,7 +449,7 @@ export default function page() { async (data: { nativeEvent: MpvOnProgressEventPayload }) => { if (isSeeking.get() || isPlaybackStopped) return; - const { position } = data.nativeEvent; + const { position, cacheSeconds } = data.nativeEvent; // MPV reports position in seconds, convert to ms const currentTime = position * 1000; @@ -459,6 +459,12 @@ export default function page() { progress.set(currentTime); + // Update cache progress (current position + buffered seconds ahead) + if (cacheSeconds !== undefined && cacheSeconds > 0) { + const cacheEnd = currentTime + cacheSeconds * 1000; + cacheProgress.set(cacheEnd); + } + // Update URL immediately after seeking, or every 30 seconds during normal playback const now = Date.now(); const shouldUpdateUrl = wasJustSeeking.get(); diff --git a/bun.lock b/bun.lock index bdcea301..326bd749 100644 --- a/bun.lock +++ b/bun.lock @@ -19,7 +19,7 @@ "@shopify/flash-list": "2.0.2", "@tanstack/query-sync-storage-persister": "^5.90.18", "@tanstack/react-pacer": "^0.19.1", - "@tanstack/react-query": "5.90.17", + "@tanstack/react-query": "5.90.20", "@tanstack/react-query-persist-client": "^5.90.18", "axios": "^1.7.9", "expo": "~54.0.31", @@ -52,12 +52,12 @@ "expo-web-browser": "~15.0.10", "i18next": "^25.0.0", "jotai": "2.16.2", - "lodash": "4.17.21", + "lodash": "4.17.23", "nativewind": "^2.0.11", "patch-package": "^8.0.0", "react": "19.1.0", "react-dom": "19.1.0", - "react-i18next": "16.5.3", + "react-i18next": "16.5.4", "react-native": "0.81.5", "react-native-awesome-slider": "^2.9.0", "react-native-bottom-tabs": "1.1.0", @@ -97,7 +97,7 @@ "devDependencies": { "@babel/core": "7.28.6", "@biomejs/biome": "2.3.11", - "@react-native-community/cli": "20.1.0", + "@react-native-community/cli": "20.1.1", "@react-native-tvos/config-tv": "0.1.4", "@types/jest": "29.5.14", "@types/lodash": "4.17.23", @@ -512,29 +512,29 @@ "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - "@react-native-community/cli": ["@react-native-community/cli@20.1.0", "", { "dependencies": { "@react-native-community/cli-clean": "20.1.0", "@react-native-community/cli-config": "20.1.0", "@react-native-community/cli-doctor": "20.1.0", "@react-native-community/cli-server-api": "20.1.0", "@react-native-community/cli-tools": "20.1.0", "@react-native-community/cli-types": "20.1.0", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "picocolors": "^1.1.1", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-441WsVtRe4nGJ9OzA+QMU1+22lA6Q2hRWqqIMKD0wjEMLqcSfOZyu2UL9a/yRpL/dRpyUsU4n7AxqKfTKO/Csg=="], + "@react-native-community/cli": ["@react-native-community/cli@20.1.1", "", { "dependencies": { "@react-native-community/cli-clean": "20.1.1", "@react-native-community/cli-config": "20.1.1", "@react-native-community/cli-doctor": "20.1.1", "@react-native-community/cli-server-api": "20.1.1", "@react-native-community/cli-tools": "20.1.1", "@react-native-community/cli-types": "20.1.1", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^5.0.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "picocolors": "^1.1.1", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "rnc-cli": "build/bin.js" } }, "sha512-aLPUx43+WSeTOaUepR2FBD5a1V0OAZ1QB2DOlRlW4fOEjtBXgv40eM/ho8g3WCvAOKfPvTvx4fZdcuovTyV81Q=="], - "@react-native-community/cli-clean": ["@react-native-community/cli-clean@20.1.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.0", "execa": "^5.0.0", "fast-glob": "^3.3.2", "picocolors": "^1.1.1" } }, "sha512-77L4DifWfxAT8ByHnkypge7GBMYpbJAjBGV+toowt5FQSGaTBDcBHCX+FFqFRukD5fH6i8sZ41Gtw+nbfCTTIA=="], + "@react-native-community/cli-clean": ["@react-native-community/cli-clean@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "execa": "^5.0.0", "fast-glob": "^3.3.2", "picocolors": "^1.1.1" } }, "sha512-6nGQ08w2+EcDwTFC4JFiW/wI2pLwzMrk9thz4um7tKRNW8sADX0IyCsfM2F4rHS720C0UNKYBZE9nAsfp8Vkcw=="], - "@react-native-community/cli-config": ["@react-native-community/cli-config@20.1.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.0", "cosmiconfig": "^9.0.0", "deepmerge": "^4.3.0", "fast-glob": "^3.3.2", "joi": "^17.2.1", "picocolors": "^1.1.1" } }, "sha512-1x9rhLLR/dKKb92Lb5O0l0EmUG08FHf+ZVyVEf9M+tX+p5QIm52MRiy43R0UAZ2jJnFApxRk+N3sxoYK4Dtnag=="], + "@react-native-community/cli-config": ["@react-native-community/cli-config@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "cosmiconfig": "^9.0.0", "deepmerge": "^4.3.0", "fast-glob": "^3.3.2", "joi": "^17.2.1", "picocolors": "^1.1.1" } }, "sha512-ajs2i56MANie/v0bMQ1BmRcrOb6MEvLT2rh/I1CA62NXGqF1Rxv6QwsN84LrADMXHRg8QiEMAIADkyDeQHt7Kg=="], - "@react-native-community/cli-config-android": ["@react-native-community/cli-config-android@20.1.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.0", "fast-glob": "^3.3.2", "fast-xml-parser": "^4.4.1", "picocolors": "^1.1.1" } }, "sha512-3A01ZDyFeCALzzPcwP/fleHoP3sGNq1UX7FzxkTrOFX8RRL9ntXNXQd27E56VU4BBxGAjAJT4Utw8pcOjJceIA=="], + "@react-native-community/cli-config-android": ["@react-native-community/cli-config-android@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "fast-glob": "^3.3.2", "fast-xml-parser": "^4.4.1", "picocolors": "^1.1.1" } }, "sha512-1iUV2rPAyoWPo8EceAFC2vZTF+pEd9YqS87c0aqpbGOFE0gs1rHEB+auVR8CdjzftR4U9sq6m2jrdst0rvpIkg=="], - "@react-native-community/cli-config-apple": ["@react-native-community/cli-config-apple@20.1.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.0", "execa": "^5.0.0", "fast-glob": "^3.3.2", "picocolors": "^1.1.1" } }, "sha512-n6JVs8Q3yxRbtZQOy05ofeb1kGtspGN3SgwPmuaqvURF9fsuS7c4/9up2Kp9C+1D2J1remPJXiZLNGOcJvfpOA=="], + "@react-native-community/cli-config-apple": ["@react-native-community/cli-config-apple@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "execa": "^5.0.0", "fast-glob": "^3.3.2", "picocolors": "^1.1.1" } }, "sha512-doepJgLJVqeJb5tNoP9hyFIcoZ1OMGO7QN/YMuCCIjbThUQe/J87XdwPol3Qrjr58KRt9xeBVz+kHeW5mtSutw=="], - "@react-native-community/cli-doctor": ["@react-native-community/cli-doctor@20.1.0", "", { "dependencies": { "@react-native-community/cli-config": "20.1.0", "@react-native-community/cli-platform-android": "20.1.0", "@react-native-community/cli-platform-apple": "20.1.0", "@react-native-community/cli-platform-ios": "20.1.0", "@react-native-community/cli-tools": "20.1.0", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", "envinfo": "^7.13.0", "execa": "^5.0.0", "node-stream-zip": "^1.9.1", "ora": "^5.4.1", "picocolors": "^1.1.1", "semver": "^7.5.2", "wcwidth": "^1.0.1", "yaml": "^2.2.1" } }, "sha512-QfJF1GVjA4PBrIT3SJ0vFFIu0km1vwOmLDlOYVqfojajZJ+Dnvl0f94GN1il/jT7fITAxom///XH3/URvi7YTQ=="], + "@react-native-community/cli-doctor": ["@react-native-community/cli-doctor@20.1.1", "", { "dependencies": { "@react-native-community/cli-config": "20.1.1", "@react-native-community/cli-platform-android": "20.1.1", "@react-native-community/cli-platform-apple": "20.1.1", "@react-native-community/cli-platform-ios": "20.1.1", "@react-native-community/cli-tools": "20.1.1", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", "envinfo": "^7.13.0", "execa": "^5.0.0", "node-stream-zip": "^1.9.1", "ora": "^5.4.1", "picocolors": "^1.1.1", "semver": "^7.5.2", "wcwidth": "^1.0.1", "yaml": "^2.2.1" } }, "sha512-eFpg5wWnV7uGqvLemshpgj2trPD8cckqxBuI4nT7sxKF/YpA/e3nnnyytHxPP5EnYfWbMcqfaq8hDJoOnJinGQ=="], - "@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@20.1.0", "", { "dependencies": { "@react-native-community/cli-config-android": "20.1.0", "@react-native-community/cli-tools": "20.1.0", "execa": "^5.0.0", "logkitty": "^0.7.1", "picocolors": "^1.1.1" } }, "sha512-TeHPDThOwDppQRpndm9kCdRCBI8AMy3HSIQ+iy7VYQXL5BtZ5LfmGdusoj7nVN/ZGn0Lc6Gwts5qowyupXdeKg=="], + "@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@20.1.1", "", { "dependencies": { "@react-native-community/cli-config-android": "20.1.1", "@react-native-community/cli-tools": "20.1.1", "execa": "^5.0.0", "logkitty": "^0.7.1", "picocolors": "^1.1.1" } }, "sha512-KPheizJQI0tVvBLy9owzpo+A9qDsDAa87e7a8xNaHnwqGpExnIzFPrbdvrltiZjstU2eB/+/UgNQxYIEd4Oc+g=="], - "@react-native-community/cli-platform-apple": ["@react-native-community/cli-platform-apple@20.1.0", "", { "dependencies": { "@react-native-community/cli-config-apple": "20.1.0", "@react-native-community/cli-tools": "20.1.0", "execa": "^5.0.0", "fast-xml-parser": "^4.4.1", "picocolors": "^1.1.1" } }, "sha512-0ih1hrYezSM2cuOlVnwBEFtMwtd8YgpTLmZauDJCv50rIumtkI1cQoOgLoS4tbPCj9U/Vn2a9BFH0DLFOOIacg=="], + "@react-native-community/cli-platform-apple": ["@react-native-community/cli-platform-apple@20.1.1", "", { "dependencies": { "@react-native-community/cli-config-apple": "20.1.1", "@react-native-community/cli-tools": "20.1.1", "execa": "^5.0.0", "fast-xml-parser": "^4.4.1", "picocolors": "^1.1.1" } }, "sha512-mQEjOzRFCcQTrCt73Q/+5WWTfUg6U2vLZv5rPuFiNrLbrwRqxVH3OLaXg5gilJkDTJC80z8iOSsdd8MRxONOig=="], - "@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@20.1.0", "", { "dependencies": { "@react-native-community/cli-platform-apple": "20.1.0" } }, "sha512-XN7Da9z4WsJxtqVtEzY8q2bv22OsvzaFP5zy5+phMWNoJlU4lf7IvBSxqGYMpQ9XhYP7arDw5vmW4W34s06rnA=="], + "@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@20.1.1", "", { "dependencies": { "@react-native-community/cli-platform-apple": "20.1.1" } }, "sha512-6vr10/oSjKkZO/BBgfFJNQTC/0CDF4WrN8iW9ss+Kt6ZL2QrBXLYz7fobrrboOlHwqqs5EyQadlEaNii7gKRJg=="], - "@react-native-community/cli-server-api": ["@react-native-community/cli-server-api@20.1.0", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.0", "body-parser": "^1.20.3", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", "nocache": "^3.0.1", "open": "^6.2.0", "pretty-format": "^29.7.0", "serve-static": "^1.13.1", "ws": "^6.2.3" } }, "sha512-Tb415Oh8syXNT2zOzLzFkBXznzGaqKCiaichxKzGCDKg6JGHp3jSuCmcTcaPeYC7oc32n/S3Psw7798r4Q/7lA=="], + "@react-native-community/cli-server-api": ["@react-native-community/cli-server-api@20.1.1", "", { "dependencies": { "@react-native-community/cli-tools": "20.1.1", "body-parser": "^1.20.3", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", "nocache": "^3.0.1", "open": "^6.2.0", "pretty-format": "^29.7.0", "serve-static": "^1.13.1", "strict-url-sanitise": "0.0.1", "ws": "^6.2.3" } }, "sha512-phHfiCa4WqfKfaoV2vGVR3ZrYQDQTpI1k+C+i6rXAxFGxPuy8IgFFVOSL543qjKPpHBVwLcA+/xAJCVpdyCtVQ=="], - "@react-native-community/cli-tools": ["@react-native-community/cli-tools@20.1.0", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "picocolors": "^1.1.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-/YmzHGOkY6Bgrv4OaA1L8rFqsBlQd1EB2/ipAoKPiieV0EcB5PUamUSuNeFU3sBZZTYQCUENwX4wgOHgFUlDnQ=="], + "@react-native-community/cli-tools": ["@react-native-community/cli-tools@20.1.1", "", { "dependencies": { "@vscode/sudo-prompt": "^9.0.0", "appdirsjs": "^1.2.4", "execa": "^5.0.0", "find-up": "^5.0.0", "launch-editor": "^2.9.1", "mime": "^2.4.1", "ora": "^5.4.1", "picocolors": "^1.1.1", "prompts": "^2.4.2", "semver": "^7.5.2" } }, "sha512-j+zX/H2X+6ZGneIDj56tZ1Hbnip5nSfnq7yGlMyF/zm3U1hKp3G1jN5v0YEfnz/zEmjr7zruh4Y06KmZrF1lrA=="], - "@react-native-community/cli-types": ["@react-native-community/cli-types@20.1.0", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-D0kDspcwgbVXyNjwicT7Bb1JgXjijTw1JJd+qxyF/a9+sHv7TU4IchV+gN38QegeXqVyM4Ym7YZIvXMFBmyJqA=="], + "@react-native-community/cli-types": ["@react-native-community/cli-types@20.1.1", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-Tp+s27I/RDONrGvWVj4IzEmga2HhJhXi8ZlZTfycMMyAcv4LG/CTPira+BUZs8nzLAJNrlJ79pVVPJPqQAe+aw=="], "@react-native-community/netinfo": ["@react-native-community/netinfo@11.4.1", "", { "peerDependencies": { "react-native": ">=0.59" } }, "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg=="], @@ -602,7 +602,7 @@ "@tanstack/react-pacer": ["@tanstack/react-pacer@0.19.1", "", { "dependencies": { "@tanstack/pacer": "0.17.1", "@tanstack/react-store": "^0.8.0" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-wfGwKLo2gosKr5tsXico+jWJ8LsWsBC8MA1HVtUY/D6dhFduEVizKxRUcvP60I3dRvnoXDbN202g4feJHlivnA=="], - "@tanstack/react-query": ["@tanstack/react-query@5.90.17", "", { "dependencies": { "@tanstack/query-core": "5.90.17" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-PGc2u9KLwohDUSchjW9MZqeDQJfJDON7y4W7REdNBgiFKxQy+Pf7eGjiFWEj5xPqKzAeHYdAb62IWI1a9UJyGQ=="], + "@tanstack/react-query": ["@tanstack/react-query@5.90.20", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw=="], "@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.90.18", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.91.15" }, "peerDependencies": { "@tanstack/react-query": "^5.90.16", "react": "^18 || ^19" } }, "sha512-ToVRTVpjzTrd9S/p7JIvGdLs+Xtz9aDMM/7+TQGSV9notY8Jt64irfAAAkZ05syftLKS+3KPgyKAnHcVeKVbWQ=="], @@ -1376,7 +1376,7 @@ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], @@ -1640,7 +1640,7 @@ "react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="], - "react-i18next": ["react-i18next@16.5.3", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-fo+/NNch37zqxOzlBYrWMx0uy/yInPkRfjSuy4lqKdaecR17nvCHnEUt3QyzA8XjQ2B/0iW/5BhaHR3ZmukpGw=="], + "react-i18next": ["react-i18next@16.5.4", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g=="], "react-is": ["react-is@19.2.3", "", {}, "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA=="], @@ -1854,6 +1854,8 @@ "strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="], + "strict-url-sanitise": ["strict-url-sanitise@0.0.1", "", {}, "sha512-nuFtF539K8jZg3FjaWH/L8eocCR6gegz5RDOsaWxfdbF5Jqr2VXWxZayjTwUzsWJDC91k2EbnJXp6FuWW+Z4hg=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -2242,7 +2244,7 @@ "@react-navigation/native-stack/color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], - "@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.90.17", "", {}, "sha512-hDww+RyyYhjhUfoYQ4es6pbgxY7LNiPWxt4l1nJqhByjndxJ7HIjDxTBtfvMr5HwjYavMrd+ids5g4Rfev3lVQ=="], + "@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], "@types/babel__core/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], diff --git a/modules/mpv-player/android/src/main/assets/subfont.ttf b/modules/mpv-player/android/src/main/assets/subfont.ttf new file mode 100644 index 00000000..23daaa4e Binary files /dev/null and b/modules/mpv-player/android/src/main/assets/subfont.ttf differ diff --git a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt index 7a6a92f8..38c55625 100644 --- a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt +++ b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt @@ -1,10 +1,13 @@ package expo.modules.mpvplayer import android.content.Context +import android.content.res.AssetManager import android.os.Handler import android.os.Looper import android.util.Log import android.view.Surface +import java.io.File +import java.io.FileOutputStream /** * MPV renderer that wraps libmpv for video playback. @@ -26,7 +29,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { } interface Delegate { - fun onPositionChanged(position: Double, duration: Double) + fun onPositionChanged(position: Double, duration: Double, cacheSeconds: Double) fun onPauseChanged(isPaused: Boolean) fun onLoadingChanged(isLoading: Boolean) fun onReadyToSeek() @@ -46,6 +49,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { // Cached state private var cachedPosition: Double = 0.0 private var cachedDuration: Double = 0.0 + private var cachedCacheSeconds: Double = 0.0 private var _isPaused: Boolean = true private var _isLoading: Boolean = false private var _playbackSpeed: Double = 1.0 @@ -101,6 +105,52 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { MPVLib.create(context) MPVLib.addObserver(this) + /** + * Create mpv config directory and copy font files to ensure SubRip subtitles load properly on Android. + * + * Technical Background: + * ==================== + * On Android, mpv requires access to a font file to render text-based subtitles, particularly SubRip (.srt) + * format subtitles. Without an available font in the config directory, mpv will fail to display subtitles + * even when subtitle tracks are properly detected and loaded. + * + * Why This Is Necessary: + * ===================== + * 1. Android's font system is isolated from native libraries like mpv. While Android has system fonts, + * mpv cannot access them directly due to sandboxing and library isolation. + * + * 2. SubRip subtitles require a font to render text overlay on video. When no font is available in the + * configured directory, mpv either: + * - Fails silently (subtitles don't appear) + * - Falls back to a default font that may not support the required character set + * - Crashes or produces rendering errors + * + * 3. By placing a font file (font.ttf) in mpv's config directory and setting that directory via + * MPVLib.setOptionString("config-dir", ...), we ensure mpv has a known, accessible font source. + * + * Reference: + * ========= + * This workaround is documented in the mpv-android project: + * https://github.com/mpv-android/mpv-android/issues/96 + * + * The issue discusses that without a font in the config directory, SubRip subtitles fail to load + * properly on Android, and the solution is to copy a font file to a known location that mpv can access. + */ + // Create mpv config directory and copy font files + val mpvDir = File(context.getExternalFilesDir(null) ?: context.filesDir, "mpv") + //Log.i(TAG, "mpv config dir: $mpvDir") + if (!mpvDir.exists()) mpvDir.mkdirs() + // This needs to be named `subfont.ttf` else it won't work + arrayOf("subfont.ttf").forEach { fileName -> + val file = File(mpvDir, fileName) + if (file.exists()) return@forEach + context.assets + .open(fileName, AssetManager.ACCESS_STREAMING) + .copyTo(FileOutputStream(file)) + } + MPVLib.setOptionString("config", "yes") + MPVLib.setOptionString("config-dir", mpvDir.path) + // Configure mpv options before initialization (based on Findroid) MPVLib.setOptionString("vo", "gpu") MPVLib.setOptionString("gpu-context", "android") @@ -124,7 +174,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { MPVLib.setOptionString("hr-seek-framedrop", "yes") // Subtitle settings - MPVLib.setOptionString("sub-scale-with-window", "yes") + MPVLib.setOptionString("sub-scale-with-window", "no") MPVLib.setOptionString("sub-use-margins", "no") MPVLib.setOptionString("subs-match-os-language", "yes") MPVLib.setOptionString("subs-fallback", "yes") @@ -283,6 +333,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { MPVLib.observeProperty("pause", MPV_FORMAT_FLAG) MPVLib.observeProperty("track-list/count", MPV_FORMAT_INT64) MPVLib.observeProperty("paused-for-cache", MPV_FORMAT_FLAG) + MPVLib.observeProperty("demuxer-cache-duration", MPV_FORMAT_DOUBLE) // Video dimensions for PiP aspect ratio MPVLib.observeProperty("video-params/w", MPV_FORMAT_INT64) MPVLib.observeProperty("video-params/h", MPV_FORMAT_INT64) @@ -561,7 +612,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { when (property) { "duration" -> { cachedDuration = value - mainHandler.post { delegate?.onPositionChanged(cachedPosition, cachedDuration) } + mainHandler.post { delegate?.onPositionChanged(cachedPosition, cachedDuration, cachedCacheSeconds) } } "time-pos" -> { cachedPosition = value @@ -570,9 +621,12 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { val shouldUpdate = _isSeeking || (now - lastProgressUpdateTime >= 1000) if (shouldUpdate) { lastProgressUpdateTime = now - mainHandler.post { delegate?.onPositionChanged(cachedPosition, cachedDuration) } + mainHandler.post { delegate?.onPositionChanged(cachedPosition, cachedDuration, cachedCacheSeconds) } } } + "demuxer-cache-duration" -> { + cachedCacheSeconds = value + } } } diff --git a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MpvPlayerView.kt b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MpvPlayerView.kt index ac0b1276..5b8e2dd3 100644 --- a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MpvPlayerView.kt +++ b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MpvPlayerView.kt @@ -307,7 +307,7 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context // MARK: - MPVLayerRenderer.Delegate - override fun onPositionChanged(position: Double, duration: Double) { + override fun onPositionChanged(position: Double, duration: Double, cacheSeconds: Double) { cachedPosition = position cachedDuration = duration @@ -319,7 +319,8 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context onProgress(mapOf( "position" to position, "duration" to duration, - "progress" to if (duration > 0) position / duration else 0.0 + "progress" to if (duration > 0) position / duration else 0.0, + "cacheSeconds" to cacheSeconds )) } diff --git a/modules/mpv-player/ios/MPVLayerRenderer.swift b/modules/mpv-player/ios/MPVLayerRenderer.swift index af346763..e2c4573a 100644 --- a/modules/mpv-player/ios/MPVLayerRenderer.swift +++ b/modules/mpv-player/ios/MPVLayerRenderer.swift @@ -5,7 +5,7 @@ import CoreVideo import AVFoundation protocol MPVLayerRendererDelegate: AnyObject { - func renderer(_ renderer: MPVLayerRenderer, didUpdatePosition position: Double, duration: Double) + func renderer(_ renderer: MPVLayerRenderer, didUpdatePosition position: Double, duration: Double, cacheSeconds: Double) func renderer(_ renderer: MPVLayerRenderer, didChangePause isPaused: Bool) func renderer(_ renderer: MPVLayerRenderer, didChangeLoading isLoading: Bool) func renderer(_ renderer: MPVLayerRenderer, didBecomeReadyToSeek: Bool) @@ -44,6 +44,7 @@ final class MPVLayerRenderer { // Thread-safe state for playback private var _cachedDuration: Double = 0 private var _cachedPosition: Double = 0 + private var _cachedCacheSeconds: Double = 0 private var _isPaused: Bool = true private var _playbackSpeed: Double = 1.0 private var _isLoading: Bool = false @@ -75,6 +76,10 @@ final class MPVLayerRenderer { get { stateQueue.sync { _cachedPosition } } set { stateQueue.async(flags: .barrier) { self._cachedPosition = newValue } } } + private var cachedCacheSeconds: Double { + get { stateQueue.sync { _cachedCacheSeconds } } + set { stateQueue.async(flags: .barrier) { self._cachedCacheSeconds = newValue } } + } private var isPaused: Bool { get { stateQueue.sync { _isPaused } } set { stateQueue.async(flags: .barrier) { self._isPaused = newValue } } @@ -164,6 +169,7 @@ final class MPVLayerRenderer { // Enable composite OSD mode - renders subtitles directly onto video frames using GPU // This is better for PiP as subtitles are baked into the video + // NOTE: Must be set BEFORE the #if targetEnvironment check or tvOS will freeze on player exit checkError(mpv_set_option_string(handle, "avfoundation-composite-osd", "yes")) // Hardware decoding with VideoToolbox @@ -340,7 +346,8 @@ final class MPVLayerRenderer { ("time-pos", MPV_FORMAT_DOUBLE), ("pause", MPV_FORMAT_FLAG), ("track-list/count", MPV_FORMAT_INT64), - ("paused-for-cache", MPV_FORMAT_FLAG) + ("paused-for-cache", MPV_FORMAT_FLAG), + ("demuxer-cache-duration", MPV_FORMAT_DOUBLE) ] for (name, format) in properties { mpv_observe_property(handle, 0, name, format) @@ -484,7 +491,7 @@ final class MPVLayerRenderer { cachedDuration = value DispatchQueue.main.async { [weak self] in guard let self else { return } - self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration) + self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration, cacheSeconds: self.cachedCacheSeconds) } } case "time-pos": @@ -499,10 +506,16 @@ final class MPVLayerRenderer { lastProgressUpdateTime = now DispatchQueue.main.async { [weak self] in guard let self else { return } - self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration) + self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration, cacheSeconds: self.cachedCacheSeconds) } } } + case "demuxer-cache-duration": + var value = Double(0) + let status = getProperty(handle: handle, name: name, format: MPV_FORMAT_DOUBLE, value: &value) + if status >= 0 { + cachedCacheSeconds = value + } case "pause": var flag: Int32 = 0 let status = getProperty(handle: handle, name: name, format: MPV_FORMAT_FLAG, value: &flag) diff --git a/modules/mpv-player/ios/MpvPlayerView.swift b/modules/mpv-player/ios/MpvPlayerView.swift index 608448b8..89502a9a 100644 --- a/modules/mpv-player/ios/MpvPlayerView.swift +++ b/modules/mpv-player/ios/MpvPlayerView.swift @@ -298,7 +298,7 @@ class MpvPlayerView: ExpoView { // MARK: - MPVLayerRendererDelegate extension MpvPlayerView: MPVLayerRendererDelegate { - func renderer(_: MPVLayerRenderer, didUpdatePosition position: Double, duration: Double) { + func renderer(_: MPVLayerRenderer, didUpdatePosition position: Double, duration: Double, cacheSeconds: Double) { cachedPosition = position cachedDuration = duration @@ -313,6 +313,7 @@ extension MpvPlayerView: MPVLayerRendererDelegate { "position": position, "duration": duration, "progress": duration > 0 ? position / duration : 0, + "cacheSeconds": cacheSeconds, ]) } } diff --git a/modules/mpv-player/src/MpvPlayer.types.ts b/modules/mpv-player/src/MpvPlayer.types.ts index dc25007b..23f86093 100644 --- a/modules/mpv-player/src/MpvPlayer.types.ts +++ b/modules/mpv-player/src/MpvPlayer.types.ts @@ -15,6 +15,8 @@ export type OnProgressEventPayload = { position: number; duration: number; progress: number; + /** Seconds of video buffered ahead of current position */ + cacheSeconds: number; }; export type OnErrorEventPayload = { diff --git a/package.json b/package.json index 2679912c..9864985a 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@shopify/flash-list": "2.0.2", "@tanstack/query-sync-storage-persister": "^5.90.18", "@tanstack/react-pacer": "^0.19.1", - "@tanstack/react-query": "5.90.17", + "@tanstack/react-query": "5.90.20", "@tanstack/react-query-persist-client": "^5.90.18", "axios": "^1.7.9", "expo": "~54.0.31", @@ -72,12 +72,12 @@ "expo-web-browser": "~15.0.10", "i18next": "^25.0.0", "jotai": "2.16.2", - "lodash": "4.17.21", + "lodash": "4.17.23", "nativewind": "^2.0.11", "patch-package": "^8.0.0", "react": "19.1.0", "react-dom": "19.1.0", - "react-i18next": "16.5.3", + "react-i18next": "16.5.4", "react-native": "0.81.5", "react-native-awesome-slider": "^2.9.0", "react-native-bottom-tabs": "1.1.0", @@ -117,7 +117,7 @@ "devDependencies": { "@babel/core": "7.28.6", "@biomejs/biome": "2.3.11", - "@react-native-community/cli": "20.1.0", + "@react-native-community/cli": "20.1.1", "@react-native-tvos/config-tv": "0.1.4", "@types/jest": "29.5.14", "@types/lodash": "4.17.23",