Skip to content

feat(rooms): 🚀 新增会议室功能#286

Closed
aklry wants to merge 1 commit intomainfrom
feat/meet
Closed

feat(rooms): 🚀 新增会议室功能#286
aklry wants to merge 1 commit intomainfrom
feat/meet

Conversation

@aklry
Copy link
Copy Markdown
Collaborator

@aklry aklry commented Feb 3, 2026

PR 描述

新增视频会议页面

PR 类型

  • 🐛 Bug 修复
  • [ √] ✨ 新功能
  • 💄 UI/UX 改进
  • ♻️ 重构
  • 🚀 性能优化
  • 📝 文档更新
  • 🔄 其他

Issue 关联

Closes #

其他信息

image image

@netlify
Copy link
Copy Markdown

netlify bot commented Feb 3, 2026

Deploy Preview for ornate-blancmange-89db6b failed. Why did it fail? →

Name Link
🔨 Latest commit 93ea346
🔍 Latest deploy log https://app.netlify.com/projects/ornate-blancmange-89db6b/deploys/69815e6a1a68760008cd2795

@aklry aklry requested a review from xun082 February 3, 2026 02:33
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @aklry, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求的核心目标是为应用程序引入一个全新的实时视频会议模块。通过集成 LiveKit 平台,用户现在可以无缝地创建和加入视频会议,享受包括音视频通信、屏幕共享和端到端加密在内的丰富功能。此次更新不仅带来了全新的会议页面和交互组件,还着重于提升用户体验,例如提供会议前设置、便捷的房间分享方式,并内置了性能优化和详细的调试工具,确保会议的流畅性和稳定性。

Highlights

  • 新增视频会议功能: 引入了基于 LiveKit 的实时视频会议功能,允许用户快速创建或加入会议室进行音视频通话。
  • LiveKit 集成: 全面集成了 LiveKit 客户端库及其 React 组件,包括房间管理、参与者视图、音视频控制、屏幕共享等核心功能。
  • 增强的用户体验: 提供了会议前加入界面、自定义控制栏、键盘快捷键、背景效果(如模糊)、噪音消除(Krisp)以及房间分享功能,提升了会议的可用性和便捷性。
  • 性能优化与调试: 增加了低 CPU 设备性能优化钩子,以及一个用于调试 LiveKit 房间状态的调试模式,并集成了 Datadog 日志,便于监控和问题排查。
  • 依赖管理自动化: 新增了 Renovate 配置,用于自动化管理依赖更新,特别是 LiveKit 相关的依赖,确保项目始终使用最新版本。
