WebRTC 屏幕共享不是视频,原因何在?

作者:Tsahi Levent-Levi
原文:https://bloggeek.me/screencasting-webrtc-different/

WebRTC 中的屏幕共享曾经很麻烦。现在只是变”棘手”了一些。要想让屏幕共享会话达到最佳质量和性能,有些事情是你必须了解的。

这些年来,我协助过不少团队把他们的屏幕共享实现做对。故事线几乎总是这样:团队先搭好一个 WebRTC 视频应用,然后在上面” bolted”上屏幕共享(用同一条管线、同一套编码器设置、同一套看板),之后开始疑惑为什么体验总是不对劲。文字模糊。幻灯片比演讲者落后整整一秒。只要有人共享一块 4K 显示器,CPU 就飙升。

屏幕共享不是视频。​ 它搭载同一个 RTCPeerConnection,它以 MediaStreamTrack 的形式出现,浏览器也乐于把它交给你,获取用的 API 看起来几乎和 getUserMedia 一模一样。但屏幕上出来的像素,与摄像头里出来的像素天差地别,如果你把它们同等对待,你交付的产品会悄悄让一半用户失望。

开箱即用的屏幕共享其实已经相当不错。​ 多年来,Google 在 Chrome 上为 Google Meet 的屏幕共享能力投入了相当大的资源。默认情况下,你几乎不用做什么就能得到一个不错的结果。但实际上,你还可以做得更好。

下面我来解释究竟有什么不同、这对 codec、码率 和监控意味着什么,以及你该怎么做。

核心要点

  • 屏幕内容是高分辨率、低运动量的,与摄像头正好相反。要优化的是清晰度,而不是帧率
  • 摄像头的码率预算对屏幕是错的:幻灯片切换时舍得花码率,不切换时几乎不花
  • 在屏幕共享轨道上把 contentHint 设为 “detail”
  • AV1 及其屏幕内容编码工具(SCC)能用少得多的比特把文字编码好。屏幕共享是个不错的 AV1 起点
  • 屏幕 + 摄像头同时存在时需要做决策:用一条还是两条 PeerConnection、要不要 simulcast,以及如何避免”镜子大厅”

屏幕内容不是摄像头内容

我先从”为什么”开始。为什么屏幕共享需要被区别对待,而不是和你”常规”的视频一样处理。

某种意义上有 4 类视频内容(大概还有更多,但这几类应该能帮你理清大部分问题):

  1. 视频会议。无聊的人头画面、真人、嘴唇在动,没别的了
  2. 屏幕共享。想象 PowerPoint 幻灯片、文字、偶尔穿插的图片,静态图像
  3. 动作片。大量运动、爆炸、追车、大量硬切和跳切
  4. 动画片。迪士尼动画电影,合成的

这些都是概括,规则也有例外。但它们代表了需要被编码的视频的不同特性集合。而在现实场景中,它们一开始就会得到不同的编码处理和不同的配置。

回到摄像头与屏幕的对比——摄像头产出的是柔和、模糊的主体上的窄带运动。而屏幕产出的是清晰的文字、锐利的边缘,以及大段完全静止的像素(那就是演讲者在讲、你努力不睡着的时候)。

视频编码器关于”下一帧会长什么样”的所有假设,在你把镜头对准一台显示器而不是摄像头的那一刻,就全部失效了。

关于屏幕共享,你需要记住这些事:

  1. 选定你的帧率,并让它适配所共享的内容类型。你需要把 CPU 和可用网络这样的约束考虑进去
  2. 你多半会在这里面对 4K 分辨率
  3. 即便你可能捕获了整块屏幕,观看者也大概率不会以全屏、以捕获时的精确分辨率来观看它

网络摄像头和显示器上的视频编码概念

视频编码器是一份标准规范的具体实现,这份规范声明了它可以用哪些工具,来生成视频解码器能够解码的码流。

这到底是什么意思?

在任意给定点上,视频编码器可以从它可用的一组工具中挑一个来据此编码。在图像或时间的某个特定部分多用一些码率预算,在其他地方少用一些。把图像分割成不同类型的宏块,做某些运动估计向量的搜索。

这会如何影响我们使用视频编码器的方式?见下文。

高分辨率、低运动量——与”会说话头像”正好相反

