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>
258 lines
9.4 KiB
Markdown
258 lines
9.4 KiB
Markdown
# 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_position`
|
||
- `current_position_sec`
|
||
- `current_event_id`
|
||
|
||
### Cue loading (per event)
|
||
When the current `eventId` changes, the module loads:
|
||
- Event detail:
|
||
- `GET /api/v3/customers/{customerId}/events/{eventId}`
|
||
- Captures `startTime` for 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_name`
|
||
- `next_cue_time_in_seconds`
|
||
- `next_cue_time_in_hms`
|
||
- `next_cue_position`
|
||
- `next_cue_tod` (24-hour)
|
||
- Previous cue:
|
||
- `prev_cue_name`
|
||
- `prev_cue_time_ago_in_seconds`
|
||
- `prev_cue_time_ago_in_hms`
|
||
- `prev_cue_position`
|
||
- `prev_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
|
||
1. Push this repo to GitHub/GitLab (public or private).
|
||
2. On the test system, add it as a Developer Module in Companion.
|
||
3. Restart Companion.
|
||
|
||
### Offline / manual
|
||
1. `yarn install`
|
||
2. `yarn build`
|
||
3. Zip the module folder (without `node_modules`) and provide to tester.
|
||
4. 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:...)`)
|
||
- **Lookup cue by name**
|
||
- Writes result into:
|
||
- `lookup_cue_found`, `lookup_cue_uuid`, `lookup_cue_position`, etc.
|
||
- **Delete cue by name**
|
||
- Finds matching cue(s), deletes by UUID via:
|
||
- `DELETE /.../cues/{uuid}`
|
||
|
||
---
|
||
|
||
## Feedbacks (for button coloring)
|
||
|
||
Recommended “production” feedback set:
|
||
1. **Connected (auth OK)** → Green
|
||
2. **Error (auth/API)** → Red
|
||
3. **Player active** (position > 0) → Blue
|
||
4. **Approaching next cue** (threshold seconds) → Yellow
|
||
5. **Playback frozen** (position not changing) → Red
|
||
|
||
Playback frozen requires:
|
||
- variable `playback_last_change_epoch_ms` to 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_error` say 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
|