2018-04-26 15:33:47 +02:00
/ *
2022-01-13 11:18:47 +01:00
* ( C ) Copyright 2017 - 2022 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 {
2022-08-17 18:04:05 +02:00
interval : number ;
httpEndpoint : string ;
2021-02-16 17:17:37 +01:00
}
2021-02-24 10:43:23 +01:00
interface JSONStatsResponse {
2022-08-17 18:04:05 +02:00
'@timestamp' : string ;
participant_id : string ;
session_id : string ;
platform : string ;
platform_description : string ;
stream : string ;
webrtc_stats : IWebrtcStats ;
2021-02-16 17:17:37 +01:00
}
2021-03-18 13:39:50 +01:00
/ * *
* Common WebRtcSTats for latest Chromium and Firefox versions
* /
2021-02-24 10:43:23 +01:00
interface IWebrtcStats {
2021-03-18 13:39:50 +01:00
inbound ? : {
2022-08-17 18:04:05 +02:00
audio :
| {
bytesReceived : number ;
packetsReceived : number ;
packetsLost : number ;
jitter : number ;
}
| { } ;
video :
| {
bytesReceived : number ;
packetsReceived : number ;
packetsLost : number ;
jitter? : number ; // Firefox
jitterBufferDelay? : number ; // Chrome
framesDecoded : number ;
firCount : number ;
nackCount : number ;
pliCount : number ;
frameHeight? : number ; // Chrome
frameWidth? : number ; // Chrome
framesDropped? : number ; // Chrome
framesReceived? : number ; // Chrome
}
| { } ;
} ;
2021-03-18 13:39:50 +01:00
outbound ? : {
2022-08-17 18:04:05 +02:00
audio :
| {
bytesSent : number ;
packetsSent : number ;
}
| { } ;
video :
| {
bytesSent : number ;
packetsSent : number ;
firCount : number ;
framesEncoded : number ;
nackCount : number ;
pliCount : number ;
qpSum : number ;
frameHeight? : number ; // Chrome
frameWidth? : number ; // Chrome
framesSent? : number ; // Chrome
}
| { } ;
} ;
2021-06-18 12:19:04 +02:00
candidatepair ? : {
2022-08-17 18:04:05 +02:00
currentRoundTripTime? : number ; // Chrome
availableOutgoingBitrate? : number ; //Chrome
2021-06-18 12:19:04 +02:00
// availableIncomingBitrate?: number // No support for any browsers (https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidatePairStats/availableIncomingBitrate)
2022-08-17 18:04:05 +02:00
} ;
}
2021-02-24 10:43:23 +01:00
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 {
2023-08-03 21:23:27 +02:00
let webrtcObj = null ;
// When cross-site (aka third-party) cookies are blocked by the browser,
// accessing localStorage in a third-party iframe throws a DOMException.
try {
webrtcObj = localStorage . getItem ( this . STATS_ITEM_NAME ) ;
}
catch ( e ) { }
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 ) ;
// webrtc object found in local storage
2022-08-17 18:04:05 +02:00
logger . warn (
'WebRtc stats enabled for stream ' + this . stream . streamId + ' of connection ' + this . stream . connection . connectionId
) ;
2021-02-16 17:17:37 +01:00
logger . warn ( 'localStorage item: ' + JSON . stringify ( webrtcStatsConfig ) ) ;
2018-04-26 15:33:47 +02:00
2021-02-24 10:43:23 +01:00
this . POST_URL = webrtcStatsConfig . httpEndpoint ;
2022-08-17 18:04:05 +02:00
this . statsInterval = webrtcStatsConfig . interval ; // Interval in seconds
2021-02-24 10:43:23 +01: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-03-18 13:39:50 +01:00
} else {
2021-02-16 17:17:37 +01:00
logger . debug ( 'WebRtc stats not enabled' ) ;
2018-04-26 15:33:47 +02:00
}
}
2021-03-20 14:04:09 +01:00
// {
// "localCandidate": {
// "id": "RTCIceCandidate_/r4P1y2Q",
// "timestamp": 1616080155617,
// "type": "local-candidate",
// "transportId": "RTCTransport_0_1",
// "isRemote": false,
// "networkType": "wifi",
// "ip": "123.45.67.89",
// "port": 63340,
// "protocol": "udp",
// "candidateType": "srflx",
// "priority": 1686052607,
// "deleted": false,
// "raw": [
// "candidate:3345412921 1 udp 1686052607 123.45.67.89 63340 typ srflx raddr 192.168.1.31 rport 63340 generation 0 ufrag 0ZtT network-id 1 network-cost 10",
// "candidate:58094482 1 udp 41885695 98.76.54.32 44431 typ relay raddr 123.45.67.89 rport 63340 generation 0 ufrag 0ZtT network-id 1 network-cost 10"
// ]
// },
// "remoteCandidate": {
// "id": "RTCIceCandidate_1YO18gph",
// "timestamp": 1616080155617,
// "type": "remote-candidate",
// "transportId": "RTCTransport_0_1",
// "isRemote": true,
// "ip": "12.34.56.78",
// "port": 64989,
// "protocol": "udp",
// "candidateType": "srflx",
// "priority": 1679819263,
// "deleted": false,
// "raw": [
// "candidate:16 1 UDP 1679819263 12.34.56.78 64989 typ srflx raddr 172.19.0.1 rport 64989",
// "candidate:16 1 UDP 1679819263 12.34.56.78 64989 typ srflx raddr 172.19.0.1 rport 64989"
// ]
// }
// }
2021-03-18 13:39:50 +01:00
// 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:
2021-03-20 14:04:09 +01:00
// - Chrome 89.0.4389.90
// - Opera 74.0.3911.232
// - Firefox 86.0.1
// - Microsoft Edge 89.0.774.54
// - Electron 11.3.0 (Chromium 87.0.4280.141)
2021-03-18 13:39:50 +01:00
// - MacOS Desktop:
2021-03-24 22:54:04 +01:00
// - Chrome 89.0.4389.90
// - Firefox 87.0
// - Opera 75.0.3969.93
2021-06-18 12:19:04 +02:00
// - Microsoft Edge 89.0.774.57
// - Safari 14.0 (14610.1.28.1.9)
2021-03-24 22:54:04 +01:00
// - Electron 11.3.0 (Chromium 87.0.4280.141)
2021-03-18 13:39:50 +01:00
// - 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
2021-03-20 14:04:09 +01:00
// - React Native 0.64
2021-03-18 13:39:50 +01:00
// - iOS:
2021-06-18 12:19:04 +02:00
// - Safari Mobile
2021-03-18 13:39:50 +01:00
// - ¿Ionic?
// - ¿React Native?
2021-02-16 18:16:47 +01:00
public getSelectedIceCandidateInfo ( ) : Promise < any > {
2021-03-18 13:39:50 +01:00
return new Promise ( async ( resolve , reject ) = > {
const statsReport : any = await this . stream . getRTCPeerConnection ( ) . getStats ( ) ;
let transportStat ;
const candidatePairs : Map < string , any > = new Map ( ) ;
const localCandidates : Map < string , any > = new Map ( ) ;
const remoteCandidates : Map < string , any > = new Map ( ) ;
statsReport . forEach ( ( stat : any ) = > {
2021-03-24 22:54:04 +01:00
if ( stat . type === 'transport' && ( platform . isChromium ( ) || platform . isSafariBrowser ( ) || platform . isReactNative ( ) ) ) {
2021-03-18 13:39:50 +01:00
transportStat = stat ;
}
switch ( stat . type ) {
case 'candidate-pair' :
candidatePairs . set ( stat . id , stat ) ;
break ;
case 'local-candidate' :
localCandidates . set ( stat . id , stat ) ;
break ;
case 'remote-candidate' :
remoteCandidates . set ( stat . id , stat ) ;
break ;
}
} ) ;
let selectedCandidatePair ;
2021-03-20 14:07:38 +01:00
if ( transportStat != null ) {
2022-08-17 18:04:05 +02:00
const selectedCandidatePairId = transportStat . selectedCandidatePairId ;
2021-03-18 13:39:50 +01:00
selectedCandidatePair = candidatePairs . get ( selectedCandidatePairId ) ;
} else {
2021-03-24 22:54:04 +01:00
// This is basically Firefox
2021-03-18 13:39:50 +01:00
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 ;
2021-02-16 18:16:47 +01:00
}
2021-03-18 13:39:50 +01:00
}
}
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 ) = > {
2022-08-17 18:04:05 +02:00
return (
! ! c . candidate &&
2021-03-18 13:39:50 +01:00
( c . candidate . indexOf ( finalLocalCandidate . ip ) >= 0 || c . candidate . indexOf ( finalLocalCandidate . address ) >= 0 ) &&
2022-08-17 18:04:05 +02:00
c . candidate . indexOf ( finalLocalCandidate . port ) >= 0
) ;
2021-02-16 18:16:47 +01:00
} ) ;
2021-03-18 13:39:50 +01:00
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 ) = > {
2022-08-17 18:04:05 +02:00
return (
! ! c . candidate &&
2021-03-18 13:39:50 +01:00
( c . candidate . indexOf ( finalRemoteCandidate . ip ) >= 0 || c . candidate . indexOf ( finalRemoteCandidate . address ) >= 0 ) &&
2022-08-17 18:04:05 +02:00
c . candidate . indexOf ( finalRemoteCandidate . port ) >= 0
) ;
2021-03-18 13:39:50 +01:00
} ) ;
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' ;
}
2022-01-26 12:17:31 +01:00
return resolve ( {
2021-03-18 13:39:50 +01:00
localCandidate : finalLocalCandidate ,
remoteCandidate : finalRemoteCandidate
} ) ;
2021-02-16 18:16:47 +01:00
} ) ;
}
2018-04-26 15:33:47 +02:00
public stopWebRtcStats() {
if ( this . webRtcStatsEnabled ) {
clearInterval ( this . webRtcStatsIntervalId ) ;
2022-08-17 18:04:05 +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-24 10:43:23 +01:00
private async sendStats ( url : string , response : JSONStatsResponse ) : Promise < void > {
2021-02-16 17:17:37 +01:00
try {
const configuration : RequestInit = {
headers : {
'Content-type' : 'application/json'
2018-06-11 13:19:52 +02:00
} ,
2021-02-24 10:43:23 +01:00
body : JSON.stringify ( response ) ,
2022-08-17 18:04:05 +02:00
method : 'POST'
2018-04-26 15:33:47 +02:00
} ;
2021-02-16 17:17:37 +01:00
await fetch ( url , configuration ) ;
} catch ( error ) {
2021-06-21 12:54:29 +02:00
logger . error ( ` sendStats error: ${ JSON . stringify ( error ) } ` ) ;
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 async sendStatsToHttpEndpoint ( ) : Promise < void > {
try {
2021-03-18 13:39:50 +01:00
const webrtcStats : IWebrtcStats = await this . getCommonStats ( ) ;
2021-02-24 10:43:23 +01:00
const response = this . generateJSONStatsResponse ( webrtcStats ) ;
await this . sendStats ( this . POST_URL , response ) ;
2021-02-16 17:17:37 +01:00
} catch ( error ) {
logger . log ( error ) ;
}
}
2018-04-26 15:33:47 +02:00
2021-03-20 14:04:09 +01:00
// 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 89.0.4389.90
// - Opera 74.0.3911.232
// - Firefox 86.0.1
// - Microsoft Edge 89.0.774.54
// - Electron 11.3.0 (Chromium 87.0.4280.141)
// - MacOS Desktop:
2021-05-19 10:46:43 +02:00
// - Chrome 89.0.4389.90
// - Opera 75.0.3969.93
// - Firefox 87.0
// - Microsoft Edge 89.0.774.57
// - Safari 14.0 (14610.1.28.1.9)
2021-06-18 12:19:04 +02:00
// - Electron 11.3.0 (Chromium 87.0.4280.141)
2021-03-20 14:04:09 +01:00
// - 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 0.64
// - iOS:
2021-06-18 12:19:04 +02:00
// - Safari Mobile
2021-03-20 14:04:09 +01:00
// - ¿Ionic?
// - ¿React Native?
2021-03-18 13:39:50 +01:00
public async getCommonStats ( ) : Promise < IWebrtcStats > {
2021-02-16 17:17:37 +01:00
return new Promise ( async ( resolve , reject ) = > {
2021-09-01 11:36:20 +02:00
try {
const statsReport : any = await this . stream . getRTCPeerConnection ( ) . getStats ( ) ;
const response : IWebrtcStats = this . getWebRtcStatsResponseOutline ( ) ;
const videoTrackStats = [ 'framesReceived' , 'framesDropped' , 'framesSent' , 'frameHeight' , 'frameWidth' ] ;
const candidatePairStats = [ 'availableOutgoingBitrate' , 'currentRoundTripTime' ] ;
statsReport . forEach ( ( stat : any ) = > {
let mediaType = stat . mediaType != null ? stat.mediaType : stat.kind ;
const addStat = ( direction : string , key : string ) : void = > {
if ( stat [ key ] != null && response [ direction ] != null ) {
2022-08-17 18:04:05 +02:00
if ( ! mediaType && videoTrackStats . indexOf ( key ) > - 1 ) {
2021-09-01 11:36:20 +02:00
mediaType = 'video' ;
}
if ( direction != null && mediaType != null && key != null && response [ direction ] [ mediaType ] != null ) {
response [ direction ] [ mediaType ] [ key ] = Number ( stat [ key ] ) ;
2022-08-17 18:04:05 +02:00
} else if ( direction != null && key != null && candidatePairStats . includes ( key ) ) {
2021-09-01 11:36:20 +02:00
// candidate-pair-stats
response [ direction ] [ key ] = Number ( stat [ key ] ) ;
}
2021-03-24 22:54:04 +01:00
}
2022-08-17 18:04:05 +02:00
} ;
2021-03-18 13:39:50 +01:00
2021-09-01 11:36:20 +02:00
switch ( stat . type ) {
2022-08-17 18:04:05 +02:00
case 'outbound-rtp' :
2021-09-01 11:36:20 +02:00
addStat ( 'outbound' , 'bytesSent' ) ;
addStat ( 'outbound' , 'packetsSent' ) ;
addStat ( 'outbound' , 'framesEncoded' ) ;
addStat ( 'outbound' , 'nackCount' ) ;
addStat ( 'outbound' , 'firCount' ) ;
addStat ( 'outbound' , 'pliCount' ) ;
addStat ( 'outbound' , 'qpSum' ) ;
break ;
2022-08-17 18:04:05 +02:00
case 'inbound-rtp' :
2021-09-01 11:36:20 +02:00
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 ;
case 'candidate-pair' :
addStat ( 'candidatepair' , 'currentRoundTripTime' ) ;
addStat ( 'candidatepair' , 'availableOutgoingBitrate' ) ;
break ;
}
} ) ;
// Delete candidatepair from response if null
2022-08-17 18:04:05 +02:00
if ( ! response ? . candidatepair || Object . keys ( < Object > response . candidatepair ) . length === 0 ) {
2021-09-01 11:36:20 +02:00
delete response . candidatepair ;
2021-03-18 13:39:50 +01:00
}
2021-06-18 12:19:04 +02:00
2021-09-01 11:36:20 +02:00
return resolve ( response ) ;
} catch ( error ) {
logger . error ( 'Error getting common stats: ' , error ) ;
return reject ( error ) ;
2021-06-18 12:19:04 +02:00
}
2021-02-16 17:17:37 +01:00
} ) ;
}
2018-04-26 15:33:47 +02:00
2021-02-24 10:43:23 +01:00
private generateJSONStatsResponse ( stats : IWebrtcStats ) : JSONStatsResponse {
2021-02-16 17:17:37 +01:00
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-03-18 13:39:50 +01:00
private getWebRtcStatsResponseOutline ( ) : IWebrtcStats {
if ( this . stream . isLocal ( ) ) {
return {
outbound : {
audio : { } ,
video : { }
2021-06-18 12:19:04 +02:00
} ,
candidatepair : { }
2021-03-18 13:39:50 +01:00
} ;
} else {
return {
inbound : {
audio : { } ,
video : { }
}
} ;
}
2018-04-26 15:33:47 +02:00
}
2022-08-17 18:04:05 +02:00
}