Add iOS 27 remote media session and system player extension
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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 }
|
||||
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() }
|
||||
return
|
||||
}
|
||||
if let session = try await RemoteMediaSession<SystemPlayerAttributes>.sessions().first(where: { $0.id == savedID }) {
|
||||
storage = session; id = savedID; running = true
|
||||
} else {
|
||||
try SystemPlayerCredentials.delete(savedID)
|
||||
UserDefaults.standard.removeObject(forKey: savedIDKey)
|
||||
}
|
||||
} catch { self.error = BridgeError.describe(error) }
|
||||
}
|
||||
|
||||
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() }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user