@observertc/observer-js 1.0.0-beta.11 → 1.0.0-beta.13
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 +76 -42
- package/dist/index.d.mts +16 -4
- package/dist/index.d.ts +16 -4
- package/dist/index.js +45 -117
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +45 -117
- package/dist/index.mjs.map +1 -1
- package/llms-full.txt +77 -43
- package/package.json +2 -3
package/llms-full.txt
CHANGED
|
@@ -257,8 +257,7 @@ observer.addAcceptMiddleware(route, filter);
|
|
|
257
257
|
// observer.removeAcceptMiddleware(route);
|
|
258
258
|
```
|
|
259
259
|
|
|
260
|
-
This is a lightweight global injection point
|
|
261
|
-
`ClientSampleProcessor` pipeline in the roadmap. When no middleware is registered, `accept()`
|
|
260
|
+
This is a lightweight global injection point. When no middleware is registered, `accept()`
|
|
262
261
|
dispatches directly with no overhead.
|
|
263
262
|
|
|
264
263
|
### `context` (the `AcceptContext`)
|
|
@@ -421,7 +420,6 @@ See [Mediasoup router observation](#mediasoup-router-observation) for the full d
|
|
|
421
420
|
| `peer-connection-added` / `peer-connection-closed` | — | lifecycle of the PC |
|
|
422
421
|
| `peer-connection-updated` | `{ context?: AcceptContext }` | the PC processed a sample |
|
|
423
422
|
| `ice-connection-state-changed` / `ice-gathering-state-changed` / `connection-state-changed` | `{ state: string }` | driven by client events |
|
|
424
|
-
| `selected-candidate-pair-changed` | — | *declared; not currently emitted* |
|
|
425
423
|
| `inbound-track-added` / `-updated` / `-removed` / `-muted` / `-unmuted` | `{ observedInboundTrack }` | |
|
|
426
424
|
| `outbound-track-added` / `-updated` / `-removed` / `-muted` / `-unmuted` | `{ observedOutboundTrack }` | |
|
|
427
425
|
| `inbound-rtp-added` / `-updated` / `-removed` | `{ observedInboundRtp }` | `-updated` fires every tick |
|
|
@@ -474,8 +472,8 @@ type ObserverConfig<AppData = Record<string, unknown>> = {
|
|
|
474
472
|
createClientAppData?: (p: { clientId: string; observedCall: ObservedCall }) => Record<string, unknown>;
|
|
475
473
|
// sink factory — produces a per-client sink that receives every accepted sample (see Sinks).
|
|
476
474
|
createClientSink?: (p: { clientId: string; observedCall: ObservedCall }) => ClientSampleSink | undefined;
|
|
477
|
-
// track-resolver factory — produces a call's RemoteTrackResolver (see Remote track resolution).
|
|
478
|
-
|
|
475
|
+
// remote-track-resolver factory — produces a call's RemoteTrackResolver (see Remote track resolution).
|
|
476
|
+
createRemoteTrackResolver?: (observedCall: ObservedCall) => RemoteTrackResolver | undefined;
|
|
479
477
|
};
|
|
480
478
|
```
|
|
481
479
|
|
|
@@ -529,7 +527,7 @@ Key members:
|
|
|
529
527
|
- `addIssue(issue: ClientIssue): void` — raise a **call-level** issue → emits `call-issue`
|
|
530
528
|
- `readonly detectors: Detectors` — server-side detector registry (empty by default; see [Detectors](#detectors-server-side-extension-point))
|
|
531
529
|
- `scoreCalculator: ScoreCalculator`, `get score()`, `readonly calculatedScore`
|
|
532
|
-
- `remoteTrackResolver?: RemoteTrackResolver` — set from `ObserverConfig.
|
|
530
|
+
- `remoteTrackResolver?: RemoteTrackResolver` — set from `ObserverConfig.createRemoteTrackResolver` at call creation (see [Remote track resolution](#remote-track-resolution-mediasoup--sfu))
|
|
533
531
|
- aggregates: `numberOfIssues`, `numberOfPeerConnections`, `numberOfInboundRtpStreams`,
|
|
534
532
|
`numberOfOutboundRtpStreams`, `numberOfDataChannels`, `maxNumberOfClients`,
|
|
535
533
|
`clientsUsedTurn: Set<string>`, `startedAt?`, `endedAt?`, `closedAt?`, `closed`
|
|
@@ -764,7 +762,7 @@ class Detectors {
|
|
|
764
762
|
|
|
765
763
|
In an SFU, one participant's **outbound** track is delivered to other participants as **inbound**
|
|
766
764
|
tracks (one **publisher** → many **subscribers**). Correlation is **opt-in** per observer: set
|
|
767
|
-
`ObserverConfig.
|
|
765
|
+
`ObserverConfig.createRemoteTrackResolver`, a factory invoked when each call is created that returns the
|
|
768
766
|
call's `RemoteTrackResolver` (or `undefined` for none).
|
|
769
767
|
|
|
770
768
|
`RemoteTrackResolver` is a generic, strategy-driven class. It subscribes to the bus (filtered to
|
|
@@ -775,7 +773,7 @@ the tracks: `inboundTrack.remoteOutboundTrack` and `outboundTrack.remoteInboundT
|
|
|
775
773
|
import { Observer, createDefaultMediasoupRemoteTrackResolverFactory } from '@observertc/observer-js';
|
|
776
774
|
|
|
777
775
|
const observer = new Observer({
|
|
778
|
-
|
|
776
|
+
createRemoteTrackResolver: createDefaultMediasoupRemoteTrackResolverFactory(),
|
|
779
777
|
});
|
|
780
778
|
|
|
781
779
|
// later, given tracks (links are kept up to date as tracks come and go):
|
|
@@ -794,7 +792,7 @@ id is just whatever links a subscribed track to the published one:
|
|
|
794
792
|
import { Observer, RemoteTrackResolver } from '@observertc/observer-js';
|
|
795
793
|
|
|
796
794
|
const observer = new Observer({
|
|
797
|
-
|
|
795
|
+
createRemoteTrackResolver: (observedCall) => new RemoteTrackResolver(observedCall, {
|
|
798
796
|
resolveOutboundTrackPublisherId: (out) => out.attachments?.mediaId as string | undefined,
|
|
799
797
|
resolveInboundTrackPublisherId: (inb) => inb.attachments?.mediaId as string | undefined,
|
|
800
798
|
resolveInboundTrackSubscriberId: (inb) => inb.attachments?.subId as string | undefined, // optional
|
|
@@ -818,19 +816,37 @@ transitions. `ObservedMediasoupRouter` captures that server-side view into a
|
|
|
818
816
|
### The concept
|
|
819
817
|
|
|
820
818
|
You hand the observer a live mediasoup `Router`; it attaches to mediasoup's own `observer` API and,
|
|
821
|
-
from then on, **passively
|
|
819
|
+
from then on, **passively tracks** the router's topology and lifecycle — with no polling and no
|
|
822
820
|
changes to your media code:
|
|
823
821
|
|
|
824
|
-
- new transports (`webrtc` / `plain` / `pipe` / `direct`), their selected `tuple`,
|
|
825
|
-
transitions
|
|
822
|
+
- new transports (`webrtc` / `plain` / `pipe` / `direct`), their selected `tuple`, ICE/DTLS/SCTP
|
|
823
|
+
state transitions and `connectedAt`;
|
|
826
824
|
- producers (codec, SSRCs/RIDs, `pause`/`resume`) and consumers (`pause`/`resume`,
|
|
827
825
|
`producerPaused`/`producerResumed`);
|
|
828
826
|
- data producers and data consumers;
|
|
829
827
|
- `createdAt` / `closedAt` for every entity above.
|
|
830
828
|
|
|
831
|
-
|
|
832
|
-
[`src/schema/MediasoupRouter.ts`](./src/schema/MediasoupRouter.ts)
|
|
833
|
-
|
|
829
|
+
It keeps all of this **in memory**, in a single `MediasoupRouterSample` exposed as
|
|
830
|
+
`observedRouter.sample` — see [`src/schema/MediasoupRouter.ts`](./src/schema/MediasoupRouter.ts). The
|
|
831
|
+
sample **accumulates for the life of the router**: closed transports/producers/consumers are kept
|
|
832
|
+
(with their `closedAt` set), not removed. Read it whenever you like — it's a plain object you own.
|
|
833
|
+
|
|
834
|
+
### Memory & large meetings
|
|
835
|
+
|
|
836
|
+
This is intentionally the **simplest** approach — everything lives in memory and nothing is sampled
|
|
837
|
+
or evicted for you. That's fine for typical rooms, but be aware of the cost at scale:
|
|
838
|
+
|
|
839
|
+
- **Consumers grow as O(N²)** on a single flat router: with `N` participants each producing audio +
|
|
840
|
+
video and consuming everyone else, the sample holds roughly `2·N·(N−1)` consumer records (≈ 19,800
|
|
841
|
+
for `N` = 100).
|
|
842
|
+
- The sample is **cumulative** — closed entities and their `history` are retained — so it also grows
|
|
843
|
+
with call duration and churn (renegotiation, simulcast layer changes, rejoins).
|
|
844
|
+
|
|
845
|
+
A 100-participant flat router can therefore reach tens of MB and keep growing. There is **no built-in
|
|
846
|
+
sink, snapshotting, or eviction** — by design. **If you run large meetings, do your own sampling:**
|
|
847
|
+
on your own cadence read `observedRouter.sample` (snapshot/serialize/persist what you need), drop what
|
|
848
|
+
you don't, and close routers you no longer track. (mediasoup also typically shards routers across
|
|
849
|
+
workers/cores, which keeps any one router small.)
|
|
834
850
|
|
|
835
851
|
### Matching peer connections — by **event**, not by storage
|
|
836
852
|
|
|
@@ -853,32 +869,54 @@ router serving many participants emits one match per participant's transport. Wh
|
|
|
853
869
|
or `false`, no matching is performed and the event never fires. The internal listener is removed
|
|
854
870
|
automatically when the router closes or the observer closes.
|
|
855
871
|
|
|
872
|
+
### Ordering contract — observe the router first
|
|
873
|
+
|
|
874
|
+
Matching is **forward-only by design**, and that is sufficient because the lifecycle ordering is
|
|
875
|
+
**guaranteed, not racy**:
|
|
876
|
+
|
|
877
|
+
- `ObservedMediasoupRouter` works purely by **subscribing to mediasoup's `observer` API**, so it can
|
|
878
|
+
only see events that happen *after* it is created. You therefore create it the moment the router
|
|
879
|
+
exists — **before** any transport is added to it — and it captures the rest going forward.
|
|
880
|
+
- A mediasoup transport is always created **on the server first**; only then can the client connect
|
|
881
|
+
to it, produce/consume, and begin shipping `ClientSample`s. So a peer connection — and the
|
|
882
|
+
`peer-connection-added` event it triggers — can never appear before its server-side WebRTC
|
|
883
|
+
transport already exists (and has been observed by the router).
|
|
884
|
+
|
|
885
|
+
Put together: by the time a `peer-connection-added` fires, the router has already recorded that
|
|
886
|
+
transport's id in `webrtcTransportIds`, so a single forward-looking listener catches every match. No
|
|
887
|
+
back-scan of existing peer connections and no re-check on transport creation are needed — the
|
|
888
|
+
observer deliberately does **not** look backwards.
|
|
889
|
+
|
|
890
|
+
**Your responsibility:** call `createObservedMediasoupRouter(...)` as early as the router exists
|
|
891
|
+
(before transports are added or samples are accepted). If you register the router *after* its
|
|
892
|
+
transports are created or after the client's first sample, those events are already in the past and
|
|
893
|
+
the corresponding matches are missed.
|
|
894
|
+
|
|
856
895
|
When the underlying mediasoup router closes, its `close` propagates to `ObservedMediasoupRouter`,
|
|
857
|
-
which emits **`mediasoup-router-removed
|
|
858
|
-
|
|
896
|
+
which sets the sample's `closedAt` and emits **`mediasoup-router-removed`** — your cue to read /
|
|
897
|
+
persist the final `observedRouter.sample` and drop your reference to it.
|
|
859
898
|
|
|
860
899
|
### Options — `observer.createObservedMediasoupRouter(settings)`
|
|
861
900
|
|
|
862
901
|
| Field | Type | Required | Meaning |
|
|
863
902
|
|-------|------|----------|---------|
|
|
864
|
-
| `router` | `mediasoup.types.Router` | yes | the live router to observe; the observer attaches to `router.observer` |
|
|
865
|
-
| `routerId` | `string` | yes | your id for the router (the sample also carries `router.id`) |
|
|
903
|
+
| `router` | `mediasoup.types.Router` | yes | the live router to observe; the observer attaches to `router.observer`. `.id` and the sample's `routerId` come from `router.id` |
|
|
866
904
|
| `appData` | `Record<string, unknown>` | no | application-owned bag on the `ObservedMediasoupRouter` |
|
|
867
|
-
| `attachments` | `Record<string, unknown>` | no | free-form data
|
|
905
|
+
| `attachments` | `Record<string, unknown>` | no | free-form data; carried on `sample.attachments` |
|
|
868
906
|
| `matchPeerConnectionByWebRtcTransportId` | `boolean` | no | opt in to peer-connection matching: emit `mediasoup-router-matched-with-peer-connection` for each peer connection whose id matches one of the router's WebRTC transport ids. Omitted / `false` → no matching, the event never fires |
|
|
869
907
|
|
|
870
908
|
Peer-connection matching is **off by default**; enable it with
|
|
871
909
|
`matchPeerConnectionByWebRtcTransportId: true`. Returns the `ObservedMediasoupRouter`, or `undefined`
|
|
872
910
|
if the observer is closed (a router with the same id returns the existing instance — both warn).
|
|
873
911
|
|
|
874
|
-
Useful members on the returned object: `.sample` (the `MediasoupRouterSample
|
|
875
|
-
|
|
876
|
-
`.close()`.
|
|
912
|
+
Useful members on the returned object: `.sample` (the in-memory `MediasoupRouterSample`, with
|
|
913
|
+
`createdAt` / `closedAt?` on it), `.appData`, `.attachments`, `.webrtcTransportIds: Set<string>`,
|
|
914
|
+
`.id`, `.close()`.
|
|
877
915
|
|
|
878
916
|
### Example
|
|
879
917
|
|
|
880
918
|
```ts
|
|
881
|
-
import { Observer } from '@observertc/observer-js';
|
|
919
|
+
import { Observer, InMemorySink } from '@observertc/observer-js';
|
|
882
920
|
import type { ObservedMediasoupRouterScope, ObservedPeerConnectionScope } from '@observertc/observer-js';
|
|
883
921
|
|
|
884
922
|
const observer = new Observer();
|
|
@@ -886,46 +924,42 @@ const observer = new Observer();
|
|
|
886
924
|
// 1) Feed client samples as usual so the observer knows about calls, clients & peer connections.
|
|
887
925
|
// (e.g. transport-layer: observer.accept(clientSample, context))
|
|
888
926
|
|
|
889
|
-
// 2) Observe the SFU side
|
|
927
|
+
// 2) Observe the SFU side; opt in to peer-connection matching. State accumulates in `.sample`.
|
|
890
928
|
const router = /* your mediasoup router */ undefined as any;
|
|
891
929
|
const observedRouter = observer.createObservedMediasoupRouter({
|
|
892
930
|
router,
|
|
893
|
-
routerId: router.id,
|
|
894
931
|
matchPeerConnectionByWebRtcTransportId: true,
|
|
895
932
|
});
|
|
896
933
|
|
|
934
|
+
// For large meetings, sample it yourself on your own cadence (see "Memory & large meetings"):
|
|
935
|
+
// setInterval(() => persist(observedRouter.sample), 10_000);
|
|
936
|
+
|
|
897
937
|
// 3) Every peer connection whose id matches one of the router's WebRTC transport ids fires this —
|
|
898
938
|
// WE decide what to do with each pairing. The payload carries the full ancestry.
|
|
899
939
|
observer.on('mediasoup-router-matched-with-peer-connection',
|
|
900
|
-
({ observedMediasoupRouter, observedCall,
|
|
940
|
+
({ observedMediasoupRouter, observedCall, observedPeerConnection }:
|
|
901
941
|
ObservedMediasoupRouterScope & ObservedPeerConnectionScope) => {
|
|
902
|
-
// e.g. remember which router serves this peer connection / client…
|
|
903
942
|
(observedPeerConnection.appData ??= {}).routerId = observedMediasoupRouter.id;
|
|
904
|
-
|
|
905
|
-
myStore.linkRouterToCall(observedCall.callId, observedMediasoupRouter.sample);
|
|
943
|
+
myStore.linkRouterToCall(observedCall.callId, observedMediasoupRouter.id);
|
|
906
944
|
},
|
|
907
945
|
);
|
|
908
946
|
|
|
909
|
-
// 4) The router closed —
|
|
947
|
+
// 4) The router closed — read/persist the final state, then drop your reference.
|
|
910
948
|
observer.on('mediasoup-router-removed', ({ observedMediasoupRouter }: ObservedMediasoupRouterScope) => {
|
|
911
|
-
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
// (optional) react to the router being registered at all:
|
|
915
|
-
observer.on('mediasoup-router-added', ({ observedMediasoupRouter }) => {
|
|
916
|
-
console.log('observing router', observedMediasoupRouter.id);
|
|
949
|
+
persist(observedMediasoupRouter.sample); // its `closedAt` is set
|
|
917
950
|
});
|
|
918
951
|
```
|
|
919
952
|
|
|
920
|
-
### Why event-driven instead of storing on the call
|
|
953
|
+
### Why event-driven matching instead of storing on the call
|
|
921
954
|
|
|
922
955
|
- **Loose coupling.** The call model stays about client telemetry; the SFU view lives on its own
|
|
923
|
-
|
|
956
|
+
`ObservedMediasoupRouter` and is associated only if and how *you* choose.
|
|
924
957
|
- **You own the association.** One router serves many peer connections (across clients and calls),
|
|
925
958
|
and the right place to keep that mapping is application-specific — so the observer hands you each
|
|
926
|
-
peer-connection match and
|
|
927
|
-
- **
|
|
928
|
-
|
|
959
|
+
peer-connection match and gets out of the way.
|
|
960
|
+
- **You own the sampling.** The router sample is plain in-memory state you read on your own terms;
|
|
961
|
+
for large meetings, sample/persist it yourself (see [Memory & large meetings](#memory--large-meetings))
|
|
962
|
+
rather than relying on the library to evict — it deliberately doesn't.
|
|
929
963
|
|
|
930
964
|
---
|
|
931
965
|
|
|
@@ -1553,7 +1587,7 @@ the 1.0.0 API provides.
|
|
|
1553
1587
|
track to the subscribed (inbound) tracks carrying it (**one publisher → many subscribers**) by a
|
|
1554
1588
|
**publisher id** (the link key). Links are maintained directly on the tracks
|
|
1555
1589
|
(`inboundTrack.remoteOutboundTrack`, `outboundTrack.remoteInboundTracks`).
|
|
1556
|
-
- **Opt-in via `ObserverConfig.
|
|
1590
|
+
- **Opt-in via `ObserverConfig.createRemoteTrackResolver`**, invoked per call. Built-in factories:
|
|
1557
1591
|
`createDefaultMediasoupRemoteTrackResolverFactory()` (producerId/consumerId attachments) and
|
|
1558
1592
|
`createP2pRemoteTrackResolverFactory()` (RTP SSRC). Custom topologies supply their own
|
|
1559
1593
|
publisher/subscriber id resolvers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@observertc/observer-js",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.13",
|
|
4
4
|
"description": "Server-side Node.js library for processing ObserveRTC Samples",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -46,13 +46,12 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@bufbuild/protobuf": "1.1.1",
|
|
48
48
|
"events": "^3.3.0",
|
|
49
|
-
"uuid": "^
|
|
49
|
+
"uuid": "^11.1.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@tsconfig/node20": "^1.0.2",
|
|
53
53
|
"@types/jest": "^27.5.2",
|
|
54
54
|
"@types/node": "^20.9.0",
|
|
55
|
-
"@types/uuid": "^8.3.4",
|
|
56
55
|
"@typescript-eslint/eslint-plugin": "^5.38.0",
|
|
57
56
|
"@typescript-eslint/parser": "^5.38.0",
|
|
58
57
|
"eslint": "^8.24.0",
|