154 lines
6.8 KiB
Swift
154 lines
6.8 KiB
Swift
import AVFoundation
|
|
import Combine
|
|
import Foundation
|
|
import MediaPlayer
|
|
|
|
@MainActor
|
|
final class AudioPlayerModel: ObservableObject {
|
|
@Published private(set) var title = ""
|
|
@Published private(set) var artist = ""
|
|
@Published private(set) var duration: Double = 0
|
|
@Published private(set) var position: Double = 0
|
|
@Published private(set) var isPlaying = false
|
|
@Published private(set) var isLoading = false
|
|
@Published private(set) var statusMessage = "Bereit"
|
|
@Published private(set) var currentURL: URL?
|
|
@Published var volume: Double = 0.8 { didSet { player.volume = Float(volume) } }
|
|
|
|
private let player = AVPlayer()
|
|
private let commandCenter = MPRemoteCommandCenter.shared()
|
|
private var observer: Any?
|
|
private var itemObservation: NSKeyValueObservation?
|
|
private var endObserver: NSObjectProtocol?
|
|
private var remoteCommandTargets: [(MPRemoteCommand, Any)] = []
|
|
private var queue: [MusicTrack] = []
|
|
private var currentIndex = 0
|
|
|
|
init() {
|
|
player.automaticallyWaitsToMinimizeStalling = true
|
|
installRemoteCommands()
|
|
observer = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.25, preferredTimescale: 600), queue: .main) { [weak self] time in
|
|
Task { @MainActor [weak self] in
|
|
guard let self else { return }
|
|
self.position = time.seconds.isFinite ? time.seconds : 0
|
|
let seconds = self.player.currentItem?.duration.seconds ?? 0
|
|
if seconds.isFinite { self.duration = seconds }
|
|
self.isPlaying = self.player.rate > 0
|
|
self.updateNowPlayingInfo()
|
|
}
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
if let observer { player.removeTimeObserver(observer) }
|
|
if let endObserver { NotificationCenter.default.removeObserver(endObserver) }
|
|
for (command, target) in remoteCommandTargets { command.removeTarget(target) }
|
|
}
|
|
|
|
func play(_ tracks: [MusicTrack], startingAt index: Int) {
|
|
guard tracks.indices.contains(index) else { return }
|
|
queue = tracks; currentIndex = index
|
|
loadCurrent(autoplay: true)
|
|
}
|
|
|
|
func isCurrentTrack(in tracks: [MusicTrack]) -> Bool {
|
|
guard let currentURL else { return false }
|
|
return tracks.contains { $0.url.standardizedFileURL == currentURL.standardizedFileURL }
|
|
}
|
|
|
|
func toggle() {
|
|
guard currentURL != nil else { statusMessage = "Wähle zuerst einen Track aus."; return }
|
|
if isPlaying { player.pause(); isPlaying = false; statusMessage = "Pausiert" }
|
|
else if player.currentItem?.status == .failed { loadCurrent(autoplay: true) }
|
|
else { player.play(); isPlaying = true; statusMessage = "Wiedergabe wird gestartet…" }
|
|
updateNowPlayingInfo()
|
|
}
|
|
|
|
func seek(_ value: Double) { player.seek(to: CMTime(seconds: value, preferredTimescale: 600)) }
|
|
func next() { guard currentIndex + 1 < queue.count else { return }; currentIndex += 1; loadCurrent(autoplay: true) }
|
|
func previous() { guard currentIndex > 0 else { return }; currentIndex -= 1; loadCurrent(autoplay: true) }
|
|
|
|
private func loadCurrent(autoplay: Bool) {
|
|
let track = queue[currentIndex]
|
|
title = track.displayTitle; artist = track.artist; currentURL = track.url
|
|
duration = 0; position = 0; isPlaying = false; isLoading = true
|
|
statusMessage = "Lade \(track.url.lastPathComponent)…"
|
|
updateNowPlayingInfo()
|
|
|
|
if let endObserver { NotificationCenter.default.removeObserver(endObserver); self.endObserver = nil }
|
|
itemObservation = nil
|
|
let item = AVPlayerItem(url: track.url)
|
|
itemObservation = item.observe(\.status, options: [.initial, .new]) { [weak self] item, _ in
|
|
Task { @MainActor in
|
|
guard let self, self.currentURL == track.url else { return }
|
|
switch item.status {
|
|
case .readyToPlay:
|
|
self.isLoading = false
|
|
let seconds = item.duration.seconds
|
|
if seconds.isFinite { self.duration = seconds }
|
|
self.statusMessage = autoplay ? "Wiedergabe" : "Bereit zum Abspielen"
|
|
if autoplay { self.player.play(); self.isPlaying = true }
|
|
self.updateNowPlayingInfo()
|
|
case .failed:
|
|
self.isLoading = false; self.isPlaying = false
|
|
self.statusMessage = "Wiedergabe fehlgeschlagen: \(item.error?.localizedDescription ?? "Unbekannter Fehler")"
|
|
self.updateNowPlayingInfo()
|
|
case .unknown: break
|
|
@unknown default: break
|
|
}
|
|
}
|
|
}
|
|
player.replaceCurrentItem(with: item)
|
|
endObserver = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: item, queue: .main) { [weak self] _ in
|
|
Task { @MainActor in self?.next() }
|
|
}
|
|
}
|
|
|
|
private func installRemoteCommands() {
|
|
let toggleTarget = commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
|
|
Task { @MainActor [weak self] in self?.toggle() }
|
|
return .success
|
|
}
|
|
let playTarget = commandCenter.playCommand.addTarget { [weak self] _ in
|
|
Task { @MainActor [weak self] in
|
|
guard let self, self.currentURL != nil, !self.isPlaying else { return }
|
|
self.toggle()
|
|
}
|
|
return .success
|
|
}
|
|
let pauseTarget = commandCenter.pauseCommand.addTarget { [weak self] _ in
|
|
Task { @MainActor [weak self] in
|
|
guard let self, self.isPlaying else { return }
|
|
self.toggle()
|
|
}
|
|
return .success
|
|
}
|
|
let nextTarget = commandCenter.nextTrackCommand.addTarget { [weak self] _ in
|
|
Task { @MainActor [weak self] in self?.next() }
|
|
return .success
|
|
}
|
|
let previousTarget = commandCenter.previousTrackCommand.addTarget { [weak self] _ in
|
|
Task { @MainActor [weak self] in self?.previous() }
|
|
return .success
|
|
}
|
|
remoteCommandTargets = [
|
|
(commandCenter.togglePlayPauseCommand, toggleTarget),
|
|
(commandCenter.playCommand, playTarget),
|
|
(commandCenter.pauseCommand, pauseTarget),
|
|
(commandCenter.nextTrackCommand, nextTarget),
|
|
(commandCenter.previousTrackCommand, previousTarget)
|
|
]
|
|
}
|
|
|
|
private func updateNowPlayingInfo() {
|
|
var info: [String: Any] = [
|
|
MPMediaItemPropertyTitle: title,
|
|
MPMediaItemPropertyArtist: artist,
|
|
MPNowPlayingInfoPropertyElapsedPlaybackTime: position,
|
|
MPNowPlayingInfoPropertyPlaybackRate: isPlaying ? 1.0 : 0.0
|
|
]
|
|
if duration > 0 { info[MPMediaItemPropertyPlaybackDuration] = duration }
|
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
|
|
}
|
|
}
|