tv-anarchy/Sources/TVAnarchyCore/PlayerTarget.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
}