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 } }