| index 00de110..4c45e81 100644 |
| --- a/test/browser/room-share.test.mjs |
| +++ b/test/browser/room-share.test.mjs |
| @@ -390,40 +390,71 @@ test('signed out is read before the status, because it can be true of a 200', () |
| // chain, and the WeakSet of elements it used to keep then refused to |
| // arm another one. |
| |
| +const FRAME_QUIET = readNumber(src, 'FRAME_QUIET'); |
| + |
| const framesBox = new Function( |
| - 'frames', 'HAS_RVFC', |
| - 'const watched = new WeakMap();\n' + lift(src, 'watchFrames') + |
| + 'frames', 'HAS_RVFC', 'Date', |
| + 'const watched = new WeakMap();\nconst FRAME_QUIET = ' + FRAME_QUIET + ';\n' + |
| + [lift(src, 'watchFrames'), lift(src, 'chainDropped')].join('\n') + |
| '\nreturn { watchFrames };', |
| ); |
| |
| -// A video element as watchFrames sees one: a stream, a connectedness, |
| -// and one pending frame callback at a time. |
| +// A video element as watchFrames sees one: a stream, a connectedness, a |
| +// clock, and one pending frame callback at a time. `frame()` both |
| +// delivers a rendered frame and moves the clock, the way a playing |
| +// video does. |
| function fakeVideo(stream) { |
| return { |
| srcObject: stream, |
| isConnected: true, |
| + currentTime: 0, |
| pending: null, |
| requestVideoFrameCallback(fn) { |
| this.pending = fn; |
| }, |
| - // frame() delivers one rendered frame to whatever chain is armed. |
| frame() { |
| const fn = this.pending; |
| this.pending = null; |
| + this.currentTime += 0.033; |
| if (fn) fn(); |
| return this; |
| }, |
| }; |
| } |
| |
| +// A stream as watchFrames sees one: its video track is the second half |
| +// of the identity, because a camera the far side rebuilt comes back on |
| +// a new track inside the same MediaStream. |
| +function fakeStream(id, track) { |
| + const tracks = track === null ? [] : [track || { id: id + '-v' }]; |
| + return { |
| + id, |
| + getVideoTracks: () => tracks, |
| + // What a rebuilt camera does to a stream that is kept: the same |
| + // MediaStream object, carrying a different track. |
| + swap(next) { |
| + tracks[0] = next; |
| + return this; |
| + }, |
| + }; |
| +} |
| + |
| +// counter builds the box over a clock the test moves, so staleness can |
| +// be reached without waiting two seconds for it. |
| function counter() { |
| const frames = new Map(); |
| - return { frames, watchFrames: framesBox(frames, true).watchFrames }; |
| + let now = 1000; |
| + const clock = { now: () => now }; |
| + return { |
| + frames, |
| + watchFrames: framesBox(frames, true, clock).watchFrames, |
| + tick: (ms) => { now += ms; }, |
| + }; |
| } |
| |
| test('one chain per tile: arming twice does not count twice', () => { |
| const c = counter(); |
| - const video = fakeVideo({ id: 's1' }); |
| + const video = fakeVideo(fakeStream('s1')); |
| c.watchFrames('p1', video); |
| c.watchFrames('p1', video); |
| video.frame().frame().frame(); |
| @@ -431,16 +462,16 @@ test('one chain per tile: arming twice does not count twice', () => { |
| }); |
| |
| test('a tile handed a new stream starts counting again', () => { |
| - // The defect itself. The element is the same one; the stream is not. |
| + // The element is the same one; the stream is not. |
| const c = counter(); |
| - const video = fakeVideo({ id: 's1' }); |
| + const video = fakeVideo(fakeStream('s1')); |
| c.watchFrames('p1', video); |
| video.frame().frame(); |
| assert.equal(c.frames.get('p1'), 2); |
| |
| // What `srcObject = stream` does: the element resets, and the pending |
| // frame callback goes with it. |
| - video.srcObject = { id: 's2' }; |
| + video.srcObject = fakeStream('s2'); |
| video.pending = null; |
| c.watchFrames('p1', video); |
| video.frame().frame().frame(); |
| @@ -448,9 +479,33 @@ test('a tile handed a new stream starts counting again', () => { |
| 'the tile stopped counting when its stream was replaced'); |
| }); |
| |
| +test('a tile whose track was rebuilt inside the same stream counts again', () => { |
| + // The defect the browser gate found, and the reason the stream alone |
| + // is not the identity: Chromium hands a rebuilt camera back inside |
| + // the *same* MediaStream, on a new receiver track. The element resets |
| + // — its clock goes back and the pending callback is discarded — and a |
| + // chain keyed on the stream would have seen nothing to re-arm for. |
| + const c = counter(); |
| + const stream = fakeStream('s1', { id: 'v1' }); |
| + const video = fakeVideo(stream); |
| + c.watchFrames('p1', video); |
| + video.frame().frame(); |
| + assert.equal(c.frames.get('p1'), 2); |
| + |
| + // The same stream object, a new track inside it, and an element that |
| + // reset itself around the swap. |
| + stream.swap({ id: 'v2' }); |
| + video.pending = null; |
| + video.currentTime = 0; |
| + c.watchFrames('p1', video); |
| + video.frame().frame().frame(); |
| + assert.equal(c.frames.get('p1'), 5, |
| + 'the tile stopped counting when its track was rebuilt under it'); |
| +}); |
| + |
| test('a chain whose tile was detached is armed again on the next render', () => { |
| const c = counter(); |
| - const video = fakeVideo({ id: 's1' }); |
| + const video = fakeVideo(fakeStream('s1')); |
| c.watchFrames('p1', video); |
| video.frame(); |
| assert.equal(c.frames.get('p1'), 1); |
| @@ -466,12 +521,46 @@ test('a chain whose tile was detached is armed again on the next render', () => |
| assert.equal(c.frames.get('p1'), 3, 'the tile never started counting again'); |
| }); |
| |
| +test('a counter that goes quiet while the video plays on is re-armed', () => { |
| + // The backstop, for a cause nobody has thought of. Whatever drops a |
| + // frame callback, the signature is the same: the element's clock |
| + // moves and the count does not. |
| + const c = counter(); |
| + const video = fakeVideo(fakeStream('s1')); |
| + c.watchFrames('p1', video); |
| + video.frame(); |
| + assert.equal(c.frames.get('p1'), 1); |
| + |
| + // The callback is gone, but the video is playing: the clock advances |
| + // with nothing to count it. |
| + video.pending = null; |
| + video.currentTime += 1; |
| + c.tick(FRAME_QUIET + 1); |
| + c.watchFrames('p1', video); |
| + video.frame().frame(); |
| + assert.equal(c.frames.get('p1'), 3, 'a dropped chain was never noticed'); |
| +}); |
| + |
| +test('a frozen tile is idle, not dropped, and is left alone', () => { |
| + // The other half of the same question, and the reason the clock is |
| + // read at all: a tile receiving nothing has a chain that is perfectly |
| + // alive and simply has nothing to count. Re-arming it every second |
| + // would pile up callbacks on a still picture. |
| + const c = counter(); |
| + const video = fakeVideo(fakeStream('s1')); |
| + c.watchFrames('p1', video); |
| + const armed = video.pending; |
| + c.tick(FRAME_QUIET * 5); |
| + c.watchFrames('p1', video); |
| + assert.equal(video.pending, armed, 'a frozen tile was re-armed'); |
| +}); |
| + |
| test('a tile that changed peer counts under the new key only', () => { |
| const c = counter(); |
| - const video = fakeVideo({ id: 's1' }); |
| + const video = fakeVideo(fakeStream('s1')); |
| c.watchFrames('p1', video); |
| video.frame(); |
| - video.srcObject = { id: 's2' }; |
| + video.srcObject = fakeStream('s2'); |
| video.pending = null; |
| c.watchFrames('p2', video); |
| video.frame().frame(); |
| @@ -483,8 +572,8 @@ test('a browser without requestVideoFrameCallback counts nothing here', () => { |
| // pollFrames is that browser's counter, and arming this one would be a |
| // second one racing it. |
| const frames = new Map(); |
| - const { watchFrames } = framesBox(frames, false); |
| - const video = fakeVideo({ id: 's1' }); |
| + const { watchFrames } = framesBox(frames, false, Date); |
| + const video = fakeVideo(fakeStream('s1')); |
| watchFrames('p1', video); |
| assert.equal(video.pending, null); |
| assert.equal(frames.size, 0); |