@jellyfish-dev/react-client-sdk
Version:
React client library for Jellyfish.
568 lines (567 loc) • 26.3 kB
JavaScript
import EventEmitter from "events";
import { JellyfishClient, } from "@jellyfish-dev/ts-client-sdk";
import { DeviceManager } from "./DeviceManager";
import { ScreenShareManager } from "./ScreenShareManager";
const NOOP = () => { };
export class Client extends EventEmitter {
tsClient;
deviceManager;
screenShareManager;
local = null;
peers = {};
components = {};
peersTracks = {};
componentsTracks = {};
bandwidthEstimation = BigInt(0);
status = null;
media = null;
devices;
currentMicrophoneTrackId = null;
currentCameraTrackId = null;
currentScreenShareTrackId = null;
constructor(config) {
super();
this.tsClient = new JellyfishClient(config?.clientConfig);
this.deviceManager = new DeviceManager(config?.deviceManagerDefaultConfig);
this.screenShareManager = new ScreenShareManager(config?.screenShareManagerDefaultConfig);
this.devices = {
init: NOOP,
start: NOOP,
camera: {
stop: NOOP,
setEnable: NOOP,
start: NOOP,
addTrack: (_trackMetadata, _simulcastConfig, _maxBandwidth) => Promise.reject(),
removeTrack: () => Promise.reject(),
replaceTrack: (_newTrackMetadata) => Promise.reject(),
broadcast: null,
status: null,
stream: null,
track: null,
enabled: false,
mediaStatus: null,
deviceInfo: null,
error: null,
devices: null,
},
microphone: {
stop: NOOP,
setEnable: NOOP,
start: NOOP,
addTrack: (_trackMetadata, _maxBandwidth) => Promise.reject(),
removeTrack: () => Promise.reject(),
replaceTrack: (_newTrackMetadata) => Promise.reject(),
broadcast: null,
status: null,
stream: null,
track: null,
enabled: false,
mediaStatus: null,
deviceInfo: null,
error: null,
devices: null,
},
screenShare: {
stop: NOOP,
setEnable: NOOP,
start: NOOP,
addTrack: (_trackMetadata, _maxBandwidth) => Promise.reject(),
removeTrack: () => Promise.reject(),
broadcast: null,
status: null,
stream: null,
track: null,
enabled: false,
mediaStatus: null,
error: null,
},
};
this.stateToSnapshot();
this.tsClient.on("socketOpen", (event) => {
this.status = "connected";
this.stateToSnapshot();
this.emit("socketOpen", event, this);
});
this.tsClient.on("socketError", (event) => {
this.stateToSnapshot();
this.emit("socketError", event, this);
});
this.tsClient.on("socketClose", (event) => {
this.stateToSnapshot();
this.emit("socketClose", event, this);
});
this.tsClient.on("authSuccess", () => {
this.status = "authenticated";
this.stateToSnapshot();
this.emit("authSuccess", this);
});
this.tsClient.on("authError", (reason) => {
this.stateToSnapshot();
this.status = "error";
this.emit("authError", reason, this);
});
this.tsClient.on("disconnected", () => {
this.status = null;
this.stateToSnapshot();
this.emit("disconnected", this);
});
this.tsClient.on("joined", (peerId, peers, components) => {
this.status = "joined";
this.stateToSnapshot();
this.emit("joined", { peerId, peers, components }, this);
});
this.tsClient.on("joinError", (metadata) => {
this.status = "error";
this.stateToSnapshot();
this.emit("joinError", metadata, this);
});
this.tsClient.on("peerJoined", (peer) => {
this.stateToSnapshot();
this.emit("peerJoined", peer, this);
});
this.tsClient.on("peerUpdated", (peer) => {
this.stateToSnapshot();
this.emit("peerUpdated", peer, this);
});
this.tsClient.on("peerLeft", (peer) => {
this.stateToSnapshot();
this.emit("peerLeft", peer, this);
});
this.tsClient.on("componentAdded", (component) => {
this.stateToSnapshot();
this.emit("componentAdded", component, this);
});
this.tsClient.on("componentUpdated", (component) => {
this.stateToSnapshot();
this.emit("componentUpdated", component, this);
});
this.tsClient.on("componentRemoved", (component) => {
this.stateToSnapshot();
this.emit("componentRemoved", component, this);
});
this.tsClient.on("trackReady", (ctx) => {
this.stateToSnapshot();
this.emit("trackReady", ctx, this);
});
this.tsClient.on("trackAdded", (ctx) => {
this.stateToSnapshot();
this.emit("trackAdded", ctx, this);
ctx.on("encodingChanged", () => {
this.stateToSnapshot();
this.emit("encodingChanged", ctx, this);
});
ctx.on("voiceActivityChanged", () => {
this.stateToSnapshot();
this.emit("voiceActivityChanged", ctx, this);
});
});
this.tsClient.on("trackRemoved", (ctx) => {
this.stateToSnapshot();
this.emit("trackRemoved", ctx, this);
ctx.removeAllListeners();
});
this.tsClient.on("trackUpdated", (ctx) => {
this.stateToSnapshot();
this.emit("trackUpdated", ctx, this);
});
this.tsClient.on("bandwidthEstimationChanged", (estimation) => {
this.stateToSnapshot();
this.emit("bandwidthEstimationChanged", estimation, this);
});
this.deviceManager.on("deviceDisabled", (event) => {
this.stateToSnapshot();
this.emit("deviceDisabled", { ...event, mediaDeviceType: "userMedia" }, this);
});
this.deviceManager.on("deviceEnabled", (event) => {
this.stateToSnapshot();
this.emit("deviceEnabled", { ...event, mediaDeviceType: "userMedia" }, this);
});
this.deviceManager.on("managerInitialized", (event) => {
this.stateToSnapshot();
this.emit("managerInitialized", { ...event, mediaDeviceType: "userMedia" }, this);
});
this.deviceManager.on("managerStarted", (event) => {
this.stateToSnapshot();
this.emit("managerStarted", { ...event, mediaDeviceType: "userMedia" }, this);
});
this.deviceManager.on("deviceStopped", (event) => {
this.stateToSnapshot();
this.emit("deviceStopped", { trackType: event.trackType, mediaDeviceType: "userMedia" }, this);
});
this.deviceManager.on("deviceReady", (event) => {
this.stateToSnapshot();
this.emit("deviceReady", { ...event, mediaDeviceType: "userMedia" }, this);
});
this.deviceManager.on("devicesStarted", (event) => {
this.stateToSnapshot();
this.emit("devicesStarted", { ...event, mediaDeviceType: "userMedia" }, this);
});
this.deviceManager.on("devicesReady", (event) => {
this.stateToSnapshot();
this.emit("devicesReady", { ...event, mediaDeviceType: "userMedia" }, this);
});
this.deviceManager.on("error", (event) => {
this.stateToSnapshot();
this.emit("error", event, this);
});
this.screenShareManager.on("deviceDisabled", (event) => {
this.stateToSnapshot();
this.emit("deviceDisabled", { trackType: event.type, mediaDeviceType: "displayMedia" }, this);
});
this.screenShareManager.on("deviceEnabled", (event) => {
this.stateToSnapshot();
this.emit("deviceEnabled", { trackType: event.type, mediaDeviceType: "displayMedia" }, this);
});
this.screenShareManager.on("deviceStopped", async (event) => {
this.stateToSnapshot();
this.emit("deviceStopped", { trackType: event.type, mediaDeviceType: "displayMedia" }, this);
});
this.screenShareManager.on("deviceReady", (event, state) => {
this.stateToSnapshot();
if (!state.videoMedia?.stream)
throw Error("Invalid screen share state");
this.emit("deviceReady", {
trackType: event.type,
stream: state.videoMedia.stream,
mediaDeviceType: "displayMedia",
}, this);
});
this.screenShareManager.on("error", (a) => {
this.stateToSnapshot();
this.emit("error", a, this);
});
this.tsClient?.on("targetTrackEncodingRequested", (event) => {
this.stateToSnapshot();
this.emit("targetTrackEncodingRequested", event, this);
});
this.tsClient?.on("localTrackAdded", (event) => {
this.stateToSnapshot();
this.emit("localTrackAdded", event, this);
});
this.tsClient?.on("localTrackRemoved", (event) => {
this.stateToSnapshot();
this.emit("localTrackRemoved", event, this);
});
this.tsClient?.on("localTrackReplaced", (event) => {
this.stateToSnapshot();
this.emit("localTrackReplaced", event, this);
});
this.tsClient?.on("localTrackBandwidthSet", (event) => {
this.stateToSnapshot();
this.emit("localTrackBandwidthSet", event, this);
});
this.tsClient?.on("localTrackEncodingBandwidthSet", (event) => {
this.stateToSnapshot();
this.emit("localTrackEncodingBandwidthSet", event, this);
});
this.tsClient?.on("localTrackEncodingEnabled", (event) => {
this.stateToSnapshot();
this.emit("localTrackEncodingEnabled", event, this);
});
this.tsClient?.on("localTrackEncodingDisabled", (event) => {
this.stateToSnapshot();
this.emit("localTrackEncodingDisabled", event, this);
});
this.tsClient?.on("localEndpointMetadataChanged", (event) => {
this.stateToSnapshot();
this.emit("localEndpointMetadataChanged", event, this);
});
this.tsClient?.on("localTrackMetadataChanged", (event) => {
this.stateToSnapshot();
this.emit("localTrackMetadataChanged", event, this);
});
this.tsClient?.on("disconnectRequested", (event) => {
this.stateToSnapshot();
this.emit("disconnectRequested", event, this);
});
}
setScreenManagerConfig(config) {
this.screenShareManager?.setConfig(config);
}
setDeviceManagerConfig(config) {
this.deviceManager?.setConfig(config);
}
trackContextToTrack(track) {
return {
rawMetadata: track.rawMetadata,
metadata: track.metadata,
trackId: track.trackId,
stream: track.stream,
simulcastConfig: track.simulcastConfig || null,
encoding: track.encoding || null,
vadStatus: track.vadStatus,
track: track.track,
metadataParsingError: track.metadataParsingError,
};
}
connect(config) {
this.status = "connecting";
this.tsClient.connect(config);
}
disconnect() {
this.status = null;
this.tsClient.disconnect();
}
addTrack(track, stream, trackMetadata, simulcastConfig = { enabled: false, activeEncodings: [], disabledEncodings: [] }, maxBandwidth = 0) {
if (!this.tsClient)
throw Error("Client not initialized");
return this.tsClient.addTrack(track, stream, trackMetadata, simulcastConfig, maxBandwidth);
}
removeTrack(trackId) {
return this.tsClient.removeTrack(trackId);
}
replaceTrack(trackId, newTrack, newTrackMetadata) {
return this.tsClient.replaceTrack(trackId, newTrack, newTrackMetadata);
}
getStatistics(selector) {
return this.tsClient.getStatistics(selector);
}
getBandwidthEstimation() {
return this.tsClient.getBandwidthEstimation();
}
setTrackBandwidth(trackId, bandwidth) {
return this.tsClient.setTrackBandwidth(trackId, bandwidth);
}
setEncodingBandwidth(trackId, rid, bandwidth) {
return this.tsClient.setEncodingBandwidth(trackId, rid, bandwidth);
}
setTargetTrackEncoding(trackId, encoding) {
return this.tsClient.setTargetTrackEncoding(trackId, encoding);
}
enableTrackEncoding(trackId, encoding) {
return this.tsClient.enableTrackEncoding(trackId, encoding);
}
disableTrackEncoding(trackId, encoding) {
return this.tsClient.disableTrackEncoding(trackId, encoding);
}
updatePeerMetadata = (peerMetadata) => {
this.tsClient.updatePeerMetadata(peerMetadata);
};
updateTrackMetadata = (trackId, trackMetadata) => {
this.tsClient.updateTrackMetadata(trackId, trackMetadata);
};
stateToSnapshot() {
if (!this.deviceManager)
Error("Device manager is null");
const screenShareManager = this.screenShareManager?.getSnapshot();
const deviceManagerSnapshot = {
audio: this?.deviceManager?.audio,
video: this?.deviceManager?.video,
};
const localEndpoint = this.tsClient.getLocalEndpoint();
const localTracks = {};
(localEndpoint?.tracks || new Map()).forEach((track) => {
localTracks[track.trackId] = this.trackContextToTrack(track);
});
const broadcastedVideoTrack = Object.values(localTracks).find((track) => track.track?.id === this.currentCameraTrackId);
const broadcastedAudioTrack = Object.values(localTracks).find((track) => track.track?.id === this.currentMicrophoneTrackId);
const screenShareVideoTrack = Object.values(localTracks).find((track) => track.track?.id === this.currentScreenShareTrackId);
const devices = {
init: (config) => {
this?.deviceManager?.init(config);
},
start: (config) => this?.deviceManager?.start(config),
camera: {
stop: () => {
this?.deviceManager?.stop("video");
},
setEnable: (value) => this?.deviceManager?.setEnable("video", value),
start: (deviceId) => {
this?.deviceManager?.start({ videoDeviceId: deviceId ?? true });
},
addTrack: (trackMetadata, simulcastConfig, maxBandwidth) => {
const media = this.deviceManager?.video.media;
if (!media || !media.stream || !media.track)
throw Error("Device is unavailable");
const { track } = media;
const prevTrack = Object.values(localTracks).find((track) => track.track?.id === this.currentCameraTrackId);
if (prevTrack)
throw Error("Track already added");
this.currentCameraTrackId = track?.id;
const stream = new MediaStream();
stream.addTrack(track);
return this.tsClient.addTrack(track, stream, trackMetadata, simulcastConfig, maxBandwidth);
},
removeTrack: () => {
const prevTrack = Object.values(localTracks).find((track) => track.track?.id === this.currentCameraTrackId);
if (!prevTrack)
throw Error("There is no video track");
this.currentCameraTrackId = null;
return this.tsClient.removeTrack(prevTrack.trackId);
},
replaceTrack: async (newTrackMetadata) => {
const prevTrack = Object.values(localTracks).find((track) => track.track?.id === this.currentCameraTrackId);
if (!prevTrack)
throw Error("There is no video track");
const track = this.devices.camera.stream?.getVideoTracks()[0];
if (!track)
throw Error("New track is empty");
this.currentCameraTrackId = track.id;
// todo This is a temporary solution to address an issue with ts-client-sdk
// Currently, ts-client does not update the track in the stream during the execution of the replaceTrack method
if (!this.devices.camera.broadcast?.stream)
throw Error("New stream is empty");
this.devices.camera.broadcast?.stream?.removeTrack(this.devices.camera.broadcast?.stream?.getVideoTracks()[0]);
this.devices.camera.broadcast?.stream.addTrack(track);
await this.tsClient.replaceTrack(prevTrack.trackId, track, newTrackMetadata);
},
broadcast: broadcastedVideoTrack ?? null,
status: deviceManagerSnapshot?.video?.devicesStatus || null,
stream: deviceManagerSnapshot?.video.media?.stream || null,
track: deviceManagerSnapshot?.video.media?.track || null,
enabled: deviceManagerSnapshot?.video.media?.enabled || false,
deviceInfo: deviceManagerSnapshot?.video.media?.deviceInfo || null,
mediaStatus: deviceManagerSnapshot?.video.mediaStatus || null,
error: deviceManagerSnapshot?.video?.error || null,
devices: deviceManagerSnapshot?.video?.devices || null,
},
microphone: {
stop: () => this?.deviceManager?.stop("audio"),
setEnable: (value) => this?.deviceManager?.setEnable("audio", value),
start: (deviceId) => {
this?.deviceManager?.start({ audioDeviceId: deviceId ?? true });
},
addTrack: (trackMetadata, maxBandwidth) => {
const media = this.deviceManager?.audio.media;
if (!media || !media.stream || !media.track)
throw Error("Device is unavailable");
const { track } = media;
const prevTrack = Object.values(localTracks).find((track) => track.track?.id === this.currentMicrophoneTrackId);
if (prevTrack)
throw Error("Track already added");
this.currentMicrophoneTrackId = track.id;
const stream = new MediaStream();
stream.addTrack(track);
return this.tsClient.addTrack(track, stream, trackMetadata, undefined, maxBandwidth);
},
removeTrack: () => {
const prevTrack = Object.values(localTracks).find((track) => track.track?.id === this.currentMicrophoneTrackId);
if (!prevTrack)
throw Error("There is no audio track");
this.currentMicrophoneTrackId = null;
return this.tsClient.removeTrack(prevTrack.trackId);
},
replaceTrack: async (newTrackMetadata) => {
const prevTrack = Object.values(localTracks).find((track) => track.track?.id === this.currentMicrophoneTrackId);
if (!prevTrack)
throw Error("There is no audio track");
const track = this.devices.microphone.stream?.getAudioTracks()[0];
if (!track)
throw Error("New track is empty");
this.currentMicrophoneTrackId = track.id;
// todo This is a temporary solution to address an issue with ts-client-sdk
// Currently, ts-client does not update the track in the stream during the execution of the replaceTrack method
if (!this.devices.microphone.broadcast?.stream)
throw Error("New stream is empty");
this.devices.microphone.broadcast?.stream?.removeTrack(this.devices.microphone.broadcast?.stream?.getAudioTracks()[0]);
this.devices.microphone.broadcast?.stream.addTrack(track);
await this.tsClient.replaceTrack(prevTrack.trackId, track, newTrackMetadata);
},
broadcast: broadcastedAudioTrack ?? null,
status: deviceManagerSnapshot?.audio?.devicesStatus || null,
stream: deviceManagerSnapshot?.audio.media?.stream || null,
track: deviceManagerSnapshot?.audio.media?.track || null,
enabled: deviceManagerSnapshot?.audio.media?.enabled || false,
deviceInfo: deviceManagerSnapshot?.audio.media?.deviceInfo || null,
mediaStatus: deviceManagerSnapshot?.video.mediaStatus || null,
error: deviceManagerSnapshot?.audio?.error || null,
devices: deviceManagerSnapshot?.audio?.devices || null,
},
screenShare: {
stop: () => {
this?.screenShareManager?.stop("video");
},
setEnable: (value) => this.screenShareManager?.setEnable("video", value),
start: (config) => {
this.screenShareManager?.start(config);
},
addTrack: (trackMetadata, maxBandwidth) => {
const media = this.screenShareManager?.getSnapshot().videoMedia;
if (!media || !media.stream || !media.track)
throw Error("Device is unavailable");
const { stream, track } = media;
const prevTrack = Object.values(localTracks).find((track) => track.track?.id === this.currentScreenShareTrackId);
if (prevTrack)
throw Error("Track already added");
this.currentScreenShareTrackId = track?.id;
return this.tsClient.addTrack(track, stream, trackMetadata, undefined, maxBandwidth);
},
removeTrack: () => {
const prevTrack = Object.values(localTracks).find((track) => track.track?.id === this.currentScreenShareTrackId);
if (!prevTrack)
throw Error("There is no video track");
this.currentScreenShareTrackId = null;
return this.tsClient.removeTrack(prevTrack.trackId);
},
broadcast: screenShareVideoTrack ?? null,
status: screenShareManager?.status || null,
mediaStatus: null,
stream: screenShareManager?.videoMedia?.stream || null,
track: screenShareManager?.videoMedia?.track || null,
enabled: screenShareManager?.videoMedia?.enabled || false,
error: screenShareManager?.error || null,
},
};
if (!this.tsClient["webrtc"]) {
this.media = deviceManagerSnapshot || null;
this.peersTracks = {};
this.componentsTracks = {};
this.devices = devices;
this.peers = {};
this.components = {};
this.local = null;
this.bandwidthEstimation = 0n;
return;
}
const peers = {};
const components = {};
const peersTracks = {};
const componentTracks = {};
Object.values(this.tsClient.getRemotePeers()).forEach((endpoint) => {
const tracks = {};
endpoint.tracks.forEach((track) => {
const mappedTrack = this.trackContextToTrack(track);
tracks[track.trackId] = mappedTrack;
peersTracks[track.trackId] = { ...mappedTrack, origin: endpoint };
});
peers[endpoint.id] = {
rawMetadata: endpoint.rawMetadata,
metadata: endpoint.metadata,
metadataParsingError: endpoint.metadataParsingError,
id: endpoint.id,
tracks,
};
});
Object.values(this.tsClient.getRemoteComponents()).forEach((endpoint) => {
const tracks = {};
endpoint.tracks.forEach((track) => {
const mappedTrack = this.trackContextToTrack(track);
tracks[track.trackId] = mappedTrack;
componentTracks[track.trackId] = { ...mappedTrack, origin: endpoint };
});
components[endpoint.id] = {
rawMetadata: endpoint.rawMetadata,
metadata: endpoint.metadata,
metadataParsingError: endpoint.metadataParsingError,
id: endpoint.id,
tracks,
};
});
this.peersTracks = peersTracks;
this.componentsTracks = componentTracks;
this.media = deviceManagerSnapshot || null;
this.local = localEndpoint
? {
id: localEndpoint.id,
metadata: localEndpoint.metadata,
metadataParsingError: localEndpoint.metadataParsingError,
rawMetadata: localEndpoint.rawMetadata,
tracks: localTracks, // to record
}
: null;
this.peers = peers;
this.components = components;
this.bandwidthEstimation = this.tsClient.getBandwidthEstimation();
this.devices = devices;
}
}