Compare commits

..

6 Commits

Author SHA1 Message Date
74e44a07ba ignore .idea 2024-05-23 14:50:45 -04:00
6b8ad1f5d4 kotlin 2, dbus-java-core 5 2024-05-23 14:50:26 -04:00
e01589aab8 docs 2024-04-18 12:02:55 -04:00
265efd1db0 use String instead of Op; support SCENE_<number> 2024-03-13 15:31:20 -04:00
e0838be9b8 remove unused Backoff.kt 2024-03-13 15:27:39 -04:00
145bf4d7b1 Connect from connect thread. Avoids stack overflow 2023-11-30 14:59:36 -05:00
8 changed files with 64 additions and 78 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ gradlew*
# Ignore Gradle build output directory # Ignore Gradle build output directory
build build
.idea

View File

@@ -2,12 +2,21 @@
OBS control from D-Bus messages OBS control from D-Bus messages
Send a D-Bus signal to perform an operation with `src/obsdc-signal`, which takes a single operation name Send a D-Bus signal to perform an operation with `src/obsdc-signal`, which takes a single operation command
as an argument. as an argument, and performs an action in OBS.
## Operations ## Operations
See [Op](src/main/kotlin/net/eksb/obsdc/Op.kt) for a list of operations. * `SCENE_NEXT` - switch to the next scene
* `SCENE_PREV` - switch to the previous scene
* `SCENE_#` - switch to the indicated scene
* replace `#` with scene index, starting with 1.
* `STUDIO_TRANSITION` - transition the studio preview and program
* `STUDIO_MODE_TOGGLE` - toggle studio mode
* `PAN_UP` - move the top unlocked source up 50px
* `PAN_DOWN` - move the top unlocked source down 50px
* `PAN_LEFT` - move the top unlocked source left 50px
* `PAN_RIGHT` - move the top unlocked source right 50pxt
## Configuration ## Configuration

View File

@@ -1,5 +1,5 @@
plugins { plugins {
id("org.jetbrains.kotlin.jvm") version "1.9.0" id("org.jetbrains.kotlin.jvm") version "2.0.0"
application application
} }
@@ -7,8 +7,8 @@ dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.3") testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.3")
implementation("com.github.hypfvieh:dbus-java-core:4.3.1") implementation("com.github.hypfvieh:dbus-java-core:5.0.0")
runtimeOnly("com.github.hypfvieh:dbus-java-transport-native-unixsocket:4.3.1") runtimeOnly("com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0")
implementation("io.obs-websocket.community:client:2.0.0") implementation("io.obs-websocket.community:client:2.0.0")

View File

@@ -1,8 +0,0 @@
package net.eksb.obsdc
class Backoff {
fun backoff() {
// TODO: actual bakoff
Thread.sleep(5_000)
}
}

View File