有些摄像头至今还不支持 HD,停留在 VGA 分辨率(大约 640×480)。很多较老的 HD 摄像头最高只到 720p(也就是 1280×720)。如今很多摄像头已经支持 1080p 全高清(也就是 1920×1080)。对我来说?720p 的视频通话完全够用。多方群聊中,每个参与者用 VGA 或更低分辨率也相当可以接受。30 帧每秒很好,但 15 fps 我也能接受。再低就觉得卡顿了。

用摄像头时,你面前还有一个人。我们的大脑几乎不费力地就能补全画面/视频流里包含的大量信息。所以通常你会希望把码率预算更多地花在更多帧上,而不是更清晰的帧上(帧数少一些)。

这里通常会有一些运动。但背景基本是静止不动的——只有一个说话的人头。了解内容类型,意味着能用合适的配置更好地校准编码器。

屏幕呢?我记得大约10 年前我换 PC 和笔记本,因为新买的显示器是 4K 分辨率。它抱怨了,而我觉得应该对那台”势利”的显示器好一点。说来话长。

今天呢?4K 显示器到处都是,笔记本上也有,几乎成了标配。5K 及更高的屏幕分辨率同样常见。

这是本网站访客的屏幕分辨率分布,来自 Google Analytics:

WebRTC 屏幕共享不是视频,原因何在?

一路 1080p 的摄像头画面被认为是高质量;1080p 显示器如今只是基线,更高分辨率也相当常见。运动特征也反了过来(长达数小时的静止画面,中间突然出现一次滚动或窗口移动),而你的编码器码率控制,恰好是为相反的问题调的。

我们想要什么?

对摄像头而言:稳定且相当高的帧率(15-30 fps),配上能适配其他所有参数的分辨率。我们宁愿在分辨率上稍微牺牲一点来换取更高 fps。你可以在这里看到一些例子。

对屏幕共享而言:我们通常直接选屏幕本身的分辨率,不做缩放,然后根据设备和网络能够达到的帧速率进行调整,低到 1 fps 甚至 0.5 fps 都可以。

为什么你的视频码率预算对屏幕是错的

摄像头会拿到一个码率预算去运作。对于高质量 HD 分辨率,它可以是 1.5-2 Mbps。也可以更低。低到 300 kbps,甚至比这还低。这里的编码器会参考带宽估计(也就是网络上可用的带宽)并把它当作自己编码预算的上限。它会时刻尝试顶到那个天花板。

当你演示一套幻灯片时,Chromium 和所用视频 codec 已经在做更聪明的事了:如果它们没有检测到变化,且帧间差值很小(200 字节),就会降低采集帧率,而只有当内容变化时才增长到 50-100 kb。这对静态内容来说,作为默认行为效果相当好。

真正懂屏幕的 codec:AV1 与屏幕内容编码

然后是视频 codec 本身。AV1自带屏幕内容编码工具(SCC),这些工具能用少得多的比特、以老 codec 无法做到的方式把文字正确地编码出来。

在内容大概率以文字为主时,提升屏幕共享质量的一个办法是切换到 AV1。也许不必全面切换,你可能想让摄像头编码器继续用别的视频 codec(直到你也一并迁移)。早期采用 AV1 的人正是这么做的:Google 把摄像头从 VP8 迁到 VP9,最后到 AV1,但在屏幕共享上一直沿用 VP8,只是最近才跳到 AV1。

这些都只是假设

显然,这些都只是假设和最佳实践,是那些通常管用的、久经考验的做法。

它会失效,比如当我们的目标是共享一段正在屏幕上播放的视频片段时。那种情况下,我们又回到了柔和模糊的物体,以及对运动而非细节/内容的更高偏好。

我们该怎么调和这些?

  1. 如果我们事先知道所共享的内容类型,那就直接做这个判断
  2. 我们可能想探测正在被共享的内容,并动态改变编码器的运行方式
  3. 或者我们可以直接问用户

我见过的多数服务都直接选方案(1),少数几家做方案(3),我不确定有没有人做方案(2),但也可能是我看漏了。

摄像头 vs 屏幕

下面是摄像头内容与屏幕内容的一份快速对比表,它涵盖了前面提到的所有要点:

