UNPKG

@jellyfish-dev/react-client-sdk

Version:
568 lines (567 loc) 26.3 kB
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; } }