@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
543 lines (483 loc) • 17.6 kB
text/typescript
import { createSelector } from 'reselect';
import { byIDCurry } from './common';
import {
selectFullAppData,
selectHMSMessages,
selectLocalPeerID,
selectLocalPeerRole,
selectMessagesMap,
selectPeers,
selectPeersMap,
selectPollsMap,
selectTracksMap,
} from './selectors';
import {
getScreenSharesByPeer,
isAudio,
isAudioPlaylist,
isTrackEnabled,
isVideo,
isVideoPlaylist,
} from './selectorUtils';
import { HMSLogger } from '../common/ui-logger';
import { HMSTranscriptionMode } from '../internal';
import {
HMSAudioTrack,
HMSGenericTypes,
HMSPeer,
HMSPeerID,
HMSRoleName,
HMSScreenVideoTrack,
HMSStore,
HMSTrack,
HMSTrackID,
HMSVideoTrack,
} from '../schema';
const selectPeerID = (_store: HMSStore, peerID: HMSPeerID | undefined) => peerID;
const selectTrackID = (_store: HMSStore, trackID: HMSTrackID | undefined) => trackID;
const selectRoleName = (_store: HMSStore, roleName: HMSRoleName | undefined) => roleName;
const selectAppDataKey = (_store: HMSStore, key: string | undefined) => key;
const selectPollID = (_store: HMSStore, pollID: string | undefined) => pollID;
const selectPeerByIDBare = createSelector([selectPeersMap, selectPeerID], (storePeers, peerID) =>
peerID ? storePeers[peerID] : null,
);
const selectTrackByIDBare = createSelector([selectTracksMap, selectTrackID], (storeTracks, trackID) =>
trackID ? storeTracks[trackID] : null,
);
const selectVideoTrackByIDBare = createSelector([selectTracksMap, selectTrackID], (storeTracks, trackID) => {
if (!trackID) {
return null;
}
const track = storeTracks[trackID] as HMSVideoTrack | undefined;
if (track?.type === 'video') {
return track;
}
return null;
});
const selectAudioTrackByIDBare = createSelector([selectTracksMap, selectTrackID], (storeTracks, trackID) => {
if (!trackID) {
return null;
}
const track = storeTracks[trackID] as HMSAudioTrack | undefined;
if (track?.type === 'audio') {
return track;
}
return null;
});
const selectScreenAudioTrackByIDBare = createSelector([selectTracksMap, selectTrackID], (storeTracks, trackID) => {
if (!trackID) {
return null;
}
const track = storeTracks[trackID] as HMSAudioTrack | undefined;
if (track?.type === 'audio' && track?.source === 'screen') {
return track;
}
return null;
});
const selectScreenVideoTrackByIDBare = createSelector([selectTracksMap, selectTrackID], (storeTracks, trackID) => {
if (!trackID) {
return null;
}
const track = storeTracks[trackID] as HMSScreenVideoTrack | undefined;
if (track?.type === 'video' && track?.source === 'screen') {
return track;
}
return null;
});
const selectPollByIDBare = createSelector([selectPollsMap, selectPollID], (storePolls, pollID) =>
pollID ? storePolls[pollID] : null,
);
/**
* Select the {@link HMSPeer} object given a peer ID.
*/
export const selectPeerByID = byIDCurry(selectPeerByIDBare);
/**
* Select a particular key from ui app data by passed in key.
* if key is not passed, full data is returned.
*/
export const selectAppData = byIDCurry(
createSelector([selectFullAppData, selectAppDataKey], (appData, key) => {
if (!appData) {
return undefined;
}
if (key) {
return appData[key];
}
return appData;
}),
);
/**
* Select a particular key from session store by passed in key.
* if key is not passed, full data is returned.
*/
export function selectSessionStore<T extends HMSGenericTypes = { sessionStore: Record<string, any> }>(): (
store: HMSStore<T>,
) => T['sessionStore'] | undefined;
export function selectSessionStore<
T extends HMSGenericTypes = { sessionStore: Record<string, any> },
K extends keyof T['sessionStore'] = keyof T['sessionStore'],
>(key: K): (store: HMSStore<T>) => T['sessionStore'][K] | undefined;
export function selectSessionStore<
T extends HMSGenericTypes = { sessionStore: Record<string, any> },
K extends keyof T['sessionStore'] = keyof T['sessionStore'],
>(key?: K) {
return (store: HMSStore<T>) => {
if (!store.sessionStore) {
return undefined;
}
if (key) {
return store.sessionStore[key];
}
return store.sessionStore;
};
}
export const selectAppDataByPath = (...keys: string[]) =>
createSelector([selectFullAppData], appData => {
if (!appData) {
return undefined;
}
if (keys && keys.length > 0) {
let value = appData;
for (const key of keys) {
if (!key) {
return value;
}
value = value?.[key];
}
return value;
}
return appData;
});
/**
* Select the name of a {@link HMSPeer} given a peer ID.
*/
export const selectPeerNameByID = byIDCurry(createSelector(selectPeerByIDBare, peer => peer?.name));
export const selectPeerTypeByID = byIDCurry(createSelector(selectPeerByIDBare, peer => peer?.type));
/**
* Select the {@link HMSTrack} object given a track ID.
*/
export const selectTrackByID = byIDCurry(selectTrackByIDBare);
/**
* Select the {@link HMSVideoTrack} object given a track ID.
*/
export const selectVideoTrackByID = byIDCurry(selectVideoTrackByIDBare);
/**
* Select the {@link HMSAudioTrack} object given a track ID.
*/
export const selectAudioTrackByID = byIDCurry(selectAudioTrackByIDBare);
/**
* Select the {@link HMSScreenAudioTrack} object given a track ID.
*/
export const selectScreenAudioTrackByID = byIDCurry(selectScreenAudioTrackByIDBare);
/**
* Select the {@link HMSScreenVideoTrack} object given a track ID.
*/
export const selectScreenVideoTrackByID = byIDCurry(selectScreenVideoTrackByIDBare);
/**
* Select the primary video track of a peer given a peer ID.
*/
export const selectVideoTrackByPeerID = byIDCurry((store: HMSStore, peerID?: HMSPeerID): HMSVideoTrack | undefined => {
const peer = selectPeerByIDBare(store, peerID);
if (peer && peer.videoTrack && peer.videoTrack !== '') {
return store.tracks[peer.videoTrack] as HMSVideoTrack;
}
return undefined;
});
/**
* Select the primary audio track of a peer given a peer ID.
*/
export const selectAudioTrackByPeerID = byIDCurry((store: HMSStore, peerID?: HMSPeerID): HMSAudioTrack | undefined => {
const peer = selectPeerByIDBare(store, peerID);
if (peer && peer.audioTrack && peer.audioTrack !== '') {
return store.tracks[peer.audioTrack] as HMSAudioTrack;
}
return undefined;
});
/**
* Select the camera stream of a peer given a peer ID.
* This is the primary video track of a peer.
*/
export const selectCameraStreamByPeerID = selectVideoTrackByPeerID;
/**
* Select an array of auxiliary tracks of a peer given a peer ID.
*/
export const selectAuxiliaryTracksByPeerID = byIDCurry((store: HMSStore, peerID?: HMSPeerID): HMSTrack[] => {
const peer = selectPeerByIDBare(store, peerID);
return peer?.auxiliaryTracks.map(trackID => store.tracks[trackID]) || [];
});
const selectSpeakerByTrackID = (store: HMSStore, trackID: HMSTrackID | undefined) => {
return trackID ? store.speakers[trackID] : null;
};
/**
* Select the audio level of a track given a track ID.
*/
export const selectTrackAudioByID = byIDCurry(
createSelector(selectSpeakerByTrackID, speaker => speaker?.audioLevel || 0),
);
/**
* Select speaker object of audioTrack of a peer given a peer ID.
*/
const selectSpeakerByPeerID = (store: HMSStore, peerID: HMSPeerID | undefined) => {
const peerAudioTrack = selectAudioTrackByPeerID(peerID)(store);
return selectSpeakerByTrackID(store, peerAudioTrack?.id);
};
/**
* Select audio level of audioTrack of a peer given a peer IDß.
*/
export const selectPeerAudioByID = byIDCurry(
createSelector(selectSpeakerByPeerID, speaker => speaker?.audioLevel || 0),
);
export const selectConnectionQualityByPeerID = byIDCurry((store: HMSStore, peerID: HMSPeerID | undefined) => {
if (peerID) {
return store.connectionQualities[peerID];
}
return undefined;
});
/**
* Select the first auxiliary audio track of a peer given a peer ID.
*/
export const selectAuxiliaryAudioByPeerID = byIDCurry((store: HMSStore, peerID?: HMSPeerID) => {
const peer = selectPeerByIDBare(store, peerID);
if (peer) {
const trackID = peer?.auxiliaryTracks.find(trackID => isAudio(store.tracks[trackID]));
return trackID ? (store.tracks[trackID] as HMSAudioTrack) : undefined;
}
return undefined;
});
export const selectVideoPlaylistVideoTrackByPeerID = byIDCurry(
createSelector(selectTracksMap, selectPeerByIDBare, (tracks, peer) => {
const trackID = peer?.auxiliaryTracks.find(trackID => {
const track = tracks[trackID];
return isVideoPlaylist(track) && isVideo(track);
});
return trackID ? (tracks[trackID] as HMSVideoTrack) : undefined;
}),
);
export const selectVideoPlaylistAudioTrackByPeerID = byIDCurry(
createSelector(selectTracksMap, selectPeerByIDBare, (tracks, peer) => {
const trackID = peer?.auxiliaryTracks.find(trackID => {
const track = tracks[trackID];
return isVideoPlaylist(track) && isAudio(track);
});
return trackID ? (tracks[trackID] as HMSAudioTrack) : undefined;
}),
);
export const selectAudioPlaylistTrackByPeerID = byIDCurry(
createSelector(selectTracksMap, selectPeerByIDBare, (tracks, peer) => {
const trackID = peer?.auxiliaryTracks.find(trackID => {
const track = tracks[trackID];
return isAudioPlaylist(track) && isAudio(track);
});
return trackID ? (tracks[trackID] as HMSAudioTrack) : undefined;
}),
);
export const selectScreenSharesByPeerId = byIDCurry(
createSelector(selectTracksMap, selectPeerByIDBare, (tracks, peer) => {
return getScreenSharesByPeer(tracks, peer);
}),
);
/**
* Select the screen share video track of a peer given a peer ID.
*/
export const selectScreenShareByPeerID = (id?: string) =>
createSelector(selectScreenSharesByPeerId(id), screenshare => {
return screenshare.video;
});
/**
* Select the screen share audio track of a peer given a peer ID.
*/
export const selectScreenShareAudioByPeerID = (id?: string) =>
createSelector(selectScreenSharesByPeerId(id), screenshare => {
return screenshare.audio;
});
/**
* Select a boolean denoting whether a peer has unmuted audio and sharing it to other peers.
*/
export const selectIsPeerAudioEnabled = byIDCurry((store: HMSStore, peerID?: string) => {
const peer = selectPeerByIDBare(store, peerID);
return isTrackEnabled(store, peer?.audioTrack);
});
/**
* Select a boolean denoting whether a peer has unmuted video and sharing it to other peers.
*/
export const selectIsPeerVideoEnabled = byIDCurry((store: HMSStore, peerID?: string) => {
const peer = selectPeerByIDBare(store, peerID);
return isTrackEnabled(store, peer?.videoTrack);
});
/**
* Select a boolean denoting whether you've muted an audio track locally(only for you) given a track ID.
*/
export const selectIsAudioLocallyMuted = byIDCurry((store: HMSStore, trackID?: string) => {
if (trackID && store.tracks[trackID]) {
return (store.tracks[trackID] as HMSAudioTrack).volume === 0;
}
return undefined;
});
/**
* Select a boolean denoting whether you've muted the primary audio track of a peer locally(only for you) given a peer ID.
*/
export const selectIsLocallyMutedByPeerID = byIDCurry((store: HMSStore, peerID?: string) => {
const peer = selectPeerByIDBare(store, peerID);
return selectIsAudioLocallyMuted(peer?.audioTrack)(store);
});
/**
* Select a boolean denoting whether you've muted the screen share audio track of a peer locally(only for you) given a peer ID.
*/
export const selectIsScreenShareLocallyMutedByPeerID = byIDCurry((store: HMSStore, peerID?: string) => {
const track = selectScreenShareAudioByPeerID(peerID)(store);
return selectIsAudioLocallyMuted(track?.id)(store);
});
/**
* Select the local audio volume of an audio track given a track ID.
*
* NOTE: **Volume** of a track is different from **Audio Level** of a track,
* - Audio Level measures the audio of a track and it comes from 100ms's servers.
* - Volume is how loud you hear the audio of a track, this is controlled by you at the client side.
*/
export const selectAudioTrackVolume = byIDCurry((store: HMSStore, trackID?: string) => {
const track = selectTrackByIDBare(store, trackID);
if (track) {
if (track.type !== 'audio') {
HMSLogger.w('Please pass audio track here');
return undefined;
}
return track.volume;
}
return undefined;
});
/**
* Select the local audio volume of the primary audio track of a peer given a peer ID.
*/
export const selectAudioVolumeByPeerID = byIDCurry((store: HMSStore, peerID?: string) => {
const peer = selectPeerByIDBare(store, peerID);
return selectAudioTrackVolume(peer?.audioTrack)(store);
});
/**
* Select the local audio volume of the screen share of a peer given a peer ID.
*/
export const selectScreenshareAudioVolumeByPeerID = byIDCurry((store: HMSStore, peerID?: string) => {
const track = selectScreenShareAudioByPeerID(peerID)(store);
return selectAudioTrackVolume(track?.id)(store);
});
/**
* Select the current simulcast layer of a track given a track ID.
*/
export const selectSimulcastLayerByTrack = byIDCurry((store: HMSStore, trackID?: string) => {
const track = selectTrackByIDBare(store, trackID);
if (track) {
if (track.type !== 'video') {
HMSLogger.w('Please pass video track here');
return undefined;
}
return track.layer;
}
return undefined;
});
const selectMessagesByPeerIDInternal = createSelector(
[selectHMSMessages, selectLocalPeerID, selectPeerID],
(messages, localPeerID, peerID) => {
if (!peerID) {
return undefined;
}
return messages.filter(message => {
// Broadcast message
if (!message.recipientPeer && !message.recipientRoles?.length) {
return false;
}
// if localPeer or peerID is not a sender remove this
if (message.sender && ![localPeerID, peerID].includes(message.sender)) {
return false;
}
// at this point we know the sender is one of local or passed in peer, check the recipient side
return [localPeerID, peerID].includes(message.recipientPeer!);
});
},
);
const selectMessagesByRoleInternal = createSelector([selectHMSMessages, selectRoleName], (messages, roleName) => {
if (!roleName) {
return undefined;
}
return messages.filter(message => {
// Not Role message - Broadcast message or Private Peer message
if (!message.recipientRoles?.length) {
return false;
}
return message.recipientRoles?.includes(roleName);
});
});
export const selectBroadcastMessages = createSelector(selectHMSMessages, messages => {
return messages.filter(message => {
return !message.recipientPeer && !message.recipientRoles?.length;
});
});
const selectUnreadMessageCountByRole = createSelector([selectMessagesByRoleInternal, selectRoleName], messages => {
if (!messages) {
return 0;
}
return messages.filter(m => !m.read).length;
});
const selectUnreadMessageCountByPeerID = createSelector([selectMessagesByPeerIDInternal, selectPeerID], messages => {
if (!messages) {
return 0;
}
return messages.filter(m => !m.read).length;
});
export const selectBroadcastMessagesUnreadCount = createSelector(selectBroadcastMessages, messages => {
return messages.filter(m => !m.read).length;
});
export const selectMessagesByPeerID = byIDCurry(selectMessagesByPeerIDInternal);
export const selectMessagesByRole = byIDCurry(selectMessagesByRoleInternal);
export const selectMessagesUnreadCountByRole = byIDCurry(selectUnreadMessageCountByRole);
export const selectMessagesUnreadCountByPeerID = byIDCurry(selectUnreadMessageCountByPeerID);
/**
* Select an array of peers of a particular role
* @param role HMSRoleName
* @returns HMSPeer[]
*/
export const selectPeersByRole = (role: HMSRoleName) =>
createSelector([selectPeers], peers => {
return peers.filter(p => p.roleName === role);
});
/**
* Select an array of peers of a particular role
* @param roles HMSRoleName[]
* @returns HMSPeer[]
*/
export const selectPeersByRoles = (roles: HMSRoleName[]) =>
createSelector([selectPeers], (peers: HMSPeer[]) => {
return peers.filter((peer: HMSPeer) => {
return peer.roleName ? roles.includes(peer.roleName) : false;
});
});
/**
* Selects the peer metadata for the passed in peer and returns it as JSON. If metadata is not present
* or conversion to JSON gives an error, an empty object is returned.
* Please directly use peer.metadata in case the metadata is not JSON by design.
*/
export const selectPeerMetadata = (peerId: HMSPeerID) =>
createSelector(selectPeerByID(peerId), peer => {
try {
return peer?.metadata && peer.metadata !== '' ? JSON.parse(peer.metadata) : {};
} catch (error) {
console.error('cannot parse peer metadata', error);
return {};
}
});
export const selectHasPeerHandRaised = (peerId: HMSPeerID) =>
createSelector(selectPeerByID(peerId), peer => {
return !!peer?.isHandRaised;
});
export const selectPeerName = (peerId: HMSPeerID) => createSelector(selectPeerByID(peerId), peer => peer?.name);
export const selectPollByID = byIDCurry(selectPollByIDBare);
export const selectMessageByMessageID = (id: string) =>
createSelector(selectMessagesMap, messages => {
return messages[id];
});
export const selectIsTranscriptionAllowedByMode = (mode: HMSTranscriptionMode) =>
createSelector(selectLocalPeerRole, role => {
if (!role?.permissions.transcriptions) {
return false;
}
// only one admin permission
return role.permissions.transcriptions[mode].length > 0;
});