|
|
|
|
@ -41,6 +41,12 @@ final class Coordinator {
|
|
|
|
|
outputs.applyLaunchRoutes()
|
|
|
|
|
audio.applyLaunchRoutes()
|
|
|
|
|
applyLoginItemArgument()
|
|
|
|
|
if let index = ProcessInfo.processInfo.arguments.firstIndex(of: "--load-scene"),
|
|
|
|
|
index + 1 < ProcessInfo.processInfo.arguments.count {
|
|
|
|
|
let name = ProcessInfo.processInfo.arguments[index + 1]
|
|
|
|
|
if let scene = store.scenes.first(where: { $0.name == name }) { loadScene(scene) }
|
|
|
|
|
else { Log.remote.error("--load-scene: no scene named \(name, privacy: .public)") }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
statusTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
|
|
|
|
MainActor.assumeIsolated { self?.sendStatus() }
|
|
|
|
|
@ -81,6 +87,7 @@ final class Coordinator {
|
|
|
|
|
|
|
|
|
|
/// Shows a source on a node's screen.
|
|
|
|
|
func show(_ assignment: VideoAssignment, onNode name: String) {
|
|
|
|
|
Log.remote.info("UI: show \(assignment.source.ndiName, privacy: .public) on \(name, privacy: .public)/\(assignment.screen.name, privacy: .public)")
|
|
|
|
|
var config = desiredConfig(forNode: name)
|
|
|
|
|
config.video.removeAll { $0.screen == assignment.screen }
|
|
|
|
|
config.video.append(assignment)
|
|
|
|
|
@ -90,6 +97,7 @@ final class Coordinator {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func stop(_ screen: ScreenRef, onNode name: String) {
|
|
|
|
|
Log.remote.info("UI: stop \(name, privacy: .public)/\(screen.name, privacy: .public)")
|
|
|
|
|
var config = desiredConfig(forNode: name)
|
|
|
|
|
config.video.removeAll { $0.screen == screen }
|
|
|
|
|
live.nodes[name] = config
|
|
|
|
|
@ -105,7 +113,7 @@ final class Coordinator {
|
|
|
|
|
private func nodeConnected(_ node: NodeBrowser.Node) {
|
|
|
|
|
if let desired = live.nodes[node.name] {
|
|
|
|
|
node.send(.apply(desired))
|
|
|
|
|
Log.ndi.info("Sent configuration to \(node.name, privacy: .public)")
|
|
|
|
|
Log.remote.info("Sent configuration to \(node.name, privacy: .public): \(desired.video.count) video")
|
|
|
|
|
} else {
|
|
|
|
|
adoptWhenStatusArrives(node)
|
|
|
|
|
}
|
|
|
|
|
@ -117,6 +125,7 @@ final class Coordinator {
|
|
|
|
|
try? await Task.sleep(for: .seconds(1))
|
|
|
|
|
guard let self, live.nodes[node.name] == nil else { return }
|
|
|
|
|
if let config = node.status?.config {
|
|
|
|
|
Log.remote.info("Adopted configuration of \(node.name, privacy: .public): \(config.video.count) video")
|
|
|
|
|
live.nodes[node.name] = config
|
|
|
|
|
store.saveLiveState(live)
|
|
|
|
|
return
|
|
|
|
|
@ -128,6 +137,7 @@ final class Coordinator {
|
|
|
|
|
// MARK: - Node: commands from the control
|
|
|
|
|
|
|
|
|
|
private func handle(_ message: RemoteMessage) {
|
|
|
|
|
Log.remote.info("Command: \(String(describing: message).prefix(200), privacy: .public)")
|
|
|
|
|
switch message {
|
|
|
|
|
case .apply(let config):
|
|
|
|
|
outputs.apply(config.video)
|
|
|
|
|
@ -206,6 +216,7 @@ final class Coordinator {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func markModified() {
|
|
|
|
|
Log.remote.debug("Live state saved: local \(self.live.local.video.count) video, nodes \(self.live.nodes.mapValues(\.video.count), privacy: .public)")
|
|
|
|
|
if live.sceneID != nil { live.modified = true }
|
|
|
|
|
store.saveLiveState(live)
|
|
|
|
|
}
|
|
|
|
|
|