@observertc/observer-js 1.0.0-beta.10 → 1.0.0-beta.12
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 +151 -55
- package/dist/index.d.mts +24 -8
- package/dist/index.d.ts +24 -8
- package/dist/index.js +145 -162
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +145 -162
- package/dist/index.mjs.map +1 -1
- package/llms-full.txt +153 -57
- package/package.json +3 -2
package/llms-full.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# observer-js (@observertc/observer-js) — full documentation
|
|
2
2
|
|
|
3
3
|
> Single-file export of the project's documentation for one-shot LLM ingestion.
|
|
4
|
-
> Concatenates: README.md, docs/logging.md, CHANGELOG.md. Generated
|
|
4
|
+
> Concatenates: README.md, docs/logging.md, CHANGELOG.md. Generated by scripts/build-llms-full.mjs —
|
|
5
5
|
> the canonical, always-current copies live in the repository; see llms.txt for the link index.
|
|
6
6
|
|
|
7
7
|
---
|
|
@@ -75,9 +75,10 @@ and emits a single, unified stream of typed events the application can react to.
|
|
|
75
75
|
11. [Remote track resolution (mediasoup / SFU)](#remote-track-resolution-mediasoup--sfu)
|
|
76
76
|
12. [Mediasoup router observation](#mediasoup-router-observation)
|
|
77
77
|
13. [Sinks (per-client sample persistence)](#sinks-per-client-sample-persistence)
|
|
78
|
-
14. [
|
|
79
|
-
15. [
|
|
80
|
-
16. [
|
|
78
|
+
14. [Injecting data into a client](#injecting-data-into-a-client)
|
|
79
|
+
15. [Logging](#logging)
|
|
80
|
+
16. [Error-handling philosophy](#error-handling-philosophy)
|
|
81
|
+
17. [Development & extension guide](#development--extension-guide)
|
|
81
82
|
|
|
82
83
|
---
|
|
83
84
|
|
|
@@ -256,8 +257,7 @@ observer.addAcceptMiddleware(route, filter);
|
|
|
256
257
|
// observer.removeAcceptMiddleware(route);
|
|
257
258
|
```
|
|
258
259
|
|
|
259
|
-
This is a lightweight global injection point
|
|
260
|
-
`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()`
|
|
261
261
|
dispatches directly with no overhead.
|
|
262
262
|
|
|
263
263
|
### `context` (the `AcceptContext`)
|
|
@@ -381,7 +381,7 @@ additional field(s) on top of that scope.
|
|
|
381
381
|
| Event | Extra | Fires when |
|
|
382
382
|
|-------|-------|-----------|
|
|
383
383
|
| `mediasoup-router-added` | — | `observer.createObservedMediasoupRouter(...)` registered a router |
|
|
384
|
-
| `mediasoup-router-matched-with-peer-connection` | `{ observedCall, observedClient, observedPeerConnection }` | a newly added peer connection's id matched one of the router's WebRTC transport ids. |
|
|
384
|
+
| `mediasoup-router-matched-with-peer-connection` | `{ observedCall, observedClient, observedPeerConnection }` | a newly added peer connection's id matched one of the router's WebRTC transport ids. **Opt-in** via `matchPeerConnectionByWebRtcTransportId: true`. |
|
|
385
385
|
| `mediasoup-router-removed` | — | the underlying mediasoup router closed (its `router.observer` `close` fired) |
|
|
386
386
|
|
|
387
387
|
See [Mediasoup router observation](#mediasoup-router-observation) for the full design and examples.
|
|
@@ -420,7 +420,6 @@ See [Mediasoup router observation](#mediasoup-router-observation) for the full d
|
|
|
420
420
|
| `peer-connection-added` / `peer-connection-closed` | — | lifecycle of the PC |
|
|
421
421
|
| `peer-connection-updated` | `{ context?: AcceptContext }` | the PC processed a sample |
|
|
422
422
|
| `ice-connection-state-changed` / `ice-gathering-state-changed` / `connection-state-changed` | `{ state: string }` | driven by client events |
|
|
423
|
-
| `selected-candidate-pair-changed` | — | *declared; not currently emitted* |
|
|
424
423
|
| `inbound-track-added` / `-updated` / `-removed` / `-muted` / `-unmuted` | `{ observedInboundTrack }` | |
|
|
425
424
|
| `outbound-track-added` / `-updated` / `-removed` / `-muted` / `-unmuted` | `{ observedOutboundTrack }` | |
|
|
426
425
|
| `inbound-rtp-added` / `-updated` / `-removed` | `{ observedInboundRtp }` | `-updated` fires every tick |
|
|
@@ -473,8 +472,8 @@ type ObserverConfig<AppData = Record<string, unknown>> = {
|
|
|
473
472
|
createClientAppData?: (p: { clientId: string; observedCall: ObservedCall }) => Record<string, unknown>;
|
|
474
473
|
// sink factory — produces a per-client sink that receives every accepted sample (see Sinks).
|
|
475
474
|
createClientSink?: (p: { clientId: string; observedCall: ObservedCall }) => ClientSampleSink | undefined;
|
|
476
|
-
// track-resolver factory — produces a call's RemoteTrackResolver (see Remote track resolution).
|
|
477
|
-
|
|
475
|
+
// remote-track-resolver factory — produces a call's RemoteTrackResolver (see Remote track resolution).
|
|
476
|
+
createRemoteTrackResolver?: (observedCall: ObservedCall) => RemoteTrackResolver | undefined;
|
|
478
477
|
};
|
|
479
478
|
```
|
|
480
479
|
|
|
@@ -528,7 +527,7 @@ Key members:
|
|
|
528
527
|
- `addIssue(issue: ClientIssue): void` — raise a **call-level** issue → emits `call-issue`
|
|
529
528
|
- `readonly detectors: Detectors` — server-side detector registry (empty by default; see [Detectors](#detectors-server-side-extension-point))
|
|
530
529
|
- `scoreCalculator: ScoreCalculator`, `get score()`, `readonly calculatedScore`
|
|
531
|
-
- `remoteTrackResolver?: RemoteTrackResolver` — set from `ObserverConfig.
|
|
530
|
+
- `remoteTrackResolver?: RemoteTrackResolver` — set from `ObserverConfig.createRemoteTrackResolver` at call creation (see [Remote track resolution](#remote-track-resolution-mediasoup--sfu))
|
|
532
531
|
- aggregates: `numberOfIssues`, `numberOfPeerConnections`, `numberOfInboundRtpStreams`,
|
|
533
532
|
`numberOfOutboundRtpStreams`, `numberOfDataChannels`, `maxNumberOfClients`,
|
|
534
533
|
`clientsUsedTurn: Set<string>`, `startedAt?`, `endedAt?`, `closedAt?`, `closed`
|
|
@@ -551,7 +550,7 @@ Key members:
|
|
|
551
550
|
- `readonly sink?: ClientSampleSink` — the per-client sink (see [Sinks](#sinks-per-client-sample-persistence)), if `createClientSink` is configured; listen on it for `close`/`error`
|
|
552
551
|
- **Injection API** (queue app data to be merged into the next sample processing):
|
|
553
552
|
`injectEvent(ClientEvent)`, `injectIssue(ClientIssue)`, `injectMetaData(ClientMetaData)`,
|
|
554
|
-
`injectExtensionStat(ExtensionStat)`, `injectAttachment(
|
|
553
|
+
`injectExtensionStat(ExtensionStat)`, `injectAttachment(attachments: Record<string, unknown>)`
|
|
555
554
|
- **Direct add API** (process immediately): `addIssue(ClientIssue)`, `addMetadata(ClientMetaData)`,
|
|
556
555
|
`addExtensionStats(ExtensionStat)`
|
|
557
556
|
- Metrics (current/derived): `currentAvgRttInMs?`, `currentMinRttInMs?`, `currentMaxRttInMs?`,
|
|
@@ -763,7 +762,7 @@ class Detectors {
|
|
|
763
762
|
|
|
764
763
|
In an SFU, one participant's **outbound** track is delivered to other participants as **inbound**
|
|
765
764
|
tracks (one **publisher** → many **subscribers**). Correlation is **opt-in** per observer: set
|
|
766
|
-
`ObserverConfig.
|
|
765
|
+
`ObserverConfig.createRemoteTrackResolver`, a factory invoked when each call is created that returns the
|
|
767
766
|
call's `RemoteTrackResolver` (or `undefined` for none).
|
|
768
767
|
|
|
769
768
|
`RemoteTrackResolver` is a generic, strategy-driven class. It subscribes to the bus (filtered to
|
|
@@ -774,7 +773,7 @@ the tracks: `inboundTrack.remoteOutboundTrack` and `outboundTrack.remoteInboundT
|
|
|
774
773
|
import { Observer, createDefaultMediasoupRemoteTrackResolverFactory } from '@observertc/observer-js';
|
|
775
774
|
|
|
776
775
|
const observer = new Observer({
|
|
777
|
-
|
|
776
|
+
createRemoteTrackResolver: createDefaultMediasoupRemoteTrackResolverFactory(),
|
|
778
777
|
});
|
|
779
778
|
|
|
780
779
|
// later, given tracks (links are kept up to date as tracks come and go):
|
|
@@ -793,7 +792,7 @@ id is just whatever links a subscribed track to the published one:
|
|
|
793
792
|
import { Observer, RemoteTrackResolver } from '@observertc/observer-js';
|
|
794
793
|
|
|
795
794
|
const observer = new Observer({
|
|
796
|
-
|
|
795
|
+
createRemoteTrackResolver: (observedCall) => new RemoteTrackResolver(observedCall, {
|
|
797
796
|
resolveOutboundTrackPublisherId: (out) => out.attachments?.mediaId as string | undefined,
|
|
798
797
|
resolveInboundTrackPublisherId: (inb) => inb.attachments?.mediaId as string | undefined,
|
|
799
798
|
resolveInboundTrackSubscriberId: (inb) => inb.attachments?.subId as string | undefined, // optional
|
|
@@ -817,19 +816,37 @@ transitions. `ObservedMediasoupRouter` captures that server-side view into a
|
|
|
817
816
|
### The concept
|
|
818
817
|
|
|
819
818
|
You hand the observer a live mediasoup `Router`; it attaches to mediasoup's own `observer` API and,
|
|
820
|
-
from then on, **passively
|
|
819
|
+
from then on, **passively tracks** the router's topology and lifecycle — with no polling and no
|
|
821
820
|
changes to your media code:
|
|
822
821
|
|
|
823
|
-
- new transports (`webrtc` / `plain` / `pipe` / `direct`), their selected `tuple`,
|
|
824
|
-
transitions
|
|
822
|
+
- new transports (`webrtc` / `plain` / `pipe` / `direct`), their selected `tuple`, ICE/DTLS/SCTP
|
|
823
|
+
state transitions and `connectedAt`;
|
|
825
824
|
- producers (codec, SSRCs/RIDs, `pause`/`resume`) and consumers (`pause`/`resume`,
|
|
826
825
|
`producerPaused`/`producerResumed`);
|
|
827
826
|
- data producers and data consumers;
|
|
828
827
|
- `createdAt` / `closedAt` for every entity above.
|
|
829
828
|
|
|
830
|
-
|
|
831
|
-
[`src/schema/MediasoupRouter.ts`](./src/schema/MediasoupRouter.ts)
|
|
832
|
-
|
|
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.)
|
|
833
850
|
|
|
834
851
|
### Matching peer connections — by **event**, not by storage
|
|
835
852
|
|
|
@@ -844,37 +861,62 @@ ancestry, so you get the router **and** the matched `observedPeerConnection`, `o
|
|
|
844
861
|
`observedCall` in one place. Stamp the `routerId` into the peer connection's / client's `appData`,
|
|
845
862
|
build your own index, attach the server sample to the call in your database — whatever fits.
|
|
846
863
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
864
|
+
This matching is **opt-in**: pass `matchPeerConnectionByWebRtcTransportId: true` to
|
|
865
|
+
`createObservedMediasoupRouter`. When enabled, as peer connections are observed
|
|
866
|
+
(`peer-connection-added`) the observer checks whether the peer connection's id is one of the router's
|
|
867
|
+
WebRTC transport ids; on a hit it emits — once per matching peer connection — and keeps watching, so a
|
|
868
|
+
router serving many participants emits one match per participant's transport. When the flag is omitted
|
|
869
|
+
or `false`, no matching is performed and the event never fires. The internal listener is removed
|
|
870
|
+
automatically when the router closes or the observer closes.
|
|
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.
|
|
852
894
|
|
|
853
895
|
When the underlying mediasoup router closes, its `close` propagates to `ObservedMediasoupRouter`,
|
|
854
|
-
which emits **`mediasoup-router-removed
|
|
855
|
-
|
|
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.
|
|
856
898
|
|
|
857
899
|
### Options — `observer.createObservedMediasoupRouter(settings)`
|
|
858
900
|
|
|
859
901
|
| Field | Type | Required | Meaning |
|
|
860
902
|
|-------|------|----------|---------|
|
|
861
|
-
| `router` | `mediasoup.types.Router` | yes | the live router to observe; the observer attaches to `router.observer` |
|
|
862
|
-
| `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` |
|
|
863
904
|
| `appData` | `Record<string, unknown>` | no | application-owned bag on the `ObservedMediasoupRouter` |
|
|
864
|
-
| `attachments` | `Record<string, unknown>` | no | free-form data
|
|
905
|
+
| `attachments` | `Record<string, unknown>` | no | free-form data; carried on `sample.attachments` |
|
|
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 |
|
|
865
907
|
|
|
866
|
-
Peer-connection matching is
|
|
867
|
-
`ObservedMediasoupRouter`, or `undefined`
|
|
868
|
-
returns the existing instance — both warn).
|
|
908
|
+
Peer-connection matching is **off by default**; enable it with
|
|
909
|
+
`matchPeerConnectionByWebRtcTransportId: true`. Returns the `ObservedMediasoupRouter`, or `undefined`
|
|
910
|
+
if the observer is closed (a router with the same id returns the existing instance — both warn).
|
|
869
911
|
|
|
870
|
-
Useful members on the returned object: `.sample` (the `MediasoupRouterSample
|
|
871
|
-
|
|
872
|
-
`.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()`.
|
|
873
915
|
|
|
874
916
|
### Example
|
|
875
917
|
|
|
876
918
|
```ts
|
|
877
|
-
import { Observer } from '@observertc/observer-js';
|
|
919
|
+
import { Observer, InMemorySink } from '@observertc/observer-js';
|
|
878
920
|
import type { ObservedMediasoupRouterScope, ObservedPeerConnectionScope } from '@observertc/observer-js';
|
|
879
921
|
|
|
880
922
|
const observer = new Observer();
|
|
@@ -882,42 +924,42 @@ const observer = new Observer();
|
|
|
882
924
|
// 1) Feed client samples as usual so the observer knows about calls, clients & peer connections.
|
|
883
925
|
// (e.g. transport-layer: observer.accept(clientSample, context))
|
|
884
926
|
|
|
885
|
-
// 2) Observe the SFU side
|
|
927
|
+
// 2) Observe the SFU side; opt in to peer-connection matching. State accumulates in `.sample`.
|
|
886
928
|
const router = /* your mediasoup router */ undefined as any;
|
|
887
|
-
const observedRouter = observer.createObservedMediasoupRouter({
|
|
929
|
+
const observedRouter = observer.createObservedMediasoupRouter({
|
|
930
|
+
router,
|
|
931
|
+
matchPeerConnectionByWebRtcTransportId: true,
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
// For large meetings, sample it yourself on your own cadence (see "Memory & large meetings"):
|
|
935
|
+
// setInterval(() => persist(observedRouter.sample), 10_000);
|
|
888
936
|
|
|
889
937
|
// 3) Every peer connection whose id matches one of the router's WebRTC transport ids fires this —
|
|
890
938
|
// WE decide what to do with each pairing. The payload carries the full ancestry.
|
|
891
939
|
observer.on('mediasoup-router-matched-with-peer-connection',
|
|
892
|
-
({ observedMediasoupRouter, observedCall,
|
|
940
|
+
({ observedMediasoupRouter, observedCall, observedPeerConnection }:
|
|
893
941
|
ObservedMediasoupRouterScope & ObservedPeerConnectionScope) => {
|
|
894
|
-
// e.g. remember which router serves this peer connection / client…
|
|
895
942
|
(observedPeerConnection.appData ??= {}).routerId = observedMediasoupRouter.id;
|
|
896
|
-
|
|
897
|
-
myStore.linkRouterToCall(observedCall.callId, observedMediasoupRouter.sample);
|
|
943
|
+
myStore.linkRouterToCall(observedCall.callId, observedMediasoupRouter.id);
|
|
898
944
|
},
|
|
899
945
|
);
|
|
900
946
|
|
|
901
|
-
// 4) The router closed —
|
|
947
|
+
// 4) The router closed — read/persist the final state, then drop your reference.
|
|
902
948
|
observer.on('mediasoup-router-removed', ({ observedMediasoupRouter }: ObservedMediasoupRouterScope) => {
|
|
903
|
-
|
|
904
|
-
});
|
|
905
|
-
|
|
906
|
-
// (optional) react to the router being registered at all:
|
|
907
|
-
observer.on('mediasoup-router-added', ({ observedMediasoupRouter }) => {
|
|
908
|
-
console.log('observing router', observedMediasoupRouter.id);
|
|
949
|
+
persist(observedMediasoupRouter.sample); // its `closedAt` is set
|
|
909
950
|
});
|
|
910
951
|
```
|
|
911
952
|
|
|
912
|
-
### Why event-driven instead of storing on the call
|
|
953
|
+
### Why event-driven matching instead of storing on the call
|
|
913
954
|
|
|
914
955
|
- **Loose coupling.** The call model stays about client telemetry; the SFU view lives on its own
|
|
915
|
-
|
|
956
|
+
`ObservedMediasoupRouter` and is associated only if and how *you* choose.
|
|
916
957
|
- **You own the association.** One router serves many peer connections (across clients and calls),
|
|
917
958
|
and the right place to keep that mapping is application-specific — so the observer hands you each
|
|
918
|
-
peer-connection match and
|
|
919
|
-
- **
|
|
920
|
-
|
|
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.
|
|
921
963
|
|
|
922
964
|
---
|
|
923
965
|
|
|
@@ -1043,6 +1085,60 @@ const observer = new Observer({ createClientSink });
|
|
|
1043
1085
|
the bus with full ancestry. `ClientSampleSinkFactory` is
|
|
1044
1086
|
`(p: { clientId: string; observedCall: ObservedCall }) => ClientSampleSink | undefined`.
|
|
1045
1087
|
|
|
1088
|
+
---
|
|
1089
|
+
|
|
1090
|
+
## Injecting data into a client
|
|
1091
|
+
|
|
1092
|
+
Sometimes the application holds data that belongs on a client's record but isn't part of the
|
|
1093
|
+
client-reported `ClientSample` — a room id or display name, an application-level event
|
|
1094
|
+
(*"recording started"*), a server-detected issue, an extension stat, or a device/meta item.
|
|
1095
|
+
`ObservedClient` exposes **injection** methods that merge such data into the client's sample stream,
|
|
1096
|
+
so it updates the live model **and** is persisted to the client's
|
|
1097
|
+
[sink](#sinks-per-client-sample-persistence) exactly like sampled data.
|
|
1098
|
+
|
|
1099
|
+
| Method | Adds to the sample's | Surfaces as |
|
|
1100
|
+
|--------|----------------------|-------------|
|
|
1101
|
+
| `injectAttachment(attachments)` | `attachments` (merged via `Object.assign`) | `observedClient.attachments` |
|
|
1102
|
+
| `injectEvent(event: ClientEvent)` | `clientEvents` | `client-event` (plus any state the event drives) |
|
|
1103
|
+
| `injectIssue(issue: ClientIssue)` | `clientIssues` | `client-issue` |
|
|
1104
|
+
| `injectMetaData(meta: ClientMetaData)` | `clientMetaItems` | `client-metadata` |
|
|
1105
|
+
| `injectExtensionStat(stat: ExtensionStat)` | `extensionStats` | `client-extension-stats` |
|
|
1106
|
+
|
|
1107
|
+
### When the injected data lands
|
|
1108
|
+
|
|
1109
|
+
Injection is timing-aware so nothing is dropped, regardless of *when* you call it:
|
|
1110
|
+
|
|
1111
|
+
- **During a sample's processing** — e.g. from inside a `client-updated` / `client-event` handler,
|
|
1112
|
+
which run within `accept()` — the data is applied to the **current** sample immediately: reflected
|
|
1113
|
+
in entity state and written to the sink as part of that sample.
|
|
1114
|
+
- **Between samples** — the data is buffered and merged into the **next** `accept()`'s sample.
|
|
1115
|
+
- **On `close()` with pending injections and no further sample** — the buffer is flushed as a final
|
|
1116
|
+
synthetic sample (applied to state and written to the sink) before the sink is ended, so a
|
|
1117
|
+
last-moment injection is never lost.
|
|
1118
|
+
|
|
1119
|
+
In every case the injected data both updates the live `ObservedClient` and reaches the per-client
|
|
1120
|
+
sink — the sink always receives the final, **injection-merged** sample (the sink write happens at the
|
|
1121
|
+
end of `accept()`, after the merge).
|
|
1122
|
+
|
|
1123
|
+
### Example
|
|
1124
|
+
|
|
1125
|
+
```ts
|
|
1126
|
+
// Enrich at creation from your app's knowledge of the participant. Injecting in `client-added`
|
|
1127
|
+
// (which runs just before the first accept) lands on the first sample.
|
|
1128
|
+
observer.on('client-added', ({ observedClient }) => {
|
|
1129
|
+
observedClient.injectAttachment({ roomId: lookupRoomId(observedClient.clientId) });
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
// Application-level signals at any time:
|
|
1133
|
+
const client = observer.getObservedCall(callId)?.getObservedClient(clientId);
|
|
1134
|
+
client?.injectEvent({ type: 'RECORDING_STARTED', timestamp: Date.now() });
|
|
1135
|
+
client?.injectIssue({ type: 'app-kicked-participant', timestamp: Date.now() });
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
`attachments` are latest-wins (like sampled `attachments`): injecting a key overwrites its previous
|
|
1139
|
+
value. `appData` is unaffected — injections flow into the sample/telemetry, not the app-owned
|
|
1140
|
+
`appData` bag (see [Ingestion](#ingestion-accept-context--lifecycle)).
|
|
1141
|
+
|
|
1046
1142
|
## Logging
|
|
1047
1143
|
|
|
1048
1144
|
`observer-js` logs through a single, swappable sink. Out of the box it writes `debug` and
|
|
@@ -1491,7 +1587,7 @@ the 1.0.0 API provides.
|
|
|
1491
1587
|
track to the subscribed (inbound) tracks carrying it (**one publisher → many subscribers**) by a
|
|
1492
1588
|
**publisher id** (the link key). Links are maintained directly on the tracks
|
|
1493
1589
|
(`inboundTrack.remoteOutboundTrack`, `outboundTrack.remoteInboundTracks`).
|
|
1494
|
-
- **Opt-in via `ObserverConfig.
|
|
1590
|
+
- **Opt-in via `ObserverConfig.createRemoteTrackResolver`**, invoked per call. Built-in factories:
|
|
1495
1591
|
`createDefaultMediasoupRemoteTrackResolverFactory()` (producerId/consumerId attachments) and
|
|
1496
1592
|
`createP2pRemoteTrackResolverFactory()` (RTP SSRC). Custom topologies supply their own
|
|
1497
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.12",
|
|
4
4
|
"description": "Server-side Node.js library for processing ObserveRTC Samples",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"build": "tsup",
|
|
33
33
|
"typecheck": "tsc --noEmit",
|
|
34
34
|
"test": "jest --config jest.config.js",
|
|
35
|
-
"test:coverage": "jest --config jest.config.js --coverage"
|
|
35
|
+
"test:coverage": "jest --config jest.config.js --coverage",
|
|
36
|
+
"docs:llms-full": "node scripts/build-llms-full.mjs"
|
|
36
37
|
},
|
|
37
38
|
"keywords": [
|
|
38
39
|
"webrtc",
|