import SwiftUI import NowPlaying @MainActor final class SystemPlayerManager: ObservableObject { @Published private(set) var running = false @Published private(set) var busy = false @Published private(set) var error: String? private var storage: Any? private var id: String? private var generation = UUID() private var updates: Task? private var lastUpdate = Date.distantPast private var lastTrack = "" private var lastPlaying = false private let savedIDKey = "MusicBridge.systemPlayerSession" func start(state: MediaState, connection: Connection) async { guard !busy, !running, state.hasSession, connection.token != nil, UIApplication.shared.applicationState == .active else { return } guard #available(iOS 27.0, *) else { error = "Системный пульт требует iOS 27."; return } busy = true; error = nil let operation = generation let newID = UUID().uuidString id = newID defer { if generation == operation { busy = false } } do { try SystemPlayerCredentials.save(connection, id: newID) _ = try SystemPlayerCredentials.load(newID) let session = try await RemoteMediaSession.start(attributes: SystemPlayerAttributes(id: newID, state: state)) guard generation == operation else { try? await session.end(); return } storage = session try await session.requestToBecomeSystemPrimary() guard generation == operation else { try? await session.end(); return } running = true UserDefaults.standard.set(newID, forKey: savedIDKey) } catch { guard generation == operation else { return } self.error = "Не удалось включить системный пульт: " + BridgeError.describe(error) end() } } func restore(connection: Connection?) async { guard #available(iOS 27.0, *), id == nil, !busy, let savedID = UserDefaults.standard.string(forKey: savedIDKey) else { return } busy = true id = savedID let operation = generation defer { if generation == operation { busy = false } } do { let saved = try SystemPlayerCredentials.load(savedID) guard let connection, saved.fingerprint == connection.fingerprint, saved.token == connection.token else { try SystemPlayerCredentials.delete(savedID) UserDefaults.standard.removeObject(forKey: savedIDKey) for session in try await RemoteMediaSession.sessions() where session.id == savedID { try await session.end() } if generation == operation { id = nil } return } if let session = try await RemoteMediaSession.sessions().first(where: { $0.id == savedID }) { guard generation == operation else { return } storage = session; id = savedID; running = true } else { guard generation == operation else { return } try SystemPlayerCredentials.delete(savedID) UserDefaults.standard.removeObject(forKey: savedIDKey) id = nil } } catch { if generation == operation { self.error = BridgeError.describe(error); id = nil } } } func receive(_ state: MediaState, connection: Connection) { guard #available(iOS 27.0, *), running, let id, let session = storage as? RemoteMediaSession else { return } guard state.hasSession else { end(); return } let track = state.title + "\n" + state.artist let playing = state.playbackStatus == "Playing" guard track != lastTrack || playing != lastPlaying || Date().timeIntervalSince(lastUpdate) >= 5 else { return } lastUpdate = Date(); lastTrack = track; lastPlaying = playing let value = SystemPlayerAttributes(id: id, state: state) let operation = generation enqueue { [weak self] in guard let self, self.generation == operation else { return } do { try SystemPlayerCredentials.save(connection, id: id) try await session.update(value) } catch { self.error = BridgeError.describe(error) } } } func end() { generation = UUID() busy = false; running = false if let id { do { try SystemPlayerCredentials.delete(id) } catch { self.error = BridgeError.describe(error) } } UserDefaults.standard.removeObject(forKey: savedIDKey) id = nil if #available(iOS 27.0, *), let session = storage as? RemoteMediaSession { enqueue { [weak self] in do { try await session.end() } catch { self?.error = BridgeError.describe(error) } } } storage = nil } private func enqueue(_ action: @escaping @MainActor () async -> Void) { let previous = updates updates = Task { await previous?.value; await action() } } }