75 lines
3.2 KiB
Swift
75 lines
3.2 KiB
Swift
import Foundation
|
|
|
|
/// Live connection state of a target, kept separate from playback so a single
|
|
/// dropped poll renders as "unreachable (last known …)" rather than wiping state.
|
|
public enum ConnectionState: String, Sendable, Equatable {
|
|
case checking, connected, unreachable
|
|
}
|
|
|
|
/// Unified playback state across every target. Volume is normalized to a
|
|
/// percentage (100 = normal) regardless of the backend's native scale.
|
|
public struct PlaybackStatus: Equatable, Sendable, Codable {
|
|
public var playing: Bool
|
|
public var paused: Bool?
|
|
public var title: String?
|
|
public var volume: Double? // percent (100 = normal)
|
|
public var position: Double? // seconds
|
|
public var duration: Double? // seconds
|
|
public var playlistPos: Int?
|
|
public var playlistCount: Int?
|
|
|
|
public init(playing: Bool, paused: Bool? = nil, title: String? = nil,
|
|
volume: Double? = nil, position: Double? = nil, duration: Double? = nil,
|
|
playlistPos: Int? = nil, playlistCount: Int? = nil) {
|
|
self.playing = playing; self.paused = paused; self.title = title
|
|
self.volume = volume; self.position = position; self.duration = duration
|
|
self.playlistPos = playlistPos; self.playlistCount = playlistCount
|
|
}
|
|
|
|
public static let idle = PlaybackStatus(playing: false)
|
|
}
|
|
|
|
/// Result of polling a target: reachability and (if reachable) the playback
|
|
/// state. `status == nil` while reachable means "reachable but no clear state".
|
|
public struct PollResult: Sendable {
|
|
public var reachable: Bool
|
|
public var status: PlaybackStatus?
|
|
public init(reachable: Bool, status: PlaybackStatus?) {
|
|
self.reachable = reachable; self.status = status
|
|
}
|
|
public static let unreachable = PollResult(reachable: false, status: nil)
|
|
}
|
|
|
|
/// A place we can send playback to. Reference type so an implementation can hold
|
|
/// connection state (e.g. a pinned SSH endpoint). Verbs translate onto an
|
|
/// existing backend — the playback intelligence lives there, not here.
|
|
public protocol PlayerTarget: AnyObject {
|
|
var id: String { get }
|
|
var name: String { get }
|
|
var kind: HostKind { get }
|
|
var detail: String { get } // human-readable endpoint, for the Devices view
|
|
var volumeScale: Int { get } // max value for the volume slider (percent)
|
|
|
|
func poll() async -> PollResult
|
|
func playPause() async // toggle
|
|
func resume() async // ensure playing
|
|
func setVolume(_ percent: Int) async
|
|
func seek(relative seconds: Int) async
|
|
func seek(toSeconds seconds: Int) async // absolute — for the scrubber + resume
|
|
func next() async
|
|
func previous() async
|
|
func stop() async
|
|
}
|
|
|
|
/// A target whose backend can toggle fullscreen on its own display (VLC's HTTP
|
|
/// `fullscreen` command). mpv-on-black already runs full-screen on the HDMI out,
|
|
/// so only the windowed VLC player conforms — the Devices tab feature-detects it.
|
|
public protocol FullscreenControllable: AnyObject {
|
|
func toggleFullscreen() async
|
|
}
|
|
|
|
/// A target whose backend keeps a playlist we can wipe in one shot (VLC's
|
|
/// `pl_empty`). Surfaced as a per-service tool in the Devices detail pane.
|
|
public protocol PlaylistClearable: AnyObject {
|
|
func clearPlaylist() async
|
|
}
|