Home | History | Annotate | Download | only in metrics
      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'] && this.playbackTimer)
    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