UNPKG

@100mslive/react-native-room-kit

Version:

100ms Room Kit provides simple & easy to use UI components to build Live Streaming & Video Conferencing experiences in your apps.

408 lines (390 loc) 17.3 kB
import { HMSPollUpdateType, HMSUpdateListenerActions, HMSWhiteboardUpdateType } from '@100mslive/react-native-hms'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Alert, Keyboard, PermissionsAndroid, Platform, StatusBar, StyleSheet, View } from 'react-native'; import Toast from 'react-native-simple-toast'; import { batch, useDispatch, useSelector, useStore } from 'react-redux'; import { Preview } from './components'; import { addCuedPollId, addNotification, addPoll, changeMeetingState, changeStartingHLSStream, setHMSLocalPeerState, setHMSRoomState, setInitialRole, setLocalPeerTrackNode, setMiniViewPeerTrackNode, setWhiteboard, updateLocalPeerTrackNode } from './redux/actions'; import { createPeerTrackNode, getRandomUserId } from './utils/functions'; import { OnLeaveReason, TerminalExceptionCodes } from './utils/types'; import { Meeting } from './components/Meeting'; import { useHMSActiveSpeakerUpdates, useHMSConfig, useHMSInstance, useHMSListeners, useHMSRoomColorPalette, useHMSRoomStyle, useHMSSessionStore, useLeaveMethods, isPublishingAllowed, useAndroidSoftInputAdjustResize, useHLSCuedPolls, useIsHLSViewer } from './hooks-util'; import { peerTrackNodeExistForPeerAndTrack, replacePeerTrackNodesWithTrack, replacePeerTrackNodes, peerTrackNodeExistForPeer } from './peerTrackNodeUtils'; import { MeetingState, NotificationTypes } from './types'; import { getJoinConfig } from './utils'; import { FullScreenIndicator } from './components/FullScreenIndicator'; import { HMSMeetingEnded } from './components/HMSMeetingEnded'; import { selectChatLayoutConfig, selectIsHLSViewer, selectLayoutConfigForRole, selectShouldGoLive, selectVideoTileLayoutConfig } from './hooks-util-selectors'; import { Chat_ChatState } from '@100mslive/types-prebuilt/elements/chat'; export const HMSRoomSetup = () => { const ignoreHLSStreamPromise = useRef(false); const didInitMeetingAction = useRef(false); const hmsInstance = useHMSInstance(); const dispatch = useDispatch(); const reduxStore = useStore(); const { leave, destroy } = useLeaveMethods(); const { getConfig, clearConfig, updateConfig } = useHMSConfig(); const meetingState = useSelector(state => state.app.meetingState); const [peerTrackNodes, setPeerTrackNodes] = useState([]); const [loading, setLoading] = useState(false); const isHLSViewer = useIsHLSViewer(); const joinMeeting = useCallback(async () => { try { setLoading(true); Keyboard.dismiss(); const hmsConfig = await getConfig(); // TODO: handle case when promise returned from `getConfig()` is resolved when Root component has been unmounted hmsInstance.join(hmsConfig); await hmsInstance.setAlwaysScreenOn(true); } catch (error) { Alert.alert(error.code, error.message, [{ text: 'Leave', style: 'destructive', onPress: () => { leave(OnLeaveReason.LEAVE); } }], { cancelable: false }); } }, [getConfig, hmsInstance]); const previewMeeting = useCallback(async () => { try { setLoading(true); const hmsConfig = await getConfig(); // TODO: handle case when promise returned from `getConfig()` is resolved when Root component has been unmounted hmsInstance.preview(hmsConfig); } catch (error) { Alert.alert(error.code, error.message, [{ text: 'Leave', style: 'destructive', onPress: () => { destroy(OnLeaveReason.LEAVE); } }], { cancelable: false }); } }, [getConfig, hmsInstance]); const startHLSStreaming = useCallback(async () => { dispatch(changeStartingHLSStream(true)); try { const d = await hmsInstance.startHLSStreaming(); console.log('Start HLS Streaming Success: ', d); } catch (e) { console.log('Start HLS Streaming Error: ', e); if (!ignoreHLSStreamPromise.current) { console.log('Unable to go live at the moment: ', e); dispatch(changeStartingHLSStream(false)); } } }, [hmsInstance]); // HMS Room, Peers, Track Listeners useHMSListeners(setPeerTrackNodes); /** * Session store is a shared realtime key-value store that is accessible by everyone in the room. * It can be utilized to implement features such as pinned text, spotlight (which brings a particular * peer to the center stage for everyone in the room) and more. * * On adding this event listener, Inside `onSessionStoreAvailableListener` function you will get an * instance of `HMSSessionStore` class, then you can use this instance to "set" or "get" the value * for a specific key on session store and listen for value change updates. * * Checkout Session Store docs fore more details ${@link https://www.100ms.live/docs/react-native/v2/how-to-guides/interact-with-room/room/session-store} */ useHMSSessionStore(); useAndroidSoftInputAdjustResize(); const meetingJoined = meetingState === MeetingState.IN_MEETING; const previewing = meetingState === MeetingState.IN_PREVIEW; // HMS Error Listener useEffect(() => { const hmsErrorHandler = error => { const terminalError = error.isTerminal || TerminalExceptionCodes.includes(error.code); if (meetingJoined) { const uid = Math.random().toString(16).slice(2); const notificationPayload = terminalError ? { id: uid, type: NotificationTypes.TERMINAL_ERROR, exception: error } : { id: uid, type: NotificationTypes.ERROR, title: error.description }; dispatch(addNotification(notificationPayload)); } else { setLoading(false); if (terminalError) { Alert.alert(error.code.toString(), error.description || 'Something went wrong', [{ text: 'Leave', style: 'destructive', onPress: () => { if (previewing) { leave(OnLeaveReason.NETWORK_ISSUES); } else { destroy(OnLeaveReason.NETWORK_ISSUES); } } }], { cancelable: false }); } else { Toast.showWithGravity(`${error === null || error === void 0 ? void 0 : error.code} ${error === null || error === void 0 ? void 0 : error.description}` || 'Something went wrong', Toast.LONG, Toast.TOP); } } }; hmsInstance.addEventListener(HMSUpdateListenerActions.ON_ERROR, hmsErrorHandler); return () => { hmsInstance.removeEventListener(HMSUpdateListenerActions.ON_ERROR); }; }, [previewing, meetingJoined, hmsInstance]); // HMS Preview Listener useEffect(() => { const onPreviewHandler = data => { setLoading(false); batch(() => { dispatch(setHMSRoomState(data.room)); dispatch(setHMSLocalPeerState(data.room.localPeer)); dispatch(changeMeetingState(MeetingState.IN_PREVIEW)); }); }; hmsInstance.addEventListener(HMSUpdateListenerActions.ON_PREVIEW, onPreviewHandler); return () => { hmsInstance.removeEventListener(HMSUpdateListenerActions.ON_PREVIEW); }; }, [hmsInstance]); // HMS Join Listener useEffect(() => { const onJoinHandler = data => { var _selectVideoTileLayou; clearConfig(); setLoading(false); const reduxState = reduxStore.getState(); const localPeer = data.room.localPeer; const peer = localPeer; const track = localPeer.videoTrack; const currentLayoutConfig = selectLayoutConfigForRole(reduxState.hmsStates.layoutConfig, localPeer.role || null); const isHLSViewer = selectIsHLSViewer(localPeer.role, currentLayoutConfig); // Creating `PeerTrackNode` for local peer const localPeerTrackNode = createPeerTrackNode(peer, track); const enableLocalTileInset = (_selectVideoTileLayou = selectVideoTileLayoutConfig(currentLayoutConfig)) === null || _selectVideoTileLayou === void 0 || (_selectVideoTileLayou = _selectVideoTileLayou.grid) === null || _selectVideoTileLayou === void 0 ? void 0 : _selectVideoTileLayou.enable_local_tile_inset; const canCreateTile = isPublishingAllowed(localPeer) && !isHLSViewer; batch(() => { const chatConfig = selectChatLayoutConfig(currentLayoutConfig); const overlayChatInitialState = chatConfig && chatConfig.is_overlay && chatConfig.initial_state; if (!!chatConfig && overlayChatInitialState === Chat_ChatState.CHAT_STATE_OPEN) { dispatch({ type: 'SET_SHOW_CHAT_VIEW', showChatView: true }); } if (canCreateTile) { if (reduxState.app.localPeerTrackNode) { dispatch(updateLocalPeerTrackNode({ peer, track: peer.videoTrack })); } else { // saving above created `PeerTrackNode` in store dispatch(setLocalPeerTrackNode(localPeerTrackNode)); } // setting local `PeerTrackNode` as node for MiniView if (enableLocalTileInset) { dispatch(setMiniViewPeerTrackNode(localPeerTrackNode)); } } dispatch(setHMSRoomState(data.room)); dispatch(setHMSLocalPeerState(data.room.localPeer)); if (data.room.localPeer.role) { dispatch(setInitialRole(data.room.localPeer.role)); } }); // If `peerTrackNodes` also contains a tile for local peer then updating it setPeerTrackNodes(prevPeerTrackNodes => { if (track && peerTrackNodeExistForPeerAndTrack(prevPeerTrackNodes, peer, track)) { return replacePeerTrackNodesWithTrack(prevPeerTrackNodes, peer, track); } if (peerTrackNodeExistForPeer(prevPeerTrackNodes, peer)) { return replacePeerTrackNodes(prevPeerTrackNodes, peer); } // setting local `PeerTrackNode` in regular peerTrackNodes array when inset tile is disabled if (!enableLocalTileInset && canCreateTile) { return [localPeerTrackNode, ...prevPeerTrackNodes]; } return prevPeerTrackNodes; }); const shouldGoLive = selectShouldGoLive(reduxState); if (shouldGoLive) { startHLSStreaming(); } dispatch(changeMeetingState(MeetingState.IN_MEETING)); }; hmsInstance.addEventListener(HMSUpdateListenerActions.ON_JOIN, onJoinHandler); return () => { hmsInstance.removeEventListener(HMSUpdateListenerActions.ON_JOIN); }; }, [startHLSStreaming, hmsInstance]); if (Platform.OS === 'android') { /** * Sets up a listener for permissions requests on Android devices. * * This listener is activated when the HMS SDK requests permissions, such as camera or microphone access. * It uses the `PermissionsAndroid` API to request these permissions from the user asynchronously. * Upon receiving the permissions, it notifies the HMS SDK that the permissions have been accepted, * allowing the SDK to proceed with operations that require these permissions. * * Note: This listener is only set up and functional on Android devices, as indicated by the `Platform.OS` check. */ useEffect(() => { const onPermissionsRequested = async ({ permissions }) => { // Requests multiple permissions using the PermissionsAndroid API. await PermissionsAndroid.requestMultiple(permissions); // Notifies the HMS SDK that the permissions have been accepted. await hmsInstance.setPermissionsAcceptedOnAndroid(); }; // Adds the permissions requested listener to the HMS SDK. hmsInstance.addEventListener(HMSUpdateListenerActions.ON_PERMISSIONS_REQUESTED, onPermissionsRequested); // Cleanup function to remove the listener when the component unmounts or dependencies change. return () => { hmsInstance.removeEventListener(HMSUpdateListenerActions.ON_PERMISSIONS_REQUESTED); }; }, [hmsInstance]); } // HMS Active Speaker Listener // dev-note: This is added here because we have `setPeerTrackNodes` here useHMSActiveSpeakerUpdates(setPeerTrackNodes, meetingJoined); const meetingEnded = meetingState === MeetingState.MEETING_ENDED; // Handling Automatically calling Preview or Join API useEffect(() => { if (!meetingEnded && !didInitMeetingAction.current) { didInitMeetingAction.current = true; // let ignore = false; const handleMeetingPreviewOrJoin = async () => { var _reduxState$hmsStates; const hmsConfig = await getConfig(); const reduxState = reduxStore.getState(); const layoutData = (_reduxState$hmsStates = reduxState.hmsStates.layoutConfig) === null || _reduxState$hmsStates === void 0 ? void 0 : _reduxState$hmsStates[0]; try { var _layoutData$screens; if (getJoinConfig().skipPreview || layoutData && (_layoutData$screens = layoutData.screens) !== null && _layoutData$screens !== void 0 && (_layoutData$screens = _layoutData$screens.preview) !== null && _layoutData$screens !== void 0 && _layoutData$screens.skip_preview_screen) { if (!hmsConfig.username) { updateConfig({ username: getRandomUserId(16) }); } joinMeeting(); } else { previewMeeting(); } } catch (error) { // TODO: handle token error gracefully console.warn('🚀 ~ file: HMSRoomSetup.tsx:119 ~ handleMeetingPreviewOrJoin ~ error:', error); } }; handleMeetingPreviewOrJoin(); return () => { // ignore = true; }; } }, [meetingEnded, getConfig, updateConfig]); useEffect(() => { const subscription = hmsInstance.interactivityCenter.addPollUpdateListener(async (poll, pollUpdateType) => { const reduxState = reduxStore.getState(); const pollsData = reduxState.polls.polls; // Send HLS Timed Metadata for poll if it is started by local peer if (poll.createdBy && reduxState.hmsStates.localPeer && poll.createdBy.peerID === reduxState.hmsStates.localPeer.peerID) { hmsInstance.sendHLSTimedMetadata([{ duration: 20, payload: `poll:${poll.pollId}` }]).then(result => { console.log('sendHLSTimedMetadata result: ', result); }).catch(error => { console.log('sendHLSTimedMetadata error: ', error); }); } batch(() => { // Update poll object in store dispatch(addPoll(poll)); // when poll is started, show notification to user if (pollUpdateType === HMSPollUpdateType.started && !pollsData[poll.pollId]) { // if user is a viewer if (isHLSViewer) { // Show notification only if poll is started 20 or more seconds ago if (poll.startedAt && Date.now() - poll.startedAt.getTime() >= 20000) { dispatch(addNotification({ id: `${poll.pollId}--${pollUpdateType}`, type: NotificationTypes.POLLS_AND_QUIZZES, payload: { poll, pollUpdateType } })); dispatch(addCuedPollId(poll.pollId)); } } // if user is not a viewer, show notification else { dispatch(addNotification({ id: `${poll.pollId}--${pollUpdateType}`, type: NotificationTypes.POLLS_AND_QUIZZES, payload: { poll, pollUpdateType } })); } } }); }); return () => { subscription.remove(); }; }, [isHLSViewer]); useEffect(() => { const subscription = hmsInstance.interactivityCenter.addWhiteboardUpdateListener(async (hmsWhiteboard, updateType) => { dispatch(setWhiteboard(updateType === HMSWhiteboardUpdateType.STARTED ? hmsWhiteboard : null)); }); return () => { subscription.remove(); }; }, []); // Syncs showing Polls with HLS Player onCue event useHLSCuedPolls(); useEffect(() => { return () => { ignoreHLSStreamPromise.current = true; }; }, []); const emptyViewStyles = useHMSRoomStyle(theme => ({ backgroundColor: theme.palette.background_dim })); const { background_dim: backgroundDimColor } = useHMSRoomColorPalette(); return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(StatusBar, { backgroundColor: backgroundDimColor, barStyle: 'light-content' }), meetingState === MeetingState.IN_PREVIEW ? /*#__PURE__*/React.createElement(Preview, { join: joinMeeting, loadingButtonState: loading }) : meetingState === MeetingState.IN_MEETING ? /*#__PURE__*/React.createElement(Meeting, { peerTrackNodes: peerTrackNodes }) : meetingState === MeetingState.MEETING_ENDED ? /*#__PURE__*/React.createElement(HMSMeetingEnded, null) : loading ? /*#__PURE__*/React.createElement(FullScreenIndicator, null) : /*#__PURE__*/React.createElement(View, { style: [styles.container, emptyViewStyles] })); }; const styles = StyleSheet.create({ container: { flex: 1 } }); //# sourceMappingURL=HMSRoomSetup.js.map