mirror of https://github.com/OpenVidu/openvidu.git
openvidu-browser: Refactored WebRtcStats
- Updated WebRtcStats file allowing send extra information with webrtc stats such as userId, sessionId, platform description ...pull/609/head
parent
d39fde73d7
commit
f286ce8131
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue