diff --git a/scripts/common.js b/scripts/common.js index 7cb4537..8c7114b 100644 --- a/scripts/common.js +++ b/scripts/common.js @@ -242,14 +242,7 @@ webrtcperf.unregisterServiceWorkers = () => { } webrtcperf.MeasuredStats = window.MeasuredStats = class { - constructor( - { ttl, maxItems, secondsPerSample, storeId } = { - ttl: 0, - maxItems: 0, - secondsPerSample: 1, - storeId: '', - }, - ) { + constructor({ ttl = 0, maxItems = 0, secondsPerSample = 1, storeId = '' }) { /** @type number */ this.ttl = ttl /** @type number */ @@ -264,6 +257,8 @@ webrtcperf.MeasuredStats = window.MeasuredStats = class { this.statsSum = 0 /** @type number */ this.statsCount = 0 + this.statsMin = undefined + this.statsMax = undefined // Restore from localStorage. this.load() } @@ -279,6 +274,8 @@ webrtcperf.MeasuredStats = window.MeasuredStats = class { stats: this.stats, statsSum: this.statsSum, statsCount: this.statsCount, + statsMin: this.statsMin, + statsMax: this.statsMax, }), ) } catch (err) { @@ -293,10 +290,12 @@ webrtcperf.MeasuredStats = window.MeasuredStats = class { try { const data = localStorage.getItem(`webrtcperf-MeasuredStats-${this.storeId}`) if (data) { - const { stats, statsSum, statsCount } = JSON.parse(data) + const { stats, statsSum, statsCount, statsMin, statsMax } = JSON.parse(data) this.stats = stats this.statsSum = statsSum this.statsCount = statsCount + this.statsMin = statsMin + this.statsMax = statsMax } } catch (err) { log(`MeasuredStats load error: ${err.message}`) @@ -307,6 +306,8 @@ webrtcperf.MeasuredStats = window.MeasuredStats = class { this.stats = [] this.statsSum = 0 this.statsCount = 0 + this.statsMin = undefined + this.statsMax = undefined this.store() } @@ -348,21 +349,25 @@ webrtcperf.MeasuredStats = window.MeasuredStats = class { * @param {number} value */ push(timestamp, value) { - const last = this.stats[this.stats.length - 1] - if (last && timestamp - last.timestamp < this.secondsPerSample * 1000) { - last.value += value - last.count += 1 - } else { - this.stats.push({ timestamp, value, count: 1 }) + if (timestamp !== undefined && value !== undefined) { + const last = this.stats[this.stats.length - 1] + if (last && timestamp - last.timestamp < this.secondsPerSample * 1000) { + last.value += value + last.count += 1 + } else { + this.stats.push({ timestamp, value, count: 1 }) + } + this.statsSum += value + this.statsCount += 1 + if (this.statsMin === undefined || value < this.statsMin) this.statsMin = value + if (this.statsMax === undefined || value > this.statsMax) this.statsMax = value } - this.statsSum += value - this.statsCount += 1 this.purge() } /** * mean - * @returns {number | undefined} mean value + * @returns {number | undefined} The mean value. */ mean() { this.purge() @@ -372,6 +377,14 @@ webrtcperf.MeasuredStats = window.MeasuredStats = class { get size() { return this.statsCount } + + get min() { + return this.statsMin + } + + get max() { + return this.statsMax + } } /** diff --git a/scripts/peer-connection.js b/scripts/peer-connection.js index ee5884e..442192e 100644 --- a/scripts/peer-connection.js +++ b/scripts/peer-connection.js @@ -15,11 +15,14 @@ webrtcperf.Timer = class { this.duration = 0 this.lastTime = 0 this.timer = null + this.startEvents = 0 + this.stopEvents = 0 } start() { if (this.timer) return this.lastTime = Date.now() + this.startEvents++ this.timer = setInterval(() => { const now = Date.now() this.duration += (now - this.lastTime) / 1000 @@ -35,6 +38,7 @@ webrtcperf.Timer = class { this.duration += (Date.now() - this.lastTime) / 1000 this.lastTime = 0 } + this.stopEvents++ } } webrtcperf.OnOffTimer = class { diff --git a/scripts/video-stats.js b/scripts/video-stats.js new file mode 100644 index 0000000..c2e29dc --- /dev/null +++ b/scripts/video-stats.js @@ -0,0 +1,96 @@ +/* global webrtcperf */ + +webrtcperf.videoStats = { + collectedVideos: new Map(), + bufferedTime: new webrtcperf.MeasuredStats({ ttl: 15 }), + width: new webrtcperf.MeasuredStats({ ttl: 15 }), + height: new webrtcperf.MeasuredStats({ ttl: 15 }), + playingTime: new webrtcperf.MeasuredStats({ ttl: 15 }), + bufferingTime: new webrtcperf.MeasuredStats({ ttl: 15 }), + bufferingEvents: new webrtcperf.MeasuredStats({ ttl: 15 }), + + scheduleNext(timeout = 2000) { + setTimeout(() => { + try { + this.update() + } catch (e) { + webrtcperf.log('VideoStats error', e) + } + }, timeout) + }, + watchVideo(video) { + if (this.collectedVideos.has(video)) return + webrtcperf.log('VideoStats watchVideo', video) + const playingTimer = new webrtcperf.Timer() + const bufferingTimer = new webrtcperf.Timer() + this.collectedVideos.set(video, { playingTimer, bufferingTimer }) + video.addEventListener('playing', () => { + playingTimer.start() + bufferingTimer.stop() + }) + video.addEventListener('waiting', () => { + playingTimer.stop() + bufferingTimer.start() + }) + }, + update() { + const now = Date.now() + document.querySelectorAll('video').forEach(el => this.watchVideo(el)) + const entries = Array.from(this.collectedVideos.entries()).filter(([video]) => !!video.src && !video.ended) + const arrayAverage = cb => + entries.length + ? entries.reduce((acc, entry) => { + return acc + cb(...entry) + }, 0) / entries.length + : undefined + + this.bufferedTime.push( + now, + arrayAverage(video => { + if (video.buffered.length) { + return Math.max(video.buffered.end(video.buffered.length - 1) - video.currentTime, 0) + } + return 0 + }), + ) + this.width.push( + now, + arrayAverage(video => video.videoWidth), + ) + this.height.push( + now, + arrayAverage(video => video.videoHeight), + ) + this.playingTime.push( + now, + arrayAverage((video, stats) => stats.playingTimer.duration), + ) + this.bufferingTime.push( + now, + arrayAverage((video, stats) => stats.bufferingTimer.duration), + ) + this.bufferingEvents.push( + now, + arrayAverage((video, stats) => stats.bufferingTimer.startEvents), + ) + this.scheduleNext() + }, + collect() { + return { + bufferedTime: this.bufferedTime.mean(), + width: this.width.mean(), + height: this.height.mean(), + playingTime: this.playingTime.mean(), + bufferingTime: this.bufferingTime.mean(), + bufferingEvents: this.bufferingEvents.mean(), + } + }, +} + +window.collectVideoStats = () => webrtcperf.videoStats.collect() + +document.addEventListener('DOMContentLoaded', () => { + if (webrtcperf.enabledForSession(window.PARAMS?.enableVideoStats)) { + webrtcperf.videoStats.scheduleNext() + } +}) diff --git a/src/rtcstats.ts b/src/rtcstats.ts index e48eff2..2ace878 100644 --- a/src/rtcstats.ts +++ b/src/rtcstats.ts @@ -71,6 +71,13 @@ export enum PageStatsNames { cpuPressure = 'cpuPressure', + videoWidth = 'videoWidth', + videoHeight = 'videoHeight', + videoBufferedTime = 'videoBufferedTime', + videoPlayingTime = 'videoPlayingTime', + videoBufferingTime = 'videoBufferingTime', + videoBufferingEvents = 'videoBufferingEvents', + /** The throttle upload rate limitation. */ throttleUpRate = 'throttleUpRate', /** The throttle upload delay. */ diff --git a/src/session.ts b/src/session.ts index 6d77a5b..0373a45 100644 --- a/src/session.ts +++ b/src/session.ts @@ -84,6 +84,14 @@ declare global { let collectVideoEndToEndNetworkDelayStats: () => number let collectCpuPressure: () => number let collectCustomMetrics: () => Promise> + let collectVideoStats: () => { + width: number + height: number + bufferedTime: number + playingTime: number + bufferingTime: number + bufferingEvents: number + } let getParticipantName: () => string } @@ -908,6 +916,7 @@ window.SERVER_USE_HTTPS = ${this.serverUseHttps}; 'https://raw.githubusercontent.com/ggerganov/ggwave/master/bindings/javascript/ggwave.js', 'scripts/e2e-audio-stats.js', 'scripts/e2e-video-stats.js', + 'scripts/video-stats.js', 'scripts/playout-delay-hint.js', 'scripts/save-tracks.js', 'scripts/pressure-stats.js', @@ -1512,6 +1521,13 @@ window.SERVER_USE_HTTPS = ${this.serverUseHttps}; const pageMemory: Record = {} const cpuPressureStats: Record = {} + const videoWidth: Record = {} + const videoHeight: Record = {} + const videoBufferedTime: Record = {} + const videoPlayingTime: Record = {} + const videoBufferingTime: Record = {} + const videoBufferingEvents: Record = {} + const throttleUpValuesRate: Record = {} const throttleUpValuesDelay: Record = {} const throttleUpValuesLoss: Record = {} @@ -1533,6 +1549,7 @@ window.SERVER_USE_HTTPS = ${this.serverUseHttps}; videoEndToEndDelay, videoEndToEndNetworkDelay, cpuPressure, + videoStats, customMetrics, } = await page.evaluate(async () => ({ peerConnectionStats: await collectPeerConnectionStats(), @@ -1540,6 +1557,7 @@ window.SERVER_USE_HTTPS = ${this.serverUseHttps}; videoEndToEndDelay: collectVideoEndToEndStats(), videoEndToEndNetworkDelay: collectVideoEndToEndNetworkDelayStats(), cpuPressure: collectCpuPressure(), + videoStats: collectVideoStats(), customMetrics: 'collectCustomMetrics' in window ? collectCustomMetrics() : null, })) const { participantName } = peerConnectionStats @@ -1600,6 +1618,14 @@ window.SERVER_USE_HTTPS = ${this.serverUseHttps}; } if (cpuPressure !== undefined) cpuPressureStats[pageKey] = cpuPressure + if (videoStats) { + videoWidth[pageKey] = videoStats.width + videoHeight[pageKey] = videoStats.height + videoBufferedTime[pageKey] = videoStats.bufferedTime + videoPlayingTime[pageKey] = videoStats.playingTime + videoBufferingTime[pageKey] = videoStats.bufferingTime + videoBufferingEvents[pageKey] = videoStats.bufferingEvents + } // Collect RTC stats. for (const s of stats) { @@ -1694,6 +1720,12 @@ window.SERVER_USE_HTTPS = ${this.serverUseHttps}; collectedStats.httpRecvBytes = httpRecvBytesStats collectedStats.httpRecvLatency = httpRecvLatencyStats collectedStats.cpuPressure = cpuPressureStats + collectedStats.videoWidth = videoWidth + collectedStats.videoHeight = videoHeight + collectedStats.videoBufferedTime = videoBufferedTime + collectedStats.videoPlayingTime = videoPlayingTime + collectedStats.videoBufferingTime = videoBufferingTime + collectedStats.videoBufferingEvents = videoBufferingEvents collectedStats.pageCpu = pageCpu collectedStats.pageMemory = pageMemory collectedStats.throttleUpRate = throttleUpValuesRate