WebRTC服务器设计小结

引言

上一篇我们分析了如何设计一套RTC PaaS服务?,从整体的角度讲述了设计一套完整的RTC PaaS服务需要的功能和服务。这一篇我们来侃一侃对服务器设计细节的思考。

首先还是重申一下我们面临的问题和挑战:

  • 网络延时(Latency/Delay)
  • 网络丢包(Packect Loss)
  • 网络乱序(Out-of-order delivery)
  • 网络抖动(Jitter/Delay variations)
  • 网络拥塞(Bandwidth Overuse/network congestion)
  • 服务的稳定性/高可用(service reliability/High Availability)
  • 服务的高并发(High Concurrency)
  • 服务的高性能(High Performance) 抛开后三项通用服务器的设计目标,前面几项一直是设计优秀的流媒体服务器,保证良好的音视频质量所必须兼顾的因素。下面很多设计的出发点和切入都与之息息相关。

调度服务器

我们以Improving Scale and Media Quality with Cascading SFUs (Boris Grozev)这篇文章作为切入点来讲一讲为什么要有调度服务器?以及调度服务器的主要职能和背后的思考。

正如文章中所讲,RTC整个系统的设计不仅要支持多个媒体服务器的灵活扩展,还要为每个入会用户提供最优的网络链路和媒体延时。因为实时通信应用对诸如带宽、延时、抖动和丢包率等网络状况非常敏感,更低的码率决定了更糟糕的视频质量,更长的网络延时意味着更长的音视频端到端延时,而网络丢包则往往会导致音频的断断续续和视频卡顿。这就是调度服务器存在的价值,为会议中的用户选择最优的网络链路,用于媒体传输。

首先,调度服务器需要管理所有的媒体服务器,需要知道媒体服务器的外网IP、服务端口、内网IP、地理位置、资源最大载荷、实时资源负载等信息,以便调度能够根据用户的信息分配最优的媒体服务器。注册+定时心跳很容易实现上述的需求,同时也比较好地支持媒体服务器(分布式SFU)的水平扩展。那么如何为用户分配最优的媒体服务器呢?

如果仅仅是按照“同一个会议的所有用户分配到同一台媒体服务器上”的策略,显然在跨国跨地域跨运营商的场景下无法实现网络链路和媒体延时的最优解。如下图所示,为星状拓扑,所有终端连接到同一台中心媒体服务器上,彼此推拉流,这种策略和中心服务器转发的视频会议架构对于用户的体验是很糟糕的,因为:

  • 用户远距离接入,尤其是跨国、跨地区时,传输链路质量没有保障。
  • 国内用户跨运营商之间接入,网络抖动大,丢包率高,影响会议质量。
  • 因CPU、内存和带宽等资源有限,单点媒体服务器容量和负载受限。 那么如何解决上述的问题?构建一个具备智能调度的实时传输网络呢?

一般情形下,整体原则如下:

  • 分区域/分运营商部署媒体服务器,用户通过接入服务实现就近接入,保障了最后一公里的质量。
  • 灵活/按需部署路由节点,通过路由分配媒体服务,能够根据实时网络质量选择最优的传输路径。
  • 需要保障传输网络内部的数据传输质量,如媒体服务器之间选择同VPC局域网或者不同VPC专线互联,云厂商保证其链路稳定性和可靠性。 实际拓扑结构如下图所示: 在媒体节点都选择三线机房BGP的前提下,常用的调度策略如下:
  • 同一个房间/会议同一地域优先分配到同一个媒体节点,避免服务器级联
  • 按照地域优先的原则,同一区划的用户优先就近分配。
  • 按照负载优先的原则,要选择符合条件中负载最小的。

一般按照用户的分布去选点和部署,然后制定一定的区划策略(需要实际的数据收集来支撑)。比如在中国部署四个点:华北、华南、华东、西南,亚洲除中国以外都分配到新加坡的节点,欧洲都分配到法兰克福的节点等。

