Pull player metrics out of the player. Support safari errors/buffering events
This commit is contained in:
parent
d972a9ee8a
commit
c50536ff81
@ -36,8 +36,17 @@ func ReportPlaybackMetrics(w http.ResponseWriter, r *http.Request) {
|
|||||||
clientID := utils.GenerateClientIDFromRequest(r)
|
clientID := utils.GenerateClientIDFromRequest(r)
|
||||||
|
|
||||||
metrics.RegisterPlaybackErrorCount(clientID, request.Errors)
|
metrics.RegisterPlaybackErrorCount(clientID, request.Errors)
|
||||||
metrics.RegisterPlayerBandwidth(clientID, request.Bandwidth)
|
if request.Bandwidth != 0.0 {
|
||||||
metrics.RegisterPlayerLatency(clientID, request.Latency)
|
metrics.RegisterPlayerBandwidth(clientID, request.Bandwidth)
|
||||||
metrics.RegisterPlayerSegmentDownloadDuration(clientID, request.DownloadDuration)
|
}
|
||||||
|
|
||||||
|
if request.Latency != 0.0 {
|
||||||
|
metrics.RegisterPlayerLatency(clientID, request.Latency)
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.DownloadDuration != 0.0 {
|
||||||
|
metrics.RegisterPlayerSegmentDownloadDuration(clientID, request.DownloadDuration)
|
||||||
|
}
|
||||||
|
|
||||||
metrics.RegisterQualityVariantChangesCount(clientID, request.QualityVariantChanges)
|
metrics.RegisterQualityVariantChangesCount(clientID, request.QualityVariantChanges)
|
||||||
}
|
}
|
||||||
|
@ -39,45 +39,13 @@ const VIDEO_OPTIONS = {
|
|||||||
export const POSTER_DEFAULT = `/img/logo.png`;
|
export const POSTER_DEFAULT = `/img/logo.png`;
|
||||||
export const POSTER_THUMB = `/thumbnail.jpg`;
|
export const POSTER_THUMB = `/thumbnail.jpg`;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
class OwncastPlayer {
|
class OwncastPlayer {
|
||||||
constructor() {
|
constructor() {
|
||||||
window.VIDEOJS_NO_DYNAMIC_STYLE = true; // style override
|
window.VIDEOJS_NO_DYNAMIC_STYLE = true; // style override
|
||||||
|
|
||||||
this.playbackMetrics = new PlaybackMetrics();
|
|
||||||
|
|
||||||
this.vjsPlayer = null;
|
this.vjsPlayer = null;
|
||||||
this.latencyCompensator = null;
|
this.latencyCompensator = null;
|
||||||
|
this.playbackMetrics = null;
|
||||||
|
|
||||||
this.appPlayerReadyCallback = null;
|
this.appPlayerReadyCallback = null;
|
||||||
this.appPlayerPlayingCallback = null;
|
this.appPlayerPlayingCallback = null;
|
||||||
@ -91,8 +59,6 @@ class OwncastPlayer {
|
|||||||
this.handleVolume = this.handleVolume.bind(this);
|
this.handleVolume = this.handleVolume.bind(this);
|
||||||
this.handleEnded = this.handleEnded.bind(this);
|
this.handleEnded = this.handleEnded.bind(this);
|
||||||
this.handleError = this.handleError.bind(this);
|
this.handleError = this.handleError.bind(this);
|
||||||
this.handleWaiting = this.handleWaiting.bind(this);
|
|
||||||
this.handleNoLongerBuffering = this.handleNoLongerBuffering.bind(this);
|
|
||||||
this.addQualitySelector = this.addQualitySelector.bind(this);
|
this.addQualitySelector = this.addQualitySelector.bind(this);
|
||||||
this.qualitySelectionMenu = null;
|
this.qualitySelectionMenu = null;
|
||||||
}
|
}
|
||||||
@ -101,26 +67,6 @@ class OwncastPlayer {
|
|||||||
this.addAirplay();
|
this.addAirplay();
|
||||||
this.addQualitySelector();
|
this.addQualitySelector();
|
||||||
|
|
||||||
// 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.playbackMetrics.trackSegmentDownloadTime(delta);
|
|
||||||
cb(request, error, response);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return oldVjsXhrCallback(...args);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add a cachebuster param to playlist URLs.
|
// Add a cachebuster param to playlist URLs.
|
||||||
videojs.Vhs.xhr.beforeRequest = (options) => {
|
videojs.Vhs.xhr.beforeRequest = (options) => {
|
||||||
if (options.uri.match('m3u8')) {
|
if (options.uri.match('m3u8')) {
|
||||||
@ -156,37 +102,27 @@ class OwncastPlayer {
|
|||||||
console.warn(err);
|
console.warn(err);
|
||||||
}
|
}
|
||||||
this.vjsPlayer.src(source);
|
this.vjsPlayer.src(source);
|
||||||
// this.vjsPlayer.play();
|
}
|
||||||
|
|
||||||
|
setupPlaybackMetrics() {
|
||||||
|
this.playbackMetrics = new PlaybackMetrics(this.vjsPlayer, videojs);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupLatencyCompensator() {
|
||||||
|
this.latencyCompensator = new LatencyCompensator(this.vjsPlayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReady() {
|
handleReady() {
|
||||||
|
console.log('handleReady');
|
||||||
this.vjsPlayer.on('error', this.handleError);
|
this.vjsPlayer.on('error', this.handleError);
|
||||||
this.vjsPlayer.on('playing', this.handlePlaying);
|
this.vjsPlayer.on('playing', this.handlePlaying);
|
||||||
this.vjsPlayer.on('waiting', this.handleWaiting);
|
|
||||||
this.vjsPlayer.on('canplaythrough', this.handleNoLongerBuffering);
|
|
||||||
this.vjsPlayer.on('volumechange', this.handleVolume);
|
this.vjsPlayer.on('volumechange', this.handleVolume);
|
||||||
this.vjsPlayer.on('ended', this.handleEnded);
|
this.vjsPlayer.on('ended', this.handleEnded);
|
||||||
|
|
||||||
this.vjsPlayer.on('ready', () => {
|
this.vjsPlayer.on('loadeddata', () => {
|
||||||
const tech = this.vjsPlayer.tech({ IWillNotUseThisInPlugins: true });
|
console.log('player loadeddata event');
|
||||||
tech.on('usage', (e) => {
|
this.setupPlaybackMetrics();
|
||||||
if (e.name === 'vhs-unknown-waiting') {
|
this.setupLatencyCompensator();
|
||||||
this.playbackMetrics.incrementErrorCount(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.name === 'vhs-rendition-change-abr') {
|
|
||||||
// Quality variant has changed
|
|
||||||
this.playbackMetrics.incrementQualityVariantChanges();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Variant changed
|
|
||||||
const trackElements = this.vjsPlayer.textTracks();
|
|
||||||
trackElements.addEventListener('cuechange', function (c) {
|
|
||||||
console.log(c);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.latencyCompensator = new LatencyCompensator(this.vjsPlayer);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.appPlayerReadyCallback) {
|
if (this.appPlayerReadyCallback) {
|
||||||
@ -205,7 +141,6 @@ class OwncastPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlePlaying() {
|
handlePlaying() {
|
||||||
this.log('on Playing');
|
|
||||||
if (this.appPlayerPlayingCallback) {
|
if (this.appPlayerPlayingCallback) {
|
||||||
// start polling
|
// start polling
|
||||||
this.appPlayerPlayingCallback();
|
this.appPlayerPlayingCallback();
|
||||||
@ -216,30 +151,6 @@ class OwncastPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.hasStartedPlayback = true;
|
this.hasStartedPlayback = true;
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
this.collectPlaybackMetrics();
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
collectPlaybackMetrics() {
|
|
||||||
const tech = this.vjsPlayer.tech({ IWillNotUseThisInPlugins: true });
|
|
||||||
if (!tech || !tech.vhs) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bandwidth = tech.vhs.systemBandwidth;
|
|
||||||
this.playbackMetrics.trackBandwidth(bandwidth);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const segment = getCurrentlyPlayingSegment(tech);
|
|
||||||
const segmentTime = segment.dateTimeObject.getTime();
|
|
||||||
const now = new Date().getTime();
|
|
||||||
const latency = now - segmentTime;
|
|
||||||
this.playbackMetrics.trackLatency(latency);
|
|
||||||
} catch (err) {
|
|
||||||
// console.warn(err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEnded() {
|
handleEnded() {
|
||||||
@ -256,17 +167,6 @@ class OwncastPlayer {
|
|||||||
if (this.appPlayerEndedCallback) {
|
if (this.appPlayerEndedCallback) {
|
||||||
this.appPlayerEndedCallback();
|
this.appPlayerEndedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.playbackMetrics.incrementErrorCount(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleWaiting(e) {
|
|
||||||
this.playbackMetrics.incrementErrorCount(1);
|
|
||||||
this.playbackMetrics.setIsBuffering(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNoLongerBuffering() {
|
|
||||||
this.playbackMetrics.setIsBuffering(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log(message) {
|
log(message) {
|
||||||
|
@ -2,7 +2,9 @@ import { URL_PLAYBACK_METRICS } from '../utils/constants.js';
|
|||||||
const METRICS_SEND_INTERVAL = 10000;
|
const METRICS_SEND_INTERVAL = 10000;
|
||||||
|
|
||||||
class PlaybackMetrics {
|
class PlaybackMetrics {
|
||||||
constructor() {
|
constructor(player, videojs) {
|
||||||
|
this.player = player;
|
||||||
|
this.supportsDetailedMetrics = false;
|
||||||
this.hasPerformedInitialVariantChange = false;
|
this.hasPerformedInitialVariantChange = false;
|
||||||
|
|
||||||
this.segmentDownloadTime = [];
|
this.segmentDownloadTime = [];
|
||||||
@ -12,12 +14,96 @@ class PlaybackMetrics {
|
|||||||
this.qualityVariantChanges = 0;
|
this.qualityVariantChanges = 0;
|
||||||
this.isBuffering = false;
|
this.isBuffering = false;
|
||||||
this.bufferingDurationTimer = 0;
|
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(() => {
|
setInterval(() => {
|
||||||
this.send();
|
this.send();
|
||||||
}, METRICS_SEND_INTERVAL);
|
}, 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) {
|
incrementErrorCount(count) {
|
||||||
this.errors += count;
|
this.errors += count;
|
||||||
}
|
}
|
||||||
@ -57,34 +143,61 @@ class PlaybackMetrics {
|
|||||||
this.latencyTracking.push(latency);
|
this.latencyTracking.push(latency);
|
||||||
}
|
}
|
||||||
|
|
||||||
async send() {
|
collectPlaybackMetrics() {
|
||||||
if (
|
const tech = this.player.tech({ IWillNotUseThisInPlugins: true });
|
||||||
this.segmentDownloadTime.length < 4 ||
|
if (!tech || !tech.vhs) {
|
||||||
this.bandwidthTracking.length < 4
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bandwidth = tech.vhs.systemBandwidth;
|
||||||
|
this.trackBandwidth(bandwidth);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async send() {
|
||||||
const errorCount = this.errors;
|
const errorCount = this.errors;
|
||||||
const average = (arr) => arr.reduce((p, c) => p + c, 0) / arr.length;
|
|
||||||
|
|
||||||
const averageDownloadDuration = average(this.segmentDownloadTime) / 1000;
|
var data;
|
||||||
const roundedAverageDownloadDuration =
|
if (this.supportsDetailedMetrics) {
|
||||||
Math.round(averageDownloadDuration * 1000) / 1000;
|
const average = (arr) => arr.reduce((p, c) => p + c, 0) / arr.length;
|
||||||
|
|
||||||
const averageBandwidth = average(this.bandwidthTracking) / 1000;
|
const averageDownloadDuration = average(this.segmentDownloadTime) / 1000;
|
||||||
const roundedAverageBandwidth = Math.round(averageBandwidth * 1000) / 1000;
|
const roundedAverageDownloadDuration =
|
||||||
|
Math.round(averageDownloadDuration * 1000) / 1000;
|
||||||
|
|
||||||
const averageLatency = average(this.latencyTracking) / 1000;
|
const averageBandwidth = average(this.bandwidthTracking) / 1000;
|
||||||
const roundedAverageLatency = Math.round(averageLatency * 1000) / 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.errors = 0;
|
||||||
this.qualityVariantChanges = 0;
|
this.qualityVariantChanges = 0;
|
||||||
this.segmentDownloadTime = [];
|
this.segmentDownloadTime = [];
|
||||||
@ -104,9 +217,38 @@ class PlaybackMetrics {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PlaybackMetrics;
|
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;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user