tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>