Bug 1420322 - Test for seamless looping; r?jwwang,padenot draft
authorChun-Min Chang <chun.m.chang@gmail.com>
Tue, 09 Jan 2018 17:44:07 +0800
changeset 717570 b64d5ebb470f01991cddea66090a6c8c978ead4f
parent 703941 f5f03ee9e6abf77964f8dc1b9d69c6ccd3f655fd
child 745309 f02cc274c5c7abae31be9c048221e4e49fe9de70
push id94737
push userbmo:cchang@mozilla.com
push dateTue, 09 Jan 2018 09:44:28 +0000
reviewersjwwang, padenot
bugs1420322
milestone59.0a1
Bug 1420322 - Test for seamless looping; r?jwwang,padenot MozReview-Commit-ID: JJScDcW8IvI
dom/media/test/mochitest.ini
dom/media/test/sine-440-1s.wav
dom/media/test/test_seamless_loop.html
--- a/dom/media/test/mochitest.ini
+++ b/dom/media/test/mochitest.ini
@@ -546,16 +546,17 @@ support-files =
   seek.webm^headers^
   seek-short.webm
   seek-short.webm^headers^
   seek_support.js
   seekLies.sjs
   seek_with_sound.ogg^headers^
   sequential.vtt
   short-cenc.mp4
+  sine-440-1s.wav
   sine.webm
   sine.webm^headers^
   sintel-short-clearkey-subsample-encrypted-audio.webm
   sintel-short-clearkey-subsample-encrypted-audio.webm^headers^
   sintel-short-clearkey-subsample-encrypted-video.webm
   sintel-short-clearkey-subsample-encrypted-video.webm^headers^
   short.mp4
   short.mp4.gz
@@ -1043,16 +1044,18 @@ skip-if = toolkit == 'android' # bug 131
 skip-if = toolkit == 'android' # android(bug 1232305)
 [test_video_dimensions.html]
 skip-if = toolkit == 'android' # bug 1298238, bug 1304535, android(bug 1232305)
 [test_resolution_change.html]
 skip-if = android_version == '19' # bug 1393866
 tags=capturestream
 [test_resume.html]
 skip-if = true # bug 1021673
