# 清华教室的音视频路由

## 背景

最近在设计上课所用设备的音视频路由，借此机会梳理一下教室里现有的音视频路由，并记录一种可行的方案。

## 教室环境

教室里原有的音视频路由大致如下。先看信号源：

- 一体机的视频和音频输出
- 通过 HDMI 接入的笔记本视频和音频输出
- 话筒的音频输出
- 教室里的摄像头，通常包括板书、近景、远景、学生视角等机位

这些信号经过一个可在讲台上操控的导播台（下称「讲台」，实际设备未必位于讲台内部），可以输出到以下位置：

- 教室的音响
- 投影、返显以及学生头顶的显示器
- 一体机的音视频输入
- 通过 USB 接入的笔记本音视频输入

画成路由图大致如下。视频部分：

```
flowchart TD
    笔记本显示输出 -->|HDMI| 讲台1[讲台]
    一体机显示输出 --> 讲台1
    教室摄像头 --> 讲台1
    教室摄像头 --> 讲台2[讲台]

    讲台1 --> 投影
    讲台1 --> 返显
    讲台1 --> 显示器
    讲台2 --> 一体机视频输入
    讲台2 -->|USB| 笔记本视频输入
```

音频部分：

```
flowchart TD
    笔记本音频输出 -->|HDMI| 讲台
    一体机音频输出 --> 讲台
    话筒 --> 讲台

    讲台 --> 音响

    话筒 --> 讲台1[讲台]
    讲台1 --> 一体机音频输入
    讲台1 -->|USB| 笔记本音频输入
```

## 针对上课需求的设计

