昨天,清华自动化大一学生的 C++大作业霸占了知乎榜首,该作业要求学生写一个类似于「雨课堂」的网络教学软件(雷课堂),可以共享屏幕、语音直播、在线答题……其实现难度、工作量似乎都超出了大一学生的能力范围,连清华特奖得主、阿里 P6 也表示无法单独完成。离提交 deadline 只剩五六周,这个作业真能写完吗?
在昨天的讨论区,我们看到大部分评论都是对于这一作业的吐槽。不过也有人提醒大家,为什么不试一试呢?毕竟,作业还是要交的。
作为雨课堂的技术支持方,快手的音视频工程师范威给出了自己的专业回答。范威有着十多年的工作经验,在春节后深度参与了雨课堂和快手项目的联合开发,为雨课堂提供了音视频底层技术支持。
根据范威的介绍,总共需要 2 周开发时间,就能拥有一个雷课堂。当然,前提是「这些知识你都学过」,还要有「丰富的踩坑经验」。
小伙伴们是不是心动了?下面来看具体分析。
一个人单撸一个雷课堂需要多少工作量?
雨课堂是清华大学研发的一款在线教育 APP,可以支持老师在线授课、分享 PPT,学生与老师进行语音互动。从音视频角度来看,这种在线授课的形式其实就是一个标准的视频会议场景。
要说如何从头开始撸一个视频会议软件,需要分前端和后端来说。这里前端指的是终端,后端指的是媒体服务器。前端的主要功能是负责音视频通信,后端的主要功能是负责媒体流的转发。
先说说前端。前端模块包括音视频采集、前处理、编解码、收发包等功能模块。目前开源的视频会议项目以 webrtc 最为流行,其代码里有 80w+行之多。要想单手从头撸一个视频会议终端,虽说不需要 80w 行代码那么多,但是还是有其难度的。
采集模块
首先是平台的支持。iOS、Android、Windows、Mac、Linux,不同平台提供的音视频技术都不尽相同。音视频的采集和前处理,需要根据不同的平台和机型进行适配,有些算法可以采用平台的能力,而有些算法需要通过软件进行处理。
先来说一下采集和前处理模块。视频通过调用系统 API 完成摄像头数据的采集,这里不详细说,感兴趣的同学可以自行查阅官方文档。视频前处理包括各种美颜滤镜算法,由于教育场景下这个功能不是刚需故而跳过。
这里说一下音频前处理。在实时语音通话时,从麦克风直接采集到的音频是包含自己说话的声音以及对方说话的声音的。这是因为本地的扬声器播放出对方的声音也会被麦克风采集进去。如果不加任何处理就发送出去的话,对方会从扬声器里听到自己说话,这种情况称为声学回声(Echo)。为了得到一个比较自然的通话效果,需要对麦克风采集的音频数据进行处理,消除掉对方说话的部分,只保留本地的声音。这个过程叫做回声消除(AEC,Acoustic Echo Cancellation)。AEC 算法最常用的方式是采用自适应滤波器来产生一个模拟回声,然后再从麦克风采集信号中将这个模拟回声抵消掉,达到消除回声的目的。
单线 AEC 架构
在实际项目中,还会对信号做一个 NLP(非线性滤波)来消除残余回声,同时为了增强信噪比还会做声学降噪和自动增益,统称为 3A(AEC/AGC/ANS)。目前几乎所有的智能手机和 Mac 平台都有硬件的 3A 算法模块,其中苹果的设备调校的比较好,而 Android 手机的 3A 效果良莠不齐,通常需要通过软件自己实现。最快的实现方式是采用系统提供的 3A 算法,这里算 1 个人天。
编码模块
接下来是编解码模块。编解码是将经过前处理的音视频原始数据进行压缩,以达到网络传输的目的。由于网络资源的限制,原始的音视频数据量太大,不能直接在网络上进行传播,必须先经过压缩。压缩分为无损压缩和有损压缩。音视频数据通常采用有损压缩的方式,可以做到非常大的压缩比。视频编码算法常用的有 H.264/H.265/VP8/VP9/AV1/AVS2 等等,音频的压缩算法有 AAC/Speex/Opus/G.711/G.729 等。现在通常采用的视频压缩算法是 H.264/H.265,而音频算法则大都采用 Opus。
压缩算法的细节非常繁杂,目前大多采用比较成熟的开源项目来实现,比如 x264,x265,ffmpeg 以及 libopus。苹果设备也提供了内置的硬件视频编解码器 videotoolbox 可以支持 H.264/H.265 的实时编解码,而 Android 则有 MediaCodec 提供相同的能力。在桌面平台上,Intel 和 NVIDIA 的很多芯片提供了 qsv 和 nvenc 功能,用于实现桌面端的硬件视频编解码。有了编码器之后,还需要对编码参数进行正确的配置,以适合实时通信的场景。主要影响音视频通话体验的参数就是码率,在其他参数不变的条件下,码率越大音视频质量就越好,而使用的网络带宽也越大。
这里为了简化实现,使用系统提供的编解码器实现,算 1 个人天。
传输模块
经过编码之后的音视频数据已经小了很多,可以进行网络传输了。Internet 网络传输协议分为 TCP 和 UDP 两种方式。TCP 协议是可靠传输,保证数据的完整性和有序性,但是缺点是在公网传输时速度比较慢,延时比较大。而 UDP 协议是不可靠协议,数据只管发,不能保证一定能够到达对方,但是优点是发送速度快,延时低。因此在实时音视频通信里,都会优先使用 UDP 协议进行数据发送。UDP 数据是以数据包为单位进行发送的,每次发送一个包,最大包大小不能超过 64K 字节。但是由于 IP 层的分片路由限制,通常一个 UDP 数据包的大小都会限制在一个 MTU(Max Transmission Unit)以内。以太网的 MTU 为 1500 字节,因此每个 UDP 包的大小大多都会限制在 1K 字节左右。而编码后的视频数据相对于这个大小还是太大了,需要对视频数据进行进一步的分包才能进行发送。
由于 UDP 协议的特性是不可靠传输,因此数据包达到的先后顺序也没有保证。为了让对方收到的音视频数据的先后顺序跟发送端一致,需要在接收端对 UDP 包进行排序。在视频会议上,通常会采用 RTP 协议对分包之后的数据包进行一层封装,每个 RTP 包都包含一个 RTP 头,里面为每个 RTP 包分配了一个序列号。这个序列号是有序递增的,因此接收端可以通过收到的 RTP 包的序列号对数据包进行排序,同时也可以知道哪些序号的数据包没有收到,从而向发送端请求重发。
上面提到 UDP 在网络传输过程中可能会丢包。由于音视频编码后的数据需要完整接收才能进行正常解码,因此采用 UDP 协议传输的 RTP 包需要能够处理丢包恢复。丢包恢复的方式有两种,一种是 FEC(前向纠错),一种是 ARQ(自动重传),通常项目上这两个方法会同时采用。
FEC 是对一组 RTP 包进行冗余编码,产生出一些冗余包,冗余包包含了这一组 RTP 包的信息,在丢包的时候可以利用冗余包里的数据,恢复出这一组 RTP 包数据。常用的 FEC 算法包括 RS、卷积码、喷泉码等。FEC 的好处是不会引入额外的延时,冗余包和数据包一起发送给对端,对端通过接收到的数据包和冗余包尝试恢复,没有额外的交互时间,但由于网络丢包的随机性,并不是每一个 FEC 包都能够被利用,这样降低了整体的带宽利用率
ARQ 则是精准的请求丢失的 RTP 包,让发送端重新发送。在 RTP 协议里,可以通过 NACK 来实现重传请求,携带上请求重传的 RTP 序列号。发送端接收到 NACK 请求后,会重新发送该序列号对应的 RTP 包到对端。NACK 和重传包的传输引入了额外的延时,因此 ARQ 会导致音视频通信的延时增加,但是带宽利用率比较高。
对于差一些的网络,网络的带宽并没有那么高,如果发送端编码的音视频数据超过了其发送的上行带宽,就会导致网络拥塞,产生丢包和卡顿。为了防止网络拥塞的发生,发送端需要对自己的上行网络带宽进行预测,并反馈给编码器,调整编码器的码率不要超过带宽上限。这个算法叫做带宽估计。带宽估计是实时音视频通信非常重要的一个算法,其准确性会很大程度上影响用户体验。带宽估计算法的策略有很多,webrtc 中采用的是 google 提出的 GCC(Google Congestion Control)算法。
整个传输模块没有现成的开源项目可用,要么自己撸要么参考 webrtc 的实现,大概需要 3-5 天。
缓冲队列
到这里,发送端模块基本就介绍完了,下面说一些接收端要做的部分。接收端在接收到 RTP 数据包之后,首先根据 RTP 的序列号进行排序,如果有丢包,则通过 FEC 和 ARQ 进行恢复和重传。得到完整有序的 RTP 包之后,对 RTP 包进行重组,组合成编码后的音视频数据。由于网络传输的不稳定性,收到的数据并不是均匀的,有可能一会儿接收的快,一会儿接收的慢,造成数据接收的波动。这种现象被称为网络抖动(Jitter)。如果这时候直接进行解码播放,那么会导致视频忽快忽慢,声音出现变声的现象。为了平滑这种网络抖动,接收端需要有一个缓冲队列,将接收到的音视频数据放入到缓冲队列中,然后再匀速的从队列中取出,从而得到比较平滑的音视频数据进行播放。
从缓冲队列取出的数据就可以进行解码了。解码就是解压缩的过程,将发送端压缩的数据还原成原始的音视频数据,才能在本地进行播放。一些编码算法(例如 H.264/H.265/Opus)为了提高压缩比,在压缩时对于前后连续的两帧音视频数据做了参考,这样就导致采用这类压缩算法的编码数据在解码的时候存在参考关系的依赖,只有前一个数据被正确解码,后一个数据才能也正确的解码。但是由于网络丢包,即使采用了 FEC 和 ARQ 等丢包恢复策略,仍然有部分音视频数据无法完整的达到接收端。这时候如果强行解码,那么视频会出现花屏,而声音会出现爆音。为了解决这个问题,对于不连续的视频数据,接收端需要向发送端请求编码一个关键帧视频数据。这个关键帧数据在解码的时候不会参考其他视频数据帧,同时可以被后续的编码视频帧参考,这样可以解决后续视频的参考关系问题。而音频可以通过 PLC(Packet Loss Concealment)算法,根据波形产生出丢失的音频数据。
这里可以参考 webrtc 的 neteq 队列来实现(没错,只有 webrtc 开源,所以同学们没有别的参考),大概需要 3 天
本地播放
最后就是本地播放解码出的音视频数据。在播放的时候,音视频数据在网络传输上并不一定是相同的速度,因此可能会产生音画不同步的问题。这里还需要一个音视频同步模块,来控制音视频播放的速度,保证声音和视频可以对齐。在对齐的时候,由于人耳对于音频的快慢变化更加敏感,所以总是调整视频的速度来对齐音频。在每个音视频数据中,都会带一个时间戳(timestamp),这个时间戳是音频和视频数据采集时生成的,相同的时间戳的音视频应该同时播放才能保证音画同步。因此在播放视频帧的时候,需要对比当前播放的音频的时间戳,调整视频播放的速度。
视频在屏幕上播放的时候,其显示的分辨率大小可能与实际编码的分辨率大小不一致。视频分辨率代表的是视频像素点的个数,分辨率越高越清晰。对于现在的手机和显示器来说,大多支持 HDPI,通过更加密集的像素点得到更加清晰细腻的图像。而摄像头采集和编码的视频分辨率并不会特别高,那么在显示的时候需要对视频进行放大。不同的视频放大算法对于清晰的影响比较大,默认的 linear 放大算法会导致图像比较模糊,采用复杂的放大算法,比如 bicubic,lanzcos,spline 等,可以得到更加清晰的画面,而一些特殊内容采用特定的算法会得到更好的视觉效果(比如人脸部分采用 softcubic 可以达到美颜效果)。
这里不考虑实现效果的话,用最简单的 opengl 渲染视频,大概 2 天。
这样,一个简简单单的实时音视频通信的终端部分就开发完了,再配上一个炫酷拉风的界面,就可以食用了。但是这里只能实现两个人之间的通信,而在线授课的场景可是一个老师对一群学生。如何实现多人之间的实时通信呢?这就需要后端的媒体服务器作中转来实现。
媒体后端
这里需要简单介绍一下多人实时音视频通信的网络拓扑结构。对于三人以上的实时通信,由于接收端同时会接收多个人传过来的音视频数据,因此如果没有服务器中转的话,需要采用网状拓扑结构,每个终端的音视频流都需要同时发送给其他所有终端,网络带宽成倍增加。为了解决这个问题,就需要采用星型拓扑结构,所有的终端将自己的音视频数据发送给中央服务器,再由服务器来做转发。这个服务器就是视频会议的后端,暨媒体服务器。
媒体服务器的实现方式有两种。一种叫做 MCU 模式,一种叫 SFU 模式。
MCU 模式是服务器将所有人的音视频数据在服务端进行解码,然后合成为一个新的音视频流。这个视频流是由所有人的视频组合出来的,而音频则是将所有人的音频混合,之后重新进行音视频编码,再发送给所有终端。这样终端播放出来的音视频流就是一个合并好的音视频流。这种模式对于媒体服务器的性能消耗很大,因此一台服务器并不能支持很多的终端。
SFU 模式则只是做 RTP 包的转发,并不做解码和合流的工作。每个终端同时会接收到多个人的音视频流,每一组音视频流需要独立进行处理和解码,然后在本地混合后播放出来。这种模式的 SFU 性能消耗比较低,能够支持的并发很高。
如果不考虑高并发和架构设计,最简单的实现大概 3 天。
把这样一个媒体服务器部署到网络上,然后由它来转发所有终端的音视频数据包,这样就可以实现多人之间的音视频通信了。
总共需要 2 周开发时间,一个最最简单的雷课堂就实现完成了。
当然,这里只是为了完成作业从功能角度来实现。在实际项目中,需要考虑到性能和架构优化,增强稳定性和音视频质量,服务端高并发和快速部署,各种算法参数调优和策略优化。没有几十人团队精细打磨 2-3 年,以及专业的音视频质量测试实验室,不可能做到业界顶尖水准。