Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / tools / perf / metrics / media.js
blobb99b5b3957603208ad9bd3cf2ea2567366eda918
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.
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   }
18   MediaMetricBase.prototype.getMetrics = function() {
19     return this.metrics;
20   };
22   MediaMetricBase.prototype.getSummary = function() {
23     return {
24       'id': this.id,
25       'metrics': this.getMetrics()
26     };
27   };
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();
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   }
62   HTMLMediaMetric.prototype = new MediaMetricBase();
63   HTMLMediaMetric.prototype.constructor = HTMLMediaMetric;
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   };
74   HTMLMediaMetric.prototype.onWillPlay = function(e) {
75     this.playbackTimer = new Timer();
76   };
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   };
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   };
106   HTMLMediaMetric.prototype.appendMetric = function(metric, value) {
107     if (!this.metrics[metric])
108       this.metrics[metric] = [];
109     this.metrics[metric].push(value);
110   }
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   };
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   };
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   };
141   function MediaMetric(element) {
142     if (element instanceof HTMLMediaElement)
143       return new HTMLMediaMetric(element);
144     throw new Error('Unrecognized media element type.');
145   }
147   function Timer() {
148     this.start_ = 0;
149     this.start();
150   }
152   Timer.prototype = {
153     start: function() {
154       this.start_ = getCurrentTime();
155     },
157     stop: function() {
158       // Return delta time since start in millisecs.
159       return Math.round((getCurrentTime() - this.start_) * 1000) / 1000;
160     }
161   };
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   }
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   }
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   }
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   }
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   }
205   window.__globalCounter = 0;
206   window.__mediaMetrics = [];
207   window.__getMediaMetric = getMediaMetric;
208   window.__getAllMetrics = getAllMetrics;
209   window.__createMediaMetricsForDocument = createMediaMetricsForDocument;
210 })();