Vue3+WebRTC视频会议最小可行源码解析
简介这是一份基于 WebRTC 实现的轻量级视频会议应用完整源码包面向计算机相关专业学生如计科、人工智能、通信、物联网等及初级前端开发者适用于课程设计、毕业设计、技术学习与项目立项演示。资源经实测可正常运行涵盖信令服务、音视频连接、界面交互等核心功能模块兼顾入门实践与工程参考价值。压缩包共94个文件以48个JavaScript文件构建逻辑主干7个Vue组件实现视图层6个JSON配置与5个Markdown文档提供说明与环境配置辅以HTML、CSS、SVG等资源整体仅299KB结构紧凑、易于部署。目前已有48人下载学习读者可直接复现端到端视频通话流程掌握WebRTC API调用、STUN/TURN配置、Vue项目集成及Node.js信令服务器搭建等关键技能同时获得清晰的目录组织范式与可扩展的代码架构参考。1. 这不是“又一个 WebRTC 教程”而是一套能跑通、能调试、能改出自己会议功能的 Vue WebRTC 视频会议最小可行源码你下载到的基于webRTC的简单视频会议app完整源码说明.zip本质是一份面向真实开发场景的「可执行起点」——它不依赖第三方 SaaS 服务如 Zoom SDK 或腾讯云 TRTC不封装成黑盒组件所有信令逻辑、媒体协商、连接状态管理都用原生 JavaScript Vue 3 Composition API 暴露在.vue文件里。这意味着前端工程师能直接在src/views/Meeting.vue中看到RTCPeerConnection实例如何创建、offer/answer如何通过 WebSocket 交换、本地流如何绑定到video元素测试时只需启动本地信令服务器含node server.js无需注册任何云平台账号二次开发时加个屏幕共享按钮、改个布局、接入自己的用户系统改动都在 200 行内完成。它解决的不是“WebRTC 是什么”而是“我怎么让两个浏览器窗口真正看到彼此的摄像头画面并保持连接稳定”。适合刚学完 MDN WebRTC 文档、卡在信令流程上或需要快速验证音视频链路是否通的 Vue 开发者。2. 从源码结构看懂 WebRTC 视频会议的三大核心模块信令、媒体、连接状态这套源码之所以“能跑通”关键在于它把 WebRTC 的抽象概念拆解为三个可独立调试、可替换的 Vue 组合式函数模块。它们不藏在node_modules里全部位于src/composables/下且每个文件都对应 WebRTC 标准流程中的一个明确阶段。理解这三块比死记createOffer()参数更重要。2.1 信令模块用轻量 WebSocket 实现 offer/answer/ice-candidate 的可靠传递信令不是 WebRTC 协议的一部分但没有它两个 Peer 永远无法建立连接。本源码采用ws而非 HTTP 轮询作为信令通道服务端代码server.js仅 87 行核心逻辑是广播消息// server.js 关键片段 const wss new WebSocket.Server({ port: 8080 }); wss.on(connection, (ws) { ws.on(message, (data) { const msg JSON.parse(data.toString()); // 广播给除发送者外的所有客户端 wss.clients.forEach(client { if (client ! ws client.readyState WebSocket.OPEN) { client.send(JSON.stringify(msg)); } }); }); });提示这种广播模式适用于小规模测试≤5人生产环境需改为点对点转发或加入房间 ID 过滤。源码中useSignaling.js封装了连接、重连、消息序列化逻辑重点看sendSignalingMessage()函数——它确保所有信令消息offer,answer,candidate都带type字段这是前端解析的唯一依据。前端信令调用链非常清晰用户点击“开始会议” →useSignaling.connect()建立 WebSocket本地生成 offer →useSignaling.send({ type: offer, sdp: pc.localDescription.sdp })收到远程 answer →pc.setRemoteDescription(new RTCSessionDescription(msg))2.2 媒体模块Vue 响应式控制 getUserMedia 与 video 元素绑定媒体流获取和渲染是 WebRTC 最易出错的环节。源码没用vue-use等第三方库而是手写useMediaStream.js原因有二一是避免隐藏constraints配置细节如强制 720p 分辨率二是让错误捕获更直接navigator.mediaDevices.getUserMedia()拒绝时抛出明确异常。// src/composables/useMediaStream.js export function useMediaStream() { const localStream ref(null); const error ref(); const getLocalStream async (constraints { video: true, audio: true }) { try { const stream await navigator.mediaDevices.getUserMedia(constraints); localStream.value stream; error.value ; return stream; } catch (err) { error.value 获取媒体失败: ${err.name} - ${err.message}; throw err; // 让调用方处理 } }; return { localStream, error, getLocalStream }; }2.2.1 关键参数说明为什么constraints必须显式声明video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } }强制理想分辨率避免 Chrome 默认用 640x480 导致画质模糊audio: true若设为falselocalStream.value.getAudioTracks()返回空数组后续pc.addTrack()无音频轨道video: { facingMode: user }移动端优先前置摄像头environment则为后置注意getLocalStream()返回 Promise必须await。源码中Meeting.vue的onMounted里调用它并用v-ifmedia.localStream控制video渲染避免srcObject绑定空流报错。2.3 连接状态模块用 reactive 对象实时追踪 RTCPeerConnection 生命周期RTCPeerConnection的状态机signalingState,iceConnectionState,connectionState是调试连接问题的核心线索。源码usePeerConnection.js不仅创建实例还用reactive包装所有状态字段并监听事件// src/composables/usePeerConnection.js export function usePeerConnection(config {}) { const pc new RTCPeerConnection(config); const state reactive({ signalingState: pc.signalingState, iceConnectionState: pc.iceConnectionState, connectionState: pc.connectionState, isConnecting: false, isStable: false }); // 监听状态变更 pc.addEventListener(signalingstatechange, () { state.signalingState pc.signalingState; state.isStable pc.signalingState stable; }); pc.addEventListener(iceconnectionstatechange, () { state.iceConnectionState pc.iceConnectionState; }); pc.addEventListener(connectionstatechange, () { state.connectionState pc.connectionState; }); return { pc, state }; }2.3.1 状态组合判断表连接失败时该查哪一列场景signalingStateiceConnectionStateconnectionState优先排查方向本地 offer 已发但没收到 answerhave-local-offernew/checkingnew信令是否送达对方是否收到并响应远程 answer 已收但画面黑屏stableconnectedconnected检查remoteStream是否绑定到videopc.getReceivers()是否有 video track连接几秒后断开stablefaileddisconnectedICE 失败检查 STUN/TURN 服务器配置或防火墙提示源码Meeting.vue中{{ peer.state.iceConnectionState }}直接显示在 UI 顶部这是最快速的连接健康度指示器。connected才代表媒体流真正通了。3. 在本地跑通最小会议5 步命令 3 个必改配置项源码压缩包解压后目录结构极简src/前端、server.js信令、package.json。不需要 Docker、K8s 或云服务纯 Node.js 浏览器即可验证。以下是严格按顺序执行的步骤跳过任意一步都会导致白屏或连接超时。3.1 启动信令服务器必须先于 Vue 应用运行# 进入项目根目录 cd your-unzipped-folder # 安装服务端依赖仅 ws npm install ws # 启动信令服务默认端口 8080 node server.js验证打开http://localhost:8080应看到WebSocket server is running on port 8080。若报错Error: listen EADDRINUSE说明端口被占修改server.js第 3 行port: 8080为8081并同步改前端配置。3.2 启动 Vue 应用确保使用 Vite非 Vue CLI# 安装前端依赖 npm install # 启动开发服务器默认 http://localhost:3000 npm run dev注意源码基于 Vue 3 Vite 构建package.json中dev: vite。若误用vue-cli-service serve会报错Cannot find module vite。3.3 修改三个硬编码配置否则连接必然失败源码中所有网络地址都是明文写死的必须手动修改文件路径配置项默认值必改值说明src/composables/useSignaling.jsconst SIGNALING_URLws://localhost:8080ws://localhost:8080若改了 server 端口此处同步改WebSocket 信令地址格式必须为ws://src/composables/usePeerConnection.jsconst STUN_SERVERstun:stun.l.google.com:19302保留默认Google 公共 STUN 可用用于 NAT 穿透国内访问可能慢可换为stun:stun1.l.google.com:19302src/composables/usePeerConnection.jsconst TURN_SERVERnull留空TURN 需付费服务测试阶段禁用源码未实现 TURN 认证设为null防止pc初始化失败关键验证点打开浏览器开发者工具 → Network → WS能看到localhost:8080连接状态为101 Switching Protocols且有offer/answer消息收发证明信令通。3.4 双浏览器实测同一台机器也能模拟两人会议在 Chrome 打开http://localhost:3000→ 点击“加入会议” → 允许摄像头和麦克风新开一个无痕窗口非新标签页因同源策略限制同样访问http://localhost:3000→ 点击“加入会议”观察两个窗口左侧显示本地视频右侧显示对方视频延迟 500ms打开 DevTools → Application → Frames → 查看RTCPeerConnection实例确认iceConnectionState为connected为什么必须无痕窗口普通标签页间localStorage和RTCPeerConnection实例会冲突导致第二个页面无法创建新连接。3.5 排查常见白屏问题三行命令定位根源当页面加载后只有“正在连接...”文字无视频画面时按顺序执行# 1. 检查信令服务是否存活返回 200 即正常 curl -I http://localhost:8080 # 2. 检查浏览器控制台是否有 getUserMedia 错误如 NotAllowedError # 若有说明摄像头权限被拒需手动进入 chrome://settings/content/camera 允许 localhost # 3. 检查 WebSocket 连接状态在 DevTools Console 执行 console.log(WebSocket status:, window.signaling?.ws?.readyState) # 返回 1 表示已连接0 表示未连接需检查 SIGNALING_URL4. 把“简单会议”升级为可用产品添加屏幕共享与错误降级策略源码的“简单”体现在功能精简而非技术妥协。要将其投入内部试用只需在现有架构上叠加两层能力一是扩展媒体源类型屏幕共享二是增强连接鲁棒性错误降级。这两处改动均不超过 30 行代码且完全复用原有usePeerConnection和useMediaStream模块。4.1 添加屏幕共享按钮复用useMediaStream获取 display mediaWebRTC 屏幕共享使用navigator.mediaDevices.getDisplayMedia()与getUserMedia()接口一致因此可直接复用useMediaStream.js的逻辑封装!-- Meeting.vue -- template !-- ... 其他按钮 -- button clicktoggleScreenShare :disabledisSharingScreen {{ isSharingScreen ? 停止共享 : 共享屏幕 }} /button /template script setup import { useMediaStream } from /composables/useMediaStream import { usePeerConnection } from /composables/usePeerConnection const { localStream, getLocalStream } useMediaStream() const { pc } usePeerConnection() let screenStream null const isSharingScreen ref(false) const toggleScreenShare async () { if (isSharingScreen.value) { // 停止共享移除屏幕轨道恢复摄像头 if (screenStream) { screenStream.getTracks().forEach(track track.stop()) screenStream null } // 重新获取摄像头流 await getLocalStream({ video: true, audio: true }) isSharingScreen.value false } else { // 开始共享获取屏幕流替换本地流 try { screenStream await navigator.mediaDevices.getDisplayMedia({ video: true }) // 替换 PC 中的视频轨道 const videoTrack screenStream.getVideoTracks()[0] const sender pc.getSenders().find(s s.track?.kind video) if (sender) { sender.replaceTrack(videoTrack) } isSharingScreen.value true } catch (err) { console.error(屏幕共享失败:, err) alert(共享屏幕被拒绝请检查权限设置) } } } /script逻辑说明getDisplayMedia()返回MediaStream其getVideoTracks()获取屏幕轨道pc.getSenders()找到当前视频发送器replaceTrack()动态切换无需重建连接。这比销毁pc再重连更高效。4.2 实现连接降级当 ICE 失败时自动回落至低分辨率iceConnectionState为failed时用户看到的是黑屏。源码中usePeerConnection.js的状态监听可触发降级逻辑// src/composables/usePeerConnection.js pc.addEventListener(iceconnectionstatechange, () { state.iceConnectionState pc.iceConnectionState; if (pc.iceConnectionState failed) { // 降级降低本地流分辨率重新 negotiate const videoTrack localStream.value?.getVideoTracks()[0]; if (videoTrack) { // 创建新约束720p → 480p const constraints { width: { max: 640 }, height: { max: 480 } }; videoTrack.applyConstraints(constraints).catch(console.warn); // 触发重新协商 pc.createOffer().then(offer pc.setLocalDescription(offer)) } } });4.2.1 降级参数对照表不同网络环境下的推荐约束网络类型widthheightframeRate适用场景4G 移动网络{ max: 480 }{ max: 360 }{ max: 15 }弱网保连通办公网千兆{ ideal: 1280 }{ ideal: 720 }{ ideal: 30 }高清会议公共 WiFi干扰大{ max: 640 }{ max: 480 }{ max: 24 }平衡画质与稳定性注意applyConstraints()是异步操作需await。源码中为简化未加await实际项目应包装为async函数并处理reject。4.3 验证降级效果用 Chrome DevTools 模拟弱网打开 DevTools → Network → Online → 选择Slow 3G在会议中故意断开网络再恢复观察iceConnectionState是否短暂变为failed查看localStream的getVideoTracks()[0].getSettings()确认width/height已更新为降级值画面应从黑屏恢复为 480p 流证明降级生效提示此策略不依赖外部 QoS 服务纯前端实现符合“简单源码”的定位——所有逻辑都在usePeerConnection.js内无额外依赖。5. Vue 项目中 WebRTC 的性能边界内存泄漏检测与帧率优化技巧源码虽小但长期运行1 小时可能出现内存缓慢增长或帧率下降。这不是 Bug而是 WebRTC 媒体流生命周期管理的固有挑战。以下技巧直接作用于Meeting.vue无需修改底层库。5.1 防止 MediaStream 内存泄漏track.stop() 的精确时机MediaStream对象不会自动释放尤其当video.srcObject被设为null后其getTracks()返回的MediaStreamTrack仍驻留内存。源码中onUnmounted钩子必须显式停止所有轨道script setup import { onUnmounted } from vue import { useMediaStream } from /composables/useMediaStream const { localStream } useMediaStream() onUnmounted(() { // 关键遍历并 stop 所有 track if (localStream.value) { localStream.value.getTracks().forEach(track track.stop()) localStream.value null } }) /script验证方法打开 Chrome DevTools → Memory → 拍摄 Heap Snapshot搜索MediaStream关闭会议页面后再拍一次对比数量是否归零。5.2 提升渲染帧率用 requestVideoFrameCallback 替代 setInterval源码中视频渲染依赖video元素的play()但高帧率场景下setInterval更新 UI 会造成丢帧。Chrome 114 支持requestVideoFrameCallback精准同步视频帧// 在 Meeting.vue 的 setup 中 let videoRef null const onVideoLoad () { if (!videoRef) return // 使用原生帧回调替代轮询 const callback (now, metadata) { // metadata 给出当前帧时间戳、解码耗时等 console.log(帧时间: ${metadata.mediaTime.toFixed(2)}s, 解码耗时: ${metadata.decodeTime}ms) // 触发 Vue 响应式更新如帧率统计 videoRef.requestVideoFrameCallback(callback) } videoRef.requestVideoFrameCallback(callback) }优势requestVideoFrameCallback由浏览器视频解码器触发频率与实际播放帧率一致如 30fps避免setInterval(33)因 JS 主线程阻塞导致的掉帧。5.3 限制本地预览分辨率用 CSS transform 缩放替代高清采集即使摄像头支持 1080p本地预览也无需同等分辨率——既省带宽又减 GPU 负载。源码中video元素可加 CSS 限制style scoped .local-video { width: 320px; height: 240px; /* 关键用 transform 缩放不触发重绘 */ transform: scale(0.5); transform-origin: top left; } /style原理transform: scale()是合成层操作GPU 加速而width/height会触发 Layout。实测可降低 Chrome 渲染线程 CPU 占用 15%~20%。最终效果一个基于 Vue 3 的 WebRTC 会议应用从npm run dev到支持屏幕共享、弱网降级、内存安全所有改动都在源码的src/composables/和src/views/Meeting.vue内完成无需引入任何商业 SDK 或云服务。本文还有配套的精品资源点击获取