openvidu-browser: Refactored WebRtcStats

- Updated WebRtcStats file allowing send extra information with webrtc stats such as userId, sessionId, platform description ...
pull/609/head
csantosm 2021-02-16 17:17:37 +01:00
parent d39fde73d7
commit f286ce8131
3 changed files with 104 additions and 356 deletions

View File

@ -747,17 +747,6 @@ export class Stream extends EventDispatcher {
return (!this.inboundStreamOpts && !!this.outboundStreamOpts);
}
/**
* @hidden
*/
getSelectedIceCandidate(): Promise<any> {
return new Promise((resolve, reject) => {
this.webRtcStats.getSelectedIceCandidateInfo()
.then(report => resolve(report))
.catch(error => reject(error));
});
}
/**
* @hidden
*/

View File

@ -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<any> {
start(): Promise<void> {
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');

View File

@ -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<any> {
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<void> {
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<void> {
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<any> {
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);
}
}
}