对于没有节点覆盖的地域用户,理论上在大池子里分配负载最低的节点。

其次,调度服务器需要支持最短路径智能路由选择和辅助实现媒体服务器级联。因为上面按照用户信息就近分配最优的节点,解决了最后一公里的问题,而媒体节点之间的路径不仅受节点健康状况和实际网络链路的影响,如服务crash、机房瘫痪、光纤挖断等,还受物理上网络拓扑的限制。如果节点的部署选择了阿里的云企业网,则不需考虑动态路由选择问题,交由云厂商去保证即可。

对于媒体服务器级联的实现,可以参考如何使用开源SFU构建RTC云服务 中的思路,另外mediasoup的级联实现可参考 Broadcasting of stream from One to Many using multiple Mediasoup Server instances

媒体服务器SFU

对于SFU来说,基本功能是媒体包的转发和QOS保障,重点还是如何在复杂的公网环境下,利用UDP实时传输音视频数据,为用户提供低延时、清晰、流畅的音视频互动体验。

大的方向上,QOS可以从两个角度来思考和发散:

  • 信道编码的角度,如常见的ARQ/Nack丢包重传,FEC,Congestion Control(REMB/TransportCC),Jitter Buffer,IDR Request等。
  • 信源编码的角度,如可分层编码SVC,大小流Simulast,Long-Term Reference (LTR) frames等。

其中常用的QOS手段介绍可参考如下资源:

从入门到进阶|如何基于WebRTC搭建一个视频会议

聊聊WebRTC网关服务器4:QoS方案分析

webrtc视频QOS方法汇总

webrtc音频QOS方法汇总

WebRTC Qos优化杂记

在这里,不想过多地罗列名词解释和定义,感兴趣的小伙伴可参考上述资源自行查阅它们的不同和适用场景。

而对于很多QOS保障的设计手段和思想都是来自于TCP和Voip(Voice over IP)的传承,所以对于一些关键词和概念的理解可查阅借鉴对应领域的资源。

比如对于拥塞控制理论的学习,离不开对TCP 拥塞控制理论和算法的研究,可参考资源:

TCP拥塞控制图解(不包括RTO,因为它太简单了)

来自Google的TCP BBR拥塞控制算法解析

30张图解: TCP 重传、滑动窗口、流量控制、拥塞控制发愁

万字长文|全(小区局域)网最强TCP/IP拥塞控制总结…

万字详文:TCP 拥塞控制详解

TCP 的那些事儿(上)

TCP 的那些事儿(下)

WebRTC GCC拥塞控制算法详解

WebRTC基于GCC的拥塞控制(上) - 算法分析

WebRTC基于GCC的拥塞控制(下) - 实现分析

WebRTC基于TransportCC和Trendline Filter的发送端码率估计(Sendside-BWE)

实时视频传输中的BBR拥塞控

What is RMCAT congestion control, and how will it affect WebRTC?

实时视频流的拥塞控制-NADA,GCC,SCReAM

Bug: webrtc:9883 Removes unused BBR congestion controller

前一段刚接触Long-Term Reference (LTR) frames,感觉是一个好东西,虽然还没有机会实践,不过已知的好处:

避免频繁请求关键帧,减小网络冲击,有助于弱网对抗和缓解网络拥塞。

所以还是要安利给大家,详细的介绍可参考:

从入门到进阶|如何基于WebRTC搭建一个视频会议

视频编码长参考帧(LTR)

Improve Encoding Efficiency and Video Quality with Adaptive LTR

Cisco ClearPath Whitepaper

下面谈谈我的几点思考:

  1. 端到端/全链路丢包率 vs 上下行分段丢包率

原生的WebRTC是基于P2P场景设计的,QOS的策略也是端到端之间生效的,接收端发现丢包后,才会向发送端发送Nack请求重传,这种如果全链路的路径(RTT)过长,影响数据重传和恢复的效率。

