发布于 2026-01-06 0 阅读
0

使用 Next.js 和 Daily 构建实时视频聊天应用程序

使用 Next.js 和 Daily 构建实时视频聊天应用程序

我们用React构建了我们最早的Daily demo之一,因为我们喜欢使用这个框架。我们并非孤例。在2020 年 Stack Overflow 开发者调查中,表示有兴趣学习 React 的开发者比对其他任何 Web 框架都多

React 的元框架如Next.js也越来越受欢迎,因此我们使用 Next.js 和Daily call 对象构建了一个基本的视频通话演示应用程序

视频聊天应用程序的屏幕截图

该演示借鉴了全新的Daily Prebuilt(我们最终会开源 Daily Prebuilt 的组件,敬请期待!),利用共享上下文自定义钩子,我们希望这能帮助您尽快启动并运行自己的应用程序。您可以直接访问代码库,或者继续阅读,抢先了解一些最基础的部分,例如核心调用循环(共享上下文和钩子)以及会议令牌的生成。

在本地运行演示

daily-demos/examples您可以在我们的✨全新✨代码库中找到我们基于 Next.js 和 Daily 的基础视频聊天演示。这是一个持续更新的代码库,它会随着 Daily 的发展和我们收到的反馈而不断成长和完善。不妨四处看看,您可能会发现一些正在开发中的其他演示。要直接体验基于 Next.js 和 Daily 的基础应用:

  1. fork 并克隆该仓库
  2. cd examples/dailyjs/basic-call
  3. 设置您的DAILY_API_KEY环境DAILY_DOMAIN变量(参见env.example
  4. yarn
  5. yarn workspace @dailyjs/basic-call dev

核心调用循环:共享上下文和钩子

正如您在 2021 年可能已经充分体会到的,视频通话中会发生很多事情。参与者会加入和离开,会静音和取消静音,更不用说网络可能会出现的各种奇怪状况了。应用程序状态很快就会变得难以管理,因此我们使用Context API来避免将不断变化的 props 传递给所有需要了解各种状态的组件。

我们的呼叫循环由六个上下文组成。它们处理四组不同的状态:设备、轨道、参与者和呼叫状态,此外还包括等候室体验和整体用户界面。

// pages/index.js

  return (
    <UIStateProvider>
      <CallProvider domain={domain} room={roomName} token={token}>
        <ParticipantsProvider>
          <TracksProvider>
            <MediaDeviceProvider>
              <WaitingRoomProvider>
                <App />
              </WaitingRoomProvider>
            </MediaDeviceProvider>
          </TracksProvider>
        </ParticipantsProvider>
      </CallProvider>
    </UIStateProvider>
  );
Enter fullscreen mode Exit fullscreen mode

有些上下文还会使用自定义钩子来抽象一些复杂性,具体取决于上下文。

玩笑开完了,接下来让我们深入探讨一下每种语境,除了<WaitingRoomProvider>……你得……等等,关于那个语境的帖子。

好了,真的,我们现在准备好了。

管理设备

<MediaDeviceProvider>权限授予整个应用程序访问通话期间使用的摄像头和麦克风的权限。

// MediaDeviceProvider.js

return (
   <MediaDeviceContext.Provider
     value={{
       cams,
       mics,
       speakers,
       camError,
       micError,
       currentDevices,
       deviceState,
       setMicDevice,
       setCamDevice,
       setSpeakersDevice,
     }}
   >
     {children}
   </MediaDeviceContext.Provider>
 );
Enter fullscreen mode Exit fullscreen mode

<MediaDeviceProvider>它依靠一个useDevices钩子来监听通话对象的变化,以确保应用程序拥有通话中设备的最新列表以及每个设备的状态。

// 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]);

Enter fullscreen mode Exit fullscreen mode

useDevices它还能处理设备错误,例如摄像头或麦克风被遮挡的情况,并在使用该设备的参与者发生某些变化时(例如他们的曲目发生变化)更新设备的状态。

跟踪轨道

不同的设备共享不同类型的音轨。例如,麦克风共享一个audio音轨;摄像头共享一个video音轨。每个音轨都有自己的状态:可播放、正在加载、已关闭等等。<TracksProvider>随着通话参与者数量的增加,这种机制简化了对所有音轨的跟踪。该上下文会监听音轨状态的变化并分发更新。例如,一种变化类型可以是参与者的音轨开始或停止播放。

// 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];
Enter fullscreen mode Exit fullscreen mode

处理参与者

<ParticipantsProvider>确保所有参与者的更新信息都能在整个应用程序中显示。它会监听参与者事件

// 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]);
Enter fullscreen mode Exit fullscreen mode

并根据事件分发状态更新:

// 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]
 );
Enter fullscreen mode Exit fullscreen mode

<ParticipantsProvider>还呼吁使用深度比较来缓存昂贵的计算,就像电话会议中的所有参与者一样:

// ParticipantsProvider.js

const allParticipants = useDeepCompareMemo(
   () => Object.values(state.participants),
   [state?.participants]
 );
Enter fullscreen mode Exit fullscreen mode

管理房间和通话状态

<CallProvider>处理通话发生的房间的配置和状态,所有设备、参与者和轨道都在其中进行交互。

<CallProvider>导入抽象钩子useCallMachine以管理调用状态。

// CallProvider.js

 const { daily, leave, join, state } = useCallMachine({
   domain,
   room,
   token,
 });
Enter fullscreen mode Exit fullscreen mode

useCallMachine例如,监听呼叫访问的变化,并相应地更新整体呼叫状态:

// 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]
 );
Enter fullscreen mode Exit fullscreen mode

<CallProvider>然后利用这些信息来验证参与者是否有权访问房间,以及是否允许他们加入通话:

// 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]);
Enter fullscreen mode Exit fullscreen mode

如果参与者需要获得加入权限,并且他们没有使用令牌加入,则该参与者将不被允许加入通话。

使用 Next.js 生成每日会议令牌

会议令牌用于控制每个用户的会议室访问权限和会话配置。它们也是Next API 路由的绝佳用例。

API 路由允许我们直接在应用内查询端点,因此无需维护单独的服务器。我们调用 Daily/meeting-tokens端点/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);
}
Enter fullscreen mode Exit fullscreen mode

在 中index.js,我们获取端点:

// 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();
Enter fullscreen mode Exit fullscreen mode

什么是 Next.js?

请随意fork、克隆和修改!您可以基于此演示进行多种开发:添加自定义用户身份验证、构建聊天组件,或者几乎任何您能想到的功能。

我们非常希望听到您对演示的看法,特别是关于如何改进它的建议。我们也想了解您认为其他框架和元框架的特定示例代码是否对您有用。

如果您希望获得更多 Daily 和 Next.js 示例代码,我们已经为您准备好了。敬请期待!

文章来源:https://dev.to/trydaily/build-a-real-time-video-chat-app-with-next-js-and-daily-1kl7