|
|
|
@ -2,7 +2,9 @@ import { URL_PLAYBACK_METRICS } from '../utils/constants.js';
@@ -2,7 +2,9 @@ import { URL_PLAYBACK_METRICS } from '../utils/constants.js';
|
|
|
|
|
const METRICS_SEND_INTERVAL = 10000; |
|
|
|
|
|
|
|
|
|
class PlaybackMetrics { |
|
|
|
|
constructor() { |
|
|
|
|
constructor(player, videojs) { |
|
|
|
|
this.player = player; |
|
|
|
|
this.supportsDetailedMetrics = false; |
|
|
|
|
this.hasPerformedInitialVariantChange = false; |
|
|
|
|
|
|
|
|
|
this.segmentDownloadTime = []; |
|
|
|
@ -12,12 +14,96 @@ class PlaybackMetrics {
@@ -12,12 +14,96 @@ class PlaybackMetrics {
|
|
|
|
|
this.qualityVariantChanges = 0; |
|
|
|
|
this.isBuffering = false; |
|
|
|
|
this.bufferingDurationTimer = 0; |
|
|
|
|
this.collectPlaybackMetricsTimer = 0; |
|
|
|
|
|
|
|
|
|
this.videoJSReady = this.videoJSReady.bind(this); |
|
|
|
|
this.handlePlaying = this.handlePlaying.bind(this); |
|
|
|
|
this.handleBuffering = this.handleBuffering.bind(this); |
|
|
|
|
this.handleEnded = this.handleEnded.bind(this); |
|
|
|
|
this.handleError = this.handleError.bind(this); |
|
|
|
|
this.collectPlaybackMetrics = this.collectPlaybackMetrics.bind(this); |
|
|
|
|
this.handleNoLongerBuffering = this.handleNoLongerBuffering.bind(this); |
|
|
|
|
|
|
|
|
|
this.player.on('canplaythrough', this.handleNoLongerBuffering); |
|
|
|
|
this.player.on('error', this.handleError); |
|
|
|
|
this.player.on('stalled', this.handleBuffering); |
|
|
|
|
this.player.on('waiting', this.handleBuffering); |
|
|
|
|
this.player.on('playing', this.handlePlaying); |
|
|
|
|
this.player.on('ended', this.handleEnded); |
|
|
|
|
|
|
|
|
|
// Keep a reference of the standard vjs xhr function.
|
|
|
|
|
const oldVjsXhrCallback = videojs.xhr; |
|
|
|
|
|
|
|
|
|
// Override the xhr function to track segment download time.
|
|
|
|
|
videojs.Vhs.xhr = (...args) => { |
|
|
|
|
if (args[0].uri.match('.ts')) { |
|
|
|
|
const start = new Date(); |
|
|
|
|
|
|
|
|
|
const cb = args[1]; |
|
|
|
|
args[1] = (request, error, response) => { |
|
|
|
|
const end = new Date(); |
|
|
|
|
const delta = end.getTime() - start.getTime(); |
|
|
|
|
this.trackSegmentDownloadTime(delta); |
|
|
|
|
cb(request, error, response); |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return oldVjsXhrCallback(...args); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
this.videoJSReady(); |
|
|
|
|
|
|
|
|
|
setInterval(() => { |
|
|
|
|
this.send(); |
|
|
|
|
}, METRICS_SEND_INTERVAL); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
videoJSReady() { |
|
|
|
|
const tech = this.player.tech({ IWillNotUseThisInPlugins: true }); |
|
|
|
|
this.supportsDetailedMetrics = !!tech; |
|
|
|
|
|
|
|
|
|
tech.on('usage', (e) => { |
|
|
|
|
if (e.name === 'vhs-unknown-waiting') { |
|
|
|
|
this.setIsBuffering(true); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (e.name === 'vhs-rendition-change-abr') { |
|
|
|
|
// Quality variant has changed
|
|
|
|
|
this.incrementQualityVariantChanges(); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Variant changed
|
|
|
|
|
const trackElements = this.player.textTracks(); |
|
|
|
|
trackElements.addEventListener('cuechange', (c) => { |
|
|
|
|
this.incrementQualityVariantChanges(); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
handlePlaying() { |
|
|
|
|
clearInterval(this.collectPlaybackMetricsTimer); |
|
|
|
|
this.collectPlaybackMetricsTimer = setInterval(() => { |
|
|
|
|
this.collectPlaybackMetrics(); |
|
|
|
|
}, 5000); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
handleEnded() { |
|
|
|
|
clearInterval(this.collectPlaybackMetricsTimer); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
handleBuffering(e) { |
|
|
|
|
this.incrementErrorCount(1); |
|
|
|
|
this.setIsBuffering(true); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
handleNoLongerBuffering() { |
|
|
|
|
this.setIsBuffering(false); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
handleError() { |
|
|
|
|
this.incrementErrorCount(1); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
incrementErrorCount(count) { |
|
|
|
|
this.errors += count; |
|
|
|
|
} |
|
|
|
@ -57,34 +143,61 @@ class PlaybackMetrics {
@@ -57,34 +143,61 @@ class PlaybackMetrics {
|
|
|
|
|
this.latencyTracking.push(latency); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async send() { |
|
|
|
|
if ( |
|
|
|
|
this.segmentDownloadTime.length < 4 || |
|
|
|
|
this.bandwidthTracking.length < 4 |
|
|
|
|
) { |
|
|
|
|
collectPlaybackMetrics() { |
|
|
|
|
const tech = this.player.tech({ IWillNotUseThisInPlugins: true }); |
|
|
|
|
if (!tech || !tech.vhs) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const errorCount = this.errors; |
|
|
|
|
const average = (arr) => arr.reduce((p, c) => p + c, 0) / arr.length; |
|
|
|
|
const bandwidth = tech.vhs.systemBandwidth; |
|
|
|
|
this.trackBandwidth(bandwidth); |
|
|
|
|
|
|
|
|
|
const averageDownloadDuration = average(this.segmentDownloadTime) / 1000; |
|
|
|
|
const roundedAverageDownloadDuration = |
|
|
|
|
Math.round(averageDownloadDuration * 1000) / 1000; |
|
|
|
|
try { |
|
|
|
|
const segment = getCurrentlyPlayingSegment(tech); |
|
|
|
|
if (!segment || !segment.dateTimeObject) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const segmentTime = segment.dateTimeObject.getTime(); |
|
|
|
|
const now = new Date().getTime(); |
|
|
|
|
const latency = now - segmentTime; |
|
|
|
|
this.trackLatency(latency); |
|
|
|
|
} catch (err) { |
|
|
|
|
console.warn(err); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const averageBandwidth = average(this.bandwidthTracking) / 1000; |
|
|
|
|
const roundedAverageBandwidth = Math.round(averageBandwidth * 1000) / 1000; |
|
|
|
|
async send() { |
|
|
|
|
const errorCount = this.errors; |
|
|
|
|
|
|
|
|
|
const averageLatency = average(this.latencyTracking) / 1000; |
|
|
|
|
const roundedAverageLatency = Math.round(averageLatency * 1000) / 1000; |
|
|
|
|
var data; |
|
|
|
|
if (this.supportsDetailedMetrics) { |
|
|
|
|
const average = (arr) => arr.reduce((p, c) => p + c, 0) / arr.length; |
|
|
|
|
|
|
|
|
|
const averageDownloadDuration = average(this.segmentDownloadTime) / 1000; |
|
|
|
|
const roundedAverageDownloadDuration = |
|
|
|
|
Math.round(averageDownloadDuration * 1000) / 1000; |
|
|
|
|
|
|
|
|
|
const averageBandwidth = average(this.bandwidthTracking) / 1000; |
|
|
|
|
const roundedAverageBandwidth = |
|
|
|
|
Math.round(averageBandwidth * 1000) / 1000; |
|
|
|
|
|
|
|
|
|
const averageLatency = average(this.latencyTracking) / 1000; |
|
|
|
|
const roundedAverageLatency = Math.round(averageLatency * 1000) / 1000; |
|
|
|
|
|
|
|
|
|
data = { |
|
|
|
|
bandwidth: roundedAverageBandwidth, |
|
|
|
|
latency: roundedAverageLatency, |
|
|
|
|
downloadDuration: roundedAverageDownloadDuration, |
|
|
|
|
errors: errorCount + this.isBuffering ? 1 : 0, |
|
|
|
|
qualityVariantChanges: this.qualityVariantChanges, |
|
|
|
|
}; |
|
|
|
|
} else { |
|
|
|
|
data = { |
|
|
|
|
errors: errorCount + this.isBuffering ? 1 : 0, |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const data = { |
|
|
|
|
bandwidth: roundedAverageBandwidth, |
|
|
|
|
latency: roundedAverageLatency, |
|
|
|
|
downloadDuration: roundedAverageDownloadDuration, |
|
|
|
|
errors: errorCount + this.isBuffering ? 1 : 0, |
|
|
|
|
qualityVariantChanges: this.qualityVariantChanges, |
|
|
|
|
}; |
|
|
|
|
this.errors = 0; |
|
|
|
|
this.qualityVariantChanges = 0; |
|
|
|
|
this.segmentDownloadTime = []; |
|
|
|
@ -104,9 +217,38 @@ class PlaybackMetrics {
@@ -104,9 +217,38 @@ class PlaybackMetrics {
|
|
|
|
|
} catch (e) { |
|
|
|
|
console.error(e); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// console.log(data);
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export default PlaybackMetrics; |
|
|
|
|
|
|
|
|
|
function getCurrentlyPlayingSegment(tech, old_segment = null) { |
|
|
|
|
var target_media = tech.vhs.playlists.media(); |
|
|
|
|
var snapshot_time = tech.currentTime(); |
|
|
|
|
|
|
|
|
|
var segment; |
|
|
|
|
var segment_time; |
|
|
|
|
|
|
|
|
|
// Itinerate trough available segments and get first within which snapshot_time is
|
|
|
|
|
for (var i = 0, l = target_media.segments.length; i < l; i++) { |
|
|
|
|
// Note: segment.end may be undefined or is not properly set
|
|
|
|
|
if (snapshot_time < target_media.segments[i].end) { |
|
|
|
|
segment = target_media.segments[i]; |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Null segment_time in case it's lower then 0.
|
|
|
|
|
if (segment) { |
|
|
|
|
segment_time = Math.max( |
|
|
|
|
0, |
|
|
|
|
snapshot_time - (segment.end - segment.duration) |
|
|
|
|
); |
|
|
|
|
// Because early segments don't have end property
|
|
|
|
|
} else { |
|
|
|
|
segment = target_media.segments[0]; |
|
|
|
|
segment_time = 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return segment; |
|
|
|
|
} |
|
|
|
|