Compare commits

..

8 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
9ded3dfdc6 update rationale section in readme 2023-11-10 11:59:23 -05:00
a4886c8ae6 systemd unit file 2023-11-10 11:55:38 -05:00
10 changed files with 93 additions and 83 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,16 +2,25 @@
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
Configuration is in `$XDG_CONFIG_HOME/net.eksb.obsdc/config.properties` Configuration is read from `$XDG_CONFIG_HOME/net.eksb.obsdc/config.properties`
### Configuration options ### Configuration options
@@ -19,6 +28,15 @@ Configuration is in `$XDG_CONFIG_HOME/net.eksb.obsdc/config.properties`
* `port` - OBS websocket port (default: `4455`) * `port` - OBS websocket port (default: `4455`)
* `password` - OBS websocket password (required) * `password` - OBS websocket password (required)
### systemd
A systemd unit file ([obsdc.service](src/main/dist/obsdc.service)) is included.
1. Copy the file to `~/.config/systemd/user/`.
2. Edit the `ExecStart` line for where you have installed obsdc.
3. `systemctl --user enable obsdc`
4. `systemctl --user start obsdc`
## Build ## Build
`gradle build` `gradle build`
@@ -49,9 +67,13 @@ using an `Obs`.
I usually code on a 4k monitor in portrait mode. To show what I am doing on video calls, I usually code on a 4k monitor in portrait mode. To show what I am doing on video calls,
I use OBS to share that monitor, but it is a 2160x4096 source in a 1920x1080 scene. I use OBS to share that monitor, but it is a 2160x4096 source in a 1920x1080 scene.
I have [a macropad](https://www.keebmonkey.com/products/megalodon-dual-layer-knob-macro-pad) with two knobs. I have
My [window manager](https://swaywm.org/) is configured to call `obsdc-signal` with `PAN_UP`/`PAN_DOWN` for the [a macropad](https://keeb.io/collections/bdn9-collection/products/bdn9-rev-2-3x3-9-key-macropad-rotary-encoder-and-rgb)
keys emitted by one knob and `PAN_RIGHT`/`PAN_LEFT` for the keys emitted by the other knob. with three knobs.
My [window manager](https://swaywm.org/) is configured to call `obsdc-signal` with
`PAN_UP`/`PAN_DOWN` for the keys emitted by the left knob,
`PAN_RIGHT`/`PAN_LEFT` for the keys emitted by the right knob, and
`SCENE_NEXT`/`SCENE_PREV` for the keys emitted by the center knob.
The regular keys on the macropad are mapped to the other actions. The regular keys on the macropad are mapped to the other actions.
So now I can quickly control what people can see without having to focus (or even make visible) the OBS window. So now I can quickly control what people can see without having to focus (or even make visible) the OBS window.

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")

10
src/main/dist/obsdc.service vendored Normal file
View File

@@ -0,0 +1,10 @@
[Unit]
Description=OBSDC
[Service]
Type=dbus
BusName=net.eksb.obsdc
ExecStart=/opt/obsdc/bin/obsdc
[Install]
WantedBy=default.target

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}\"")
} }
} }
} }

View File

@@ -1 +1,2 @@
org.slf4j.simpleLogger.log.net.eksb=debug org.slf4j.simpleLogger.log.net.eksb=debug
org.slf4j.simpleLogger.log.org.freedesktop.dbus=debug