mirror of https://github.com/OpenVidu/openvidu.git
325 lines
14 KiB
TypeScript
325 lines
14 KiB
TypeScript
import { Stream } from './Stream';
|
|
import * as adapter from 'webrtc-adapter';
|
|
|
|
export class WebRtcStats {
|
|
|
|
private webRtcStatsEnabled: boolean = false;
|
|
private webRtcStatsIntervalId: number;
|
|
private statsInterval: number = 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
|
|
}
|
|
}
|
|
}
|
|
|
|
constructor(private stream: Stream) { }
|
|
|
|
public isEnabled(): boolean {
|
|
return this.webRtcStatsEnabled;
|
|
}
|
|
|
|
public initWebRtcStats(): void {
|
|
|
|
let elastestInstrumentation = localStorage.getItem('elastest-instrumentation');
|
|
|
|
if (elastestInstrumentation) {
|
|
// ElasTest instrumentation object found in local storage
|
|
|
|
console.warn("WebRtc stats enabled for stream " + this.stream.streamId + " of connection " + this.stream.connection.connectionId);
|
|
|
|
this.webRtcStatsEnabled = true;
|
|
|
|
let instrumentation = JSON.parse(elastestInstrumentation);
|
|
this.statsInterval = instrumentation.webrtc.interval; // Interval in seconds
|
|
|
|
console.warn("localStorage item: " + JSON.stringify(instrumentation));
|
|
|
|
this.webRtcStatsIntervalId = setInterval(() => {
|
|
this.sendStatsToHttpEndpoint(instrumentation);
|
|
}, this.statsInterval * 1000);
|
|
|
|
return;
|
|
}
|
|
|
|
console.debug("WebRtc stats not enabled");
|
|
}
|
|
|
|
public stopWebRtcStats() {
|
|
if (this.webRtcStatsEnabled) {
|
|
clearInterval(this.webRtcStatsIntervalId);
|
|
console.warn("WebRtc stats stopped for disposed stream " + this.stream.streamId + " of connection " + this.stream.connection.connectionId);
|
|
}
|
|
}
|
|
|
|
private sendStatsToHttpEndpoint(instrumentation): void {
|
|
|
|
let sendPost = (json) => {
|
|
let http: XMLHttpRequest = new XMLHttpRequest();
|
|
let 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) {
|
|
console.log("WebRtc stats succesfully sent to " + url + " for stream " + this.stream.streamId + " of connection " + this.stream.connection.connectionId);
|
|
}
|
|
}
|
|
http.send(json);
|
|
}
|
|
|
|
let f = (stats) => {
|
|
|
|
if (adapter.browserDetails.browser === 'firefox') {
|
|
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')
|
|
)) {
|
|
|
|
let metricId = 'webrtc_inbound_' + stat.mediaType + '_' + stat.ssrc;
|
|
let jitter = stat.jitter * 1000;
|
|
|
|
let metrics = {
|
|
"bytesReceived": (stat.bytesReceived - this.stats.inbound[stat.mediaType].bytesReceived) / this.statsInterval,
|
|
"jitter": jitter,
|
|
"packetsReceived": (stat.packetsReceived - this.stats.inbound[stat.mediaType].packetsReceived) / this.statsInterval,
|
|
"packetsLost": (stat.packetsLost - this.stats.inbound[stat.mediaType].packetsLost) / this.statsInterval
|
|
};
|
|
let 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",
|
|
"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')
|
|
)) {
|
|
|
|
let metricId = 'webrtc_outbound_' + stat.mediaType + '_' + stat.ssrc;
|
|
|
|
let metrics = {
|
|
"bytesSent": (stat.bytesSent - this.stats.outbound[stat.mediaType].bytesSent) / this.statsInterval,
|
|
"packetsSent": (stat.packetsSent - this.stats.outbound[stat.mediaType].packetsSent) / this.statsInterval
|
|
};
|
|
let 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",
|
|
"type": metricId,
|
|
"stream_type": "composed_metrics",
|
|
"units": units
|
|
}
|
|
json[metricId] = metrics;
|
|
|
|
sendPost(JSON.stringify(json));
|
|
}
|
|
});
|
|
} else if (adapter.browserDetails.browser === 'chrome') {
|
|
for (let key of Object.keys(stats)) {
|
|
let 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
|
|
let metricId = 'webrtc_inbound_' + stat.mediaType + '_' + stat.ssrc;
|
|
|
|
let 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
|
|
};
|
|
let 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",
|
|
"type": metricId,
|
|
"stream_type": "composed_metrics",
|
|
"units": units
|
|
}
|
|
json[metricId] = metrics;
|
|
|
|
sendPost(JSON.stringify(json));
|
|
} else if ('bytesSent' in stat) {
|
|
// outbound-rtp
|
|
let metricId = 'webrtc_outbound_' + stat.mediaType + '_' + stat.ssrc;
|
|
|
|
let metrics = {
|
|
"bytesSent": (stat.bytesSent - this.stats.outbound[stat.mediaType].bytesSent) / this.statsInterval,
|
|
"packetsSent": (stat.packetsSent - this.stats.outbound[stat.mediaType].packetsSent) / this.statsInterval
|
|
};
|
|
let 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",
|
|
"type": metricId,
|
|
"stream_type": "composed_metrics",
|
|
"units": units
|
|
}
|
|
json[metricId] = metrics;
|
|
|
|
sendPost(JSON.stringify(json));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
this.getStatsAgnostic(this.stream.getRTCPeerConnection(), null, f, (error) => { console.log(error) });
|
|
}
|
|
|
|
private standardizeReport(response) {
|
|
if (adapter.browserDetails.browser === 'firefox') {
|
|
return response;
|
|
}
|
|
|
|
var standardReport = {};
|
|
response.result().forEach(function (report) {
|
|
var standardStats = {
|
|
id: report.id,
|
|
timestamp: report.timestamp,
|
|
type: report.type
|
|
};
|
|
report.names().forEach(function (name) {
|
|
standardStats[name] = report.stat(name);
|
|
});
|
|
standardReport[standardStats.id] = standardStats;
|
|
});
|
|
|
|
return standardReport;
|
|
}
|
|
|
|
private getStatsAgnostic(pc, selector, successCb, failureCb) {
|
|
if (adapter.browserDetails.browser === 'firefox') {
|
|
// getStats takes args in different order in Chrome and Firefox
|
|
return pc.getStats(selector, (response) => {
|
|
var report = this.standardizeReport(response);
|
|
successCb(report);
|
|
}, failureCb);
|
|
} else if (adapter.browserDetails.browser === 'chrome') {
|
|
// In Chrome, the first two arguments are reversed
|
|
return pc.getStats((response) => {
|
|
var report = this.standardizeReport(response);
|
|
successCb(report);
|
|
}, selector, failureCb);
|
|
}
|
|
}
|
|
|
|
} |