Fix Now Playing controls when switching between MPV and AVPlayer backends

When switching from AVPlayer to MPV backend, Now Playing controls (play/pause/seek) were disabled because AVPlayer maintained control of the remote command center and audio session. This fix ensures MPV can properly reclaim control.

Key changes:
- Clear AVPlayer's current item when switching to MPV to release media control
- Clear Now Playing info and set playback state to stopped before MPV takes over
- Reset remote command center by removing all targets (including AVPlayer's internal handlers) and re-adding custom handlers
- Force audio session deactivation/reactivation with .notifyOthersOnDeactivation
- Add forceReactivate parameter to setupAudioSessionForNowPlaying() for backend switches
- Ensure stream loading continues after Now Playing setup (don't return early)

The fix properly handles the transition by:
1. Clearing AVPlayer's media session completely
2. Scheduling async Now Playing setup without blocking stream loading
3. Resetting remote command handlers to reclaim control from AVPlayer
4. Re-activating audio session to establish MPV as the active player
This commit is contained in:
Arkadiusz Fal
2025-11-18 16:43:17 +01:00
parent e6b6778ba1
commit 4c5b801c45

View File

@@ -640,8 +640,45 @@ final class PlayerModel: ObservableObject {
fromBackend.pause() fromBackend.pause()
} }
// Update Now Playing when switching backends to ensure the new backend takes control // When switching away from AVPlayer, clear its current item to release Now Playing control
updateNowPlayingInfo() #if !os(macOS)
if from == .appleAVPlayer && to == .mpv {
avPlayerBackend.avPlayer.replaceCurrentItem(with: nil)
// Clear Now Playing info entirely before MPV takes over
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
MPNowPlayingInfoCenter.default().playbackState = .stopped
logger.info("Cleared AVPlayer's Now Playing control")
// Schedule Now Playing setup after a brief delay, but don't block stream loading
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let self else { return }
// Re-activate audio session when switching to MPV to ensure Now Playing controls work
// Force deactivate/reactivate to take control from AVPlayer
self.setupAudioSessionForNowPlaying(forceReactivate: true)
// Reset and re-enable remote commands to take control from AVPlayer
self.updateRemoteCommandCenter(reset: true)
// Set up Now Playing for MPV
self.updateNowPlayingInfo()
logger.info("Set up Now Playing for MPV backend")
}
// Continue to load the stream (don't return early)
} else if to == .mpv {
// Re-activate audio session when switching to MPV to ensure Now Playing controls work
// Force deactivate/reactivate to take control from AVPlayer
setupAudioSessionForNowPlaying(forceReactivate: true)
// Re-enable remote commands to take control from AVPlayer
updateRemoteCommandCenter()
updateNowPlayingInfo()
} else {
updateNowPlayingInfo()
}
#else
updateNowPlayingInfo()
#endif
guard var stream, changingStream else { guard var stream, changingStream else {
return return
@@ -656,6 +693,12 @@ final class PlayerModel: ObservableObject {
toBackend.play() toBackend.play()
// Update Now Playing after resuming playback on new backend // Update Now Playing after resuming playback on new backend
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
#if !os(macOS)
if to == .mpv {
self?.setupAudioSessionForNowPlaying(forceReactivate: true)
self?.updateRemoteCommandCenter(reset: true)
}
#endif
self?.updateNowPlayingInfo() self?.updateNowPlayingInfo()
} }
} }
@@ -950,13 +993,27 @@ final class PlayerModel: ObservableObject {
} }
} }
func updateRemoteCommandCenter() { func updateRemoteCommandCenter(reset: Bool = false) {
let commandCenter = MPRemoteCommandCenter.shared() let commandCenter = MPRemoteCommandCenter.shared()
let skipForwardCommand = commandCenter.skipForwardCommand let skipForwardCommand = commandCenter.skipForwardCommand
let skipBackwardCommand = commandCenter.skipBackwardCommand let skipBackwardCommand = commandCenter.skipBackwardCommand
let previousTrackCommand = commandCenter.previousTrackCommand let previousTrackCommand = commandCenter.previousTrackCommand
let nextTrackCommand = commandCenter.nextTrackCommand let nextTrackCommand = commandCenter.nextTrackCommand
// If resetting (e.g., after AVPlayer was active), remove all targets and re-add them
if reset {
logger.info("Resetting remote command center to reclaim control from AVPlayer")
commandCenter.playCommand.removeTarget(nil)
commandCenter.pauseCommand.removeTarget(nil)
commandCenter.togglePlayPauseCommand.removeTarget(nil)
commandCenter.changePlaybackPositionCommand.removeTarget(nil)
skipForwardCommand.removeTarget(nil)
skipBackwardCommand.removeTarget(nil)
previousTrackCommand.removeTarget(nil)
nextTrackCommand.removeTarget(nil)
remoteCommandCenterConfigured = false
}
if !remoteCommandCenterConfigured { if !remoteCommandCenterConfigured {
remoteCommandCenterConfigured = true remoteCommandCenterConfigured = true
@@ -986,25 +1043,21 @@ final class PlayerModel: ObservableObject {
return .success return .success
} }
commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget { [weak self] _ in commandCenter.playCommand.addTarget { [weak self] _ in
self?.play() self?.play()
return .success return .success
} }
commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.addTarget { [weak self] _ in commandCenter.pauseCommand.addTarget { [weak self] _ in
self?.pause() self?.pause()
return .success return .success
} }
commandCenter.togglePlayPauseCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
self?.togglePlay() self?.togglePlay()
return .success return .success
} }
commandCenter.changePlaybackPositionCommand.isEnabled = true
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] remoteEvent in commandCenter.changePlaybackPositionCommand.addTarget { [weak self] remoteEvent in
guard let event = remoteEvent as? MPChangePlaybackPositionCommandEvent else { return .commandFailed } guard let event = remoteEvent as? MPChangePlaybackPositionCommandEvent else { return .commandFailed }
@@ -1014,6 +1067,12 @@ final class PlayerModel: ObservableObject {
} }
} }
// Always re-enable commands to ensure they work after backend switches
commandCenter.playCommand.isEnabled = true
commandCenter.pauseCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.isEnabled = true
commandCenter.changePlaybackPositionCommand.isEnabled = true
switch Defaults[.systemControlsCommands] { switch Defaults[.systemControlsCommands] {
case .seek: case .seek:
previousTrackCommand.isEnabled = false previousTrackCommand.isEnabled = false
@@ -1149,13 +1208,19 @@ final class PlayerModel: ObservableObject {
} }
} }
func setupAudioSessionForNowPlaying() { func setupAudioSessionForNowPlaying(forceReactivate: Bool = false) {
#if !os(macOS) #if !os(macOS)
do { do {
let audioSession = AVAudioSession.sharedInstance() let audioSession = AVAudioSession.sharedInstance()
// If forcing reactivation (e.g., after backend switch), deactivate first
if forceReactivate {
try? audioSession.setActive(false, options: .notifyOthersOnDeactivation)
}
try audioSession.setCategory(.playback, mode: .moviePlayback, options: []) try audioSession.setCategory(.playback, mode: .moviePlayback, options: [])
try audioSession.setActive(true, options: []) try audioSession.setActive(true, options: [])
logger.info("Audio session activated for Now Playing") logger.info("Audio session activated for Now Playing (forceReactivate: \(forceReactivate))")
} catch { } catch {
logger.error("Failed to set up audio session: \(error)") logger.error("Failed to set up audio session: \(error)")
} }