回到我的课程。我希望能在多个信号源之间方便地切换，包括 Mac 笔记本、鸿蒙电脑以及一台便携摄像头。讲台自带的导播功能不足以支撑这么复杂的切换，手上又没有 ATEM Mini 导播台（怀念以前学生节的日子），于是打算在 Mac 笔记本上用 [OBS](<https://obsproject.com/>) 做软件导播。

那么音视频路由该如何设计？下面是我最终采用的路由方式。视频部分：

```
flowchart TD
    鸿蒙电脑 -->|HDMI| 采集卡
    采集卡 -->|Type-C| Mac电脑
    便携摄像头 -->|USB| Mac电脑
    Mac电脑 -->|HDMI| 讲台
    Mac电脑 --> OBS直播或录像
    教室摄像头 --> 讲台1[讲台]
    讲台 --> 投影
    讲台 --> 返显
    讲台 --> 显示器
    讲台1 -->|USB| Mac电脑
```

音频部分：

```
flowchart TD
    鸿蒙电脑 -->|HDMI| 采集卡
    采集卡 -->|Type-C| Mac电脑
    便携摄像头 -->|USB| Mac电脑
    Mac电脑 -->|HDMI| 讲台
    Mac电脑 --> OBS直播或录像
    话筒 --> 讲台
    话筒 --> 讲台1[讲台]
    讲台 --> 音响
    讲台1 -->|USB| Mac电脑
```

这样，Mac 上的 OBS 就能获得来自鸿蒙电脑、Mac 自身屏幕、教室摄像头和话筒的音视频输入；再通过 OBS 的 Projector 把画面输出到扩展屏，经由讲台投到教室的各种投影和显示器上，音频则从音响放出来。之后要录像或直播，直接使用 OBS 自带的功能即可。

针对上课 4K 30 FPS 但是静态为主的场景，在 OBS 设置里，Output 选择 Simple，Video Bitrate 选 8000 Kbps，Video Encoder 选 Hardware (Apple, H.264)，下面 Recording Quality 选 Same as stream，Recording Format 选 Matroska Video (.mkv)。

可以打开 OBS 的 View -\> Stats，看看实时码率，有没有 missed or skipped frame。

### 基于 HDMI 分配器的候选方案

另一个候选方案是使用 HDMI 分配器：把展示用的鸿蒙电脑信号一分为二，一份直连讲台投出，另一份经采集卡进入 Mac 电脑的 OBS。此时视频拓扑变为：

```
flowchart TD
    鸿蒙电脑 -->|HDMI| 分配器[HDMI分配器]
    分配器 -->|HDMI| 采集卡
    采集卡 -->|Type-C| Mac电脑
    便携摄像头 -->|USB| Mac电脑
    分配器 -->|HDMI| 讲台
    Mac电脑 --> OBS直播或录像
    教室摄像头 --> 讲台1[讲台]
    讲台 --> 投影
    讲台 --> 返显
    讲台 --> 显示器
    讲台1 -->|USB| Mac电脑
```

这种设计把 OBS 放到了旁路，避开了采集卡可能出现的一些问题（后文会提到）；缺点是投出来的内容只能来自鸿蒙电脑，无法通过 OBS 二次加工。

## 设备选型

采集卡用的是绿联的 [UG307-95348 4K60Hz MS2130S 视频采集卡](<https://www.lulian.cn/product/1537.html>)，USB 名称是 UGREEN 95348，VID 0x2b89，PID 0x5348。

便携摄像头用的是：

- 绿联 [CM717-25442 2K USB 400W 像素摄像头](<https://www.lulian.cn/product/1815.html>)，USB 名称是 UGREEN Camera 2K，VID 0x0c45，PID 0x636f；
- 绿联 CM831-65381 4K USB 800W 像素摄像头，USB 名称是 UGREEN Camera 4K，VID 0xeba4，PID 0x6579。

HDMI 分配器用的是绿联的 [AP502-55493 4K60Hz 一进二出 HDMI 分配器](<https://www.lulian.cn/product/1527.html>)，输入规格为 5V/1A，支持一路 HDMI 输入、两路 HDMI 输出。从 EDID 来看，采用的是 [IT6664](<https://www.ite.com.tw/tw/product/cate1/IT6664>) 方案，可以通过拨码开关切换不同的模式：

- 1 上 2 上（默认）：当两路输出都接上时，输入侧看到的 EDID 由两路输出的 EDID 共同决定，设备显示 ITE-6664；只接一路输出时，输入侧直接透传该路的 EDID
- 1 上 2 下：强制 1080P60Hz，设备显示 UGREEN-FHD
- 1 下 2 上：强制 4K60Hz，设备显示 UGREEN-UHD
- 1 下 2 下：复制 OUT1 设备的 EDID，如果没有 OUT1，则 fallback 到 1080P60Hz

仅供参考，不构成购买建议。

## 遇到的实际问题

在使用绿联 UG307-95348 采集卡的过程中，还遇到并修复了一些清晰度和颜色问题，具体方法见 [修复绿联 UG307-95348 HDMI 采集卡清晰度与颜色问题](<https://jia.je/blog/posts/hardware/fix-ugreen-95348/index.md>)。

在使用绿联 AP502-55493 HDMI 分配器的过程中，也遇到了一些显示问题：在特定教室里，在默认的 1 上 2 上配置（自动计算 EDID）下，两路输出只有一路可以正常显示，比如 OUT1 接教室的投影是正常的，OUT2 接采集卡就黑屏。调整成 1 下 2 下，即复制 OUT1 设备的 EDID，把 OUT1 接教室的投影，把 OUT2 接上面的采集卡，这两路又都能正常工作。初步怀疑和 EDID 有关系，后续考虑把两路输出的 EDID 导出来对比一下，也看看 HDMI 分配器给输入侧暴露的 EDID 是怎么样的。目前抓取了一下输出侧的 EDID，有问题的教室的投影，最高支持 4K 60Hz 4:4:4，而采集卡和没问题的教室的投影，最高只支持 4K 60Hz 4:2:0，怀疑和这个有关。

音频方面也踩到了一些坑，而且都和立体声有关。

第一个坑：某个教室录出来的音频虽然标称双声道，但实际上只有左声道有声音，右声道几乎静音。

第二个坑：另一个教室录出来的音频同样是双声道，但右声道是左声道的反相，两个声道一旦叠加就会互相抵消。

上面两张图都是用 [stereo\_check.py](<https://jia.je/blog/posts/misc/stereo_check.py>) 绘制的。

和教室管理员沟通后发现，第二个坑对应的教室在更换 HDMI 线之后，现象就变得和第一个坑一样了，即只有左声道有声音、右声道静音。不过背后的原理，目前还没搞清楚。

此外，上课途中还遇到过突发情况：采集卡采集的视频出现闪屏和黑屏，不确定是采集卡的问题还是 HDMI 线的问题。下课后又无法复现，不知道是否和温度有关。

## 常用工具

ffmpeg 常用命令行：

```shell
# 截取视频中的一帧
ffmpeg -i source.mp4 -ss hh:mm:ss.xxx -frames:v 1 output.png
# 截取视频中的一部分，以左上角为坐标原点，x 轴向右，y 轴向下，从 (x,y) 到 (x+w, y+h)
# 用 https://ffmpeg.party/tools/cropper/ 辅助确定坐标
ffmpeg -i source.mp4 -vf "crop=w:h:x:y" output.mp4
# 原样保留视频
ffmpeg -i source.mp4 -c:v copy output.mp4
# 标准化视频中音频的响度
ffmpeg -i source.mp4 -af "loudnorm=I=-16:TP=-1.5:LRA=11" output.mp4
# 只保留双声道里的左声道
ffmpeg -i source.mp4 -af "pan=mono|c0=c0" output.mp4
# 只保留左声道的同时，标准化音频响度
ffmpeg -i source.mp4 -af "pan=mono|c0=c0,loudnorm=I=-16:dual_mono=true:TP=-1.5:LRA=11:print_format=summary" -ar 48k output.mp4
# 只保留左声道，标准化音频响度，重新用 libx264 CRF 22 编码
ffmpeg -i source.mp4 -c:v libx264 -preset slow -crf 22 -af "pan=mono|c0=c0,loudnorm=I=-16:dual_mono=true:TP=-1.5:LRA=11:print_format=summary" -ar 48k output.mp4
# 拼接两个输入视频，只保留左声道，标准化音频响度，重新用 libx264 CRF 22 编码
ffmpeg -i source1.mp4 -i source4.mp4 -filter_complex "[0:v:0][0:a:0][1:v:0][1:a:0]concat=n=2:v=1:a=1[outv][outa]; [outa]pan=mono|c0=c0,loudnorm=I=-16:dual_mono=true:TP=-1.5:LRA=11:print_format=summary[outa_final]" -map "[outv]" -map "[outa_final]" -c:v libx264 -preset slow -crf 22 -ar 48000 output.mp4
# 原样保留音频
ffmpeg -i source.mp4 -c:a copy output.mp4
# 测量前 10s 的音量大小
ffmpeg -i source.mp4 -af "volumedetect" -f null -t 10 -
```

LosslessCut：以关键帧的粒度，快速剪辑
