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// This file contains common utilities to find video/audio elements on a page
6// and collect metrics for each.
7
8(function() {
9  // MediaMetric class responsible for collecting metrics on a media element.
10  // It attaches required event listeners in order to collect different metrics.
11  function MediaMetricBase(element) {
12    checkElementIsNotBound(element);
13    this.metrics = {};
14    this.id = '';
15    this.element = element;
16  }
17
18  MediaMetricBase.prototype.getMetrics = function() {
19    return this.metrics;
20  };
21
22  MediaMetricBase.prototype.getSummary = function() {
23    return {
24      'id': this.id,
25      'metrics': this.getMetrics()
26    };
27  };
28
29  function HTMLMediaMetric(element) {
30    MediaMetricBase.prototype.constructor.call(this, element);
31    // Set the basic event handlers for HTML5 media element.
32    var metric = this;
33    function onVideoLoad(event) {
34      // If a 'Play' action is performed, then playback_timer != undefined.
35      if (metric.playbackTimer == undefined)
36        metric.playbackTimer = new Timer();
37    }
38    // For the cases where autoplay=true, and without a 'play' action, we want
39    // to start playbackTimer at 'play' or 'loadedmetadata' events.
40    this.element.addEventListener('play', onVideoLoad);
41    this.element.addEventListener('loadedmetadata', onVideoLoad);
42    this.element.addEventListener('playing', function(e) {
43        metric.onPlaying(e);
44      });
45    this.element.addEventListener('ended', function(e) {
46        metric.onEnded(e);
47      });
48    this.setID();
49
50    // Listen to when a Telemetry actions gets called.
51    this.element.addEventListener('willPlay', function (e) {
52        metric.onWillPlay(e);
53      }, false);
54    this.element.addEventListener('willSeek', function (e) {
55        metric.onWillSeek(e);
56      }, false);
57    this.element.addEventListener('willLoop', function (e) {
58        metric.onWillLoop(e);
59      }, false);
60  }
61
62  HTMLMediaMetric.prototype = new MediaMetricBase();
63  HTMLMediaMetric.prototype.constructor = HTMLMediaMetric;
64
65  HTMLMediaMetric.prototype.setID = function() {
66    if (this.element.id)
67      this.id = this.element.id;
68    else if (this.element.src)
69      this.id = this.element.src.substring(this.element.src.lastIndexOf("/")+1);
70    else
71      this.id = 'media_' + window.__globalCounter++;
72  };
73
74  HTMLMediaMetric.prototype.onWillPlay = function(e) {
75    this.playbackTimer = new Timer();
76  };
77
78  HTMLMediaMetric.prototype.onWillSeek = function(e) {
79    var seekLabel = '';
80    if (e.seekLabel)
81      seekLabel = '_' + e.seekLabel;
82    var metric = this;
83    var onSeeked = function(e) {
84        metric.appendMetric('seek' + seekLabel, metric.seekTimer.stop())
85        e.target.removeEventListener('seeked', onSeeked);
86      };
87    this.seekTimer = new Timer();
88    this.element.addEventListener('seeked', onSeeked);
89  };
90
91  HTMLMediaMetric.prototype.onWillLoop = function(e) {
92    var loopTimer = new Timer();
93    var metric = this;
94    var loopCount = e.loopCount;
95    var onEndLoop = function(e) {
96        var actualDuration = loopTimer.stop();
97        var idealDuration = metric.element.duration * loopCount;
98        var avg_loop_time = (actualDuration - idealDuration) / loopCount;
99        metric.metrics['avg_loop_time'] =
100            Math.round(avg_loop_time * 1000) / 1000;
101        e.target.removeEventListener('endLoop', onEndLoop);
102      };
103    this.element.addEventListener('endLoop', onEndLoop);
104  };
105
106  HTMLMediaMetric.prototype.appendMetric = function(metric, value) {
107    if (!this.metrics[metric])
108      this.metrics[metric] = [];
109    this.metrics[metric].push(value);
110  }
111
112  HTMLMediaMetric.prototype.onPlaying = function(event) {
113    // Playing event can fire more than once if seeking.
114    if (!this.metrics['time_to_play'] && this.playbackTimer)
115      this.metrics['time_to_play'] = this.playbackTimer.stop();
116  };
117
118  HTMLMediaMetric.prototype.onEnded = function(event) {
119    var time_to_end = this.playbackTimer.stop() - this.metrics['time_to_play'];
120    // TODO(shadi): Measure buffering time more accurately using events such as
121    // stalled, waiting, progress, etc. This works only when continuous playback
122    // is used.
123    this.metrics['buffering_time'] = time_to_end - this.element.duration * 1000;
124  };
125
126  HTMLMediaMetric.prototype.getMetrics = function() {
127    var decodedFrames = this.element.webkitDecodedFrameCount;
128    var droppedFrames = this.element.webkitDroppedFrameCount;
129    // Audio media does not report decoded/dropped frame count
130    if (decodedFrames != undefined)
131      this.metrics['decoded_frame_count'] = decodedFrames;
132    if (droppedFrames != undefined)
133      this.metrics['dropped_frame_count'] = droppedFrames;
134    this.metrics['decoded_video_bytes'] =
135        this.element.webkitVideoDecodedByteCount || 0;
136    this.metrics['decoded_audio_bytes'] =
137        this.element.webkitAudioDecodedByteCount || 0;
138    return this.metrics;
139  };
140
141  function MediaMetric(element) {
142    if (element instanceof HTMLMediaElement)
143      return new HTMLMediaMetric(element);
144    throw new Error('Unrecognized media element type.');
145  }
146
147  function Timer() {
148    this.start_ = 0;
149    this.start();
150  }
151
152  Timer.prototype = {
153    start: function() {
154      this.start_ = getCurrentTime();
155    },
156
157    stop: function() {
158      // Return delta time since start in millisecs.
159      return Math.round((getCurrentTime() - this.start_) * 1000) / 1000;
160    }
161  };
162
163  function checkElementIsNotBound(element) {
164    if (!element)
165      return;
166    if (getMediaMetric(element))
167      throw new Error('Can not create MediaMetric for same element twice.');
168  }
169
170  function getMediaMetric(element) {
171    for (var i = 0; i < window.__mediaMetrics.length; i++) {
172      if (window.__mediaMetrics[i].element == element)
173        return window.__mediaMetrics[i];
174    }
175    return null;
176  }
177
178  function createMediaMetricsForDocument() {
179    // Searches for all video and audio elements on the page and creates a
180    // corresponding media metric instance for each.
181    var mediaElements = document.querySelectorAll('video, audio');
182    for (var i = 0; i < mediaElements.length; i++)
183      window.__mediaMetrics.push(new MediaMetric(mediaElements[i]));
184  }
185
186  function getCurrentTime() {
187    if (window.performance)
188      return (performance.now ||
189              performance.mozNow ||
190              performance.msNow ||
191              performance.oNow ||
192              performance.webkitNow).call(window.performance);
193    else
194      return Date.now();
195  }
196
197  function getAllMetrics() {
198    // Returns a summary (info + metrics) for all media metrics.
199    var metrics = [];
200    for (var i = 0; i < window.__mediaMetrics.length; i++)
201      metrics.push(window.__mediaMetrics[i].getSummary());
202    return metrics;
203  }
204
205  window.__globalCounter = 0;
206  window.__mediaMetrics = [];
207  window.__getMediaMetric = getMediaMetric;
208  window.__getAllMetrics = getAllMetrics;
209  window.__createMediaMetricsForDocument = createMediaMetricsForDocument;
210})();
211