We built one of our first Daily demos with React, because we like working with the framework. We’re not alone. More developers expressed interest in learning React than in picking up any other web framework in the 2020 Stack Overflow Developer Survey.
Meta frameworks for React like Next.js are also gaining traction, so we built a basic video call demo app using Next.js and the Daily call object.
The demo draws inspiration from the new Daily Prebuilt (We’ll eventually open source Daily Prebuilt’s components, stay tuned!), using shared contexts and custom hooks that we hope help get your own apps up and running ASAP. Dive right into the repository or read on for a sneak peek at some of the most foundational pieces, like the core call loop (shared contexts and hooks) and generating meeting tokens.
Run the demo locally
You can find our basic Next.js and Daily video chat demo in our ✨ new ✨ daily-demos/examples
repository. This is a living repo. It’ll grow and evolve as Daily does and as we receive feedback. Poke around and you might notice a few other demos in progress. To hop right into the basic Next.js and Daily app:
- Fork and clone the repository
cd examples/dailyjs/basic-call
- Set your
DAILY_API_KEY
andDAILY_DOMAIN
environment variables (see env.example) yarn
yarn workspace @dailyjs/basic-call dev
The core call loop: shared contexts and hooks
As you’re probably well aware in the year 2021, lots of things can happen on video calls. Participants join and leave, mute and unmute their devices, not to mention the funny things networks can decide to do. Application state can get unwieldy quickly, so we make use of the Context API to avoid passing ever-changing props to all the different components that need to know about the many states.
Six contexts make up what we refer to as our call loop. They handle four different sets of state: devices, tracks, participants, and call state, in addition to a waiting room experience and the overall user interface.
// pages/index.js
return (
<UIStateProvider>
<CallProvider domain={domain} room={roomName} token={token}>
<ParticipantsProvider>
<TracksProvider>
<MediaDeviceProvider>
<WaitingRoomProvider>
<App />
</WaitingRoomProvider>
</MediaDeviceProvider>
</TracksProvider>
</ParticipantsProvider>
</CallProvider>
</UIStateProvider>
);
Some of the contexts also make use of custom hooks that abstract some complexity, depending on the, well, context.
With that pun out of the way, let’s dive into each of the contexts except for <WaitingRoomProvider>
, You’ll have to...wait for a post on that one.
Okay, really, we’re ready now.
Managing devices
The <MediaDeviceProvider>
grants the entire app access to the cams and mics used during the call.
// MediaDeviceProvider.js
return (
<MediaDeviceContext.Provider
value={{
cams,
mics,
speakers,
camError,
micError,
currentDevices,
deviceState,
setMicDevice,
setCamDevice,
setSpeakersDevice,
}}
>
{children}
</MediaDeviceContext.Provider>
);
<MediaDeviceProvider>
relies on a useDevices
hook to listen for changes to the call object to make sure the app has an up to date list of the devices on the call and each device’s state.
// useDevices.js
const updateDeviceState = useCallback(async () => {
try {
const { devices } = await callObject.enumerateDevices();
const { camera, mic, speaker } = await callObject.getInputDevices();
const [defaultCam, ...videoDevices] = devices.filter(
(d) => d.kind === 'videoinput' && d.deviceId !== ''
);
setCams(
[
defaultCam,
...videoDevices.sort((a, b) => sortByKey(a, b, 'label', false)),
].filter(Boolean)
);
const [defaultMic, ...micDevices] = devices.filter(
(d) => d.kind === 'audioinput' && d.deviceId !== ''
);
setMics(
[
defaultMic,
...micDevices.sort((a, b) => sortByKey(a, b, 'label', false)),
].filter(Boolean)
);
const [defaultSpeaker, ...speakerDevices] = devices.filter(
(d) => d.kind === 'audiooutput' && d.deviceId !== ''
);
setSpeakers(
[
defaultSpeaker,
...speakerDevices.sort((a, b) => sortByKey(a, b, 'label', false)),
].filter(Boolean)
);
setCurrentDevices({
camera,
mic,
speaker,
});
} catch (e) {
setDeviceState(DEVICE_STATE_NOT_SUPPORTED);
}
}, [callObject]);
useDevices
also handles device errors, like if a cam or mic is blocked, and updates a device’s state when something changes for the participant using the device, like if their tracks change.
Keeping track of tracks
Different devices share different kinds of tracks. A microphone shares an audio
type track; a camera shares video
. Each track contains its own state: playable, loading, off, etc. <TracksProvider>
simplifies keeping track of all those tracks as the number of call participants grows. This context listens for changes in track state and dispatches updates. One type of change, for example, could be when a participant’s tracks start or stop.
// TracksProvider.js
export const TracksProvider = ({ children }) => {
const { callObject } = useCallState();
const [state, dispatch] = useReducer(tracksReducer, initialTracksState);
useEffect(() => {
if (!callObject) return false;
const handleTrackStarted = ({ participant, track }) => {
dispatch({
type: TRACK_STARTED,
participant,
track,
});
};
const handleTrackStopped = ({ participant, track }) => {
if (participant) {
dispatch({
type: TRACK_STOPPED,
participant,
track,
});
}
};
/** Other things happen here **/
callObject.on('track-started', handleTrackStarted);
callObject.on('track-stopped', handleTrackStopped);
}, [callObject];
Handling participants
<ParticipantsProvider>
makes sure any and all participant updates are available across the app. It listens for participant events:
// ParticipantsProvider.js
useEffect(() => {
if (!callObject) return false;
const events = [
'joined-meeting',
'participant-joined',
'participant-updated',
'participant-left',
];
// Listen for changes in state
events.forEach((event) => callObject.on(event, handleNewParticipantsState));
// Stop listening for changes in state
return () =>
events.forEach((event) =>
callObject.off(event, handleNewParticipantsState)
);
}, [callObject, handleNewParticipantsState]);
And dispatches state updates depending on the event:
// ParticipantsProvider.js
const handleNewParticipantsState = useCallback(
(event = null) => {
switch (event?.action) {
case 'participant-joined':
dispatch({
type: PARTICIPANT_JOINED,
participant: event.participant,
});
break;
case 'participant-updated':
dispatch({
type: PARTICIPANT_UPDATED,
participant: event.participant,
});
break;
case 'participant-left':
dispatch({
type: PARTICIPANT_LEFT,
participant: event.participant,
});
break;
default:
break;
}
},
[dispatch]
);
<ParticipantsProvider>
also calls on use-deep-compare to memoize expensive calculations, like all of the participants on the call:
// ParticipantsProvider.js
const allParticipants = useDeepCompareMemo(
() => Object.values(state.participants),
[state?.participants]
);
Managing room and call state
<CallProvider>
handles configuration and state for the room where the call happens, where all those devices, participants, and tracks interact.
<CallProvider>
imports the abstraction hook useCallMachine
to manage call state.
// CallProvider.js
const { daily, leave, join, state } = useCallMachine({
domain,
room,
token,
});
useCallMachine
listens for changes in call access, for example, and updates overall call state accordingly:
// useCallMachine.js
useEffect(() => {
if (!daily) return false;
daily.on('access-state-updated', handleAccessStateUpdated);
return () => daily.off('access-state-updated', handleAccessStateUpdated);
}, [daily, handleAccessStateUpdated]);
// Other things happen here
const handleAccessStateUpdated = useCallback(
async ({ access }) => {
if (
[CALL_STATE_ENDED, CALL_STATE_AWAITING_ARGS, CALL_STATE_READY].includes(
state
)
) {
return;
}
if (
access === ACCESS_STATE_UNKNOWN ||
access?.level === ACCESS_STATE_NONE
) {
setState(CALL_STATE_NOT_ALLOWED);
return;
}
const meetingState = daily.meetingState();
if (
access?.level === ACCESS_STATE_LOBBY &&
meetingState === MEETING_STATE_JOINED
) {
return;
}
join();
},
[daily, state, join]
);
<CallProvider>
then uses that information, to do things like verify a participant’s access to a room, and whether or not they’re permitted to join the call:
// CallProvider.js
useEffect(() => {
if (!daily) return;
const { access } = daily.accessState();
if (access === ACCESS_STATE_UNKNOWN) return;
const requiresPermission = access?.level === ACCESS_STATE_LOBBY;
setPreJoinNonAuthorized(requiresPermission && !token);
}, [state, daily, token]);
If the participant requires permission to join, and they’re not joining with a token, then the participant will not be allowed into the call.
Generating Daily meeting tokens with Next.js
Meeting tokens control room access and session configuration on a per-user basis. They’re also a great use case for Next API routes.
API routes let us query endpoints directly within our app, so we don’t have to maintain a separate server. We call the Daily /meeting-tokens
endpoint in /pages/api/token.js
:
// pages/api/token.js
export default async function handler(req, res) {
const { roomName, isOwner } = req.body;
if (req.method === 'POST' && roomName) {
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.DAILY_API_KEY}`,
},
body: JSON.stringify({
properties: { room_name: roomName, is_owner: isOwner },
}),
};
const dailyRes = await fetch(
`${process.env.DAILY_REST_DOMAIN}/meeting-tokens`,
options
);
const { token, error } = await dailyRes.json();
if (error) {
return res.status(500).json({ error });
}
return res.status(200).json({ token, domain: process.env.DAILY_DOMAIN });
}
return res.status(500);
}
In index.js
, we fetch the endpoint:
// pages/index.js
const res = await fetch('/api/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ roomName: room, isOwner }),
});
const resJson = await res.json();
What’s Next.js?
Please fork, clone, and hack away! There are lots of ways you could start building on top of this demo: adding custom user authentication, building a chat component, or pretty much anything that springs to mind.
We’d appreciate hearing what you think about the demo, especially how we could improve it. We’re also curious about other framework and meta-framework specific sample code that you’d find useful.
If you’re hoping for more Daily and Next.js sample code, we’ve got you covered. Come back soon!
Top comments (0)