Version 1.1 App Store
This commit is contained in:
@@ -11,6 +11,12 @@ import AppKit
|
||||
private typealias PlayerArtworkImage = NSImage
|
||||
#endif
|
||||
|
||||
enum SleepTimerMode: Equatable, Hashable {
|
||||
case off
|
||||
case minutes(Int)
|
||||
case endOfBook
|
||||
}
|
||||
|
||||
@Observable
|
||||
@MainActor
|
||||
final class PlayerEngine {
|
||||
@@ -21,6 +27,11 @@ final class PlayerEngine {
|
||||
var isReady: Bool = false
|
||||
var errorMessage: String?
|
||||
|
||||
var sleepTimer: SleepTimerMode = .off
|
||||
/// Verbleibende Wallclock-Sekunden bis der Sleep-Timer auslöst (0 wenn off).
|
||||
/// Pausiert mit der Wiedergabe; bei `.endOfBook` rate-skaliert aus der Restspielzeit.
|
||||
var sleepRemainingSeconds: Double = 0
|
||||
|
||||
private var player: AVQueuePlayer?
|
||||
private var trackDurations: [Double] = []
|
||||
private var trackPlayerItems: [AVPlayerItem] = []
|
||||
@@ -28,6 +39,7 @@ final class PlayerEngine {
|
||||
private var timeObserver: Any?
|
||||
private var endObservers: [NSObjectProtocol] = []
|
||||
private var isSeeking: Bool = false
|
||||
private var sleepTickTask: Task<Void, Never>?
|
||||
|
||||
var itemId: String?
|
||||
|
||||
@@ -127,12 +139,16 @@ final class PlayerEngine {
|
||||
player?.play()
|
||||
player?.rate = rate
|
||||
isPlaying = true
|
||||
if case .minutes = sleepTimer, sleepRemainingSeconds > 0, sleepTickTask == nil {
|
||||
startSleepTickTask()
|
||||
}
|
||||
updateNowPlayingInfo()
|
||||
}
|
||||
|
||||
func pause() {
|
||||
player?.pause()
|
||||
isPlaying = false
|
||||
cancelSleepTickTask()
|
||||
updateNowPlayingInfo()
|
||||
}
|
||||
|
||||
@@ -143,6 +159,9 @@ final class PlayerEngine {
|
||||
func setRate(_ newRate: Float) {
|
||||
rate = newRate
|
||||
if isPlaying { player?.rate = newRate }
|
||||
if case .endOfBook = sleepTimer {
|
||||
sleepRemainingSeconds = wallclockRemainingUntilEndOfBook()
|
||||
}
|
||||
updateNowPlayingInfo()
|
||||
}
|
||||
|
||||
@@ -221,9 +240,13 @@ final class PlayerEngine {
|
||||
let wasPlaying = isPlaying
|
||||
isPlaying = player.timeControlStatus == .playing
|
||||
if wasPlaying != isPlaying { updateNowPlayingInfo() }
|
||||
updateEndOfBookSleep()
|
||||
}
|
||||
|
||||
func teardown() {
|
||||
cancelSleepTickTask()
|
||||
sleepTimer = .off
|
||||
sleepRemainingSeconds = 0
|
||||
if let token = timeObserver { player?.removeTimeObserver(token) }
|
||||
timeObserver = nil
|
||||
for obs in endObservers { NotificationCenter.default.removeObserver(obs) }
|
||||
@@ -248,6 +271,57 @@ final class PlayerEngine {
|
||||
clearNowPlayingInfo()
|
||||
}
|
||||
|
||||
// MARK: - Sleep timer
|
||||
|
||||
func setSleepTimer(_ mode: SleepTimerMode) {
|
||||
cancelSleepTickTask()
|
||||
sleepTimer = mode
|
||||
switch mode {
|
||||
case .off:
|
||||
sleepRemainingSeconds = 0
|
||||
case .minutes(let m):
|
||||
sleepRemainingSeconds = Double(m * 60)
|
||||
if isPlaying { startSleepTickTask() }
|
||||
case .endOfBook:
|
||||
sleepRemainingSeconds = wallclockRemainingUntilEndOfBook()
|
||||
}
|
||||
}
|
||||
|
||||
private func startSleepTickTask() {
|
||||
cancelSleepTickTask()
|
||||
sleepTickTask = Task { @MainActor [weak self] in
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||
guard !Task.isCancelled, let self else { return }
|
||||
self.sleepRemainingSeconds = max(0, self.sleepRemainingSeconds - 0.5)
|
||||
if self.sleepRemainingSeconds <= 0 {
|
||||
self.sleepTimer = .off
|
||||
self.pause()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func cancelSleepTickTask() {
|
||||
sleepTickTask?.cancel()
|
||||
sleepTickTask = nil
|
||||
}
|
||||
|
||||
private func updateEndOfBookSleep() {
|
||||
guard case .endOfBook = sleepTimer else { return }
|
||||
sleepRemainingSeconds = wallclockRemainingUntilEndOfBook()
|
||||
if sleepRemainingSeconds <= 0 {
|
||||
sleepTimer = .off
|
||||
}
|
||||
}
|
||||
|
||||
private func wallclockRemainingUntilEndOfBook() -> Double {
|
||||
let playback = max(0, totalDuration - absoluteCurrentTime)
|
||||
let r = max(0.1, Double(rate))
|
||||
return playback / r
|
||||
}
|
||||
|
||||
// MARK: - Now-playing / remote commands
|
||||
|
||||
private func configureRemoteCommandsIfNeeded() {
|
||||
|
||||
Reference in New Issue
Block a user