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)
|
||||
}
|
||||
|
||||
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 cmSeekTime: CMTime = CMTimeMakeWithSeconds(Float64(seekTime), preferredTimescale: Int32(timeScale))
|
||||
let cmSeekTime: CMTime = CMTimeMakeWithSeconds(
|
||||
Float64(seekTime), preferredTimescale: Int32(timeScale)
|
||||
)
|
||||
let current: CMTime = playerItem.currentTime()
|
||||
let tolerance: CMTime = CMTimeMake(value: Int64(seekTolerance), timescale: Int32(timeScale))
|
||||
|
||||
@@ -141,63 +146,11 @@ enum RCTPlayerOperations {
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
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 var receivingRemoveControlEvents = false {
|
||||
var receivingRemoveControlEvents = false {
|
||||
didSet {
|
||||
if receivingRemoveControlEvents {
|
||||
try? AVAudioSession.sharedInstance().setCategory(.playback)
|
||||
try? AVAudioSession.sharedInstance().setActive(true)
|
||||
AudioSessionManager.shared.setRemoteControlEventsActive(true)
|
||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||
} else {
|
||||
UIApplication.shared.endReceivingRemoteControlEvents()
|
||||
AudioSessionManager.shared.setRemoteControlEventsActive(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import React
|
||||
// MARK: - RCTVideo
|
||||
|
||||
class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverHandler {
|
||||
private var _player: AVPlayer?
|
||||
var _player: AVPlayer?
|
||||
private var _playerItem: AVPlayerItem?
|
||||
private var _source: VideoSource?
|
||||
private var _playerLayer: AVPlayerLayer?
|
||||
@@ -30,7 +30,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
private var _controls = false
|
||||
|
||||
/* 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 _rate: Float = 1.0
|
||||
private var _maxBitRate: Float?
|
||||
@@ -44,12 +44,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
private var _selectedTextTrackCriteria: SelectedTrackCriteria = .none()
|
||||
private var _selectedAudioTrackCriteria: SelectedTrackCriteria = .none()
|
||||
private var _playbackStalled = false
|
||||
private var _playInBackground = false
|
||||
var _playInBackground = false
|
||||
private var _preventsDisplaySleepDuringVideoPlayback = true
|
||||
private var _preferredForwardBufferDuration: Float = 0.0
|
||||
private var _playWhenInactive = false
|
||||
private var _ignoreSilentSwitch: String = "inherit" // inherit, ignore, obey
|
||||
private var _mixWithOthers: String = "inherit" // inherit, mix, duck
|
||||
var _ignoreSilentSwitch: String = "inherit" // inherit, ignore, obey
|
||||
var _mixWithOthers: String = "inherit" // inherit, mix, duck
|
||||
private var _resizeMode: String = "cover"
|
||||
private var _fullscreen = false
|
||||
private var _fullscreenAutorotate = true
|
||||
@@ -60,7 +60,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
private var _filterEnabled = false
|
||||
private var _presentingViewController: UIViewController?
|
||||
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
|
||||
private var _lastBitrate = -2.0
|
||||
private var _enterPictureInPictureOnLeave = false {
|
||||
@@ -204,6 +204,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
_eventDispatcher = eventDispatcher
|
||||
|
||||
AudioSessionManager.shared.registerView(view: self)
|
||||
|
||||
#if os(iOS)
|
||||
if _enterPictureInPictureOnLeave {
|
||||
initPictureinPicture()
|
||||
@@ -286,6 +288,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
_imaAdsManager.releaseAds()
|
||||
_imaAdsManager = nil
|
||||
#endif
|
||||
AudioSessionManager.shared.unregisterView(view: self)
|
||||
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
self.removePlayerLayer()
|
||||
@@ -298,7 +301,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
#if os(iOS)
|
||||
_pip = nil
|
||||
#endif
|
||||
|
||||
ReactNativeVideoManager.shared.unregisterView(newInstance: self)
|
||||
AudioSessionManager.shared.unregisterView(view: self)
|
||||
}
|
||||
|
||||
// MARK: - App lifecycle handlers
|
||||
@@ -767,13 +772,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
@objc
|
||||
func setEnterPictureInPictureOnLeave(_ enterPictureInPictureOnLeave: Bool) {
|
||||
#if os(iOS)
|
||||
let audioSession = AVAudioSession.sharedInstance()
|
||||
do {
|
||||
try audioSession.setCategory(.playback)
|
||||
try audioSession.setActive(true, options: [])
|
||||
} catch {}
|
||||
if _enterPictureInPictureOnLeave != enterPictureInPictureOnLeave {
|
||||
_enterPictureInPictureOnLeave = enterPictureInPictureOnLeave
|
||||
|
||||
AudioSessionManager.shared.playerPropertiesChanged(view: self)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -792,14 +794,15 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
@objc
|
||||
func setIgnoreSilentSwitch(_ ignoreSilentSwitch: String?) {
|
||||
_ignoreSilentSwitch = ignoreSilentSwitch ?? "inherit"
|
||||
RCTPlayerOperations.configureAudio(ignoreSilentSwitch: _ignoreSilentSwitch, mixWithOthers: _mixWithOthers, audioOutput: _audioOutput)
|
||||
applyModifiers()
|
||||
|
||||
AudioSessionManager.shared.playerPropertiesChanged(view: self)
|
||||
}
|
||||
|
||||
@objc
|
||||
func setMixWithOthers(_ mixWithOthers: String?) {
|
||||
_mixWithOthers = mixWithOthers ?? "inherit"
|
||||
applyModifiers()
|
||||
|
||||
AudioSessionManager.shared.playerPropertiesChanged(view: self)
|
||||
}
|
||||
|
||||
@objc
|
||||
@@ -814,8 +817,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
_player?.rate = 0.0
|
||||
}
|
||||
} else {
|
||||
RCTPlayerOperations.configureAudio(ignoreSilentSwitch: _ignoreSilentSwitch, mixWithOthers: _mixWithOthers, audioOutput: _audioOutput)
|
||||
|
||||
if _adPlaying {
|
||||
#if USE_GOOGLE_IMA
|
||||
_imaAdsManager.getAdsManager()?.resume()
|
||||
@@ -832,6 +833,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
_paused = paused
|
||||
AudioSessionManager.shared.playerPropertiesChanged(view: self)
|
||||
}
|
||||
|
||||
@objc
|
||||
@@ -896,18 +898,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
@objc
|
||||
func setAudioOutput(_ audioOutput: String) {
|
||||
_audioOutput = audioOutput
|
||||
RCTPlayerOperations.configureAudio(ignoreSilentSwitch: _ignoreSilentSwitch, mixWithOthers: _mixWithOthers, audioOutput: _audioOutput)
|
||||
do {
|
||||
if audioOutput == "speaker" {
|
||||
#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)")
|
||||
}
|
||||
|
||||
// Notify AudioSessionManager about the change instead of directly configuring
|
||||
AudioSessionManager.shared.playerPropertiesChanged(view: self)
|
||||
}
|
||||
|
||||
@objc
|
||||
@@ -982,13 +975,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
setSelectedTextTrack(_selectedTextTrackCriteria)
|
||||
setAudioOutput(_audioOutput)
|
||||
setSelectedAudioTrack(_selectedAudioTrackCriteria)
|
||||
setResizeMode(_resizeMode)
|
||||
setRepeat(_repeat)
|
||||
setControls(_controls)
|
||||
setPaused(_paused)
|
||||
setAllowsExternalPlayback(_allowsExternalPlayback)
|
||||
|
||||
AudioSessionManager.shared.playerPropertiesChanged(view: self)
|
||||
}
|
||||
|
||||
@objc
|
||||
@@ -1370,6 +1364,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
player.pause()
|
||||
NowPlayingInfoCenterManager.shared.removePlayer(player: player)
|
||||
}
|
||||
|
||||
// Unregister from AudioSessionManager
|
||||
AudioSessionManager.shared.unregisterView(view: self)
|
||||
|
||||
_playerItem = nil
|
||||
_source = nil
|
||||
_chapters = nil
|
||||
|
||||
Reference in New Issue
Block a user