Featured image of post mini-vedio:一个 12MB 的单文件加密视频通话应用

mini-vedio:一个 12MB 的单文件加密视频通话应用

用 Go + Wails v3 + Vue3 打造双击即用的 WebRTC 端到端加密音视频通话:局域网自动发现、6 位房间码跨网直连、SAS 防中间人——全程不需要自己的服务器。

为什么再做一个视频通话软件

先承认:这个赛道已经非常拥挤了。腾讯会议、Zoom、飞书……但每次我只是想让两台设备临时通个话——比如帮远程的父母看看电脑出了什么问题、和另一台机器联调个东西——我面对的永远是同一套流程:

  1. 双方都去下载几百 MB 的安装包,注册登录;
  2. 建会议、发链接、等对方进房;
  3. 接受"本次通话内容将被收集处理"的隐私条款;
  4. 一个会议软件常驻后台,占掉大几百 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 里实现

先看整体架构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
┌────────────────── 单个 exeGo + Wails v3)──────────────────┐
   WebView2 窗口                     Go 内核                   
  ┌──────────────────────────┐      ┌──────────────────────┐  
   前端(Vue3, 内嵌打包产物)        LAN 发现(internal/lan)  
    getUserMedia 采集/预览   │◄────►│ 信令中继 + 事件推送      
    RTCPeerConnection        绑定  设备身份 / SAS        
    房间码/邀请码/聊天/共享          serve/signal 子命令     
  └────────────┬─────────────┘      └──────────┬───────────┘  
                                                            
        媒体:DTLS-SRTP 端到端          信令:只搬运 SDP 文本    
└───────────────┼───────────────────────────────┼──────────────┘
                                               
         对端 WebView2 ◄──── P2P 媒体直连 ────► 对端(信令通道)

技术栈: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,体积与构建复杂度双双爆炸,还会丢掉系统级回声消除。这就是"不自己造媒体引擎"的论证过程。

目录结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
mini-vedio/
├── main.go              # Wails 入口:窗口、权限、serve 子命令分发
├── lanservice.go        # 绑定服务:Peers/SendOffer/AcceptCall + 事件转发
├── signal.go            # signal 子命令:WSS 房间码中继(自建信令)
├── serve.go             # serve 子命令:自签 HTTPS,手机浏览器入口
├── diagnostics.go       # 结构化诊断日志(JSONL,2MB 轮转)
├── device.go            # 设备标识持久化
├── internal/lan/        # UDP 广播发现 + 信令中继(纯标准库)
├── frontend/src/
   ├── components/      # VideoRoom / ChatPanel / IncomingCall …
   └── services/        # call.ts 状态机 / relay.ts / signal.ts / sas.ts
├── bindings/            # wails3 generate bindings 产物
└── scripts/build.ps1    # 一键构建 + 体积断言

前后端的边界非常干净:前端 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 核对码

1
sha256(字典序拼接的双方 DTLS 证书指纹) → 前 3 字节 → 显示为 ABC-123

通话界面常显 6 位核对码,双方口头比对一致后点"已核实"。如果信令被中间人劫持、双方各自与攻击者建立了连接,两边的指纹拼接结果必然不同,核对码对不上,阴谋当场败露。这和 Signal 通话的安全号码(safety numbers)是同一原理,但做到了 6 位字符的极简形态。加密由机器保证,身份由人眼保证——这是无服务器架构下很优雅的信任闭环。

另外一些硬规矩:CSP 严格限制页面只能加载内嵌资源、远程脚本一律禁止;发布版禁用 devtools;零遥测零统计;诊断日志结构化 JSONL 存本地且轮转,永不记录 SDP 正文、邀请码、密码或令牌

痛点四:偶尔需要手机参与

电脑运行 mini-vedio.exe serve,同一 WiFi 下手机浏览器打开打印出的地址,加载后切到蜂窝流量即可跨网通话。前端已 PWA 化,手机浏览器"添加到主屏幕"即可全屏使用,也可以用 PWABuilder 打包成 APK 安装——一份前端代码,桌面/网页/安卓三端通用。

使用中的样子

进入通话后,除了音视频,还有两个高频能力:屏幕共享(给别人演示或排障的利器)和文字聊天(经 DataChannel 点对点传输,同样不经过任何服务器)。

屏幕共享

聊天消息

通话界面里常显 SAS 核对码;通话布局支持大小画面互换、双击全屏;右侧聊天面板可拖拽调宽;摄像头关闭时显示占位头像而不是一屏黑帧——这些细节都来自真实使用中的不适。

写在最后

mini-vedio 不打算替代会议软件。它瞄准的是另一件事:让"临时通个话"回归它本来的复杂度——双击、给码、连接,三个动作,然后是一条端到端加密的通道。

回顾整个项目,最想分享的其实是两个决策方法论:

  1. 站在生态的肩膀上,别重复造轮子。Chromium 的 WebRTC 栈是地球上被锤炼得最狠的媒体引擎之一,白拿比自研划算得多。判断标准很简单:这个能力是否已经存在于系统/运行时里?在的话,别打包第二份。
  2. 给约束建账本。体积预算、依赖白名单、被否决的方案(Electron/Tauri/Pion……连同否决理由)全部写在项目的 AGENTS.md 里,每次改动前对照自检。约束不写下来,就一定会被慢慢稀释——体积记录表和构建断言就是防回归的堤坝。

项目完全开源,推送 v* 标签即触发 CI 自动构建发布。如果你也偶尔需要在两台设备之间"就通个话",欢迎试试。

Licensed under CC BY-NC-SA 4.0
Last updated on Sep 14, 2026 23:28 CST
本站总字数:193.5k 字
载入天数...载入时分秒... ·