-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
refactor(ios): audio session manager #3850
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
import AVFoundation | ||
import Foundation | ||
|
||
class AudioSessionManager { | ||
static let shared = AudioSessionManager() | ||
|
||
private var videoViews = NSHashTable<RCTVideo>.weakObjects() | ||
|
||
func registerView(view: RCTVideo) { | ||
if videoViews.contains(view) { | ||
return | ||
} | ||
|
||
videoViews.add(view) | ||
} | ||
|
||
func removePlayer(view: RCTVideo) { | ||
if !videoViews.contains(view) { | ||
return | ||
} | ||
|
||
videoViews.remove(view) | ||
|
||
if videoViews.allObjects.isEmpty { | ||
try? AVAudioSession().setActive(false) | ||
} | ||
} | ||
|
||
private func getCategory(silentSwitchObey: Bool, earpiece: Bool, pip: Bool, needsPlayback: Bool) -> AVAudioSession.Category { | ||
if needsPlayback { | ||
if earpiece { | ||
RCTLogWarn( | ||
""" | ||
You can't set \"audioOutput\"=\"earpiece\" and \"ignoreSilentSwitch\"=\"obey\" | ||
at the same time (in same or different components) - skipping those props | ||
""" | ||
) | ||
return .playback | ||
} | ||
|
||
if silentSwitchObey { | ||
RCTLogWarn( | ||
""" | ||
You can't use \"playInBackground or \"notificationControls with \"ignoreSilentSwitch\"=\"obey\" | ||
at the same time (in same or different components) - skipping \"ignoreSilentSwitch\" prop | ||
""" | ||
) | ||
return .playback | ||
} | ||
} | ||
|
||
if silentSwitchObey { | ||
if earpiece { | ||
RCTLogWarn( | ||
""" | ||
You can't set \"audioOutput\"=\"earpiece\" and \"ignoreSilentSwitch\"=\"obey\" | ||
at the same time (in same or different components) - skipping those props | ||
""" | ||
) | ||
return .playback | ||
} | ||
|
||
if pip { | ||
RCTLogWarn( | ||
""" | ||
You use \"pictureInPicture\"=\"true\" and \"ignoreSilentSwitch\"=\"obey\" | ||
at the same time (in same or different components) - skipping those props | ||
""" | ||
) | ||
return .playback | ||
} | ||
|
||
return .ambient | ||
} | ||
|
||
return earpiece ? .playAndRecord : .playback | ||
} | ||
|
||
func updateAudioSessionCategory() { | ||
let audioSession = AVAudioSession() | ||
var options: AVAudioSession.CategoryOptions = [] | ||
|
||
let isAnyPlayerPlaying = videoViews.allObjects.contains { view in | ||
view._player?.isMuted == false || (view._player != nil && view._player?.rate != 0) | ||
} | ||
|
||
let anyPlayerShowNotificationControls = videoViews.allObjects.contains { view in | ||
view._showNotificationControls | ||
} | ||
|
||
let anyPlayerNeedsPiP = videoViews.allObjects.contains { view in | ||
view.isPipEnabled() | ||
} | ||
|
||
let anyPlayerNeedsBackgroundPlayback = videoViews.allObjects.contains { view in | ||
view._playInBackground | ||
} | ||
|
||
let canAllowMixing = !anyPlayerShowNotificationControls && !anyPlayerNeedsBackgroundPlayback | ||
|
||
if canAllowMixing { | ||
let shouldEnableMixing = videoViews.allObjects.contains { view in | ||
view._mixWithOthers == "mix" | ||
} | ||
|
||
let shouldEnableDucking = videoViews.allObjects.contains { view in | ||
view._mixWithOthers == "duck" | ||
} | ||
|
||
if shouldEnableMixing && shouldEnableDucking { | ||
RCTLogWarn("You are trying to set \"mixWithOthers\" to \"mix\" and \"duck\" at the same time (in different components) - skiping prop") | ||
} else { | ||
if shouldEnableMixing { | ||
options.insert(.mixWithOthers) | ||
} | ||
|
||
if shouldEnableDucking { | ||
options.insert(.duckOthers) | ||
} | ||
} | ||
} | ||
|
||
let isAnyPlayerUsingEarpiece = videoViews.allObjects.contains { view in | ||
view._audioOutput == "earpiece" | ||
} | ||
|
||
var isSilentSwitchIgnore = videoViews.allObjects.contains { view in | ||
view._ignoreSilentSwitch == "ignore" | ||
} | ||
|
||
var isSilentSwitchObey = videoViews.allObjects.contains { view in | ||
view._ignoreSilentSwitch == "obey" | ||
} | ||
|
||
if isSilentSwitchObey && isSilentSwitchIgnore { | ||
RCTLogWarn("You are trying to set \"ignoreSilentSwitch\" to \"ignore\" and \"obey\" at the same time (in diffrent components) - skiping prop") | ||
isSilentSwitchObey = false | ||
isSilentSwitchIgnore = false | ||
} | ||
|
||
let needUpdateCategory = isAnyPlayerUsingEarpiece || isSilentSwitchIgnore || isSilentSwitchObey || canAllowMixing | ||
|
||
if anyPlayerNeedsPiP || anyPlayerShowNotificationControls || needUpdateCategory { | ||
let category = getCategory( | ||
silentSwitchObey: isSilentSwitchObey, | ||
earpiece: isAnyPlayerUsingEarpiece, | ||
pip: anyPlayerNeedsPiP, | ||
needsPlayback: canAllowMixing | ||
) | ||
|
||
do { | ||
try audioSession.setCategory(category, mode: .moviePlayback, options: canAllowMixing ? options : []) | ||
} catch { | ||
RCTLogWarn("Failed to update audio session category. This can cause issue with background audio playback and PiP or notification controls") | ||
} | ||
} | ||
|
||
if isAnyPlayerPlaying { | ||
do { | ||
try audioSession.setActive(true) | ||
} catch { | ||
RCTLogWarn("Failed activate audio session. This can cause issue audio playback") | ||
} | ||
} | ||
|
||
if isAnyPlayerUsingEarpiece { | ||
do { | ||
if isAnyPlayerUsingEarpiece { | ||
try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.none) | ||
} else { | ||
#if os(iOS) || os(visionOS) | ||
try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.speaker) | ||
#endif | ||
} | ||
} catch { | ||
print("Error occurred: \(error.localizedDescription)") | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,7 @@ import React | |
// MARK: - RCTVideo | ||
|
||
class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverHandler { | ||
private var _player: AVPlayer? | ||
private(set) var _player: AVPlayer? | ||
private var _playerItem: AVPlayerItem? | ||
private var _source: VideoSource? | ||
private var _playerLayer: AVPlayerLayer? | ||
|
@@ -31,7 +31,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" | ||
private(set) var _audioOutput: String = "speaker" | ||
private var _volume: Float = 1.0 | ||
private var _rate: Float = 1.0 | ||
private var _maxBitRate: Float? | ||
|
@@ -46,12 +46,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |
private var _selectedTextTrackCriteria: SelectedTrackCriteria? | ||
private var _selectedAudioTrackCriteria: SelectedTrackCriteria? | ||
private var _playbackStalled = false | ||
private var _playInBackground = false | ||
private(set) 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 | ||
private(set) var _ignoreSilentSwitch: String! = "inherit" // inherit, ignore, obey | ||
private(set) var _mixWithOthers: String! = "inherit" // inherit, mix, duck | ||
private var _resizeMode: String! = "cover" | ||
private var _fullscreen = false | ||
private var _fullscreenAutorotate = true | ||
|
@@ -62,7 +62,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |
private var _filterEnabled = false | ||
private var _presentingViewController: UIViewController? | ||
private var _startPosition: Float64 = -1 | ||
private var _showNotificationControls = false | ||
private(set) 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 _pictureInPictureEnabled = false { | ||
|
@@ -208,6 +208,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |
#endif | ||
|
||
_eventDispatcher = eventDispatcher | ||
AudioSessionManager.shared.registerView(view: self) | ||
|
||
#if os(iOS) | ||
if _pictureInPictureEnabled { | ||
|
@@ -260,6 +261,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |
|
||
required init?(coder aDecoder: NSCoder) { | ||
super.init(coder: aDecoder) | ||
AudioSessionManager.shared.registerView(view: self) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no unregisterView call, is it really expected ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I will add unregisterView to deinit 👍 |
||
#if USE_GOOGLE_IMA | ||
_imaAdsManager = RCTIMAAdsManager(video: self, pipEnabled: isPipEnabled) | ||
#endif | ||
|
@@ -277,6 +279,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |
|
||
if let player = _player { | ||
NowPlayingInfoCenterManager.shared.removePlayer(player: player) | ||
AudioSessionManager.shared.removePlayer(view: self) | ||
} | ||
|
||
#if os(iOS) | ||
|
@@ -586,6 +589,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |
} | ||
} | ||
} | ||
|
||
AudioSessionManager.shared.updateAudioSessionCategory() | ||
} | ||
|
||
self._videoLoadStarted = true | ||
|
@@ -694,6 +699,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |
@objc | ||
func setPlayInBackground(_ playInBackground: Bool) { | ||
_playInBackground = playInBackground | ||
AudioSessionManager.shared.updateAudioSessionCategory() | ||
} | ||
|
||
@objc | ||
|
@@ -718,17 +724,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |
@objc | ||
func setPictureInPicture(_ pictureInPicture: Bool) { | ||
#if os(iOS) | ||
let audioSession = AVAudioSession.sharedInstance() | ||
do { | ||
try audioSession.setCategory(.playback) | ||
try audioSession.setActive(true, options: []) | ||
} catch {} | ||
if pictureInPicture { | ||
_pictureInPictureEnabled = true | ||
} else { | ||
_pictureInPictureEnabled = false | ||
} | ||
_pip?.setPictureInPicture(pictureInPicture) | ||
AudioSessionManager.shared.updateAudioSessionCategory() | ||
#endif | ||
} | ||
|
||
|
@@ -746,7 +748,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |
@objc | ||
func setIgnoreSilentSwitch(_ ignoreSilentSwitch: String?) { | ||
_ignoreSilentSwitch = ignoreSilentSwitch | ||
RCTPlayerOperations.configureAudio(ignoreSilentSwitch: _ignoreSilentSwitch, mixWithOthers: _mixWithOthers, audioOutput: _audioOutput) | ||
AudioSessionManager.shared.updateAudioSessionCategory() | ||
applyModifiers() | ||
} | ||
|
||
|
@@ -768,7 +770,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | |
_player?.rate = 0.0 | ||
} | ||
} else { | ||
RCTPlayerOperations.configureAudio(ignoreSilentSwitch: _ignoreSilentSwitch, mixWithOthers: _mixWithOthers, audioOutput: _audioOutput) | ||
AudioSessionManager.shared.updateAudioSessionCategory() | ||
|
||
if _adPlaying { | ||
#if USE_GOOGLE_IMA | ||
|
@@ -850,18 +852,7 @@ 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)") | ||
} | ||
AudioSessionManager.shared.updateAudioSessionCategory() | ||
} | ||
|
||
@objc | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know in the original code theres no case for switching back to inherit, would it be possible to add it in ?
Because once you set it to 'mix' or 'duck' theres no way to set it back to 'inherit'