You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
Music/MusicShared/Sources/MusicShared/RemoteProtocol.swift

193 lines
6.3 KiB

import Foundation
// MARK: - Protocol Version
/// Current version of the remote control wire protocol.
public nonisolated let RemoteProtocolVersion: Int = 1
// MARK: - Supporting Types
/// Snapshot of the host's playback state, sent to remote clients.
public nonisolated struct PlaybackStatePayload: Codable, Equatable, Sendable {
public var trackId: Int64?
public var isPlaying: Bool
public var currentTime: Double
public var duration: Double
public var volume: Float
public var isShuffled: Bool
public init(
trackId: Int64? = nil,
isPlaying: Bool,
currentTime: Double,
duration: Double,
volume: Float,
isShuffled: Bool
) {
self.trackId = trackId
self.isPlaying = isPlaying
self.currentTime = currentTime
self.duration = duration
self.volume = volume
self.isShuffled = isShuffled
}
}
/// Exchanged during connection setup to agree on protocol version.
public nonisolated struct HandshakeMessage: Codable, Equatable, Sendable {
public var protocolVersion: Int
public var appVersion: String
public init(protocolVersion: Int, appVersion: String) {
self.protocolVersion = protocolVersion
self.appVersion = appVersion
}
}
// MARK: - RemoteCommand
/// Commands sent from a remote client to the host.
/// Wire format: `{"type":"<case>","payload":{...}}` (payload omitted for cases with no associated values).
public nonisolated enum RemoteCommand: Equatable, Sendable {
case play(trackId: Int64, queueIds: [Int64])
case pause
case resume
case next
case previous
case seek(position: Double)
case setVolume(level: Float)
case toggleShuffle
case refreshDB
}
extension RemoteCommand: Codable {
private enum TypeKey: String, Codable {
case play, pause, resume, next, previous, seek, setVolume, toggleShuffle, refreshDB
}
private enum CodingKeys: String, CodingKey {
case type, payload
}
// Payload structs for cases with associated values
private struct PlayPayload: Codable {
var trackId: Int64
var queueIds: [Int64]
}
private struct SeekPayload: Codable {
var position: Double
}
private struct VolumePayload: Codable {
var level: Float
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .play(let trackId, let queueIds):
try container.encode(TypeKey.play, forKey: .type)
try container.encode(PlayPayload(trackId: trackId, queueIds: queueIds), forKey: .payload)
case .pause:
try container.encode(TypeKey.pause, forKey: .type)
case .resume:
try container.encode(TypeKey.resume, forKey: .type)
case .next:
try container.encode(TypeKey.next, forKey: .type)
case .previous:
try container.encode(TypeKey.previous, forKey: .type)
case .seek(let position):
try container.encode(TypeKey.seek, forKey: .type)
try container.encode(SeekPayload(position: position), forKey: .payload)
case .setVolume(let level):
try container.encode(TypeKey.setVolume, forKey: .type)
try container.encode(VolumePayload(level: level), forKey: .payload)
case .toggleShuffle:
try container.encode(TypeKey.toggleShuffle, forKey: .type)
case .refreshDB:
try container.encode(TypeKey.refreshDB, forKey: .type)
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(TypeKey.self, forKey: .type)
switch type {
case .play:
let payload = try container.decode(PlayPayload.self, forKey: .payload)
self = .play(trackId: payload.trackId, queueIds: payload.queueIds)
case .pause:
self = .pause
case .resume:
self = .resume
case .next:
self = .next
case .previous:
self = .previous
case .seek:
let payload = try container.decode(SeekPayload.self, forKey: .payload)
self = .seek(position: payload.position)
case .setVolume:
let payload = try container.decode(VolumePayload.self, forKey: .payload)
self = .setVolume(level: payload.level)
case .toggleShuffle:
self = .toggleShuffle
case .refreshDB:
self = .refreshDB
}
}
}
// MARK: - HostEvent
/// Events sent from the host to remote clients.
/// Wire format: `{"type":"<case>","payload":{...}}` (payload omitted for cases with no associated values).
public nonisolated enum HostEvent: Equatable, Sendable {
case playbackState(PlaybackStatePayload)
case dbReady
case error(message: String)
}
extension HostEvent: Codable {
private enum TypeKey: String, Codable {
case playbackState, dbReady, error
}
private enum CodingKeys: String, CodingKey {
case type, payload
}
private struct ErrorPayload: Codable {
var message: String
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .playbackState(let payload):
try container.encode(TypeKey.playbackState, forKey: .type)
try container.encode(payload, forKey: .payload)
case .dbReady:
try container.encode(TypeKey.dbReady, forKey: .type)
case .error(let message):
try container.encode(TypeKey.error, forKey: .type)
try container.encode(ErrorPayload(message: message), forKey: .payload)
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(TypeKey.self, forKey: .type)
switch type {
case .playbackState:
let payload = try container.decode(PlaybackStatePayload.self, forKey: .payload)
self = .playbackState(payload)
case .dbReady:
self = .dbReady
case .error:
let payload = try container.decode(ErrorPayload.self, forKey: .payload)
self = .error(message: payload.message)
}
}
}