diff options
| author | Luke Hoersten <[email protected]> | 2026-06-30 20:35:45 -0500 |
|---|---|---|
| committer | Luke Hoersten <[email protected]> | 2026-06-30 20:35:45 -0500 |
| commit | 39aeeff0bcdfaa5ac4520e1fc7b90266c761c3f5 (patch) | |
| tree | a92b47133a9dd9aa96af0429c0c2784e41f5279f /scrypted | |
| parent | d71bdc9167fda3d5401ec3212a9d2269da7002fd (diff) | |
scrypted: gate snapshot POST on the live stream not yet painting
Now that the prebuffered stream paints in ~0.7s, the parallel snapshot usually
loses the race and, if slow (a Unifi takePicture cache-miss can be several
seconds), would land after live video and overpaint it with a staler still.
pushSnapshot takes a shouldPaint predicate; startStream passes
() => !firstStreamFrameSeen (flipped by the ffmpeg first-frame handler), so a
snapshot that finishes after the stream is already painting is dropped before
the POST. Preserves the snapshot as a gap-filler for the slow paths (live-edge
fallback / cold prebuffer) where the stream hasn't painted yet.
Diffstat (limited to 'scrypted')
| -rw-r--r-- | scrypted/scrypted-viewport.ts | 28 |
1 files changed, 23 insertions, 5 deletions
diff --git a/scrypted/scrypted-viewport.ts b/scrypted/scrypted-viewport.ts index 1115db0..381580b 100644 --- a/scrypted/scrypted-viewport.ts +++ b/scrypted/scrypted-viewport.ts @@ -1007,11 +1007,13 @@ class ScryptedViewportProvider extends ScryptedDeviceBase // image and resolves in 50–300ms, vs. the ~5s before the first // ffmpeg-emitted frame lands. The snapshot fills the gap so the panel // shows the camera near-instantly on tap/event. Fire-and-forget — - // runs in parallel with stream socket bring-up. If the stream's first - // frame arrives before the snapshot finishes, the snapshot just - // overpaints stale data on top of a fresher frame for ~1 paint cycle. + // runs in parallel with stream socket bring-up. Flipped true by the + // ffmpeg first-frame handler below; the snapshot's final POST is gated + // on !firstStreamFrameSeen so a slow snapshot can't overpaint live + // video with a staler still once the stream is already painting. // Errors are silent so a missing snapshot path doesn't break start. - this.pushSnapshot(v, cam, tEvent).catch(() => {}); + let firstStreamFrameSeen = false; + this.pushSnapshot(v, cam, tEvent, () => !firstStreamFrameSeen).catch(() => {}); // Fetch the panel's native dimensions from the firmware and // cache them on the viewport's storage. Falls back to 800x480 @@ -1368,6 +1370,9 @@ class ScryptedViewportProvider extends ScryptedDeviceBase if (!firstFfmpegFrameLogged) { this.console.log(`stream "${v.name}": first ffmpeg frame +${since()}ms (jpeg=${(frame.length / 1024).toFixed(0)}KB)`); firstFfmpegFrameLogged = true; + // Live video is now painting — suppress a late snapshot + // POST so it can't overpaint fresh frames with a still. + firstStreamFrameSeen = true; } // Drop only when the socket isn't connected yet @@ -1587,7 +1592,13 @@ class ScryptedViewportProvider extends ScryptedDeviceBase // use it for landscape (no rotate needed). // 3. ffmpeg one-shot — old slow path, ~500-700ms cold start. // Always works; the safety net. - private async pushSnapshot(v: Viewport, cam: any, tEvent: number = Date.now()) { + // shouldPaint gates the final POST: the snapshot races the live stream + // and now usually LOSES (prebuffered video paints in ~0.7s). If the stream + // already produced a frame by the time our snapshot is ready, posting it + // would overpaint fresh video with a staler still — so we skip. The + // snapshot stays valuable only as a gap-filler when the stream is slow + // (live-edge fallback / cold prebuffer), where shouldPaint() is still true. + private async pushSnapshot(v: Viewport, cam: any, tEvent: number = Date.now(), shouldPaint: () => boolean = () => true) { const since = () => Date.now() - tEvent; this.console.log(`snapshot "${v.name}": start +${since()}ms`); let mo: any; @@ -1680,6 +1691,13 @@ class ScryptedViewportProvider extends ScryptedDeviceBase if (transformed.length < 4) return; this.console.log(`snapshot "${v.name}": transform +${since()}ms via ${path} (${(transformed.length / 1024).toFixed(0)}KB)`); + // Last check before painting: if the live stream already produced a + // frame, don't overpaint it with this (now staler) snapshot. + if (!shouldPaint()) { + this.console.log(`snapshot "${v.name}": skipped post +${since()}ms — live stream already painting`); + return; + } + try { this.console.log(`snapshot "${v.name}": post sent +${since()}ms`); const res = await fetch(`http://${v.host}/frame`, { |
