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>
244 lines
8.6 KiB
Markdown
244 lines
8.6 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
|