tor-browser

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

mediastreamaudiosourcenode-from-context-with-different-rate.https.html (15641B)


      1 <!DOCTYPE html>
      2 <html class="a">
      3 <head>
      4 <title>Connecting to MediaStreamAudioSourceNode from nodes in different rates</title>
      5 <script src="/resources/testharness.js"></script>
      6 <script src="/resources/testharnessreport.js"></script>
      7 </head>
      8 <body class="a">
      9 <script>
     10 function createSineWaveInput(rate, frequency = 440) {
     11  const ctx = new AudioContext({ sampleRate: rate });
     12  const osc = ctx.createOscillator();
     13  osc.type = "sine";
     14  osc.frequency.value = frequency;
     15  const dest = new MediaStreamAudioDestinationNode(ctx);
     16  osc.connect(dest);
     17  return { ctx, osc, dest };
     18 }
     19 
     20 async function waitForMessage(detectorNode, eventChecker = null) {
     21  assert_not_equals(
     22    detectorNode.context.state,
     23    "closed",
     24    `state of detector at rate ${detectorNode.context.sampleRate} should not be closed`
     25  );
     26  assert_equals(
     27    detectorNode.port.onmessage,
     28    null,
     29    "port.onmessage should be null before calling waitForMessage."
     30  );
     31 
     32  return new Promise((resolve, reject) => {
     33    let appendix = [];
     34    detectorNode.port.onmessage = (event) => {
     35      if (eventChecker && !eventChecker(event.data)) {
     36        appendix.push(event.data);
     37        return;
     38      }
     39      // Clear the handler after receiving a message.
     40      detectorNode.port.onmessage = null;
     41      resolve({ data: event.data, appendix });
     42    };
     43  });
     44 }
     45 
     46 async function createAudioSource(rate, stream, usage) {
     47  const ctx = new AudioContext({ sampleRate: rate });
     48  const stm =
     49    usage === TRACK_USAGES.CLONED
     50      ? new MediaStream(stream.getTracks().map((track) => track.clone()))
     51      : stream;
     52  const sourceNode = ctx.createMediaStreamSource(stm);
     53  sourceNode.connect(ctx.destination);
     54  return sourceNode;
     55 }
     56 
     57 async function createDetectorNode(ctx) {
     58  await ctx.audioWorklet.addModule("silence-detector.js");
     59  const detectorNode = new AudioWorkletNode(ctx, "silence-detector");
     60  return detectorNode;
     61 }
     62 
     63 // Helper functions to wait for a message, satisfying eventChecker() if provided,
     64 // on each detector node, after performing an action.
     65 async function waitForMessagesAfterAction(
     66  detectorNodes,
     67  action,
     68  eventChecker = null
     69 ) {
     70  const msgPromises = detectorNodes.map((node) =>
     71    waitForMessage(node, eventChecker)
     72  );
     73  await action();
     74  return await Promise.all(msgPromises);
     75 }
     76 
     77 // Helper function to create test pairs and wait for them to become non-silent.
     78 async function createAndStartTestPairs(input, dstRates, usage) {
     79  // Create multiple MediaStreamAudioSourceNodes with different AudioContext sample rates.
     80  const pairs = [];
     81  for (const rate of dstRates) {
     82    const sourceNode = await createAudioSource(
     83      rate,
     84      input.dest.stream,
     85      usage
     86    );
     87    const detectorNode = await createDetectorNode(sourceNode.context);
     88    sourceNode.connect(detectorNode);
     89 
     90    pairs.push({ sourceNode, detectorNode });
     91  }
     92 
     93  // Make sure all detectors are not silent after starting the oscillator.
     94  const msgs = await waitForMessagesAfterAction(
     95    pairs.map((p) => p.detectorNode),
     96    () => input.osc.start(),
     97    (data) => data.isSilentChanged
     98  );
     99  msgs.forEach((msg, i) => {
    100    assert_false(
    101      msg.data.isSilent,
    102      `Detector in context with rate ${pairs[i].detectorNode.context.sampleRate} should not be silent after oscillator starts.`
    103    );
    104  });
    105 
    106  return pairs;
    107 }
    108 
    109 async function ensureAudioSourceIsNotSilent(sourceNode) {
    110  const detectorNode = await createDetectorNode(sourceNode.context);
    111 
    112  const msg = (
    113    await waitForMessagesAfterAction(
    114      [detectorNode],
    115      () => {
    116        sourceNode.connect(detectorNode);
    117      },
    118      (data) => data.isSilentChanged
    119    )
    120  )[0];
    121 
    122  assert_false(
    123    msg.data.isSilent,
    124    `Audio source in context with rate ${sourceNode.context.sampleRate} should not be silent.`
    125  );
    126  detectorNode.disconnect();
    127 }
    128 
    129 // Test template that handles setup, execution, and cleanup
    130 async function setupAndRunTest(tone, srcRate, dstRates, usage, testFn) {
    131  assert_false(
    132    dstRates.includes(srcRate),
    133    "dstRates should not include srcRate."
    134  );
    135 
    136  const input = createSineWaveInput(srcRate, tone);
    137  const pairs = await createAndStartTestPairs(input, dstRates, usage);
    138 
    139  try {
    140    await testFn(input, pairs);
    141  } finally {
    142    // Clean up AudioContexts
    143    for (const { sourceNode } of pairs) {
    144      if (sourceNode.context.state !== "closed") {
    145        await sourceNode.context.close();
    146      }
    147    }
    148    if (input.ctx.state !== "closed") {
    149      await input.ctx.close();
    150    }
    151  }
    152 }
    153 
    154 // Test that closing a single AudioContext stops only that sourceNode, leaving others unaffected.
    155 async function testClosingOneContextStopsOnlyIt(
    156  tone,
    157  srcRate,
    158  dstRates,
    159  usage
    160 ) {
    161  await setupAndRunTest(
    162    tone,
    163    srcRate,
    164    dstRates,
    165    usage,
    166    async (input, pairs) => {
    167      const pairToClose = pairs[0];
    168      const pairsToCheck = pairs.slice(1);
    169 
    170      for (const { sourceNode } of pairsToCheck) {
    171        await ensureAudioSourceIsNotSilent(sourceNode);
    172      }
    173    }
    174  );
    175 }
    176 
    177 // Test that suspending a single AudioContext silences only that detector, leaving others unaffected.
    178 async function testSuspendingOneContextSilencesOnlyIt(
    179  tone,
    180  srcRate,
    181  dstRates,
    182  usage
    183 ) {
    184  await setupAndRunTest(
    185    tone,
    186    srcRate,
    187    dstRates,
    188    usage,
    189    async (input, pairs) => {
    190      const pairToSuspend = pairs[0];
    191      const pairsToCheck = pairs.slice(1);
    192 
    193      await pairToSuspend.sourceNode.context.suspend();
    194 
    195      for (const { sourceNode } of pairsToCheck) {
    196        await ensureAudioSourceIsNotSilent(sourceNode);
    197      }
    198    }
    199  );
    200 }
    201 
    202 // Test that disconnecting a single source node silences only its detector, leaving others unaffected.
    203 async function testDisconnectingOneSourceSilencesOnlyIt(
    204  tone,
    205  srcRate,
    206  dstRates,
    207  usage
    208 ) {
    209  await setupAndRunTest(
    210    tone,
    211    srcRate,
    212    dstRates,
    213    usage,
    214    async (input, pairs) => {
    215      const pairToDisconnect = pairs[0];
    216      const pairsToCheck = pairs.slice(1);
    217 
    218      pairToDisconnect.sourceNode.disconnect();
    219 
    220      for (const { sourceNode } of pairsToCheck) {
    221        await ensureAudioSourceIsNotSilent(sourceNode);
    222      }
    223    }
    224  );
    225 }
    226 
    227 // Test template for operations that silence one MediaStream.
    228 async function testSilencingOneMediaStream(
    229  tone,
    230  srcRate,
    231  dstRates,
    232  usage,
    233  stmOp,
    234  silenceChecker = null
    235 ) {
    236  await setupAndRunTest(
    237    tone,
    238    srcRate,
    239    dstRates,
    240    usage,
    241    async (input, pairs) => {
    242      const pairToOperate = pairs[0];
    243 
    244      // Determine which pairs should become silent based on track usage
    245      const pairsToBecomeSilent =
    246        usage === TRACK_USAGES.CLONED ? [pairToOperate] : pairs;
    247      const pairsToRemainActive =
    248        usage === TRACK_USAGES.CLONED ? pairs.slice(1) : [];
    249 
    250      // Perform the operation and wait for expected detectors to become silent
    251      const msgs = await waitForMessagesAfterAction(
    252        pairsToBecomeSilent.map((p) => p.detectorNode),
    253        async () => {
    254          await stmOp(pairToOperate.sourceNode.mediaStream);
    255        }
    256      );
    257 
    258      // Verify detectors lost their input
    259      msgs.forEach((msg, i) => {
    260        const detectorNode = pairsToBecomeSilent[i].detectorNode;
    261        silenceChecker
    262          ? silenceChecker(msg, detectorNode)
    263          : assert_true(
    264              !msg.data.hasInput,
    265              `Detector in context with rate ${detectorNode.context.sampleRate} should have no input after operation.`
    266            );
    267      });
    268 
    269      // Verify remaining detectors are still active
    270      for (const { sourceNode } of pairsToRemainActive) {
    271        await ensureAudioSourceIsNotSilent(sourceNode);
    272      }
    273    }
    274  );
    275 }
    276 
    277 // Test the effect of stopping MediaStreamTracks on detectors.
    278 // With cloned tracks: stopping one stream affects only its corresponding detector.
    279 // With shared tracks: stopping makes all detectors lose their inputs.
    280 async function testStoppingOneMediaStream(tone, srcRate, dstRates, usage) {
    281  await testSilencingOneMediaStream(
    282    tone,
    283    srcRate,
    284    dstRates,
    285    usage,
    286    async (stream) => {
    287      stream.getTracks().forEach((track) => {
    288        track.stop();
    289      });
    290    }
    291  );
    292 }
    293 
    294 // Test that disabling a MediaStream's tracks affects either just its own detector or all detectors.
    295 // With cloned tracks: disabling affects only its corresponding detector.
    296 // With shared tracks: disabling affects all detectors.
    297 async function testDisablingOneMediaStream(tone, srcRate, dstRates, usage) {
    298  await testSilencingOneMediaStream(
    299    tone,
    300    srcRate,
    301    dstRates,
    302    usage,
    303    async (stream) => {
    304      stream.getTracks().forEach((track) => {
    305        track.enabled = false;
    306      });
    307    },
    308    // Disabling tracks should make detectors lose input:
    309    // https://bugzilla.mozilla.org/show_bug.cgi?id=2005070
    310    async (msg, detectorNode) => {
    311      assert_true(
    312        msg.data.isSilent,
    313        `Detector in context with rate ${detectorNode.context.sampleRate} should be silent after disabling tracks.`
    314      );
    315    }
    316  );
    317 }
    318 
    319 // Test that removing tracks from a single MediaStream affects only that detector, leaving others unaffected.
    320 async function testRemovingTracksInOneMediaStream(
    321  tone,
    322  srcRate,
    323  dstRates,
    324  usage
    325 ) {
    326  await setupAndRunTest(
    327    tone,
    328    srcRate,
    329    dstRates,
    330    usage,
    331    async (input, pairs) => {
    332      const pairToModify = pairs[0];
    333      const pairsToCheck = pairs.slice(1);
    334 
    335      const tracks = pairToModify.sourceNode.mediaStream.getTracks();
    336      tracks.forEach((track) => {
    337        pairToModify.sourceNode.mediaStream.removeTrack(track);
    338      });
    339 
    340      for (const { sourceNode } of pairsToCheck) {
    341        await ensureAudioSourceIsNotSilent(sourceNode);
    342      }
    343    }
    344  );
    345 }
    346 
    347 // Test the impact of stopping the original input stream on detectors.
    348 // When tracks are cloned: detectors continue to receive audio.
    349 // When tracks are shared: detectors lose their input or become silent after stopping the source stream.
    350 async function testStoppingInputStream(tone, srcRate, dstRates, usage) {
    351  await setupAndRunTest(
    352    tone,
    353    srcRate,
    354    dstRates,
    355    usage,
    356    async (input, pairs) => {
    357      const stopInputStream = () => {
    358        input.dest.stream.getTracks().forEach((track) => track.stop());
    359      };
    360 
    361      if (usage === TRACK_USAGES.CLONED) {
    362        stopInputStream();
    363        for (const { sourceNode } of pairs) {
    364          await ensureAudioSourceIsNotSilent(sourceNode);
    365        }
    366      } else {
    367        const msgs = await waitForMessagesAfterAction(
    368          pairs.map((p) => p.detectorNode),
    369          stopInputStream
    370        );
    371        msgs.forEach((msg, i) => {
    372          assert_true(
    373            msg.data.isSilent || !msg.data.hasInput,
    374            `Detector in context with rate ${pairs[i].detectorNode.context.sampleRate} should be silent or have no input after stopping the shared input stream.`
    375          );
    376        });
    377      }
    378    }
    379  );
    380 }
    381 
    382 // Test that suspending the input AudioContext silences all detectors.
    383 async function testSuspendingInputContextSilencesAll(
    384  tone,
    385  srcRate,
    386  dstRates,
    387  usage
    388 ) {
    389  await setupAndRunTest(
    390    tone,
    391    srcRate,
    392    dstRates,
    393    usage,
    394    async (input, pairs) => {
    395      const msgs = await waitForMessagesAfterAction(
    396        pairs.map((p) => p.detectorNode),
    397        async () => {
    398          await input.ctx.suspend();
    399        }
    400      );
    401      msgs.forEach((msg, i) => {
    402        assert_true(
    403          msg.data.isSilent,
    404          `Detector in context with rate ${pairs[i].detectorNode.context.sampleRate} should be silent after suspending the input context.`
    405        );
    406      });
    407    }
    408  );
    409 }
    410 
    411 // Test that closing the input AudioContext silences all detectors.
    412 async function testClosingInputContextSilencesAll(
    413  tone,
    414  srcRate,
    415  dstRates,
    416  usage
    417 ) {
    418  await setupAndRunTest(
    419    tone,
    420    srcRate,
    421    dstRates,
    422    usage,
    423    async (input, pairs) => {
    424      const msgs = await waitForMessagesAfterAction(
    425        pairs.map((p) => p.detectorNode),
    426        async () => {
    427          await input.ctx.close();
    428        }
    429      );
    430      msgs.forEach((msg, i) => {
    431        assert_true(
    432          msg.data.isSilent,
    433          `Detector in context with rate ${pairs[i].detectorNode.context.sampleRate} should be silent after closing the input context.`
    434        );
    435      });
    436    }
    437  );
    438 }
    439 
    440 const SOURCE_CONTEXT_RATE = 48000;
    441 const DEST_CONTEXT_RATES = [32000, 44100, 96000];
    442 
    443 const TRACK_USAGES = {
    444  CLONED: "cloned",
    445  SHARED: "shared",
    446 };
    447 const SCENARIOS = [
    448  { trackUsage: TRACK_USAGES.CLONED, description: "cloned tracks" },
    449  { trackUsage: TRACK_USAGES.SHARED, description: "shared tracks" },
    450 ];
    451 
    452 const NOTE_FREQUENCIES = {
    453  C4: 261.63,
    454  D4: 293.66,
    455  E4: 329.63,
    456  F4: 349.23,
    457  G4: 392.00,
    458  A4: 440.00,
    459  B4: 493.88,
    460  C5: 523.25,
    461  D5: 587.33,
    462  E5: 659.25,
    463 };
    464 
    465 for (const { trackUsage, description } of SCENARIOS) {
    466  // Tests for connection from one source context to multiple destination contexts with different sample rates.
    467 
    468  promise_test(async t => {
    469    await testClosingOneContextStopsOnlyIt(NOTE_FREQUENCIES.C4, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage);
    470  }, `Test closing one AudioContext stops only it (${description})`);
    471 
    472  promise_test(async t => {
    473    await testSuspendingOneContextSilencesOnlyIt(NOTE_FREQUENCIES.D4, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage);
    474  }, `Test suspending one AudioContext silences only it (${description})`);
    475 
    476  promise_test(async t => {
    477    await testDisconnectingOneSourceSilencesOnlyIt(NOTE_FREQUENCIES.E4, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage);
    478  }, `Test disconnecting one MediaStreamAudioSourceNode silences only it (${description})`);
    479 
    480  promise_test(async t => {
    481    await testStoppingOneMediaStream(NOTE_FREQUENCIES.F4, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage);
    482  }, `Test stopping one MediaStream's tracks silences ${trackUsage === TRACK_USAGES.CLONED ? "only its detector" : "all detectors"} (${description})`);
    483 
    484  promise_test(async t => {
    485    await testDisablingOneMediaStream(NOTE_FREQUENCIES.G4, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage);
    486  }, `Test disabling one MediaStream's tracks silences ${trackUsage === TRACK_USAGES.CLONED ? "only its detector" : "all detectors"} (${description})`);
    487 
    488  promise_test(async t => {
    489    await testRemovingTracksInOneMediaStream(NOTE_FREQUENCIES.A4, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage);
    490  }, `Test removing tracks from one MediaStream silences only it} (${description})`);
    491 
    492  promise_test(async t => {
    493    await testStoppingInputStream(NOTE_FREQUENCIES.B4, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage);
    494  }, `Test stopping the input MediaStream's tracks silences ${trackUsage === TRACK_USAGES.CLONED ? "nothing" : "all detectors"} (${description})`);
    495 
    496  promise_test(async t => {
    497    await testSuspendingInputContextSilencesAll(NOTE_FREQUENCIES.C5, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage);
    498  }, `Test suspending the input AudioContext silences all detectors (${description})`);
    499 
    500  promise_test(async t => {
    501    await testClosingInputContextSilencesAll(NOTE_FREQUENCIES.D5, SOURCE_CONTEXT_RATE, DEST_CONTEXT_RATES, trackUsage);
    502  }, `Test closing the input AudioContext silences all detectors (${description})`);
    503 
    504  // Tests for one source context to multiple destination contexts with identical sample rates.
    505 
    506  const dstRates = [DEST_CONTEXT_RATES[0], DEST_CONTEXT_RATES[0]];
    507  promise_test(async t => {
    508    await testRemovingTracksInOneMediaStream(NOTE_FREQUENCIES.E5, SOURCE_CONTEXT_RATE, dstRates, trackUsage);
    509  }, `Test removing tracks from one MediaStream silences only its detector when destination rates are the same (${description}, ${SOURCE_CONTEXT_RATE}->${dstRates[0]})`);
    510 }
    511 
    512 </script>
    513 </body>
    514 </html>