Skip to content

Commit

Permalink
feat(dashviewer): Video performance instrumentation (#1071)
Browse files Browse the repository at this point in the history
* chore(dashviewer): video playback performance instrumentation

* chore(dashviewer): emit metric on beforeunload event

* chore: adding missing jsdoc

* chore: moved loadUI around and fixed unit tests

* chore: addressing PR comments

* chore: moving loadUI in DashViewer back

* chore: adding new unit test coverage

* chore: fix CI unit test issue

* chore: attempt to fix CI test issue #2

* chore: attempt #3 to fix unit tests

* chore: add mediaEl check in processMetrics

* chore: addressing a few more PR comments

* chore: removing seeked from quickSeek

* chore: more PR comments
  • Loading branch information
ConradJChan authored and mergify[bot] committed Sep 17, 2019
1 parent 09b22cf commit ed492c1
Show file tree
Hide file tree
Showing 7 changed files with 410 additions and 46 deletions.
14 changes: 14 additions & 0 deletions src/lib/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,17 @@ export const USER_DOCUMENT_THUMBNAIL_EVENTS = {
};

export const MISSING_EXTERNAL_REFS = 'missing_x_refs';

export const MEDIA_METRIC = {
bufferFill: 'bufferFill',
duration: 'duration',
lagRatio: 'lagRatio',
seeked: 'seeked',
totalBufferLag: 'totalBufferLag',
watchLength: 'watchLength',
};

export const MEDIA_METRIC_EVENTS = {
bufferFill: 'media_metric_buffer_fill',
endPlayback: 'media_metric_end_playback',
};
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe('lib/viewers/box3d/video360/Video360Viewer', () => {
options.container = containerEl;
options.location = {};
viewer = new Video360Viewer(options);
sandbox.stub(viewer, 'processMetrics');
});

afterEach(() => {
Expand Down
93 changes: 86 additions & 7 deletions src/lib/viewers/media/DashViewer.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import VideoBaseViewer from './VideoBaseViewer';
import PreviewError from '../../PreviewError';
import fullscreen from '../../Fullscreen';
import Timer from '../../Timer';
import { appendQueryParams, getProp } from '../../util';
import { getRepresentation } from '../../file';
import { MEDIA_STATIC_ASSETS_VERSION } from '../../constants';
import getLanguageName from '../../lang';
import { ERROR_CODE, VIEWER_EVENT } from '../../events';
import { ERROR_CODE, VIEWER_EVENT, MEDIA_METRIC, MEDIA_METRIC_EVENTS } from '../../events';
import './Dash.scss';

const CSS_CLASS_DASH = 'bp-media-dash';
Expand All @@ -30,14 +31,15 @@ class DashViewer extends VideoBaseViewer {

this.api = options.api;
// Bind context for callbacks
this.loadeddataHandler = this.loadeddataHandler.bind(this);
this.adaptationHandler = this.adaptationHandler.bind(this);
this.shakaErrorHandler = this.shakaErrorHandler.bind(this);
this.requestFilter = this.requestFilter.bind(this);
this.handleBuffering = this.handleBuffering.bind(this);
this.getBandwidthInterval = this.getBandwidthInterval.bind(this);
this.handleAudioTrack = this.handleAudioTrack.bind(this);
this.handleQuality = this.handleQuality.bind(this);
this.handleSubtitle = this.handleSubtitle.bind(this);
this.handleAudioTrack = this.handleAudioTrack.bind(this);
this.getBandwidthInterval = this.getBandwidthInterval.bind(this);
this.loadeddataHandler = this.loadeddataHandler.bind(this);
this.requestFilter = this.requestFilter.bind(this);
this.shakaErrorHandler = this.shakaErrorHandler.bind(this);
}

/**
Expand Down Expand Up @@ -106,7 +108,8 @@ class DashViewer extends VideoBaseViewer {
load() {
this.mediaUrl = this.options.representation.content.url_template;
this.watermarkCacheBust = Date.now();
this.mediaEl.addEventListener('loadeddata', this.loadeddataHandler);

this.addEventListenersForMediaLoad();

return Promise.all([this.loadAssets(this.getJSAssets()), this.getRepStatus().getPromise()])
.then(() => {
Expand Down Expand Up @@ -161,6 +164,7 @@ class DashViewer extends VideoBaseViewer {
this.player = new shaka.Player(this.mediaEl);
this.player.addEventListener('adaptation', this.adaptationHandler);
this.player.addEventListener('error', this.shakaErrorHandler);
this.player.addEventListener('buffering', this.handleBuffering);
this.player.configure({
abr: {
enabled: false,
Expand All @@ -182,6 +186,81 @@ class DashViewer extends VideoBaseViewer {
this.player.load(this.mediaUrl, this.startTimeInSeconds).catch(this.shakaErrorHandler);
}

/**
* Handles the buffering events from shaka player
*
* @see {@link https://shaka-player-demo.appspot.com/docs/api/shaka.Player.html#.event:BufferingEvent}
* @param {Object} object - BufferingEvent object
* @param {boolean} object.buffering - Indicates whether the player is buffering or not
*/
handleBuffering({ buffering }) {
const tag = this.createTimerTag(MEDIA_METRIC.totalBufferLag);

if (buffering) {
Timer.start(tag);
} else {
Timer.stop(tag);
this.metrics[MEDIA_METRIC.totalBufferLag] += Timer.get(tag).elapsed;
Timer.reset(tag);
}
}

/**
* Processes the buffer fill metric which represents the initial buffer time before playback begins
* @override
* @emits MEDIA_METRIC_EVENTS.bufferFill
* @return {void}
*/
processBufferFillMetric() {
const tag = this.createTimerTag(MEDIA_METRIC.bufferFill);
const bufferFill = Timer.get(tag).elapsed;
this.metrics[MEDIA_METRIC.bufferFill] = bufferFill;

this.emitMetric(MEDIA_METRIC_EVENTS.bufferFill, bufferFill);
}

/**
* Processes the media playback metrics
* @override
* @emits MEDIA_METRIC_EVENTS.endPlayback
* @return {void}
*/
processMetrics() {
if (!this.loaded) {
return;
}

const totalBufferLag = this.metrics[MEDIA_METRIC.totalBufferLag];
const watchLength = this.determineWatchLength();

this.metrics[MEDIA_METRIC.totalBufferLag] = totalBufferLag;
this.metrics[MEDIA_METRIC.lagRatio] = totalBufferLag / watchLength;
this.metrics[MEDIA_METRIC.duration] = this.mediaEl ? this.mediaEl.duration * 1000 : 0;
this.metrics[MEDIA_METRIC.watchLength] = watchLength;

this.emitMetric(MEDIA_METRIC_EVENTS.endPlayback, { ...this.metrics });
}

/**
* Determines the watch length, or how much of the media was consumed
* @return {number} - The watch length in milliseconds
*/
determineWatchLength() {
if (!this.mediaEl || !this.mediaEl.played) {
return -1;
}

const playedParts = this.mediaEl.played;
let playLength = 0;
for (let i = 0; i < playedParts.length; i += 1) {
const start = playedParts.start(i);
const end = playedParts.end(i);
playLength += end - start;
}

return playLength * 1000;
}

/**
* A networking filter to append representation URLs with tokens
* Manifest type will use an asset name. Segments will not.
Expand Down
Loading

0 comments on commit ed492c1

Please sign in to comment.