音视频开发——直播推流&拉流技术

944 阅读14分钟

一、推流架构

推流SDK客户端的模块主要有三个,推流采集端、队列控制模块、推流端。其中每个模块的主要流程如下,本文的主要目的就是拆分推流流程。

1.1 采集端

视频采集:通过Camera采集视频。

音频采集:通过麦克风采集音频。

视频后处理:美颜、滤镜、贴纸、翻转等特效。

音频后处理:重采样、3A处理等。

视频编码:支持硬编码和软编码,同时支持H264和HEVC编码,特别要注意编码的特殊情况。

音频编码:AAC编码

采集端的很多功能都是平台相关的,相机采集、编码等Android和iOS上处理都不一样。尤其是Android平台,很多机型和芯片,当然会有一些特殊的情况需要兼容下,这个后面我们会详细描述的。

1.2 队列控制

队列控制模块是推流SDK非常重要的控制模块,推流SDK就是一个很简单的“生产者——消费者模型”,采集端是生产者,推流端是消费者,采集端采集的是本地数据,推流端和服务端交互,正常情况下都是推流端会出现延迟,像弱网情况下推流端消费肯定会很慢,但是采集端速度并不会慢下来,如果没有队列控制这个降压阀,那很多数据就会堆积在队列中,本地数据不断堆积,最终导致OOM。

队列控制应该如何控制?

视频的数据比音频大很多,所以队列控制主要是视频基准,音频跟着视频相应丢帧。

编码之后视频队列大小设置为60,在推流的过程中,发现视频的队列已满,需要丢弃队列最前面的一帧,然后再入队新的一帧,音频队列也要同步操作,对应时间点的音频数据也要丢掉。

1.3 推流端

推流采用的是RTMP协议,RTMP是Adobe公司开发的,算是事实上的工业标准,全称是Real Time Messaging Protocol,虽然实时性要比HLS好一点,但是也还有几秒左右的延迟。它的底层是基于TCP协议。

RTMP协议的建连流程如下:

RTMP建连需要商量两件事情:

版本号:客户端和服务器的版本号不一致,无法继续工作 时间戳,视频播放过程中,时间戳很重要,如果没有,后续的音视频同步无法继续开展。 首先,客户端发送 C0 表示自己的版本号,不必等对方的回复,然后发送 C1 表示自己的时间戳。服务器只有在收到 C0 的时候,才能返回 S0,表明自己的版本号,如果版本不匹配,可以断开连接。服务器发送完 S0 后,也不用等什么,就直接发送自己的时间戳 S1。客户端收到 S1 的时候,发一个知道了对方时间戳的 ACK C2。同理服务器收到 C1 的时候,发一个知道了对方时间戳的 ACK S2。

推流的过程,就是将 NALU 放在 Message 里面发送,这个也称为 RTMP Packet 包。Message 的格式就像这样。

一定要记住音频和视频的头部要单独发送,在发送视频头部的视频,需要将NALU起始标识符去掉,RTMP不需要它们。

三、技术要点

3.1 声音处理

在采集完声音之后,需要对音频进行3A处理,即声学回声消除(AEC)、背景噪声抑制(ANS)、自动增益控制(AGC),3A处理在声音后期处理中非常重要,在推流场景的声音处理中应用十分广泛。

AEC

回声消除(AEC)是指在二线传输的两个方向上同时间、同频谱地占用线路,在线路两个方向传输的信号完全混在一起,本端发信号的回波就成为了本端信号的干扰信号,利用自适滤波器可抵消回波以达到较好的接收信号质量,即为回声消除。

回声消除的原理就是利用接收到的音频与本地采集的音频做对比,添加反向的人造回声,将远端的声音消除。

ANS

背景噪声抑制(ANS)指的是将声音中的背景噪声识别并进行消除的处理。

背景噪声分平衡噪声和瞬时噪声,平稳噪声频谱稳定,瞬时噪声频谱能量方差小,利用噪声的特点,对音频数据添加反向波形处理即可消除。

目前,对于平稳的噪声已经有很多种简单方法能够成功抑制,但是生活中常见的一些瞬态噪声却依然缺乏好办法。

