ARTICLE AD BOX
I have a Swift app, and I have been trying to get lock screen buttons to change. What I want are the buttons to be next/previous video buttons, but what I am getting is 10 sec skip buttons, but only only the lock screen. Nothing I have tried worked.
PlayerManger
import Foundation import Combine import SwiftUI import AVKit import AVFoundation import MediaPlayer @MainActor final class PlayerManager: ObservableObject { static let shared = PlayerManager() let player = AVPlayer() @Published private(set) var currentVideo: VideoItem? @Published private(set) var isPlaying: Bool = false var isShuffledEnabled: Bool { playbackQueue.isShuffledEnabled } @Published var presentationState: PlayerPresentationState = .hidden @Published private(set) var currentTime: Double = 0 @Published private(set) var duration: Double = 0 @Published var isScrubbing: Bool = false @Published private(set) var playbackQueue : PlaybackQueue = .init() private var timeObserverToken: Any? private var cachedArtwork: MPMediaItemArtwork? private var cachedArtworkVideoID: UUID? private var cancellables = Set<AnyCancellable>() init() { configureAudioSession() setupRemoteControls() setupAudioSessionObservers() NotificationCenter.default.addObserver( forName: AVPlayerItem.didPlayToEndTimeNotification, object: nil, queue: .main ) { [weak self] _ in guard let self else { return } Task { @MainActor in self.next() } } timeObserverToken = player.addPeriodicTimeObserver( forInterval: CMTime(seconds: 0.5, preferredTimescale: 600), queue: .main ) { [weak self] time in guard let self else { return } Task { @MainActor in guard !self.isScrubbing else { return } self.currentTime = time.seconds self.duration = self.player.currentItem?.duration.seconds ?? 0 if self.isPlaying { self.updateNowPlayingInfo() } } } player.publisher(for: \.timeControlStatus) .receive(on: RunLoop.main) .sink { [weak self] status in guard let self else { return } self.isPlaying = (status == .playing) self.updateNowPlayingInfo() } .store(in: &cancellables) player.audiovisualBackgroundPlaybackPolicy = .continuesIfPossible } deinit { if let token = timeObserverToken { player.removeTimeObserver(token) } } private func configureAudioSession() { do { try AVAudioSession.sharedInstance().setCategory( .playback, mode: .moviePlayback, options: [] ) try AVAudioSession.sharedInstance().setActive(true) } catch { print("❌ Audio session setup failed:", error) } } private func setupAudioSessionObservers() { NotificationCenter.default.addObserver( self, selector: #selector(handleAudioSessionInterruption), name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance() ) NotificationCenter.default.addObserver( self, selector: #selector(handleAudioSessionRouteChange), name: AVAudioSession.routeChangeNotification, object: AVAudioSession.sharedInstance() ) } @objc private func handleAudioSessionInterruption(_ notification: Notification) { guard let userInfo = notification.userInfo, let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return } Task { @MainActor in switch type { case .began: break case .ended: let shouldResume = (userInfo[AVAudioSessionInterruptionOptionKey] as? UInt) .flatMap { AVAudioSession.InterruptionOptions(rawValue: $0) } .map { $0.contains(.shouldResume) } ?? true if shouldResume && self.isPlaying { try? AVAudioSession.sharedInstance().setActive(true) self.player.play() self.isPlaying = true self.updateNowPlayingInfo() } @unknown default: break } } } @objc private func handleAudioSessionRouteChange(_ notification: Notification) { guard let userInfo = notification.userInfo, let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return } Task { @MainActor in switch reason { case .oldDeviceUnavailable: self.player.pause() self.isPlaying = false self.updateNowPlayingInfo() default: if self.isPlaying { self.player.play() } } } } // MARK: - Playback func startPlayback(_ video: VideoItem) { guard FileManager.default.fileExists(atPath: video.fileURL.path) else { print("❌ File does not exist:", video.fileURL) return } try? AVAudioSession.sharedInstance().setActive(true) currentVideo = video if cachedArtworkVideoID != video.id, let image = UIImage(data: video.thumbnailData) { cachedArtwork = MPMediaItemArtwork(boundsSize: image.size) { _ in image } cachedArtworkVideoID = video.id } let item = AVPlayerItem(asset: AVURLAsset(url: video.fileURL)) player.replaceCurrentItem(with: item) player.play() UIApplication.shared.beginReceivingRemoteControlEvents() isPlaying = true presentationState = .fullscreen updateNowPlayingInfo() MPNowPlayingInfoCenter.default().playbackState = .playing updateRemoteCommandAvailability() player.currentItem?.publisher(for: \.status) .filter { $0 == .readyToPlay } .first() .sink { [weak self] _ in guard let self else { return } self.updateNowPlayingInfo() } .store(in: &cancellables) } func play(videos: [VideoItem], startAt index: Int) { playbackQueue.load(videos, startAt: index) updateRemoteCommandAvailability() guard let video = playbackQueue.current else { return } startPlayback(video) } func enterMiniPlayer() { guard currentVideo != nil else { presentationState = .hidden return } presentationState = .mini } func dismissPlayer() { presentationState = .hidden player.pause() isPlaying = false MPNowPlayingInfoCenter.default().nowPlayingInfo = nil try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) } func togglePlayPause() { if isPlaying { player.pause() isPlaying = false MPNowPlayingInfoCenter.default().playbackState = .paused } else { try? AVAudioSession.sharedInstance().setActive(true) player.play() isPlaying = true MPNowPlayingInfoCenter.default().playbackState = .playing } updateNowPlayingInfo() } func next() { guard let nextVideo = playbackQueue.next() else { return } updateRemoteCommandAvailability() startPlayback(nextVideo) } func previous() { guard let previousVideo = playbackQueue.previous() else { return } updateRemoteCommandAvailability() startPlayback(previousVideo) } func toggleShuffle() { playbackQueue.toggleShuffle() } // MARK: - NOW PLAYING func updateNowPlayingInfo() { guard let video = currentVideo else { return } var info: [String: Any] = [ MPMediaItemPropertyTitle: video.title, MPMediaItemPropertyPlaybackDuration: video.duration, MPNowPlayingInfoPropertyElapsedPlaybackTime: player.currentTime().seconds, MPNowPlayingInfoPropertyPlaybackRate: isPlaying ? 1.0 : 0.0, MPNowPlayingInfoPropertyDefaultPlaybackRate: 1.0, MPNowPlayingInfoPropertyMediaType: MPNowPlayingInfoMediaType.none.rawValue, // MPNowPlayingInfoPropertyIsLiveStream: false, MPNowPlayingInfoPropertyPlaybackQueueIndex: playbackQueue.currentIndex, MPNowPlayingInfoPropertyPlaybackQueueCount: playbackQueue.items.count, ] if let cachedArtwork { info[MPMediaItemPropertyArtwork] = cachedArtwork } MPNowPlayingInfoCenter.default().nowPlayingInfo = info } func setupRemoteControls() { let cc = MPRemoteCommandCenter.shared() cc.skipForwardCommand.removeTarget(nil) cc.skipForwardCommand.isEnabled = false // cc.skipForwardCommand.preferredIntervals = [1] // cc.skipForwardCommand.addTarget { [weak self] _ in // guard let self else { return .commandFailed } // Task { @MainActor in // self.next() // } // return .success // } cc.skipBackwardCommand.removeTarget(nil) cc.skipBackwardCommand.isEnabled = false // cc.skipBackwardCommand.preferredIntervals = [1] // cc.skipBackwardCommand.addTarget { [weak self] _ in // guard let self else { return .commandFailed } // Task { @MainActor in // self.previous() // } // return .success // } cc.seekForwardCommand.isEnabled = false cc.seekForwardCommand.removeTarget(nil) cc.seekBackwardCommand.isEnabled = false cc.seekBackwardCommand.removeTarget(nil) cc.playCommand.removeTarget(nil) cc.playCommand.isEnabled = true cc.playCommand.addTarget { [weak self] _ in guard let self else { return .commandFailed } Task { @MainActor in try? AVAudioSession.sharedInstance().setActive(true) self.player.play() self.isPlaying = true self.updateNowPlayingInfo() } return .success } cc.pauseCommand.removeTarget(nil) cc.pauseCommand.isEnabled = true cc.pauseCommand.addTarget { [weak self] _ in guard let self else { return .commandFailed } Task { @MainActor in self.player.pause() self.isPlaying = false self.updateNowPlayingInfo() } return .success } cc.togglePlayPauseCommand.removeTarget(nil) cc.togglePlayPauseCommand.isEnabled = true cc.togglePlayPauseCommand.addTarget { [weak self] _ in guard let self else { return .commandFailed } Task { @MainActor in self.togglePlayPause() } return .success } cc.nextTrackCommand.removeTarget(nil) cc.nextTrackCommand.isEnabled = true cc.nextTrackCommand.addTarget { [weak self] _ in guard let self else { return .commandFailed } Task { @MainActor in self.next() } return .success } cc.previousTrackCommand.removeTarget(nil) cc.previousTrackCommand.isEnabled = true cc.previousTrackCommand.addTarget { [weak self] _ in guard let self else { return .commandFailed } Task { @MainActor in self.previous() } return .success } cc.changePlaybackPositionCommand.removeTarget(nil) cc.changePlaybackPositionCommand.isEnabled = true cc.changePlaybackPositionCommand.addTarget { [weak self] event in guard let self, let event = event as? MPChangePlaybackPositionCommandEvent else { return .commandFailed } Task { @MainActor in let time = CMTime(seconds: event.positionTime, preferredTimescale: 600) self.player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero) self.currentTime = event.positionTime self.updateNowPlayingInfo() } return .success } } private func updateRemoteCommandAvailability() { let cc = MPRemoteCommandCenter.shared() cc.skipForwardCommand.isEnabled = false cc.skipBackwardCommand.isEnabled = false cc.nextTrackCommand.isEnabled = true cc.previousTrackCommand.isEnabled = true } // MARK: Scrubbing func beginScrubbing() { isScrubbing = true } func scrub(to time: Double, live: Bool = false) { currentTime = time guard live else { return } let cmTime = CMTime(seconds: time, preferredTimescale: 600) let tolerance = CMTime(seconds: 0.2, preferredTimescale: 600) player.seek(to: cmTime, toleranceBefore: tolerance, toleranceAfter: tolerance) } func endScrubbing() { isScrubbing = false let cmTime = CMTime(seconds: currentTime, preferredTimescale: 600) player.seek( to: cmTime, toleranceBefore: .zero, toleranceAfter: .zero ) { [weak self] _ in guard let self else { return } Task { @MainActor in if self.isPlaying { self.player.play() } self.updateNowPlayingInfo() } } } }PlaybackQueue:
@MainActor final class PlaybackQueue { enum OrderMode { case sequential case shuffled } enum RepeatMode { case none case repeatAll case repeatOne } private(set) var items: [VideoItem] = [] private(set) var currentIndex: Int = 0 private(set) var orderMode: OrderMode = .sequential private(set) var repeatMode: RepeatMode = .none private var recentHistory: [Int] = [] private let historyLimit: Int = 3 // Public var current: VideoItem? { guard items.indices.contains(currentIndex) else { return nil } return items[currentIndex] } var isShuffledEnabled: Bool { orderMode == .shuffled } func load(_ videos: [VideoItem], startAt index: Int) { items = videos currentIndex = index orderMode = .sequential recentHistory.removeAll() } func next() -> VideoItem? { guard !items.isEmpty else { return nil } if repeatMode == .repeatOne { return current } switch orderMode { case .sequential: if repeatMode == .none && currentIndex == items.count - 1 { return nil } currentIndex = (currentIndex + 1) % items.count case .shuffled: guard items.count > 1 else { return current } var newIndex: Int repeat { newIndex = Int.random(in: 0..<items.count) } while newIndex == currentIndex || recentHistory.contains(newIndex) currentIndex = newIndex recentHistory.append(newIndex) if recentHistory.count > historyLimit { recentHistory.removeFirst() } } return current } func previous() -> VideoItem? { guard !items.isEmpty else { return nil } currentIndex = (currentIndex - 1 + items.count) % items.count return current } func enableShuffleStartingRandom() { guard !items.isEmpty else { return } orderMode = .shuffled if items.count == 1 { currentIndex = 0 return } var randomIndex = Int.random(in: 0..<items.count) // Avoid restarting same video if already playing if randomIndex == currentIndex { randomIndex = (randomIndex + 1) % items.count } currentIndex = randomIndex } func toggleShuffle() { orderMode = (orderMode == .sequential) ? .shuffled : .sequential } func toggleRepeatMode() { switch repeatMode { case .none: repeatMode = .repeatAll case .repeatAll: repeatMode = .repeatOne case .repeatOne: repeatMode = .none } } }now the things that I have attempted is changing:
mode: .moviePlayback to default.
I have tried not including the skipForwardCommand, etc, and it still didnt work.
the closest thing that I got to work, which still wasn't it, was doing:
And all this did was keep the skip 10 button, but it did do the next video. I would prefer it to be the next button, if at all possible.
and
MPNowPlayingInfoPropertyMediaType: MPNowPlayingInfoMediaType.none.rawValue,I have made it .audio, and .video.
I have all the permissions in the info.plist up, and the background modes set to Audio, Air play, etc.


I looked at these previous posts, but they are a little outdated, and haven't worked for me.
AVPlayer Lock Screen Controls
Swift: add forward/backward 15 seconds buttons on lock screen
How do I make my app work with the media control buttons on lock screen?
If there is anything else I can provide, please ask.
thank you
