UNPKG

@observertc/client-monitor-js

Version:

ObserveRTC Client Integration Javascript Library

1 lines 11.7 kB
import{schemaVersion as e}from"./schema/ClientSample";import{createLogger as t}from"./utils/logger";import i from"eventemitter3";import{ClientEventTypes as n}from"./schema/ClientEventTypes";import{Sources as s}from"./sources/Sources";import{Detectors as o}from"./detectors/Detectors";import{CpuPerformanceDetector as r}from"./detectors/CpuPerformanceDetector";import{DefaultScoreCalculator as a}from"./scores/DefaultScoreCalculator";import{inferSourceType as c}from"./sources/inferSourceType";import{ClientEventPayloadProvider as l}from"./sources/ClientEventPayloadProvider";const d=t("ClientMonitor");export class ClientMonitor extends i{static samplingSchemaVersion=e;mappedPeerConnections=new Map;detectors;clientEventPayloadProvider=new l;scoreCalculator;closed=!1;lastSampledAt=0;lastCollectingStatsAt=0;cpuPerformanceAlertOn=!1;sendingAudioBitrate=-1;sendingVideoBitrate=-1;receivingAudioBitrate=-1;receivingVideoBitrate=-1;totalAvailableIncomingBitrate=-1;totalAvailableOutgoingBitrate=-1;avgRttInSec=-1;score=5;scoreReasons;_browser;_sources;_timer;_samplingTick=0;_collectingCounter=0;_clientEvents=[];_clientMetaItems=[];_clientIssues=[];_extensionStats=[];durationOfCollectingStatsInMs=0;config;attachments;constructor(e){super(),this.config={...e,collectingPeriodInMs:e?.collectingPeriodInMs??2e3,samplingPeriodInMs:e?.samplingPeriodInMs??8e3,integrateNavigatorMediaDevices:e?.integrateNavigatorMediaDevices??!0,addClientJointEventOnCreated:e?.addClientJointEventOnCreated??!0,addClientLeftEventOnClose:e?.addClientLeftEventOnClose??!0,videoFreezesDetector:e?.videoFreezesDetector??{createIssue:!0},dryInboundTrackDetector:e?.dryInboundTrackDetector??{thresholdInMs:5e3,createIssue:!0},dryOutboundTrackDetector:e?.dryOutboundTrackDetector??{thresholdInMs:5e3,createIssue:!0},audioDesyncDetector:e?.audioDesyncDetector??{fractionalCorrectionAlertOffThreshold:.1,fractionalCorrectionAlertOnThreshold:.05,createIssue:!0},syntheticSamplesDetector:{minSynthesizedSamplesDuration:0},congestionDetector:e?.congestionDetector??{sensitivity:"medium"},cpuPerformanceDetector:e?.cpuPerformanceDetector??{fpsVolatilityThresholds:{lowWatermark:.1,highWatermark:.3},durationOfCollectingStatsThreshold:{lowWatermark:5e3,highWatermark:1e4},createIssue:!0},playoutDiscrepancyDetector:{lowSkewThreshold:2,highSkewThreshold:5,createIssue:!0},longPcConnectionEstablishmentDetector:e?.longPcConnectionEstablishmentDetector??{thresholdInMs:5e3,createIssue:!0},bufferingEventsForSamples:e?.bufferingEventsForSamples??!1},this._sources=new s(this),this.scoreCalculator=new a(this),this.setCollectingPeriod(this.config.collectingPeriodInMs),this.config.samplingPeriodInMs&&this.setSamplingPeriod(this.config.samplingPeriodInMs),!0===this.config.addClientJointEventOnCreated&&this.addClientJoinEvent(),this.config.integrateNavigatorMediaDevices&&this._sources.watchNavigatorMediaDevices();try{this._sources.fetchUserAgentData()}catch(e){d.error("Failed to fetch user agent data",e)}this.detectors=new o(new r(this))}get clientId(){return this.config.clientId}set clientId(e){this.config.clientId=e}get callId(){return this.config.callId}set callId(e){this.config.callId=e}get appData(){return this.config.appData}set appData(e){this.config.appData=e}set browser(e){if(!this.closed&&e){this._browser&&d.warn("Browser info is already set, cannot change it"),this._browser=e;for(const e of this.peerConnections)this._sources.addStatsAdapters(e)}}set onsamplecreated(e){this.on("sample-created",e)}set onstatscollected(e){this.on("stats-collected",e)}set onclientevent(e){this.on("client-event",e)}set onissue(e){this.on("issue",e)}get browser(){return this._browser}close(){this.closed||(clearInterval(this._timer),this._timer=void 0,this.config.addClientLeftEventOnClose&&this.addClientLeftEvent({}),0<this._samplingTick&&this.createSample(),this.closed=!0,this.emit("close"))}on(e,t){return super.on(e,t),this}once(e,t){return super.once(e,t),this}off(e,t){return super.off(e,t),this}emit(e,...t){return super.emit(e,...t)}async collect(){this.closed&&d.warn("ClientMonitor is closed, cannot collet stats"),this.lastCollectingStatsAt=Date.now();const e=[];if(await Promise.allSettled(this.peerConnections.map((async t=>{try{const i=await t.collect();e.push([t.peerConnectionId,i])}catch(e){d.error(`Failed to get stats from peer connection ${t.peerConnectionId}`,e)}}))),this.sendingAudioBitrate=this.peerConnections.reduce(((e,t)=>e+(t.sendingAudioBitrate??0)),0),this.sendingVideoBitrate=this.peerConnections.reduce(((e,t)=>e+(t.sendingVideoBitrate??0)),0),this.receivingAudioBitrate=this.peerConnections.reduce(((e,t)=>e+(t.receivingAudioBitrate??0)),0),this.receivingVideoBitrate=this.peerConnections.reduce(((e,t)=>e+(t.receivingVideoBitrate??0)),0),this.totalAvailableIncomingBitrate=this.peerConnections.reduce(((e,t)=>e+(t.totalAvailableIncomingBitrate??0)),0),this.totalAvailableOutgoingBitrate=this.peerConnections.reduce(((e,t)=>e+(t.totalAvailableOutgoingBitrate??0)),0),this.avgRttInSec=this.peerConnections.reduce(((e,t)=>e+(t.avgRttInSec??0)),0)/this.peerConnections.length,this.durationOfCollectingStatsInMs=Date.now()-this.lastCollectingStatsAt,this.tracks.forEach((e=>e.update())),this.detectors.update(),this.scoreCalculator.update(),this.emit("stats-collected",{clientMonitor:this,startedAt:this.lastCollectingStatsAt,collectedStats:e,durationOfCollectingStatsInMs:this.durationOfCollectingStatsInMs}),0<this._samplingTick){++this._collectingCounter%this._samplingTick===0&&this.createSample()}return e}getPeerConnectionMonitor(e){return this.mappedPeerConnections.get(e)}setScore(e,t){this.closed||(this.score=e,this.scoreReasons=t,this.emit("score",{clientMonitor:this,clientScore:e,currentReasons:t??{}}))}createSample(){if(this.closed)return;const e={clientId:this.clientId,timestamp:Date.now(),callId:this.callId,attachments:this.attachments,peerConnections:this.peerConnections.map((e=>e.createSample())),clientEvents:this._clientEvents,clientMetaItems:this._clientMetaItems,clientIssues:this._clientIssues,extensionStats:this._extensionStats,score:this.score};this._clientEvents=[],this._clientMetaItems=[],this._clientIssues=[],this._extensionStats=[];const t=Date.now();return e?(this.emit("sample-created",{clientMonitor:this,sample:e}),this.lastSampledAt=t,e):void 0}addPeerConnectionMonitor(e){if(!this.closed){if(this.mappedPeerConnections.has(e.peerConnectionId))return d.warn(`PeerConnectionMonitor with id ${e.peerConnectionId} already exists`);e.once("close",(()=>{this.mappedPeerConnections.delete(e.peerConnectionId)})),this.mappedPeerConnections.set(e.peerConnectionId,e),this.emit("new-peerconnnection-monitor",{peerConnectionMonitor:e,clientMonitor:this})}}addClientJoinEvent(e){this.closed||this.addEvent({type:n.CLIENT_JOINED,payload:{...e?.payload},timestamp:e?.timestamp??Date.now()})}addClientLeftEvent(e){this.closed||this.addEvent({type:n.CLIENT_LEFT,payload:{...e?.payload},timestamp:e?.timestamp??Date.now()})}addEvent(e){if(this.closed)return;if(!this._samplingTick&&!this.config.bufferingEventsForSamples)return;const t=e.timestamp??Date.now(),i=e.payload?JSON.stringify(e.payload):void 0;this._clientEvents.push({...e,payload:i,timestamp:t}),this.emit("client-event",{...e,payload:e.payload,timestamp:t})}addIssue(e){if(this.closed)return;if(!this._samplingTick&&!this.config.bufferingEventsForSamples)return;const t=e.payload?JSON.stringify(e.payload):void 0,i=e.timestamp??Date.now();this._clientIssues.push({...e,payload:t,timestamp:i}),this.emit("issue",{...e,payload:e.payload,timestamp:i})}addMetaData(e){if(this.closed)return;if(!this._samplingTick&&!this.config.bufferingEventsForSamples)return;const t=e.timestamp??Date.now();this._clientMetaItems.push({type:e.type,payload:e.payload?JSON.stringify(e.payload):void 0,timestamp:t}),this.emit("meta",{...e,payload:e.payload,timestamp:t})}addExtensionStats(e){if(this.closed)return;if(!this._samplingTick&&!this.config.bufferingEventsForSamples)return;const t=e.payload?JSON.stringify(e.payload):void 0;this._extensionStats.push({type:e.type,payload:t}),this.emit("extension-stats",{...e,payload:e.payload})}addSource(e,t){if(this.closed)return d.warn("Cannot add source to closed ClientMonitor");if(!t&&!(t=c(e)))return d.warn("Cannot add source to ClientMonitor, because it is not a valid source",e);switch(t){case"RTCPeerConnection":this._sources.addRTCPeerConnection({peerConnection:e});break;case"mediasoup-device":this._sources.addMediasoupDevice(e);break;case"mediasoup-transport":this._sources.addMediasoupTransport(e);break;default:return d.warn("Cannot add source to ClientMonitor, because it is not a valid source",e)}}removeSource(e,t){if(this.closed)return d.warn("Cannot remove source from closed ClientMonitor");if(!t&&!(t=c(e)))return d.warn("Cannot remove source from ClientMonitor, because it is not a valid source",e);switch(t){case"RTCPeerConnection":this._sources.removeRTCPeerConnection(e);break;case"mediasoup-device":this._sources.removeMediasoupDevice(e);break;case"mediasoup-transport":this._sources.removeMediasoupTransport(e);break;default:return d.warn("Cannot remove source from ClientMonitor, because it is not a valid source",e)}}fetchUserAgentData(){return this._sources.fetchUserAgentData()}watchNavigatorMediaDevices(){this._sources.watchNavigatorMediaDevices()}get peerConnections(){return[...this.mappedPeerConnections.values()]}get codecs(){return[...this.peerConnections.flatMap((e=>e.codecs))]}get inboundRtps(){return[...this.peerConnections.flatMap((e=>e.inboundRtps))]}get outboundRtps(){return[...this.peerConnections.flatMap((e=>e.outboundRtps))]}get remoteInboundRtps(){return[...this.peerConnections.flatMap((e=>e.remoteInboundRtps))]}get remoteOutboundRtps(){return[...this.peerConnections.flatMap((e=>e.remoteOutboundRtps))]}get mediaSources(){return[...this.peerConnections.flatMap((e=>e.mediaSources))]}get mediaPlayouts(){return[...this.peerConnections.flatMap((e=>e.mediaPlayouts))]}get dataChannels(){return[...this.peerConnections.flatMap((e=>e.dataChannels))]}get iceCandidatePairs(){return[...this.peerConnections.flatMap((e=>e.iceCandidatePairs))]}get iceCandidates(){return[...this.peerConnections.flatMap((e=>e.iceCandidates))]}get iceTransports(){return[...this.peerConnections.flatMap((e=>e.iceTransports))]}get certificates(){return[...this.peerConnections.flatMap((e=>e.certificates))]}get tracks(){return[...this.peerConnections.flatMap((e=>e.tracks))]}getTrackMonitor(e){return this.getInboundTrackMonitor(e)??this.getOutboundTrackMonitor(e)}getInboundTrackMonitor(e){return this.peerConnections.find((t=>t.mappedInboundTracks.has(e)))?.mappedInboundTracks.get(e)}getOutboundTrackMonitor(e){return this.peerConnections.find((t=>t.mappedOutboundTracks.has(e)))?.mappedOutboundTracks.get(e)}setCollectingPeriod(e){this._timer&&clearInterval(this._timer),this._timer=void 0,this.config.collectingPeriodInMs=e;try{if(!this.config.collectingPeriodInMs)return;this._timer=setInterval((()=>{this.collect().catch((e=>d.error(e)))}),this.config.collectingPeriodInMs)}finally{this._setSamplingTick()}}setSamplingPeriod(e){this.config.samplingPeriodInMs=e,this._setSamplingTick()}_setSamplingTick(){void 0!==this.config.collectingPeriodInMs&&void 0!==this.config.samplingPeriodInMs?this.config.collectingPeriodInMs<1||this.config.samplingPeriodInMs<1?this._samplingTick=0:(this.config.samplingPeriodInMs%this.config.collectingPeriodInMs!==0&&d.warn(`The samplingPeriodInMs (${this.config.samplingPeriodInMs}) should be a multiple of collectingPeriodInMs (${this.config.collectingPeriodInMs}), otherwise the sampling will not be accurate`),this._samplingTick=Math.max(1,Math.floor(this.config.samplingPeriodInMs/this.config.collectingPeriodInMs))):this._samplingTick=0}}