20
loading...
This website collects cookies to deliver better user experience
react-native-daily-js
library and Daily’s customizable call object. App
component as the top-level parent component. It will render the Header
component with the app title and information. It will also conditionally render either the InCall
component, which handles the Daily audio call, or the PreJoinRoom
, which has a form to join a Daily audio call, depending on our app state.InCall
component has the most complexity because it handles our Daily call.InCall
contains the following components:Counter
component, which displays how much time is left in the callCopyLinkBox
to copy and share the room codeTray
to control your local microphone, raise your hand, or leave the callParticipant
component for each participant. It renders:
Menu
component in certain conditions. (More on that below)DailyMenuView
component, which provides the participant’s audio for the call.
Note: In a React project, you would just render an <audio>
element.
App
component wraps its contents in the CallProvider
component (our context), which means all of our app’s contents can access the data set in our call context.// App.jsx
function App() {
return (
<CallProvider>
<AppContent />
</CallProvider>
);
}
CallProvider
. (We can’t cover every detail here, so let us know if you have questions.) CallProvider
:createRoom
) with the Daily REST API. We’re using a Netlify serverless function for this but you can use the Daily REST API endpoints however works best for your app.createToken
) for meeting moderators with the Daily REST API. (Same as above regarding using serverless functions.)joinRoom
) leaveCall
)handleMute
, handleUnmute
)raiseHand
, lowerHand
)// CallProvider.jsx
export const CallProvider = ({children}) => {
const [view, setView] = useState(PREJOIN); // pre-join | in-call
const [callFrame, setCallFrame] = useState(null);
const [participants, setParticipants] = useState([]);
const [room, setRoom] = useState(null);
const [error, setError] = useState(null);
const [roomExp, setRoomExp] = useState(null);
const [activeSpeakerId, setActiveSpeakerId] = useState(null);
const [updateParticipants, setUpdateParticipants] = useState(null);
…
return (
<CallContext.Provider
value={{
getAccountType,
changeAccountType,
handleMute,
handleUnmute,
displayName,
joinRoom,
leaveCall,
endCall,
removeFromCall,
raiseHand,
lowerHand,
activeSpeakerId,
error,
participants,
room,
roomExp,
view,
}}>
{children}
</CallContext.Provider>
);
};
${username}_MOD
for moderators).sendAppMessage
.app-message
event listener, which is added in CallProvider
: callFrame.on('app-message', handleAppMessage);
handleAppMessage
, which will update the appended string on the username to the new account type (e.g._LISTENER
to _SPEAKER
).// CallProvider.jsx
const handleAppMessage = async (evt) => {
console.log('[APP MESSAGE]', evt);
try {
switch (evt.data.msg) {
case MSG_MAKE_MODERATOR:
console.log('[LEAVING]');
await callFrame.leave();
console.log('[REJOINING AS MOD]');
let userName = evt?.data?.userName;
// Remove the raised hand emoji
if (userName?.includes('✋')) {
const split = userName.split('✋ ');
userName = split.length === 2 ? split[1] : split[0];
}
joinRoom({
moderator: true,
userName,
name: room?.name,
});
break;
case MSG_MAKE_SPEAKER:
updateUsername(SPEAKER);
break;
case MSG_MAKE_LISTENER:
updateUsername(LISTENER);
break;
case FORCE_EJECT:
//seeya
leaveCall();
break;
}
} catch (e) {
console.error(e);
}
};
callFrame.leave()
) and then immediately rejoin them as a moderator with an owner token.is_owner
token property must be true
. See our token configuration docs for more information.CallProvider
as they’re used.PreJoinRoom
component is a form with three inputs (first name, last name, join code), and a button to submit the form. Only the first name is a required field; the last name is optional and if no join code is provided, we take that to mean the user wants to create a new room to join. // PreJoinRoom.jsx
const PreJoinRoom = ({handleLinkPress}) => {
const {joinRoom, error} = useCallState();
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [roomName, setRoomName] = useState('');
const [submitting, setSubmitting] = useState(false);
const [required, setRequired] = useState(false);
const submitForm = useCallback(
(e) => {
e.preventDefault();
if (!firstName?.trim()) {
setRequired(true);
return;
}
if (submitting) return;
setSubmitting(true);
setRequired(false);
let userName =
firstName?.trim() + (lastName?.trim() || '');
let name = '';
if (roomName?.trim()?.length) {
name = roomName;
/**
* We track the account type by appending it to the username.
* This is a quick solution for a demo; not a production-worthy solution!
*/
userName = `${userName}_${LISTENER}`;
} else {
userName = `${userName}_${MOD}`;
}
joinRoom({userName, name});
},
[firstName, lastName, roomName, joinRoom],
);
submitForm
, we first make sure the first name is filled out. If not, we update our required
state value, which blocks the form from being submitted.let userName = firstName?.trim() + (lastName?.trim() ? ${lastName?.trim()} : '');
roomName
) provided in the form, we assign that to our name
variable and update the username to have _LISTENER
appended to it.name
and append _MOD
to the username. As mentioned, the person creating the room is the moderator by default so we track that in the name.if (roomName?.trim()?.length) {
name = roomName;
userName = `${userName}_${LISTENER}`;
} else {
userName = `${userName}_${MOD}`;
}
userName
and optional room name
, we can then call joinRoom
, a method from CallProvider
.const joinRoom = async ({userName, name, moderator}) => {
if (callFrame) {
callFrame.leave();
}
let roomInfo = {name};
/**
* The first person to join will need to create the room first
*/
if (!name && !moderator) {
roomInfo = await createRoom();
}
setRoom(roomInfo);
/**
* When a moderator makes someone else a moderator,
* they first leave and then rejoin with a token.
* In that case, we create a token for the new mod here.
*/
let newToken;
if (moderator) {
// create a token for new moderators
newToken = await createToken(room?.name);
}
const call = Daily.createCallObject({videoSource: false});
const options = {
// This can be changed to your Daily domain
url: `https://devrel.daily.co/${roomInfo?.name}`,
userName,
};
if (roomInfo?.token) {
options.token = roomInfo?.token;
}
if (newToken?.token) {
options.token = newToken.token;
}
await call
.join(options)
.then(() => {
setError(false);
setCallFrame(call);
call.setLocalAudio(false);
setView(INCALL);
})
.catch((err) => {
if (err) {
setError(err);
}
});
};
joinRoom
has the following steps:createRoom
method mentioned above if a room name isn’t providedconst call = Daily.createCallObject({videoSource: false});
(We’ll go into more detail about the videoSource
property below.)const options = {
url: `https://devrel.daily.co/${roomInfo?.name}`,
userName,
};
view
value to incall
await call
.join(options)
.then(() => {
setError(false);
setCallFrame(call);
/**
* Now mute, so everyone joining is muted by default.
*/
call.setLocalAudio(false);
setView(INCALL);
})
InCall
component because of this condition in App.js
:{view === INCALL && <InCall handleLinkPress={handleLinkPress} />}
react-native-daily-js
library to get our audio working. InCall
component renders a Participant
component for each participant in the call, and displays them in the UI based on who can speak. Moderators and speakers are shown at the top and listeners are at the bottom.Speakers
section, which includes moderators and speakers, i.e. anyone who can unmute themselves.// InCall.jsx
const mods = useMemo(() => participants?.filter((p) => p?.owner), [
participants,
getAccountType,
]);
const speakers = useMemo(
(p) =>
participants?.filter((p) => {
return getAccountType(p?.user_name) === SPEAKER;
}),
[participants, getAccountType],
);
Participant
component is not visible in the UI, though: the DailyMediaView
component!// Participant.jsx
import {DailyMediaView} from '@daily-co/react-native-daily-js';
const Participant = ({participant, local, modCount, zIndex}) => {
...
{audioTrack && (
<DailyMediaView
id={`audio-${participant.user_id}`}
videoTrack={null}
audioTrack={audioTrack}
/>
)}
...
react-native-daily-js
and accepts audio and/or video tracks from your participants list, also provided by Daily's call object (recall: callObject.participants()
). Since this is an audio-only app, we set videoTrack
to null, and audioTrack
to each participant’s audio track:// Participant.jsx
const audioTrack = useMemo(
() =>
participant?.tracks?.audio?.state === 'playable'
? participant?.tracks?.audio?.track
: null,
[participant?.tracks?.audio?.state],
);
updateParticipant
method:CallProvider.jsx
const handleMute = useCallback(
(p) => {
if (!callFrame) return;
console.log('[MUTING]');
if (p?.user_id === 'local') {
callFrame.setLocalAudio(false);
} else {
callFrame.updateParticipant(p?.session_id, {
setAudio: false,
});
}
setUpdateParticipants(`unmute-${p?.user_id}-${Date.now()}`);
},
[callFrame],
);
CallProvider
, we have one handleMute
method for participants to mute themselves or others. If they’re muting themselves, they call setLocalAudio(false)
. If they’re muting someone else, they call updateParticipant
with the to-be-muted participant’s session_id
and a properties object with setAudio
equal to false
.videoSource
to false when you create the local call object instance. const call = Daily.createCallObject({videoSource: false});
daily-js