@observertc/observer-js 1.3.0-rc.3 → 1.3.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -404,7 +404,7 @@ See [Mediasoup router observation](#mediasoup-router-observation) for the full d
404
404
  | Event | Extra | Fires when |
405
405
  |-------|-------|-----------|
406
406
  | `client-added` | — | a client is created |
407
- | `client-sink-created` | `{ sink: Sink<ClientSample> }` | the client's sample sink was created by `createClientSink`; fires right after `client-added` |
407
+ | `client-sink-created` | `{ sink: Sink<ClientSample> }` | the client's sample sink was created through `saveClientSamples`; fires right after `client-added` |
408
408
  | `client-updated` | `{ sample: ClientSample, elapsedTimeInMs: number, context?: AcceptContext }` | the client processed a sample |
409
409
  | `client-closed` | — | the client closed |
410
410
  | `client-joined` | — | first `CLIENT_JOINED` event seen |
@@ -480,7 +480,10 @@ type ObserverConfig<AppData = Record<string, unknown>> = {
480
480
  createCallAppData?: (p: { callId: string; observer: Observer; acceptCtx?: AcceptContext }) => Record<string, unknown>;
481
481
  createClientAppData?: (p: { clientId: string; observedCall: ObservedCall; acceptCtx?: AcceptContext }) => Record<string, unknown>;
482
482
  // a JSONL file per client: return its path, or undefined for none (see Sinks).
483
- createClientSink?: (p: { id: string; clientId: string; observedCall: ObservedCall }) => SinkDestination<ClientSample> | undefined;
483
+ // where each client's samples are saved: exactly one of jsonlPath / sink, each a per-client callback (see Sinks).
484
+ saveClientSamples?:
485
+ | { jsonlPath: (p: { id: string; clientId: string; observedCall: ObservedCall }) => string | undefined }
486
+ | { sink: (p: { id: string; clientId: string; observedCall: ObservedCall }) => Sink<ClientSample> | undefined };
484
487
  // remote-track-resolver factory — produces a call's RemoteTrackResolver (see Remote track resolution).
485
488
  createRemoteTrackResolver?: (observedCall: ObservedCall) => RemoteTrackResolver | undefined;
486
489
  };
@@ -1947,7 +1950,7 @@ and never crashes the process.
1947
1950
  import type { Sink, SinkSettings } from '@observertc/observer-js';
1948
1951
 
1949
1952
  interface Sink<T = unknown> { // typically: class X extends EventEmitter implements Sink<T>
1950
- readonly id: string;
1953
+ readonly id: string; // unique per observer
1951
1954
  write(record: T): boolean; // `false` once ended (nothing written), or on backpressure
1952
1955
  end(): void; // idempotent; MUST emit `ended` synchronously, then flush and emit `close`
1953
1956
  on / once / off(event, listener); // the events below
@@ -1977,18 +1980,20 @@ its id — refuse writes after it, and emit `close` / `error` for its destinatio
1977
1980
  ### Client samples
1978
1981
 
1979
1982
  A client writes every accepted sample to the sink registered under its **client id** in its call's
1980
- registry (`observedClient.sink` returns it), and ends that sink when it closes. `createClientSink`
1981
- decides it, per client, by returning a **`SinkDestination`** — exactly one of `jsonlPath` and `sink`:
1983
+ registry (`observedClient.sink` returns it), and ends that sink when it closes. The
1984
+ **`saveClientSamples`** config option decides it — exactly one of two per-client callbacks, the same
1985
+ `jsonlPath` / `sink` pair as `createExtensionStats` takes:
1982
1986
 
1983
- | Returns | Sink |
1984
- |---------|------|
1985
- | `{ jsonlPath }` | a JSONL file at that path: a `JsonlFileSink` created by the library under the client id |
1986
- | `{ sink }` | your `Sink<ClientSample>`; its `id` must be `params.id`, the client id |
1987
- | `undefined` | none |
1987
+ | `saveClientSamples` | Called per client with `{ id, observedCall, clientId }`, returns | Sink |
1988
+ |---------------------|------------------------------------------------------------------|------|
1989
+ | `{ jsonlPath }` | the path of the client's file, or `undefined` | a `JsonlFileSink` at that path, created by the library under the client id |
1990
+ | `{ sink }` | your `Sink<ClientSample>`, or `undefined` | that sink; its `id` must be `id` |
1988
1991
 
1989
1992
  ```ts
1990
1993
  const observer = new Observer({
1991
- createClientSink: ({ observedCall, clientId }) => ({ jsonlPath: `./stats/${observedCall.callId}__${clientId}.jsonl` }),
1994
+ saveClientSamples: {
1995
+ jsonlPath: ({ observedCall, clientId }) => `./stats/${observedCall.callId}__${clientId}.jsonl`,
1996
+ },
1992
1997
  });
1993
1998
 
1994
1999
  observer.on('client-sink-created', ({ observedClient, sink }) => {
@@ -1996,10 +2001,15 @@ observer.on('client-sink-created', ({ observedClient, sink }) => {
1996
2001
  });
1997
2002
  ```
1998
2003
 
1999
- `createClientSink` is called only when the client id is free as a sink id, so a clash opens no file.
2000
- A returned sink with a different id is ended and not used. Giving both `jsonlPath` and `sink` is a
2001
- compile error (and refused at runtime). For a JSONL file with a custom serializer, return
2002
- `{ sink: new JsonlFileSink({ id, path, serializeSample }) }`.
2004
+ **A client's sample sink id is its client id.** The `id` the callbacks receive is that id, and
2005
+ with `jsonlPath` the library uses it for you. Sink ids are unique per observer, so **client ids
2006
+ must be unique across the observer's open calls** — client monitors generate uuids, so this
2007
+ normally holds. A client whose id is already a sink id gets no sink, and a warning is logged.
2008
+
2009
+ The callback is called only when the id is free, so a clash opens no file; `undefined` gives that
2010
+ client no sink. A returned sink with a different id is ended and not used. Giving both `jsonlPath`
2011
+ and `sink` is a compile error (and refused at runtime). For a JSONL file with a custom serializer,
2012
+ use `sink: ({ id }) => new JsonlFileSink({ id, path, serializeSample })`.
2003
2013
 
2004
2014
  Any other destination works the same way — return your own implementation:
2005
2015
 
@@ -2031,7 +2041,9 @@ class HttpSink extends EventEmitter implements ClientSampleSink {
2031
2041
  }
2032
2042
 
2033
2043
  new Observer({
2034
- createClientSink: ({ id, clientId }) => ({ sink: new HttpSink({ id, url: `https://stats.example.com/${clientId}` }) }),
2044
+ saveClientSamples: {
2045
+ sink: ({ id, clientId }) => new HttpSink({ id, url: `https://stats.example.com/${clientId}` }), // id: the client id
2046
+ },
2035
2047
  });
2036
2048
 
2037
2049
  // Or later, on `client-added`, if the client has no sink yet:
package/dist/index.d.mts CHANGED
@@ -59,6 +59,12 @@ type SinkDestination<T = unknown> = {
59
59
  sink: Sink<T>;
60
60
  jsonlPath?: never;
61
61
  };
62
+ /** What the `saveClientSamples` callbacks receive: `id` is the client's sample sink id, the client id. */
63
+ type ClientSinkParams<Call = unknown> = {
64
+ id: string;
65
+ observedCall: Call;
66
+ clientId: string;
67
+ };
62
68
 
63
69
  /** A sink class `SinkRegistry.create` can construct: its constructor takes one settings object (with `id`). */
64
70
  type SinkConstructor = new (settings: any) => Sink<any>;
@@ -2190,7 +2196,6 @@ declare class ObservedOutboundTrack implements OutboundTrackSample {
2190
2196
  readonly id: string;
2191
2197
  readonly kind: MediaKind;
2192
2198
  private readonly _peerConnection;
2193
- private readonly _outboundRtps?;
2194
2199
  private readonly _mediaSource?;
2195
2200
  private _visited;
2196
2201
  appData?: Record<string, unknown>;
@@ -2222,11 +2227,12 @@ declare class ObservedOutboundTrack implements OutboundTrackSample {
2222
2227
  * latest tick. Needs a `RemoteTrackResolver`: without links there are no receivers to ask.
2223
2228
  */
2224
2229
  receivedRmsAudioLevel?: number;
2225
- constructor(timestamp: number, id: string, kind: MediaKind, _peerConnection: ObservedPeerConnection, _outboundRtps?: ObservedOutboundRtp[] | undefined, _mediaSource?: ObservedMediaSource | undefined);
2230
+ constructor(timestamp: number, id: string, kind: MediaKind, _peerConnection: ObservedPeerConnection, _mediaSource?: ObservedMediaSource | undefined);
2226
2231
  get score(): number | undefined;
2227
2232
  get visited(): boolean;
2228
2233
  get degraded(): boolean;
2229
2234
  getPeerConnection(): ObservedPeerConnection;
2235
+ /** The outbound RTPs of this track's media source, looked up on every call. */
2230
2236
  getOutboundRtps(): ObservedOutboundRtp[] | undefined;
2231
2237
  getMediaSource(): ObservedMediaSource | undefined;
2232
2238
  /** The capture device behind this track, when the client sent `AUDIO_INPUT_DEVICE` metadata for it. */
@@ -2689,6 +2695,7 @@ declare class ObservedPeerConnection extends EventEmitter {
2689
2695
  private _updateMediaSourceStats;
2690
2696
  private _updateOutboundRtpStats;
2691
2697
  private _updateOutboundTrackSample;
2698
+ private _removeOutboundTrack;
2692
2699
  private _updatePeerConnectionTransportStats;
2693
2700
  private _updateRemoteInboundRtpStats;
2694
2701
  private _updateRemoteOutboundRtpStats;
@@ -4004,7 +4011,7 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
4004
4011
  /** The distinct `publicAddress`es currently known by this client's open peer connections. */
4005
4012
  /**
4006
4013
  * The sink this client's samples are written to: whatever is registered under its client id in
4007
- * `call.sinks` (the one `createClientSink` returned, or a sink the application added), if any.
4014
+ * `call.sinks` (the one created through `saveClientSamples`, or a sink the application added), if any.
4008
4015
  */
4009
4016
  get sink(): Sink<ClientSample> | undefined;
4010
4017
  get publicAddresses(): string[];
@@ -4090,6 +4097,7 @@ declare class RemoteTrackResolver {
4090
4097
  readonly observedCall: ObservedCall;
4091
4098
  private readonly resolvers;
4092
4099
  private readonly _publisherIdToOutboundTrack;
4100
+ private readonly _outboundTrackPublisherIds;
4093
4101
  private readonly _subscriberIdToInboundTrack;
4094
4102
  /**
4095
4103
  * Tracks whose publisher id the strategy could not resolve **yet**.
@@ -5883,8 +5891,8 @@ declare class ObservedCall<AppData extends Record<string, unknown> = Record<stri
5883
5891
  private _onClientUpdate;
5884
5892
  private _clientJoined;
5885
5893
  private _clientLeft;
5886
- /** Emit an Observer-bus event scoped to this call. */
5887
5894
  private _createClientSink;
5895
+ /** Emit an Observer-bus event scoped to this call. */
5888
5896
  private _notify;
5889
5897
  }
5890
5898
 
@@ -6445,6 +6453,14 @@ type ClientAppDataFactory = (params: {
6445
6453
  observedCall: ObservedCall;
6446
6454
  acceptCtx?: AcceptContext;
6447
6455
  }) => Record<string, unknown>;
6456
+ /** `ObserverConfig.saveClientSamples`: exactly one of the two per-client callbacks. */
6457
+ type SaveClientSamplesConfig = {
6458
+ jsonlPath: (params: ClientSinkParams<ObservedCall>) => string | undefined;
6459
+ sink?: never;
6460
+ } | {
6461
+ sink: (params: ClientSinkParams<ObservedCall>) => Sink<ClientSample> | undefined;
6462
+ jsonlPath?: never;
6463
+ };
6448
6464
  type ObserverConfig<AppData extends Record<string, unknown> = Record<string, unknown>> = {
6449
6465
  /**
6450
6466
  * Application-owned data for the observer itself. Never read or modified by the library.
@@ -6598,22 +6614,15 @@ type ObserverConfig<AppData extends Record<string, unknown> = Record<string, unk
6598
6614
  /** Same as `createCallAppData`, for clients. Receives the (already-created) parent call. */
6599
6615
  createClientAppData?: ClientAppDataFactory;
6600
6616
  /**
6601
- * The sink a newly-created client writes its samples to: `{ jsonlPath }` (a `JsonlFileSink` is
6602
- * created at that path under the client id), `{ sink }` (a `Sink` whose `id` is `params.id`, the
6603
- * client id), or `undefined` for none. The sink is added to `observedCall.sinks` and is ended
6604
- * when the client closes; `client-sink-created` announces it.
6605
- *
6606
- * ```ts
6607
- * createClientSink: ({ observedCall, clientId }) => ({ jsonlPath: `./stats/${observedCall.callId}__${clientId}.jsonl` }),
6608
- * ```
6617
+ * Where each client's samples are saved: exactly one of `jsonlPath` (return the path of the
6618
+ * client's JSONL file) and `sink` (return a sink whose `id` is `params.id`), each called per
6619
+ * client; `undefined` gives that client no sink. See the README's *Sinks* section.
6609
6620
  *
6610
- * Called only when the client id is free as a sink id, so a clash opens no file.
6621
+ * A client's sample sink id is its client id (`params.id`). Sink ids are unique per observer, so
6622
+ * client ids must be unique across the observer's open calls; a client whose id is already a sink
6623
+ * id gets no sink, and a warning is logged.
6611
6624
  */
6612
- createClientSink?: (params: {
6613
- id: string;
6614
- observedCall: ObservedCall;
6615
- clientId: string;
6616
- }) => SinkDestination<ClientSample> | undefined;
6625
+ saveClientSamples?: SaveClientSamplesConfig;
6617
6626
  /**
6618
6627
  * Optional factory invoked when a call is created, producing the call's `RemoteTrackResolver`
6619
6628
  * (or `undefined` for none). Use the built-ins
@@ -7183,7 +7192,7 @@ declare function setObserverLogger(logger: ObserverLogger): void;
7183
7192
  /** The lifecycle events a client sample sink may emit. Same as {@link SinkEvents}. */
7184
7193
  type ClientSampleSinkEvents = SinkEvents;
7185
7194
  /**
7186
- * A per-client destination for accepted samples. Return one from `ObserverConfig.createClientSink`
7195
+ * A per-client destination for accepted samples. Return one from `ObserverConfig.saveClientSamples.sink`
7187
7196
  * (with the `id` it receives), or create one under the client id with
7188
7197
  * `observedCall.sinks.create(MySink, { id: clientId, ... })`.
7189
7198
  *
@@ -7253,4 +7262,4 @@ declare function createInMemorySink(samples?: ClientSample[]): InMemorySink;
7253
7262
  declare function createDefaultMediasoupRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
7254
7263
  declare function createP2pRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
7255
7264
 
7256
- export { type AcceptContext, type AcceptMiddleware, type AcceptMiddlewarePayload, type ActiveClientIssue, type ActiveIssueTracker, ActiveIssuesRegistry, AudioEchoDetector, type AudioEchoDetectorConfig, AudioEchoTypes, type AudioInputDevice, type AvailableCallScopeDetectorsConfigs, type AvailableDetectorsConfigs, type AvailableObserverScopeDetectorsConfigs, type AvailableValidatorConfigs, CODEC_MISMATCH_ISSUE, type CalculatedScore, type CallAppDataFactory, CallConcurrentIssueDetector, type CallConcurrentIssueDetectorConfig, type CallConcurrentIssueGroup, CallConcurrentIssueTypes, type CallHealth, CallHealthAggregator, type CallIssue, type CallIssueSpread, type CallScopedEventName, type CallSummary, type CallSummaryClients, CallSummaryCollector, type CallSummaryConfig, type CallSummaryEnricher, type CallSummaryEnrichers, type CallSummaryScores, type CallSummarySection, type CallSummaryTruncation, type CallSummaryTurnServers, type CertificateStats, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientHealth, type ClientHealthThresholds, type ClientIssue, type ClientLocation, type ClientLocationResolver, type ClientMetaData, ClientMetaTypes, type ClientPopulation, type ClientPopulationAxis, ClientPopulationIssueDetector, type ClientPopulationIssueDetectorConfig, ClientPopulationIssueTypes, type ClientSample, type ClientSampleSink, type ClientSampleSinkEvents, type CodecConsistencyReportPayload, CodecConsistencyValidator, type CodecConsistencyValidatorConfig, type CodecEvidence, type CodecStats, type CorroboratedPublisherFault, type CreateExtensionStatsSettings, type DataChannelStats, type Detector, Detectors, type ExtensionStat, ExtensionStatsRegistry, GEOHASH_CELL_SIZES, type IceCandidatePairStats, type IceCandidateStats, type IceTransportStats, InMemorySink, type InboundRtpStats, type InboundTrackSample, type Issue, type IssueBase, type IssueConclusion, IssueFanOutDetector, type IssueFanOutDetectorConfig, IssueFanOutTypes, type IssueFaultDomain, JsonlFileSink, type JsonlFileSinkOptions, LOWEST_COMMON_DENOMINATOR_ISSUE, type Logger, type MannKendallResult, type MediaKind, type MediaPlayoutStats, type MediaSourceStats, type MediasoupConsumerSample, type MediasoupConsumerSampleEvent, type MediasoupDataConsumerSample, type MediasoupDataProducerSample, type MediasoupDirectTransportSample, type MediasoupDirectTransportSampleEventMap, type MediasoupPipeTransportSample, type MediasoupPipeTransportSampleEventMap, type MediasoupPlainTransportSample, type MediasoupPlainTransportSampleEventMap, type MediasoupProducerSample, type MediasoupProducerSampleEvent, type MediasoupRouterSample, type MediasoupSampleEnricher, type MediasoupTransportSample, type MediasoupWebRtcTransportSample, type MediasoupWebRtcTransportSampleEventMap, type Middleware, MiddlewareProcessor, ObservedCall, type ObservedCallScope, type ObservedCallSettings, ObservedCertificate, ObservedClient, ObservedClientIssueRegistry, type ObservedClientScope, type ObservedClientSettings, ObservedCodec, ObservedDataChannel, ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedInboundRtp, ObservedInboundTrack, ObservedMediaPlayout, ObservedMediaSource, ObservedMediasoupRouter, type ObservedMediasoupRouterEvents, type ObservedMediasoupRouterScope, type ObservedMediasoupRouterSettings, ObservedOutboundRtp, ObservedOutboundTrack, ObservedPeerConnection, type ObservedPeerConnectionScope, ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp, ObservedTURN, type ObservedTURNEventMap, ObservedTurnServer, Observer, ObserverConcurrentIssueDetector, type ObserverConcurrentIssueDetectorConfig, type ObserverConcurrentIssueGroup, ObserverConcurrentIssueTypes, type ObserverEventBase, type ObserverEvents, type ObserverIssue, type ObserverIssueSpread, type ObserverLogger, type OutboundRtpStats, type OutboundTrackSample, type PageHinkleyResult, type PeerConnectionSample, type PeerConnectionTransportStats, type PsnrSum, PublisherFaultCorroborationDetector, type PublisherFaultCorroborationDetectorConfig, PublisherFaultTypes, type QualityLimitationDurations, RESOLVED_ISSUE_SUFFIX, RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS, RTT_HISTOGRAM_NUMBER_OF_BUCKETS, type RemoteInboundRtpStats, type RemoteOutboundRtpStats, type RemoteTrackLinkEvidence, RemoteTrackResolver, type RemoteTrackResolverFactory, type RemoteTrackResolverReportPayload, RemoteTrackResolverValidator, type RemoteTrackResolverValidatorConfig, type RemoteTrackResolvers, type ResolvedActiveClientIssue, type RttSummary, type RunningValidator, SCORE_HISTOGRAM_BUCKET_WIDTH, SCORE_HISTOGRAM_NUMBER_OF_BUCKETS, type SampleRejectedReason, type ScoreCalculator, type ScoreSummary, SfuCongestionDetector, type SfuCongestionDetectorBucket, type SfuCongestionDetectorConfig, type SfuCongestionDetectorEvaluation, type SfuCongestionDetectorReport, type SimulcastReceiverEvidence, type SimulcastReceiverReportPayload, SimulcastReceiverValidator, type SimulcastReceiverValidatorConfig, type Sink, type SinkConstructor, type SinkCreateSettings, type SinkDestination, type SinkEvents, SinkRegistry, type SinkSettings, SlidingWindow, type SlidingWindowEntry, type StatsSummary, TrackDeliveryMismatchDetector, type TrackDeliveryMismatchDetectorConfig, TrackDeliveryMismatchTypes, TrendTester, type TrendTesterConfig, type TurnServerHealth, TurnServerHealthDetector, type TurnServerHealthDetectorConfig, TurnServerHealthTypes, TurnServerOutageDetector, type TurnServerOutageDetectorConfig, TurnServerOutageTypes, UNRESOLVED_TRACK_LINKS_ISSUE, UnconsumedTrackDetector, type UnconsumedTrackDetectorConfig, UnconsumedTrackTypes, type ValidationReport, type Validator, type ValidatorName, baseIssueType, concludeCallIssue, concludeObserverIssue, correlation, counterDelta, createCallSummary, createDefaultMediasoupRemoteTrackResolverFactory, createInMemorySink, createJsonlFileSink, createLogger, createP2pRemoteTrackResolverFactory, defaultCallSummaryConfig, defaultClientHealthThresholds, geohash, isClientIssueResolutionEntry, isPublicIpAddress, issuePayloadAsString, mannKendall, mannKendallVerdict, median, medianAbsoluteDeviation, pageHinkley, percentile, percentileOfHistogram, percentileOfSorted, robustZScore, schemaVersion, setObserverLogger, summarize };
7265
+ export { type AcceptContext, type AcceptMiddleware, type AcceptMiddlewarePayload, type ActiveClientIssue, type ActiveIssueTracker, ActiveIssuesRegistry, AudioEchoDetector, type AudioEchoDetectorConfig, AudioEchoTypes, type AudioInputDevice, type AvailableCallScopeDetectorsConfigs, type AvailableDetectorsConfigs, type AvailableObserverScopeDetectorsConfigs, type AvailableValidatorConfigs, CODEC_MISMATCH_ISSUE, type CalculatedScore, type CallAppDataFactory, CallConcurrentIssueDetector, type CallConcurrentIssueDetectorConfig, type CallConcurrentIssueGroup, CallConcurrentIssueTypes, type CallHealth, CallHealthAggregator, type CallIssue, type CallIssueSpread, type CallScopedEventName, type CallSummary, type CallSummaryClients, CallSummaryCollector, type CallSummaryConfig, type CallSummaryEnricher, type CallSummaryEnrichers, type CallSummaryScores, type CallSummarySection, type CallSummaryTruncation, type CallSummaryTurnServers, type CertificateStats, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientHealth, type ClientHealthThresholds, type ClientIssue, type ClientLocation, type ClientLocationResolver, type ClientMetaData, ClientMetaTypes, type ClientPopulation, type ClientPopulationAxis, ClientPopulationIssueDetector, type ClientPopulationIssueDetectorConfig, ClientPopulationIssueTypes, type ClientSample, type ClientSampleSink, type ClientSampleSinkEvents, type ClientSinkParams, type CodecConsistencyReportPayload, CodecConsistencyValidator, type CodecConsistencyValidatorConfig, type CodecEvidence, type CodecStats, type CorroboratedPublisherFault, type CreateExtensionStatsSettings, type DataChannelStats, type Detector, Detectors, type ExtensionStat, ExtensionStatsRegistry, GEOHASH_CELL_SIZES, type IceCandidatePairStats, type IceCandidateStats, type IceTransportStats, InMemorySink, type InboundRtpStats, type InboundTrackSample, type Issue, type IssueBase, type IssueConclusion, IssueFanOutDetector, type IssueFanOutDetectorConfig, IssueFanOutTypes, type IssueFaultDomain, JsonlFileSink, type JsonlFileSinkOptions, LOWEST_COMMON_DENOMINATOR_ISSUE, type Logger, type MannKendallResult, type MediaKind, type MediaPlayoutStats, type MediaSourceStats, type MediasoupConsumerSample, type MediasoupConsumerSampleEvent, type MediasoupDataConsumerSample, type MediasoupDataProducerSample, type MediasoupDirectTransportSample, type MediasoupDirectTransportSampleEventMap, type MediasoupPipeTransportSample, type MediasoupPipeTransportSampleEventMap, type MediasoupPlainTransportSample, type MediasoupPlainTransportSampleEventMap, type MediasoupProducerSample, type MediasoupProducerSampleEvent, type MediasoupRouterSample, type MediasoupSampleEnricher, type MediasoupTransportSample, type MediasoupWebRtcTransportSample, type MediasoupWebRtcTransportSampleEventMap, type Middleware, MiddlewareProcessor, ObservedCall, type ObservedCallScope, type ObservedCallSettings, ObservedCertificate, ObservedClient, ObservedClientIssueRegistry, type ObservedClientScope, type ObservedClientSettings, ObservedCodec, ObservedDataChannel, ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedInboundRtp, ObservedInboundTrack, ObservedMediaPlayout, ObservedMediaSource, ObservedMediasoupRouter, type ObservedMediasoupRouterEvents, type ObservedMediasoupRouterScope, type ObservedMediasoupRouterSettings, ObservedOutboundRtp, ObservedOutboundTrack, ObservedPeerConnection, type ObservedPeerConnectionScope, ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp, ObservedTURN, type ObservedTURNEventMap, ObservedTurnServer, Observer, ObserverConcurrentIssueDetector, type ObserverConcurrentIssueDetectorConfig, type ObserverConcurrentIssueGroup, ObserverConcurrentIssueTypes, type ObserverEventBase, type ObserverEvents, type ObserverIssue, type ObserverIssueSpread, type ObserverLogger, type OutboundRtpStats, type OutboundTrackSample, type PageHinkleyResult, type PeerConnectionSample, type PeerConnectionTransportStats, type PsnrSum, PublisherFaultCorroborationDetector, type PublisherFaultCorroborationDetectorConfig, PublisherFaultTypes, type QualityLimitationDurations, RESOLVED_ISSUE_SUFFIX, RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS, RTT_HISTOGRAM_NUMBER_OF_BUCKETS, type RemoteInboundRtpStats, type RemoteOutboundRtpStats, type RemoteTrackLinkEvidence, RemoteTrackResolver, type RemoteTrackResolverFactory, type RemoteTrackResolverReportPayload, RemoteTrackResolverValidator, type RemoteTrackResolverValidatorConfig, type RemoteTrackResolvers, type ResolvedActiveClientIssue, type RttSummary, type RunningValidator, SCORE_HISTOGRAM_BUCKET_WIDTH, SCORE_HISTOGRAM_NUMBER_OF_BUCKETS, type SampleRejectedReason, type SaveClientSamplesConfig, type ScoreCalculator, type ScoreSummary, SfuCongestionDetector, type SfuCongestionDetectorBucket, type SfuCongestionDetectorConfig, type SfuCongestionDetectorEvaluation, type SfuCongestionDetectorReport, type SimulcastReceiverEvidence, type SimulcastReceiverReportPayload, SimulcastReceiverValidator, type SimulcastReceiverValidatorConfig, type Sink, type SinkConstructor, type SinkCreateSettings, type SinkDestination, type SinkEvents, SinkRegistry, type SinkSettings, SlidingWindow, type SlidingWindowEntry, type StatsSummary, TrackDeliveryMismatchDetector, type TrackDeliveryMismatchDetectorConfig, TrackDeliveryMismatchTypes, TrendTester, type TrendTesterConfig, type TurnServerHealth, TurnServerHealthDetector, type TurnServerHealthDetectorConfig, TurnServerHealthTypes, TurnServerOutageDetector, type TurnServerOutageDetectorConfig, TurnServerOutageTypes, UNRESOLVED_TRACK_LINKS_ISSUE, UnconsumedTrackDetector, type UnconsumedTrackDetectorConfig, UnconsumedTrackTypes, type ValidationReport, type Validator, type ValidatorName, baseIssueType, concludeCallIssue, concludeObserverIssue, correlation, counterDelta, createCallSummary, createDefaultMediasoupRemoteTrackResolverFactory, createInMemorySink, createJsonlFileSink, createLogger, createP2pRemoteTrackResolverFactory, defaultCallSummaryConfig, defaultClientHealthThresholds, geohash, isClientIssueResolutionEntry, isPublicIpAddress, issuePayloadAsString, mannKendall, mannKendallVerdict, median, medianAbsoluteDeviation, pageHinkley, percentile, percentileOfHistogram, percentileOfSorted, robustZScore, schemaVersion, setObserverLogger, summarize };
package/dist/index.d.ts CHANGED
@@ -59,6 +59,12 @@ type SinkDestination<T = unknown> = {
59
59
  sink: Sink<T>;
60
60
  jsonlPath?: never;
61
61
  };
62
+ /** What the `saveClientSamples` callbacks receive: `id` is the client's sample sink id, the client id. */
63
+ type ClientSinkParams<Call = unknown> = {
64
+ id: string;
65
+ observedCall: Call;
66
+ clientId: string;
67
+ };
62
68
 
63
69
  /** A sink class `SinkRegistry.create` can construct: its constructor takes one settings object (with `id`). */
64
70
  type SinkConstructor = new (settings: any) => Sink<any>;
@@ -2190,7 +2196,6 @@ declare class ObservedOutboundTrack implements OutboundTrackSample {
2190
2196
  readonly id: string;
2191
2197
  readonly kind: MediaKind;
2192
2198
  private readonly _peerConnection;
2193
- private readonly _outboundRtps?;
2194
2199
  private readonly _mediaSource?;
2195
2200
  private _visited;
2196
2201
  appData?: Record<string, unknown>;
@@ -2222,11 +2227,12 @@ declare class ObservedOutboundTrack implements OutboundTrackSample {
2222
2227
  * latest tick. Needs a `RemoteTrackResolver`: without links there are no receivers to ask.
2223
2228
  */
2224
2229
  receivedRmsAudioLevel?: number;
2225
- constructor(timestamp: number, id: string, kind: MediaKind, _peerConnection: ObservedPeerConnection, _outboundRtps?: ObservedOutboundRtp[] | undefined, _mediaSource?: ObservedMediaSource | undefined);
2230
+ constructor(timestamp: number, id: string, kind: MediaKind, _peerConnection: ObservedPeerConnection, _mediaSource?: ObservedMediaSource | undefined);
2226
2231
  get score(): number | undefined;
2227
2232
  get visited(): boolean;
2228
2233
  get degraded(): boolean;
2229
2234
  getPeerConnection(): ObservedPeerConnection;
2235
+ /** The outbound RTPs of this track's media source, looked up on every call. */
2230
2236
  getOutboundRtps(): ObservedOutboundRtp[] | undefined;
2231
2237
  getMediaSource(): ObservedMediaSource | undefined;
2232
2238
  /** The capture device behind this track, when the client sent `AUDIO_INPUT_DEVICE` metadata for it. */
@@ -2689,6 +2695,7 @@ declare class ObservedPeerConnection extends EventEmitter {
2689
2695
  private _updateMediaSourceStats;
2690
2696
  private _updateOutboundRtpStats;
2691
2697
  private _updateOutboundTrackSample;
2698
+ private _removeOutboundTrack;
2692
2699
  private _updatePeerConnectionTransportStats;
2693
2700
  private _updateRemoteInboundRtpStats;
2694
2701
  private _updateRemoteOutboundRtpStats;
@@ -4004,7 +4011,7 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
4004
4011
  /** The distinct `publicAddress`es currently known by this client's open peer connections. */
4005
4012
  /**
4006
4013
  * The sink this client's samples are written to: whatever is registered under its client id in
4007
- * `call.sinks` (the one `createClientSink` returned, or a sink the application added), if any.
4014
+ * `call.sinks` (the one created through `saveClientSamples`, or a sink the application added), if any.
4008
4015
  */
4009
4016
  get sink(): Sink<ClientSample> | undefined;
4010
4017
  get publicAddresses(): string[];
@@ -4090,6 +4097,7 @@ declare class RemoteTrackResolver {
4090
4097
  readonly observedCall: ObservedCall;
4091
4098
  private readonly resolvers;
4092
4099
  private readonly _publisherIdToOutboundTrack;
4100
+ private readonly _outboundTrackPublisherIds;
4093
4101
  private readonly _subscriberIdToInboundTrack;
4094
4102
  /**
4095
4103
  * Tracks whose publisher id the strategy could not resolve **yet**.
@@ -5883,8 +5891,8 @@ declare class ObservedCall<AppData extends Record<string, unknown> = Record<stri
5883
5891
  private _onClientUpdate;
5884
5892
  private _clientJoined;
5885
5893
  private _clientLeft;
5886
- /** Emit an Observer-bus event scoped to this call. */
5887
5894
  private _createClientSink;
5895
+ /** Emit an Observer-bus event scoped to this call. */
5888
5896
  private _notify;
5889
5897
  }
5890
5898
 
@@ -6445,6 +6453,14 @@ type ClientAppDataFactory = (params: {
6445
6453
  observedCall: ObservedCall;
6446
6454
  acceptCtx?: AcceptContext;
6447
6455
  }) => Record<string, unknown>;
6456
+ /** `ObserverConfig.saveClientSamples`: exactly one of the two per-client callbacks. */
6457
+ type SaveClientSamplesConfig = {
6458
+ jsonlPath: (params: ClientSinkParams<ObservedCall>) => string | undefined;
6459
+ sink?: never;
6460
+ } | {
6461
+ sink: (params: ClientSinkParams<ObservedCall>) => Sink<ClientSample> | undefined;
6462
+ jsonlPath?: never;
6463
+ };
6448
6464
  type ObserverConfig<AppData extends Record<string, unknown> = Record<string, unknown>> = {
6449
6465
  /**
6450
6466
  * Application-owned data for the observer itself. Never read or modified by the library.
@@ -6598,22 +6614,15 @@ type ObserverConfig<AppData extends Record<string, unknown> = Record<string, unk
6598
6614
  /** Same as `createCallAppData`, for clients. Receives the (already-created) parent call. */
6599
6615
  createClientAppData?: ClientAppDataFactory;
6600
6616
  /**
6601
- * The sink a newly-created client writes its samples to: `{ jsonlPath }` (a `JsonlFileSink` is
6602
- * created at that path under the client id), `{ sink }` (a `Sink` whose `id` is `params.id`, the
6603
- * client id), or `undefined` for none. The sink is added to `observedCall.sinks` and is ended
6604
- * when the client closes; `client-sink-created` announces it.
6605
- *
6606
- * ```ts
6607
- * createClientSink: ({ observedCall, clientId }) => ({ jsonlPath: `./stats/${observedCall.callId}__${clientId}.jsonl` }),
6608
- * ```
6617
+ * Where each client's samples are saved: exactly one of `jsonlPath` (return the path of the
6618
+ * client's JSONL file) and `sink` (return a sink whose `id` is `params.id`), each called per
6619
+ * client; `undefined` gives that client no sink. See the README's *Sinks* section.
6609
6620
  *
6610
- * Called only when the client id is free as a sink id, so a clash opens no file.
6621
+ * A client's sample sink id is its client id (`params.id`). Sink ids are unique per observer, so
6622
+ * client ids must be unique across the observer's open calls; a client whose id is already a sink
6623
+ * id gets no sink, and a warning is logged.
6611
6624
  */
6612
- createClientSink?: (params: {
6613
- id: string;
6614
- observedCall: ObservedCall;
6615
- clientId: string;
6616
- }) => SinkDestination<ClientSample> | undefined;
6625
+ saveClientSamples?: SaveClientSamplesConfig;
6617
6626
  /**
6618
6627
  * Optional factory invoked when a call is created, producing the call's `RemoteTrackResolver`
6619
6628
  * (or `undefined` for none). Use the built-ins
@@ -7183,7 +7192,7 @@ declare function setObserverLogger(logger: ObserverLogger): void;
7183
7192
  /** The lifecycle events a client sample sink may emit. Same as {@link SinkEvents}. */
7184
7193
  type ClientSampleSinkEvents = SinkEvents;
7185
7194
  /**
7186
- * A per-client destination for accepted samples. Return one from `ObserverConfig.createClientSink`
7195
+ * A per-client destination for accepted samples. Return one from `ObserverConfig.saveClientSamples.sink`
7187
7196
  * (with the `id` it receives), or create one under the client id with
7188
7197
  * `observedCall.sinks.create(MySink, { id: clientId, ... })`.
7189
7198
  *
@@ -7253,4 +7262,4 @@ declare function createInMemorySink(samples?: ClientSample[]): InMemorySink;
7253
7262
  declare function createDefaultMediasoupRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
7254
7263
  declare function createP2pRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
7255
7264
 
7256
- export { type AcceptContext, type AcceptMiddleware, type AcceptMiddlewarePayload, type ActiveClientIssue, type ActiveIssueTracker, ActiveIssuesRegistry, AudioEchoDetector, type AudioEchoDetectorConfig, AudioEchoTypes, type AudioInputDevice, type AvailableCallScopeDetectorsConfigs, type AvailableDetectorsConfigs, type AvailableObserverScopeDetectorsConfigs, type AvailableValidatorConfigs, CODEC_MISMATCH_ISSUE, type CalculatedScore, type CallAppDataFactory, CallConcurrentIssueDetector, type CallConcurrentIssueDetectorConfig, type CallConcurrentIssueGroup, CallConcurrentIssueTypes, type CallHealth, CallHealthAggregator, type CallIssue, type CallIssueSpread, type CallScopedEventName, type CallSummary, type CallSummaryClients, CallSummaryCollector, type CallSummaryConfig, type CallSummaryEnricher, type CallSummaryEnrichers, type CallSummaryScores, type CallSummarySection, type CallSummaryTruncation, type CallSummaryTurnServers, type CertificateStats, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientHealth, type ClientHealthThresholds, type ClientIssue, type ClientLocation, type ClientLocationResolver, type ClientMetaData, ClientMetaTypes, type ClientPopulation, type ClientPopulationAxis, ClientPopulationIssueDetector, type ClientPopulationIssueDetectorConfig, ClientPopulationIssueTypes, type ClientSample, type ClientSampleSink, type ClientSampleSinkEvents, type CodecConsistencyReportPayload, CodecConsistencyValidator, type CodecConsistencyValidatorConfig, type CodecEvidence, type CodecStats, type CorroboratedPublisherFault, type CreateExtensionStatsSettings, type DataChannelStats, type Detector, Detectors, type ExtensionStat, ExtensionStatsRegistry, GEOHASH_CELL_SIZES, type IceCandidatePairStats, type IceCandidateStats, type IceTransportStats, InMemorySink, type InboundRtpStats, type InboundTrackSample, type Issue, type IssueBase, type IssueConclusion, IssueFanOutDetector, type IssueFanOutDetectorConfig, IssueFanOutTypes, type IssueFaultDomain, JsonlFileSink, type JsonlFileSinkOptions, LOWEST_COMMON_DENOMINATOR_ISSUE, type Logger, type MannKendallResult, type MediaKind, type MediaPlayoutStats, type MediaSourceStats, type MediasoupConsumerSample, type MediasoupConsumerSampleEvent, type MediasoupDataConsumerSample, type MediasoupDataProducerSample, type MediasoupDirectTransportSample, type MediasoupDirectTransportSampleEventMap, type MediasoupPipeTransportSample, type MediasoupPipeTransportSampleEventMap, type MediasoupPlainTransportSample, type MediasoupPlainTransportSampleEventMap, type MediasoupProducerSample, type MediasoupProducerSampleEvent, type MediasoupRouterSample, type MediasoupSampleEnricher, type MediasoupTransportSample, type MediasoupWebRtcTransportSample, type MediasoupWebRtcTransportSampleEventMap, type Middleware, MiddlewareProcessor, ObservedCall, type ObservedCallScope, type ObservedCallSettings, ObservedCertificate, ObservedClient, ObservedClientIssueRegistry, type ObservedClientScope, type ObservedClientSettings, ObservedCodec, ObservedDataChannel, ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedInboundRtp, ObservedInboundTrack, ObservedMediaPlayout, ObservedMediaSource, ObservedMediasoupRouter, type ObservedMediasoupRouterEvents, type ObservedMediasoupRouterScope, type ObservedMediasoupRouterSettings, ObservedOutboundRtp, ObservedOutboundTrack, ObservedPeerConnection, type ObservedPeerConnectionScope, ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp, ObservedTURN, type ObservedTURNEventMap, ObservedTurnServer, Observer, ObserverConcurrentIssueDetector, type ObserverConcurrentIssueDetectorConfig, type ObserverConcurrentIssueGroup, ObserverConcurrentIssueTypes, type ObserverEventBase, type ObserverEvents, type ObserverIssue, type ObserverIssueSpread, type ObserverLogger, type OutboundRtpStats, type OutboundTrackSample, type PageHinkleyResult, type PeerConnectionSample, type PeerConnectionTransportStats, type PsnrSum, PublisherFaultCorroborationDetector, type PublisherFaultCorroborationDetectorConfig, PublisherFaultTypes, type QualityLimitationDurations, RESOLVED_ISSUE_SUFFIX, RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS, RTT_HISTOGRAM_NUMBER_OF_BUCKETS, type RemoteInboundRtpStats, type RemoteOutboundRtpStats, type RemoteTrackLinkEvidence, RemoteTrackResolver, type RemoteTrackResolverFactory, type RemoteTrackResolverReportPayload, RemoteTrackResolverValidator, type RemoteTrackResolverValidatorConfig, type RemoteTrackResolvers, type ResolvedActiveClientIssue, type RttSummary, type RunningValidator, SCORE_HISTOGRAM_BUCKET_WIDTH, SCORE_HISTOGRAM_NUMBER_OF_BUCKETS, type SampleRejectedReason, type ScoreCalculator, type ScoreSummary, SfuCongestionDetector, type SfuCongestionDetectorBucket, type SfuCongestionDetectorConfig, type SfuCongestionDetectorEvaluation, type SfuCongestionDetectorReport, type SimulcastReceiverEvidence, type SimulcastReceiverReportPayload, SimulcastReceiverValidator, type SimulcastReceiverValidatorConfig, type Sink, type SinkConstructor, type SinkCreateSettings, type SinkDestination, type SinkEvents, SinkRegistry, type SinkSettings, SlidingWindow, type SlidingWindowEntry, type StatsSummary, TrackDeliveryMismatchDetector, type TrackDeliveryMismatchDetectorConfig, TrackDeliveryMismatchTypes, TrendTester, type TrendTesterConfig, type TurnServerHealth, TurnServerHealthDetector, type TurnServerHealthDetectorConfig, TurnServerHealthTypes, TurnServerOutageDetector, type TurnServerOutageDetectorConfig, TurnServerOutageTypes, UNRESOLVED_TRACK_LINKS_ISSUE, UnconsumedTrackDetector, type UnconsumedTrackDetectorConfig, UnconsumedTrackTypes, type ValidationReport, type Validator, type ValidatorName, baseIssueType, concludeCallIssue, concludeObserverIssue, correlation, counterDelta, createCallSummary, createDefaultMediasoupRemoteTrackResolverFactory, createInMemorySink, createJsonlFileSink, createLogger, createP2pRemoteTrackResolverFactory, defaultCallSummaryConfig, defaultClientHealthThresholds, geohash, isClientIssueResolutionEntry, isPublicIpAddress, issuePayloadAsString, mannKendall, mannKendallVerdict, median, medianAbsoluteDeviation, pageHinkley, percentile, percentileOfHistogram, percentileOfSorted, robustZScore, schemaVersion, setObserverLogger, summarize };
7265
+ export { type AcceptContext, type AcceptMiddleware, type AcceptMiddlewarePayload, type ActiveClientIssue, type ActiveIssueTracker, ActiveIssuesRegistry, AudioEchoDetector, type AudioEchoDetectorConfig, AudioEchoTypes, type AudioInputDevice, type AvailableCallScopeDetectorsConfigs, type AvailableDetectorsConfigs, type AvailableObserverScopeDetectorsConfigs, type AvailableValidatorConfigs, CODEC_MISMATCH_ISSUE, type CalculatedScore, type CallAppDataFactory, CallConcurrentIssueDetector, type CallConcurrentIssueDetectorConfig, type CallConcurrentIssueGroup, CallConcurrentIssueTypes, type CallHealth, CallHealthAggregator, type CallIssue, type CallIssueSpread, type CallScopedEventName, type CallSummary, type CallSummaryClients, CallSummaryCollector, type CallSummaryConfig, type CallSummaryEnricher, type CallSummaryEnrichers, type CallSummaryScores, type CallSummarySection, type CallSummaryTruncation, type CallSummaryTurnServers, type CertificateStats, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientHealth, type ClientHealthThresholds, type ClientIssue, type ClientLocation, type ClientLocationResolver, type ClientMetaData, ClientMetaTypes, type ClientPopulation, type ClientPopulationAxis, ClientPopulationIssueDetector, type ClientPopulationIssueDetectorConfig, ClientPopulationIssueTypes, type ClientSample, type ClientSampleSink, type ClientSampleSinkEvents, type ClientSinkParams, type CodecConsistencyReportPayload, CodecConsistencyValidator, type CodecConsistencyValidatorConfig, type CodecEvidence, type CodecStats, type CorroboratedPublisherFault, type CreateExtensionStatsSettings, type DataChannelStats, type Detector, Detectors, type ExtensionStat, ExtensionStatsRegistry, GEOHASH_CELL_SIZES, type IceCandidatePairStats, type IceCandidateStats, type IceTransportStats, InMemorySink, type InboundRtpStats, type InboundTrackSample, type Issue, type IssueBase, type IssueConclusion, IssueFanOutDetector, type IssueFanOutDetectorConfig, IssueFanOutTypes, type IssueFaultDomain, JsonlFileSink, type JsonlFileSinkOptions, LOWEST_COMMON_DENOMINATOR_ISSUE, type Logger, type MannKendallResult, type MediaKind, type MediaPlayoutStats, type MediaSourceStats, type MediasoupConsumerSample, type MediasoupConsumerSampleEvent, type MediasoupDataConsumerSample, type MediasoupDataProducerSample, type MediasoupDirectTransportSample, type MediasoupDirectTransportSampleEventMap, type MediasoupPipeTransportSample, type MediasoupPipeTransportSampleEventMap, type MediasoupPlainTransportSample, type MediasoupPlainTransportSampleEventMap, type MediasoupProducerSample, type MediasoupProducerSampleEvent, type MediasoupRouterSample, type MediasoupSampleEnricher, type MediasoupTransportSample, type MediasoupWebRtcTransportSample, type MediasoupWebRtcTransportSampleEventMap, type Middleware, MiddlewareProcessor, ObservedCall, type ObservedCallScope, type ObservedCallSettings, ObservedCertificate, ObservedClient, ObservedClientIssueRegistry, type ObservedClientScope, type ObservedClientSettings, ObservedCodec, ObservedDataChannel, ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedInboundRtp, ObservedInboundTrack, ObservedMediaPlayout, ObservedMediaSource, ObservedMediasoupRouter, type ObservedMediasoupRouterEvents, type ObservedMediasoupRouterScope, type ObservedMediasoupRouterSettings, ObservedOutboundRtp, ObservedOutboundTrack, ObservedPeerConnection, type ObservedPeerConnectionScope, ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp, ObservedTURN, type ObservedTURNEventMap, ObservedTurnServer, Observer, ObserverConcurrentIssueDetector, type ObserverConcurrentIssueDetectorConfig, type ObserverConcurrentIssueGroup, ObserverConcurrentIssueTypes, type ObserverEventBase, type ObserverEvents, type ObserverIssue, type ObserverIssueSpread, type ObserverLogger, type OutboundRtpStats, type OutboundTrackSample, type PageHinkleyResult, type PeerConnectionSample, type PeerConnectionTransportStats, type PsnrSum, PublisherFaultCorroborationDetector, type PublisherFaultCorroborationDetectorConfig, PublisherFaultTypes, type QualityLimitationDurations, RESOLVED_ISSUE_SUFFIX, RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS, RTT_HISTOGRAM_NUMBER_OF_BUCKETS, type RemoteInboundRtpStats, type RemoteOutboundRtpStats, type RemoteTrackLinkEvidence, RemoteTrackResolver, type RemoteTrackResolverFactory, type RemoteTrackResolverReportPayload, RemoteTrackResolverValidator, type RemoteTrackResolverValidatorConfig, type RemoteTrackResolvers, type ResolvedActiveClientIssue, type RttSummary, type RunningValidator, SCORE_HISTOGRAM_BUCKET_WIDTH, SCORE_HISTOGRAM_NUMBER_OF_BUCKETS, type SampleRejectedReason, type SaveClientSamplesConfig, type ScoreCalculator, type ScoreSummary, SfuCongestionDetector, type SfuCongestionDetectorBucket, type SfuCongestionDetectorConfig, type SfuCongestionDetectorEvaluation, type SfuCongestionDetectorReport, type SimulcastReceiverEvidence, type SimulcastReceiverReportPayload, SimulcastReceiverValidator, type SimulcastReceiverValidatorConfig, type Sink, type SinkConstructor, type SinkCreateSettings, type SinkDestination, type SinkEvents, SinkRegistry, type SinkSettings, SlidingWindow, type SlidingWindowEntry, type StatsSummary, TrackDeliveryMismatchDetector, type TrackDeliveryMismatchDetectorConfig, TrackDeliveryMismatchTypes, TrendTester, type TrendTesterConfig, type TurnServerHealth, TurnServerHealthDetector, type TurnServerHealthDetectorConfig, TurnServerHealthTypes, TurnServerOutageDetector, type TurnServerOutageDetectorConfig, TurnServerOutageTypes, UNRESOLVED_TRACK_LINKS_ISSUE, UnconsumedTrackDetector, type UnconsumedTrackDetectorConfig, UnconsumedTrackTypes, type ValidationReport, type Validator, type ValidatorName, baseIssueType, concludeCallIssue, concludeObserverIssue, correlation, counterDelta, createCallSummary, createDefaultMediasoupRemoteTrackResolverFactory, createInMemorySink, createJsonlFileSink, createLogger, createP2pRemoteTrackResolverFactory, defaultCallSummaryConfig, defaultClientHealthThresholds, geohash, isClientIssueResolutionEntry, isPublicIpAddress, issuePayloadAsString, mannKendall, mannKendallVerdict, median, medianAbsoluteDeviation, pageHinkley, percentile, percentileOfHistogram, percentileOfSorted, robustZScore, schemaVersion, setObserverLogger, summarize };
package/dist/index.js CHANGED
@@ -312,11 +312,11 @@ var SinkRegistry = class {
312
312
  }
313
313
  /** End every sink of this registry and of its descendants. */
314
314
  clear() {
315
- for (const sink of [...this._sinks.values()]) {
315
+ for (const [id, sink] of [...this._sinks.entries()]) {
316
316
  try {
317
317
  sink.end();
318
318
  } catch (err) {
319
- logger.warn("Sink %s failed to end: %o", sink.id, err);
319
+ logger.warn("Sink %s failed to end: %o", id, err);
320
320
  }
321
321
  }
322
322
  }
@@ -1912,19 +1912,17 @@ function isPublicIpAddress(address) {
1912
1912
 
1913
1913
  // src/ObservedOutboundTrack.ts
1914
1914
  var ObservedOutboundTrack = class {
1915
- constructor(timestamp, id, kind, _peerConnection, _outboundRtps, _mediaSource) {
1915
+ constructor(timestamp, id, kind, _peerConnection, _mediaSource) {
1916
1916
  this.timestamp = timestamp;
1917
1917
  this.id = id;
1918
1918
  this.kind = kind;
1919
1919
  this._peerConnection = _peerConnection;
1920
- this._outboundRtps = _outboundRtps;
1921
1920
  this._mediaSource = _mediaSource;
1922
1921
  }
1923
1922
  timestamp;
1924
1923
  id;
1925
1924
  kind;
1926
1925
  _peerConnection;
1927
- _outboundRtps;
1928
1926
  _mediaSource;
1929
1927
  _visited = false;
1930
1928
  appData;
@@ -1975,8 +1973,11 @@ var ObservedOutboundTrack = class {
1975
1973
  getPeerConnection() {
1976
1974
  return this._peerConnection;
1977
1975
  }
1976
+ /** The outbound RTPs of this track's media source, looked up on every call. */
1978
1977
  getOutboundRtps() {
1979
- return this._outboundRtps;
1978
+ const mediaSource = this._mediaSource;
1979
+ if (!mediaSource) return void 0;
1980
+ return [...this._peerConnection.observedOutboundRtps.values()].filter((rtp) => rtp.mediaSourceId === mediaSource.id);
1980
1981
  }
1981
1982
  getMediaSource() {
1982
1983
  return this._mediaSource;
@@ -1995,7 +1996,7 @@ var ObservedOutboundTrack = class {
1995
1996
  this.remoteFractionLost = void 0;
1996
1997
  this.remoteRttInMs = void 0;
1997
1998
  this.qualityLimitationReason = void 0;
1998
- for (const rtp of this._outboundRtps ?? []) {
1999
+ for (const rtp of this.getOutboundRtps() ?? []) {
1999
2000
  this.bitrate = (this.bitrate ?? 0) + rtp.bitrate;
2000
2001
  this.deltaPacketsSent = (this.deltaPacketsSent ?? 0) + rtp.deltaPacketsSent;
2001
2002
  if (rtp.remoteFractionLost !== void 0) {
@@ -2660,9 +2661,7 @@ var ObservedPeerConnection = class extends import_node_events2.EventEmitter {
2660
2661
  }
2661
2662
  for (const observedOutboundTrack of [...this.observedOutboundTracks.values()]) {
2662
2663
  if (observedOutboundTrack.visited) continue;
2663
- this.observedOutboundTracks.delete(observedOutboundTrack.id);
2664
- this.emit("removed-outbound-track", observedOutboundTrack);
2665
- this._notify("outbound-track-removed", { ...this.eventScope, observedOutboundTrack });
2664
+ this._removeOutboundTrack(observedOutboundTrack);
2666
2665
  }
2667
2666
  for (const observedPeerConnectionTransport of [...this.observedPeerConnectionTransports.values()]) {
2668
2667
  if (observedPeerConnectionTransport.visited) continue;
@@ -2923,6 +2922,11 @@ var ObservedPeerConnection = class extends import_node_events2.EventEmitter {
2923
2922
  }
2924
2923
  _updateOutboundTrackSample(stats) {
2925
2924
  let observedOutboundTrack = this.observedOutboundTracks.get(stats.id);
2925
+ const mediaSource = [...this.observedMediaSources.values()].filter((source) => source.trackIdentifier === stats.id).pop();
2926
+ if (observedOutboundTrack && observedOutboundTrack.getMediaSource() !== mediaSource) {
2927
+ this._removeOutboundTrack(observedOutboundTrack);
2928
+ observedOutboundTrack = void 0;
2929
+ }
2926
2930
  if (!observedOutboundTrack) {
2927
2931
  if (!stats.timestamp || !stats.id || !stats.kind) {
2928
2932
  return logger3.warn(
@@ -2930,15 +2934,12 @@ var ObservedPeerConnection = class extends import_node_events2.EventEmitter {
2930
2934
  stats
2931
2935
  );
2932
2936
  }
2933
- const observedMediaSource = [...this.observedMediaSources.values()].find((mediaSource) => mediaSource.trackIdentifier === stats.id);
2934
- const outboundRtps = observedMediaSource ? [...this.observedOutboundRtps.values()].filter((outboundRtp) => outboundRtp.mediaSourceId === observedMediaSource?.id) : void 0;
2935
2937
  observedOutboundTrack = new ObservedOutboundTrack(
2936
2938
  stats.timestamp,
2937
2939
  stats.id,
2938
2940
  stats.kind,
2939
2941
  this,
2940
- outboundRtps,
2941
- observedMediaSource
2942
+ mediaSource
2942
2943
  );
2943
2944
  observedOutboundTrack.update(stats);
2944
2945
  this.observedOutboundTracks.set(stats.id, observedOutboundTrack);
@@ -2949,6 +2950,11 @@ var ObservedPeerConnection = class extends import_node_events2.EventEmitter {
2949
2950
  this._notify("outbound-track-updated", { ...this.eventScope, observedOutboundTrack });
2950
2951
  return observedOutboundTrack;
2951
2952
  }
2953
+ _removeOutboundTrack(observedOutboundTrack) {
2954
+ this.observedOutboundTracks.delete(observedOutboundTrack.id);
2955
+ this.emit("removed-outbound-track", observedOutboundTrack);
2956
+ this._notify("outbound-track-removed", { ...this.eventScope, observedOutboundTrack });
2957
+ }
2952
2958
  _updatePeerConnectionTransportStats(stats) {
2953
2959
  let observedPeerConnectionTransport = this.observedPeerConnectionTransports.get(stats.id);
2954
2960
  if (!observedPeerConnectionTransport) {
@@ -3258,7 +3264,7 @@ var ObservedClient = class extends import_node_events3.EventEmitter {
3258
3264
  /** The distinct `publicAddress`es currently known by this client's open peer connections. */
3259
3265
  /**
3260
3266
  * The sink this client's samples are written to: whatever is registered under its client id in
3261
- * `call.sinks` (the one `createClientSink` returned, or a sink the application added), if any.
3267
+ * `call.sinks` (the one created through `saveClientSamples`, or a sink the application added), if any.
3262
3268
  */
3263
3269
  get sink() {
3264
3270
  return this.call.sinks.get(this.clientId);
@@ -5411,17 +5417,26 @@ var ObservedCall = class extends import_node_events4.EventEmitter {
5411
5417
  if (!client.leftAt) return;
5412
5418
  this.endedAt = Math.max(this.endedAt ?? client.leftAt, client.leftAt);
5413
5419
  }
5414
- /** Emit an Observer-bus event scoped to this call. */
5415
5420
  _createClientSink(clientId) {
5416
- const createClientSink = this.observer.config.createClientSink;
5417
- if (!createClientSink) return void 0;
5421
+ const config = this.observer.config.saveClientSamples;
5422
+ if (!config || !config.jsonlPath && !config.sink) return void 0;
5423
+ if (config.jsonlPath && config.sink) {
5424
+ logger6.warn("saveClientSamples: give either jsonlPath or sink, not both; no client samples are saved");
5425
+ return void 0;
5426
+ }
5418
5427
  if (this.observer.sinks.get(clientId)) {
5419
5428
  logger6.warn("No sample sink for client %s in call %s: sink id %s is already in use", clientId, this.callId, clientId);
5420
5429
  return void 0;
5421
5430
  }
5422
- const destination = createClientSink({ id: clientId, observedCall: this, clientId });
5423
- return destination ? this.sinks.createFromDestination(clientId, destination) : void 0;
5431
+ const params = { id: clientId, observedCall: this, clientId };
5432
+ if (config.jsonlPath) {
5433
+ const jsonlPath = config.jsonlPath(params);
5434
+ return jsonlPath === void 0 ? void 0 : this.sinks.createFromDestination(clientId, { jsonlPath });
5435
+ }
5436
+ const sink = config.sink?.(params);
5437
+ return sink ? this.sinks.createFromDestination(clientId, { sink }) : void 0;
5424
5438
  }
5439
+ /** Emit an Observer-bus event scoped to this call. */
5425
5440
  _notify(type, ...args) {
5426
5441
  this.observer.emit(type, ...args);
5427
5442
  }
@@ -8169,6 +8184,7 @@ var RemoteTrackResolver = class {
8169
8184
  observer.off("outbound-track-updated", onOutboundUpdated);
8170
8185
  this._pendingInboundTracks.clear();
8171
8186
  this._pendingOutboundTracks.clear();
8187
+ this._outboundTrackPublisherIds.clear();
8172
8188
  });
8173
8189
  observer.on("inbound-track-added", onInboundAdded);
8174
8190
  observer.on("inbound-track-removed", onInboundRemoved);
@@ -8180,6 +8196,8 @@ var RemoteTrackResolver = class {
8180
8196
  observedCall;
8181
8197
  resolvers;
8182
8198
  _publisherIdToOutboundTrack = /* @__PURE__ */ new Map();
8199
+ // The publisher id each outbound track was indexed under, so removal does not depend on its current attachments.
8200
+ _outboundTrackPublisherIds = /* @__PURE__ */ new Map();
8183
8201
  _subscriberIdToInboundTrack = /* @__PURE__ */ new Map();
8184
8202
  /**
8185
8203
  * Tracks whose publisher id the strategy could not resolve **yet**.
@@ -8255,6 +8273,7 @@ var RemoteTrackResolver = class {
8255
8273
  }
8256
8274
  this._pendingOutboundTracks.delete(outboundTrack);
8257
8275
  this._publisherIdToOutboundTrack.set(publisherId, outboundTrack);
8276
+ this._outboundTrackPublisherIds.set(outboundTrack, publisherId);
8258
8277
  this.observedCall.unconsumedOutboundTracks.add(outboundTrack);
8259
8278
  for (const observedClient of this.observedCall.observedClients.values()) {
8260
8279
  for (const observedPeerConnection of observedClient.observedPeerConnections.values()) {
@@ -8269,7 +8288,8 @@ var RemoteTrackResolver = class {
8269
8288
  }
8270
8289
  _removeOutboundTrack(outboundTrack) {
8271
8290
  this._pendingOutboundTracks.delete(outboundTrack);
8272
- const publisherId = this.resolvers.resolveOutboundTrackPublisherId(outboundTrack);
8291
+ const publisherId = this._outboundTrackPublisherIds.get(outboundTrack);
8292
+ this._outboundTrackPublisherIds.delete(outboundTrack);
8273
8293
  if (publisherId && this._publisherIdToOutboundTrack.get(publisherId) === outboundTrack) {
8274
8294
  this._publisherIdToOutboundTrack.delete(publisherId);
8275
8295
  }