瞬态噪声的共同特点就是突发性极强,在时域上呈振荡衰弱的形式,持续时间在十几毫秒至上百毫秒不等;在频域上分布很宽,瞬态噪声的频谱基本上是和正常语音的频谱混叠在一起,很难进行抑制。

AGC

自动增益控制(AGC)主要用于调整音量幅值,提高语音通信系统在带噪声环境中的性能。

人们正常交谈的音量在 40-60dB 之间,低于 25dB 的声音听起来很吃力,而超过 100dB 的声音会让人感到不适,AGC 的作用就是将音量调整到人接受的范围。

音频响度及麦克风拾音控制是保证音视频沟通质量的重要技术手段,一般来说,音频标准、传输条件、人为失误等因素都可能导致音频信号之间出现声音突变或者响度不一致的情况,这时候就需要对音频信号放大或缩小以得到自然清晰的语音通信。

3.2 视频处理

相机采集的原始数据首先要进行帧处理,主播通常会应用一些特效,例如美颜、滤镜等,这些都会在视频帧后处理流程中开展,帧处理之后才会进行编码处理。

H264和H265编码还有点不同。

H264头部由SPS和PPS组成,SPS是序列参数集,包括一个图像序列的所有信息,如图像尺寸、视频格式等;PPS是图像参数集,包括一个图像的所有分片的相关信息,如图片类型、序列号。

H264的码流结构如下:

起始码 + SPS + 起始码 + PPS + 起始码 + SEI + 起始码 + I帧 + 起始码 + P帧 + ......

H265头部除了SPS和PPS之外,还有一个VPS,VPS是视频参数集。

H265码流结构如下:

起始码 + VPS + 起始码 + SPS + 起始码 + PPS + 起始码 + SEI + 起始码 + I帧 + 起始码 + P帧 + ......

其中H264和H265的码流类型有两种,一种是Annexb格式,另一种是MP4格式,使用最广泛的还是Annexb格式,本文主要以Annexb为例。

起始码只是起到分割的作用,并不是有效的视频数据,起始码也有两种:

4字节的00 00 00 01 3字节的00 00 01 但是图像编码中也有可能出现00 00 00 01和00 00 01,出现这种情况怎么办?

00 00 00 修改为 00 00 03 00 00 00 01 修改为 00 00 03 01 00 00 02 修改为 00 00 03 02 00 00 03 修改为 00 00 03 03 这样在编码的过程中就不会出现混淆了。

3.3 推流控制

上文也说了在采集端和推流段其实是通过一个队列控制的,采集端采集本地的视频和音频,然后编码好了放入队列中,推流段从队列中取出视频和音频然后根据特定的格式发送到服务端。相当于采集端是往水池中注水,推流段是放水。

采集端是很快的,推流段是受限于网络的,如果网络状态比较好的情况下,可以达到一个较好的平衡,但是一旦网络变差,队列就会出现堆积,我们不可能让队列无限堆积,需要设置一个队列阈值,当然队列已满,需要将队列中原有的数据抛弃,将新数据入队。这样就会出现丢帧,这时候推流段可以适当降低码率降低丢帧的概率。

3.4 支持FLV-HEVC

flv 是不支持H265的,需要手动修改FLV支持H265编码和解码的解析,当然也需要拉流段支持FLV-H265。

下面贴上ffmpeg支持flv(h265编码和解码)的修改代码,对应ffmpeg的tag是n5.1

diff --git a/libavformat/flv.h b/libavformat/flv.h
index 3571b90279..91f006520c 100644
--- a/libavformat/flv.h
+++ b/libavformat/flv.h
@@ -110,6 +110,7 @@ enum {
     FLV_CODECID_H264    = 7,
     FLV_CODECID_REALH263= 8,
     FLV_CODECID_MPEG4   = 9,
+    FLV_CODECID_HEVC    = 12,
 };
