I'm getting crazy with this bug: in most windows hosts running chromium, this website of mine (basically playing a video and stopping at precise frames: to reproduce, just wait a few seconds for the page to be white (video loaded) before clicking "Next stop continuous") hangs, always exactly at the same frame, somewhere around this frame (I'm on linux now, so it may be the frame before or after):
However, I don't understand why this works on linux but not on windows… my guess is that the hardware windows decoder does something different than linux, but other projects like this one manage to get it working.
More precisely, the code hangs on this loop, printing The decoder is overwhelmed, let's wait before sending new stuff in the queue of size:
repeatedly:
while (!this.cachedFrames.has(idToExcluded)) {
// Someone else started to run this function. Let us stop then.
if (this.decoder.decodeQueueSize > this.maxDecodeQueueSize) {
console.log("The decoder is overwhelmed, let's wait before sending new stuff in the queue of size: ", this.decoder.decodeQueueSize);
} else {
console.debug("Starting to decoding frame ", this.nextFrameToAskForDecode, this.decoder.decodeQueueSize, this.maxDecodeQueueSize);
if(this.nextFrameToAskForDecode >= this.allNonDecodedFrames.length) {
// We arrived at the end of the video: let us flush (unless someone else is already running this function)
await this.abortIfNeeded(this.decoder.flush(), "flush");
return nextElementToDecode;
} else {
this.decoder.decode(this.allNonDecodedFrames[this.nextFrameToAskForDecode].nonDecodedFrame);
this.nextFrameToAskForDecode++;
}
}
// since the decoding is asynchronously done, we need to stop temporarily the code to give
// time to the decoder to run.
// This is needed, otherwise the decoder will not have time to start its job and we will get into
// an infinite loop.
console.debug("Give a bit of time to decoder");
await this.abortIfNeeded(wait(4), "foowait");
console.debug("Decoder had enough time");
}
Any idea what I'm doing wrong?
EDIT
The source code is available in https://github.com/leo-colisson/blenderpoint-web, and is reasonably simple (I mean, webcodec is never trivial due to demuxer etc): I basically have two files, index.html
that basically contains a bit of doc and calls the worker in worker.js
. So to test locally, just do:
$ git clone --recurse-submodules https://github.com/leo-colisson/blenderpoint-web/
$ cd blenderpoint-web
$ npx serve .
(the last npx serve .
command just starts a web server in the current folder assuming you have nodejs installed, since webcodec requires secure contexts, but you can use your favorite web server here) Then, go to the printed URL (page index.html
) and click "Browse" to select the video file Demo_24fps.mp4
that is included in the video. Click "Next stop (continuous)" to trigger the bug.
I managed to get a step further, the W3C recommendation to close the frame as soon as possible seems to be the key:
Authors are encouraged to call close() on output VideoFrames immediately when frames are no longer needed. The underlying media resources are owned by the VideoDecoder and failing to release them (or waiting for garbage collection) can cause decoding to stall.
In my case I was caching the N previous frames (to be able to play them backward for at least a few seconds) but it seems like in windows this freeze the decoder since apparently the resources to keep the frames are in control of the decoder. Now I need to see if I can find a way to support my backward-play feature… Either I need to spend some time to re-encode the video backward (it may take some time but is certainly the most robust option), or I find a way to move the cache to a part of the memory that is in control of the browser and not the decoder. But at least I know what I'm trying to avoid now!
EDIT: problem solved Since I cannot close the frame right after showing it in the canvas (to be able to play the animation backward I maintain a cache of the images seen in the last few seconds), I used instead:
async _onDecodedFrame(frameInDecoder) {
const frame = await createImageBitmap(frameInDecoder);
frameInDecoder.close();
…
}
where this function is called inside new VideoDecoder({ output: this._onDecodedFrame.bind(this), … })
, and then I use the obtained frame as before.
Explaination: createImageBitmap
basically allows me to turn the frame (stored in the decoder) inside an object stored in the browser. This way the decoder is never stalled! Note that frameInDecoder.clone()
or new VideoFrame(frameInDecoder)
does not work, I guess that all VideoFrame
are stored inside the decoder and have therefore the same issue.