refactor(ios): add audio session manager (#4466)
This commit is contained in:
325
ios/Video/AudioSessionManager.swift
Normal file
325
ios/Video/AudioSessionManager.swift
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
import AVFoundation
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class AudioSessionManager {
|
||||||
|
static let shared = AudioSessionManager()
|
||||||
|
|
||||||
|
private var videoViews = NSHashTable<RCTVideo>.weakObjects()
|
||||||
|
private var isAudioSessionActive = false
|
||||||
|
private var remoteControlEventsActive = false
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
// Subscribe to audio interruption notifications
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleAudioSessionInterruption),
|
||||||
|
name: AVAudioSession.interruptionNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
// Subscribe to route change notifications
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleAudioRouteChange),
|
||||||
|
name: AVAudioSession.routeChangeNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public API
|
||||||
|
|
||||||
|
func registerView(view: RCTVideo) {
|
||||||
|
if videoViews.contains(view) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
videoViews.add(view)
|
||||||
|
updateAudioSessionConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unregisterView(view: RCTVideo) {
|
||||||
|
if !videoViews.contains(view) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
videoViews.remove(view)
|
||||||
|
updateAudioSessionConfiguration()
|
||||||
|
|
||||||
|
if videoViews.allObjects.isEmpty && !remoteControlEventsActive {
|
||||||
|
deactivateAudioSession()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAudioSessionConfiguration() {
|
||||||
|
// Activate audio session if needed
|
||||||
|
let isAnyPlayerPlaying = videoViews.allObjects.contains { view in
|
||||||
|
return !view.isMuted() && view._player != nil && view._player?.rate != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAnyPlayerPlaying || remoteControlEventsActive {
|
||||||
|
activateAudioSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
configureAudioSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle remote control events from NowPlayingInfoCenterManager
|
||||||
|
func setRemoteControlEventsActive(_ active: Bool) {
|
||||||
|
remoteControlEventsActive = active
|
||||||
|
|
||||||
|
if active {
|
||||||
|
// Force playback category and activate session when remote control events are active
|
||||||
|
configureForRemoteControlEvents()
|
||||||
|
} else {
|
||||||
|
// If no active players, we can deactivate the session
|
||||||
|
if !videoViews.allObjects.contains(where: { view in
|
||||||
|
return view._player != nil && view._player?.rate != 0
|
||||||
|
}) {
|
||||||
|
deactivateAudioSession()
|
||||||
|
} else {
|
||||||
|
// Otherwise reconfigure based on current players
|
||||||
|
updateAudioSessionConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification that a player's properties have changed
|
||||||
|
func playerPropertiesChanged(view: RCTVideo) {
|
||||||
|
// Only update if this is a registered view
|
||||||
|
if videoViews.contains(view) {
|
||||||
|
updateAudioSessionConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Audio Session Configuration
|
||||||
|
|
||||||
|
private func configureForRemoteControlEvents() {
|
||||||
|
let audioSession = AVAudioSession.sharedInstance()
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Remote control events always need playback category
|
||||||
|
try audioSession.setCategory(.playback, mode: .moviePlayback)
|
||||||
|
activateAudioSession()
|
||||||
|
} catch {
|
||||||
|
print(
|
||||||
|
"Failed to configure audio session for remote control events: \(error.localizedDescription)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureAudioSession() {
|
||||||
|
let audioSession = AVAudioSession.sharedInstance()
|
||||||
|
var options: AVAudioSession.CategoryOptions = []
|
||||||
|
|
||||||
|
// Check player properties
|
||||||
|
let anyPlayerShowNotificationControls = videoViews.allObjects.contains { view in
|
||||||
|
return view._showNotificationControls
|
||||||
|
}
|
||||||
|
|
||||||
|
let anyPlayerNeedsPiP = videoViews.allObjects.contains { view in
|
||||||
|
return view.isPictureInPictureActive()
|
||||||
|
}
|
||||||
|
|
||||||
|
let anyPlayerNeedsBackgroundPlayback = videoViews.allObjects.contains { view in
|
||||||
|
return view._playInBackground
|
||||||
|
}
|
||||||
|
|
||||||
|
let canAllowMixing = !anyPlayerShowNotificationControls && !anyPlayerNeedsBackgroundPlayback
|
||||||
|
|
||||||
|
if canAllowMixing {
|
||||||
|
let shouldEnableMixing = videoViews.allObjects.contains { view in
|
||||||
|
return view._mixWithOthers == "mix"
|
||||||
|
}
|
||||||
|
|
||||||
|
let shouldEnableDucking = videoViews.allObjects.contains { view in
|
||||||
|
return view._mixWithOthers == "duck"
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldEnableMixing && shouldEnableDucking {
|
||||||
|
print(
|
||||||
|
"Warning: Conflicting mixWithOthers settings found (mix vs duck) - defaulting to mix"
|
||||||
|
)
|
||||||
|
options.insert(.mixWithOthers)
|
||||||
|
} else {
|
||||||
|
if shouldEnableMixing {
|
||||||
|
options.insert(.mixWithOthers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldEnableDucking {
|
||||||
|
options.insert(.duckOthers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let isAnyPlayerUsingEarpiece = videoViews.allObjects.contains { view in
|
||||||
|
return view._audioOutput == "earpiece"
|
||||||
|
}
|
||||||
|
|
||||||
|
let isSilentSwitchIgnore = videoViews.allObjects.contains { view in
|
||||||
|
return view._ignoreSilentSwitch == "ignore"
|
||||||
|
}
|
||||||
|
|
||||||
|
let isSilentSwitchObey = videoViews.allObjects.contains { view in
|
||||||
|
return view._ignoreSilentSwitch == "obey"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine audio category based on player requirements
|
||||||
|
let category = determineAudioCategory(
|
||||||
|
silentSwitchObey: isSilentSwitchObey,
|
||||||
|
silentSwitchIgnore: isSilentSwitchIgnore,
|
||||||
|
earpiece: isAnyPlayerUsingEarpiece,
|
||||||
|
pip: anyPlayerNeedsPiP,
|
||||||
|
backgroundPlayback: anyPlayerNeedsBackgroundPlayback,
|
||||||
|
notificationControls: anyPlayerShowNotificationControls
|
||||||
|
)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try audioSession.setCategory(
|
||||||
|
category, mode: .moviePlayback, options: canAllowMixing ? options : []
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configure audio port
|
||||||
|
if isAnyPlayerUsingEarpiece, audioSession.category == .playAndRecord {
|
||||||
|
#if os(iOS) || os(visionOS)
|
||||||
|
try audioSession.overrideOutputAudioPort(.speaker)
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
try audioSession.overrideOutputAudioPort(.none)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Failed to configure audio session: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func determineAudioCategory(
|
||||||
|
silentSwitchObey: Bool,
|
||||||
|
silentSwitchIgnore: Bool,
|
||||||
|
earpiece: Bool,
|
||||||
|
pip: Bool,
|
||||||
|
backgroundPlayback: Bool,
|
||||||
|
notificationControls: Bool
|
||||||
|
) -> AVAudioSession.Category {
|
||||||
|
// Handle conflicting settings
|
||||||
|
if silentSwitchObey && silentSwitchIgnore {
|
||||||
|
print(
|
||||||
|
"Warning: Conflicting ignoreSilentSwitch settings found (obey vs ignore) - defaulting to ignore"
|
||||||
|
)
|
||||||
|
return .playback
|
||||||
|
}
|
||||||
|
|
||||||
|
// PiP, background playback, or notification controls require playback category
|
||||||
|
if pip || backgroundPlayback || notificationControls || remoteControlEventsActive {
|
||||||
|
if silentSwitchObey {
|
||||||
|
print(
|
||||||
|
"Warning: ignoreSilentSwitch=obey cannot be used with PiP, backgroundPlayback, or notification controls - using playback category"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if earpiece {
|
||||||
|
print(
|
||||||
|
"Warning: audioOutput=earpiece cannot be used with PiP, backgroundPlayback, or notification controls - using playback category"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return .playback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Earpiece requires playAndRecord
|
||||||
|
if earpiece {
|
||||||
|
if silentSwitchObey {
|
||||||
|
print(
|
||||||
|
"Warning: audioOutput=earpiece cannot be used with ignoreSilentSwitch=obey - using playAndRecord category"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return .playAndRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
// Honor silent switch if requested
|
||||||
|
if silentSwitchObey {
|
||||||
|
return .ambient
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to playback for most cases
|
||||||
|
return .playback
|
||||||
|
}
|
||||||
|
|
||||||
|
private func activateAudioSession() {
|
||||||
|
if isAudioSessionActive {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
isAudioSessionActive = true
|
||||||
|
} catch {
|
||||||
|
print("Failed to activate audio session: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deactivateAudioSession() {
|
||||||
|
if !isAudioSessionActive {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try AVAudioSession.sharedInstance().setActive(
|
||||||
|
false, options: .notifyOthersOnDeactivation
|
||||||
|
)
|
||||||
|
isAudioSessionActive = false
|
||||||
|
} catch {
|
||||||
|
print("Failed to deactivate audio session: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Notification Handlers
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func handleAudioSessionInterruption(notification: Notification) {
|
||||||
|
guard let userInfo = notification.userInfo,
|
||||||
|
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
|
||||||
|
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case .began:
|
||||||
|
// Audio session interrupted, nothing to do as players will pause automatically
|
||||||
|
break
|
||||||
|
|
||||||
|
case .ended:
|
||||||
|
// Interruption ended, check if we should resume audio session
|
||||||
|
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
|
||||||
|
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
||||||
|
if options.contains(.shouldResume) {
|
||||||
|
updateAudioSessionConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func handleAudioRouteChange(notification: Notification) {
|
||||||
|
guard let userInfo = notification.userInfo,
|
||||||
|
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
|
||||||
|
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue)
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch reason {
|
||||||
|
case .categoryChange, .override, .wakeFromSleep, .newDeviceAvailable, .oldDeviceUnavailable:
|
||||||
|
// Reconfigure audio session when route changes
|
||||||
|
updateAudioSessionConfiguration()
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -128,9 +128,14 @@ enum RCTPlayerOperations {
|
|||||||
await player?.currentItem?.select(mediaOption, in: group)
|
await player?.currentItem?.select(mediaOption, in: group)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func seek(player: AVPlayer, playerItem: AVPlayerItem, paused: Bool, seekTime: Float, seekTolerance: Float, completion: @escaping (Bool) -> Void) {
|
static func seek(
|
||||||
|
player: AVPlayer, playerItem: AVPlayerItem, paused: Bool, seekTime: Float,
|
||||||
|
seekTolerance: Float, completion: @escaping (Bool) -> Void
|
||||||
|
) {
|
||||||
let timeScale = 1000
|
let timeScale = 1000
|
||||||
let cmSeekTime: CMTime = CMTimeMakeWithSeconds(Float64(seekTime), preferredTimescale: Int32(timeScale))
|
let cmSeekTime: CMTime = CMTimeMakeWithSeconds(
|
||||||
|
Float64(seekTime), preferredTimescale: Int32(timeScale)
|
||||||
|
)
|
||||||
let current: CMTime = playerItem.currentTime()
|
let current: CMTime = playerItem.currentTime()
|
||||||
let tolerance: CMTime = CMTimeMake(value: Int64(seekTolerance), timescale: Int32(timeScale))
|
let tolerance: CMTime = CMTimeMake(value: Int64(seekTolerance), timescale: Int32(timeScale))
|
||||||
|
|
||||||
@@ -141,63 +146,11 @@ enum RCTPlayerOperations {
|
|||||||
|
|
||||||
if !paused { player.pause() }
|
if !paused { player.pause() }
|
||||||
|
|
||||||
player.seek(to: cmSeekTime, toleranceBefore: tolerance, toleranceAfter: tolerance, completionHandler: { (finished: Bool) in
|
player.seek(
|
||||||
|
to: cmSeekTime, toleranceBefore: tolerance, toleranceAfter: tolerance,
|
||||||
|
completionHandler: { (finished: Bool) in
|
||||||
completion(finished)
|
completion(finished)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func configureAudio(ignoreSilentSwitch: String, mixWithOthers: String, audioOutput: String) {
|
|
||||||
let audioSession: AVAudioSession! = AVAudioSession.sharedInstance()
|
|
||||||
var category: AVAudioSession.Category?
|
|
||||||
var options: AVAudioSession.CategoryOptions?
|
|
||||||
|
|
||||||
if ignoreSilentSwitch == "ignore" {
|
|
||||||
category = audioOutput == "earpiece" ? AVAudioSession.Category.playAndRecord : AVAudioSession.Category.playback
|
|
||||||
} else if ignoreSilentSwitch == "obey" {
|
|
||||||
category = AVAudioSession.Category.ambient
|
|
||||||
}
|
|
||||||
|
|
||||||
if mixWithOthers == "mix" {
|
|
||||||
options = .mixWithOthers
|
|
||||||
} else if mixWithOthers == "duck" {
|
|
||||||
options = .duckOthers
|
|
||||||
}
|
|
||||||
|
|
||||||
if let category, let options {
|
|
||||||
do {
|
|
||||||
try audioSession.setCategory(category, options: options)
|
|
||||||
} catch {
|
|
||||||
debugPrint("[RCTPlayerOperations] Problem setting up AVAudioSession category and options. Error: \(error).")
|
|
||||||
#if !os(tvOS)
|
|
||||||
// Handle specific set category and option combination error
|
|
||||||
// setCategory:AVAudioSessionCategoryPlayback withOptions:mixWithOthers || duckOthers
|
|
||||||
// Failed to set category, error: 'what' Error Domain=NSOSStatusErrorDomain
|
|
||||||
// https://developer.apple.com/forums/thread/714598
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
do {
|
|
||||||
debugPrint("[RCTPlayerOperations] Reseting AVAudioSession category to playAndRecord with defaultToSpeaker options.")
|
|
||||||
try audioSession.setCategory(
|
|
||||||
audioOutput == "earpiece" ? AVAudioSession.Category.playAndRecord : AVAudioSession.Category.playback,
|
|
||||||
options: AVAudioSession.CategoryOptions.defaultToSpeaker
|
|
||||||
)
|
)
|
||||||
} catch {
|
|
||||||
debugPrint("[RCTPlayerOperations] Reseting AVAudioSession category and options problem. Error: \(error).")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
} else if let category, options == nil {
|
|
||||||
do {
|
|
||||||
try audioSession.setCategory(category)
|
|
||||||
} catch {
|
|
||||||
debugPrint("[RCTPlayerOperations] Problem setting up AVAudioSession category. Error: \(error).")
|
|
||||||
}
|
|
||||||
} else if category == nil, let options {
|
|
||||||
do {
|
|
||||||
try audioSession.setCategory(audioSession.category, options: options)
|
|
||||||
} catch {
|
|
||||||
debugPrint("[RCTPlayerOperations] Problem setting up AVAudioSession options. Error: \(error).")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ class NowPlayingInfoCenterManager {
|
|||||||
|
|
||||||
private let remoteCommandCenter = MPRemoteCommandCenter.shared()
|
private let remoteCommandCenter = MPRemoteCommandCenter.shared()
|
||||||
|
|
||||||
private var receivingRemoveControlEvents = false {
|
var receivingRemoveControlEvents = false {
|
||||||
didSet {
|
didSet {
|
||||||
if receivingRemoveControlEvents {
|
if receivingRemoveControlEvents {
|
||||||
try? AVAudioSession.sharedInstance().setCategory(.playback)
|
AudioSessionManager.shared.setRemoteControlEventsActive(true)
|
||||||
try? AVAudioSession.sharedInstance().setActive(true)
|
|
||||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||||
} else {
|
} else {
|
||||||
UIApplication.shared.endReceivingRemoteControlEvents()
|
UIApplication.shared.endReceivingRemoteControlEvents()
|
||||||
|
AudioSessionManager.shared.setRemoteControlEventsActive(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import React
|
|||||||
// MARK: - RCTVideo
|
// MARK: - RCTVideo
|
||||||
|
|
||||||
class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverHandler {
|
class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverHandler {
|
||||||
private var _player: AVPlayer?
|
var _player: AVPlayer?
|
||||||
private var _playerItem: AVPlayerItem?
|
private var _playerItem: AVPlayerItem?
|
||||||
private var _source: VideoSource?
|
private var _source: VideoSource?
|
||||||
private var _playerLayer: AVPlayerLayer?
|
private var _playerLayer: AVPlayerLayer?
|
||||||
@@ -30,7 +30,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
private var _controls = false
|
private var _controls = false
|
||||||
|
|
||||||
/* Keep track of any modifiers, need to be applied after each play */
|
/* Keep track of any modifiers, need to be applied after each play */
|
||||||
private var _audioOutput: String = "speaker"
|
var _audioOutput: String = "speaker"
|
||||||
private var _volume: Float = 1.0
|
private var _volume: Float = 1.0
|
||||||
private var _rate: Float = 1.0
|
private var _rate: Float = 1.0
|
||||||
private var _maxBitRate: Float?
|
private var _maxBitRate: Float?
|
||||||
@@ -44,12 +44,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
private var _selectedTextTrackCriteria: SelectedTrackCriteria = .none()
|
private var _selectedTextTrackCriteria: SelectedTrackCriteria = .none()
|
||||||
private var _selectedAudioTrackCriteria: SelectedTrackCriteria = .none()
|
private var _selectedAudioTrackCriteria: SelectedTrackCriteria = .none()
|
||||||
private var _playbackStalled = false
|
private var _playbackStalled = false
|
||||||
private var _playInBackground = false
|
var _playInBackground = false
|
||||||
private var _preventsDisplaySleepDuringVideoPlayback = true
|
private var _preventsDisplaySleepDuringVideoPlayback = true
|
||||||
private var _preferredForwardBufferDuration: Float = 0.0
|
private var _preferredForwardBufferDuration: Float = 0.0
|
||||||
private var _playWhenInactive = false
|
private var _playWhenInactive = false
|
||||||
private var _ignoreSilentSwitch: String = "inherit" // inherit, ignore, obey
|
var _ignoreSilentSwitch: String = "inherit" // inherit, ignore, obey
|
||||||
private var _mixWithOthers: String = "inherit" // inherit, mix, duck
|
var _mixWithOthers: String = "inherit" // inherit, mix, duck
|
||||||
private var _resizeMode: String = "cover"
|
private var _resizeMode: String = "cover"
|
||||||
private var _fullscreen = false
|
private var _fullscreen = false
|
||||||
private var _fullscreenAutorotate = true
|
private var _fullscreenAutorotate = true
|
||||||
@@ -60,7 +60,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
private var _filterEnabled = false
|
private var _filterEnabled = false
|
||||||
private var _presentingViewController: UIViewController?
|
private var _presentingViewController: UIViewController?
|
||||||
private var _startPosition: Float64 = -1
|
private var _startPosition: Float64 = -1
|
||||||
private var _showNotificationControls = false
|
var _showNotificationControls = false
|
||||||
// Buffer last bitrate value received. Initialized to -2 to ensure -1 (sometimes reported by AVPlayer) is not missed
|
// Buffer last bitrate value received. Initialized to -2 to ensure -1 (sometimes reported by AVPlayer) is not missed
|
||||||
private var _lastBitrate = -2.0
|
private var _lastBitrate = -2.0
|
||||||
private var _enterPictureInPictureOnLeave = false {
|
private var _enterPictureInPictureOnLeave = false {
|
||||||
@@ -204,6 +204,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
|
|
||||||
_eventDispatcher = eventDispatcher
|
_eventDispatcher = eventDispatcher
|
||||||
|
|
||||||
|
AudioSessionManager.shared.registerView(view: self)
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if _enterPictureInPictureOnLeave {
|
if _enterPictureInPictureOnLeave {
|
||||||
initPictureinPicture()
|
initPictureinPicture()
|
||||||
@@ -286,6 +288,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
_imaAdsManager.releaseAds()
|
_imaAdsManager.releaseAds()
|
||||||
_imaAdsManager = nil
|
_imaAdsManager = nil
|
||||||
#endif
|
#endif
|
||||||
|
AudioSessionManager.shared.unregisterView(view: self)
|
||||||
|
|
||||||
NotificationCenter.default.removeObserver(self)
|
NotificationCenter.default.removeObserver(self)
|
||||||
self.removePlayerLayer()
|
self.removePlayerLayer()
|
||||||
@@ -298,7 +301,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
_pip = nil
|
_pip = nil
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
ReactNativeVideoManager.shared.unregisterView(newInstance: self)
|
ReactNativeVideoManager.shared.unregisterView(newInstance: self)
|
||||||
|
AudioSessionManager.shared.unregisterView(view: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - App lifecycle handlers
|
// MARK: - App lifecycle handlers
|
||||||
@@ -767,13 +772,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
@objc
|
@objc
|
||||||
func setEnterPictureInPictureOnLeave(_ enterPictureInPictureOnLeave: Bool) {
|
func setEnterPictureInPictureOnLeave(_ enterPictureInPictureOnLeave: Bool) {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
let audioSession = AVAudioSession.sharedInstance()
|
|
||||||
do {
|
|
||||||
try audioSession.setCategory(.playback)
|
|
||||||
try audioSession.setActive(true, options: [])
|
|
||||||
} catch {}
|
|
||||||
if _enterPictureInPictureOnLeave != enterPictureInPictureOnLeave {
|
if _enterPictureInPictureOnLeave != enterPictureInPictureOnLeave {
|
||||||
_enterPictureInPictureOnLeave = enterPictureInPictureOnLeave
|
_enterPictureInPictureOnLeave = enterPictureInPictureOnLeave
|
||||||
|
|
||||||
|
AudioSessionManager.shared.playerPropertiesChanged(view: self)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -792,14 +794,15 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
@objc
|
@objc
|
||||||
func setIgnoreSilentSwitch(_ ignoreSilentSwitch: String?) {
|
func setIgnoreSilentSwitch(_ ignoreSilentSwitch: String?) {
|
||||||
_ignoreSilentSwitch = ignoreSilentSwitch ?? "inherit"
|
_ignoreSilentSwitch = ignoreSilentSwitch ?? "inherit"
|
||||||
RCTPlayerOperations.configureAudio(ignoreSilentSwitch: _ignoreSilentSwitch, mixWithOthers: _mixWithOthers, audioOutput: _audioOutput)
|
|
||||||
applyModifiers()
|
AudioSessionManager.shared.playerPropertiesChanged(view: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
func setMixWithOthers(_ mixWithOthers: String?) {
|
func setMixWithOthers(_ mixWithOthers: String?) {
|
||||||
_mixWithOthers = mixWithOthers ?? "inherit"
|
_mixWithOthers = mixWithOthers ?? "inherit"
|
||||||
applyModifiers()
|
|
||||||
|
AudioSessionManager.shared.playerPropertiesChanged(view: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
@@ -814,8 +817,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
_player?.rate = 0.0
|
_player?.rate = 0.0
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
RCTPlayerOperations.configureAudio(ignoreSilentSwitch: _ignoreSilentSwitch, mixWithOthers: _mixWithOthers, audioOutput: _audioOutput)
|
|
||||||
|
|
||||||
if _adPlaying {
|
if _adPlaying {
|
||||||
#if USE_GOOGLE_IMA
|
#if USE_GOOGLE_IMA
|
||||||
_imaAdsManager.getAdsManager()?.resume()
|
_imaAdsManager.getAdsManager()?.resume()
|
||||||
@@ -832,6 +833,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
}
|
}
|
||||||
|
|
||||||
_paused = paused
|
_paused = paused
|
||||||
|
AudioSessionManager.shared.playerPropertiesChanged(view: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
@@ -896,18 +898,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
@objc
|
@objc
|
||||||
func setAudioOutput(_ audioOutput: String) {
|
func setAudioOutput(_ audioOutput: String) {
|
||||||
_audioOutput = audioOutput
|
_audioOutput = audioOutput
|
||||||
RCTPlayerOperations.configureAudio(ignoreSilentSwitch: _ignoreSilentSwitch, mixWithOthers: _mixWithOthers, audioOutput: _audioOutput)
|
|
||||||
do {
|
// Notify AudioSessionManager about the change instead of directly configuring
|
||||||
if audioOutput == "speaker" {
|
AudioSessionManager.shared.playerPropertiesChanged(view: self)
|
||||||
#if os(iOS) || os(visionOS)
|
|
||||||
try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.speaker)
|
|
||||||
#endif
|
|
||||||
} else if audioOutput == "earpiece" {
|
|
||||||
try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.none)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
print("Error occurred: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
@@ -982,13 +975,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSelectedTextTrack(_selectedTextTrackCriteria)
|
setSelectedTextTrack(_selectedTextTrackCriteria)
|
||||||
setAudioOutput(_audioOutput)
|
|
||||||
setSelectedAudioTrack(_selectedAudioTrackCriteria)
|
setSelectedAudioTrack(_selectedAudioTrackCriteria)
|
||||||
setResizeMode(_resizeMode)
|
setResizeMode(_resizeMode)
|
||||||
setRepeat(_repeat)
|
setRepeat(_repeat)
|
||||||
setControls(_controls)
|
setControls(_controls)
|
||||||
setPaused(_paused)
|
setPaused(_paused)
|
||||||
setAllowsExternalPlayback(_allowsExternalPlayback)
|
setAllowsExternalPlayback(_allowsExternalPlayback)
|
||||||
|
|
||||||
|
AudioSessionManager.shared.playerPropertiesChanged(view: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
@@ -1370,6 +1364,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
player.pause()
|
player.pause()
|
||||||
NowPlayingInfoCenterManager.shared.removePlayer(player: player)
|
NowPlayingInfoCenterManager.shared.removePlayer(player: player)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unregister from AudioSessionManager
|
||||||
|
AudioSessionManager.shared.unregisterView(view: self)
|
||||||
|
|
||||||
_playerItem = nil
|
_playerItem = nil
|
||||||
_source = nil
|
_source = nil
|
||||||
_chapters = nil
|
_chapters = nil
|
||||||
|
|||||||
Reference in New Issue
Block a user