diff --git a/app/android/app/src/main/java/com/movix/app/MainActivity.kt b/app/android/app/src/main/java/com/movix/app/MainActivity.kt
index 57ea5f4..2e461dc 100644
--- a/app/android/app/src/main/java/com/movix/app/MainActivity.kt
+++ b/app/android/app/src/main/java/com/movix/app/MainActivity.kt
@@ -50,6 +50,11 @@ class MainActivity : ReactActivity() {
} catch (_: Throwable) {}
}
+ /** Entrée PiP déclenchée explicitement par le bouton PiP du lecteur web. */
+ fun enterPipNow() {
+ enterPipSafely()
+ }
+
private fun enterPipSafely() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
try {
diff --git a/app/android/app/src/main/java/com/movix/app/pip/PipModule.kt b/app/android/app/src/main/java/com/movix/app/pip/PipModule.kt
index e8392f7..488a18a 100644
--- a/app/android/app/src/main/java/com/movix/app/pip/PipModule.kt
+++ b/app/android/app/src/main/java/com/movix/app/pip/PipModule.kt
@@ -28,6 +28,18 @@ class PipModule(reactContext: ReactApplicationContext) :
}
}
+ /**
+ * Bascule immédiatement l'Activity en Picture-in-Picture. Appelé quand
+ * l'utilisateur clique sur le bouton PiP du lecteur web (le WebView Android
+ * n'ayant pas l'API Web PiP, on shimme l'appel jusqu'ici).
+ */
+ @ReactMethod
+ fun enterPipNow() {
+ currentActivity?.runOnUiThread {
+ (currentActivity as? MainActivity)?.enterPipNow()
+ }
+ }
+
// Requis par l'interface NativeModule côté event-emitter ; no-op ici.
@ReactMethod
fun addListener(eventName: String) {
diff --git a/app/ios/Movix/AppDelegate.mm b/app/ios/Movix/AppDelegate.mm
index 2bd7a04..f5faf69 100644
--- a/app/ios/Movix/AppDelegate.mm
+++ b/app/ios/Movix/AppDelegate.mm
@@ -31,7 +31,7 @@
initWithApplicationID:kGCKDefaultMediaReceiverApplicationID];
GCKCastOptions *castOptions = [[GCKCastOptions alloc] initWithDiscoveryCriteria:criteria];
castOptions.physicalVolumeButtonsWillControlDeviceVolume = YES;
- [GCKCastContext setSharedInstanceWith:castOptions];
+ [GCKCastContext setSharedInstanceWithOptions:castOptions];
#endif
return [super application:application didFinishLaunchingWithOptions:launchOptions];
diff --git a/app/ios/Movix/Cast/CastModule.swift b/app/ios/Movix/Cast/CastModule.swift
index 816892b..2f8d32a 100644
--- a/app/ios/Movix/Cast/CastModule.swift
+++ b/app/ios/Movix/Cast/CastModule.swift
@@ -5,36 +5,74 @@ import GoogleCast
#endif
/// Bridge React Native → iOS Google Cast SDK.
-/// Symétrique au CastModule Android.
-/// Quand le SDK GoogleCast n'est pas disponible (pod non installé),
-/// toutes les méthodes retournent false / nil sans crasher.
+/// Symétrique au CastModule Android (com.movix.app.cast.CastModule).
+///
+/// Quand le SDK GoogleCast n'est pas lié (pod absent), toutes les méthodes
+/// dégradent proprement (isSupported → false) sans crasher, grâce aux gardes
+/// `#if canImport(GoogleCast)`.
+///
+/// Évènements émis (via RCTDeviceEventEmitter, reçus par DeviceEventEmitter JS) :
+/// CAST_SESSION_STARTED { deviceName, durationSec }
+/// CAST_SESSION_RESUMED { deviceName, durationSec }
+/// CAST_SESSION_ENDED { error }
+/// CAST_SESSION_FAILED { error }
@objc(CastModule)
class CastModule: RCTEventEmitter {
- private static let CAST_SESSION_STARTED = "CAST_SESSION_STARTED"
- private static let CAST_SESSION_RESUMED = "CAST_SESSION_RESUMED"
- private static let CAST_SESSION_ENDED = "CAST_SESSION_ENDED"
- private static let CAST_SESSION_FAILED = "CAST_SESSION_FAILED"
+ private var hasListeners = false
- override static func requiresMainQueueSetup() -> Bool { false }
+ // Requête de lecture mémorisée tant qu'aucune session n'est active : jouée
+ // dès qu'un appareil est sélectionné (didStart). Mirroir du pendingLoad Android.
+ private var pendingURL: String?
+ private var pendingTitle: String = "Movix"
+ private var pendingPoster: String?
+ private var pendingPosition: Double = 0
+
+ override static func requiresMainQueueSetup() -> Bool { true }
override func supportedEvents() -> [String]! {
return [
- CastModule.CAST_SESSION_STARTED,
- CastModule.CAST_SESSION_RESUMED,
- CastModule.CAST_SESSION_ENDED,
- CastModule.CAST_SESSION_FAILED,
+ "CAST_SESSION_STARTED",
+ "CAST_SESSION_RESUMED",
+ "CAST_SESSION_ENDED",
+ "CAST_SESSION_FAILED",
]
}
+ override func startObserving() {
+ hasListeners = true
+#if canImport(GoogleCast)
+ DispatchQueue.main.async {
+ GCKCastContext.sharedInstance().sessionManager.add(self)
+ }
+#endif
+ }
+
+ override func stopObserving() {
+ hasListeners = false
+#if canImport(GoogleCast)
+ DispatchQueue.main.async {
+ GCKCastContext.sharedInstance().sessionManager.remove(self)
+ }
+#endif
+ }
+
+ private func emit(_ name: String, _ body: [String: Any]?) {
+ if hasListeners { sendEvent(withName: name, body: body) }
+ }
+
+ // MARK: - Méthodes exposées à JS
+
@objc
func isSupported(
_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock
) {
#if canImport(GoogleCast)
- let context = GCKCastContext.sharedInstance()
- resolve(context.castState != .noDevicesAvailable)
+ // Cast est disponible dès que le SDK est lié (le picker affichera
+ // « aucun appareil » s'il n'y a pas de Chromecast à proximité). Identique
+ // au comportement Android qui renvoie true si Play Services est présent.
+ resolve(true)
#else
resolve(false)
#endif
@@ -65,30 +103,26 @@ class CastModule: RCTEventEmitter {
reject: @escaping RCTPromiseRejectBlock
) {
#if canImport(GoogleCast)
- guard let mediaUrl = URL(string: url) else {
- resolve(false)
+ let scheme = url.components(separatedBy: ":").first?.lowercased() ?? ""
+ if scheme != "http" && scheme != "https" {
+ reject("INVALID_URL", "Only http(s) URLs are castable", nil)
return
}
- let metadata = GCKMediaMetadata(metadataType: .movie)
- metadata.setString(title, forKey: kGCKMetadataKeyTitle)
- if let posterStr = poster, let posterUrl = URL(string: posterStr) {
- metadata.addImage(GCKImage(url: posterUrl, width: 480, height: 720))
+ DispatchQueue.main.async {
+ let manager = GCKCastContext.sharedInstance().sessionManager
+ if let session = manager.currentCastSession, session.connectionState == .connected {
+ self.playMedia(on: session, url: url, title: title, poster: poster, position: currentTimeSec)
+ resolve(true)
+ return
+ }
+ // Pas de session : mémoriser puis ouvrir le picker. didStart jouera le média.
+ self.pendingURL = url
+ self.pendingTitle = title
+ self.pendingPoster = poster
+ self.pendingPosition = currentTimeSec
+ GCKCastContext.sharedInstance().presentCastDialog()
+ resolve(true)
}
- let builder = GCKMediaInformationBuilder(contentURL: mediaUrl)
- builder.contentType = "application/x-mpegURL"
- builder.metadata = metadata
- let mediaInfo = builder.build()
-
- let options = GCKMediaLoadOptions()
- options.playPosition = currentTimeSec
-
- let session = GCKCastContext.sharedInstance().sessionManager.currentCastSession
- guard let remoteClient = session?.remoteMediaClient else {
- resolve(false)
- return
- }
- remoteClient.loadMedia(mediaInfo, with: options)
- resolve(true)
#else
resolve(false)
#endif
@@ -100,9 +134,13 @@ class CastModule: RCTEventEmitter {
reject: @escaping RCTPromiseRejectBlock
) {
#if canImport(GoogleCast)
- let session = GCKCastContext.sharedInstance().sessionManager.currentCastSession
- session?.remoteMediaClient?.stop()
- resolve(true)
+ DispatchQueue.main.async {
+ self.pendingURL = nil
+ let manager = GCKCastContext.sharedInstance().sessionManager
+ manager.currentCastSession?.remoteMediaClient?.stop()
+ _ = manager.endSessionAndStopCasting(true)
+ resolve(true)
+ }
#else
resolve(false)
#endif
@@ -127,7 +165,8 @@ class CastModule: RCTEventEmitter {
reject: @escaping RCTPromiseRejectBlock
) {
#if canImport(GoogleCast)
- let pos = GCKCastContext.sharedInstance().sessionManager.currentCastSession?.remoteMediaClient?.approximateStreamPosition() ?? 0
+ let pos = GCKCastContext.sharedInstance().sessionManager
+ .currentCastSession?.remoteMediaClient?.approximateStreamPosition() ?? 0
resolve(pos)
#else
resolve(0)
@@ -140,15 +179,84 @@ class CastModule: RCTEventEmitter {
reject: @escaping RCTPromiseRejectBlock
) {
#if canImport(GoogleCast)
- let mgr = GCKCastContext.sharedInstance().sessionManager
- switch mgr.connectionState {
- case .connected: resolve("connected")
- case .connecting: resolve("starting")
+ switch GCKCastContext.sharedInstance().sessionManager.connectionState {
+ case .connected: resolve("connected")
+ case .connecting: resolve("starting")
case .disconnecting: resolve("ending")
- default: resolve("idle")
+ default: resolve("idle")
}
#else
resolve("idle")
#endif
}
+
+#if canImport(GoogleCast)
+ private func playMedia(
+ on session: GCKCastSession,
+ url: String,
+ title: String,
+ poster: String?,
+ position: Double
+ ) {
+ guard let mediaUrl = URL(string: url) else { return }
+ let metadata = GCKMediaMetadata(metadataType: .movie)
+ metadata.setString(title, forKey: kGCKMetadataKeyTitle)
+ if let posterStr = poster, let posterUrl = URL(string: posterStr) {
+ metadata.addImage(GCKImage(url: posterUrl, width: 480, height: 720))
+ }
+ let builder = GCKMediaInformationBuilder(contentURL: mediaUrl)
+ builder.streamType = .buffered
+ builder.contentType = "application/x-mpegURL"
+ builder.metadata = metadata
+ let mediaInfo = builder.build()
+
+ let options = GCKMediaLoadOptions()
+ options.playPosition = position
+ session.remoteMediaClient?.loadMedia(mediaInfo, with: options)
+ }
+
+ private func durationSec(for session: GCKSession) -> Double {
+ let ms = session.remoteMediaClient?.mediaStatus?.mediaInformation?.streamDuration ?? 0
+ return ms.isFinite ? ms : 0
+ }
+#endif
}
+
+#if canImport(GoogleCast)
+extension CastModule: GCKSessionManagerListener {
+
+ func sessionManager(_ sessionManager: GCKSessionManager, didStart session: GCKSession) {
+ emit("CAST_SESSION_STARTED", [
+ "deviceName": session.device.friendlyName ?? "",
+ "durationSec": durationSec(for: session),
+ ])
+ if let url = pendingURL, let castSession = session as? GCKCastSession {
+ pendingURL = nil
+ playMedia(on: castSession, url: url, title: pendingTitle,
+ poster: pendingPoster, position: pendingPosition)
+ }
+ }
+
+ func sessionManager(_ sessionManager: GCKSessionManager, didResumeSession session: GCKSession) {
+ emit("CAST_SESSION_RESUMED", [
+ "deviceName": session.device.friendlyName ?? "",
+ "durationSec": durationSec(for: session),
+ ])
+ if let url = pendingURL, let castSession = session as? GCKCastSession {
+ pendingURL = nil
+ playMedia(on: castSession, url: url, title: pendingTitle,
+ poster: pendingPoster, position: pendingPosition)
+ }
+ }
+
+ func sessionManager(_ sessionManager: GCKSessionManager, didEnd session: GCKSession, withError error: Error?) {
+ pendingURL = nil
+ emit("CAST_SESSION_ENDED", ["error": (error == nil) ? 0 : 1])
+ }
+
+ func sessionManager(_ sessionManager: GCKSessionManager, didFailToStart session: GCKSession, withError error: Error) {
+ pendingURL = nil
+ emit("CAST_SESSION_FAILED", ["error": 1])
+ }
+}
+#endif
diff --git a/app/ios/Movix/Info.plist b/app/ios/Movix/Info.plist
index 82f296c..e8799c3 100644
--- a/app/ios/Movix/Info.plist
+++ b/app/ios/Movix/Info.plist
@@ -68,5 +68,13 @@
tg
+
+ NSLocalNetworkUsageDescription
+ Movix utilise le réseau local pour détecter les appareils Chromecast et y diffuser vos films et séries.
+ NSBonjourServices
+
+ _googlecast._tcp
+ _CC1AD845._googlecast._tcp
+
diff --git a/app/ios/Movix/Movix-Bridging-Header.h b/app/ios/Movix/Movix-Bridging-Header.h
index dea7ff6..8992b22 100644
--- a/app/ios/Movix/Movix-Bridging-Header.h
+++ b/app/ios/Movix/Movix-Bridging-Header.h
@@ -1,2 +1,3 @@
#import
#import
+#import
diff --git a/app/ios/MovixApp.xcodeproj/project.pbxproj b/app/ios/MovixApp.xcodeproj/project.pbxproj
index 9d401a9..c2133bf 100644
--- a/app/ios/MovixApp.xcodeproj/project.pbxproj
+++ b/app/ios/MovixApp.xcodeproj/project.pbxproj
@@ -9,12 +9,14 @@
/* Begin PBXBuildFile section */
00E356F31AD99517003FC87E /* MovixAppTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* MovixAppTests.m */; };
0C80B921A6F3F58F76C31292 /* libPods-MovixApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-MovixApp.a */; };
- AA000003AA000003AA000003 /* UpdateModule.m in Sources */ = {isa = PBXBuildFile; fileRef = AA000002AA000002AA000002 /* UpdateModule.m */; };
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
23DB1FEBE14D043071FF72DB /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
+ AA000003AA000003AA000003 /* UpdateModule.m in Sources */ = {isa = PBXBuildFile; fileRef = AA000002AA000002AA000002 /* UpdateModule.m */; };
+ BE9F0E7B05C77A4A13E2705C /* CastModule.m in Sources */ = {isa = PBXBuildFile; fileRef = B46C23372B0FE5CEE689641D /* CastModule.m */; };
+ E4CA553D3CD03CF09B4BAE0F /* CastModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10ABD37C5C2515A420C986DF /* CastModule.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -31,9 +33,8 @@
00E356EE1AD99517003FC87E /* MovixAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MovixAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
00E356F21AD99517003FC87E /* MovixAppTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MovixAppTests.m; sourceTree = ""; };
+ 10ABD37C5C2515A420C986DF /* CastModule.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CastModule.swift; path = Movix/Cast/CastModule.swift; sourceTree = ""; };
13B07F961A680F5B00A75B9A /* MovixApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MovixApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
- AA000001AA000001AA000001 /* UpdateModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = UpdateModule.h; path = Movix/UpdateModule.h; sourceTree = ""; };
- AA000002AA000002AA000002 /* UpdateModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = UpdateModule.m; path = Movix/UpdateModule.m; sourceTree = ""; };
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Movix/AppDelegate.h; sourceTree = ""; };
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Movix/AppDelegate.mm; sourceTree = ""; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Movix/Images.xcassets; sourceTree = ""; };
@@ -44,6 +45,9 @@
5709B34CF0A7D63546082F79 /* Pods-MovixApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MovixApp.release.xcconfig"; path = "Target Support Files/Pods-MovixApp/Pods-MovixApp.release.xcconfig"; sourceTree = ""; };
5DCACB8F33CDC322A6C60F78 /* libPods-MovixApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-MovixApp.a"; sourceTree = BUILT_PRODUCTS_DIR; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Movix/LaunchScreen.storyboard; sourceTree = ""; };
+ AA000001AA000001AA000001 /* UpdateModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = UpdateModule.h; path = Movix/UpdateModule.h; sourceTree = ""; };
+ AA000002AA000002AA000002 /* UpdateModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = UpdateModule.m; path = Movix/UpdateModule.m; sourceTree = ""; };
+ B46C23372B0FE5CEE689641D /* CastModule.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = CastModule.m; path = Movix/Cast/CastModule.m; sourceTree = ""; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
@@ -95,6 +99,7 @@
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
13B07FB71A68108700A75B9A /* main.m */,
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
+ FC36AD23F4567B83F6F5BE82 /* Cast */,
);
name = MovixApp;
sourceTree = "";
@@ -148,6 +153,15 @@
path = Pods;
sourceTree = "";
};
+ FC36AD23F4567B83F6F5BE82 /* Cast */ = {
+ isa = PBXGroup;
+ children = (
+ 10ABD37C5C2515A420C986DF /* CastModule.swift */,
+ B46C23372B0FE5CEE689641D /* CastModule.m */,
+ );
+ name = Cast;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -346,6 +360,8 @@
AA000003AA000003AA000003 /* UpdateModule.m in Sources */,
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
+ E4CA553D3CD03CF09B4BAE0F /* CastModule.swift in Sources */,
+ BE9F0E7B05C77A4A13E2705C /* CastModule.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -415,6 +431,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-MovixApp.debug.xcconfig */;
buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES;
@@ -438,6 +455,7 @@
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_OBJC_BRIDGING_HEADER = "Movix/Movix-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -449,6 +467,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-MovixApp.release.xcconfig */;
buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES;
@@ -471,6 +490,7 @@
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_OBJC_BRIDGING_HEADER = "Movix/Movix-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
@@ -552,7 +572,10 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
- OTHER_LDFLAGS = "$(inherited) ";
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ " ",
+ );
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -627,7 +650,10 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
- OTHER_LDFLAGS = "$(inherited) ";
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ " ",
+ );
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
diff --git a/app/ios/Podfile b/app/ios/Podfile
index 0463f0a..81f09ea 100644
--- a/app/ios/Podfile
+++ b/app/ios/Podfile
@@ -18,9 +18,9 @@ target 'MovixApp' do
config = use_native_modules!
# Google Cast SDK (iOS Chromecast support).
- # Pour activer Chromecast, décommente la ligne ci-dessous et assure-toi
- # d'avoir `use_frameworks! :linkage => :static` (requis par le SDK Cast).
- # pod 'google-cast-sdk-no-bluetooth', '~> 4.8'
+ # Depuis 4.8.4, le SDK est livré en static framework qui embarque Protobuf :
+ # plus besoin de `use_frameworks!`, compatible avec le setup RN par défaut.
+ pod 'google-cast-sdk', '~> 4.8.4'
use_react_native!(
:path => config[:reactNativePath],
diff --git a/app/src/injection/android-pip-shim.ts b/app/src/injection/android-pip-shim.ts
new file mode 100644
index 0000000..72802bb
--- /dev/null
+++ b/app/src/injection/android-pip-shim.ts
@@ -0,0 +1,80 @@
+/**
+ * Shim Picture-in-Picture pour Android.
+ *
+ * Le WebView système Android n'implémente PAS l'API Web Picture-in-Picture
+ * (`document.pictureInPictureEnabled` / `HTMLVideoElement.requestPictureInPicture`).
+ * Quand l'utilisateur clique sur le bouton PiP du lecteur Movix, celui-ci
+ * détecte l'absence de l'API et affiche « votre navigateur ne supporte pas le
+ * PiP ».
+ *
+ * Sur Android, le PiP est une fonctionnalité au niveau de l'Activity, pas de
+ * l'élément