如上图所示,在SFU架构下,重传请求不再是全链路反馈,而是在客户端和服务器之间分段进行。一方面,服务器具备NACK请求的能力,一旦感知到上行链路的丢包,及时向发送端请求重传。另一方面,服务器能够及时响应接收端的NACK请求。丢包重传的效率提升,有助于减少端到端延时,改善音视频体验。

从实现细节上,一方面可以选择不变Seq,接收到发送端的RTP包,修改ssrc,就直接转发给接收端,这样存在一个严重的问题,链路的下游感知到的丢包都是个累积丢包,即某个用户下行丢包了,有可能是真是用户与其就近接入的SFU节点之间下行网络很差,也有可能是链路的上游引入的丢包,如推流端的上行丢包或者媒体服务器和媒体服务器级联转发丢包。这样如果推流端上行很差,即使下游感知到了丢包,并发送Nack请求重传,也是要而不得的,这种无效的重传请求是非常低效的。 这种情况下接收端计算获得的丢包率是端到端/全链路的丢包率。

上面的设计显然是糟糕的,改进的手段就是经过SFU媒体节点转发时要区分original Seq和重新打上的新的Seq,original Seq可以用于计算全链路端到端的丢包率,而每一段重新打上的Seq用于计算该段下行的丢包率,这样只有在各自链路的每一段下行丢包,才会触发丢包重传请求,从而提升数据重传和恢复的效率。

  1. 端到端/全链路RTT vs 上下行分段RTT

首先我们参考Voice over IP End-to-End Delay Measurements的论述先大致展示一下端到端/全链路的延时有哪些:

当我们评估实时通信系统的性能和质量的时候,端到端延时是一个很重要的数据和参考标准。

对于客户端来说,由于需要决定渲染前的等待延时,全链路端到端的延时统计显的尤为重要。而对于SFU Nack重传控制也需要准确的RTT值,不过这个RTT值是上下行分段RTT值,对于分段RTT的计算可以采用RTCP-XR的方案。

  1. SFU需要对RTP进行reorder吗?需要Jitter Buffer吗?

这个问题只要想清楚Jitter Buffer的作用就可以了。顾名思义,抖动缓冲,通过在接收端维护一个数据缓冲区,来对抗一定程度的网络抖动,丢包和乱序,本质还是为了解码的丝滑,为了减少网络乱序和抖动对解码的影响。

SFU媒体节点只对音视频媒体包进行转发,不进行重新编解码,所以没必要进行缓存、重排,更不需要设计Jitter Buffer,因为一方面缓存会引入不必要的延时;另一方面用户侧在拉流播放解码渲染之前会对网络接收的媒体包进行缓存和重排,进而组帧,此时Jitter Buffer的设计需要考虑接收延时和卡顿之间的平衡。

其中客户端侧视频Jitter Buffer的控制逻辑可参考WebRTC视频JitterBuffer详解,音频请参考WebRTC的NetEQ。

webRTC中音频相关的netEQ(一):概述

webRTC中音频相关的netEQ(二):数据结构

webRTC中音频相关的netEQ(三):存取包和延时计算

webRTC中音频相关的netEQ(四):控制命令决策

webRTC中音频相关的netEQ(五):DSP处理

浅谈 WebRTC NetEQ

同样的道理,也就解释了为什么设计一个MCU混流服务器时必须考虑Jitter Buffer了。

4. 1对多的场景,某个用户下行网络很差,丢包很高,如何避免影响所有人?

在1对多的视频会议场景下,各个接收端的带宽能力和设备性能不尽相同,如果因为某个接收端用户的下行网络状况很差,丢包很高,亦或是设备性能很差,强制要求对端发送端上行降低分辨率和码率,而影响到会议中其他用户的音视频体验,这种显然是不可取的。

解决这个问题,首先是进行分段的拥塞控制策略,理想情况下,发送端会根据上行的带宽估计值控制源端编码和发送码率,SFU则会利用下行的带宽估计值,来控制下发给各接收端的最高码率。

