// // VoicePlayerManager.swift // QuickLocation // // Created by 八条 on 2026/6/5. // import AVFoundation final class VoicePlayerManager { static let shared = VoicePlayerManager() private var player: AVPlayer? private var timeObserver: Any? private var currentURL: String? private var onStateChange: ((State) -> Void)? private var onFinished: (() -> Void)? enum State { case idle, playing, paused } private(set) var state: State = .idle private init() {} func play(urlString: String, playbackView: VoicePlaybackView, onStateChange: @escaping (State) -> Void, onFinished: @escaping () -> Void) { // Stop current if different source if currentURL != urlString { stop() } guard let url = URL(string: urlString) else { return } currentURL = urlString self.onStateChange = onStateChange self.onFinished = onFinished if player == nil { player = AVPlayer(url: url) } if state == .paused { player?.play() state = .playing onStateChange(.playing) playbackView.play() } else { player?.replaceCurrentItem(with: AVPlayerItem(url: url)) player?.play() state = .playing onStateChange(.playing) playbackView.play() if let obs = timeObserver { player?.removeTimeObserver(obs) } timeObserver = player?.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 10), queue: .main) { [weak self] time in guard let self = self, let duration = self.player?.currentItem?.duration, duration.isValid, !duration.isIndefinite, CMTimeCompare(time, duration) >= 0 else { return } self.stop() self.onFinished?() } } } func isCurrent(url: String) -> Bool { currentURL == url } func pause() { player?.pause() state = .paused onStateChange?(.paused) } func stop() { player?.pause() player?.replaceCurrentItem(with: nil) if let obs = timeObserver { player?.removeTimeObserver(obs) timeObserver = nil } let prevFinished = onFinished onStateChange = nil onFinished = nil player = nil currentURL = nil state = .idle prevFinished?() } } protocol VoicePlaybackView: AnyObject { func play() func stop() }