1. 从想法到现实为什么选择UniApp做远程视频系统大家好我是老张一个在智能硬件和物联网领域摸爬滚打了十来年的开发者。这些年我经手过不少需要远程查看视频的项目从早期的工业巡检到现在的智能家居。我发现很多开发者尤其是刚入行的朋友一听到“远程摄像头”、“实时预览”这些词第一反应就是“这得用原生开发吧肯定很复杂”。其实不然今天我就想用这篇文章彻底打破这个迷思。我们这次要做的是一个“家庭宠物远程看护”应用。想象一下这个场景你白天在公司上班心里总惦记着家里的猫主子有没有捣乱、水碗空了没。这时候你掏出手机打开我们开发的应用就能实时看到家里摄像头拍到的画面甚至还能喊两嗓子跟它互动。这个需求听起来很具体但背后的技术栈——摄像头接入、视频流传输、多端预览——正是很多物联网和安防项目的核心。那为什么我强烈推荐用UniApp来做这件事呢原因有三点也是我踩过无数坑之后的经验之谈。第一跨端成本极低。这个项目的核心价值在于“随时能看”。宠物主人可能用苹果手机也可能用安卓手机甚至想在小程序里快速打开看一眼。如果你用原生开发iOS和Android两套代码人力时间直接翻倍。而UniApp“一次开发多端发布”的特性让你用Vue.js写一套代码就能编译成iOS、Android、乃至各种小程序的应用。对于创业团队或个人开发者来说这简直是“降维打击”能让你把宝贵精力集中在业务逻辑和体验优化上。第二生态成熟坑少。UniApp发展到现在其插件市场DCloud插件市场已经非常丰富。像摄像头调用、视频播放、网络请求这些基础能力都有现成的、经过大量项目验证的插件可用。这意味着你不用从零去研究安卓的Camera2 API或iOS的AVFoundation大大降低了技术门槛和开发风险。我们这次实战就会用到一些关键插件。第三性能足够应对主流场景。很多人担心H5或跨端方案的性能尤其是视频这种“重量级”媒体。确实对于需要超低延迟毫秒级的竞技游戏或工业控制原生仍是首选。但对于我们“宠物看护”这种场景延迟在1-3秒内都是完全可接受的。UniApp通过原生渲染和JS桥接的优化配合合适的传输协议比如我们后面会讲的WebRTC完全能提供流畅的实时预览体验。我实测过在一般的家庭Wi-Fi环境下效果非常不错。所以如果你正想涉足物联网、远程监控或任何需要视频联动的领域但又苦于原生开发的高昂成本那么跟着我这次从零开始的UniApp实战会是一个绝佳的起点。我们不止是贴代码更要搞清楚每一步背后的“为什么”构建一个真正可用的系统。2. 蓝图设计系统架构与核心流程拆解在动手写代码之前我们必须把整个系统的蓝图画清楚。脑子里有张地图写代码时才不会迷路。我们这个“宠物远程看护”系统虽然最终呈现给用户的只是一个简单的手机App但背后其实是一个典型的客户端-服务器-客户端C-S-C架构。别被名词吓到我用人话给你捋一遍。整个系统跑起来其实就是三步走抓取、传输、展示。第一步抓取视频流。这发生在“看守端”。可能是一部旧的安卓手机专门放在家里对着猫窝。这个设备上的UniApp应用需要调用摄像头硬件获得实时的视频数据。这里的关键是稳定和低功耗毕竟可能要长时间开着。第二步传输视频流。这是最核心的一环。视频数据不能直接从家里的旧手机飞到你的办公手机上中间需要一个“中转站”也就是服务器。服务器的核心作用不是存视频当然也可以存而是转发。它接收看守端发来的视频流再立即转发给一个或多个预览端比如你的手机。为什么需要中转因为家庭网络通常没有公网IP你的手机无法直接连接家里的设备。服务器就是这个“中间人”确保连接能建立起来。第三步展示视频流。这发生在“预览端”也就是你随身携带的手机。这个设备上的UniApp应用需要从服务器拉取视频流并流畅地渲染在屏幕上。这里的关键是延迟低、画面清晰、操作流畅。为了更直观我把这个数据流画成了下面这个简单的表格你可以对照着看角色设备示例核心任务关键技术点看守端 (Publisher)旧手机、摄像头设备捕获视频编码并推送到服务器摄像头权限、视频采集、编码压缩、推流协议服务器 (Signaling Relay)云服务器 (如2核4G的Linux主机)信令交换、流媒体中继转发信令服务器WebSocket、媒体服务器如SRS预览端 (Subscriber)日常用的手机从服务器拉流解码并实时播放拉流协议、视频解码、播放器渲染那么技术栈怎么选看守端和预览端都是UniAppVue.js这个不变。服务器我推荐用Node.js Express做信令服务负责设备间的“打招呼”用SRSSimple Realtime Server做媒体服务器负责真正的视频流搬运工。SRS是个开源的流媒体服务器对WebRTC和RTMP支持都非常好部署也简单。传输协议上我们重点用WebRTC。它最大的优点是可以实现P2P穿透在理想情况下两个客户端能直接连接延迟极低。即使穿透失败需要服务器中转Relay其延迟也远低于传统的RTMP/HLS方案。这对于想和宠物实时互动的你来说体验提升是巨大的。3. 实战第一步搭建开发环境与项目初始化工欲善其事必先利其器。咱们先把手头的工具和环境准备好。放心步骤我都给你细化好了跟着做就行。3.1 基础环境安装首先你需要安装Node.js。这是现代前端开发的基础我们的很多工具都依赖它。去官网下载LTS长期支持版安装就行。安装完后打开命令行Windows用CMD或PowerShellMac用终端输入node -v和npm -v能看到版本号就说明成功了。接下来是重头戏安装UniApp的开发工具HBuilderX。这是DCloud官方推出的IDE对UniApp的支持是最好的有完善的代码提示、真机运行和云打包功能。你去DCloud官网下载“App开发版”安装过程就是一路下一步。我建议你把它安装在默认路径避免一些奇怪的权限问题。3.2 创建你的第一个UniApp项目打开HBuilderX点击左上角“文件” - “新建” - “项目”。会弹出一个对话框。项目类型选择“uni-app”。模板这里我建议新手选择“默认模板”即可。它结构清晰没有太多预设内容方便我们从头构建。项目名称比如就叫PetLive。位置选一个你熟悉的文件夹。Vue版本选择3。Vue 3的Composition API写起来更灵活生态也是未来主流。点击创建HBuilderX会自动生成项目结构。你主要需要关注以下几个目录和文件pages目录存放所有页面。每个页面一个文件夹里面是Vue文件。static目录存放静态资源如图片、字体。App.vue应用的根组件可以在这里设置全局样式和逻辑。main.js应用的主入口文件。pages.json页面路由和全局样式配置文件非常重要。manifest.json应用配置比如App图标、启动图、权限申请等打包前必须配置。3.3 安装必备的插件UniApp的强大在于其插件市场。我们需要安装两个核心插件来操作摄像头和视频。摄像头插件在HBuilderX中点击“工具” - “插件安装”。搜索getUserMedia或直接搜索“摄像头”。我常用的是uni-getUserMedia这个插件。它封装了多端的摄像头调用API使用起来比直接操作浏览器原生API更简单、兼容性更好。点击插件详情页的“使用HBuilderX导入插件”它会自动将插件代码集成到你的项目中。视频播放插件同样在插件市场搜索“视频播放”。uni-video或官方推荐的播放器组件通常就够用了。我们预览端需要用它来播放远程视频流。安装完插件后记得在pages.json的easycom规则里确认一下通常插件会自动配置这样你就可以在页面里直接使用组件无需手动导入了。3.4 配置App权限关键步骤这是很多新手会栽跟头的地方。应用需要访问摄像头和麦克风如果你需要语音必须在打包前明确声明。打开manifest.json文件切换到“App模块配置”标签页。在“权限配置”里勾选“摄像头”和“麦克风”。在“iOS设置”和“Android设置”里找到“权限配置”同样需要添加对应的摄像头和录音权限描述。对于iOS你还需要在“隐私描述”里填写使用摄像头的理由比如“用于远程查看宠物实时画面”。这个描述会显示在系统向用户请求权限的弹窗上写得清楚友好能提高用户授权率。环境搭好了项目建好了插件也齐了。就像盖房子地基和建材都备妥了接下来我们就要开始砌第一堵墙——实现看守端的视频采集。4. 看守端开发捕获并推送摄像头视频流现在我们来打造放在家里的“看守端”。它的核心任务就一个打开摄像头把拍到的画面源源不断地发送给服务器。4.1 创建页面与获取摄像头权限首先在pages目录下新建一个文件夹比如叫publisher在里面新建一个index.vue文件。这就是看守端的主页面。在template部分我们先搭建一个简单的界面template view classpublisher-container !-- 本地视频预览窗口 -- video classlocal-video :src-objectlocalStream autoplay muted playsinline controls /video view classcontrol-panel button taptoggleCamera :disabled!isCameraReady切换摄像头/button button tapstartPublishing :disabledisPublishing || !isCameraReady开始推流/button button tapstopPublishing :disabled!isPublishing停止推流/button text classstatus-text状态{{ statusText }}/text /view /view /template注意几个属性autoplay自动播放、muted静音避免回声、playsinline在移动端网页内播放而非全屏这对移动端体验至关重要。接下来是重头戏在script setup里写逻辑我们用Vue 3的Composition API更简洁script setup import { ref, onMounted, onUnmounted } from vue; import { uniGetUserMedia } from /js_sdk/uni-getusermedia; // 引入插件路径根据实际调整 // 响应式数据 const localStream ref(null); // 本地媒体流 const isCameraReady ref(false); // 摄像头是否就绪 const isPublishing ref(false); // 是否正在推流 const statusText ref(准备中...); const currentCamera ref(front); // front or back // 初始化摄像头 const initCamera async () { statusText.value 正在请求摄像头权限...; try { // 调用插件API获取媒体流 const stream await uniGetUserMedia.getUserMedia({ video: { facingMode: currentCamera.value front ? user : environment, width: { ideal: 1280 }, // 理想分辨率设备会尽量满足 height: { ideal: 720 }, frameRate: { ideal: 24 } // 理想帧率平衡流畅与功耗 }, audio: false // 宠物看护先不要音频需要的话改为true }); localStream.value stream; isCameraReady.value true; statusText.value 摄像头已就绪; console.log(摄像头初始化成功流信息:, stream); } catch (err) { console.error(无法访问摄像头:, err); statusText.value 摄像头访问失败 err.message; // 这里可以给用户更友好的提示比如引导他去设置打开权限 } }; // 切换前后摄像头 const toggleCamera async () { stopCamera(); // 先停止当前流 currentCamera.value currentCamera.value front ? back : front; await initCamera(); // 重新初始化 }; // 停止摄像头 const stopCamera () { if (localStream.value) { localStream.value.getTracks().forEach(track track.stop()); localStream.value null; } isCameraReady.value false; }; // 开始推流到服务器 const startPublishing () { if (!localStream.value || isPublishing.value) return; statusText.value 正在连接服务器并推流...; isPublishing.value true; // 这里调用推流逻辑我们下一节详细实现 // connectAndPublish(localStream.value); }; // 停止推流 const stopPublishing () { isPublishing.value false; statusText.value 推流已停止; // 这里调用停止推流的逻辑 }; // 生命周期 onMounted(() { initCamera(); }); onUnmounted(() { stopCamera(); stopPublishing(); }); /script这段代码的核心是initCamera函数。它使用我们安装的插件按照指定的参数前后摄像头、分辨率、帧率去请求摄像头权限并获取视频流。获取成功后赋值给localStream并绑定到video标签的srcObject属性上画面就显示出来了。4.2 实现WebRTC推流逻辑摄像头画面有了怎么把它送到服务器呢这就是startPublishing函数里要干的事。我们需要建立WebRTC连接。WebRTC推流不直接发送流数据而是先通过一个“信令服务器”交换网络信息SDP Offer/Answer然后建立点对点或中转连接。首先我们需要一个信令服务器来交换SDP。这里为了简化我们先模拟一个流程。在实际项目中你需要一个Node.js WebSocket的后端服务。假设我们有一个信令服务器地址ws://your-signaling-server.com。看守端的推流逻辑大致如下创建RTCPeerConnection这是WebRTC的核心对象代表一个端到端的连接。const peerConnection new RTCPeerConnection(configuration); // configuration包含STUN/TURN服务器信息添加本地流将摄像头获取的localStream添加到这个连接中。localStream.value.getTracks().forEach(track { peerConnection.addTrack(track, localStream.value); });创建Offer并设置本地描述看守端主动创建一个“邀约”Offer描述它想要建立的连接类型和拥有的媒体流。const offer await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer);通过信令服务器发送Offer将这个Offer通过WebSocket发送给服务器服务器再转发给“预览端”或者媒体服务器。接收Answer并设置远程描述收到来自服务器转发的“应答”Answer后将其设置为远程描述。// 假设通过WebSocket收到answer await peerConnection.setRemoteDescription(answer);处理ICE候选在连接建立过程中双方会交换网络地址信息ICE Candidate也需要通过信令服务器转发。peerConnection.onicecandidate (event) { if (event.candidate) { // 通过WebSocket发送event.candidate给对端 } };一旦这个“握手”过程完成视频数据通道就建立起来了视频流会自动开始传输。对于看守端主要的代码就是创建连接、添加轨道、发起邀约。真正的网络协商和流传输由RTCPeerConnection对象在底层完成。5. 服务器端搭建信令与流媒体中继看守端和预览端不能直接“找到”对方需要一个“中间人”这就是服务器。我们的服务器要承担两个核心角色信令交换和流媒体中继。听起来高大上其实我们分两步走用现成的工具搭起来并不难。5.1 搭建信令服务器WebSocket信令服务器就像电话局的“接线员”。它的工作很简单让两个客户端看守端和预览端互相交换“联系方式”SDP和ICE Candidate。我们用一个最简单的Node.js ws库来实现。在你的服务器上可以用一台云服务器本地测试也可以用本地IP新建一个文件夹比如叫signaling-server。初始化项目并安装依赖npm init -y npm install ws express创建主文件server.jsconst WebSocket require(ws); const express require(express); const app express(); const port 8080; // 信令服务器端口 // 创建一个HTTP服务器Express和WebSocket共享同一个端口 const server app.listen(port, () { console.log(信令服务器运行在 http://localhost:${port}); }); const wss new WebSocket.Server({ server }); // 存储连接的客户端简单用房间号映射 const rooms {}; wss.on(connection, (ws, req) { console.log(新的客户端连接); ws.on(message, (message) { try { const data JSON.parse(message); const { type, roomId, payload } data; if (type join) { // 客户端加入房间 if (!rooms[roomId]) { rooms[roomId] []; } rooms[roomId].push(ws); ws.roomId roomId; console.log(客户端加入房间: ${roomId}); // 通知房间内其他用户有新用户加入用于点对点场景 broadcastToRoom(roomId, { type: new-peer }, ws); } else if (type offer || type answer || type candidate) { // 转发SDP Offer/Answer 或 ICE Candidate // 这里逻辑是A发给服务器的信令服务器原样转发给房间内除A以外的所有客户端 // 在1对1场景下就是转发给另一个客户端 broadcastToRoom(ws.roomId, data, ws); } } catch (e) { console.error(处理消息出错:, e); } }); ws.on(close, () { console.log(客户端断开连接); // 清理房间 if (ws.roomId rooms[ws.roomId]) { rooms[ws.roomId] rooms[ws.roomId].filter(client client ! ws); if (rooms[ws.roomId].length 0) { delete rooms[ws.roomId]; } } }); }); function broadcastToRoom(roomId, data, excludeWs null) { if (!rooms[roomId]) return; rooms[roomId].forEach(client { if (client ! excludeWs client.readyState WebSocket.OPEN) { client.send(JSON.stringify(data)); } }); } // 提供一个简单的HTTP接口用于健康检查 app.get(/health, (req, res) { res.send(Signaling Server is OK); });这个服务器逻辑很清晰客户端通过WebSocket连接上来发送一个join消息加入某个“房间”比如房间号可以是“宠物之家”。之后这个房间里的任何一个客户端发送offer、answer或candidate消息服务器都会把它转发给房间里的其他客户端。这样就完成了信令的中转。5.2 搭建媒体服务器SRS信令服务器只负责“传话”真正的视频流数据量巨大需要专门的媒体服务器来转发。这里我推荐SRS它轻量、开源对WebRTC支持非常好。在服务器上可以和信令服务器同一台注意端口别冲突我们用Docker来运行SRS这是最省事的方式。确保服务器安装了Docker。一行命令启动SRSdocker run -d --name srs -p 1935:1935 -p 1985:1985 -p 8080:8080 -p 8000:8000/udp \ ossrs/srs:5.0 ./objs/srs -c conf/srs.conf这条命令拉取SRS镜像并运行映射了几个关键端口1935: RTMP端口如果你未来想兼容RTMP推流。1985: HTTP API端口用于管理。8080: HTTP端口用于播放HLS流等。8000: WebRTC使用的UDP端口非常重要。配置SRS支持WebRTCSRS默认配置可能未开启WebRTC。你需要进入容器内部修改配置或者使用一个预置的WebRTC配置文件。更简单的方法是直接使用SRS官方提供的WebRTC示例配置。你可以访问SRS的GitHub仓库找到trunk/conf/rtc2rtmp.conf这个配置文件将其内容复制到本地然后通过Docker卷挂载进去运行。一个更完整的启动命令示例使用自定义配置文件# 1. 在宿主机创建配置文件目录并放入你的srs.conf mkdir -p ~/srs/conf # 将你的配置文件例如从官网示例复制的放入 ~/srs/conf/srs.conf # 2. 运行容器挂载配置文件 docker run -d --name srs \ -p 1935:1935 -p 1985:1985 -p 8080:8080 -p 8000:8000/udp \ -v ~/srs/conf/srs.conf:/usr/local/srs/conf/srs.conf \ ossrs/srs:5.0SRS启动后它会监听8000端口的UDP数据WebRTC流。看守端推流端的WebRTC连接其ICE Candidate中的服务器地址就应该指向这个SRS服务器的IP和8000端口。SRS接收到流之后会进行转发预览端拉流端同样通过WebRTC协议连接到SRS的8000端口来拉取视频流。这样一个简易但完整的信令媒体中继服务器就搭建好了。看守端通过信令服务器交换SDP最终与SRS媒体服务器建立WebRTC连接并推流预览端同样通过信令服务器交换SDP与SRS建立连接并拉流。所有视频数据都通过SRS高效转发。6. 预览端开发拉流与实时播放服务器搭好了流也推上来了最后一步就是在我们随身携带的手机上看到实时画面。这就是预览端的工作。6.1 创建预览页面与拉流逻辑在pages目录下再新建一个文件夹比如叫subscriber里面创建index.vue。这个页面更专注主要就是一个视频播放器。模板部分很简单template view classsubscriber-container !-- 远程视频播放窗口 -- video classremote-video refvideoPlayer autoplay playsinline controls /video view classcontrol-panel input v-modelroomId placeholder输入房间号如pet-home / button tapstartWatching :disabledisWatching开始观看/button button tapstopWatching :disabled!isWatching停止观看/button text classstatus-text延迟{{ latency }}ms | 状态{{ statusText }}/text /view /view /template这里我们增加了一个输入框用于输入房间号确保连接到正确的视频源。核心逻辑在script setup中script setup import { ref, onUnmounted } from vue; const videoPlayer ref(null); // 视频DOM元素引用 const roomId ref(pet-home); // 默认房间号 const isWatching ref(false); const statusText ref(等待连接...); const latency ref(0); let peerConnection null; let signalingSocket null; // WebSocket连接 let latencyTimer null; // 开始观看 const startWatching async () { if (isWatching.value) return; statusText.value 正在连接信令服务器...; // 1. 连接信令服务器 (这里替换成你的实际地址) const wsUrl ws://your-signaling-server.com; signalingSocket new WebSocket(wsUrl); signalingSocket.onopen () { statusText.value 信令服务器已连接加入房间...; // 发送加入房间消息 signalingSocket.send(JSON.stringify({ type: join, roomId: roomId.value })); }; signalingSocket.onmessage async (event) { const data JSON.parse(event.data); switch (data.type) { case offer: // 收到看守端或SRS发来的Offer await handleOffer(data.payload); break; case candidate: // 收到ICE Candidate await handleCandidate(data.payload); break; // ... 处理其他信令类型 } }; // 2. 创建RTCPeerConnection (作为接收方) const config { iceServers: [ { urls: stun:stun.l.google.com:19302 }, // 免费STUN服务器 // 如果P2P穿透失败需要配置TURN服务器这里先省略 ] }; peerConnection new RTCPeerConnection(config); // 3. 监听远程流到来 peerConnection.ontrack (event) { console.log(收到远程视频流); if (videoPlayer.value event.streams event.streams[0]) { videoPlayer.value.srcObject event.streams[0]; statusText.value 正在播放远程视频; isWatching.value true; startLatencyCheck(); // 开始检测延迟 } }; peerConnection.onicecandidate (event) { if (event.candidate signalingSocket.readyState WebSocket.OPEN) { // 将自己的ICE Candidate发送给信令服务器 signalingSocket.send(JSON.stringify({ type: candidate, roomId: roomId.value, payload: event.candidate })); } }; }; // 处理收到的Offer并创建Answer const handleOffer async (offer) { if (!peerConnection) return; await peerConnection.setRemoteDescription(new RTCSessionDescription(offer)); const answer await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); // 将Answer通过信令服务器发送回去 if (signalingSocket signalingSocket.readyState WebSocket.OPEN) { signalingSocket.send(JSON.stringify({ type: answer, roomId: roomId.value, payload: answer })); } }; // 处理收到的ICE Candidate const handleCandidate async (candidate) { if (!peerConnection) return; try { await peerConnection.addIceCandidate(new RTCIceCandidate(candidate)); } catch (e) { console.error(添加ICE Candidate失败:, e); } }; // 简单的延迟检测通过RTCPeerConnection的统计信息 const startLatencyCheck () { latencyTimer setInterval(async () { if (!peerConnection) return; const stats await peerConnection.getStats(); stats.forEach(report { if (report.type remote-inbound-rtp report.kind video) { // 计算近似延迟实际更复杂 latency.value Math.round(report.roundTripTime * 1000) || 0; } }); }, 2000); // 每2秒检测一次 }; // 停止观看 const stopWatching () { isWatching.value false; statusText.value 已停止; if (latencyTimer) clearInterval(latencyTimer); if (peerConnection) { peerConnection.close(); peerConnection null; } if (signalingSocket) { signalingSocket.close(); signalingSocket null; } if (videoPlayer.value) { videoPlayer.value.srcObject null; } }; onUnmounted(() { stopWatching(); }); /script预览端的逻辑和看守端是镜像的但角色是“应答者”。它先连接信令服务器并加入房间然后等待接收offer。收到offer后设置远程描述创建answer并发送回去。同时双方不断交换ICE Candidate。当连接建立peerConnection.ontrack事件被触发远程视频流就赋值给video标签的srcObject画面就出来了。6.2 播放器优化与状态反馈视频能播只是第一步体验要好还得优化。UniApp的video组件有一些原生特性可以用controls: 显示原生控制条播放/暂停、进度、全屏。show-center-play-btn: 是否显示中间播放按钮。enable-play-gesture: 是否开启播放手势点按播放/暂停。object-fit: 视频填充模式cover保持比例填满或contain保持比例全部显示。此外状态反馈很重要。我们代码里已经有了statusText和latency的显示。在实际项目中你还可以监听peerConnection.connectionState和iceConnectionState来获取更精确的连接状态如connected,disconnected,failed并给用户相应的提示比如“连接中”、“网络不稳定”、“连接失败请重试”。7. 性能调优与踩坑指南系统跑通了但可能你会发现延迟有点高或者手机发热严重。别急这是优化阶段了。这部分是我多年实战积累的经验能帮你避开很多坑。7.1 视频参数调优平衡清晰度与流畅度在getUserMedia时我们设置了视频参数。这几个参数对性能影响巨大分辨率width/height1280x720720P是移动端实时视频的甜点。再高如1080P对网络和编解码压力剧增延迟飙升再低如480P画面可能太模糊。可以用ideal和max约束。video: { width: { ideal: 1280, max: 1920 }, height: { ideal: 720, max: 1080 }, frameRate: { ideal: 24, max: 30 } }帧率frameRate24fps是人眼感觉流畅的下限也是电影常用帧率。对于宠物看护24fps完全足够能比30fps节省约20%的码率和计算开销。码率控制WebRTC会自动进行码率适应Adaptive Bitrate但在弱网下我们可以通过RTCPeerConnection的RTCRtpSender接口进行更手动的限制但这属于进阶操作。7.2 网络适应与降级策略网络环境千变万化必须有应对策略。STUN/TURN服务器我们配置里用了谷歌的公共STUN服务器。但在复杂的NAT网络如公司网、某些运营商网络下P2P可能失败。必须部署自己的TURN服务器如使用coturn项目作为数据中转的保底方案。TURN服务器流量会产生成本但能极大提升连接成功率。网络状态监听通过peerConnection.iceConnectionState变化来监听网络状态。当变为disconnected或failed时可以尝试自动重连或者提示用户“网络不佳正在尝试重连...”。清晰度自适应更复杂的方案是 simulcast 或 SVC可伸缩视频编码让服务器根据预览端的网络状况动态下发不同分辨率的视频流。对于初期项目可以先做一个手动切换清晰度的按钮。7.3 客户端资源管理与常见坑内存泄漏这是单页面应用SPA的常见问题。务必在页面卸载onUnmounted或应用隐藏时停止所有媒体流stream.getTracks().forEach(track track.stop())并关闭WebSocket和RTCPeerConnection连接。否则摄像头指示灯可能常亮后台持续耗电。iOS的播放策略iOS系统为了省电禁止视频自动播放且有声音。这就是为什么我们给video标签加muted和playsinline属性。如果需要声音必须在一个真实的用户触摸事件如按钮点击回调里手动设置videoElement.muted false并调用videoElement.play()。安卓兼容性不同安卓厂商对WebRTC的支持有细微差异。遇到问题首先检查HBuilderX的基础库版本是否够新其次可以尝试使用uni.createVideoContext来更底层地控制播放器。真机调试开发阶段一定要用HBuilderX的“真机运行”功能在实体手机上测试。模拟器无法测试摄像头和真实的网络环境。遇到权限问题仔细检查manifest.json的配置并去手机的系统设置里确认应用权限已开启。7.4 安全与隐私考量最后安全无小事。信令传输加密生产环境务必使用WSSWebSocket Secure和HTTPS防止信令被窃听或篡改。房间号与鉴权我们示例中的房间号是明文的。实际应用中应该由服务器在用户登录后动态生成一个复杂、唯一的房间号或Token并验证客户端的身份后才能加入房间防止未经授权的窥视。前端代码混淆使用HBuilderX发布时开启代码混淆和压缩增加反编译难度。用户提示在应用启动或首次使用摄像头时用清晰的文案告知用户摄像头正在被使用以及用途如“用于远程查看您的宠物”这不仅是体验也是应用商店审核的要求。走到这一步一个完整的、可运行的远程摄像头视频接入与实时预览系统就已经在你手中了。从家里的旧手机看守端到云服务器再到你掌中的手机预览端视频流已经能够穿越网络实时呈现。这个过程里我们不仅写了代码更搭建了一套微型的流媒体架构。