Skip to content

No stall/freeze signal when render loop stops receiving frames #3

Description

@zoharbabin

Why

The render loop is driven entirely by requestVideoFrameCallback (or requestAnimationFrame as fallback). If the underlying track goes silent while readyState/paused/ended still look normal — a real WebRTC failure mode (e.g. the remote peer's encoder hangs, or a network path drops without tearing down the connection) — the callback simply stops firing. renderFrame() never gets called again, the canvas silently freezes on the last frame, and the page has no way to detect this happened. There is no timeout, watchdog, or event that surfaces a stall.

What (confirmed in code)

The library's full emitted event surface (core, excluding the generic plugin emit() passthrough) is exactly:

Event Location
error src/chromakey.js:672, src/chromakey.js:1136
started src/chromakey.js:752
autotune src/chromakey.js:888
backend src/chromakey.js:1079
pluginerror src/chromakey.js:994-996

None of these fire in response to a stalled/frozen stream — error only fires on the native video element's error event or an internal render/setup exception, not on frame delivery stopping.

The render loop (_startLoop, src/chromakey.js:1199-1215) uses requestVideoFrameCallback (preferred, lines 1200/1204/1206) or requestAnimationFrame (fallback, lines 1208-1213, gated on !video.paused && !video.ended). Neither path tracks elapsed wall-clock time between successive callback invocations, so there's no way to notice "the last frame callback was N seconds ago and nothing new has arrived."

Confirmed no existing stall-detection primitive anywhere in the codebase:

  • No watchdog, no setTimeout at all in the file (the only timer is a setInterval inside the plugin API's every(ms, fn) helper, src/chromakey.js:954, which is a generic facility for plugin authors, not used internally for stall detection).
  • readyState is checked in several places (lines 678, 679, 739, 827, 867, 1255) but only as a one-shot "is it ready enough to render this call" gate, never compared across time to detect that it stopped changing.
  • MediaStreamTrack.readyState is never referenced anywhere in src/.
  • The requestVideoFrameCallback metadata argument (which carries presentation/media timestamps usable for stall detection) is discarded — the callback (src/chromakey.js:1201) takes no parameters at all.

How (suggested fix)

  • Track the timestamp of the last successful frame callback.
  • Run a lightweight watchdog (e.g. setInterval, or a check piggybacked on the existing render loop) that compares "now" against the last-frame timestamp. If it exceeds a threshold (configurable, with a sane default — e.g. 2-3 seconds) while the video is not paused/ended, dispatch a new event, e.g. stalled, with detail including the elapsed time.
  • Optionally dispatch a paired recovered (or similar) event once frames resume after a stall was flagged, so integrators can clear any "reconnecting" UI they showed.
  • Make the threshold configurable via the existing options object, consistent with how other tunables are exposed.
  • Don't confuse this with the native waiting/stalled events on the <video> element — those fire based on buffering state and are not reliable for a live WebRTC track that reports readyState/paused as healthy while frames have actually stopped.

Success criteria

  • A test (or manual repro) that freezes frame delivery on a live track (readyState/paused/ended unaffected) and asserts a stalled event fires within the configured threshold, with useful detail.
  • No stalled event fires during normal playback, intentional pause(), or ended — only on unexpected frame-delivery interruption.
  • Threshold is documented and configurable; default value is called out in the README.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions