webrtc-issue-detector
Advanced tools
Comparing version 1.3.2 to 1.4.0
@@ -1,1 +0,1 @@ | ||
"use strict";var e,t,s;function o(){}function n(){n.init.call(this)}function r(e){return void 0===e._maxListeners?n.defaultMaxListeners:e._maxListeners}function i(e,t,s){if(t)e.call(s);else for(var o=e.length,n=f(e,o),r=0;r<o;++r)n[r].call(s)}function a(e,t,s,o){if(t)e.call(s,o);else for(var n=e.length,r=f(e,n),i=0;i<n;++i)r[i].call(s,o)}function c(e,t,s,o,n){if(t)e.call(s,o,n);else for(var r=e.length,i=f(e,r),a=0;a<r;++a)i[a].call(s,o,n)}function u(e,t,s,o,n,r){if(t)e.call(s,o,n,r);else for(var i=e.length,a=f(e,i),c=0;c<i;++c)a[c].call(s,o,n,r)}function d(e,t,s,o){if(t)e.apply(s,o);else for(var n=e.length,r=f(e,n),i=0;i<n;++i)r[i].apply(s,o)}function p(e,t,s,n){var i,a,c,u;if("function"!=typeof s)throw new TypeError('"listener" argument must be a function');if((a=e._events)?(a.newListener&&(e.emit("newListener",t,s.listener?s.listener:s),a=e._events),c=a[t]):(a=e._events=new o,e._eventsCount=0),c){if("function"==typeof c?c=a[t]=n?[s,c]:[c,s]:n?c.unshift(s):c.push(s),!c.warned&&(i=r(e))&&i>0&&c.length>i){c.warned=!0;var d=new Error("Possible EventEmitter memory leak detected. "+c.length+" "+t+" listeners added. Use emitter.setMaxListeners() to increase limit");d.name="MaxListenersExceededWarning",d.emitter=e,d.type=t,d.count=c.length,u=d,"function"==typeof console.warn?console.warn(u):console.log(u)}}else c=a[t]=s,++e._eventsCount;return e}function l(e,t,s){var o=!1;function n(){e.removeListener(t,n),o||(o=!0,s.apply(e,arguments))}return n.listener=s,n}function h(e){var t=this._events;if(t){var s=t[e];if("function"==typeof s)return 1;if(s)return s.length}return 0}function f(e,t){for(var s=new Array(t);t--;)s[t]=e[t];return s}Object.defineProperty(exports,"__esModule",{value:!0}),o.prototype=Object.create(null),n.EventEmitter=n,n.usingDomains=!1,n.prototype.domain=void 0,n.prototype._events=void 0,n.prototype._maxListeners=void 0,n.defaultMaxListeners=10,n.init=function(){this.domain=null,n.usingDomains&&undefined.active,this._events&&this._events!==Object.getPrototypeOf(this)._events||(this._events=new o,this._eventsCount=0),this._maxListeners=this._maxListeners||void 0},n.prototype.setMaxListeners=function(e){if("number"!=typeof e||e<0||isNaN(e))throw new TypeError('"n" argument must be a positive number');return this._maxListeners=e,this},n.prototype.getMaxListeners=function(){return r(this)},n.prototype.emit=function(e){var t,s,o,n,r,p,l,h="error"===e;if(p=this._events)h=h&&null==p.error;else if(!h)return!1;if(l=this.domain,h){if(t=arguments[1],!l){if(t instanceof Error)throw t;var f=new Error('Uncaught, unspecified "error" event. ('+t+")");throw f.context=t,f}return t||(t=new Error('Uncaught, unspecified "error" event')),t.domainEmitter=this,t.domain=l,t.domainThrown=!1,l.emit("error",t),!1}if(!(s=p[e]))return!1;var m="function"==typeof s;switch(o=arguments.length){case 1:i(s,m,this);break;case 2:a(s,m,this,arguments[1]);break;case 3:c(s,m,this,arguments[1],arguments[2]);break;case 4:u(s,m,this,arguments[1],arguments[2],arguments[3]);break;default:for(n=new Array(o-1),r=1;r<o;r++)n[r-1]=arguments[r];d(s,m,this,n)}return!0},n.prototype.addListener=function(e,t){return p(this,e,t,!1)},n.prototype.on=n.prototype.addListener,n.prototype.prependListener=function(e,t){return p(this,e,t,!0)},n.prototype.once=function(e,t){if("function"!=typeof t)throw new TypeError('"listener" argument must be a function');return this.on(e,l(this,e,t)),this},n.prototype.prependOnceListener=function(e,t){if("function"!=typeof t)throw new TypeError('"listener" argument must be a function');return this.prependListener(e,l(this,e,t)),this},n.prototype.removeListener=function(e,t){var s,n,r,i,a;if("function"!=typeof t)throw new TypeError('"listener" argument must be a function');if(!(n=this._events))return this;if(!(s=n[e]))return this;if(s===t||s.listener&&s.listener===t)0==--this._eventsCount?this._events=new o:(delete n[e],n.removeListener&&this.emit("removeListener",e,s.listener||t));else if("function"!=typeof s){for(r=-1,i=s.length;i-- >0;)if(s[i]===t||s[i].listener&&s[i].listener===t){a=s[i].listener,r=i;break}if(r<0)return this;if(1===s.length){if(s[0]=void 0,0==--this._eventsCount)return this._events=new o,this;delete n[e]}else!function(e,t){for(var s=t,o=s+1,n=e.length;o<n;s+=1,o+=1)e[s]=e[o];e.pop()}(s,r);n.removeListener&&this.emit("removeListener",e,a||t)}return this},n.prototype.off=function(e,t){return this.removeListener(e,t)},n.prototype.removeAllListeners=function(e){var t,s;if(!(s=this._events))return this;if(!s.removeListener)return 0===arguments.length?(this._events=new o,this._eventsCount=0):s[e]&&(0==--this._eventsCount?this._events=new o:delete s[e]),this;if(0===arguments.length){for(var n,r=Object.keys(s),i=0;i<r.length;++i)"removeListener"!==(n=r[i])&&this.removeAllListeners(n);return this.removeAllListeners("removeListener"),this._events=new o,this._eventsCount=0,this}if("function"==typeof(t=s[e]))this.removeListener(e,t);else if(t)do{this.removeListener(e,t[t.length-1])}while(t[0]);return this},n.prototype.listeners=function(e){var t,s=this._events;return s&&(t=s[e])?"function"==typeof t?[t.listener||t]:function(e){for(var t=new Array(e.length),s=0;s<t.length;++s)t[s]=e[s].listener||e[s];return t}(t):[]},n.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):h.call(e,t)},n.prototype.listenerCount=h,n.prototype.eventNames=function(){return this._eventsCount>0?Reflect.ownKeys(this._events):[]};class m extends n{}exports.EventType=void 0,(e=exports.EventType||(exports.EventType={})).Issue="issue",e.NetworkScoresUpdated="network-scores-updated",exports.IssueType=void 0,(t=exports.IssueType||(exports.IssueType={})).Network="network",t.CPU="cpu",t.Server="server",t.Stream="stream",exports.IssueReason=void 0,(s=exports.IssueReason||(exports.IssueReason={})).OutboundNetworkQuality="outbound-network-quality",s.InboundNetworkQuality="inbound-network-quality",s.OutboundNetworkMediaLatency="outbound-network-media-latency",s.InboundNetworkMediaLatency="inbound-network-media-latency",s.NetworkMediaSyncFailure="network-media-sync-failure",s.OutboundNetworkThroughput="outbound-network-throughput",s.InboundNetworkThroughput="inbound-network-throughput",s.EncoderCPUThrottling="encoder-cpu-throttling",s.DecoderCPUThrottling="decoder-cpu-throttling",s.ServerIssue="server-issue",s.VideoCodecMismatchIssue="codec-mismatch",s.LowInboundMOS="low-inbound-mean-opinion-score",s.LowOutboundMOS="low-outbound-mean-opinion-score";class v extends n{static STATS_REPORT_READY_EVENT="stats-report-ready";isStopped=!1;reportTimer;getStatsInterval;compositeStatsParser;constructor(e){super(),this.compositeStatsParser=e.compositeStatsParser,this.getStatsInterval=e.getStatsInterval??1e4}get isRunning(){return!!this.reportTimer&&!this.isStopped}startReporting(){if(this.reportTimer)return;const e=()=>setTimeout((()=>{this.isStopped||this.parseReports().finally((()=>e()))}),this.getStatsInterval);this.isStopped=!1,this.reportTimer=e()}stopReporting(){this.isStopped=!0,this.reportTimer&&(clearTimeout(this.reportTimer),this.reportTimer=void 0)}async parseReports(){(await this.compositeStatsParser.parse()).forEach((e=>{this.emit(v.STATS_REPORT_READY_EVENT,e)}))}}class g{#e={};calculate(e){const t=this.calcucateOutboundScore(e),s=this.calculateInboundScore(e);return this.#e[e.connection.id]=e,{outbound:t,inbound:s}}calcucateOutboundScore(e){const t=[...e.remote?.audio.inbound||[],...e.remote?.video.inbound||[]];if(!t.length)return;const s=this.#e[e.connection.id];if(!s)return;const o=[...s.remote?.audio.inbound||[],...s.remote?.video.inbound||[]],{packetsSent:n}=e.connection,r=s.connection.packetsSent,i=t.reduce(((e,t)=>{const s=o.find((e=>e.ssrc===t.ssrc));return{sumJitter:e.sumJitter+t.jitter,packetsLost:e.packetsLost+t.packetsLost,lastPacketsLost:e.lastPacketsLost+(s?.packetsLost||0)}}),{sumJitter:0,packetsLost:0,lastPacketsLost:0}),a=1e3*e.connection.currentRoundTripTime||0,{sumJitter:c}=i,u=c/t.length,d=n-r,p=i.packetsLost-i.lastPacketsLost,l=d&&p?Math.round(100*p/(d+p)):0;return this.calculateMOS({avgJitter:u,rtt:a,packetsLoss:l})}calculateInboundScore(e){const t=[...e.audio?.inbound,...e.video?.inbound];if(!t.length)return;const s=this.#e[e.connection.id];if(!s)return;const o=[...s.video?.inbound,...s.audio?.inbound],{packetsReceived:n}=e.connection,r=s.connection.packetsReceived,i=t.reduce(((e,t)=>{const s=o.find((e=>e.ssrc===t.ssrc));return{sumJitter:e.sumJitter+t.jitter,packetsLost:e.packetsLost+t.packetsLost,lastPacketsLost:e.lastPacketsLost+(s?.packetsLost||0)}}),{sumJitter:0,packetsLost:0,lastPacketsLost:0}),a=1e3*e.connection.currentRoundTripTime||0,{sumJitter:c}=i,u=c/t.length,d=n-r,p=i.packetsLost-i.lastPacketsLost,l=d&&p?Math.round(100*p/(d+p)):0;return this.calculateMOS({avgJitter:u,rtt:a,packetsLoss:l})}calculateMOS({avgJitter:e,rtt:t,packetsLoss:s}){const o=t+2*e+10;let n=o<160?93.2-o/40:93.2-o/120-10;return n-=2.5*s,1+.035*n+7e-6*n*(n-60)*(100-n)}}class y{#t=1e5;detect(e){const t=[],{availableOutgoingBitrate:s}=e.connection;if(void 0===s)return t;const o=e.audio.outbound.reduce(((e,t)=>e+t.targetBitrate),0),n=e.video.outbound.reduce(((e,t)=>e+t.bitrate),0);return o||n?o>s?(t.push({type:exports.IssueType.Network,reason:exports.IssueReason.OutboundNetworkThroughput,debug:`availableOutgoingBitrate: ${s}, audioStreamsTotalTargetBitrate: ${o}`}),t):n>0&&s<this.#t?(t.push({type:exports.IssueType.Network,reason:exports.IssueReason.OutboundNetworkThroughput,debug:`availableOutgoingBitrate: ${s}, videoStreamsTotalBitrate: ${n}`}),t):t:t}}class b{#e={};#s=.5;detect(e){const t=this.processData(e);return this.#e[e.connection.id]=e,t}processData(e){const t=e.video.inbound.filter((e=>e.framesDropped>0)),s=[],o=this.#e[e.connection.id]?.video.inbound;return o?(t.forEach((e=>{const t=o.find((t=>t.ssrc===e.ssrc));if(!t)return;if(e.framesDropped===t.framesDropped)return;const n=e.framesReceived-t.framesReceived,r=e.framesDecoded-t.framesDecoded,i=e.framesDropped-t.framesDropped;if(0===n&&0===r)return;const a=i/n;a>=this.#s&&s.push({type:exports.IssueType.CPU,reason:exports.IssueReason.DecoderCPUThrottling,ssrc:e.ssrc,debug:`framesDropped: ${Math.round(100*a)} , deltaFramesDropped: ${i}, deltaFramesReceived: ${n}`})})),s):s}}class S{#e={};#o=.15;detect(e){const t=this.processData(e);return this.#e[e.connection.id]=e,t}processData(e){const t=e.video.outbound.filter((e=>e.framesEncoded>0)),s=[],o=this.#e[e.connection.id]?.video.outbound;return o?(t.forEach((e=>{const t=o.find((t=>t.ssrc===e.ssrc));if(!t)return;if(e.framesEncoded===t.framesEncoded)return;const n=e.framesEncoded-t.framesEncoded,r=e.framesSent-t.framesSent;if(0===n)return;if(n===r)return;const i=r/n;i>=this.#o&&s.push({type:exports.IssueType.Network,reason:exports.IssueReason.OutboundNetworkThroughput,ssrc:e.ssrc,debug:`missedFrames: ${Math.round(100*i)}%`})})),s):s}}class w{#e={};detect(e){const t=this.processData(e);return this.#e[e.connection.id]=e,t}processData(e){const t=[],s=[...e.audio?.inbound,...e.video?.inbound];if(!s.length)return t;const o=this.#e[e.connection.id];if(!o)return t;const n=[...o.video?.inbound,...o.audio?.inbound],{packetsReceived:r}=e.connection,i=o.connection.packetsReceived,a=s.reduce(((e,t)=>{const s=n.find((e=>e.ssrc===t.ssrc)),o=s?.jitterBufferDelay||0,r=s?.jitterBufferEmittedCount||0,i=t.jitterBufferDelay-o,a=t.jitterBufferEmittedCount-r,c=i&&a?1e3*i/a:0;return{sumJitter:e.sumJitter+t.jitter,sumJitterBufferDelayMs:e.sumJitterBufferDelayMs+c,packetsLost:e.packetsLost+t.packetsLost,lastPacketsLost:e.lastPacketsLost+(s?.packetsLost||0)}}),{sumJitter:0,sumJitterBufferDelayMs:0,packetsLost:0,lastPacketsLost:0}),c=1e3*e.connection.currentRoundTripTime||0,{sumJitter:u,sumJitterBufferDelayMs:d}=a,p=u/s.length,l=d/s.length,h=r-i,f=a.packetsLost-a.lastPacketsLost,m=h&&f?Math.round(100*f/(h+f)):0,v=m>5,g=p>=200,y=c>=250&&!g&&!v,b=v&&g,S=g&&l>500,w=`packetLoss: ${m}%, jitter: ${p}, rtt: ${c}, jitterBuffer: ${l}ms`;return(!v&&g||g||v)&&t.push({type:exports.IssueType.Network,reason:exports.IssueReason.InboundNetworkQuality,iceCandidate:e.connection.local.id,debug:w}),y&&t.push({type:exports.IssueType.Server,reason:exports.IssueReason.ServerIssue,iceCandidate:e.connection.remote.id,debug:w}),b&&t.push({type:exports.IssueType.Network,reason:exports.IssueReason.InboundNetworkMediaLatency,iceCandidate:e.connection.local.id,debug:w}),S&&t.push({type:exports.IssueType.Network,reason:exports.IssueReason.NetworkMediaSyncFailure,iceCandidate:e.connection.local.id,debug:w}),t}}class k{#e={};detect(e){const t=this.processData(e);return this.#e[e.connection.id]=e,t}processData(e){const t=e.audio.inbound,s=[],o=this.#e[e.connection.id]?.audio.inbound;return o?(t.forEach((e=>{const t=o.find((t=>t.ssrc===e.ssrc));if(!t)return;const n=e.track.insertedSamplesForDeceleration+e.track.removedSamplesForAcceleration,r=t.track.insertedSamplesForDeceleration+t.track.removedSamplesForAcceleration;if(n===r)return;const i=e.track.totalSamplesReceived-t.track.totalSamplesReceived,a=n-r,c=Math.round(100*a/i);c>5&&s.push({type:exports.IssueType.Network,reason:exports.IssueReason.NetworkMediaSyncFailure,ssrc:e.ssrc,debug:`correctedSamplesPercentage: ${c}%`})})),s):s}}class I{#e={};detect(e){const t=this.processData(e);return this.#e[e.connection.id]=e,t}processData(e){const t=[],s=[...e.remote?.audio.inbound||[],...e.remote?.video.inbound||[]];if(!s.length)return t;const o=this.#e[e.connection.id];if(!o)return t;const n=[...o.remote?.audio.inbound||[],...o.remote?.video.inbound||[]],{packetsSent:r}=e.connection,i=o.connection.packetsSent,a=s.reduce(((e,t)=>{const s=n.find((e=>e.ssrc===t.ssrc));return{sumJitter:e.sumJitter+t.jitter,packetsLost:e.packetsLost+t.packetsLost,lastPacketsLost:e.lastPacketsLost+(s?.packetsLost||0)}}),{sumJitter:0,packetsLost:0,lastPacketsLost:0}),c=1e3*e.connection.currentRoundTripTime||0,{sumJitter:u}=a,d=u/s.length,p=r-i,l=a.packetsLost-a.lastPacketsLost,h=p&&l?Math.round(100*l/(p+l)):0,f=h>5,m=d>=200,v=!f&&m||m||f,g=`packetLoss: ${h}%, jitter: ${d}, rtt: ${c}`;return f&&m&&t.push({type:exports.IssueType.Network,reason:exports.IssueReason.OutboundNetworkMediaLatency,iceCandidate:e.connection.local.id,debug:g}),v&&t.push({type:exports.IssueType.Network,reason:exports.IssueReason.OutboundNetworkQuality,iceCandidate:e.connection.local.id,debug:g}),t}}class L{#e={};detect(e){const t=this.processData(e);return this.#e[e.connection.id]=e,t}processData(e){const t=e.video.outbound.filter((e=>"none"!==e.qualityLimitationReason)),s=[],o=this.#e[e.connection.id]?.video.outbound;return o?(t.forEach((e=>{const t=o.find((t=>t.ssrc===e.ssrc));t&&(e.framesSent>t.framesSent||("cpu"===e.qualityLimitationReason&&s.push({type:exports.IssueType.CPU,reason:exports.IssueReason.EncoderCPUThrottling,ssrc:e.ssrc,debug:"qualityLimitationReason: cpu"}),"bandwidth"===e.qualityLimitationReason&&s.push({type:exports.IssueType.Network,reason:exports.IssueReason.OutboundNetworkThroughput,ssrc:e.ssrc,debug:"qualityLimitationReason: bandwidth"})))})),s):s}}class T{UNKNOWN_DECODER="unknown";#e={};#n={};detect(e){const t=this.processData(e);return this.#e[e.connection.id]=e,t}processData(e){const t=[],{id:s}=e.connection,o=this.#e[s]?.video.inbound;return e.video.inbound.forEach((e=>{const{decoderImplementation:n,ssrc:r}=e,i=o?.find((e=>e.ssrc===r));i&&(n===this.UNKNOWN_DECODER?this.hadLastDecoderWithIssue(s,r)||(this.setLastDecoderWithIssue(s,r,this.UNKNOWN_DECODER),t.push({ssrc:r,type:exports.IssueType.Stream,reason:exports.IssueReason.VideoCodecMismatchIssue,trackIdentifier:e.track.trackIdentifier,debug:`mimeType: ${e.mimeType}, decoderImplementation: ${n}`})):this.setLastDecoderWithIssue(s,r,void 0))})),t}setLastDecoderWithIssue(e,t,s){const o=this.#n[e]??{};void 0===s?delete o[t]:o[t]=s,this.#n[e]=o}hadLastDecoderWithIssue(e,t){const s=this.#n[e];return(s&&s[t])===this.UNKNOWN_DECODER}}const R=e=>"closed"===e.iceConnectionState||"closed"===e.connectionState,P=(e,t,s)=>8*((e,t,s)=>{if(!t)return 0;const o=e[s],n=t[s];if(null==o||null==n)return 0;const r=Math.floor(e.timestamp)-Math.floor(t.timestamp);return 0===r?0:(Number(o)-Number(n))/r*1e3})(e,t,s);class C{connections=[];statsParser;constructor(e){this.statsParser=e.statsParser}listConnections(){return[...this.connections]}addPeerConnection(e){this.connections.push({id:e.id??String(Date.now()+Math.random().toString(32)),pc:e.pc})}removePeerConnection(e){const t=this.connections.findIndex((({pc:t})=>t===e.pc));t>=0&&this.removeConnectionsByIndexes([t])}async parse(){const e=[],t=this.connections.map((async(t,s)=>{if(!R(t.pc))return this.statsParser.parse(t);e.unshift(s)}));e.length&&this.removeConnectionsByIndexes(e);return(await Promise.all(t)).filter((e=>void 0!==e))}removeConnectionsByIndexes(e){e.forEach((e=>{this.connections.splice(e,1)}))}}class x{prevStats=new Map;allowedReportTypes=["candidate-pair","inbound-rtp","outbound-rtp","remote-outbound-rtp","remote-inbound-rtp","track","transport"];logger;constructor(e){this.logger=e.logger}async parse(e){if(!R(e.pc))return this.getConnectionStats(e);this.logger.debug("Skip stats parsing. Connection is closed.",{connection:e})}async getConnectionStats(e){const{pc:t,id:s}=e;try{const o=Date.now(),n=await Promise.all(t.getReceivers().map((e=>e.getStats()))),r=await Promise.all(t.getSenders().map((e=>e.getStats())));return{id:s,stats:this.mapReportsStats([...n,...r],e),timeTaken:Date.now()-o}}catch(e){return void this.logger.error("Failed to get stats for PC",{id:s,pc:t,error:e})}}mapReportsStats(e,t){const s={audio:{inbound:[],outbound:[]},video:{inbound:[],outbound:[]},connection:{},remote:{video:{inbound:[],outbound:[]},audio:{inbound:[],outbound:[]}}};e.forEach((e=>{e.forEach((t=>{this.allowedReportTypes.includes(t.type)&&this.updateMappedStatsWithReportItemData(t,s,e)}))}));const o=this.prevStats.get(t.id);return o&&this.propagateStatsWithRateValues(s,o.stats),this.prevStats.set(t.id,{stats:s,ts:Date.now()}),s}updateMappedStatsWithReportItemData(e,t,s){const o=e.type;if("candidate-pair"===o&&"succeeded"===e.state&&e.nominated)return void(t.connection=this.prepareConnectionStats(e,s));const n=this.getMediaType(e);if(n)if("outbound-rtp"!==o)if("inbound-rtp"!==o)"remote-outbound-rtp"!==o?"remote-inbound-rtp"===o&&(this.mapConnectionStatsIfNecessary(t,e,s),t.remote[n].inbound.push({...e})):t.remote[n].outbound.push({...e});else{const o=s.get(e.trackId)||s.get(e.mediaSourceId)||{};this.mapConnectionStatsIfNecessary(t,e,s);const r={...e,track:{...o}};t[n].inbound.push(r)}else{const o=s.get(e.trackId)||s.get(e.mediaSourceId)||{},r={...e,track:{...o}};t[n].outbound.push(r)}}getMediaType(e){const t=e.mediaType||e.kind;if(!["audio","video"].includes(t)){const{id:t}=e;if(!t)return;return String(t).includes("Video")?"video":String(t).includes("Audio")?"audio":void 0}return t}propagateStatsWithRateValues(e,t){e.audio.inbound.forEach((e=>{const s=t.audio.inbound.find((({id:t})=>t===e.id));e.bitrate=P(e,s,"bytesReceived"),e.packetRate=P(e,s,"packetsReceived")})),e.audio.outbound.forEach((e=>{const s=t.audio.outbound.find((({id:t})=>t===e.id));e.bitrate=P(e,s,"bytesSent"),e.packetRate=P(e,s,"packetsSent")})),e.video.inbound.forEach((e=>{const s=t.video.inbound.find((({id:t})=>t===e.id));e.bitrate=P(e,s,"bytesReceived"),e.packetRate=P(e,s,"packetsReceived")})),e.video.outbound.forEach((e=>{const s=t.video.outbound.find((({id:t})=>t===e.id));e.bitrate=P(e,s,"bytesSent"),e.packetRate=P(e,s,"packetsSent")}))}mapConnectionStatsIfNecessary(e,t,s){if(e.connection.id||!t.transportId)return;const o=s.get(t.transportId);if(o&&o.selectedCandidatePairId){const t=s.get(o.selectedCandidatePairId);e.connection=this.prepareConnectionStats(t,s)}}prepareConnectionStats(e,t){if(!e||!t)return{};const s={...e};if(s.remoteCandidateId){const e=t.get(s.remoteCandidateId);s.remote={...e}}if(s.localCandidateId){const e=t.get(s.localCandidateId);s.local={...e}}return s}}exports.AvailableOutgoingBitrateIssueDetector=y,exports.CompositeRTCStatsParser=C,exports.FramesDroppedIssueDetector=b,exports.FramesEncodedSentIssueDetector=S,exports.InboundNetworkIssueDetector=w,exports.NetworkMediaSyncIssueDetector=k,exports.NetworkScoresCalculator=g,exports.OutboundNetworkIssueDetector=I,exports.PeriodicWebRTCStatsReporter=v,exports.QualityLimitationsIssueDetector=L,exports.RTCStatsParser=x,exports.VideoCodecMismatchDetector=T,exports.WebRTCIssueEmitter=m,exports.default=class{eventEmitter;#r=!1;detectors=[];networkScoresCalculator;statsReporter;compositeStatsParser;logger;constructor(e){this.logger=e.logger??{debug:()=>{},info:()=>{},warn:()=>{},error:()=>{}},this.eventEmitter=e.issueEmitter??new m,e.onIssues&&this.eventEmitter.on(exports.EventType.Issue,e.onIssues),e.onNetworkScoresUpdated&&this.eventEmitter.on(exports.EventType.NetworkScoresUpdated,e.onNetworkScoresUpdated),this.detectors=e.detectors??[new L,new b,new S,new w,new I,new k,new y,new T],this.networkScoresCalculator=e.networkScoresCalculator??new g,this.compositeStatsParser=e.compositeStatsParser??new C({statsParser:new x({logger:this.logger})}),this.statsReporter=e.statsReporter??new v({compositeStatsParser:this.compositeStatsParser,getStatsInterval:e.getStatsInterval??5e3}),window.wid=this,this.wrapRTCPeerConnection(),this.statsReporter.on(v.STATS_REPORT_READY_EVENT,(t=>{this.detectIssues({data:t.stats,ignoreSSRCList:e.ignoreSSRCList}),this.calculateNetworkScores(t.stats)}))}watchNewPeerConnections(){if(this.#r)throw new Error("WebRTCIssueDetector is already started");this.#r=!0,this.statsReporter.startReporting()}stopWatchingNewPeerConnections(){if(!this.#r)throw new Error("WebRTCIssueDetector is already stopped");this.#r=!1,this.statsReporter.stopReporting()}handleNewPeerConnection(e){this.#r?(this.logger.debug("Handling new peer connection",e),this.compositeStatsParser.addPeerConnection({pc:e})):this.logger.debug("Skip handling new peer connection. Detector is not running",e)}emitIssues(e){this.eventEmitter.emit(exports.EventType.Issue,e)}detectIssues({data:e,ignoreSSRCList:t}){let s=this.detectors.reduce(((t,s)=>[...t,...s.detect(e)]),[]);t?.length&&(s=s.filter((e=>!e.ssrc||!t.includes(e.ssrc)))),s.length>0&&this.emitIssues(s)}calculateNetworkScores(e){const t=this.networkScoresCalculator.calculate(e);this.eventEmitter.emit(exports.EventType.NetworkScoresUpdated,t)}wrapRTCPeerConnection(){if(!window.RTCPeerConnection)return;const e=window.RTCPeerConnection,t=e=>this.handleNewPeerConnection(e);function s(s){const o=new e(s);return t(o),o}s.prototype=e.prototype,window.RTCPeerConnection=s}}; | ||
"use strict";var e,t,s;function o(){}function n(){n.init.call(this)}function r(e){return void 0===e._maxListeners?n.defaultMaxListeners:e._maxListeners}function i(e,t,s){if(t)e.call(s);else for(var o=e.length,n=m(e,o),r=0;r<o;++r)n[r].call(s)}function a(e,t,s,o){if(t)e.call(s,o);else for(var n=e.length,r=m(e,n),i=0;i<n;++i)r[i].call(s,o)}function c(e,t,s,o,n){if(t)e.call(s,o,n);else for(var r=e.length,i=m(e,r),a=0;a<r;++a)i[a].call(s,o,n)}function u(e,t,s,o,n,r){if(t)e.call(s,o,n,r);else for(var i=e.length,a=m(e,i),c=0;c<i;++c)a[c].call(s,o,n,r)}function d(e,t,s,o){if(t)e.apply(s,o);else for(var n=e.length,r=m(e,n),i=0;i<n;++i)r[i].apply(s,o)}function p(e,t,s,n){var i,a,c,u;if("function"!=typeof s)throw new TypeError('"listener" argument must be a function');if((a=e._events)?(a.newListener&&(e.emit("newListener",t,s.listener?s.listener:s),a=e._events),c=a[t]):(a=e._events=new o,e._eventsCount=0),c){if("function"==typeof c?c=a[t]=n?[s,c]:[c,s]:n?c.unshift(s):c.push(s),!c.warned&&(i=r(e))&&i>0&&c.length>i){c.warned=!0;var d=new Error("Possible EventEmitter memory leak detected. "+c.length+" "+t+" listeners added. Use emitter.setMaxListeners() to increase limit");d.name="MaxListenersExceededWarning",d.emitter=e,d.type=t,d.count=c.length,u=d,"function"==typeof console.warn?console.warn(u):console.log(u)}}else c=a[t]=s,++e._eventsCount;return e}function l(e,t,s){var o=!1;function n(){e.removeListener(t,n),o||(o=!0,s.apply(e,arguments))}return n.listener=s,n}function h(e){var t=this._events;if(t){var s=t[e];if("function"==typeof s)return 1;if(s)return s.length}return 0}function m(e,t){for(var s=new Array(t);t--;)s[t]=e[t];return s}Object.defineProperty(exports,"__esModule",{value:!0}),o.prototype=Object.create(null),n.EventEmitter=n,n.usingDomains=!1,n.prototype.domain=void 0,n.prototype._events=void 0,n.prototype._maxListeners=void 0,n.defaultMaxListeners=10,n.init=function(){this.domain=null,n.usingDomains&&undefined.active,this._events&&this._events!==Object.getPrototypeOf(this)._events||(this._events=new o,this._eventsCount=0),this._maxListeners=this._maxListeners||void 0},n.prototype.setMaxListeners=function(e){if("number"!=typeof e||e<0||isNaN(e))throw new TypeError('"n" argument must be a positive number');return this._maxListeners=e,this},n.prototype.getMaxListeners=function(){return r(this)},n.prototype.emit=function(e){var t,s,o,n,r,p,l,h="error"===e;if(p=this._events)h=h&&null==p.error;else if(!h)return!1;if(l=this.domain,h){if(t=arguments[1],!l){if(t instanceof Error)throw t;var m=new Error('Uncaught, unspecified "error" event. ('+t+")");throw m.context=t,m}return t||(t=new Error('Uncaught, unspecified "error" event')),t.domainEmitter=this,t.domain=l,t.domainThrown=!1,l.emit("error",t),!1}if(!(s=p[e]))return!1;var f="function"==typeof s;switch(o=arguments.length){case 1:i(s,f,this);break;case 2:a(s,f,this,arguments[1]);break;case 3:c(s,f,this,arguments[1],arguments[2]);break;case 4:u(s,f,this,arguments[1],arguments[2],arguments[3]);break;default:for(n=new Array(o-1),r=1;r<o;r++)n[r-1]=arguments[r];d(s,f,this,n)}return!0},n.prototype.addListener=function(e,t){return p(this,e,t,!1)},n.prototype.on=n.prototype.addListener,n.prototype.prependListener=function(e,t){return p(this,e,t,!0)},n.prototype.once=function(e,t){if("function"!=typeof t)throw new TypeError('"listener" argument must be a function');return this.on(e,l(this,e,t)),this},n.prototype.prependOnceListener=function(e,t){if("function"!=typeof t)throw new TypeError('"listener" argument must be a function');return this.prependListener(e,l(this,e,t)),this},n.prototype.removeListener=function(e,t){var s,n,r,i,a;if("function"!=typeof t)throw new TypeError('"listener" argument must be a function');if(!(n=this._events))return this;if(!(s=n[e]))return this;if(s===t||s.listener&&s.listener===t)0==--this._eventsCount?this._events=new o:(delete n[e],n.removeListener&&this.emit("removeListener",e,s.listener||t));else if("function"!=typeof s){for(r=-1,i=s.length;i-- >0;)if(s[i]===t||s[i].listener&&s[i].listener===t){a=s[i].listener,r=i;break}if(r<0)return this;if(1===s.length){if(s[0]=void 0,0==--this._eventsCount)return this._events=new o,this;delete n[e]}else!function(e,t){for(var s=t,o=s+1,n=e.length;o<n;s+=1,o+=1)e[s]=e[o];e.pop()}(s,r);n.removeListener&&this.emit("removeListener",e,a||t)}return this},n.prototype.off=function(e,t){return this.removeListener(e,t)},n.prototype.removeAllListeners=function(e){var t,s;if(!(s=this._events))return this;if(!s.removeListener)return 0===arguments.length?(this._events=new o,this._eventsCount=0):s[e]&&(0==--this._eventsCount?this._events=new o:delete s[e]),this;if(0===arguments.length){for(var n,r=Object.keys(s),i=0;i<r.length;++i)"removeListener"!==(n=r[i])&&this.removeAllListeners(n);return this.removeAllListeners("removeListener"),this._events=new o,this._eventsCount=0,this}if("function"==typeof(t=s[e]))this.removeListener(e,t);else if(t)do{this.removeListener(e,t[t.length-1])}while(t[0]);return this},n.prototype.listeners=function(e){var t,s=this._events;return s&&(t=s[e])?"function"==typeof t?[t.listener||t]:function(e){for(var t=new Array(e.length),s=0;s<t.length;++s)t[s]=e[s].listener||e[s];return t}(t):[]},n.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):h.call(e,t)},n.prototype.listenerCount=h,n.prototype.eventNames=function(){return this._eventsCount>0?Reflect.ownKeys(this._events):[]};class f extends n{}exports.EventType=void 0,(e=exports.EventType||(exports.EventType={})).Issue="issue",e.NetworkScoresUpdated="network-scores-updated",exports.IssueType=void 0,(t=exports.IssueType||(exports.IssueType={})).Network="network",t.CPU="cpu",t.Server="server",t.Stream="stream",exports.IssueReason=void 0,(s=exports.IssueReason||(exports.IssueReason={})).OutboundNetworkQuality="outbound-network-quality",s.InboundNetworkQuality="inbound-network-quality",s.OutboundNetworkMediaLatency="outbound-network-media-latency",s.InboundNetworkMediaLatency="inbound-network-media-latency",s.NetworkMediaSyncFailure="network-media-sync-failure",s.OutboundNetworkThroughput="outbound-network-throughput",s.InboundNetworkThroughput="inbound-network-throughput",s.EncoderCPUThrottling="encoder-cpu-throttling",s.DecoderCPUThrottling="decoder-cpu-throttling",s.ServerIssue="server-issue",s.VideoCodecMismatchIssue="codec-mismatch",s.LowInboundMOS="low-inbound-mean-opinion-score",s.LowOutboundMOS="low-outbound-mean-opinion-score";class v extends n{static STATS_REPORT_READY_EVENT="stats-report-ready";isStopped=!1;reportTimer;getStatsInterval;compositeStatsParser;constructor(e){super(),this.compositeStatsParser=e.compositeStatsParser,this.getStatsInterval=e.getStatsInterval??1e4}get isRunning(){return!!this.reportTimer&&!this.isStopped}startReporting(){if(this.reportTimer)return;const e=()=>setTimeout((()=>{this.isStopped||this.parseReports().finally((()=>e()))}),this.getStatsInterval);this.isStopped=!1,this.reportTimer=e()}stopReporting(){this.isStopped=!0,this.reportTimer&&(clearTimeout(this.reportTimer),this.reportTimer=void 0)}async parseReports(){(await this.compositeStatsParser.parse()).forEach((e=>{this.emit(v.STATS_REPORT_READY_EVENT,e)}))}}class g{#e={};calculate(e){const t=this.calculateOutboundScore(e),s=this.calculateInboundScore(e);return this.#e[e.connection.id]=e,{outbound:t,inbound:s}}calculateOutboundScore(e){const t=[...e.remote?.audio.inbound||[],...e.remote?.video.inbound||[]];if(!t.length)return;const s=this.#e[e.connection.id];if(!s)return;const o=[...s.remote?.audio.inbound||[],...s.remote?.video.inbound||[]],{packetsSent:n}=e.connection,r=s.connection.packetsSent,i=t.reduce(((e,t)=>{const s=o.find((e=>e.ssrc===t.ssrc));return{sumJitter:e.sumJitter+t.jitter,packetsLost:e.packetsLost+t.packetsLost,lastPacketsLost:e.lastPacketsLost+(s?.packetsLost||0)}}),{sumJitter:0,packetsLost:0,lastPacketsLost:0}),a=1e3*e.connection.currentRoundTripTime||0,{sumJitter:c}=i,u=c/t.length,d=n-r,p=i.packetsLost-i.lastPacketsLost,l=d&&p?Math.round(100*p/(d+p)):0;return this.calculateMOS({avgJitter:u,rtt:a,packetsLoss:l})}calculateInboundScore(e){const t=[...e.audio?.inbound,...e.video?.inbound];if(!t.length)return;const s=this.#e[e.connection.id];if(!s)return;const o=[...s.video?.inbound,...s.audio?.inbound],{packetsReceived:n}=e.connection,r=s.connection.packetsReceived,i=t.reduce(((e,t)=>{const s=o.find((e=>e.ssrc===t.ssrc));return{sumJitter:e.sumJitter+t.jitter,packetsLost:e.packetsLost+t.packetsLost,lastPacketsLost:e.lastPacketsLost+(s?.packetsLost||0)}}),{sumJitter:0,packetsLost:0,lastPacketsLost:0}),a=1e3*e.connection.currentRoundTripTime||0,{sumJitter:c}=i,u=c/t.length,d=n-r,p=i.packetsLost-i.lastPacketsLost,l=d&&p?Math.round(100*p/(d+p)):0;return this.calculateMOS({avgJitter:u,rtt:a,packetsLoss:l})}calculateMOS({avgJitter:e,rtt:t,packetsLoss:s}){const o=t+2*e+10;let n=o<160?93.2-o/40:93.2-o/120-10;return n-=2.5*s,1+.035*n+7e-6*n*(n-60)*(100-n)}}class S{#t=1e5;detect(e){const t=[],{availableOutgoingBitrate:s}=e.connection;if(void 0===s)return t;const o=e.audio.outbound.reduce(((e,t)=>e+t.targetBitrate),0),n=e.video.outbound.reduce(((e,t)=>e+t.bitrate),0);return o||n?o>s?(t.push({type:exports.IssueType.Network,reason:exports.IssueReason.OutboundNetworkThroughput,debug:`availableOutgoingBitrate: ${s}, audioStreamsTotalTargetBitrate: ${o}`}),t):n>0&&s<this.#t?(t.push({type:exports.IssueType.Network,reason:exports.IssueReason.OutboundNetworkThroughput,debug:`availableOutgoingBitrate: ${s}, videoStreamsTotalBitrate: ${n}`}),t):t:t}}class y{#e={};#s=.5;detect(e){const t=this.processData(e);return this.#e[e.connection.id]=e,t}processData(e){const t=e.video.inbound.filter((e=>e.framesDropped>0)),s=[],o=this.#e[e.connection.id]?.video.inbound;return o?(t.forEach((e=>{const t=o.find((t=>t.ssrc===e.ssrc));if(!t)return;if(e.framesDropped===t.framesDropped)return;const n=e.framesReceived-t.framesReceived,r=e.framesDecoded-t.framesDecoded,i=e.framesDropped-t.framesDropped;if(0===n&&0===r)return;const a=i/n;a>=this.#s&&s.push({type:exports.IssueType.CPU,reason:exports.IssueReason.DecoderCPUThrottling,ssrc:e.ssrc,debug:`framesDropped: ${Math.round(100*a)} , deltaFramesDropped: ${i}, deltaFramesReceived: ${n}`})})),s):s}}class b{#e={};#o=.15;detect(e){const t=this.processData(e);return this.#e[e.connection.id]=e,t}processData(e){const t=e.video.outbound.filter((e=>e.framesEncoded>0)),s=[],o=this.#e[e.connection.id]?.video.outbound;return o?(t.forEach((e=>{const t=o.find((t=>t.ssrc===e.ssrc));if(!t)return;if(e.framesEncoded===t.framesEncoded)return;const n=e.framesEncoded-t.framesEncoded,r=e.framesSent-t.framesSent;if(0===n)return;if(n===r)return;const i=r/n;i>=this.#o&&s.push({type:exports.IssueType.Network,reason:exports.IssueReason.OutboundNetworkThroughput,ssrc:e.ssrc,debug:`missedFrames: ${Math.round(100*i)}%`})})),s):s}}class w{#e={};detect(e){const t=this.processData(e);return this.#e[e.connection.id]=e,t}processData(e){const t=[],s=[...e.audio?.inbound,...e.video?.inbound];if(!s.length)return t;const o=this.#e[e.connection.id];if(!o)return t;const n=[...o.video?.inbound,...o.audio?.inbound],{packetsReceived:r}=e.connection,i=o.connection.packetsReceived,a=s.reduce(((e,t)=>{const s=n.find((e=>e.ssrc===t.ssrc)),o=s?.jitterBufferDelay||0,r=s?.jitterBufferEmittedCount||0,i=t.jitterBufferDelay-o,a=t.jitterBufferEmittedCount-r,c=i&&a?1e3*i/a:0;return{sumJitter:e.sumJitter+t.jitter,sumJitterBufferDelayMs:e.sumJitterBufferDelayMs+c,packetsLost:e.packetsLost+t.packetsLost,lastPacketsLost:e.lastPacketsLost+(s?.packetsLost||0)}}),{sumJitter:0,sumJitterBufferDelayMs:0,packetsLost:0,lastPacketsLost:0}),c=1e3*e.connection.currentRoundTripTime||0,{sumJitter:u,sumJitterBufferDelayMs:d}=a,p=u/s.length,l=d/s.length,h=r-i,m=a.packetsLost-a.lastPacketsLost,f=h&&m?Math.round(100*m/(h+m)):0,v=f>5,g=p>=200,S=c>=250&&!g&&!v,y=v&&g,b=g&&l>500,w=`packetLoss: ${f}%, jitter: ${p}, rtt: ${c}, jitterBuffer: ${l}ms`;return(!v&&g||g||v)&&t.push({type:exports.IssueType.Network,reason:exports.IssueReason.InboundNetworkQuality,iceCandidate:e.connection.local.id,debug:w}),S&&t.push({type:exports.IssueType.Server,reason:exports.IssueReason.ServerIssue,iceCandidate:e.connection.remote.id,debug:w}),y&&t.push({type:exports.IssueType.Network,reason:exports.IssueReason.InboundNetworkMediaLatency,iceCandidate:e.connection.local.id,debug:w}),b&&t.push({type:exports.IssueType.Network,reason:exports.IssueReason.NetworkMediaSyncFailure,iceCandidate:e.connection.local.id,debug:w}),t}}class k{#e={};detect(e){const t=this.processData(e);return this.#e[e.connection.id]=e,t}processData(e){const t=e.audio.inbound,s=[],o=this.#e[e.connection.id]?.audio.inbound;return o?(t.forEach((e=>{const t=o.find((t=>t.ssrc===e.ssrc));if(!t)return;const n=e.track.insertedSamplesForDeceleration+e.track.removedSamplesForAcceleration,r=t.track.insertedSamplesForDeceleration+t.track.removedSamplesForAcceleration;if(n===r)return;const i=e.track.totalSamplesReceived-t.track.totalSamplesReceived,a=n-r,c=Math.round(100*a/i);c>5&&s.push({type:exports.IssueType.Network,reason:exports.IssueReason.NetworkMediaSyncFailure,ssrc:e.ssrc,debug:`correctedSamplesPercentage: ${c}%`})})),s):s}}class T{#e={};detect(e){const t=this.processData(e);return this.#e[e.connection.id]=e,t}processData(e){const t=[],s=[...e.remote?.audio.inbound||[],...e.remote?.video.inbound||[]];if(!s.length)return t;const o=this.#e[e.connection.id];if(!o)return t;const n=[...o.remote?.audio.inbound||[],...o.remote?.video.inbound||[]],{packetsSent:r}=e.connection,i=o.connection.packetsSent,a=s.reduce(((e,t)=>{const s=n.find((e=>e.ssrc===t.ssrc));return{sumJitter:e.sumJitter+t.jitter,packetsLost:e.packetsLost+t.packetsLost,lastPacketsLost:e.lastPacketsLost+(s?.packetsLost||0)}}),{sumJitter:0,packetsLost:0,lastPacketsLost:0}),c=1e3*e.connection.currentRoundTripTime||0,{sumJitter:u}=a,d=u/s.length,p=r-i,l=a.packetsLost-a.lastPacketsLost,h=p&&l?Math.round(100*l/(p+l)):0,m=h>5,f=d>=200,v=!m&&f||f||m,g=`packetLoss: ${h}%, jitter: ${d}, rtt: ${c}`;return m&&f&&t.push({type:exports.IssueType.Network,reason:exports.IssueReason.OutboundNetworkMediaLatency,iceCandidate:e.connection.local.id,debug:g}),v&&t.push({type:exports.IssueType.Network,reason:exports.IssueReason.OutboundNetworkQuality,iceCandidate:e.connection.local.id,debug:g}),t}}class I{#e={};detect(e){const t=this.processData(e);return this.#e[e.connection.id]=e,t}processData(e){const t=e.video.outbound.filter((e=>"none"!==e.qualityLimitationReason)),s=[],o=this.#e[e.connection.id]?.video.outbound;return o?(t.forEach((e=>{const t=o.find((t=>t.ssrc===e.ssrc));t&&(e.framesSent>t.framesSent||("cpu"===e.qualityLimitationReason&&s.push({type:exports.IssueType.CPU,reason:exports.IssueReason.EncoderCPUThrottling,ssrc:e.ssrc,debug:"qualityLimitationReason: cpu"}),"bandwidth"===e.qualityLimitationReason&&s.push({type:exports.IssueType.Network,reason:exports.IssueReason.OutboundNetworkThroughput,ssrc:e.ssrc,debug:"qualityLimitationReason: bandwidth"})))})),s):s}}class C{UNKNOWN_DECODER="unknown";#e={};#n={};detect(e){const t=this.processData(e);return this.#e[e.connection.id]=e,t}processData(e){const t=[],{id:s}=e.connection,o=this.#e[s]?.video.inbound;return e.video.inbound.forEach((e=>{const{decoderImplementation:n,ssrc:r}=e,i=o?.find((e=>e.ssrc===r));i&&(n===this.UNKNOWN_DECODER?this.hadLastDecoderWithIssue(s,r)||(this.setLastDecoderWithIssue(s,r,this.UNKNOWN_DECODER),t.push({ssrc:r,type:exports.IssueType.Stream,reason:exports.IssueReason.VideoCodecMismatchIssue,trackIdentifier:e.track.trackIdentifier,debug:`mimeType: ${e.mimeType}, decoderImplementation: ${n}`})):this.setLastDecoderWithIssue(s,r,void 0))})),t}setLastDecoderWithIssue(e,t,s){const o=this.#n[e]??{};void 0===s?delete o[t]:o[t]=s,this.#n[e]=o}hadLastDecoderWithIssue(e,t){const s=this.#n[e];return(s&&s[t])===this.UNKNOWN_DECODER}}const L=e=>"closed"===e.iceConnectionState||"closed"===e.connectionState,R=(e,t,s)=>8*((e,t,s)=>{if(!t)return 0;const o=e[s],n=t[s];if(null==o||null==n)return 0;const r=Math.floor(e.timestamp)-Math.floor(t.timestamp);return 0===r?0:(Number(o)-Number(n))/r*1e3})(e,t,s);class P{connections=[];statsParser;constructor(e){this.statsParser=e.statsParser}listConnections(){return[...this.connections]}addPeerConnection(e){this.connections.push({id:e.id??String(Date.now()+Math.random().toString(32)),pc:e.pc})}removePeerConnection(e){const t=this.connections.findIndex((({pc:t})=>t===e.pc));t>=0&&this.removeConnectionsByIndexes([t])}async parse(){const e=[],t=this.connections.map((async(t,s)=>{if(!L(t.pc))return this.statsParser.parse(t);e.unshift(s)}));e.length&&this.removeConnectionsByIndexes(e);return(await Promise.all(t)).filter((e=>void 0!==e))}removeConnectionsByIndexes(e){e.forEach((e=>{this.connections.splice(e,1)}))}}class x{prevStats=new Map;prevStatsCleanupTimers=new Map;allowedReportTypes=new Set(["candidate-pair","inbound-rtp","outbound-rtp","remote-outbound-rtp","remote-inbound-rtp","track","transport"]);logger;prevConnectionStatsTtlMs;constructor(e){this.logger=e.logger,this.prevConnectionStatsTtlMs=e.prevConnectionStatsTtlMs??55e3}get previouslyParsedStatsConnectionsIds(){return[...this.prevStats.keys()]}async parse(e){if(!L(e.pc))return this.getConnectionStats(e);this.logger.debug("Skip stats parsing. Connection is closed.",{connection:e})}async getConnectionStats(e){const{pc:t,id:s}=e;try{const o=Date.now(),n=await Promise.all(t.getReceivers().map((e=>e.getStats()))),r=await Promise.all(t.getSenders().map((e=>e.getStats())));return{id:s,stats:this.mapReportsStats([...n,...r],e),timeTaken:Date.now()-o}}catch(e){return void this.logger.error("Failed to get stats for PC",{id:s,pc:t,error:e})}}mapReportsStats(e,t){const s={audio:{inbound:[],outbound:[]},video:{inbound:[],outbound:[]},connection:{},remote:{video:{inbound:[],outbound:[]},audio:{inbound:[],outbound:[]}}};e.forEach((e=>{e.forEach((t=>{this.allowedReportTypes.has(t.type)&&this.updateMappedStatsWithReportItemData(t,s,e)}))}));const{id:o}=t,n=this.prevStats.get(o);return n&&this.propagateStatsWithRateValues(s,n.stats),this.prevStats.set(o,{stats:s,ts:Date.now()}),this.resetStatsCleanupTimer(o),s}updateMappedStatsWithReportItemData(e,t,s){const o=e.type;if("candidate-pair"===o&&"succeeded"===e.state&&e.nominated)return void(t.connection=this.prepareConnectionStats(e,s));const n=this.getMediaType(e);if(n)if("outbound-rtp"!==o)if("inbound-rtp"!==o)"remote-outbound-rtp"!==o?"remote-inbound-rtp"===o&&(this.mapConnectionStatsIfNecessary(t,e,s),t.remote[n].inbound.push({...e})):t.remote[n].outbound.push({...e});else{const o=s.get(e.trackId)||s.get(e.mediaSourceId)||{};this.mapConnectionStatsIfNecessary(t,e,s);const r={...e,track:{...o}};t[n].inbound.push(r)}else{const o=s.get(e.trackId)||s.get(e.mediaSourceId)||{},r={...e,track:{...o}};t[n].outbound.push(r)}}getMediaType(e){const t=e.mediaType||e.kind;if(!["audio","video"].includes(t)){const{id:t}=e;if(!t)return;return String(t).includes("Video")?"video":String(t).includes("Audio")?"audio":void 0}return t}propagateStatsWithRateValues(e,t){e.audio.inbound.forEach((e=>{const s=t.audio.inbound.find((({id:t})=>t===e.id));e.bitrate=R(e,s,"bytesReceived"),e.packetRate=R(e,s,"packetsReceived")})),e.audio.outbound.forEach((e=>{const s=t.audio.outbound.find((({id:t})=>t===e.id));e.bitrate=R(e,s,"bytesSent"),e.packetRate=R(e,s,"packetsSent")})),e.video.inbound.forEach((e=>{const s=t.video.inbound.find((({id:t})=>t===e.id));e.bitrate=R(e,s,"bytesReceived"),e.packetRate=R(e,s,"packetsReceived")})),e.video.outbound.forEach((e=>{const s=t.video.outbound.find((({id:t})=>t===e.id));e.bitrate=R(e,s,"bytesSent"),e.packetRate=R(e,s,"packetsSent")}))}mapConnectionStatsIfNecessary(e,t,s){if(e.connection.id||!t.transportId)return;const o=s.get(t.transportId);if(o&&o.selectedCandidatePairId){const t=s.get(o.selectedCandidatePairId);e.connection=this.prepareConnectionStats(t,s)}}prepareConnectionStats(e,t){if(!e||!t)return{};const s={...e};if(s.remoteCandidateId){const e=t.get(s.remoteCandidateId);s.remote={...e}}if(s.localCandidateId){const e=t.get(s.localCandidateId);s.local={...e}}return s}resetStatsCleanupTimer(e){const t=this.prevStatsCleanupTimers.get(e);t&&clearTimeout(t);const s=setTimeout((()=>{this.prevStats.delete(e),this.prevStatsCleanupTimers.delete(e)}),this.prevConnectionStatsTtlMs);this.prevStatsCleanupTimers.set(e,s)}}exports.AvailableOutgoingBitrateIssueDetector=S,exports.CompositeRTCStatsParser=P,exports.FramesDroppedIssueDetector=y,exports.FramesEncodedSentIssueDetector=b,exports.InboundNetworkIssueDetector=w,exports.NetworkMediaSyncIssueDetector=k,exports.NetworkScoresCalculator=g,exports.OutboundNetworkIssueDetector=T,exports.PeriodicWebRTCStatsReporter=v,exports.QualityLimitationsIssueDetector=I,exports.RTCStatsParser=x,exports.VideoCodecMismatchDetector=C,exports.WebRTCIssueEmitter=f,exports.default=class{eventEmitter;#r=!1;detectors=[];networkScoresCalculator;statsReporter;compositeStatsParser;logger;constructor(e){this.logger=e.logger??{debug:()=>{},info:()=>{},warn:()=>{},error:()=>{}},this.eventEmitter=e.issueEmitter??new f,e.onIssues&&this.eventEmitter.on(exports.EventType.Issue,e.onIssues),e.onNetworkScoresUpdated&&this.eventEmitter.on(exports.EventType.NetworkScoresUpdated,e.onNetworkScoresUpdated),this.detectors=e.detectors??[new I,new y,new b,new w,new T,new k,new S,new C],this.networkScoresCalculator=e.networkScoresCalculator??new g,this.compositeStatsParser=e.compositeStatsParser??new P({statsParser:new x({logger:this.logger})}),this.statsReporter=e.statsReporter??new v({compositeStatsParser:this.compositeStatsParser,getStatsInterval:e.getStatsInterval??5e3}),window.wid=this,this.wrapRTCPeerConnection(),this.statsReporter.on(v.STATS_REPORT_READY_EVENT,(t=>{this.detectIssues({data:t.stats,ignoreSSRCList:e.ignoreSSRCList}),this.calculateNetworkScores(t.stats)}))}watchNewPeerConnections(){if(this.#r)throw new Error("WebRTCIssueDetector is already started");this.#r=!0,this.statsReporter.startReporting()}stopWatchingNewPeerConnections(){if(!this.#r)throw new Error("WebRTCIssueDetector is already stopped");this.#r=!1,this.statsReporter.stopReporting()}handleNewPeerConnection(e){this.#r?(this.logger.debug("Handling new peer connection",e),this.compositeStatsParser.addPeerConnection({pc:e})):this.logger.debug("Skip handling new peer connection. Detector is not running",e)}emitIssues(e){this.eventEmitter.emit(exports.EventType.Issue,e)}detectIssues({data:e,ignoreSSRCList:t}){let s=this.detectors.reduce(((t,s)=>[...t,...s.detect(e)]),[]);t?.length&&(s=s.filter((e=>!e.ssrc||!t.includes(e.ssrc)))),s.length>0&&this.emitIssues(s)}calculateNetworkScores(e){const t=this.networkScoresCalculator.calculate(e);this.eventEmitter.emit(exports.EventType.NetworkScoresUpdated,t)}wrapRTCPeerConnection(){if(!window.RTCPeerConnection)return;const e=window.RTCPeerConnection,t=e=>this.handleNewPeerConnection(e);function s(s){const o=new e(s);return t(o),o}s.prototype=e.prototype,window.RTCPeerConnection=s}}; |
@@ -1,1 +0,1 @@ | ||
var t,e,s;function n(){}function o(){o.init.call(this)}function r(t){return void 0===t._maxListeners?o.defaultMaxListeners:t._maxListeners}function i(t,e,s){if(e)t.call(s);else for(var n=t.length,o=f(t,n),r=0;r<n;++r)o[r].call(s)}function a(t,e,s,n){if(e)t.call(s,n);else for(var o=t.length,r=f(t,o),i=0;i<o;++i)r[i].call(s,n)}function c(t,e,s,n,o){if(e)t.call(s,n,o);else for(var r=t.length,i=f(t,r),a=0;a<r;++a)i[a].call(s,n,o)}function u(t,e,s,n,o,r){if(e)t.call(s,n,o,r);else for(var i=t.length,a=f(t,i),c=0;c<i;++c)a[c].call(s,n,o,r)}function d(t,e,s,n){if(e)t.apply(s,n);else for(var o=t.length,r=f(t,o),i=0;i<o;++i)r[i].apply(s,n)}function p(t,e,s,o){var i,a,c,u;if("function"!=typeof s)throw new TypeError('"listener" argument must be a function');if((a=t._events)?(a.newListener&&(t.emit("newListener",e,s.listener?s.listener:s),a=t._events),c=a[e]):(a=t._events=new n,t._eventsCount=0),c){if("function"==typeof c?c=a[e]=o?[s,c]:[c,s]:o?c.unshift(s):c.push(s),!c.warned&&(i=r(t))&&i>0&&c.length>i){c.warned=!0;var d=new Error("Possible EventEmitter memory leak detected. "+c.length+" "+e+" listeners added. Use emitter.setMaxListeners() to increase limit");d.name="MaxListenersExceededWarning",d.emitter=t,d.type=e,d.count=c.length,u=d,"function"==typeof console.warn?console.warn(u):console.log(u)}}else c=a[e]=s,++t._eventsCount;return t}function l(t,e,s){var n=!1;function o(){t.removeListener(e,o),n||(n=!0,s.apply(t,arguments))}return o.listener=s,o}function h(t){var e=this._events;if(e){var s=e[t];if("function"==typeof s)return 1;if(s)return s.length}return 0}function f(t,e){for(var s=new Array(e);e--;)s[e]=t[e];return s}n.prototype=Object.create(null),o.EventEmitter=o,o.usingDomains=!1,o.prototype.domain=void 0,o.prototype._events=void 0,o.prototype._maxListeners=void 0,o.defaultMaxListeners=10,o.init=function(){this.domain=null,o.usingDomains&&undefined.active,this._events&&this._events!==Object.getPrototypeOf(this)._events||(this._events=new n,this._eventsCount=0),this._maxListeners=this._maxListeners||void 0},o.prototype.setMaxListeners=function(t){if("number"!=typeof t||t<0||isNaN(t))throw new TypeError('"n" argument must be a positive number');return this._maxListeners=t,this},o.prototype.getMaxListeners=function(){return r(this)},o.prototype.emit=function(t){var e,s,n,o,r,p,l,h="error"===t;if(p=this._events)h=h&&null==p.error;else if(!h)return!1;if(l=this.domain,h){if(e=arguments[1],!l){if(e instanceof Error)throw e;var f=new Error('Uncaught, unspecified "error" event. ('+e+")");throw f.context=e,f}return e||(e=new Error('Uncaught, unspecified "error" event')),e.domainEmitter=this,e.domain=l,e.domainThrown=!1,l.emit("error",e),!1}if(!(s=p[t]))return!1;var m="function"==typeof s;switch(n=arguments.length){case 1:i(s,m,this);break;case 2:a(s,m,this,arguments[1]);break;case 3:c(s,m,this,arguments[1],arguments[2]);break;case 4:u(s,m,this,arguments[1],arguments[2],arguments[3]);break;default:for(o=new Array(n-1),r=1;r<n;r++)o[r-1]=arguments[r];d(s,m,this,o)}return!0},o.prototype.addListener=function(t,e){return p(this,t,e,!1)},o.prototype.on=o.prototype.addListener,o.prototype.prependListener=function(t,e){return p(this,t,e,!0)},o.prototype.once=function(t,e){if("function"!=typeof e)throw new TypeError('"listener" argument must be a function');return this.on(t,l(this,t,e)),this},o.prototype.prependOnceListener=function(t,e){if("function"!=typeof e)throw new TypeError('"listener" argument must be a function');return this.prependListener(t,l(this,t,e)),this},o.prototype.removeListener=function(t,e){var s,o,r,i,a;if("function"!=typeof e)throw new TypeError('"listener" argument must be a function');if(!(o=this._events))return this;if(!(s=o[t]))return this;if(s===e||s.listener&&s.listener===e)0==--this._eventsCount?this._events=new n:(delete o[t],o.removeListener&&this.emit("removeListener",t,s.listener||e));else if("function"!=typeof s){for(r=-1,i=s.length;i-- >0;)if(s[i]===e||s[i].listener&&s[i].listener===e){a=s[i].listener,r=i;break}if(r<0)return this;if(1===s.length){if(s[0]=void 0,0==--this._eventsCount)return this._events=new n,this;delete o[t]}else!function(t,e){for(var s=e,n=s+1,o=t.length;n<o;s+=1,n+=1)t[s]=t[n];t.pop()}(s,r);o.removeListener&&this.emit("removeListener",t,a||e)}return this},o.prototype.off=function(t,e){return this.removeListener(t,e)},o.prototype.removeAllListeners=function(t){var e,s;if(!(s=this._events))return this;if(!s.removeListener)return 0===arguments.length?(this._events=new n,this._eventsCount=0):s[t]&&(0==--this._eventsCount?this._events=new n:delete s[t]),this;if(0===arguments.length){for(var o,r=Object.keys(s),i=0;i<r.length;++i)"removeListener"!==(o=r[i])&&this.removeAllListeners(o);return this.removeAllListeners("removeListener"),this._events=new n,this._eventsCount=0,this}if("function"==typeof(e=s[t]))this.removeListener(t,e);else if(e)do{this.removeListener(t,e[e.length-1])}while(e[0]);return this},o.prototype.listeners=function(t){var e,s=this._events;return s&&(e=s[t])?"function"==typeof e?[e.listener||e]:function(t){for(var e=new Array(t.length),s=0;s<e.length;++s)e[s]=t[s].listener||t[s];return e}(e):[]},o.listenerCount=function(t,e){return"function"==typeof t.listenerCount?t.listenerCount(e):h.call(t,e)},o.prototype.listenerCount=h,o.prototype.eventNames=function(){return this._eventsCount>0?Reflect.ownKeys(this._events):[]};class m extends o{}!function(t){t.Issue="issue",t.NetworkScoresUpdated="network-scores-updated"}(t||(t={})),function(t){t.Network="network",t.CPU="cpu",t.Server="server",t.Stream="stream"}(e||(e={})),function(t){t.OutboundNetworkQuality="outbound-network-quality",t.InboundNetworkQuality="inbound-network-quality",t.OutboundNetworkMediaLatency="outbound-network-media-latency",t.InboundNetworkMediaLatency="inbound-network-media-latency",t.NetworkMediaSyncFailure="network-media-sync-failure",t.OutboundNetworkThroughput="outbound-network-throughput",t.InboundNetworkThroughput="inbound-network-throughput",t.EncoderCPUThrottling="encoder-cpu-throttling",t.DecoderCPUThrottling="decoder-cpu-throttling",t.ServerIssue="server-issue",t.VideoCodecMismatchIssue="codec-mismatch",t.LowInboundMOS="low-inbound-mean-opinion-score",t.LowOutboundMOS="low-outbound-mean-opinion-score"}(s||(s={}));class g extends o{static STATS_REPORT_READY_EVENT="stats-report-ready";isStopped=!1;reportTimer;getStatsInterval;compositeStatsParser;constructor(t){super(),this.compositeStatsParser=t.compositeStatsParser,this.getStatsInterval=t.getStatsInterval??1e4}get isRunning(){return!!this.reportTimer&&!this.isStopped}startReporting(){if(this.reportTimer)return;const t=()=>setTimeout((()=>{this.isStopped||this.parseReports().finally((()=>t()))}),this.getStatsInterval);this.isStopped=!1,this.reportTimer=t()}stopReporting(){this.isStopped=!0,this.reportTimer&&(clearTimeout(this.reportTimer),this.reportTimer=void 0)}async parseReports(){(await this.compositeStatsParser.parse()).forEach((t=>{this.emit(g.STATS_REPORT_READY_EVENT,t)}))}}class v{#t={};calculate(t){const e=this.calcucateOutboundScore(t),s=this.calculateInboundScore(t);return this.#t[t.connection.id]=t,{outbound:e,inbound:s}}calcucateOutboundScore(t){const e=[...t.remote?.audio.inbound||[],...t.remote?.video.inbound||[]];if(!e.length)return;const s=this.#t[t.connection.id];if(!s)return;const n=[...s.remote?.audio.inbound||[],...s.remote?.video.inbound||[]],{packetsSent:o}=t.connection,r=s.connection.packetsSent,i=e.reduce(((t,e)=>{const s=n.find((t=>t.ssrc===e.ssrc));return{sumJitter:t.sumJitter+e.jitter,packetsLost:t.packetsLost+e.packetsLost,lastPacketsLost:t.lastPacketsLost+(s?.packetsLost||0)}}),{sumJitter:0,packetsLost:0,lastPacketsLost:0}),a=1e3*t.connection.currentRoundTripTime||0,{sumJitter:c}=i,u=c/e.length,d=o-r,p=i.packetsLost-i.lastPacketsLost,l=d&&p?Math.round(100*p/(d+p)):0;return this.calculateMOS({avgJitter:u,rtt:a,packetsLoss:l})}calculateInboundScore(t){const e=[...t.audio?.inbound,...t.video?.inbound];if(!e.length)return;const s=this.#t[t.connection.id];if(!s)return;const n=[...s.video?.inbound,...s.audio?.inbound],{packetsReceived:o}=t.connection,r=s.connection.packetsReceived,i=e.reduce(((t,e)=>{const s=n.find((t=>t.ssrc===e.ssrc));return{sumJitter:t.sumJitter+e.jitter,packetsLost:t.packetsLost+e.packetsLost,lastPacketsLost:t.lastPacketsLost+(s?.packetsLost||0)}}),{sumJitter:0,packetsLost:0,lastPacketsLost:0}),a=1e3*t.connection.currentRoundTripTime||0,{sumJitter:c}=i,u=c/e.length,d=o-r,p=i.packetsLost-i.lastPacketsLost,l=d&&p?Math.round(100*p/(d+p)):0;return this.calculateMOS({avgJitter:u,rtt:a,packetsLoss:l})}calculateMOS({avgJitter:t,rtt:e,packetsLoss:s}){const n=e+2*t+10;let o=n<160?93.2-n/40:93.2-n/120-10;return o-=2.5*s,1+.035*o+7e-6*o*(o-60)*(100-o)}}class b{#e=1e5;detect(t){const n=[],{availableOutgoingBitrate:o}=t.connection;if(void 0===o)return n;const r=t.audio.outbound.reduce(((t,e)=>t+e.targetBitrate),0),i=t.video.outbound.reduce(((t,e)=>t+e.bitrate),0);return r||i?r>o?(n.push({type:e.Network,reason:s.OutboundNetworkThroughput,debug:`availableOutgoingBitrate: ${o}, audioStreamsTotalTargetBitrate: ${r}`}),n):i>0&&o<this.#e?(n.push({type:e.Network,reason:s.OutboundNetworkThroughput,debug:`availableOutgoingBitrate: ${o}, videoStreamsTotalBitrate: ${i}`}),n):n:n}}class w{#t={};#s=.5;detect(t){const e=this.processData(t);return this.#t[t.connection.id]=t,e}processData(t){const n=t.video.inbound.filter((t=>t.framesDropped>0)),o=[],r=this.#t[t.connection.id]?.video.inbound;return r?(n.forEach((t=>{const n=r.find((e=>e.ssrc===t.ssrc));if(!n)return;if(t.framesDropped===n.framesDropped)return;const i=t.framesReceived-n.framesReceived,a=t.framesDecoded-n.framesDecoded,c=t.framesDropped-n.framesDropped;if(0===i&&0===a)return;const u=c/i;u>=this.#s&&o.push({type:e.CPU,reason:s.DecoderCPUThrottling,ssrc:t.ssrc,debug:`framesDropped: ${Math.round(100*u)} , deltaFramesDropped: ${c}, deltaFramesReceived: ${i}`})})),o):o}}class S{#t={};#n=.15;detect(t){const e=this.processData(t);return this.#t[t.connection.id]=t,e}processData(t){const n=t.video.outbound.filter((t=>t.framesEncoded>0)),o=[],r=this.#t[t.connection.id]?.video.outbound;return r?(n.forEach((t=>{const n=r.find((e=>e.ssrc===t.ssrc));if(!n)return;if(t.framesEncoded===n.framesEncoded)return;const i=t.framesEncoded-n.framesEncoded,a=t.framesSent-n.framesSent;if(0===i)return;if(i===a)return;const c=a/i;c>=this.#n&&o.push({type:e.Network,reason:s.OutboundNetworkThroughput,ssrc:t.ssrc,debug:`missedFrames: ${Math.round(100*c)}%`})})),o):o}}class k{#t={};detect(t){const e=this.processData(t);return this.#t[t.connection.id]=t,e}processData(t){const n=[],o=[...t.audio?.inbound,...t.video?.inbound];if(!o.length)return n;const r=this.#t[t.connection.id];if(!r)return n;const i=[...r.video?.inbound,...r.audio?.inbound],{packetsReceived:a}=t.connection,c=r.connection.packetsReceived,u=o.reduce(((t,e)=>{const s=i.find((t=>t.ssrc===e.ssrc)),n=s?.jitterBufferDelay||0,o=s?.jitterBufferEmittedCount||0,r=e.jitterBufferDelay-n,a=e.jitterBufferEmittedCount-o,c=r&&a?1e3*r/a:0;return{sumJitter:t.sumJitter+e.jitter,sumJitterBufferDelayMs:t.sumJitterBufferDelayMs+c,packetsLost:t.packetsLost+e.packetsLost,lastPacketsLost:t.lastPacketsLost+(s?.packetsLost||0)}}),{sumJitter:0,sumJitterBufferDelayMs:0,packetsLost:0,lastPacketsLost:0}),d=1e3*t.connection.currentRoundTripTime||0,{sumJitter:p,sumJitterBufferDelayMs:l}=u,h=p/o.length,f=l/o.length,m=a-c,g=u.packetsLost-u.lastPacketsLost,v=m&&g?Math.round(100*g/(m+g)):0,b=v>5,w=h>=200,S=d>=250&&!w&&!b,k=b&&w,y=w&&f>500,L=`packetLoss: ${v}%, jitter: ${h}, rtt: ${d}, jitterBuffer: ${f}ms`;return(!b&&w||w||b)&&n.push({type:e.Network,reason:s.InboundNetworkQuality,iceCandidate:t.connection.local.id,debug:L}),S&&n.push({type:e.Server,reason:s.ServerIssue,iceCandidate:t.connection.remote.id,debug:L}),k&&n.push({type:e.Network,reason:s.InboundNetworkMediaLatency,iceCandidate:t.connection.local.id,debug:L}),y&&n.push({type:e.Network,reason:s.NetworkMediaSyncFailure,iceCandidate:t.connection.local.id,debug:L}),n}}class y{#t={};detect(t){const e=this.processData(t);return this.#t[t.connection.id]=t,e}processData(t){const n=t.audio.inbound,o=[],r=this.#t[t.connection.id]?.audio.inbound;return r?(n.forEach((t=>{const n=r.find((e=>e.ssrc===t.ssrc));if(!n)return;const i=t.track.insertedSamplesForDeceleration+t.track.removedSamplesForAcceleration,a=n.track.insertedSamplesForDeceleration+n.track.removedSamplesForAcceleration;if(i===a)return;const c=t.track.totalSamplesReceived-n.track.totalSamplesReceived,u=i-a,d=Math.round(100*u/c);d>5&&o.push({type:e.Network,reason:s.NetworkMediaSyncFailure,ssrc:t.ssrc,debug:`correctedSamplesPercentage: ${d}%`})})),o):o}}class L{#t={};detect(t){const e=this.processData(t);return this.#t[t.connection.id]=t,e}processData(t){const n=[],o=[...t.remote?.audio.inbound||[],...t.remote?.video.inbound||[]];if(!o.length)return n;const r=this.#t[t.connection.id];if(!r)return n;const i=[...r.remote?.audio.inbound||[],...r.remote?.video.inbound||[]],{packetsSent:a}=t.connection,c=r.connection.packetsSent,u=o.reduce(((t,e)=>{const s=i.find((t=>t.ssrc===e.ssrc));return{sumJitter:t.sumJitter+e.jitter,packetsLost:t.packetsLost+e.packetsLost,lastPacketsLost:t.lastPacketsLost+(s?.packetsLost||0)}}),{sumJitter:0,packetsLost:0,lastPacketsLost:0}),d=1e3*t.connection.currentRoundTripTime||0,{sumJitter:p}=u,l=p/o.length,h=a-c,f=u.packetsLost-u.lastPacketsLost,m=h&&f?Math.round(100*f/(h+f)):0,g=m>5,v=l>=200,b=!g&&v||v||g,w=`packetLoss: ${m}%, jitter: ${l}, rtt: ${d}`;return g&&v&&n.push({type:e.Network,reason:s.OutboundNetworkMediaLatency,iceCandidate:t.connection.local.id,debug:w}),b&&n.push({type:e.Network,reason:s.OutboundNetworkQuality,iceCandidate:t.connection.local.id,debug:w}),n}}class P{#t={};detect(t){const e=this.processData(t);return this.#t[t.connection.id]=t,e}processData(t){const n=t.video.outbound.filter((t=>"none"!==t.qualityLimitationReason)),o=[],r=this.#t[t.connection.id]?.video.outbound;return r?(n.forEach((t=>{const n=r.find((e=>e.ssrc===t.ssrc));n&&(t.framesSent>n.framesSent||("cpu"===t.qualityLimitationReason&&o.push({type:e.CPU,reason:s.EncoderCPUThrottling,ssrc:t.ssrc,debug:"qualityLimitationReason: cpu"}),"bandwidth"===t.qualityLimitationReason&&o.push({type:e.Network,reason:s.OutboundNetworkThroughput,ssrc:t.ssrc,debug:"qualityLimitationReason: bandwidth"})))})),o):o}}class C{UNKNOWN_DECODER="unknown";#t={};#o={};detect(t){const e=this.processData(t);return this.#t[t.connection.id]=t,e}processData(t){const n=[],{id:o}=t.connection,r=this.#t[o]?.video.inbound;return t.video.inbound.forEach((t=>{const{decoderImplementation:i,ssrc:a}=t,c=r?.find((t=>t.ssrc===a));c&&(i===this.UNKNOWN_DECODER?this.hadLastDecoderWithIssue(o,a)||(this.setLastDecoderWithIssue(o,a,this.UNKNOWN_DECODER),n.push({ssrc:a,type:e.Stream,reason:s.VideoCodecMismatchIssue,trackIdentifier:t.track.trackIdentifier,debug:`mimeType: ${t.mimeType}, decoderImplementation: ${i}`})):this.setLastDecoderWithIssue(o,a,void 0))})),n}setLastDecoderWithIssue(t,e,s){const n=this.#o[t]??{};void 0===s?delete n[e]:n[e]=s,this.#o[t]=n}hadLastDecoderWithIssue(t,e){const s=this.#o[t];return(s&&s[e])===this.UNKNOWN_DECODER}}const R=t=>"closed"===t.iceConnectionState||"closed"===t.connectionState,T=(t,e,s)=>8*((t,e,s)=>{if(!e)return 0;const n=t[s],o=e[s];if(null==n||null==o)return 0;const r=Math.floor(t.timestamp)-Math.floor(e.timestamp);return 0===r?0:(Number(n)-Number(o))/r*1e3})(t,e,s);class D{connections=[];statsParser;constructor(t){this.statsParser=t.statsParser}listConnections(){return[...this.connections]}addPeerConnection(t){this.connections.push({id:t.id??String(Date.now()+Math.random().toString(32)),pc:t.pc})}removePeerConnection(t){const e=this.connections.findIndex((({pc:e})=>e===t.pc));e>=0&&this.removeConnectionsByIndexes([e])}async parse(){const t=[],e=this.connections.map((async(e,s)=>{if(!R(e.pc))return this.statsParser.parse(e);t.unshift(s)}));t.length&&this.removeConnectionsByIndexes(t);return(await Promise.all(e)).filter((t=>void 0!==t))}removeConnectionsByIndexes(t){t.forEach((t=>{this.connections.splice(t,1)}))}}class E{prevStats=new Map;allowedReportTypes=["candidate-pair","inbound-rtp","outbound-rtp","remote-outbound-rtp","remote-inbound-rtp","track","transport"];logger;constructor(t){this.logger=t.logger}async parse(t){if(!R(t.pc))return this.getConnectionStats(t);this.logger.debug("Skip stats parsing. Connection is closed.",{connection:t})}async getConnectionStats(t){const{pc:e,id:s}=t;try{const n=Date.now(),o=await Promise.all(e.getReceivers().map((t=>t.getStats()))),r=await Promise.all(e.getSenders().map((t=>t.getStats())));return{id:s,stats:this.mapReportsStats([...o,...r],t),timeTaken:Date.now()-n}}catch(t){return void this.logger.error("Failed to get stats for PC",{id:s,pc:e,error:t})}}mapReportsStats(t,e){const s={audio:{inbound:[],outbound:[]},video:{inbound:[],outbound:[]},connection:{},remote:{video:{inbound:[],outbound:[]},audio:{inbound:[],outbound:[]}}};t.forEach((t=>{t.forEach((e=>{this.allowedReportTypes.includes(e.type)&&this.updateMappedStatsWithReportItemData(e,s,t)}))}));const n=this.prevStats.get(e.id);return n&&this.propagateStatsWithRateValues(s,n.stats),this.prevStats.set(e.id,{stats:s,ts:Date.now()}),s}updateMappedStatsWithReportItemData(t,e,s){const n=t.type;if("candidate-pair"===n&&"succeeded"===t.state&&t.nominated)return void(e.connection=this.prepareConnectionStats(t,s));const o=this.getMediaType(t);if(o)if("outbound-rtp"!==n)if("inbound-rtp"!==n)"remote-outbound-rtp"!==n?"remote-inbound-rtp"===n&&(this.mapConnectionStatsIfNecessary(e,t,s),e.remote[o].inbound.push({...t})):e.remote[o].outbound.push({...t});else{const n=s.get(t.trackId)||s.get(t.mediaSourceId)||{};this.mapConnectionStatsIfNecessary(e,t,s);const r={...t,track:{...n}};e[o].inbound.push(r)}else{const n=s.get(t.trackId)||s.get(t.mediaSourceId)||{},r={...t,track:{...n}};e[o].outbound.push(r)}}getMediaType(t){const e=t.mediaType||t.kind;if(!["audio","video"].includes(e)){const{id:e}=t;if(!e)return;return String(e).includes("Video")?"video":String(e).includes("Audio")?"audio":void 0}return e}propagateStatsWithRateValues(t,e){t.audio.inbound.forEach((t=>{const s=e.audio.inbound.find((({id:e})=>e===t.id));t.bitrate=T(t,s,"bytesReceived"),t.packetRate=T(t,s,"packetsReceived")})),t.audio.outbound.forEach((t=>{const s=e.audio.outbound.find((({id:e})=>e===t.id));t.bitrate=T(t,s,"bytesSent"),t.packetRate=T(t,s,"packetsSent")})),t.video.inbound.forEach((t=>{const s=e.video.inbound.find((({id:e})=>e===t.id));t.bitrate=T(t,s,"bytesReceived"),t.packetRate=T(t,s,"packetsReceived")})),t.video.outbound.forEach((t=>{const s=e.video.outbound.find((({id:e})=>e===t.id));t.bitrate=T(t,s,"bytesSent"),t.packetRate=T(t,s,"packetsSent")}))}mapConnectionStatsIfNecessary(t,e,s){if(t.connection.id||!e.transportId)return;const n=s.get(e.transportId);if(n&&n.selectedCandidatePairId){const e=s.get(n.selectedCandidatePairId);t.connection=this.prepareConnectionStats(e,s)}}prepareConnectionStats(t,e){if(!t||!e)return{};const s={...t};if(s.remoteCandidateId){const t=e.get(s.remoteCandidateId);s.remote={...t}}if(s.localCandidateId){const t=e.get(s.localCandidateId);s.local={...t}}return s}}class I{eventEmitter;#r=!1;detectors=[];networkScoresCalculator;statsReporter;compositeStatsParser;logger;constructor(e){this.logger=e.logger??{debug:()=>{},info:()=>{},warn:()=>{},error:()=>{}},this.eventEmitter=e.issueEmitter??new m,e.onIssues&&this.eventEmitter.on(t.Issue,e.onIssues),e.onNetworkScoresUpdated&&this.eventEmitter.on(t.NetworkScoresUpdated,e.onNetworkScoresUpdated),this.detectors=e.detectors??[new P,new w,new S,new k,new L,new y,new b,new C],this.networkScoresCalculator=e.networkScoresCalculator??new v,this.compositeStatsParser=e.compositeStatsParser??new D({statsParser:new E({logger:this.logger})}),this.statsReporter=e.statsReporter??new g({compositeStatsParser:this.compositeStatsParser,getStatsInterval:e.getStatsInterval??5e3}),window.wid=this,this.wrapRTCPeerConnection(),this.statsReporter.on(g.STATS_REPORT_READY_EVENT,(t=>{this.detectIssues({data:t.stats,ignoreSSRCList:e.ignoreSSRCList}),this.calculateNetworkScores(t.stats)}))}watchNewPeerConnections(){if(this.#r)throw new Error("WebRTCIssueDetector is already started");this.#r=!0,this.statsReporter.startReporting()}stopWatchingNewPeerConnections(){if(!this.#r)throw new Error("WebRTCIssueDetector is already stopped");this.#r=!1,this.statsReporter.stopReporting()}handleNewPeerConnection(t){this.#r?(this.logger.debug("Handling new peer connection",t),this.compositeStatsParser.addPeerConnection({pc:t})):this.logger.debug("Skip handling new peer connection. Detector is not running",t)}emitIssues(e){this.eventEmitter.emit(t.Issue,e)}detectIssues({data:t,ignoreSSRCList:e}){let s=this.detectors.reduce(((e,s)=>[...e,...s.detect(t)]),[]);e?.length&&(s=s.filter((t=>!t.ssrc||!e.includes(t.ssrc)))),s.length>0&&this.emitIssues(s)}calculateNetworkScores(e){const s=this.networkScoresCalculator.calculate(e);this.eventEmitter.emit(t.NetworkScoresUpdated,s)}wrapRTCPeerConnection(){if(!window.RTCPeerConnection)return;const t=window.RTCPeerConnection,e=t=>this.handleNewPeerConnection(t);function s(s){const n=new t(s);return e(n),n}s.prototype=t.prototype,window.RTCPeerConnection=s}}export{b as AvailableOutgoingBitrateIssueDetector,D as CompositeRTCStatsParser,t as EventType,w as FramesDroppedIssueDetector,S as FramesEncodedSentIssueDetector,k as InboundNetworkIssueDetector,s as IssueReason,e as IssueType,y as NetworkMediaSyncIssueDetector,v as NetworkScoresCalculator,L as OutboundNetworkIssueDetector,g as PeriodicWebRTCStatsReporter,P as QualityLimitationsIssueDetector,E as RTCStatsParser,C as VideoCodecMismatchDetector,m as WebRTCIssueEmitter,I as default}; | ||
var t,e,s;function n(){}function o(){o.init.call(this)}function r(t){return void 0===t._maxListeners?o.defaultMaxListeners:t._maxListeners}function i(t,e,s){if(e)t.call(s);else for(var n=t.length,o=f(t,n),r=0;r<n;++r)o[r].call(s)}function a(t,e,s,n){if(e)t.call(s,n);else for(var o=t.length,r=f(t,o),i=0;i<o;++i)r[i].call(s,n)}function c(t,e,s,n,o){if(e)t.call(s,n,o);else for(var r=t.length,i=f(t,r),a=0;a<r;++a)i[a].call(s,n,o)}function u(t,e,s,n,o,r){if(e)t.call(s,n,o,r);else for(var i=t.length,a=f(t,i),c=0;c<i;++c)a[c].call(s,n,o,r)}function d(t,e,s,n){if(e)t.apply(s,n);else for(var o=t.length,r=f(t,o),i=0;i<o;++i)r[i].apply(s,n)}function p(t,e,s,o){var i,a,c,u;if("function"!=typeof s)throw new TypeError('"listener" argument must be a function');if((a=t._events)?(a.newListener&&(t.emit("newListener",e,s.listener?s.listener:s),a=t._events),c=a[e]):(a=t._events=new n,t._eventsCount=0),c){if("function"==typeof c?c=a[e]=o?[s,c]:[c,s]:o?c.unshift(s):c.push(s),!c.warned&&(i=r(t))&&i>0&&c.length>i){c.warned=!0;var d=new Error("Possible EventEmitter memory leak detected. "+c.length+" "+e+" listeners added. Use emitter.setMaxListeners() to increase limit");d.name="MaxListenersExceededWarning",d.emitter=t,d.type=e,d.count=c.length,u=d,"function"==typeof console.warn?console.warn(u):console.log(u)}}else c=a[e]=s,++t._eventsCount;return t}function l(t,e,s){var n=!1;function o(){t.removeListener(e,o),n||(n=!0,s.apply(t,arguments))}return o.listener=s,o}function h(t){var e=this._events;if(e){var s=e[t];if("function"==typeof s)return 1;if(s)return s.length}return 0}function f(t,e){for(var s=new Array(e);e--;)s[e]=t[e];return s}n.prototype=Object.create(null),o.EventEmitter=o,o.usingDomains=!1,o.prototype.domain=void 0,o.prototype._events=void 0,o.prototype._maxListeners=void 0,o.defaultMaxListeners=10,o.init=function(){this.domain=null,o.usingDomains&&undefined.active,this._events&&this._events!==Object.getPrototypeOf(this)._events||(this._events=new n,this._eventsCount=0),this._maxListeners=this._maxListeners||void 0},o.prototype.setMaxListeners=function(t){if("number"!=typeof t||t<0||isNaN(t))throw new TypeError('"n" argument must be a positive number');return this._maxListeners=t,this},o.prototype.getMaxListeners=function(){return r(this)},o.prototype.emit=function(t){var e,s,n,o,r,p,l,h="error"===t;if(p=this._events)h=h&&null==p.error;else if(!h)return!1;if(l=this.domain,h){if(e=arguments[1],!l){if(e instanceof Error)throw e;var f=new Error('Uncaught, unspecified "error" event. ('+e+")");throw f.context=e,f}return e||(e=new Error('Uncaught, unspecified "error" event')),e.domainEmitter=this,e.domain=l,e.domainThrown=!1,l.emit("error",e),!1}if(!(s=p[t]))return!1;var m="function"==typeof s;switch(n=arguments.length){case 1:i(s,m,this);break;case 2:a(s,m,this,arguments[1]);break;case 3:c(s,m,this,arguments[1],arguments[2]);break;case 4:u(s,m,this,arguments[1],arguments[2],arguments[3]);break;default:for(o=new Array(n-1),r=1;r<n;r++)o[r-1]=arguments[r];d(s,m,this,o)}return!0},o.prototype.addListener=function(t,e){return p(this,t,e,!1)},o.prototype.on=o.prototype.addListener,o.prototype.prependListener=function(t,e){return p(this,t,e,!0)},o.prototype.once=function(t,e){if("function"!=typeof e)throw new TypeError('"listener" argument must be a function');return this.on(t,l(this,t,e)),this},o.prototype.prependOnceListener=function(t,e){if("function"!=typeof e)throw new TypeError('"listener" argument must be a function');return this.prependListener(t,l(this,t,e)),this},o.prototype.removeListener=function(t,e){var s,o,r,i,a;if("function"!=typeof e)throw new TypeError('"listener" argument must be a function');if(!(o=this._events))return this;if(!(s=o[t]))return this;if(s===e||s.listener&&s.listener===e)0==--this._eventsCount?this._events=new n:(delete o[t],o.removeListener&&this.emit("removeListener",t,s.listener||e));else if("function"!=typeof s){for(r=-1,i=s.length;i-- >0;)if(s[i]===e||s[i].listener&&s[i].listener===e){a=s[i].listener,r=i;break}if(r<0)return this;if(1===s.length){if(s[0]=void 0,0==--this._eventsCount)return this._events=new n,this;delete o[t]}else!function(t,e){for(var s=e,n=s+1,o=t.length;n<o;s+=1,n+=1)t[s]=t[n];t.pop()}(s,r);o.removeListener&&this.emit("removeListener",t,a||e)}return this},o.prototype.off=function(t,e){return this.removeListener(t,e)},o.prototype.removeAllListeners=function(t){var e,s;if(!(s=this._events))return this;if(!s.removeListener)return 0===arguments.length?(this._events=new n,this._eventsCount=0):s[t]&&(0==--this._eventsCount?this._events=new n:delete s[t]),this;if(0===arguments.length){for(var o,r=Object.keys(s),i=0;i<r.length;++i)"removeListener"!==(o=r[i])&&this.removeAllListeners(o);return this.removeAllListeners("removeListener"),this._events=new n,this._eventsCount=0,this}if("function"==typeof(e=s[t]))this.removeListener(t,e);else if(e)do{this.removeListener(t,e[e.length-1])}while(e[0]);return this},o.prototype.listeners=function(t){var e,s=this._events;return s&&(e=s[t])?"function"==typeof e?[e.listener||e]:function(t){for(var e=new Array(t.length),s=0;s<e.length;++s)e[s]=t[s].listener||t[s];return e}(e):[]},o.listenerCount=function(t,e){return"function"==typeof t.listenerCount?t.listenerCount(e):h.call(t,e)},o.prototype.listenerCount=h,o.prototype.eventNames=function(){return this._eventsCount>0?Reflect.ownKeys(this._events):[]};class m extends o{}!function(t){t.Issue="issue",t.NetworkScoresUpdated="network-scores-updated"}(t||(t={})),function(t){t.Network="network",t.CPU="cpu",t.Server="server",t.Stream="stream"}(e||(e={})),function(t){t.OutboundNetworkQuality="outbound-network-quality",t.InboundNetworkQuality="inbound-network-quality",t.OutboundNetworkMediaLatency="outbound-network-media-latency",t.InboundNetworkMediaLatency="inbound-network-media-latency",t.NetworkMediaSyncFailure="network-media-sync-failure",t.OutboundNetworkThroughput="outbound-network-throughput",t.InboundNetworkThroughput="inbound-network-throughput",t.EncoderCPUThrottling="encoder-cpu-throttling",t.DecoderCPUThrottling="decoder-cpu-throttling",t.ServerIssue="server-issue",t.VideoCodecMismatchIssue="codec-mismatch",t.LowInboundMOS="low-inbound-mean-opinion-score",t.LowOutboundMOS="low-outbound-mean-opinion-score"}(s||(s={}));class v extends o{static STATS_REPORT_READY_EVENT="stats-report-ready";isStopped=!1;reportTimer;getStatsInterval;compositeStatsParser;constructor(t){super(),this.compositeStatsParser=t.compositeStatsParser,this.getStatsInterval=t.getStatsInterval??1e4}get isRunning(){return!!this.reportTimer&&!this.isStopped}startReporting(){if(this.reportTimer)return;const t=()=>setTimeout((()=>{this.isStopped||this.parseReports().finally((()=>t()))}),this.getStatsInterval);this.isStopped=!1,this.reportTimer=t()}stopReporting(){this.isStopped=!0,this.reportTimer&&(clearTimeout(this.reportTimer),this.reportTimer=void 0)}async parseReports(){(await this.compositeStatsParser.parse()).forEach((t=>{this.emit(v.STATS_REPORT_READY_EVENT,t)}))}}class g{#t={};calculate(t){const e=this.calculateOutboundScore(t),s=this.calculateInboundScore(t);return this.#t[t.connection.id]=t,{outbound:e,inbound:s}}calculateOutboundScore(t){const e=[...t.remote?.audio.inbound||[],...t.remote?.video.inbound||[]];if(!e.length)return;const s=this.#t[t.connection.id];if(!s)return;const n=[...s.remote?.audio.inbound||[],...s.remote?.video.inbound||[]],{packetsSent:o}=t.connection,r=s.connection.packetsSent,i=e.reduce(((t,e)=>{const s=n.find((t=>t.ssrc===e.ssrc));return{sumJitter:t.sumJitter+e.jitter,packetsLost:t.packetsLost+e.packetsLost,lastPacketsLost:t.lastPacketsLost+(s?.packetsLost||0)}}),{sumJitter:0,packetsLost:0,lastPacketsLost:0}),a=1e3*t.connection.currentRoundTripTime||0,{sumJitter:c}=i,u=c/e.length,d=o-r,p=i.packetsLost-i.lastPacketsLost,l=d&&p?Math.round(100*p/(d+p)):0;return this.calculateMOS({avgJitter:u,rtt:a,packetsLoss:l})}calculateInboundScore(t){const e=[...t.audio?.inbound,...t.video?.inbound];if(!e.length)return;const s=this.#t[t.connection.id];if(!s)return;const n=[...s.video?.inbound,...s.audio?.inbound],{packetsReceived:o}=t.connection,r=s.connection.packetsReceived,i=e.reduce(((t,e)=>{const s=n.find((t=>t.ssrc===e.ssrc));return{sumJitter:t.sumJitter+e.jitter,packetsLost:t.packetsLost+e.packetsLost,lastPacketsLost:t.lastPacketsLost+(s?.packetsLost||0)}}),{sumJitter:0,packetsLost:0,lastPacketsLost:0}),a=1e3*t.connection.currentRoundTripTime||0,{sumJitter:c}=i,u=c/e.length,d=o-r,p=i.packetsLost-i.lastPacketsLost,l=d&&p?Math.round(100*p/(d+p)):0;return this.calculateMOS({avgJitter:u,rtt:a,packetsLoss:l})}calculateMOS({avgJitter:t,rtt:e,packetsLoss:s}){const n=e+2*t+10;let o=n<160?93.2-n/40:93.2-n/120-10;return o-=2.5*s,1+.035*o+7e-6*o*(o-60)*(100-o)}}class S{#e=1e5;detect(t){const n=[],{availableOutgoingBitrate:o}=t.connection;if(void 0===o)return n;const r=t.audio.outbound.reduce(((t,e)=>t+e.targetBitrate),0),i=t.video.outbound.reduce(((t,e)=>t+e.bitrate),0);return r||i?r>o?(n.push({type:e.Network,reason:s.OutboundNetworkThroughput,debug:`availableOutgoingBitrate: ${o}, audioStreamsTotalTargetBitrate: ${r}`}),n):i>0&&o<this.#e?(n.push({type:e.Network,reason:s.OutboundNetworkThroughput,debug:`availableOutgoingBitrate: ${o}, videoStreamsTotalBitrate: ${i}`}),n):n:n}}class b{#t={};#s=.5;detect(t){const e=this.processData(t);return this.#t[t.connection.id]=t,e}processData(t){const n=t.video.inbound.filter((t=>t.framesDropped>0)),o=[],r=this.#t[t.connection.id]?.video.inbound;return r?(n.forEach((t=>{const n=r.find((e=>e.ssrc===t.ssrc));if(!n)return;if(t.framesDropped===n.framesDropped)return;const i=t.framesReceived-n.framesReceived,a=t.framesDecoded-n.framesDecoded,c=t.framesDropped-n.framesDropped;if(0===i&&0===a)return;const u=c/i;u>=this.#s&&o.push({type:e.CPU,reason:s.DecoderCPUThrottling,ssrc:t.ssrc,debug:`framesDropped: ${Math.round(100*u)} , deltaFramesDropped: ${c}, deltaFramesReceived: ${i}`})})),o):o}}class w{#t={};#n=.15;detect(t){const e=this.processData(t);return this.#t[t.connection.id]=t,e}processData(t){const n=t.video.outbound.filter((t=>t.framesEncoded>0)),o=[],r=this.#t[t.connection.id]?.video.outbound;return r?(n.forEach((t=>{const n=r.find((e=>e.ssrc===t.ssrc));if(!n)return;if(t.framesEncoded===n.framesEncoded)return;const i=t.framesEncoded-n.framesEncoded,a=t.framesSent-n.framesSent;if(0===i)return;if(i===a)return;const c=a/i;c>=this.#n&&o.push({type:e.Network,reason:s.OutboundNetworkThroughput,ssrc:t.ssrc,debug:`missedFrames: ${Math.round(100*c)}%`})})),o):o}}class k{#t={};detect(t){const e=this.processData(t);return this.#t[t.connection.id]=t,e}processData(t){const n=[],o=[...t.audio?.inbound,...t.video?.inbound];if(!o.length)return n;const r=this.#t[t.connection.id];if(!r)return n;const i=[...r.video?.inbound,...r.audio?.inbound],{packetsReceived:a}=t.connection,c=r.connection.packetsReceived,u=o.reduce(((t,e)=>{const s=i.find((t=>t.ssrc===e.ssrc)),n=s?.jitterBufferDelay||0,o=s?.jitterBufferEmittedCount||0,r=e.jitterBufferDelay-n,a=e.jitterBufferEmittedCount-o,c=r&&a?1e3*r/a:0;return{sumJitter:t.sumJitter+e.jitter,sumJitterBufferDelayMs:t.sumJitterBufferDelayMs+c,packetsLost:t.packetsLost+e.packetsLost,lastPacketsLost:t.lastPacketsLost+(s?.packetsLost||0)}}),{sumJitter:0,sumJitterBufferDelayMs:0,packetsLost:0,lastPacketsLost:0}),d=1e3*t.connection.currentRoundTripTime||0,{sumJitter:p,sumJitterBufferDelayMs:l}=u,h=p/o.length,f=l/o.length,m=a-c,v=u.packetsLost-u.lastPacketsLost,g=m&&v?Math.round(100*v/(m+v)):0,S=g>5,b=h>=200,w=d>=250&&!b&&!S,k=S&&b,y=b&&f>500,L=`packetLoss: ${g}%, jitter: ${h}, rtt: ${d}, jitterBuffer: ${f}ms`;return(!S&&b||b||S)&&n.push({type:e.Network,reason:s.InboundNetworkQuality,iceCandidate:t.connection.local.id,debug:L}),w&&n.push({type:e.Server,reason:s.ServerIssue,iceCandidate:t.connection.remote.id,debug:L}),k&&n.push({type:e.Network,reason:s.InboundNetworkMediaLatency,iceCandidate:t.connection.local.id,debug:L}),y&&n.push({type:e.Network,reason:s.NetworkMediaSyncFailure,iceCandidate:t.connection.local.id,debug:L}),n}}class y{#t={};detect(t){const e=this.processData(t);return this.#t[t.connection.id]=t,e}processData(t){const n=t.audio.inbound,o=[],r=this.#t[t.connection.id]?.audio.inbound;return r?(n.forEach((t=>{const n=r.find((e=>e.ssrc===t.ssrc));if(!n)return;const i=t.track.insertedSamplesForDeceleration+t.track.removedSamplesForAcceleration,a=n.track.insertedSamplesForDeceleration+n.track.removedSamplesForAcceleration;if(i===a)return;const c=t.track.totalSamplesReceived-n.track.totalSamplesReceived,u=i-a,d=Math.round(100*u/c);d>5&&o.push({type:e.Network,reason:s.NetworkMediaSyncFailure,ssrc:t.ssrc,debug:`correctedSamplesPercentage: ${d}%`})})),o):o}}class L{#t={};detect(t){const e=this.processData(t);return this.#t[t.connection.id]=t,e}processData(t){const n=[],o=[...t.remote?.audio.inbound||[],...t.remote?.video.inbound||[]];if(!o.length)return n;const r=this.#t[t.connection.id];if(!r)return n;const i=[...r.remote?.audio.inbound||[],...r.remote?.video.inbound||[]],{packetsSent:a}=t.connection,c=r.connection.packetsSent,u=o.reduce(((t,e)=>{const s=i.find((t=>t.ssrc===e.ssrc));return{sumJitter:t.sumJitter+e.jitter,packetsLost:t.packetsLost+e.packetsLost,lastPacketsLost:t.lastPacketsLost+(s?.packetsLost||0)}}),{sumJitter:0,packetsLost:0,lastPacketsLost:0}),d=1e3*t.connection.currentRoundTripTime||0,{sumJitter:p}=u,l=p/o.length,h=a-c,f=u.packetsLost-u.lastPacketsLost,m=h&&f?Math.round(100*f/(h+f)):0,v=m>5,g=l>=200,S=!v&&g||g||v,b=`packetLoss: ${m}%, jitter: ${l}, rtt: ${d}`;return v&&g&&n.push({type:e.Network,reason:s.OutboundNetworkMediaLatency,iceCandidate:t.connection.local.id,debug:b}),S&&n.push({type:e.Network,reason:s.OutboundNetworkQuality,iceCandidate:t.connection.local.id,debug:b}),n}}class C{#t={};detect(t){const e=this.processData(t);return this.#t[t.connection.id]=t,e}processData(t){const n=t.video.outbound.filter((t=>"none"!==t.qualityLimitationReason)),o=[],r=this.#t[t.connection.id]?.video.outbound;return r?(n.forEach((t=>{const n=r.find((e=>e.ssrc===t.ssrc));n&&(t.framesSent>n.framesSent||("cpu"===t.qualityLimitationReason&&o.push({type:e.CPU,reason:s.EncoderCPUThrottling,ssrc:t.ssrc,debug:"qualityLimitationReason: cpu"}),"bandwidth"===t.qualityLimitationReason&&o.push({type:e.Network,reason:s.OutboundNetworkThroughput,ssrc:t.ssrc,debug:"qualityLimitationReason: bandwidth"})))})),o):o}}class P{UNKNOWN_DECODER="unknown";#t={};#o={};detect(t){const e=this.processData(t);return this.#t[t.connection.id]=t,e}processData(t){const n=[],{id:o}=t.connection,r=this.#t[o]?.video.inbound;return t.video.inbound.forEach((t=>{const{decoderImplementation:i,ssrc:a}=t,c=r?.find((t=>t.ssrc===a));c&&(i===this.UNKNOWN_DECODER?this.hadLastDecoderWithIssue(o,a)||(this.setLastDecoderWithIssue(o,a,this.UNKNOWN_DECODER),n.push({ssrc:a,type:e.Stream,reason:s.VideoCodecMismatchIssue,trackIdentifier:t.track.trackIdentifier,debug:`mimeType: ${t.mimeType}, decoderImplementation: ${i}`})):this.setLastDecoderWithIssue(o,a,void 0))})),n}setLastDecoderWithIssue(t,e,s){const n=this.#o[t]??{};void 0===s?delete n[e]:n[e]=s,this.#o[t]=n}hadLastDecoderWithIssue(t,e){const s=this.#o[t];return(s&&s[e])===this.UNKNOWN_DECODER}}const T=t=>"closed"===t.iceConnectionState||"closed"===t.connectionState,R=(t,e,s)=>8*((t,e,s)=>{if(!e)return 0;const n=t[s],o=e[s];if(null==n||null==o)return 0;const r=Math.floor(t.timestamp)-Math.floor(e.timestamp);return 0===r?0:(Number(n)-Number(o))/r*1e3})(t,e,s);class D{connections=[];statsParser;constructor(t){this.statsParser=t.statsParser}listConnections(){return[...this.connections]}addPeerConnection(t){this.connections.push({id:t.id??String(Date.now()+Math.random().toString(32)),pc:t.pc})}removePeerConnection(t){const e=this.connections.findIndex((({pc:e})=>e===t.pc));e>=0&&this.removeConnectionsByIndexes([e])}async parse(){const t=[],e=this.connections.map((async(e,s)=>{if(!T(e.pc))return this.statsParser.parse(e);t.unshift(s)}));t.length&&this.removeConnectionsByIndexes(t);return(await Promise.all(e)).filter((t=>void 0!==t))}removeConnectionsByIndexes(t){t.forEach((t=>{this.connections.splice(t,1)}))}}class E{prevStats=new Map;prevStatsCleanupTimers=new Map;allowedReportTypes=new Set(["candidate-pair","inbound-rtp","outbound-rtp","remote-outbound-rtp","remote-inbound-rtp","track","transport"]);logger;prevConnectionStatsTtlMs;constructor(t){this.logger=t.logger,this.prevConnectionStatsTtlMs=t.prevConnectionStatsTtlMs??55e3}get previouslyParsedStatsConnectionsIds(){return[...this.prevStats.keys()]}async parse(t){if(!T(t.pc))return this.getConnectionStats(t);this.logger.debug("Skip stats parsing. Connection is closed.",{connection:t})}async getConnectionStats(t){const{pc:e,id:s}=t;try{const n=Date.now(),o=await Promise.all(e.getReceivers().map((t=>t.getStats()))),r=await Promise.all(e.getSenders().map((t=>t.getStats())));return{id:s,stats:this.mapReportsStats([...o,...r],t),timeTaken:Date.now()-n}}catch(t){return void this.logger.error("Failed to get stats for PC",{id:s,pc:e,error:t})}}mapReportsStats(t,e){const s={audio:{inbound:[],outbound:[]},video:{inbound:[],outbound:[]},connection:{},remote:{video:{inbound:[],outbound:[]},audio:{inbound:[],outbound:[]}}};t.forEach((t=>{t.forEach((e=>{this.allowedReportTypes.has(e.type)&&this.updateMappedStatsWithReportItemData(e,s,t)}))}));const{id:n}=e,o=this.prevStats.get(n);return o&&this.propagateStatsWithRateValues(s,o.stats),this.prevStats.set(n,{stats:s,ts:Date.now()}),this.resetStatsCleanupTimer(n),s}updateMappedStatsWithReportItemData(t,e,s){const n=t.type;if("candidate-pair"===n&&"succeeded"===t.state&&t.nominated)return void(e.connection=this.prepareConnectionStats(t,s));const o=this.getMediaType(t);if(o)if("outbound-rtp"!==n)if("inbound-rtp"!==n)"remote-outbound-rtp"!==n?"remote-inbound-rtp"===n&&(this.mapConnectionStatsIfNecessary(e,t,s),e.remote[o].inbound.push({...t})):e.remote[o].outbound.push({...t});else{const n=s.get(t.trackId)||s.get(t.mediaSourceId)||{};this.mapConnectionStatsIfNecessary(e,t,s);const r={...t,track:{...n}};e[o].inbound.push(r)}else{const n=s.get(t.trackId)||s.get(t.mediaSourceId)||{},r={...t,track:{...n}};e[o].outbound.push(r)}}getMediaType(t){const e=t.mediaType||t.kind;if(!["audio","video"].includes(e)){const{id:e}=t;if(!e)return;return String(e).includes("Video")?"video":String(e).includes("Audio")?"audio":void 0}return e}propagateStatsWithRateValues(t,e){t.audio.inbound.forEach((t=>{const s=e.audio.inbound.find((({id:e})=>e===t.id));t.bitrate=R(t,s,"bytesReceived"),t.packetRate=R(t,s,"packetsReceived")})),t.audio.outbound.forEach((t=>{const s=e.audio.outbound.find((({id:e})=>e===t.id));t.bitrate=R(t,s,"bytesSent"),t.packetRate=R(t,s,"packetsSent")})),t.video.inbound.forEach((t=>{const s=e.video.inbound.find((({id:e})=>e===t.id));t.bitrate=R(t,s,"bytesReceived"),t.packetRate=R(t,s,"packetsReceived")})),t.video.outbound.forEach((t=>{const s=e.video.outbound.find((({id:e})=>e===t.id));t.bitrate=R(t,s,"bytesSent"),t.packetRate=R(t,s,"packetsSent")}))}mapConnectionStatsIfNecessary(t,e,s){if(t.connection.id||!e.transportId)return;const n=s.get(e.transportId);if(n&&n.selectedCandidatePairId){const e=s.get(n.selectedCandidatePairId);t.connection=this.prepareConnectionStats(e,s)}}prepareConnectionStats(t,e){if(!t||!e)return{};const s={...t};if(s.remoteCandidateId){const t=e.get(s.remoteCandidateId);s.remote={...t}}if(s.localCandidateId){const t=e.get(s.localCandidateId);s.local={...t}}return s}resetStatsCleanupTimer(t){const e=this.prevStatsCleanupTimers.get(t);e&&clearTimeout(e);const s=setTimeout((()=>{this.prevStats.delete(t),this.prevStatsCleanupTimers.delete(t)}),this.prevConnectionStatsTtlMs);this.prevStatsCleanupTimers.set(t,s)}}class I{eventEmitter;#r=!1;detectors=[];networkScoresCalculator;statsReporter;compositeStatsParser;logger;constructor(e){this.logger=e.logger??{debug:()=>{},info:()=>{},warn:()=>{},error:()=>{}},this.eventEmitter=e.issueEmitter??new m,e.onIssues&&this.eventEmitter.on(t.Issue,e.onIssues),e.onNetworkScoresUpdated&&this.eventEmitter.on(t.NetworkScoresUpdated,e.onNetworkScoresUpdated),this.detectors=e.detectors??[new C,new b,new w,new k,new L,new y,new S,new P],this.networkScoresCalculator=e.networkScoresCalculator??new g,this.compositeStatsParser=e.compositeStatsParser??new D({statsParser:new E({logger:this.logger})}),this.statsReporter=e.statsReporter??new v({compositeStatsParser:this.compositeStatsParser,getStatsInterval:e.getStatsInterval??5e3}),window.wid=this,this.wrapRTCPeerConnection(),this.statsReporter.on(v.STATS_REPORT_READY_EVENT,(t=>{this.detectIssues({data:t.stats,ignoreSSRCList:e.ignoreSSRCList}),this.calculateNetworkScores(t.stats)}))}watchNewPeerConnections(){if(this.#r)throw new Error("WebRTCIssueDetector is already started");this.#r=!0,this.statsReporter.startReporting()}stopWatchingNewPeerConnections(){if(!this.#r)throw new Error("WebRTCIssueDetector is already stopped");this.#r=!1,this.statsReporter.stopReporting()}handleNewPeerConnection(t){this.#r?(this.logger.debug("Handling new peer connection",t),this.compositeStatsParser.addPeerConnection({pc:t})):this.logger.debug("Skip handling new peer connection. Detector is not running",t)}emitIssues(e){this.eventEmitter.emit(t.Issue,e)}detectIssues({data:t,ignoreSSRCList:e}){let s=this.detectors.reduce(((e,s)=>[...e,...s.detect(t)]),[]);e?.length&&(s=s.filter((t=>!t.ssrc||!e.includes(t.ssrc)))),s.length>0&&this.emitIssues(s)}calculateNetworkScores(e){const s=this.networkScoresCalculator.calculate(e);this.eventEmitter.emit(t.NetworkScoresUpdated,s)}wrapRTCPeerConnection(){if(!window.RTCPeerConnection)return;const t=window.RTCPeerConnection,e=t=>this.handleNewPeerConnection(t);function s(s){const n=new t(s);return e(n),n}s.prototype=t.prototype,window.RTCPeerConnection=s}}export{S as AvailableOutgoingBitrateIssueDetector,D as CompositeRTCStatsParser,t as EventType,b as FramesDroppedIssueDetector,w as FramesEncodedSentIssueDetector,k as InboundNetworkIssueDetector,s as IssueReason,e as IssueType,y as NetworkMediaSyncIssueDetector,g as NetworkScoresCalculator,L as OutboundNetworkIssueDetector,v as PeriodicWebRTCStatsReporter,C as QualityLimitationsIssueDetector,E as RTCStatsParser,P as VideoCodecMismatchDetector,m as WebRTCIssueEmitter,I as default}; |
@@ -5,3 +5,3 @@ import { NetworkScores, INetworkScoresCalculator, WebRTCStatsParsed } from './types'; | ||
calculate(data: WebRTCStatsParsed): NetworkScores; | ||
private calcucateOutboundScore; | ||
private calculateOutboundScore; | ||
private calculateInboundScore; | ||
@@ -8,0 +8,0 @@ private calculateMOS; |
import { ConnectionInfo, StatsParser, StatsReportItem, Logger } from '../types'; | ||
interface WebRTCStatsParserParams { | ||
logger: Logger; | ||
prevConnectionStatsTtlMs?: number; | ||
} | ||
declare class RTCStatsParser implements StatsParser { | ||
private readonly prevStats; | ||
private readonly prevStatsCleanupTimers; | ||
private readonly allowedReportTypes; | ||
private readonly logger; | ||
private readonly prevConnectionStatsTtlMs; | ||
constructor(params: WebRTCStatsParserParams); | ||
get previouslyParsedStatsConnectionsIds(): string[]; | ||
parse(connection: ConnectionInfo): Promise<StatsReportItem | undefined>; | ||
@@ -18,3 +22,4 @@ private getConnectionStats; | ||
private prepareConnectionStats; | ||
private resetStatsCleanupTimer; | ||
} | ||
export default RTCStatsParser; |
{ | ||
"name": "webrtc-issue-detector", | ||
"version": "1.3.2", | ||
"version": "1.4.0", | ||
"description": "WebRTC diagnostic tool that detects issues with network or user devices", | ||
@@ -10,2 +10,14 @@ "main": "dist/bundle-cjs.js", | ||
"author": "Roman Kuzakov <roman.kuzakov@gmail.com>", | ||
"maintainers": [ | ||
{ | ||
"name": "Vladimir Panov", | ||
"email": "panov.va@mail.ru", | ||
"url": "https://github.com/panov-va" | ||
}, | ||
{ | ||
"name": "Evgeny Melnikov", | ||
"email": "melnikov_evg@mail.ru", | ||
"url": "https://github.com/evgmel" | ||
} | ||
], | ||
"license": "MIT", | ||
@@ -15,3 +27,8 @@ "private": false, | ||
"keywords": [ | ||
"webrtc" | ||
"webrtc", | ||
"stats", | ||
"rtcstatsreport", | ||
"network", | ||
"issues", | ||
"mos calculator" | ||
], | ||
@@ -24,2 +41,3 @@ "files": [ | ||
"lint": "eslint ./src", | ||
"lint:tests": "cd test && eslint ./", | ||
"test": "NODE_ENV=test mocha --config test/utils/runners/mocha/.mocharc.js" | ||
@@ -36,3 +54,3 @@ }, | ||
"@types/chai-subset": "^1.3.3", | ||
"@types/faker": "^6.6.9", | ||
"@types/faker": "^5.5.9", | ||
"@types/mocha": "^9.1.1", | ||
@@ -53,3 +71,3 @@ "@types/node": "12", | ||
"faker": "^5.5.3", | ||
"mocha": "^10.0.0", | ||
"mocha": "^10.2.0", | ||
"rollup": "^2.78.0", | ||
@@ -56,0 +74,0 @@ "rollup-plugin-bundle-size": "^1.0.3", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
73641
753