2
2
mirror of https://github.com/mastodon/mastodon-ios synced 2025-04-11 22:58:02 +02:00

More careful handling of AVAudioSession

(May contribute to…)
Contributes to #1074  [BUG] When run on Mac, iOS app prevents sleep
This commit is contained in:
shannon 2025-01-23 10:20:19 -05:00
parent eac3222e65
commit e9b58d3047
2 changed files with 48 additions and 23 deletions

View File

@ -20,9 +20,7 @@ final class MediaPreviewVideoViewController: UIViewController {
let previewImageView = UIImageView()
deinit {
playerViewController.player?.pause()
try? AVAudioSession.sharedInstance().setCategory(.ambient)
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
viewModel.playbackState = .paused
}
}
@ -32,11 +30,12 @@ extension MediaPreviewVideoViewController {
override func viewDidLoad() {
super.viewDidLoad()
playerViewController.willMove(toParent: self)
addChild(playerViewController)
playerViewController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(playerViewController.view)
playerViewController.view.pinToParent()
playerViewController.didMove(toParent: self)
playerViewController.view.pinToParent()
if let contentOverlayView = playerViewController.contentOverlayView {
previewImageView.translatesAutoresizingMaskIntoConstraints = false
@ -56,7 +55,6 @@ extension MediaPreviewVideoViewController {
playerViewController.showsPlaybackControls = false
}
viewModel.player?.play()
viewModel.playbackState = .playing
if let previewURL = viewModel.item.previewURL {
@ -76,12 +74,6 @@ extension MediaPreviewVideoViewController {
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
playerViewController.didMove(toParent: self)
}
}
// MARK: - ShareActivityProvider

View File

@ -20,7 +20,7 @@ final class MediaPreviewVideoViewModel {
let item: Item
// output
public private(set) var player: AVPlayer?
public let player: AVPlayer?
private var playerLooper: AVPlayerLooper?
@Published var playbackState = PlaybackState.unknown
@ -31,14 +31,14 @@ final class MediaPreviewVideoViewModel {
switch item {
case .video(let mediaContext):
guard let assertURL = mediaContext.assetURL else { return }
let playerItem = AVPlayerItem(url: assertURL)
guard let assetURL = mediaContext.assetURL else { player = nil; return }
let playerItem = AVPlayerItem(url: assetURL)
let _player = AVPlayer(playerItem: playerItem)
self.player = _player
case .gif(let mediaContext):
guard let assertURL = mediaContext.assetURL else { return }
let playerItem = AVPlayerItem(url: assertURL)
guard let assetURL = mediaContext.assetURL else { player = nil; return }
let playerItem = AVPlayerItem(url: assetURL)
let _player = AVQueuePlayer(playerItem: playerItem)
_player.isMuted = true
self.player = _player
@ -48,24 +48,24 @@ final class MediaPreviewVideoViewModel {
}
}
guard let player = player else {
assertionFailure()
guard let player else {
assertionFailure("no url for playable media")
return
}
// setup player state observer
$playbackState
.receive(on: DispatchQueue.main)
.sink { status in
.sink { [weak self] status in
guard let self, let player = self.player else { return }
switch status {
case .unknown, .buffering, .readyToPlay:
break
case .playing:
try? AVAudioSession.sharedInstance().setCategory(.playback)
try? AVAudioSession.sharedInstance().setActive(true)
MediaPreviewVideoViewModel.startAudioSession()
player.play()
case .paused, .stopped, .failed:
try? AVAudioSession.sharedInstance().setCategory(.ambient) // set to ambient to allow mixed (needed for GIFV)
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
MediaPreviewVideoViewModel.endAudioSession()
}
}
.store(in: &disposeBag)
@ -98,6 +98,39 @@ final class MediaPreviewVideoViewModel {
.store(in: &disposeBag)
}
// MARK: Manage AVAudioSession
static var activeAudioSessionRequestCounter = 0
static func startAudioSession() {
Task { @MainActor in
activeAudioSessionRequestCounter += 1
guard activeAudioSessionRequestCounter == 1 else { return }
try? AVAudioSession.sharedInstance().setCategory(.playback, options: [.mixWithOthers])
// https://developer.apple.com/documentation/avfaudio/avaudiosession/setactive(_:options:)
// "If you attempt to activate a session with category record or playAndRecord when another app is already hosting a call, then your session fails with the error AVAudioSessionErrorInsufficientPriority."
// "The session fails to activate if another audio session has higher priority than yours (such as a phone call) and neither audio session allows mixing."
// "mixWithOthers: If you set the audio session category to ambient, the session automatically sets this option. If you set this option, your app mixes its audio with audio playing in background apps, such as the Music app."
// CONCLUSION: Since we are never attempting to record and we allow mixing with others, activating the session should never fail, so there is no need to handle an error here.
try? AVAudioSession.sharedInstance().setActive(true)
}
}
static func endAudioSession() {
Task { @MainActor in
activeAudioSessionRequestCounter -= 1
guard activeAudioSessionRequestCounter == 0 else { return }
try? AVAudioSession.sharedInstance().setCategory(.ambient) // set to ambient to allow mixed (needed for GIFV)
// https://developer.apple.com/documentation/avfaudio/avaudiosession/setactive(_:options:)
// "Deactivating an audio session with running audio objects stops the objects, makes the session inactive, and returns an AVAudioSessionErrorCodeIsBusy error."
// "When your app deactivates a session, the return value is false but the active state changes to deactivate."
// CONCLUSION: Deactivating a session always succeeds, even when an error is thrown, so any error thrown here can be ignored.
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
}
}
deinit {
if playbackState == .playing {
MediaPreviewVideoViewModel.endAudioSession()
}
}
}
extension MediaPreviewVideoViewModel {