@tribe-nest/media-client
v0.4.0
Published
Client SDK for the media network. `./core` is the HEADLESS half: a WebSocket client speaking media-protocol frames, backoff-with-jitter reconnection and a pure room-state reducer. Zero DOM, zero mediasoup-client, zero React, because the load harness, the
Readme
@tribe-nest/media-client
The client SDK for the media network: the headless core (P3a), the room API
over mediasoup-client, and the React hooks (P3b).
src/core/ the ./core subpath. ISOMORPHIC: no DOM, no mediasoup-client,
no React.
signal.ts one socket, media-protocol frames, request/reply correlation,
and the reason a connection ended
reconnect.ts backoff with jitter, and what to do about a draining node
state.ts a pure reducer: peers, producers, active speakers, recording
socket.ts the WebSocketLike shape and the injected factory
src/room/ the `.` export. `MediaRoom` over mediasoup-client: the device,
one transport per direction, publish/subscribe, the reconnect
ladder driven by `decideReconnect`, and the derived room state.
src/react/ the ./react subpath. `MediaRoomProvider` and the hooks Forge's
`CallStage` is built on.
src/protocol.ts the ./protocol subpath: a re-export of @tribe-nest/media-protocol.package.json carries top-level main/types pointing at the . build as
well as the exports map, for tooling that reads only the former.
mediasoup-client is a hard dependency because the . and ./react subpaths
need it and @tribe-nest/forge depends on this package unconditionally. A
core-only consumer (the load harness, the egress client, the SIP gateway) still
installs it but never loads it: nothing reachable from ./core imports it, and
src/core/_tests/coreBoundary.spec.ts is what holds that line.
Why ./core is not a browser SDK
The load harness, the egress client and the SIP gateway all need a protocol
client, and none of them is a browser. Without a headless subpath each of them
hand-rolls a socket, and three hand-rolled clients drift from each other and
from the node. src/core/_tests/coreBoundary.spec.ts and the DOM-free lib in
tsconfig.json are what hold the line.
The room API and mediasoup-client live at ., the React hooks at ./react,
and Forge's call UI at @tribe-nest/forge/media. None of them is reachable from
./core.
The three things the core owns
Correlation. A request carries an id and gets exactly one reply with that
id; an event carries no id. That is the whole protocol, so it is the whole
of MediaSignal.
The handshake. join is the first frame on the socket and carries the
token. Never a query parameter: a query string lands in load-balancer access
logs, and a join ticket in a log is a join ticket for anyone who can read logs.
connect() resolves only when both the join reply and the joined event have
arrived, in either order, because the reply says the node accepted the token and
the event carries the room.
Why the connection ended. A bare socket close tells a caller nothing, and
draining, roomClosed and a refusal need three different responses. The
DisconnectCause is what decideReconnect decides on.
Credentials are a callback, never a value
const signal = new MediaSignal({
getCredentials: async () => {
const { mediaUrl, token } = await mintJoinToken();
return { mediaUrl, token };
},
});A join ticket expires in minutes and a call lasts an hour. A token passed once is not a naming choice, it is a first-reconnect failure.
Testing
npm run test runs everything against an in-process fake node
(src/core/_tests/fakeSignalServer.ts). No network, no mediasoup, no browser.
The fake parses every frame the client sends with the contract's own
requestFrameSchema, so the question the suite answers is "does this client
emit frames the frozen schema accepts", not "does this client talk to this
fake".
Integration against a real node (P1) is not here and is not stubbed.
