import AVFoundation import Foundation import MediaPlayer /// An internet radio stream the user saved in Browse > Web Radio. struct WebStation: Codable, Hashable, Identifiable { var id = UUID().uuidString var name: String var url: String } /// Plays a web radio stream through AVPlayer, outside the stem engine. Starting a station pauses /// the queue, and playing the queue again stops the station. Main-thread only, like PlaybackManager. @Observable final class WebRadio: @unchecked Sendable { private let pm: PlaybackManager @ObservationIgnored private let player = AVPlayer() @ObservationIgnored weak var nowPlaying: NowPlaying? private(set) var station: WebStation? private(set) var playing = false /// A station is loaded (playing or paused); the lock screen belongs to the radio. var active: Bool { station != nil } init(pm: PlaybackManager) { self.pm = pm player.automaticallyWaitsToMinimizeStalling = true pm.addListener { [weak self] in guard let self, self.active, pm.playWhenReady else { return } self.stop() } } func play(_ s: WebStation) { guard let url = URL(string: s.url) else { return } station = s if pm.playWhenReady { pm.pause() } let a = AVAudioSession.sharedInstance() try? a.setCategory(.playback, mode: .default, policy: .longFormAudio) try? a.setActive(true) player.replaceCurrentItem(with: AVPlayerItem(url: url)) player.play() playing = true publish() } func resume() { guard active else { return } // Live streams go stale while paused; rejoin at the live edge. if let s = station { play(s) } } func pause() { player.pause() playing = false publish() } func toggle() { if playing { pause() } else { resume() } } func stop() { player.pause() player.replaceCurrentItem(with: nil) station = nil playing = false nowPlaying?.refresh() } private func publish() { guard let s = station else { return } let c = MPNowPlayingInfoCenter.default() c.nowPlayingInfo = [ MPMediaItemPropertyTitle: s.name, MPMediaItemPropertyArtist: "Web Radio", MPNowPlayingInfoPropertyIsLiveStream: true, MPNowPlayingInfoPropertyPlaybackRate: playing ? 1.0 : 0.0, ] c.playbackState = playing ? .playing : .paused } }