Package: @stream-io/video-filters-web 0.8.3
Environment: Chrome 151 (Windows), MediaStreamTrackProcessor/MediaStreamTrackGenerator path (insertable streams)
Symptom
Every time a processor pipeline is torn down (calling stop() on a filter created via createVirtualBackgroundStreamFilter / createBlurStreamFilter, e.g. when switching effects or turning the camera off), Chrome logs:
A VideoFrame was garbage collected without being closed. Applications should call close() on frames when done with them to prevent stalls.
Steady-state processing is clean — we measured 60 s of continuous blur processing with forced GC and got zero warnings. The warning appears reliably once per teardown (Chrome dedupes the message per GC pass, so one log line can represent several leaked frames).
Root cause
In BaseVideoProcessor.start() (dist index.es.js, the TransformStream created around the pipeThrough/pipeTo chain):
- Input frames are handled correctly (finally { frame.close() }).
- Output frames (new VideoFrame(this.canvas, …) returned from transform()) are enqueued into the TransformStream. When stop() calls this.abortController.abort(), output frames still queued between the transform and the generator's writable are dropped by the Streams machinery
— which does not close VideoFrame chunks — so they reach GC unclosed.
- Additionally, the abort signal is only checked before await this.transform(frame). A frame produced while the abort lands mid-transform is enqueued into the already-aborted pipe and leaks the same way.
Suggested fix
const transformStream = new TransformStream({
transform: async (frame, controller) => {
try {
if (this.abortController.signal.aborted) return frame.close();
// ...
const processed = await this.transform(frame);
if (this.abortController.signal.aborted) { // re-check after the await
processed.close();
return;
}
controller.enqueue(processed);
} catch (e) {
this.hooks.onError?.(e);
} finally {
frame.close();
}
},
flush: () => this.onFlush(),
cancel: () => this.onFlush(), // frames queued in the readable side are still
// dropped by the spec on abort — see note below
});
For the frames already sitting in the transform's readable queue at abort time, the Streams spec drops chunks without closing them; one option is piping to the generator without the abort signal and instead closing the generator to end the pipe, or draining the readable and
closing each remaining frame before aborting.
Impact
Cosmetic in most cases (a handful of frames per teardown, GC reclaims them), but the warning is noisy for apps that rebuild the pipeline frequently (effect/model switching), and unclosed frames hold buffers from the camera's frame pool, which Chrome explicitly warns can stall
capture.
Package: @stream-io/video-filters-web 0.8.3
Environment: Chrome 151 (Windows), MediaStreamTrackProcessor/MediaStreamTrackGenerator path (insertable streams)
Symptom
Every time a processor pipeline is torn down (calling stop() on a filter created via createVirtualBackgroundStreamFilter / createBlurStreamFilter, e.g. when switching effects or turning the camera off), Chrome logs:
A VideoFrame was garbage collected without being closed. Applications should call close() on frames when done with them to prevent stalls.
Steady-state processing is clean — we measured 60 s of continuous blur processing with forced GC and got zero warnings. The warning appears reliably once per teardown (Chrome dedupes the message per GC pass, so one log line can represent several leaked frames).
Root cause
In BaseVideoProcessor.start() (dist index.es.js, the TransformStream created around the pipeThrough/pipeTo chain):
— which does not close VideoFrame chunks — so they reach GC unclosed.
Suggested fix
const transformStream = new TransformStream({
transform: async (frame, controller) => {
try {
if (this.abortController.signal.aborted) return frame.close();
// ...
const processed = await this.transform(frame);
if (this.abortController.signal.aborted) { // re-check after the await
processed.close();
return;
}
controller.enqueue(processed);
} catch (e) {
this.hooks.onError?.(e);
} finally {
frame.close();
}
},
flush: () => this.onFlush(),
cancel: () => this.onFlush(), // frames queued in the readable side are still
// dropped by the spec on abort — see note below
});
For the frames already sitting in the transform's readable queue at abort time, the Streams spec drops chunks without closing them; one option is piping to the generator without the abort signal and instead closing the generator to end the pipe, or draining the readable and
closing each remaining frame before aborting.
Impact
Cosmetic in most cases (a handful of frames per teardown, GC reclaims them), but the warning is noisy for apps that rebuild the pipeline frequently (effect/model switching), and unclosed frames hold buffers from the camera's frame pool, which Chrome explicitly warns can stall
capture.