@observertc/observer-js
Advanced tools
Comparing version 0.42.1 to 0.42.2
@@ -41,3 +41,3 @@ "use strict"; | ||
function calculateBaseVideoScore(track, newIssues) { | ||
var _a; | ||
var _a, _b; | ||
if (!track.highestLayer) { | ||
@@ -62,3 +62,3 @@ return; | ||
// let's assume vp8 for now | ||
const bppRange = exports.BPP_RANGES[track.contentType][track.codec]; | ||
const bppRange = exports.BPP_RANGES[track.contentType][(_a = track.codec) !== null && _a !== void 0 ? _a : 'vp8']; | ||
if (bpp / 2 < bppRange.low) { | ||
@@ -97,3 +97,3 @@ score.score = 1.0; | ||
severity: issue.severity, | ||
text: (_a = issue.description) !== null && _a !== void 0 ? _a : 'Issue occurred', | ||
text: (_b = issue.description) !== null && _b !== void 0 ? _b : 'Issue occurred', | ||
}); | ||
@@ -100,0 +100,0 @@ } |
import { CallEventReport, CallMetaReport, ClientDataChannelReport, ClientExtensionReport, IceCandidatePairReport, InboundAudioTrackReport, InboundVideoTrackReport, ObserverEventReport, OutboundAudioTrackReport, OutboundVideoTrackReport, PeerConnectionTransportReport, SfuEventReport, SfuExtensionReport, SfuInboundRtpPadReport, SfuMetaReport, SfuOutboundRtpPadReport, SfuSctpStreamReport, SFUTransportReport } from '@observertc/report-schemas-js'; | ||
export type MediaKind = 'audio' | 'video'; | ||
export type SupportedVideoCodecType = 'vp8' | 'vp9' | 'h264' | 'h265'; | ||
export interface ObserverSinkContext { | ||
@@ -4,0 +5,0 @@ readonly callEventReports: CallEventReport[]; |
@@ -8,7 +8,5 @@ /// <reference types="node" /> | ||
import { ClientSample } from '@observertc/sample-schemas-js'; | ||
import { ClientIssue } from './monitors/CallSummary'; | ||
import { ObservedOutboundAudioTrack } from './ObservedOutboundAudioTrack'; | ||
import { ObservedOutboundVideoTrack } from './ObservedOutboundVideoTrack'; | ||
import { CalculatedScore } from './common/CalculatedScore'; | ||
type ClientIssueDetectionConfig = Pick<ClientIssue, 'severity'>; | ||
export type ObservedCallModel = { | ||
@@ -46,3 +44,4 @@ serviceId: string; | ||
private _updated; | ||
private _ended?; | ||
ended?: number; | ||
started?: number; | ||
constructor(_model: ObservedCallModel, observer: Observer, appData: AppData); | ||
@@ -54,3 +53,2 @@ get serviceId(): string; | ||
get updated(): number; | ||
get ended(): number | undefined; | ||
get clients(): ReadonlyMap<string, ObservedClient>; | ||
@@ -63,10 +61,7 @@ get score(): CalculatedScore | undefined; | ||
appData: ClientAppData; | ||
generateClientJoinedReport?: boolean; | ||
joined?: number; | ||
detectIssues?: { | ||
rejoin: ClientIssue['severity'] | ClientIssueDetectionConfig; | ||
}; | ||
joinedTimestamp?: number; | ||
createClientJoinedReport?: boolean; | ||
createClientLeftReport?: boolean; | ||
}): ObservedClient<ClientAppData>; | ||
} | ||
export {}; | ||
//# sourceMappingURL=ObservedCall.d.ts.map |
@@ -33,2 +33,3 @@ "use strict"; | ||
this._updated = Date.now(); | ||
this.started = Date.now(); | ||
this.setMaxListeners(Infinity); | ||
@@ -54,5 +55,2 @@ } | ||
} | ||
get ended() { | ||
return this._ended; | ||
} | ||
get clients() { | ||
@@ -95,3 +93,8 @@ return this._clients; | ||
this._closed = true; | ||
this._ended = timestamp !== null && timestamp !== void 0 ? timestamp : Date.now(); | ||
if (timestamp) { | ||
this.ended = timestamp; | ||
} | ||
else if (!this.ended) { | ||
this.ended = Date.now(); | ||
} | ||
Array.from(this._clients.values()).forEach((client) => client.close()); | ||
@@ -105,7 +108,6 @@ this.emit('close'); | ||
createClient(config) { | ||
var _a; | ||
if (this._closed) | ||
throw new Error(`Call ${this.callId} is closed`); | ||
const { appData, generateClientJoinedReport, joined = Date.now(), detectIssues = { | ||
rejoin: 'minor' | ||
} } = config, model = __rest(config, ["appData", "generateClientJoinedReport", "joined", "detectIssues"]); | ||
const { appData, createClientJoinedReport, createClientLeftReport, joinedTimestamp = Date.now() } = config, model = __rest(config, ["appData", "createClientJoinedReport", "createClientLeftReport", "joinedTimestamp"]); | ||
const result = new ObservedClient_1.ObservedClient(model, this, appData); | ||
@@ -123,18 +125,11 @@ const onUpdate = ({ sample }) => { | ||
this._clients.set(result.clientId, result); | ||
if (generateClientJoinedReport) { | ||
this.reports.addCallEventReport((0, callEventReports_1.createClientJoinedEventReport)(this.serviceId, result.mediaUnitId, this.roomId, this.callId, result.clientId, joined, result.userId, result.marker)); | ||
if (createClientJoinedReport) { | ||
this.reports.addCallEventReport((0, callEventReports_1.createClientJoinedEventReport)(this.serviceId, result.mediaUnitId, this.roomId, this.callId, result.clientId, (_a = result.joined) !== null && _a !== void 0 ? _a : joinedTimestamp, result.userId, result.marker)); | ||
} | ||
if (detectIssues === null || detectIssues === void 0 ? void 0 : detectIssues.rejoin) { | ||
const issueBaseConfig = typeof detectIssues.rejoin === 'object' ? detectIssues.rejoin : { | ||
severity: detectIssues.rejoin | ||
}; | ||
const rejoinedClientIssueListener = (event) => { | ||
event.lastJoined; | ||
result.addIssue(Object.assign({ timestamp: Date.now() }, issueBaseConfig)); | ||
}; | ||
result.once('close', () => { | ||
result.off('rejoined', rejoinedClientIssueListener); | ||
}); | ||
result.on('rejoined', rejoinedClientIssueListener); | ||
} | ||
result.once('close', () => { | ||
var _a; | ||
if (createClientLeftReport) { | ||
this.reports.addCallEventReport((0, callEventReports_1.createClientLeftEventReport)(this.serviceId, result.mediaUnitId, this.roomId, this.callId, result.clientId, (_a = result.left) !== null && _a !== void 0 ? _a : Date.now(), result.userId, result.marker)); | ||
} | ||
}); | ||
this.emit('newclient', result); | ||
@@ -141,0 +136,0 @@ return result; |
/// <reference types="node" /> | ||
import { Browser, ClientSample, Engine, IceLocalCandidate, IceRemoteCandidate, OperationSystem, Platform } from '@observertc/sample-schemas-js'; | ||
import { Browser, ClientSample, Engine, IceLocalCandidate, IceRemoteCandidate, MediaCodecStats, MediaDevice, OperationSystem, Platform } from '@observertc/sample-schemas-js'; | ||
import { ObservedCall } from './ObservedCall'; | ||
@@ -76,2 +76,13 @@ import { EventEmitter } from 'events'; | ||
} | ||
type PendingPeerConnectionTimestamp = { | ||
type: 'opened' | 'closed'; | ||
peerConnectionId: string; | ||
timestamp: number; | ||
}; | ||
type PendingMediaTrackTimestamp = { | ||
type: 'added' | 'removed'; | ||
peerConnectionId: string; | ||
mediaTrackId: string; | ||
timestamp: number; | ||
}; | ||
export declare class ObservedClient<AppData extends Record<string, unknown> = Record<string, unknown>> extends EventEmitter { | ||
@@ -86,6 +97,6 @@ private readonly _model; | ||
private _closed; | ||
private _joinedEventAt?; | ||
private _acceptedSamples; | ||
private _timeZoneOffsetInHours?; | ||
private _left?; | ||
joined?: number; | ||
left?: number; | ||
score?: CalculatedScore; | ||
@@ -121,8 +132,8 @@ usingTURN: boolean; | ||
mediaConstraints: string[]; | ||
readonly mediaDevices: string[]; | ||
readonly codecs: string[]; | ||
readonly mediaDevices: MediaDevice[]; | ||
readonly mediaCodecs: MediaCodecStats[]; | ||
readonly userMediaErrors: string[]; | ||
readonly issues: ClientIssue[]; | ||
readonly ωpendingCreatedTracksTimestamp: Map<string, number>; | ||
readonly ωpendingCreatedPeerConnectionTimestamp: Map<string, number>; | ||
ωpendingPeerConnectionTimestamps: PendingPeerConnectionTimestamp[]; | ||
ωpendingMediaTrackTimestamps: PendingMediaTrackTimestamp[]; | ||
constructor(_model: ObservedClientModel, call: ObservedCall, appData: AppData); | ||
@@ -166,4 +177,4 @@ getSfu<T extends Record<string, unknown> = Record<string, unknown>>(): ObservedSfu<T> | undefined; | ||
private _createPeerConnection; | ||
private _addClientLeftReport; | ||
} | ||
export {}; | ||
//# sourceMappingURL=ObservedClient.d.ts.map |
@@ -21,3 +21,2 @@ "use strict"; | ||
const utils_1 = require("./common/utils"); | ||
const callEventReports_1 = require("./common/callEventReports"); | ||
const CallEventType_1 = require("./common/CallEventType"); | ||
@@ -61,7 +60,7 @@ const logger = (0, logger_1.createLogger)('ObservedClient'); | ||
this.mediaDevices = []; | ||
this.codecs = []; | ||
this.mediaCodecs = []; | ||
this.userMediaErrors = []; | ||
this.issues = []; | ||
this.ωpendingCreatedTracksTimestamp = new Map(); | ||
this.ωpendingCreatedPeerConnectionTimestamp = new Map(); | ||
this.ωpendingPeerConnectionTimestamps = []; | ||
this.ωpendingMediaTrackTimestamps = []; | ||
this.setMaxListeners(Infinity); | ||
@@ -150,5 +149,2 @@ } | ||
this._closed = true; | ||
if (!this._left) { | ||
this._addClientLeftReport(); | ||
} | ||
Array.from(this._peerConnections.values()).forEach((peerConnection) => peerConnection.close()); | ||
@@ -201,3 +197,3 @@ this.emit('close'); | ||
accept(sample) { | ||
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26; | ||
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29; | ||
if (this._closed) | ||
@@ -296,17 +292,56 @@ throw new Error(`Client ${this.clientId} is closed`); | ||
} | ||
for (let _27 of (_p = sample.customCallEvents) !== null && _p !== void 0 ? _p : []) { | ||
const { timestamp } = _27, callEvent = __rest(_27, ["timestamp"]); | ||
for (let _30 of (_p = sample.customCallEvents) !== null && _p !== void 0 ? _p : []) { | ||
const { timestamp } = _30, callEvent = __rest(_30, ["timestamp"]); | ||
switch (callEvent.name) { | ||
case CallEventType_1.CallEventType.CLIENT_JOINED: { | ||
const lastJoined = this._joinedEventAt; | ||
this._joinedEventAt = timestamp !== null && timestamp !== void 0 ? timestamp : sample.timestamp; | ||
if (lastJoined && this._joinedEventAt && lastJoined !== this._joinedEventAt) { | ||
const lastJoined = this.joined; | ||
this.joined = timestamp; | ||
// if it is joined before and it is joined again | ||
if (lastJoined && this.joined && lastJoined !== this.joined) { | ||
this.emit('rejoined', { lastJoined }); | ||
} | ||
// in case we have a left event before the joined event | ||
if (this.left && this.joined && this.left < this.joined) { | ||
this.left = undefined; | ||
} | ||
break; | ||
} | ||
case CallEventType_1.CallEventType.CLIENT_LEFT: { | ||
this._left = timestamp; | ||
this.left = timestamp; | ||
break; | ||
} | ||
case CallEventType_1.CallEventType.PEER_CONNECTION_OPENED: { | ||
callEvent.peerConnectionId && this.ωpendingPeerConnectionTimestamps.push({ | ||
type: 'opened', | ||
peerConnectionId: callEvent.peerConnectionId, | ||
timestamp: timestamp !== null && timestamp !== void 0 ? timestamp : sample.timestamp, | ||
}); | ||
break; | ||
} | ||
case CallEventType_1.CallEventType.PEER_CONNECTION_CLOSED: { | ||
callEvent.peerConnectionId && this.ωpendingPeerConnectionTimestamps.push({ | ||
type: 'closed', | ||
peerConnectionId: callEvent.peerConnectionId, | ||
timestamp: timestamp !== null && timestamp !== void 0 ? timestamp : sample.timestamp, | ||
}); | ||
break; | ||
} | ||
case CallEventType_1.CallEventType.MEDIA_TRACK_ADDED: { | ||
callEvent.peerConnectionId && callEvent.mediaTrackId && this.ωpendingMediaTrackTimestamps.push({ | ||
type: 'added', | ||
peerConnectionId: callEvent.peerConnectionId, | ||
mediaTrackId: callEvent.mediaTrackId, | ||
timestamp: timestamp !== null && timestamp !== void 0 ? timestamp : sample.timestamp, | ||
}); | ||
break; | ||
} | ||
case CallEventType_1.CallEventType.MEDIA_TRACK_REMOVED: { | ||
callEvent.peerConnectionId && callEvent.mediaTrackId && this.ωpendingMediaTrackTimestamps.push({ | ||
type: 'removed', | ||
peerConnectionId: callEvent.peerConnectionId, | ||
mediaTrackId: callEvent.mediaTrackId, | ||
timestamp: timestamp !== null && timestamp !== void 0 ? timestamp : sample.timestamp, | ||
}); | ||
break; | ||
} | ||
case CallEventType_1.CallEventType.CLIENT_ISSUE: { | ||
@@ -356,3 +391,5 @@ const severity = callEvent.value ? callEvent.value : 'minor'; | ||
this.reports.addCallMetaReport(callMetaReport); | ||
codec.mimeType && this.codecs.push(codec.mimeType); | ||
if (codec.mimeType && !this.mediaCodecs.find((c) => c.mimeType === codec.mimeType)) { | ||
this.mediaCodecs.push(codec); | ||
} | ||
} | ||
@@ -372,3 +409,5 @@ for (const iceServer of (_t = sample.iceServers) !== null && _t !== void 0 ? _t : []) { | ||
this.reports.addCallMetaReport(callMetaReport); | ||
mediaDevice.label && this.mediaDevices.push(mediaDevice.label); | ||
if (mediaDevice.id && !this.mediaDevices.find((d) => d.id === mediaDevice.id)) { | ||
this.mediaDevices.push(mediaDevice); | ||
} | ||
} | ||
@@ -553,2 +592,36 @@ for (const mediaSource of (_v = sample.mediaSources) !== null && _v !== void 0 ? _v : []) { | ||
} | ||
// try to set the timestamps of the peer connections | ||
if (0 < this.ωpendingPeerConnectionTimestamps.length) { | ||
const newPendingTimestamps = []; | ||
for (const pendingTimestamp of this.ωpendingPeerConnectionTimestamps) { | ||
const peerConnection = this._peerConnections.get(pendingTimestamp.peerConnectionId); | ||
if (!peerConnection) { | ||
newPendingTimestamps.push(pendingTimestamp); | ||
continue; | ||
} | ||
if (pendingTimestamp.type === 'opened') | ||
peerConnection.opened = pendingTimestamp.timestamp; | ||
else if (pendingTimestamp.type === 'closed') | ||
peerConnection.closedTimestamp = pendingTimestamp.timestamp; | ||
} | ||
this.ωpendingPeerConnectionTimestamps = newPendingTimestamps; | ||
} | ||
// try to set the timestamps of the media tracks | ||
if (0 < this.ωpendingMediaTrackTimestamps.length) { | ||
const newPendingTimestamps = []; | ||
for (const pendingTimestamp of this.ωpendingMediaTrackTimestamps) { | ||
const peerConnection = this._peerConnections.get(pendingTimestamp.peerConnectionId); | ||
const mediaTrack = (_22 = (_21 = (_20 = peerConnection === null || peerConnection === void 0 ? void 0 : peerConnection.inboundAudioTracks.get(pendingTimestamp.mediaTrackId)) !== null && _20 !== void 0 ? _20 : peerConnection === null || peerConnection === void 0 ? void 0 : peerConnection.inboundVideoTracks.get(pendingTimestamp.mediaTrackId)) !== null && _21 !== void 0 ? _21 : peerConnection === null || peerConnection === void 0 ? void 0 : peerConnection.outboundAudioTracks.get(pendingTimestamp.mediaTrackId)) !== null && _22 !== void 0 ? _22 : peerConnection === null || peerConnection === void 0 ? void 0 : peerConnection.outboundVideoTracks.get(pendingTimestamp.mediaTrackId); | ||
if (!mediaTrack) { | ||
newPendingTimestamps.push(pendingTimestamp); | ||
continue; | ||
} | ||
if (pendingTimestamp.type === 'added') | ||
mediaTrack.added = pendingTimestamp.timestamp; | ||
else if (pendingTimestamp.type === 'removed') | ||
mediaTrack.removed = pendingTimestamp.timestamp; | ||
} | ||
this.ωpendingMediaTrackTimestamps = newPendingTimestamps; | ||
} | ||
// close resources that are not visited | ||
for (const peerConnection of this._peerConnections.values()) { | ||
@@ -625,8 +698,8 @@ if (!peerConnection.visited) { | ||
this.deltaDataChannelBytesSent += peerConnection.deltaDataChannelBytesSent; | ||
this.availableIncomingBitrate += (_20 = peerConnection.availableIncomingBitrate) !== null && _20 !== void 0 ? _20 : 0; | ||
this.availableOutgoingBitrate += (_21 = peerConnection.availableOutgoingBitrate) !== null && _21 !== void 0 ? _21 : 0; | ||
sumRttInMs += (_22 = peerConnection.avgRttInMs) !== null && _22 !== void 0 ? _22 : 0; | ||
this.availableIncomingBitrate += (_23 = peerConnection.availableIncomingBitrate) !== null && _23 !== void 0 ? _23 : 0; | ||
this.availableOutgoingBitrate += (_24 = peerConnection.availableOutgoingBitrate) !== null && _24 !== void 0 ? _24 : 0; | ||
sumRttInMs += (_25 = peerConnection.avgRttInMs) !== null && _25 !== void 0 ? _25 : 0; | ||
anyPeerConnectionUsingTurn || (anyPeerConnectionUsingTurn = peerConnection.usingTURN); | ||
minPcScore = Math.min(minPcScore, (_24 = (_23 = peerConnection.score) === null || _23 === void 0 ? void 0 : _23.score) !== null && _24 !== void 0 ? _24 : Number.MAX_SAFE_INTEGER); | ||
maxPcScore = Math.max(maxPcScore, (_26 = (_25 = peerConnection.score) === null || _25 === void 0 ? void 0 : _25.score) !== null && _26 !== void 0 ? _26 : Number.MIN_SAFE_INTEGER); | ||
minPcScore = Math.min(minPcScore, (_27 = (_26 = peerConnection.score) === null || _26 === void 0 ? void 0 : _26.score) !== null && _27 !== void 0 ? _27 : Number.MAX_SAFE_INTEGER); | ||
maxPcScore = Math.max(maxPcScore, (_29 = (_28 = peerConnection.score) === null || _28 === void 0 ? void 0 : _28.score) !== null && _29 !== void 0 ? _29 : Number.MIN_SAFE_INTEGER); | ||
} | ||
@@ -708,9 +781,3 @@ this.score = { | ||
} | ||
_addClientLeftReport() { | ||
if (this._left) | ||
return; | ||
this._left = Date.now(); | ||
this.reports.addCallEventReport((0, callEventReports_1.createClientLeftEventReport)(this.serviceId, this.mediaUnitId, this.roomId, this.callId, this.clientId, this._left, this.userId, this.marker)); | ||
} | ||
} | ||
exports.ObservedClient = ObservedClient; |
@@ -51,2 +51,4 @@ /// <reference types="node" /> | ||
visited: boolean; | ||
added?: number; | ||
removed?: number; | ||
private readonly _stats; | ||
@@ -53,0 +55,0 @@ private _closed; |
@@ -152,2 +152,4 @@ "use strict"; | ||
this.visited = true; | ||
// a peer connection is active if it has at least one active track | ||
this.peerConnection.visited = true; | ||
this._updated = now; | ||
@@ -154,0 +156,0 @@ this.emit('update', { |
@@ -8,2 +8,3 @@ /// <reference types="node" /> | ||
import { ObservedOutboundVideoTrack } from './ObservedOutboundVideoTrack'; | ||
import { SupportedVideoCodecType } from './common/types'; | ||
export type ObservedInboundVideoTrackModel = { | ||
@@ -51,4 +52,6 @@ trackId: string; | ||
visited: boolean; | ||
added?: number; | ||
removed?: number; | ||
contentType: 'lowmotion' | 'standard' | 'highmotion'; | ||
codec: 'vp8' | 'vp9' | 'h264'; | ||
codec?: SupportedVideoCodecType; | ||
private readonly _stats; | ||
@@ -55,0 +58,0 @@ private _closed; |
@@ -14,3 +14,2 @@ "use strict"; | ||
this.contentType = 'standard'; | ||
this.codec = 'vp8'; | ||
this._stats = new Map(); | ||
@@ -161,2 +160,4 @@ this._closed = false; | ||
this.visited = true; | ||
// a peer connection is active if it has at least one active track | ||
this.peerConnection.visited = true; | ||
this._updated = now; | ||
@@ -163,0 +164,0 @@ this.emit('update', { |
@@ -45,2 +45,4 @@ /// <reference types="node" /> | ||
visited: boolean; | ||
added?: number; | ||
removed?: number; | ||
bitrate: number; | ||
@@ -47,0 +49,0 @@ rttInMs?: number; |
@@ -123,15 +123,5 @@ "use strict"; | ||
this._stats.set(sample.ssrc, stats); | ||
// this.bitrate = [ ...this._stats.values() ].reduce((acc, stat) => acc + stat.bitrate, 0); | ||
// this.rttInMs = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.rttInMs ?? 0), 0) / (this._stats.size || 1); | ||
// this.totalLostPackets = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.packetsLost ?? 0), 0); | ||
// this.totalSentPackets = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.packetsSent ?? 0), 0); | ||
// this.totalSentBytes = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.bytesSent ?? 0), 0); | ||
// this.totalSentFrames = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.deltaSentPackets ?? 0), 0); | ||
// this.deltaEncodedFrames = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.deltaEncodedFrames ?? 0), 0); | ||
// this.deltaSentFrames = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.deltaSentFrames ?? 0), 0); | ||
// this.deltaLostPackets = [ ...this._stats.values() ].reduce((acc, stat) => acc + stat.deltaLostPackets, 0); | ||
// this.deltaSentPackets = [ ...this._stats.values() ].reduce((acc, stat) => acc + stat.deltaSentPackets, 0); | ||
// this.deltaSentBytes = [ ...this._stats.values() ].reduce((acc, stat) => acc + stat.deltaSentBytes, 0); | ||
// setting up sfu connection as it is not always available at the first sample | ||
this.visited = true; | ||
// a peer connection is active if it has at least one active track | ||
this.peerConnection.visited = true; | ||
this._updated = now; | ||
@@ -138,0 +128,0 @@ this.emit('update', { |
@@ -8,2 +8,3 @@ /// <reference types="node" /> | ||
import { ClientIssue } from './monitors/CallSummary'; | ||
import { SupportedVideoCodecType } from './common/types'; | ||
export type ObservedOutboundVideoTrackModel = { | ||
@@ -46,4 +47,6 @@ trackId: string; | ||
visited: boolean; | ||
added?: number; | ||
removed?: number; | ||
contentType: 'lowmotion' | 'standard' | 'highmotion'; | ||
codec: 'vp8' | 'vp9' | 'h264'; | ||
codec?: SupportedVideoCodecType; | ||
highestLayer?: ObservedOutboundVideoTrackStats; | ||
@@ -50,0 +53,0 @@ bitrate: number; |
@@ -14,3 +14,2 @@ "use strict"; | ||
this.contentType = 'standard'; | ||
this.codec = 'vp8'; | ||
this.bitrate = 0; | ||
@@ -139,2 +138,4 @@ this.totalLostPackets = 0; | ||
this.visited = true; | ||
// a peer connection is active if it has at least one active track | ||
this.peerConnection.visited = true; | ||
this._updated = now; | ||
@@ -141,0 +142,0 @@ this.emit('update', { |
@@ -45,2 +45,4 @@ /// <reference types="node" /> | ||
visited: boolean; | ||
opened?: number; | ||
closedTimestamp?: number; | ||
private _elapsedTimeSinceLastUpdate?; | ||
@@ -47,0 +49,0 @@ private _statsTimestamp?; |
@@ -54,6 +54,4 @@ /// <reference types="node" /> | ||
appData: T; | ||
started?: number; | ||
reportCallStarted?: boolean; | ||
reportCallEnded?: boolean; | ||
ended?: number; | ||
}): ObservedCall<T>; | ||
@@ -60,0 +58,0 @@ createObservedSfu<AppData extends Record<string, unknown> = Record<string, unknown>>(model: ObservedSfuModel, appData: AppData): ObservedSfu<AppData>; |
@@ -67,6 +67,7 @@ "use strict"; | ||
createObservedCall(config) { | ||
var _a; | ||
if (this._closed) { | ||
throw new Error('Attempted to create a call source on a closed observer'); | ||
} | ||
const { appData, started = Date.now(), ended, reportCallEnded = true, reportCallStarted = true } = config, model = __rest(config, ["appData", "started", "ended", "reportCallEnded", "reportCallStarted"]); | ||
const { appData, reportCallEnded = true, reportCallStarted = true } = config, model = __rest(config, ["appData", "reportCallEnded", "reportCallStarted"]); | ||
const call = new ObservedCall_1.ObservedCall(Object.assign(Object.assign({}, model), { serviceId: this.config.defaultServiceId }), this, appData); | ||
@@ -78,7 +79,8 @@ if (this._closed) | ||
call.once('close', () => { | ||
var _a; | ||
this._observedCalls.delete(call.callId); | ||
reportCallEnded && this.reports.addCallEventReport((0, callEventReports_1.createCallEndedEventReport)(call.serviceId, call.roomId, call.callId, ended !== null && ended !== void 0 ? ended : Date.now())); | ||
reportCallEnded && this.reports.addCallEventReport((0, callEventReports_1.createCallEndedEventReport)(call.serviceId, call.roomId, call.callId, (_a = call.ended) !== null && _a !== void 0 ? _a : Date.now())); | ||
}); | ||
this._observedCalls.set(call.callId, call); | ||
reportCallStarted && this.reports.addCallEventReport((0, callEventReports_1.createCallStartedEventReport)(call.serviceId, call.roomId, call.callId, started)); | ||
reportCallStarted && this.reports.addCallEventReport((0, callEventReports_1.createCallStartedEventReport)(call.serviceId, call.roomId, call.callId, (_a = call.started) !== null && _a !== void 0 ? _a : Date.now())); | ||
this.emit('newcall', call); | ||
@@ -85,0 +87,0 @@ return call; |
{ | ||
"name": "@observertc/observer-js", | ||
"version": "0.42.1", | ||
"version": "0.42.2", | ||
"description": "Server Side NodeJS Library for processing ObserveRTC Samples", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
@@ -85,3 +85,3 @@ import { ClientIssue } from '../monitors/CallSummary'; | ||
// let's assume vp8 for now | ||
const bppRange = BPP_RANGES[track.contentType][track.codec]; | ||
const bppRange = BPP_RANGES[track.contentType][track.codec ?? 'vp8']; | ||
@@ -88,0 +88,0 @@ if (bpp / 2 < bppRange.low) { |
@@ -23,2 +23,3 @@ import { | ||
export type MediaKind = 'audio' | 'video'; | ||
export type SupportedVideoCodecType = 'vp8' | 'vp9' | 'h264' | 'h265'; | ||
@@ -25,0 +26,0 @@ // export type EvaluatorMiddleware = Middleware<EvaluatorContext>; |
import { EventEmitter } from 'events'; | ||
import { ObservedClient, ObservedClientEvents, ObservedClientModel } from './ObservedClient'; | ||
import { ObservedClient, ObservedClientModel } from './ObservedClient'; | ||
import { Observer } from './Observer'; | ||
import { createClientJoinedEventReport } from './common/callEventReports'; | ||
import { createClientJoinedEventReport, createClientLeftEventReport } from './common/callEventReports'; | ||
import { getMedian, PartialBy } from './common/utils'; | ||
@@ -9,3 +9,2 @@ import { CallEventReport } from '@observertc/report-schemas-js'; | ||
import { ClientSample } from '@observertc/sample-schemas-js'; | ||
import { ClientIssue } from './monitors/CallSummary'; | ||
import { ObservedOutboundAudioTrack } from './ObservedOutboundAudioTrack'; | ||
@@ -15,4 +14,2 @@ import { ObservedOutboundVideoTrack } from './ObservedOutboundVideoTrack'; | ||
type ClientIssueDetectionConfig = Pick<ClientIssue, 'severity'>; | ||
export type ObservedCallModel = { | ||
@@ -54,3 +51,4 @@ serviceId: string; | ||
private _updated = Date.now(); | ||
private _ended?: number; | ||
public ended?: number; | ||
public started?: number = Date.now(); | ||
@@ -89,6 +87,2 @@ public constructor( | ||
public get ended() { | ||
return this._ended; | ||
} | ||
public get clients(): ReadonlyMap<string, ObservedClient> { | ||
@@ -134,4 +128,9 @@ return this._clients; | ||
this._closed = true; | ||
this._ended = timestamp ?? Date.now(); | ||
if (timestamp) { | ||
this.ended = timestamp; | ||
} else if (!this.ended) { | ||
this.ended = Date.now(); | ||
} | ||
Array.from(this._clients.values()).forEach((client) => client.close()); | ||
@@ -154,7 +153,8 @@ | ||
appData: ClientAppData, | ||
generateClientJoinedReport?: boolean, | ||
joined?: number, | ||
detectIssues?: { | ||
rejoin: ClientIssue['severity'] | ClientIssueDetectionConfig, | ||
}, | ||
// in case we generate the CLIENT_JOINED report we can set the timestamp of the event | ||
// in that case we should set the client.left to the timestamp of the leaving. | ||
// by default the timestamp is set to the current time | ||
joinedTimestamp?: number, | ||
createClientJoinedReport?: boolean, | ||
createClientLeftReport?: boolean, | ||
}) { | ||
@@ -165,7 +165,5 @@ if (this._closed) throw new Error(`Call ${this.callId} is closed`); | ||
appData, | ||
generateClientJoinedReport, | ||
joined = Date.now(), | ||
detectIssues = { | ||
rejoin: 'minor' | ||
}, | ||
createClientJoinedReport, | ||
createClientLeftReport, | ||
joinedTimestamp = Date.now(), | ||
...model | ||
@@ -190,3 +188,3 @@ } = config; | ||
if (generateClientJoinedReport) { | ||
if (createClientJoinedReport) { | ||
this.reports.addCallEventReport(createClientJoinedEventReport( | ||
@@ -198,3 +196,3 @@ this.serviceId, | ||
result.clientId, | ||
joined, | ||
result.joined ?? joinedTimestamp, | ||
result.userId, | ||
@@ -205,21 +203,17 @@ result.marker, | ||
if (detectIssues?.rejoin) { | ||
const issueBaseConfig = typeof detectIssues.rejoin === 'object' ? detectIssues.rejoin : { | ||
severity: detectIssues.rejoin | ||
}; | ||
const rejoinedClientIssueListener = (event: ObservedClientEvents['rejoined'][0]) => { | ||
event.lastJoined; | ||
result.addIssue({ | ||
timestamp: Date.now(), | ||
...issueBaseConfig, | ||
}); | ||
}; | ||
result.once('close', () => { | ||
if (createClientLeftReport) { | ||
this.reports.addCallEventReport(createClientLeftEventReport( | ||
this.serviceId, | ||
result.mediaUnitId, | ||
this.roomId, | ||
this.callId, | ||
result.clientId, | ||
result.left ?? Date.now(), | ||
result.userId, | ||
result.marker, | ||
)); | ||
} | ||
}); | ||
result.once('close', () => { | ||
result.off('rejoined', rejoinedClientIssueListener); | ||
}); | ||
result.on('rejoined', rejoinedClientIssueListener); | ||
} | ||
this.emit('newclient', result); | ||
@@ -226,0 +220,0 @@ |
@@ -1,2 +0,2 @@ | ||
import { Browser, ClientSample, Engine, IceLocalCandidate, IceRemoteCandidate, OperationSystem, Platform } from '@observertc/sample-schemas-js'; | ||
import { Browser, ClientSample, Engine, IceLocalCandidate, IceRemoteCandidate, MediaCodecStats, MediaDevice, OperationSystem, Platform } from '@observertc/sample-schemas-js'; | ||
import { ObservedCall } from './ObservedCall'; | ||
@@ -9,3 +9,2 @@ import { EventEmitter } from 'events'; | ||
import { PartialBy, isValidUuid } from './common/utils'; | ||
import { createClientLeftEventReport } from './common/callEventReports'; | ||
import { CallEventType } from './common/CallEventType'; | ||
@@ -75,2 +74,15 @@ import { ObservedSfu } from './ObservedSfu'; | ||
type PendingPeerConnectionTimestamp = { | ||
type: 'opened' | 'closed' | ||
peerConnectionId: string; | ||
timestamp: number; | ||
} | ||
type PendingMediaTrackTimestamp = { | ||
type: 'added' | 'removed' | ||
peerConnectionId: string; | ||
mediaTrackId: string; | ||
timestamp: number; | ||
} | ||
export class ObservedClient<AppData extends Record<string, unknown> = Record<string, unknown>> extends EventEmitter { | ||
@@ -86,6 +98,8 @@ | ||
private _joinedEventAt?: number; | ||
private _acceptedSamples = 0; | ||
private _timeZoneOffsetInHours?: number; | ||
private _left?: number; | ||
// the timestamp of the CLIENT_JOINED event | ||
public joined?: number; | ||
public left?: number; | ||
@@ -125,9 +139,9 @@ public score?: CalculatedScore; | ||
public readonly mediaDevices: string[] = []; | ||
public readonly codecs: string[] = []; | ||
public readonly mediaDevices: MediaDevice[] = []; | ||
public readonly mediaCodecs: MediaCodecStats[] = []; | ||
public readonly userMediaErrors: string[] = []; | ||
public readonly issues: ClientIssue[] = []; | ||
public readonly ωpendingCreatedTracksTimestamp = new Map<string, number>(); | ||
public readonly ωpendingCreatedPeerConnectionTimestamp = new Map<string, number>(); | ||
public ωpendingPeerConnectionTimestamps: PendingPeerConnectionTimestamp[] = []; | ||
public ωpendingMediaTrackTimestamps: PendingMediaTrackTimestamp[] = []; | ||
@@ -246,5 +260,2 @@ public constructor( | ||
if (!this._left) { | ||
this._addClientLeftReport(); | ||
} | ||
Array.from(this._peerConnections.values()).forEach((peerConnection) => peerConnection.close()); | ||
@@ -501,15 +512,54 @@ | ||
case CallEventType.CLIENT_JOINED: { | ||
const lastJoined = this._joinedEventAt; | ||
const lastJoined = this.joined; | ||
this._joinedEventAt = timestamp ?? sample.timestamp; | ||
this.joined = timestamp; | ||
if (lastJoined && this._joinedEventAt && lastJoined !== this._joinedEventAt) { | ||
// if it is joined before and it is joined again | ||
if (lastJoined && this.joined && lastJoined !== this.joined) { | ||
this.emit('rejoined', { lastJoined }); | ||
} | ||
// in case we have a left event before the joined event | ||
if (this.left && this.joined && this.left < this.joined) { | ||
this.left = undefined; | ||
} | ||
break; | ||
} | ||
case CallEventType.CLIENT_LEFT: { | ||
this._left = timestamp; | ||
this.left = timestamp; | ||
break; | ||
} | ||
case CallEventType.PEER_CONNECTION_OPENED: { | ||
callEvent.peerConnectionId && this.ωpendingPeerConnectionTimestamps.push({ | ||
type: 'opened', | ||
peerConnectionId: callEvent.peerConnectionId, | ||
timestamp: timestamp ?? sample.timestamp, | ||
}); | ||
break; | ||
} | ||
case CallEventType.PEER_CONNECTION_CLOSED: { | ||
callEvent.peerConnectionId && this.ωpendingPeerConnectionTimestamps.push({ | ||
type: 'closed', | ||
peerConnectionId: callEvent.peerConnectionId, | ||
timestamp: timestamp ?? sample.timestamp, | ||
}); | ||
break; | ||
} | ||
case CallEventType.MEDIA_TRACK_ADDED: { | ||
callEvent.peerConnectionId && callEvent.mediaTrackId && this.ωpendingMediaTrackTimestamps.push({ | ||
type: 'added', | ||
peerConnectionId: callEvent.peerConnectionId, | ||
mediaTrackId: callEvent.mediaTrackId, | ||
timestamp: timestamp ?? sample.timestamp, | ||
}); | ||
break; | ||
} | ||
case CallEventType.MEDIA_TRACK_REMOVED: { | ||
callEvent.peerConnectionId && callEvent.mediaTrackId && this.ωpendingMediaTrackTimestamps.push({ | ||
type: 'removed', | ||
peerConnectionId: callEvent.peerConnectionId, | ||
mediaTrackId: callEvent.mediaTrackId, | ||
timestamp: timestamp ?? sample.timestamp, | ||
}); | ||
break; | ||
} | ||
case CallEventType.CLIENT_ISSUE: { | ||
@@ -603,3 +653,5 @@ const severity = callEvent.value ? callEvent.value as ClientIssue['severity'] : 'minor'; | ||
this.reports.addCallMetaReport(callMetaReport); | ||
codec.mimeType && this.codecs.push(codec.mimeType); | ||
if (codec.mimeType && !this.mediaCodecs.find((c) => c.mimeType === codec.mimeType)) { | ||
this.mediaCodecs.push(codec); | ||
} | ||
} | ||
@@ -631,5 +683,7 @@ | ||
}, this.userId); | ||
this.reports.addCallMetaReport(callMetaReport); | ||
mediaDevice.label && this.mediaDevices.push(mediaDevice.label); | ||
if (mediaDevice.id && !this.mediaDevices.find((d) => d.id === mediaDevice.id)) { | ||
this.mediaDevices.push(mediaDevice); | ||
} | ||
} | ||
@@ -862,2 +916,44 @@ | ||
// try to set the timestamps of the peer connections | ||
if (0 < this.ωpendingPeerConnectionTimestamps.length) { | ||
const newPendingTimestamps: PendingPeerConnectionTimestamp[] = []; | ||
for (const pendingTimestamp of this.ωpendingPeerConnectionTimestamps) { | ||
const peerConnection = this._peerConnections.get(pendingTimestamp.peerConnectionId); | ||
if (!peerConnection) { | ||
newPendingTimestamps.push(pendingTimestamp); | ||
continue; | ||
} | ||
if (pendingTimestamp.type === 'opened') peerConnection.opened = pendingTimestamp.timestamp; | ||
else if (pendingTimestamp.type === 'closed') peerConnection.closedTimestamp = pendingTimestamp.timestamp; | ||
} | ||
this.ωpendingPeerConnectionTimestamps = newPendingTimestamps; | ||
} | ||
// try to set the timestamps of the media tracks | ||
if (0 < this.ωpendingMediaTrackTimestamps.length) { | ||
const newPendingTimestamps: PendingMediaTrackTimestamp[] = []; | ||
for (const pendingTimestamp of this.ωpendingMediaTrackTimestamps) { | ||
const peerConnection = this._peerConnections.get(pendingTimestamp.peerConnectionId); | ||
const mediaTrack = peerConnection?.inboundAudioTracks.get(pendingTimestamp.mediaTrackId) ?? | ||
peerConnection?.inboundVideoTracks.get(pendingTimestamp.mediaTrackId) ?? | ||
peerConnection?.outboundAudioTracks.get(pendingTimestamp.mediaTrackId) ?? | ||
peerConnection?.outboundVideoTracks.get(pendingTimestamp.mediaTrackId); | ||
if (!mediaTrack) { | ||
newPendingTimestamps.push(pendingTimestamp); | ||
continue; | ||
} | ||
if (pendingTimestamp.type === 'added') mediaTrack.added = pendingTimestamp.timestamp; | ||
else if (pendingTimestamp.type === 'removed') mediaTrack.removed = pendingTimestamp.timestamp; | ||
} | ||
this.ωpendingMediaTrackTimestamps = newPendingTimestamps; | ||
} | ||
// close resources that are not visited | ||
for (const peerConnection of this._peerConnections.values()) { | ||
@@ -1050,18 +1146,2 @@ if (!peerConnection.visited) { | ||
} | ||
private _addClientLeftReport() { | ||
if (this._left) return; | ||
this._left = Date.now(); | ||
this.reports.addCallEventReport(createClientLeftEventReport( | ||
this.serviceId, | ||
this.mediaUnitId, | ||
this.roomId, | ||
this.callId, | ||
this.clientId, | ||
this._left, | ||
this.userId, | ||
this.marker, | ||
)); | ||
} | ||
} |
@@ -57,2 +57,7 @@ import { EventEmitter } from 'events'; | ||
// timestamp of the MEDIA_TRACK_ADDED event | ||
public added?: number; | ||
// timestamp of the MEDIA_TRACK_REMOVED event | ||
public removed?: number; | ||
private readonly _stats = new Map<number, ObservedInboundAudioTrackStats>(); | ||
@@ -261,2 +266,5 @@ | ||
this.visited = true; | ||
// a peer connection is active if it has at least one active track | ||
this.peerConnection.visited = true; | ||
this._updated = now; | ||
@@ -263,0 +271,0 @@ |
@@ -8,2 +8,3 @@ import { EventEmitter } from 'events'; | ||
import { ObservedOutboundVideoTrack } from './ObservedOutboundVideoTrack'; | ||
import { SupportedVideoCodecType } from './common/types'; | ||
@@ -58,4 +59,9 @@ export type ObservedInboundVideoTrackModel = { | ||
// timestamp of the MEDIA_TRACK_ADDED event | ||
public added?: number; | ||
// timestamp of the MEDIA_TRACK_REMOVED event | ||
public removed?: number; | ||
public contentType: 'lowmotion' | 'standard' | 'highmotion' = 'standard'; | ||
public codec: 'vp8' | 'vp9' | 'h264' = 'vp8'; | ||
public codec?: SupportedVideoCodecType; | ||
@@ -272,2 +278,5 @@ private readonly _stats = new Map<number, ObservedInboundVideoTrackStats>(); | ||
this.visited = true; | ||
// a peer connection is active if it has at least one active track | ||
this.peerConnection.visited = true; | ||
this._updated = now; | ||
@@ -274,0 +283,0 @@ this.emit('update', { |
@@ -53,2 +53,7 @@ import { EventEmitter } from 'events'; | ||
// timestamp of the MEDIA_TRACK_ADDED event | ||
public added?: number; | ||
// timestamp of the MEDIA_TRACK_REMOVED event | ||
public removed?: number; | ||
public bitrate = 0; | ||
@@ -226,19 +231,6 @@ public rttInMs?: number; | ||
// this.bitrate = [ ...this._stats.values() ].reduce((acc, stat) => acc + stat.bitrate, 0); | ||
// this.rttInMs = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.rttInMs ?? 0), 0) / (this._stats.size || 1); | ||
// this.totalLostPackets = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.packetsLost ?? 0), 0); | ||
// this.totalSentPackets = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.packetsSent ?? 0), 0); | ||
// this.totalSentBytes = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.bytesSent ?? 0), 0); | ||
// this.totalSentFrames = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.deltaSentPackets ?? 0), 0); | ||
// this.deltaEncodedFrames = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.deltaEncodedFrames ?? 0), 0); | ||
// this.deltaSentFrames = [ ...this._stats.values() ].reduce((acc, stat) => acc + (stat.deltaSentFrames ?? 0), 0); | ||
// this.deltaLostPackets = [ ...this._stats.values() ].reduce((acc, stat) => acc + stat.deltaLostPackets, 0); | ||
// this.deltaSentPackets = [ ...this._stats.values() ].reduce((acc, stat) => acc + stat.deltaSentPackets, 0); | ||
// this.deltaSentBytes = [ ...this._stats.values() ].reduce((acc, stat) => acc + stat.deltaSentBytes, 0); | ||
// setting up sfu connection as it is not always available at the first sample | ||
this.visited = true; | ||
// a peer connection is active if it has at least one active track | ||
this.peerConnection.visited = true; | ||
this._updated = now; | ||
@@ -245,0 +237,0 @@ this.emit('update', { |
@@ -8,2 +8,3 @@ import { EventEmitter } from 'events'; | ||
import { ClientIssue } from './monitors/CallSummary'; | ||
import { SupportedVideoCodecType } from './common/types'; | ||
@@ -54,4 +55,9 @@ export type ObservedOutboundVideoTrackModel = { | ||
// timestamp of the MEDIA_TRACK_ADDED event | ||
public added?: number; | ||
// timestamp of the MEDIA_TRACK_REMOVED event | ||
public removed?: number; | ||
public contentType: 'lowmotion' | 'standard' | 'highmotion' = 'standard'; | ||
public codec: 'vp8' | 'vp9' | 'h264' = 'vp8'; | ||
public codec?: SupportedVideoCodecType; | ||
public highestLayer?: ObservedOutboundVideoTrackStats; | ||
@@ -246,2 +252,5 @@ | ||
this.visited = true; | ||
// a peer connection is active if it has at least one active track | ||
this.peerConnection.visited = true; | ||
this._updated = now; | ||
@@ -248,0 +257,0 @@ this.emit('update', { |
@@ -45,3 +45,8 @@ import { EventEmitter } from 'events'; | ||
public visited = true; | ||
// timestamp of the PEER_CONNECTION_OPENED event | ||
public opened?: number; | ||
// timestamp of the PEER_CONNECTION_CLOSED event | ||
public closedTimestamp?: number; | ||
private _elapsedTimeSinceLastUpdate?: number; | ||
@@ -48,0 +53,0 @@ private _statsTimestamp?: number; |
@@ -107,3 +107,3 @@ import { createLogger } from './common/logger'; | ||
public createObservedCall<T extends Record<string, unknown> = Record<string, unknown>>( | ||
config: PartialBy<ObservedCallModel, 'serviceId'> & { appData: T, started?: number, reportCallStarted?: boolean, reportCallEnded?: boolean, ended?: number } | ||
config: PartialBy<ObservedCallModel, 'serviceId'> & { appData: T, reportCallStarted?: boolean, reportCallEnded?: boolean } | ||
): ObservedCall<T> { | ||
@@ -116,4 +116,2 @@ if (this._closed) { | ||
appData, | ||
started = Date.now(), | ||
ended, | ||
reportCallEnded = true, | ||
@@ -137,3 +135,3 @@ reportCallStarted = true, | ||
call.callId, | ||
ended ?? Date.now(), | ||
call.ended ?? Date.now(), | ||
)); | ||
@@ -147,3 +145,3 @@ }); | ||
call.callId, | ||
started, | ||
call.started ?? Date.now(), | ||
)); | ||
@@ -150,0 +148,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
576683
12153
145