From f286ce813106c8041cce23c81c1ac83abb9612e5 Mon Sep 17 00:00:00 2001 From: csantosm <4a.santos@gmail.com> Date: Tue, 16 Feb 2021 17:17:37 +0100 Subject: [PATCH] openvidu-browser: Refactored WebRtcStats - Updated WebRtcStats file allowing send extra information with webrtc stats such as userId, sessionId, platform description ... --- openvidu-browser/src/OpenVidu/Stream.ts | 11 - .../OpenViduInternal/WebRtcPeer/WebRtcPeer.ts | 2 +- .../WebRtcStats/WebRtcStats.ts | 447 ++++-------------- 3 files changed, 104 insertions(+), 356 deletions(-) diff --git a/openvidu-browser/src/OpenVidu/Stream.ts b/openvidu-browser/src/OpenVidu/Stream.ts index 35e02898..89f8732d 100644 --- a/openvidu-browser/src/OpenVidu/Stream.ts +++ b/openvidu-browser/src/OpenVidu/Stream.ts @@ -747,17 +747,6 @@ export class Stream extends EventDispatcher { return (!this.inboundStreamOpts && !!this.outboundStreamOpts); } - /** - * @hidden - */ - getSelectedIceCandidate(): Promise { - return new Promise((resolve, reject) => { - this.webRtcStats.getSelectedIceCandidateInfo() - .then(report => resolve(report)) - .catch(error => reject(error)); - }); - } - /** * @hidden */ diff --git a/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts b/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts index 6c701f3a..d7d70589 100644 --- a/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts +++ b/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts @@ -93,7 +93,7 @@ export class WebRtcPeer { * callback is expected to send the SDP offer, in order to obtain an SDP * answer from another peer. */ - start(): Promise { + start(): Promise { return new Promise((resolve, reject) => { if (this.pc.signalingState === 'closed') { reject('The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue'); diff --git a/openvidu-browser/src/OpenViduInternal/WebRtcStats/WebRtcStats.ts b/openvidu-browser/src/OpenViduInternal/WebRtcStats/WebRtcStats.ts index 3c1327b7..cd8f0b27 100644 --- a/openvidu-browser/src/OpenViduInternal/WebRtcStats/WebRtcStats.ts +++ b/openvidu-browser/src/OpenViduInternal/WebRtcStats/WebRtcStats.ts @@ -29,39 +29,29 @@ const logger: OpenViduLogger = OpenViduLogger.getInstance(); */ let platform: PlatformUtils; +interface WebrtcStatsConfig { + interval: number, + httpEndpoint: string +} + +interface JSONStats { + '@timestamp': string, + participant_id: string, + session_id: string, + platform: string, + platform_description: string, + stream: string, + webrtc_stats: RTCStatsReport +} + export class WebRtcStats { + private readonly STATS_ITEM_NAME = 'webrtc-stats-config'; + private webRtcStatsEnabled = false; private webRtcStatsIntervalId: NodeJS.Timer; private statsInterval = 1; - private stats: any = { - inbound: { - audio: { - bytesReceived: 0, - packetsReceived: 0, - packetsLost: 0 - }, - video: { - bytesReceived: 0, - packetsReceived: 0, - packetsLost: 0, - framesDecoded: 0, - nackCount: 0 - } - }, - outbound: { - audio: { - bytesSent: 0, - packetsSent: 0, - }, - video: { - bytesSent: 0, - packetsSent: 0, - framesEncoded: 0, - nackCount: 0 - } - } - }; + private POST_URL: string; constructor(private stream: Stream) { platform = PlatformUtils.getInstance(); @@ -73,28 +63,26 @@ export class WebRtcStats { public initWebRtcStats(): void { - const elastestInstrumentation = localStorage.getItem('elastest-instrumentation'); + const webrtcObj = localStorage.getItem(this.STATS_ITEM_NAME); - if (!!elastestInstrumentation) { - // ElasTest instrumentation object found in local storage - - logger.warn('WebRtc stats enabled for stream ' + this.stream.streamId + ' of connection ' + this.stream.connection.connectionId); + if (!!webrtcObj) { this.webRtcStatsEnabled = true; + const webrtcStatsConfig: WebrtcStatsConfig = JSON.parse(webrtcObj); + this.POST_URL = webrtcStatsConfig.httpEndpoint; + this.statsInterval = webrtcStatsConfig.interval; // Interval in seconds - const instrumentation = JSON.parse(elastestInstrumentation); - this.statsInterval = instrumentation.webrtc.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)); - logger.warn('localStorage item: ' + JSON.stringify(instrumentation)); - - this.webRtcStatsIntervalId = setInterval(() => { - this.sendStatsToHttpEndpoint(instrumentation); + this.webRtcStatsIntervalId = setInterval(async () => { + await this.sendStatsToHttpEndpoint(); }, this.statsInterval * 1000); - return; + }else { + logger.debug('WebRtc stats not enabled'); } - - logger.debug('WebRtc stats not enabled'); } public stopWebRtcStats() { @@ -104,320 +92,91 @@ export class WebRtcStats { } } - public getSelectedIceCandidateInfo(): Promise { - return new Promise((resolve, reject) => { - this.getStatsAgnostic(this.stream.getRTCPeerConnection(), - (stats) => { - if (platform.isChromeBrowser() || platform.isChromeMobileBrowser() || platform.isOperaBrowser() || platform.isOperaMobileBrowser()) { - let localCandidateId, remoteCandidateId, googCandidatePair; - const localCandidates = {}; - const remoteCandidates = {}; - for (const key in stats) { - const stat = stats[key]; - if (stat.type === 'localcandidate') { - localCandidates[stat.id] = stat; - } else if (stat.type === 'remotecandidate') { - remoteCandidates[stat.id] = stat; - } else if (stat.type === 'googCandidatePair' && (stat.googActiveConnection === 'true')) { - googCandidatePair = stat; - localCandidateId = stat.localCandidateId; - remoteCandidateId = stat.remoteCandidateId; - } - } - let finalLocalCandidate = localCandidates[localCandidateId]; - if (!!finalLocalCandidate) { - const candList = this.stream.getLocalIceCandidateList(); - const cand = candList.filter((c: RTCIceCandidate) => { - return (!!c.candidate && - c.candidate.indexOf(finalLocalCandidate.ipAddress) >= 0 && - c.candidate.indexOf(finalLocalCandidate.portNumber) >= 0 && - c.candidate.indexOf(finalLocalCandidate.priority) >= 0); - }); - finalLocalCandidate.raw = !!cand[0] ? cand[0].candidate : 'ERROR: Cannot find local candidate in list of sent ICE candidates'; - } else { - finalLocalCandidate = 'ERROR: No active local ICE candidate. Probably ICE-TCP is being used'; - } - - let finalRemoteCandidate = remoteCandidates[remoteCandidateId]; - if (!!finalRemoteCandidate) { - const candList = this.stream.getRemoteIceCandidateList(); - const cand = candList.filter((c: RTCIceCandidate) => { - return (!!c.candidate && - c.candidate.indexOf(finalRemoteCandidate.ipAddress) >= 0 && - c.candidate.indexOf(finalRemoteCandidate.portNumber) >= 0 && - c.candidate.indexOf(finalRemoteCandidate.priority) >= 0); - }); - finalRemoteCandidate.raw = !!cand[0] ? cand[0].candidate : 'ERROR: Cannot find remote candidate in list of received ICE candidates'; - } else { - finalRemoteCandidate = 'ERROR: No active remote ICE candidate. Probably ICE-TCP is being used'; - } - - resolve({ - googCandidatePair, - localCandidate: finalLocalCandidate, - remoteCandidate: finalRemoteCandidate - }); - } else { - reject('Selected ICE candidate info only available for Chrome'); - } + private async sendStats(url: string, json: JSONStats): Promise { + try { + const configuration: RequestInit = { + headers: { + 'Content-type': 'application/json' }, - (error) => { - reject(error); - }); - }); - } - - private sendStatsToHttpEndpoint(instrumentation): void { - - const sendPost = (json) => { - const http: XMLHttpRequest = new XMLHttpRequest(); - const url: string = instrumentation.webrtc.httpEndpoint; - http.open('POST', url, true); - - http.setRequestHeader('Content-type', 'application/json'); - - http.onreadystatechange = () => { // Call a function when the state changes. - if (http.readyState === 4 && http.status === 200) { - logger.log('WebRtc stats successfully sent to ' + url + ' for stream ' + this.stream.streamId + ' of connection ' + this.stream.connection.connectionId); - } + body: JSON.stringify(json), + method: 'POST', }; - http.send(json); - }; + await fetch(url, configuration); - const f = (stats) => { - - if (platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) { - stats.forEach((stat) => { - - let json = {}; - - if ((stat.type === 'inbound-rtp') && - ( - // Avoid firefox empty outbound-rtp statistics - stat.nackCount !== null && - stat.isRemote === false && - stat.id.startsWith('inbound') && - stat.remoteId.startsWith('inbound') - )) { - - const metricId = 'webrtc_inbound_' + stat.mediaType + '_' + stat.ssrc; - const jit = stat.jitter * 1000; - - const metrics = { - bytesReceived: (stat.bytesReceived - this.stats.inbound[stat.mediaType].bytesReceived) / this.statsInterval, - jitter: jit, - packetsReceived: (stat.packetsReceived - this.stats.inbound[stat.mediaType].packetsReceived) / this.statsInterval, - packetsLost: (stat.packetsLost - this.stats.inbound[stat.mediaType].packetsLost) / this.statsInterval - }; - const units = { - bytesReceived: 'bytes', - jitter: 'ms', - packetsReceived: 'packets', - packetsLost: 'packets' - }; - if (stat.mediaType === 'video') { - metrics['framesDecoded'] = (stat.framesDecoded - this.stats.inbound.video.framesDecoded) / this.statsInterval; - metrics['nackCount'] = (stat.nackCount - this.stats.inbound.video.nackCount) / this.statsInterval; - units['framesDecoded'] = 'frames'; - units['nackCount'] = 'packets'; - - this.stats.inbound.video.framesDecoded = stat.framesDecoded; - this.stats.inbound.video.nackCount = stat.nackCount; - } - - this.stats.inbound[stat.mediaType].bytesReceived = stat.bytesReceived; - this.stats.inbound[stat.mediaType].packetsReceived = stat.packetsReceived; - this.stats.inbound[stat.mediaType].packetsLost = stat.packetsLost; - - json = { - '@timestamp': new Date(stat.timestamp).toISOString(), - 'exec': instrumentation.exec, - 'component': instrumentation.component, - 'stream': 'webRtc', - 'et_type': metricId, - 'stream_type': 'composed_metrics', - 'units': units - }; - json[metricId] = metrics; - - sendPost(JSON.stringify(json)); - - } else if ((stat.type === 'outbound-rtp') && - ( - // Avoid firefox empty inbound-rtp statistics - stat.isRemote === false && - stat.id.toLowerCase().includes('outbound') - )) { - - const metricId = 'webrtc_outbound_' + stat.mediaType + '_' + stat.ssrc; - - const metrics = { - bytesSent: (stat.bytesSent - this.stats.outbound[stat.mediaType].bytesSent) / this.statsInterval, - packetsSent: (stat.packetsSent - this.stats.outbound[stat.mediaType].packetsSent) / this.statsInterval - }; - const units = { - bytesSent: 'bytes', - packetsSent: 'packets' - }; - if (stat.mediaType === 'video') { - metrics['framesEncoded'] = (stat.framesEncoded - this.stats.outbound.video.framesEncoded) / this.statsInterval; - units['framesEncoded'] = 'frames'; - - this.stats.outbound.video.framesEncoded = stat.framesEncoded; - } - - this.stats.outbound[stat.mediaType].bytesSent = stat.bytesSent; - this.stats.outbound[stat.mediaType].packetsSent = stat.packetsSent; - - json = { - '@timestamp': new Date(stat.timestamp).toISOString(), - 'exec': instrumentation.exec, - 'component': instrumentation.component, - 'stream': 'webRtc', - 'et_type': metricId, - 'stream_type': 'composed_metrics', - 'units': units - }; - json[metricId] = metrics; - - sendPost(JSON.stringify(json)); - } - }); - } else if (platform.isChromeBrowser() || platform.isChromeMobileBrowser() || platform.isOperaBrowser() || platform.isOperaMobileBrowser()) { - for (const key of Object.keys(stats)) { - const stat = stats[key]; - if (stat.type === 'ssrc') { - - let json = {}; - - if ('bytesReceived' in stat && ( - (stat.mediaType === 'audio' && 'audioOutputLevel' in stat) || - (stat.mediaType === 'video' && 'qpSum' in stat) - )) { - // inbound-rtp - const metricId = 'webrtc_inbound_' + stat.mediaType + '_' + stat.ssrc; - - const metrics = { - bytesReceived: (stat.bytesReceived - this.stats.inbound[stat.mediaType].bytesReceived) / this.statsInterval, - jitter: stat.googJitterBufferMs, - packetsReceived: (stat.packetsReceived - this.stats.inbound[stat.mediaType].packetsReceived) / this.statsInterval, - packetsLost: (stat.packetsLost - this.stats.inbound[stat.mediaType].packetsLost) / this.statsInterval - }; - const units = { - bytesReceived: 'bytes', - jitter: 'ms', - packetsReceived: 'packets', - packetsLost: 'packets' - }; - if (stat.mediaType === 'video') { - metrics['framesDecoded'] = (stat.framesDecoded - this.stats.inbound.video.framesDecoded) / this.statsInterval; - metrics['nackCount'] = (stat.googNacksSent - this.stats.inbound.video.nackCount) / this.statsInterval; - units['framesDecoded'] = 'frames'; - units['nackCount'] = 'packets'; - - this.stats.inbound.video.framesDecoded = stat.framesDecoded; - this.stats.inbound.video.nackCount = stat.googNacksSent; - } - - this.stats.inbound[stat.mediaType].bytesReceived = stat.bytesReceived; - this.stats.inbound[stat.mediaType].packetsReceived = stat.packetsReceived; - this.stats.inbound[stat.mediaType].packetsLost = stat.packetsLost; - - json = { - '@timestamp': new Date(stat.timestamp).toISOString(), - 'exec': instrumentation.exec, - 'component': instrumentation.component, - 'stream': 'webRtc', - 'et_type': metricId, - 'stream_type': 'composed_metrics', - 'units': units - }; - json[metricId] = metrics; - - sendPost(JSON.stringify(json)); - } else if ('bytesSent' in stat) { - // outbound-rtp - const metricId = 'webrtc_outbound_' + stat.mediaType + '_' + stat.ssrc; - - const metrics = { - bytesSent: (stat.bytesSent - this.stats.outbound[stat.mediaType].bytesSent) / this.statsInterval, - packetsSent: (stat.packetsSent - this.stats.outbound[stat.mediaType].packetsSent) / this.statsInterval - }; - const units = { - bytesSent: 'bytes', - packetsSent: 'packets' - }; - if (stat.mediaType === 'video') { - metrics['framesEncoded'] = (stat.framesEncoded - this.stats.outbound.video.framesEncoded) / this.statsInterval; - units['framesEncoded'] = 'frames'; - - this.stats.outbound.video.framesEncoded = stat.framesEncoded; - } - - this.stats.outbound[stat.mediaType].bytesSent = stat.bytesSent; - this.stats.outbound[stat.mediaType].packetsSent = stat.packetsSent; - - json = { - '@timestamp': new Date(stat.timestamp).toISOString(), - 'exec': instrumentation.exec, - 'component': instrumentation.component, - 'stream': 'webRtc', - 'et_type': metricId, - 'stream_type': 'composed_metrics', - 'units': units - }; - json[metricId] = metrics; - - sendPost(JSON.stringify(json)); - } - } - } - } - }; - - this.getStatsAgnostic(this.stream.getRTCPeerConnection(), f, (error) => { logger.log(error); }); + } catch (error) { + logger.error(error); + } } - private standardizeReport(response) { - logger.log(response); - const standardReport = {}; + 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); + } catch (error) { + logger.log(error); + } + } - if (platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) { - Object.keys(response).forEach(key => { - logger.log(response[key]); + 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)); + }); + } else { + const statsReport = await this.stream.getRTCPeerConnection().getStats(); + resolve(this.standardizeReport(statsReport)); + } + + }); + + } + + private generateJSONStats(stats: RTCStatsReport): JSONStats { + return { + '@timestamp': new Date().toISOString(), + participant_id: this.stream.connection.data, + session_id: this.stream.session.sessionId, + platform: platform.getName(), + platform_description: platform.getDescription(), + stream: 'webRTC', + webrtc_stats: stats + }; + } + + private standardizeReport(response: RTCStatsReport | any) { + let standardReport = {}; + + 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 response; + + return standardReport; } - response.result().forEach(report => { - const standardStats = { - id: report.id, - timestamp: report.timestamp, - type: report.type - }; - report.names().forEach((name) => { - standardStats[name] = report.stat(name); + // Others platforms + response.forEach((values) => { + let standardStats: any = {}; + Object.keys(values).forEach((value: any) => { + standardStats[value] = values[value]; }); - standardReport[standardStats.id] = standardStats; + standardReport[standardStats.id] = standardStats }); return standardReport; } - - private getStatsAgnostic(pc, successCb, failureCb) { - if (platform.isFirefoxBrowser() || platform.isFirefoxMobileBrowser()) { - // getStats takes args in different order in Chrome and Firefox - return pc.getStats(null).then(response => { - const report = this.standardizeReport(response); - successCb(report); - }).catch(failureCb); - } else if (platform.isChromeBrowser() || platform.isChromeMobileBrowser() || platform.isOperaBrowser() || platform.isOperaMobileBrowser()) { - // In Chrome, the first two arguments are reversed - return pc.getStats((response) => { - const report = this.standardizeReport(response); - successCb(report); - }, null, failureCb); - } - } - } \ No newline at end of file