diff --git a/bun.lockb b/bun.lockb index af54647bf..c4c91e157 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 5d79f37f7..bdcc35629 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@mdi/font": "^7.0.96", "@mdi/js": "^7.2.96", "@sentry/vue": "^8.20.0", + "@peermetrics/webrtc-stats": "^5.7.1", "@types/hammerjs": "^2.0.45", "@vueuse/core": "9.8.1", "@vueuse/math": "^10.1.2", diff --git a/src/composables/webRTC.ts b/src/composables/webRTC.ts index 6ce5f2582..58f75290b 100644 --- a/src/composables/webRTC.ts +++ b/src/composables/webRTC.ts @@ -41,7 +41,7 @@ export class WebRTCManager { private connected = ref(false) private consumerId: string | undefined private streamName: string | undefined - private session: Session | undefined + public session: Session | undefined private rtcConfiguration: RTCConfiguration private selectedICEIPs: string[] = [] private selectedICEProtocols: string[] = [] diff --git a/src/libs/webrtc/session.ts b/src/libs/webrtc/session.ts index 47ee611ac..46916bd56 100644 --- a/src/libs/webrtc/session.ts +++ b/src/libs/webrtc/session.ts @@ -18,7 +18,7 @@ export class Session { public status: string private ended: boolean private signaller: Signaller - private peerConnection: RTCPeerConnection + public peerConnection: RTCPeerConnection private availableICEIPs: string[] private selectedICEIPs: string[] private selectedICEProtocols: string[] diff --git a/src/stores/omniscientLogger.ts b/src/stores/omniscientLogger.ts index 4da8c535f..25fd0a183 100644 --- a/src/stores/omniscientLogger.ts +++ b/src/stores/omniscientLogger.ts @@ -1,8 +1,13 @@ +import { WebRTCStats } from '@peermetrics/webrtc-stats' import { defineStore } from 'pinia' -import { ref } from 'vue' +import { ref, watch } from 'vue' + +import { WebRTCStatsEvent, WebRTCVideoStat } from '@/types/video' import { useVideoStore } from './video' +export const webrtcStats = new WebRTCStats({ getStatsInterval: 250 }) + export const useOmniscientLoggerStore = defineStore('omniscient-logger', () => { const videoStore = useVideoStore() @@ -79,4 +84,87 @@ export const useOmniscientLoggerStore = defineStore('omniscient-logger', () => { }) } fpsMeter() + + // Routine to log the WebRTC statistics + + // Monitor the active streams to add the connections to the WebRTC statistics + watch(videoStore.activeStreams, (streams) => { + Object.keys(streams).forEach((streamName) => { + const session = streams[streamName]?.webRtcManager.session + if (!session || !session.peerConnection) return + if (webrtcStats.peersToMonitor[session.consumerId]) return + webrtcStats.addConnection({ + pc: session.peerConnection, // RTCPeerConnection instance + peerId: session.consumerId, // any string that helps you identify this peer, + connectionId: session.id, // optional, an id that you can use to keep track of this connection + remote: false, // optional, override the global remote flag + }) + }) + }) + + // Track the WebRTC statistics, warn about changes in cumulative values and log the average values + const historyLength = 30 // Number of samples to keep in the history + const cumulativeKeys: WebRTCVideoStat[] = ['packetsLost', 'totalFreezesDuration', 'framesDropped'] // Keys that have cumulative values + const averageKeys: WebRTCVideoStat[] = ['packetRate', 'jitter'] // Keys that have average values + const storedKeys = [...cumulativeKeys, ...averageKeys] // Keys to store in the history + + const webrtcStatsAverageLogDelay = 1000 + let lastWebrtcStatsAverageLog = new Date() + const webRtcStatsHistory = ref<{ [id in string]: { [stat in string]: (number | string)[] } }>({}) + + webrtcStats.on('stats', (ev: WebRTCStatsEvent) => { + try { + const videoData = ev.data.video.inbound[0] + if (videoData === undefined) return + + // Initialize the peer's statistics if they do not exist + if (webRtcStatsHistory.value[ev.peerId] === undefined) webRtcStatsHistory.value[ev.peerId] = {} + + storedKeys.forEach((key) => { + // Initialize the key array if it does not exist + if (webRtcStatsHistory.value[ev.peerId][key] === undefined) webRtcStatsHistory.value[ev.peerId][key] = [] + + webRtcStatsHistory.value[ev.peerId][key].push(videoData[key]) + + // Keep only the last 'historyLength' samples + const keyArray = webRtcStatsHistory.value[ev.peerId][key] + keyArray.splice(0, keyArray.length - historyLength) + webRtcStatsHistory.value[ev.peerId][key] = keyArray + }) + + // Warn about changes in cumulative values + cumulativeKeys.forEach((key) => { + const keyArray = webRtcStatsHistory.value[ev.peerId][key] + if (keyArray.length < 2) return + + const lastValue = keyArray[keyArray.length - 1] + const prevValue = keyArray[keyArray.length - 2] + + if (typeof lastValue !== 'number' || typeof prevValue !== 'number') return + + if (lastValue > prevValue) { + console.warn(`Cumulative value '${key}' increased for peer '${ev.peerId}': ${lastValue.toFixed(2)}.`) + } + }) + + // Log the average values recursively + if (new Date().getTime() - lastWebrtcStatsAverageLog.getTime() > webrtcStatsAverageLogDelay) { + averageKeys.forEach((key) => { + const keyArray = webRtcStatsHistory.value[ev.peerId][key] + if (keyArray.find((value) => typeof value !== 'number')) return + const average = (keyArray as number[]).reduce((a, b) => a + b, 0) / keyArray.length + console.debug(`Average value '${key}' for peer '${ev.peerId}': ${average.toFixed(4)}.`) + }) + lastWebrtcStatsAverageLog = new Date() + } + } catch (error) { + console.error('Error while logging WebRTC statistics:', error) + } + }) + + return { + streamsFrameRateHistory, + appFrameRateHistory, + webRtcStatsHistory, + } }) diff --git a/src/types/shims.d.ts b/src/types/shims.d.ts index eab8effc8..f2a572713 100644 --- a/src/types/shims.d.ts +++ b/src/types/shims.d.ts @@ -4,6 +4,7 @@ declare module 'gamepad.js' declare module 'vuetify' declare module 'vuetify/lib/components' declare module 'vuetify/lib/directives' +declare module '@peermetrics/webrtc-stats' declare module 'vue-virtual-scroller' { import Vue, { ComponentOptions, PluginObject, Component } from 'vue' diff --git a/src/types/video.ts b/src/types/video.ts index 3077bf30d..f7903feef 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -130,3 +130,64 @@ export const getBlobExtensionContainer = (blob: Blob): VideoExtensionContainer | } return undefined } + +export type WebRTCVideoStats = { + id: string + timestamp: number + type: string + codecId: string + kind: string + mediaType: string + ssrc: number + transportId: string + jitter: number + packetsLost: number + packetsReceived: number + bytesReceived: number + firCount: number + frameHeight: number + frameWidth: number + framesAssembledFromMultiplePackets: number + framesDecoded: number + framesDropped: number + framesPerSecond: number + framesReceived: number + freezeCount: number + headerBytesReceived: number + jitterBufferDelay: number + jitterBufferEmittedCount: number + jitterBufferMinimumDelay: number + jitterBufferTargetDelay: number + keyFramesDecoded: number + lastPacketReceivedTimestamp: number + mid: string + nackCount: number + pauseCount: number + pliCount: number + remoteId: string + totalAssemblyTime: number + totalDecodeTime: number + totalFreezesDuration: number + totalInterFrameDelay: number + totalPausesDuration: number + totalProcessingDelay: number + totalSquaredInterFrameDelay: number + trackIdentifier: string + clockRate: number + mimeType: string + payloadType: number + bitrate: number + packetRate: number +} +export type WebRTCVideoStat = keyof WebRTCVideoStats + +export type WebRTCStatsEvent = { + peerId: string + data: { + video: { + inbound: { + [index: number]: WebRTCVideoStats + } + } + } +}