然而,现实问题是,当SFU只有一路视频可以转发时,如何根据各链路的带宽情况进行下发控制,有点巧妇难为无米之炊的感觉。这里要借助于两种源端编码策略 - Simulcast和SVC。

Simulcast:大小流的方式,指的是同时编码/发送多路视频流,比如常规发送一路720p,外加一路180p的流,这样在SFU下发给接收端的时候,可以根据下行带宽的限制,选择下发不同分辨率的流,照顾到每个端的体验。

应用Simulcast的系统示意:

SVC:可伸缩编码,使用基于层次的方法,提供时间或空间可伸缩编码组合。在RTC的应用中,通常会选用时域SVC,通过改变帧率来实现伸缩性。SFU可以根据下行的实际带宽,从同一路SVC视频流中解析出不同的时域分层,分别传输给各个接收端,同样可以实现差异化的视频流转发。 注:该节论述大量引用从入门到进阶|如何基于WebRTC搭建一个视频会议中的论述和图片,如有侵权请联系删除。

  1. 实时通信带宽预测和拥塞控制算法是否需要兼顾公平性?一味地抢带宽是道德的吗?

这一块目前还没有想的特别清楚,先把问题抛出来。因为研究过TCP拥塞控制的都知道,在设计拥塞控制算法的时候要兼顾公平性的原则,而作为需要保证音视频质量的应用来说,是要保守、退让来兼顾公平性?还是多发多抢?目前心中还没有答案,但是感觉一味地抢带宽是不道德、不和谐、不美好的。

自研 VS 开源项目改造

目前比较热门的开源媒体服务器有:

开源项目编程语言网络IO库LicenseJanusC语言glibcGPL v3MediaSoupC++/Node.jslibuvISCLicodeC++/Node.jsboost AsioMITOpen WebRTC Toolkit Media ServerC++/Node.jsboost AsioApache-2.0Jitsi VideobridgeJavaApache-2.0IONGoMITKurentoC++Apache-2.0

网上已经有很多他们之间的比较,这里不作为今天的重点。我想讨论的是在前期选型的时候,该选择自研还是参考开源项目改造扩展?是选择C语言?C++?还是Go语言呢?

如果有着丰富的视频会议经验和一个牛逼的团队,自研肯定是无可厚非的,因为自己设计的系统当然有着100%的控制权,能够参考以往的经验选择最适合、最简洁的方案。而能否自研成功,当然是需要天时、地利、人和。 对于大部分小公司来说,改造一个优秀的开源项目,肯定是最佳的选择。

在这里,建议大家选择一个优秀的网络IO库或者框架和高效便捷的编程语言,无疑目前Go是比较完美的选择,你值得拥有。不过GC对于流媒体实时性的影响需要我们注意和研究。

欢迎大家留言交流,欢迎大家关注我个人公众号!你们的鼓励是我前进的动力。

Reference

iwashi.co/2016/09/03/…

WebRTC MediaServer(SFU/MCU)の情報まとめ

webrtchacks.com/sfu-cascadi…

Improving Scale and Media Quality with Cascading SFUs (Boris Grozev)

zhuanlan.zhihu.com/p/37400934

聊聊WebRTC网关服务器1:如何选择服务端端口方案?

zhuanlan.zhihu.com/p/37493371

聊聊WebRTC网关服务器2:如何选择PeerConnection方案?

zhuanlan.zhihu.com/p/37538078

聊聊WebRTC网关服务器3:如何优化Server的线程方案?

zhuanlan.zhihu.com/p/37589412

聊聊WebRTC网关服务器4:QoS方案分析

zhuanlan.zhihu.com/p/130406233

从入门到进阶|如何基于WebRTC搭建一个视频会议

mp.weixin.qq.com/s/1ttedzc7V…

如何使用开源SFU构建RTC云服务

理解 YUV ?

前言

