Peering

Peering / 博客

Peering 如何把 Mac 屏幕串流到你的 iPhone

远程桌面是一路有着特殊要求的视频流:观看的人同时也是操控的人。下面的每个设计选择,都是为了让这个回路尽可能短。

捕获:向系统索取画面

Mac 应用使用 Apple 的 ScreenCaptureKit 来获取帧。来源可以是整个显示器、某个应用的所有窗口,或单个窗口, 你可以在它们之间切换而不会断开连接。这也是 macOS 第一次会请求“屏幕录制”权限的原因: 没有它,Mac 只能交出一幅黑屏。

一台打开了代码编辑器的 Mac 桌面,正实时串流到运行 Peering 的 iPhone。
手机上的画面是一路编码后的视频流,而不是一连串截图。

编码:低延迟,不重排序

帧会交给 Mac 上 Apple 的 VideoToolbox 编码器,编码为 H.264 或 HEVC,取决于手机声明自己能解码哪一种。 这些设置与流媒体服务所用的恰好相反:

解码器的设置信息(参数集)只作为一条独立的消息发送一次,不会在每一帧中重复。 中途加入的手机会先收到这条消息,然后是一个关键帧。

发送:帧可以丢弃,关键帧受保护

每一帧都带有一个 25 字节的小型头部:是否为关键帧、像素尺寸和呈现时间。在经中继或直连的连接上, 它会被切成约 1,080 字节的数据报,每个数据报都用 ChaCha20-Poly1305 加密,密钥只有这两台设备知道。 每个数据报中的计数器会送入一个 64 项的重放窗口,所以被截获的数据包无法在之后被重放。

拥塞时的规则很直接:视频可以丢弃,关键帧不可以。如果新的一帧已准备好,而旧的一帧还在等待发出, 旧帧就会被替换,但尚未送达的关键帧永远不会被普通帧替换。显示屏幕此刻的样子, 比显示曾经存在过的每一帧更重要。

在家庭网络中,画面改走它自己的 TCP 连接,那里的网络足够可靠,同样的规则很少需要触发。 每个套接字都设置了 TCP_NODELAY。默认把小写入批量合并的行为,每一跳都会增加几十毫秒, 这在下载时察觉不到,在你移动光标时却非常明显。

恢复:用画面的损失来换,而不是用延迟来换

UDP 不会重传,所以丢失一个数据报就意味着一帧受损。手机不会等它,而是继续解码, 损坏会在下一个关键帧时消除,最多相隔两秒。如果你在 Wi-Fi 不好的时候注意到画面有拖影, 看到的就是这个。暂停再恢复画面,会立即强制生成一个关键帧。

如果快速路径完全停止传送,串流就会以较低的码率退回到控制连接, 正如连接那篇文章所述,并在快速路径恢复后切回。

显示:适配手机,而不是适配 Mac

手机会选择一个质量档位,也可以请求更少的每秒帧数,Mac 会把它限制在合理的范围内。 应用可以让整个桌面适配屏幕,也可以填满屏幕,甚至可以请 Mac 提供一个形状与手机相同的虚拟显示器, 这样横向的显示器就不会被挤进竖向的屏幕。点击画面的操作则沿相反方向传送, 这是一次轻点如何变成一次点击要讲的内容。

常见问题

Peering 使用什么视频编解码器?

H.264 或 HEVC,由 Mac 通过 VideoToolbox 编码,没有 B 帧,并采用低延迟设置。iPhone 会告诉 Mac 它能解码哪一种。

为什么画面有时会短暂拖影?

丢失的网络数据包会让一帧受损,画面会在下一个关键帧时恢复,关键帧至少每两秒出现一次。Peering 宁可短暂拖影,也不等待重传。