@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
193 lines (167 loc) • 7.1 kB
text/typescript
import { Store } from './store';
import { DeviceManager } from '../device-manager';
import { DeviceType, HMSRole } from '../interfaces';
import InitialSettings from '../interfaces/settings';
import { SimulcastLayers } from '../interfaces/simulcast-layers';
import { HMSPeerUpdate, HMSTrackUpdate, HMSUpdateListener } from '../interfaces/update-listener';
import { HMSLocalTrack } from '../media/tracks';
import HMSTransport from '../transport';
export default class RoleChangeManager {
constructor(
private store: Store,
private transport: HMSTransport,
private deviceManager: DeviceManager,
private publish: (settings: InitialSettings) => Promise<void>,
private removeAuxiliaryTrack: (trackId: string) => void,
private listener?: HMSUpdateListener,
) {}
handleLocalPeerRoleUpdate = async ({ oldRole, newRole }: { oldRole: HMSRole; newRole: HMSRole }) => {
const localPeer = this.store.getLocalPeer();
if (!localPeer) {
return;
}
await this.diffRolesAndPublishTracks({ oldRole, newRole });
this.listener?.onPeerUpdate(HMSPeerUpdate.ROLE_UPDATED, localPeer);
};
diffRolesAndPublishTracks = async ({ oldRole, newRole }: { oldRole: HMSRole; newRole: HMSRole }) => {
const wasPublishing = new Set(oldRole.publishParams.allowed);
const isPublishing = new Set(newRole.publishParams.allowed);
const removeVideo = this.removeTrack(wasPublishing, isPublishing, 'video');
const removeAudio = this.removeTrack(wasPublishing, isPublishing, 'audio');
const removeScreen = this.removeTrack(wasPublishing, isPublishing, 'screen');
const videoHasSimulcastDifference = this.hasSimulcastDifference(
oldRole.publishParams.simulcast?.video,
newRole.publishParams.simulcast?.video,
);
const screenHasSimulcastDifference = this.hasSimulcastDifference(
oldRole.publishParams.simulcast?.screen,
newRole.publishParams.simulcast?.screen,
);
const prevVideoEnabled = this.store.getLocalPeer()?.videoTrack?.enabled;
await this.removeAudioTrack(removeAudio);
await this.removeVideoTracks(removeVideo || videoHasSimulcastDifference);
await this.removeScreenTracks(removeScreen || screenHasSimulcastDifference);
const settings = this.getSettings();
if (videoHasSimulcastDifference) {
settings.isVideoMuted = !prevVideoEnabled;
}
// call publish with new settings, local track manager will diff policies
await this.publish(settings);
await this.syncDevices(settings, newRole);
};
private async syncDevices(initialSettings: InitialSettings, newRole: HMSRole) {
if ((!initialSettings.isAudioMuted || !initialSettings.isVideoMuted) && newRole.publishParams.allowed.length > 0) {
await this.deviceManager.init(true);
}
}
private async removeVideoTracks(removeVideo: boolean) {
if (!removeVideo) {
return;
}
const localPeer = this.store.getLocalPeer();
// TODO check auxillary tracks for regular audio and video too
if (localPeer?.videoTrack) {
// TODO: stop processed track and cleanup plugins loop non async
// vb can throw change role off otherwise
if (localPeer.videoTrack.isPublished) {
await this.transport.unpublish([localPeer.videoTrack]);
} else {
await localPeer.videoTrack.cleanup();
}
this.listener?.onTrackUpdate(HMSTrackUpdate.TRACK_REMOVED, localPeer.videoTrack, localPeer);
localPeer.videoTrack = undefined;
}
await this.removeAuxTracks(track => track.source !== 'screen' && track.type === 'video');
}
private async removeAudioTrack(removeAudio: boolean) {
if (!removeAudio) {
return;
}
const localPeer = this.store.getLocalPeer();
if (localPeer?.audioTrack) {
if (localPeer.audioTrack.isPublished) {
await this.transport.unpublish([localPeer.audioTrack]);
} else {
await localPeer.audioTrack.cleanup();
}
this.listener?.onTrackUpdate(HMSTrackUpdate.TRACK_REMOVED, localPeer.audioTrack, localPeer);
localPeer.audioTrack = undefined;
}
await this.removeAuxTracks(track => track.source !== 'screen' && track.type === 'audio');
}
private async removeScreenTracks(removeScreen: boolean) {
if (!removeScreen) {
return;
}
await this.removeAuxTracks(track => track.source === 'screen');
}
private async removeAuxTracks(predicate: (track: HMSLocalTrack) => boolean) {
const localPeer = this.store.getLocalPeer();
if (localPeer?.auxiliaryTracks) {
const localAuxTracks = [...localPeer.auxiliaryTracks];
for (const track of localAuxTracks) {
if (predicate(track)) {
await this.removeAuxiliaryTrack(track.trackId);
}
}
}
}
private removeTrack(wasPublishing: Set<string>, isPublishing: Set<string>, type: string) {
return wasPublishing.has(type) && !isPublishing.has(type);
}
private hasSimulcastDifference(oldLayers?: SimulcastLayers, newLayers?: SimulcastLayers) {
if (!oldLayers && !newLayers) {
return false;
}
if (oldLayers?.layers?.length !== newLayers?.layers?.length) {
return true;
}
// return true if anyone layer has different maxBitrate/maxFramerate
return !!oldLayers?.layers?.some(layer => {
const newLayer = newLayers?.layers?.find(newLayer => newLayer.rid === layer.rid);
return newLayer?.maxBitrate !== layer.maxBitrate || newLayer?.maxFramerate !== layer.maxFramerate;
});
}
private getSettings(): InitialSettings {
const { isAudioMuted, isVideoMuted } = this.getMutedStatus();
const { audioInputDeviceId, audioOutputDeviceId } = this.getAudioDeviceSettings();
const videoDeviceId = this.getVideoInputDeviceId();
return {
isAudioMuted: isAudioMuted,
isVideoMuted: isVideoMuted,
audioInputDeviceId: audioInputDeviceId,
audioOutputDeviceId: audioOutputDeviceId,
videoDeviceId: videoDeviceId,
};
}
private getMutedStatus(): { isAudioMuted: boolean; isVideoMuted: boolean } {
const initialSettings = this.store.getConfig()?.settings;
return {
isAudioMuted: initialSettings?.isAudioMuted ?? true,
isVideoMuted: initialSettings?.isVideoMuted ?? true,
};
}
private getAudioDeviceSettings(): { audioInputDeviceId: string; audioOutputDeviceId: string } {
const initialSettings = this.store.getConfig()?.settings;
const audioInputDeviceId =
this.deviceManager.currentSelection[DeviceType.audioInput]?.deviceId ||
initialSettings?.audioInputDeviceId ||
'default';
const audioOutputDeviceId =
this.deviceManager.currentSelection[DeviceType.audioOutput]?.deviceId ||
initialSettings?.audioOutputDeviceId ||
'default';
return {
audioInputDeviceId,
audioOutputDeviceId,
};
}
private getVideoInputDeviceId(): string {
const initialSettings = this.store.getConfig()?.settings;
return (
this.deviceManager.currentSelection[DeviceType.videoInput]?.deviceId ||
initialSettings?.videoDeviceId ||
'default'
);
}
}