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']) 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