2026-09-14 00:17:07 +03:00
|
|
|
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<Void, Never>?
|
|
|
|
|
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 }
|
2026-09-14 00:48:25 +03:00
|
|
|
busy = true
|
|
|
|
|
id = savedID
|
|
|
|
|
let operation = generation
|
|
|
|
|
defer { if generation == operation { busy = false } }
|
2026-09-14 00:17:07 +03:00
|
|
|
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<SystemPlayerAttributes>.sessions() where session.id == savedID { try await session.end() }
|
2026-09-14 00:48:25 +03:00
|
|
|
if generation == operation { id = nil }
|
2026-09-14 00:17:07 +03:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if let session = try await RemoteMediaSession<SystemPlayerAttributes>.sessions().first(where: { $0.id == savedID }) {
|
2026-09-14 00:48:25 +03:00
|
|
|
guard generation == operation else { return }
|
2026-09-14 00:17:07 +03:00
|
|
|
storage = session; id = savedID; running = true
|
|
|
|
|
} else {
|
2026-09-14 00:48:25 +03:00
|
|
|
guard generation == operation else { return }
|
2026-09-14 00:17:07 +03:00
|
|
|
try SystemPlayerCredentials.delete(savedID)
|
|
|
|
|
UserDefaults.standard.removeObject(forKey: savedIDKey)
|
2026-09-14 00:48:25 +03:00
|
|
|
id = nil
|
2026-09-14 00:17:07 +03:00
|
|
|
}
|
2026-09-14 00:48:25 +03:00
|
|
|
} catch { if generation == operation { self.error = BridgeError.describe(error); id = nil } }
|
2026-09-14 00:17:07 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func receive(_ state: MediaState, connection: Connection) {
|
|
|
|
|
guard #available(iOS 27.0, *), running, let id,
|
|
|
|
|
let session = storage as? RemoteMediaSession<SystemPlayerAttributes> 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<SystemPlayerAttributes> {
|
|
|
|
|
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() }
|
|
|
|
|
}
|
|
|
|
|
}
|