Bug 1032848 - Part 3: Add tests for HTMLCanvasElement::CaptureStream. r=mt, r=jgilbert, r=jesup
authorAndreas Pehrson <pehrsons@gmail.com>
Wed, 13 May 2015 14:04:51 +0800
changeset 275060 62c3b79f3f288f27680765d05d68f496645209e0
parent 275059 fc943ddde1c4d7fca5eb951e928ae4d9dba427cf
child 275061 e5d045d49b40e951dd3b90fcaafa1a71341ae599
push id4932
push userjlund@mozilla.com
push dateMon, 10 Aug 2015 18:23:06 +0000
treeherdermozilla-beta@6dd5a4f5f745 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmt, jgilbert, jesup
bugs1032848
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1032848 - Part 3: Add tests for HTMLCanvasElement::CaptureStream. r=mt, r=jgilbert, r=jesup
dom/canvas/test/captureStream_common.js
dom/canvas/test/crossorigin/test_canvas2d_crossorigin.html
dom/canvas/test/crossorigin/test_video_crossorigin.html
dom/canvas/test/image_red_crossorigin_credentials.png
dom/canvas/test/image_red_crossorigin_credentials.png^headers^
dom/canvas/test/mochitest.ini
dom/canvas/test/test_capture.html
dom/canvas/test/webgl-mochitest.ini
dom/canvas/test/webgl-mochitest/test_capture.html
dom/canvas/test/webgl-mochitest/webgl-util.js
dom/media/tests/mochitest/head.js
dom/media/tests/mochitest/mochitest.ini
dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_2d.html
dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_webgl.html
new file mode 100644
--- /dev/null
+++ b/dom/canvas/test/captureStream_common.js
@@ -0,0 +1,174 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/*
+ * Util base class to help test a captured canvas element. Initializes the
+ * output canvas (used for testing the color of video elements), and optionally
+ * overrides the default element |width| and |height|.
+ */
+function CaptureStreamTestHelper(width, height) {
+  this.cout = document.createElement('canvas');
+  if (width) {
+    this.elemWidth = width;
+  }
+  if (height) {
+    this.elemHeight = height;
+  }
+  this.cout.width = this.elemWidth;
+  this.cout.height = this.elemHeight;
+  document.body.appendChild(this.cout);
+}
+
+CaptureStreamTestHelper.prototype = {
+  /* Predefined colors for use in the methods below. */
+  black: { data: [0, 0, 0, 255], name: "black" },
+  green: { data: [0, 255, 0, 255], name: "green" },
+  red: { data: [255, 0, 0, 255], name: "red" },
+
+  /* Default element size for createAndAppendElement() */
+  elemWidth: 100,
+  elemHeight: 100,
+
+  /* Request a frame from the stream played by |video|. */
+  requestFrame: function (video) {
+    info("Requesting frame from " + video.id);
+    video.mozSrcObject.requestFrame();
+  },
+
+  /* Tests the top left pixel of |video| against |refData|. Format [R,G,B,A]. */
+  testPixel: function (video, refData, threshold) {
+    var ctxout = this.cout.getContext('2d');
+    ctxout.drawImage(video, 0, 0);
+    var pixel = ctxout.getImageData(0, 0, 1, 1).data;
+    return pixel.every((val, i) => Math.abs(val - refData[i]) <= threshold);
+  },
+
+  /*
+   * Returns a promise that resolves when the pixel matches. Use |threshold|
+   * for fuzzy matching the color on each channel, in the range [0,255].
+   */
+  waitForPixel: function (video, refColor, threshold, infoString) {
+    return new Promise(resolve => {
+      info("Testing " + video.id + " against [" + refColor.data.join(',') + "]");
+      CaptureStreamTestHelper2D.prototype.clear.call(this, this.cout);
+      video.ontimeupdate = () => {
+        if (this.testPixel(video, refColor.data, threshold)) {
+          ok(true, video.id + " " + infoString);
+          video.ontimeupdate = null;
+          resolve();
+        }
+      };
+    });
+  },
+
+  /*
+   * Returns a promise that resolves after |timeout| ms of playback or when a
+   * pixel of |video| becomes the color |refData|. The test is failed if the
+   * timeout is not reached.
+   */
+  waitForPixelToTimeout: function (video, refColor, threshold, timeout, infoString) {
+    return new Promise(resolve => {
+      info("Waiting for " + video.id + " to time out after " + timeout +
+           "ms against [" + refColor.data.join(',') + "] - " + refColor.name);
+      CaptureStreamTestHelper2D.prototype.clear.call(this, this.cout);
+      var startTime = video.currentTime;
+      video.ontimeupdate = () => {
+        if (this.testPixel(video, refColor.data, threshold)) {
+          ok(false, video.id + " " + infoString);
+          video.ontimeupdate = null;
+          resolve();
+        } else if (video.currentTime > startTime + (timeout / 1000.0)) {
+          ok(true, video.id + " " + infoString);
+          video.ontimeupdate = null;
+          resolve();
+        }
+      };
+    });
+  },
+
+  /* Create an element of type |type| with id |id| and append it to the body. */
+  createAndAppendElement: function (type, id) {
+    var e = document.createElement(type);
+    e.id = id;
+    e.width = this.elemWidth;
+    e.height = this.elemHeight;
+    if (type === 'video') {
+      e.autoplay = true;
+    }
+    document.body.appendChild(e);
+    return e;
+  },
+}
+
+/* Sub class holding 2D-Canvas specific helpers. */
+function CaptureStreamTestHelper2D(width, height) {
+  CaptureStreamTestHelper.call(this, width, height);
+}
+
+CaptureStreamTestHelper2D.prototype = Object.create(CaptureStreamTestHelper.prototype);
+CaptureStreamTestHelper2D.prototype.constructor = CaptureStreamTestHelper2D;
+
+/* Clear all drawn content on |canvas|. */
+CaptureStreamTestHelper2D.prototype.clear = function(canvas) {
+  var ctx = canvas.getContext('2d');
+  ctx.clearRect(0, 0, canvas.width, canvas.height);
+};
+
+/* Draw the color |color| to the source canvas |canvas|. Format [R,G,B,A]. */
+CaptureStreamTestHelper2D.prototype.drawColor = function(canvas, color) {
+  var ctx = canvas.getContext('2d');
+  var rgba = color.data.slice(); // Copy to not overwrite the original array
+  info("Drawing color " + rgba.join(','));
+  rgba[3] = rgba[3] / 255.0; // Convert opacity to double in range [0,1]
+  ctx.fillStyle = "rgba(" + rgba.join(',') + ")";
+
+  // Only fill top left corner to test that output is not flipped or rotated.
+  ctx.fillRect(0, 0, canvas.width / 2, canvas.height / 2);
+};
+
+/* Test that the given 2d canvas is NOT origin-clean. */
+CaptureStreamTestHelper2D.prototype.testNotClean = function(canvas) {
+  var ctx = canvas.getContext('2d');
+  var error = "OK";
+  try {
+    var data = ctx.getImageData(0, 0, 1, 1);
+  } catch(e) {
+    error = e.name;
+  }
+  is(error, "SecurityError",
+     "Canvas '" + canvas.id + "' should not be origin-clean");
+};
+
+/* Sub class holding WebGL specific helpers. */
+function CaptureStreamTestHelperWebGL(width, height) {
+  CaptureStreamTestHelper.call(this, width, height);
+}
+
+CaptureStreamTestHelperWebGL.prototype = Object.create(CaptureStreamTestHelper.prototype);
+CaptureStreamTestHelperWebGL.prototype.constructor = CaptureStreamTestHelperWebGL;
+
+/* Set the (uniform) color location for future draw calls. */
+CaptureStreamTestHelperWebGL.prototype.setFragmentColorLocation = function(colorLocation) {
+  this.colorLocation = colorLocation;
+};
+
+/* Clear the given WebGL context with |color|. */
+CaptureStreamTestHelperWebGL.prototype.clearColor = function(canvas, color) {
+  info("WebGL: clearColor(" + color.name + ")");
+  var gl = canvas.getContext('webgl');
+  var conv = color.data.map(i => i / 255.0);
+  gl.clearColor(conv[0], conv[1], conv[2], conv[3]);
+  gl.clear(gl.COLOR_BUFFER_BIT);
+};
+
+/* Set an already setFragmentColorLocation() to |color| and drawArrays() */
+CaptureStreamTestHelperWebGL.prototype.drawColor = function(canvas, color) {
+  info("WebGL: drawArrays(" + color.name + ")");
+  var gl = canvas.getContext('webgl');
+  var conv = color.data.map(i => i / 255.0);
+  gl.uniform4f(this.colorLocation, conv[0], conv[1], conv[2], conv[3]);
+  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+};
--- a/dom/canvas/test/crossorigin/test_canvas2d_crossorigin.html
+++ b/dom/canvas/test/crossorigin/test_canvas2d_crossorigin.html
@@ -68,16 +68,27 @@ function testImage(url, crossOriginAttri
     } catch (e) {
       actual_error = e.name;
     }
 
     verifyError(actual_error, expected_error,
                 "drawImage then get image data on " + url +
                 " with crossOrigin=" + this.crossOrigin);
 
