diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 986345a..cf2f721 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -3,6 +3,7 @@ on: push: branches: - master + - develop pull_request: branches: - master diff --git a/etc/model-generator/package.json b/etc/model-generator/package.json index 54f6e56..964fb42 100644 --- a/etc/model-generator/package.json +++ b/etc/model-generator/package.json @@ -1,6 +1,6 @@ { "name": "model-generator-for-observer-js", - "version": "1.0.0", + "version": "1.0.0-beta.3", "description": "Simple NodeJS to generate Models for observer-js", "main": "index.js", "scripts": { diff --git a/src/ObservedCall.ts b/src/ObservedCall.ts index 4f651a6..484ffb4 100644 --- a/src/ObservedCall.ts +++ b/src/ObservedCall.ts @@ -52,21 +52,6 @@ export class ObservedCall = Record = Record = Record(settings, this); const wasEmpty = this.observedClients.size === 0; const onUpdate = () => this._onClientUpdate(result); @@ -247,32 +236,9 @@ export class ObservedCall = Record { @@ -122,6 +123,8 @@ export class ObservedCallEventMonitor { public onMediaSourceAdded?: (mediaSource: ObservedMediaSource, ctx: Context) => void; public onMediaSourceRemoved?: (mediaSource: ObservedMediaSource, ctx: Context) => void; + public onTrackReport?: (trackReport: TrackReport, ctx: Context) => void; + private _onCallAdded(call: ObservedCall) { const onCallEmpty = () => this.onCallEmpty?.(call, this.context); const onCallNotEmpty = () => this.onCallNotEmpty?.(call, this.context); @@ -154,6 +157,7 @@ export class ObservedCallEventMonitor { const onUserMediaError = (error: string) => this._onUserMediaError(observedClient, error); const onClientUpdated = (...args: ObservedClientEvents['update']) => this.onClientUpdated?.(observedClient, args[0], this.context); const onClientEvent = (event: ClientEvent) => this.onClientEvent?.(observedClient, event, this.context); + const onTrackReport = (trackReport: TrackReport) => this._onTrackReport(observedClient, trackReport); observedClient.once('close', () => { observedClient.off('newpeerconnection', this._onPeerConnconnectionAdded); @@ -166,6 +170,7 @@ export class ObservedCallEventMonitor { observedClient.off('usingturn', onUsingTurn); observedClient.off('update', onClientUpdated); observedClient.off('clientEvent', onClientEvent); + observedClient.off('trackreport', onTrackReport); this.onClientClosed?.(observedClient, this.context); }); @@ -181,6 +186,7 @@ export class ObservedCallEventMonitor { observedClient.on('extensionStats', onClientExtensionStats); observedClient.on('update', onClientUpdated); observedClient.on('clientEvent', onClientEvent); + observedClient.on('trackreport', onTrackReport); this.onClientAdded?.(observedClient, this.context); } @@ -389,4 +395,8 @@ export class ObservedCallEventMonitor { private _onUsingTurn(observedClient: ObservedClient, usingTurn: boolean) { this.onClientUsingTurn?.(observedClient, usingTurn, this.context); } + + private _onTrackReport(client: ObservedClient, trackReport: TrackReport) { + this.onTrackReport?.(trackReport, this.context); + } } \ No newline at end of file diff --git a/src/ObservedClient.ts b/src/ObservedClient.ts index 30d1ce7..cf81ecc 100644 --- a/src/ObservedClient.ts +++ b/src/ObservedClient.ts @@ -10,6 +10,9 @@ import { ClientMetaTypes } from './schema/ClientMetaTypes'; import { parseJsonAs } from './common/utils'; import { CalculatedScore } from './scores/CalculatedScore'; import { Detectors } from './detectors/Detectors'; +import { ClientReport, TrackReport } from './Reports'; +import { ObservedInboundTrack } from './ObservedInboundTrack'; +import { ObservedOutboundTrack } from './ObservedOutboundTrack'; const logger = createLogger('ObservedClient'); @@ -33,6 +36,8 @@ export type ObservedClientEvents = { clientEvent: [ClientEvent]; newpeerconnection: [ObservedPeerConnection]; + + trackreport: [TrackReport]; }; export declare interface ObservedClient { @@ -77,20 +82,24 @@ export class ObservedClient = Record = Record = Record = Record = Record = Record = Record = Record = Record = Record { + const newObservedPeerConnection = new ObservedPeerConnection(sample.peerConnectionId, this); + const onInboundTrackRemoved = (track: ObservedInboundTrack) => { + this.emit('trackreport', { + direction: 'inbound', + ...track.report, + }); + }; + const onOutboundTrackRemoved = (track: ObservedOutboundTrack) => { + this.emit('trackreport', { + direction: 'outbound', + ...track.report, + }); + }; + + newObservedPeerConnection.once('close', () => { + newObservedPeerConnection?.off('removed-inbound-track', onInboundTrackRemoved); + newObservedPeerConnection?.off('removed-outbound-track', onOutboundTrackRemoved); this.observedPeerConnections.delete(sample.peerConnectionId); }); - this.observedPeerConnections.set(sample.peerConnectionId, observedPeerConnection); + newObservedPeerConnection.on('removed-inbound-track', onInboundTrackRemoved); + newObservedPeerConnection.on('removed-outbound-track', onOutboundTrackRemoved); + this.observedPeerConnections.set(sample.peerConnectionId, newObservedPeerConnection); - this.emit('newpeerconnection', observedPeerConnection); + this.emit('newpeerconnection', newObservedPeerConnection); + + observedPeerConnection = newObservedPeerConnection; } observedPeerConnection.accept(sample); diff --git a/src/ObservedInboundTrack.ts b/src/ObservedInboundTrack.ts index 2f883b9..5aff01f 100644 --- a/src/ObservedInboundTrack.ts +++ b/src/ObservedInboundTrack.ts @@ -5,6 +5,7 @@ import { Detectors } from './detectors/Detectors'; import { ObservedPeerConnection } from './ObservedPeerConnection'; import { ObservedInboundRtp } from './ObservedInboundRtp'; import { ObservedMediaPlayout } from './ObservedMediaPlayout'; +import { InboundTrackReport } from './Reports'; export class ObservedInboundTrack implements InboundTrackSample { public readonly detectors: Detectors; @@ -13,6 +14,7 @@ export class ObservedInboundTrack implements InboundTrackSample { value: undefined, }; public appData?: Record; + public report: InboundTrackReport; private _visited = false; @@ -32,6 +34,21 @@ export class ObservedInboundTrack implements InboundTrackSample { private readonly _mediaPlayout?: ObservedMediaPlayout, ) { this.detectors = new Detectors(); + + this.report = { + trackId: this.id, + kind: this.kind, + fractionLostDistribution: { + gtOrEq050: 0, + lt001: 0, + lt005: 0, + lt010: 0, + lt020: 0, + lt050: 0, + count: 0, + sum: 0, + }, + }; } public get score() { @@ -68,8 +85,21 @@ export class ObservedInboundTrack implements InboundTrackSample { this.timestamp = stats.timestamp; this.calculatedScore.value = stats.score; this.attachments = stats.attachments; - + + const fl = this._inboundRtp?.fractionLost; + + if (fl !== undefined) { + if (fl < 0.01) this.report.fractionLostDistribution.lt001 += 1; + else if (fl < 0.05) this.report.fractionLostDistribution.lt005 += 1; + else if (fl < 0.1) this.report.fractionLostDistribution.lt010 += 1; + else if (fl < 0.2) this.report.fractionLostDistribution.lt020 += 1; + else if (fl < 0.5) this.report.fractionLostDistribution.lt050 += 1; + else this.report.fractionLostDistribution.gtOrEq050 += 1; + + this.report.fractionLostDistribution.count += 1; + this.report.fractionLostDistribution.sum += fl; + } + this.detectors.update(); } - } \ No newline at end of file diff --git a/src/ObservedOutboundTrack.ts b/src/ObservedOutboundTrack.ts index a841345..e9e9cc1 100644 --- a/src/ObservedOutboundTrack.ts +++ b/src/ObservedOutboundTrack.ts @@ -5,11 +5,14 @@ import { Detectors } from './detectors/Detectors'; import { ObservedPeerConnection } from './ObservedPeerConnection'; import { ObservedOutboundRtp } from './ObservedOutboundRtp'; import { ObservedMediaSource } from './ObservedMediaSource'; +import { OutboundTrackReport } from './Reports'; export class ObservedOutboundTrack implements OutboundTrackSample { public readonly detectors: Detectors; private _visited = false; public appData?: Record; + public readonly report: OutboundTrackReport; + public readonly calculatedScore: CalculatedScore = { weight: 1, value: undefined, @@ -30,6 +33,11 @@ export class ObservedOutboundTrack implements OutboundTrackSample { private readonly _outboundRtps?: ObservedOutboundRtp[], private readonly _mediaSource?: ObservedMediaSource, ) { + this.report = { + trackId: this.id, + kind: this.kind as 'audio' | 'video', + }; + this.detectors = new Detectors(); } diff --git a/src/ObservedPeerConnection.ts b/src/ObservedPeerConnection.ts index 25cb2bc..3ef53f2 100644 --- a/src/ObservedPeerConnection.ts +++ b/src/ObservedPeerConnection.ts @@ -128,9 +128,9 @@ export class ObservedPeerConnection extends EventEmitter { public closedAt?: number; public updated = Date.now(); - public connectionState?: string; - public iceConnectionState?: string; - public iceGatheringState?: string; + public connectionState?: 'new' | 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed'; + public iceConnectionState?: 'new' | 'checking' | 'connected' | 'completed' | 'failed' | 'disconnected' | 'closed'; + public iceGatheringState?: 'new' | 'gathering' | 'complete'; public availableIncomingBitrate = 0; public availableOutgoingBitrate = 0; @@ -286,6 +286,22 @@ export class ObservedPeerConnection extends EventEmitter { if (this.closed) return; this.closed = true; + this.observedCertificates.forEach((cert) => this.emit('removed-certificate', cert)); + this.observedCodecs.forEach((codec) => this.emit('removed-codec', codec)); + this.observedDataChannels.forEach((dc) => this.emit('removed-data-channel', dc)); + this.observedIceCandidates.forEach((candidate) => this.emit('removed-ice-candidate', candidate)); + this.observedIceCandidatesPair.forEach((pair) => this.emit('removed-ice-candidate-pair', pair)); + this.observedIceTransports.forEach((transport) => this.emit('removed-ice-transport', transport)); + this.observedInboundRtps.forEach((rtp) => this.emit('removed-inbound-rtp', rtp)); + this.observedInboundTracks.forEach((track) => this.emit('removed-inbound-track', track)); + this.observedMediaPlayouts.forEach((playout) => this.emit('removed-media-playout', playout)); + this.observedMediaSources.forEach((source) => this.emit('removed-media-source', source)); + this.observedOutboundRtps.forEach((rtp) => this.emit('removed-outbound-rtp', rtp)); + this.observedOutboundTracks.forEach((track) => this.emit('removed-outbound-track', track)); + this.observedPeerConnectionTransports.forEach((transport) => this.emit('removed-peer-connection-transport', transport)); + this.observedRemoteInboundRtps.forEach((rtp) => this.emit('removed-remote-inbound-rtp', rtp)); + this.observedRemoteOutboundRtps.forEach((rtp) => this.emit('removed-remote-outbound-rtp', rtp)); + this.observedCertificates.clear(); this.observedCodecs.clear(); this.observedDataChannels.clear(); diff --git a/src/Observer.ts b/src/Observer.ts index 469c7ad..84124b0 100644 --- a/src/Observer.ts +++ b/src/Observer.ts @@ -31,6 +31,8 @@ export type ObserverConfig = Record = Record = Record this._onObservedCallUpdated(observedCall); @@ -211,11 +212,7 @@ export class Observer = Record(ctx?: CTX): ObserverEventMonitor { diff --git a/src/ObserverEventMonitor.ts b/src/ObserverEventMonitor.ts index b847903..8b4f9bc 100644 --- a/src/ObserverEventMonitor.ts +++ b/src/ObserverEventMonitor.ts @@ -14,6 +14,7 @@ import { ObservedOutboundRtp } from './ObservedOutboundRtp'; import { ObservedOutboundTrack } from './ObservedOutboundTrack'; import { ObservedPeerConnection } from './ObservedPeerConnection'; import { Observer } from './Observer'; +import { TrackReport } from './Reports'; import { ClientEvent, ClientIssue, ClientMetaData, ClientSample, ExtensionStat } from './schema/ClientSample'; export class ObserverEventMonitor { @@ -160,6 +161,8 @@ export class ObserverEventMonitor { public onMediaSourceRemoved?: (mediaSource: ObservedMediaSource, ctx: Context) => void; public onMediaSourceUpdated?: (mediaSource: ObservedMediaSource, ctx: Context) => void; + public onTrackReport?: (report: TrackReport, observedClient: ObservedClient, ctx: Context) => void; + private _onCallAdded(call: ObservedCall) { const onCallEmpty = () => this.onCallEmpty?.(call, this.context); const onCallNotEmpty = () => this.onCallNotEmpty?.(call, this.context); @@ -192,6 +195,7 @@ export class ObserverEventMonitor { const onUserMediaError = (error: string) => this._onUserMediaError(observedClient, error); const onClientUpdated = (...args: ObservedClientEvents['update']) => this.onClientUpdated?.(observedClient, args[0], this.context); const onClientEvent = (event: ClientEvent) => this.onClientEvent?.(observedClient, event, this.context); + const onTrackReport = (report: TrackReport) => this._onTrackReport(report, observedClient); observedClient.once('close', () => { observedClient.off('newpeerconnection', this._onPeerConnconnectionAdded); @@ -205,6 +209,7 @@ export class ObserverEventMonitor { observedClient.off('extensionStats', onClientExtensionStats); observedClient.off('update', onClientUpdated); observedClient.off('clientEvent', onClientEvent); + observedClient.off('trackreport', onTrackReport); this.onClientClosed?.(observedClient, this.context); }); @@ -220,6 +225,7 @@ export class ObserverEventMonitor { observedClient.on('extensionStats', onClientExtensionStats); observedClient.on('update', onClientUpdated); observedClient.on('clientEvent', onClientEvent); + observedClient.on('trackreport', onTrackReport); this.onClientAdded?.(observedClient, this.context); } @@ -548,4 +554,8 @@ export class ObserverEventMonitor { private _onUsingTurn(observedClient: ObservedClient, usingTurn: boolean) { this.onClientUsingTurn?.(observedClient, usingTurn, this.context); } + + private _onTrackReport(report: TrackReport, observedClient: ObservedClient) { + this.onTrackReport?.(report, observedClient, this.context); + } } \ No newline at end of file diff --git a/src/Reports.ts b/src/Reports.ts new file mode 100644 index 0000000..ffd2344 --- /dev/null +++ b/src/Reports.ts @@ -0,0 +1,87 @@ +export type InboundAudioTrackReport = { + trackId: string, + fractionLostDistribution: { + lt001: number; + lt005: number; + lt010: number; + lt020: number; + lt050: number; + gtOrEq050: number; + count: number; + sum: number; + }, +}; + +export type InboundVideoTrackReport = { + trackId: string, + fractionLostDistribution: { + lt001: number; + lt005: number; + lt010: number; + lt020: number; + lt050: number; + gtOrEq050: number; + count: number; + sum: number; + }, +}; + +export type OutboundAudioTrackReport = { + trackId: string, +}; + +export type OutboundVideoTrackReport = { + trackId: string, +}; + +export type InboundTrackReport = ({ kind: 'audio' } & InboundAudioTrackReport) +| ({ kind: 'video' } & InboundVideoTrackReport); + +export type OutboundTrackReport = ({ kind: 'audio' } & OutboundAudioTrackReport) +| ({ kind: 'video' } & OutboundVideoTrackReport); + +export type TrackReport = ({ direction: 'inbound' } & InboundTrackReport) +| ({ direction: 'outbound' } & OutboundTrackReport); + +export type ClientReport = { + callId: string; + clientId: string; + + totalDataChannelBytesReceived: number; + totalDataChannelBytesSent: number; + totalDataChannelMessagesReceived: number; + totalDataChannelMessagesSent: number; + totalInboundRtpPacketsReceived: number; + totalInboundRtpPacketsLost: number; + totalInboundRtpBytesReceived: number; + totalOutboundRtpPacketsSent: number; + totalOutboundRtpBytesSent: number; + totalAudioBytesReceived: number; + totalVideoBytesReceived: number; + totalAudioBytesSent: number; + totalVideoBytesSent: number; + totalNumberOfIssues: number; + + issues: Record; + + rttDistribution: { + lt50ms: number; + lt150ms: number; + lt300ms: number; + gtOrEq300ms: number; + count: number; + sum: number; + }, + + scoreDistribution: { + '0': number; + '1': number; + '2': number; + '3': number; + '4': number; + '5': number; + + count: number; + sum: number; + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 2cb6a7a..05bbef0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,3 +26,7 @@ export { ScoreCalculator } from './scores/ScoreCalculator'; export { ObservedClientEventMonitor } from './ObservedClientEventMonitor'; export { ObserverEventMonitor } from './ObserverEventMonitor'; export { Middleware } from './common/Middleware'; +export type { + TrackReport, + ClientReport, +} from './Reports';