YUV 是一种彩色编码系统,主要用在视频、图形处理流水线中(pipeline)。相对于 RGB 颜色空间,设计 YUV 的目的就是为了编码、传输的方便,减少带宽占用和信息出错。

人眼的视觉特点是对亮度更铭感,对位置、色彩相对来说不铭感。在视频编码系统中为了降低带宽,可以保存更多的亮度信息(luma),保存较少的色差信息(chroma)。

Y’UV、YUV、YCbCr、YPbPr 几个概念其实是一回事儿。由于历史关系,Y’UV、YUV 主要是用在彩色电视中,用于模拟信号表示。YCbCr 是用在数字视频、图像的压缩和传输,如 MPEG、JPEG。今天大家所讲的 YUV 其实就是指 YCbCr。Y 表示亮度(luma),CbCr 表示色度(chroma)。

luminance 亮度,luma 是在视频编码系统中指亮度值;chrominance 色度,chroma 是在视频编码系统中指色度值

Y’UV 设计的初衷是为了使彩色电视能够兼容黑白电视。对于黑白电视信号,没有色度信息也就是(UV),那么在彩色电视显示的时候指显示亮度信息。

Y’UV 不是 Absolute Color Space,只是一种 RGB 的信息编码,实际的显示还是通过 RGB 来显示。Y’,U,V 叫做不同的 component 。

subsamping

人眼的视觉特点是对亮度更铭感,对位置、色彩相对来说不铭感。在视频编码系统中为了降低带宽,可以保存更多的亮度信息(luma),保存较少的色差信息(chroma)。这叫做 chrominance subsamping, 色度二次采样。原则:在数字图像中,(1) 每一个图形像素都要包含 luma(亮度)值;(2)几个图形像素共用一个 Cb + Cr 值,一般是 2、4、8 个像素。

要想理解本节内容,需要理解一个前提假设就是:对于一个 w 宽、h 高的像素图,在水平方向,一行有 w 个像素;在垂直方向,一列有 h 个像素,整个图形有 w * h 个像素。我们把这个像素叫做图形像素

如果用 YCbCr 像素格式来表示像素图,那么要搞清楚亮度和图形像素的关系,色度和图形像素的关系。

通常对 yuv444,yuv422,yuv420 的解释是后面三个数字分别对应前面三个字母。拿 yuv422 来说,y 对应 4,表示四个图形像素中,每个都有亮度值;u 对应 2,表示四个图形像素中,Cb 只占用两个像素;v 对应 2, 表示四个图形像素中, Cr 占用两个像素。对于 yuv422 模式,这样解释是没有问题。但是对于 yuv420 解释就不对了,不能说四个图形像素中,Cr 占用 0 个像素吧?

现在我们通过下图来理解一下 yuv 各种格式后面数字的含义。图来源于 Chrominance Subsampling in Digital Images

如上图中所示,左侧一列,每一个小矩形是图形像素表示,黑框矩形是色度像素表示,小黑点是表示色度像素值(Cb+Cr),表示图形像素和色度像素在水平和垂直方向的比例关系。比如,

4:4:0 水平方向是1/1,垂直方向是1/2,表示一个色度像素对应了两个图形像素。

4:2:2 水平方向是1/2,垂直方向是1/1,表示一个色度像素对应了两个图形像素。

4:2:0 水平方向是1/2,垂直方向是1/2,表示一个色度像素对应了四个图形像素。

右侧一列是二次采样模式记号表示, 是 J:a:b 模式,实心黑色圆圈表示包含色度像素(Cb+Cr),空心圆圈表示不包含色度像素。对于 J:a:b 模式,主要是围绕参考块的概念定义的,这个参考块是一个 J x 2 的矩形,J 通常是 4。这样,此参考块就是宽度有 4 个像素、高度有 2 个像素的矩形。a 表示参考块的第一行包含的色度像素样本数,b 表示在参考块的第二行包含的色度像素样本数。

