搭建云上自修室

我平时用一套基于 Typst 的 Zettelkasten 系统记笔记管理知识库某天突发奇想能不能直播自习过程一方面当作对自己的监督一方面分享一下日常的编码工作流平台选了 B 站技术栈就是我日常在用的这套 wiki 系统

听起来很简单但真正开始做的时候才发现问题远比预期多——隐私推流方式音频同步每一个都折腾了好一阵

隐私Stub 而非删除

第一个问题是隐私我的 wiki 里有不少私人笔记直播时不能让观众看到最直觉的做法是把敏感笔记删掉或者过滤掉引用但这在 Zettelkasten 系统里行不通——笔记之间有大量 @timestamp@timestamp 交叉引用删除一个笔记就会导致其他笔记编译报错而清理引用又会改动大量文件merge 回主分支时冲突成灾

最终我选了一个更聪明的方案stub空壳替换敏感笔记不删除而是替换成只保留文件名和 Typst label 的空壳


            
/* hidden */

            
#import "../include.typ": *

            
#show: zettel

            


            
= [Hidden] <2602xxxxxx>

            
/* hidden */

            
#import "../include.typ": *

            
#show: zettel

            


            
= [Hidden] <2602xxxxxx>

            
/* hidden */

            
#import "../include.typ": *

            
#show: zettel

            


            
= [Hidden] <2602xxxxxx>

            
/* hidden */

            
#import "../include.typ": *

            
#show: zettel

            


            
= [Hidden] <2602xxxxxx>

这样其他笔记中的引用仍然能正常解析编译不报错同时其他笔记文件完全没有改动merge 零冲突

具体实现上我写了一个 zk_delete_by_tag.pyzk_delete_by_tag.py 脚本支持三种模式--stub--stub 将匹配笔记替换为空壳直播分支专用--delete--delete 永久删除主分支用--restore-stubs--restore-stubs 在合并时将 stub 还原为真实内容配合一个 post-checkoutpost-checkout Git Hook切到 streamstream 分支时自动 stub 并提交切回 mainmain 时自动合并且过滤掉 stub 文件整个流程是mainmain 永远是 source of truthstreamstream 是纯派生分支两个分支无冲突地双向同步

推流iPad 副屏方案

解决了隐私问题下一步是怎么推流一开始我调研了各种 macOS 上的录屏/直播工具 (OBS哔哩哔哩客户端等)结果发现要么延迟太高要么对我的编码环境干扰太大我对直播工具的核心诉求是不能影响编码体验MacOS 这边的性能必须优先保障

试了几轮之后换了个完全不同的思路——不在 Mac 上推流而是用 iPad这就涉及到将 Mac 的屏幕内容和系统音频同步到 iPad 上让 iPad 负责推流这个方案的好处是Mac 只需要专注于编码推流的性能压力完全转嫁给 iPad直播工具对 Mac 的干扰降到最低

HDMI + Orion/Genki Studio性能与显示效果不佳

最早调研的是物理采集方案Mac 通过 HDMI 线接一块 Genki Studio 采集卡采集卡插进 iPad 的 Type-C 口iPad 通过 iPadOS 17 原生支持的 UVC 协议把采集卡识别成摄像头再用 Orion 在 iPad 上把 Mac 的屏幕调出来哔哩哔哩客户端对 iPad 屏幕录屏推流理论上 Mac 性能损耗为零

硬件接好打开 Orion画面出来了但终端的颜色全错了原因在于 USB 视频采集的 YUV 色彩范围映射出现偏差采集卡默认使用 MPEG 有限范围而非 Full 范围导致精心调校的终端配色全部被压缩变暗白色变灰对比度整体塌陷这个问题不在软件层面是硬件采集链路的固有缺陷调整 Orion 的显示参数也只是治标对于一个全天候盯着终端写代码的直播来说如果观众看到的画面和我实际在用的完全不是同一个东西这一定是不可接受的这个方案就这样被淘汰了

SideCar + 哔哩哔哩客户端最终方案

最终生效的思路方案很简单通过 SideCar 把 iPad 变成 Mac 的副屏然后在 iPad 上用哔哩哔哩客户端直接推流这样 Mac 只需要负责编码推流的压力全部转嫁给 iPad视频部分就这么解决了

顺便直播时需要展示当前播放的音乐但不能在编码屏幕上加 overlay 干扰写代码 (客户端也不能做到在推流时加 overlay)我直接用 Simple-Bar 的 Now Playing 功能在副屏顶部的状态栏里显示歌曲信息观众看得到我写代码不受影响

还有一个小细节直播过程中必须禁掉一切通知弹窗macOS 自带的“专注模式”可以做到这一点设置一个自定义模式直播时自动开启就行

音频同步

视频搞定了音频反而成了最大的坑我的核心诉求是一边写代码一边听音乐直播时观众也能听到同样的音乐并且在我的 Simple-Bar 上显示音乐这个需求看似简单但版权限制和技术实现都带来了不小的挑战

实现这个的途径基本上是两个方案

  • iPad to Mac在 iPad 上播放音乐通过某种方式把正在播放的歌曲信息和音频同步到 Mac 上哔哩哔哩客户端直接内录 iPad 的音频输出难点在于同步音频信息的实现
  • Mac to iPad在 Mac 上播放音乐通过某种方式把音频推送到 iPad 上哔哩哔哩客户端内录 iPad 的音频输出难点在于音频传播的性能和稳定性

