| <!doctype html> |
| <html> |
| <meta name="timeout" content="long"> |
| |
| <head> |
| <title>MediaRecorder canvas media source</title> |
| <link rel="help" |
| href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-mimeType"> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="/resources/testdriver.js"></script> |
| <script src="/resources/testdriver-vendor.js"></script> |
| <script src="../mediacapture-streams/permission-helper.js"></script> |
| </head> |
| |
| <body> |
| <canvas id="canvas"></canvas> |
| <script> |
| |
| async_test(test => { |
| const CANVAS_WIDTH = 256; |
| const CANVAS_HEIGHT = 144; |
| |
| // Empty video frames from this resolution consistently have ~750 bytes in my |
| // tests, while valid video frames usually contain 7-8KB. A threshold of |
| // 1.5KB consistently fails when video frames are empty but passes when video |
| // frames are non-empty. |
| const THRESHOLD_FOR_EMPTY_FRAMES = 1500; |
| |
| const CAMERA_CONSTRAINTS = { |
| video: { |
| width: { ideal: CANVAS_WIDTH }, |
| height: { ideal: CANVAS_HEIGHT } |
| } |
| }; |
| |
| function useUserMedia(constraints) { |
| let activeStream = null; |
| |
| function startCamera() { |
| return navigator.mediaDevices.getUserMedia(constraints).then( |
| (stream) => { |
| activeStream = stream; |
| return stream; |
| } |
| ); |
| } |
| |
| function stopCamera() { |
| activeStream?.getTracks().forEach((track) => track.stop()); |
| } |
| |
| return { startCamera, stopCamera }; |
| } |
| |
| function useMediaRecorder(stream, frameSizeCallback) { |
| const mediaRecorder = new MediaRecorder( |
| stream, |
| {} |
| ); |
| |
| mediaRecorder.ondataavailable = event => { |
| const {size} = event.data; |
| frameSizeCallback(size); |
| |
| if (mediaRecorder.state !== "inactive") { |
| mediaRecorder.stop(); |
| } |
| }; |
| |
| mediaRecorder.start(1000); |
| } |
| |
| const canvas = document.querySelector("canvas"); |
| const ctx = canvas.getContext("2d", { |
| alpha: false, |
| }); |
| |
| canvas.width = CANVAS_WIDTH; |
| canvas.height = CANVAS_HEIGHT; |
| |
| const {startCamera, stopCamera} = useUserMedia(CAMERA_CONSTRAINTS); |
| startCamera().then(async stream => { |
| const videoTrack = stream.getVideoTracks()[0]; |
| const { readable: readableStream } = new MediaStreamTrackProcessor({ |
| track: videoTrack |
| }); |
| |
| const composedTrackGenerator = new MediaStreamTrackGenerator({ |
| kind: "video" |
| }); |
| const sink = composedTrackGenerator.writable; |
| |
| ctx.fillStyle = "#333"; |
| ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); |
| |
| const transformer = new TransformStream({ |
| async transform(cameraFrame, controller) { |
| if (cameraFrame && cameraFrame?.codedWidth > 0) { |
| const leftPos = (CANVAS_WIDTH - cameraFrame.displayWidth) / 2; |
| const topPos = (CANVAS_HEIGHT - cameraFrame.displayHeight) / 2; |
| |
| ctx.drawImage(cameraFrame, leftPos, topPos); |
| |
| const newFrame = new VideoFrame(canvas, { |
| timestamp: cameraFrame.timestamp |
| }); |
| cameraFrame.close(); |
| controller.enqueue(newFrame); |
| } |
| } |
| }); |
| |
| readableStream.pipeThrough(transformer).pipeTo(sink); |
| |
| const compositedMediaStream = new MediaStream([composedTrackGenerator]); |
| |
| useMediaRecorder(compositedMediaStream, test.step_func_done(size => { |
| assert_greater_than(size, THRESHOLD_FOR_EMPTY_FRAMES); |
| stopCamera(); |
| })); |
| }); |
| }, "MediaRecorder returns frames containing video content"); |
| |
| </script> |
| </body> |
| |
| </html> |