|  | // Copyright 2013 The Chromium Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | // This file contains common utilities to find video/audio elements on a page | 
|  | // and collect metrics for each. | 
|  |  | 
|  | (function() { | 
|  | // MediaMetric class responsible for collecting metrics on a media element. | 
|  | // It attaches required event listeners in order to collect different metrics. | 
|  | function MediaMetricBase(element) { | 
|  | checkElementIsNotBound(element); | 
|  | this.metrics = {}; | 
|  | this.id = ''; | 
|  | this.element = element; | 
|  | } | 
|  |  | 
|  | MediaMetricBase.prototype.getMetrics = function() { | 
|  | return this.metrics; | 
|  | }; | 
|  |  | 
|  | MediaMetricBase.prototype.getSummary = function() { | 
|  | return { | 
|  | 'id': this.id, | 
|  | 'metrics': this.getMetrics() | 
|  | }; | 
|  | }; | 
|  |  | 
|  | function HTMLMediaMetric(element) { | 
|  | MediaMetricBase.prototype.constructor.call(this, element); | 
|  | // Set the basic event handlers for HTML5 media element. | 
|  | var metric = this; | 
|  | function onVideoLoad(event) { | 
|  | // If a 'Play' action is performed, then playback_timer != undefined. | 
|  | if (metric.playbackTimer == undefined) | 
|  | metric.playbackTimer = new Timer(); | 
|  | } | 
|  | // For the cases where autoplay=true, and without a 'play' action, we want | 
|  | // to start playbackTimer at 'play' or 'loadedmetadata' events. | 
|  | this.element.addEventListener('play', onVideoLoad); | 
|  | this.element.addEventListener('loadedmetadata', onVideoLoad); | 
|  | this.element.addEventListener('playing', function(e) { | 
|  | metric.onPlaying(e); | 
|  | }); | 
|  | this.element.addEventListener('ended', function(e) { | 
|  | metric.onEnded(e); | 
|  | }); | 
|  | this.setID(); | 
|  |  | 
|  | // Listen to when a Telemetry actions gets called. | 
|  | this.element.addEventListener('willPlay', function (e) { | 
|  | metric.onWillPlay(e); | 
|  | }, false); | 
|  | this.element.addEventListener('willSeek', function (e) { | 
|  | metric.onWillSeek(e); | 
|  | }, false); | 
|  | this.element.addEventListener('willLoop', function (e) { | 
|  | metric.onWillLoop(e); | 
|  | }, false); | 
|  | } | 
|  |  | 
|  | HTMLMediaMetric.prototype = new MediaMetricBase(); | 
|  | HTMLMediaMetric.prototype.constructor = HTMLMediaMetric; | 
|  |  | 
|  | HTMLMediaMetric.prototype.setID = function() { | 
|  | if (this.element.id) | 
|  | this.id = this.element.id; | 
|  | else if (this.element.src) | 
|  | this.id = this.element.src.substring(this.element.src.lastIndexOf("/")+1); | 
|  | else | 
|  | this.id = 'media_' + window.__globalCounter++; | 
|  | }; | 
|  |  | 
|  | HTMLMediaMetric.prototype.onWillPlay = function(e) { | 
|  | this.playbackTimer = new Timer(); | 
|  | }; | 
|  |  | 
|  | HTMLMediaMetric.prototype.onWillSeek = function(e) { | 
|  | var seekLabel = ''; | 
|  | if (e.seekLabel) | 
|  | seekLabel = '_' + e.seekLabel; | 
|  | var metric = this; | 
|  | var onSeeked = function(e) { | 
|  | metric.appendMetric('seek' + seekLabel, metric.seekTimer.stop()) | 
|  | e.target.removeEventListener('seeked', onSeeked); | 
|  | }; | 
|  | this.seekTimer = new Timer(); | 
|  | this.element.addEventListener('seeked', onSeeked); | 
|  | }; | 
|  |  | 
|  | HTMLMediaMetric.prototype.onWillLoop = function(e) { | 
|  | var loopTimer = new Timer(); | 
|  | var metric = this; | 
|  | var loopCount = e.loopCount; | 
|  | var onEndLoop = function(e) { | 
|  | var actualDuration = loopTimer.stop(); | 
|  | var idealDuration = metric.element.duration * loopCount; | 
|  | var avg_loop_time = (actualDuration - idealDuration) / loopCount; | 
|  | metric.metrics['avg_loop_time'] = | 
|  | Math.round(avg_loop_time * 1000) / 1000; | 
|  | e.target.removeEventListener('endLoop', onEndLoop); | 
|  | }; | 
|  | this.element.addEventListener('endLoop', onEndLoop); | 
|  | }; | 
|  |  | 
|  | HTMLMediaMetric.prototype.appendMetric = function(metric, value) { | 
|  | if (!this.metrics[metric]) | 
|  | this.metrics[metric] = []; | 
|  | this.metrics[metric].push(value); | 
|  | } | 
|  |  | 
|  | HTMLMediaMetric.prototype.onPlaying = function(event) { | 
|  | // Playing event can fire more than once if seeking. | 
|  | if (!this.metrics['time_to_play'] && this.playbackTimer) | 
|  | this.metrics['time_to_play'] = this.playbackTimer.stop(); | 
|  | }; | 
|  |  | 
|  | HTMLMediaMetric.prototype.onEnded = function(event) { | 
|  | var time_to_end = this.playbackTimer.stop() - this.metrics['time_to_play']; | 
|  | // TODO(shadi): Measure buffering time more accurately using events such as | 
|  | // stalled, waiting, progress, etc. This works only when continuous playback | 
|  | // is used. | 
|  | this.metrics['buffering_time'] = time_to_end - this.element.duration * 1000; | 
|  | }; | 
|  |  | 
|  | HTMLMediaMetric.prototype.getMetrics = function() { | 
|  | var decodedFrames = this.element.webkitDecodedFrameCount; | 
|  | var droppedFrames = this.element.webkitDroppedFrameCount; | 
|  | // Audio media does not report decoded/dropped frame count | 
|  | if (decodedFrames != undefined) | 
|  | this.metrics['decoded_frame_count'] = decodedFrames; | 
|  | if (droppedFrames != undefined) | 
|  | this.metrics['dropped_frame_count'] = droppedFrames; | 
|  | this.metrics['decoded_video_bytes'] = | 
|  | this.element.webkitVideoDecodedByteCount || 0; | 
|  | this.metrics['decoded_audio_bytes'] = | 
|  | this.element.webkitAudioDecodedByteCount || 0; | 
|  | return this.metrics; | 
|  | }; | 
|  |  | 
|  | function MediaMetric(element) { | 
|  | if (element instanceof HTMLMediaElement) | 
|  | return new HTMLMediaMetric(element); | 
|  | throw new Error('Unrecognized media element type.'); | 
|  | } | 
|  |  | 
|  | function Timer() { | 
|  | this.start_ = 0; | 
|  | this.start(); | 
|  | } | 
|  |  | 
|  | Timer.prototype = { | 
|  | start: function() { | 
|  | this.start_ = getCurrentTime(); | 
|  | }, | 
|  |  | 
|  | stop: function() { | 
|  | // Return delta time since start in millisecs. | 
|  | return Math.round((getCurrentTime() - this.start_) * 1000) / 1000; | 
|  | } | 
|  | }; | 
|  |  | 
|  | function checkElementIsNotBound(element) { | 
|  | if (!element) | 
|  | return; | 
|  | if (getMediaMetric(element)) | 
|  | throw new Error('Can not create MediaMetric for same element twice.'); | 
|  | } | 
|  |  | 
|  | function getMediaMetric(element) { | 
|  | for (var i = 0; i < window.__mediaMetrics.length; i++) { | 
|  | if (window.__mediaMetrics[i].element == element) | 
|  | return window.__mediaMetrics[i]; | 
|  | } | 
|  | return null; | 
|  | } | 
|  |  | 
|  | function createMediaMetricsForDocument() { | 
|  | // Searches for all video and audio elements on the page and creates a | 
|  | // corresponding media metric instance for each. | 
|  | var mediaElements = document.querySelectorAll('video, audio'); | 
|  | for (var i = 0; i < mediaElements.length; i++) | 
|  | window.__mediaMetrics.push(new MediaMetric(mediaElements[i])); | 
|  | } | 
|  |  | 
|  | function getCurrentTime() { | 
|  | if (window.performance) | 
|  | return (performance.now || | 
|  | performance.mozNow || | 
|  | performance.msNow || | 
|  | performance.oNow || | 
|  | performance.webkitNow).call(window.performance); | 
|  | else | 
|  | return Date.now(); | 
|  | } | 
|  |  | 
|  | function getAllMetrics() { | 
|  | // Returns a summary (info + metrics) for all media metrics. | 
|  | var metrics = []; | 
|  | for (var i = 0; i < window.__mediaMetrics.length; i++) | 
|  | metrics.push(window.__mediaMetrics[i].getSummary()); | 
|  | return metrics; | 
|  | } | 
|  |  | 
|  | window.__globalCounter = 0; | 
|  | window.__mediaMetrics = []; | 
|  | window.__getMediaMetric = getMediaMetric; | 
|  | window.__getAllMetrics = getAllMetrics; | 
|  | window.__createMediaMetricsForDocument = createMediaMetricsForDocument; | 
|  | })(); |