摄像头屏幕内容
画面人脸、柔和边缘、一个”人头像”文字、锐利边缘、幻灯片和代码
分辨率VGA 到 1080p,必要时缩小1080p 及以上,以原生分辨率发送
帧率15-30 fps低至 0.5-1 fps,有东西动时再提高
运动特征持续且幅度小长段静止,然后突然滚动或切幻灯片
压力下牺牲什么分辨率帧率
码率CBR,填满带宽估计值VBR,变化时拉高,静止时接近零
Codec你视频用的那个尽可能用带屏幕内容编码的 AV1
contentHint“motion”“detail”(或 “text”)
例外情况摄像头并非”静止不动”共享屏幕上播放的视频片段:按运动处理

contentHint:所有人都会忘记的那一行 API

好。那么摄像头和屏幕是不同的,我们希望为各自配置不同的视频编码器——以及整条媒体处理管线。怎么做?

WebRTC 在 MediaStreamTrack 上给了我们一个参数 contentHint。它可以是 “motion”、”detail” 或 “text”。你已经知道我对屏幕共享的建议是什么了——要么 “detail”,要么 “text”。它们都偏向清晰度而非运动平滑度。我不确定谁在用 “text”,所以对大多数做屏幕共享的应用,最佳实践是设为 “detail”。你检查过自己的屏幕共享媒体流设置的是什么吗?你到底有没有认真设对?

于是我们有了两个现成选项:

  1. 用 motion,适合动态视频(比如共享一段 YouTube 视频)
  2. 用 detail/清晰度,适合演示

我想提出第三种做法。实践中没见过,但理论上可行:

你从其中一种方案开始,比如 detail/清晰度。对被共享的区域,在若干帧的时间跨度内采样随机像素(这可以用可插入媒体流来做)。如果大多数像素都变了,就切到 motion;如果大多数不变,就坚持 detail/清晰度——然后持续做这种动态采样。理论上,这能让你兼得两者之长。理论上而已。至少这是我为了提升任意内容类型的屏幕共享质量会做的一个实验。

另一个不错的想法是 Gustavo Garcia 几年前提出的,它用的是 WebRTC metrics,而不是试图去”看”图像。

你的屏幕共享要 simulcast 吗?

过去的标准做法是在屏幕共享时不做 simulcast。它吃掉的 CPU 太多。

现在情况依然如此,不过稍微讲究一点的话,还是可以管理并优化一些。

对于 1:1 不要 simulcast。你的摄像头和屏幕共享都不要。在大型群里,可以考虑 simulcast,用较高视频分辨率,同时并行发送它的四分之一(宽的一半、高的一半)。

如果你走这条路,请充分测试,并理解这么做的 CPU 成本。高分辨率对 CPU 和内存的压榨方式,是普通摄像头场景中不会遇到的。

单条 PeerConnection 还是两条

接下来是要决定 PeerConnection 的数量。

你可以把摄像头和屏幕共享都放进同一条 PeerConnection 里发送。它能工作。直到网络开始吃紧。那时候,哪一路视频流应该被优先保障?摄像头还是屏幕共享?浏览器内部的 WebRTC 库会不会做出你期望且想要的优先级排序?

用单条 PeerConnection 承载摄像头 + 屏幕共享,会遇到这些挑战:

  • 带宽分割:默认情况下,WebRTC 会在一条连接上的多条视频轨道之间平均分配可用带宽。例如,如果连接被限制在 1.5 Mbps,它会给摄像头分 750 kbps,给屏幕共享分 750 kbps。这很可能不是你想要的
  • 资源浪费:即便其中一路源是静态的(比如一套幻灯片)、并不需要完整的 750 kbps,浏览器往往也无法自动把”省下来”的带宽shift到另一条轨道上(摄像头),结果两路质量都变差
  • 你可以用 RTCRtpSender.setParameters() API 让其中一路优先于另一路,但效果仍不理想。更好,但依然不够好

用两条独立的 PeerConnection,两者会争抢可用带宽,但浏览器实现在这方面似乎就是比单条连接做得更好。别信我,信 Google,他们正是为这件事实现两条 PeerConnection 的。

Hall of Mirrors

