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 = {};