81 lines
2.5 KiB
Swift
81 lines
2.5 KiB
Swift
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
|
|
}
|
|
}
|