1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 // The tests here cover the end-to-end functionality of tab capturing and
6 // playback as video. The page generates video test patterns that rotate
7 // cyclicly, and the rendering output of the tab is captured into a
8 // LocalMediaStream. This stream is then piped into a video element for
9 // playback, and a canvas is used to examine the frames of the video for
10 // expected content. The stream may be plumbed one of two ways, depending on
11 // the 'method' query param:
13 // local: LocalMediaStream --> DOM Video Element
14 // webrtc: LocalMediaStream --> PeerConnection (sender)
15 // --> PeerConnection (receiver) --> DOM Video Element
17 // The test pattern cycles as a color fill of red, then green, then blue.
18 var colors = [ [ 255, 0, 0 ], [ 0, 255, 0 ], [ 0, 0, 255 ] ];
21 // Capture parameters.
26 // The stream to playback in the video element.
27 var receiveStream = null;
29 // waitForExpectedColors() removes elements from this array as each is observed.
30 // When it becomes empty, the test succeeds.
31 var expectedColors = [ [ 255, 0, 0 ], [ 0, 255, 0 ], [ 0, 0, 255 ] ];
33 function updateTestPattern() {
35 this.canvas = document.createElement("canvas");
36 this.canvas.width = 320;
37 this.canvas.height = 200;
38 this.canvas.style.position = "absolute";
39 this.canvas.style.top = "0px";
40 this.canvas.style.left = "0px";
41 this.canvas.style.width = "100%";
42 this.canvas.style.height = "100%";
43 document.body.appendChild(this.canvas);
45 var context = this.canvas.getContext("2d");
46 // Fill with solid color.
47 context.fillStyle = "rgb(" + colors[curIdx] + ")";
48 context.fillRect(0, 0, this.canvas.width, this.canvas.height);
49 // Draw the circle that moves around the page.
50 context.fillStyle = "rgb(" + colors[(curIdx + 1) % colors.length] + ")";
52 if (!this.frameNumber) {
57 var i = this.frameNumber % 200;
58 var t = (this.frameNumber + 3000) * (0.01 + i / 8000.0);
59 var x = (Math.sin(t) * 0.45 + 0.5) * this.canvas.width;
60 var y = (Math.cos(t * 0.9) * 0.45 + 0.5) * this.canvas.height;
61 context.arc(x, y, 16, 0, 2 * Math.PI, false);
66 function renderTestPatternLoop() {
67 requestAnimationFrame(renderTestPatternLoop);
70 if (!this.stepTimeMillis) {
71 this.stepTimeMillis = 100;
73 var now = new Date().getTime();
74 if (!this.nextSteppingAt) {
75 this.nextSteppingAt = now + this.stepTimeMillis;
76 } else if (now >= this.nextSteppingAt) {
78 if (curIdx >= colors.length) { // Completed a cycle.
80 // Increase the wait time between switching test patterns for overloaded
81 // bots that aren't capturing all the frames of video.
82 this.stepTimeMillis *= 1.25;
84 this.nextSteppingAt = now + this.stepTimeMillis;
88 function waitForExpectedColors(colorDeviation) {
89 // If needed, create the video and canvas elements, but no need to append them
92 this.video = document.createElement("video");
93 this.video.width = width;
94 this.video.height = height;
95 this.video.addEventListener("error", chrome.test.fail);
96 this.video.src = URL.createObjectURL(receiveStream);
99 this.readbackCanvas = document.createElement("canvas");
100 this.readbackCanvas.width = width;
101 this.readbackCanvas.height = height;
104 // Only bother examining a video frame if the video timestamp has advanced.
105 var currentVideoTimestamp = this.video.currentTime;
106 if (!this.lastVideoTimestamp ||
107 this.lastVideoTimestamp < currentVideoTimestamp) {
108 this.lastVideoTimestamp = currentVideoTimestamp;
110 // Grab a snapshot of the center pixel of the video.
111 var ctx = this.readbackCanvas.getContext("2d");
112 ctx.drawImage(video, 0, 0, width, height);
113 var imageData = ctx.getImageData(width / 2, height / 2, 1, 1);
114 var pixel = [ imageData.data[0], imageData.data[1], imageData.data[2] ];
116 // Does the pixel match one of the expected colors?
117 for (var i = 0; i < expectedColors.length; ++i) {
118 var curColor = expectedColors[i];
119 if (Math.abs(pixel[0] - curColor[0]) <= colorDeviation &&
120 Math.abs(pixel[1] - curColor[1]) <= colorDeviation &&
121 Math.abs(pixel[2] - curColor[2]) <= colorDeviation) {
122 console.debug("Observed expected color RGB(" + curColor +
123 ") in the video as RGB(" + pixel + ")");
124 expectedColors.splice(i, 1);
129 if (expectedColors.length == 0) {
130 chrome.test.succeed();
132 setTimeout(function () { waitForExpectedColors(colorDeviation); },
137 chrome.test.runTests([
138 function endToEndTest() {
139 // The receive port changes between browser_test invocations, and is passed
140 // as an query parameter in the URL.
141 var transportMethod; // Should be: local or webrtc.
142 var colorDeviation; // How far from the expected intensity ([0,255] scale)?
144 transportMethod = window.location.search.match(/(\?|&)method=(\w+)/)[2];
145 chrome.test.assertTrue(transportMethod == 'local' ||
146 transportMethod == 'webrtc');
147 colorDeviation = parseInt(
148 window.location.search.match(/(\?|&)colorDeviation=(\d+)/)[2]);
149 chrome.test.assertTrue(colorDeviation >= 0 && colorDeviation <= 255);
151 chrome.test.fail("Error parsing query params -- " + err.message);
155 // Start rendering test patterns.
156 renderTestPatternLoop();
158 chrome.tabCapture.capture(
167 maxFrameRate: frameRate,
171 function remoteTheStream(captureStream) {
172 chrome.test.assertTrue(!!captureStream);
173 if (transportMethod == 'local') {
174 receiveStream = captureStream;
175 waitForExpectedColors(colorDeviation);
176 } else if (transportMethod == 'webrtc') {
177 var sender = new webkitRTCPeerConnection(null);
178 var receiver = new webkitRTCPeerConnection(null);
179 sender.onicecandidate = function (event) {
180 if (event.candidate) {
181 receiver.addIceCandidate(new RTCIceCandidate(event.candidate));
184 receiver.onicecandidate = function (event) {
185 if (event.candidate) {
186 sender.addIceCandidate(new RTCIceCandidate(event.candidate));
189 receiver.onaddstream = function (event) {
190 receiveStream = event.stream;
191 waitForExpectedColors(colorDeviation);
193 sender.addStream(captureStream);
194 sender.createOffer(function (sender_description) {
195 sender.setLocalDescription(sender_description);
196 receiver.setRemoteDescription(sender_description);
197 receiver.createAnswer(function (receiver_description) {
198 receiver.setLocalDescription(receiver_description);
199 sender.setRemoteDescription(receiver_description);
203 chrome.test.fail("Unknown transport method: " + transportMethod);
209 // TODO(miu): Once the WebAudio API is finalized, we should add a test to emit a
210 // tone from the sender page, and have the receiver page check for the audio