​
enum {
diff --git a/libavformat/flvdec.c b/libavformat/flvdec.c
index 8dba92661b..0762a4569d 100644
--- a/libavformat/flvdec.c
+++ b/libavformat/flvdec.c
@@ -321,6 +321,8 @@ static int flv_same_video_codec(AVCodecParameters *vpar, int flags)
         return vpar->codec
@@ -367,6 +369,11 @@ static int flv_set_video_codec(AVFormatContext *s, AVStream *vstream,
         vstreami->need_parsing = AVSTREAM_PARSE_HEADERS;
         ret = 3;     // not 4, reading packet type will consume one byte
         break;
+    case FLV_CODECID_HEVC:
+        par->codec_id = AV_CODEC_ID_HEVC;
+        vstreami->need_parsing = AVSTREAM_PARSE_HEADERS;
+        ret = 3;     // not 4, reading packet type will consume one byte
+        break;
     case FLV_CODECID_MPEG4:
         par->codec_id = AV_CODEC_ID_MPEG4;
         ret = 3;
@@ -1240,6 +1247,7 @@ retry_duration:
if (st->codecpar->codec_id == AV_CODEC_ID_AAC ||
         st->codecpar->codec_id == AV_CODEC_ID_H264 ||
+        st->codecpar->codec_id == AV_CODEC_ID_HEVC ||
         st->codecpar->codec_id == AV_CODEC_ID_MPEG4) {
         int type = avio_r8(s->pb);
         size--;
@@ -1249,7 +1257,7 @@ retry_duration:
             goto leave;
         }
​
-        if (st->codecpar->codec_id == AV_CODEC_ID_H264 || st->codecpar->codec_id == AV_CODEC_ID_MPEG4) {
+        if (st->codecpar->codec_id == AV_CODEC_ID_H264 || st->codecpar->codec_id == AV_CODEC_ID_HEVC || st->codecpar->codec_id == AV_CODEC_ID_MPEG4) {
             // sign extension
             int32_t cts = (avio_rb24(s->pb) + 0xff800000) ^ 0xff800000;
             pts
             }
         }
         if (type == 0 && (!st->codecpar->extradata
 || st->codecpar->codec_id == AV_CODEC_ID_AAC ||
-            st->codecpar->codec_id == AV_CODEC_ID_H264)) {
+            st->codecpar->codec_id == AV_CODEC_ID_H264 || st->codecpar->codec_id == AV_CODEC_ID_HEVC)) {
             AVDictionaryEntry *t;
​
             if (st->codecpar->extradata) {
diff --git a/libavformat/flvenc.c b/libavformat/flvenc.c
index 770ca319ed..b13c2eaf0a 100644
--- a/libavformat/flvenc.c
+++ b/libavformat/flvenc.c
@@ -27,6 +27,7 @@
 #include "libavcodec/mpeg4audio.h"
 #include "avio.h"
 #include "avc.h"
+#include "hevc.h"
 #include "avformat.h"
 #include "flv.h"
 #include "internal.h"
@@ -45,6 +46,7 @@ static const AVCodecTag flv_video_codec_ids[] = {
{ AV_CODEC_ID_VP6,      FLV_CODECID_VP6 },
     { AV_CODEC_ID_VP6A,     FLV_CODECID_VP6A },
     { AV_CODEC_ID_H264,     FLV_CODECID_H264 },
+    { AV_CODEC_ID_HEVC,     FLV_CODECID_HEVC },
     { AV_CODEC_ID_NONE,     0 }
 };
​
@@ -478,9 +480,9 @@ static int unsupported_codec(AVFormatContext *s,
 {
const AVCodecDescriptor *desc = avcodec_descriptor_get(codec_id);
     av_log(s, AV_LOG_ERROR,
-           "%s codec %s not compatible with flv\n",
+           "%s codec %s not compatible with flv %d\n",
             type,
-            desc ? desc->name : "unknown
");
+            desc ? desc->name : "unknown", codec_id);
     return AVERROR(ENOSYS);
 }
 @@ -489,7 +491,7 @@ static void flv_write_codec_header(AVFormatContext* s, AVCodecParameters* par, i
     AVIOContext *pb = s->pb;
     FLVContext *flv = s->priv_data
-    if (par->codec_id == AV_CODEC_ID_AAC || par->codec_id == AV_CODEC_ID_H264
+    if (par->codec_id == AV_CODEC_ID_AAC || par->codec_id == AV_CODEC_ID_H264 || par->codec_id == AV_CODEC_ID_HEVC
             || par->codec_id == AV_CODEC_ID_MPEG4) {
         int64_t pos;
         avio_w8(pb,
@@ -536,7 +538,10 @@ static void flv_write_codec_header(AVFormatContext* s, AVCodecParameters* par, i
             avio_w8(pb, par->codec_tag | FLV_FRAME_KEY); // flags
             avio_w8(pb, 0); // AVC sequence header
             avio_wb24(pb, 0); // composition time
-            ff_isom_write_avcc(pb, par->extradata, par->extradata_size);
+            if (par->codec_id == AV_CODEC_ID_H264)
+                ff_isom_write_avcc(pb, par->extradata, par->extradata_size);
+            else
+                ff_isom_write_hvcc(pb, par->extradata, par->extradata_size, 0);
         }
}
         data_size = avio_tell(pb) - pos;
         avio_seek(pb, -data_size - 10, SEEK_CUR);
@@ -834,12 +839,12 @@ static int flv_write_packet(AVFormatContext *s, AVPacket *pkt)
     if (par->codec_id == AV_CODEC_ID_VP6F || par->codec_id == AV_CODEC_ID_VP6A ||
         par->codec_id == AV_CODEC_ID_VP6  || par->codec_id == AV_CODEC_ID_AAC)
         flags_size = 2;
-    else if (par->codec_id == AV_CODEC_ID_H264 || par->codec_id == AV_CODEC_ID_MPEG4)
+    else if (par->codec_id == AV_CODEC_ID_H264 || par->codec_id == AV_CODEC_ID_HEVC || par->codec_id == AV_CODEC_ID_MPEG4)
         flags_size = 5;
     else
flags_size = 1;
​
-    if (par->codec_id == AV_CODEC_ID_AAC || par->codec_id == AV_CODEC_ID_H264
+    if (par->codec_id == AV_CODEC_ID_AAC || par->codec_id == AV_CODEC_ID_H264 || par->codec_id == AV_CODEC_ID_HEVC
             || par->codec_id == AV_CODEC_ID_MPEG4) {
         size_t side_size
uint8_t *side = av_packet_get_side_data(pkt, AV_PKT_DATA_NEW_EXTRADATA, &side_size);
@@ -860,7 +865,7 @@ static int flv_write_packet(AVFormatContext *s, AVPacket *pkt)
                "Packets are not in the proper order with respect to DTS\n");
         return AVERROR(EINVAL);
     }
​
-    if (par->codec_id == AV_CODEC_ID_H264 || par->codec_id == AV_CODEC_ID_MPEG4) {
+    if (par->codec_id == AV_CODEC_ID_H264 || par->codec_id == AV_CODEC_ID_HEVC || par->codec_id == AV_CODEC_ID_MPEG4) {
         if (pkt->pts == AV_NOPTS_VALUE) {
             av_log(s, AV_LOG_ERROR, "Packet is missing PTS\n");
             return AVERROR(EINVAL);
@@ -905,6 +910,10 @@ static int flv_write_packet(AVFormatContext *s, AVPacket *pkt)
         if (par->extradata_size > 0 && *(uint8_t*)par->extradata != 1)
             if ((ret = ff_avc_parse_nal_units_buf(pkt->data, &data, &size)) < 0)
                 return ret;
+    } else if (par->codec_id == AV_CODEC_ID_HEVC) {
+        if (par->extradata_size > 0 && *(uint8_t*)par->extradata != 1)
+            if ((ret = ff_hevc_annexb2mp4_buf(pkt->data, &data, &size, 0, NULL)) < 0)
+                return ret;
     } else if (par->codec_id == AV_CODEC_ID_AAC && pkt->size > 2 &&
                (AV_RB16(pkt->data) & 0xfff0) == 0xfff0) {
         if (!s->streams[pkt->stream_index]->nb_frames) {
@@ -977,7 +986,7 @@ static int flv_write_packet(AVFormatContext *s, AVPacket *pkt)
                              (FFALIGN(par->height, 16) - par->height));
         } else if (par->codec_id == AV_CODEC_ID_AAC)
             avio_w8(pb, 1); // AAC raw
-        else if (par->codec_id == AV_CODEC_ID_H264 || par->codec_id == AV_CODEC_ID_MPEG4) {
+        else if (par->codec_id == AV_CODEC_ID_H264 || par->codec_id == AV_COD_ID_HEVC || par->codec_id == AV_CODEC_ID_MPEG4) {
             avio_w8(pb, 1); // AVC NALU
             avio_wb24(pb, pkt->pts - pkt->dts);
         }

直播推流总结

一、采集

手机直播源码开发中的采集主要包含视频采集和音频采集,关于视频主要是通过摄像头采集,音频主要通过麦克风进行采集这点大家是知道的。

但是视频采集这里面涉及到摄像头的相关操作以及摄像头的参数设置,这里需要注意的是,由于安卓手机厂家众多,软硬件更新速度快,因此系统和摄像头需要进行大范围适配,部分用户被排除在可用范围内。

二、前处理

手机直播源码开发中的视频处理,现在主播直播过程当中美颜是一定要有的,毕竟更容易吸引粉丝的关注,另外现在的视频美颜处理也是很强大的,也可以根据自己的喜好添加一些好看的滤镜,还有好玩的特效。

手机直播源码开发中的音频处理。有时直播间只有主播一人说话不仅主播会觉得尴尬,连用户都会觉得尴尬,这时候就需要一些额外的声音以增加直播气氛,比如笑声或者鼓掌声。

三、编码

手机直播源码开发中的编码处理可以这样理解,如果我们直接把通过摄像头和麦克风采集到的这些音视频进行传输的话,由于视频体积大,占用的带宽资源会很多,容易造成资源的浪费。这时就需要对采集到的视频进行编码处理。视频的编码分为软编码和硬编码。音频进行编码也分为软编和硬编两种。

手机直播源码

四、打包

手机直播源码开发中的打包处理是音视频在传输过程中需要定义相应的格式,这样传输到对端的时候才能正确地被解析出来。打包是需要TCP协议(Transmission Control Protocol )的。

TCP是一种传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP需要给每一个包一个序号,在保证文件传输的可靠性的同时也保证了从传送到接收端实体包的顺序接收。

五、差网络处理

手机直播源码开发中视频的传输是在网络状况好的情况下,视频传输流畅,不会堆积音视频,也不会造成视频延迟卡顿,而网络状况不好的情况下,音视频数据发不出去,就容易造成卡顿延迟,这样就需要我们采用缓存区设计、丢帧处理、网络检测、降码率对音视频进行处理。

六、发送

手机直播源码开发中这一步是最简单的了,需要将经过各种处理的数据发送出去,不论是RTMP还是HTTP-HLV都可以通过TCP建立连接。

拉流技术

拉流是指服务器已有直播内容,根据协议类型(如RTMP、RTP、RTSP、HTTP等),与服务器建立连接并接收数据,进行拉取的过程。拉流端的核心处理在播放器端的解码和渲染,在互动直播中还需集成聊天室、点赞和礼物系统等功能。

拉流端现在支持RTMP、HLS、HDL(HTTP-FLV)三种协议,其中,在网络稳定的情况下,对于HDL协议的延时控制可达1s,完全满足互动直播的业务需求。RTMP是Adobe的专利协议,开源软件和开源库都支持的比较好,延时一般在1-3秒。HLS是苹果提出的基于HTTP的流媒体传输协议,优先是跨平台性比较好,HTML5可以直接打开播放,移动端兼容性良好,但是缺点是延迟比较高。

以上是音视频开发中直播的推流与拉流简单介绍;更多音视频的技术开发学习,前往传送直达↓↓↓ :link.juejin.cn/?target=htt…参考学习。需要技术要点都记录在此文档之中。

推流和拉流的区别?

用户最终看到一条直播视频,可能会通过两种方式达成,一种是主动把内容传输到服务器,缓存在边缘节点,终端用户随时可以调取观看,这是推流;一种是当终端用户想要看某个内容时,将内容从源站调取,并缓存到边缘节点,这是拉流。