4:4:0 参考块第一行包含四个色度样本,第二行没有包含色度样本。

4:2:2 参考块第一行包含两个色度样本,第二行也包含两个色度样本,他们是交替出现。

4:2:0 参考块第一行包含两个色度样本,第二行没有包含色度样本。

现在我们发现 yuv444,yuv422,yuv420 yuv 等像素格式的本质是:每个图形像素都会包含亮度值,但是某几个图形像素会共用一个色度值,这个比例关系就是通过 4 x 2 的矩形参考块来定的。这样很容易理解类似 yuv440,yuv420 这样的格式了。

平面格式(Planar formats)

平面格式是指用三个不同的数组来表示 YCbCr 的三个 Component,每一个 Component 都是通过不同的平面表示。为此,每一个 Component 会对应一个 plane。

yuv420p 也叫 i420 就是 yuv420 planar 表示。yuv420p 一共有三个平面分别是 Y,U,V,每一个平面都是用 8 bit 二进制数字表示,我们把 8 bit 称作位深度。

根据前面的介绍,如果用 yuv420p 来表示分辨率为 1280 * 720 的图片,需要占用多少存储空间呢?

每一个像素都需要一个 luma 值,即 y。那么总共需要 1280 * 720 = 921600 bytes。

每四个像素需要一个 chroma u 值,那么总共需要 1280 * 720 / 4 = 230400 bytes。

每四个像素需要一个 chroma v 值,那么总共需要 1280 * 720 / 4 = 230400 bytes。

把 y、u、v 三个 plane 加起来就是:921600 + 230400 + 230400 = 1382400 bytes。

现在我们找一个 jpeg 图片,通过 ffmpeg 转成 yuv,生成图形的分辨率是 1280 *720 ,具体命令如下:

1
ffmpeg -i yuv_to_jpeg_0.jpeg -s 1280x720 -pix_fmt yuv420p test-yuv420p.yuv

查看生成的 test-yuv420p.yuv 文件属性,系统显示的大小和我们计算的完全吻合。

压缩格式(Packed formats)

压缩格式是指用一个数组表示 YCbCr,每一个 component 是交替出现的。

ffmpeg 中对 yuv420p 像素格式大小计算

yuv420p 的格式描述在 libavutil/pixdesc.c 的 173 行。