+[test_seamless_loop.html]
+skip-if = toolkit == 'android' # bug 1242112, android(bug 1232305)
 [test_seek_negative.html]
 skip-if = toolkit == 'android' # bug 1295443, bug 1306787, android(bug 1232305)
 [test_seek_nosrc.html]
 [test_seek_out_of_range.html]
 skip-if = toolkit == 'android' # bug 1299382, android(bug 1232305)
 [test_seek_promise_bug1344357.html]
 skip-if = toolkit == 'android' # bug 1299382, android(bug 1232305)
 [test_seek-1.html]
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1cb3c22fe4898eb1c16a7adea81dc8b7ad85c2e7
GIT binary patch
literal 88278
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
new file mode 100644
--- /dev/null
+++ b/dom/media/test/test_seamless_loop.html
@@ -0,0 +1,182 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<head>
+  <title>Test for seamlee looping</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<div id="content"></div>
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+// ============================================================================
+//   Helpers
+// ============================================================================
+function setupEnvironment() {
+  return new Promise((resolve, reject) => {
+    let prefs = {
+      'set': [
+        // Uncomment below to check if the test fails.
+        // ['media.seamless-looping', false],
+      ]
+    };
+    SpecialPowers.pushPrefEnv(prefs, () => { resolve(); });
+  });
+}
+
+// ============================================================================
+//   Test
+// ============================================================================
+const TIMES = 2;        // The looping time for the test.
+const BUF_SIZE = 4096;  // The buffer size of the ScriptProcessor node.
+const SOURCE = "sine-440-1s.wav";
+
+var seamless = true;    // Set to false if the test fails in seamless check.
+var sourceBuffer;       // The decoded audio buffer used to compare with the
+                        // captured buffer data.
+
+var audioElement = new Audio();
+audioElement.src = SOURCE;
+audioElement.loop = true;
+
+var context = new AudioContext();
+var mediaElementSource = context.createMediaElementSource(audioElement);
+var processor = context.createScriptProcessor(BUF_SIZE, 1, 1);
+mediaElementSource.connect(processor);
+processor.connect(context.destination);
+
+processor.processedLengths = []; // Total number of the processed samples.
+
+// The event handler called when audioprocess event is dispatched to
+// ScriptProcessorNode. In this callback, we repeatedly check if the data
+// after first play matches the original decoded data perfectly.
+processor.onaudioprocess = function(evt) {
+  let inputBuffer = evt.inputBuffer;
+  let outputBuffer = evt.outputBuffer;
+
+  for (let ch = 0; ch < outputBuffer.numberOfChannels; ++ch) {
+    if (processor.processedLengths[ch] === undefined) {
+      processor.processedLengths[ch] = 0;
+    }
+    let sourceData = sourceBuffer.getChannelData(ch);
+
+    let inputData = inputBuffer.getChannelData(ch);
+    let outputData = outputBuffer.getChannelData(ch);
+
+    for (var sample = 0; sample < inputBuffer.length; ++sample, ++processor.processedLengths[ch]) {
+      // Pass through the data.
+      outputData[sample] = inputData[sample];
+
+      // Ignore the check if the it already failed in the previous examination,
+      // or before the audio start looping.
+      //
+      // Note: The inputData will be all 0 at the begining (about 1152 data)
+      // even the sourceData is not 0. Probably we need some time to ready the
+      // audio playback.
+      if (!seamless || processor.processedLengths[ch] < sourceData.length) {
+        continue;
+      }
+
+      // Compare the captured data with the original data.
+      let index = processor.processedLengths[ch] % sourceData.length;
+      if (sourceData[index] != inputData[sample]) {
+        seamless = false;
+      }
+    }
+  }
+};
+
+// Get the decoded audio data
+function decodeAudioData() {
+  return new Promise((resolve, reject) => {
+    let request = new XMLHttpRequest();
+    request.open("GET", SOURCE, true);
+    request.responseType = "arraybuffer";
+    request.onload = function() {
+      var audioData = request.response;
+      context.decodeAudioData(audioData,
+                              (buffer) => { resolve(buffer); },
+                              (e) => { reject(e.err); });
+    }
+    request.send();
+  });
+}
+
+// Numbers of the received events.
+var eventCount = {
+  loop: 0, // This is not standard event. It's counted for test only.
+  play: 0,
+  seeking: 0,
+  seeked: 0,
+};
+
+// Event callback for play, seeking, and seeked.
+function eventCounter(evt) {
+  ++eventCount[evt.type];
+  info(evt.type + ": " + eventCount[evt.type]);
+}
+
+// Event callback for timeupdate.
+function loopCounter(evt) {
+  let ae = evt.target;
+  info("currentTime: " + ae.currentTime);
+  if (loopCounter.lastTime !== undefined &&
+      loopCounter.lastTime > ae.currentTime) {
+    ok(ae.loop, "audio is looped when loop is disable.");
+    ++eventCount.loop;
+    info("Loop " + eventCount.loop + " times.");
+    if (eventCount.loop >= TIMES) {
+      info("Cancel loop.");
+      ae.loop = false;
+    }
+  }
+  loopCounter.lastTime = ae.currentTime;
+}
+
+// Event callback for the end event. It's the endpoint of the test.
+function terminator(evt) {
+  let ae = evt.target;
+  ok(ae.currentTime == ae.duration, "The end time should be same as the duration.");
+  ok(eventCount.play == 1, "The play event must be received only once.");
+  ok(eventCount.seeking == TIMES, "The number of received seeking events must be same as looping-times.");
+  ok(eventCount.seeked == TIMES, "The number of received seeked events must be same as looping-times.");
+  ok(seamless, "The looping is not seamless.")
+
+  context.close();
+  SimpleTest.finish();
+}
+
+function registerEventListeners(audio) {
+  let listeners = {
+    play: eventCounter,
+    timeupdate: loopCounter,
+    seeking: eventCounter,
+    seeked: eventCounter,
+    ended: terminator,
+  };
+
+  for (evt in listeners) {
+    audio.addEventListener(evt, listeners[evt], false);
+  }
+}
+
+setupEnvironment()
+.then(function() {
+  return decodeAudioData();
+})
+.then((buffer) => {
+  sourceBuffer = buffer;
+  registerEventListeners(audioElement);
+  audioElement.play();
+})
+.catch(function(reason) {
+  ok(false, "Error: " + reason);
+  SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
\ No newline at end of file