这是个音视频的入门项目,实现的功能是:将RV1126采集到的音视频数据通过FFmpeg库以FLV协议复合流的形式推流至RTMP服务器(nginx搭建的)。
整个项目大致细节如下图所示,具体细节在下文会一一提及:
第一部分:RV1126采集及编码部分
整个项目的开头从VI模块开始,当然如果有音频就还需要AI,这里先讲视频部分的。我们先介绍一下RV1126的VI模块。
1.1 VI模块:
OK,可以看到下方是一个VI的初始化代码,步骤就是:1、设置VI模块属性;2、EnableVI模块通道;3、开启VI流。
#include <stdio.h>
#include "stdint.h"
#include "rkmedia_api.h"
#include "rkmedia_venc.h"
#define VI_PIPE_ID 0
#define VI_CHN_ID 0
typedef struct rv1126_vi_devMsg{
uint8_t chn_id;
VI_CHN_ATTR_S vi_attr;
}rv1126_vi_devMsg_t;
int rv1126_vi_init(rv1126_vi_devMsg_t* vi){
if(!vi){
return -1;
}
vi->vi_attr.enBufType = VI_CHN_BUF_TYPE_MMAP;
vi->vi_attr.enPixFmt = IMAGE_TYPE_NV12;
vi->vi_attr.enWorkMode = VI_WORK_MODE_NORMAL;
vi->vi_attr.pcVideoNode = "rkispp_scale0";
vi->vi_attr.u32BufCnt = 3;
vi->vi_attr.u32Width = 1920;
vi->vi_attr.u32Height = 1080;
int ret = RK_MPI_VI_SetChnAttr(VI_PIPE_ID, vi->chn_id, &(vi->vi_attr));
if(ret){
printf("VI set failed!\n");
return -1;
}
printf("VI set success....\n");
ret = RK_MPI_VI_EnableChn(VI_PIPE_ID, vi->chn_id);
if(ret){
printf("VI enable failed!\n");
return -1;
}
printf("VI enable success....\n");
ret = RK_MPI_VI_StartStream(VI_PIPE_ID, vi->chn_id);
if(ret){
printf("VI start failed!\n");
return -1;
}
printf("VI start success....\n");
return 0;
}
我们首先看到 VENC_CHN_ATTR_S 这个结构体,它是用来描述我的VI模块(视频输入模块)的器件属性。其中:
1、Width、Height为当前你的摄像头输入视频分辨率的宽和高。
2、pcVideoNode,视频的输入节点,如下图所示:
3、enworkMode,VI的工作模式,有VI_WORK_MODE_NORMAL 和 VI_WORK_MODE_LUMA_ONLY两种工作类型,VI_WORK_MODE_LUMA_ONLY模式下,VI 模块只捕捉图像的亮度信息 (luma),而忽略色度信息 (chroma)。所以一般我们没有特殊需要,就使用VI_WORK_MODE_NORMAL即可。
4、enPixfmt,VI模块YUV存储方式的选择,我们这里选择YUV NV12的Semi-Planar存储方式,即四个pixel的Y(亮度)值对应一组UV(色度)。由于人类对色度不像亮度敏感,因此这样的采集方式是可以保证图片在人看起来没什么差异的情况下还能压缩一部分存储空间的。
有关YUV的采集格式分类和存储形式分类参考:YUV格式详解【全】-优快云博客
5、enBufType,这个是VI模块的一个缓冲区类型选项。共有两个选项,VI_CHN_BUF_TYPE_DMA 和 VI_CHN_BUF_TYPE_MMAP。后者的好处在于:当使用 MMAP
模式时,内核空间的缓冲区会直接映射到用户空间,应用程序可以直接访问这些内存区域。减少了数据复制的开销,提高了数据传输的效率。所以这里选择用MMAP存储映射的缓冲方式。
6、u32BufCnt,帧缓冲区数量,这个和帧缓存的概念差不多,他会在这里缓存一些帧内容,防止掉帧等问题。这里数量我们默认选择3。
经过上述步骤,可以把VI模块初始化成功。
1.2 RGA模块:
RGA(Raster Graphic Acceleration Unit),这个模块的作用其实就是实现对图像的裁剪、缩放、旋转、格式转换(如NV12转NV21)、图片叠加则需要单独调用librga.so库。
#include <stdio.h>
#include "stdint.h"
#include "rkmedia_api.h"
#include "rkmedia_venc.h"
#define RGA_CHN_ID 0
typedef struct rv1126_rga_devMsg{
uint8_t chn_id;
RGA_ATTR_S rga_attr;
}rv1126_rga_devMsg_t;
int rv1126_rga_init(rv1126_rga_devMsg_t* rga){
if(!rga){
return -1;
}
rga->rga_attr.stImgIn.imgType = IMAGE_TYPE_NV12;
rga->rga_attr.stImgIn.u32Width = 1920;
rga->rga_attr.stImgIn.u32Height = 1080;
rga->rga_attr.stImgIn.u32HorStride = 1920;
rga->rga_attr.stImgIn.u32VirStride = 1080;
rga->rga_attr.stImgIn.u32X = 0;
rga->rga_attr.stImgIn.u32Y = 0;
rga->rga_attr.stImgOut.u32Y = 0;
rga->rga_attr.stImgOut.imgType = IMAGE_TYPE_NV12;
rga->rga_attr.stImgOut.u32Width = 1280;
rga->rga_attr.stImgOut.u32Height = 720;
rga->rga_attr.stImgOut.u32HorStride = 1280;
rga->rga_attr.stImgOut.u32VirStride = 720;
rga->rga_attr.stImgOut.u32X = 0;
rga->rga_attr.bEnBufPool = RK_TRUE;
rga->rga_attr.u16BufPoolCnt = 3;
rga->rga_attr.u16Rotaion = 0;
int ret = RK_MPI_RGA_CreateChn(rga->chn_id, &(rga->rga_attr));
if(ret){
printf("rga create failed!\n");
return -1;
}
printf("rga create success....\n");
return 0;
}
设置 RGA 模块,就需要设置 RGA_ATTR_S 结构体属性:
1、stImgIn 和 stImgOut:分别对应RGA模块输入视频的各个参数和输出视频的各个参数。每个都有(1)u32X,u32Y,坐标原点;(2)Width 和 Height,当前视频的分辨率宽和高;(3)u32HorStride 和 u32VirStride,虚宽 和 虚高,。这里虚宽虚高必须要大于等于Width 和 Height,这个虚宽高如果设定的比Width 和 Height大,目的可能是为了字节对齐或是减少cache miss的情况等。一般虚宽高设置和宽高一致即可;(4)imgType,视频存储形式。
2、u16BufPoolCnt 和 bEnBufPool:缓冲区数量及是否使能缓冲区的标志。
3、Rotation,旋转角度,取值范围:0,90,180,270。
设置完成 RGA 的属性后,开启通道即可,但是后续需要 Bind VI 和 RGA的通道,这样才能正常得到视频流。
1.3VENC模块:
VENC模块是RV1126的视频编码模块,支持 H264/H265/MJPEG/JPEG 编码。在本项目中我们使用 H.264/AVC 压缩视频数据。
我们先简单讲述以下H.264压缩的实现:
1.3.1 H.264/AVC:
VAL层:
首先,我们会将一个图片分割成一个个小的blocks,如图:
在H.264中,一般是16x16的blocks,并且根据图片细节,我们会将这些个16x16的小blocks再切分成16x8,8x16,4x4等更小的blocks。
接着,这些被细分的blcoks都会通过一个系统,如下图:
其中:
(1)input就是那些一个个的small blocks;这里有个error,代表的就是帧内预测图像和帧间预测图像和原始图像的残差信息。
(2)T、Q是DCT变换、量化的过程,T-1、Q-1就是反变换和反量化过程。DCT将图像分成由不同频率组成的小块,然后进行量化。在量化过程中,舍弃高频分量,剩下的低频分量被保存下来用于后面的图像重建。效果如下图:
所以其实这一部分实际是有损压缩,将人眼并不敏感的高频分量舍弃以获取最大的压缩比。
(3)Intra Prediction:帧内预测,通过已经编码好的像素块,以不同方向预测其他像素块数据的方式。看下面的动态图,H.264的话只支持九种方向预测(这里有些忘记了)。通过这样的方式,我们就可以减少原始数据量的传输和存储,从而压缩帧内的空间冗余。
实际例子(这里用一下别的大佬的原图),可以看见预测的基本不差:
(4)Inter Prediction:帧间预测,减少了视频的时间冗余。第一步,编码器在参考帧中寻找与当前块最相似的区域。这个过程称为运动估计(Motion Estimation)。第二步,找到最匹配的区域后,编码器计算该区域与当前块之间的位移,称为运动矢量(Motion Vector)。第三步,使用运动矢量对参考帧进行补偿,生成预测块。这个过程称为运动补偿(Motion Compensation)。通过这种方法,编码器不需要重新存储当前块的完整图像信息,只需要传输运动矢量和后续对比实际块的残差,大大减少数据量。
(5)Entropy Coding:熵编码,数据压缩中的一种无损编码技术,用于去除冗余信息,从而减少数据的表示长度。
到最后其实一个个small blocks都会以下图形式进行循环,熵编码记录帧的残差信息,预测好的块用于下一次的帧内预测和进入图片缓冲区组合成一个完整的帧供帧间预测的使用:
NAL层:
所有熵编码的bit流和Picture Buffer中的图像数据都会在该层转化成一个个NALU单元,最后按下述格式传输(当然,可以没有B frame):
每个NALU = [start code] + [NALU header] + [NALU payload]
start code:一般分为0x000001和0x00000001两种
NALU header:NALU类型(5bit)、重要性位(2bit)、禁止位(1bit)
下图是NALU的TYPE类型对应值:
SPS(Sequence Parameter Set):SPS为解码器提供了解码整个视频序列所需的全局信息。图像的宽度和高度、帧率(时间信息)、编码配置文件和级别、颜色格式和色度、取样参考帧的最大数量、视频格式(如