Changelog
  • package.json
    • 新增了 LiveKit 相关的依赖包,包括 @livekit/components-reactlivekit-clientlivekit-server-sdk 等,用于实现视频会议功能。
    • 添加了 @datadog/browser-logs 用于集成 Datadog 日志服务。
    • 引入了 tinykeys 库,用于处理键盘快捷键。
  • pnpm-lock.yaml
    • 更新了 pnpm-lock.yaml 文件,以反映 package.json 中新增和更新的依赖项及其解析版本。
  • renovate.json
    • 新增了 Renovate 配置文件,用于自动化管理项目依赖,特别是针对 LiveKit 依赖设置了自动合并策略。
  • src/app/dashboard/page.tsx
    • 引入了 useRouter 钩子,用于页面导航。
    • 为仪表盘的“立即开始视频会议”快速操作按钮添加了 href 属性,使其能够跳转到新的会议室页面。
    • 新增了 handleQuickActionClick 函数,处理快速操作按钮的路由跳转逻辑。
  • src/app/rooms/[roomName]/PageClientImpl.tsx
    • 新增文件,实现了视频会议房间的客户端逻辑,包括 LiveKit 连接、错误处理、参与者头像叠加显示、E2EE 设置以及自定义控制栏的集成。
  • src/app/rooms/[roomName]/PreJoinScreen.tsx
    • 新增文件,提供了会议前加入房间的界面,允许用户输入姓名并选择是否启用摄像头和麦克风。
  • src/app/rooms/[roomName]/page.tsx
    • 新增文件,作为视频会议房间的入口页面,负责解析 URL 参数(如房间名、编码信息)并将其传递给客户端实现组件 PageClientImpl
  • src/app/rooms/[roomName]/style.css
    • 新增文件,包含针对 LiveKit 参与者占位符的自定义 CSS 样式,用于隐藏默认 SVG 并添加自定义头像占位符。
  • src/app/rooms/_components/CameraSettings.tsx
    • 新增文件,实现了摄像头设置组件,允许用户切换摄像头、调整背景效果(如背景模糊)。
  • src/app/rooms/_components/CustomControlBar.tsx
    • 新增文件,提供了自定义的会议控制栏,包含麦克风、摄像头、屏幕共享、聊天、房间分享和断开连接按钮。
  • src/app/rooms/_components/Debug.tsx
    • 新增文件,实现了 LiveKit 房间的调试模式组件,可显示房间和参与者信息,并集成了 Datadog 日志,通过 Shift+D 快捷键切换显示。
  • src/app/rooms/_components/KeyboardShortcuts.tsx
    • 新增文件,实现了键盘快捷键功能,例如 Cmd/Ctrl-Shift-A 切换麦克风,Cmd/Ctrl-Shift-V 切换摄像头。
  • src/app/rooms/_components/MicrophoneSettings.tsx
    • 新增文件,实现了麦克风设置组件,包括启用/禁用 Krisp 噪音消除功能。
  • src/app/rooms/_components/RecordingIndicator.tsx
    • 新增文件,显示会议录制状态的指示器,并在录制开始时通过 toast 提示用户。
  • src/app/rooms/_components/SettingsMenu.tsx
    • 新增文件,提供了会议设置菜单,包含媒体设备(摄像头、麦克风、扬声器)和录制功能的配置选项。
  • src/app/rooms/_components/ShareRoomButton.tsx
    • 新增文件,实现了房间分享对话框,允许用户复制房间号和分享链接。
  • src/app/rooms/layout.tsx
    • 新增文件,定义了视频会议页面的布局,引入了 LiveKit 组件样式,并配置了 react-hot-toastToaster 组件。
  • src/app/rooms/page.tsx
    • 新增文件,作为视频会议功能的主入口页面,提供了快速创建房间和加入现有房间的界面,并支持通过 URL 参数自动填充房间号。
  • src/app/rooms/styles/Debug.module.css
    • 新增文件,为调试模式组件提供了特定的 CSS 样式。
  • src/app/rooms/styles/Home.module.css
    • 新增文件,为视频会议主页(创建/加入房间界面)提供了特定的 CSS 样式。
  • src/app/rooms/styles/SettingsMenu.module.css
    • 新增文件,为设置菜单的选项卡提供了特定的 CSS 样式。
  • src/app/rooms/styles/globals.css
    • 新增文件,包含了视频会议功能所需的全局 CSS 样式,包括 LiveKit 组件的默认样式和自定义布局样式。
  • src/app/rooms/types/index.ts
    • 新增文件,定义了 LiveKit 会议相关的 TypeScript 类型接口,如 SessionPropsTokenResultConnectionDetails
  • src/hooks/usePerfomanceOptimiser.ts
    • 新增文件,实现了 useLowCPUOptimizer 钩子,用于在低 CPU 设备上优化 LiveKit 视频流的性能。
  • src/hooks/useSetupE2EE.ts
    • 新增文件,实现了 useSetupE2EE 钩子,用于设置 LiveKit 的端到端加密功能。
  • src/services/meet/index.ts
    • 新增文件,定义了 meetApi 服务,封装了与后端 LiveKit 房间管理相关的 API 调用,包括快速创建、加入、列出和获取房间信息。
  • src/services/meet/type.ts
    • 新增文件,定义了 meetApi 服务中使用的 TypeScript 类型接口,如 LiveKitConnectionDetailsQuickCreateRoomParamsRoom
  • src/utils/meet/client-utils.ts
    • 新增文件,提供了一些客户端工具函数,如解码密码、生成房间 ID 和检测低功耗设备。
Activity
  • 此拉取请求引入了全新的视频会议功能,涉及大量新文件和依赖。
  • 作者 aklry 提交了代码,并在 PR 描述中明确指出这是一项新功能。
  • PR 描述中包含了两个 UI 截图,展示了新会议室功能的用户界面,表明已完成初步的 UI/UX 实现。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

此 PR 新增了基于 LiveKit 的视频会议功能。主要变更包括:添加了 /rooms 路由,包含创建/加入房间的大厅页面和视频会议房间页面;引入了 LiveKit 相关的依赖,并添加了大量新的 React 组件、Hooks 和 API 服务来支持会议功能,例如会前准备界面、自定义控制条、性能优化钩子等;更新了 package.jsonpnpm-lock.yaml 以包含新依赖,并添加了 renovate.json 来管理依赖更新;在仪表盘页面添加了进入会议功能的快捷入口。整体来看,这是一个结构清晰、功能完整的新特性实现。我的审查意见主要集中在以下几个方面:

  • 依赖管理:建议调整 renovate.json 配置,对核心库的自动合并策略采取更谨慎的态度。
  • 代码健壮性:指出了一个在 PageClientImpl.tsx 中可能导致 Room 配置不更新的 useMemo 使用问题。
  • 用户体验:提出了改进离开房间后的重定向逻辑,以及优化复制操作的用户反馈机制,避免使用 alert
  • 代码可维护性:建议将组件中大量的内联样式重构为 CSS Modules,以提高代码的可读性和可维护性。

