UNPKG

@100mslive/hms-video-store

Version:

@100mslive Core SDK which abstracts the complexities of webRTC while providing a reactive store for data management with a unidirectional data flow

529 lines (469 loc) • 15.6 kB
import { KnownRoles, TrackStateEntry } from './StoreInterfaces'; import { HTTPAnalyticsTransport } from '../../analytics/HTTPAnalyticsTransport'; import { DeviceStorageManager } from '../../device-manager/DeviceStorage'; import { ErrorFactory } from '../../error/ErrorFactory'; import { HMSAction } from '../../error/HMSAction'; import { HMSConfig, HMSFrameworkInfo, HMSPermissionType, HMSPoll, HMSSpeaker, HMSTranscriptionMode, HMSWhiteboard, } from '../../interfaces'; import { SelectedDevices } from '../../interfaces/devices'; import { IErrorListener } from '../../interfaces/error-listener'; import { HMSSimulcastLayerDefinition, RID, SimulcastLayer, SimulcastLayers, simulcastMapping, } from '../../interfaces/simulcast-layers'; import { HMSAudioTrack, HMSLocalTrack, HMSRemoteAudioTrack, HMSRemoteVideoTrack, HMSTrack, HMSTrackSource, HMSTrackType, HMSVideoTrack, } from '../../media/tracks'; import { NoiseCancellationPlugin, Plugins, PolicyParams, TranscriptionPluginPermissions, WhiteBoardPluginPermissions, } from '../../notification-manager'; import HMSLogger from '../../utils/logger'; import { ENV } from '../../utils/support'; import { createUserAgent } from '../../utils/user-agent'; import HMSRoom from '../models/HMSRoom'; import { HMSLocalPeer, HMSPeer, HMSRemotePeer } from '../models/peer'; class Store { private TAG = '[Store]:'; private room?: HMSRoom; private knownRoles: KnownRoles = {}; private localPeerId?: string; private peers: Record<string, HMSPeer> = {}; private tracks = new Map<HMSTrack, HMSTrack>(); private templateAppData?: Record<string, string>; // Not used currently. Will be used exclusively for preview tracks. // private previewTracks: Record<string, HMSTrack> = {}; private peerTrackStates: Record<string, TrackStateEntry> = {}; private speakers: HMSSpeaker[] = []; private videoLayers?: SimulcastLayers; // private screenshareLayers?: SimulcastLayers; private config?: HMSConfig; private errorListener?: IErrorListener; private roleDetailsArrived = false; private env: ENV = ENV.PROD; private simulcastEnabled = false; private userAgent: string = createUserAgent(this.env); private polls = new Map<string, HMSPoll>(); private whiteboards = new Map<string, HMSWhiteboard>(); getConfig() { return this.config; } setSimulcastEnabled(enabled: boolean) { this.simulcastEnabled = enabled; } removeRemoteTracks() { this.tracks.forEach(track => { if (track instanceof HMSRemoteAudioTrack || track instanceof HMSRemoteVideoTrack) { this.removeTrack(track); delete this.peerTrackStates[track.trackId]; } }); } getEnv() { return this.env; } getPublishParams() { const peer = this.getLocalPeer(); const role = peer?.asRole || peer?.role; return role?.publishParams; } getRoom() { return this.room; } getPolicyForRole(role: string) { return this.knownRoles[role]; } getKnownRoles() { return this.knownRoles; } getTemplateAppData() { return this.templateAppData; } getLocalPeer() { if (this.localPeerId && this.peers[this.localPeerId]) { return this.peers[this.localPeerId] as HMSLocalPeer; } return undefined; } getRemotePeers() { return Object.values(this.peers).filter(peer => !peer.isLocal) as HMSRemotePeer[]; } getPeers(): HMSPeer[] { return Object.values(this.peers); } getPeerMap() { return this.peers; } getPeerById(peerId: string) { if (this.peers[peerId]) { return this.peers[peerId]; } return undefined; } getTracksMap() { return this.tracks; } getTracks() { return Array.from(this.tracks.values()); } getVideoTracks() { return this.getTracks().filter(track => track.type === HMSTrackType.VIDEO) as HMSVideoTrack[]; } getRemoteVideoTracks() { return this.getTracks().filter(track => track instanceof HMSRemoteVideoTrack) as HMSRemoteVideoTrack[]; } getAudioTracks() { return this.getTracks().filter(track => track.type === HMSTrackType.AUDIO) as HMSAudioTrack[]; } getPeerTracks(peerId?: string) { const peer = peerId ? this.peers[peerId] : undefined; const tracks: HMSTrack[] = []; peer?.videoTrack && tracks.push(peer.videoTrack); peer?.audioTrack && tracks.push(peer.audioTrack); return tracks.concat(peer?.auxiliaryTracks || []); } getLocalPeerTracks() { return this.getPeerTracks(this.localPeerId) as HMSLocalTrack[]; } hasTrack(track: HMSTrack) { return this.tracks.has(track); } getTrackById(trackId: string) { const track = Array.from(this.tracks.values()).find(track => track.trackId === trackId); if (track) { return track; } const localPeer = this.getLocalPeer(); /** * handle case of audio level coming from server for local peer's track where local peer * didn't initially gave audio permission. So track.firstTrackId is that of dummy track and * this.tracks[trackId] doesn't exist. * Example repro which this solves - * - call preview with audio muted, unmute audio in preview then join the room, now initial * track id is that from dummy track but the track id which server knows will be different */ if (localPeer) { if (localPeer.audioTrack?.isPublishedTrackId(trackId)) { return localPeer.audioTrack; } else if (localPeer.videoTrack?.isPublishedTrackId(trackId)) { return localPeer.videoTrack; } } return undefined; } getPeerByTrackId(trackId: string) { const track = Array.from(this.tracks.values()).find(track => track.trackId === trackId); return track?.peerId ? this.peers[track.peerId] : undefined; } getSpeakers() { return this.speakers; } getSpeakerPeers() { return this.speakers.map(speaker => speaker.peer); } getUserAgent() { return this.userAgent; } createAndSetUserAgent(frameworkInfo?: HMSFrameworkInfo) { this.userAgent = createUserAgent(this.env, frameworkInfo); } setRoom(room: HMSRoom) { this.room = room; } setKnownRoles(params: PolicyParams) { this.knownRoles = params.known_roles; this.addPluginsToRoles(params.plugins); this.roleDetailsArrived = true; this.templateAppData = params.app_data; if (!this.simulcastEnabled) { return; } const publishParams = this.knownRoles[params.name]?.publishParams; this.videoLayers = this.convertSimulcastLayers(publishParams.simulcast?.video); // this.screenshareLayers = this.convertSimulcastLayers(publishParams.simulcast?.screen); this.updatePeersPolicy(); } hasRoleDetailsArrived(): boolean { return this.roleDetailsArrived; } // eslint-disable-next-line complexity setConfig(config: HMSConfig) { DeviceStorageManager.rememberDevices(Boolean(config.rememberDeviceSelection)); if (config.rememberDeviceSelection) { const devices: SelectedDevices | undefined = DeviceStorageManager.getSelection(); if (devices) { if (!config.settings) { config.settings = {}; } if (devices.audioInput?.deviceId) { config.settings.audioInputDeviceId = config.settings.audioInputDeviceId || devices.audioInput.deviceId; } if (devices.audioOutput?.deviceId) { config.settings.audioOutputDeviceId = config.settings.audioOutputDeviceId || devices.audioOutput.deviceId; } if (devices.videoInput?.deviceId) { config.settings.videoDeviceId = config.settings.videoDeviceId || devices.videoInput.deviceId; } } } config.autoManageVideo = config.autoManageVideo !== false; config.autoManageWakeLock = config.autoManageWakeLock !== false; this.config = config; this.setEnv(); } addPeer(peer: HMSPeer) { this.peers[peer.peerId] = peer; if (peer.isLocal) { this.localPeerId = peer.peerId; } } /** * @param {HMSTrack} track the published track that has to be added * * Note: Only use this method to add published tracks not preview traks */ addTrack(track: HMSTrack) { this.tracks.set(track, track); } getTrackState(trackId: string) { return this.peerTrackStates[trackId]; } setTrackState(trackStateEntry: TrackStateEntry) { this.peerTrackStates[trackStateEntry.trackInfo.track_id] = trackStateEntry; } removeTrackState(trackId: string) { delete this.peerTrackStates[trackId]; } removePeer(peerId: string) { if (this.localPeerId === peerId) { this.localPeerId = undefined; } delete this.peers[peerId]; } removeTrack(track: HMSTrack) { this.tracks.delete(track); } updateSpeakers(speakers: HMSSpeaker[]) { this.speakers = speakers; } async updateAudioOutputVolume(value: number) { for (const track of this.getAudioTracks()) { await track.setVolume(value); } } async updateAudioOutputDevice(device: MediaDeviceInfo) { const promises: Promise<void>[] = []; this.getAudioTracks().forEach(track => { if (track instanceof HMSRemoteAudioTrack) { promises.push(track.setOutputDevice(device)); } }); await Promise.all(promises); } getSimulcastLayers(source: HMSTrackSource): SimulcastLayer[] { // Enable only when backend enables and source is video or screen. ignore videoplaylist if (!this.simulcastEnabled || !['screen', 'regular'].includes(source)) { return []; } if (source === 'screen') { return []; //this.screenshareLayers?.layers || []; uncomment this when screenshare simulcast supported } return this.videoLayers?.layers || []; } /** * Convert maxBitrate from kbps to bps * @internal * @param simulcastLayers * @returns {SimulcastLayers} */ private convertSimulcastLayers(simulcastLayers?: SimulcastLayers) { if (!simulcastLayers) { return; } return { ...simulcastLayers, layers: (simulcastLayers.layers || []).map(layer => { return { ...layer, maxBitrate: layer.maxBitrate * 1000, }; }), }; } getSimulcastDefinitionsForPeer(peer: HMSPeer, source: HMSTrackSource) { // TODO: remove screen check when screenshare simulcast is supported if ([!peer || !peer.role, source === 'screen', !this.simulcastEnabled].some(value => !!value)) { return []; } const publishParams = this.getPolicyForRole(peer.role!.name).publishParams; let simulcastLayers: SimulcastLayers | undefined; let width: number; let height: number; if (source === 'regular') { simulcastLayers = publishParams.simulcast?.video; width = publishParams.video.width; height = publishParams.video.height; } else if (source === 'screen') { simulcastLayers = publishParams.simulcast?.screen; width = publishParams.screen.width; height = publishParams.screen.height; } return ( simulcastLayers?.layers?.map(value => { const layer = simulcastMapping[value.rid as RID]; const resolution = { width: Math.floor(width / value.scaleResolutionDownBy), height: Math.floor(height / value.scaleResolutionDownBy), }; return { layer, resolution, } as HMSSimulcastLayerDefinition; }) || [] ); } setPoll(poll: HMSPoll) { this.polls.set(poll.id, poll); } getPoll(id: string): HMSPoll | undefined { return this.polls.get(id); } setWhiteboard(whiteboard: HMSWhiteboard) { this.whiteboards.set(whiteboard.id, whiteboard); } getWhiteboards() { return this.whiteboards; } getWhiteboard(id?: string): HMSWhiteboard | undefined { return id ? this.whiteboards.get(id) : this.whiteboards.values().next().value; } getErrorListener() { return this.errorListener; } cleanup() { const tracks = this.getTracks(); for (const track of tracks) { track.cleanup(); } this.room = undefined; this.config = undefined; this.localPeerId = undefined; this.roleDetailsArrived = false; } setErrorListener(listener: IErrorListener) { this.errorListener = listener; } private updatePeersPolicy() { this.getPeers().forEach(peer => { if (!peer.role) { this.errorListener?.onError(ErrorFactory.GenericErrors.InvalidRole(HMSAction.VALIDATION, '')); return; } peer.role = this.getPolicyForRole(peer.role.name); }); } private addPluginsToRoles(plugins: PolicyParams['plugins']) { if (!plugins) { return; } Object.keys(plugins).forEach(plugin => { const pluginName = plugin as keyof PolicyParams['plugins']; switch (pluginName) { case Plugins.WHITEBOARD: { this.addWhiteboardPluginToRole(plugins[pluginName]); break; } case Plugins.TRANSCRIPTIONS: { this.addTranscriptionsPluginToRole(plugins[pluginName]); break; } case Plugins.NOISE_CANCELLATION: { this.handleNoiseCancellationPlugin(plugins[pluginName]); break; } default: { break; } } }); } private addPermissionToRole = ( role: string, pluginName: keyof PolicyParams['plugins'], permission: HMSPermissionType, mode?: HMSTranscriptionMode, ) => { if (!this.knownRoles[role]) { HMSLogger.d(this.TAG, `role ${role} is not present in given roles`, this.knownRoles); return; } const rolePermissions = this.knownRoles[role].permissions; if (pluginName === Plugins.TRANSCRIPTIONS && mode) { // currently only admin is allowed, so no issue rolePermissions[pluginName] = { ...rolePermissions[pluginName], [mode]: [permission], }; } else if (pluginName === Plugins.WHITEBOARD) { if (!rolePermissions[pluginName]) { rolePermissions[pluginName] = []; } rolePermissions[pluginName]?.push(permission); } }; private addWhiteboardPluginToRole = (plugin?: WhiteBoardPluginPermissions) => { const permissions = plugin?.permissions; permissions?.admin?.forEach(role => this.addPermissionToRole(role, Plugins.WHITEBOARD, 'admin')); permissions?.reader?.forEach(role => this.addPermissionToRole(role, Plugins.WHITEBOARD, 'read')); permissions?.writer?.forEach(role => this.addPermissionToRole(role, Plugins.WHITEBOARD, 'write')); }; private addTranscriptionsPluginToRole = (plugin: TranscriptionPluginPermissions[] = []) => { for (const transcription of plugin) { transcription.permissions?.admin?.forEach(role => this.addPermissionToRole(role, Plugins.TRANSCRIPTIONS, 'admin', transcription.mode), ); } }; private handleNoiseCancellationPlugin = (plugin?: NoiseCancellationPlugin) => { if (!this.room) { return; } // it will be called again after internalConnect room initialization, even after network disconnection this.room.isNoiseCancellationEnabled = !!plugin?.enabled && !!this.room.isNoiseCancellationEnabled; }; private setEnv() { const endPoint = this.config?.initEndpoint!; const url = endPoint.split('https://')[1]; let env: ENV = ENV.PROD; if (url.startsWith(ENV.PROD)) { env = ENV.PROD; } else if (url.startsWith(ENV.QA)) { env = ENV.QA; } else if (url.startsWith(ENV.DEV)) { env = ENV.DEV; } this.env = env; HTTPAnalyticsTransport.setEnv(env); } } export { Store };