@observertc/observer-js 1.3.0-rc.4 → 1.3.0-rc.6

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
@@ -1975,6 +1975,7 @@ its id — refuse writes after it, and emit `close` / `error` for its destinatio
1975
1975
  | Export | Notes |
1976
1976
  |--------|-------|
1977
1977
  | `JsonlFileSink<T = ClientSample>` / `createJsonlFileSink<T>({ path, serializeSample? })` | appends one JSON line per record; wraps `fs.WriteStream` and re-emits its events; exposes `readonly path`. The directory **must exist** |
1978
+ | `RollingJsonlFileSink<T>` | JSONL over a sequence of files for long-lived owners: rolls on `maxBytes`, `maxAgeMs` or `roll()`; `file-closed` per finished file. See [Rolling files](#rolling-files) |
1978
1979
  | `InMemorySink` / `createInMemorySink(samples?)` | collects client samples into `.samples`; emits `close` on `end()` |
1979
1980
 
1980
1981
  ### Client samples
@@ -2101,6 +2102,43 @@ and the extension stats' sinks are ended. The owner's other sinks end with its `
2101
2102
  usual. `extensionStats.close()` can also be called directly; it ends only the extension stats'
2102
2103
  sinks.
2103
2104
 
2105
+ ### Rolling files
2106
+
2107
+ `JsonlFileSink` writes one file for the sink's whole life — right for calls and clients, which come
2108
+ and go. An owner that lives for days or months, such as the observer itself, needs its records cut
2109
+ into several files. `RollingJsonlFileSink` does that: it writes JSONL to a sequence of files, closing
2110
+ the current one and opening the next (*rolling*) before a write that would exceed `maxBytes`, once
2111
+ the current file has been open for `maxAgeMs`, or when `roll()` is called.
2112
+
2113
+ ```ts
2114
+ const sink = new RollingJsonlFileSink({
2115
+ id: 'sfu-cpu',
2116
+ path: ({ index, openedAt }) => `./stats/sfu-cpu-${openedAt}-${index}.jsonl`,
2117
+ maxBytes: 100 * 1024 * 1024,
2118
+ maxAgeMs: 60 * 60 * 1000,
2119
+ });
2120
+
2121
+ sink.on('file-closed', (path, index) => uploadFile(path)); // flushed & fd closed → ready
2122
+
2123
+ observer.createExtensionStats({ id: 'sfu-cpu', sink });
2124
+ setInterval(async () => observer.acceptExtensionStats('sfu-cpu', await readCpu()), 5_000).unref();
2125
+ ```
2126
+
2127
+ | Option / member | Meaning |
2128
+ |-----------------|---------|
2129
+ | `path({ index, openedAt })` | the path of each file, called when it is opened (`index` 0, 1, 2, …; `openedAt` = `Date.now()`). Must give a new path per `index` |
2130
+ | `maxBytes?`, `maxAgeMs?` | roll before a write that would make the file larger than `maxBytes`, or once it has been open for `maxAgeMs` |
2131
+ | `serializeSample?` | custom serializer; defaults to `JSON.stringify` |
2132
+ | `roll()` | close the current file and continue in a new one |
2133
+ | `path`, `index` | the file currently written to |
2134
+
2135
+ - Nothing is dropped: each record goes to exactly one file.
2136
+ - The limits are checked on `write`, so the sink runs no timers: a file is not rolled while nothing
2137
+ is written to it, and an empty file is never rolled (`roll()` on one is a no-op).
2138
+ - `file-closed` (`path`, `index`) fires for every finished file, in index order, the last one
2139
+ included. `close` fires once, after `end()` has closed every file. The owner ends the sink as
2140
+ usual — here when the observer closes.
2141
+
2104
2142
  ### Other records: e.g. mediasoup pipe transport stats
2105
2143
 
2106
2144
  ```ts
package/dist/index.d.mts CHANGED
@@ -2196,7 +2196,6 @@ declare class ObservedOutboundTrack implements OutboundTrackSample {
2196
2196
  readonly id: string;
2197
2197
  readonly kind: MediaKind;
2198
2198
  private readonly _peerConnection;
2199
- private readonly _outboundRtps?;
2200
2199
  private readonly _mediaSource?;
2201
2200
  private _visited;
2202
2201
  appData?: Record<string, unknown>;
@@ -2228,11 +2227,12 @@ declare class ObservedOutboundTrack implements OutboundTrackSample {
2228
2227
  * latest tick. Needs a `RemoteTrackResolver`: without links there are no receivers to ask.
2229
2228
  */
2230
2229
  receivedRmsAudioLevel?: number;
2231
- 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);
2232
2231
  get score(): number | undefined;
2233
2232
  get visited(): boolean;
2234
2233
  get degraded(): boolean;
2235
2234
  getPeerConnection(): ObservedPeerConnection;
2235
+ /** The outbound RTPs of this track's media source, looked up on every call. */
2236
2236
  getOutboundRtps(): ObservedOutboundRtp[] | undefined;
2237
2237
  getMediaSource(): ObservedMediaSource | undefined;
2238
2238
  /** The capture device behind this track, when the client sent `AUDIO_INPUT_DEVICE` metadata for it. */
@@ -2695,6 +2695,7 @@ declare class ObservedPeerConnection extends EventEmitter {
2695
2695
  private _updateMediaSourceStats;
2696
2696
  private _updateOutboundRtpStats;
2697
2697
  private _updateOutboundTrackSample;
2698
+ private _removeOutboundTrack;
2698
2699
  private _updatePeerConnectionTransportStats;
2699
2700
  private _updateRemoteInboundRtpStats;
2700
2701
  private _updateRemoteOutboundRtpStats;
@@ -4096,6 +4097,7 @@ declare class RemoteTrackResolver {
4096
4097
  readonly observedCall: ObservedCall;
4097
4098
  private readonly resolvers;
4098
4099
  private readonly _publisherIdToOutboundTrack;
4100
+ private readonly _outboundTrackPublisherIds;
4099
4101
  private readonly _subscriberIdToInboundTrack;
4100
4102
  /**
4101
4103
  * Tracks whose publisher id the strategy could not resolve **yet**.
@@ -7234,6 +7236,74 @@ declare class JsonlFileSink<T = ClientSample> extends EventEmitter implements Si
7234
7236
  /** Build a JSONL file sink for a single path. */
7235
7237
  declare function createJsonlFileSink<T = ClientSample>(options: JsonlFileSinkOptions<T>): JsonlFileSink<T>;
7236
7238
 
7239
+ /** The file a `RollingJsonlFileSink` is about to open: the sequence number and when it opens. */
7240
+ type RollingJsonlFileInfo = {
7241
+ /** 0 for the first file, then 1, 2, … */
7242
+ index: number;
7243
+ /** `Date.now()` when the file is opened. */
7244
+ openedAt: number;
7245
+ };
7246
+ type RollingJsonlFileSinkOptions<T = unknown> = Partial<SinkSettings> & {
7247
+ /** The path of each file, called when it is opened. Must give a new path per `index`. */
7248
+ path: (file: RollingJsonlFileInfo) => string;
7249
+ /** Roll before a write that would make the current file larger than this, in bytes. */
7250
+ maxBytes?: number;
7251
+ /** Roll before a write once the current file has been open for this long, in ms. */
7252
+ maxAgeMs?: number;
7253
+ /** Optional custom serializer; defaults to `JSON.stringify`. */
7254
+ serializeSample?: (record: T) => string;
7255
+ };
7256
+ type RollingJsonlFileSinkEvents = {
7257
+ ended: SinkEvents['ended'];
7258
+ close: SinkEvents['close'];
7259
+ error: SinkEvents['error'];
7260
+ finish: SinkEvents['finish'];
7261
+ drain: SinkEvents['drain'];
7262
+ /** A file is complete: flushed and its descriptor closed. Fires for every file, the last one included. */
7263
+ 'file-closed': [path: string, index: number];
7264
+ };
7265
+ declare interface RollingJsonlFileSink {
7266
+ on<E extends keyof RollingJsonlFileSinkEvents>(event: E, listener: (...args: RollingJsonlFileSinkEvents[E]) => void): this;
7267
+ once<E extends keyof RollingJsonlFileSinkEvents>(event: E, listener: (...args: RollingJsonlFileSinkEvents[E]) => void): this;
7268
+ off<E extends keyof RollingJsonlFileSinkEvents>(event: E, listener: (...args: RollingJsonlFileSinkEvents[E]) => void): this;
7269
+ emit<E extends keyof RollingJsonlFileSinkEvents>(event: E, ...args: RollingJsonlFileSinkEvents[E]): boolean;
7270
+ }
7271
+ /**
7272
+ * A JSONL file sink for long-lived owners (e.g. an observer's extension stats over days): it writes
7273
+ * to a sequence of files, closing the current one and opening the next — *rolling* — when it would
7274
+ * exceed `maxBytes`, when it has been open for `maxAgeMs`, or when `roll()` is called. Nothing is
7275
+ * dropped: each record goes to exactly one file.
7276
+ *
7277
+ * The limits are checked on `write`, so a file is never rolled while nothing is written to it, and
7278
+ * an empty file is never rolled. `file-closed` announces each finished file (ready to upload or
7279
+ * move), in index order; `close` fires once, after every file is closed by `end()`.
7280
+ */
7281
+ declare class RollingJsonlFileSink<T = unknown> extends EventEmitter implements Sink<T> {
7282
+ readonly id: string;
7283
+ private readonly _options;
7284
+ private readonly _serializeSample;
7285
+ private _stream;
7286
+ private _path;
7287
+ private _index;
7288
+ private _openedAt;
7289
+ private _bytes;
7290
+ private _ended;
7291
+ private _closing;
7292
+ constructor(options: RollingJsonlFileSinkOptions<T>);
7293
+ /** The file currently written to. */
7294
+ get path(): string;
7295
+ /** The index of the file currently written to. */
7296
+ get index(): number;
7297
+ get ended(): boolean;
7298
+ write(record: T): boolean;
7299
+ /** Close the current file and continue in a new one. A no-op on an empty file or an ended sink. */
7300
+ roll(): void;
7301
+ /** No more writes: emits `ended`, closes the current file (`file-closed`), then emits `close`. */
7302
+ end(): void;
7303
+ private _open;
7304
+ private _closeCurrent;
7305
+ }
7306
+
7237
7307
  declare interface InMemorySink {
7238
7308
  on<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
7239
7309
  once<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
@@ -7260,4 +7330,4 @@ declare function createInMemorySink(samples?: ClientSample[]): InMemorySink;
7260
7330
  declare function createDefaultMediasoupRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
7261
7331
  declare function createP2pRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
7262
7332
 
7263
- 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 };
7333
+ 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 RollingJsonlFileInfo, RollingJsonlFileSink, type RollingJsonlFileSinkEvents, type RollingJsonlFileSinkOptions, 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
@@ -2196,7 +2196,6 @@ declare class ObservedOutboundTrack implements OutboundTrackSample {
2196
2196
  readonly id: string;
2197
2197
  readonly kind: MediaKind;
2198
2198
  private readonly _peerConnection;
2199
- private readonly _outboundRtps?;
2200
2199
  private readonly _mediaSource?;
2201
2200
  private _visited;
2202
2201
  appData?: Record<string, unknown>;
@@ -2228,11 +2227,12 @@ declare class ObservedOutboundTrack implements OutboundTrackSample {
2228
2227
  * latest tick. Needs a `RemoteTrackResolver`: without links there are no receivers to ask.
2229
2228
  */
2230
2229
  receivedRmsAudioLevel?: number;
2231
- 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);
2232
2231
  get score(): number | undefined;
2233
2232
  get visited(): boolean;
2234
2233
  get degraded(): boolean;
2235
2234
  getPeerConnection(): ObservedPeerConnection;
2235
+ /** The outbound RTPs of this track's media source, looked up on every call. */
2236
2236
  getOutboundRtps(): ObservedOutboundRtp[] | undefined;
2237
2237
  getMediaSource(): ObservedMediaSource | undefined;
2238
2238
  /** The capture device behind this track, when the client sent `AUDIO_INPUT_DEVICE` metadata for it. */
@@ -2695,6 +2695,7 @@ declare class ObservedPeerConnection extends EventEmitter {
2695
2695
  private _updateMediaSourceStats;
2696
2696
  private _updateOutboundRtpStats;
2697
2697
  private _updateOutboundTrackSample;
2698
+ private _removeOutboundTrack;
2698
2699
  private _updatePeerConnectionTransportStats;
2699
2700
  private _updateRemoteInboundRtpStats;
2700
2701
  private _updateRemoteOutboundRtpStats;
@@ -4096,6 +4097,7 @@ declare class RemoteTrackResolver {
4096
4097
  readonly observedCall: ObservedCall;
4097
4098
  private readonly resolvers;
4098
4099
  private readonly _publisherIdToOutboundTrack;
4100
+ private readonly _outboundTrackPublisherIds;
4099
4101
  private readonly _subscriberIdToInboundTrack;
4100
4102
  /**
4101
4103
  * Tracks whose publisher id the strategy could not resolve **yet**.
@@ -7234,6 +7236,74 @@ declare class JsonlFileSink<T = ClientSample> extends EventEmitter implements Si
7234
7236
  /** Build a JSONL file sink for a single path. */
7235
7237
  declare function createJsonlFileSink<T = ClientSample>(options: JsonlFileSinkOptions<T>): JsonlFileSink<T>;
7236
7238
 
7239
+ /** The file a `RollingJsonlFileSink` is about to open: the sequence number and when it opens. */
7240
+ type RollingJsonlFileInfo = {
7241
+ /** 0 for the first file, then 1, 2, … */
7242
+ index: number;
7243
+ /** `Date.now()` when the file is opened. */
7244
+ openedAt: number;
7245
+ };
7246
+ type RollingJsonlFileSinkOptions<T = unknown> = Partial<SinkSettings> & {
7247
+ /** The path of each file, called when it is opened. Must give a new path per `index`. */
7248
+ path: (file: RollingJsonlFileInfo) => string;
7249
+ /** Roll before a write that would make the current file larger than this, in bytes. */
7250
+ maxBytes?: number;
7251
+ /** Roll before a write once the current file has been open for this long, in ms. */
7252
+ maxAgeMs?: number;
7253
+ /** Optional custom serializer; defaults to `JSON.stringify`. */
7254
+ serializeSample?: (record: T) => string;
7255
+ };
7256
+ type RollingJsonlFileSinkEvents = {
7257
+ ended: SinkEvents['ended'];
7258
+ close: SinkEvents['close'];
7259
+ error: SinkEvents['error'];
7260
+ finish: SinkEvents['finish'];
7261
+ drain: SinkEvents['drain'];
7262
+ /** A file is complete: flushed and its descriptor closed. Fires for every file, the last one included. */
7263
+ 'file-closed': [path: string, index: number];
7264
+ };
7265
+ declare interface RollingJsonlFileSink {
7266
+ on<E extends keyof RollingJsonlFileSinkEvents>(event: E, listener: (...args: RollingJsonlFileSinkEvents[E]) => void): this;
7267
+ once<E extends keyof RollingJsonlFileSinkEvents>(event: E, listener: (...args: RollingJsonlFileSinkEvents[E]) => void): this;
7268
+ off<E extends keyof RollingJsonlFileSinkEvents>(event: E, listener: (...args: RollingJsonlFileSinkEvents[E]) => void): this;
7269
+ emit<E extends keyof RollingJsonlFileSinkEvents>(event: E, ...args: RollingJsonlFileSinkEvents[E]): boolean;
7270
+ }
7271
+ /**
7272
+ * A JSONL file sink for long-lived owners (e.g. an observer's extension stats over days): it writes
7273
+ * to a sequence of files, closing the current one and opening the next — *rolling* — when it would
7274
+ * exceed `maxBytes`, when it has been open for `maxAgeMs`, or when `roll()` is called. Nothing is
7275
+ * dropped: each record goes to exactly one file.
7276
+ *
7277
+ * The limits are checked on `write`, so a file is never rolled while nothing is written to it, and
7278
+ * an empty file is never rolled. `file-closed` announces each finished file (ready to upload or
7279
+ * move), in index order; `close` fires once, after every file is closed by `end()`.
7280
+ */
7281
+ declare class RollingJsonlFileSink<T = unknown> extends EventEmitter implements Sink<T> {
7282
+ readonly id: string;
7283
+ private readonly _options;
7284
+ private readonly _serializeSample;
7285
+ private _stream;
7286
+ private _path;
7287
+ private _index;
7288
+ private _openedAt;
7289
+ private _bytes;
7290
+ private _ended;
7291
+ private _closing;
7292
+ constructor(options: RollingJsonlFileSinkOptions<T>);
7293
+ /** The file currently written to. */
7294
+ get path(): string;
7295
+ /** The index of the file currently written to. */
7296
+ get index(): number;
7297
+ get ended(): boolean;
7298
+ write(record: T): boolean;
7299
+ /** Close the current file and continue in a new one. A no-op on an empty file or an ended sink. */
7300
+ roll(): void;
7301
+ /** No more writes: emits `ended`, closes the current file (`file-closed`), then emits `close`. */
7302
+ end(): void;
7303
+ private _open;
7304
+ private _closeCurrent;
7305
+ }
7306
+
7237
7307
  declare interface InMemorySink {
7238
7308
  on<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
7239
7309
  once<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
@@ -7260,4 +7330,4 @@ declare function createInMemorySink(samples?: ClientSample[]): InMemorySink;
7260
7330
  declare function createDefaultMediasoupRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
7261
7331
  declare function createP2pRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
7262
7332
 
7263
- 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 };
7333
+ 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 RollingJsonlFileInfo, RollingJsonlFileSink, type RollingJsonlFileSinkEvents, type RollingJsonlFileSinkOptions, 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
@@ -84,6 +84,7 @@ __export(src_exports, {
84
84
  RTT_HISTOGRAM_NUMBER_OF_BUCKETS: () => RTT_HISTOGRAM_NUMBER_OF_BUCKETS,
85
85
  RemoteTrackResolver: () => RemoteTrackResolver,
86
86
  RemoteTrackResolverValidator: () => RemoteTrackResolverValidator,
87
+ RollingJsonlFileSink: () => RollingJsonlFileSink,
87
88
  SCORE_HISTOGRAM_BUCKET_WIDTH: () => SCORE_HISTOGRAM_BUCKET_WIDTH,
88
89
  SCORE_HISTOGRAM_NUMBER_OF_BUCKETS: () => SCORE_HISTOGRAM_NUMBER_OF_BUCKETS,
89
90
  SfuCongestionDetector: () => SfuCongestionDetector,
@@ -1912,19 +1913,17 @@ function isPublicIpAddress(address) {
1912
1913
 
1913
1914
  // src/ObservedOutboundTrack.ts
1914
1915
  var ObservedOutboundTrack = class {
1915
- constructor(timestamp, id, kind, _peerConnection, _outboundRtps, _mediaSource) {
1916
+ constructor(timestamp, id, kind, _peerConnection, _mediaSource) {
1916
1917
  this.timestamp = timestamp;
1917
1918
  this.id = id;
1918
1919
  this.kind = kind;
1919
1920
  this._peerConnection = _peerConnection;
1920
- this._outboundRtps = _outboundRtps;
1921
1921
  this._mediaSource = _mediaSource;
1922
1922
  }
1923
1923
  timestamp;
1924
1924
  id;
1925
1925
  kind;
1926
1926
  _peerConnection;
1927
- _outboundRtps;
1928
1927
  _mediaSource;
1929
1928
  _visited = false;
1930
1929
  appData;
@@ -1975,8 +1974,11 @@ var ObservedOutboundTrack = class {
1975
1974
  getPeerConnection() {
1976
1975
  return this._peerConnection;
1977
1976
  }
1977
+ /** The outbound RTPs of this track's media source, looked up on every call. */
1978
1978
  getOutboundRtps() {
1979
- return this._outboundRtps;
1979
+ const mediaSource = this._mediaSource;
1980
+ if (!mediaSource) return void 0;
1981
+ return [...this._peerConnection.observedOutboundRtps.values()].filter((rtp) => rtp.mediaSourceId === mediaSource.id);
1980
1982
  }
1981
1983
  getMediaSource() {
1982
1984
  return this._mediaSource;
@@ -1995,7 +1997,7 @@ var ObservedOutboundTrack = class {
1995
1997
  this.remoteFractionLost = void 0;
1996
1998
  this.remoteRttInMs = void 0;
1997
1999
  this.qualityLimitationReason = void 0;
1998
- for (const rtp of this._outboundRtps ?? []) {
2000
+ for (const rtp of this.getOutboundRtps() ?? []) {
1999
2001
  this.bitrate = (this.bitrate ?? 0) + rtp.bitrate;
2000
2002
  this.deltaPacketsSent = (this.deltaPacketsSent ?? 0) + rtp.deltaPacketsSent;
2001
2003
  if (rtp.remoteFractionLost !== void 0) {
@@ -2660,9 +2662,7 @@ var ObservedPeerConnection = class extends import_node_events2.EventEmitter {
2660
2662
  }
2661
2663
  for (const observedOutboundTrack of [...this.observedOutboundTracks.values()]) {
2662
2664
  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 });
2665
+ this._removeOutboundTrack(observedOutboundTrack);
2666
2666
  }
2667
2667
  for (const observedPeerConnectionTransport of [...this.observedPeerConnectionTransports.values()]) {
2668
2668
  if (observedPeerConnectionTransport.visited) continue;
@@ -2923,6 +2923,11 @@ var ObservedPeerConnection = class extends import_node_events2.EventEmitter {
2923
2923
  }
2924
2924
  _updateOutboundTrackSample(stats) {
2925
2925
  let observedOutboundTrack = this.observedOutboundTracks.get(stats.id);
2926
+ const mediaSource = [...this.observedMediaSources.values()].filter((source) => source.trackIdentifier === stats.id).pop();
2927
+ if (observedOutboundTrack && observedOutboundTrack.getMediaSource() !== mediaSource) {
2928
+ this._removeOutboundTrack(observedOutboundTrack);
2929
+ observedOutboundTrack = void 0;
2930
+ }
2926
2931
  if (!observedOutboundTrack) {
2927
2932
  if (!stats.timestamp || !stats.id || !stats.kind) {
2928
2933
  return logger3.warn(
@@ -2930,15 +2935,12 @@ var ObservedPeerConnection = class extends import_node_events2.EventEmitter {
2930
2935
  stats
2931
2936
  );
2932
2937
  }
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
2938
  observedOutboundTrack = new ObservedOutboundTrack(
2936
2939
  stats.timestamp,
2937
2940
  stats.id,
2938
2941
  stats.kind,
2939
2942
  this,
2940
- outboundRtps,
2941
- observedMediaSource
2943
+ mediaSource
2942
2944
  );
2943
2945
  observedOutboundTrack.update(stats);
2944
2946
  this.observedOutboundTracks.set(stats.id, observedOutboundTrack);
@@ -2949,6 +2951,11 @@ var ObservedPeerConnection = class extends import_node_events2.EventEmitter {
2949
2951
  this._notify("outbound-track-updated", { ...this.eventScope, observedOutboundTrack });
2950
2952
  return observedOutboundTrack;
2951
2953
  }
2954
+ _removeOutboundTrack(observedOutboundTrack) {
2955
+ this.observedOutboundTracks.delete(observedOutboundTrack.id);
2956
+ this.emit("removed-outbound-track", observedOutboundTrack);
2957
+ this._notify("outbound-track-removed", { ...this.eventScope, observedOutboundTrack });
2958
+ }
2952
2959
  _updatePeerConnectionTransportStats(stats) {
2953
2960
  let observedPeerConnectionTransport = this.observedPeerConnectionTransports.get(stats.id);
2954
2961
  if (!observedPeerConnectionTransport) {
@@ -8113,9 +8120,96 @@ var TrendTester = class {
8113
8120
  }
8114
8121
  };
8115
8122
 
8116
- // src/sinks/InMemorySink.ts
8123
+ // src/sinks/RollingJsonlFileSink.ts
8124
+ var fs2 = __toESM(require("fs"));
8117
8125
  var import_node_events8 = require("events");
8118
- var InMemorySink = class extends import_node_events8.EventEmitter {
8126
+ var RollingJsonlFileSink = class extends import_node_events8.EventEmitter {
8127
+ id;
8128
+ _options;
8129
+ _serializeSample;
8130
+ _stream;
8131
+ _path;
8132
+ _index = -1;
8133
+ _openedAt = 0;
8134
+ _bytes = 0;
8135
+ _ended = false;
8136
+ // Resolves once every file closed so far has closed and been announced, in index order: the
8137
+ // streams close independently, so a later file could otherwise be announced before an earlier one.
8138
+ _closing = Promise.resolve();
8139
+ constructor(options) {
8140
+ super();
8141
+ this._options = options;
8142
+ this._serializeSample = options.serializeSample ?? ((record) => JSON.stringify(record));
8143
+ this._open();
8144
+ this.id = options.id ?? this._path;
8145
+ }
8146
+ /** The file currently written to. */
8147
+ get path() {
8148
+ return this._path;
8149
+ }
8150
+ /** The index of the file currently written to. */
8151
+ get index() {
8152
+ return this._index;
8153
+ }
8154
+ get ended() {
8155
+ return this._ended;
8156
+ }
8157
+ write(record) {
8158
+ if (this._ended) return false;
8159
+ const line = `${this._serializeSample(record)}
8160
+ `;
8161
+ const bytes = Buffer.byteLength(line);
8162
+ const { maxBytes, maxAgeMs } = this._options;
8163
+ if (maxAgeMs !== void 0 && maxAgeMs <= Date.now() - this._openedAt) this.roll();
8164
+ if (maxBytes !== void 0 && maxBytes < this._bytes + bytes) this.roll();
8165
+ this._bytes += bytes;
8166
+ return this._stream.write(line);
8167
+ }
8168
+ /** Close the current file and continue in a new one. A no-op on an empty file or an ended sink. */
8169
+ roll() {
8170
+ if (this._ended || this._bytes === 0) return;
8171
+ this._closeCurrent();
8172
+ this._open();
8173
+ }
8174
+ /** No more writes: emits `ended`, closes the current file (`file-closed`), then emits `close`. */
8175
+ end() {
8176
+ if (this._ended) return;
8177
+ this._ended = true;
8178
+ this.emit("ended");
8179
+ this._closeCurrent();
8180
+ void this._closing.then(() => {
8181
+ this.emit("finish");
8182
+ this.emit("close");
8183
+ });
8184
+ }
8185
+ _open() {
8186
+ ++this._index;
8187
+ this._openedAt = Date.now();
8188
+ this._bytes = 0;
8189
+ this._path = this._options.path({ index: this._index, openedAt: this._openedAt });
8190
+ const stream = fs2.createWriteStream(this._path, { flags: "a" });
8191
+ stream.on("drain", () => this.emit("drain"));
8192
+ stream.on("error", (err) => this.emit("error", err));
8193
+ this._stream = stream;
8194
+ }
8195
+ _closeCurrent() {
8196
+ const stream = this._stream;
8197
+ const path = this._path;
8198
+ const index = this._index;
8199
+ const closed = new Promise((resolve) => {
8200
+ stream.once("close", () => resolve());
8201
+ stream.once("error", () => resolve());
8202
+ });
8203
+ this._closing = Promise.all([this._closing, closed]).then(() => {
8204
+ this.emit("file-closed", path, index);
8205
+ });
8206
+ stream.end();
8207
+ }
8208
+ };
8209
+
8210
+ // src/sinks/InMemorySink.ts
8211
+ var import_node_events9 = require("events");
8212
+ var InMemorySink = class extends import_node_events9.EventEmitter {
8119
8213
  id;
8120
8214
  samples;
8121
8215
  _ended = false;
@@ -8178,6 +8272,7 @@ var RemoteTrackResolver = class {
8178
8272
  observer.off("outbound-track-updated", onOutboundUpdated);
8179
8273
  this._pendingInboundTracks.clear();
8180
8274
  this._pendingOutboundTracks.clear();
8275
+ this._outboundTrackPublisherIds.clear();
8181
8276
  });
8182
8277
  observer.on("inbound-track-added", onInboundAdded);
8183
8278
  observer.on("inbound-track-removed", onInboundRemoved);
@@ -8189,6 +8284,8 @@ var RemoteTrackResolver = class {
8189
8284
  observedCall;
8190
8285
  resolvers;
8191
8286
  _publisherIdToOutboundTrack = /* @__PURE__ */ new Map();
8287
+ // The publisher id each outbound track was indexed under, so removal does not depend on its current attachments.
8288
+ _outboundTrackPublisherIds = /* @__PURE__ */ new Map();
8192
8289
  _subscriberIdToInboundTrack = /* @__PURE__ */ new Map();
8193
8290
  /**
8194
8291
  * Tracks whose publisher id the strategy could not resolve **yet**.
@@ -8264,6 +8361,7 @@ var RemoteTrackResolver = class {
8264
8361
  }
8265
8362
  this._pendingOutboundTracks.delete(outboundTrack);
8266
8363
  this._publisherIdToOutboundTrack.set(publisherId, outboundTrack);
8364
+ this._outboundTrackPublisherIds.set(outboundTrack, publisherId);
8267
8365
  this.observedCall.unconsumedOutboundTracks.add(outboundTrack);
8268
8366
  for (const observedClient of this.observedCall.observedClients.values()) {
8269
8367
  for (const observedPeerConnection of observedClient.observedPeerConnections.values()) {
@@ -8278,7 +8376,8 @@ var RemoteTrackResolver = class {
8278
8376
  }
8279
8377
  _removeOutboundTrack(outboundTrack) {
8280
8378
  this._pendingOutboundTracks.delete(outboundTrack);
8281
- const publisherId = this.resolvers.resolveOutboundTrackPublisherId(outboundTrack);
8379
+ const publisherId = this._outboundTrackPublisherIds.get(outboundTrack);
8380
+ this._outboundTrackPublisherIds.delete(outboundTrack);
8282
8381
  if (publisherId && this._publisherIdToOutboundTrack.get(publisherId) === outboundTrack) {
8283
8382
  this._publisherIdToOutboundTrack.delete(publisherId);
8284
8383
  }
@@ -8371,6 +8470,7 @@ function createP2pRemoteTrackResolverFactory() {
8371
8470
  RTT_HISTOGRAM_NUMBER_OF_BUCKETS,
8372
8471
  RemoteTrackResolver,
8373
8472
  RemoteTrackResolverValidator,
8473
+ RollingJsonlFileSink,
8374
8474
  SCORE_HISTOGRAM_BUCKET_WIDTH,
8375
8475
  SCORE_HISTOGRAM_NUMBER_OF_BUCKETS,
8376
8476
  SfuCongestionDetector,