Timers 1.4.2: ProPresenter digits no longer flick back a second
ProPresenter reports running timers as truncated whole seconds, and the poller re-anchored the tiles' local count at the bottom of that second on every 500 ms poll — so the displayed digit ran up to a second ahead and got yanked back a step whenever a fresh poll sampled the next flip later than the last one did. Tiles flickered ...12:33 → 12:34 → 12:33... about once a second. phase.js turns the quantized reports into one stable anchor per timer: the only information in a poll is a FLIP (the report stepping one second in the direction of travel), which dates the true boundary to the midpoint of the two polls that straddle it. While reports stay consistent with the anchor (±450 ms) the anchor holds, so tiles count along one steady clock; a report the anchor can't explain (reset, seek, resume, accumulated drift) re-anchors. Timers carry the anchor as a per-timer asOf the client extrapolates from (optional in the provider contract, documented in the guide); the client additionally refuses to tick a running card backwards over sub-1.5 s jitter. phase.selftest.js (66 checks) drives a simulated ProPresenter — truncated reports, jittered polls — through the tracker and the client's render math: digits never step backwards, dwells hold 1 s (the ~2 s zero-crossing dwell is trunc's honest behavior), anchors stay within tolerance of the true value, resets snap, pause/resume re-locks, 0.2% clock drift is absorbed. Verified live against tools/pp-timers-mock.js: 78 s of 100 ms DOM sampling across four running cards — every step ±1 in the card's own direction, dwells 998–1002 ms, zero regressions; upstream kill/restart still recovers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
7a736b3db1
commit
2f4c357bf5
@@ -123,10 +123,13 @@ export function framesToTc(n, nominal, df) {
|
||||
* elapsed ms otherwise, or null when the snapshot can't say. The values as
|
||||
* of the snapshot (`elapsedMs` / `remainingMs`) are preferred and carried
|
||||
* forward while running; `startedAt` is the fallback when they are absent.
|
||||
* A timer measured at its own instant carries `asOf` (a phase-locked
|
||||
* ProPresenter value — see the server's phase.js) which beats the source's
|
||||
* snapshot time.
|
||||
*/
|
||||
export function valueAt(timer, sourceUpdatedAt, now) {
|
||||
const running = timer.state === 'running';
|
||||
const at = Number(sourceUpdatedAt) || now;
|
||||
const at = Number(timer.asOf) || Number(sourceUpdatedAt) || now;
|
||||
const dt = running ? Math.max(0, now - at) : 0;
|
||||
const has = (v) => v !== null && v !== undefined && Number.isFinite(v);
|
||||
if (timer.kind === 'countdown') {
|
||||
@@ -647,7 +650,21 @@ export default function create({ root, moduleApi }) {
|
||||
statusTone = timer.status?.tone || 'muted';
|
||||
}
|
||||
} else {
|
||||
const value = valueAt(timer, source.updatedAt, now);
|
||||
let value = valueAt(timer, source.updatedAt, now);
|
||||
// A running card must never tick backwards: an anchor correction or
|
||||
// clock-offset wobble can re-place a second boundary by a few
|
||||
// hundred ms, and a digit that regresses reads as broken. A small
|
||||
// step against the direction of travel holds the shown value (time
|
||||
// re-passes it within the jitter bound); a big one is a real
|
||||
// reset/seek and snaps.
|
||||
if (timer.state === 'running' && value !== null) {
|
||||
const dir = timer.kind === 'countdown' ? -1 : 1;
|
||||
const prev = card.mono;
|
||||
if (prev && (value - prev) * dir < 0 && Math.abs(value - prev) < 1500) value = prev;
|
||||
card.mono = value;
|
||||
} else {
|
||||
card.mono = null;
|
||||
}
|
||||
text = value === null ? '—' : fmtDuration(value, { ...digits, up: timer.kind === 'countdown' });
|
||||
tone = toneFor(timer, value);
|
||||
statusText = timer.status?.text || defaultStatus(timer, value, tone);
|
||||
|
||||
Reference in New Issue
Block a user