mirror of
https://github.com/yattee/yattee.git
synced 2025-11-20 17:02:21 +00:00
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:
@@ -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)")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user