这些修改将有助于提升新功能的稳定性和长期可维护性。

};
}, [props.options.hq, props.options.codec, e2eeEnabled, keyProvider, worker]);

const room = React.useMemo(() => new Room(roomOptions), []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

React.useMemo 的依赖项数组为空,导致 room 对象仅在组件首次挂载时创建一次。然而,roomOptions 自身是 memoized 的,并且其依赖项 [props.options.hq, props.options.codec, e2eeEnabled, keyProvider, worker] 可能会在组件生命周期内发生变化。当 roomOptions 变化时,Room 实例不会被重新创建,这会导致新的配置(如视频质量 hq 或编解码器 codec)无法生效。

建议将 roomOptions 添加到 useMemo 的依赖项数组中,以确保在配置变更时能重新创建 Room 实例。

Suggested change
const room = React.useMemo(() => new Room(roomOptions), []);
const room = React.useMemo(() => new Room(roomOptions), [roomOptions]);

Comment on lines +12 to +35
const [copied] = React.useState(false);

const shareUrl =
typeof window !== 'undefined' ? `${window.location.origin}?join=${roomName}` : '';

// 快速复制房间号(点击按钮时)
// const handleQuickCopy = () => {
// navigator.clipboard.writeText(roomName).then(() => {
// setCopied(true);
// setTimeout(() => setCopied(false), 2000);
// });
// };

const handleCopyRoomId = () => {
navigator.clipboard.writeText(roomName).then(() => {
alert('✅ 房间号已复制:' + roomName);
});
};

const copyShareLink = () => {
navigator.clipboard.writeText(shareUrl).then(() => {
alert('✅ 分享链接已复制!');
});
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

copied 状态被声明为常量,无法更新,导致“复制房间号”按钮在点击后无法提供正确的 UI 反馈。此外,使用 alert 会中断用户流程,体验不佳。

建议重构复制逻辑:

  1. copied 状态声明修改为 const [copied, setCopied] = React.useState(false);
  2. 修改 handleCopyRoomIdcopyShareLink 函数,在复制成功后更新状态,并使用 setTimeout 在短暂延迟后重置状态。
  3. 移除 alert,让 UI 变化(如改变按钮文本从“复制”变为“✓ 已复制”)作为反馈,以提升用户体验。
export function ShareRoomButton({ roomName, onClose }: ShareRoomButtonProps) {
  const [copied, setCopied] = React.useState(false);

  const shareUrl =
    typeof window !== 'undefined' ? `${window.location.origin}?join=${roomName}` : '';

  const copyToClipboard = (text: string) => {
    navigator.clipboard.writeText(text).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    });
  };

  const handleCopyRoomId = () => {
    copyToClipboard(roomName);
  };

  const copyShareLink = () => {
    copyToClipboard(shareUrl);
  };

Comment on lines +11 to +15
"matchSourceUrlPrefixes": ["https://github.com/livekit/"],
"rangeStrategy": "replace",
"groupName": "LiveKit dependencies (non-major)",
"automerge": true
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

自动合并 LiveKit 的依赖项更新存在风险。即使是次版本(minor)更新也可能引入破坏性变更或 bug,特别是对于像视频会议这样的核心功能。建议移除 automerge: true 或将其设置为 false,以便在合并前进行人工审核,确保稳定性。

      "automerge": false

Comment on lines +299 to +304
console.log('🖼️ 设置用户头像:', props.connectionDetails.userAvatar);
room.localParticipant.setMetadata(
JSON.stringify({
avatar: '/favicon.svg',
}),
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

else 代码块中,console.log 尝试输出 props.connectionDetails.userAvatar,但此时它的值为 undefined 或 falsy。这可能会导致在调试时产生困惑。建议在此处记录正在设置的默认头像路径,或者移除这条日志。

          } else {
            console.log('🖼️ 未提供用户头像,设置默认头像:', '/favicon.svg');
            room.localParticipant.setMetadata(
              JSON.stringify({
                avatar: '/favicon.svg',
              }),
            );
          }

);

const router = useRouter();
const handleOnLeave = React.useCallback(() => router.push('/'), [router]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

用户离开房间后被重定向到根路径 /。考虑到此功能模块的入口是 /rooms,为了保持用户体验的一致性,建议将重定向目标修改为 /rooms

  const handleOnLeave = React.useCallback(() => router.push('/rooms'), [router]);

Comment on lines +50 to +247
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
padding: '2rem',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
}}
>
<div
style={{
background: 'rgba(255, 255, 255, 0.98)',
borderRadius: '20px',
padding: '3rem',
maxWidth: '480px',
width: '100%',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
}}
>
<h1
style={{
fontSize: '2rem',
fontWeight: '700',
color: '#1a1a1a',
marginBottom: '0.5rem',
textAlign: 'center',
}}
>
Join Room
</h1>

