Vanilagy / webm-muxer

WebM multiplexer in pure TypeScript with support for WebCodecs API, video & audio.
https://vanilagy.github.io/webm-muxer/demo
MIT License
197 stars 12 forks source link

Chrome throwing error and failing to export video #31

Closed talltyler closed 7 months ago

talltyler commented 7 months ago

I had code that was working, I don't think anything changed but I'm now getting this error.

A VideoFrame was garbage collected without being closed. Applications should call close() on frames when done with them to prevent stalls.

I'm not explicitly calling close() anywhere but neither are you in your example code.

I'm using the newest published version of this library and passing canvas frames into code like this.


export default class WebM {
  constructor(width, height, transparent = true, fps) {
    this.muxer = new Muxer({
      target: new ArrayBufferTarget(),
      video: {
        codec: 'V_VP9',
        width: width,
        height: height,
        frameRate: fps,
        alpha: transparent,
      },
      audio: undefined,
      firstTimestampBehavior: 'offset',
    });

    this.videoEncoder = new VideoEncoder({
      output: (chunk, meta) => this.muxer.addVideoChunk(chunk, meta),
      error: (error) => reject(error),
    });
    this.videoEncoder.configure({
      codec: 'vp09.00.10.08',
      width: width,
      height: height,
      bitrate: 1e6,
    });
  }

  addFrame(frame, time, frameIndex) {
    return new Promise((resolve) => {
      this.videoEncoder.encode(new VideoFrame(frame, { timestamp: time * 1000 }), {
        keyFrame: !(frameIndex % 50),
      });
      resolve();
    });
  }

  generate() {
    return new Promise((resolve, reject) => {
      this.videoEncoder
        .flush()
        .then(() => {
          this.muxer.finalize();
          resolve(new Blob([this.muxer.target.buffer], { type: 'video/webm' }));
        })
        .catch(reject);
    });
  }
}
Vanilagy commented 7 months ago

I actually do call close in my demo code! Here:

// ...
const encodeVideoFrame = () => {
    let elapsedTime = document.timeline.currentTime - startTime;
    let frame = new VideoFrame(canvas, {
        timestamp: framesGenerated * 1e6 / 30
    });
    framesGenerated++;

    let needsKeyFrame = elapsedTime - lastKeyFrame >= 10000;
    if (needsKeyFrame) lastKeyFrame = elapsedTime;

    videoEncoder.encode(frame, { keyFrame: needsKeyFrame });
    frame.close(); // <=== THIS LINE HERE

    recordingStatus.textContent =
        `${elapsedTime % 1000 < 500 ? '🔴' : '⚫'} Recording - ${(elapsedTime / 1000).toFixed(1)} s`;
};
// ...

The warning you're talking about is typically benign and just wants to encourage the programmer to manually GC video frames for better memory management.

In your line:

this.videoEncoder.encode(new VideoFrame(frame, { timestamp: time * 1000 }), {
  keyFrame: !(frameIndex % 50),
});

you'll need to keep a reference to the frame to garbage collect it later on:

let frame = new VideoFrame(frame, { timestamp: time * 1000 });
this.videoEncoder.encode(frame, {
  keyFrame: !(frameIndex % 50),
});
frame.close();
talltyler commented 7 months ago

You are totally correct, all of this is working as it should, thank you for the reply.