Last.fm版权墙

第一个尝试是在 iPad 上播放音乐通过 Last.fm Scrobble 接口轮询 API在 Mac 端的 Simple-Bar 上显示正在播放的歌曲这个想法很直观做起来也不难但 iPad 上直播时B 站客户端会限制音频来源——版权限制导致音乐根本播不出声虽然这个方案理论上是最为优雅的但最终还是落不了地 (我不觉得我有本事 Hack 苹果录屏的限制)遗憾离场

网易云音乐API 延迟

我平时用的是网易有一个 NeteaseCloudMusicApi 专门逆向了网易云的接口理论上可以通过它获得账户正在播放的音频这样便可以通过轮询 API 的方式来获得当前网易云音乐播放的歌曲信息和音频数据这在某种意义下就是 Last.fm 方案的网易云版本

然而网易云版本并不存在当前播放音乐API只有最近播放音乐API这就导致了一个问题API 返回的歌曲信息和实际正在播放的歌曲之间会有明显的延迟甚至可能出现不同步的情况对于直播来说这种延迟是不可接受的因为观众会感觉到音乐和视频不同步影响观看体验因此这个方案也被迫放弃了

AirFoil性能拉胯

既然 iPad 上播不了音乐那就反过来在 Mac 上播放AirFoil 把音频推送到 iPad配合 MacOS 的 Midi 设置把 AirFoil 设为系统默认输出版权问题确实绕过了但 AirFoil 的性能太差音频延迟明显而且占用不少系统资源影响编码体验不可接受1

FFmpeg + RTP性能与稳定性拉胯

AirFoil 的失败证明 GUI 工具在这个场景下走不通作为一名终端玩家下一步想法很自然——上 FFmpeg思路是BlackHole 把 Mac 的系统音频截获再用 FFmpeg 读取 BlackHole 的输出以 RTP 协议直接推送到 iPad 的 IP 地址iPad 端用 VLC 接收播放纯命令行零 GUI 开销M3 芯片的硬件音频编码器直接上理论上优雅至极

调了一个下午 FFmpeg 参数解决了 avfoundationavfoundation 输入的采样率协商问题RTP 包大小和 VLC 的抖动缓冲区也都手动调过但现实很骨感RTP over UDP 在 Wi-Fi 和基于数据线的局域网组网下本质不稳定网络抖动直接导致缓冲区欠载爆音断续比 AirFoil 还难听换成走本地 MediaMTX 做 RTMP 中继再转 iPad增加了一层复杂度延迟和断续反而增加了CLI 再帅稳不住就是稳不住最终忍痛放弃了这个我最想让它成功的方案

SonoBus + BlackHole最终方案

最终找到了 SonoBus——一个开源的低延迟音频传输工具架构如下

  • 用 BlackHole虚拟音频设备把 Mac 的系统音频路由到 SonoBus
  • SonoBus 在 Mac 和 iPad 之间同步音频
  • iPad 端接收到音频后哔哩哔哩客户端直接推流出去

性能不错延迟可以接受为了进一步压低延迟我还折腾了 USB 直连方案用一个 USB 网卡加上 macOS 互联网共享让 iPad 通过 USB 连接获取网络相对于使用校园网共享 Opus 音频流效果更好不过 macOS 的互联网共享 GUI 会拒绝 802.1X 网络作为共享源很遗憾我们的 Tsinghua-SecureTsinghua-Secure 就赫然在此只能手动配置 pfctlpfctldnsmasqdnsmasq折腾完之后延迟降到 10ms 以内丢包率低于 0.1%直播中音频稳定性不错最后拜托 Claude Code 将其脚本化实现一键启动和监控

同时我自己写代码还得听音乐所以需要 AirPods 作为监听设备macOS 的多输出设备功能可以同时输出到 SonoBus 和 AirPods我给推流端的音质稍微降了一点反正观众听不出来会经过 B 站压缩AirPods 端保持原始音质

最早建立时 SonoBus 推流时有沙沙的背景杂音和电流声排查下来根因是 Wi-Fi 传输抖动导致 SonoBus 缓冲区欠载不是音质本身的问题切到 USB 直连方案后噪音根本消除另外还有一个关键点Mac 和 iPad 两端的采样率必须一致都设为 48 kHziPad 端有时候采样率会莫名降到 24 kHz需要手动确认并调整 (重启 App 能解决)调整好之后音质就完全没问题了2

各司其职

最终的架构是这样的Mac 专注编码iPad 专注推流视频走 SideCar 副屏音频走 SonoBus + BlackHole 同步AirPods 负责监听Simple-Bar 显示正在播放的音乐post-checkoutpost-checkout Hook 自动管理隐私整套方案不需要在 Mac 上安装任何重量级的直播软件编码环境的性能完全不受影响这其实很像是 Unix 哲学在直播领域的一个应用优秀的软件们各司其职组合成强大系统

  1. 1我还在淘宝上花了几块钱购买 AirFoil 的授权最终还是放弃了
  2. 2当然 Opus 编码的压缩损失是不可避免的但在这个场景下完全可以接受

Back to all articles

Loading comments...