为什么再做一个视频通话软件
先承认:这个赛道已经非常拥挤了。腾讯会议、Zoom、飞书……但每次我只是想让两台设备临时通个话——比如帮远程的父母看看电脑出了什么问题、和另一台机器联调个东西——我面对的永远是同一套流程:
- 双方都去下载几百 MB 的安装包,注册登录;
- 建会议、发链接、等对方进房;
- 接受"本次通话内容将被收集处理"的隐私条款;
- 一个会议软件常驻后台,占掉大几百 MB 内存。
而真正本质的需求只有一句话:两台设备之间,建一条加密的音视频通道。这件事在 WebRTC 时代本不该这么重。
于是有了 mini-vedio——一个 约 12MB 的单个 exe,双击即用,无安装、无注册、无服务器、媒体全程端到端加密 的点对点音视频通话应用。

三种连接方式,覆盖从局域网到完全离线
视频通话最难的部分从来不是媒体传输(WebRTC 已经解决了),而是信令——双方如何交换建连所需的 SDP/ICE 信息。mini-vedio 用三级模式覆盖了几乎所有网络场景,且三种模式共用同一套 SignalMessage JSON 格式:
| 模式 | 场景 | 机制 |
|---|---|---|
| 局域网发现 | 同一 WiFi/路由器 | UDP 逐网卡定向广播心跳,自动列出"附近的设备",点击即呼,零配置 |
| 6 位房间码(默认) | 双方可上公共互联网 | 「创建新通话」生成房间码,微信发给对方,对方输入即通——走免注册的公共 MQTT 中继,不需要自己的服务器 |
| 邀请码(备用) | 公共中继不可达 / 完全离线 | 整包 SDP 压缩成一段文本,双方像换微信一样人工互换,纯带外传递 |

