Resi Cues 1.0.1: the next cue's name keeps moving on a 30-second Resi

On a Resi that publishes every ~30s, the board could start a few seconds
before a publish and learn a 2s cadence from that partial first gap. Every
honest 30s gap after that was thrown out as a pause, so playback read as
paused 15s into every update: the countdown froze, and the next cue -- and
the name on the Timers card -- stopped moving on.

- The partial gap right after startup is no longer learned, and one outlier
  gap can no longer lock out the real cadence.
- Until a cadence is measured, a pause is only called after 45s, not 15s.
- The Timers card is titled with the cue's name; its status line now says
  "Next cue" / "Held" instead of repeating the name.
- server.test.js awaited nothing, so an async test raced the next one and
  failed on main; tests now run one at a time.

Same fix as the Resi -> ProPresenter bridge and the Companion module 0.1.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 14:59:12 -04:00
co-authored by Claude Opus 5.5
parent 8ca88ba10b
commit 0f0adb4c60
5 changed files with 63 additions and 20 deletions
+18 -4
View File
@@ -61,6 +61,8 @@ function createPlayhead(options) {
let offsets = []; // { at, off } — off is mediaMs - wall time it was read
let spreads = []; // how far apart honest readings sit: this link's noise
let gaps = []; // how long between the playhead actually moving
let changes = 0; // readings that differed from the one before
let rejected = false; // the last gap looked like a pause and was skipped
let base; // the trusted offset
let lastMedia;
let lastNewAt;
@@ -90,15 +92,25 @@ function createPlayhead(options) {
spreads = spreads.concat(-delta).slice(-20);
}
}
if (lastNewAt !== undefined) {
// The first reading lands at an arbitrary point in Resi's cycle, so the gap
// from it to the next one is only a fraction of the real cadence. Learning
// from it locked in a 2s cadence that rejected every honest 30s gap.
if (lastNewAt !== undefined && changes > 0) {
const gap = t - lastNewAt;
// The gap either side of a pause is not Resi's cadence — recording it
// would inflate the estimate and make the next pause slow to notice.
// But a pause is one gap; twice in a row is the cadence having changed.
const worst = gaps.length ? Math.max.apply(null, gaps) : 0;
if (gap > 0 && gap < 60000 && (!worst || gap <= worst * 3)) {
gaps = gaps.concat(gap).slice(-8);
if (gap > 0 && gap < 60000) {
if (!worst || gap <= worst * 3 || rejected) {
gaps = gaps.concat(gap).slice(-8);
rejected = false;
} else {
rejected = true;
}
}
}
if (lastNewAt !== undefined) changes++;
offsets = offsets.concat({ at: t, off });
lastMedia = mediaMs;
lastNewAt = t;
@@ -118,7 +130,9 @@ function createPlayhead(options) {
// Only call it paused once the playhead has been still for clearly longer than
// Resi's own cadence. A fixed timeout mistakes a slow update for a stop, and a
// countdown that freezes mid-service is the worse failure of the two.
const pauseAfter = Math.max(15000, Math.min(45000, cadenceMs * 2.5 + 6000));
// Until a full gap has been measured the cadence is unknown; assume the slowest
// Resi plausibly is, or a 30s publisher reads as paused 15s after every start.
const pauseAfter = cadenceMs ? Math.max(15000, Math.min(45000, cadenceMs * 2.5 + 6000)) : 45000;
const playing = base !== undefined && t - lastNewAt < pauseAfter;
if (!playing) {
return { estMs: lastEst !== undefined ? lastEst : lastMedia, playing: false, cadenceMs };