Add support for showing media playback controls on the lock screen

This commit is contained in:
Stefan Ceriu
2024-09-06 12:57:20 +03:00
committed by Stefan Ceriu
parent d0d24e2d09
commit a4166de502
15 changed files with 153 additions and 33 deletions
@@ -148,8 +148,8 @@ class AudioPlayer: NSObject, AudioPlayerProtocol {
releaseAudioSessionTask = Task { [weak self] in
try? await Task.sleep(for: .seconds(timeInterval))
guard !Task.isCancelled else { return }
guard let self else { return }
self.releaseAudioSession()
self?.releaseAudioSession()
}
}
@@ -180,10 +180,10 @@ class AudioPlayer: NSObject, AudioPlayerProtocol {
switch playerItem.status {
case .failed:
self.setInternalState(.error(playerItem.error ?? AudioPlayerError.genericError))
setInternalState(.error(playerItem.error ?? AudioPlayerError.genericError))
case .readyToPlay:
guard state == .loading else { return }
self.setInternalState(.readyToPlay)
setInternalState(.readyToPlay)
default:
break
}
@@ -193,20 +193,20 @@ class AudioPlayer: NSObject, AudioPlayerProtocol {
guard let self else { return }
if internalAudioPlayer.rate == 0 {
if self.isStopped {
self.setInternalState(.stopped)
if isStopped {
setInternalState(.stopped)
} else {
self.setInternalState(.paused)
setInternalState(.paused)
}
} else {
self.setInternalState(.playing)
setInternalState(.playing)
}
}
NotificationCenter.default.publisher(for: Notification.Name.AVPlayerItemDidPlayToEndTime)
.sink { [weak self] _ in
guard let self else { return }
self.setInternalState(.finishedPlaying)
setInternalState(.finishedPlaying)
}
.store(in: &cancellables)
}
@@ -7,6 +7,7 @@
import Combine
import Foundation
import MediaPlayer
import UIKit
enum AudioPlayerPlaybackState {
@@ -25,16 +26,15 @@ enum AudioPlayerStateIdentifier {
@MainActor
class AudioPlayerState: ObservableObject, Identifiable {
let id: AudioPlayerStateIdentifier
let title: String
private(set) var duration: Double
let waveform: EstimatedWaveform
@Published private(set) var progress: Double
@Published private(set) var playbackState: AudioPlayerPlaybackState
/// It's similar to `playbackState`, with the a difference: `.loading`
/// updates are delayed by a fixed amount of time
@Published private(set) var playerButtonPlaybackState: AudioPlayerPlaybackState
@Published private(set) var progress: Double
var showProgressIndicator: Bool {
progress > 0
}
private weak var audioPlayer: AudioPlayerProtocol?
private var audioPlayerSubscription: AnyCancellable?
@@ -44,6 +44,10 @@ class AudioPlayerState: ObservableObject, Identifiable {
/// The file url that the last player attached to this object has loaded.
/// The file url persists even if the AudioPlayer will be detached later.
private(set) var fileURL: URL?
var showProgressIndicator: Bool {
progress > 0
}
var isAttached: Bool {
audioPlayer != nil
@@ -53,8 +57,9 @@ class AudioPlayerState: ObservableObject, Identifiable {
displayLink != nil
}
init(id: AudioPlayerStateIdentifier, duration: Double, waveform: EstimatedWaveform? = nil, progress: Double = 0.0) {
init(id: AudioPlayerStateIdentifier, title: String, duration: Double, waveform: EstimatedWaveform? = nil, progress: Double = 0.0) {
self.id = id
self.title = title
self.duration = duration
self.waveform = waveform ?? EstimatedWaveform(data: [])
self.progress = progress
@@ -137,12 +142,19 @@ class AudioPlayerState: ObservableObject, Identifiable {
}
startPublishProgress()
playbackState = .playing
case .didPausePlaying, .didStopPlaying, .didFinishPlaying:
setUpRemoteCommandCenter()
case .didPausePlaying:
stopPublishProgress()
playbackState = .stopped
if case .didFinishPlaying = action {
progress = 0.0
}
case .didStopPlaying:
playbackState = .stopped
stopPublishProgress()
tearDownRemoteCommandCenter()
case .didFinishPlaying:
playbackState = .stopped
progress = 0.0
stopPublishProgress()
tearDownRemoteCommandCenter()
case .didFailWithError:
stopPublishProgress()
playbackState = .error
@@ -163,6 +175,8 @@ class AudioPlayerState: ObservableObject, Identifiable {
if let currentTime = audioPlayer?.currentTime, duration > 0 {
progress = currentTime / duration
}
updateNowPlayingInfoCenter()
}
private func stopPublishProgress() {
@@ -191,6 +205,93 @@ class AudioPlayerState: ObservableObject, Identifiable {
.removeDuplicates()
.weakAssign(to: \.playerButtonPlaybackState, on: self)
}
private func setUpRemoteCommandCenter() {
UIApplication.shared.beginReceivingRemoteControlEvents()
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.removeTarget(nil)
commandCenter.playCommand.addTarget { [weak self] _ in
guard let audioPlayer = self?.audioPlayer else {
return MPRemoteCommandHandlerStatus.commandFailed
}
audioPlayer.play()
return MPRemoteCommandHandlerStatus.success
}
commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.removeTarget(nil)
commandCenter.pauseCommand.addTarget { [weak self] _ in
guard let audioPlayer = self?.audioPlayer else {
return MPRemoteCommandHandlerStatus.commandFailed
}
audioPlayer.pause()
return MPRemoteCommandHandlerStatus.success
}
commandCenter.skipForwardCommand.isEnabled = true
commandCenter.skipForwardCommand.removeTarget(nil)
commandCenter.skipForwardCommand.addTarget { [weak self] event in
guard let audioPlayer = self?.audioPlayer, let skipEvent = event as? MPSkipIntervalCommandEvent else {
return MPRemoteCommandHandlerStatus.commandFailed
}
Task {
await audioPlayer.seek(to: audioPlayer.currentTime + skipEvent.interval)
}
return MPRemoteCommandHandlerStatus.success
}
commandCenter.skipBackwardCommand.isEnabled = true
commandCenter.skipBackwardCommand.removeTarget(nil)
commandCenter.skipBackwardCommand.addTarget { [weak self] event in
guard let audioPlayer = self?.audioPlayer, let skipEvent = event as? MPSkipIntervalCommandEvent else {
return MPRemoteCommandHandlerStatus.commandFailed
}
Task {
await audioPlayer.seek(to: audioPlayer.currentTime - skipEvent.interval)
}
return MPRemoteCommandHandlerStatus.success
}
}
private func tearDownRemoteCommandCenter() {
UIApplication.shared.endReceivingRemoteControlEvents()
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
nowPlayingInfoCenter.nowPlayingInfo = nil
nowPlayingInfoCenter.playbackState = .stopped
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.isEnabled = false
commandCenter.playCommand.removeTarget(nil)
commandCenter.pauseCommand.isEnabled = false
commandCenter.pauseCommand.removeTarget(nil)
commandCenter.skipForwardCommand.isEnabled = false
commandCenter.skipForwardCommand.removeTarget(nil)
commandCenter.skipBackwardCommand.isEnabled = false
commandCenter.skipBackwardCommand.removeTarget(nil)
}
private func updateNowPlayingInfoCenter() {
guard let audioPlayer else {
return
}
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
nowPlayingInfoCenter.nowPlayingInfo = [MPMediaItemPropertyTitle: title,
MPMediaItemPropertyPlaybackDuration: audioPlayer.duration as Any,
MPNowPlayingInfoPropertyElapsedPlaybackTime: audioPlayer.currentTime as Any]
}
}
extension AudioPlayerState: Equatable {