+    try {
+      c.captureStream(0);
+      actual_error = OK;
+    } catch (e) {
+      actual_error = e.name;
+    }
+
+    verifyError(actual_error, expected_error,
+                "drawImage then capture stream on " + url +
+                " with crossOrigin=" + this.crossOrigin);
+
     // Now test patterns
     c = document.createElement("canvas");
     c.width = this.width;
     c.height = this.height;
     ctx = c.getContext("2d");
     ctx.fillStyle = ctx.createPattern(this, "");
     ctx.fillRect(0, 0, c.width, c.height);
     try {
@@ -86,16 +97,27 @@ function testImage(url, crossOriginAttri
     } catch (e) {
       actual_error = e.name;
     }
 
     verifyError(actual_error, expected_error,
                 "createPattern+fill then get image data on " + url +
                 " with crossOrigin=" + this.crossOrigin);
 
+    try {
+      c.captureStream(0);
+      actual_error = OK;
+    } catch (e) {
+      actual_error = e.name;
+    }
+
+    verifyError(actual_error, expected_error,
+                "createPattern+fill then capture stream on " + url +
+                " with crossOrigin=" + this.crossOrigin);
+
     testDone();
   };
 
   image.onerror = function(event) {
     verifyError(BAD_URI_ERR, expected_error,
                 "image error handler for " + url +
                 " with crossOrigin=" + this.crossOrigin);
 
@@ -125,62 +147,70 @@ const attrValues = [
   [ "missing-value-default", "none" ],
   [ "", "anonymous" ],
   [ "just-crossOrigin-without-value", "anonymous" ],
   [ "anonymous", "anonymous" ],
   [ "use-credentials", "use-credentials" ],
   [ "foobar", "anonymous" ]
 ];
 
-for (var imgIdx = 0; imgIdx < imageFiles.length; ++imgIdx) {
-  for (var hostnameIdx = 0; hostnameIdx < hostnames.length; ++hostnameIdx) {
-    var hostnameData = hostnames[hostnameIdx];
-    var url = "http://" + hostnameData[0] + testPath + imageFiles[imgIdx][0];
-    for (var attrValIdx = 0; attrValIdx < attrValues.length; ++attrValIdx) {
-      var attrValData = attrValues[attrValIdx];
-      // Now compute the expected result
-      var expected_error;
-      if (hostnameData[1] == "same-origin") {
-        // Same-origin; these should all Just Work
-        expected_error = OK;
-      } else {
-        // Cross-origin
-        is(hostnameData[1], "cross-origin",
-           "what sort of host is " + hostnameData[0]);
-        var CORSMode = attrValData[1];
-        if (CORSMode == "none") {
-          // Doesn't matter what headers the server sends; we're not
-          // using CORS on our end.
-          expected_error = "SecurityError";
+function beginTest() {
+  for (var imgIdx = 0; imgIdx < imageFiles.length; ++imgIdx) {
+    for (var hostnameIdx = 0; hostnameIdx < hostnames.length; ++hostnameIdx) {
+      var hostnameData = hostnames[hostnameIdx];
+      var url = "http://" + hostnameData[0] + testPath + imageFiles[imgIdx][0];
+      for (var attrValIdx = 0; attrValIdx < attrValues.length; ++attrValIdx) {
+        var attrValData = attrValues[attrValIdx];
+        // Now compute the expected result
+        var expected_error;
+        if (hostnameData[1] == "same-origin") {
+          // Same-origin; these should all Just Work
+          expected_error = OK;
         } else {
-          // Check whether the server will let us talk to them
-          var CORSHeaders = imageFiles[imgIdx][1];
-          // We're going to look for CORS headers from the server
-          if (CORSHeaders == "none") {
-            // No CORS headers from server; load will fail.
-            expected_error = BAD_URI_ERR;
-          } else if (CORSHeaders == "allow-all-anon") {
-            // Server only allows anonymous requests
-            if (CORSMode == "anonymous") {
-              expected_error = OK;
+          // Cross-origin
+          is(hostnameData[1], "cross-origin",
+             "what sort of host is " + hostnameData[0]);
+          var CORSMode = attrValData[1];
+          if (CORSMode == "none") {
+            // Doesn't matter what headers the server sends; we're not
+            // using CORS on our end.
+            expected_error = "SecurityError";
+          } else {
+            // Check whether the server will let us talk to them
+            var CORSHeaders = imageFiles[imgIdx][1];
+            // We're going to look for CORS headers from the server
+            if (CORSHeaders == "none") {
+              // No CORS headers from server; load will fail.
+              expected_error = BAD_URI_ERR;
+            } else if (CORSHeaders == "allow-all-anon") {
+              // Server only allows anonymous requests
+              if (CORSMode == "anonymous") {
+                expected_error = OK;
+              } else {
+                is(CORSMode, "use-credentials",
+                   "What other CORS modes are there?");
+                // A load with credentials against a server that only
+                // allows anonymous loads will fail.
+                expected_error = BAD_URI_ERR;
+              }
             } else {
-              is(CORSMode, "use-credentials",
-                 "What other CORS modes are there?");
-              // A load with credentials against a server that only
-              // allows anonymous loads will fail.
-              expected_error = BAD_URI_ERR;
+              is(CORSHeaders, "allow-single-server-creds",
+                 "What other CORS headers could there be?");
+              // Our server should allow both anonymous and non-anonymous requests
+              expected_error = OK;
             }
-          } else {
-            is(CORSHeaders, "allow-single-server-creds",
-               "What other CORS headers could there be?");
-            // Our server should allow both anonymous and non-anonymous requests
-            expected_error = OK;
           }
         }
+        testImage(url, attrValData[0], expected_error);
       }
-      testImage(url, attrValData[0], expected_error);
     }
   }
 }
+
+var prefs = [
+  [ "canvas.capturestream.enabled", true ],
+];
+SpecialPowers.pushPrefEnv({ "set" : prefs }, beginTest);
+
 </script>
 </pre>
 </body>
 </html>
--- a/dom/canvas/test/crossorigin/test_video_crossorigin.html
+++ b/dom/canvas/test/crossorigin/test_video_crossorigin.html
@@ -23,46 +23,74 @@ SimpleTest.requestFlakyTimeout("untriage
 
 function createCanvas(width, height) {
   var c = document.createElement("canvas");
   c.width = width;
   c.height = height;
   return c;
 }
 
-function testCanvasDrawImage(v) {
-  var c = createCanvas(v.width, v.height);
-  var ctx = c.getContext("2d");
-  ctx.drawImage(v, 0, 0);
-
+function checkGetImageData(ctx, v) {
   try {
     var data = ctx.getImageData(0, 0, 1, 1);
     ok(true, "drawImage '" + v.src + "' then getImageData with crossOrigin='" + v.crossOrigin + "' worked");
   } catch(error) {
     ok(!v.crossOrigin && error.name === "SecurityError", "drawImage '" + v.src + "' then getImageData with crossOrigin='" + v.crossOrigin + "' failed");
     v.tainted = true;
   }
 }
 
+function checkGetImageDataTainted(ctx, v) {
+  try {
+    var data = ctx.getImageData(0, 0, 1, 1);
+    ok(false, "changing the CORS mode should not allow reading data from remote videos");
+  } catch (error) {
+    ok(error.name === "SecurityError", "changing the CORS mode, drawImage '" + v.src + "' then getImageData with crossOrigin='" + v.crossOrigin + "' failed");
+  }
+}
+
+function checkCaptureStream(c, v) {
+  try {
+    var stream = c.captureStream(0);
+    ok(true, "drawImage '" + v.src + "' then captureStream with crossOrigin='" + v.crossOrigin + "' worked");
+  } catch(error) {
+    ok(!v.crossOrigin && error.name === "SecurityError", "drawImage '" + v.src + "' then captureStream with crossOrigin='" + v.crossOrigin + "' failed");
+    v.tainted = true;
+  }
+}
+
+function checkCaptureStreamTainted(c, v) {
+  try {
+    var stream = c.captureStream(0);
+    ok(false, "changing the CORS mode should not allow capturing a stream from remote videos");
+  } catch (error) {
+    ok(error.name === "SecurityError", "changing the CORS mode, drawImage '" + v.src + "' then captureStream with crossOrigin='" + v.crossOrigin + "' failed");
+  }
+}
+
+function testCanvasDrawImage(v) {
+  var c = createCanvas(v.width, v.height);
+  var ctx = c.getContext("2d");
+  ctx.drawImage(v, 0, 0);
+
+  checkGetImageData(ctx, v);
+  checkCaptureStream(c, v);
+}
+
 function testCanvasCreatePattern(v) {
   var c = createCanvas(v.width, v.height);
   var ctx = c.getContext("2d");
   ctx.fillStyle = ctx.createPattern(v, "");
   ctx.fillRect(0, 0, c.width, c.height);
 
-  try {
-    var data = ctx.getImageData(0, 0, 1, 1);
-    ok(true, "createPattern '" + v.src + "' then getImageData with crossOrigin='" + v.crossOrigin + "' worked");
-  } catch(error) {
-    ok(!v.crossOrigin && error.name === "SecurityError", "createPattern '" + v.src + "' then getImageData with crossOrigin='" + v.crossOrigin + "' failed");
-    v.tainted = true;
-  }
+  checkGetImageData(ctx, v);
+  checkCaptureStream(c, v);
 }
 
-function testWebGL(v) {
+function testWebGL(gl, v) {
   var tex = gl.createTexture();
   gl.bindTexture(gl.TEXTURE_2D, tex);
 
   try {
     gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, v);
     ok(true, "createTexture from '" + v.src + "' with crossOrigin='" + v.crossOrigin + "' worked");
   } catch (error) {
     ok(!v.crossOrigin && error.name === "SecurityError", "createTexture from '" + v.src + "' with crossOrigin='" + v.crossOrigin + "' failed");
@@ -70,31 +98,27 @@ function testWebGL(v) {
   }
 }
 
 function testTaintedCanvas(v) {
   var c = createCanvas(v.width, v.height);
   var ctx = c.getContext("2d");
   ctx.drawImage(v, 0, 0);
 
-  try {
-    var data = ctx.getImageData(0, 0, 1, 1);
-    ok(false, "changing the CORS mode should not allow reading data from remote videos");
-  } catch (error) {
-    ok(error.name === "SecurityError", "changing the CORS mode, drawImage '" + v.src + "' then getImageData with crossOrigin='" + v.crossOrigin + "' failed");
-  }
+  checkGetImageDataTainted(ctx, v);
+  checkCaptureStreamTainted(c, v);
 }
 
 function vidDataSuccess(e) {
   ok(!e.target.error, "Load '" + e.target.src + "' with crossOrigin='" + e.target.crossOrigin + "'");
 
   testCanvasDrawImage(e.target);
   testCanvasCreatePattern(e.target);
-  if (gl) {
-    testWebGL(e.target);
+  if (document.gl) {
+    testWebGL(document.gl, e.target);
   }
   // If we change the CORS mode after loading the file without CORS it should still throw a security error
   if (e.target.tainted) {
     e.target.crossOrigin = "anonymous";
     testTaintedCanvas(e.target);
   }
 
   doneTest(e);
@@ -116,17 +140,17 @@ function startTest(test, token) {
   if (test.cors === "just-crossOrigin-without-value") {
     var div = document.createElement('div');
     div.innerHTML="<video crossOrigin>";
     v = div.children[0];
   } else if (test.cors !== "missing-value-default") {
     v.crossOrigin = test.cors;
   }
   v.token = token;
-  manager.started(token);
+  document.manager.started(token);
   v.autoplay = true;
   v.preload = "auto";
   v.style.display = "none";
   if (test.nameIntent === test.corsIntent || test.corsIntent === "none" ||
       (test.nameIntent === "use-credentials" && test.corsIntent === "anonymous")) {
     v.addEventListener("loadeddata", vidDataSuccess, false);
     v.addEventListener("error", vidLoadFailure, false);
   } else {
@@ -135,60 +159,66 @@ function startTest(test, token) {
   }
   v.src = test.name;
   document.body.appendChild(v);
 }
 
 function doneTest(e) {
   var v = e.target;
   v.parentNode.removeChild(v);
-  manager.finished(v.token);
-}
-
-var videoFile = getPlayableVideo(gSmallTests);
-if (!videoFile) {
-  SimpleTest.finish();
-}
-videoFile = "?name=tests/dom/media/test/" + videoFile.name + "&type=" + videoFile.type;
-
-var gl;
-try {
-  gl = createCanvas(16, 16).getContext("experimental-webgl");
-} catch (ex) {
-  // Mac OS X 10.5 doesn't support WebGL, so we won't run the WebGL tests
+  document.manager.finished(v.token);
 }
 
-var manager = new MediaTestManager;
-var corsTests = [];
+function beginTest() {
+  var videoFile = getPlayableVideo(gSmallTests);
+  if (!videoFile) {
+    SimpleTest.finish();
+  }
+  videoFile = "?name=tests/dom/media/test/" + videoFile.name + "&type=" + videoFile.type;
+
+  document.manager = new MediaTestManager;
+  var corsTests = [];
 
-const host = "http://example.com/tests/dom/canvas/test/crossorigin/video.sjs";
-const serverAttrValues = [
-  [ "&cors=none", "none" ],
-  [ "&cors=anonymous", "anonymous" ],
-  [ "&cors=use-credentials", "use-credentials" ]
-];
-const clientAttrValues = [
-  [ "missing-value-default", "none" ],
-  [ "", "anonymous" ],
-  [ "just-crossOrigin-without-value", "anonymous" ],
-  [ "anonymous", "anonymous" ],
-  [ "use-credentials", "use-credentials" ],
-  [ "foobar", "anonymous" ]
+  const host = "http://example.com/tests/dom/canvas/test/crossorigin/video.sjs";
+  const serverAttrValues = [
+    [ "&cors=none", "none" ],
+    [ "&cors=anonymous", "anonymous" ],
+    [ "&cors=use-credentials", "use-credentials" ]
+  ];
+  const clientAttrValues = [
+    [ "missing-value-default", "none" ],
+    [ "", "anonymous" ],
+    [ "just-crossOrigin-without-value", "anonymous" ],
+    [ "anonymous", "anonymous" ],
+    [ "use-credentials", "use-credentials" ],
+    [ "foobar", "anonymous" ]
+  ];
+
+  // Build the video file test array
+  for (var i = 0; i < serverAttrValues.length; i++) {
+    for (var n = 0; n < clientAttrValues.length; n++) {
+      corsTests.push({
+        name: host + videoFile + serverAttrValues[i][0],
+        nameIntent: serverAttrValues[i][1],
+        cors: clientAttrValues[n][0],
+        corsIntent: clientAttrValues[n][1]
+      });
+    }
+  }
+  try {
+    document.gl = createCanvas(16, 16).getContext("experimental-webgl");
+  } catch (ex) {
+    // Mac OS X 10.5 doesn't support WebGL, so we won't run the WebGL tests
+  }
+  document.manager.runTests(corsTests, startTest);
+}
+
+var prefs = [
+  [ "canvas.capturestream.enabled", true ],
 ];
 
-// Build the video file test array
-for (var i = 0; i < serverAttrValues.length; i++) {
-	for (var n = 0; n < clientAttrValues.length; n++) {
-		corsTests.push({
-      name: host + videoFile + serverAttrValues[i][0],
-      nameIntent: serverAttrValues[i][1],
-      cors: clientAttrValues[n][0],
-      corsIntent: clientAttrValues[n][1]
-    });
-	}
-}
-
-manager.runTests(corsTests, startTest);
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({ "set" : prefs }, beginTest);
 
 </script>
 </pre>
 </body>
 </html>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a6e195d59cce3c15ef12512bb58ed4f409eb95ae
GIT binary patch
literal 87
zc%17D@N?(olHy`uVBq!ia0vp^DL`z*$P6SW{C@KnNHGWagt-1^V32&oX%6J_d%8G=
jXow~!NU$zW$Y5e%+QYyoefRMMpcI3rtDnm{r-UW|6k!xK
new file mode 100644
--- /dev/null
+++ b/dom/canvas/test/image_red_crossorigin_credentials.png^headers^
@@ -0,0 +1,2 @@
+Access-Control-Allow-Origin: http://mochi.test:8888
+Access-Control-Allow-Credentials: true
--- a/dom/canvas/test/mochitest.ini
+++ b/dom/canvas/test/mochitest.ini
@@ -9,16 +9,18 @@ support-files =
   image_ggrr-256x256.png
   image_green-16x16.png
   image_green-1x1.png
   image_green-redirect
   image_green-redirect^headers^
   image_green.png
   image_red-16x16.png
   image_red.png
+  image_red_crossorigin_credentials.png
+  image_red_crossorigin_credentials.png^headers^
   image_redtransparent.png
   image_rgrg-256x256.png
   image_rrgg-256x256.png
   image_transparent.png
   image_transparent50.png
   image_yellow.png
   image_yellow75.png
 
@@ -219,16 +221,18 @@ skip-if = (toolkit == 'gonk' && debug) |
 [test_canvas_focusring.html]
 skip-if = (toolkit == 'gonk' && !debug) || os == 'win' #specialpowers.wrap
 [test_canvas_font_setter.html]
 [test_canvas_path.html]
 [test_hitregion_canvas.html]
 [test_hitregion_event.html]
 skip-if = os == "android" || appname == "b2g"
 [test_canvas_strokeStyle_getter.html]
+[test_capture.html]
+support-files = captureStream_common.js
 [test_drawImageIncomplete.html]
 [test_drawImage_document_domain.html]
 [test_drawImage_edge_cases.html]
 [test_drawWindow.html]
 support-files = file_drawWindow_source.html file_drawWindow_common.js
 skip-if = buildapp == 'mulet' || (buildapp == 'b2g' && toolkit != 'gonk')
 [test_ImageData_ctor.html]
 [test_isPointInStroke.html]
new file mode 100644
--- /dev/null
+++ b/dom/canvas/test/test_capture.html
@@ -0,0 +1,111 @@
+<!DOCTYPE HTML>
+<meta http-equiv="content-type" content="text/html; charset=utf-8" />
+
+<title>Canvas2D test: CaptureStream()</title>
+
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="captureStream_common.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<body>
+<script>
+var c;       // Canvas element captured by streams.
+var h;       // CaptureStreamTestHelper holding utility test functions.
+var vauto;   // Video element with captureStream stream in automatic mode.
+var vmanual; // Video element with captureStream stream in manual (fps 0) mode.
+var vrate;   // Video element with captureStream stream with fixed frame rate.
+
+function checkDrawColorInitialRed() {
+  info("Checking that all video elements become red after first drawColor(red).");
+
+  h.drawColor(c, h.red);
+
+  vauto.mozSrcObject = c.captureStream();
+  vmanual.mozSrcObject = c.captureStream(0);
+  vrate.mozSrcObject = c.captureStream(10);
+
+  ok(h.testPixel(vauto, [0, 0, 0, 0], 0), "vauto hould not be drawn to before stable state");
+  ok(h.testPixel(vrate, [0, 0, 0, 0], 0), "vrate Should not be drawn to before stable state");
+  ok(h.testPixel(vmanual, [0, 0, 0, 0], 0), "vmanual Should not be drawn to before stable state");
+
+  return Promise.resolve()
+    .then(() => h.waitForPixel(vauto, h.red, 0, "should become red automatically"))
+    .then(() => h.waitForPixel(vrate, h.red, 0, "should become red automatically"))
+    .then(() => h.waitForPixel(vmanual, h.red, 0, "should become red when we get" +
+                                               " to stable state (first frame)"));
+}
+
+function checkDrawColorGreen() {
+  info("Checking that drawColor(green) propagates properly to video elements.");
+  h.drawColor(c, h.green);
+
+  return Promise.resolve()
+    .then(() => h.waitForPixel(vauto, h.green, 0, "should become green automatically"))
+    .then(() => h.waitForPixel(vrate, h.green, 0, "should become green automatically"))
+    .then(() => h.waitForPixel(vmanual, h.red, 0, "should still be red"))
+    .then(() => h.requestFrame(vmanual))
+    .then(() => h.waitForPixel(vmanual, h.green, 0, "should become green after requstFrame()"));
+}
+
+function checkRequestFrameOrderGuarantee() {
+  info("Checking that requestFrame() immediately before and after drawColor() " +
+       "calls results in the expected frame seen in the stream.");
+
+  return Promise.resolve()
+    .then(() => h.waitForPixel(vmanual, h.green, 0, "should still be green"))
+    .then(() => h.drawColor(c, h.red))   // 1. Draw canvas red
+    .then(() => h.requestFrame(vmanual)) // 2. Immediately request a frame
+    .then(() => h.drawColor(c, h.green)) // 3. Immediately draw canvas green
+    .then(() => h.waitForPixel(vmanual, h.red, 0, "should become red after call order test"))
+    .then(() => h.waitForPixelToTimeout(vmanual, h.green, 0, 500, "should not become green after call order test"));
+}
+
+function checkDrawImageNotCleanRed() {
+  info("Checking that drawImage with not origin-clean image renders streams useless.");
+  var ctx = c.getContext('2d');
+  var notCleanRed = new Image();
+
+  return new Promise((resolve, reject) => {
+    notCleanRed.onload = resolve;
+    notCleanRed.onerror = () => reject(new Error("Failed to load tainted image."));
+    notCleanRed.src = "http://example.com/tests/dom/canvas/test/image_red_crossorigin_credentials.png";
+    document.body.appendChild(notCleanRed);
+  })
+    .then(() => ctx.drawImage(notCleanRed, 0, 0, c.width, c.height))
+    .then(() => h.testNotClean(c))
+    .then(() => h.waitForPixelToTimeout(vauto, h.red, 0, 1000, "should not become red"))
+    .then(() => h.waitForPixelToTimeout(vrate, h.red, 0, 0, "should not become red"))
+    .then(() => h.waitForPixel(vmanual, h.green, 0, "should still be green"))
+    .then(() => h.requestFrame(vmanual))
+    .then(() => h.waitForPixelToTimeout(vmanual, h.red, 0, 1000, "should not become red"));
+}
+
+function finish() {
+  ok(true, 'Test complete.');
+  SimpleTest.finish();
+}
+
+function beginTest() {
+  h = new CaptureStreamTestHelper2D();
+
+  c = h.createAndAppendElement('canvas', 'c');
+  vauto = h.createAndAppendElement('video', 'vauto');
+  vmanual = h.createAndAppendElement('video', 'vmanual');
+  vrate = h.createAndAppendElement('video', 'vrate');
+
+  Promise.resolve()
+    .then(checkDrawColorInitialRed)
+    .then(checkDrawColorGreen)
+    .then(checkRequestFrameOrderGuarantee)
+    .then(checkDrawColorGreen) // Restore video elements to green.
+    .then(checkDrawImageNotCleanRed)
+    .then(finish);
+}
+
+SimpleTest.waitForExplicitFinish();
+
+var prefs = [
+  [ "canvas.capturestream.enabled", true ],
+];
+SpecialPowers.pushPrefEnv({ "set" : prefs }, beginTest);
+</script>
+
--- a/dom/canvas/test/webgl-mochitest.ini
+++ b/dom/canvas/test/webgl-mochitest.ini
@@ -4,16 +4,18 @@ skip-if = ((os == 'linux') && (buildapp 
 
 support-files =
   webgl-mochitest/driver-info.js
   webgl-mochitest/webgl-util.js
 
 [webgl-mochitest/test_backbuffer_channels.html]
 fail-if = (os == 'b2g')
 [webgl-mochitest/test_depth_readpixels.html]
+[webgl-mochitest/test_capture.html]
+support-files = captureStream_common.js
 [webgl-mochitest/test_draw.html]
 [webgl-mochitest/test_fb_param.html]
 [webgl-mochitest/test_fb_param_crash.html]
 [webgl-mochitest/test_hidden_alpha.html]
 skip-if = (os == 'b2g') || buildapp == 'mulet' # Mulet - bug 1093639 (crashes in libLLVM-3.0.so)
 [webgl-mochitest/test_implicit_color_buffer_float.html]
 [webgl-mochitest/test_highp_fs.html]
 [webgl-mochitest/test_no_arr_points.html]
new file mode 100644
--- /dev/null
+++ b/dom/canvas/test/webgl-mochitest/test_capture.html
@@ -0,0 +1,200 @@
+<!DOCTYPE HTML>
+<meta http-equiv="content-type" content="text/html; charset=utf-8" />
+
+<title>WebGL test: CaptureStream()</title>
+
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<script src="../captureStream_common.js">
+<script src="driver-info.js"></script>
+<script src="webgl-util.js"></script>
+<script id="vs" type="x-shader/x-vertex">
+
+attribute vec2 aVertCoord;
+
+void main(void) {
+  gl_Position = vec4(aVertCoord, 0.0, 1.0);
+}
+
+</script>
+<script id="fs" type="x-shader/x-fragment">
+
+precision mediump float;
+uniform vec4 uColor;
+
+void main(void) {
+  gl_FragColor = uColor;
+}
+
+</script>
+<body>
+<script>
+
+// Globals. Initialized during beginTest().
+var c;       // Canvas element captured by streams.
+var gl;      // WebGLContext of |c|.
+var h;       // CaptureStreamTestHelper holding utility test functions.
+var vauto;   // Video element with captureStream stream in automatic mode.
+var vmanual; // Video element with captureStream stream in manual (fps 0) mode.
+var vrate;   // Video element with captureStream stream with fixed frame rate.
+
+/* Fails the test if there was a GL error */
+function checkGLError(info) {
+  var error = gl.getError();
+  // Comparing strings for sake of log output in hex format.
+  is("0x" + error.toString(16), "0x0", "WebGL error [" + info + "]");
+}
+
+function checkClearColorInitialRed() {
+  info("Checking that clearing to red works for first frame.");
+
+  h.clearColor(c, h.red);
+
+  vauto.mozSrcObject = c.captureStream();
+  vmanual.mozSrcObject = c.captureStream(0);
+  vrate.mozSrcObject = c.captureStream(10);
+
+  ok(h.testPixel(vauto, [0, 0, 0, 0], 0), "Should not be drawn to before stable state");
+  ok(h.testPixel(vrate, [0, 0, 0, 0], 0), "Should not be drawn to before stable state");
+  ok(h.testPixel(vmanual, [0, 0, 0, 0], 0), "Should not be drawn to before stable state");
+
+  return Promise.resolve()
+    .then(() => h.waitForPixel(vauto, h.red, 0, "should become red automatically"))
+    .then(() => h.waitForPixel(vrate, h.red, 0, "should become red automatically"))
+    .then(() => h.waitForPixel(vmanual, h.red, 0, "should become red when we get to stable state (first frame)"))
+}
+
+function checkDrawColorGreen() {
+  info("Checking that drawColor() results in green frames.");
+  h.drawColor(c, h.green);
+  checkGLError('after DrawColor');
+  return Promise.resolve()
+    .then(() => h.waitForPixel(vauto, h.green, 0, "should become green automatically"))
+    .then(() => h.waitForPixel(vrate, h.green, 0, "should become green automatically"))
+    .then(() => h.waitForPixel(vmanual, h.red, 0, "should still be red"))
+    .then(() => h.requestFrame(vmanual))
+    .then(() => h.waitForPixel(vmanual, h.green, 0, "should become green after requstFrame()"))
+}
+
+function checkClearColorRed() {
+  info("Checking that clearing to red works.");
+  h.clearColor(c, h.red);
+  return Promise.resolve()
+    .then(() => h.waitForPixel(vauto, h.red, 0, "should become red automatically"))
+    .then(() => h.waitForPixel(vrate, h.red, 0, "should become red automatically"))
+    .then(() => h.waitForPixel(vmanual, h.green, 0, "should still be green"))
+    .then(() => h.requestFrame(vmanual))
+    .then(() => h.waitForPixel(vmanual, h.red, 0, "should become red after requestFrame()"))
+}
+
+function checkRequestFrameOrderGuarantee() {
+  info("Checking that requestFrame() immediately before and after draw " +
+       "calls results in the expected frame seen in the stream.");
+  return Promise.resolve()
+    .then(() => h.waitForPixel(vmanual, h.red, 0, "should still be red"))
+    .then(() => h.drawColor(c, h.green)) // 1. Draw canvas green
+    .then(() => h.requestFrame(vmanual)) // 2. Immediately request a frame
+    .then(() => h.clearColor(c, h.red))  // 3. Immediately clear to red
+    .then(() => h.waitForPixel(vmanual, h.green, 0, "should become green after call order test"))
+    .then(() => h.waitForPixelToTimeout(vmanual, h.red, 0, 500, "should not become red after call order test"));
+}
+
+function checkCapturingForbidden() {
+  info("Checking that capturing a WebGL context with " +
+       "`preservDrawingBuffer: false` is forbidden.");
+  var c2 = h.createAndAppendElement("canvas", "c2");
+  var gl2 = WebGLUtil.getWebGL("c2", false, { preserveDrawingBuffer: false });
+
+  var checkThrows = function(f, expected, fName) {
+    try {
+      f();
+      ok(false, fName + " should throw when not preserving drawing buffer");
+    } catch(e) {
+      is(e.name, expected, fName + " forbidden when not preserving drawing buffer");
+    }
+  };
+
+  checkThrows(() => c2.captureStream(), "NS_ERROR_FAILURE", "captureStream()");
+  checkThrows(() => c2.captureStream(0), "NS_ERROR_FAILURE", "captureStream(0)");
+  checkThrows(() => c2.captureStream(10), "NS_ERROR_FAILURE", "captureStream(10)");
+}
+
+function finish() {
+  ok(true, 'Test complete.');
+  SimpleTest.finish();
+}
+
+function beginTest() {
+  h = new CaptureStreamTestHelperWebGL();
+
+  c = h.createAndAppendElement('canvas', 'c');
+  vauto = h.createAndAppendElement('video', 'vauto');
+  vmanual = h.createAndAppendElement('video', 'vmanual');
+  vrate = h.createAndAppendElement('video', 'vrate');
+
+  gl = WebGLUtil.getWebGL('c', false, { preserveDrawingBuffer: true });
+  if (!gl) {
+    todo(false, 'WebGL is unavailable.');
+    finish();
+    return;
+  }
+
+  function errorFunc(str) {
+    ok(false, 'Error: ' + str);
+  }
+  WebGLUtil.setErrorFunc(errorFunc);
+  WebGLUtil.setWarningFunc(errorFunc);
+
+  gl.disable(gl.DEPTH_TEST);
+
+  prog = WebGLUtil.createProgramByIds(gl, 'vs', 'fs');
+  if (!prog) {
+    ok(false, 'Program linking should succeed.');
+    return;
+  }
+
+  // Setup vertex coordinates for drawing a rectangle across the whole canvas.
+
+  prog.aVertCoord = gl.getAttribLocation(prog, "aVertCoord");
+  ok(prog.aVertCoord >= 0, '`aVertCoord` should be valid.');
+
+  var vertCoordArr = new Float32Array([
+    -1, -1,
+     1, -1,
+    -1,  1,
+     1,  1,
+  ]);
+  var vertCoordBuff = gl.createBuffer();
+  gl.bindBuffer(gl.ARRAY_BUFFER, vertCoordBuff);
+  gl.bufferData(gl.ARRAY_BUFFER, vertCoordArr, gl.STATIC_DRAW);
+
+  gl.useProgram(prog);
+  gl.enableVertexAttribArray(prog.aVertCoord);
+  gl.vertexAttribPointer(prog.aVertCoord, 2, gl.FLOAT, false, 0, 0);
+
+  // Setup the helper with a pointer to how to change fragment color.
+
+  var uColorLocation = gl.getUniformLocation(prog, "uColor");
+  h.setFragmentColorLocation(uColorLocation);
+
+  checkGLError('after setup');
+
+  // Run tests.
+
+  Promise.resolve()
+    .then(checkClearColorInitialRed)
+    .then(checkDrawColorGreen)
+    .then(checkClearColorRed)
+    .then(checkRequestFrameOrderGuarantee)
+    .then(checkCapturingForbidden)
+    .then(finish);
+}
+
+SimpleTest.waitForExplicitFinish();
+
+var prefs = [
+  [ "canvas.capturestream.enabled", true ],
+];
+SpecialPowers.pushPrefEnv({ "set" : prefs }, beginTest);
+</script>
+
--- a/dom/canvas/test/webgl-mochitest/webgl-util.js
+++ b/dom/canvas/test/webgl-mochitest/webgl-util.js
@@ -29,29 +29,29 @@ WebGLUtil = (function() {
 
   function warning(str) {
     gWarningFunc(str);
   }
 
   // ---------------------------------------------------------------------------
   // WebGL helpers
 
-  function getWebGL(canvasId, requireConformant) {
+  function getWebGL(canvasId, requireConformant, attributes) {
     // `requireConformant` will default to falsey if it is not supplied.
 
     var canvas = document.getElementById(canvasId);
 
     var gl = null;
     try {
-      gl = canvas.getContext('webgl');
+      gl = canvas.getContext('webgl', attributes);
     } catch(e) {}
 
     if (!gl && !requireConformant) {
       try {
-        gl = canvas.getContext('experimental-webgl');
+        gl = canvas.getContext('experimental-webgl', attributes);
       } catch(e) {}
     }
 
     if (!gl) {
       error('WebGL context could not be retrieved from \'' + canvasId + '\'.');
       return null;
     }
 
--- a/dom/media/tests/mochitest/head.js
+++ b/dom/media/tests/mochitest/head.js
@@ -125,16 +125,17 @@ function setupEnvironment() {
     return;
   }
 
   // Running as a Mochitest.
   SimpleTest.requestFlakyTimeout("WebRTC inherently depends on timeouts");
   window.finish = () => SimpleTest.finish();
   SpecialPowers.pushPrefEnv({
     'set': [
+      ['canvas.capturestream.enabled', true],
       ['dom.messageChannel.enabled', true],
       ['media.peerconnection.enabled', true],
       ['media.peerconnection.identity.enabled', true],
       ['media.peerconnection.identity.timeout', 12000],
       ['media.peerconnection.default_iceservers', '[]'],
       ['media.navigator.permission.disabled', true],
       ['media.getusermedia.screensharing.enabled', true],
       ['media.getusermedia.screensharing.allowed_domains', "mochi.test"]
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -98,16 +98,20 @@ skip-if = toolkit == 'gonk' || buildapp 
 [test_peerConnection_bug834153.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g (Bug 1059867)
 [test_peerConnection_bug1013809.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g emulator seems to be too slow (Bug 1016498 and 1008080)
 [test_peerConnection_bug1042791.html]
 skip-if = buildapp == 'b2g' || buildapp == 'mulet' || os == 'android' # bug 1043403 # Bug 1141029 Mulet parity with B2G Desktop for TC
 [test_peerConnection_capturedVideo.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
+[test_peerConnection_captureStream_canvas_2d.html]
+skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
+[test_peerConnection_captureStream_canvas_webgl.html]
+skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
 [test_peerConnection_close.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g (Bug 1059867)
 [test_peerConnection_errorCallbacks.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g (Bug 1059867)
 [test_peerConnection_forwarding_basicAudioVideoCombined.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
 [test_peerConnection_noTrickleAnswer.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g (Bug 1059867)
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_2d.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+  <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+createHTML({
+  bug: "1032848",
+  title: "Canvas(2D)::CaptureStream as video-only input to peerconnection",
+  visible: true
+});
+
+runNetworkTest(() => {
+  var test = new PeerConnectionTest();
+  var vremote;
+  var h = new CaptureStreamTestHelper2D();
+  var canvas = document.createElement('canvas');
+  canvas.id = 'source_canvas';
+  canvas.width = canvas.height = 10;
+  document.getElementById('content').appendChild(canvas);
+
+  test.setMediaConstraints([{video: true}], []);
+  test.chain.replace("PC_LOCAL_GUM", [
+    function DRAW_LOCAL_GREEN(test) {
+      h.drawColor(canvas, h.green);
+    },
+    function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+      var stream = canvas.captureStream(10);
+      test.pcLocal.attachMedia(stream, 'video', 'local');
+    }
+  ]);
+  test.chain.append([
+    function FIND_REMOTE_VIDEO() {
+      vremote = document.getElementById('pcRemote_remote1_video');
+      ok(!!vremote, "Should have remote video element for pcRemote");
+    },
+    function WAIT_FOR_REMOTE_GREEN() {
+      return h.waitForPixel(vremote, h.green, 128, "pcRemote's remote should become green");
+    },
+    function DRAW_LOCAL_RED() {
+      h.drawColor(canvas, h.red);
+    },
+    function WAIT_FOR_REMOTE_RED() {
+      return h.waitForPixel(vremote, h.red, 128, "pcRemote's remote should become red");
+    }
+  ]);
+  test.run();
+});
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_captureStream_canvas_webgl.html
@@ -0,0 +1,108 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+  <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+  <script type="application/javascript" src="/tests/dom/canvas/test/webgl-mochitest/webgl-util.js"></script>
+</head>
+<body>
+<pre id="test">
+<script id="v-shader" type="x-shader/x-vertex">
+  attribute vec2 aPosition;
+  void main() {
+   gl_Position = vec4(aPosition, 0, 1);
+}
+</script>
+<script id="f-shader" type="x-shader/x-fragment">
+  precision mediump float;
+  uniform vec4 uColor;
+  void main() { gl_FragColor = uColor; }
+</script>
+<script type="application/javascript;version=1.8">
+createHTML({
+  bug: "1032848",
+  title: "Canvas(WebGL)::CaptureStream as video-only input to peerconnection"
+});
+
+runNetworkTest(() => {
+  var test = new PeerConnectionTest();
+  var vremote;
+  var h = new CaptureStreamTestHelperWebGL();
+  var canvas = document.createElement('canvas');
+  canvas.id = 'source_canvas';
+  canvas.width = canvas.height = 10;
+  document.getElementById('content').appendChild(canvas);
+
+  var gl = WebGLUtil.getWebGL(canvas.id, false, { preserveDrawingBuffer: true });
+  if (!gl) {
+    todo(false, "WebGL unavailable.");
+    networkTestFinished();
+    return;
+  }
+
+  var errorFunc = str => ok(false, 'Error: ' + str);
+  WebGLUtil.setErrorFunc(errorFunc);
+  WebGLUtil.setWarningFunc(errorFunc);
+
+  test.setMediaConstraints([{video: true}], []);
+  test.chain.replace("PC_LOCAL_GUM", [
+    function WEBGL_SETUP(test) {
+      var program = WebGLUtil.createProgramByIds(gl, 'v-shader', 'f-shader');
+
+      if (!program) {
+        ok(false, "Program should link");
+        return Promise.reject();
+      }
+      gl.useProgram(program);
+
+      var uColorLocation = gl.getUniformLocation(program, "uColor");
+      h.setFragmentColorLocation(uColorLocation);
+
+      var squareBuffer = gl.createBuffer();
+      gl.bindBuffer(gl.ARRAY_BUFFER, squareBuffer);
+
+      var vertices = [ 0,  0,
+                      -1,  0,
+                       0,  1,
+                      -1,  1 ];
+      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
+      squareBuffer.itemSize = 2;
+      squareBuffer.numItems = 4;
+
+      program.aPosition = gl.getAttribLocation(program, "aPosition");
+      gl.enableVertexAttribArray(program.aPosition);
+      gl.vertexAttribPointer(program.aPosition, squareBuffer.itemSize, gl.FLOAT, false, 0, 0);
+    },
+    function DRAW_LOCAL_GREEN(test) {
+      h.drawColor(canvas, h.green);
+    },
+    function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+      test.pcLocal.canvasStream = canvas.captureStream(0.0);
+      is(test.pcLocal.canvasStream.canvas, canvas, "Canvas attribute is correct");
+      test.pcLocal.attachMedia(test.pcLocal.canvasStream, 'video', 'local');
+    }
+  ]);
+  test.chain.append([
+    function FIND_REMOTE_VIDEO() {
+      vremote = document.getElementById('pcRemote_remote1_video');
+      ok(!!vremote, "Should have remote video element for pcRemote");
+    },
+    function WAIT_FOR_REMOTE_GREEN() {
+      return h.waitForPixel(vremote, h.green, 128, "pcRemote's remote should become green");
+    },
+    function DRAW_LOCAL_RED() {
+      h.drawColor(canvas, h.red);
+    },
+    function REQUEST_FRAME(test) {
+      test.pcLocal.canvasStream.requestFrame();
+    },
+    function WAIT_FOR_REMOTE_RED() {
+      return h.waitForPixel(vremote, h.red, 128, "pcRemote's remote should become red");
+    }
+  ]);
+  test.run();
+});
+</script>
+</pre>
+</body>
+</html>