MediaRecorder-canvas-media-source.https.html (6176B)
1 <!doctype html> 2 <html> 3 <meta name="timeout" content="long"> 4 5 <head> 6 <title>MediaRecorder canvas media source</title> 7 <meta name=variant content="?mimeType=''"> 8 <meta name=variant content="?mimeType=video/webm;codecs=vp8,opus"> 9 <meta name=variant content="?mimeType=video/webm;codecs=vp9,opus"> 10 <meta name=variant content="?mimeType=video/webm;codecs=av1,opus"> 11 <meta name=variant content="?mimeType=video/mp4;codecs=avc1.64003E,mp4a.40.2"> 12 <meta name=variant content="?mimeType=video/mp4;codecs=avc3.64003E,mp4a.40.2"> 13 <meta name=variant content="?mimeType=video/mp4;codecs=vp9,opus"> 14 <meta name=variant content="?mimeType=video/mp4;codecs=av01,opus"> 15 <meta name=variant content="?mimeType=video/mp4;codecs=av01,mp4a.40.2"> 16 <meta name=variant content="?mimeType=video/mp4;codecs=hvc1.1.6.L186.B0,opus"> 17 <meta name=variant content="?mimeType=video/mp4;codecs=hev1.1.6.L186.B0,opus"> 18 <meta name=variant content="?mimeType=video/mp4;codecs=hvc1.1.6.L186.B0,mp4a.40.2"> 19 <meta name=variant content="?mimeType=video/mp4;codecs=hev1.1.6.L186.B0,mp4a.40.2"> 20 <meta name=variant content="?mimeType=video/x-matroska;codecs=hvc1.1.6.L186.B0,opus"> 21 <meta name=variant content="?mimeType=video/x-matroska;codecs=hev1.1.6.L186.B0,opus"> 22 <meta name=variant content="?mimeType=video/mp4"> 23 <link rel="help" 24 href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-mimeType"> 25 <script src="/resources/testharness.js"></script> 26 <script src="/resources/testharnessreport.js"></script> 27 <script src="/resources/testdriver.js"></script> 28 <script src="/resources/testdriver-vendor.js"></script> 29 <script src="../mediacapture-streams/permission-helper.js"></script> 30 </head> 31 32 <body> 33 <script> 34 async function createWorker(script) { 35 script += "self.postMessage('ready');"; 36 const blob = new Blob([script], { type: 'text/javascript' }); 37 const url = URL.createObjectURL(blob); 38 const worker = new Worker(url); 39 await new Promise(resolve => worker.onmessage = resolve); 40 return worker; 41 } 42 43 async_test(test => { 44 const CANVAS_WIDTH = 256; 45 const CANVAS_HEIGHT = 144; 46 47 let large_size_data_available = false; 48 49 // Empty video frames from this resolution consistently have ~750 bytes in my 50 // tests, while valid video frames usually contain 7-8KB. A threshold of 51 // 1.5KB consistently fails when video frames are empty but passes when video 52 // frames are non-empty. 53 const THRESHOLD_FOR_EMPTY_FRAMES = 1500; 54 55 const CAMERA_CONSTRAINTS = { 56 video: { 57 width: { ideal: CANVAS_WIDTH }, 58 height: { ideal: CANVAS_HEIGHT } 59 } 60 }; 61 62 function useUserMedia(constraints) { 63 let activeStream = null; 64 65 function startCamera() { 66 return navigator.mediaDevices.getUserMedia(constraints).then( 67 (stream) => { 68 activeStream = stream; 69 return stream; 70 } 71 ); 72 } 73 74 function stopCamera() { 75 activeStream?.getTracks().forEach((track) => track.stop()); 76 } 77 78 return { startCamera, stopCamera }; 79 } 80 81 function useMediaRecorder(stream, mimeType, frameSizeCallback) { 82 const mediaRecorder = new MediaRecorder( 83 stream, { mimeType } 84 ); 85 86 mediaRecorder.ondataavailable = event => { 87 const {size} = event.data; 88 frameSizeCallback(size); 89 90 if (mediaRecorder.state !== "inactive") { 91 mediaRecorder.stop(); 92 } 93 }; 94 95 mediaRecorder.onstop = event => { 96 assert_equals(large_size_data_available, true), 97 "onstop is called after valid data is available"; 98 }; 99 100 mediaRecorder.start(1000); 101 } 102 103 const params = new URLSearchParams(window.location.search); 104 const mimeType = params.get('mimeType'); 105 if (mimeType && !MediaRecorder.isTypeSupported(mimeType)) { 106 test.done(); 107 return; 108 } 109 110 const {startCamera, stopCamera} = useUserMedia(CAMERA_CONSTRAINTS); 111 startCamera().then(async stream => { 112 const videoTrack = stream.getVideoTracks()[0]; 113 const worker = await createWorker(` 114 const CANVAS_WIDTH = ${CANVAS_WIDTH}; 115 const CANVAS_HEIGHT = ${CANVAS_HEIGHT}; 116 onmessage = e => { 117 const videoTrack = e.data; 118 const { readable: readableStream } = new MediaStreamTrackProcessor({ 119 track: videoTrack 120 }); 121 122 const composedTrackGenerator = new VideoTrackGenerator(); 123 const sink = composedTrackGenerator.writable; 124 125 const canvas = new OffscreenCanvas(CANVAS_WIDTH, CANVAS_HEIGHT); 126 const ctx = canvas.getContext("2d", { 127 alpha: false, 128 }); 129 ctx.fillStyle = "#333"; 130 ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); 131 132 const transformer = new TransformStream({ 133 async transform(cameraFrame, controller) { 134 if (cameraFrame && cameraFrame?.codedWidth > 0) { 135 const leftPos = (CANVAS_WIDTH - cameraFrame.displayWidth) / 2; 136 const topPos = (CANVAS_HEIGHT - cameraFrame.displayHeight) / 2; 137 138 ctx.drawImage(cameraFrame, leftPos, topPos); 139 140 const newFrame = new VideoFrame(canvas, { 141 timestamp: cameraFrame.timestamp 142 }); 143 cameraFrame.close(); 144 controller.enqueue(newFrame); 145 } 146 } 147 }); 148 149 readableStream.pipeThrough(transformer).pipeTo(sink); 150 self.postMessage(composedTrackGenerator.track, [composedTrackGenerator.track]); 151 } 152 `); 153 try { 154 worker.postMessage(videoTrack, [videoTrack]); 155 } catch(e) { 156 test.step_func_done(() => { 157 assert_unreached("MediaStreamTrack transfer not supported"); 158 })(); 159 } 160 const composedTrack = await new Promise((resolve, reject) => { 161 worker.onmessage = e => resolve(e.data); 162 test.step_timeout(() => reject("unable to get composited track"), 2000); 163 }); 164 const compositedMediaStream = new MediaStream([composedTrack]); 165 166 useMediaRecorder(compositedMediaStream, mimeType, (size => { 167 if (size > THRESHOLD_FOR_EMPTY_FRAMES) { 168 large_size_data_available = true; 169 stopCamera(); 170 test.done(); 171 } 172 })); 173 }); 174 }, "MediaRecorder returns frames containing video content"); 175 </script> 176 </body> 177 178 </html>