room.js: the render watch leaves a paused element to the Play control
The browser gate taught this one immediately: `playfallback` went red on the first full run after the render watch landed, and it was right to. An element the autoplay policy refused is paused, its clock is stopped, and frames are arriving — which is the render watch's exact signature and none of its business. §5's answer to a refused autoplay is the Play control in the tile's own caption; a watch that quietly called `play()` again would take the decision away from the person and hide the button that says so. Which is what it did: the scenario waited twenty-five seconds for a control that had already been un-paused out of existence. So `paused` joins the conditions. The watch is about an element that is playing and not painting; a paused one is somebody else's business. One test, mutation-verified: dropping the condition fails it and only it. node --check, 204 node tests, make ci.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CzvnuKhLzudvq7jZ7SvVt2
2 files changed,
+22
−2
internal/meet/static/room.js+8 −1test/browser/room-lane.test.mjs+14 −1
diff --git a/internal/meet/static/room.js b/internal/meet/static/room.js| index 7b6d2d2..32fab0f 100644 |
| --- a/internal/meet/static/room.js |
| +++ b/internal/meet/static/room.js |
| @@ -4122,7 +4122,14 @@ |
| // Pure over the tile record, and lifted by name in the node test. |
| function judgeRender(t, decoded) { |
| const track = t && t.track; |
| - const playable = !!(t && t.video && t.stream && track && |
| + // `paused` is in here for a reason the browser gate had to teach: |
| + // an element the autoplay policy refused is paused, its clock is |
| + // stopped, and frames are arriving — which is this watch's exact |
| + // signature and none of its business. §5's answer to that is the |
| + // Play control in the tile's own caption, and a watch that quietly |
| + // called play() again would take the decision away from the person |
| + // and hide the button that says so. |
| + const playable = !!(t && t.video && t.stream && track && !t.video.paused && |
| track.readyState !== 'ended' && !track.muted); |
| if (!playable || typeof decoded !== 'number') { |
| t.renderTime = -1; |
diff --git a/test/browser/room-lane.test.mjs b/test/browser/room-lane.test.mjs| index 6b42efa..583fcbe 100644 |
| --- a/test/browser/room-lane.test.mjs |
| +++ b/test/browser/room-lane.test.mjs |
| @@ -984,7 +984,7 @@ const render = renderBox(RENDER_STALL_POLLS, RENDER_REPLAYS); |
| function fakeTile(opts) { |
| const o = opts || {}; |
| return { |
| - video: { currentTime: 0 }, |
| + video: { currentTime: 0, paused: !!o.paused }, |
| stream: {}, |
| track: { readyState: o.readyState || 'live', muted: !!o.muted }, |
| renderTime: -1, |
| @@ -1040,6 +1040,19 @@ test('a tile that starts painting again forgets the replay it needed', () => { |
| assert.equal(t.renderReplays, 0, 'a picture that moves again must clear the budget'); |
| }); |
| +test('an element the autoplay policy paused is left to the Play control', () => { |
| + // The browser gate taught this one. A refused autoplay leaves the |
| + // element paused with its clock stopped while frames arrive, which is |
| + // this watch's exact signature and none of its business: §5's answer |
| + // is the Play control in the tile's caption, and a watch that called |
| + // play() again would take the decision away from the person and hide |
| + // the button that says so. |
| + const t = fakeTile({ paused: true }); |
| + const said = paint(t, [[10, 0], [20, 0], [30, 0], [40, 0], [50, 0], [60, 0]]); |
| + assert.deepEqual(said, ['', '', '', '', '', '']); |
| + assert.equal(t.renderReplays, 0); |
| +}); |
| + |
| test('a muted or ended track is nobody here to repair', () => { |
| for (const t of [fakeTile({ muted: true }), fakeTile({ readyState: 'ended' })]) { |
| const said = paint(t, [[10, 1.5], [20, 1.5], [30, 1.5], [40, 1.5], [50, 1.5]]); |