1

Ok so the question is a bit much, but it actually mentions every part of the "bug" I'm experiencing. I'm not sure if it is really a bug or if there is something I don't understand yet. I created a demo on codepen to reproduce the weird behavior.

https://codepen.io/rroyerrivard/pen/jOwBLbB

Like I wrote in the codepen, there seems to be a bug in Chrome for which the stream of an HTMLCanvasElement does not get refreshed on every image draw when it is hidden. But for that to happen, there needs to be these 4 conditions all at once.

  • We must feed an HTMLVideoElement with a MediaStream gotten from a call to HTMLCanvasElement.captureStream() (instead of directly showing the HTMLCanvasElement).
  • The HTMLCanvasElement from which we get the MediaStream must be hidden (either not in the DOM or having it hidden with css).
  • We must draw on the HTMLCanvasElement from an OffscreenCanvas that we get from a call to HTMLCanvasElement.transferControlToOffscreen().
  • The draw on the OffscreenCanvas must be done in a web worker that got the OffscreenCanvas transferred to.

I was unfortunate enough to hit all these conditions at once in a web app at work. I can avoid the bug by not using the transferControlToOffscreen() call and draw an ImageBitmap in the main thread after receiving it from the web worker, but that reduces the FPS by roughly 18%.

Is this a known bug? Is there a way to force the MediaStream to refresh on every draw of the OffscreenCanvas?

Raphael Royer-Rivard
  • 2,252
  • 1
  • 30
  • 53

1 Answers1

1

I guess it is expected behavior yes.

The thing here is that you made your worker thread wait using setTimeout and MessageEvents from the main thread.
The OffscreenCanvas will commit its bitmap to the placeholder canvas in the Worker's rendering frame. But by default the Worker won't enter this rendering frame. You need to request it, by using requestAnimationFrame.
Having the placeholder visible in the page will internally make the request for the OffscreenCanvas to commit its bitmap when the placeholder canvas will itself get rendered (i.e in the main thread's rendering frame), that's why it works when the placeholder canvas is visible.

Note that we used to have a OffscreenCanvas.commit() method but it has been deprecated when requestAnimationFrame made its way in WorkerContexts.

So use requestAnimationFrame in your Worker to actually force the commit of the bitmap to the placeholder canvas.

const video = document.querySelector("video");
const select = document.querySelector("select");
const canvas = document.createElement("canvas");
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker(getWorkerURL());
worker.postMessage({ offscreen }, [offscreen]);
select.oninput = e => worker.postMessage({ method: select.value });
worker.onmessage = (evt) => {
  video.srcObject = canvas.captureStream();
};

function getWorkerURL() {
  return URL.createObjectURL(
    new Blob([
      document.querySelector("[type='text/worker']").textContent
    ], { type: "text/javascript" })
  );
}
canvas { border: 1px solid }
<video controls autoplay></video>
<label>waiting method:<select><option>rAF</option><option>setTimeout</option></select></label>
<script type="text/worker">
  let ctx;
  let waiting_method = "rAF";
  onmessage = ({ data: { offscreen, method } }) => {
    if (offscreen) {
      ctx = offscreen.getContext("2d");
      draw();
      postMessage("ready");
    }
    else if (method) {
      waiting_method = method;
    }
  };
  function draw() {
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    ctx.fillText(new Date().getTime(), 30, 30);
    if (waiting_method === "rAF") {
      requestAnimationFrame(draw);
    }
    else {
      setTimeout(draw, 1000/30);
    }
  }
</script>

Now, I guess we could also expect the call to captureStream() to actually trigger the same internal request for commit that the visible placeholder canvas triggers, so you may want to file an issue at https://crbug.com regardless.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Thanks for your suggestion @Kaiido. I tried adding `requestAnimationFrame()` and it fixes the lag, but I cannot use it as my web app needs to process the frames even when a different browser tab is focused. – Raphael Royer-Rivard Sep 13 '21 at 15:40
  • Actually, calling `requestAnimationFrame()` without waiting for the callback to run the worker code fixes the bug and does not freeze the web worker when a browser different tab is focused. I still need to test in a different environment to see if the image refreshes at the right rate when the tab is not focused though. Will post update after testing. – Raphael Royer-Rivard Sep 13 '21 at 16:54
  • Something in my pipeline is completely freezing the image when the page is hidden. I can't test it directly in my codepen demo, but in my web app I send the canvas' captured stream to another user (in my case in another tab), and this way I can see that the image freezes. I do not get that problem when drawing the image directly in the canvas in the UI thread. So something related to the draw on the offscreen canvas in the web worker is preventing the stream to update. – Raphael Royer-Rivard Sep 13 '21 at 21:42
  • Well rAF is throttled for background documents, even Worker's one. If what you wanted was to keep drawing even when the tab is in background, then don't use a Worker, use a WebAudio timer: https://stackoverflow.com/a/40691112/3702797 – Kaiido Sep 13 '21 at 23:45
  • The real reason I'm using a web worker is because there is heavy computation done in my web app before drawing on the offscreen canvas (calling a neural network for segmentation). And since I'm calling rAF next to my draw code (the draw code is not in the rAF callback), my draw code still gets called when the tab is hidden. I've made sure of it with logs. However, the canvas' MediaStream is not updating at all when the tab is hidden. I don't think using an audio timer would help here. – Raphael Royer-Rivard Sep 14 '21 at 17:46