Resi cue countdowns on ProPresenter and in Companion
The Resi -> ProPresenter bridge, its installer and site kit, and the Companion module resi-studio-cues, as running at North Attleboro. Bridge (resi_to_propresenter.py) - Follows a Resi decoder's playhead and keeps ProPresenter's NextCue timer counting down to the next cue; optional cue name as a stage message, sent once per cue (ProPresenter flashes it on every send) and off by default. - Playhead estimate no longer learns a cadence from the partial first gap after a start, so a 30s Resi doesn't read as paused half the time. - Backs off on 429, holds 2 min before retrying a failed startup login, and logs Resi's actual reason (wrong password, USER_LOCKED, rate limit). - --setup asks for the login, decoder, ProPresenter and timer. install_bridge.sh installs it as a login service (outside ~/Documents, which launchd may not read); make_kit.sh builds the kit for another site. Companion module resi-studio-cues 0.1.4 (source/, package committed) - Same playhead fix; login and request hold-off (15s doubling to 5 min); a 401/403 on a fresh session is a permission, not a reason to sign in again; init never fails on a Resi problem, so Companion can't restart it into a login loop. Tests: test_playhead.py, test_stage_message.py, test_rate_limit.py and source/backoff_test.js run offline; test_end_to_end.py drives a real ProPresenter and refuses to run without an explicit flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+243
@@ -0,0 +1,243 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user