mediasfu-reactnative
Version:
MediaSFU Prebuilt React Native SDK
640 lines (636 loc) • 26.1 kB
JavaScript
/* eslint-disable no-catch-shadow */
import React, { useState, useEffect, useRef } from 'react';
import { View, Text, TextInput, Pressable, Image, StyleSheet, ScrollView, KeyboardAvoidingView, Platform, } from 'react-native';
import Orientation from 'react-native-orientation-locker';
import RNPickerSelect from 'react-native-picker-select';
import { checkLimitsAndMakeRequest } from '../../methods/utils/checkLimitsAndMakeRequest';
import { createRoomOnMediaSFU } from '../../methods/utils/createRoomOnMediaSFU';
import { joinRoomOnMediaSFU } from '../../methods/utils/joinRoomOnMediaSFU';
/**
* PreJoinPage component allows users to either create a new room or join an existing one.
*
* @component
* @param {PreJoinPageOptions} props - The properties for the PreJoinPage component.
* @param {PreJoinPageParameters} props.parameters - Various parameters required for the component.
* @param {ShowAlert} [props.parameters.showAlert] - Function to show alert messages.
* @param {() => void} props.parameters.updateIsLoadingModalVisible - Function to update the loading modal visibility.
* @param {ConnectSocketType} props.parameters.connectSocket - Function to connect to the socket.
* @param {ConnectSocketType} props.parameters.connectLocalSocket - Function to connect to the local socket.
* @param {Socket} props.parameters.updateSocket - Function to update the socket.
* @param {Socket} props.parameters.updateLocalSocket - Function to update the local socket.
* @param {() => void} props.parameters.updateValidated - Function to update the validation status.
* @param {string} [props.parameters.imgSrc] - The source URL for the logo image.
* @param {Credentials} [props.credentials=user_credentials] - The user credentials containing the API username and API key.
* @param {boolean} [props.returnUI=false] - Flag to determine if the component should return the UI.
* @param {CreateMediaSFURoomOptions | JoinMediaSFURoomOptions} [props.noUIPreJoinOptions] - The options for creating/joining a room without UI.
* @param {string} [props.localLink=''] - The link to the local server.
* @param {boolean} [props.connectMediaSFU=true] - Flag to determine if the component should connect to MediaSFU.
* @param {CreateRoomOnMediaSFUType} [props.createMediaSFURoom] - Function to create a room on MediaSFU.
* @param {JoinRoomOnMediaSFUType} [props.joinMediaSFURoom] - Function to join a room on MediaSFU.
*
* @returns {JSX.Element} The rendered PreJoinPage component.
*
* @example
* ```tsx
* import React from 'react';
* import { PreJoinPage } from 'mediasfu-reactnative';
* import { JoinLocalRoomOptions } from 'mediasfu-reactnative';
*
* function App() {
* * const showAlertFunction = (message: string) => console.log(message);
* const updateLoadingFunction = (visible: boolean) => console.log(`Loading: ${visible}`);
* const connectSocketFunction = () => {}; // Connect socket function
* const updateSocketFunction = (socket: Socket) => {}; // Update socket function
* const updateValidatedFunction = (validated: boolean) => {}; // Update validated function
* const updateApiUserNameFunction = (userName: string) => {}; // Update API username function
* const updateApiTokenFunction = (token: string) => {}; // Update API token function
* const updateLinkFunction = (link: string) => {}; // Update link function
* const updateRoomNameFunction = (roomName: string) => {}; // Update room name function
* const updateMemberFunction = (member: string) => {}; // Update member function
*
* return (
* <PreJoinPage
* parameters={{
* showAlert: showAlertFunction,
* updateIsLoadingModalVisible: updateLoadingFunction,
* connectSocket: connectSocketFunction,
* updateSocket: updateSocketFunction,
* updateValidated: updateValidatedFunction,
* updateApiUserName: updateApiUserNameFunction,
* updateApiToken: updateApiTokenFunction,
* updateLink: updateLinkFunction,
* updateRoomName: updateRoomNameFunction,
* updateMember: updateMemberFunction,
* imgSrc: 'https://example.com/logo.png'
* }}
* credentials={{
* apiUserName: 'user123',
* apiKey: 'apikey123'
* }}
* returnUI={true}
* noUIPreJoinOptions={{
* action: 'create',
* capacity: 10,
* duration: 15,
* eventType: 'broadcast',
* userName: 'Prince',
* }}
* connectMediaSFU={true}
* localLink='http://localhost:3000'
* />
* );
* };
*
*
* export default App;
* ```
*/
const PreJoinPage = ({ localLink = '', connectMediaSFU = true, parameters, credentials, returnUI = false, noUIPreJoinOptions, createMediaSFURoom = createRoomOnMediaSFU, joinMediaSFURoom = joinRoomOnMediaSFU, }) => {
// State variables
const [isCreateMode, setIsCreateMode] = useState(false);
const [name, setName] = useState('');
const [duration, setDuration] = useState('');
const [eventType, setEventType] = useState('');
const [capacity, setCapacity] = useState('');
const [eventID, setEventID] = useState('');
const [error, setError] = useState('');
const pending = useRef(false);
const localConnected = useRef(false);
const localData = useRef(undefined);
const initSocket = useRef(undefined);
const { showAlert, updateIsLoadingModalVisible, connectLocalSocket, updateSocket, updateValidated, updateApiUserName, updateApiToken, updateLink, updateRoomName, updateMember, } = parameters;
const handleCreateRoom = async () => {
var _a, _b;
if (pending.current) {
return;
}
pending.current = true;
let payload = {};
if (returnUI) {
if (!name || !duration || !eventType || !capacity) {
setError('Please fill all the fields.');
return;
}
payload = {
action: 'create',
duration: parseInt(duration, 10),
capacity: parseInt(capacity, 10),
eventType: eventType,
userName: name,
recordOnly: false,
};
}
else {
if (noUIPreJoinOptions &&
'action' in noUIPreJoinOptions &&
noUIPreJoinOptions.action === 'create') {
payload = noUIPreJoinOptions;
}
else {
pending.current = false;
throw new Error('Invalid options provided for creating a room without UI.');
}
}
updateIsLoadingModalVisible(true);
if (localLink.length > 0) {
const secureCode = Math.random().toString(30).substring(2, 14) +
Math.random().toString(30).substring(2, 14);
let eventID = new Date().getTime().toString(30) +
new Date().getUTCMilliseconds() +
Math.floor(10 + Math.random() * 99).toString();
eventID = 'm' + eventID;
const eventRoomParams = (_a = localData.current) === null || _a === void 0 ? void 0 : _a.meetingRoomParams_;
eventRoomParams.type = eventType;
const createData = {
eventID: eventID,
duration: payload.duration,
capacity: payload.capacity,
userName: payload.userName,
scheduledDate: new Date(),
secureCode: secureCode,
waitRoom: false,
recordingParams: (_b = localData.current) === null || _b === void 0 ? void 0 : _b.recordingParams_,
eventRoomParams: eventRoomParams,
videoPreference: null,
audioPreference: null,
audioOutputPreference: null,
mediasfuURL: '',
};
// socket in main window is required and for no local room, no use of initSocket
// for local room, initSocket becomes the local socket, and localSocket is the connection to MediaSFU (if connectMediaSFU is true)
// else localSocket is the same as initSocket
if (connectMediaSFU &&
initSocket.current &&
localData.current &&
localData.current.apiUserName &&
localData.current.apiKey) {
payload.recordOnly = true; // allow production to mediasfu only; no consumption
const response = await roomCreator({
payload,
apiUserName: localData.current.apiUserName,
apiKey: localData.current.apiKey,
validate: false,
});
if (response &&
response.success &&
response.data &&
'roomName' in response.data) {
createData.eventID = response.data.roomName;
createData.secureCode = response.data.secureCode || '';
createData.mediasfuURL = response.data.publicURL;
await createLocalRoom({
createData: createData,
link: response.data.link,
});
}
else {
pending.current = false;
updateIsLoadingModalVisible(false);
setError('Unable to create room on MediaSFU.');
try {
updateSocket(initSocket.current);
await createLocalRoom({ createData: createData });
pending.current = false;
}
catch (error) {
pending.current = false;
updateIsLoadingModalVisible(false);
setError(`Unable to create room. ${error}`);
}
}
}
else {
try {
updateSocket(initSocket.current);
await createLocalRoom({ createData: createData });
pending.current = false;
}
catch (error) {
pending.current = false;
updateIsLoadingModalVisible(false);
setError(`Unable to create room. ${error}`);
}
}
}
else {
await roomCreator({
payload,
apiUserName: credentials.apiUserName,
apiKey: credentials.apiKey,
validate: true,
});
pending.current = false;
}
};
const handleJoinRoom = async () => {
if (pending.current) {
return;
}
pending.current = true;
let payload = {};
if (returnUI) {
if (!name || !eventID) {
setError('Please fill all the fields.');
return;
}
payload = {
action: 'join',
meetingID: eventID,
userName: name,
};
}
else {
if (noUIPreJoinOptions &&
'action' in noUIPreJoinOptions &&
noUIPreJoinOptions.action === 'join') {
payload = noUIPreJoinOptions;
}
else {
throw new Error('Invalid options provided for joining a room without UI.');
}
}
if (localLink.length > 0 && !localLink.includes('mediasfu.com')) {
const joinData = {
eventID: payload.meetingID,
userName: payload.userName,
secureCode: '',
videoPreference: null,
audioPreference: null,
audioOutputPreference: null,
};
await joinLocalRoom({ joinData: joinData });
pending.current = false;
return;
}
updateIsLoadingModalVisible(true);
const response = await joinMediaSFURoom({
payload,
apiUserName: credentials.apiUserName,
apiKey: credentials.apiKey,
localLink: localLink,
});
if (response.success && response.data && 'roomName' in response.data) {
await checkLimitsAndMakeRequest({
apiUserName: response.data.roomName,
apiToken: response.data.secret,
link: response.data.link,
userName: payload.userName,
parameters: parameters,
});
setError('');
pending.current = false;
}
else {
pending.current = false;
updateIsLoadingModalVisible(false);
setError(`Unable to join room. ${response.data
? 'error' in response.data
? response.data.error
: ''
: ''}`);
}
};
const joinLocalRoom = async ({ joinData, link = localLink, }) => {
var _a;
(_a = initSocket.current) === null || _a === void 0 ? void 0 : _a.emit('joinEventRoom', joinData, (response) => {
var _a;
if (response.success) {
updateSocket(initSocket.current);
updateApiUserName(((_a = localData.current) === null || _a === void 0 ? void 0 : _a.apiUserName) || '');
updateApiToken(response.secret);
updateLink(link);
updateRoomName(joinData.eventID);
updateMember(joinData.userName);
updateIsLoadingModalVisible(false);
updateValidated(true);
}
else {
updateIsLoadingModalVisible(false);
setError(`Unable to join room. ${response.reason}`);
}
});
};
const createLocalRoom = async ({ createData, link = localLink, }) => {
var _a;
(_a = initSocket.current) === null || _a === void 0 ? void 0 : _a.emit('createRoom', createData, (response) => {
var _a;
if (response.success) {
updateSocket(initSocket.current);
updateApiUserName(((_a = localData.current) === null || _a === void 0 ? void 0 : _a.apiUserName) || '');
updateApiToken(response.secret);
updateLink(link);
updateRoomName(createData.eventID);
// local needs islevel updated from here
// we update member as `userName` + '_2' and split it in the room
updateMember(createData.userName + '_2');
updateIsLoadingModalVisible(false);
updateValidated(true);
}
else {
updateIsLoadingModalVisible(false);
setError(`Unable to create room. ${response.reason}`);
}
});
};
const roomCreator = async ({ payload, apiUserName, apiKey, validate = true, }) => {
const response = await createMediaSFURoom({
payload,
apiUserName: apiUserName,
apiKey: apiKey,
localLink: localLink,
});
if (response.success && response.data && 'roomName' in response.data) {
await checkLimitsAndMakeRequest({
apiUserName: response.data.roomName,
apiToken: response.data.secret,
link: response.data.link,
userName: payload.userName,
parameters: parameters,
validate: validate,
});
return response;
}
else {
updateIsLoadingModalVisible(false);
setError(`Unable to create room. ${response.data
? 'error' in response.data
? response.data.error
: ''
: ''}`);
}
};
const checkProceed = async ({ returnUI, noUIPreJoinOptions, }) => {
if (!returnUI && noUIPreJoinOptions) {
if ('action' in noUIPreJoinOptions &&
noUIPreJoinOptions.action === 'create') {
// update all the required parameters and call
const createOptions = noUIPreJoinOptions;
if (!createOptions.userName ||
!createOptions.duration ||
!createOptions.eventType ||
!createOptions.capacity) {
throw new Error('Please provide all the required parameters: userName, duration, eventType, capacity');
}
await handleCreateRoom();
}
else if ('action' in noUIPreJoinOptions &&
noUIPreJoinOptions.action === 'join') {
// update all the required parameters and call
const joinOptions = noUIPreJoinOptions;
if (!joinOptions.userName || !joinOptions.meetingID) {
throw new Error('Please provide all the required parameters: userName, meetingID');
}
await handleJoinRoom();
}
else {
throw new Error('Invalid options provided for creating/joining a room without UI.');
}
}
};
useEffect(() => {
if (localLink.length > 0 &&
!localConnected.current &&
!initSocket.current) {
try {
connectLocalSocket === null || connectLocalSocket === void 0 ? void 0 : connectLocalSocket({ link: localLink }).then((response) => {
localData.current = response.data;
initSocket.current = response.socket;
localConnected.current = true;
if (!returnUI && noUIPreJoinOptions) {
checkProceed({ returnUI, noUIPreJoinOptions });
}
}).catch((error) => {
showAlert === null || showAlert === void 0 ? void 0 : showAlert({
message: `Unable to connect to ${localLink}. ${error}`,
type: 'danger',
duration: 3000,
});
});
}
catch (_a) {
showAlert === null || showAlert === void 0 ? void 0 : showAlert({
message: `Unable to connect to ${localLink}. Something went wrong.`,
type: 'danger',
duration: 3000,
});
}
}
else if (localLink.length === 0 && !initSocket.current) {
if (!returnUI && noUIPreJoinOptions) {
checkProceed({ returnUI, noUIPreJoinOptions });
}
}
}, []);
const handleToggleMode = () => {
setIsCreateMode(!isCreateMode);
setError('');
};
/**
* Locks the orientation to portrait mode when the component mounts and unlocks on unmount.
*/
useEffect(() => {
Orientation.lockToPortrait();
return () => {
Orientation.unlockAllOrientations();
};
}, []);
if (!returnUI) {
return <></>;
}
return (<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={styles.keyboardAvoidingContainer}>
<ScrollView contentContainerStyle={styles.scrollContainer}>
<View style={[
styles.container,
Platform.OS === 'web' && { maxWidth: 600, alignSelf: 'center' },
]}>
{/* Logo */}
<View style={styles.logoContainer}>
<Image source={{
uri: parameters.imgSrc ||
'https://mediasfu.com/images/logo192.png',
}} style={styles.logoImage}/>
</View>
{/* Input Fields */}
<View style={styles.inputContainer}>
{isCreateMode ? (<>
<TextInput style={styles.inputField} placeholder="Display Name" value={name} onChangeText={setName} autoCapitalize="none" autoCorrect={false} accessibilityLabel="Display Name" placeholderTextColor="gray"/>
<TextInput style={styles.inputField} placeholder="Duration (minutes)" value={duration} onChangeText={setDuration} keyboardType="numeric" autoCapitalize="none" autoCorrect={false} accessibilityLabel="Duration (minutes)" placeholderTextColor="gray"/>
<RNPickerSelect onValueChange={(value) => {
setEventType(value);
}} items={[
{ label: 'Chat', value: 'chat' },
{ label: 'Broadcast', value: 'broadcast' },
{ label: 'Webinar', value: 'webinar' },
{ label: 'Conference', value: 'conference' },
]} value={eventType} style={pickerSelectStyles} placeholder={{
label: 'Select Event Type',
value: '',
color: 'gray',
}} useNativeAndroidPickerStyle={false}/>
<View style={styles.gap}/>
<TextInput style={styles.inputField} placeholder="Room Capacity" value={capacity} onChangeText={setCapacity} keyboardType="numeric" autoCapitalize="none" autoCorrect={false} accessibilityLabel="Room Capacity" placeholderTextColor="gray"/>
<Pressable style={styles.actionButton} onPress={handleCreateRoom} accessibilityRole="button" accessibilityLabel="Create Room">
<Text style={styles.actionButtonText}>Create Room</Text>
</Pressable>
</>) : (<>
<TextInput style={styles.inputField} placeholder="Display Name" value={name} onChangeText={setName} autoCapitalize="none" autoCorrect={false} accessibilityLabel="Display Name" placeholderTextColor="gray"/>
<TextInput style={styles.inputField} placeholder="Event ID" value={eventID} onChangeText={setEventID} autoCapitalize="none" autoCorrect={false} accessibilityLabel="Event ID" placeholderTextColor="gray"/>
<Pressable style={styles.actionButton} onPress={handleJoinRoom} accessibilityRole="button" accessibilityLabel="Join Room">
<Text style={styles.actionButtonText}>Join Room</Text>
</Pressable>
</>)}
{error !== '' && <Text style={styles.errorText}>{error}</Text>}
</View>
{/* OR Separator */}
<View style={styles.orContainer}>
<Text style={styles.orText}>OR</Text>
</View>
{/* Toggle Mode Button */}
<View style={styles.toggleContainer}>
<Pressable style={styles.toggleButton} onPress={handleToggleMode} accessibilityRole="button" accessibilityLabel={isCreateMode ? 'Switch to Join Mode' : 'Switch to Create Mode'}>
<Text style={styles.toggleButtonText}>
{isCreateMode ? 'Switch to Join Mode' : 'Switch to Create Mode'}
</Text>
</Pressable>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>);
};
export default PreJoinPage;
/**
* Stylesheet for the PreJoinPage component.
*/
const styles = StyleSheet.create({
keyboardAvoidingContainer: {
flex: 1,
},
scrollContainer: {
flexGrow: 1,
justifyContent: 'center',
backgroundColor: '#53C6E0',
paddingVertical: 10,
maxHeight: '100%',
},
container: {
flex: 1,
paddingHorizontal: '10%',
justifyContent: 'center',
alignItems: 'center',
},
logoContainer: {
marginBottom: 30,
alignItems: 'center',
},
logoImage: {
width: 100,
height: 100,
borderRadius: 50,
},
inputContainer: {
width: '100%',
marginBottom: 10,
},
inputField: {
height: 40,
width: '100%',
borderColor: 'black',
borderWidth: 1,
marginBottom: 15,
paddingHorizontal: 15,
borderRadius: 8,
backgroundColor: '#ffffff',
fontSize: 16,
},
actionButton: {
backgroundColor: 'black',
paddingVertical: 8,
borderRadius: 8,
alignItems: 'center',
marginBottom: 10,
},
actionButtonText: {
color: 'white',
fontWeight: 'bold',
fontSize: 14,
},
toggleContainer: {
alignItems: 'center',
justifyContent: 'center',
},
toggleButton: {
backgroundColor: 'black',
paddingVertical: 5,
paddingHorizontal: 25,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
toggleButtonText: {
color: 'white',
fontWeight: 'bold',
fontSize: 16,
},
errorText: {
color: 'red',
textAlign: 'center',
marginTop: 10,
fontSize: 14,
},
orContainer: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 20,
},
orText: {
color: 'black',
fontSize: 16,
fontWeight: 'bold',
marginHorizontal: 10,
},
gap: {
marginBottom: 10,
},
});
const pickerSelectStyles = StyleSheet.create({
inputIOS: {
height: 40,
borderColor: 'gray',
borderWidth: 1,
marginBottom: 20,
paddingHorizontal: 5,
borderRadius: 5,
backgroundColor: '#ffffff',
fontSize: 16,
color: 'black',
paddingRight: 20,
},
inputAndroid: {
height: 40,
borderColor: 'gray',
borderWidth: 1,
marginBottom: 10,
paddingHorizontal: 10,
borderRadius: 5,
backgroundColor: '#ffffff',
fontSize: 16,
color: 'black',
paddingRight: 20,
},
inputWeb: {
height: 30,
borderColor: 'gray',
borderWidth: 1,
marginBottom: 10,
paddingHorizontal: 10,
borderRadius: 5,
backgroundColor: '#ffffff',
fontSize: 16,
color: 'black',
paddingRight: 20,
},
placeholder: {
color: 'gray',
},
});
//# sourceMappingURL=PreJoinPage.js.map