openvidu-browser: WebRTC stats refactoring

pull/621/head
pabloFuente 2021-03-18 13:39:50 +01:00
parent 076fb233e2
commit 6b9f2be9d6
2 changed files with 200 additions and 146 deletions

View File

@ -2,7 +2,7 @@ import platform = require("platform");
export class PlatformUtils { export class PlatformUtils {
protected static instance: PlatformUtils; protected static instance: PlatformUtils;
constructor() {} constructor() { }
static getInstance(): PlatformUtils { static getInstance(): PlatformUtils {
if (!this.instance) { if (!this.instance) {
@ -156,6 +156,17 @@ export class PlatformUtils {
return false; return false;
} }
/**
* @hidden
*/
public isChromium(): boolean {
return this.isChromeBrowser() || this.isChromeMobileBrowser() ||
this.isOperaBrowser() || this.isOperaMobileBrowser() ||
this.isEdgeBrowser() || this.isEdgeMobileBrowser() ||
this.isIonicAndroid() || this.isIonicIos() ||
this.isElectron();
}
/** /**
* @hidden * @hidden
*/ */

View File

@ -44,24 +44,34 @@ interface JSONStatsResponse {
webrtc_stats: IWebrtcStats webrtc_stats: IWebrtcStats
} }
/**
* Common WebRtcSTats for latest Chromium and Firefox versions
*/
interface IWebrtcStats { interface IWebrtcStats {
inbound: { inbound?: {
audio: { audio: {
bytesReceived: number, bytesReceived: number,
packetsReceived: number, packetsReceived: number,
packetsLost: number, packetsLost: number,
jitter: number, jitter: number
delayMs: number
} | {}, } | {},
video: { video: {
bytesReceived: number, bytesReceived: number,
packetsReceived: number, packetsReceived: number,
packetsLost: number, packetsLost: number,
jitter?: number, // Firefox
jitterBufferDelay?: number, // Chrome
framesDecoded: number, framesDecoded: number,
nackCount: number firCount: number,
nackCount: number,
pliCount: number,
frameHeight?: number, // Chrome
frameWidth?: number, // Chrome
framesDropped?: number, // Chrome
framesReceived?: number // Chrome
} | {} } | {}
} | {}, },
outbound: { outbound?: {
audio: { audio: {
bytesSent: number, bytesSent: number,
packetsSent: number, packetsSent: number,
@ -69,10 +79,16 @@ interface IWebrtcStats {
video: { video: {
bytesSent: number, bytesSent: number,
packetsSent: number, packetsSent: number,
firCount: number,
framesEncoded: number, framesEncoded: number,
nackCount: number nackCount: number,
pliCount: number,
qpSum: number,
frameHeight?: number, // Chrome
frameWidth?: number, // Chrome
framesSent?: number // Chrome
} | {} } | {}
} | {} }
}; };
export class WebRtcStats { export class WebRtcStats {
@ -110,71 +126,121 @@ export class WebRtcStats {
await this.sendStatsToHttpEndpoint(); await this.sendStatsToHttpEndpoint();
}, this.statsInterval * 1000); }, this.statsInterval * 1000);
}else { } else {
logger.debug('WebRtc stats not enabled'); logger.debug('WebRtc stats not enabled');
} }
} }
// Used in test-app // Have been tested in:
// - Linux Desktop:
// - Chrome 89.0.4389.90
// - Opera 74.0.3911.218
// - Firefox 86
// - Microsoft Edge 91.0.825.0
// - Electron 11.3.0 (Chromium 87.0.4280.141)
// - Windows Desktop:
// - Chrome
// - ¿Opera?
// - Firefox
// - Microsoft Edge
// - ¿Electron?
// - MacOS Desktop:
// - Chrome
// - ¿Opera?
// - Firefox
// - ¿Electron?
// - Android:
// - Chrome Mobile 89.0.4389.90
// - Opera 62.3.3146.57763
// - Firefox Mobile 86.6.1
// - Microsoft Edge Mobile 46.02.4.5147
// - Ionic 5
// - ¿React Native?
// - iOS:
// - Safari Mobile
// - ¿Ionic?
// - ¿React Native?
public getSelectedIceCandidateInfo(): Promise<any> { public getSelectedIceCandidateInfo(): Promise<any> {
return new Promise((resolve, reject) => { return new Promise(async (resolve, reject) => {
this.getStats().then(
(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]; const statsReport: any = await this.stream.getRTCPeerConnection().getStats();
if (!!finalRemoteCandidate) { let transportStat;
const candList = this.stream.getRemoteIceCandidateList(); const candidatePairs: Map<string, any> = new Map();
const cand = candList.filter((c: RTCIceCandidate) => { const localCandidates: Map<string, any> = new Map();
return (!!c.candidate && const remoteCandidates: Map<string, any> = new Map();
c.candidate.indexOf(finalRemoteCandidate.ipAddress) >= 0 && statsReport.forEach((stat: any) => {
c.candidate.indexOf(finalRemoteCandidate.portNumber) >= 0 && if (platform.isChromium() && stat.type === 'transport') {
c.candidate.indexOf(finalRemoteCandidate.priority) >= 0); transportStat = stat;
}); }
finalRemoteCandidate.raw = !!cand[0] ? cand[0].candidate : 'ERROR: Cannot find remote candidate in list of received ICE candidates'; console.log(stat.type);
} else { console.log(stat);
finalRemoteCandidate = 'ERROR: No active remote ICE candidate. Probably ICE-TCP is being used'; switch (stat.type) {
} case 'candidate-pair':
candidatePairs.set(stat.id, stat);
resolve({ break;
googCandidatePair, case 'local-candidate':
localCandidate: finalLocalCandidate, localCandidates.set(stat.id, stat);
remoteCandidate: finalRemoteCandidate break;
}); case 'remote-candidate':
} else { remoteCandidates.set(stat.id, stat);
reject('Selected ICE candidate info only available for Chrome'); break;
}
});
let selectedCandidatePair;
if (platform.isChromium()) {
const selectedCandidatePairId = transportStat.selectedCandidatePairId
selectedCandidatePair = candidatePairs.get(selectedCandidatePairId);
} else {
// Firefox
const length = candidatePairs.size;
const iterator = candidatePairs.values();
for (let i = 0; i < length; i++) {
const candidatePair = iterator.next().value;
if (candidatePair['selected']) {
selectedCandidatePair = candidatePair;
break;
} }
}).catch((error) => { }
reject(error); }
const localCandidateId = selectedCandidatePair.localCandidateId;
const remoteCandidateId = selectedCandidatePair.remoteCandidateId;
let finalLocalCandidate = localCandidates.get(localCandidateId);
if (!!finalLocalCandidate) {
const candList = this.stream.getLocalIceCandidateList();
const cand = candList.filter((c: RTCIceCandidate) => {
return (!!c.candidate &&
(c.candidate.indexOf(finalLocalCandidate.ip) >= 0 || c.candidate.indexOf(finalLocalCandidate.address) >= 0) &&
c.candidate.indexOf(finalLocalCandidate.port) >= 0 &&
c.candidate.indexOf(finalLocalCandidate.priority) >= 0);
}); });
finalLocalCandidate.raw = [];
for (let c of cand) {
finalLocalCandidate.raw.push(c.candidate);
}
} else {
finalLocalCandidate = 'ERROR: No active local ICE candidate. Probably ICE-TCP is being used';
}
let finalRemoteCandidate = remoteCandidates.get(remoteCandidateId);
if (!!finalRemoteCandidate) {
const candList = this.stream.getRemoteIceCandidateList();
const cand = candList.filter((c: RTCIceCandidate) => {
return (!!c.candidate &&
(c.candidate.indexOf(finalRemoteCandidate.ip) >= 0 || c.candidate.indexOf(finalRemoteCandidate.address) >= 0) &&
c.candidate.indexOf(finalRemoteCandidate.port) >= 0);
});
finalRemoteCandidate.raw = [];
for (let c of cand) {
finalRemoteCandidate.raw.push(c.candidate);
}
} else {
finalRemoteCandidate = 'ERROR: No active remote ICE candidate. Probably ICE-TCP is being used';
}
resolve({
localCandidate: finalLocalCandidate,
remoteCandidate: finalRemoteCandidate
});
}); });
} }
@ -203,7 +269,7 @@ export class WebRtcStats {
private async sendStatsToHttpEndpoint(): Promise<void> { private async sendStatsToHttpEndpoint(): Promise<void> {
try { try {
const webrtcStats: IWebrtcStats = await this.getStats(); const webrtcStats: IWebrtcStats = await this.getCommonStats();
const response = this.generateJSONStatsResponse(webrtcStats); const response = this.generateJSONStatsResponse(webrtcStats);
await this.sendStats(this.POST_URL, response); await this.sendStats(this.POST_URL, response);
} catch (error) { } catch (error) {
@ -211,80 +277,54 @@ export class WebRtcStats {
} }
} }
private async getStats(): Promise<IWebrtcStats> { public async getCommonStats(): Promise<IWebrtcStats> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
if (platform.isChromeBrowser() || platform.isChromeMobileBrowser() || platform.isOperaBrowser() || platform.isOperaMobileBrowser()) {
const pc: any = this.stream.getRTCPeerConnection(); const statsReport: any = await this.stream.getRTCPeerConnection().getStats();
pc.getStats((statsReport) => { const response = this.getWebRtcStatsResponseOutline();
const stats = statsReport.result().filter((stat) => stat.type === 'ssrc');
const response = this.initWebRtcStatsResponse();
stats.forEach(stat => { statsReport.forEach((stat: any) => {
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){ const mediaType = stat.mediaType != null ? stat.mediaType : stat.kind;
response.inbound[mediaType].bytesReceived = Number(stat.stat('bytesReceived')); const addStat = (direction: string, key: string): void => {
response.inbound[mediaType].packetsReceived = Number(stat.stat('packetsReceived')); if (stat[key] != null && response[direction] != null) {
response.inbound[mediaType].packetsLost = Number(stat.stat('packetsLost')); response[direction][mediaType][key] = Number(stat[key]);
response.inbound[mediaType].jitter = Number(stat.stat('googJitterBufferMs'));
response.inbound[mediaType].delayMs = Number(stat.stat('googCurrentDelayMs'));
if(mediaType === 'video'){
response.inbound['video'].framesDecoded = Number(stat.stat('framesDecoded'));
response.inbound['video'].nackCount = Number(stat.stat('nackCount'));
}
} else if(isOutboundRtp) {
response.outbound[mediaType].bytesSent = Number(stat.stat('bytesSent'));
response.outbound[mediaType].packetsSent = Number(stat.stat('packetsSent'));
if(mediaType === 'video'){
response.outbound['video'].framesEncoded = Number(stat.stat('framesEncoded'));
response.outbound['video'].nackCount = Number(stat.stat('nackCount'));
}
}
});
resolve(response);
});
} else {
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 = Number(stat.bytesSent);
response.outbound[mediaType].packetsSent = Number(stat.packetsSent);
if(mediaType === 'video'){
response.outbound[mediaType].framesEncoded = Number(stat.framesEncoded);
}
break;
case "inbound-rtp":
response.inbound[mediaType].bytesReceived = Number(stat.bytesReceived);
response.inbound[mediaType].packetsReceived = Number(stat.packetsReceived);
response.inbound[mediaType].packetsLost = Number(stat.packetsLost);
response.inbound[mediaType].jitter = Number(stat.jitter);
if (mediaType === 'video') {
response.inbound[mediaType].framesDecoded = Number(stat.framesDecoded);
response.inbound[mediaType].nackCount = Number(stat.nackCount);
}
break;
} }
}); }
return resolve(response);
}
switch (stat.type) {
case "outbound-rtp":
addStat('outbound', 'bytesSent');
addStat('outbound', 'packetsSent');
addStat('outbound', 'framesEncoded');
addStat('outbound', 'nackCount');
addStat('outbound', 'firCount');
addStat('outbound', 'pliCount');
addStat('outbound', 'qpSum');
break;
case "inbound-rtp":
addStat('inbound', 'bytesReceived');
addStat('inbound', 'packetsReceived');
addStat('inbound', 'packetsLost');
addStat('inbound', 'jitter');
addStat('inbound', 'framesDecoded');
addStat('inbound', 'nackCount');
addStat('inbound', 'firCount');
addStat('inbound', 'pliCount');
break;
case 'track':
addStat('inbound', 'jitterBufferDelay');
addStat('inbound', 'framesReceived');
addStat('outbound', 'framesDropped');
addStat('outbound', 'framesSent');
addStat(this.stream.isLocal() ? 'outbound' : 'inbound', 'frameHeight');
addStat(this.stream.isLocal() ? 'outbound' : 'inbound', 'frameWidth');
break;
}
});
return resolve(response);
}); });
} }
private generateJSONStatsResponse(stats: IWebrtcStats): JSONStatsResponse { private generateJSONStatsResponse(stats: IWebrtcStats): JSONStatsResponse {
@ -299,19 +339,22 @@ export class WebRtcStats {
}; };
} }
private initWebRtcStatsResponse(): IWebrtcStats { private getWebRtcStatsResponseOutline(): IWebrtcStats {
if (this.stream.isLocal()) {
return { return {
inbound: { outbound: {
audio: {}, audio: {},
video: {} video: {}
}, }
outbound: { };
audio: {}, } else {
video: {} return {
} inbound: {
}; audio: {},
video: {}
}
};
}
} }
} }