From f72c658eeefb043be24514df1583a0cc11d1d239 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Wed, 18 Dec 2024 19:01:49 +0100 Subject: [PATCH 01/13] Don't keep strong reference to targetViews --- .../THEOplayerRCTPresentationModeManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift b/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift index 4b9471ac5..ea553ddb0 100644 --- a/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift +++ b/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift @@ -12,8 +12,8 @@ public class THEOplayerRCTPresentationModeManager { 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 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? From 8c87a77d3ca5b2ade4d58ac4a6117cc66a8ceae7 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Wed, 18 Dec 2024 19:06:17 +0100 Subject: [PATCH 02/13] Add changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ff3a5fc..c002d3d5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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). +## [Unreleased] + +### Fixed + +- Fixed a memory leak on iOS, where the presentationModeManager was holding a strong reference to the fullscreen's target and return views + ## [8.11.1] - 24-12-18 ### Fixed From 137ef70e8a5e9e97aabefd2c544be081bc36987e Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Thu, 19 Dec 2024 13:09:54 +0100 Subject: [PATCH 03/13] Rework BackgroundAudioConfig delegate flow to weak view usage --- ...OplayerRCTView+BackgroundAudioConfig.swift | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/ios/backgroundAudio/THEOplayerRCTView+BackgroundAudioConfig.swift b/ios/backgroundAudio/THEOplayerRCTView+BackgroundAudioConfig.swift index 4d2ff6728..5a4e48ffd 100644 --- a/ios/backgroundAudio/THEOplayerRCTView+BackgroundAudioConfig.swift +++ b/ios/backgroundAudio/THEOplayerRCTView+BackgroundAudioConfig.swift @@ -11,10 +11,10 @@ struct BackgroundAudioConfig { var audioSessionMode: AVAudioSession.Mode = .moviePlayback } -extension THEOplayerRCTView: BackgroundPlaybackDelegate { +extension THEOplayerRCTView { func initBackgroundAudio() { - self.player?.backgroundPlaybackDelegate = self + self.player?.backgroundPlaybackDelegate = CustomBackgroundPlaybackDelegate(self) } func destroyBackgroundAudio() { @@ -27,13 +27,6 @@ extension THEOplayerRCTView: BackgroundPlaybackDelegate { 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 { @@ -95,3 +88,15 @@ extension THEOplayerRCTView: BackgroundPlaybackDelegate { struct DefaultBackgroundPlaybackDelegate: BackgroundPlaybackDelegate { func shouldContinueAudioPlaybackInBackground() -> Bool { false } } + +struct CustomBackgroundPlaybackDelegate: BackgroundPlaybackDelegate { + private weak var theoPlayerView: THEOplayerRCTView? + init(_ view: THEOplayerRCTView?) { + theoPlayerView = view + } + + func shouldContinueAudioPlaybackInBackground() -> Bool { + theoPlayerView?.nowPlayingManager.updateNowPlaying() + return theoPlayerView?.backgroundAudioConfig.enabled ?? false + } +} From ce94755da595067c1bc911ef669035f359a01d41 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Thu, 19 Dec 2024 13:10:53 +0100 Subject: [PATCH 04/13] Rework PipConfig delegation flow to weak view usage --- ios/pip/THEOplayerRCTView+PipConfig.swift | 34 +++++++++++++++-------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/ios/pip/THEOplayerRCTView+PipConfig.swift b/ios/pip/THEOplayerRCTView+PipConfig.swift index 459a29c3c..cf16dce5a 100644 --- a/ios/pip/THEOplayerRCTView+PipConfig.swift +++ b/ios/pip/THEOplayerRCTView+PipConfig.swift @@ -25,20 +25,30 @@ 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 = CustomNativePictureInPictureDelegate(self) } } } - - // 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 +} + +class CustomNativePictureInPictureDelegate: NSObject, AVPictureInPictureControllerDelegate { + private weak var theoPlayerView: THEOplayerRCTView? + init(_ view: THEOplayerRCTView?) { + theoPlayerView = view } + + @available(tvOS 14.0, *) + public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + if let view = self.theoPlayerView { + 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.theoPlayerView { + view.presentationModeManager.presentationModeContext.pipContext = .PIP_RESTORED + } + } } From 031bb3cb636b36a9861086df86ddb4ac0e3dab63 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Thu, 19 Dec 2024 13:11:46 +0100 Subject: [PATCH 05/13] Drop ios destroy method bridging --- ios/THEOplayerRCTBridge.m | 2 -- ios/THEOplayerRCTPlayerAPI.swift | 10 ---------- ios/THEOplayerRCTViewManager.swift | 9 --------- 3 files changed, 21 deletions(-) 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/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() - } - } } From d454bdfc89351ef7779c2180112582dcebe609cc Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Thu, 19 Dec 2024 13:12:14 +0100 Subject: [PATCH 06/13] Drop destroy method on THEOplayer interface and facade for mobile --- src/api/player/THEOplayer.ts | 5 ----- src/internal/THEOplayerView.tsx | 2 +- src/internal/adapter/THEOplayerAdapter.ts | 6 ------ 3 files changed, 1 insertion(+), 12 deletions(-) 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; } From 64ead72fc0d983aef66cfd3b54bee34f9512ac56 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Thu, 19 Dec 2024 13:13:09 +0100 Subject: [PATCH 07/13] Trigger ios native player destruction from THEOplayerView deinit --- ios/THEOplayerRCTView.swift | 39 ++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/ios/THEOplayerRCTView.swift b/ios/THEOplayerRCTView.swift index a3c0738da..4fc6ac1ea 100644 --- a/ios/THEOplayerRCTView.swift +++ b/ios/THEOplayerRCTView.swift @@ -71,6 +71,23 @@ 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.pipControlsManager.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() @@ -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) From 0a261b199284d4c2f6b7d96d787410f049d10bb0 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Thu, 19 Dec 2024 13:13:46 +0100 Subject: [PATCH 08/13] Revert ViewController wrapping that keeps strong reference to THEOplayerView --- ios/THEOplayerRCTView.swift | 9 ----- ...THEOplayerRCTPresentationModeManager.swift | 34 ++++++++++++++----- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/ios/THEOplayerRCTView.swift b/ios/THEOplayerRCTView.swift index 4fc6ac1ea..6259d040d 100644 --- a/ios/THEOplayerRCTView.swift +++ b/ios/THEOplayerRCTView.swift @@ -9,7 +9,6 @@ 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 @@ -94,14 +93,6 @@ public class THEOplayerRCTView: UIView { 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 - } } } diff --git a/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift b/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift index ea553ddb0..22f425c9b 100644 --- a/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift +++ b/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift @@ -11,6 +11,8 @@ 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 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 @@ -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 } From b6a0765039d721a811f56eba4465517212fad32c Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Thu, 19 Dec 2024 13:18:52 +0100 Subject: [PATCH 09/13] Add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c002d3d5a..d9d1d043d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### 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. ## [8.11.1] - 24-12-18 From 08ff7856ccee818771201bab46628987414cc164 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Thu, 19 Dec 2024 16:45:52 +0100 Subject: [PATCH 10/13] Extract backgroundAudio management to separate class --- ios/THEOplayerRCTView.swift | 8 +- .../THEOplayerRCTBackgroundAudioManager.swift | 105 ++++++++++++++++++ ...OplayerRCTView+BackgroundAudioConfig.swift | 86 +------------- 3 files changed, 116 insertions(+), 83 deletions(-) create mode 100644 ios/backgroundAudio/THEOplayerRCTBackgroundAudioManager.swift diff --git a/ios/THEOplayerRCTView.swift b/ios/THEOplayerRCTView.swift index 6259d040d..42a73c15b 100644 --- a/ios/THEOplayerRCTView.swift +++ b/ios/THEOplayerRCTView.swift @@ -15,6 +15,7 @@ public class THEOplayerRCTView: UIView { var adEventHandler: THEOplayerRCTAdsEventHandler var castEventHandler: THEOplayerRCTCastEventHandler var presentationModeManager: THEOplayerRCTPresentationModeManager + var backgroundAudioManager: THEOplayerRCTBackgroundAudioManager var nowPlayingManager: THEOplayerRCTNowPlayingManager var remoteCommandsManager: THEOplayerRCTRemoteCommandsManager var pipControlsManager: THEOplayerRCTPipControlsManager @@ -35,8 +36,8 @@ public class THEOplayerRCTView: UIView { } var backgroundAudioConfig = BackgroundAudioConfig() { didSet { - self.updateInterruptionNotifications() - self.updateAVAudioSessionMode() + self.backgroundAudioManager.updateInterruptionNotifications() + self.backgroundAudioManager.updateAVAudioSessionMode() } } @@ -60,6 +61,7 @@ public class THEOplayerRCTView: UIView { self.adEventHandler = THEOplayerRCTAdsEventHandler() self.castEventHandler = THEOplayerRCTCastEventHandler() self.presentationModeManager = THEOplayerRCTPresentationModeManager() + self.backgroundAudioManager = THEOplayerRCTBackgroundAudioManager() self.nowPlayingManager = THEOplayerRCTNowPlayingManager() self.remoteCommandsManager = THEOplayerRCTRemoteCommandsManager() self.pipControlsManager = THEOplayerRCTPipControlsManager() @@ -80,6 +82,7 @@ public class THEOplayerRCTView: UIView { self.nowPlayingManager.destroy() self.remoteCommandsManager.destroy() self.pipControlsManager.destroy() + self.backgroundAudioManager.destroy() self.destroyBackgroundAudio() self.player?.removeAllIntegrations() @@ -106,6 +109,7 @@ public class THEOplayerRCTView: UIView { self.textTrackEventHandler.setPlayer(player) self.mediaTrackEventHandler.setPlayer(player) self.presentationModeManager.setPlayer(player, view: self) + self.backgroundAudioManager.setPlayer(player, view: self) self.adEventHandler.setPlayer(player) self.castEventHandler.setPlayer(player) self.nowPlayingManager.setPlayer(player) 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 5a4e48ffd..ff0039c43 100644 --- a/ios/backgroundAudio/THEOplayerRCTView+BackgroundAudioConfig.swift +++ b/ios/backgroundAudio/THEOplayerRCTView+BackgroundAudioConfig.swift @@ -2,101 +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 { - func initBackgroundAudio() { - self.player?.backgroundPlaybackDelegate = CustomBackgroundPlaybackDelegate(self) - } - - func destroyBackgroundAudio() { guard let player = self.player else { return } - player.backgroundPlaybackDelegate = DefaultBackgroundPlaybackDelegate() - NotificationCenter.default.removeObserver(self, - name: AVAudioSession.interruptionNotification, - object: AVAudioSession.sharedInstance()) - } - - 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()) - } + player.backgroundPlaybackDelegate = self.backgroundAudioManager } - 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) - } - } - - @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 } -} - -struct CustomBackgroundPlaybackDelegate: BackgroundPlaybackDelegate { - private weak var theoPlayerView: THEOplayerRCTView? - init(_ view: THEOplayerRCTView?) { - theoPlayerView = view - } - func shouldContinueAudioPlaybackInBackground() -> Bool { - theoPlayerView?.nowPlayingManager.updateNowPlaying() - return theoPlayerView?.backgroundAudioConfig.enabled ?? false + return false } } From bf26bd64b0d2f12540e2824515281fb491e19b50 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Thu, 19 Dec 2024 16:46:18 +0100 Subject: [PATCH 11/13] Call missing destroy for PresentationModeManager --- ios/THEOplayerRCTView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/THEOplayerRCTView.swift b/ios/THEOplayerRCTView.swift index 42a73c15b..ad75b2e74 100644 --- a/ios/THEOplayerRCTView.swift +++ b/ios/THEOplayerRCTView.swift @@ -82,6 +82,7 @@ public class THEOplayerRCTView: UIView { self.nowPlayingManager.destroy() self.remoteCommandsManager.destroy() self.pipControlsManager.destroy() + self.presentationModeManager.destroy() self.backgroundAudioManager.destroy() self.destroyBackgroundAudio() From 6c1767efa5f7b8d0cf9f34bd86153964aeac8a9d Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Thu, 19 Dec 2024 16:51:30 +0100 Subject: [PATCH 12/13] Add interruption config to example app --- example/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'); From 121883a3f2bf1e8a08c35c7bdadf5349c029f905 Mon Sep 17 00:00:00 2001 From: William Van Haevre Date: Thu, 19 Dec 2024 17:59:48 +0100 Subject: [PATCH 13/13] Extract pip delegate handling to separate class --- ios/THEOplayerRCTView.swift | 8 ++++-- ios/pip/THEOplayerRCTPipManager.swift | 34 +++++++++++++++++++++++ ios/pip/THEOplayerRCTView+PipConfig.swift | 25 ++--------------- 3 files changed, 42 insertions(+), 25 deletions(-) create mode 100644 ios/pip/THEOplayerRCTPipManager.swift diff --git a/ios/THEOplayerRCTView.swift b/ios/THEOplayerRCTView.swift index ad75b2e74..9c5ef2e77 100644 --- a/ios/THEOplayerRCTView.swift +++ b/ios/THEOplayerRCTView.swift @@ -18,6 +18,7 @@ public class THEOplayerRCTView: UIView { var backgroundAudioManager: THEOplayerRCTBackgroundAudioManager var nowPlayingManager: THEOplayerRCTNowPlayingManager var remoteCommandsManager: THEOplayerRCTRemoteCommandsManager + var pipManager: THEOplayerRCTPipManager var pipControlsManager: THEOplayerRCTPipControlsManager var adsConfig = AdsConfig() @@ -64,6 +65,7 @@ public class THEOplayerRCTView: UIView { self.backgroundAudioManager = THEOplayerRCTBackgroundAudioManager() self.nowPlayingManager = THEOplayerRCTNowPlayingManager() self.remoteCommandsManager = THEOplayerRCTRemoteCommandsManager() + self.pipManager = THEOplayerRCTPipManager() self.pipControlsManager = THEOplayerRCTPipControlsManager() super.init(frame: .zero) @@ -81,6 +83,7 @@ public class THEOplayerRCTView: UIView { self.castEventHandler.destroy() self.nowPlayingManager.destroy() self.remoteCommandsManager.destroy() + self.pipManager.destroy() self.pipControlsManager.destroy() self.presentationModeManager.destroy() self.backgroundAudioManager.destroy() @@ -109,13 +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.backgroundAudioManager.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) } 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 cf16dce5a..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,30 +25,9 @@ extension THEOplayerRCTView: AVPictureInPictureControllerDelegate { if let player = self.player, var pipController = player.pip { if #available(iOS 14.0, tvOS 14.0, *) { - pipController.nativePictureInPictureDelegate = CustomNativePictureInPictureDelegate(self) + pipController.nativePictureInPictureDelegate = self.pipManager } } } } -class CustomNativePictureInPictureDelegate: NSObject, AVPictureInPictureControllerDelegate { - private weak var theoPlayerView: THEOplayerRCTView? - init(_ view: THEOplayerRCTView?) { - theoPlayerView = view - } - - @available(tvOS 14.0, *) - public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { - if let view = self.theoPlayerView { - 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.theoPlayerView { - view.presentationModeManager.presentationModeContext.pipContext = .PIP_RESTORED - } - } -}