From f4ac82de058b06bbc8dfbc26e830fe280b3ef2a7 Mon Sep 17 00:00:00 2001 From: stephen Date: Sat, 21 Oct 2023 12:05:44 -0400 Subject: [PATCH] cleanup, docs --- src/main/kotlin/net/eksb/obsdc/Backoff.kt | 1 + src/main/kotlin/net/eksb/obsdc/DBus.kt | 62 +++++------- src/main/kotlin/net/eksb/obsdc/Main.kt | 9 +- src/main/kotlin/net/eksb/obsdc/Obs.kt | 109 +++++++++++++--------- src/main/kotlin/net/eksb/obsdc/Op.kt | 8 ++ 5 files changed, 102 insertions(+), 87 deletions(-) diff --git a/src/main/kotlin/net/eksb/obsdc/Backoff.kt b/src/main/kotlin/net/eksb/obsdc/Backoff.kt index e47797d..fa1b0e8 100644 --- a/src/main/kotlin/net/eksb/obsdc/Backoff.kt +++ b/src/main/kotlin/net/eksb/obsdc/Backoff.kt @@ -2,6 +2,7 @@ package net.eksb.obsdc class Backoff { fun backoff() { + // TODO: actual bakoff Thread.sleep(5_000) } } \ No newline at end of file diff --git a/src/main/kotlin/net/eksb/obsdc/DBus.kt b/src/main/kotlin/net/eksb/obsdc/DBus.kt index b729a50..2534d3d 100644 --- a/src/main/kotlin/net/eksb/obsdc/DBus.kt +++ b/src/main/kotlin/net/eksb/obsdc/DBus.kt @@ -9,45 +9,43 @@ import org.freedesktop.dbus.messages.DBusSignal import org.slf4j.LoggerFactory import java.util.concurrent.BlockingQueue import java.util.concurrent.TimeUnit -import kotlin.concurrent.thread -class DBus(private val q: BlockingQueue) { +/** + * Listen to signals on the session DBUS, and send the ops to [q]. + * + * To send an op signal: + * `dbus-send --session --type=signal --dest=net.eksb.obsdc / net.eksb.Obsdc.Signal string:{OP_NAME}` + * + * `{OP_NAME}` is the name of an [Op]. + * + * `src/scripts/obs-signal` takes an [Op] name and sends the signal. + */ +class DBus(private val q: BlockingQueue): AutoCloseable { - private val thread = thread( - start = true, - isDaemon = false, - name = "dbus", - ) { - DBusConnectionBuilder.forSessionBus().build().use { dbus -> + // To monitor DBUS: `dbus-monitor` + // To see what is registered: `qdbus net.eksb.obsdc /` + val dbus = DBusConnectionBuilder.forSessionBus().build() + + init { // These lines are not necessary to handle signals, but are necessary to register methods. dbus.requestBusName("net.eksb.obsdc") dbus.exportObject("/", ObsdcDBusInterfaceImpl()) dbus.addSigHandler { signal -> - log.info("signal: ${signal.op}") + log.debug("signal: ${signal.op}") val op = Op.valueOf(signal.op) - log.info("op: ${op}") + log.debug("op: ${op}") try { q.offer(op, 1, TimeUnit.SECONDS) - } catch (e:InterruptedException) { + } catch (e: InterruptedException) { log.debug("queue offer interrupted") } } - while (true) { - try { - Thread.sleep(60_000) - } catch (e:InterruptedException) { - log.info("interrupted") - break - } - } - } - log.info("done") } - fun stop() { - thread.interrupt() + override fun close() { + dbus.close() } private val log = LoggerFactory.getLogger(DBus::class.java) @@ -62,24 +60,10 @@ interface ObsdcDBusInterface: DBusInterface { fun echo(message:String): String class Signal(path:String, val op:String): DBusSignal(path, op) } + class ObsdcDBusInterfaceImpl: ObsdcDBusInterface { override fun echo(message: String):String { return message } override fun getObjectPath(): String = "/" -} - -/* -Monitor: -`dbus-monitor` - -See what is registered: -`qdbus net.eksb.obsdc /` - -Send signal: -`dbus-send --session --type=signal --dest=net.eksb.obsdc / net.eksb.Obsdc.Signal string:a` - -Call method: -`dbus-send --session --type=method_call --print-reply --dest=net.eksb.obsdc / net.eksb.Obsdc.echo string:b` -`qdbus net.eksb.obsdc / net.eksb.Obsdc.echo hello` - */ +} \ No newline at end of file diff --git a/src/main/kotlin/net/eksb/obsdc/Main.kt b/src/main/kotlin/net/eksb/obsdc/Main.kt index 5ade101..8b9961d 100644 --- a/src/main/kotlin/net/eksb/obsdc/Main.kt +++ b/src/main/kotlin/net/eksb/obsdc/Main.kt @@ -6,8 +6,13 @@ import java.util.concurrent.LinkedBlockingQueue object Main { @JvmStatic fun main(args: Array) { + // Create a queue to send ops coming in from DBUS to OBS. val q:BlockingQueue = LinkedBlockingQueue() - DBus(q) // forks non-daemon thread - Obs(q).start() // forks non-daemon thread + + // Listen for DBUS signals and send to q. + DBus(q) + + // Send requests to OBS + Obs(q) // forks a non-daemon thread. } } \ No newline at end of file diff --git a/src/main/kotlin/net/eksb/obsdc/Obs.kt b/src/main/kotlin/net/eksb/obsdc/Obs.kt index 73a9159..fc3a4d2 100644 --- a/src/main/kotlin/net/eksb/obsdc/Obs.kt +++ b/src/main/kotlin/net/eksb/obsdc/Obs.kt @@ -14,22 +14,33 @@ import java.util.concurrent.atomic.AtomicBoolean import kotlin.concurrent.thread import org.slf4j.LoggerFactory -// protocol docs: https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md +/** + * Send ops from [q] to OBS. + * + * protocol docs: https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md + */ class Obs(private val q:BlockingQueue): AutoCloseable { + /** How much to pan. */ private val panAmount = 50F private val backoff = Backoff() - private val started = AtomicBoolean(false) + /** Set if this is closed. */ + private val closed = AtomicBoolean(false) + + /** Set when connected, unset when disconnected. */ private val connected = AtomicBoolean(false) + + /** Set when ready, unset on error or disconnect. */ private val ready = AtomicBoolean(false) private val controller = OBSRemoteController.builder() + // TODO: configure these .host("localhost") .port(4455) - .password("R3tRkVXhFofJ2wRF") // TODO put this in a file - .autoConnect(false) + .password("R3tRkVXhFofJ2wRF") + .autoConnect(true) .connectionTimeout(3) .lifecycle() .onReady(::onReady) @@ -38,7 +49,7 @@ class Obs(private val q:BlockingQueue): AutoCloseable { .onCommunicatorError(::onCommError) .onDisconnect(::onDisconnect) .onConnect { - log.info("connected") + log.debug("connected") connected.set(true) } .and() @@ -50,47 +61,36 @@ class Obs(private val q:BlockingQueue): AutoCloseable { }) } - fun start() { - if (!started.compareAndExchange(false,true)) { - controller.connect() - } - } - fun stop() { - if (started.compareAndExchange(true,false)) { - controller.disconnect() - } - } - private fun onClose(e:WebSocketCloseCode) { - log.info("closed: ${e.code}") + log.debug("closed: ${e.code}") ready.set(false) } private fun onControllerError(e:ReasonThrowable) { - log.info("controller error - ${e.reason}",e.throwable) - if (started.get() && ! connected.get()) { - log.info("connection failed") + log.debug("controller error - ${e.reason}",e.throwable) + if (!closed.get() && !connected.get()) { + log.debug("connection failed") backoff.backoff() - log.info("reconnect after connection failed...") + log.debug("reconnect after connection failed...") controller.connect() } } private fun onCommError(e:ReasonThrowable) { - log.info("comm error - ${e.reason}",e.throwable) + log.debug("comm error - ${e.reason}",e.throwable) } private fun onDisconnect() { - log.info("disconnected") + log.debug("disconnected") ready.set(false) connected.set(false) - if (started.get()) { + if (! closed.get()) { backoff.backoff() - log.info("reconnect after disconnected..") + log.debug("reconnect after disconnected..") controller.connect() } } private fun onReady() { - log.info("ready") + log.debug("ready") ready.set(true) // The docs say that you are only supposed to send requests from the [onReady] handler, // but you cannot block the [onReady] handler. @@ -100,26 +100,36 @@ class Obs(private val q:BlockingQueue): AutoCloseable { // So we keep track of if it is ready, and make requests from another thread ([opThread]). } + /** + * Thread that polls [q], checks if ready, and calls [op] to send requests to OBS. + * If not ready, ops are dropped. + */ private val opThread = thread(name="obs-op", isDaemon=false, start=true) { while(true) { val op = q.take() - log.info("got op: ${op}") + log.debug("got op: ${op}") if (ready.get()) { try { op(op) } catch (e:InterruptedException) { - log.info("op thread interrupted") + log.debug("op thread interrupted") break } catch (e:Exception) { log.error("op ${op} failed", e ) } } else { // This would be way more complicated if we had to buffer ops. - log.info("skipping op ${op} because not yet ready") + log.debug("skipping op ${op} because not yet ready") } } } + /** + * Send the request to OBS for the op. + * + * When the op requires multiple chained requests, those requests are made here in response handlers, + * and this method blocks until all requests are complete. + */ private fun op(op:Op) { when(op) { Op.SCENE_1 -> scene { scenes -> scenes.firstOrNull() } @@ -127,33 +137,40 @@ class Obs(private val q:BlockingQueue): AutoCloseable { Op.SCENE_3 -> scene { scenes -> scenes.asSequence().drop(2).firstOrNull() } Op.STUDIO_TRANSITION -> { controller.triggerStudioModeTransition { response -> - log.info("studio transitioned: ${response.isSuccessful}") + log.debug("studio transitioned: ${response.isSuccessful}") } } - Op.PAN_UP -> pan { old -> positionY(old.positionY - panAmount ) } - Op.PAN_DOWN -> pan { old -> positionY(old.positionY + panAmount ) } - Op.PAN_LEFT -> pan { old -> positionX(old.positionX - panAmount ) } - Op.PAN_RIGHT -> pan { old -> positionX(old.positionX + panAmount ) } + Op.PAN_UP -> transform { old -> positionY(old.positionY - panAmount ) } + Op.PAN_DOWN -> transform { old -> positionY(old.positionY + panAmount ) } + Op.PAN_LEFT -> transform { old -> positionX(old.positionX - panAmount ) } + Op.PAN_RIGHT -> transform { old -> positionX(old.positionX + panAmount ) } } } + /** + * Select a scene from the scene list with the supplied [selector] and set the selected scene (if any) + * as the current program scene. + */ private fun scene(selector:(List)->Scene?) { controller.getSceneList { response -> val scene = selector(response.scenes.sortedBy(Scene::getSceneIndex).reversed()) - log.info("select scene ${scene?.sceneName} index:${scene?.sceneIndex}") + log.debug("select scene ${scene?.sceneName} index:${scene?.sceneIndex}") if (scene != null) { controller.setCurrentProgramScene(scene.sceneName) { response -> - log.info("selected scene ${scene.sceneName}: ${response.isSuccessful}") + log.debug("selected scene ${scene.sceneName}: ${response.isSuccessful}") } } } } - // Pan the bottom-most non-locked item. - private fun pan(block:TransformBuilder.(Transform)->TransformBuilder) { + /** + * Generate a transform for the lowest non-locked item in the current program scene with the + * supplied [transformBuilder], and apply that transform to the item. + */ + private fun transform(transformBuilder:TransformBuilder.(Transform)->TransformBuilder) { controller.getCurrentProgramScene { response -> val sceneName = response.currentProgramSceneName - log.info("scene name: ${sceneName}") + log.debug("scene name: ${sceneName}") controller.getSceneItemList(sceneName) { response -> val items = response.sceneItems // Even though locked status is in the response from OBS, the library does not parse it. @@ -180,17 +197,17 @@ class Obs(private val q:BlockingQueue): AutoCloseable { .map { (locked,item) -> item } .sortedBy { item -> item.sceneItemIndex } .firstOrNull() - log.info("item to pan: ${item?.sceneItemId}") + log.debug("item to pan: ${item?.sceneItemId}") if (item != null) { controller.getSceneItemTransform(sceneName, item.sceneItemId) { response -> val transform = response.sceneItemTransform - log.info("position: ${transform.positionX} x ${transform.positionY}") - val newTransform = block(Transform.builder(), transform).build() + log.debug("position: ${transform.positionX} x ${transform.positionY}") + val newTransform = transformBuilder(Transform.builder(), transform).build() controller.setSceneItemTransform(sceneName, item.sceneItemId, newTransform) { response -> - log.info("transform successful: ${response.isSuccessful}") + log.debug("transform successful: ${response.isSuccessful}") // Have to set the current scene to take effect if in studio mode. controller.setCurrentProgramScene(sceneName) { response -> - log.info("set current program to ${sceneName}: ${response.isSuccessful}") + log.debug("set current program to ${sceneName}: ${response.isSuccessful}") } } } @@ -201,8 +218,8 @@ class Obs(private val q:BlockingQueue): AutoCloseable { } override fun close() { - log.info("close") - stop() + log.debug("close") + controller.disconnect() opThread.interrupt() } diff --git a/src/main/kotlin/net/eksb/obsdc/Op.kt b/src/main/kotlin/net/eksb/obsdc/Op.kt index b639f4d..7b33282 100644 --- a/src/main/kotlin/net/eksb/obsdc/Op.kt +++ b/src/main/kotlin/net/eksb/obsdc/Op.kt @@ -1,13 +1,21 @@ package net.eksb.obsdc enum class Op { + /** If in studio mode, transition between scenes. */ STUDIO_TRANSITION, + /** Activate the first scene. */ SCENE_1, + /** Activate the second scene. */ SCENE_2, + /** Activate the third scene. */ SCENE_3, + /** Move the bottom-most unlocked source in the active scene up. */ PAN_UP, + /** Move the bottom-most unlocked source in the active scene down. */ PAN_DOWN, + /** Move the bottom-most unlocked source in the active scene left. */ PAN_LEFT, + /** Move the bottom-most unlocked source in the active scene right. */ PAN_RIGHT, ; } \ No newline at end of file