Files
ProdDash/modules/resi-studio-cues
christianandClaude Opus 5 82c552955d Resi cue countdowns reach the stage screen without Companion
The ask was a Stream Deck button showing the time until the next Resi cue.
What it turned into is simpler: ProPresenter already renders countdowns, and
it already has an HTTP API, so nothing has to sit in between except something
that knows when the next cue is.

`resi_to_propresenter.py` watches a decoder's playhead and keeps a
ProPresenter countdown timer armed with the seconds until its next cue, with
the cue's name as a stage message beside it. Both halves of that come from
Resi's cloud rather than the decoder — the decoder reports its position
upstream and `central.resi.io` hands it back — so there is nothing to install
at the decoder end and nothing polling it every second.

The playhead is estimated rather than repeated. Resi republishes `position`
only every few seconds, irregularly, and each reading is already stale when
it appears; sent straight through, the countdown sat three seconds behind and
lurched. So each reading is kept as media time minus wall time and the
largest recent value is trusted — the reading that arrived with the least
delay — which a slow round trip can no longer drag backwards. A reading
outside the link's measured noise floor counts as a seek: drop the lock, snap,
and re-read the cue list immediately instead of on its timer. Playback is
only called paused once the playhead has been still for longer than Resi's own
cadence, because a fixed timeout mistakes a slow update for a stop and freezes
the countdown mid-service.

This is not a tile. There is no `module.json`, so `readManifests` skips the
folder and it never reaches the + picker; it lives under `modules/` because
that is where the production tooling lives. That is also why it carries a
README when no real module does — there is no *Included modules* entry it
could have instead — and the repo README says so where the module list ends.

`install-agent.command` sets up a LaunchAgent so it runs from login and
restarts if it stops. It deliberately runs a copy in Application Support: a
login agent cannot read `~/Documents` without Full Disk Access, and macOS
answers "Operation not permitted" rather than asking. Settings live there too,
outside the checkout, the same way module config does. The Resi password is
read from the Keychain and never written to disk; with none set the agent
says so once and waits rather than relaunching in a loop.

`test_playhead.py` drives the estimator on a fake clock against nine
simulated Resi profiles — update cadences of one to twelve seconds, staleness
to three, irregular timing, variable round trips — and six of seven deliberate
breakages of the estimator turn it red. `test_end_to_end.py` runs the real
loop against a real ProPresenter with a simulated decoder, and has: it armed
for the first cue, counted down in step, rolled over to the second and stopped
when the cues ran out. Neither covers the live Resi calls, which need real
credentials.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 11:35:40 -04:00
..

Resi Studio Cues

Puts a live "time until the next Resi cue" countdown on the stage screen.

This folder is not a ProdDash tile. It has no module.json, so the shell's module scan skips it. It lives here because this is where the production tooling lives, not because ProdDash loads it. That is also why it carries its own README, unlike the real modules, which are documented in the repo README's Included modules.


What it does

resi_to_propresenter.py watches a Resi decoder's playhead, works out how long until its next cue point, and keeps a ProPresenter countdown timer armed with that number. ProPresenter renders the countdown itself, so the stage screen is smooth and styled in whatever the stage layout uses, and nothing has to be pushed every second. The cue's name goes up as a ProPresenter stage message alongside it.

The cue list and the playhead both come from Resi's cloud, not from the decoder — the decoder reports its position upstream, and central.resi.io hands it back. Nothing has to be installed at the decoder end.

Why the playhead is estimated rather than displayed

Resi republishes a decoder's position only every few seconds, on an irregular cadence, and each reading is already a little stale when it appears. Sent straight to ProPresenter, the countdown sits several seconds behind and lurches.

So every reading is kept as media time minus wall time, and the largest recent value is the one trusted — that is the reading that reached us with the least delay, so a slow round trip or an unlucky poll cannot drag the countdown backwards. A reading that falls outside the link's measured noise floor is treated as a seek: the lock is dropped, the estimate snaps, and the cue list is re-read at once instead of on its timer. Playback is only called paused once the playhead has been still for clearly longer than Resi's own cadence, because a fixed timeout mistakes a slow update for a stop and freezes the countdown mid-service.

How quickly a fired cue can possibly show on stage is bounded by how often Resi republishes the position. Nothing on this end beats that; the log prints the measured cadence so you can see what it is.

Running it

Settings live outside the checkout, at ~/Library/Application Support/ResiCues/config.json, so a git pull never touches them. config.example.json here is the template. The Resi password is read from the Keychain and is never written to disk:

security add-generic-password -a "you@yourchurch.org" -s resi-propresenter -w

Then pick a decoder:

python3 resi_to_propresenter.py --list

and put its playerHardwareId into player in the config (leave it blank if the account has only one). --dry-run prints what it would send without touching ProPresenter.

Start it at login: double-click install-agent.command. It writes a LaunchAgent that runs the script at login and restarts it if it stops, logging to ~/Library/Logs/resi-propresenter.log. Run it again after a pull to repoint the agent.

ProPresenter needs its network API on (Settings › Network) and a countdown timer whose name matches timer_name in the config — NextCue by default — placed on the stage layout, with a Message object for the cue name.

Tests

python3 test_playhead.py     # the estimator against 9 simulated Resi profiles
python3 test_end_to_end.py http://10.1.14.225:1025

test_playhead.py drives the estimator on a fake clock against a simulated Resi with a configurable update cadence, staleness, cadence jitter and network round trip; pass those four as arguments to run one profile. test_end_to_end.py runs the real script loop against a real ProPresenter with a simulated decoder, checks the timer is armed and counts down in step across a cue rollover, and puts ProPresenter back as it found it.

Both suites are written so a deliberate break turns them red — six of seven mutations of the estimator do. Neither covers the live Resi calls, which need real credentials.

The Companion sibling

Before the stage-screen version, the same estimator shipped as a Bitfocus Companion module for Stream Deck buttons — a patched build of reykroona/companion-module-resi-studio-cues (MIT), with the estimator, a PLAYING/PAUSED variable and resync actions added. It is not needed for the stage display and is not checked in here; the packaged .tgz and its patched sources are in ~/Desktop/Resi Companion/.

Resi's API

Undocumented and unsupported — it is what studio.resi.io itself uses. Don't open a Resi ticket about it. Authentication is POST /api/v3/login?newToken=true with {userName, password}, which sets a livingas1 cookie used as Authorization: X-Bearer <cookie>; from there /api_v2.svc/users/me, /api/v3/customers/{id}/venues, /api_v2.svc/users/{venueUuid}/players, /api/v3/customers/{id}/events/{eventId} and /api_v2.svc/streamprofiles/{profileId}/events/{eventId}/cues.