mirror of https://github.com/OpenVidu/openvidu.git
openvidu-browser: WebRTC stats refactoring
parent
076fb233e2
commit
6b9f2be9d6
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue