import Domain import Foundation public actor PlaybackSession { public typealias Now = @Sendable () -> ContinuousClock.Instant private let engine: any OutputEngine private let now: Now private var state = PlaybackState() private var originalOrder: [QueueItem.ID] = [] private var observers: [UUID: AsyncStream.Continuation] = [:] private var eventTask: Task? private var preparedKey: [QueueItem.ID?] = [] private var failureStreak = 0 private let restartThreshold: TimeInterval = 3 public init(engine: any OutputEngine, now: @escaping Now = { .now }) { self.engine = engine self.now = now } public func states() -> AsyncStream { listenToEngine() let key = UUID() let (stream, continuation) = AsyncStream.makeStream(bufferingPolicy: .bufferingNewest(1)) observers[key] = continuation continuation.onTermination = { [weak self] _ in Task { await self?.removeObserver(key) } } continuation.yield(state) return stream } public var snapshot: PlaybackState { state } public func send(_ command: PlaybackCommand) async { listenToEngine() failureStreak = 0 switch command { case .play: await resume() case .pause: await pause() case .togglePlayPause: state.status == .playing ? await pause() : await resume() case let .seek(position): await seek(to: position) case .next: await advance(auto: false) case .previous: await goBack() case let .setQueue(tracks, startIndex, shuffled): await setQueue(tracks, startIndex: startIndex, shuffled: shuffled) case let .insertNext(tracks): insert(tracks, atEnd: false) case let .insertLast(tracks): insert(tracks, atEnd: true) case let .jump(id): await jump(to: id) case let .move(item, after): move(item, after: after) case let .remove(id): state.upcoming.removeAll { $0.id == id } case .clearUpcoming: state.upcoming.removeAll() case .clearHistory: state.history.removeAll() case let .setShuffle(isOn): setShuffle(isOn) case let .setRepeat(mode): state.repeatMode = mode case let .setVolume(volume): guard volume.isFinite else { return } state.volume = min(max(volume, 0), 1) await engine.setVolume(state.volume) } publish() await syncPrepared() } private func listenToEngine() { guard eventTask == nil else { return } let events = engine.events eventTask = Task { [weak self] in for await event in events { await self?.handle(event) } } } private func handle(_ event: OutputEvent) async { switch event { case let .finished(id) where id == state.current?.id: failureStreak = 0 await finishCurrent() case let .failed(id) where id == state.current?.id: await failCurrent() case let .anchor(id, position, rate) where id == state.current?.id: state.anchor = PlaybackAnchor(position: position, instant: now(), rate: state.status == .playing ? rate : 0) default: return } publish() await syncPrepared() } private func finishCurrent() async { if state.repeatMode == .one { await start(at: 0, playing: true) } else { await advance(auto: true) } } private func failCurrent() async { failureStreak += 1 guard failureStreak <= state.history.count + state.upcoming.count else { anchor(0, playing: false) await engine.stop() return } await advance(auto: true) } private var nextToPrepare: QueueItem? { if let next = state.upcoming.first { return next } switch state.repeatMode { case .off: return nil case .one: return state.current case .all: return state.history.first ?? state.current } } private func syncPrepared() async { let key = [state.current?.id, nextToPrepare?.id] guard key != preparedKey else { return } preparedKey = key await engine.prepare(state.current == nil ? nil : nextToPrepare) } private func removeObserver(_ key: UUID) { observers[key] = nil } private func publish() { observers.values.forEach { $0.yield(state) } } private func anchor(_ position: TimeInterval, playing: Bool) { state.anchor = PlaybackAnchor(position: position, instant: now(), rate: playing ? 1 : 0) state.status = playing ? .playing : .paused } private var position: TimeInterval { let raw = state.anchor.position(at: now()) return min(max(raw, 0), state.current?.track.duration ?? 0) } private func resume() async { guard state.current != nil, state.status != .playing || state.anchor.rate == 0 else { return } anchor(position, playing: true) publish() await engine.play() } private func pause() async { guard state.status == .playing else { return } anchor(position, playing: false) publish() await engine.pause() } private func seek(to target: TimeInterval) async { guard let current = state.current else { return } let clamped = min(max(target, 0), current.track.duration) anchor(clamped, playing: state.status == .playing) publish() await engine.seek(to: clamped) } private func start(at position: TimeInterval, playing: Bool) async { guard let current = state.current else { state.status = .stopped state.anchor = PlaybackAnchor(position: 0, instant: now(), rate: 0) await engine.stop() return } state.anchor = PlaybackAnchor(position: position, instant: now(), rate: 0) state.status = playing ? .playing : .paused publish() await engine.load(current, at: position) anchor(position, playing: playing) if playing { await engine.play() } else { await engine.pause() } } private func setQueue(_ tracks: [Track], startIndex: Int, shuffled: Bool) async { let items = tracks.map { QueueItem(track: $0) } originalOrder = items.map(\.id) state.history = [] state.isShuffled = shuffled guard !items.isEmpty else { state.current = nil state.upcoming = [] await start(at: 0, playing: false) return } if shuffled { var rest = items let first = rest.remove(at: Int.random(in: rest.indices)) state.current = first state.upcoming = rest.shuffled() } else { let index = min(max(startIndex, 0), items.count - 1) state.history = Array(items[..