diff options
Diffstat (limited to 'testing/web-platform/tests/mediacapture-insertable-streams/VideoTrackGenerator.https.html')
-rw-r--r-- | testing/web-platform/tests/mediacapture-insertable-streams/VideoTrackGenerator.https.html | 327 |
1 files changed, 327 insertions, 0 deletions
diff --git a/testing/web-platform/tests/mediacapture-insertable-streams/VideoTrackGenerator.https.html b/testing/web-platform/tests/mediacapture-insertable-streams/VideoTrackGenerator.https.html new file mode 100644 index 0000000000..2c81c7604a --- /dev/null +++ b/testing/web-platform/tests/mediacapture-insertable-streams/VideoTrackGenerator.https.html @@ -0,0 +1,327 @@ +<!DOCTYPE html> +<html> +<head> +<title>MediaStream Insertable Streams - VideoTrackGenerator</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../webrtc/RTCPeerConnection-helper.js"></script> +</head> +<body> + <p class="instructions">If prompted, use the accept button to give permission to use your audio and video devices.</p> + <h1 class="instructions">Description</h1> + <p class="instructions">This test checks that generating video MediaStreamTracks from VideoTrackGenerator works as expected.</p> + <script> + + const pixelColour = [50, 100, 150, 255]; + const height = 240; + const width = 320; + function makeVideoFrame(timestamp) { + const canvas = new OffscreenCanvas(width, height); + + const ctx = canvas.getContext('2d', {alpha: false}); + ctx.fillStyle = `rgba(${pixelColour.join()})`; + ctx.fillRect(0, 0, width, height); + + return new VideoFrame(canvas, {timestamp, alpha: 'discard'}); + } + + async function getVideoFrame() { + const stream = await getNoiseStream({video: true}); + const input_track = stream.getTracks()[0]; + const processor = new MediaStreamTrackProcessor(input_track); + const reader = processor.readable.getReader(); + const result = await reader.read(); + input_track.stop(); + return result.value; + } + + function assertPixel(t, bytes, expected, epsilon = 5) { + for (let i = 0; i < bytes.length; i++) { + t.step(() => { + assert_less_than(Math.abs(bytes[i] - expected[i]), epsilon, "Mismatched pixel"); + }); + } + } + + async function initiateSingleTrackCall(t, track, output) { + const caller = new RTCPeerConnection(); + t.add_cleanup(() => caller.close()); + const callee = new RTCPeerConnection(); + t.add_cleanup(() => callee.close()); + caller.addTrack(track); + t.add_cleanup(() => track.stop()); + + exchangeIceCandidates(caller, callee); + // Wait for the first track. + const e = await exchangeOfferAndListenToOntrack(t, caller, callee); + output.srcObject = new MediaStream([e.track]); + // Exchange answer. + await exchangeAnswer(caller, callee); + await waitForConnectionStateChange(callee, ['connected']); + } + + promise_test(async t => { + const videoFrame = await getVideoFrame(); + const originalWidth = videoFrame.displayWidth; + const originalHeight = videoFrame.displayHeight; + const originalTimestamp = videoFrame.timestamp; + const generator = new VideoTrackGenerator(); + t.add_cleanup(() => generator.track.stop()); + + // Use a MediaStreamTrackProcessor as a sink for |generator| to verify + // that |processor| actually forwards the frames written to its writable + // field. + const processor = new MediaStreamTrackProcessor(generator); + const reader = processor.readable.getReader(); + const readerPromise = new Promise(async resolve => { + const result = await reader.read(); + assert_equals(result.value.displayWidth, originalWidth); + assert_equals(result.value.displayHeight, originalHeight); + assert_equals(result.value.timestamp, originalTimestamp); + resolve(); + }); + + generator.writable.getWriter().write(videoFrame); + return readerPromise; + }, 'Tests that VideoTrackGenerator forwards frames to sink'); + + promise_test(async t => { + const videoFrame = makeVideoFrame(1); + const originalWidth = videoFrame.displayWidth; + const originalHeight = videoFrame.displayHeight; + const generator = new VideoTrackGenerator(); + t.add_cleanup(() => generator.track.stop()); + + const video = document.createElement("video"); + video.autoplay = true; + video.width = 320; + video.height = 240; + video.srcObject = new MediaStream([generator.track]); + video.play(); + + // Wait for the video element to be connected to the generator and + // generate the frame. + video.onloadstart = () => generator.writable.getWriter().write(videoFrame); + + return new Promise((resolve)=> { + video.ontimeupdate = t.step_func(() => { + const canvas = document.createElement("canvas"); + canvas.width = originalWidth; + canvas.height = originalHeight; + const context = canvas.getContext('2d'); + context.drawImage(video, 0, 0); + // Pick a pixel in the centre of the video and check that it has the colour of the frame provided. + const pixel = context.getImageData(videoFrame.displayWidth/2, videoFrame.displayHeight/2, 1, 1); + assertPixel(t, pixel.data, pixelColour); + resolve(); + }); + }); + }, 'Tests that frames are actually rendered correctly in a stream used for a video element.'); + + promise_test(async t => { + const generator = new VideoTrackGenerator(); + t.add_cleanup(() => generator.track.stop()); + + // Write frames for the duration of the test. + const writer = generator.writable.getWriter(); + let timestamp = 0; + const intervalId = setInterval( + t.step_func(async () => { + if (generator.track.readyState === 'live') { + timestamp++; + await writer.write(makeVideoFrame(timestamp)); + } + }), + 40); + t.add_cleanup(() => clearInterval(intervalId)); + + const video = document.createElement('video'); + video.autoplay = true; + video.width = width; + video.height = height; + video.muted = true; + + await initiateSingleTrackCall(t, generator.track, video); + + return new Promise(resolve => { + video.ontimeupdate = t.step_func(() => { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const context = canvas.getContext('2d'); + context.drawImage(video, 0, 0); + // Pick a pixel in the centre of the video and check that it has the + // colour of the frame provided. + const pixel = context.getImageData(width / 2, height / 2, 1, 1); + // Encoding/decoding can add noise, so increase the threshhold to 8. + assertPixel(t, pixel.data, pixelColour, 8); + resolve(); + }); + }); + }, 'Tests that frames are actually rendered correctly in a stream sent over a peer connection.'); + + + promise_test(async t => { + const generator = new VideoTrackGenerator(); + t.add_cleanup(() => generator.track.stop()); + + const inputCanvas = new OffscreenCanvas(width, height); + + const inputContext = inputCanvas.getContext('2d', {alpha: false}); + // draw four quadrants + const colorUL = [255, 0, 0, 255]; + inputContext.fillStyle = `rgba(${colorUL.join()})`; + inputContext.fillRect(0, 0, width / 2, height / 2); + const colorUR = [255, 255, 0, 255]; + inputContext.fillStyle = `rgba(${colorUR.join()})`; + inputContext.fillRect(width / 2, 0, width / 2, height / 2); + const colorLL = [0, 255, 0, 255]; + inputContext.fillStyle = `rgba(${colorLL.join()})`; + inputContext.fillRect(0, height / 2, width / 2, height / 2); + const colorLR = [0, 255, 255, 255]; + inputContext.fillStyle = `rgba(${colorLR.join()})`; + inputContext.fillRect(width / 2, height / 2, width / 2, height / 2); + + // Write frames for the duration of the test. + const writer = generator.writable.getWriter(); + let timestamp = 0; + const intervalId = setInterval( + t.step_func(async () => { + if (generator.track.readyState === 'live') { + timestamp++; + await writer.write(new VideoFrame( + inputCanvas, {timestamp: timestamp, alpha: 'discard'})); + } + }), + 40); + t.add_cleanup(() => clearInterval(intervalId)); + + const caller = new RTCPeerConnection(); + t.add_cleanup(() => caller.close()); + const callee = new RTCPeerConnection(); + t.add_cleanup(() => callee.close()); + const sender = caller.addTrack(generator.track); + + exchangeIceCandidates(caller, callee); + // Wait for the first track. + const e = await exchangeOfferAndListenToOntrack(t, caller, callee); + + // Exchange answer. + await exchangeAnswer(caller, callee); + await waitForConnectionStateChange(callee, ['connected']); + const params = sender.getParameters(); + params.encodings.forEach(e => e.scaleResolutionDownBy = 2); + sender.setParameters(params); + + const processor = new MediaStreamTrackProcessor(e.track); + const reader = processor.readable.getReader(); + + // The first frame may not have had scaleResolutionDownBy applied + const numTries = 5; + for (let i = 1; i <= numTries; i++) { + const {value: outputFrame} = await reader.read(); + if (outputFrame.displayWidth !== width / 2) { + assert_less_than(i, numTries, `First ${numTries} frames were the wrong size.`); + outputFrame.close(); + continue; + } + + assert_equals(outputFrame.displayWidth, width / 2); + assert_equals(outputFrame.displayHeight, height / 2); + + const outputCanvas = new OffscreenCanvas(width / 2, height / 2); + const outputContext = outputCanvas.getContext('2d', {alpha: false}); + outputContext.drawImage(outputFrame, 0, 0); + outputFrame.close(); + // Check the four quadrants + const pixelUL = outputContext.getImageData(width / 8, height / 8, 1, 1); + assertPixel(t, pixelUL.data, colorUL); + const pixelUR = + outputContext.getImageData(width * 3 / 8, height / 8, 1, 1); + assertPixel(t, pixelUR.data, colorUR); + const pixelLL = + outputContext.getImageData(width / 8, height * 3 / 8, 1, 1); + assertPixel(t, pixelLL.data, colorLL); + const pixelLR = + outputContext.getImageData(width * 3 / 8, height * 3 / 8, 1, 1); + assertPixel(t, pixelLR.data, colorLR); + break; + } + }, 'Tests that frames are sent correctly with RTCRtpEncodingParameters.scaleResolutionDownBy.'); + + promise_test(async t => { + const generator = new VideoTrackGenerator(); + t.add_cleanup(() => generator.track.stop()); + + const writer = generator.writable.getWriter(); + const frame = makeVideoFrame(1); + await writer.write(frame); + + assert_equals(generator.track.kind, "video"); + assert_equals(generator.track.readyState, "live"); + }, "Tests that creating a VideoTrackGenerator works as expected"); + + promise_test(async t => { + const generator = new VideoTrackGenerator(); + t.add_cleanup(() => generator.track.stop()); + + const writer = generator.writable.getWriter(); + const frame = makeVideoFrame(1); + await writer.write(frame); + + assert_throws_dom("InvalidStateError", () => frame.clone(), "VideoFrame wasn't destroyed on write."); + }, "Tests that VideoFrames are destroyed on write."); + + promise_test(async t => { + const generator = new VideoTrackGenerator(); + t.add_cleanup(() => generator.track.stop()); + + const writer = generator.writable.getWriter(); + const frame = makeVideoFrame(1); + assert_throws_js(TypeError, writer.write(frame)); + }, "Mismatched frame and generator kind throws on write."); + + promise_test(async t => { + const generator = new VideoTrackGenerator(); + t.add_cleanup(() => generator.track.stop()); + + // Use a MediaStreamTrackProcessor as a sink for |generator| to verify + // that |processor| actually forwards the frames written to its writable + // field. + const processor = new MediaStreamTrackProcessor(generator.track); + const reader = processor.readable.getReader(); + const videoFrame = makeVideoFrame(1); + + const writer = generator.writable.getWriter(); + const videoFrame1 = makeVideoFrame(1); + writer.write(videoFrame1); + const result1 = await reader.read(); + assert_equals(result1.value.timestamp, 1); + generator.muted = true; + + // This frame is expected to be discarded. + const videoFrame2 = makeVideoFrame(2); + writer.write(videoFrame2); + generator.muted = false; + + const videoFrame3 = makeVideoFrame(3); + writer.write(videoFrame3); + const result3 = await reader.read(); + assert_equals(result3.value.timestamp, 3); + + // Set up a read ahead of time, then mute, enqueue and unmute. + const promise5 = reader.read(); + generator.muted = true; + writer.write(makeVideoFrame(4)); // Expected to be discarded. + generator.muted = false; + writer.write(makeVideoFrame(5)); + const result5 = await promise5; + assert_equals(result5.value.timestamp, 5); + }, 'Tests that VideoTrackGenerator forwards frames only when unmuted'); + + // Note - tests for mute/unmute events will be added once + // https://github.com/w3c/mediacapture-transform/issues/81 is resolved + + </script> +</body> +</html> |