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