You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
Music/Music/Services/AudioService.swift

103 lines
3.1 KiB

import AVFoundation
import Observation
// With the project-wide `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor` setting,
// this class is implicitly @MainActor. That's intentional: @Observable properties
// (isPlaying, currentTime, duration, volume) drive the UI and must be updated on
// the main actor. `deinit` is marked `nonisolated` because Swift does not allow
// actor-isolated deinits it simply nils out the player reference so ARC handles
// cleanup safely without crossing actor boundaries.
@Observable
final class AudioService {
var isPlaying = false
var currentTime: Double = 0
var duration: Double = 0
var volume: Float = 0.65 {
didSet { player?.volume = volume }
}
private var player: AVPlayer?
private var timeObserver: Any?
private var endObserver: NSObjectProtocol?
var onTrackFinished: (() -> Void)?
func play(url: URL) {
cleanup()
let item = AVPlayerItem(url: url)
player = AVPlayer(playerItem: item)
player?.volume = volume
timeObserver = player?.addPeriodicTimeObserver(
forInterval: CMTime(seconds: 0.5, preferredTimescale: 600),
queue: .main
) { [weak self] time in
guard let self else { return }
self.currentTime = time.seconds
if let dur = self.player?.currentItem?.duration, dur.isValid, !dur.isIndefinite {
self.duration = dur.seconds
}
}
endObserver = NotificationCenter.default.addObserver(
forName: .AVPlayerItemDidPlayToEndTime,
object: item,
queue: .main
) { [weak self] _ in
self?.isPlaying = false
self?.currentTime = 0
self?.onTrackFinished?()
}
player?.play()
isPlaying = true
}
func pause() {
player?.pause()
isPlaying = false
}
func resume() {
player?.play()
isPlaying = true
}
func togglePlayPause() {
if isPlaying { pause() } else { resume() }
}
func seek(to time: Double) {
player?.seek(to: CMTime(seconds: time, preferredTimescale: 600))
}
func stop() {
cleanup()
isPlaying = false
currentTime = 0
duration = 0
}
private func cleanup() {
if let obs = timeObserver {
player?.removeTimeObserver(obs)
timeObserver = nil
}
if let obs = endObserver {
NotificationCenter.default.removeObserver(obs)
endObserver = nil
}
player?.pause()
player = nil
}
// `nonisolated` is required because Swift does not allow actor-isolated deinits.
// The player and observers are nilled out here; ARC will handle actual deallocation
// safely without needing to hop to the main actor.
nonisolated deinit {
// Cannot call the MainActor-isolated cleanup() from deinit.
// The weak references in closures will be invalidated when self is deallocated,
// and AVPlayer/NotificationCenter observers are released via ARC.
}
}