import ActivityKit import SwiftUI @MainActor final class LiveActivityManager: ObservableObject { @Published private(set) var running = false @Published private(set) var error: String? private var activity: Activity? private var updates: Task? private var observer: Task? private var last: MusicActivityAttributes.ContentState? init() { let existing = Activity.activities .filter { $0.activityState == .active || $0.activityState == .stale } if let first = existing.first { activity = first last = first.content.state running = true observe(first) } for extra in existing.dropFirst() { Task { await extra.end(nil, dismissalPolicy: .immediate) } } } static func content(_ state: MediaState, now: Date = Date()) -> MusicActivityAttributes.ContentState { .init(title: MusicActivityAttributes.ContentState.bounded(state.title.isEmpty ? "Без названия" : state.title), artist: MusicActivityAttributes.ContentState.bounded(state.artist), playing: state.playbackStatus == "Playing", position: max(0, state.position(elapsed: 0) - state.startSeconds), duration: max(0, state.endSeconds - state.startSeconds), updatedAt: now, connected: true) } func start(_ state: MediaState) { guard !running, state.hasSession, UIApplication.shared.applicationState == .active else { return } guard ActivityAuthorizationInfo().areActivitiesEnabled else { error = "Разрешите Live Activities в настройках MusicBridge на iPhone." return } do { let value = Self.content(state) let new = try Activity.request(attributes: MusicActivityAttributes(), content: ActivityContent(state: value, staleDate: Date().addingTimeInterval(15)), pushType: nil) activity = new last = value running = true error = nil observe(new) } catch { self.error = "Не удалось включить Live Activity: " + error.localizedDescription } } func receive(_ state: MediaState) { guard state.hasSession else { end(); return } guard let activity, running else { return } let value = Self.content(state) // Coalesce ordinary position ticks, but publish track/playback changes immediately. if let last, last.connected, last.title == value.title, last.artist == value.artist, last.playing == value.playing, last.duration == value.duration, abs(last.position - value.position) < 5, value.updatedAt.timeIntervalSince(last.updatedAt) < 5 { return } last = value enqueue { await activity.update(ActivityContent(state: value, staleDate: value.updatedAt.addingTimeInterval(15))) } } func unavailable() { guard let activity, var value = last, value.connected else { return } value.connected = false last = value enqueue { await activity.update(ActivityContent(state: value, staleDate: Date())) } } func end() { guard let activity else { return } self.activity = nil running = false last = nil observer?.cancel() enqueue { await activity.end(nil, dismissalPolicy: .immediate) } } private func enqueue(_ operation: @escaping @MainActor () async -> Void) { let previous = updates updates = Task { await previous?.value; await operation() } } private func observe(_ activity: Activity) { observer?.cancel() observer = Task { [weak self] in for await state in activity.activityStateUpdates { guard !Task.isCancelled, let self, self.activity?.id == activity.id else { return } if state == .dismissed || state == .ended { self.activity = nil; self.running = false; self.last = nil return } } } } }