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.
5 // This file contains common utilities to find video/audio elements on a page
6 // and collect metrics for each.
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);
15 this.element = element;
18 MediaMetricBase.prototype.getMetrics = function() {
22 MediaMetricBase.prototype.getSummary = function() {
25 'metrics': this.getMetrics()
29 function HTMLMediaMetric(element) {
30 MediaMetricBase.prototype.constructor.call(this, element);
31 // Set the basic event handlers for HTML5 media element.
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();
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) {
45 this.element.addEventListener('ended', function(e) {
50 // Listen to when a Telemetry actions gets called.
51 this.element.addEventListener('willPlay', function (e) {
54 this.element.addEventListener('willSeek', function (e) {
57 this.element.addEventListener('willLoop', function (e) {
62 HTMLMediaMetric.prototype = new MediaMetricBase();
63 HTMLMediaMetric.prototype.constructor = HTMLMediaMetric;
65 HTMLMediaMetric.prototype.setID = function() {
67 this.id = this.element.id;
68 else if (this.element.src)
69 this.id = this.element.src.substring(this.element.src.lastIndexOf("/")+1);
71 this.id = 'media_' + window.__globalCounter++;
74 HTMLMediaMetric.prototype.onWillPlay = function(e) {
75 this.playbackTimer = new Timer();
78 HTMLMediaMetric.prototype.onWillSeek = function(e) {
81 seekLabel = '_' + e.seekLabel;
83 var onSeeked = function(e) {
84 metric.appendMetric('seek' + seekLabel, metric.seekTimer.stop())
85 e.target.removeEventListener('seeked', onSeeked);
87 this.seekTimer = new Timer();
88 this.element.addEventListener('seeked', onSeeked);
91 HTMLMediaMetric.prototype.onWillLoop = function(e) {
92 var loopTimer = new Timer();
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);
103 this.element.addEventListener('endLoop', onEndLoop);
106 HTMLMediaMetric.prototype.appendMetric = function(metric, value) {
107 if (!this.metrics[metric])
108 this.metrics[metric] = [];
109 this.metrics[metric].push(value);
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();
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
123 this.metrics['buffering_time'] = time_to_end - this.element.duration * 1000;
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;
141 function MediaMetric(element) {
142 if (element instanceof HTMLMediaElement)
143 return new HTMLMediaMetric(element);
144 throw new Error('Unrecognized media element type.');
154 this.start_ = getCurrentTime();
158 // Return delta time since start in millisecs.
159 return Math.round((getCurrentTime() - this.start_) * 1000) / 1000;
163 function checkElementIsNotBound(element) {
166 if (getMediaMetric(element))
167 throw new Error('Can not create MediaMetric for same element twice.');
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];
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]));
186 function getCurrentTime() {
187 if (window.performance)
188 return (performance.now ||
189 performance.mozNow ||
192 performance.webkitNow).call(window.performance);
197 function getAllMetrics() {
198 // Returns a summary (info + metrics) for all media metrics.
200 for (var i = 0; i < window.__mediaMetrics.length; i++)
201 metrics.push(window.__mediaMetrics[i].getSummary());
205 window.__globalCounter = 0;
206 window.__mediaMetrics = [];
207 window.__getMediaMetric = getMediaMetric;
208 window.__getAllMetrics = getAllMetrics;
209 window.__createMediaMetricsForDocument = createMediaMetricsForDocument;