mirror of
https://github.com/yattee/yattee.git
synced 2025-12-07 16:48:15 +00:00
Fix iOS Now Playing Info Center integration for AVPlayer backend
This commit enables proper Now Playing Info Center integration on iOS, allowing video playback information to appear in Control Center and Lock Screen with working remote controls. Key changes: - Activate audio session on app launch with setCategory(.playback, mode: .moviePlayback) and setActive(true) - Set up remote commands on first play() call instead of during app initialization to avoid claiming Now Playing slot prematurely - Remove removeTarget(nil) calls that were claiming Now Playing without content - Enable remote commands (play, pause, toggle, seek) explicitly and add proper target handlers - Use backend.isPlaying instead of PlayerModel.isPlaying to avoid race conditions - Include playback rate (1.0 for playing, 0.0 for paused) in Now Playing info - Update Now Playing info on main queue for thread safety - Update Now Playing when switching between backends - Remove audio session deactivation from pause() and stop() methods Note: This fix works for AVPlayer backend. MPV backend has fundamental incompatibility with iOS Now Playing system.
This commit is contained in:
@@ -198,9 +198,6 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
guard avPlayer.timeControlStatus != .paused else {
|
guard avPlayer.timeControlStatus != .paused else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
#if !os(macOS)
|
|
||||||
model.setAudioSessionActive(false)
|
|
||||||
#endif
|
|
||||||
avPlayer.pause()
|
avPlayer.pause()
|
||||||
model.objectWillChange.send()
|
model.objectWillChange.send()
|
||||||
}
|
}
|
||||||
@@ -214,9 +211,6 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
#if !os(macOS)
|
|
||||||
model.setAudioSessionActive(false)
|
|
||||||
#endif
|
|
||||||
avPlayer.replaceCurrentItem(with: nil)
|
avPlayer.replaceCurrentItem(with: nil)
|
||||||
hasStarted = false
|
hasStarted = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -406,6 +406,10 @@ final class MPVBackend: PlayerBackend {
|
|||||||
seek(to: 0, seekType: .loopRestart)
|
seek(to: 0, seekType: .loopRestart)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
|
model.setAudioSessionActive(true)
|
||||||
|
#endif
|
||||||
|
|
||||||
client?.play()
|
client?.play()
|
||||||
|
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
@@ -418,9 +422,6 @@ final class MPVBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func pause() {
|
func pause() {
|
||||||
#if !os(macOS)
|
|
||||||
model.setAudioSessionActive(false)
|
|
||||||
#endif
|
|
||||||
stopClientUpdates()
|
stopClientUpdates()
|
||||||
stopRefreshRateUpdates()
|
stopRefreshRateUpdates()
|
||||||
|
|
||||||
@@ -442,9 +443,6 @@ final class MPVBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
#if !os(macOS)
|
|
||||||
model.setAudioSessionActive(false)
|
|
||||||
#endif
|
|
||||||
stopClientUpdates()
|
stopClientUpdates()
|
||||||
stopRefreshRateUpdates()
|
stopRefreshRateUpdates()
|
||||||
client?.stop()
|
client?.stop()
|
||||||
|
|||||||
@@ -102,6 +102,8 @@ final class MPVClient: ObservableObject {
|
|||||||
// Set the number of threads dynamically
|
// Set the number of threads dynamically
|
||||||
checkError(mpv_set_option_string(mpv, "vd-lavc-threads", "\(threads)"))
|
checkError(mpv_set_option_string(mpv, "vd-lavc-threads", "\(threads)"))
|
||||||
|
|
||||||
|
// AUDIO //
|
||||||
|
|
||||||
// GPU //
|
// GPU //
|
||||||
|
|
||||||
checkError(mpv_set_option_string(mpv, "hwdec", Defaults[.mpvHWdec]))
|
checkError(mpv_set_option_string(mpv, "hwdec", Defaults[.mpvHWdec]))
|
||||||
|
|||||||
@@ -409,6 +409,9 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func play() {
|
func play() {
|
||||||
|
if !remoteCommandCenterConfigured {
|
||||||
|
updateRemoteCommandCenter()
|
||||||
|
}
|
||||||
backend.play()
|
backend.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,17 +640,24 @@ final class PlayerModel: ObservableObject {
|
|||||||
fromBackend.pause()
|
fromBackend.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update Now Playing when switching backends to ensure the new backend takes control
|
||||||
|
updateNowPlayingInfo()
|
||||||
|
|
||||||
guard var stream, changingStream else {
|
guard var stream, changingStream else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let stream = toBackend.stream, toBackend.video == fromBackend.video {
|
if let stream = toBackend.stream, toBackend.video == fromBackend.video {
|
||||||
toBackend.seek(to: fromBackend.currentTime?.seconds ?? .zero, seekType: .backendSync) { finished in
|
toBackend.seek(to: fromBackend.currentTime?.seconds ?? .zero, seekType: .backendSync) { [weak self] finished in
|
||||||
guard finished else {
|
guard finished else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if wasPlaying {
|
if wasPlaying {
|
||||||
toBackend.play()
|
toBackend.play()
|
||||||
|
// Update Now Playing after resuming playback on new backend
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
|
self?.updateNowPlayingInfo()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -862,8 +872,6 @@ final class PlayerModel: ObservableObject {
|
|||||||
|
|
||||||
func handleQueueChange() {
|
func handleQueueChange() {
|
||||||
Defaults[.queue] = queue
|
Defaults[.queue] = queue
|
||||||
|
|
||||||
updateRemoteCommandCenter()
|
|
||||||
controls.objectWillChange.send()
|
controls.objectWillChange.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -897,7 +905,9 @@ final class PlayerModel: ObservableObject {
|
|||||||
func handlePlaybackModeChange() {
|
func handlePlaybackModeChange() {
|
||||||
Defaults[.playbackMode] = playbackMode
|
Defaults[.playbackMode] = playbackMode
|
||||||
|
|
||||||
updateRemoteCommandCenter()
|
if currentItem != nil {
|
||||||
|
updateRemoteCommandCenter()
|
||||||
|
}
|
||||||
|
|
||||||
guard playbackMode == .related else {
|
guard playbackMode == .related else {
|
||||||
autoplayItem = nil
|
autoplayItem = nil
|
||||||
@@ -953,17 +963,6 @@ final class PlayerModel: ObservableObject {
|
|||||||
let interval = TimeInterval(systemControlsSeekDuration) ?? 10
|
let interval = TimeInterval(systemControlsSeekDuration) ?? 10
|
||||||
let preferredIntervals = [NSNumber(value: interval)]
|
let preferredIntervals = [NSNumber(value: interval)]
|
||||||
|
|
||||||
// Remove existing targets to avoid duplicates
|
|
||||||
skipForwardCommand.removeTarget(nil)
|
|
||||||
skipBackwardCommand.removeTarget(nil)
|
|
||||||
previousTrackCommand.removeTarget(nil)
|
|
||||||
nextTrackCommand.removeTarget(nil)
|
|
||||||
commandCenter.playCommand.removeTarget(nil)
|
|
||||||
commandCenter.pauseCommand.removeTarget(nil)
|
|
||||||
commandCenter.togglePlayPauseCommand.removeTarget(nil)
|
|
||||||
commandCenter.changePlaybackPositionCommand.removeTarget(nil)
|
|
||||||
|
|
||||||
// Re-add targets for handling commands
|
|
||||||
skipForwardCommand.preferredIntervals = preferredIntervals
|
skipForwardCommand.preferredIntervals = preferredIntervals
|
||||||
skipBackwardCommand.preferredIntervals = preferredIntervals
|
skipBackwardCommand.preferredIntervals = preferredIntervals
|
||||||
|
|
||||||
@@ -987,21 +986,25 @@ 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 }
|
||||||
|
|
||||||
@@ -1114,7 +1117,8 @@ final class PlayerModel: ObservableObject {
|
|||||||
mediaType = MPMediaType.anyVideo.rawValue as NSNumber
|
mediaType = MPMediaType.anyVideo.rawValue as NSNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare the Now Playing info dictionary
|
let backendIsPlaying = backend.isPlaying
|
||||||
|
|
||||||
var nowPlayingInfo: [String: AnyObject] = [
|
var nowPlayingInfo: [String: AnyObject] = [
|
||||||
MPMediaItemPropertyTitle: video.displayTitle as AnyObject,
|
MPMediaItemPropertyTitle: video.displayTitle as AnyObject,
|
||||||
MPMediaItemPropertyArtist: video.displayAuthor as AnyObject,
|
MPMediaItemPropertyArtist: video.displayAuthor as AnyObject,
|
||||||
@@ -1122,7 +1126,9 @@ final class PlayerModel: ObservableObject {
|
|||||||
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentTime as AnyObject,
|
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentTime as AnyObject,
|
||||||
MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject,
|
MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject,
|
||||||
MPNowPlayingInfoPropertyPlaybackQueueIndex: 1 as AnyObject,
|
MPNowPlayingInfoPropertyPlaybackQueueIndex: 1 as AnyObject,
|
||||||
MPMediaItemPropertyMediaType: mediaType
|
MPMediaItemPropertyMediaType: mediaType,
|
||||||
|
MPNowPlayingInfoPropertyPlaybackRate: (backendIsPlaying ? 1.0 : 0.0) as AnyObject,
|
||||||
|
MPNowPlayingInfoPropertyDefaultPlaybackRate: 1.0 as AnyObject
|
||||||
]
|
]
|
||||||
|
|
||||||
if !currentArtwork.isNil {
|
if !currentArtwork.isNil {
|
||||||
@@ -1138,7 +1144,9 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
DispatchQueue.main.async {
|
||||||
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateCurrentArtwork() {
|
func updateCurrentArtwork() {
|
||||||
@@ -1303,7 +1311,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
try AVAudioSession.sharedInstance().setActive(setActive)
|
try AVAudioSession.sharedInstance().setActive(setActive)
|
||||||
} catch {
|
} catch {
|
||||||
self.logger.error("Error setting up audio session: \(error)")
|
self.logger.error("Error setting audio session to \(setActive): \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,14 +22,13 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
OrientationTracker.shared.startDeviceOrientationTracking()
|
OrientationTracker.shared.startDeviceOrientationTracking()
|
||||||
OrientationModel.shared.startOrientationUpdates()
|
OrientationModel.shared.startOrientationUpdates()
|
||||||
|
|
||||||
// Configure the audio session for playback
|
|
||||||
do {
|
do {
|
||||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
|
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
|
||||||
|
try AVAudioSession.sharedInstance().setActive(true)
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to set audio session category: \(error)")
|
logger.error("Failed to set audio session category: \(error)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin receiving remote control events
|
|
||||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user