Home | History | Annotate | Download | only in mse_cases
      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