// 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; })();