1 // Copyright 2013 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.
6 // The file runs a series of Media Source Entensions (MSE) operations on a
7 // video tag. The test takes several URL parameters described in
8 //loadTestParams() function.
11 function getPerfTimestamp() {
12 return performance
.now();
15 var pageStartTime
= getPerfTimestamp();
19 function parseQueryParameters() {
21 var r
= /([^&=]+)=?([^&]*)/g;
23 function d(s
) { return decodeURIComponent(s
.replace(/\+/g, ' ')); }
26 while (match
= r
.exec(window
.location
.search
.substring(1)))
27 params
[d(match
[1])] = d(match
[2]);
33 function loadTestParams() {
34 var queryParameters
= parseQueryParameters();
36 testParams
.testType
= queryParameters
["testType"] || "AV";
37 testParams
.useAppendStream
= (queryParameters
["useAppendStream"] == "true");
38 testParams
.doNotWaitForBodyOnLoad
=
39 (queryParameters
["doNotWaitForBodyOnLoad"] == "true");
40 testParams
.startOffset
= 0;
41 testParams
.appendSize
= parseInt(queryParameters
["appendSize"] || "65536");
42 testParams
.graphDuration
=
43 parseInt(queryParameters
["graphDuration"] || "1000");
46 function plotTimestamps(timestamps
, graphDuration
, element
) {
49 var c
= document
.getElementById('c');
50 var ctx
= c
.getContext('2d');
53 { label
: 'Page Load Total',
57 { label
: 'body.onload Delay',
61 { label
: 'Test Total',
62 start
: timestamps
.testStartTime
,
63 end
: timestamps
.testEndTime
,
65 { label
: 'MediaSource opening',
66 start
: timestamps
.mediaSourceOpenStartTime
,
67 end
: timestamps
.mediaSourceOpenEndTime
,
71 var maxAppendEndTime
= 0;
72 for (var i
= 0; i
< timestamps
.appenders
.length
; ++i
) {
73 var appender
= timestamps
.appenders
[i
];
74 bars
.push({ label
: 'XHR',
75 start
: appender
.xhrStartTime
,
76 end
: appender
.xhrEndTime
,
78 bars
.push({ label
: 'Append',
79 start
: appender
.appendStartTime
,
80 end
: appender
.appendEndTime
,
82 if (appender
.appendEndTime
> maxAppendEndTime
) {
83 maxAppendEndTime
= appender
.appendEndTime
;
88 label
: 'Post Append Delay',
89 start
: maxAppendEndTime
,
90 end
: timestamps
.testEndTime
,
93 var minTimestamp
= Number
.MAX_VALUE
;
94 for (var i
= 0; i
< bars
.length
; ++i
) {
95 minTimestamp
= Math
.min(minTimestamp
, bars
[i
].start
);
98 var graphWidth
= c
.width
- 100;
99 function convertTimestampToX(t
) {
100 return graphWidth
* (t
- minTimestamp
) / graphDuration
;
103 var barThickness
= 20;
104 c
.height
= bars
.length
* barThickness
;
105 ctx
.font
= (0.75 * barThickness
) + 'px arial';
106 for (var i
= 0; i
< bars
.length
; ++i
) {
108 var xStart
= convertTimestampToX(bar
.start
);
109 var xEnd
= convertTimestampToX(bar
.end
);
110 ctx
.fillStyle
= bar
.color
;
111 ctx
.fillRect(xStart
, y
, xEnd
- xStart
, barThickness
);
113 ctx
.fillStyle
= 'black';
114 var text
= bar
.label
+ ' (' + (bar
.end
- bar
.start
).toFixed(3) + ' ms)';
115 ctx
.fillText(text
, xEnd
+ 10, y
+ (0.75 * barThickness
));
118 reportTelemetryMediaMetrics(bars
, element
);
121 function displayResults(stats
) {
122 var statsDiv
= document
.getElementById('stats');
125 statsDiv
.innerHTML
= "Test failed";
129 var statsMarkup
= "Test passed<br><table>";
130 for (var i
in stats
) {
131 statsMarkup
+= "<tr><td style=\"text-align:right\">" + i
+ ":</td><td>" +
132 stats
[i
].toFixed(3) + " ms</td>";
134 statsMarkup
+= "</table>";
135 statsDiv
.innerHTML
= statsMarkup
;
138 function reportTelemetryMediaMetrics(stats
, element
) {
140 for (var i
= 0; i
< stats
.length
; ++i
) {
142 var label
= bar
.label
.toLowerCase().replace(/\s+|\./g, '_');
143 var value
= (bar
.end
- bar
.start
).toFixed(3);
144 console
.log("appending to telemetry " + label
+ " : " + value
);
145 _AppendMetric(metrics
, label
, value
);
147 window
.__testMetrics
= {
153 function _AppendMetric(metrics
, metric
, value
) {
154 if (!metrics
[metric
])
155 metrics
[metric
] = [];
156 metrics
[metric
].push(value
);
159 function updateControls(testParams
) {
160 var testTypeElement
= document
.getElementById("testType");
161 for (var i
in testTypeElement
.options
) {
162 var option
= testTypeElement
.options
[i
];
163 if (option
.value
== testParams
.testType
) {
164 testTypeElement
.selectedIndex
= option
.index
;
168 document
.getElementById("useAppendStream").checked
=
169 testParams
.useAppendStream
;
170 document
.getElementById("doNotWaitForBodyOnLoad").checked
=
171 testParams
.doNotWaitForBodyOnLoad
;
172 document
.getElementById("appendSize").value
= testParams
.appendSize
;
173 document
.getElementById("graphDuration").value
= testParams
.graphDuration
;
176 function BufferAppender(mimetype
, url
, id
, startOffset
, appendSize
) {
177 this.mimetype
= mimetype
;
180 this.startOffset
= startOffset
;
181 this.appendSize
= appendSize
;
182 this.xhr
= new XMLHttpRequest();
183 this.sourceBuffer
= null;
186 BufferAppender
.prototype.start = function() {
187 this.xhr
.addEventListener('loadend', this.onLoadEnd
.bind(this));
188 this.xhr
.open('GET', this.url
);
189 this.xhr
.setRequestHeader('Range', 'bytes=' + this.startOffset
+ '-' +
190 (this.startOffset
+ this.appendSize
- 1));
191 this.xhr
.responseType
= 'arraybuffer';
194 this.xhrStartTime
= getPerfTimestamp();
197 BufferAppender
.prototype.onLoadEnd = function() {
198 this.xhrEndTime
= getPerfTimestamp();
199 this.attemptAppend();
202 BufferAppender
.prototype.onSourceOpen = function(mediaSource
) {
203 if (this.sourceBuffer
)
205 this.sourceBuffer
= mediaSource
.addSourceBuffer(this.mimetype
);
208 BufferAppender
.prototype.attemptAppend = function() {
209 if (!this.xhr
.response
|| !this.sourceBuffer
)
212 this.appendStartTime
= getPerfTimestamp();
214 if (this.sourceBuffer
.appendBuffer
) {
215 this.sourceBuffer
.addEventListener('updateend',
216 this.onUpdateEnd
.bind(this));
217 this.sourceBuffer
.appendBuffer(this.xhr
.response
);
219 this.sourceBuffer
.append(new Uint8Array(this.xhr
.response
));
220 this.appendEndTime
= getPerfTimestamp();
226 BufferAppender
.prototype.onUpdateEnd = function() {
227 this.appendEndTime
= getPerfTimestamp();
230 BufferAppender
.prototype.onPlaybackStarted = function() {
231 var now
= getPerfTimestamp();
232 this.playbackStartTime
= now
;
233 if (this.sourceBuffer
.updating
) {
234 // Still appending but playback has already started so just abort the XHR
236 this.sourceBuffer
.abort();
241 BufferAppender
.prototype.getXHRLoadDuration = function() {
242 return this.xhrEndTime
- this.xhrStartTime
;
245 BufferAppender
.prototype.getAppendDuration = function() {
246 return this.appendEndTime
- this.appendStartTime
;
249 function StreamAppender(mimetype
, url
, id
, startOffset
, appendSize
) {
250 this.mimetype
= mimetype
;
253 this.startOffset
= startOffset
;
254 this.appendSize
= appendSize
;
255 this.xhr
= new XMLHttpRequest();
256 this.sourceBuffer
= null;
257 this.appendStarted
= false;
260 StreamAppender
.prototype.start = function() {
261 this.xhr
.addEventListener('readystatechange',
262 this.attemptAppend
.bind(this));
263 this.xhr
.addEventListener('loadend', this.onLoadEnd
.bind(this));
264 this.xhr
.open('GET', this.url
);
265 this.xhr
.setRequestHeader('Range', 'bytes=' + this.startOffset
+ '-' +
266 (this.startOffset
+ this.appendSize
- 1));
267 this.xhr
.responseType
= 'stream';
268 if (this.xhr
.responseType
!= 'stream') {
269 EndTest("XHR does not support 'stream' responses.");
273 this.xhrStartTime
= getPerfTimestamp();
276 StreamAppender
.prototype.onLoadEnd = function() {
277 this.xhrEndTime
= getPerfTimestamp();
278 this.attemptAppend();
281 StreamAppender
.prototype.onSourceOpen = function(mediaSource
) {
282 if (this.sourceBuffer
)
284 this.sourceBuffer
= mediaSource
.addSourceBuffer(this.mimetype
);
287 StreamAppender
.prototype.attemptAppend = function() {
288 if (this.xhr
.readyState
< this.xhr
.LOADING
) {
292 if (!this.xhr
.response
|| !this.sourceBuffer
|| this.appendStarted
)
295 this.appendStartTime
= getPerfTimestamp();
296 this.appendStarted
= true;
297 this.sourceBuffer
.addEventListener('updateend',
298 this.onUpdateEnd
.bind(this));
299 this.sourceBuffer
.appendStream(this.xhr
.response
);
302 StreamAppender
.prototype.onUpdateEnd = function() {
303 this.appendEndTime
= getPerfTimestamp();
306 StreamAppender
.prototype.onPlaybackStarted = function() {
307 var now
= getPerfTimestamp();
308 this.playbackStartTime
= now
;
309 if (this.sourceBuffer
.updating
) {
310 // Still appending but playback has already started so just abort the XHR
312 this.sourceBuffer
.abort();
314 if (!this.appendEndTime
)
315 this.appendEndTime
= now
;
317 if (!this.xhrEndTime
)
318 this.xhrEndTime
= now
;
322 StreamAppender
.prototype.getXHRLoadDuration = function() {
323 return this.xhrEndTime
- this.xhrStartTime
;
326 StreamAppender
.prototype.getAppendDuration = function() {
327 return this.appendEndTime
- this.appendStartTime
;
330 // runAppendTest() sets testDone to true once all appends finish.
331 var testDone
= false;
332 function runAppendTest(mediaElement
, appenders
, doneCallback
) {
333 var testStartTime
= getPerfTimestamp();
334 var mediaSourceOpenStartTime
;
335 var mediaSourceOpenEndTime
;
337 for (var i
= 0; i
< appenders
.length
; ++i
) {
338 appenders
[i
].start();
341 function onSourceOpen(event
) {
342 var mediaSource
= event
.target
;
344 mediaSourceOpenEndTime
= getPerfTimestamp();
346 for (var i
= 0; i
< appenders
.length
; ++i
) {
347 appenders
[i
].onSourceOpen(mediaSource
);
350 for (var i
= 0; i
< appenders
.length
; ++i
) {
351 appenders
[i
].attemptAppend(mediaSource
);
358 if (window
['MediaSource']) {
359 mediaSource
= new window
.MediaSource();
360 mediaSource
.addEventListener('sourceopen', onSourceOpen
);
362 mediaSource
= new window
.WebKitMediaSource();
363 mediaSource
.addEventListener('webkitsourceopen', onSourceOpen
);
368 function checkForCurrentTimeChange() {
372 if (mediaElement
.readyState
< mediaElement
.HAVE_METADATA
||
373 mediaElement
.currentTime
<= 0) {
374 listener
= window
.requestAnimationFrame(checkForCurrentTimeChange
);
378 var testEndTime
= getPerfTimestamp();
379 for (var i
= 0; i
< appenders
.length
; ++i
) {
380 appenders
[i
].onPlaybackStarted(mediaSource
);
384 window
.clearInterval(listener
);
385 window
.clearTimeout(timeout
);
388 stats
.total
= testEndTime
- testStartTime
;
389 stats
.sourceOpen
= mediaSourceOpenEndTime
- mediaSourceOpenStartTime
;
390 stats
.maxXHRLoadDuration
= appenders
[0].getXHRLoadDuration();
391 stats
.maxAppendDuration
= appenders
[0].getAppendDuration();
394 timestamps
.testStartTime
= testStartTime
;
395 timestamps
.testEndTime
= testEndTime
;
396 timestamps
.mediaSourceOpenStartTime
= mediaSourceOpenStartTime
;
397 timestamps
.mediaSourceOpenEndTime
= mediaSourceOpenEndTime
;
398 timestamps
.appenders
= [];
400 for (var i
= 1; i
< appenders
.length
; ++i
) {
401 var appender
= appenders
[i
];
402 var xhrLoadDuration
= appender
.getXHRLoadDuration();
403 var appendDuration
= appender
.getAppendDuration();
405 if (xhrLoadDuration
> stats
.maxXHRLoadDuration
)
406 stats
.maxXHRLoadDuration
= xhrLoadDuration
;
408 if (appendDuration
> stats
.maxAppendDuration
)
409 stats
.maxAppendDuration
= appendDuration
;
412 for (var i
= 0; i
< appenders
.length
; ++i
) {
413 var appender
= appenders
[i
];
414 var appenderTimestamps
= {};
415 appenderTimestamps
.xhrStartTime
= appender
.xhrStartTime
;
416 appenderTimestamps
.xhrEndTime
= appender
.xhrEndTime
;
417 appenderTimestamps
.appendStartTime
= appender
.appendStartTime
;
418 appenderTimestamps
.appendEndTime
= appender
.appendEndTime
;
419 appenderTimestamps
.playbackStartTime
= appender
.playbackStartTime
;
420 timestamps
.appenders
.push(appenderTimestamps
);
423 mediaElement
.pause();
425 pageEndTime
= getPerfTimestamp();
426 doneCallback(stats
, timestamps
);
429 listener
= window
.requestAnimationFrame(checkForCurrentTimeChange
);
431 timeout
= setTimeout(function() {
436 window
.cancelAnimationFrame(listener
);
438 mediaElement
.pause();
440 EndTest("Test timed out.");
443 mediaSourceOpenStartTime
= getPerfTimestamp();
444 mediaElement
.src
= URL
.createObjectURL(mediaSource
);
447 function onBodyLoad() {
448 bodyLoadTime
= getPerfTimestamp();
450 if (!testParams
.doNotWaitForBodyOnLoad
) {
455 function startTest() {
456 updateControls(testParams
);
460 if (testParams
.useAppendStream
&& !window
.MediaSource
)
461 EndTest("Can't use appendStream() because the unprefixed MediaSource " +
462 "object is not present.");
464 var Appender
= testParams
.useAppendStream
? StreamAppender
: BufferAppender
;
466 if (testParams
.testType
.indexOf("A") != -1) {
468 new Appender("audio/mp4; codecs=\"mp4a.40.2\"",
471 testParams
.startOffset
,
472 testParams
.appendSize
));
475 if (testParams
.testType
.indexOf("V") != -1) {
477 new Appender("video/mp4; codecs=\"avc1.640028\"",
480 testParams
.startOffset
,
481 testParams
.appendSize
));
484 var video
= document
.getElementById("v");
485 video
.addEventListener("error", function(e
) {
486 console
.log("video error!");
487 EndTest("Video error: " + video
.error
);
490 video
.id
= getTestID();
491 runAppendTest(video
, appenders
, function(stats
, timestamps
) {
492 displayResults(stats
);
493 plotTimestamps(timestamps
, testParams
.graphDuration
, video
);
494 EndTest("Call back call done.");
498 function EndTest(msg
) {
499 console
.log("Ending test: " + msg
);
500 window
.__testDone
= true;
503 function getTestID() {
504 var id
= testParams
.testType
;
505 if (testParams
.useAppendStream
)
509 if (testParams
.doNotWaitForBodyOnLoad
)
516 function setupTest() {
518 document
.body
.onload
= onBodyLoad
;
520 if (testParams
.doNotWaitForBodyOnLoad
) {
525 window
["setupTest"] = setupTest
;
526 window
.__testDone
= false;
527 window
.__testMetrics
= {};