From fd3c2e87051f5029cdec39d877b576a62f48e219 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 12 Aug 2022 16:41:29 +0200 Subject: Add playback metric endpoint sent to OTEL --- .../player/shared/control-bar/p2p-info-button.ts | 4 +- .../manager-options/manager-options-builder.ts | 8 ++ client/src/assets/player/shared/metrics/index.ts | 1 + .../assets/player/shared/metrics/metrics-plugin.ts | 128 +++++++++++++++++++++ .../p2p-media-loader/p2p-media-loader-plugin.ts | 23 ++-- .../player/shared/peertube/peertube-plugin.ts | 4 + .../src/assets/player/shared/stats/stats-card.ts | 4 +- .../player/shared/webtorrent/webtorrent-plugin.ts | 6 +- 8 files changed, 159 insertions(+), 19 deletions(-) create mode 100644 client/src/assets/player/shared/metrics/index.ts create mode 100644 client/src/assets/player/shared/metrics/metrics-plugin.ts (limited to 'client/src/assets/player/shared') diff --git a/client/src/assets/player/shared/control-bar/p2p-info-button.ts b/client/src/assets/player/shared/control-bar/p2p-info-button.ts index 36517e125..1979654ad 100644 --- a/client/src/assets/player/shared/control-bar/p2p-info-button.ts +++ b/client/src/assets/player/shared/control-bar/p2p-info-button.ts @@ -87,9 +87,9 @@ class P2pInfoButton extends Button { const httpStats = data.http const downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed) - const uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed) + const uploadSpeed = bytes(p2pStats.uploadSpeed) const totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded) - const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded) + const totalUploaded = bytes(p2pStats.uploaded) const numPeers = p2pStats.numPeers subDivWebtorrent.title = this.player().localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' diff --git a/client/src/assets/player/shared/manager-options/manager-options-builder.ts b/client/src/assets/player/shared/manager-options/manager-options-builder.ts index bc70bb12f..07678493d 100644 --- a/client/src/assets/player/shared/manager-options/manager-options-builder.ts +++ b/client/src/assets/player/shared/manager-options/manager-options-builder.ts @@ -44,6 +44,14 @@ export class ManagerOptionsBuilder { 'isLive', 'videoUUID' ]) + }, + metrics: { + mode: this.mode, + + ...pick(commonOptions, [ + 'metricsUrl', + 'videoUUID' + ]) } } diff --git a/client/src/assets/player/shared/metrics/index.ts b/client/src/assets/player/shared/metrics/index.ts new file mode 100644 index 000000000..85d75cdc7 --- /dev/null +++ b/client/src/assets/player/shared/metrics/index.ts @@ -0,0 +1 @@ +export * from './metrics-plugin' diff --git a/client/src/assets/player/shared/metrics/metrics-plugin.ts b/client/src/assets/player/shared/metrics/metrics-plugin.ts new file mode 100644 index 000000000..1b2349eba --- /dev/null +++ b/client/src/assets/player/shared/metrics/metrics-plugin.ts @@ -0,0 +1,128 @@ +import videojs from 'video.js' +import { PlaybackMetricCreate } from '../../../../../../shared/models' +import { MetricsPluginOptions, PlayerMode, PlayerNetworkInfo } from '../../types' + +const Plugin = videojs.getPlugin('plugin') + +class MetricsPlugin extends Plugin { + private readonly metricsUrl: string + private readonly videoUUID: string + private readonly mode: PlayerMode + + private downloadedBytesP2P = 0 + private downloadedBytesHTTP = 0 + private uploadedBytesP2P = 0 + + private resolutionChanges = 0 + private errors = 0 + + private lastPlayerNetworkInfo: PlayerNetworkInfo + + private metricsInterval: any + + private readonly CONSTANTS = { + METRICS_INTERVAL: 15000 + } + + constructor (player: videojs.Player, options: MetricsPluginOptions) { + super(player) + + this.metricsUrl = options.metricsUrl + this.videoUUID = options.videoUUID + this.mode = options.mode + + this.player.one('play', () => { + this.runMetricsInterval() + + this.trackBytes() + this.trackResolutionChange() + this.trackErrors() + }) + } + + dispose () { + if (this.metricsInterval) clearInterval(this.metricsInterval) + } + + private runMetricsInterval () { + this.metricsInterval = setInterval(() => { + let resolution: number + let fps: number + + if (this.mode === 'p2p-media-loader') { + const level = this.player.p2pMediaLoader().getCurrentLevel() + if (!level) return + + resolution = Math.min(level.height || 0, level.width || 0) + + const framerate = level?.attrs['FRAME-RATE'] + fps = framerate + ? parseInt(framerate, 10) + : undefined + } else { // webtorrent + const videoFile = this.player.webtorrent().getCurrentVideoFile() + if (!videoFile) return + + resolution = videoFile.resolution.id + fps = videoFile.fps + } + + const body: PlaybackMetricCreate = { + resolution, + fps, + + playerMode: this.mode, + + resolutionChanges: this.resolutionChanges, + + errors: this.errors, + + downloadedBytesP2P: this.downloadedBytesP2P, + downloadedBytesHTTP: this.downloadedBytesHTTP, + + uploadedBytesP2P: this.uploadedBytesP2P, + + videoId: this.videoUUID + } + + this.resolutionChanges = 0 + + this.downloadedBytesP2P = 0 + this.downloadedBytesHTTP = 0 + + this.uploadedBytesP2P = 0 + + this.errors = 0 + + const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' }) + + return fetch(this.metricsUrl, { method: 'POST', body: JSON.stringify(body), headers }) + }, this.CONSTANTS.METRICS_INTERVAL) + } + + private trackBytes () { + this.player.on('p2pInfo', (_event, data: PlayerNetworkInfo) => { + this.downloadedBytesHTTP += data.http.downloaded - (this.lastPlayerNetworkInfo?.http.downloaded || 0) + this.downloadedBytesP2P += data.p2p.downloaded - (this.lastPlayerNetworkInfo?.p2p.downloaded || 0) + + this.uploadedBytesP2P += data.p2p.uploaded - (this.lastPlayerNetworkInfo?.p2p.uploaded || 0) + + this.lastPlayerNetworkInfo = data + }) + } + + private trackResolutionChange () { + this.player.on('engineResolutionChange', () => { + this.resolutionChanges++ + }) + } + + private trackErrors () { + this.player.on('error', () => { + this.errors++ + }) + } +} + +videojs.registerPlugin('metrics', MetricsPlugin) +export { MetricsPlugin } diff --git a/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts index e5f099dea..54d87aea5 100644 --- a/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts +++ b/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts @@ -2,10 +2,10 @@ import Hlsjs from 'hls.js' import videojs from 'video.js' import { Events, Segment } from '@peertube/p2p-media-loader-core' import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from '@peertube/p2p-media-loader-hlsjs' +import { logger } from '@root-helpers/logger' import { timeToInt } from '@shared/core-utils' import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../../types' import { registerConfigPlugin, registerSourceHandler } from './hls-plugin' -import { logger } from '@root-helpers/logger' registerConfigPlugin(videojs) registerSourceHandler(videojs) @@ -29,9 +29,7 @@ class P2pMediaLoaderPlugin extends Plugin { } private statsHTTPBytes = { pendingDownload: [] as number[], - pendingUpload: [] as number[], - totalDownload: 0, - totalUpload: 0 + totalDownload: 0 } private startTime: number @@ -123,6 +121,8 @@ class P2pMediaLoaderPlugin extends Plugin { this.statsP2PBytes.numPeers = 1 + this.options.redundancyUrlManager.countBaseUrls() this.runStats() + + this.hlsjs.on(Hlsjs.Events.LEVEL_SWITCHED, () => this.player.trigger('engineResolutionChange')) } private runStats () { @@ -134,10 +134,13 @@ class P2pMediaLoaderPlugin extends Plugin { }) this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, _segment, bytes: number) => { - const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes + if (method !== 'p2p') { + logger.error(`Received upload from unknown method ${method}`) + return + } - elem.pendingUpload.push(bytes) - elem.totalUpload += bytes + this.statsP2PBytes.pendingUpload.push(bytes) + this.statsP2PBytes.totalUpload += bytes }) this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++) @@ -148,20 +151,16 @@ class P2pMediaLoaderPlugin extends Plugin { const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload) const httpDownloadSpeed = this.arraySum(this.statsHTTPBytes.pendingDownload) - const httpUploadSpeed = this.arraySum(this.statsHTTPBytes.pendingUpload) this.statsP2PBytes.pendingDownload = [] this.statsP2PBytes.pendingUpload = [] this.statsHTTPBytes.pendingDownload = [] - this.statsHTTPBytes.pendingUpload = [] return this.player.trigger('p2pInfo', { source: 'p2p-media-loader', http: { downloadSpeed: httpDownloadSpeed, - uploadSpeed: httpUploadSpeed, - downloaded: this.statsHTTPBytes.totalDownload, - uploaded: this.statsHTTPBytes.totalUpload + downloaded: this.statsHTTPBytes.totalDownload }, p2p: { downloadSpeed: p2pDownloadSpeed, diff --git a/client/src/assets/player/shared/peertube/peertube-plugin.ts b/client/src/assets/player/shared/peertube/peertube-plugin.ts index 69a7b2d65..83c32415e 100644 --- a/client/src/assets/player/shared/peertube/peertube-plugin.ts +++ b/client/src/assets/player/shared/peertube/peertube-plugin.ts @@ -144,6 +144,8 @@ class PeerTubePlugin extends Plugin { this.listenFullScreenChange() } + // --------------------------------------------------------------------------- + private runUserViewing () { let lastCurrentTime = this.startTime let lastViewEvent: VideoViewEvent @@ -205,6 +207,8 @@ class PeerTubePlugin extends Plugin { return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers }) } + // --------------------------------------------------------------------------- + private listenFullScreenChange () { this.player.on('fullscreenchange', () => { if (this.player.isFullscreen()) this.player.focus() diff --git a/client/src/assets/player/shared/stats/stats-card.ts b/client/src/assets/player/shared/stats/stats-card.ts index b65adcfca..1199d3285 100644 --- a/client/src/assets/player/shared/stats/stats-card.ts +++ b/client/src/assets/player/shared/stats/stats-card.ts @@ -95,9 +95,9 @@ class StatsCard extends Component { const httpStats = data.http this.playerNetworkInfo.downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed).join(' ') - this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed).join(' ') + this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed).join(' ') this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ') - this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ') + this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded).join(' ') this.playerNetworkInfo.numPeers = p2pStats.numPeers this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s' diff --git a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts index 9fd5f593e..fa3f48a9a 100644 --- a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts +++ b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts @@ -204,6 +204,8 @@ class WebTorrentPlugin extends Plugin { } this.updateVideoFile(newVideoFile, options) + + this.player.trigger('engineResolutionChange') } flushVideoFile (videoFile: VideoFile, destroyRenderer = true) { @@ -506,9 +508,7 @@ class WebTorrentPlugin extends Plugin { source: 'webtorrent', http: { downloadSpeed: 0, - uploadSpeed: 0, - downloaded: 0, - uploaded: 0 + downloaded: 0 }, p2p: { downloadSpeed: this.torrent.downloadSpeed, -- cgit v1.2.3