canvas.captureStream: doesn't capture frames in a background tab
Categories
(Core :: WebRTC: Audio/Video, defect, P2)
Tracking
()
Tracking | Status | |
---|---|---|
firefox98 | --- | fixed |
People
(Reporter: tristan.fraipont, Assigned: pehrsons)
References
(Regressed 1 open bug, )
Details
Attachments
(7 files)
3.69 KB,
text/html
|
Details | |
15.57 KB,
video/webm
|
Details | |
1.95 KB,
patch
|
Details | Diff | Splinter Review | |
48 bytes,
text/x-phabricator-request
|
Details | Review | |
48 bytes,
text/x-phabricator-request
|
Details | Review | |
48 bytes,
text/x-phabricator-request
|
Details | Review | |
48 bytes,
text/x-phabricator-request
|
Details | Review |
Comment 2•8 years ago
|
||
Updated•8 years ago
|
Updated•8 years ago
|
Comment 5•7 years ago
|
||
Comment 6•6 years ago
|
||
This doesn't seem to be exclusive to the MediaRecorder btw, it also effects active (canvas based) MediaStreams.
A bit more information:
I can reproduce the behaviour on both Windows and macOS with Firefox 66.0.5.
I've also re-tried the requestFrame invocation Kaiido mentioned.
- captureStream without requestFrame
- requestFrame with captureStream(0)
- both
Here is a recording
showing FF (left) and Chrome (74 on the right) and the behaviour
of a MediaStream acquired via captureStream vs getUserMedia in the two browsers
when the tab becomes inactive (covered by a different window).
Note:
Might be worth noting that the fiddle still works, but you need to run it pressing the "run" button
because the AudioContext requires a user gesture to start in Chrome.
Any updates on this? I feel like this should be a higher priority than it is.
For many apps (recording, live streaming, etc), dropping frames like this when the window is blurred is a nasty user experience. Unfortunately, for many of these types of apps, Firefox isn't usable because of this bug.
Would love to see it fixed soon!
Comment 10•5 years ago
|
||
Just ran into this ourselves. This is prevent our app from supporting Firefox. Anything that can be done to help move this along???
Comment 11•4 years ago
|
||
We are also running into this issue, and it is very problematic for our app's experience when using FF vs Chrome.
Comment 12•4 years ago
|
||
A totally experimental/naive patch that resolves the issue when capturing from a Canvas, if it is helpful at all.
Comment 13•4 years ago
|
||
Also ran into this. Doesn't anyone of the team want to incorporate the diff? We need to recommend Chrome to our customers cause of this.
Comment 14•4 years ago
|
||
this issue is really an issue for our web application as well. please apply the provided fix!
Comment 15•4 years ago
|
||
Lee, what would it take for <canvas>
to continue drawing when not in foreground when it's being recorded via captureStream
? This is a clear webcompat problem and we need to fix it. What do you think of the patch attached here?
Comment 16•4 years ago
|
||
Andreas Pehrson wrote the code that the patch in question modifies, so he would be the better person to answer that question.
Assignee | ||
Comment 17•4 years ago
|
||
I don't think the patch would work as it seems to be creating a new refresh driver and hooking it up to be driven by a timer.
We are deliberate about the refresh driver we use. It has to be in sync with webgl rendering, or else the front buffer may be empty when a capture happens.
We should be able to avoid the throttling while a canvas has a pending capture. But I'm not sure where that is managed, or who would be the right person to ask. Lee, would you know this?
Assignee | ||
Comment 18•4 years ago
|
||
I think this boils down to animation frame callbacks not being called when hidden. If there are no animation frame callbacks, and no useful timers (because they're throttled), how would one be able to draw to the canvas, even if it is able to capture frames while in the background?
This to me seems to require spec work, even if comment 17 was fixed. How does it work in Chrome?
Assignee | ||
Comment 19•4 years ago
|
||
Right, you run the drawing with a ScriptProcessorNode instead of requestAnimationFrame.
That will work ok until ScriptProcessorNode is removed (we have AudioWorklet as a way better alternative now) but then we're stuck in comment 18 anyway.
Comment 20•4 years ago
|
||
The activeness of a tab is generally managed by the front-end code, fwiw. See BrowsingContext::IsActive
and related code.
Comment 21•4 years ago
|
||
Mike, how do we solve this for Picture In Picture? Do we do something fancy with tab activeness in the front-end for it?
Alternatively, we could keep track of whether there's any ongoing capture and tweak this code to consider the PresShell active anyways.
Comment 22•4 years ago
|
||
(In reply to Emilio Cobos Álvarez (:emilio) from comment #21)
Mike, how do we solve this for Picture In Picture? Do we do something fancy with tab activeness in the front-end for it?
Alternatively, we could keep track of whether there's any ongoing capture and tweak this code to consider the PresShell active anyways.
PiP actually suffers from the same issue. Some videos will pause or degrade in quality when in a background tab. We track this in bug 1598654, where I tried a hacky PiP-centric solution, but it didn't stick.
Comment 23•4 years ago
|
||
(In reply to Andreas Pehrson [:pehrsons] from comment #17)
I don't think the patch would work as it seems to be creating a new refresh driver and hooking it up to be driven by a timer.
We are deliberate about the refresh driver we use. It has to be in sync with webgl rendering, or else the front buffer may be empty when a capture happens.
We should be able to avoid the throttling while a canvas has a pending capture. But I'm not sure where that is managed, or who would be the right person to ask. Lee, would you know this?
Maybe Matt or Nical might have some input.
Reporter | ||
Comment 24•4 years ago
|
||
(In reply to Andreas Pehrson [:pehrsons] from comment #19)
Right, you run the drawing with a ScriptProcessorNode instead of requestAnimationFrame.
That will work ok until ScriptProcessorNode is removed (we have AudioWorklet as a way better alternative now) but then we're stuck in comment 18 anyway.
No I run the drawing with an AudioScheduledSourceNode which is not scheduled to be deprecated anytime soon no, and there are many other ways to not get throttled in a background tab.
(from comment #18)
I think this boils down to animation frame callbacks not being called when hidden. If there are no animation frame callbacks, and no useful timers (because they're throttled), how would one be able to draw to the canvas, even if it is able to capture frames while in the background?
This to me seems to require spec work, even if comment 17 was fixed. How does it work in Chrome?
Once again we fall here.
Why should this be tied to rAF callbacks?
I don't have any telemetry info at hand, but from what I saw in the wild most uses of a canvas stream + MediaRecorder are to apply some filters or recognition results over an other source (e.g a video or a webcam stream).
In this more than common case, and in many others, one doesn't want to record at the screen refresh rate (120Hz on new mobile phones, anything between 50 to 240Hz on desktops), they generally want a more conventional frame-rate between 25FPS and 30FPS, maybe even 60FPS, because that's the source's rate anyway, or to avoid recording 4 times more frames than most readers are able to cope with or just to avoid having 4 times bigger files than necessary.
And for this task rAF is the worst* timer you can find (*right after requestIdleCallback
):
There is no way to tell at which frequency it will fire, every user will produce different results only because the refresh-rate of their monitor is different. This refresh-rate can even change during the recording if the user moves the browser's tab to a second monitor. And as demonstrated here, it gets throttled when the document is blurred.
Don't get me wrong, rAF is great at what it does: offer a mean to hook to the presentation time so that animations that makes it to the monitor are in sync with the monitor.
But here we just don't have to take the monitor into consideration at all.
So yes, we fall back in this poor design decision of tying this API to the undefined "paint" time of the canvas, which you read as being the next "painting frame", while as also demonstrated here, at least the 2D API has means to force these paints synchronously: ctx.getImageData
, ctx.drawImage(canvas,x,y)
, canvas.toDataURL()
, and even if not really required, in the facts even canvas.toBlob()
does this.
So why not simply make CanvasCaptureMediaStreamTrack.requestFrame()
do the same?
Comment 25•4 years ago
|
||
I'd absolutely agree with @Kaiido's points here. Additionally, something to consider is a frame event or callback request on a video or canvas element. That way, we could "genlock" paints to the target canvas to frames from a source video. But, I'd happily settle for a frame callback on the captured media stream if that's all we can get.
requestAnimationFrame()
was a hack in this context... we were only using it because it's all we had, aside from the even hackier callbacks with the Web Audio API ScriptProcessorNode. If we had some frame clock that would allow us to paint to the canvas at a predictable framerate, that would be a better resolution to this problem.
Please do however keep in mind that other throttling, such as for setTimeout()
/setInterval()
and perhaps requestAnimationFrame()
still likely needs to be disabled to avoid side effects. The scripts painting to the canvas may require other code to run that uses these timers.
Assignee | ||
Comment 26•4 years ago
|
||
(In reply to Andreas Pehrson [:pehrsons] from comment #19)
Right, you run the drawing with a ScriptProcessorNode instead of requestAnimationFrame.
To nuance this: A running AudioContext will disable timer throttling, so if timers are used for scheduling one doesn't need a ScriptProcessorNode.
Canvas.captureStream was not designed as a tool to transform every frame in a video track, so of course some pain points will arise when used as such.
I do have sympathy for the use case, since it's the only tool available for transforming video right now. A better tool would be what insertable streams claims to do for raw media, though that hasn't properly materialized yet.
To be pragmatic, I think we could possibly do something like comment 18 suggests to keep this working while in the background.
If you argue for changing behavior of requestFrame()
you should do so on the spec.
Comment 27•3 years ago
|
||
Same or a very similar problem appears in a WebRTC stream transformation use case and there seems to be no workaround.
Even if you use an alternative timer source, such as a timer worker or an audio oscillator, the canvas stream appears throttled to 1 fps when the tab is in the background leading to an obviously unpleasant call experience if one of the peers decides to multitask.
Comment 28•3 years ago
|
||
We are also forced to nudge users to Google Chrome because of that.
Updated•3 years ago
|
Comment 29•3 years ago
|
||
Jib, any chance you could give us some clarity on what the standards involved here mandate we do?
Updated•3 years ago
|
Comment 30•3 years ago
•
|
||
This is a complicated. At its core, canvas.captureStream() creates a sink for a canvas other than the (visible) page, which breaks the assumption that canvas paints can be throttled in background tabs without visible consequences.
I see at least 3 questions:
-
When is a canvas painted? From bug 1613602 comment 1: the spec says:
- "A new frame is requested from the canvas when [[frameCaptureRequested]] is true and the canvas is painted.", but when a canvas is painted is not properly defined (#29). A bit of digging suggests "canvas paint cycle" / "paint mechanisms" is distinct from "user agent rendering algorithm" / "rasterization". cc @annevk can you help clarify?
-
Should requestFrame() trigger "paint" or something like it? Kaiido makes a case in comment 24 (which should be raised in issue #28):
So yes, we fall back in this poor design decision of tying this API to the undefined "paint" time of the canvas, which you read as being the next "painting frame", while as also demonstrated here, at least the 2D API has means to force these paints synchronously:
ctx.getImageData
,ctx.drawImage(canvas,x,y)
,canvas.toDataURL()
, and even if not really required, in the facts evencanvas.toBlob()
does this.So why not simply make
CanvasCaptureMediaStreamTrack.requestFrame()
do the same? -
Rendering opportunities in background tabs are subject to "user agent throttling for performance reasons". But is rendering == canvas paint?
Developers have shown clever uses of non-rendering timers to drive canvas actions, which begs the question why Firefox's MediaRecorder cannot keep up, when Chrome's apparently can. Since years have passed, we should probably look at web compat over specs at this point.
Though curiously, as a test, when I compared this direct camera record fiddle with one that goes an extra step through canvas using requestAnimationFrame
, I got some surprising results with the latter:
- Navigating to a different tab in Firefox predictably causes <1 fps, but in Chrome the video stalls entirely.
So from this test, I'd say comment 18 doesn't sound like the right solution.
Assignee | ||
Comment 31•3 years ago
|
||
(In reply to Jan-Ivar Bruaroey [:jib] (needinfo? me) from comment #30)
Though curiously, as a test, when I compared this direct camera record fiddle with one that goes an extra step through canvas using
requestAnimationFrame
, I got some surprising results with the latter:
- Navigating to a different tab in Firefox predictably causes <1 fps, but in Chrome the video stalls entirely.
So from this test, I'd say comment 18 doesn't sound like the right solution.
I don't follow this reasoning. How does keeping rAF running in these background tabs not solve the stalling problem?
I should add that we internally run the frame-capturing of canvases off the refresh driver, which also drives rAF callbacks, in order to capture a webgl buffer before it is swapped away. The order of events there is important and chrome got it wrong in the past (so tried to update the spec to deal with it for them), not sure how they fare now.
Comment 32•3 years ago
|
||
I suspect what Jan-Ivar is saying is that might not necessarily result in interop with Chrome?
I had forgotten https://github.com/w3c/mediacapture-fromelement existed. It seems to me it should be integrated with the HTML Standard directly. It's indeed not clear to me what a canvas paint might be. Per the HTML Standard the canvas is always up-to-date and there is no explicit paint step. "Update the rendering" is when style/layout happens and various related events fire, typically/ideally at 60pfs.
Perhaps one thing that might help to look at is to make requestFrame()
use logic similar to getImageData()
/toBlob()
and friends as those reportedly do the right thing. I would also love it if someone could explain the security story for requestFrame()
. Is it basically so that the resulting MediaStream
is opaque once the canvas is tainted in some way? Do we have tests for that? Not propagating tainting is a frequent source of bugs...
Another thing to look out for here is that this cannot be used to circumvent throttling of background pages.
Hope that helps a bit.
Assignee | ||
Comment 33•3 years ago
|
||
(In reply to Anne (:annevk) from comment #32)
I suspect what Jan-Ivar is saying is that might not necessarily result in interop with Chrome?
I had forgotten https://github.com/w3c/mediacapture-fromelement existed. It seems to me it should be integrated with the HTML Standard directly. It's indeed not clear to me what a canvas paint might be. Per the HTML Standard the canvas is always up-to-date and there is no explicit paint step. "Update the rendering" is when style/layout happens and various related events fire, typically/ideally at 60pfs.
Perhaps one thing that might help to look at is to make
requestFrame()
use logic similar togetImageData()
/toBlob()
and friends as those reportedly do the right thing. I would also love it if someone could explain the security story forrequestFrame()
. Is it basically so that the resultingMediaStream
is opaque once the canvas is tainted in some way? Do we have tests for that? Not propagating tainting is a frequent source of bugs...
Test for capturing a tainted canvas. Test for tainting a captured canvas.
Another thing to look out for here is that this cannot be used to circumvent throttling of background pages.
Well, Google Meet in Chrome is drawing to the canvas, and capturing from it in the background somehow. I suspect their capturing is decoupled from drawing (related: comment 31 - ours is not) and the drawing in Meet is driven by something else than requestAnimationFrame. Maybe requestVideoFrameCallback? For instance on whereby.com blurred video stalls in the background in Chrome.
Hope that helps a bit.
Comment 34•3 years ago
|
||
(In reply to Andreas Pehrson [:pehrsons] from comment #33)
(In reply to Anne (:annevk) from comment #32)
I suspect what Jan-Ivar is saying is that might not necessarily result in interop with Chrome?
I had forgotten https://github.com/w3c/mediacapture-fromelement existed. It seems to me it should be integrated with the HTML Standard directly. It's indeed not clear to me what a canvas paint might be. Per the HTML Standard the canvas is always up-to-date and there is no explicit paint step. "Update the rendering" is when style/layout happens and various related events fire, typically/ideally at 60pfs.
Perhaps one thing that might help to look at is to make
requestFrame()
use logic similar togetImageData()
/toBlob()
and friends as those reportedly do the right thing. I would also love it if someone could explain the security story forrequestFrame()
. Is it basically so that the resultingMediaStream
is opaque once the canvas is tainted in some way? Do we have tests for that? Not propagating tainting is a frequent source of bugs...Test for capturing a tainted canvas. Test for tainting a captured canvas.
Another thing to look out for here is that this cannot be used to circumvent throttling of background pages.
Well, Google Meet in Chrome is drawing to the canvas, and capturing from it in the background somehow. I suspect their capturing is decoupled from drawing (related: comment 31 - ours is not) and the drawing in Meet is driven by something else than requestAnimationFrame. Maybe requestVideoFrameCallback? For instance on whereby.com blurred video stalls in the background in Chrome.
Hope that helps a bit.
In our app (Bash Video) we used a worker to post messages at a set rate to the Window which would then call requestFrame to prevent background stalling in Chrome.
Comment 35•3 years ago
|
||
(In reply to Anne (:annevk) from comment #32)
I suspect what Jan-Ivar is saying is that might not necessarily result in interop with Chrome?
Unthrottling rAF in Firefox may solve it, but at the cost of undoing a potentially significant background optimization (I have 100s of open tabs, likely with fewer ads than most).
So we'd probably only be able to turn this on if we see captureStream
in use, but then we've created an escape hatch from throttling that pages may use maliciously, and it would only hurt Firefox users. That seems unwise.
This would widen the behavior gap with Chrome, who has solved this differently somehow. Since it's not tightly spec'ed, I suppose browsers' throttling stories don't technically need to match. But from a web developer pov, it'd probably be easier if they did. I wish we knew more about what Chrome does here.
(In reply to Andreas Pehrson [:pehrsons] from comment #33)
Well, Google Meet in Chrome is drawing to the canvas, and capturing from it in the background somehow. I suspect their capturing is decoupled from drawing (related: comment 31 - ours is not) and the drawing in Meet is driven by something else than requestAnimationFrame. Maybe requestVideoFrameCallback?
A version of the through-canvas fiddle using requestVideoFrameCallback also stalls when in the background in Chrome FWIW.
Assignee | ||
Comment 36•3 years ago
|
||
(In reply to Jan-Ivar Bruaroey [:jib] (needinfo? me) from comment #35)
(In reply to Andreas Pehrson [:pehrsons] from comment #33)
Well, Google Meet in Chrome is drawing to the canvas, and capturing from it in the background somehow. I suspect their capturing is decoupled from drawing (related: comment 31 - ours is not) and the drawing in Meet is driven by something else than requestAnimationFrame. Maybe requestVideoFrameCallback?
A version of the through-canvas fiddle using requestVideoFrameCallback also stalls when in the background in Chrome FWIW.
So I did some debugging in Chrome 95 today. They have MediaStreamTrackProcessor present in code but it's not in use. Instead there are two canvases on which captureStream(0) is used. There is a "timer worker" that sends messages to main thread to trigger requestFrame()
. So basically comment 34.
If this approach is viable in Firefox (and from talking to :farre I think it is) we could for instance simulate the refresh driver ticks to allow a canvas-captureStream to capture frames from a canvas when the refresh driver (and rAF callbacks) is throttled. Anne, jib, does this sound like a decent short-term fix to you? The long-term fix I imagine is whatever comes out of mediacapture-transform.
Updated•3 years ago
|
Updated•3 years ago
|
Comment 38•3 years ago
|
||
I filed https://github.com/w3c/mediacapture-fromelement/issues/94 on better HTML integration.
It seems that allowing a worker with postMessage()
to bypass throttling in that matter will become problematic. Why would that not be abused in the same way timers have been in the past? It's also still not clear to me why toBlob()
already works, but requestFrame()
does not. Can the latter not pull directly from the canvas and instead the canvas has to actively push frames into the MediaStream
?
Assignee | ||
Comment 39•3 years ago
|
||
(In reply to Anne (:annevk) from comment #38)
I filed https://github.com/w3c/mediacapture-fromelement/issues/94 on better HTML integration.
It seems that allowing a worker with
postMessage()
to bypass throttling in that matter will become problematic. Why would that not be abused in the same way timers have been in the past? It's also still not clear to me whytoBlob()
already works, butrequestFrame()
does not. Can the latter not pull directly from the canvas and instead the canvas has to actively push frames into theMediaStream
?
My interpretation of the spec is that requestFrame()
(and the term "request a frame to be captured") sets the internal slot [[frameCaptureRequested]]
to true
. The capturing then happens with the same timing as for the other captureStream()
modes.
Also, issue #28.
Comment 40•3 years ago
|
||
(In reply to Jan-Ivar Bruaroey [:jib] (needinfo? me) from comment #35)
(In reply to Anne (:annevk) from comment #32)
I suspect what Jan-Ivar is saying is that might not necessarily result in interop with Chrome?
Unthrottling rAF in Firefox may solve it, but at the cost of undoing a potentially significant background optimization (I have 100s of open tabs, likely with fewer ads than most).
So we'd probably only be able to turn this on if we see
captureStream
in use, but then we've created an escape hatch from throttling that pages may use maliciously, and it would only hurt Firefox users. That seems unwise.
Please weigh these concerns against the fact that software is broken in Firefox today, due to this throttling. This also hurts Firefox users.
I think the solution of disabling throttling when captureStream is used is correct, in lieu of a frame timer specific to the drawing context of the Canvas/CanvasCaptureMediaStream. That would be a better solution, solving for the issue documented here, and others. It also avoids the hack of worker postMessage.
Reporter | ||
Comment 41•3 years ago
|
||
Please, don't make rAF even more broken, and for something that is unrelated to rAF to begin with.
The problem is that Firefox's MediaRecorder is hooked to rAF with no good reasons, that's what needs to be fixed. Use a monotonic timer that doesn't get throttled like Chrome is apparently doing.
Yes it's unfortunate that we have to use hacks to workaround throttling, but breaking rAF which does promise exactly that it's battery friendly and throttles nicely when the page is hidden is the-wrong-direction™. Hopefully there will be better solutions than these hacks (maybe from the scheduler API?), but that we need hacks is not a reason to break existing APIs.
Assignee | ||
Comment 42•3 years ago
|
||
(In reply to Kaiido from comment #41)
The problem is that Firefox's MediaRecorder is hooked to rAF with no good reasons, that's what needs to be fixed. Use a monotonic timer that doesn't get throttled like Chrome is apparently doing.
Let's be clear here. MediaRecorder is irrelevant. Canvas captureStreams are tied to rAF, for the reason mentioned in comment 31.
Reporter | ||
Comment 43•3 years ago
|
||
(In reply to Andreas Pehrson [:pehrsons] from comment #42)
Let's be clear here. MediaRecorder is irrelevant. Canvas captureStreams are tied to rAF, for the reason mentioned in comment 31.
Yes sorry, replace "MediaRecorder" with "CanvasCaptureMediaStream[not yet Track...]" in my previous comment.
But I don't see why the argument in comment 31 is any compelling, rAF is still the worst place to capture frames, there is obviously an issue with captureStream(0)
which ought to work like drawImage
, and there is still no good reason to break rAF for this.
If you really need to, then break the perfs of the only webgl context that is being captured like Chrome does, but don't break everything for this API. From my point of view you are proposing to break everything to maintain a workaround.
Updated•3 years ago
|
Assignee | ||
Updated•3 years ago
|
Assignee | ||
Comment 44•3 years ago
|
||
Assignee | ||
Comment 45•3 years ago
|
||
Assignee | ||
Comment 46•3 years ago
|
||
This allows the canvas element to be notified when a draw that would trigger
a captureStream-frame to be captured.
Assignee | ||
Comment 47•3 years ago
|
||
Note that this is only triggered if the application is able to draw to the
canvas while the refresh driver is throttled.
Updated•3 years ago
|
Updated•3 years ago
|
Comment 48•3 years ago
|
||
Comment 49•3 years ago
|
||
bugherder |
https://hg.mozilla.org/mozilla-central/rev/d930203c9295
https://hg.mozilla.org/mozilla-central/rev/7eb441eeec30
https://hg.mozilla.org/mozilla-central/rev/4cb7ff5d0a69
https://hg.mozilla.org/mozilla-central/rev/25c186bc96eb
Updated•1 year ago
|
Description
•