1
2
3
4
5
6
7
8
9
10
11
12
13
173 static const AVPixFmtDescriptor av_pix_fmt_descriptors[AV_PIX_FMT_NB] = {
174 [AV_PIX_FMT_YUV420P] = {
175 .name = "yuv420p", // 像素格式名称
176 .nb_components = 3, // 表示有三个 component ,也是三个 plane
177 .log2_chroma_w = 1, // 表示色度(chroma) 像素和图形像素的水平比例关系
178 .log2_chroma_h = 1, // 表示色度(chroma) 像素和图形像素的垂直比例关系
179 .comp = {
180 { 0, 1, 0, 0, 8, 0, 7, 1 }, /* Y 平面,step 是 1, 位深度是8 bit */
181 { 1, 1, 0, 0, 8, 0, 7, 1 }, /* U 平面,step 是 1, 位深度是8 bit */
182 { 2, 1, 0, 0, 8, 0, 7, 1 }, /* V 平面,step 是 1, 位深度是8 bit */
183 },
184 .flags = AV_PIX_FMT_FLAG_PLANAR,
185 },

所有的 YUV 像素格式表示都在 av_pix_fmt_descriptors 表中完成,我可以把这叫做像素格式描述表。

yuv420p 像素格式在水平方向(行)大小计算在 libavutil/imgutils.c 的 54 行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
53 static inline
54 int image_get_linesize(int width, int plane,
55 int max_step, int max_step_comp,
56 const AVPixFmtDescriptor *desc)
57 {
58 int s, shifted_w, linesize;
59
60 if (!desc)
61 return AVERROR(EINVAL);
62
63 if (width < 0)
64 return AVERROR(EINVAL);
// max_step_comp 的取值: 0:y,1:u,2:v。对于 y 平面,每一个图形像素需要一个亮度值,
// 所以这里比例因子是 0;对于 u、v 平面来说,色度像素和图形像素在水平和垂直方向都是 2/1 的关系,
// 所以计算行的时候,比例因子取像素格式描述表中的 log2_chroma_w。对于 yuv420p 来说,取值是 1 ,
// 因为是通过移位运算完成的,右移 1 位,相当于是除以 2。
65 s = (max_step_comp == 1 || max_step_comp == 2) ? desc->log2_chroma_w : 0;
66 shifted_w = ((width + (1 << s) - 1)) >> s;
67 if (shifted_w && max_step > INT_MAX / shifted_w)
68 return AVERROR(EINVAL);
69 linesize = max_step * shifted_w;
70
// 如果像素描述表中的单位是 bit,那么这里转换成 bytes,右移 3 位,就是除以 8。
71 if (desc->flags & AV_PIX_FMT_FLAG_BITSTREAM)
72 linesize = (linesize + 7) >> 3;
73 return linesize;
74 }

yuv420p 像素格式在垂直方向(列)大小计算在 libavutil/imgutils.c 的 111 行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
111 int av_image_fill_pointers(uint8_t *data[4], enum AVPixelFormat pix_fmt, int height,
112 uint8_t *ptr, const int linesizes[4])
113 {
114 int i, total_size, size[4] = { 0 }, has_plane[4] = { 0 };
115
116 const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(pix_fmt);
117 memset(data , 0, sizeof(data[0])*4);
118
119 if (!desc || desc->flags & AV_PIX_FMT_FLAG_HWACCEL)
120 return AVERROR(EINVAL);
121
122 data[0] = ptr;
123 if (linesizes[0] > (INT_MAX - 1024) / height)
124 return AVERROR(EINVAL);
125 size[0] = linesizes[0] * height;
126
127 if (desc->flags & AV_PIX_FMT_FLAG_PAL ||
128 desc->flags & FF_PSEUDOPAL) {
129 data[1] = ptr + size[0]; /* palette is stored here as 256 32 bits words */
130 return size[0] + 256 * 4;
131 }
132
133 for (i = 0; i < 4; i++)
134 has_plane[desc->comp[i].plane] = 1;
135
136 total_size = size[0];
137 for (i = 1; i < 4 && has_plane[i]; i++) {
// i 的取值: 0:y,1:u,2:v。对于 y 平面,每一个图形像素需要一个亮度值,
// 所以这里比例因子是 0;对于 u、v 平面来说,色度像素和图形像素在水平和垂直方向都是 2/1 的关系,
// 所以计算列的时候,比例因子取像素格式描述表中的 log2_chroma_h。对于 yuv420p 来说,取值是 1 ,
// 因为是通过移位运算完成的,右移 1 位,相当于是除以 2。
138 int h, s = (i == 1 || i == 2) ? desc->log2_chroma_h : 0;
139 data[i] = data[i-1] + size[i-1];
140 h = (height + (1 << s) - 1) >> s;
141 if (linesizes[i] > INT_MAX / h)
142 return AVERROR(EINVAL);
// 每一平面的行和列做乘法,就是像素总数。
143 size[i] = h * linesizes[i];
144 if (total_size > INT_MAX - size[i])
145 return AVERROR(EINVAL);
// 每一个平面的像素数相加,就是图片占用的像素总数。
146 total_size += size[i];
147 }
148
149 return total_size;
150 }

后记

本人对 YUV,YCbCr 格式早就熟悉,但是没有仔细推敲过,理解上总是感觉差点儿意思。索性找时间仔细阅读了本文引用到的四篇文章,结合 ffmpeg 源码,总算搞清楚 yuv444,yuv420 这几种格式后面数字的含义了。按照自己的理解写了本文,希望对您有所帮助。

参考