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. 4 5 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. 9 10 (function() { 11 function getPerfTimestamp() { 12 return performance.now(); 13 } 14 15 var pageStartTime = getPerfTimestamp(); 16 var bodyLoadTime; 17 var pageEndTime; 18 19 function parseQueryParameters() { 20 var params = {}; 21 var r = /([^&=]+)=?([^&]*)/g; 22 23 function d(s) { return decodeURIComponent(s.replace(/\+/g, ' ')); } 24 25 var match; 26 while (match = r.exec(window.location.search.substring(1))) 27 params[d(match[1])] = d(match[2]); 28 29 return params; 30 } 31 32 var testParams; 33 function loadTestParams() { 34 var queryParameters = parseQueryParameters(); 35 testParams = {}; 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"); 44 } 45 46 function plotTimestamps(timestamps, graphDuration, element) { 47 if (!timestamps) 48 return; 49 var c = document.getElementById('c'); 50 var ctx = c.getContext('2d'); 51 52 var bars = [ 53 { label: 'Page Load Total', 54 start: pageStartTime, 55 end: pageEndTime, 56 color: '#404040' }, 57 { label: 'body.onload Delay', 58 start: pageStartTime, 59 end: bodyLoadTime, 60 color: '#808080' }, 61 { label: 'Test Total', 62 start: timestamps.testStartTime, 63 end: timestamps.testEndTime, 64 color: '#00FF00' }, 65 { label: 'MediaSource opening', 66 start: timestamps.mediaSourceOpenStartTime, 67 end: timestamps.mediaSourceOpenEndTime, 68 color: '#008800' } 69 ]; 70 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, 77 color: '#0088FF' }); 78 bars.push({ label: 'Append', 79 start: appender.appendStartTime, 80 end: appender.appendEndTime, 81 color: '#00FFFF' }); 82 if (appender.appendEndTime > maxAppendEndTime) { 83 maxAppendEndTime = appender.appendEndTime; 84 } 85 } 86 87 bars.push({ 88 label: 'Post Append Delay', 89 start: maxAppendEndTime, 90 end: timestamps.testEndTime, 91 color: '#B0B0B0' }); 92 93 var minTimestamp = Number.MAX_VALUE; 94 for (var i = 0; i < bars.length; ++i) { 95 minTimestamp = Math.min(minTimestamp, bars[i].start); 96 } 97 98 var graphWidth = c.width - 100; 99 function convertTimestampToX(t) { 100 return graphWidth * (t - minTimestamp) / graphDuration; 101 } 102 var y = 0; 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) { 107 var bar = bars[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); 112 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)); 116 y += barThickness; 117 } 118 reportTelemetryMediaMetrics(bars, element); 119 } 120 121 function displayResults(stats) { 122 var statsDiv = document.getElementById('stats'); 123 124 if (!stats) { 125 statsDiv.innerHTML = "Test failed"; 126 return; 127 } 128 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>"; 133 } 134 statsMarkup += "</table>"; 135 statsDiv.innerHTML = statsMarkup; 136 } 137 138 function reportTelemetryMediaMetrics(stats, element) { 139 var metrics = {}; 140 for (var i = 0; i < stats.length; ++i) { 141 var bar = stats[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); 146 } 147 window.__testMetrics = { 148 "id": element.id, 149 "metrics": metrics 150 }; 151 } 152 153 function _AppendMetric(metrics, metric, value) { 154 if (!metrics[metric]) 155 metrics[metric] = []; 156 metrics[metric].push(value); 157 } 158 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; 165 } 166 } 167 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; 174 } 175 176 function BufferAppender(mimetype, url, id, startOffset, appendSize) { 177 this.mimetype = mimetype; 178 this.url = url; 179 this.id = id; 180 this.startOffset = startOffset; 181 this.appendSize = appendSize; 182 this.xhr = new XMLHttpRequest(); 183 this.sourceBuffer = null; 184 } 185 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'; 192 this.xhr.send(); 193 194 this.xhrStartTime = getPerfTimestamp(); 195 }; 196 197 BufferAppender.prototype.onLoadEnd = function() { 198 this.xhrEndTime = getPerfTimestamp(); 199 this.attemptAppend(); 200 }; 201 202 BufferAppender.prototype.onSourceOpen = function(mediaSource) { 203 if (this.sourceBuffer) 204 return; 205 this.sourceBuffer = mediaSource.addSourceBuffer(this.mimetype); 206 }; 207 208 BufferAppender.prototype.attemptAppend = function() { 209 if (!this.xhr.response || !this.sourceBuffer) 210 return; 211 212 this.appendStartTime = getPerfTimestamp(); 213 214 if (this.sourceBuffer.appendBuffer) { 215 this.sourceBuffer.addEventListener('updateend', 216 this.onUpdateEnd.bind(this)); 217 this.sourceBuffer.appendBuffer(this.xhr.response); 218 } else { 219 this.sourceBuffer.append(new Uint8Array(this.xhr.response)); 220 this.appendEndTime = getPerfTimestamp(); 221 } 222 223 this.xhr = null; 224 }; 225 226 BufferAppender.prototype.onUpdateEnd = function() { 227 this.appendEndTime = getPerfTimestamp(); 228 }; 229 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 235 // and append. 236 this.sourceBuffer.abort(); 237 this.xhr.abort(); 238 } 239 }; 240 241 BufferAppender.prototype.getXHRLoadDuration = function() { 242 return this.xhrEndTime - this.xhrStartTime; 243 }; 244 245 BufferAppender.prototype.getAppendDuration = function() { 246 return this.appendEndTime - this.appendStartTime; 247 }; 248 249 function StreamAppender(mimetype, url, id, startOffset, appendSize) { 250 this.mimetype = mimetype; 251 this.url = url; 252 this.id = id; 253 this.startOffset = startOffset; 254 this.appendSize = appendSize; 255 this.xhr = new XMLHttpRequest(); 256 this.sourceBuffer = null; 257 this.appendStarted = false; 258 } 259 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."); 270 } 271 this.xhr.send(); 272 273 this.xhrStartTime = getPerfTimestamp(); 274 }; 275 276 StreamAppender.prototype.onLoadEnd = function() { 277 this.xhrEndTime = getPerfTimestamp(); 278 this.attemptAppend(); 279 }; 280 281 StreamAppender.prototype.onSourceOpen = function(mediaSource) { 282 if (this.sourceBuffer) 283 return; 284 this.sourceBuffer = mediaSource.addSourceBuffer(this.mimetype); 285 }; 286 287 StreamAppender.prototype.attemptAppend = function() { 288 if (this.xhr.readyState < this.xhr.LOADING) { 289 return; 290 } 291 292 if (!this.xhr.response || !this.sourceBuffer || this.appendStarted) 293 return; 294 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); 300 }; 301 302 StreamAppender.prototype.onUpdateEnd = function() { 303 this.appendEndTime = getPerfTimestamp(); 304 }; 305 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 311 // and append. 312 this.sourceBuffer.abort(); 313 this.xhr.abort(); 314 if (!this.appendEndTime) 315 this.appendEndTime = now; 316 317 if (!this.xhrEndTime) 318 this.xhrEndTime = now; 319 } 320 }; 321 322 StreamAppender.prototype.getXHRLoadDuration = function() { 323 return this.xhrEndTime - this.xhrStartTime; 324 }; 325 326 StreamAppender.prototype.getAppendDuration = function() { 327 return this.appendEndTime - this.appendStartTime; 328 }; 329 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; 336 337 for (var i = 0; i < appenders.length; ++i) { 338 appenders[i].start(); 339 } 340 341 function onSourceOpen(event) { 342 var mediaSource = event.target; 343 344 mediaSourceOpenEndTime = getPerfTimestamp(); 345 346 for (var i = 0; i < appenders.length; ++i) { 347 appenders[i].onSourceOpen(mediaSource); 348 } 349 350 for (var i = 0; i < appenders.length; ++i) { 351 appenders[i].attemptAppend(mediaSource); 352 } 353 354 mediaElement.play(); 355 } 356 357 var mediaSource; 358 if (window['MediaSource']) { 359 mediaSource = new window.MediaSource(); 360 mediaSource.addEventListener('sourceopen', onSourceOpen); 361 } else { 362 mediaSource = new window.WebKitMediaSource(); 363 mediaSource.addEventListener('webkitsourceopen', onSourceOpen); 364 } 365 366 var listener; 367 var timeout; 368 function checkForCurrentTimeChange() { 369 if (testDone) 370 return; 371 372 if (mediaElement.readyState < mediaElement.HAVE_METADATA || 373 mediaElement.currentTime <= 0) { 374 listener = window.requestAnimationFrame(checkForCurrentTimeChange); 375 return; 376 } 377 378 var testEndTime = getPerfTimestamp(); 379 for (var i = 0; i < appenders.length; ++i) { 380 appenders[i].onPlaybackStarted(mediaSource); 381 } 382 383 testDone = true; 384 window.clearInterval(listener); 385 window.clearTimeout(timeout); 386 387 var stats = {}; 388 stats.total = testEndTime - testStartTime; 389 stats.sourceOpen = mediaSourceOpenEndTime - mediaSourceOpenStartTime; 390 stats.maxXHRLoadDuration = appenders[0].getXHRLoadDuration(); 391 stats.maxAppendDuration = appenders[0].getAppendDuration(); 392 393 var timestamps = {}; 394 timestamps.testStartTime = testStartTime; 395 timestamps.testEndTime = testEndTime; 396 timestamps.mediaSourceOpenStartTime = mediaSourceOpenStartTime; 397 timestamps.mediaSourceOpenEndTime = mediaSourceOpenEndTime; 398 timestamps.appenders = []; 399 400 for (var i = 1; i < appenders.length; ++i) { 401 var appender = appenders[i]; 402 var xhrLoadDuration = appender.getXHRLoadDuration(); 403 var appendDuration = appender.getAppendDuration(); 404 405 if (xhrLoadDuration > stats.maxXHRLoadDuration) 406 stats.maxXHRLoadDuration = xhrLoadDuration; 407 408 if (appendDuration > stats.maxAppendDuration) 409 stats.maxAppendDuration = appendDuration; 410 } 411 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); 421 } 422 423 mediaElement.pause(); 424 425 pageEndTime = getPerfTimestamp(); 426 doneCallback(stats, timestamps); 427 }; 428 429 listener = window.requestAnimationFrame(checkForCurrentTimeChange); 430 431 timeout = setTimeout(function() { 432 if (testDone) 433 return; 434 435 testDone = true; 436 window.cancelAnimationFrame(listener); 437 438 mediaElement.pause(); 439 doneCallback(null); 440 EndTest("Test timed out."); 441 }, 10000); 442 443 mediaSourceOpenStartTime = getPerfTimestamp(); 444 mediaElement.src = URL.createObjectURL(mediaSource); 445 }; 446 447 function onBodyLoad() { 448 bodyLoadTime = getPerfTimestamp(); 449 450 if (!testParams.doNotWaitForBodyOnLoad) { 451 startTest(); 452 } 453 } 454 455 function startTest() { 456 updateControls(testParams); 457 458 var appenders = []; 459 460 if (testParams.useAppendStream && !window.MediaSource) 461 EndTest("Can't use appendStream() because the unprefixed MediaSource " + 462 "object is not present."); 463 464 var Appender = testParams.useAppendStream ? StreamAppender : BufferAppender; 465 466 if (testParams.testType.indexOf("A") != -1) { 467 appenders.push( 468 new Appender("audio/mp4; codecs=\"mp4a.40.2\"", 469 "audio.mp4", 470 "a", 471 testParams.startOffset, 472 testParams.appendSize)); 473 } 474 475 if (testParams.testType.indexOf("V") != -1) { 476 appenders.push( 477 new Appender("video/mp4; codecs=\"avc1.640028\"", 478 "video.mp4", 479 "v", 480 testParams.startOffset, 481 testParams.appendSize)); 482 } 483 484 var video = document.getElementById("v"); 485 video.addEventListener("error", function(e) { 486 console.log("video error!"); 487 EndTest("Video error: " + video.error); 488 }); 489 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."); 495 }); 496 } 497 498 function EndTest(msg) { 499 console.log("Ending test: " + msg); 500 window.__testDone = true; 501 } 502 503 function getTestID() { 504 var id = testParams.testType; 505 if (testParams.useAppendStream) 506 id += "_stream" 507 else 508 id += "_buffer" 509 if (testParams.doNotWaitForBodyOnLoad) 510 id += "_pre_load" 511 else 512 id += "_post_load" 513 return id; 514 } 515 516 function setupTest() { 517 loadTestParams(); 518 document.body.onload = onBodyLoad; 519 520 if (testParams.doNotWaitForBodyOnLoad) { 521 startTest(); 522 } 523 } 524 525 window["setupTest"] = setupTest; 526 window.__testDone = false; 527 window.__testMetrics = {}; 528 })(); 529