mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
chromecast test
This commit is contained in:
parent
94e165f0b0
commit
f126f81c13
21 changed files with 1105 additions and 19 deletions
|
|
@ -1,4 +1,9 @@
|
|||
apply plugin: "com.android.application"
|
||||
// @generated begin safeExtGet - expo prebuild (DO NOT MODIFY) sync-be4acad6508a6820102c74ab393bd7ab1093e6c0
|
||||
def safeExtGet(prop, fallback) {
|
||||
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||
}
|
||||
// @generated end safeExtGet
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
|
||||
|
|
@ -185,6 +190,9 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
// @generated begin react-native-google-cast-dependencies - expo prebuild (DO NOT MODIFY) sync-3822a3c86222e7aca74039b551612aab7e75365d
|
||||
implementation "com.google.android.gms:play-services-cast-framework:${safeExtGet('castFrameworkVersion', '+')}"
|
||||
// @generated end react-native-google-cast-dependencies
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
|
|
@ -214,4 +222,7 @@ dependencies {
|
|||
|
||||
// Include only FFmpeg decoder AAR to avoid duplicates with Maven Media3
|
||||
implementation files("libs/lib-decoder-ffmpeg-release.aar")
|
||||
|
||||
// Google Cast SDK
|
||||
implementation "com.google.android.gms:play-services-cast-framework:+"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
</intent>
|
||||
</queries>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false">
|
||||
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:value="com.reactnative.googlecast.GoogleCastOptionsProvider"/>
|
||||
<meta-data android:name="com.reactnative.googlecast.RECEIVER_APPLICATION_ID" android:value="CC1AD845"/>
|
||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="@string/expo_runtime_version"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ERROR_RECOVERY_ONLY"/>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnable
|
|||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
import expo.modules.ReactActivityDelegateWrapper
|
||||
import com.reactnative.googlecast.api.RNGCCastContext
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
@ -17,6 +18,12 @@ class MainActivity : ReactActivity() {
|
|||
// This is required for expo-splash-screen.
|
||||
setTheme(R.style.AppTheme);
|
||||
super.onCreate(null)
|
||||
// @generated begin react-native-google-cast-onCreate - expo prebuild (DO NOT MODIFY) sync-489050f2bf9933a98bbd9d93137016ae14c22faa
|
||||
RNGCCastContext.getSharedInstance(this)
|
||||
// @generated end react-native-google-cast-onCreate
|
||||
|
||||
// Initialize Google Cast context
|
||||
RNGCCastContext.getSharedInstance(this)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
// @generated begin react-native-google-cast-version-import - expo prebuild (DO NOT MODIFY) sync-751dea5919495636a44001495c681e3442af2777
|
||||
ext {
|
||||
castFrameworkVersion = "+"
|
||||
}
|
||||
// @generated end react-native-google-cast-version-import
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
|
|
|||
8
app.json
8
app.json
|
|
@ -87,6 +87,14 @@
|
|||
"supportsBackgroundPlayback": true
|
||||
}
|
||||
],
|
||||
[
|
||||
"react-native-google-cast",
|
||||
{
|
||||
"androidReceiverAppId": "CC1AD845",
|
||||
"iosReceiverAppId": "CC1AD845",
|
||||
"iosStartDiscoveryAfterFirstTapOnCastButton": true
|
||||
}
|
||||
],
|
||||
"react-native-bottom-tabs"
|
||||
],
|
||||
"updates": {
|
||||
|
|
|
|||
242
cast.md
Normal file
242
cast.md
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
nstallation
|
||||
|
||||
$ npm install react-native-google-cast --save
|
||||
|
||||
or
|
||||
|
||||
$ yarn add react-native-google-cast
|
||||
|
||||
Expo
|
||||
|
||||
Since Expo SDK 42, you can use this library in a custom-built Expo app. There is a config plugin included to auto-configure react-native-google-cast when the native code is generated (npx expo prebuild).
|
||||
|
||||
This package cannot be used in Expo Go because it requires custom native code. You need to build a standalone app instead.
|
||||
Add the config plugin to the plugins array of your app.json or app.config.js/ts:
|
||||
|
||||
{
|
||||
"expo": {
|
||||
"plugins": ["react-native-google-cast"]
|
||||
}
|
||||
}
|
||||
Next, rebuild your app as described in the "Adding custom native code" guide.
|
||||
|
||||
Then ignore the rest of this page and continue to Setup.
|
||||
|
||||
iOS
|
||||
|
||||
Thanks to autolinking, the package and its Google Cast SDK dependency are automatically installed when you run pod install.
|
||||
|
||||
The latest Google Cast SDK (currently 4.8.3) requires iOS 14 or newer. However, React Native 0.76+ already requires iOS 15.1 or higher. If you need to support older iOS versions, use an older version of the library but note that some features might not be available.
|
||||
Before v4.8.1, Google Cast used to publish different variants of the SDK based on whether they included Guest Mode support. That feature has been removed in the latest versions so now there's only a single SDK variant.
|
||||
Android
|
||||
|
||||
The react-native-google-cast library is autolinked but we need to add the Google Cast SDK dependency to android/app/build.gradle:
|
||||
|
||||
dependencies {
|
||||
// ...
|
||||
implementation "com.google.android.gms:play-services-cast-framework:+"
|
||||
}
|
||||
By default, the latest version (+) of the Cast SDK is used.
|
||||
|
||||
To use a specific version, add castFrameworkVersion in the root android/build.gradle:
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "34.0.0"
|
||||
minSdkVersion = 22
|
||||
compileSdkVersion = 34
|
||||
targetSdkVersion = 34
|
||||
castFrameworkVersion = "22.1.0" // <-- Cast SDK version
|
||||
}
|
||||
}
|
||||
and update android/app/build.gradle:
|
||||
|
||||
dependencies {
|
||||
// ...
|
||||
implementation "com.google.android.gms:play-services-cast-framework:${safeExtGet('castFrameworkVersion', '+')}"
|
||||
}
|
||||
|
||||
def safeExtGet(prop, fallback) {
|
||||
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||
}
|
||||
etup
|
||||
|
||||
Expo
|
||||
|
||||
If you're using Expo, you can configure your build using the included plugin (see below) and then continue to Usage.
|
||||
|
||||
The plugin provides props for extra customization. Every time you change the props or plugins, you'll need to rebuild (and prebuild) the native app. If no extra properties are added, defaults will be used.
|
||||
|
||||
receiverAppId (string): custom receiver app id. Default CC1AD845 (default receiver provided by Google). Sets both iosReceiverAppId and androidReceiverAppId.
|
||||
expandedController (boolean): Whether to use the default expanded controller. Default true.
|
||||
androidReceiverAppId (string): custom receiver app id. Default CC1AD845.
|
||||
androidPlayServicesCastFrameworkVersion (string): Version for the Android Cast SDK. Default + (latest).
|
||||
iosReceiverAppId (string): custom receiver app id. Default CC1AD845.
|
||||
iosDisableDiscoveryAutostart (boolean): Whether the discovery of Cast devices should not start automatically at context initialization time. Default false. if set to true, you'll need to start it later by calling DiscoveryManager.startDiscovery.
|
||||
iosStartDiscoveryAfterFirstTapOnCastButton (boolean): Whether cast devices discovery start only after a user taps on the Cast button for the first time. Default true. If set to false, discovery will start as soon as the SDK is initialized. Note that this will ask the user for network permissions immediately when the app is opened for the first time.
|
||||
iosSuspendSessionsWhenBackgrounded (boolean): Whether sessions should be suspended when the sender application goes into the background (and resumed when it returns to the foreground). Default true. It is appropriate to set this to false in applications that are able to maintain network connections indefinitely while in the background.
|
||||
{
|
||||
"expo": {
|
||||
"plugins": [
|
||||
[
|
||||
"react-native-google-cast",
|
||||
{
|
||||
"receiverAppId": "...",
|
||||
"iosStartDiscoveryAfterFirstTapOnCastButton": false
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
iOS
|
||||
|
||||
In AppDelegate.swift (or AppDelegate.mm) add
|
||||
|
||||
Swift
|
||||
Objective-C
|
||||
// 1.1. add import at the top
|
||||
import GoogleCast
|
||||
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||
) -> Bool {
|
||||
// ...
|
||||
// 1.2. add inside application:didFinishLaunchingWithOptions
|
||||
let receiverAppID = kGCKDefaultMediaReceiverApplicationID // or "ABCD1234"
|
||||
let criteria = GCKDiscoveryCriteria(applicationID: receiverAppID)
|
||||
let options = GCKCastOptions(discoveryCriteria: criteria)
|
||||
GCKCastContext.setSharedInstanceWith(options)
|
||||
// ...
|
||||
}
|
||||
// ...
|
||||
}
|
||||
If using a custom web receiver, replace kGCKDefaultMediaReceiverApplicationID with your receiver app id.
|
||||
|
||||
You need to add local network permissions to Info.plist:
|
||||
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_googlecast._tcp</string>
|
||||
<string>_CC1AD845._googlecast._tcp</string>
|
||||
</array>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>${PRODUCT_NAME} uses the local network to discover Cast-enabled devices on your WiFi network.</string>
|
||||
If using a custom receiver, make sure to replace CC1AD845 with your custom receiver app id.
|
||||
|
||||
You may also customize the local network usage description (See #355).
|
||||
|
||||
Furthermore, a dialog asking the user for the local network permission will now be displayed immediately when the app is opened.
|
||||
|
||||
(optional) By default, Cast device discovery is initiated when the user taps the Cast button. If it's the first time, the local network access interstitial will appear, followed by the iOS Local Network Access permissions dialog.
|
||||
|
||||
You may customize this behavior in AppDelegate.m by either:
|
||||
|
||||
setting disableDiscoveryAutostart to true:
|
||||
|
||||
options.disableDiscoveryAutostart = true
|
||||
Note: If you disable discovery autostart, you'll need to start it later by calling startDiscovery.
|
||||
or setting startDiscoveryAfterFirstTapOnCastButton to false. In this case, discovery will start as soon as the SDK is initialized.
|
||||
|
||||
options.startDiscoveryAfterFirstTapOnCastButton = false
|
||||
Android
|
||||
|
||||
Add to AndroidManifest.xml (in android/app/src/main):
|
||||
|
||||
<application ...>
|
||||
...
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
android:value="com.reactnative.googlecast.GoogleCastOptionsProvider" />
|
||||
</application>
|
||||
Additionally, if you're using a custom receiver, also add (replace ABCD1234 with your receiver app id):
|
||||
|
||||
<meta-data
|
||||
android:name="com.reactnative.googlecast.RECEIVER_APPLICATION_ID"
|
||||
android:value="ABCD1234" />
|
||||
Alternatively, you may provide your own OptionsProvider class. See GoogleCastOptionsProvider.java for inspiration.
|
||||
|
||||
In your MainActivity.kt or MainActivity.java, initialize CastContext by overriding the onCreate method.
|
||||
|
||||
Kotlin
|
||||
Java
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.Nullable
|
||||
import com.reactnative.googlecast.api.RNGCCastContext
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
// ...
|
||||
|
||||
override fun onCreate(@Nullable savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// lazy load Google Cast context (if supported on this device)
|
||||
RNGCCastContext.getSharedInstance(this)
|
||||
}
|
||||
}
|
||||
This works if you're extending ReactActivity (or NavigationActivity if you're using react-native-navigation). If you're extending a different activity, make sure it is a descendant of androidx.appcompat.app.AppCompatActivity.
|
||||
|
||||
The Cast framework requires Google Play Services to be available on your device. If your device doesn't have them by default, you can install them either from the Play Store, from OpenGApps or follow tutorials online.
|
||||
|
||||
Usage
|
||||
|
||||
First, render Cast button which handles session and enables users to connect to Cast devices. You can then get the current connected client, and call loadMedia as needed.
|
||||
|
||||
import React from 'react'
|
||||
import { CastButton, useRemoteMediaClient } from 'react-native-google-cast'
|
||||
|
||||
function MyComponent() {
|
||||
// This will automatically rerender when client is connected to a device
|
||||
// (after pressing the button that's rendered below)
|
||||
const client = useRemoteMediaClient()
|
||||
|
||||
if (client) {
|
||||
// Send the media to your Cast device as soon as we connect to a device
|
||||
// (though you'll probably want to call this later once user clicks on a video or something)
|
||||
client.loadMedia({
|
||||
mediaInfo: {
|
||||
contentUrl:
|
||||
'https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/mp4/BigBuckBunny.mp4',
|
||||
contentType: 'video/mp4',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// This will render native Cast button.
|
||||
// When a user presses it, a Cast dialog will prompt them to select a Cast device to connect to.
|
||||
return <CastButton style={{ width: 24, height: 24, tintColor: 'black' }} />
|
||||
}
|
||||
You can provide many different attributes, such as in this example:
|
||||
|
||||
client.loadMedia({
|
||||
mediaInfo: {
|
||||
contentUrl:
|
||||
'https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/mp4/BigBuckBunny.mp4',
|
||||
contentType: 'video/mp4',
|
||||
metadata: {
|
||||
images: [
|
||||
{
|
||||
url: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/images/480x270/BigBuckBunny.jpg',
|
||||
},
|
||||
],
|
||||
title: 'Big Buck Bunny',
|
||||
subtitle:
|
||||
'A large and lovable rabbit deals with three tiny bullies, led by a flying squirrel, who are determined to squelch his happiness.',
|
||||
studio: 'Blender Foundation',
|
||||
type: 'movie',
|
||||
},
|
||||
streamDuration: 596, // seconds
|
||||
},
|
||||
startTime: 10, // seconds
|
||||
})
|
||||
Please see the MediaLoadRequest documentation for available options.
|
||||
|
||||
(Android) Handle missing Google Play Services
|
||||
|
||||
On Android, you can use CastContext.getPlayServicesState() to check if Google Play Services are installed on the device. You can then call CastContext.showPlayServicesErrorDialog to inform the user and prompt them to install.
|
||||
|
||||
CastContext.getPlayServicesState().then((state) => {
|
||||
if (state && state !== PlayServicesState.SUCCESS)
|
||||
CastContext.showPlayServicesErrorDialog(state)
|
||||
})
|
||||
|
|
@ -297,6 +297,7 @@
|
|||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/KSPlayer/KSPlayer_KSPlayer.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
|
||||
|
|
@ -325,6 +326,14 @@
|
|||
"${PODS_CONFIGURATION_BUILD_DIR}/Sentry/Sentry.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.bundle",
|
||||
"${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.4_static_xcframework/GoogleCast.xcframework/ios-arm64/GoogleCast.framework/GoogleCastCoreResources.bundle",
|
||||
"${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.4_static_xcframework/GoogleCast.xcframework/ios-arm64/GoogleCast.framework/GoogleCastUIResources.bundle",
|
||||
"${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.4_static_xcframework/GoogleCast.xcframework/ios-arm64/GoogleCast.framework/GoogleCastOptionalUIResources.bundle",
|
||||
"${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.4_static_xcframework/GoogleCast.xcframework/ios-arm64/GoogleCast.framework/MaterialDialogs.bundle",
|
||||
"${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.4_static_xcframework/GoogleCast.xcframework/ios-arm64/GoogleCast.framework/GoogleSansBold.bundle",
|
||||
"${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.4_static_xcframework/GoogleCast.xcframework/ios-arm64/GoogleCast.framework/GoogleSansMedium.bundle",
|
||||
"${PODS_ROOT}/google-cast-sdk/GoogleCastSDK-ios-4.8.4_static_xcframework/GoogleCast.xcframework/ios-arm64/GoogleCast.framework/GoogleSansRegular.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/google-cast-sdk/GoogleCast.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios/LottiePrivacyInfo.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/lottie-react-native/Lottie_React_Native_Privacy.bundle",
|
||||
);
|
||||
|
|
@ -340,6 +349,7 @@
|
|||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoLocalization_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/KSPlayer_KSPlayer.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
|
||||
|
|
@ -368,6 +378,14 @@
|
|||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Sentry.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevLauncher.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevMenu.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleCastCoreResources.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleCastUIResources.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleCastOptionalUIResources.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialDialogs.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSansBold.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSansMedium.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSansRegular.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleCast.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/LottiePrivacyInfo.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Lottie_React_Native_Privacy.bundle",
|
||||
);
|
||||
|
|
@ -461,7 +479,7 @@
|
|||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
|
||||
PRODUCT_NAME = "Nuvio";
|
||||
PRODUCT_NAME = Nuvio;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -492,8 +510,8 @@
|
|||
"-lc++",
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.nuvio.app";
|
||||
PRODUCT_NAME = "Nuvio";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
|
||||
PRODUCT_NAME = Nuvio;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import Expo
|
||||
// @generated begin react-native-google-cast-import - expo prebuild (DO NOT MODIFY) sync-4cd300bca26a1d1fcc83f4baf37b0e62afcc1867
|
||||
#if canImport(GoogleCast) && os(iOS)
|
||||
import GoogleCast
|
||||
#endif
|
||||
// @generated end react-native-google-cast-import
|
||||
import React
|
||||
import ReactAppDependencyProvider
|
||||
|
||||
|
|
@ -13,6 +18,18 @@ public class AppDelegate: ExpoAppDelegate {
|
|||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||
) -> Bool {
|
||||
// @generated begin react-native-google-cast-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-3f476aa248b3451597781fe1ea72c7d4127ed7f9
|
||||
#if canImport(GoogleCast) && os(iOS)
|
||||
let receiverAppID = "CC1AD845"
|
||||
let criteria = GCKDiscoveryCriteria(applicationID: receiverAppID)
|
||||
let options = GCKCastOptions(discoveryCriteria: criteria)
|
||||
options.disableDiscoveryAutostart = false
|
||||
options.startDiscoveryAfterFirstTapOnCastButton = true
|
||||
options.suspendSessionsWhenBackgrounded = true
|
||||
GCKCastContext.setSharedInstanceWith(options)
|
||||
GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true
|
||||
#endif
|
||||
// @generated end react-native-google-cast-didFinishLaunchingWithOptions
|
||||
let delegate = ReactNativeDelegate()
|
||||
let factory = ExpoReactNativeFactory(delegate: delegate)
|
||||
delegate.dependencyProvider = RCTAppDependencyProvider()
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@
|
|||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_http._tcp</string>
|
||||
<string>_googlecast._tcp</string>
|
||||
<string>_CC1AD845._googlecast._tcp</string>
|
||||
</array>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Allow $(PRODUCT_NAME) to access your local network</string>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
<string>C56D.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
|
|
|
|||
|
|
@ -328,6 +328,7 @@ PODS:
|
|||
- FFmpegKit/FFmpegKit (= 6.1.0)
|
||||
- FFmpegKit/FFmpegKit (6.1.0):
|
||||
- Libass
|
||||
- google-cast-sdk (4.8.4)
|
||||
- hermes-engine (0.81.4):
|
||||
- hermes-engine/Pre-built (= 0.81.4)
|
||||
- hermes-engine/Pre-built (0.81.4)
|
||||
|
|
@ -405,6 +406,7 @@ PODS:
|
|||
- ReactNativeDependencies
|
||||
- Yoga
|
||||
- MobileVLCKit (3.6.1b1)
|
||||
- PromisesObjC (2.4.0)
|
||||
- RCTDeprecation (0.81.4)
|
||||
- RCTRequired (0.81.4)
|
||||
- RCTTypeSafety (0.81.4):
|
||||
|
|
@ -1758,6 +1760,10 @@ PODS:
|
|||
- React
|
||||
- react-native-get-random-values (1.11.0):
|
||||
- React-Core
|
||||
- react-native-google-cast (4.9.1):
|
||||
- google-cast-sdk
|
||||
- PromisesObjC
|
||||
- React
|
||||
- react-native-netinfo (11.4.1):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (5.6.1):
|
||||
|
|
@ -2763,6 +2769,7 @@ DEPENDENCIES:
|
|||
- react-native-bottom-tabs (from `../node_modules/react-native-bottom-tabs`)
|
||||
- "react-native-device-brightness (from `../node_modules/@adrianso/react-native-device-brightness`)"
|
||||
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
|
||||
- react-native-google-cast (from `../node_modules/react-native-google-cast`)
|
||||
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
|
||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
||||
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
|
||||
|
|
@ -2812,11 +2819,13 @@ DEPENDENCIES:
|
|||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- google-cast-sdk
|
||||
- libavif
|
||||
- libdav1d
|
||||
- libwebp
|
||||
- lottie-ios
|
||||
- MobileVLCKit
|
||||
- PromisesObjC
|
||||
- ReachabilitySwift
|
||||
- SDWebImage
|
||||
- SDWebImageAVIFCoder
|
||||
|
|
@ -2990,6 +2999,8 @@ EXTERNAL SOURCES:
|
|||
:path: "../node_modules/@adrianso/react-native-device-brightness"
|
||||
react-native-get-random-values:
|
||||
:path: "../node_modules/react-native-get-random-values"
|
||||
react-native-google-cast:
|
||||
:path: "../node_modules/react-native-google-cast"
|
||||
react-native-netinfo:
|
||||
:path: "../node_modules/@react-native-community/netinfo"
|
||||
react-native-safe-area-context:
|
||||
|
|
@ -3136,6 +3147,7 @@ SPEC CHECKSUMS:
|
|||
EXUpdatesInterface: 5adf50cb41e079c861da6d9b4b954c3db9a50734
|
||||
FBLazyVector: 9e0cd874afd81d9a4d36679daca991b58b260d42
|
||||
FFmpegKit: 3885085fbbc320745838ee4c8a1f9c5e5953dab2
|
||||
google-cast-sdk: 32f65af50d164e3c475e79ad123db3cc26fbcd37
|
||||
hermes-engine: 35c763d57c9832d0eef764316ca1c4d043581394
|
||||
ImageColors: 51cd79f7a9d2524b7a681c660b0a50574085563b
|
||||
KSPlayer: f163ac6195f240b6fa5b8225aeb39ec811a70c62
|
||||
|
|
@ -3146,6 +3158,7 @@ SPEC CHECKSUMS:
|
|||
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
|
||||
lottie-react-native: cbe3d931a7c24f7891a8e8032c2bb9b2373c4b9c
|
||||
MobileVLCKit: 2d9c7c373393ae43086aeeff890bf0b1afc15c5c
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
RCTDeprecation: 7487d6dda857ccd4cb3dd6ecfccdc3170e85dcbc
|
||||
RCTRequired: 54128b7df8be566881d48c7234724a78cb9b6157
|
||||
RCTTypeSafety: d2b07797a79e45d7b19e1cd2f53c79ab419fe217
|
||||
|
|
@ -3184,6 +3197,7 @@ SPEC CHECKSUMS:
|
|||
react-native-bottom-tabs: e37c9d1565b1ee48c4c0e4b4fa4b804775f82dfa
|
||||
react-native-device-brightness: 1a997350d060c3df9f303b1df84a4f7c5cbeb924
|
||||
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
|
||||
react-native-google-cast: 7be68a5d0b7eeb95a5924c3ecef8d319ef6c0a44
|
||||
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
|
||||
react-native-safe-area-context: 42a1b4f8774b577d03b53de7326e3d5757fe9513
|
||||
react-native-slider: 8c562583722c396a3682f451f0b6e68e351ec3b9
|
||||
|
|
|
|||
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -71,6 +71,7 @@
|
|||
"react-native-bottom-tabs": "^0.12.2",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-get-random-values": "^1.11.0",
|
||||
"react-native-google-cast": "^4.9.1",
|
||||
"react-native-image-colors": "^2.5.0",
|
||||
"react-native-immersive-mode": "^2.0.2",
|
||||
"react-native-markdown-display": "^7.0.2",
|
||||
|
|
@ -10801,6 +10802,16 @@
|
|||
"react-native": ">=0.56"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-google-cast": {
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-google-cast/-/react-native-google-cast-4.9.1.tgz",
|
||||
"integrity": "sha512-/HvIKAaWHtG6aTNCxrNrqA2ftWGkfH0M/2iN+28pdGUXpKmueb33mgL1m8D4zzwEODQMcmpfoCsym1IwDvugBQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-image-colors": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-image-colors/-/react-native-image-colors-2.5.0.tgz",
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@
|
|||
"react-native-bottom-tabs": "^0.12.2",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-get-random-values": "^1.11.0",
|
||||
"react-native-google-cast": "^4.9.1",
|
||||
"react-native-image-colors": "^2.5.0",
|
||||
"react-native-immersive-mode": "^2.0.2",
|
||||
"react-native-markdown-display": "^7.0.2",
|
||||
|
|
|
|||
|
|
@ -96,8 +96,8 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
|||
// Robustly determine if the item is in the library (saved)
|
||||
const isSaved = typeof isSavedProp === 'boolean' ? isSavedProp : !!item.inLibrary;
|
||||
const isWatched = !!isWatchedProp;
|
||||
const inTraktWatchlist = isAuthenticated && isInWatchlist(item.id, item.type);
|
||||
const inTraktCollection = isAuthenticated && isInCollection(item.id, item.type);
|
||||
const inTraktWatchlist = isAuthenticated && isInWatchlist(item.id, item.type as 'movie' | 'show');
|
||||
const inTraktCollection = isAuthenticated && isInCollection(item.id, item.type as 'movie' | 'show');
|
||||
|
||||
let menuOptions = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
|||
import { useMetadata } from '../../hooks/useMetadata';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
import { usePlayerGestureControls } from '../../hooks/usePlayerGestureControls';
|
||||
import { useChromecast } from '../../hooks/useChromecast';
|
||||
|
||||
import {
|
||||
DEFAULT_SUBTITLE_SIZE,
|
||||
|
|
@ -210,6 +211,11 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const initialSeekTargetRef = useRef<number | null>(null);
|
||||
const initialSeekVerifiedRef = useRef(false);
|
||||
const isSourceSeekableRef = useRef<boolean | null>(null);
|
||||
|
||||
// Cast-related state
|
||||
const [isCasting, setIsCasting] = useState(false);
|
||||
const [wasPlayingBeforeCast, setWasPlayingBeforeCast] = useState(false);
|
||||
const [castPositionBeforeConnect, setCastPositionBeforeConnect] = useState(0);
|
||||
const fadeAnim = useRef(new Animated.Value(1)).current;
|
||||
const [isOpeningAnimationComplete, setIsOpeningAnimationComplete] = useState(false);
|
||||
const openingFadeAnim = useRef(new Animated.Value(0)).current;
|
||||
|
|
@ -485,6 +491,125 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const [currentStreamProvider, setCurrentStreamProvider] = useState<string | undefined>(streamProvider);
|
||||
const [currentStreamName, setCurrentStreamName] = useState<string | undefined>(streamName);
|
||||
const isMounted = useRef(true);
|
||||
|
||||
// Initialize Chromecast hook (after all required state variables are declared)
|
||||
const chromecast = useChromecast();
|
||||
|
||||
// Cast handler functions
|
||||
const handleCastPress = useCallback(() => {
|
||||
if (!chromecast.isCastAvailable) {
|
||||
logger.warn('[AndroidVideoPlayer] Chromecast not available');
|
||||
return;
|
||||
}
|
||||
|
||||
if (chromecast.isCastConnected) {
|
||||
// Already connected, show picker to disconnect or switch devices
|
||||
chromecast.showCastPicker();
|
||||
} else {
|
||||
// Not connected, show picker to connect
|
||||
chromecast.showCastPicker();
|
||||
}
|
||||
}, [chromecast]);
|
||||
|
||||
const loadMediaToCast = useCallback(async () => {
|
||||
if (!chromecast.isCastConnected || !currentStreamUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Determine content type based on stream URL
|
||||
let contentType = 'video/mp4';
|
||||
if (currentStreamUrl.includes('.m3u8') || currentStreamUrl.includes('hls')) {
|
||||
contentType = 'application/x-mpegURL';
|
||||
} else if (currentStreamUrl.includes('.mpd') || currentStreamUrl.includes('dash')) {
|
||||
contentType = 'application/dash+xml';
|
||||
}
|
||||
|
||||
// Prepare metadata
|
||||
const metadata = {
|
||||
title: title,
|
||||
subtitle: episodeTitle || `${season ? `S${season}` : ''}${episode ? `E${episode}` : ''}`,
|
||||
images: backdrop ? [{ url: backdrop }] : undefined,
|
||||
studio: currentStreamProvider || streamProvider || 'Nuvio',
|
||||
type: type === 'series' ? 'series' as const : 'movie' as const,
|
||||
};
|
||||
|
||||
const mediaInfo = {
|
||||
contentUrl: currentStreamUrl,
|
||||
contentType,
|
||||
metadata,
|
||||
streamDuration: duration,
|
||||
startTime: currentTime,
|
||||
customData: {
|
||||
headers: headers,
|
||||
quality: currentQuality || quality,
|
||||
streamProvider: currentStreamProvider || streamProvider,
|
||||
}
|
||||
};
|
||||
|
||||
await chromecast.loadMedia(mediaInfo);
|
||||
|
||||
// Store state before casting
|
||||
setWasPlayingBeforeCast(!paused);
|
||||
setCastPositionBeforeConnect(currentTime);
|
||||
setIsCasting(true);
|
||||
|
||||
// Pause local playback
|
||||
setPaused(true);
|
||||
|
||||
logger.log('[AndroidVideoPlayer] Media loaded to Cast device');
|
||||
} catch (error) {
|
||||
logger.error('[AndroidVideoPlayer] Error loading media to Cast:', error);
|
||||
}
|
||||
}, [chromecast, currentStreamUrl, title, episodeTitle, season, episode, backdrop, currentStreamProvider, streamProvider, type, duration, currentTime, headers, currentQuality, quality, paused]);
|
||||
|
||||
// Handle Cast connection changes
|
||||
useEffect(() => {
|
||||
if (chromecast.isCastConnected && !isCasting) {
|
||||
// Just connected to Cast device, load media
|
||||
loadMediaToCast();
|
||||
} else if (!chromecast.isCastConnected && isCasting) {
|
||||
// Disconnected from Cast device, resume local playback
|
||||
setIsCasting(false);
|
||||
|
||||
// Resume local playback at Cast's last position
|
||||
if (chromecast.currentPosition > 0) {
|
||||
setCurrentTime(chromecast.currentPosition);
|
||||
// Seek to the position if video is loaded
|
||||
if (videoRef.current && duration > 0) {
|
||||
videoRef.current.seek(chromecast.currentPosition);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore playing state if it was playing before casting
|
||||
if (wasPlayingBeforeCast) {
|
||||
setPaused(false);
|
||||
}
|
||||
|
||||
logger.log('[AndroidVideoPlayer] Resumed local playback after Cast disconnect');
|
||||
}
|
||||
}, [chromecast.isCastConnected, isCasting, loadMediaToCast, chromecast.currentPosition, duration, wasPlayingBeforeCast]);
|
||||
|
||||
// Sync Cast playback state with local state
|
||||
useEffect(() => {
|
||||
if (isCasting && chromecast.isCastConnected) {
|
||||
// Update current time from Cast device
|
||||
if (chromecast.currentPosition !== currentTime) {
|
||||
setCurrentTime(chromecast.currentPosition);
|
||||
}
|
||||
|
||||
// Update duration from Cast device
|
||||
if (chromecast.duration !== duration && chromecast.duration > 0) {
|
||||
setDuration(chromecast.duration);
|
||||
}
|
||||
|
||||
// Sync paused state
|
||||
if (chromecast.isPlaying !== !paused) {
|
||||
setPaused(!chromecast.isPlaying);
|
||||
}
|
||||
}
|
||||
}, [isCasting, chromecast.isCastConnected, chromecast.currentPosition, chromecast.duration, chromecast.isPlaying, currentTime, duration, paused]);
|
||||
|
||||
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
|
||||
const [showErrorModal, setShowErrorModal] = useState(false);
|
||||
|
|
@ -1589,8 +1714,15 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
|
||||
const skip = useCallback((seconds: number) => {
|
||||
const newTime = Math.max(0, Math.min(currentTime + seconds, duration - END_EPSILON));
|
||||
seekToTime(newTime);
|
||||
}, [currentTime, duration]);
|
||||
|
||||
if (isCasting && chromecast.isCastConnected) {
|
||||
// Forward seek command to Cast device
|
||||
chromecast.seek(newTime);
|
||||
} else {
|
||||
// Local playback
|
||||
seekToTime(newTime);
|
||||
}
|
||||
}, [isCasting, chromecast, currentTime, duration]);
|
||||
|
||||
const cycleAspectRatio = useCallback(() => {
|
||||
// Prevent rapid successive resize operations
|
||||
|
|
@ -2480,13 +2612,23 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
};
|
||||
|
||||
const togglePlayback = useCallback(() => {
|
||||
const newPausedState = !paused;
|
||||
setPaused(newPausedState);
|
||||
if (isCasting && chromecast.isCastConnected) {
|
||||
// Forward play/pause command to Cast device
|
||||
if (paused) {
|
||||
chromecast.play();
|
||||
} else {
|
||||
chromecast.pause();
|
||||
}
|
||||
} else {
|
||||
// Local playback
|
||||
const newPausedState = !paused;
|
||||
setPaused(newPausedState);
|
||||
|
||||
if (duration > 0) {
|
||||
traktAutosync.handleProgressUpdate(currentTime, duration, true);
|
||||
if (duration > 0) {
|
||||
traktAutosync.handleProgressUpdate(currentTime, duration, true);
|
||||
}
|
||||
}
|
||||
}, [paused, currentTime, duration, traktAutosync]);
|
||||
}, [isCasting, chromecast, paused, duration, currentTime, traktAutosync]);
|
||||
|
||||
// Handle next episode button press
|
||||
const handlePlayNextEpisode = useCallback(async () => {
|
||||
|
|
@ -3466,7 +3608,11 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
onSlidingComplete={handleSlidingComplete}
|
||||
buffered={buffered}
|
||||
formatTime={formatTime}
|
||||
playerBackend={useVLC ? 'VLC' : 'ExoPlayer'}
|
||||
playerBackend={useVLC ? 'VLC' : 'ExoPlayer'}
|
||||
// Cast props
|
||||
isCastConnected={chromecast.isCastConnected}
|
||||
castDevice={chromecast.castDevice?.name}
|
||||
onCastPress={handleCastPress}
|
||||
/>
|
||||
|
||||
{showPauseOverlay && (
|
||||
|
|
@ -4041,6 +4187,55 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
</Animated.View>
|
||||
)}
|
||||
|
||||
{/* Cast Error Overlay */}
|
||||
{chromecast.error && (
|
||||
<Animated.View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: screenDimensions.height * 0.1,
|
||||
left: screenDimensions.width / 2 - 100,
|
||||
opacity: 1,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<View style={{
|
||||
backgroundColor: 'rgba(255, 0, 0, 0.9)',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4,
|
||||
elevation: 5,
|
||||
maxWidth: 200,
|
||||
}}>
|
||||
<Text style={{
|
||||
color: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
Cast Error: {chromecast.error}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={{ marginTop: 4 }}
|
||||
onPress={chromecast.clearError}
|
||||
>
|
||||
<Text style={{
|
||||
color: '#FFFFFF',
|
||||
fontSize: 10,
|
||||
textDecorationLine: 'underline',
|
||||
}}>
|
||||
Dismiss
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
{/* Speed Activated Overlay */}
|
||||
{showSpeedActivatedOverlay && (
|
||||
<Animated.View
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
|||
import { useMetadata } from '../../hooks/useMetadata';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
import { usePlayerGestureControls } from '../../hooks/usePlayerGestureControls';
|
||||
import { useChromecast } from '../../hooks/useChromecast';
|
||||
|
||||
import {
|
||||
DEFAULT_SUBTITLE_SIZE,
|
||||
|
|
@ -268,6 +269,12 @@ const KSPlayerCore: React.FC = () => {
|
|||
const [isAirPlayActive, setIsAirPlayActive] = useState<boolean>(false);
|
||||
const [allowsAirPlay, setAllowsAirPlay] = useState<boolean>(true);
|
||||
|
||||
// Cast state
|
||||
const chromecast = useChromecast();
|
||||
const [isCasting, setIsCasting] = useState(false);
|
||||
const [wasPlayingBeforeCast, setWasPlayingBeforeCast] = useState(false);
|
||||
const [castPositionBeforeConnect, setCastPositionBeforeConnect] = useState(0);
|
||||
|
||||
// Silent startup-timeout retry state
|
||||
const startupRetryCountRef = useRef(0);
|
||||
const startupRetryTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
|
@ -1212,7 +1219,12 @@ const KSPlayerCore: React.FC = () => {
|
|||
|
||||
const skip = (seconds: number) => {
|
||||
const newTime = Math.max(0, Math.min(currentTime + seconds, duration - END_EPSILON));
|
||||
seekToTime(newTime);
|
||||
|
||||
if (isCasting && chromecast.isCastConnected) {
|
||||
chromecast.seek(newTime);
|
||||
} else {
|
||||
seekToTime(newTime);
|
||||
}
|
||||
};
|
||||
|
||||
const onAudioTracks = (data: { audioTracks: AudioTrack[] }) => {
|
||||
|
|
@ -1823,7 +1835,15 @@ const KSPlayerCore: React.FC = () => {
|
|||
};
|
||||
|
||||
const togglePlayback = () => {
|
||||
setPaused(!paused);
|
||||
if (isCasting && chromecast.isCastConnected) {
|
||||
if (paused) {
|
||||
chromecast.play();
|
||||
} else {
|
||||
chromecast.pause();
|
||||
}
|
||||
} else {
|
||||
setPaused(!paused);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle next episode button press
|
||||
|
|
@ -2309,6 +2329,57 @@ const KSPlayerCore: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// Cast handlers
|
||||
const handleCastPress = useCallback(() => {
|
||||
if (!chromecast.isCastAvailable) {
|
||||
logger.warn('[KSPlayerCore] Chromecast not available');
|
||||
return;
|
||||
}
|
||||
chromecast.showCastPicker();
|
||||
}, [chromecast]);
|
||||
|
||||
const loadMediaToCast = useCallback(async () => {
|
||||
if (!chromecast.isCastConnected || !currentStreamUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let contentType = 'video/mp4';
|
||||
if (currentStreamUrl.includes('.m3u8') || currentStreamUrl.includes('hls')) {
|
||||
contentType = 'application/x-mpegURL';
|
||||
}
|
||||
|
||||
const mediaInfo = {
|
||||
contentUrl: currentStreamUrl,
|
||||
contentType,
|
||||
metadata: {
|
||||
title: title,
|
||||
subtitle: episodeTitle || `${season ? `S${season}` : ''}${episode ? `E${episode}` : ''}`,
|
||||
images: backdrop ? [{ url: backdrop }] : undefined,
|
||||
studio: currentStreamProvider || streamProvider || 'Nuvio',
|
||||
},
|
||||
streamDuration: duration,
|
||||
startTime: currentTime,
|
||||
customData: {
|
||||
headers: headers,
|
||||
quality: currentQuality || quality,
|
||||
}
|
||||
};
|
||||
|
||||
await chromecast.loadMedia(mediaInfo);
|
||||
setWasPlayingBeforeCast(!paused);
|
||||
setCastPositionBeforeConnect(currentTime);
|
||||
setIsCasting(true);
|
||||
setPaused(true);
|
||||
|
||||
logger.log('[KSPlayerCore] Media loaded to Cast device');
|
||||
} catch (error) {
|
||||
logger.error('[KSPlayerCore] Error loading media to Cast:', error);
|
||||
}
|
||||
}, [chromecast, currentStreamUrl, title, episodeTitle, season, episode, backdrop,
|
||||
currentStreamProvider, streamProvider, duration, currentTime, headers,
|
||||
currentQuality, quality, paused]);
|
||||
|
||||
const handleSelectStream = async (newStream: any) => {
|
||||
if (newStream.url === currentStreamUrl) {
|
||||
setShowSourcesModal(false);
|
||||
|
|
@ -2398,6 +2469,43 @@ const KSPlayerCore: React.FC = () => {
|
|||
}
|
||||
}, [isVideoLoaded, initialPosition, duration]);
|
||||
|
||||
// Handle Cast connection changes
|
||||
useEffect(() => {
|
||||
if (chromecast.isCastConnected && !isCasting) {
|
||||
loadMediaToCast();
|
||||
} else if (!chromecast.isCastConnected && isCasting) {
|
||||
setIsCasting(false);
|
||||
|
||||
if (chromecast.currentPosition > 0) {
|
||||
seekToTime(chromecast.currentPosition);
|
||||
}
|
||||
|
||||
if (wasPlayingBeforeCast) {
|
||||
setPaused(false);
|
||||
}
|
||||
|
||||
logger.log('[KSPlayerCore] Resumed local playback after Cast disconnect');
|
||||
}
|
||||
}, [chromecast.isCastConnected, isCasting, loadMediaToCast, chromecast.currentPosition, wasPlayingBeforeCast]);
|
||||
|
||||
// Sync Cast playback state
|
||||
useEffect(() => {
|
||||
if (isCasting && chromecast.isCastConnected) {
|
||||
if (chromecast.currentPosition !== currentTime) {
|
||||
setCurrentTime(chromecast.currentPosition);
|
||||
}
|
||||
|
||||
if (chromecast.duration !== duration && chromecast.duration > 0) {
|
||||
setDuration(chromecast.duration);
|
||||
}
|
||||
|
||||
if (chromecast.isPlaying !== !paused) {
|
||||
setPaused(!chromecast.isPlaying);
|
||||
}
|
||||
}
|
||||
}, [isCasting, chromecast.isCastConnected, chromecast.currentPosition,
|
||||
chromecast.duration, chromecast.isPlaying, currentTime, duration, paused]);
|
||||
|
||||
return (
|
||||
<View style={[
|
||||
styles.container,
|
||||
|
|
@ -2487,6 +2595,34 @@ const KSPlayerCore: React.FC = () => {
|
|||
<ActivityIndicator size="large" color="#E50914" />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{/* Cast Error Overlay */}
|
||||
{chromecast.error && (
|
||||
<View style={{
|
||||
position: 'absolute',
|
||||
top: screenDimensions.height * 0.1,
|
||||
alignSelf: 'center',
|
||||
zIndex: 1000,
|
||||
}}>
|
||||
<View style={{
|
||||
backgroundColor: 'rgba(255, 0, 0, 0.9)',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
maxWidth: 200,
|
||||
}}>
|
||||
<Text style={{ color: '#FFFFFF', fontSize: 12, textAlign: 'center' }}>
|
||||
Cast Error: {chromecast.error}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={chromecast.clearError}>
|
||||
<Text style={{ color: '#FFFFFF', fontSize: 10, textAlign: 'center', textDecorationLine: 'underline' }}>
|
||||
Dismiss
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
|
@ -2696,6 +2832,9 @@ const KSPlayerCore: React.FC = () => {
|
|||
isAirPlayActive={isAirPlayActive}
|
||||
allowsAirPlay={allowsAirPlay}
|
||||
onAirPlayPress={handleAirPlayPress}
|
||||
isCastConnected={chromecast.isCastConnected}
|
||||
castDevice={chromecast.castDevice?.name}
|
||||
onCastPress={handleCastPress}
|
||||
/>
|
||||
|
||||
{showPauseOverlay && (
|
||||
|
|
|
|||
52
src/components/player/controls/ChromecastButton.tsx
Normal file
52
src/components/player/controls/ChromecastButton.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import React from 'react';
|
||||
import { Platform, ViewStyle, TouchableOpacity, View } from 'react-native';
|
||||
import { CastButton } from 'react-native-google-cast';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
|
||||
interface ChromecastButtonProps {
|
||||
size?: number;
|
||||
color?: string;
|
||||
activeColor?: string;
|
||||
style?: ViewStyle;
|
||||
isConnected?: boolean;
|
||||
onPress?: () => void;
|
||||
}
|
||||
|
||||
export const ChromecastButton: React.FC<ChromecastButtonProps> = ({
|
||||
size = 24,
|
||||
color = 'white',
|
||||
activeColor = '#E50914',
|
||||
style,
|
||||
isConnected = false,
|
||||
onPress
|
||||
}) => {
|
||||
|
||||
const currentColor = isConnected ? activeColor : color;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
{
|
||||
padding: 8,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
style
|
||||
]}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={{ width: size, height: size }}>
|
||||
<CastButton
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
tintColor: currentColor,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChromecastButton;
|
||||
|
|
@ -7,6 +7,7 @@ import Slider from '@react-native-community/slider';
|
|||
import { styles } from '../utils/playerStyles'; // Updated styles
|
||||
import { getTrackDisplayName } from '../utils/playerUtils';
|
||||
import { useTheme } from '../../../contexts/ThemeContext';
|
||||
import ChromecastButton from './ChromecastButton';
|
||||
|
||||
interface PlayerControlsProps {
|
||||
showControls: boolean;
|
||||
|
|
@ -49,6 +50,10 @@ interface PlayerControlsProps {
|
|||
isAirPlayActive?: boolean;
|
||||
allowsAirPlay?: boolean;
|
||||
onAirPlayPress?: () => void;
|
||||
// Chromecast props
|
||||
isCastConnected?: boolean;
|
||||
castDevice?: string;
|
||||
onCastPress?: () => void;
|
||||
}
|
||||
|
||||
export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
||||
|
|
@ -90,6 +95,9 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|||
isAirPlayActive,
|
||||
allowsAirPlay,
|
||||
onAirPlayPress,
|
||||
isCastConnected,
|
||||
castDevice,
|
||||
onCastPress,
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
|
|
@ -317,8 +325,8 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|||
)}
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||
{/* AirPlay Button - iOS only, KSAVPlayer only */}
|
||||
{Platform.OS === 'ios' && onAirPlayPress && playerBackend === 'KSAVPlayer' && (
|
||||
{/* AirPlay Button - temporarily hidden */}
|
||||
{false && Platform.OS === 'ios' && onAirPlayPress && playerBackend === 'KSAVPlayer' && (
|
||||
<TouchableOpacity
|
||||
style={{ padding: 8 }}
|
||||
onPress={onAirPlayPress}
|
||||
|
|
@ -330,6 +338,17 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* Chromecast Button - temporarily hidden */}
|
||||
{false && onCastPress && (
|
||||
<ChromecastButton
|
||||
size={closeIconSize}
|
||||
color="white"
|
||||
activeColor="#E50914"
|
||||
onPress={onCastPress}
|
||||
isConnected={isCastConnected}
|
||||
/>
|
||||
)}
|
||||
<TouchableOpacity style={styles.closeButton} onPress={handleClose}>
|
||||
<Ionicons name="close" size={closeIconSize} color="white" />
|
||||
</TouchableOpacity>
|
||||
|
|
|
|||
340
src/hooks/useChromecast.ts
Normal file
340
src/hooks/useChromecast.ts
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import {
|
||||
useRemoteMediaClient,
|
||||
useCastSession,
|
||||
CastContext,
|
||||
PlayServicesState,
|
||||
MediaPlayerState,
|
||||
} from 'react-native-google-cast';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export interface CastDevice {
|
||||
id: string;
|
||||
name: string;
|
||||
isConnected: boolean;
|
||||
}
|
||||
|
||||
export interface CastMediaInfo {
|
||||
contentUrl: string;
|
||||
contentType: string;
|
||||
metadata?: {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
images?: Array<{ url: string }>;
|
||||
studio?: string;
|
||||
type?: 'movie' | 'series';
|
||||
};
|
||||
streamDuration?: number;
|
||||
startTime?: number;
|
||||
customData?: any;
|
||||
}
|
||||
|
||||
export interface UseChromecastReturn {
|
||||
// Connection state
|
||||
isCastConnected: boolean;
|
||||
castDevice: CastDevice | null;
|
||||
isCastAvailable: boolean;
|
||||
|
||||
// Media control
|
||||
loadMedia: (mediaInfo: CastMediaInfo) => Promise<void>;
|
||||
play: () => Promise<void>;
|
||||
pause: () => Promise<void>;
|
||||
seek: (position: number) => Promise<void>;
|
||||
|
||||
// Playback state from Cast device
|
||||
currentPosition: number;
|
||||
duration: number;
|
||||
isPlaying: boolean;
|
||||
|
||||
// Device management
|
||||
showCastPicker: () => void;
|
||||
disconnect: () => Promise<void>;
|
||||
|
||||
// Error handling
|
||||
error: string | null;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useChromecast = (): UseChromecastReturn => {
|
||||
// Cast SDK hooks
|
||||
const client = useRemoteMediaClient();
|
||||
const session = useCastSession();
|
||||
|
||||
// State
|
||||
const [isCastConnected, setIsCastConnected] = useState(false);
|
||||
const [castDevice, setCastDevice] = useState<CastDevice | null>(null);
|
||||
const [isCastAvailable, setIsCastAvailable] = useState(false);
|
||||
const [currentPosition, setCurrentPosition] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Refs for cleanup
|
||||
const positionUpdateInterval = useRef<NodeJS.Timeout | null>(null);
|
||||
const lastPositionRef = useRef(0);
|
||||
|
||||
// Check Cast availability
|
||||
useEffect(() => {
|
||||
// Cast is available on both iOS and Android
|
||||
// The actual availability will be determined by the Cast SDK
|
||||
setIsCastAvailable(true);
|
||||
}, []);
|
||||
|
||||
// Monitor Cast session changes
|
||||
useEffect(() => {
|
||||
if (!session) {
|
||||
setIsCastConnected(false);
|
||||
setCastDevice(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateDeviceInfo = async () => {
|
||||
try {
|
||||
const device = await session.getCastDevice();
|
||||
if (device) {
|
||||
const deviceInfo: CastDevice = {
|
||||
id: device.deviceId,
|
||||
name: device.friendlyName || device.deviceId,
|
||||
isConnected: true
|
||||
};
|
||||
|
||||
setIsCastConnected(true);
|
||||
setCastDevice(deviceInfo);
|
||||
|
||||
logger.log('[useChromecast] Connected to Cast device:', deviceInfo.name);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[useChromecast] Error getting device info:', error);
|
||||
}
|
||||
};
|
||||
|
||||
updateDeviceInfo();
|
||||
|
||||
return () => {
|
||||
setIsCastConnected(false);
|
||||
setCastDevice(null);
|
||||
logger.log('[useChromecast] Disconnected from Cast device');
|
||||
};
|
||||
}, [session]);
|
||||
|
||||
// Monitor media status updates
|
||||
useEffect(() => {
|
||||
if (!client || !isCastConnected) {
|
||||
setCurrentPosition(0);
|
||||
setDuration(0);
|
||||
setIsPlaying(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateMediaStatus = async () => {
|
||||
try {
|
||||
const mediaStatus = await client.getMediaStatus();
|
||||
if (mediaStatus) {
|
||||
const playerState = mediaStatus.playerState;
|
||||
const isPlaying = playerState === MediaPlayerState.PLAYING;
|
||||
const isPaused = playerState === MediaPlayerState.PAUSED;
|
||||
const position = (isPlaying || isPaused)
|
||||
? (mediaStatus.streamPosition || 0)
|
||||
: 0;
|
||||
|
||||
setCurrentPosition(position);
|
||||
setDuration(mediaStatus.mediaInfo?.streamDuration || 0);
|
||||
setIsPlaying(isPlaying);
|
||||
|
||||
// Update last position for comparison
|
||||
lastPositionRef.current = position;
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently handle errors - media status might not be available yet
|
||||
}
|
||||
};
|
||||
|
||||
// Initial update
|
||||
updateMediaStatus();
|
||||
|
||||
// Set up periodic updates (every 1 second)
|
||||
positionUpdateInterval.current = setInterval(updateMediaStatus, 1000);
|
||||
|
||||
return () => {
|
||||
if (positionUpdateInterval.current) {
|
||||
clearInterval(positionUpdateInterval.current);
|
||||
positionUpdateInterval.current = null;
|
||||
}
|
||||
};
|
||||
}, [client, isCastConnected]);
|
||||
|
||||
// Load media to Cast device
|
||||
const loadMedia = useCallback(async (mediaInfo: CastMediaInfo) => {
|
||||
if (!client || !isCastConnected) {
|
||||
setError('No Cast device connected');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
const mediaLoadRequest = {
|
||||
mediaInfo: {
|
||||
contentUrl: mediaInfo.contentUrl,
|
||||
contentType: mediaInfo.contentType,
|
||||
streamDuration: mediaInfo.streamDuration,
|
||||
customData: mediaInfo.customData
|
||||
},
|
||||
startTime: mediaInfo.startTime || 0,
|
||||
autoplay: true
|
||||
};
|
||||
|
||||
logger.log('[useChromecast] Loading media:', {
|
||||
url: mediaInfo.contentUrl,
|
||||
type: mediaInfo.contentType,
|
||||
duration: mediaInfo.streamDuration,
|
||||
startTime: mediaInfo.startTime
|
||||
});
|
||||
|
||||
await client.loadMedia(mediaLoadRequest);
|
||||
|
||||
logger.log('[useChromecast] Media loaded successfully');
|
||||
} catch (err) {
|
||||
const errorMessage = `Failed to load media: ${err}`;
|
||||
logger.error('[useChromecast] Error loading media:', err);
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
}
|
||||
}, [client, isCastConnected]);
|
||||
|
||||
// Play media
|
||||
const play = useCallback(async () => {
|
||||
if (!client || !isCastConnected) {
|
||||
setError('No Cast device connected');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
await client.play();
|
||||
logger.log('[useChromecast] Play command sent');
|
||||
} catch (err) {
|
||||
const errorMessage = `Failed to play: ${err}`;
|
||||
logger.error('[useChromecast] Error playing:', err);
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
}
|
||||
}, [client, isCastConnected]);
|
||||
|
||||
// Pause media
|
||||
const pause = useCallback(async () => {
|
||||
if (!client || !isCastConnected) {
|
||||
setError('No Cast device connected');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
await client.pause();
|
||||
logger.log('[useChromecast] Pause command sent');
|
||||
} catch (err) {
|
||||
const errorMessage = `Failed to pause: ${err}`;
|
||||
logger.error('[useChromecast] Error pausing:', err);
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
}
|
||||
}, [client, isCastConnected]);
|
||||
|
||||
// Seek to position
|
||||
const seek = useCallback(async (position: number) => {
|
||||
if (!client || !isCastConnected) {
|
||||
setError('No Cast device connected');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
await client.seek({ position });
|
||||
logger.log('[useChromecast] Seek command sent to position:', position);
|
||||
} catch (err) {
|
||||
const errorMessage = `Failed to seek: ${err}`;
|
||||
logger.error('[useChromecast] Error seeking:', err);
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
}
|
||||
}, [client, isCastConnected]);
|
||||
|
||||
// Show Cast device picker
|
||||
const showCastPicker = useCallback(() => {
|
||||
if (!isCastAvailable) {
|
||||
setError('Chromecast not available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
// Use the CastContext directly - the library should handle this
|
||||
CastContext.showCastDialog();
|
||||
logger.log('[useChromecast] Cast dialog shown');
|
||||
} catch (err) {
|
||||
const errorMessage = `Failed to show Cast dialog: ${err}`;
|
||||
logger.error('[useChromecast] Error showing Cast dialog:', err);
|
||||
setError(errorMessage);
|
||||
}
|
||||
}, [isCastAvailable]);
|
||||
|
||||
// Disconnect from Cast device
|
||||
const disconnect = useCallback(async () => {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
// For now, just log the disconnect attempt
|
||||
// The actual disconnection will be handled by the Cast SDK
|
||||
logger.log('[useChromecast] Disconnect requested');
|
||||
} catch (err) {
|
||||
const errorMessage = `Failed to disconnect: ${err}`;
|
||||
logger.error('[useChromecast] Error disconnecting:', err);
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
// Clear error
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (positionUpdateInterval.current) {
|
||||
clearInterval(positionUpdateInterval.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// Connection state
|
||||
isCastConnected,
|
||||
castDevice,
|
||||
isCastAvailable,
|
||||
|
||||
// Media control
|
||||
loadMedia,
|
||||
play,
|
||||
pause,
|
||||
seek,
|
||||
|
||||
// Playback state
|
||||
currentPosition,
|
||||
duration,
|
||||
isPlaying,
|
||||
|
||||
// Device management
|
||||
showCastPicker,
|
||||
disconnect,
|
||||
|
||||
// Error handling
|
||||
error,
|
||||
clearError
|
||||
};
|
||||
};
|
||||
|
|
@ -185,5 +185,7 @@ export const useMetadataAssets = (
|
|||
foundTmdbId,
|
||||
setBannerImage,
|
||||
bannerSource,
|
||||
logoLoadError: false,
|
||||
setLogoLoadError: () => {},
|
||||
};
|
||||
};
|
||||
|
|
@ -74,13 +74,13 @@ export const useUpdatePopup = (): UseUpdatePopupReturn => {
|
|||
// The app will automatically reload with the new version
|
||||
console.log('Update installed successfully');
|
||||
} else {
|
||||
toastService.showError('Installation Failed', 'Unable to install the update. Please try again later or check your internet connection.');
|
||||
toastService.error('Installation Failed', 'Unable to install the update. Please try again later or check your internet connection.');
|
||||
// Show popup again after failed installation
|
||||
setShowUpdatePopup(true);
|
||||
}
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error('Error installing update:', error);
|
||||
toastService.showError('Installation Error', 'An error occurred while installing the update. Please try again later.');
|
||||
toastService.error('Installation Error', 'An error occurred while installing the update. Please try again later.');
|
||||
// Show popup again after error
|
||||
setShowUpdatePopup(true);
|
||||
} finally {
|
||||
|
|
|
|||
Loading…
Reference in a new issue