mirror of
https://github.com/owncast/owncast.git
synced 2024-10-10 19:16:02 +00:00
Add playback performance metrics. Closes #1930
This commit is contained in:
parent
04597908a5
commit
221b9c8f0f
@ -82,6 +82,11 @@ export const fatalErrorStateAtom = atom<DisplayableError>({
|
|||||||
default: null,
|
default: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const clockSkewAtom = atom<Number>({
|
||||||
|
key: 'clockSkewAtom',
|
||||||
|
default: 0.0,
|
||||||
|
});
|
||||||
|
|
||||||
// Chat is visible if the user wishes it to be visible AND the required
|
// Chat is visible if the user wishes it to be visible AND the required
|
||||||
// chat state is set.
|
// chat state is set.
|
||||||
export const isChatVisibleSelector = selector({
|
export const isChatVisibleSelector = selector({
|
||||||
@ -132,6 +137,7 @@ export function ClientConfigStore() {
|
|||||||
const setChatDisplayName = useSetRecoilState<string>(chatDisplayNameAtom);
|
const setChatDisplayName = useSetRecoilState<string>(chatDisplayNameAtom);
|
||||||
const setClientConfig = useSetRecoilState<ClientConfig>(clientConfigStateAtom);
|
const setClientConfig = useSetRecoilState<ClientConfig>(clientConfigStateAtom);
|
||||||
const setServerStatus = useSetRecoilState<ServerStatus>(serverStatusState);
|
const setServerStatus = useSetRecoilState<ServerStatus>(serverStatusState);
|
||||||
|
const setClockSkew = useSetRecoilState<Number>(clockSkewAtom);
|
||||||
const [chatMessages, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessagesAtom);
|
const [chatMessages, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessagesAtom);
|
||||||
const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
|
const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
|
||||||
const setAppState = useSetRecoilState<AppStateOptions>(appStateAtom);
|
const setAppState = useSetRecoilState<AppStateOptions>(appStateAtom);
|
||||||
@ -170,6 +176,10 @@ export function ClientConfigStore() {
|
|||||||
try {
|
try {
|
||||||
const status = await ServerStatusService.getStatus();
|
const status = await ServerStatusService.getStatus();
|
||||||
setServerStatus(status);
|
setServerStatus(status);
|
||||||
|
const { serverTime } = status;
|
||||||
|
|
||||||
|
const clockSkew = new Date(serverTime).getTime() - Date.now();
|
||||||
|
setClockSkew(clockSkew);
|
||||||
|
|
||||||
if (status.online) {
|
if (status.online) {
|
||||||
sendEvent(AppStateEvent.Online);
|
sendEvent(AppStateEvent.Online);
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import VideoJS from './player';
|
import VideoJS from './player';
|
||||||
import ViewerPing from './viewer-ping';
|
import ViewerPing from './viewer-ping';
|
||||||
import VideoPoster from './VideoPoster';
|
import VideoPoster from './VideoPoster';
|
||||||
import { getLocalStorage, setLocalStorage } from '../../utils/localStorage';
|
import { getLocalStorage, setLocalStorage } from '../../utils/localStorage';
|
||||||
import { isVideoPlayingAtom } from '../stores/ClientConfigStore';
|
import { isVideoPlayingAtom, clockSkewAtom } from '../stores/ClientConfigStore';
|
||||||
|
import PlaybackMetrics from './metrics/playback';
|
||||||
|
|
||||||
const PLAYER_VOLUME = 'owncast_volume';
|
const PLAYER_VOLUME = 'owncast_volume';
|
||||||
|
|
||||||
const ping = new ViewerPing();
|
const ping = new ViewerPing();
|
||||||
|
let playbackMetrics = null;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
source: string;
|
source: string;
|
||||||
@ -20,6 +22,7 @@ export default function OwncastPlayer(props: Props) {
|
|||||||
const playerRef = React.useRef(null);
|
const playerRef = React.useRef(null);
|
||||||
const { source, online } = props;
|
const { source, online } = props;
|
||||||
const [videoPlaying, setVideoPlaying] = useRecoilState<boolean>(isVideoPlayingAtom);
|
const [videoPlaying, setVideoPlaying] = useRecoilState<boolean>(isVideoPlayingAtom);
|
||||||
|
const clockSkew = useRecoilValue<Number>(clockSkewAtom);
|
||||||
|
|
||||||
const setSavedVolume = () => {
|
const setSavedVolume = () => {
|
||||||
try {
|
try {
|
||||||
@ -113,7 +116,7 @@ export default function OwncastPlayer(props: Props) {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePlayerReady = player => {
|
const handlePlayerReady = (player, videojs) => {
|
||||||
playerRef.current = player;
|
playerRef.current = player;
|
||||||
|
|
||||||
setSavedVolume();
|
setSavedVolume();
|
||||||
@ -147,8 +150,17 @@ export default function OwncastPlayer(props: Props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
player.on('volumechange', handleVolume);
|
player.on('volumechange', handleVolume);
|
||||||
|
|
||||||
|
playbackMetrics = new PlaybackMetrics(player, videojs);
|
||||||
|
playbackMetrics.setClockSkew(clockSkew);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (playbackMetrics) {
|
||||||
|
playbackMetrics.setClockSkew(clockSkew);
|
||||||
|
}
|
||||||
|
}, [clockSkew]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'grid' }}>
|
<div style={{ display: 'grid' }}>
|
||||||
{online && (
|
{online && (
|
||||||
|
@ -1,7 +1,30 @@
|
|||||||
import { URL_PLAYBACK_METRICS } from '../utils/constants.js';
|
/* eslint-disable no-plusplus */
|
||||||
|
const URL_PLAYBACK_METRICS = `/api/metrics/playback`;
|
||||||
const METRICS_SEND_INTERVAL = 10000;
|
const METRICS_SEND_INTERVAL = 10000;
|
||||||
const MAX_VALID_LATENCY_SECONDS = 40; // Anything > this gets thrown out.
|
const MAX_VALID_LATENCY_SECONDS = 40; // Anything > this gets thrown out.
|
||||||
|
|
||||||
|
function getCurrentlyPlayingSegment(tech) {
|
||||||
|
const targetMedia = tech.vhs.playlists.media();
|
||||||
|
const snapshotTime = tech.currentTime();
|
||||||
|
let segment;
|
||||||
|
|
||||||
|
// Iterate trough available segments and get first within which snapshot_time is
|
||||||
|
// eslint-disable-next-line no-plusplus
|
||||||
|
for (let i = 0, l = targetMedia.segments.length; i < l; i++) {
|
||||||
|
// Note: segment.end may be undefined or is not properly set
|
||||||
|
if (snapshotTime < targetMedia.segments[i].end) {
|
||||||
|
segment = targetMedia.segments[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!segment) {
|
||||||
|
[segment] = targetMedia.segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
return segment;
|
||||||
|
}
|
||||||
|
|
||||||
class PlaybackMetrics {
|
class PlaybackMetrics {
|
||||||
constructor(player, videojs) {
|
constructor(player, videojs) {
|
||||||
this.player = player;
|
this.player = player;
|
||||||
@ -37,11 +60,13 @@ class PlaybackMetrics {
|
|||||||
const oldVjsXhrCallback = videojs.xhr;
|
const oldVjsXhrCallback = videojs.xhr;
|
||||||
|
|
||||||
// Override the xhr function to track segment download time.
|
// Override the xhr function to track segment download time.
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
videojs.Vhs.xhr = (...args) => {
|
videojs.Vhs.xhr = (...args) => {
|
||||||
if (args[0].uri.match('.ts')) {
|
if (args[0].uri.match('.ts')) {
|
||||||
const start = new Date();
|
const start = new Date();
|
||||||
|
|
||||||
const cb = args[1];
|
const cb = args[1];
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
args[1] = (request, error, response) => {
|
args[1] = (request, error, response) => {
|
||||||
const end = new Date();
|
const end = new Date();
|
||||||
const delta = end.getTime() - start.getTime();
|
const delta = end.getTime() - start.getTime();
|
||||||
@ -70,7 +95,7 @@ class PlaybackMetrics {
|
|||||||
const tech = this.player.tech({ IWillNotUseThisInPlugins: true });
|
const tech = this.player.tech({ IWillNotUseThisInPlugins: true });
|
||||||
this.supportsDetailedMetrics = !!tech;
|
this.supportsDetailedMetrics = !!tech;
|
||||||
|
|
||||||
tech.on('usage', (e) => {
|
tech.on('usage', e => {
|
||||||
if (e.name === 'vhs-unknown-waiting') {
|
if (e.name === 'vhs-unknown-waiting') {
|
||||||
this.setIsBuffering(true);
|
this.setIsBuffering(true);
|
||||||
}
|
}
|
||||||
@ -83,7 +108,7 @@ class PlaybackMetrics {
|
|||||||
|
|
||||||
// Variant changed
|
// Variant changed
|
||||||
const trackElements = this.player.textTracks();
|
const trackElements = this.player.textTracks();
|
||||||
trackElements.addEventListener('cuechange', (c) => {
|
trackElements.addEventListener('cuechange', () => {
|
||||||
this.incrementQualityVariantChanges();
|
this.incrementQualityVariantChanges();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -99,7 +124,7 @@ class PlaybackMetrics {
|
|||||||
clearInterval(this.collectPlaybackMetricsTimer);
|
clearInterval(this.collectPlaybackMetricsTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBuffering(e) {
|
handleBuffering() {
|
||||||
this.incrementErrorCount(1);
|
this.incrementErrorCount(1);
|
||||||
this.setIsBuffering(true);
|
this.setIsBuffering(true);
|
||||||
}
|
}
|
||||||
@ -199,19 +224,22 @@ class PlaybackMetrics {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we're paused then do nothing.
|
||||||
|
if (this.player.paused()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const errorCount = this.errors;
|
const errorCount = this.errors;
|
||||||
|
|
||||||
var data;
|
let data;
|
||||||
if (this.supportsDetailedMetrics) {
|
if (this.supportsDetailedMetrics) {
|
||||||
const average = (arr) => arr.reduce((p, c) => p + c, 0) / arr.length;
|
const average = arr => arr.reduce((p, c) => p + c, 0) / arr.length;
|
||||||
|
|
||||||
const averageDownloadDuration = average(this.segmentDownloadTime) / 1000;
|
const averageDownloadDuration = average(this.segmentDownloadTime) / 1000;
|
||||||
const roundedAverageDownloadDuration =
|
const roundedAverageDownloadDuration = Math.round(averageDownloadDuration * 1000) / 1000;
|
||||||
Math.round(averageDownloadDuration * 1000) / 1000;
|
|
||||||
|
|
||||||
const averageBandwidth = average(this.bandwidthTracking) / 1000;
|
const averageBandwidth = average(this.bandwidthTracking) / 1000;
|
||||||
const roundedAverageBandwidth =
|
const roundedAverageBandwidth = Math.round(averageBandwidth * 1000) / 1000;
|
||||||
Math.round(averageBandwidth * 1000) / 1000;
|
|
||||||
|
|
||||||
const averageLatency = average(this.latencyTracking) / 1000;
|
const averageLatency = average(this.latencyTracking) / 1000;
|
||||||
const roundedAverageLatency = Math.round(averageLatency * 1000) / 1000;
|
const roundedAverageLatency = Math.round(averageLatency * 1000) / 1000;
|
||||||
@ -252,26 +280,3 @@ class PlaybackMetrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Iterate 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 = target_media.segments[0];
|
|
||||||
segment_time = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return segment;
|
|
||||||
}
|
|
@ -9,7 +9,7 @@ require('video.js/dist/video-js.css');
|
|||||||
// import { PLAYER_VOLUME, URL_STREAM } from '../../utils/constants.js';
|
// import { PLAYER_VOLUME, URL_STREAM } from '../../utils/constants.js';
|
||||||
interface Props {
|
interface Props {
|
||||||
options: any;
|
options: any;
|
||||||
onReady: (player: videojs.Player) => void;
|
onReady: (player: videojs.Player, vjsInstance: videojs) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VideoJS(props: Props) {
|
export function VideoJS(props: Props) {
|
||||||
@ -25,7 +25,7 @@ export function VideoJS(props: Props) {
|
|||||||
// eslint-disable-next-line no-multi-assign
|
// eslint-disable-next-line no-multi-assign
|
||||||
const player = (playerRef.current = videojs(videoElement, options, () => {
|
const player = (playerRef.current = videojs(videoElement, options, () => {
|
||||||
player.log('player is ready');
|
player.log('player is ready');
|
||||||
return onReady && onReady(player);
|
return onReady && onReady(player, videojs);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// TODO: Add airplay support, video settings menu, latency compensator, etc.
|
// TODO: Add airplay support, video settings menu, latency compensator, etc.
|
||||||
|
@ -5,11 +5,13 @@ export interface ServerStatus {
|
|||||||
lastDisconnectTime?: Date;
|
lastDisconnectTime?: Date;
|
||||||
versionNumber?: string;
|
versionNumber?: string;
|
||||||
streamTitle?: string;
|
streamTitle?: string;
|
||||||
|
serverTime: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeEmptyServerStatus(): ServerStatus {
|
export function makeEmptyServerStatus(): ServerStatus {
|
||||||
return {
|
return {
|
||||||
online: false,
|
online: false,
|
||||||
viewerCount: 0,
|
viewerCount: 0,
|
||||||
|
serverTime: new Date(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user