import Foundation /// A screen identified across restarts and machines: by name first, position as fallback. /// (CGDirectDisplayIDs differ between Macs and can change after a reboot.) public struct ScreenRef: Codable, Hashable, Sendable { public var name: String /// Position in the system's screen list (0 = main screen). public var index: Int public init(name: String, index: Int) { self.name = name self.index = index } } /// One NDI source fullscreen on one screen. public struct VideoAssignment: Codable, Hashable, Sendable { public var screen: ScreenRef public var source: Source public init(screen: ScreenRef, source: Source) { self.screen = screen self.source = source } } /// One audio source on one output channel. public struct AudioAssignment: Codable, Hashable, Sendable { public enum SourceSpec: Codable, Hashable, Sendable { case ndi(name: String) /// Input device by UID; the name is the fallback when the UID changes (e.g. other USB port). case input(uid: String, name: String) } public var source: SourceSpec public var deviceUID: String public var deviceName: String public var channel: Int // 0-based public var selection: ChannelSelection public var gainDB: Float public var muted: Bool public init(source: SourceSpec, deviceUID: String, deviceName: String, channel: Int, selection: ChannelSelection = .mono, gainDB: Float = 0, muted: Bool = false) { self.source = source self.deviceUID = deviceUID self.deviceName = deviceName self.channel = channel self.selection = selection self.gainDB = gainDB self.muted = muted } /// Identity of the route (what goes where), without its settings. public var key: String { let sourceKey = switch source { case .ndi(let name): "ndi:\(name)" case .input(let uid, _): "input:\(uid)" } return "\(sourceKey)→\(deviceUID)#\(channel)" } } /// Everything one Mac outputs. public struct MachineConfig: Codable, Hashable, Sendable { public var video: [VideoAssignment] public var audio: [AudioAssignment] public init(video: [VideoAssignment] = [], audio: [AudioAssignment] = []) { self.video = video self.audio = audio } public static let empty = MachineConfig() } /// A complete setup of the control Mac and its nodes, e.g. "OP-Live". public struct Scene: Codable, Identifiable, Hashable, Sendable { public var id: UUID public var name: String public var control: MachineConfig /// Node name (computer name) → what that node shows. public var nodes: [String: MachineConfig] public init(id: UUID = UUID(), name: String, control: MachineConfig = .empty, nodes: [String: MachineConfig] = [:]) { self.id = id self.name = name self.control = control self.nodes = nodes } }