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

refactor(ios): audio session manager #3850

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
180 changes: 180 additions & 0 deletions ios/Video/AudioSessionManager.swift
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"
}

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'

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)")
}
}
}
}
4 changes: 2 additions & 2 deletions ios/Video/NowPlayingInfoCenterManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ class NowPlayingInfoCenterManager {
private var receivingRemoveControlEvents = false {
didSet {
if receivingRemoveControlEvents {
try? AVAudioSession.sharedInstance().setCategory(.playback)
try? AVAudioSession.sharedInstance().setActive(true)
UIApplication.shared.beginReceivingRemoteControlEvents()
} else {
UIApplication.shared.endReceivingRemoteControlEvents()
}

AudioSessionManager.shared.updateAudioSessionCategory()
}
}

Expand Down
41 changes: 16 additions & 25 deletions ios/Video/RCTVideo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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?
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -208,6 +208,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
#endif

_eventDispatcher = eventDispatcher
AudioSessionManager.shared.registerView(view: self)

#if os(iOS)
if _pictureInPictureEnabled {
Expand Down Expand Up @@ -260,6 +261,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
AudioSessionManager.shared.registerView(view: self)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no unregisterView call, is it really expected ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I will add unregisterView to deinit 👍
(but this I guess would be fine as it is because there is week object list so it would deinit anyway)

#if USE_GOOGLE_IMA
_imaAdsManager = RCTIMAAdsManager(video: self, pipEnabled: isPipEnabled)
#endif
Expand All @@ -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)
Expand Down Expand Up @@ -586,6 +589,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
}
}
}

AudioSessionManager.shared.updateAudioSessionCategory()
}

self._videoLoadStarted = true
Expand Down Expand Up @@ -694,6 +699,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
@objc
func setPlayInBackground(_ playInBackground: Bool) {
_playInBackground = playInBackground
AudioSessionManager.shared.updateAudioSessionCategory()
}

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

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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading