diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ff3a5fc..8f6e61a39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [8.12.0] - 25-01-09 + +### Fixed + +- Fixed a memory leak on iOS, where the presentationModeManager was holding a strong reference to the fullscreen's target and return views +- Fixed an issue on iOS where the destruction of the THEOplayerView was not always propagated correctly over the iOS Bridge, resulting in an occasional memory leak. +- Fixed an issue where, when requesting a text track's cues, the time properties would sometimes be in seconds instead of milliseconds. +- Fixed a rare crash on Android due to a `java.lang.NullPointerException` when creating the THEOplayerView. +- Fixed an issue on Android where R8 minification would obfuscate some API class names, which could lead to a crash. + +### Added + +- Added a `adLoadTimeout` property to `GoogleImaConfiguration` to control the amount of time that the SDK will wait before moving onto the next ad or main content. + ## [8.11.1] - 24-12-18 ### Fixed diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro index cf5c28b15..e96ea03ed 100644 --- a/android/proguard-rules.pro +++ b/android/proguard-rules.pro @@ -1,3 +1,7 @@ # Do no warn if any of the API classes we resolve with compileOnly are missing because the feature # is disabled: it is expected. -dontwarn com.theoplayer.android.api.** +-dontwarn com.google.android.gms.cast.** + +# We rely on gson to instantiate some source classes from json, so make sure they are not obfuscated. +-keep,includedescriptorclasses class com.theoplayer.android.api.source.** { *; } diff --git a/android/src/main/java/com/theoplayer/PlayerConfigAdapter.kt b/android/src/main/java/com/theoplayer/PlayerConfigAdapter.kt index 0f42b3df8..b7f44cfff 100644 --- a/android/src/main/java/com/theoplayer/PlayerConfigAdapter.kt +++ b/android/src/main/java/com/theoplayer/PlayerConfigAdapter.kt @@ -28,6 +28,7 @@ private const val PROP_RETRY_MAX_BACKOFF = "maximumBackoff" private const val PROP_CAST_CONFIGURATION = "cast" private const val PROP_ADS_CONFIGURATION = "ads" private const val PROP_IMA_CONFIGURATION = "ima" +private const val PROP_IMA_AD_LOAD_TIMEOUT = "adLoadTimeout" private const val PROP_MEDIA_CONTROL = "mediaControl" private const val PROP_PPID = "ppid" private const val PROP_MAX_REDIRECTS = "maxRedirects" @@ -169,11 +170,17 @@ class PlayerConfigAdapter(private val configProps: ReadableMap?) { } } } - // bitrate is configured under the ima config + // bitrate and timeout are configured under the ima config configProps?.getMap(PROP_ADS_CONFIGURATION)?.getMap(PROP_IMA_CONFIGURATION)?.run { if (hasKey(PROP_BITRATE)) { bitrateKbps = getInt(PROP_BITRATE) } + + // The time needs to be in milliseconds on android but seconds on ios. + // we unify the prop from javascript by multiplying it by 1000 here + if (hasKey(PROP_IMA_AD_LOAD_TIMEOUT)) { + setLoadVideoTimeout(getInt(PROP_IMA_AD_LOAD_TIMEOUT) * 1000) + } } } } diff --git a/android/src/main/java/com/theoplayer/ReactTHEOplayerContext.kt b/android/src/main/java/com/theoplayer/ReactTHEOplayerContext.kt index e8bdd9dfd..93e552db8 100644 --- a/android/src/main/java/com/theoplayer/ReactTHEOplayerContext.kt +++ b/android/src/main/java/com/theoplayer/ReactTHEOplayerContext.kt @@ -205,7 +205,7 @@ class ReactTHEOplayerContext private constructor( } private fun initializePlayerView() { - playerView = object : THEOplayerView(reactContext.currentActivity!!, configAdapter.playerConfig()) { + playerView = object : THEOplayerView(reactContext, configAdapter.playerConfig()) { private fun measureAndLayout() { measure( MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY), diff --git a/example/src/App.tsx b/example/src/App.tsx index 17696e5d5..2e88ea532 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -83,7 +83,7 @@ export default function App() { player.source = SOURCES[0].source; - player.backgroundAudioConfiguration = { enabled: true }; + player.backgroundAudioConfiguration = { enabled: true, shouldResumeAfterInterruption: true }; player.pipConfiguration = { startsAutomatically: true }; console.log('THEOplayer is ready'); diff --git a/ios/THEOplayerRCTBridge.m b/ios/THEOplayerRCTBridge.m index 5f795882c..a59bb8d13 100644 --- a/ios/THEOplayerRCTBridge.m +++ b/ios/THEOplayerRCTBridge.m @@ -112,8 +112,6 @@ @interface RCT_EXTERN_REMAP_MODULE(THEORCTPlayerModule, THEOplayerRCTPlayerAPI, RCT_EXTERN_METHOD(setTextTrackStyle:(nonnull NSNumber *)node textTrackStyle:(NSDictionary)textTrackStyle) -RCT_EXTERN_METHOD(destroyPlayer:(nonnull NSNumber *)node); - @end // ---------------------------------------------------------------------------- diff --git a/ios/THEOplayerRCTPlayerAPI.swift b/ios/THEOplayerRCTPlayerAPI.swift index 9d2eff052..789176d97 100644 --- a/ios/THEOplayerRCTPlayerAPI.swift +++ b/ios/THEOplayerRCTPlayerAPI.swift @@ -356,14 +356,4 @@ class THEOplayerRCTPlayerAPI: NSObject, RCTBridgeModule { } } } - - @objc(destroyPlayer:) - func destroyPlayer(_ node: NSNumber) -> Void { - DispatchQueue.main.async { - if let theView = self.bridge.uiManager.view(forReactTag: node) as? THEOplayerRCTView { - theView.destroyPlayer() - } - } - } - } diff --git a/ios/THEOplayerRCTView.swift b/ios/THEOplayerRCTView.swift index a3c0738da..9c5ef2e77 100644 --- a/ios/THEOplayerRCTView.swift +++ b/ios/THEOplayerRCTView.swift @@ -9,15 +9,16 @@ public class THEOplayerRCTView: UIView { public private(set) var player: THEOplayer? public private(set) var mainEventHandler: THEOplayerRCTMainEventHandler public private(set) var broadcastEventHandler: THEOplayerRCTBroadcastEventHandler - let theoPlayerViewController = UIViewController() var textTrackEventHandler: THEOplayerRCTTextTrackEventHandler var mediaTrackEventHandler: THEOplayerRCTMediaTrackEventHandler var metadataTrackEventHandler: THEOplayerRCTSideloadedMetadataTrackEventHandler var adEventHandler: THEOplayerRCTAdsEventHandler var castEventHandler: THEOplayerRCTCastEventHandler var presentationModeManager: THEOplayerRCTPresentationModeManager + var backgroundAudioManager: THEOplayerRCTBackgroundAudioManager var nowPlayingManager: THEOplayerRCTNowPlayingManager var remoteCommandsManager: THEOplayerRCTRemoteCommandsManager + var pipManager: THEOplayerRCTPipManager var pipControlsManager: THEOplayerRCTPipControlsManager var adsConfig = AdsConfig() @@ -36,8 +37,8 @@ public class THEOplayerRCTView: UIView { } var backgroundAudioConfig = BackgroundAudioConfig() { didSet { - self.updateInterruptionNotifications() - self.updateAVAudioSessionMode() + self.backgroundAudioManager.updateInterruptionNotifications() + self.backgroundAudioManager.updateAVAudioSessionMode() } } @@ -61,8 +62,10 @@ public class THEOplayerRCTView: UIView { self.adEventHandler = THEOplayerRCTAdsEventHandler() self.castEventHandler = THEOplayerRCTCastEventHandler() self.presentationModeManager = THEOplayerRCTPresentationModeManager() + self.backgroundAudioManager = THEOplayerRCTBackgroundAudioManager() self.nowPlayingManager = THEOplayerRCTNowPlayingManager() self.remoteCommandsManager = THEOplayerRCTRemoteCommandsManager() + self.pipManager = THEOplayerRCTPipManager() self.pipControlsManager = THEOplayerRCTPipControlsManager() super.init(frame: .zero) @@ -71,20 +74,32 @@ public class THEOplayerRCTView: UIView { required init?(coder aDecoder: NSCoder) { fatalError("[NATIVE] init(coder:) has not been implemented") } + + deinit { + self.mainEventHandler.destroy() + self.textTrackEventHandler.destroy() + self.mediaTrackEventHandler.destroy() + self.adEventHandler.destroy() + self.castEventHandler.destroy() + self.nowPlayingManager.destroy() + self.remoteCommandsManager.destroy() + self.pipManager.destroy() + self.pipControlsManager.destroy() + self.presentationModeManager.destroy() + self.backgroundAudioManager.destroy() + + self.destroyBackgroundAudio() + self.player?.removeAllIntegrations() + self.player?.destroy() + self.player = nil + if DEBUG_THEOPLAYER_INTERACTION { PrintUtils.printLog(logText: "[NATIVE] THEOplayer instance destroyed.") } + } override public func layoutSubviews() { super.layoutSubviews() if let player = self.player { player.frame = self.frame player.autoresizingMask = [.flexibleBottomMargin, .flexibleHeight, .flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleWidth] - - // wrap theoPlayerViewController around the view - if theoPlayerViewController.parent == nil, - let parentViewController = self.findViewController() { - parentViewController.addChild(self.theoPlayerViewController) - self.theoPlayerViewController.didMove(toParent: parentViewController) - self.theoPlayerViewController.view = self - } } } @@ -97,12 +112,14 @@ public class THEOplayerRCTView: UIView { self.mainEventHandler.setPlayer(player) self.textTrackEventHandler.setPlayer(player) self.mediaTrackEventHandler.setPlayer(player) - self.presentationModeManager.setPlayer(player, view: self) self.adEventHandler.setPlayer(player) self.castEventHandler.setPlayer(player) self.nowPlayingManager.setPlayer(player) self.remoteCommandsManager.setPlayer(player) self.pipControlsManager.setPlayer(player) + self.presentationModeManager.setPlayer(player, view: self) + self.backgroundAudioManager.setPlayer(player, view: self) + self.pipManager.setView(view: self) // Attach player to view player.addAsSubview(of: self) } @@ -137,28 +154,6 @@ public class THEOplayerRCTView: UIView { return self.player } - // MARK: - Destroy Player - - public func destroyPlayer() { - self.mainEventHandler.destroy() - self.textTrackEventHandler.destroy() - self.mediaTrackEventHandler.destroy() - self.adEventHandler.destroy() - self.castEventHandler.destroy() - self.nowPlayingManager.destroy() - self.remoteCommandsManager.destroy() - self.pipControlsManager.destroy() - - self.destroyBackgroundAudio() - self.player?.removeAllIntegrations() - self.player?.destroy() - self.player = nil - if DEBUG_THEOPLAYER_INTERACTION { PrintUtils.printLog(logText: "[NATIVE] THEOplayer instance destroyed.") } - - self.theoPlayerViewController.view = nil - self.theoPlayerViewController.removeFromParent() - } - func processMetadataTracks(metadataTrackDescriptions: [TextTrackDescription]?) { THEOplayerRCTSideloadedMetadataProcessor.loadTrackInfoFromTrackDescriptions(metadataTrackDescriptions) { tracksInfo in self.mainEventHandler.setLoadedMetadataTracksInfo(tracksInfo) diff --git a/ios/THEOplayerRCTViewManager.swift b/ios/THEOplayerRCTViewManager.swift index 227314582..20b605151 100644 --- a/ios/THEOplayerRCTViewManager.swift +++ b/ios/THEOplayerRCTViewManager.swift @@ -16,13 +16,4 @@ class THEOplayerRCTViewManager: RCTViewManager { override class func requiresMainQueueSetup() -> Bool { return true } - - @objc func destroy(_ node: NSNumber) { - DispatchQueue.main.async { - let theView = self.bridge.uiManager.view( - forReactTag: node - ) as! THEOplayerRCTView - theView.destroyPlayer() - } - } } diff --git a/ios/ads/THEOplayerRCTView+Ads.swift b/ios/ads/THEOplayerRCTView+Ads.swift index b15229cfc..e0ad54d6e 100644 --- a/ios/ads/THEOplayerRCTView+Ads.swift +++ b/ios/ads/THEOplayerRCTView+Ads.swift @@ -57,6 +57,10 @@ extension THEOplayerRCTView { imaRenderSettings.mimeTypes = allowedMimeTypes } + if let adLoadTimeout = self.adsConfig.adsImaConfig.adLoadTimeout { + imaRenderSettings.loadVideoTimeout = adLoadTimeout + } + // setup integration let imaIntegration = GoogleIMAIntegrationFactory.createIntegration(on: player, with: imaSettings) imaIntegration.renderingSettings = imaRenderSettings diff --git a/ios/ads/THEOplayerRCTView+AdsConfig.swift b/ios/ads/THEOplayerRCTView+AdsConfig.swift index 145a3a10b..191a02042 100644 --- a/ios/ads/THEOplayerRCTView+AdsConfig.swift +++ b/ios/ads/THEOplayerRCTView+AdsConfig.swift @@ -21,14 +21,16 @@ struct AdsImaConfig { var autoPlayAdBreaks: Bool? var sessionID: String? var bitrate: Int + var adLoadTimeout: TimeInterval? - init(maxRedirects: UInt, enableDebugMode: Bool, ppid: String? = nil, featureFlags: [String : String]? = nil, autoPlayAdBreaks: Bool? = nil, sessionID: String? = nil, bitrate: Int? = -1) { + init(maxRedirects: UInt, enableDebugMode: Bool, ppid: String? = nil, featureFlags: [String : String]? = nil, autoPlayAdBreaks: Bool? = nil, sessionID: String? = nil, bitrate: Int? = -1, adLoadTimeout: TimeInterval? = nil) { self.maxRedirects = maxRedirects self.enableDebugMode = enableDebugMode self.ppid = ppid self.featureFlags = featureFlags self.autoPlayAdBreaks = autoPlayAdBreaks self.sessionID = sessionID + self.adLoadTimeout = adLoadTimeout #if canImport(THEOplayerGoogleIMAIntegration) self.bitrate = bitrate ?? kIMAAutodetectBitrate #else @@ -67,6 +69,9 @@ extension THEOplayerRCTView { if let bitrate = adsImaConfig["bitrate"] as? Int { self.adsConfig.adsImaConfig.bitrate = bitrate } + if let adLoadTimeout = adsImaConfig["adLoadTimeout"] as? TimeInterval { + self.adsConfig.adsImaConfig.adLoadTimeout = adLoadTimeout + } } } } diff --git a/ios/backgroundAudio/THEOplayerRCTBackgroundAudioManager.swift b/ios/backgroundAudio/THEOplayerRCTBackgroundAudioManager.swift new file mode 100644 index 000000000..147a1973a --- /dev/null +++ b/ios/backgroundAudio/THEOplayerRCTBackgroundAudioManager.swift @@ -0,0 +1,105 @@ +// TTHEOplayerRCTBackgroundAudioManager.swift + +import Foundation +import THEOplayerSDK +import AVFAudio +import AVKit + +struct BackgroundAudioConfig { + var enabled: Bool = false + var shouldResumeAfterInterruption: Bool = false + var audioSessionMode: AVAudioSession.Mode = .moviePlayback +} + +class THEOplayerRCTBackgroundAudioManager: NSObject, BackgroundPlaybackDelegate { + // MARK: Members + private weak var player: THEOplayer? + private weak var view: THEOplayerRCTView? + + // MARK: - player setup / breakdown + func setPlayer(_ player: THEOplayer, view: THEOplayerRCTView?) { + self.player = player + self.view = view + } + + // MARK: - destruction + func destroy() { + self.cancelInterruptionNotifications() + } + + // MARK: - logic + func shouldContinueAudioPlaybackInBackground() -> Bool { + if let view = self.view { + view.nowPlayingManager.updateNowPlaying() + return view.backgroundAudioConfig.enabled + } + return false + } + + func cancelInterruptionNotifications() { + NotificationCenter.default.removeObserver(self, + name: AVAudioSession.interruptionNotification, + object: AVAudioSession.sharedInstance()) + } + + func updateInterruptionNotifications() { + guard let view = self.view else { return } + + // Get the default notification center instance. + if view.backgroundAudioConfig.shouldResumeAfterInterruption { + NotificationCenter.default.addObserver(self, + selector: #selector(handleInterruption), + name: AVAudioSession.interruptionNotification, + object: AVAudioSession.sharedInstance()) + } else { + NotificationCenter.default.removeObserver(self, + name: AVAudioSession.interruptionNotification, + object: AVAudioSession.sharedInstance()) + } + } + + func updateAVAudioSessionMode() { + guard let view = self.view else { return } + + do { + THEOplayer.automaticallyManageAudioSession = (view.backgroundAudioConfig.audioSessionMode == .moviePlayback) + try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: view.backgroundAudioConfig.audioSessionMode) + if view.backgroundAudioConfig.audioSessionMode != .moviePlayback { + if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[NATIVE] AVAudioSession mode updated to \(view.backgroundAudioConfig.audioSessionMode.rawValue)") } + } + } catch { + if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[NATIVE] Unable to update AVAudioSession mode to \(view.backgroundAudioConfig.audioSessionMode.rawValue): \(error)") } + } + } + + @objc func handleInterruption(notification: Notification) { + guard let userInfo = notification.userInfo, + let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, + let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { + return + } + + // Switch over the interruption type. + switch type { + case .began: + // An interruption began. Update the UI as necessary. + if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[NATIVE] An interruption began")} + case .ended: + // An interruption ended. Resume playback, if appropriate. + if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[NATIVE] An interruption ended")} + guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return } + let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) + if options.contains(.shouldResume) { + // An interruption ended. Resume playback. + if let player = self.player { + if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[NATIVE] Ended interruption should resume playback => play()")} + player.play() + } + } else { + // An interruption ended. Don't resume playback. + if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[NATIVE] Ended interruption should not resume playback.")} + } + default: () + } + } +} diff --git a/ios/backgroundAudio/THEOplayerRCTView+BackgroundAudioConfig.swift b/ios/backgroundAudio/THEOplayerRCTView+BackgroundAudioConfig.swift index 4d2ff6728..ff0039c43 100644 --- a/ios/backgroundAudio/THEOplayerRCTView+BackgroundAudioConfig.swift +++ b/ios/backgroundAudio/THEOplayerRCTView+BackgroundAudioConfig.swift @@ -2,96 +2,25 @@ import Foundation import THEOplayerSDK -import AVFAudio -import AVKit - -struct BackgroundAudioConfig { - var enabled: Bool = false - var shouldResumeAfterInterruption: Bool = false - var audioSessionMode: AVAudioSession.Mode = .moviePlayback -} - -extension THEOplayerRCTView: BackgroundPlaybackDelegate { +extension THEOplayerRCTView { func initBackgroundAudio() { - self.player?.backgroundPlaybackDelegate = self - } - - func destroyBackgroundAudio() { guard let player = self.player else { return } - player.backgroundPlaybackDelegate = DefaultBackgroundPlaybackDelegate() - NotificationCenter.default.removeObserver(self, - name: AVAudioSession.interruptionNotification, - object: AVAudioSession.sharedInstance()) - } - - public func shouldContinueAudioPlaybackInBackground() -> Bool { - // Make sure to go to the background with updated NowPlayingInfo - self.nowPlayingManager.updateNowPlaying() - - return self.backgroundAudioConfig.enabled - } - - func updateInterruptionNotifications() { - // Get the default notification center instance. - if self.backgroundAudioConfig.shouldResumeAfterInterruption { - NotificationCenter.default.addObserver(self, - selector: #selector(handleInterruption), - name: AVAudioSession.interruptionNotification, - object: AVAudioSession.sharedInstance()) - } else { - NotificationCenter.default.removeObserver(self, - name: AVAudioSession.interruptionNotification, - object: AVAudioSession.sharedInstance()) - } - } - - func updateAVAudioSessionMode() { - do { - THEOplayer.automaticallyManageAudioSession = (self.backgroundAudioConfig.audioSessionMode == .moviePlayback) - try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: self.backgroundAudioConfig.audioSessionMode) - if self.backgroundAudioConfig.audioSessionMode != .moviePlayback { - print("[NATIVE] AVAudioSession mode updated to \(self.backgroundAudioConfig.audioSessionMode.rawValue)") - } - } catch { - print("[NATIVE] Unable to update AVAudioSession mode to \(self.backgroundAudioConfig.audioSessionMode.rawValue): ", error) - } + player.backgroundPlaybackDelegate = self.backgroundAudioManager } - @objc func handleInterruption(notification: Notification) { - guard let userInfo = notification.userInfo, - let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, - let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { + func destroyBackgroundAudio() { + guard let player = self.player else { return } - - // Switch over the interruption type. - switch type { - case .began: - // An interruption began. Update the UI as necessary. - if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[INTERRUPTION] An interruption began")} - case .ended: - // An interruption ended. Resume playback, if appropriate. - if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[INTERRUPTION] An interruption ended")} - guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return } - let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) - if options.contains(.shouldResume) { - // An interruption ended. Resume playback. - if let player = self.player { - if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[INTERRUPTION] Ended interruption should resume playback => play()")} - player.play() - } - } else { - // An interruption ended. Don't resume playback. - if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[INTERRUPTION] Ended interruption should not resume playback.")} - } - default: () - } + player.backgroundPlaybackDelegate = DefaultBackgroundPlaybackDelegate() } } struct DefaultBackgroundPlaybackDelegate: BackgroundPlaybackDelegate { - func shouldContinueAudioPlaybackInBackground() -> Bool { false } + func shouldContinueAudioPlaybackInBackground() -> Bool { + return false + } } diff --git a/ios/pip/THEOplayerRCTPipManager.swift b/ios/pip/THEOplayerRCTPipManager.swift new file mode 100644 index 000000000..6203470e3 --- /dev/null +++ b/ios/pip/THEOplayerRCTPipManager.swift @@ -0,0 +1,34 @@ +// TTHEOplayerRCTPipManager.swift + +import Foundation +import AVKit +import THEOplayerSDK + +class THEOplayerRCTPipManager: NSObject, AVPictureInPictureControllerDelegate { + + // MARK: Members + private weak var view: THEOplayerRCTView? + + // MARK: - player setup / breakdown + func setView(view: THEOplayerRCTView?) { + self.view = view + } + + func destroy() {} + + // MARK: - AVPictureInPictureControllerDelegate + @available(tvOS 14.0, *) + public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + if let view = self.view { + view.presentationModeManager.presentationModeContext.pipContext = .PIP_CLOSED + view.pipControlsManager.willStartPip() + } + } + + @available(tvOS 14.0, *) + public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { + if let view = self.view { + view.presentationModeManager.presentationModeContext.pipContext = .PIP_RESTORED + } + } +} diff --git a/ios/pip/THEOplayerRCTView+PipConfig.swift b/ios/pip/THEOplayerRCTView+PipConfig.swift index 459a29c3c..2b3009d86 100644 --- a/ios/pip/THEOplayerRCTView+PipConfig.swift +++ b/ios/pip/THEOplayerRCTView+PipConfig.swift @@ -8,7 +8,7 @@ struct PipConfig { var canStartPictureInPictureAutomaticallyFromInline: Bool = false } -extension THEOplayerRCTView: AVPictureInPictureControllerDelegate { +extension THEOplayerRCTView { func playerPipConfiguration() -> PiPConfiguration { let builder = PiPConfigurationBuilder() @@ -25,20 +25,9 @@ extension THEOplayerRCTView: AVPictureInPictureControllerDelegate { if let player = self.player, var pipController = player.pip { if #available(iOS 14.0, tvOS 14.0, *) { - pipController.nativePictureInPictureDelegate = self + pipController.nativePictureInPictureDelegate = self.pipManager } } } - - // MARK: - AVPictureInPictureControllerDelegate - @available(tvOS 14.0, *) - public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { - self.presentationModeManager.presentationModeContext.pipContext = .PIP_CLOSED - self.pipControlsManager.willStartPip() - } - - @available(tvOS 14.0, *) - public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { - self.presentationModeManager.presentationModeContext.pipContext = .PIP_RESTORED - } } + diff --git a/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift b/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift index 4b9471ac5..22f425c9b 100644 --- a/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift +++ b/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift @@ -11,9 +11,11 @@ public class THEOplayerRCTPresentationModeManager { var presentationModeContext = THEOplayerRCTPresentationModeContext() private var presentationMode: THEOplayerSDK.PresentationMode = .inline private var rnInlineMode: THEOplayerSDK.PresentationMode = .inline // while native player is inline, RN player can be inline or fullsceen + private var movingChildVCs: [UIViewController] = [] // list of playerView's child VCs that need to be reparented while moving the playerView + - private var containerView: UIView? // view containing the playerView and it's siblings (e.g. UI) - private var inlineParentView: UIView? // target view for inline representation + private weak var containerView: UIView? // view containing the playerView and it's siblings (e.g. UI) + private weak var inlineParentView: UIView? // target view for inline representation // MARK: Events var onNativePresentationModeChange: RCTDirectEventBlock? @@ -25,6 +27,7 @@ public class THEOplayerRCTPresentationModeManager { func destroy() { // dettach listeners self.dettachListeners() + self.clearMovingVCs() } // MARK: - player setup / breakdown @@ -37,22 +40,35 @@ public class THEOplayerRCTPresentationModeManager { } // MARK: - logic - - private func moveView(_ movingView: UIView, to targetView: UIView) { - guard let theoPlayerViewController = (self.view as? THEOplayerRCTView)?.theoPlayerViewController else { return } + private func storeMovingVCs(for view: UIView) { + if let viewController = view.findViewController() { + viewController.children.forEach { childVC in + self.movingChildVCs.append(childVC) + } + } + } - // detach the viewController from its parent - theoPlayerViewController.removeFromParent() + private func clearMovingVCs() { + self.movingChildVCs = [] + } + + private func moveView(_ movingView: UIView, to targetView: UIView) { + // detach the moving viewControllers from their parent + self.movingChildVCs.forEach { movedVC in + movedVC.removeFromParent() + } // move the actual view movingView.removeFromSuperview() targetView.addSubview(movingView) targetView.bringSubviewToFront(movingView) - // attach the viewController to its new parent + // attach the moving viewControllers to their new parent if let targetViewController = targetView.findViewController() { - targetViewController.addChild(theoPlayerViewController) - theoPlayerViewController.didMove(toParent: targetViewController) + self.movingChildVCs.forEach { movedVC in + targetViewController.addChild(movedVC) + movedVC.didMove(toParent: targetViewController) + } } } @@ -63,6 +79,7 @@ public class THEOplayerRCTPresentationModeManager { // move the player if let containerView = self.containerView, let fullscreenParentView = self.view?.findParentViewOfType(RCTRootContentView.self) { + self.storeMovingVCs(for: containerView) self.moveView(containerView, to: fullscreenParentView) // start hiding home indicator @@ -79,6 +96,7 @@ public class THEOplayerRCTPresentationModeManager { if let containerView = self.containerView, let inlineParentView = self.inlineParentView { self.moveView(containerView, to: inlineParentView) + self.clearMovingVCs() } self.rnInlineMode = .inline } diff --git a/package-lock.json b/package-lock.json index 057151145..7d9dd9f28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-native-theoplayer", - "version": "8.11.1", + "version": "8.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-native-theoplayer", - "version": "8.11.1", + "version": "8.12.0", "license": "SEE LICENSE AT https://www.theoplayer.com/terms", "dependencies": { "buffer": "^6.0.3" diff --git a/package.json b/package.json index 236c12f1d..5b2c28afd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-theoplayer", - "version": "8.11.1", + "version": "8.12.0", "description": "A THEOplayer video component for react-native.", "main": "lib/commonjs/index", "module": "lib/module/index", diff --git a/src/api/ads/GoogleImaConfiguration.ts b/src/api/ads/GoogleImaConfiguration.ts index 419578e7e..3cc637449 100644 --- a/src/api/ads/GoogleImaConfiguration.ts +++ b/src/api/ads/GoogleImaConfiguration.ts @@ -52,4 +52,10 @@ export interface GoogleImaConfiguration { * @defaultValue `-1` */ bitrate?: number; + + /** + * The amount of time that the SDK will wait before moving onto the next ad for loading. + * This value will be specified in seconds. + */ + adLoadTimeout?: number; } diff --git a/src/api/player/THEOplayer.ts b/src/api/player/THEOplayer.ts index 4811c1313..eab3f017f 100644 --- a/src/api/player/THEOplayer.ts +++ b/src/api/player/THEOplayer.ts @@ -72,11 +72,6 @@ export interface THEOplayer extends EventDispatcher { */ pause(): void; - /** - * destroy the player. - */ - destroy(): void; - /** * Whether the player is paused. */ diff --git a/src/internal/THEOplayerView.tsx b/src/internal/THEOplayerView.tsx index 1dfaf696d..2f0165c2e 100644 --- a/src/internal/THEOplayerView.tsx +++ b/src/internal/THEOplayerView.tsx @@ -144,7 +144,7 @@ export class THEOplayerView extends PureComponent im } } - destroy(): void { - if (Platform.OS === 'ios') { - NativePlayerModule.destroyPlayer(this._view.nativeHandle); - } - } - public get version(): PlayerVersion { return this._playerVersion; } diff --git a/src/internal/adapter/WebEventForwarder.ts b/src/internal/adapter/WebEventForwarder.ts index 8f30f6bdf..832dd2a29 100644 --- a/src/internal/adapter/WebEventForwarder.ts +++ b/src/internal/adapter/WebEventForwarder.ts @@ -18,7 +18,7 @@ import type { VolumeChangeEvent as NativeVolumeChangeEvent, DimensionChangeEvent as NativeDimensionChangeEvent, } from 'theoplayer'; -import type { AdEvent, MediaTrack, TextTrack, TimeRange } from 'react-native-theoplayer'; +import type { AdEvent, MediaTrack, TimeRange } from 'react-native-theoplayer'; import { AdEventType, CastState, @@ -267,7 +267,7 @@ export class WebEventForwarder { track.addEventListener('removecue', this.onRemoveTextTrackCue(track)); track.addEventListener('entercue', this.onEnterTextTrackCue(track)); track.addEventListener('exitcue', this.onExitTextTrackCue(track)); - this._facade.dispatchEvent(new DefaultTextTrackListEvent(TrackListEventType.ADD_TRACK, track as TextTrack)); + this._facade.dispatchEvent(new DefaultTextTrackListEvent(TrackListEventType.ADD_TRACK, fromNativeTextTrack(track))); }; private readonly onRemoveTextTrack = (event: RemoveTrackEvent) => { @@ -276,11 +276,11 @@ export class WebEventForwarder { track.removeEventListener('removecue', this.onRemoveTextTrackCue(track)); track.removeEventListener('entercue', this.onEnterTextTrackCue(track)); track.removeEventListener('exitcue', this.onExitTextTrackCue(track)); - this._facade.dispatchEvent(new DefaultTextTrackListEvent(TrackListEventType.REMOVE_TRACK, track as NativeTextTrack as TextTrack)); + this._facade.dispatchEvent(new DefaultTextTrackListEvent(TrackListEventType.REMOVE_TRACK, fromNativeTextTrack(track))); }; private readonly onChangeTextTrack = (event: TrackChangeEvent) => { - this._facade.dispatchEvent(new DefaultTextTrackListEvent(TrackListEventType.CHANGE_TRACK, event.track as NativeTextTrack as TextTrack)); + this._facade.dispatchEvent(new DefaultTextTrackListEvent(TrackListEventType.CHANGE_TRACK, fromNativeTextTrack(event.track as NativeTextTrack))); }; private readonly onAddAudioTrack = (event: AddTrackEvent) => { diff --git a/src/internal/adapter/event/DefaultEventDispatcher.ts b/src/internal/adapter/event/DefaultEventDispatcher.ts index 56ffba07c..96c0dab94 100644 --- a/src/internal/adapter/event/DefaultEventDispatcher.ts +++ b/src/internal/adapter/event/DefaultEventDispatcher.ts @@ -1,17 +1,25 @@ -import type { EventDispatcher, EventMap, StringKeyOf } from '../../../api/event/EventDispatcher'; -import type { EventListener } from '../../../api/event/EventListener'; +import type { EventDispatcher, EventMap, StringKeyOf } from 'react-native-theoplayer'; +import type { EventListener } from 'react-native-theoplayer'; import { arrayRemoveElement } from '../../utils/arrayUtil'; export class DefaultEventDispatcher>> implements EventDispatcher { readonly _eventListeners: Map, EventListener]>[]> = new Map(); - addEventListener>(type: K, listener: EventListener): void { + addEventListener>(type: K | readonly K[], listener: EventListener): void { + if (typeof listener !== 'function') { + return; + } else if (Array.isArray(type)) { + type.forEach((t) => this.addSingleEventListener(t, listener)); + } else { + this.addSingleEventListener(type as K, listener); + } + } + + private addSingleEventListener>(type: K, listener: EventListener): void { if (!this._eventListeners.has(type)) { - // @ts-ignore - this._eventListeners.set(type, [listener]); + this._eventListeners.set(type, [listener as EventListener]>]); } else { - // @ts-ignore - this._eventListeners.get(type)?.push(listener); + this._eventListeners.get(type)?.push(listener as EventListener]>); } } @@ -26,7 +34,17 @@ export class DefaultEventDispatcher>> im } }; - removeEventListener>(type: K, listener: EventListener): void { + removeEventListener>(type: K | readonly K[], listener: EventListener): void { + if (typeof listener !== 'function') { + return; + } else if (Array.isArray(type)) { + type.forEach((t) => this.removeSingleEventListener(t, listener)); + } else { + this.removeSingleEventListener(type as K, listener); + } + } + + private removeSingleEventListener>(type: K, listener: EventListener): void { const listeners = this._eventListeners.get(type); if (listeners) { arrayRemoveElement(listeners, listener);