From b791ad0c38bb353478745b484daf7de1998a3e30 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 11:17:53 +0000 Subject: [PATCH] feat: real Chromecast on iOS + fix Android in-app PiP button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Android PiP button fix: - The Android System WebView does not implement the Web Picture-in-Picture API, so the Movix player's PiP button showed "browser doesn't support PiP". - New android-pip-shim.ts injects a PiP Web API shim (Android only): document.pictureInPictureEnabled = true + HTMLVideoElement.requestPictureInPicture override that routes to native Activity PiP via an ENTER_PIP bridge message. - bridge.ts handles ENTER_PIP → PipModule.enterPipNow() - PipModule.enterPipNow() + MainActivity.enterPipNow() trigger enterPictureInPictureMode - iOS keeps native WebKit PiP (shim is Android-only) Chromecast on iOS (now actually wired into the build): - Podfile: pod 'google-cast-sdk', '~> 4.8.4' — since 4.8.4 the SDK ships as a static framework bundling Protobuf, so NO use_frameworks! is needed (compatible with the default RN static-library setup). Previous attempt used a non-existent pod name (google-cast-sdk-no-arc) which broke `pod install`. - CastModule.swift rewritten to mirror the Android CastModule exactly: pending-load + presentCastDialog picker flow, GCKSessionManagerListener emitting CAST_SESSION_STARTED/RESUMED/ENDED/FAILED with {deviceName, durationSec, error}, loadMedia/stop/getCurrentDeviceName/getCurrentPositionSec/getSessionState. All Cast API guarded by #if canImport(GoogleCast) so it compiles with or without the pod (degrades to isSupported=false). - project.pbxproj: registered CastModule.swift + CastModule.m in the MovixApp target's Sources phase and wired SWIFT_OBJC_BRIDGING_HEADER (first Swift in the app target) — done via the xcodeproj gem so the project graph stays valid. - Bridging header now imports RCTEventEmitter.h. - AppDelegate.mm: GCKCastContext initialized via setSharedInstanceWithOptions: (correct ObjC selector) with the Default Media Receiver app ID. - Info.plist: NSLocalNetworkUsageDescription + NSBonjourServices (_googlecast._tcp, _CC1AD845._googlecast._tcp) required for device discovery on iOS 14+. The existing iOS-settings cast toggle (AirPlay vs Chromecast) now drives a real Chromecast path: in Chromecast mode the cast shim is injected and the native iOS CastModule responds supported=true. https://claude.ai/code/session_01X52sah6aUu3ou26uJWHgzr --- .../main/java/com/movix/app/MainActivity.kt | 5 + .../main/java/com/movix/app/pip/PipModule.kt | 12 ++ app/ios/Movix/AppDelegate.mm | 2 +- app/ios/Movix/Cast/CastModule.swift | 196 ++++++++++++++---- app/ios/Movix/Info.plist | 8 + app/ios/Movix/Movix-Bridging-Header.h | 1 + app/ios/MovixApp.xcodeproj/project.pbxproj | 36 +++- app/ios/Podfile | 6 +- app/src/injection/android-pip-shim.ts | 80 +++++++ app/src/injection/inject.ts | 7 + app/src/services/bridge.ts | 15 ++ 11 files changed, 315 insertions(+), 53 deletions(-) create mode 100644 app/src/injection/android-pip-shim.ts 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