Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix/ios drop destroy player requirement #466

Merged
merged 13 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ 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
- 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

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 0 additions & 2 deletions ios/THEOplayerRCTBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

// ----------------------------------------------------------------------------
Expand Down
10 changes: 0 additions & 10 deletions ios/THEOplayerRCTPlayerAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}

}
63 changes: 29 additions & 34 deletions ios/THEOplayerRCTView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -36,8 +37,8 @@ public class THEOplayerRCTView: UIView {
}
var backgroundAudioConfig = BackgroundAudioConfig() {
didSet {
self.updateInterruptionNotifications()
self.updateAVAudioSessionMode()
self.backgroundAudioManager.updateInterruptionNotifications()
self.backgroundAudioManager.updateAVAudioSessionMode()
}
}

Expand All @@ -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)
Expand All @@ -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
}
}
}

Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 0 additions & 9 deletions ios/THEOplayerRCTViewManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}
105 changes: 105 additions & 0 deletions ios/backgroundAudio/THEOplayerRCTBackgroundAudioManager.swift
Original file line number Diff line number Diff line change
@@ -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: ()
}
}
}
87 changes: 8 additions & 79 deletions ios/backgroundAudio/THEOplayerRCTView+BackgroundAudioConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Loading
Loading