# OBS pill A pill on the island for screen recording with OBS Studio. It exists only while OBS is running and controls the running instance: start and stop recording, pause and resume, switch scene, show or hide the webcam. The always-visible sliver shows recording state. ## Out of scope - Mic mute: the user has a physical button for it. - Camera hardware settings (zoom, focus, brightness through `v4l2-ctl`). - Replay buffer, streaming, virtual camera. ## Transport obs-websocket (protocol v5), built into OBS 28+, spoken directly from QML with `QtWebSockets`' `WebSocket`, which is installed. Events are pushed, so nothing polls OBS except the elapsed time while recording (below). Rejected: a Python bridge (the stdlib has no websocket client, so either a hand-rolled framing layer or a new dependency) and `obs-cmd` (a new dependency, polled, laggy state). ### One-time setup Enable the server in OBS: Tools > WebSocket Server Settings. Authentication stays on: obs-websocket listens on every interface and has no localhost-only option, so turning it off would expose recording control to the LAN. ### Password Read from OBS's own `~/.config/obs-studio/plugin_config/obs-websocket/config.json` (`server_password`, `server_port`) with a `FileView`. No copy is stored anywhere, and a password changed in OBS is picked up on the next connect. ### Handshake 1. Server sends `Hello` (op 0). With auth it carries `challenge` and `salt`. 2. Auth string: `secret = base64(sha256(password + salt))`, `auth = base64(sha256(secret + challenge))`. QML has no SHA256, so one `Process` per connect runs `openssl dgst -sha256 -binary | base64` twice, with the inputs on stdin, never on the command line. 3. Client sends `Identify` (op 1) with `rpcVersion: 1`, `authentication`, and `eventSubscriptions` = Scenes (4) | Outputs (64) | SceneItems (128) = 196. 4. `Identified` (op 2): connected. Request the initial state. ## `Obs.qml` singleton (`desktop/island/`) State: - `connected`: true after `Identified`. This doubles as "OBS is running": no process check. - `recording`, `paused` - `duration`: milliseconds, from `GetRecordStatus.outputDuration` - `scenes`: names, in OBS order; `scene`: current program scene - `camId`, `camOn`: the scene item in the current scene whose `inputKind` is `v4l2_input`, found automatically; `camId` is -1 when the scene has none Requests sent: | Action | Request | | --- | --- | | initial / on change | `GetRecordStatus`, `GetSceneList`, `GetSceneItemList {sceneName}` | | start/stop | `ToggleRecord` | | pause/resume | `ToggleRecordPause` | | scene | `SetCurrentProgramScene {sceneName}` | | webcam | `SetSceneItemEnabled {sceneName, sceneItemId, sceneItemEnabled}` | Events handled: `RecordStateChanged` (sets `recording`/`paused` from `outputState`), `CurrentProgramSceneChanged` and `SceneListChanged` (re-read scenes and the webcam item), `SceneItemEnableStateChanged` (updates `camOn` when the id matches). Responses are matched by `requestId`; the request type prefixes the id, so a plain switch on it routes the reply. Elapsed time: `GetRecordStatus` every 1s while `recording`. OBS owns the clock, so pause freezes it with no local bookkeeping. ponytail: one local request per second, only while recording. Reconnect: a `Timer` every 3s while not connected sets `active` on the `WebSocket`. A closed or errored socket clears all state, which hides the pill. An auth failure (close code 4009) logs once and keeps retrying, since a password change in OBS fixes it without a restart. ## The pill (`ObsPill.qml`) Right of the status pill, slides with the others, exists only while `Obs.connected`. Left to right: - **Record**: idle icon, or a red dot and `mm:ss` (`h:mm:ss` past an hour) while recording. Click toggles recording. - **Pause**: only while recording. Click toggles pause; the dot dims while paused. - **Scene**: current scene name. Click steps to the next, wheel steps previous (up) and next (down), wrapping. Hidden with fewer than two scenes. - **Webcam**: camera icon, struck while hidden. Click toggles. Absent when `camId` is -1. The pill names its own panels (AGENTS.md): it opens none, so it never expands. ## Sliver While recording, the sliver pulses red (opacity animation), drawn over the privacy segments; paused, it holds steady at reduced opacity. Red is already "screen capture" in the privacy colours, and OBS's screen source likely lights that segment whenever OBS is open, so the pulse, not the colour, is what says "recording". Not recording: the sliver is unchanged. ## Pure logic and its check In `Logic.js`, covered by `test_logic.js`: - `obsElapsed(ms)`: `0` -> `00:00`, `61000` -> `01:01`, `3661000` -> `1:01:01` - `obsStep(list, cur, d)`: next/previous scene with wrap, `cur` missing -> first ## Errors - Server disabled or OBS closed: connect fails, pill absent, retry loop. - Auth failure: logged, retry loop. - A failed request (`requestStatus.result` false): logged with its comment; state is not changed optimistically, it follows OBS's events.