Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
June 18, 2021 09:50 pm GMT

Build a real time video chat app with Next.js and Daily

We built one of our first Daily demos with React, because we like working with the framework. Were 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.

Screenshot of a video chat app

The demo draws inspiration from the new Daily Prebuilt (Well eventually open source Daily Prebuilts 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. Itll 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:

  1. Fork and clone the repository
  2. cd examples/dailyjs/basic-call
  3. Set your DAILY_API_KEY and DAILY_DOMAIN environment variables (see env.example)
  4. yarn
  5. yarn workspace @dailyjs/basic-call dev

The core call loop: shared contexts and hooks

As youre 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, lets dive into each of the contexts except for <WaitingRoomProvider>, Youll have to...wait for a post on that one.

Okay, really, were ready now.

Managing devices

The <MediaDeviceProvider> grants the entire app access to the cams and mics used during the call.

// MediaDeviceProvider.jsreturn (   <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 devices state.

// useDevices.jsconst 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 devices 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 participants tracks start or stop.

// TracksProvider.jsexport 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.jsconst 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.jsconst 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.jsuseEffect(() => {   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 participants access to a room, and whether or not theyre permitted to join the call:

// CallProvider.jsuseEffect(() => {   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 theyre 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. Theyre also a great use case for Next API routes.

API routes let us query endpoints directly within our app, so we dont have to maintain a separate server. We call the Daily /meeting-tokens endpoint in /pages/api/token.js:

// pages/api/token.jsexport 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.jsconst res = await fetch('/api/token', {     method: 'POST',     headers: {       'Content-Type': 'application/json',     },     body: JSON.stringify({ roomName: room, isOwner }),   });   const resJson = await res.json();

Whats 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.

Wed appreciate hearing what you think about the demo, especially how we could improve it. Were also curious about other framework and meta-framework specific sample code that youd find useful.

If youre hoping for more Daily and Next.js sample code, weve got you covered. Come back soon!


Original Link: https://dev.to/trydaily/build-a-real-time-video-chat-app-with-next-js-and-daily-1kl7

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To