TypeScript client
This guide lists everything @fishjam-cloud/ts-client can do, with a short example for each feature.
The TypeScript client has no UI bindings: you call methods on a FishjamClient and listen to its events.
If you have not used it before, start with the TypeScript Quick Start.
Unless stated otherwise, the examples assume you have already created a client:
import {FishjamClient } from "@fishjam-cloud/ts-client"; constclient = newFishjamClient ();
Connect to a room
Create the client
constclient = newFishjamClient ({debug : true, // log SDK internals to the consolereconnect : true, // the default, see "Reconnect" below });
Join
connect resolves once you have joined the room and rejects if the token is rejected or the room can't be joined:
awaitclient .connect ({url : `wss://fishjam.io/api/v1/connect/${FISHJAM_ID }`,token :peerToken ,peerMetadata : {}, });
Handle connection errors
Fishjam closes the connection with a reason you can check with isAuthError and isJoinError:
client .on ("authError", (reason ) => { // "missing token" | "invalid token" | "expired token" | "room not found" | "peer not found"console .error ("Token rejected:",reason ); });client .on ("joinError", (reason ) => { // e.g. "reached peers limit" | "room not found"console .error ("Could not join:",reason ); });client .on ("connectionError", ({message }) => {console .error ("Media connection failed:",message ); });
Other connection events: connectionStarted, socketOpen, socketClose, socketError, authSuccess, joined and disconnected.
Leave
// Leave the room and close the connectionclient .disconnect ();
client.leave() only leaves the room and keeps the connection open. In most apps you want disconnect().
Reconnect
The client reconnects automatically when the connection drops. By default it makes up to 3 attempts, waiting 500 ms, 1000 ms and 1500 ms. It does not retry after an authentication or join error.
constclient = newFishjamClient ({reconnect : {maxAttempts : 5,initialDelay : 1000, // ms to wait before the first attemptdelay : 1000, // ms added to the wait for each next attemptaddTracksOnReconnect : true, // publish your tracks again after reconnecting }, });client .on ("reconnectionStarted", () =>console .log ("Reconnecting..."));client .on ("reconnected", () =>console .log ("Reconnected"));client .on ("reconnectionRetriesLimitReached", () =>console .log ("Gave up"));
Pass reconnect: false to turn it off. client.isReconnecting() tells you if a reconnection is in progress.
Publish media
Camera and microphone
addTrack takes a MediaStreamTrack and its metadata, and returns the track ID:
conststream = awaitnavigator .mediaDevices .getUserMedia ({video : true,audio : true, }); constcameraTrackId = awaitclient .addTrack (stream .getVideoTracks ()[0], {type : "camera",paused : false, }); constmicTrackId = awaitclient .addTrack (stream .getAudioTracks ()[0], {type : "microphone",paused : false, });
Available track types: camera, microphone, screenShareVideo, screenShareAudio, customVideo and customAudio.
Other peers see the type in the track's metadata.
In an audio-only room, adding a video track throws TrackTypeError.
Switch device
replaceTrack swaps the source of a published track without creating a new one:
constnewStream = awaitnavigator .mediaDevices .getUserMedia ({video : {deviceId : {exact :deviceId } }, }); awaitclient .replaceTrack (cameraTrackId ,newStream .getVideoTracks ()[0]);
Mute and unmute
Replace the track with null and mark it as paused, so other peers know it is muted:
// Mute awaitclient .replaceTrack (cameraTrackId , null);client .updateTrackMetadata (cameraTrackId , {type : "camera",paused : true }); // Unmute awaitclient .replaceTrack (cameraTrackId ,cameraTrack );client .updateTrackMetadata (cameraTrackId , {type : "camera",paused : false });
Share your screen
constscreen = awaitnavigator .mediaDevices .getDisplayMedia ({video : true }); constscreenTrackId = awaitclient .addTrack (screen .getVideoTracks ()[0], {type : "screenShareVideo",paused : false, });
Stop publishing
awaitclient .removeTrack (screenTrackId );
The client also emits events about your own tracks: localTrackAdded, localTrackRemoved, localTrackReplaced, localTrackMuted, localTrackUnmuted and localTrackMetadataChanged.
Receive media
Peers
client .on ("joined", (myPeerId ,peersInRoom ) => {console .log ( `Joined as ${myPeerId }, ${peersInRoom .length } peers already here`, ); });client .on ("peerJoined", (peer ) =>console .log ("Joined:",peer .id ));client .on ("peerUpdated", (peer ) =>console .log ("Updated:",peer .id ));client .on ("peerLeft", (peer ) =>console .log ("Left:",peer .id ));
Tracks
A remote track goes through these events:
trackAdded: the track exists, but no media has arrived yettrackReady: media is arriving,ctx.trackandctx.streamare settrackUpdated: its metadata changed, for example it was pausedtrackRemoved: the peer stopped publishing it, or left
client .on ("trackReady", (ctx ) => { if (!ctx .track ) return; constelement =document .createElement (ctx .track .kind === "video" ? "video" : "audio", );element .id =ctx .trackId ;element .autoplay = true;element .srcObject = newMediaStream ([ctx .track ]);document .body .appendChild (element );console .log (`${ctx .endpoint .id } sent a ${ctx .metadata ?.type } track`); });client .on ("trackUpdated", (ctx ) => {console .log (`Track ${ctx .trackId } paused:`,ctx .metadata ?.paused ); });client .on ("trackRemoved", (ctx ) => {document .getElementById (ctx .trackId )?.remove (); });
Register these listeners before calling connect, so you don't miss the tracks of peers who are already in the room.
Read the current state
Besides events, you can read a snapshot of the room at any time:
constme =client .getLocalPeer (); constpeers =client .getRemotePeers (); // keyed by peer ID consttracks =client .getRemoteTracks (); // keyed by track ID
Voice activity
Each remote audio track reports whether its peer is speaking:
client .on ("trackAdded", (ctx ) => { if (ctx .metadata ?.type !== "microphone") return;ctx .on ("voiceActivityChanged", () => {console .log (`${ctx .endpoint .id } is ${ctx .vadStatus }`); // "speech" | "silence" }); });
Components
Server-side components, such as recordings, are reported separately from peers:
client .on ("componentAdded", (component ) => {console .log ("Component added:",component .type ); // e.g. "recording" }); constcomponents =client .getRemoteComponents ();
Also available: componentUpdated and componentRemoved.
Metadata
Pass a type to FishjamClient to get typed peer metadata:
typePeerMetadata = {name : string }; constclient = newFishjamClient <PeerMetadata >(); awaitclient .connect ({url : `wss://fishjam.io/api/v1/connect/${FISHJAM_ID }`,token :peerToken ,peerMetadata : {name : "Alice" }, }); // Change it later; other peers get a "peerUpdated" eventclient .updatePeerMetadata ({name : "Alice (away)" });client .on ("peerUpdated", (peer ) => {console .log (peer .metadata ?.peer ?.name ); // set by the peerconsole .log (peer .metadata ?.server ); // set by your backend when creating the peer });
See Metadata for how peer and server metadata differ.
Simulcast
With simulcast, you publish your video in several qualities and each receiver picks one.
Publish several qualities
import {Variant } from "@fishjam-cloud/ts-client"; constcameraTrackId = awaitclient .addTrack (cameraTrack , {type : "camera",paused : false }, {enabled : true,enabledVariants : [Variant .VARIANT_LOW ,Variant .VARIANT_MEDIUM ,Variant .VARIANT_HIGH , ],disabledVariants : [], }, ); // Temporarily stop sending one of the qualities awaitclient .disableTrackEncoding (cameraTrackId ,Variant .VARIANT_HIGH ); awaitclient .enableTrackEncoding (cameraTrackId ,Variant .VARIANT_HIGH );
Choose the received quality
import {Variant } from "@fishjam-cloud/ts-client"; // For example: low quality for thumbnails, high for the active speakerclient .setTargetTrackEncoding (remoteTrackId ,Variant .VARIANT_LOW );client .on ("encodingChanged", (ctx ) => { // reason: "other" | "encodingInactive" | "lowBandwidth"console .log (`Now receiving ${ctx .encoding }, reason: ${ctx .encodingReason }`); });
If the chosen quality is not available, for example because the sender disabled it or your bandwidth is too low, Fishjam sends another one until it is.
Limit bandwidth
All limits are in kilobits per second.
// Cap a track when publishing it awaitclient .addTrack (cameraTrack , {type : "camera",paused : false },simulcastConfig , 1500, ); // Change the cap later awaitclient .setTrackBandwidth (cameraTrackId , 800); // For simulcast tracks, cap a single quality awaitclient .setEncodingBandwidth (cameraTrackId ,Variant .VARIANT_HIGH , 1200);
To see how much bandwidth Fishjam estimates you can receive (in bits per second):
console .log (client .getBandwidthEstimation ());client .on ("bandwidthEstimationChanged", (estimation ) => {console .log (`Estimated incoming bandwidth: ${estimation } bps`); });
Data channels
Send binary data to everyone in the room. Use the reliable channel for chat messages and the lossy channel for frequent updates where only the latest value matters. See Data channels for details.
Call these after joining the room:
constunsubscribe =client .subscribeData ( (data ) =>console .log ("Received:", newTextDecoder ().decode (data )), {reliable : true }, );client .on ("dataChannelsReady", () => {client .publishData (newTextEncoder ().encode ("Hello!"), {reliable : true }); });client .on ("dataChannelsError", (error ) =>console .error (error )); awaitclient .createDataChannels ();
client.getDataChannelsReadiness() tells you if the channels are ready.
Livestreaming
For livestreams, the package exports two standalone functions. They don't need a FishjamClient.
Publish a livestream
Get a streamer token from your backend:
import {publishLivestream } from "@fishjam-cloud/ts-client"; conststream = awaitnavigator .mediaDevices .getUserMedia ({video : true,audio : true, }); const {stopPublishing } = awaitpublishLivestream (stream , "https://fishjam.io/api/v1/live/api/whip",streamerToken , ); // Later awaitstopPublishing ();
Watch a livestream
For a private livestream, pass a viewer token. For a public one, add the room ID to the URL and skip the token:
import {LivestreamError ,receiveLivestream } from "@fishjam-cloud/ts-client"; try { const {stream ,stop } = awaitreceiveLivestream ( "https://fishjam.io/api/v1/live/api/whep",viewerToken , );video .srcObject =stream ; } catch (error ) { if (error ===LivestreamError .STREAM_NOT_FOUND ) {console .log ("The stream has not started yet"); } }
Possible errors: UNAUTHORIZED, STREAM_NOT_FOUND, STREAMER_ALREADY_CONNECTED and UNKNOWN_ERROR.
Statistics
// Standard WebRTC statistics for the whole connection conststats = awaitclient .getStatistics (); // Volume of your microphone, from 0 to 1, e.g. for a level meter constaudioLevel = awaitclient .getLocalTrackAudioLevel (micTrackId );console .log (audioLevel ?.level );
getStatistics returns the browser's RTCStatsReport, the same data as RTCPeerConnection.getStats().
It is a map of reports, each with a type. The most useful ones:
outbound-rtp: media you send, for examplebytesSent,framesPerSecondandqualityLimitationReasoninbound-rtp: media you receive, for examplebytesReceived,packetsLost,jitterandframesDroppedcandidate-pair: the connection itself, for examplecurrentRoundTripTimeandavailableOutgoingBitrate
Pass a MediaStreamTrack to get the statistics of that track only.
Not included
The React and React Native SDKs add features on top of the TypeScript client. With the TypeScript client, use browser APIs directly instead:
- Listing and selecting cameras and microphones: use
navigator.mediaDevices - Camera effects and custom sources: process or create the
MediaStreamTrackyourself beforeaddTrack - Picture in Picture: use the browser's Picture-in-Picture API
- Sandbox API helpers: call the Sandbox API with
fetch, as in the quick start