2018-04-26 15:33:47 +02:00
|
|
|
/*
|
2020-02-04 11:25:54 +01:00
|
|
|
* (C) Copyright 2017-2020 OpenVidu (https://openvidu.io)
|
2018-04-26 15:33:47 +02:00
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
|
|
|
// tslint:disable:no-string-literal
|
|
|
|
|
2018-05-08 13:01:34 +02:00
|
|
|
import { Stream } from '../../OpenVidu/Stream';
|
2020-05-04 20:01:56 +02:00
|
|
|
import { OpenViduLogger } from '../Logger/OpenViduLogger';
|
2020-10-13 16:13:37 +02:00
|
|
|
import { PlatformUtils } from '../Utils/Platform';
|
2020-05-04 20:01:56 +02:00
|
|
|
/**
|
|
|
|
* @hidden
|
|
|
|
*/
|
|
|
|
const logger: OpenViduLogger = OpenViduLogger.getInstance();
|
2020-10-13 16:13:37 +02:00
|
|
|
/**
|
|
|
|
* @hidden
|
|
|
|
*/
|
2020-11-26 13:17:55 +01:00
|
|
|
let platform: PlatformUtils;
|
2018-04-26 15:33:47 +02:00
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-04-26 15:33:47 +02:00
|
|
|
export class WebRtcStats {
|
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
private readonly STATS_ITEM_NAME = 'webrtc-stats-config';
|
|
|
|
|
2018-04-26 15:33:47 +02:00
|
|
|
private webRtcStatsEnabled = false;
|
|
|
|
private webRtcStatsIntervalId: NodeJS.Timer;
|
|
|
|
private statsInterval = 1;
|
2021-02-16 17:17:37 +01:00
|
|
|
private POST_URL: string;
|
2018-04-26 15:33:47 +02:00
|
|
|
|
2020-11-26 13:17:55 +01:00
|
|
|
constructor(private stream: Stream) {
|
|
|
|
platform = PlatformUtils.getInstance();
|
|
|
|
}
|
2018-04-26 15:33:47 +02:00
|
|
|
|
|
|
|
public isEnabled(): boolean {
|
|
|
|
return this.webRtcStatsEnabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
public initWebRtcStats(): void {
|
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
const webrtcObj = localStorage.getItem(this.STATS_ITEM_NAME);
|
2018-04-26 15:33:47 +02:00
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
if (!!webrtcObj) {
|
2018-04-26 15:33:47 +02:00
|
|
|
|
|
|
|
this.webRtcStatsEnabled = true;
|
2021-02-16 17:17:37 +01:00
|
|
|
const webrtcStatsConfig: WebrtcStatsConfig = JSON.parse(webrtcObj);
|
|
|
|
this.POST_URL = webrtcStatsConfig.httpEndpoint;
|
|
|
|
this.statsInterval = webrtcStatsConfig.interval; // Interval in seconds
|
2018-04-26 15:33:47 +02:00
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
// 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));
|
2018-04-26 15:33:47 +02:00
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
this.webRtcStatsIntervalId = setInterval(async () => {
|
|
|
|
await this.sendStatsToHttpEndpoint();
|
2018-04-26 15:33:47 +02:00
|
|
|
}, this.statsInterval * 1000);
|
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
}else {
|
|
|
|
logger.debug('WebRtc stats not enabled');
|
2018-04-26 15:33:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-16 18:16:47 +01:00
|
|
|
// Used in test-app
|
|
|
|
public getSelectedIceCandidateInfo(): Promise<any> {
|
|
|
|
return new Promise((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];
|
|
|
|
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');
|
|
|
|
}
|
|
|
|
}).catch((error) => {
|
|
|
|
reject(error);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-04-26 15:33:47 +02:00
|
|
|
public stopWebRtcStats() {
|
|
|
|
if (this.webRtcStatsEnabled) {
|
|
|
|
clearInterval(this.webRtcStatsIntervalId);
|
2020-05-04 20:01:56 +02:00
|
|
|
logger.warn('WebRtc stats stopped for disposed stream ' + this.stream.streamId + ' of connection ' + this.stream.connection.connectionId);
|
2018-04-26 15:33:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
private async sendStats(url: string, json: JSONStats): Promise<void> {
|
|
|
|
try {
|
|
|
|
const configuration: RequestInit = {
|
|
|
|
headers: {
|
|
|
|
'Content-type': 'application/json'
|
2018-06-11 13:19:52 +02:00
|
|
|
},
|
2021-02-16 17:17:37 +01:00
|
|
|
body: JSON.stringify(json),
|
|
|
|
method: 'POST',
|
2018-04-26 15:33:47 +02:00
|
|
|
};
|
2021-02-16 17:17:37 +01:00
|
|
|
await fetch(url, configuration);
|
2018-04-26 15:33:47 +02:00
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
} catch (error) {
|
|
|
|
logger.error(error);
|
|
|
|
}
|
|
|
|
}
|
2018-04-26 15:33:47 +02:00
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
2018-04-26 15:33:47 +02:00
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
private async getStats(): Promise<any> {
|
2018-04-26 15:33:47 +02:00
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
return new Promise(async (resolve, reject) => {
|
|
|
|
if (platform.isChromeBrowser() || platform.isChromeMobileBrowser() || platform.isOperaBrowser() || platform.isOperaMobileBrowser()) {
|
2018-04-26 15:33:47 +02:00
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
const pc: any = this.stream.getRTCPeerConnection();
|
|
|
|
pc.getStats((statsReport) => {
|
|
|
|
resolve(this.standardizeReport(statsReport));
|
2018-04-26 15:33:47 +02:00
|
|
|
});
|
2021-02-16 17:17:37 +01:00
|
|
|
} else {
|
|
|
|
const statsReport = await this.stream.getRTCPeerConnection().getStats();
|
|
|
|
resolve(this.standardizeReport(statsReport));
|
|
|
|
}
|
2018-04-26 15:33:47 +02:00
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
});
|
2018-04-26 15:33:47 +02:00
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
}
|
2018-04-26 15:33:47 +02:00
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
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
|
2018-04-26 15:33:47 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
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;
|
2018-06-11 13:19:52 +02:00
|
|
|
});
|
2021-02-16 17:17:37 +01:00
|
|
|
|
|
|
|
return standardReport;
|
2018-04-26 15:33:47 +02:00
|
|
|
}
|
|
|
|
|
2021-02-16 17:17:37 +01:00
|
|
|
// Others platforms
|
|
|
|
response.forEach((values) => {
|
|
|
|
let standardStats: any = {};
|
|
|
|
Object.keys(values).forEach((value: any) => {
|
|
|
|
standardStats[value] = values[value];
|
2018-04-26 15:33:47 +02:00
|
|
|
});
|
2021-02-16 17:17:37 +01:00
|
|
|
standardReport[standardStats.id] = standardStats
|
2018-04-26 15:33:47 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
return standardReport;
|
|
|
|
}
|
|
|
|
}
|