Skip to content

video-filters-web: queued output VideoFrames leak on stop() — "A VideoFrame was garbage collected without being closed" #2384

Description

@ham-c4b

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):

  1. Input frames are handled correctly (finally { frame.close() }).
  2. 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.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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