@@ -19,7 +19,7 @@ import org.slf4j.LoggerFactory
* *
* `src/scripts/obsdc-signal` takes an [Op] name and sends the signal. * `src/scripts/obsdc-signal` takes an [Op] name and sends the signal.
*/ */
class DBus(private val opRunner:OpRunner): AutoCloseable { class DBus(private val opHandler:(String)->Unit): AutoCloseable {
// To monitor DBUS: `dbus-monitor` // To monitor DBUS: `dbus-monitor`
// To see what is registered: `qdbus net.eksb.obsdc /` // To see what is registered: `qdbus net.eksb.obsdc /`
@@ -37,9 +37,7 @@ class DBus(private val opRunner:OpRunner): AutoCloseable {
dbus.addSigHandler<ObsdcDBusInterface.Signal> { signal -> dbus.addSigHandler<ObsdcDBusInterface.Signal> { signal ->
log.debug("signal: ${signal.op}") log.debug("signal: ${signal.op}")
val op = Op.valueOf(signal.op) opHandler.invoke(signal.op)
log.debug("op: ${op}")
opRunner.run(op)
} }
log.debug("DBUS initialized") log.debug("DBUS initialized")
} }

View File

@@ -28,12 +28,14 @@ class Obs(
/** Queue of requests to run. */ /** Queue of requests to run. */
private val q:BlockingQueue<Req> = LinkedBlockingQueue() private val q:BlockingQueue<Req> = LinkedBlockingQueue()
/** Backoff on errors. */
private val backoff = Backoff()
/** Flag to set when closed to stop queue poll loop. */ /** Flag to set when closed to stop queue poll loop. */
private val closed = AtomicBoolean(false) private val closed = AtomicBoolean(false)
/**
* Flag set when we start trying to connect, unset when disconnected.
* Used to determine if we should reconnect on controller error.
*/
private val connectingOrConnected = AtomicBoolean(false)
/** /**
* Flag to set when connected, unset when disconnected. * Flag to set when connected, unset when disconnected.
* Used to determine if we should reconnect on controller error. * Used to determine if we should reconnect on controller error.
@@ -69,9 +71,21 @@ class Obs(
addShutdownHook { addShutdownHook {
close() close()
} }
// connect() blocks until OBS is up, so fork it. // Thread that connects if we are not connected/connecting.
thread(name="obs-init-connect", isDaemon=true, start=true) { thread(name="obs-connect", isDaemon=true, start=true) {
while(!closed.get()) {
if (connectingOrConnected.compareAndSet(false,true)) {
log.debug("Not closed, not connected. Try to connect...")
try {
// Only call connect from here; if you try to call connect from [onControllerError] or [onDisconnect]
// eventually you will get stack overflow.
controller.connect() controller.connect()
} catch (e:Exception) {
log.warn("failed to connect: ${e.message}", e)
}
}
Thread.sleep(connectionTimeout.toLong() * 1000L) // in case the error was immediate
}
} }
} }
@@ -82,11 +96,10 @@ class Obs(
private fun onControllerError(e:ReasonThrowable) { private fun onControllerError(e:ReasonThrowable) {
log.debug("controller error - ${e.reason}",e.throwable) log.debug("controller error - ${e.reason}",e.throwable)
if (!closed.get() && !connected.get()) { // If we are not connected, a controller error means that connection failed and we will not connect.
log.debug("connection failed") // If we are connected, it does not mean we are/will be disconnected.
backoff.backoff() if (!connected.get()) {
log.debug("reconnect after connection failed...") connectingOrConnected.set(false)
controller.connect()
} }
} }
private fun onCommError(e:ReasonThrowable) { private fun onCommError(e:ReasonThrowable) {
@@ -94,12 +107,8 @@ class Obs(
} }
private fun onDisconnect() { private fun onDisconnect() {
log.debug("disconnected") log.debug("disconnected")
connectingOrConnected.set(false)
connected.set(false) connected.set(false)
if (! closed.get()) {
backoff.backoff()
log.debug("reconnect after disconnected..")
controller.connect()
}
} }
private fun onReady() { private fun onReady() {

View File

@@ -1,27 +0,0 @@
package net.eksb.obsdc
enum class Op {
/** If in studio mode, transition between scenes. */
STUDIO_TRANSITION,
/** Enable/disable studio mode. */
STUDIO_MODE_TOGGLE,
/** Activate the previous scene. */
SCENE_PREV,
/** Activate the next scene. */
SCENE_NEXT,
/** 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,
;
}

View File

@@ -9,11 +9,11 @@ import io.obswebsocket.community.client.model.SceneItem.Transform.TransformBuild
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
/** /**
* Use an [obs] to run [Op]s. * Use an [obs] to run an operation.
* *
* Call [run] to run an OBS operation. * Call [invoke] to run an OBS operation.
*/ */
class OpRunner(private val obs:Obs) { class OpRunner(private val obs:Obs): (String)->Unit {
/** How much to pan. */ /** How much to pan. */
private val panAmount = 50F private val panAmount = 50F
@@ -21,12 +21,14 @@ class OpRunner(private val obs:Obs) {
private val controller = obs.controller private val controller = obs.controller
/** /**
* Run the specified [Op]. * Run the specified [op].
*
* @param op Operation to run.
*/ */
fun run(op:Op) { override fun invoke(op:String) {
obs.submit { controller -> obs.submit { controller ->
when(op) { when {
Op.SCENE_NEXT -> scene { scenes, current -> op == "SCENE_NEXT" -> scene { scenes, current ->
if (current != null) { if (current != null) {
scenes.asSequence() scenes.asSequence()
.dropWhile { scene -> scene != current } .dropWhile { scene -> scene != current }
@@ -37,7 +39,7 @@ class OpRunner(private val obs:Obs) {
null null
} }
} }
Op.SCENE_PREV -> scene { scenes, current -> op == "SCENE_PREV" -> scene { scenes, current ->
if (current != null) { if (current != null) {
scenes.reversed().asSequence() scenes.reversed().asSequence()
.dropWhile { scene -> scene != current } .dropWhile { scene -> scene != current }
@@ -48,19 +50,21 @@ class OpRunner(private val obs:Obs) {
null null
} }
} }
Op.SCENE_1 -> scene { scenes, current -> scenes.firstOrNull() } op.startsWith("SCENE_") -> scene { scenes, current ->
Op.SCENE_2 -> scene { scenes, current -> scenes.asSequence().drop(1).firstOrNull() } val drop = op.drop("SCENE_".length).toInt() - 1
Op.SCENE_3 -> scene { scenes, current -> scenes.asSequence().drop(2).firstOrNull() } scenes.asSequence().drop(drop).firstOrNull()
Op.STUDIO_TRANSITION -> { }
op == "STUDIO_TRANSITION" -> {
controller.triggerStudioModeTransition { response -> controller.triggerStudioModeTransition { response ->
log.debug("studio transitioned: ${response.isSuccessful}") log.debug("studio transitioned: ${response.isSuccessful}")
} }
} }
Op.STUDIO_MODE_TOGGLE -> studioModeToggle() op == "STUDIO_MODE_TOGGLE" -> studioModeToggle()
Op.PAN_UP -> transform { old -> positionY(old.positionY - panAmount ) } op == "PAN_UP" -> transform { old -> positionY(old.positionY - panAmount ) }
Op.PAN_DOWN -> 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_LEFT" -> transform { old -> positionX(old.positionX - panAmount ) }
Op.PAN_RIGHT -> transform { old -> positionX(old.positionX + panAmount ) } op == "PAN_RIGHT" -> transform { old -> positionX(old.positionX + panAmount ) }
else -> log.error("Unhandled op \"${op}\"")
} }
} }
} }