While the connection wasn't connected, nothing in its settings could be saved. The saved decoder showed as "??" because the list only fills once Resi answers, and a cue refresh that had never been saved showed as 0, below its minimum. Either blocks Save, including the login change that would have fixed the connection. That's how North Attleboro's connection stayed on a locked login. - The saved decoder is always a choice, "(not connected to Resi yet)" until Resi answers. - An upgrade script fills settings an older connection never saved with their defaults (and leaves a complete one alone). - Event details are read once per event instead of on every cue refresh: one Resi request fewer every few seconds on a shared account. - Cue requests share the login/rate-limit hold-off; "Loaded N cues" and repeated cue-refresh failures are logged once, not every few seconds. - "inf0" in the decoder info poll label. source/config_test.js covers all of it (6 tests; the first fails on 0.1.4). backoff_test.js 5/5 and the estimator matrix still pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9.4 KiB
Resi Studio Cues (Bitfocus Companion Module)
This module logs into studio.resi.io / central.resi.io with a username + password, reads decoder/player playback position, loads cue points for the current event, and exposes variables/actions for cue timing and cue management.
The main goal is to drive things like a countdown clock in ProPresenter (time until next cue, next cue name, etc).
Features
Authentication
- Logs in via
POST /api/v3/login?newToken=true - Stores auth cookies (
refreshToken,livingas1) and uses them on subsequent requests - Exposes status via:
- Instance status (green/red in Companion)
- Variables:
auth_status,last_error
Player polling (live)
- Polls decoder/player position from:
GET /api_v2.svc/users/{venueUuid}/players
- Updates variables like:
current_positioncurrent_position_seccurrent_event_id
Cue loading (per event)
When the current eventId changes, the module loads:
- Event detail:
GET /api/v3/customers/{customerId}/events/{eventId}- Captures
startTimefor ToD (time-of-day) math
- Cues:
GET /api_v2.svc/streamprofiles/{eventProfileId}/events/{eventId}/cues?canISetCues=1
Cues are stored locally and used to compute next/prev cue timing from the live playback position.
Cue timing variables
- Next cue:
next_cue_namenext_cue_time_in_secondsnext_cue_time_in_hmsnext_cue_positionnext_cue_tod(24-hour)
- Previous cue:
prev_cue_nameprev_cue_time_ago_in_secondsprev_cue_time_ago_in_hmsprev_cue_positionprev_cue_tod(24-hour)
Cue management actions
- Add cue (player position or custom time)
- Lookup cue by name (stores into
lookup_*variables) - Delete cue by name (looks up UUID, deletes by UUID)
Installation / Testing
Best (recommended): Developer Module via Git
- Push this repo to GitHub/GitLab (public or private).
- On the test system, add it as a Developer Module in Companion.
- Restart Companion.
Offline / manual
yarn installyarn build- Zip the module folder (without
node_modules) and provide to tester. - Tester places it in Companion’s modules folder and restarts Companion.
(Exact folder paths vary by OS / Companion version.)
Configuration
Typical config fields:
- Username / Email
- Password (secret)
- Default Player (optional)
- Polling intervals
- Playback position refresh (fast, e.g. 2000ms)
- Account/venue refresh (slow, e.g. 60000ms)
- Cue refresh while playing (optional, e.g. 15000ms)
- Fallback when no next cue (optional)
Note: dynamic "read-only" status display inside config is not supported by Companion config fields. Use Instance Status + module variables for connection state.
Actions
- Refresh profile: Reload
/users/me - Refresh lists: Reload venues + players list
- Select decoder/player: Choose which player to poll
- Add cue at player position (or custom time)
- Options:
- Cue name
- Private cue
- Position source: player vs custom
- Custom time string
- Text fields support Companion variables (e.g.
$(internal:...))
- Options:
- Lookup cue by name
- Writes result into:
lookup_cue_found,lookup_cue_uuid,lookup_cue_position, etc.
- Writes result into:
- Delete cue by name
- Finds matching cue(s), deletes by UUID via:
DELETE /.../cues/{uuid}
- Finds matching cue(s), deletes by UUID via:
Feedbacks (for button coloring)
Recommended “production” feedback set:
- Connected (auth OK) → Green
- Error (auth/API) → Red
- Player active (position > 0) → Blue
- Approaching next cue (threshold seconds) → Yellow
- Playback frozen (position not changing) → Red
Playback frozen requires:
- variable
playback_last_change_epoch_msto be updated when playback position changes.
Presets Included
Status
- Connected / Error indicator
- Player active indicator
- Playback frozen warning
Cues
- Next cue display (with T- countdown)
- Previous cue display
Maintenance
- Refresh profile
- Refresh venues/players list
Cue actions
- Add cue @ player position
- Add cue @ custom time
- Lookup cue by name
- Delete cue by name
Notes / Limitations
- The Resi API endpoints used here are not public/stable; breaking changes may occur.
- Avoid polling cues every 2 seconds. Recommended cue refresh while playing is 10–30 seconds unless you have a specific need.
- Companion config fields are not truly read-only, so status is exposed via variables + instance status.
Roadmap Ideas
- Optional periodic cue refresh while playing (rate-limited)
- Extra feedback: "No player selected"
- Extra feedback: "No next cue"
- More presets for ProPresenter integration workflows
Playhead accuracy (0.1.0)
Resi only republishes a decoder's position every few seconds, on an irregular
cadence, and each reading is already slightly stale when it appears. Showing that
number raw makes the countdown jump in steps and sit several seconds behind.
This build instead keeps a running estimate:
- every reading is stored as media time minus wall time; the largest recent value is the least-delayed reading and is the one trusted, so a slow round trip or an unlucky poll can no longer drag the countdown backwards
- the display is recomputed every second from that offset, so the countdown ticks smoothly between Resi's updates instead of stepping
- a reading that differs from the locked offset by more than the link's measured noise is treated as a seek - the lock is dropped, the estimate snaps, and the cue list is re-fetched immediately rather than on its timer
- playback is only called PAUSED once the playhead has been still for clearly longer than Resi's own reporting cadence, so an irregular gap no longer freezes the countdown
position_update_interval_sec reports how often Resi is actually updating. That
number is the floor on how fast a jump can possibly be noticed.
Beating that floor
If cues are fired from Companion, the module does not have to wait for Resi to notice. Add one of these to the same button:
- Resync playhead to cue - give it the cue name; the estimate moves there at once
- Resync playhead to the NEXT cue
The next real reading from Resi confirms or corrects it.
Settings that matter
- Player position poll (ms) - 1000 is recommended. Accuracy no longer depends on this, but a faster poll notices a jump sooner.
- Cue list refresh (ms) - 5000. Also forced immediately whenever a seek is seen.
- Position trim (ms) - leave at 0. Only needed if the whole pipeline is consistently behind; positive values push the estimate forward.
Startup fix (0.1.2)
On a Resi that publishes every ~30s, the module could start a few seconds before a
publish and learn a 2s cadence from that partial first gap. It then threw out every
real 30s gap, so playback_state flipped to PAUSED 15s into each update - the
countdown froze and the next cue lagged, worst with cues close together.
- the partial gap after startup (or after a manual resync) is no longer learned
- a single outlier gap can no longer lock out the real cadence
- until the cadence is measured, PAUSED waits 45s instead of 15s
Resi login back-off (0.1.3)
0.1.2 retried a failed Resi login on every player poll (once a second, for ever), and answered every 401/403 by signing in again. With a wrong password, a rate limit or a login that may not see decoders, that is thousands of sign-ins an hour: Resi replies with 429 and then locks the account (it locked one at North Attleboro).
- after any failed login, or a 429 on any request, every Resi call holds off: 15s, doubling to 5min, or as long as Resi's Retry-After asks
- a 401/403 renews the session only if it is over a minute old; a refusal straight after signing in is a permission ("ask a Resi admin for access") and holds off
- status and
last_errorsay which: locked (USER_LOCKED), rate-limited, password rejected, no access, or unreachable - the log says it once per problem, not once a second
- the cache-busting parameter is no longer dropped for good on a 401/403/429
Start-up never crashes on a Resi problem (0.1.4)
In 0.1.3 a failed Resi login at start-up (here: a locked account) made the module's init fail. Companion counts that as a crash and restarts the module every few seconds, and each restart signed in again from scratch, outside the hold-off.
- init no longer fails when Resi refuses or is down: it logs "Not connected to Resi yet … will keep trying", starts the account poll, and the hold-off paces retries
- once Resi answers, the configured decoder is picked up automatically
- saving new credentials clears the old hold-off and starts over
Settings that always save (0.1.5)
While the connection wasn't connected to Resi, its settings couldn't be saved at all: the saved decoder showed as "??" because the list only fills once Resi answers, and a cue refresh that had never been saved showed as 0, below its minimum. Either one blocks Save in Companion, including the login change that would have fixed the connection.
- the saved decoder is always offered, as "(not connected to Resi yet)" until Resi answers
- an upgrade step fills any setting an older connection never saved with its default
- event details are read once per event instead of on every cue refresh, which is one Resi request fewer every few seconds
- cue requests share the login/rate-limit hold-off, and "Loaded N cues" and repeated cue-refresh failures are logged once, not every few seconds