mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-31 11:08:26 +01:00
Compare commits
1 Commits
ci/eas-rel
...
renovate/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f52f704091 |
132
.github/workflows/release.yml
vendored
132
.github/workflows/release.yml
vendored
@@ -1,132 +0,0 @@
|
||||
name: 🚀 Release (EAS Build + Submit)
|
||||
|
||||
# Cloud EAS build + auto-submit for iOS, tvOS and Android on merge to main.
|
||||
# A manual approval gate (the `production` GitHub Environment) pauses the run
|
||||
# before any build/submit starts. Configure required reviewers on that
|
||||
# environment in repo Settings → Environments → production.
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
approve:
|
||||
name: 🔐 Approve release
|
||||
runs-on: ubuntu-24.04
|
||||
environment: production
|
||||
steps:
|
||||
- name: ✅ Release approved
|
||||
run: echo "Release approved for ${{ github.sha }}"
|
||||
|
||||
release:
|
||||
name: 🚀 ${{ matrix.name }}
|
||||
needs: approve
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: 🍎 iOS
|
||||
platform: ios
|
||||
profile: production
|
||||
- name: 📺 tvOS
|
||||
platform: ios
|
||||
profile: production_tv
|
||||
- name: 🤖 Android
|
||||
platform: android
|
||||
profile: production
|
||||
|
||||
steps:
|
||||
- name: 📥 Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
show-progress: false
|
||||
|
||||
- name: 🍞 Setup Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: 💾 Cache Bun dependencies
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-cache
|
||||
|
||||
- name: 📦 Install dependencies and reload submodules
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
bun run submodule-reload
|
||||
|
||||
- name: 🏗️ Setup EAS
|
||||
uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main
|
||||
with:
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
eas-cache: true
|
||||
|
||||
# tvOS uses local credentials (EAS can't manage tvOS provisioning
|
||||
# remotely, including the TopShelf extension target). Restore the
|
||||
# gitignored credentials.json + cert + profiles from secrets so the
|
||||
# cloud build can sign with `credentialsSource: local`.
|
||||
- name: 🔐 Restore tvOS signing credentials
|
||||
if: matrix.profile == 'production_tv'
|
||||
env:
|
||||
EAS_CREDENTIALS_JSON: ${{ secrets.EAS_CREDENTIALS_JSON }}
|
||||
TVOS_DIST_CERT_P12_BASE64: ${{ secrets.TVOS_DIST_CERT_P12_BASE64 }}
|
||||
TVOS_APP_PROFILE_BASE64: ${{ secrets.TVOS_APP_PROFILE_BASE64 }}
|
||||
TVOS_TOPSHELF_PROFILE_BASE64: ${{ secrets.TVOS_TOPSHELF_PROFILE_BASE64 }}
|
||||
run: |
|
||||
mkdir -p certs profiles
|
||||
printf '%s' "$EAS_CREDENTIALS_JSON" > credentials.json
|
||||
echo "$TVOS_DIST_CERT_P12_BASE64" | base64 -d > certs/distribution.p12
|
||||
echo "$TVOS_APP_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_tvOS_App_Store.mobileprovision
|
||||
echo "$TVOS_TOPSHELF_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_TopShelf_tvOS_App_Store.mobileprovision
|
||||
|
||||
# iOS + tvOS submit upload to App Store Connect with an ASC API key.
|
||||
# EAS reads it from EXPO_ASC_API_KEY_PATH / EXPO_ASC_KEY_ID /
|
||||
# EXPO_ASC_ISSUER_ID (set on the build step below). Write the .p8,
|
||||
# tolerating either raw-PEM or base64-encoded secret content.
|
||||
- name: 🔐 Restore App Store Connect API key
|
||||
if: matrix.platform == 'ios'
|
||||
env:
|
||||
APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
|
||||
run: |
|
||||
if printf '%s' "$APPLE_KEY_CONTENT" | grep -q "BEGIN PRIVATE KEY"; then
|
||||
printf '%s' "$APPLE_KEY_CONTENT" > "$RUNNER_TEMP/asc_api_key.p8"
|
||||
else
|
||||
printf '%s' "$APPLE_KEY_CONTENT" | base64 -d > "$RUNNER_TEMP/asc_api_key.p8"
|
||||
fi
|
||||
|
||||
# Android submit needs a Google Play service account JSON. eas.json's
|
||||
# submit.production.android.serviceAccountKeyPath points at this file.
|
||||
- name: 🔐 Restore Google Play service account
|
||||
if: matrix.platform == 'android'
|
||||
env:
|
||||
GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
|
||||
run: printf '%s' "$GOOGLE_SERVICE_ACCOUNT_KEY" > google-service-account.json
|
||||
|
||||
- name: 🚀 Build & submit (${{ matrix.name }})
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
|
||||
# Consumed by eas submit for iOS/tvOS; ignored for Android.
|
||||
EXPO_ASC_API_KEY_PATH: ${{ runner.temp }}/asc_api_key.p8
|
||||
EXPO_ASC_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
|
||||
EXPO_ASC_ISSUER_ID: ${{ secrets.APPLE_KEY_ISSUER_ID }}
|
||||
run: |
|
||||
eas build \
|
||||
--platform ${{ matrix.platform }} \
|
||||
--profile ${{ matrix.profile }} \
|
||||
--auto-submit \
|
||||
--non-interactive
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -76,6 +76,3 @@ build/
|
||||
.claude/
|
||||
.agents/skills/**
|
||||
skills-lock.json
|
||||
|
||||
# CI-injected Google Play service account key (written at build time)
|
||||
google-service-account.json
|
||||
|
||||
7
app.json
7
app.json
@@ -2,7 +2,7 @@
|
||||
"expo": {
|
||||
"name": "Streamyfin",
|
||||
"slug": "streamyfin",
|
||||
"version": "0.54.1",
|
||||
"version": "0.54.0",
|
||||
"orientation": "default",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "streamyfin",
|
||||
@@ -36,6 +36,7 @@
|
||||
"appleTeamId": "MWD5K362T8"
|
||||
},
|
||||
"android": {
|
||||
"versionCode": 93,
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/icon-android-plain.png",
|
||||
"monochromeImage": "./assets/images/icon-android-themed.png",
|
||||
@@ -143,8 +144,8 @@
|
||||
[
|
||||
"./plugins/withGitPod.js",
|
||||
{
|
||||
"podName": "MPVKit",
|
||||
"podspecUrl": "https://raw.githubusercontent.com/mpv-ios/MPVKit/0.41.0-av/MPVKit.podspec"
|
||||
"podName": "MPVKit-GPL",
|
||||
"podspecUrl": "https://raw.githubusercontent.com/streamyfin/MPVKit/0.40.0-av/MPVKit-GPL.podspec"
|
||||
}
|
||||
]
|
||||
],
|
||||
|
||||
@@ -825,10 +825,12 @@ export default function DirectPlayerPage() {
|
||||
],
|
||||
);
|
||||
|
||||
/** PiP handler for MPV */
|
||||
const _onPictureInPictureChange = useCallback(
|
||||
(e: { nativeEvent: { isActive: boolean } }) => {
|
||||
const { isActive } = e.nativeEvent;
|
||||
setIsPipMode(isActive);
|
||||
// Hide controls when entering PiP
|
||||
if (isActive) {
|
||||
_setShowControls(false);
|
||||
}
|
||||
@@ -846,9 +848,6 @@ export default function DirectPlayerPage() {
|
||||
|
||||
// Memoize video ref functions to prevent unnecessary re-renders
|
||||
const startPictureInPicture = useCallback(async () => {
|
||||
// Hide controls BEFORE entering PiP so the window captures a clean view
|
||||
_setShowControls(false);
|
||||
setIsPipMode(true);
|
||||
return videoRef.current?.startPictureInPicture?.();
|
||||
}, []);
|
||||
|
||||
@@ -1254,7 +1253,6 @@ export default function DirectPlayerPage() {
|
||||
nowPlayingMetadata={nowPlayingMetadata}
|
||||
onProgress={onProgress}
|
||||
onPlaybackStateChange={onPlaybackStateChanged}
|
||||
onPictureInPictureChange={_onPictureInPictureChange}
|
||||
onLoad={() => setIsVideoLoaded(true)}
|
||||
onError={(e: { nativeEvent: MpvOnErrorEventPayload }) => {
|
||||
console.error("Video Error:", e.nativeEvent);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
diff --git a/node_modules/react-native-bottom-tabs/ios/BottomAccessoryProvider.swift b/node_modules/react-native-bottom-tabs/ios/BottomAccessoryProvider.swift
|
||||
diff --git a/node_modules/react-native-bottom-tabs/.bun-tag-b32ab1c60a5dfcf7 b/.bun-tag-b32ab1c60a5dfcf7
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
||||
diff --git a/ios/BottomAccessoryProvider.swift b/ios/BottomAccessoryProvider.swift
|
||||
index 539efee7156599e1fc795e11bf411b7dfaf12ec7..b2af39a2e6b014e9b1ae0a51b21115c19280df69 100644
|
||||
--- a/node_modules/react-native-bottom-tabs/ios/BottomAccessoryProvider.swift
|
||||
+++ b/node_modules/react-native-bottom-tabs/ios/BottomAccessoryProvider.swift
|
||||
--- a/ios/BottomAccessoryProvider.swift
|
||||
+++ b/ios/BottomAccessoryProvider.swift
|
||||
@@ -8,7 +8,7 @@ import SwiftUI
|
||||
self.delegate = delegate
|
||||
}
|
||||
@@ -11,10 +14,10 @@ index 539efee7156599e1fc795e11bf411b7dfaf12ec7..b2af39a2e6b014e9b1ae0a51b21115c1
|
||||
@available(iOS 26.0, *)
|
||||
public func emitPlacementChanged(_ placement: TabViewBottomAccessoryPlacement?) {
|
||||
var placementValue = "none"
|
||||
diff --git a/node_modules/react-native-bottom-tabs/ios/TabView/NewTabView.swift b/node_modules/react-native-bottom-tabs/ios/TabView/NewTabView.swift
|
||||
diff --git a/ios/TabView/NewTabView.swift b/ios/TabView/NewTabView.swift
|
||||
index 22c52cdf25ad0f7398d89197cb431ca8dc8e0f99..81411376e68803de8bd83515d42565cfa95daf2b 100644
|
||||
--- a/node_modules/react-native-bottom-tabs/ios/TabView/NewTabView.swift
|
||||
+++ b/node_modules/react-native-bottom-tabs/ios/TabView/NewTabView.swift
|
||||
--- a/ios/TabView/NewTabView.swift
|
||||
+++ b/ios/TabView/NewTabView.swift
|
||||
@@ -78,11 +78,11 @@ struct ConditionalBottomAccessoryModifier: ViewModifier {
|
||||
}
|
||||
|
||||
@@ -53,10 +56,10 @@ index 22c52cdf25ad0f7398d89197cb431ca8dc8e0f99..81411376e68803de8bd83515d42565cf
|
||||
}
|
||||
#endif
|
||||
+
|
||||
diff --git a/node_modules/react-native-bottom-tabs/ios/TabViewImpl.swift b/node_modules/react-native-bottom-tabs/ios/TabViewImpl.swift
|
||||
diff --git a/ios/TabViewImpl.swift b/ios/TabViewImpl.swift
|
||||
index 72938be90540ea3a483d7db9a80fb74c04d31272..277278ffdd9268a96cb09869eb1d0c0d5e6ad300 100644
|
||||
--- a/node_modules/react-native-bottom-tabs/ios/TabViewImpl.swift
|
||||
+++ b/node_modules/react-native-bottom-tabs/ios/TabViewImpl.swift
|
||||
--- a/ios/TabViewImpl.swift
|
||||
+++ b/ios/TabViewImpl.swift
|
||||
@@ -281,7 +281,7 @@ extension View {
|
||||
|
||||
@ViewBuilder
|
||||
@@ -66,10 +69,10 @@ index 72938be90540ea3a483d7db9a80fb74c04d31272..277278ffdd9268a96cb09869eb1d0c0d
|
||||
if #available(iOS 26.0, macOS 26.0, *) {
|
||||
if let behavior {
|
||||
self.tabBarMinimizeBehavior(behavior.convert())
|
||||
diff --git a/node_modules/react-native-bottom-tabs/ios/TabViewProps.swift b/node_modules/react-native-bottom-tabs/ios/TabViewProps.swift
|
||||
diff --git a/ios/TabViewProps.swift b/ios/TabViewProps.swift
|
||||
index 9cfb29a983b34d3f84fc7a678d19ef4ff30e0325..6a5854483e66200b71722bbac12e100742222bd3 100644
|
||||
--- a/node_modules/react-native-bottom-tabs/ios/TabViewProps.swift
|
||||
+++ b/node_modules/react-native-bottom-tabs/ios/TabViewProps.swift
|
||||
--- a/ios/TabViewProps.swift
|
||||
+++ b/ios/TabViewProps.swift
|
||||
@@ -6,7 +6,7 @@ internal enum MinimizeBehavior: String {
|
||||
case onScrollUp
|
||||
case onScrollDown
|
||||
@@ -1,7 +1,7 @@
|
||||
diff --git a/node_modules/react-native-ios-utilities/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift b/node_modules/react-native-ios-utilities/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift
|
||||
diff --git a/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift b/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift
|
||||
index 09be306d5aa39337c5114c2ad6ba7513218e0751..24ff8ee2c36fef8632a7e012514fd04db9bf89fd 100644
|
||||
--- a/node_modules/react-native-ios-utilities/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift
|
||||
+++ b/node_modules/react-native-ios-utilities/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift
|
||||
--- a/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift
|
||||
+++ b/ios/Sources/Extensions+Helpers/RCTView+Helpers.swift
|
||||
@@ -25,15 +25,14 @@ public extension RCTView {
|
||||
return rootView.recursivelyFindSubview(whereType: targetType);
|
||||
};
|
||||
@@ -1,7 +1,10 @@
|
||||
diff --git a/node_modules/react-native-udp/react-native-udp.podspec b/node_modules/react-native-udp/react-native-udp.podspec
|
||||
diff --git a/node_modules/react-native-udp/.bun-tag-ea7df8754aa4db91 b/.bun-tag-ea7df8754aa4db91
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
||||
diff --git a/react-native-udp.podspec b/react-native-udp.podspec
|
||||
index 7450cc7d0862aadfb47d796929c801a3dc423a57..fa3e42c0152ef2d87536b8c2e484f64d525e35ec 100644
|
||||
--- a/node_modules/react-native-udp/react-native-udp.podspec
|
||||
+++ b/node_modules/react-native-udp/react-native-udp.podspec
|
||||
--- a/react-native-udp.podspec
|
||||
+++ b/react-native-udp.podspec
|
||||
@@ -9,7 +9,8 @@ Pod::Spec.new do |s|
|
||||
s.homepage = package_json["homepage"]
|
||||
s.license = package_json["license"]
|
||||
33
bun.lock
33
bun.lock
@@ -90,7 +90,7 @@
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-volume-manager": "^2.0.8",
|
||||
"react-native-web": "^0.21.0",
|
||||
"react-native-worklets": "0.8.3",
|
||||
"react-native-worklets": "0.9.1",
|
||||
"sonner-native": "0.21.2",
|
||||
"tailwindcss": "3.3.2",
|
||||
"use-debounce": "^10.0.4",
|
||||
@@ -114,6 +114,11 @@
|
||||
},
|
||||
},
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"react-native-ios-utilities@5.2.0": "bun-patches/react-native-ios-utilities@5.2.0.patch",
|
||||
"react-native-udp@4.1.7": "bun-patches/react-native-udp@4.1.7.patch",
|
||||
"react-native-bottom-tabs@1.2.0": "bun-patches/react-native-bottom-tabs@1.2.0.patch",
|
||||
},
|
||||
"packages": {
|
||||
"@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="],
|
||||
|
||||
@@ -195,11 +200,11 @@
|
||||
|
||||
"@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ=="],
|
||||
|
||||
"@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="],
|
||||
"@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA=="],
|
||||
|
||||
"@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A=="],
|
||||
|
||||
"@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA=="],
|
||||
"@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g=="],
|
||||
|
||||
"@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg=="],
|
||||
|
||||
@@ -215,13 +220,13 @@
|
||||
|
||||
"@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ=="],
|
||||
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="],
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg=="],
|
||||
|
||||
"@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.29.7", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-transform-destructuring": "^7.29.7", "@babel/plugin-transform-parameters": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A=="],
|
||||
|
||||
"@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng=="],
|
||||
|
||||
"@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg=="],
|
||||
"@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ=="],
|
||||
|
||||
"@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g=="],
|
||||
|
||||
@@ -1603,7 +1608,7 @@
|
||||
|
||||
"react-native-web": ["react-native-web@0.21.2", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg=="],
|
||||
|
||||
"react-native-worklets": ["react-native-worklets@0.8.3", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "convert-source-map": "^2.0.0", "semver": "^7.7.3" }, "peerDependencies": { "@babel/core": "*", "@react-native/metro-config": "*", "react": "*", "react-native": "0.81 - 0.85" } }, "sha512-oCBJROyLU7yG/1R8s0INMflygTH71bx+5XcYkH0CM938TlhSoVbiunE1WVW5FZa51vwYqfLie/IXMX2s1Kh3eg=="],
|
||||
"react-native-worklets": ["react-native-worklets@0.9.1", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.28.6", "@babel/plugin-transform-classes": "^7.28.6", "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", "@babel/plugin-transform-optional-chaining": "^7.28.6", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/preset-typescript": "^7.28.5", "convert-source-map": "^2.0.0", "semver": "^7.7.4" }, "peerDependencies": { "@babel/core": "*", "@react-native/metro-config": "*", "react": "*", "react-native": "0.83 - 0.86" } }, "sha512-kb6lGtBI5Ap41tvBPM09Np472r2GXuJ+jRApIFy1eXBk699eChG3U+lyqRC2/wz/VDpaJAy6i5XPcceNOoH3mA=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
|
||||
|
||||
@@ -1995,6 +2000,14 @@
|
||||
|
||||
"@react-native/babel-preset/@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="],
|
||||
|
||||
"@react-native/babel-preset/@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="],
|
||||
|
||||
"@react-native/babel-preset/@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA=="],
|
||||
|
||||
"@react-native/babel-preset/@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="],
|
||||
|
||||
"@react-native/babel-preset/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg=="],
|
||||
|
||||
"@react-native/codegen/@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="],
|
||||
|
||||
"@react-native/community-cli-plugin/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
|
||||
@@ -2017,6 +2030,14 @@
|
||||
|
||||
"babel-preset-expo/@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
|
||||
|
||||
"babel-preset-expo/@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.27.1", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA=="],
|
||||
|
||||
"babel-preset-expo/@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.28.4", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.28.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA=="],
|
||||
|
||||
"babel-preset-expo/@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA=="],
|
||||
|
||||
"babel-preset-expo/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg=="],
|
||||
|
||||
"brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
@@ -105,14 +105,14 @@ const AudioSlider: React.FC<AudioSliderProps> = ({ setVisibility }) => {
|
||||
maximumValue={max}
|
||||
thumbWidth={0}
|
||||
onValueChange={handleValueChange}
|
||||
renderBubble={() => null}
|
||||
renderThumb={() => null}
|
||||
containerStyle={{
|
||||
borderRadius: 50,
|
||||
}}
|
||||
theme={{
|
||||
minimumTrackTintColor: "#FDFDFD",
|
||||
maximumTrackTintColor: "#5A5A5A",
|
||||
bubbleBackgroundColor: "transparent", // Hide the value bubble
|
||||
bubbleTextColor: "transparent", // Hide the value text
|
||||
}}
|
||||
/>
|
||||
<Ionicons
|
||||
|
||||
@@ -88,14 +88,14 @@ const BrightnessSlider = () => {
|
||||
maximumValue={max}
|
||||
thumbWidth={0}
|
||||
onValueChange={handleValueChange}
|
||||
renderBubble={() => null}
|
||||
renderThumb={() => null}
|
||||
containerStyle={{
|
||||
borderRadius: 50,
|
||||
}}
|
||||
theme={{
|
||||
minimumTrackTintColor: "#FDFDFD",
|
||||
maximumTrackTintColor: "#5A5A5A",
|
||||
bubbleBackgroundColor: "transparent", // Hide the value bubble
|
||||
bubbleTextColor: "transparent", // Hide the value text
|
||||
}}
|
||||
/>
|
||||
<Ionicons
|
||||
|
||||
30
eas.json
30
eas.json
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 9.1.0",
|
||||
"appVersionSource": "remote"
|
||||
"version": ">= 9.1.0"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
@@ -53,14 +52,14 @@
|
||||
},
|
||||
"production": {
|
||||
"environment": "production",
|
||||
"autoIncrement": true,
|
||||
"channel": "0.54.0",
|
||||
"android": {
|
||||
"image": "latest"
|
||||
}
|
||||
},
|
||||
"production-apk": {
|
||||
"environment": "production",
|
||||
"autoIncrement": true,
|
||||
"channel": "0.54.0",
|
||||
"android": {
|
||||
"buildType": "apk",
|
||||
"image": "latest"
|
||||
@@ -68,7 +67,7 @@
|
||||
},
|
||||
"production-apk-tv": {
|
||||
"environment": "production",
|
||||
"autoIncrement": true,
|
||||
"channel": "0.54.0",
|
||||
"android": {
|
||||
"buildType": "apk",
|
||||
"image": "latest"
|
||||
@@ -79,7 +78,7 @@
|
||||
},
|
||||
"production_tv": {
|
||||
"environment": "production",
|
||||
"autoIncrement": true,
|
||||
"channel": "0.54.0",
|
||||
"env": {
|
||||
"EXPO_TV": "1"
|
||||
},
|
||||
@@ -89,22 +88,7 @@
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"production": {
|
||||
"ios": {
|
||||
"appleTeamId": "MWD5K362T8",
|
||||
"ascAppId": "6593660679"
|
||||
},
|
||||
"android": {
|
||||
"serviceAccountKeyPath": "./google-service-account.json",
|
||||
"track": "internal",
|
||||
"releaseStatus": "completed"
|
||||
}
|
||||
},
|
||||
"production_tv": {
|
||||
"ios": {
|
||||
"appleTeamId": "MWD5K362T8",
|
||||
"ascAppId": "6593660679"
|
||||
}
|
||||
}
|
||||
"production": {},
|
||||
"production_tv": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,43 +236,37 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach surface and ensure video output is active.
|
||||
*
|
||||
* During PiP transitions, the surface is destroyed and recreated by Android.
|
||||
* We keep the VO pipeline alive (not killed with vo=null) so that rendering
|
||||
* resumes immediately when the new surface is attached — avoiding the black
|
||||
* screen that occurs when the VO is fully re-initialized via setOptionString.
|
||||
* Attach surface and re-enable video output.
|
||||
* Based on Findroid's implementation.
|
||||
*/
|
||||
fun attachSurface(surface: Surface) {
|
||||
this.surface = surface
|
||||
Log.i(TAG, "[PiP] attachSurface — isRunning=$isRunning, vo=$voDriver, surface=${surface.hashCode()}")
|
||||
if (isRunning) {
|
||||
MPVLib.attachSurface(surface)
|
||||
// Re-enable video output after attaching surface (Findroid approach)
|
||||
MPVLib.setOptionString("force-window", "yes")
|
||||
// Read back vo to confirm it's still active
|
||||
val activeVo = try { MPVLib.getPropertyString("vo") } catch (e: Exception) { null }
|
||||
Log.i(TAG, "[PiP] attachSurface — attached, activeVo=$activeVo")
|
||||
MPVLib.setOptionString("vo", voDriver)
|
||||
Log.i(TAG, "Surface attached, video output re-enabled (vo=$voDriver)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Detach surface without killing the VO pipeline.
|
||||
*
|
||||
* The previous approach (vo=null / force-window=no) destroyed the entire video
|
||||
* output pipeline on every surface transition. During PiP mode, the rapid
|
||||
* destroy/recreate cycle caused a black screen because setOptionString("vo", ...)
|
||||
* did not properly re-initialize rendering into the new PiP surface.
|
||||
*
|
||||
* By keeping the VO alive, frames are simply dropped while no surface is
|
||||
* attached, and rendering resumes immediately when the new surface arrives.
|
||||
* Detach surface and disable video output.
|
||||
* Based on Findroid's implementation.
|
||||
*/
|
||||
fun detachSurface() {
|
||||
this.surface = null
|
||||
Log.i(TAG, "[PiP] detachSurface — isRunning=$isRunning, vo=$voDriver")
|
||||
if (isRunning) {
|
||||
try {
|
||||
// Disable video output before detaching surface (Findroid approach)
|
||||
MPVLib.setOptionString("vo", "null")
|
||||
MPVLib.setOptionString("force-window", "no")
|
||||
Log.i(TAG, "Video output disabled before surface detach")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to disable video output: ${e.message}")
|
||||
}
|
||||
|
||||
MPVLib.detachSurface()
|
||||
val activeVo = try { MPVLib.getPropertyString("vo") } catch (e: Exception) { null }
|
||||
Log.i(TAG, "[PiP] detachSurface — detached, activeVo=$activeVo (should still be $voDriver)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,24 +277,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
||||
fun updateSurfaceSize(width: Int, height: Int) {
|
||||
if (isRunning) {
|
||||
MPVLib.setPropertyString("android-surface-size", "${width}x$height")
|
||||
Log.i(TAG, "[PiP] updateSurfaceSize — ${width}x${height}")
|
||||
} else {
|
||||
Log.w(TAG, "[PiP] updateSurfaceSize — called but renderer not running")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force mpv to render a frame to the current surface.
|
||||
* Steps forward one frame then seeks back to the original position.
|
||||
* Used after PiP entry to work around mpv stopping pixel output.
|
||||
*/
|
||||
fun forceRedraw() {
|
||||
if (!isRunning) return
|
||||
val pos = cachedPosition
|
||||
Log.i(TAG, "[PiP] forceRedraw — stepping frame then seeking to $pos")
|
||||
MPVLib.command(arrayOf("frame-step"))
|
||||
if (pos > 0) {
|
||||
MPVLib.command(arrayOf("seek", pos.toString(), "absolute"))
|
||||
Log.i(TAG, "Surface size updated: ${width}x$height")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,7 +692,9 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
||||
// dropped), so we (re)apply here for embedded and external alike.
|
||||
// This is what makes a carried-over subtitle show up on the next
|
||||
// episode without a manual re-selection.
|
||||
initialAudioId?.let { if (it > 0) setAudioTrack(it) }
|
||||
if (initialAudioId != null && initialAudioId > 0) {
|
||||
setAudioTrack(initialAudioId)
|
||||
}
|
||||
initialSubtitleId?.let { setSubtitleTrack(it) } ?: disableSubtitles()
|
||||
|
||||
if (!isReadyToSeek) {
|
||||
|
||||
@@ -198,7 +198,7 @@ class MpvPlayerModule : Module() {
|
||||
}
|
||||
|
||||
// Defines events that the view can send to JavaScript
|
||||
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady", "onPictureInPictureChange")
|
||||
Events("onLoad", "onPlaybackStateChange", "onProgress", "onError", "onTracksReady")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,12 @@ package expo.modules.mpvplayer
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import android.view.TextureView
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.widget.FrameLayout
|
||||
import expo.modules.kotlin.AppContext
|
||||
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
@@ -31,27 +28,26 @@ data class VideoLoadConfig(
|
||||
|
||||
/**
|
||||
* MpvPlayerView - ExpoView that hosts the MPV player.
|
||||
* Uses TextureView for reliable Picture-in-Picture support.
|
||||
* This mirrors the iOS MpvPlayerView implementation.
|
||||
*/
|
||||
class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context, appContext),
|
||||
MPVLayerRenderer.Delegate, TextureView.SurfaceTextureListener {
|
||||
|
||||
class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context, appContext),
|
||||
MPVLayerRenderer.Delegate, SurfaceHolder.Callback {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MpvPlayerView"
|
||||
}
|
||||
|
||||
|
||||
// Event dispatchers
|
||||
val onLoad by EventDispatcher()
|
||||
val onPlaybackStateChange by EventDispatcher()
|
||||
val onProgress by EventDispatcher()
|
||||
val onError by EventDispatcher()
|
||||
val onTracksReady by EventDispatcher()
|
||||
val onPictureInPictureChange by EventDispatcher()
|
||||
|
||||
private var textureView: TextureView
|
||||
|
||||
private var surfaceView: SurfaceView
|
||||
private var renderer: MPVLayerRenderer? = null
|
||||
private var pipController: PiPController? = null
|
||||
|
||||
|
||||
private var currentUrl: String? = null
|
||||
private var cachedPosition: Double = 0.0
|
||||
private var cachedDuration: Double = 0.0
|
||||
@@ -60,29 +56,23 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
||||
private var pendingConfig: VideoLoadConfig? = null
|
||||
private var rendererStarted: Boolean = false
|
||||
private var pendingSurface: Surface? = null
|
||||
private var surfaceTexture: SurfaceTexture? = null
|
||||
|
||||
// PiP state tracking
|
||||
private var isWaitingForPiPTransition: Boolean = false
|
||||
private var isPiPSurfaceForced: Boolean = false
|
||||
private val pipHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
init {
|
||||
setBackgroundColor(Color.BLACK)
|
||||
|
||||
// Create TextureView for video rendering (composites into app window for PiP support)
|
||||
textureView = TextureView(context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
// Create SurfaceView for video rendering
|
||||
surfaceView = SurfaceView(context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
surfaceTextureListener = this@MpvPlayerView
|
||||
holder.addCallback(this@MpvPlayerView)
|
||||
}
|
||||
addView(textureView)
|
||||
addView(surfaceView)
|
||||
|
||||
// Initialize PiP controller with Expo's AppContext for proper activity access
|
||||
pipController = PiPController(context, appContext)
|
||||
pipController?.setPlayerView(textureView)
|
||||
pipController?.setPlayerView(surfaceView)
|
||||
pipController?.delegate = object : PiPController.Delegate {
|
||||
override fun onPlay() {
|
||||
play()
|
||||
@@ -95,23 +85,6 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
||||
override fun onSeekBy(seconds: Double) {
|
||||
seekBy(seconds)
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPiP: Boolean) {
|
||||
if (isInPiP) {
|
||||
if (!isWaitingForPiPTransition) {
|
||||
isWaitingForPiPTransition = true
|
||||
pipHandler.removeCallbacksAndMessages(null)
|
||||
for (delay in longArrayOf(500, 1000, 1500, 2000)) {
|
||||
pipHandler.postDelayed({ forcePiPBufferSize() }, delay)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
isWaitingForPiPTransition = false
|
||||
pipHandler.removeCallbacksAndMessages(null)
|
||||
restoreFromPiP()
|
||||
}
|
||||
onPictureInPictureChange(mapOf("isActive" to isInPiP))
|
||||
}
|
||||
}
|
||||
|
||||
// Renderer is created lazily in loadVideo once we have the voDriver setting
|
||||
@@ -129,29 +102,32 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
||||
try {
|
||||
renderer?.start(voDriver ?: "gpu-next")
|
||||
rendererStarted = true
|
||||
Log.i(TAG, "Renderer started with vo=$voDriver")
|
||||
|
||||
// If surface was created before renderer started, attach it now
|
||||
pendingSurface?.let { surface ->
|
||||
renderer?.attachSurface(surface)
|
||||
pendingSurface = null
|
||||
Log.i(TAG, "Attached pending surface after renderer start")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start renderer: ${e.message}")
|
||||
onError(mapOf("error" to "Failed to start renderer: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TextureView.SurfaceTextureListener
|
||||
|
||||
override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
|
||||
this.surfaceTexture = surfaceTexture
|
||||
val surface = Surface(surfaceTexture)
|
||||
surfaceTexture.setDefaultBufferSize(width, height)
|
||||
|
||||
// MARK: - SurfaceHolder.Callback
|
||||
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
Log.i(TAG, "Surface created")
|
||||
surfaceReady = true
|
||||
|
||||
if (rendererStarted) {
|
||||
renderer?.attachSurface(surface)
|
||||
renderer?.attachSurface(holder.surface)
|
||||
} else {
|
||||
pendingSurface = surface
|
||||
// Renderer not started yet - store surface to attach after start
|
||||
pendingSurface = holder.surface
|
||||
Log.i(TAG, "Surface created before renderer started, storing as pending")
|
||||
}
|
||||
|
||||
// If we have a pending load, execute it now
|
||||
@@ -161,23 +137,19 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
||||
pendingConfig = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureSizeChanged(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
|
||||
surfaceTexture.setDefaultBufferSize(width, height)
|
||||
|
||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||
Log.i(TAG, "Surface changed: ${width}x${height}")
|
||||
// Update MPV with the new surface size (Findroid approach)
|
||||
renderer?.updateSurfaceSize(width, height)
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
|
||||
this.surfaceTexture = null
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
Log.i(TAG, "Surface destroyed")
|
||||
surfaceReady = false
|
||||
renderer?.detachSurface()
|
||||
return false // mpv manages the SurfaceTexture
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) {
|
||||
// Called every frame — no action needed, mpv drives rendering directly
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Video Loading
|
||||
|
||||
fun loadVideo(config: VideoLoadConfig) {
|
||||
@@ -197,10 +169,10 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
||||
|
||||
loadVideoInternal(config)
|
||||
}
|
||||
|
||||
|
||||
private fun loadVideoInternal(config: VideoLoadConfig) {
|
||||
currentUrl = config.url
|
||||
|
||||
|
||||
renderer?.load(
|
||||
url = config.url,
|
||||
headers = config.headers,
|
||||
@@ -209,173 +181,124 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
||||
initialSubtitleId = config.initialSubtitleId,
|
||||
initialAudioId = config.initialAudioId
|
||||
)
|
||||
|
||||
|
||||
if (config.autoplay) {
|
||||
play()
|
||||
}
|
||||
|
||||
|
||||
onLoad(mapOf("url" to config.url))
|
||||
}
|
||||
|
||||
|
||||
// Convenience method for simple loads
|
||||
fun loadVideo(url: String, headers: Map<String, String>? = null) {
|
||||
loadVideo(VideoLoadConfig(url = url, headers = headers))
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Playback Controls
|
||||
|
||||
|
||||
fun play() {
|
||||
intendedPlayState = true
|
||||
renderer?.play()
|
||||
pipController?.setPlaybackRate(1.0)
|
||||
}
|
||||
|
||||
|
||||
fun pause() {
|
||||
intendedPlayState = false
|
||||
renderer?.pause()
|
||||
pipController?.setPlaybackRate(0.0)
|
||||
}
|
||||
|
||||
|
||||
fun seekTo(position: Double) {
|
||||
renderer?.seekTo(position)
|
||||
}
|
||||
|
||||
|
||||
fun seekBy(offset: Double) {
|
||||
renderer?.seekBy(offset)
|
||||
}
|
||||
|
||||
|
||||
fun setSpeed(speed: Double) {
|
||||
renderer?.setSpeed(speed)
|
||||
}
|
||||
|
||||
|
||||
fun getSpeed(): Double {
|
||||
return renderer?.getSpeed() ?: 1.0
|
||||
}
|
||||
|
||||
|
||||
fun isPaused(): Boolean {
|
||||
return renderer?.isPausedState ?: true
|
||||
}
|
||||
|
||||
|
||||
fun getCurrentPosition(): Double {
|
||||
return cachedPosition
|
||||
}
|
||||
|
||||
|
||||
fun getDuration(): Double {
|
||||
return cachedDuration
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Picture in Picture
|
||||
|
||||
|
||||
fun startPictureInPicture() {
|
||||
isWaitingForPiPTransition = true
|
||||
Log.i(TAG, "startPictureInPicture called")
|
||||
pipController?.startPictureInPicture()
|
||||
|
||||
// Resize buffer to match PiP window after animation settles
|
||||
pipHandler.removeCallbacksAndMessages(null)
|
||||
for (delay in longArrayOf(500, 1000, 1500, 2000)) {
|
||||
pipHandler.postDelayed({ forcePiPBufferSize() }, delay)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the SurfaceTexture buffer AND TextureView layout to match the PiP
|
||||
* visible rect so mpv renders at the PiP window's actual dimensions.
|
||||
*/
|
||||
private fun forcePiPBufferSize() {
|
||||
if (!isWaitingForPiPTransition || !surfaceReady) return
|
||||
|
||||
val rect = Rect()
|
||||
textureView.getGlobalVisibleRect(rect)
|
||||
val visW = rect.width()
|
||||
val visH = rect.height()
|
||||
val vw = textureView.width
|
||||
val vh = textureView.height
|
||||
|
||||
if (visW <= 0 || visH <= 0 || (vw == visW && vh == visH)) return
|
||||
|
||||
surfaceTexture?.setDefaultBufferSize(visW, visH)
|
||||
renderer?.updateSurfaceSize(visW, visH)
|
||||
|
||||
// Force TextureView layout to match PiP visible area.
|
||||
// layoutParams alone doesn't work during PiP because the parent
|
||||
// never re-lays out its children.
|
||||
textureView.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(visW, View.MeasureSpec.EXACTLY),
|
||||
View.MeasureSpec.makeMeasureSpec(visH, View.MeasureSpec.EXACTLY)
|
||||
)
|
||||
textureView.layout(0, 0, visW, visH)
|
||||
isPiPSurfaceForced = true
|
||||
}
|
||||
|
||||
private fun restoreFromPiP() {
|
||||
if (!isPiPSurfaceForced) return
|
||||
isPiPSurfaceForced = false
|
||||
|
||||
val lp = textureView.layoutParams
|
||||
lp.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
lp.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
textureView.layoutParams = lp
|
||||
textureView.requestLayout()
|
||||
}
|
||||
|
||||
|
||||
fun stopPictureInPicture() {
|
||||
isWaitingForPiPTransition = false
|
||||
pipHandler.removeCallbacksAndMessages(null)
|
||||
pipController?.stopPictureInPicture()
|
||||
}
|
||||
|
||||
|
||||
fun isPictureInPictureSupported(): Boolean {
|
||||
return pipController?.isPictureInPictureSupported() ?: false
|
||||
}
|
||||
|
||||
|
||||
fun isPictureInPictureActive(): Boolean {
|
||||
return pipController?.isPictureInPictureActive() ?: false
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Subtitle Controls
|
||||
|
||||
|
||||
fun getSubtitleTracks(): List<Map<String, Any>> {
|
||||
return renderer?.getSubtitleTracks() ?: emptyList()
|
||||
}
|
||||
|
||||
|
||||
fun setSubtitleTrack(trackId: Int) {
|
||||
renderer?.setSubtitleTrack(trackId)
|
||||
}
|
||||
|
||||
|
||||
fun disableSubtitles() {
|
||||
renderer?.disableSubtitles()
|
||||
}
|
||||
|
||||
|
||||
fun getCurrentSubtitleTrack(): Int {
|
||||
return renderer?.getCurrentSubtitleTrack() ?: 0
|
||||
}
|
||||
|
||||
|
||||
fun addSubtitleFile(url: String, select: Boolean = true) {
|
||||
renderer?.addSubtitleFile(url, select)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Subtitle Positioning
|
||||
|
||||
|
||||
fun setSubtitlePosition(position: Int) {
|
||||
renderer?.setSubtitlePosition(position)
|
||||
}
|
||||
|
||||
|
||||
fun setSubtitleScale(scale: Double) {
|
||||
renderer?.setSubtitleScale(scale)
|
||||
}
|
||||
|
||||
|
||||
fun setSubtitleMarginY(margin: Int) {
|
||||
renderer?.setSubtitleMarginY(margin)
|
||||
}
|
||||
|
||||
|
||||
fun setSubtitleAlignX(alignment: String) {
|
||||
renderer?.setSubtitleAlignX(alignment)
|
||||
}
|
||||
|
||||
|
||||
fun setSubtitleAlignY(alignment: String) {
|
||||
renderer?.setSubtitleAlignY(alignment)
|
||||
}
|
||||
|
||||
|
||||
fun setSubtitleFontSize(size: Int) {
|
||||
renderer?.setSubtitleFontSize(size)
|
||||
}
|
||||
@@ -393,15 +316,15 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
||||
}
|
||||
|
||||
// MARK: - Audio Track Controls
|
||||
|
||||
|
||||
fun getAudioTracks(): List<Map<String, Any>> {
|
||||
return renderer?.getAudioTracks() ?: emptyList()
|
||||
}
|
||||
|
||||
|
||||
fun setAudioTrack(trackId: Int) {
|
||||
renderer?.setAudioTrack(trackId)
|
||||
}
|
||||
|
||||
|
||||
fun getCurrentAudioTrack(): Int {
|
||||
return renderer?.getCurrentAudioTrack() ?: 0
|
||||
}
|
||||
@@ -426,16 +349,16 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
||||
}
|
||||
|
||||
// MARK: - MPVLayerRenderer.Delegate
|
||||
|
||||
|
||||
override fun onPositionChanged(position: Double, duration: Double, cacheSeconds: Double) {
|
||||
cachedPosition = position
|
||||
cachedDuration = duration
|
||||
|
||||
|
||||
// Update PiP progress
|
||||
if (pipController?.isPictureInPictureActive() == true) {
|
||||
pipController?.setCurrentTime(position, duration)
|
||||
}
|
||||
|
||||
|
||||
onProgress(mapOf(
|
||||
"position" to position,
|
||||
"duration" to duration,
|
||||
@@ -443,51 +366,50 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
||||
"cacheSeconds" to cacheSeconds
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
override fun onPauseChanged(isPaused: Boolean) {
|
||||
// Sync PiP playback rate
|
||||
pipController?.setPlaybackRate(if (isPaused) 0.0 else 1.0)
|
||||
|
||||
|
||||
onPlaybackStateChange(mapOf(
|
||||
"isPaused" to isPaused,
|
||||
"isPlaying" to !isPaused
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
override fun onLoadingChanged(isLoading: Boolean) {
|
||||
onPlaybackStateChange(mapOf(
|
||||
"isLoading" to isLoading
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
override fun onReadyToSeek() {
|
||||
onPlaybackStateChange(mapOf(
|
||||
"isReadyToSeek" to true
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
override fun onTracksReady() {
|
||||
onTracksReady(emptyMap<String, Any>())
|
||||
}
|
||||
|
||||
|
||||
override fun onVideoDimensionsChanged(width: Int, height: Int) {
|
||||
// Update PiP controller with video dimensions for proper aspect ratio
|
||||
pipController?.setVideoDimensions(width, height)
|
||||
}
|
||||
|
||||
|
||||
override fun onError(message: String) {
|
||||
onError(mapOf("error" to message))
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Cleanup
|
||||
|
||||
|
||||
fun cleanup() {
|
||||
isWaitingForPiPTransition = false
|
||||
pipHandler.removeCallbacksAndMessages(null)
|
||||
pipController?.stopPictureInPicture()
|
||||
renderer?.stop()
|
||||
surfaceTexture = null
|
||||
surfaceReady = false
|
||||
surfaceView.holder.removeCallback(this)
|
||||
}
|
||||
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
cleanup()
|
||||
|
||||
@@ -1,62 +1,51 @@
|
||||
package expo.modules.mpvplayer
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.app.PictureInPictureParams
|
||||
import android.app.RemoteAction
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.util.Rational
|
||||
import android.view.View
|
||||
import androidx.annotation.RequiresApi
|
||||
import expo.modules.kotlin.AppContext
|
||||
|
||||
/**
|
||||
* Picture-in-Picture controller for Android.
|
||||
* This mirrors the iOS PiPController implementation.
|
||||
*/
|
||||
class PiPController(private val context: Context, private val appContext: AppContext? = null) {
|
||||
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PiPController"
|
||||
private const val DEFAULT_ASPECT_WIDTH = 16
|
||||
private const val DEFAULT_ASPECT_HEIGHT = 9
|
||||
private const val ACTION_PIP_PLAY_PAUSE = "expo.modules.mpvplayer.PIP_PLAY_PAUSE"
|
||||
private const val ACTION_PIP_SKIP_FORWARD = "expo.modules.mpvplayer.PIP_SKIP_FORWARD"
|
||||
private const val ACTION_PIP_SKIP_BACKWARD = "expo.modules.mpvplayer.PIP_SKIP_BACKWARD"
|
||||
}
|
||||
|
||||
|
||||
interface Delegate {
|
||||
fun onPlay()
|
||||
fun onPause()
|
||||
fun onSeekBy(seconds: Double)
|
||||
fun onPictureInPictureModeChanged(isInPiP: Boolean)
|
||||
}
|
||||
|
||||
|
||||
var delegate: Delegate? = null
|
||||
|
||||
|
||||
private var currentPosition: Double = 0.0
|
||||
private var currentDuration: Double = 0.0
|
||||
private var playbackRate: Double = 1.0
|
||||
|
||||
|
||||
// Video dimensions for proper aspect ratio
|
||||
private var videoWidth: Int = 0
|
||||
private var videoHeight: Int = 0
|
||||
|
||||
// Reference to the player view for source rect
|
||||
private var playerView: View? = null
|
||||
|
||||
// PiP state tracking
|
||||
private var isInPiPMode: Boolean = false
|
||||
private var pipEntryNotified: Boolean = false
|
||||
private val pipHandler = Handler(Looper.getMainLooper())
|
||||
private var lifecycleCallbacks: Application.ActivityLifecycleCallbacks? = null
|
||||
private var lifecycleRegistered = false
|
||||
private var pipBroadcastReceiver: BroadcastReceiver? = null
|
||||
|
||||
|
||||
/**
|
||||
* Check if Picture-in-Picture is supported on this device
|
||||
*/
|
||||
fun isPictureInPictureSupported(): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
|
||||
@@ -64,7 +53,10 @@ class PiPController(private val context: Context, private val appContext: AppCon
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if Picture-in-Picture is currently active
|
||||
*/
|
||||
fun isPictureInPictureActive(): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val activity = getActivity()
|
||||
@@ -72,69 +64,69 @@ class PiPController(private val context: Context, private val appContext: AppCon
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start Picture-in-Picture mode
|
||||
*/
|
||||
fun startPictureInPicture() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
|
||||
val activity = getActivity() ?: run {
|
||||
Log.e(TAG, "Cannot start PiP: no activity")
|
||||
return
|
||||
}
|
||||
|
||||
if (!isPictureInPictureSupported()) {
|
||||
Log.e(TAG, "PiP not supported on this device")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val params = buildPiPParams(forEntering = true)
|
||||
val result = activity.enterPictureInPictureMode(params)
|
||||
|
||||
if (!result) {
|
||||
Log.e(TAG, "enterPictureInPictureMode rejected by system")
|
||||
isInPiPMode = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val activity = getActivity()
|
||||
if (activity == null) {
|
||||
Log.e(TAG, "Cannot start PiP: no activity found")
|
||||
return
|
||||
}
|
||||
|
||||
isInPiPMode = true
|
||||
pipEntryNotified = true
|
||||
delegate?.onPictureInPictureModeChanged(true)
|
||||
registerLifecycleCallbacks()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to enter PiP: ${e.message}")
|
||||
|
||||
if (!isPictureInPictureSupported()) {
|
||||
Log.e(TAG, "PiP not supported on this device")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val params = buildPiPParams(forEntering = true)
|
||||
activity.enterPictureInPictureMode(params)
|
||||
Log.i(TAG, "Entered PiP mode")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to enter PiP: ${e.message}")
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "PiP requires Android O or higher")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Stop Picture-in-Picture mode
|
||||
*/
|
||||
fun stopPictureInPicture() {
|
||||
isInPiPMode = false
|
||||
pipEntryNotified = false
|
||||
unregisterLifecycleCallbacks()
|
||||
// On Android, exiting PiP is typically done by the user
|
||||
// or by finishing the activity. We can request to move task to back.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val activity = getActivity()
|
||||
if (activity?.isInPictureInPictureMode == true) {
|
||||
// Move task to back which will exit PiP
|
||||
activity.moveTaskToBack(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isCurrentlyInPiP(): Boolean = isInPiPMode
|
||||
|
||||
|
||||
/**
|
||||
* Update the current playback position and duration
|
||||
* Note: We don't update PiP params here as we're not using progress in PiP controls
|
||||
*/
|
||||
fun setCurrentTime(position: Double, duration: Double) {
|
||||
currentPosition = position
|
||||
currentDuration = duration
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the playback rate (0.0 for paused, 1.0 for playing)
|
||||
*/
|
||||
fun setPlaybackRate(rate: Double) {
|
||||
playbackRate = rate
|
||||
|
||||
if (rate > 0) {
|
||||
registerLifecycleCallbacks()
|
||||
}
|
||||
|
||||
// Update PiP params so autoEnterEnabled and action icons track play/pause state
|
||||
|
||||
// Update PiP params to reflect play/pause state
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val activity = getActivity()
|
||||
if (activity != null) {
|
||||
if (activity?.isInPictureInPictureMode == true) {
|
||||
try {
|
||||
activity.setPictureInPictureParams(buildPiPParams())
|
||||
} catch (e: Exception) {
|
||||
@@ -143,19 +135,28 @@ class PiPController(private val context: Context, private val appContext: AppCon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the video dimensions for proper aspect ratio calculation
|
||||
*/
|
||||
fun setVideoDimensions(width: Int, height: Int) {
|
||||
if (width > 0 && height > 0) {
|
||||
videoWidth = width
|
||||
videoHeight = height
|
||||
Log.i(TAG, "Video dimensions set: ${width}x${height}")
|
||||
|
||||
// Update PiP params if active
|
||||
updatePiPParamsIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the player view reference for source rect hint
|
||||
*/
|
||||
fun setPlayerView(view: View?) {
|
||||
playerView = view
|
||||
}
|
||||
|
||||
|
||||
private fun updatePiPParamsIfNeeded() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val activity = getActivity()
|
||||
@@ -168,16 +169,23 @@ class PiPController(private val context: Context, private val appContext: AppCon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build Picture-in-Picture params for the current player state.
|
||||
* Calculates proper aspect ratio and source rect based on video and view dimensions.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun buildPiPParams(forEntering: Boolean = false): PictureInPictureParams {
|
||||
val view = playerView
|
||||
val viewWidth = view?.width ?: 0
|
||||
val viewHeight = view?.height ?: 0
|
||||
|
||||
|
||||
// Display aspect ratio from view (exactly like Findroid)
|
||||
val displayAspectRatio = Rational(viewWidth.coerceAtLeast(1), viewHeight.coerceAtLeast(1))
|
||||
|
||||
// Video aspect ratio with 2.39:1 clamping
|
||||
|
||||
// Video aspect ratio with 2.39:1 clamping (exactly like Findroid)
|
||||
// Findroid: Rational(it.width.coerceAtMost((it.height * 2.39f).toInt()),
|
||||
// it.height.coerceAtMost((it.width * 2.39f).toInt()))
|
||||
val aspectRatio = if (videoWidth > 0 && videoHeight > 0) {
|
||||
Rational(
|
||||
videoWidth.coerceAtMost((videoHeight * 2.39f).toInt()),
|
||||
@@ -186,235 +194,70 @@ class PiPController(private val context: Context, private val appContext: AppCon
|
||||
} else {
|
||||
Rational(DEFAULT_ASPECT_WIDTH, DEFAULT_ASPECT_HEIGHT)
|
||||
}
|
||||
|
||||
|
||||
// Source rect hint calculation (exactly like Findroid)
|
||||
val sourceRectHint = if (viewWidth > 0 && viewHeight > 0 && videoWidth > 0 && videoHeight > 0) {
|
||||
if (displayAspectRatio < aspectRatio) {
|
||||
// Letterboxing - black bars top/bottom
|
||||
val space = ((viewHeight - (viewWidth.toFloat() / aspectRatio.toFloat())) / 2).toInt()
|
||||
Rect(0, space, viewWidth, (viewWidth.toFloat() / aspectRatio.toFloat()).toInt() + space)
|
||||
Rect(
|
||||
0,
|
||||
space,
|
||||
viewWidth,
|
||||
(viewWidth.toFloat() / aspectRatio.toFloat()).toInt() + space
|
||||
)
|
||||
} else {
|
||||
// Pillarboxing - black bars left/right
|
||||
val space = ((viewWidth - (viewHeight.toFloat() * aspectRatio.toFloat())) / 2).toInt()
|
||||
Rect(space, 0, (viewHeight.toFloat() * aspectRatio.toFloat()).toInt() + space, viewHeight)
|
||||
Rect(
|
||||
space,
|
||||
0,
|
||||
(viewHeight.toFloat() * aspectRatio.toFloat()).toInt() + space,
|
||||
viewHeight
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
val builder = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(aspectRatio)
|
||||
|
||||
|
||||
sourceRectHint?.let { builder.setSourceRectHint(it) }
|
||||
|
||||
ensurePiPReceiverRegistered()
|
||||
builder.setActions(buildPiPActions())
|
||||
|
||||
|
||||
// On Android 12+, enable auto-enter (like Findroid)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
builder.setAutoEnterEnabled(forEntering || playbackRate > 0)
|
||||
builder.setAutoEnterEnabled(true)
|
||||
}
|
||||
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
|
||||
private fun getActivity(): Activity? {
|
||||
// First try Expo's AppContext (preferred in React Native)
|
||||
appContext?.currentActivity?.let { return it }
|
||||
|
||||
|
||||
// Fallback: Try to get from context wrapper chain
|
||||
var ctx = context
|
||||
while (ctx is android.content.ContextWrapper) {
|
||||
if (ctx is Activity) return ctx
|
||||
if (ctx is Activity) {
|
||||
return ctx
|
||||
}
|
||||
ctx = ctx.baseContext
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle-based PiP Detection
|
||||
|
||||
private fun registerLifecycleCallbacks() {
|
||||
if (lifecycleRegistered) return
|
||||
|
||||
val app = context.applicationContext as? Application ?: run {
|
||||
Log.w(TAG, "Cannot access Application for lifecycle callbacks, falling back to polling")
|
||||
startFallbackPolling()
|
||||
return
|
||||
|
||||
/**
|
||||
* Handle PiP action (called from activity when user taps PiP controls)
|
||||
*/
|
||||
fun handlePiPAction(action: String) {
|
||||
when (action) {
|
||||
"play" -> delegate?.onPlay()
|
||||
"pause" -> delegate?.onPause()
|
||||
"skip_forward" -> delegate?.onSeekBy(10.0)
|
||||
"skip_backward" -> delegate?.onSeekBy(-10.0)
|
||||
}
|
||||
|
||||
lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks {
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
if (!isInPiPMode) return
|
||||
if (!activity.isInPictureInPictureMode) {
|
||||
isInPiPMode = false
|
||||
pipEntryNotified = false
|
||||
delegate?.onPictureInPictureModeChanged(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
// Proactively hide controls when user leaves while playing,
|
||||
// before the PiP window captures the UI. onActivityStopped
|
||||
// will restore if PiP didn't actually enter.
|
||||
if (playbackRate > 0 && !isInPiPMode) {
|
||||
isInPiPMode = true
|
||||
pipEntryNotified = true
|
||||
delegate?.onPictureInPictureModeChanged(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {
|
||||
pipHandler.postDelayed({
|
||||
val inPip = activity.isInPictureInPictureMode
|
||||
|
||||
if (inPip && !isInPiPMode) {
|
||||
isInPiPMode = true
|
||||
pipEntryNotified = true
|
||||
delegate?.onPictureInPictureModeChanged(true)
|
||||
return@postDelayed
|
||||
}
|
||||
|
||||
if (!isInPiPMode) return@postDelayed
|
||||
if (inPip) return@postDelayed
|
||||
|
||||
// Not in PiP after 1s — check again to avoid false positive during transition
|
||||
pipHandler.postDelayed({
|
||||
if (!isInPiPMode) return@postDelayed
|
||||
if (!activity.isInPictureInPictureMode) {
|
||||
isInPiPMode = false
|
||||
pipEntryNotified = false
|
||||
delegate?.onPictureInPictureModeChanged(false)
|
||||
}
|
||||
}, 1500)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
isInPiPMode = false
|
||||
}
|
||||
}
|
||||
|
||||
app.registerActivityLifecycleCallbacks(lifecycleCallbacks)
|
||||
lifecycleRegistered = true
|
||||
}
|
||||
|
||||
private fun unregisterLifecycleCallbacks() {
|
||||
if (!lifecycleRegistered) return
|
||||
lifecycleCallbacks?.let {
|
||||
(context.applicationContext as? Application)
|
||||
?.unregisterActivityLifecycleCallbacks(it)
|
||||
}
|
||||
lifecycleCallbacks = null
|
||||
lifecycleRegistered = false
|
||||
pipHandler.removeCallbacksAndMessages(null)
|
||||
unregisterPiPBroadcastReceiver()
|
||||
}
|
||||
|
||||
private fun startFallbackPolling() {
|
||||
var falseReadCount = 0
|
||||
pipHandler.removeCallbacksAndMessages(null)
|
||||
pipHandler.postDelayed(object : Runnable {
|
||||
override fun run() {
|
||||
if (!isInPiPMode) return
|
||||
|
||||
var ctx = context
|
||||
var activity: Activity? = null
|
||||
while (ctx is android.content.ContextWrapper) {
|
||||
if (ctx is Activity) { activity = ctx; break }
|
||||
ctx = ctx.baseContext
|
||||
}
|
||||
|
||||
val stillInPip = activity?.isInPictureInPictureMode == true
|
||||
|
||||
if (!stillInPip) {
|
||||
falseReadCount++
|
||||
if (falseReadCount >= 3) {
|
||||
isInPiPMode = false
|
||||
delegate?.onPictureInPictureModeChanged(false)
|
||||
return
|
||||
}
|
||||
pipHandler.postDelayed(this, 500)
|
||||
return
|
||||
}
|
||||
|
||||
falseReadCount = 0
|
||||
pipHandler.postDelayed(this, 1000)
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// MARK: - PiP Remote Actions
|
||||
|
||||
private fun ensurePiPReceiverRegistered() {
|
||||
if (pipBroadcastReceiver != null) return
|
||||
|
||||
pipBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
ACTION_PIP_PLAY_PAUSE -> {
|
||||
if (playbackRate > 0) delegate?.onPause() else delegate?.onPlay()
|
||||
}
|
||||
ACTION_PIP_SKIP_FORWARD -> delegate?.onSeekBy(10.0)
|
||||
ACTION_PIP_SKIP_BACKWARD -> delegate?.onSeekBy(-10.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val filter = IntentFilter().apply {
|
||||
addAction(ACTION_PIP_PLAY_PAUSE)
|
||||
addAction(ACTION_PIP_SKIP_FORWARD)
|
||||
addAction(ACTION_PIP_SKIP_BACKWARD)
|
||||
}
|
||||
val registerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Context.RECEIVER_EXPORTED
|
||||
} else {
|
||||
0
|
||||
}
|
||||
context.applicationContext.registerReceiver(pipBroadcastReceiver, filter, registerFlags)
|
||||
}
|
||||
|
||||
private fun unregisterPiPBroadcastReceiver() {
|
||||
pipBroadcastReceiver?.let {
|
||||
try {
|
||||
context.applicationContext.unregisterReceiver(it)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
pipBroadcastReceiver = null
|
||||
}
|
||||
|
||||
private fun buildPiPActions(): List<RemoteAction> {
|
||||
val isPlaying = playbackRate > 0
|
||||
|
||||
return listOf(
|
||||
RemoteAction(
|
||||
Icon.createWithResource(context, android.R.drawable.ic_media_rew),
|
||||
"Rewind", "Skip backward 10 seconds",
|
||||
createPiPPendingIntent(ACTION_PIP_SKIP_BACKWARD)
|
||||
),
|
||||
RemoteAction(
|
||||
Icon.createWithResource(
|
||||
context,
|
||||
if (isPlaying) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play
|
||||
),
|
||||
if (isPlaying) "Pause" else "Play",
|
||||
if (isPlaying) "Pause playback" else "Resume playback",
|
||||
createPiPPendingIntent(ACTION_PIP_PLAY_PAUSE)
|
||||
),
|
||||
RemoteAction(
|
||||
Icon.createWithResource(context, android.R.drawable.ic_media_ff),
|
||||
"Fast Forward", "Skip forward 10 seconds",
|
||||
createPiPPendingIntent(ACTION_PIP_SKIP_FORWARD)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createPiPPendingIntent(action: String): android.app.PendingIntent {
|
||||
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
return android.app.PendingIntent.getBroadcast(
|
||||
context.applicationContext, 0, Intent(action), flags
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'MpvPlayer'
|
||||
s.version = '1.0.0'
|
||||
s.summary = 'MPV-based video player for Streamyfin (Expo module)'
|
||||
s.author = 'Streamyfin'
|
||||
s.homepage = 'https://github.com/streamyfin/streamyfin'
|
||||
s.platforms = { :ios => '15.1', :tvos => '15.1' }
|
||||
s.source = { git: '' }
|
||||
s.name = 'MpvPlayer'
|
||||
s.version = '1.0.0'
|
||||
s.summary = 'MPVKit for Expo'
|
||||
s.description = 'MPVKit for Expo'
|
||||
s.author = 'mpvkit'
|
||||
s.homepage = 'https://github.com/mpvkit/MPVKit'
|
||||
s.platforms = {
|
||||
:ios => '15.1',
|
||||
:tvos => '15.1'
|
||||
}
|
||||
s.source = { git: 'https://github.com/mpvkit/MPVKit.git' }
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
s.dependency 'MPVKit'
|
||||
s.dependency 'MPVKit-GPL'
|
||||
|
||||
# Swift/Objective-C compatibility
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||
'VALID_ARCHS' => 'arm64',
|
||||
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
|
||||
'DEBUG_INFORMATION_FORMAT' => 'dwarf',
|
||||
'STRIP_INSTALLED_PRODUCT' => 'YES',
|
||||
'DEPLOYMENT_POSTPROCESSING' => 'YES',
|
||||
}
|
||||
|
||||
s.user_target_xcconfig = {
|
||||
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386'
|
||||
}
|
||||
|
||||
s.source_files = "*.{h,m,mm,swift,hpp,cpp}"
|
||||
|
||||
@@ -25,10 +25,6 @@ export type OnErrorEventPayload = {
|
||||
|
||||
export type OnTracksReadyEventPayload = Record<string, never>;
|
||||
|
||||
export type OnPictureInPictureChangePayload = {
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
export type NowPlayingMetadata = {
|
||||
title?: string;
|
||||
artist?: string;
|
||||
@@ -81,9 +77,6 @@ export type MpvPlayerViewProps = {
|
||||
onProgress?: (event: { nativeEvent: OnProgressEventPayload }) => void;
|
||||
onError?: (event: { nativeEvent: OnErrorEventPayload }) => void;
|
||||
onTracksReady?: (event: { nativeEvent: OnTracksReadyEventPayload }) => void;
|
||||
onPictureInPictureChange?: (event: {
|
||||
nativeEvent: OnPictureInPictureChangePayload;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export interface MpvPlayerViewRef {
|
||||
|
||||
@@ -7,8 +7,6 @@ import { MpvPlayerViewProps, MpvPlayerViewRef } from "./MpvPlayer.types";
|
||||
const NativeView: React.ComponentType<MpvPlayerViewProps & { ref?: any }> =
|
||||
requireNativeView("MpvPlayer");
|
||||
|
||||
const PIP_LOG = "[PiP] MpvPlayerView.tsx:";
|
||||
|
||||
export default React.forwardRef<MpvPlayerViewRef, MpvPlayerViewProps>(
|
||||
function MpvPlayerView(props, ref) {
|
||||
const nativeRef = useRef<any>(null);
|
||||
@@ -42,24 +40,16 @@ export default React.forwardRef<MpvPlayerViewRef, MpvPlayerViewProps>(
|
||||
return await nativeRef.current?.getDuration();
|
||||
},
|
||||
startPictureInPicture: async () => {
|
||||
console.log(PIP_LOG, "startPictureInPicture → native");
|
||||
await nativeRef.current?.startPictureInPicture();
|
||||
console.log(PIP_LOG, "startPictureInPicture ← native returned");
|
||||
},
|
||||
stopPictureInPicture: async () => {
|
||||
console.log(PIP_LOG, "stopPictureInPicture → native");
|
||||
await nativeRef.current?.stopPictureInPicture();
|
||||
console.log(PIP_LOG, "stopPictureInPicture ← native returned");
|
||||
},
|
||||
isPictureInPictureSupported: async () => {
|
||||
const result = await nativeRef.current?.isPictureInPictureSupported();
|
||||
console.log(PIP_LOG, "isPictureInPictureSupported =", result);
|
||||
return result;
|
||||
return await nativeRef.current?.isPictureInPictureSupported();
|
||||
},
|
||||
isPictureInPictureActive: async () => {
|
||||
const result = await nativeRef.current?.isPictureInPictureActive();
|
||||
console.log(PIP_LOG, "isPictureInPictureActive =", result);
|
||||
return result;
|
||||
return await nativeRef.current?.isPictureInPictureActive();
|
||||
},
|
||||
getSubtitleTracks: async () => {
|
||||
return await nativeRef.current?.getSubtitleTracks();
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-volume-manager": "^2.0.8",
|
||||
"react-native-web": "^0.21.0",
|
||||
"react-native-worklets": "0.8.3",
|
||||
"react-native-worklets": "0.9.1",
|
||||
"sonner-native": "0.21.2",
|
||||
"tailwindcss": "3.3.2",
|
||||
"use-debounce": "^10.0.4",
|
||||
@@ -162,5 +162,10 @@
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"unrs-resolver"
|
||||
]
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"react-native-udp@4.1.7": "bun-patches/react-native-udp@4.1.7.patch",
|
||||
"react-native-bottom-tabs@1.2.0": "bun-patches/react-native-bottom-tabs@1.2.0.patch",
|
||||
"react-native-ios-utilities@5.2.0": "bun-patches/react-native-ios-utilities@5.2.0.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,28 +39,6 @@ function buildPatch() {
|
||||
" end",
|
||||
" end",
|
||||
"",
|
||||
" # iOS 26 / Xcode 26: the APP target itself compiles ExpoModulesProvider.swift,",
|
||||
" # which imports SwiftUI-based modules (ExpoUI, ExpoGlassEffect, GlassPoster, ExpoBlur, …).",
|
||||
" # That emits a `-framework SwiftUICore` autolink into the app executable's OWN object",
|
||||
" # files, so the pods-only flag above is not enough — the app's link still fails with",
|
||||
" # `cannot link directly with 'SwiftUICore'`. Drop the autolink on the user app target",
|
||||
" # too. Phone-only — tvOS has no SwiftUICore split and must stay untouched.",
|
||||
" if ENV['EXPO_TV'] != '1'",
|
||||
" installer.aggregate_targets.each do |agg|",
|
||||
" next unless agg.user_project",
|
||||
" agg.user_project.native_targets.each do |target|",
|
||||
" target.build_configurations.each do |cfg|",
|
||||
" existing = cfg.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)'",
|
||||
" existing = existing.join(' ') if existing.is_a?(Array)",
|
||||
" unless existing.include?('-disable-autolink-framework -Xfrontend SwiftUICore')",
|
||||
" cfg.build_settings['OTHER_SWIFT_FLAGS'] = existing + ' -Xfrontend -disable-autolink-framework -Xfrontend SwiftUICore'",
|
||||
" end",
|
||||
" end",
|
||||
" end",
|
||||
" agg.user_project.save",
|
||||
" end",
|
||||
" end",
|
||||
"",
|
||||
" # Safely patch RCTThirdPartyComponentsProvider.mm to avoid startup crash on unlinked Fabric components",
|
||||
' filepath = "#{installer.sandbox.root}/../build/generated/ios/ReactCodegen/RCTThirdPartyComponentsProvider.mm"',
|
||||
" if File.exist?(filepath)",
|
||||
|
||||
@@ -53,7 +53,7 @@ const initialApi = (() => {
|
||||
const id = getOrSetDeviceId();
|
||||
const deviceName = getDeviceNameSync();
|
||||
const jellyfinInstance = new Jellyfin({
|
||||
clientInfo: { name: "Streamyfin", version: "0.54.1" },
|
||||
clientInfo: { name: "Streamyfin", version: "0.54.0" },
|
||||
deviceInfo: {
|
||||
name: deviceName,
|
||||
id,
|
||||
@@ -128,7 +128,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
const id = getOrSetDeviceId();
|
||||
const deviceName = getDeviceNameSync();
|
||||
return new Jellyfin({
|
||||
clientInfo: { name: "Streamyfin", version: "0.54.1" },
|
||||
clientInfo: { name: "Streamyfin", version: "0.54.0" },
|
||||
deviceInfo: {
|
||||
name: deviceName,
|
||||
id,
|
||||
@@ -162,7 +162,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
return {
|
||||
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
||||
Platform.OS === "android" ? "Android" : "iOS"
|
||||
}, DeviceId="${deviceId}", Version="0.54.1"`,
|
||||
}, DeviceId="${deviceId}", Version="0.54.0"`,
|
||||
};
|
||||
}, [deviceId]);
|
||||
|
||||
|
||||
@@ -29,10 +29,6 @@
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
||||
Reference in New Issue
Block a user