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.
Why
The render loop is driven entirely by
requestVideoFrameCallback(orrequestAnimationFrameas fallback). If the underlying track goes silent whilereadyState/paused/endedstill 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:errorsrc/chromakey.js:672,src/chromakey.js:1136startedsrc/chromakey.js:752autotunesrc/chromakey.js:888backendsrc/chromakey.js:1079pluginerrorsrc/chromakey.js:994-996None of these fire in response to a stalled/frozen stream —
erroronly fires on the nativevideoelement'serrorevent or an internal render/setup exception, not on frame delivery stopping.The render loop (
_startLoop,src/chromakey.js:1199-1215) usesrequestVideoFrameCallback(preferred, lines 1200/1204/1206) orrequestAnimationFrame(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:
watchdog, nosetTimeoutat all in the file (the only timer is asetIntervalinside the plugin API'severy(ms, fn)helper,src/chromakey.js:954, which is a generic facility for plugin authors, not used internally for stall detection).readyStateis 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.readyStateis never referenced anywhere insrc/.requestVideoFrameCallbackmetadata 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)
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 notpaused/ended, dispatch a new event, e.g.stalled, with detail including the elapsed time.recovered(or similar) event once frames resume after a stall was flagged, so integrators can clear any "reconnecting" UI they showed.waiting/stalledevents on the<video>element — those fire based on buffering state and are not reliable for a live WebRTC track that reportsreadyState/pausedas healthy while frames have actually stopped.Success criteria
stalledevent fires within the configured threshold, with usefuldetail.stalledevent fires during normal playback, intentionalpause(), orended— only on unexpected frame-delivery interruption.