<p
style={{
color: '#666',
textAlign: 'center',
marginBottom: '1rem',
fontSize: '0.95rem',
}}
>
Room: <strong>{roomName}</strong>
</p>

<p
style={{
color: '#888',
textAlign: 'center',
marginBottom: '1.5rem',
fontSize: '0.85rem',
lineHeight: '1.4',
}}
>
💡 提示:即使没有摄像头也可以加入,只用麦克风聊天
</p>

<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '1.5rem' }}>
<label
htmlFor="username"
style={{
display: 'block',
marginBottom: '0.5rem',
color: '#374151',
fontSize: '0.875rem',
fontWeight: '600',
}}
>
Your Name *
</label>
<input
id="username"
type="text"
placeholder="Enter your name"
value={username}
onChange={(e) => setUsername(e.target.value)}
disabled={isJoining}
autoFocus
style={{
width: '100%',
padding: '0.875rem 1rem',
border: '2px solid #e5e7eb',
borderRadius: '12px',
fontSize: '1rem',
transition: 'border-color 0.2s',
outline: 'none',
}}
onFocus={(e) => (e.target.style.borderColor = '#667eea')}
onBlur={(e) => (e.target.style.borderColor = '#e5e7eb')}
required
/>
</div>

<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '1rem',
marginBottom: '2rem',
}}
>
<label
style={{
display: 'flex',
alignItems: 'center',
gap: '0.75rem',
cursor: 'pointer',
padding: '0.75rem',
background: videoEnabled ? 'rgba(102, 126, 234, 0.1)' : 'rgba(0, 0, 0, 0.05)',
borderRadius: '12px',
transition: 'background 0.2s',
}}
>
<input
type="checkbox"
checked={videoEnabled}
onChange={(e) => setVideoEnabled(e.target.checked)}
disabled={isJoining}
style={{
width: '20px',
height: '20px',
cursor: 'pointer',
}}
/>
<span
style={{
fontSize: '0.95rem',
fontWeight: '500',
color: '#1a1a1a',
}}
>
📹 Enable Camera
</span>
</label>

<label
style={{
display: 'flex',
alignItems: 'center',
gap: '0.75rem',
cursor: 'pointer',
padding: '0.75rem',
background: audioEnabled ? 'rgba(102, 126, 234, 0.1)' : 'rgba(0, 0, 0, 0.05)',
borderRadius: '12px',
transition: 'background 0.2s',
}}
>
<input
type="checkbox"
checked={audioEnabled}
onChange={(e) => setAudioEnabled(e.target.checked)}
disabled={isJoining}
style={{
width: '20px',
height: '20px',
cursor: 'pointer',
}}
/>
<span
style={{
fontSize: '0.95rem',
fontWeight: '500',
color: '#1a1a1a',
}}
>
🎤 Enable Microphone
</span>
</label>
</div>

<button
type="submit"
disabled={isJoining || !username.trim()}
style={{
width: '100%',
padding: '1rem',
background:
isJoining || !username.trim()
? '#e5e7eb'
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
border: 'none',
borderRadius: '12px',
fontSize: '1rem',
fontWeight: '600',
cursor: isJoining || !username.trim() ? 'not-allowed' : 'pointer',
transition: 'all 0.2s',
boxShadow:
isJoining || !username.trim() ? 'none' : '0 4px 15px rgba(102, 126, 234, 0.4)',
}}
>
{isJoining ? 'Joining...' : 'Join Room'}
</button>
</form>
</div>
</div>
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

该组件中大量使用了内联样式(inline styles)。这使得代码难以阅读和维护,并且与项目中其他地方使用的 CSS Modules 或 Tailwind CSS 风格不一致。建议将这些样式提取到单独的 CSS Module 文件中(例如 PreJoinScreen.module.css),并使用 className 进行引用。这样可以提高代码的可读性、可维护性,并更好地利用 CSS 的能力(如伪类、媒体查询等)。

Comment on lines +144 to +157
style={{
width: '100%',
padding: '1.25rem',
fontSize: '1.1rem',
fontWeight: '600',
background: isCreating
? 'rgba(102, 126, 234, 0.5)'
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
borderRadius: '8px',
cursor: isCreating ? 'not-allowed' : 'pointer',
transition: 'all 0.2s',
opacity: isCreating ? 0.7 : 1,
}}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

此组件中的按钮等元素使用了大量内联样式。为了代码的可读性和可维护性,建议将这些样式移至对应的 CSS Module 文件(styles/Home.module.css)中,并用类名(className)代替。这有助于保持代码整洁,并使样式更易于复用和管理。

@aklry aklry closed this Feb 3, 2026
@aklry aklry deleted the feat/meet branch February 9, 2026 09:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant