From e545fd1630f11a163f8c8d61937daad48582edb1 Mon Sep 17 00:00:00 2001 From: csantosm <4a.santos@gmail.com> Date: Wed, 24 Feb 2021 10:43:23 +0100 Subject: [PATCH] openvidu-browser: Refactored webRtcStats --- .../WebRtcStats/WebRtcStats.ts | 155 +++++++++++++----- 1 file changed, 111 insertions(+), 44 deletions(-) diff --git a/openvidu-browser/src/OpenViduInternal/WebRtcStats/WebRtcStats.ts b/openvidu-browser/src/OpenViduInternal/WebRtcStats/WebRtcStats.ts index 478718ba..1812d4be 100644 --- a/openvidu-browser/src/OpenViduInternal/WebRtcStats/WebRtcStats.ts +++ b/openvidu-browser/src/OpenViduInternal/WebRtcStats/WebRtcStats.ts @@ -34,16 +34,45 @@ interface WebrtcStatsConfig { httpEndpoint: string } -interface JSONStats { +interface JSONStatsResponse { '@timestamp': string, participant_id: string, session_id: string, platform: string, platform_description: string, stream: string, - webrtc_stats: RTCStatsReport + webrtc_stats: IWebrtcStats } +interface IWebrtcStats { + inbound: { + audio: { + bytesReceived: number, + packetsReceived: number, + packetsLost: number + } | {}, + video: { + bytesReceived: number, + packetsReceived: number, + packetsLost: number, + framesDecoded: number, + nackCount: number + } | {} + } | {}, + outbound: { + audio: { + bytesSent: number, + packetsSent: number, + } | {}, + video: { + bytesSent: number, + packetsSent: number, + framesEncoded: number, + nackCount: number + } | {} + } | {} +}; + export class WebRtcStats { private readonly STATS_ITEM_NAME = 'webrtc-stats-config'; @@ -66,16 +95,15 @@ export class WebRtcStats { const webrtcObj = localStorage.getItem(this.STATS_ITEM_NAME); if (!!webrtcObj) { - this.webRtcStatsEnabled = true; const webrtcStatsConfig: WebrtcStatsConfig = JSON.parse(webrtcObj); - this.POST_URL = webrtcStatsConfig.httpEndpoint; - this.statsInterval = webrtcStatsConfig.interval; // Interval in seconds - // webrtc object found in local storage logger.warn('WebRtc stats enabled for stream ' + this.stream.streamId + ' of connection ' + this.stream.connection.connectionId); logger.warn('localStorage item: ' + JSON.stringify(webrtcStatsConfig)); + this.POST_URL = webrtcStatsConfig.httpEndpoint; + this.statsInterval = webrtcStatsConfig.interval; // Interval in seconds + this.webRtcStatsIntervalId = setInterval(async () => { await this.sendStatsToHttpEndpoint(); }, this.statsInterval * 1000); @@ -155,13 +183,13 @@ export class WebRtcStats { } } - private async sendStats(url: string, json: JSONStats): Promise { + private async sendStats(url: string, response: JSONStatsResponse): Promise { try { const configuration: RequestInit = { headers: { 'Content-type': 'application/json' }, - body: JSON.stringify(json), + body: JSON.stringify(response), method: 'POST', }; await fetch(url, configuration); @@ -173,34 +201,88 @@ export class WebRtcStats { private async sendStatsToHttpEndpoint(): Promise { try { - const stats: RTCStatsReport = await this.getStats(); - const json = this.generateJSONStats(stats); - // this.parseAndSendStats(stats); - await this.sendStats(this.POST_URL, json); + const webrtcStats: IWebrtcStats = await this.getStats(); + const response = this.generateJSONStatsResponse(webrtcStats); + await this.sendStats(this.POST_URL, response); } catch (error) { logger.log(error); } } - private async getStats(): Promise { + private async getStats(): Promise { return new Promise(async (resolve, reject) => { if (platform.isChromeBrowser() || platform.isChromeMobileBrowser() || platform.isOperaBrowser() || platform.isOperaMobileBrowser()) { const pc: any = this.stream.getRTCPeerConnection(); pc.getStats((statsReport) => { - resolve(this.standardizeReport(statsReport)); + const stats = statsReport.result().filter((stat) => stat.type === 'ssrc'); + const response = this.initWebRtcStatsResponse(); + + stats.forEach(stat => { + const valueNames: string[] = stat.names(); + const mediaType = stat.stat("mediaType"); + const isAudio = mediaType === 'audio' && valueNames.includes('audioOutputLevel'); + const isVideo = mediaType === 'video' && valueNames.includes('qpSum'); + const isIndoundRtp = valueNames.includes('bytesReceived') && (isAudio || isVideo); + const isOutboundRtp = valueNames.includes('bytesSent'); + + if(isIndoundRtp){ + response.inbound[mediaType].bytesReceived = stat.stat('bytesReceived'); + response.inbound[mediaType].packetsReceived = stat.stat('packetsReceived'); + response.inbound[mediaType].packetsLost = stat.stat('packetsLost'); + if(mediaType === 'video'){ + response.inbound['video'].framesDecoded = stat.stat('framesDecoded'); + response.inbound['video'].nackCount = stat.stat('nackCount'); + } + + } else if(isOutboundRtp) { + response.outbound[mediaType].bytesSent = stat.stat('bytesSent'); + response.outbound[mediaType].packetsSent = stat.stat('packetsSent'); + if(mediaType === 'video'){ + response.outbound['video'].framesEncoded = stat.stat('framesEncoded'); + response.outbound['video'].nackCount = stat.stat('nackCount'); + } + } + }); + resolve(response); + }); } else { - const statsReport = await this.stream.getRTCPeerConnection().getStats(); - resolve(this.standardizeReport(statsReport)); + const statsReport:any = await this.stream.getRTCPeerConnection().getStats(null); + const response = this.initWebRtcStatsResponse(); + statsReport.forEach((stat: any) => { + + const mediaType = stat.mediaType; + // isRemote property has been deprecated from Firefox 66 https://blog.mozilla.org/webrtc/getstats-isremote-66/ + switch (stat.type) { + case "outbound-rtp": + response.outbound[mediaType].bytesSent = stat.bytesSent; + response.outbound[mediaType].packetsSent = stat.packetsSent; + if(mediaType === 'video'){ + response.outbound[mediaType].framesEncoded = stat.framesEncoded; + } + break; + case "inbound-rtp": + response.inbound[mediaType].bytesReceived = stat.bytesReceived; + response.inbound[mediaType].packetsReceived = stat.packetsReceived; + response.inbound[mediaType].packetsLost = stat.packetsLost; + if (mediaType === 'video') { + response.inbound[mediaType].framesDecoded = stat.framesDecoded; + response.inbound[mediaType].nackCount = stat.nackCount; + } + break; + } + }); + + return resolve(response); } }); } - private generateJSONStats(stats: RTCStatsReport): JSONStats { + private generateJSONStatsResponse(stats: IWebrtcStats): JSONStatsResponse { return { '@timestamp': new Date().toISOString(), participant_id: this.stream.connection.data, @@ -212,34 +294,19 @@ export class WebRtcStats { }; } - private standardizeReport(response: RTCStatsReport | any) { - let standardReport = {}; + private initWebRtcStatsResponse(): IWebrtcStats { - if (platform.isChromeBrowser() || platform.isChromeMobileBrowser() || platform.isOperaBrowser() || platform.isOperaMobileBrowser()) { - response.result().forEach(report => { - const standardStats = { - id: report.id, - timestamp: report.timestamp, - type: report.type - }; - report.names().forEach((name) => { - standardStats[name] = report.stat(name); - }); - standardReport[standardStats.id] = standardStats; - }); + return { + inbound: { + audio: {}, + video: {} + }, + outbound: { + audio: {}, + video: {} + } + }; - return standardReport; - } - - // Others platforms - response.forEach((values) => { - let standardStats: any = {}; - Object.keys(values).forEach((value: any) => { - standardStats[value] = values[value]; - }); - standardReport[standardStats.id] = standardStats - }); - - return standardReport; } + } \ No newline at end of file