WebRTC入门实战:从一对一通话Demo到避坑指南
1. 从一次视频通话卡顿说起WebRTC到底解决了什么问题第一次接触WebRTC是因为一个很具体的需求做一个网页端的多人视频通话功能要求打开浏览器就能用不装插件、不装客户端。当时第一反应是去找现成的第三方服务但算了一下成本按分钟计费的模式在用户量上来之后压力不小。于是开始研究能不能自己搭一套这才真正走进了WebRTC的世界。WebRTC的全称是Web Real-Time Communication翻译过来就是网页实时通信。它本质上是一套由浏览器原生支持的API和协议集合让两个浏览器之间可以直接传输音频、视频和数据不需要经过中间服务器转发媒体流。这个“直接”非常关键——传统的视频通话方案媒体流通常要经过服务器中转服务器带宽成本高、延迟也大而WebRTC的设计目标是让媒体流走P2P通道服务器只负责帮忙“牵线搭桥”。它适合谁如果你是想在Web应用里加入音视频通话、屏幕共享、文件传输、实时协作白板这类功能的开发者WebRTC基本是绕不开的技术底座。哪怕你最终用的是某个封装好的SDK底层大概率还是WebRTC。对于前端工程师来说它意味着不需要深入理解编解码和网络协议的全貌也能快速搭出可用的实时通信功能对于后端或全栈工程师来说理解它的信令机制和网络穿透原理是保证通话稳定性的关键。但WebRTC也不是银弹。它的学习曲线在前几步还算平缓越往后越陡——尤其是涉及到NAT穿透、网络抖动、多人通话架构的时候坑一个接一个。我见过不少团队在Demo阶段跑得很顺一上真实网络环境就各种连不上、卡顿、断流。所以这篇内容我打算从入门的角度把WebRTC的核心机制、最小可用Demo的搭建过程、以及实际落地时最容易踩的坑系统地梳理一遍。不堆砌术语尽量用实际操作的视角来讲。2. WebRTC的三大核心模块不是“一个API”而是一套协作体系很多人以为WebRTC就是一个getUserMedia加一个RTCPeerConnection就完事了实际上它是一套由多个模块协同工作的体系。理解这几个模块各自的职责和它们之间的边界是后续排查问题的前提。2.1 媒体采集getUserMedia与设备适配的现实差距getUserMedia是WebRTC的入口API负责从摄像头和麦克风采集音视频流。调用方式看起来很简单const stream await navigator.mediaDevices.getUserMedia({ video: true, audio: true });但实际项目中这段代码背后要考虑的事情远比想象中多。首先是设备枚举用户的电脑上可能插了多个摄像头和麦克风你得先通过enumerateDevices拿到设备列表让用户选择而不是粗暴地直接用默认设备。其次是分辨率约束video: true会使用设备的默认分辨率可能是320x240也可能是1280x720完全不可控。正确的做法是通过约束条件指定const constraints { video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } }, audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } };这里的ideal和exact区别很大。ideal表示“尽量满足满足不了就降级”exact表示“必须满足否则直接报错”。在实际项目中除非你对分辨率有硬性要求否则建议用ideal避免因为设备不支持某个分辨率导致整个采集失败。还有一个容易被忽略的点移动端浏览器对getUserMedia的限制比桌面端严格得多。iOS上的Safari要求页面必须通过HTTPS加载而且首次调用必须由用户手势比如点击按钮触发不能在页面加载时自动调用。这个限制在开发阶段很容易被忽略因为桌面端Chrome往往不会报错。2.2 连接建立RTCPeerConnection与信令的配合逻辑RTCPeerConnection是WebRTC最核心的对象它负责建立、维护和关闭两个端点之间的媒体通道。但这里有个关键认知RTCPeerConnection本身不负责交换连接信息。两个浏览器要建立连接必须先交换各自的网络地址、端口、编解码能力等信息这个交换过程叫“信令”Signaling而WebRTC规范并没有规定信令用什么协议——你可以用WebSocket、HTTP轮询、甚至邮件只要双方能交换到信息就行。这个设计经常让初学者困惑为什么WebRTC不把信令也包了原因是WebRTC希望保持灵活性让开发者根据自己已有的基础设施来选择信令方式。实际项目中绝大多数方案用的是WebSocket因为它双向、低延迟、实现简单。信令交换的核心内容是SDPSession Description Protocol和ICE Candidate。SDP描述了媒体的编解码格式、分辨率、加密方式等信息ICE Candidate则是候选的网络路径。整个流程大致是这样的主叫方创建Offer调用createOffer()生成SDP通过信令服务器发给被叫方。被叫方收到Offer后调用createAnswer()生成应答SDP回传给主叫方。双方在交换SDP的过程中各自的ICE框架会收集本地的候选地址通过信令服务器互相交换。双方拿到对方的候选地址后尝试建立连接选择最优路径。这个流程听起来不复杂但实际写代码时时序问题是最容易出错的。比如ICE Candidate可能在SDP交换完成之前就已经开始收集了如果你没有在setRemoteDescription之前把候选缓存起来就会丢失部分候选地址导致连接失败。我后面会专门讲这个坑。2.3 数据传输RTCDataChannel的适用边界除了音视频WebRTC还提供了RTCDataChannel用于在两个端点之间传输任意数据。它支持两种模式可靠传输类似TCP和不可靠传输类似UDP。可靠模式下数据保证按序到达不可靠模式下数据可能丢失或乱序但延迟更低。RTCDataChannel的典型应用场景包括实时文字聊天、游戏状态同步、文件传输、远程控制指令等。但要注意它的传输速率受限于底层SCTP协议和网络状况不适合传输大文件——虽然理论上可以传但实际体验远不如直接用HTTP分片上传。const dataChannel peerConnection.createDataChannel(myChannel, { ordered: true, maxRetransmits: 3 }); dataChannel.onopen () { dataChannel.send(Hello from the other side); }; dataChannel.onmessage (event) { console.log(Received:, event.data); };这里maxRetransmits和ordered的组合决定了通道的可靠性级别。如果你做的是实时游戏同步可以设置ordered: false和maxRetransmits: 0让数据以最快速度到达丢包就丢包如果是文件传输则应该用默认的可靠模式。3. 搭建最小可用Demo从零跑通一次一对一通话理论讲再多不如实际跑一遍。这一章我会用一个最简化的方案带你从零搭出一个能在一对一之间通话的Demo。之所以强调“最小可用”是因为很多教程一上来就引入复杂的框架和第三方库反而让人看不清WebRTC本身的工作机制。3.1 信令服务器的选型与最简实现前面说了WebRTC不管信令所以我们需要自己搭一个。对于Demo来说用Node.js加ws库写一个WebSocket服务是最快的方案const WebSocket require(ws); const wss new WebSocket.Server({ port: 8080 }); const clients new Map(); wss.on(connection, (ws) { const id Math.random().toString(36).substring(2, 8); clients.set(id, ws); ws.send(JSON.stringify({ type: welcome, id })); ws.on(message, (message) { const data JSON.parse(message); // 转发给目标客户端 const target clients.get(data.target); if (target target.readyState WebSocket.OPEN) { target.send(JSON.stringify({ type: data.type, from: id, payload: data.payload })); } }); ws.on(close, () { clients.delete(id); }); });这个服务做的事情很简单维护一个客户端ID到WebSocket连接的映射收到消息后根据目标ID转发。实际生产环境当然要考虑房间管理、鉴权、断线重连等但Demo阶段这样就够了。选WebSocket而不是HTTP轮询的原因很直接信令交换对延迟敏感轮询的间隔会导致连接建立时间变长体验很差。WebSocket建立一次连接后可以持续双向通信信令消息几乎可以实时到达。3.2 主叫与被叫的完整交互流程拆解有了信令服务器接下来就是两端页面的逻辑。我把主叫方和被叫方的代码放在同一个HTML文件里通过按钮区分角色这样方便调试。主叫方的核心逻辑async function startCall() { // 1. 采集本地媒体 localStream await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); document.getElementById(localVideo).srcObject localStream; // 2. 创建PeerConnection pc new RTCPeerConnection(iceConfig); // 3. 添加本地轨道 localStream.getTracks().forEach(track { pc.addTrack(track, localStream); }); // 4. 监听远端轨道 pc.ontrack (event) { document.getElementById(remoteVideo).srcObject event.streams[0]; }; // 5. 监听ICE候选 pc.onicecandidate (event) { if (event.candidate) { sendSignal({ type: ice-candidate, target: remoteId, payload: event.candidate }); } }; // 6. 创建Offer const offer await pc.createOffer(); await pc.setLocalDescription(offer); // 7. 通过信令发送Offer sendSignal({ type: offer, target: remoteId, payload: offer }); }被叫方的逻辑async function handleOffer(offer, fromId) { // 1. 采集本地媒体 localStream await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); document.getElementById(localVideo).srcObject localStream; // 2. 创建PeerConnection pc new RTCPeerConnection(iceConfig); // 3. 添加本地轨道 localStream.getTracks().forEach(track { pc.addTrack(track, localStream); }); // 4. 设置远端描述 await pc.setRemoteDescription(new RTCSessionDescription(offer)); // 5. 创建Answer const answer await pc.createAnswer(); await pc.setLocalDescription(answer); // 6. 发送Answer sendSignal({ type: answer, target: fromId, payload: answer }); // 7. 监听ICE候选 pc.onicecandidate (event) { if (event.candidate) { sendSignal({ type: ice-candidate, target: fromId, payload: event.candidate }); } }; // 8. 监听远端轨道 pc.ontrack (event) { document.getElementById(remoteVideo).srcObject event.streams[0]; }; }这段代码看起来直白但有几个细节如果处理不好就会出问题。第一setRemoteDescription必须在createAnswer之前调用否则生成的Answer不包含正确的媒体协商信息。第二onicecandidate的监听要在setLocalDescription之前设置好否则可能丢失早期候选。第三收到ICE Candidate时如果此时remoteDescription还没设置需要先缓存起来等设置完再逐个添加。3.3 ICE配置STUN和TURN的角色区分在创建RTCPeerConnection时需要传入一个ICE服务器配置const iceConfig { iceServers: [ { urls: stun:stun.example.com:3478 }, { urls: turn:turn.example.com:3478, username: user, credential: pass } ] };STUN服务器的作用是帮助端点发现自己的公网地址。当两个端点都在NAT后面时它们不知道自己的公网IP和端口是什么STUN服务器会告诉它们。TURN服务器则是中继服务器当P2P连接无法建立时比如双方都在对称NAT后面媒体流会通过TURN服务器转发。这里有个实际经验STUN服务器可以自己搭也可以用公开的但TURN服务器几乎必须自己搭。公开的STUN服务不少但公开的TURN服务几乎没有因为TURN消耗服务器带宽没人愿意免费提供。自己搭TURN可以用开源的coturn配置不算复杂但要注意带宽成本和并发连接数。还有一个常见误区很多人以为配了STUN就不需要TURN了。实际上在真实的网络环境中大概有10%到20%的情况下P2P连接无法建立必须依赖TURN中继。如果你的应用对连通率有要求TURN是必须的。4. 实际落地时最容易踩的五个坑Demo跑通只是第一步真正上线之后才会发现各种意料之外的问题。这一章我整理了五个在实际项目中最常遇到的坑以及对应的排查思路和解决方案。4.1 ICE Candidate丢失时序问题的典型表现这个坑我在前面提过但值得单独拿出来讲因为它太常见了。现象是两端SDP交换成功但连接始终建立不起来查看日志发现ICE Candidate数量很少或者根本没有。根本原因是ICE Candidate的收集和SDP的交换是并行的。主叫方调用setLocalDescription之后ICE框架就开始收集候选地址每收集到一个就触发onicecandidate事件。如果此时被叫方还没有设置好remoteDescription这些候选就会被丢弃。解决方案是维护一个候选缓存队列let pendingCandidates []; pc.onicecandidate (event) { if (event.candidate) { if (pc.remoteDescription) { pc.addIceCandidate(event.candidate); } else { pendingCandidates.push(event.candidate); } } }; // 在setRemoteDescription之后 await pc.setRemoteDescription(description); pendingCandidates.forEach(candidate { pc.addIceCandidate(candidate); }); pendingCandidates [];这个模式在实际项目中几乎是标配建议直接封装成工具函数。4.2 编解码协商失败SDP中的m行不匹配SDP是文本格式的里面用m开头的行来描述媒体类型和编解码格式。如果两端的编解码能力没有交集协商就会失败。比如一端只支持VP8另一端只支持H.264那就没法通话。排查方法是把两端的SDP都打印出来对比mvideo行后面的编解码列表。正常情况下Chrome和Firefox都支持VP8和H.264所以这个问题在桌面端不常见。但在移动端尤其是某些定制浏览器上编解码支持情况可能不同。如果确实遇到不匹配可以通过修改SDP来强制指定编解码器但这属于比较高级的操作一般建议在createOffer时通过参数限制const offer await pc.createOffer({ offerToReceiveVideo: true, offerToReceiveAudio: true });然后在setLocalDescription之前修改SDP中的编解码优先级。不过这种操作兼容性风险较高非必要不建议。4.3 网络切换导致的连接中断移动端设备在WiFi和蜂窝网络之间切换时IP地址会变化原有的ICE连接就会失效。WebRTC本身有ICE重启机制但需要手动触发pc.oniceconnectionstatechange () { if (pc.iceConnectionState disconnected) { // 尝试ICE重启 pc.restartIce(); } };restartIce()会重新触发Offer/Answer流程收集新的候选地址。但这个过程需要信令服务器配合而且会导致短暂的通话中断。实际项目中通常还会加一个重连提示让用户知道网络正在恢复。4.4 回声消除不生效音频约束的隐藏陷阱回声是视频通话中最影响体验的问题之一。WebRTC默认开启了回声消除echoCancellation: true但在某些设备上效果并不理想。原因可能是多方面的麦克风增益过高、扬声器音量过大、或者设备本身的音频处理能力有限。除了依赖WebRTC的默认处理还可以通过约束条件做进一步控制const audioConstraints { echoCancellation: true, noiseSuppression: true, autoGainControl: true, // 某些设备支持更精细的控制 googEchoCancellation: true, googAutoGainControl: true, googNoiseSuppression: true };带goog前缀的约束是Chrome特有的其他浏览器可能不支持。实际使用时建议先检测浏览器类型再决定传哪些约束。4.5 多人通话的架构选择Mesh、SFU还是MCU一对一通话是P2P但三个人以上就不能简单用P2P了。假设有5个人通话如果每个人都要和另外4个人建立P2P连接总共需要10条连接每个人的上行带宽要支撑4路视频流这在移动端基本不现实。常见的多人通话架构有三种架构原理优点缺点适用场景Mesh每个端点与其他所有端点建立P2P连接实现简单延迟低上行带宽随人数线性增长3-4人小型通话SFU每个端点只上传一路流到服务器服务器转发给其他人上行带宽恒定扩展性好服务器带宽成本高中大型通话MCU服务器混流后分发给每个端点客户端压力最小服务器CPU消耗大延迟高大型会议对于入门项目如果人数不超过4人Mesh方案是最容易实现的。超过4人建议直接上SFU开源的方案有mediasoup、Janus等。MCU现在用得越来越少因为服务器混流的计算成本太高而且灵活性差。5. 从入门到进阶下一步该往哪个方向深入跑通一对一Demo、理解了信令和ICE的基本原理之后下一步的深入方向取决于你的实际需求。如果只是做一个简单的网页通话功能现有的知识基本够用了。但如果要做产品级的实时通信应用还有几个方向值得投入时间。第一个方向是SFU架构的深入。这是多人通话的主流方案涉及到流的路由、带宽自适应、分层编码等技术。开源的mediasoup是一个很好的学习起点它的文档和示例都比较完善。第二个方向是网络质量监测与自适应。WebRTC提供了getStats()API可以获取丢包率、延迟、抖动、带宽估计等指标。基于这些指标做动态调整比如降低分辨率、切换编解码器是保证通话质量的关键。setInterval(async () { const stats await pc.getStats(); stats.forEach(report { if (report.type inbound-rtp report.kind video) { console.log(丢包率:, report.packetsLost / report.packetsReceived); console.log(抖动:, report.jitter); } }); }, 2000);第三个方向是端到端的加密与安全。WebRTC强制使用SRTP加密媒体流DTLS加密数据通道这已经是比较高的安全基线。但在某些场景下可能还需要额外的应用层加密比如端到端加密的群聊。第四个方向是与现有业务系统的集成。比如把通话功能嵌入到客服系统、在线教育平台、远程医疗应用中这就涉及到与业务逻辑的深度耦合包括通话录制、屏幕共享、白板协作等扩展功能。我在实际项目中的体会是WebRTC的入门门槛其实不高官方文档和MDN上的示例足够让你跑通一个Demo。真正的挑战在于真实网络环境的复杂性和各种边界情况的处理。建议在入门阶段就把ICE Candidate缓存、网络状态监听、错误处理这些基础机制做扎实后面扩展到多人通话或者复杂场景时会省很多力气。另外一个小技巧调试WebRTC时Chrome的chrome://webrtc-internals页面非常有用它能看到完整的信令流程、ICE候选状态、媒体统计信息。遇到连接问题时先打开这个页面看一遍往往能快速定位到问题出在哪个环节。

相关新闻

WinCE TCP通信实战:从Winsock选型到心跳重连的避坑指南

WinCE TCP通信实战:从Winsock选型到心跳重连的避坑指南

简介:这份资源面向在Windows CE嵌入式平台上进行网络开发的工程师与学习者,提供基于Visual Studio 2005环境的TCP通信样例工程,帮助理解并掌握WinCE设备上的Winsock网络编程。包内共29个文件,以h头文件、cpp源文件、vcproj工程文件…

2026/10/14 18:29:51 阅读更多 →
STM32F407+FSMC+W5100S工业以太网稳定设计指南

STM32F407+FSMC+W5100S工业以太网稳定设计指南

简介:本资源是一套基于STM32F407VET6单片机与W5100S以太网模块的FSMC接口网络通信完整工程,面向嵌入式初学者及物联网硬件开发人员,解决STM32平台下高速以太网外设驱动与TCP协议栈应用落地难题。压缩包共143个文件,含56个C源码与5…

2026/10/14 18:29:51 阅读更多 →
Obsidian量子测试知识图谱:开源模板与实操指南

Obsidian量子测试知识图谱:开源模板与实操指南

1. 为什么是 Obsidian,以及量子测试场景的真实痛点1.1 量子测试不是普通软件测试先从一个很实际的问题说起:做量子计算相关的测试工作,到底有多少种资料要管?我自己入坑量子测试之前,以为它和普通软件测试差不太多——…

2026/10/14 18:29:51 阅读更多 →

最新新闻

aigc怎么降重?2026年10月亲测有效的降AI率教程,知网AIGC疑似度从90%多降到个位数!

aigc怎么降重?2026年10月亲测有效的降AI率教程,知网AIGC疑似度从90%多降到个位数!

辛辛苦苦写完的论文提交检测后AI率却特别高? 明明我们是自己写的,为啥会这样,只好到处去发帖子求降AI的小妙招,各种降AI率的方法都试了一遍,但是论文的AI率还是没降下去?一头雾水,也找不到有效…

2026/10/14 19:17:29 阅读更多 →
蒙特卡洛场景生成:新能源功率预测与风光出力不确定性分析实战

蒙特卡洛场景生成:新能源功率预测与风光出力不确定性分析实战

做新能源并网的人大概都有过这种体验:早上看气象预报,风速曲线很漂亮,功率预测模型也给出了一个看着很稳的出力曲线。结果下午风向一转,风电场实际出力直接打了七折。调度电话紧跟着就来了——你的预测偏差怎么这么大,…

2026/10/14 19:17:29 阅读更多 →
051、广播数据与扫描响应数据分工不明确引发的问题

051、广播数据与扫描响应数据分工不明确引发的问题

一个让我在客户现场熬到凌晨三点的坑 前年做一个低功耗传感器节点项目,设备需要在广播包里塞入传感器类型标识、电量等级、以及一个动态变化的状态标志位。扫描响应包里放设备名称和自定义的厂商自定义数据。 功能在实验室跑得好好的。到了现场,客户用他…

2026/10/14 19:17:29 阅读更多 →
VSCode C语言开发指南:大纲视图与代码导航效率提升全攻略

VSCode C语言开发指南:大纲视图与代码导航效率提升全攻略

1. 从“找不到函数”说起:大纲视图到底解决了什么问题先说一个我前几天遇到的场景。有位同事拿了一个老项目给我看,单个源文件有两千多行,他在里面找一个字符串处理的函数,愣是滚了五分钟没找着。我说你按一下 CtrlShiftO&#xf…

2026/10/14 19:17:29 阅读更多 →
有限差分法解二维热传导:Matlab显式与隐式格式从入门到避坑

有限差分法解二维热传导:Matlab显式与隐式格式从入门到避坑

简介:这份资源面向学习数值计算、偏微分方程求解的高校学生与工程技术人员,聚焦用Matlab实现有限差分法求解二维热传导问题。内容围绕热传导方程∂u/∂tα(∂u/∂x∂u/∂y)展开,涵盖网格划分、边界条件设定、温度场初始化、时间步长迭代更新&…

2026/10/14 19:17:29 阅读更多 →
为什么会计专业论文总被打回?90%的问题出在格式排版

为什么会计专业论文总被打回?90%的问题出在格式排版

带毕业论文这些年,我发现会计专业学生有个共同困惑:内容明明没问题,论文却一次次被导师打回,批注上写的全是"格式不规范"。有学生改了五稿,查重率只有8%,却因为参考文献格式、表格编号、页眉页脚…

2026/10/14 19:16:25 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →