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