这个分层设计的关键在于优雅降级:公共中继建连失败时自动切换到邀请码模式;路由器开了 AP 隔离导致局域网发现失败时,会明确提示改用房间码或邀请码,而不是让用户面对一个含糊的"连接失败"。
对隐私敏感的场景,同一个 exe 还能一键变身为自建信令服务器(mini-vedio.exe signal),配合 cloudflared 隧道即可获得自己的公网信令地址——房间码照用,但中继完全自主可控。
架构:核心决策只有一个——媒体引擎不在 Go 里实现
先看整体架构:
|
|
技术栈:Go 1.26 + Wails v3 + Vue 3 + TypeScript + Vite + Tailwind CSS v4。前端构建产物 go:embed 内嵌进 exe,最终交付物就是那一个文件。
这个架构里最重要的一行字是:Go 不碰任何媒体数据。
摄像头/麦克风采集、H.264/VP8/Opus 编解码、回声消除(AEC3)、降噪、DTLS-SRTP 加密、ICE 穿透——全部直接使用 WebView2(Chromium)内置的 WebRTC 栈。Go 侧只承担窗口壳、局域网发现、信令转发和设置存储这些"控制面"工作。
这个决策带来一连串的收益:
- 不引入 Pion、不引入 FFmpeg、不引入任何编解码库,代码量和体积直接压到底;
- 白拿 Chromium 的回声消除与硬件编解码——自己用 Go 写媒体处理,很难追平 AEC3 的质量;
- 媒体加密由 WebRTC 规范强制保证(DTLS-SRTP 不可关闭),安全基线天生就在;
- 唯一的代价是依赖系统 WebView2 运行时——而 Win10/11 基本预装,启动时检测缺失会引导安装。
作为对照,一个常见的 Go WebRTC 思路是 Go + Pion 自管媒体,但 Pion 只管传输不管采集和编码,采集与编码需要 CGO/FFmpeg,体积与构建复杂度双双爆炸,还会丢掉系统级回声消除。这就是"不自己造媒体引擎"的论证过程。
目录结构
|
|
前后端的边界非常干净:前端 call.ts 维护一个 idle → connecting → connected → failed 的通话状态机,所有 RTCPeerConnection 的生命周期都在这里;Go 通过 Wails bindings 暴露 Peers/SendOffer/AcceptCall 等方法,用事件把局域网发现结果推回前端。
解决的痛点
痛点一:安装与注册的摩擦
单文件 exe、双击即用、系统自带 WebView2 即可运行。跨网通话的默认路径(房间码)走免注册的公共 MQTT 中继,前端 relay.ts 手写了 MQTT 3.1.1 报文 over WSS——不引任何 WebSocket/MQTT 库,为一个功能省下一整个依赖树。房间的设计上还有个细节:6 位房间码的首字符编码了所选 broker(A=emqx、B=hivemq),保证双方确定性落到同一台中继上,不会出现"都输入了房间码却在等对方"的分叉。
痛点二:Electron 式的体积臃肿
桌面应用 + Web 前端的常规答案是 Electron,代价是 150–200MB 的产物和数 GB 的 node_modules。mini-vedio 给自己定下硬约束:exe ≤ 13MB,并在构建脚本里内置体积断言,超了直接构建失败。每次发布的体积都有记录可查,防止悄悄膨胀:
| 里程碑 | exe 大小 |
|---|---|
| 基础版本(Wails + 前端 + LAN + 诊断日志) | 12.29 MB |
| PWA 化(manifest/SW/图标) | 12.31 MB |
| 房间码默认化(公共 MQTT 中继 + ICE 缓冲 + bye) | 12.46 MB |
体积控制没有黑科技,靠的是几条纪律:纯 Go 零 CGO、-ldflags "-s -w" -trimpath、能用标准库就不引库、前端依赖保持最小集。
痛点三:隐私——“我的通话到底被谁听到”
这是设计里最值得展开的部分。mini-vedio 的信任模型分两层:
媒体层:DTLS-SRTP 端到端加密,WebRTC 规范强制,媒体数据从一台设备直达另一台设备,任何中继都接触不到内容。
信令层:信令即使经过第三方(公共 MQTT broker)转发,也只是搬运 SDP/ICE 文本,被窃听也不破坏媒体机密性。但信令通道理论上存在中间人风险——于是在此之上加了一道 SAS 核对码:
|
|
通话界面常显 6 位核对码,双方口头比对一致后点"已核实"。如果信令被中间人劫持、双方各自与攻击者建立了连接,两边的指纹拼接结果必然不同,核对码对不上,阴谋当场败露。这和 Signal 通话的安全号码(safety numbers)是同一原理,但做到了 6 位字符的极简形态。加密由机器保证,身份由人眼保证——这是无服务器架构下很优雅的信任闭环。
另外一些硬规矩:CSP 严格限制页面只能加载内嵌资源、远程脚本一律禁止;发布版禁用 devtools;零遥测零统计;诊断日志结构化 JSONL 存本地且轮转,永不记录 SDP 正文、邀请码、密码或令牌。
痛点四:偶尔需要手机参与
电脑运行 mini-vedio.exe serve,同一 WiFi 下手机浏览器打开打印出的地址,加载后切到蜂窝流量即可跨网通话。前端已 PWA 化,手机浏览器"添加到主屏幕"即可全屏使用,也可以用 PWABuilder 打包成 APK 安装——一份前端代码,桌面/网页/安卓三端通用。
使用中的样子
进入通话后,除了音视频,还有两个高频能力:屏幕共享(给别人演示或排障的利器)和文字聊天(经 DataChannel 点对点传输,同样不经过任何服务器)。


通话界面里常显 SAS 核对码;通话布局支持大小画面互换、双击全屏;右侧聊天面板可拖拽调宽;摄像头关闭时显示占位头像而不是一屏黑帧——这些细节都来自真实使用中的不适。
写在最后
mini-vedio 不打算替代会议软件。它瞄准的是另一件事:让"临时通个话"回归它本来的复杂度——双击、给码、连接,三个动作,然后是一条端到端加密的通道。
回顾整个项目,最想分享的其实是两个决策方法论:
- 站在生态的肩膀上,别重复造轮子。Chromium 的 WebRTC 栈是地球上被锤炼得最狠的媒体引擎之一,白拿比自研划算得多。判断标准很简单:这个能力是否已经存在于系统/运行时里?在的话,别打包第二份。
- 给约束建账本。体积预算、依赖白名单、被否决的方案(Electron/Tauri/Pion……连同否决理由)全部写在项目的 AGENTS.md 里,每次改动前对照自检。约束不写下来,就一定会被慢慢稀释——体积记录表和构建断言就是防回归的堤坝。
项目完全开源,推送 v* 标签即触发 CI 自动构建发布。如果你也偶尔需要在两台设备之间"就通个话",欢迎试试。