这和麦克风—扬声器的反馈回路是类似的问题。站在扬声器前,把麦克风拿到生日聚会上,给你的耳朵来点特别的享受……所谓”镜子大厅”,是指用户共享了整块屏幕,而屏幕上又包含了他本人正在演示内容的本地画面,于是变成了一间镜子大厅。

我在以色列辅导12 年级的学生做他们最后一个编程项目。通常我会挑那些做视频会议方向的。今年有个孩子跟我说:”屏幕共享对我们来说已经能用了,但有件东西我们必须给你看看,它会把你的眉毛都震飞”(用希伯来语说出来非常好笑)。他们看到的就是这个——Hall of Mirrors。

WebRTC 屏幕共享不是视频,原因何在?

如果你不喜欢这个瑕疵(很多人不喜欢),那么有办法绕开(比如不要在正在被共享的那块屏幕上显示演讲者自己的画面)。请记住这一点,并给它应有的处理。

一定要读一下这篇相关的文章。

标签页、窗口还是显示器?三种界面,三套问题

我们到底在共享什么?

getDisplayMedia() 给用户一个选择器,包含三种 DisplaySurface 类型,每一种都有不同的分辨率、刷新率和隐私含义。把它们视为可互换的,正是无障碍 bug 和数据泄露的来源。

还要理解的是,你最终发送(或接收)的分辨率很可能会动态变化。如果共享的是一个标签页或窗口,用户可以在通话过程中对它调整大小。

系统音频、标签页音频,以及权限迷宫

共享屏幕时,音频这块有微妙之处。你希望设备上产生的音频也一并被共享吗?

在这里,调用 getDisplayMedia() 时可以加上 audio: true 表示希望获取音频。用户仍然可以选择不共享自己的音频。这在各浏览器上”还行”,但有几个需要注意的坑:

  • 先从行为层面的说起。你的应用想要共享音频,并不意味着用户同意了。所以要检查 getDisplayMedia() 的 promise 返回后,你的应用里实际被批准了什么
  • 用户的麦克风可能也在传输这里扬声器放出来的声音。设备发出的音频也可能包含通话另一端的音频。要把这些因素考虑进去,并在 various浏览器和操作系统上测试。在 Chrome 上有一些开关可以抑制它——systemAudio 的 “include”/”exclude” 决定共享屏幕时是否提供系统音频。windowAudio 的 “exclude”/”window”/”system” 对窗口做同样的事。suppressLocalAudioPlayback 会阻止被共享的标签页在本地播放,这样演讲者就不会听到两遍。restrictOwnAudio 是较新的选项,会把捕获页面自身的音频从被捕获内容中移除

你的 WebRTC 看板里缺失的屏幕共享 KPI

在多数情况下,你无从判断一路视频流是不是屏幕共享。WebRTC 里有一些线索,比如我们在 rtcStats 就用到了这类线索。

你可能想做(而且应该做)的事,是为媒体流引入应用层信号,标明它是屏幕共享还是摄像头——这样在需要调试或排障时,你可以查统计数据并更好地做关联分析。一路 3 fps 的视频流,对屏幕共享媒体流来说可能正是医生开的处方,对摄像头画面来说却可能指向严重问题。

给产品与工程的速查表

下面这张表把以上内容总结了一遍:

领域该怎么做多数团队现在的做法
Codec在屏幕轨道上用 AV1(设备扛得住的话),即便摄像头仍留在 VP8、VP9 或 H.264与摄像头用同一个 codec 和同一套设置
码率让它浮动:幻灯片切换和滚动时设高上限,屏幕静止时接近零。可以探索用两条 PeerConnection 的方案,每条视频流一条复用摄像头的码率预算
contentHint屏幕轨道设 “detail”,当用户共享视频片段时设 “motion”完全不设
表面选择知道它是标签页、窗口还是显示器。预期分辨率会在通话中变化。让演讲者自己的画面不出现在被共享的屏幕上三者一视同仁,假设备想分辨率固定不变
音频采集要求音频、检查是否真的拿到了轨道、没拿到时告知用户假定音频会跟着来,或者干脆跳过
监控为屏幕分享建立独立的指标:冻结率、发送分辨率 vs 采集分辨率、fps、编码器 CPU折进通用的视频质量看板里

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/72584.html

赞 (0)

相关推荐