FFmpeg之视频播放

在前一篇中的解封装的最后,把音视频的压缩数据分别放入了视频和音频的队列中,接下来就是在视频播放部分从队列中取出压缩数据进行解码播放。

主要的结构体和函数释义

结构体

AVCodecContext

1
相关函数:avcodec_alloc_context3(申请一块AVCodecContext空间)、avcodec_free_context(释放AVCodecContext空间)

AVFrane

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1> uint8_t *data[AV_NUM_DATA_POINTERS]:数据内容,视频表示一行数据,音频表示一个通道数据

2> int line_size[AV_NUM_DATA_POINTERS]:数据大小,视频表示一行数据大小,音频表示一个通道数据大小

3> int width, int height:视频参数

4> int nb_samples:音频参数,表示单通道的样本数量,如1024,一个样本2个字节

5> int64_t pts,int64_t dts:显示时间、解码时间

6> int sample_rate, uint64_t channel_layout, int channels:音频参数

7> format:像素格式

相关函数av_frame_alloc(申请内存)、av_frame_free(释放内存)、av_frame_ref(增加引用计数)、av_frame_clone(拷贝frame)、av_frame_unref(减少引用计数)

函数

1> avcodec_register_all:注册所有的解码器

2> avcodec_find_decoder:查找对应的解码器

1
2
3
@param enum AVCodecID id:解码器id

@return AVCodec *:解码器

3> avcodec_find_decoder_by_name:通过名字查找解码器

4> avcodec_open2:打开

1
2
3
4
5
@param AVCodecContext *avctx:解码上下文

@param AVCodec *codec:解码器,如果上下文alloc已经指定avcodec可以不传

@param AVDictionary **options:参数

5> avcodec_parameters_to_context:复制解码参数,主要用于从stream->codecpar拷贝参数

6> avcode_send_packet:异步解码发送数据

1
2
3
@param AVCodecContext *avctx:解码上下文

@param AVPacket *avpkt:packet数据

7> avcodec_receive_frame:异步解码接收数据

1
2
3
@param AVCodecContext *avctx:解码上下文

@param AVFrame *frame:解码后的frame

8> avcodec_close:关闭解码器

sws_getContext:获取上下文

sws_getCachedContext:获取上下文,与sws_getContext区别在于内存空间由调用者自己管理

1
2
3
4
5
6
7
8
9
10
11
12
13
@param SwsContext *context:格式转换上下文

@param int srcW, int srcH, enum AVPixelFormat srcFormat:输入格式

@param int dstW, int dstH, enum AVPixelFormat dstFormat:输出格式

@param int flags:转换算法,如线性差值算法

@param SwsFilter *srcFilter:通常不用

@param SwsFilter *dstFilter:通常不用

@param const double **param:算法参数,不涉及可不指定

11> sws_scale:转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@param SwsContext *context:格式转换上下文

@param const uint8_t *const srcSlice[]:源数据数组,二维数组数据

@param const int srcStride[]:一行数据的宽度,linesize

@param int srcSliceY:用不到

@param int srcSliceH:图像高度,遍历一行行数据

@param uint8_t *dst[]:输出数据地址

@param const int dstStride[]:输出数据长度

@return:输出的数据高度

sws_freeContext:释放上下文空间

解码播放流程

两个队列

  1. 存放压缩数据的队列 queue<AVPacket *>
  2. 存放解码后原始数据的队列 queue<AVFrame *>

两个线程

  1. 第一个线程是取出队列的压缩包 进行解码,解码后的原始包 丢到frame对别中去
  2. 第二个线程是从frame队列中取出原始包进行播放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void VideoChannel::start() {
isPlaying = true;

packets.setWork(1);
frames.setWork(1);

//取出队列的压缩包 解码 解码后的原始包push到frame队列中去
pthread_create(&pid_video_decode, nullptr, task_video_decode, this);

//从frame队列中取出原始包 播放
pthread_create(&pid_video_play, nullptr, task_video_play, this);

LOGD("video channel start")
}

解码

解码可以认为是AVPacket 到 AVFrame的过程

把队列里面的压缩包(AVPacket *)取出来,然后解码成(AVFrame * )原始包 —-> 保存队列

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
void VideoChannel::video_decode() {
AVPacket *packet = nullptr;
while (isPlaying){
int res = packets.getQueueAndDel(packet);
if(!isPlaying){
releaseAVPacket(&packet);
break;
}
if(!res){
continue;
}
// 最新的FFmpeg,和旧版本差别很大, 新版本:1.发送pkt(压缩包)给缓冲区, 2.从缓冲区拿出来(原始包)
res = avcodec_send_packet(codecContext, packet);
if(res){
LOGE("video channel decode AVPacket fail !!!")
releaseAVPacket(&packet);
break;// avcodec_send_packet 出现了错误,结束循环
}
//下面是从FFmpeg缓冲区获取原始包
AVFrame *frame = av_frame_alloc();
res = avcodec_receive_frame(codecContext, frame);
if(res == AVERROR(EAGAIN)){
// B帧 B帧参考前面成功 B帧参考后面失败 可能是P帧没有出来,再拿一次就行了
continue;
}else if(res != 0){
LOGE("video channel receive AVFrame fail !!!")
releaseAVPacket(&packet);
break;//错误了
}
if(isPlaying){
//拿到了原始包
frames.insert(frame);
}
releaseAVPacket(&packet);
}
releaseAVPacket(&packet);
}

原始包数据进行转换(YUV->RGBA)

把frames队列中的原始包AVFrame *取出来播放原始包YUV数据 使用libswscale 转换为RGBA数据

由于FFmpeg在解码视频时一般情况而言视频数据会被解码为YUV数据,而ANativeWindow并不能直接 显示YUV数 据的图像,所以需要将YUV转换为RGB进行显示。而FFmpeg的swscale模块就提供了颜色空 间转换的功能。 FFmpeg的swscale转换效率可能存在问题,如ijkPlayer中使用的是google的libyuv库进 行的转换。

转换算法
SWS_FAST_BILINEAR == 很快 可能会模糊
SWS_BILINEAR 适中算法

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
47
48
void VideoChannel::video_play() {

AVFrame *frame = nullptr;
uint8_t *dest_data[4];//RGBA 4字节
int dst_lineSize[4];//RGBA

av_image_alloc(dest_data, dst_lineSize,
codecContext->width, codecContext->height,
AV_PIX_FMT_RGBA, 1);

//YUV -> RGBA
SwsContext *swsContext = sws_getContext(
//输入
codecContext->width,
codecContext->height,
codecContext->pix_fmt,
//输出
codecContext->width,
codecContext->height,
AV_PIX_FMT_RGBA,
SWS_BILINEAR,nullptr, nullptr, nullptr);
while (isPlaying){
int res = frames.getQueueAndDel(frame);
if(!isPlaying){
releaseAVFrame(&frame);
break;//如果关闭了播放,跳出循环
}
if(!res){
continue;//有可能生产者队列填充数据过慢,需要等一等
}

sws_scale(swsContext,
//输入
frame->data, frame->linesize,
0, codecContext->height,
//输出
dest_data, dst_lineSize);

//ANativeWindow渲染
if(renderCallback && isPlaying)
renderCallback(dest_data[0], codecContext->width, codecContext->height, dst_lineSize[0]);
releaseAVFrame(&frame);//释放原始包,因为已经被渲染过了,没用了
}
releaseAVFrame(&frame);
isPlaying = false;
av_freep(dest_data[0]);
sws_freeContext(swsContext);
}

使用ANativeWindow进行画面渲染

实例化ANativeWindow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
extern "C"
JNIEXPORT void JNICALL
Java_cn_jiajunhui_lib_jjhplayer_JJHPlayer_nativeSetSurface(JNIEnv *env, jobject thiz,
jobject surface) {
pthread_mutex_lock(&mutex);

//先释放之前的显示窗口
if(window){
ANativeWindow_release(window);
window = nullptr;
}

//创建新的窗口用于视频显示
window = ANativeWindow_fromSurface(env, surface);

LOGD("set surface init window")

pthread_mutex_unlock(&mutex);
}

向ANativeWindow渲染数据

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
//函数指针
//实现渲染
void renderCallback(uint8_t *src_data, int width, int height, int src_lineSize){
pthread_mutex_lock(&mutex);
if(!window){
pthread_mutex_unlock(&mutex);//出现问题后,必须考虑到,释放锁,避免出现死锁问题
return;
}

//设置窗口的大小,各个属性
ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888);

//window的缓冲区buffer
ANativeWindow_Buffer windowBuffer;

//如果我在渲染的时候是被锁住的,那我就无法渲染,我需要释放,防止出现死锁
if(ANativeWindow_lock(window, &windowBuffer, nullptr)){
LOGE("window is locked!!!");
ANativeWindow_release(window);
window = nullptr;
pthread_mutex_unlock(&mutex);
return;
}

//开始真正渲染,window没有被锁住 把RGBA数据 ---> 字节对齐 渲染
uint8_t *dst_data = static_cast<uint8_t *>(windowBuffer.bits);
int dst_lineSize = windowBuffer.stride * 4;

for(int i=0;i<windowBuffer.height;i++){
memcpy(dst_data + i * dst_lineSize, src_data + i * src_lineSize, dst_lineSize);
}

//数据刷新
//解锁后 并且刷新 window_buffer 的数据显示画面
ANativeWindow_unlockAndPost(window);

pthread_mutex_unlock(&mutex);
}

渲染时的字节对齐

在得到了RGBA格式的时候后就可以向ANativeWindow填充。但是在数据填充时,需要根据 window_buffer.stride 来一行行拷贝,如:

1
2
3
4
5
6
7
8
uint8_t *dst_data = static_cast<uint8_t *>(window_buffer.bits);
//一行需要多少像素 * 4(RGBA)
int32_t dst_linesize = window_buffer.stride * 4; uint8_t *src_data = data; //需要显示的数据
int32_t src_linesize = linesize; //数据每行字节数 //一次拷贝一行
for (int i = 0; i < window_buffer.height; ++i) {
memcpy(dst_data + i * dst_linesize, src_data + i * src_linesize,
src_linesize);
}

以我们播放的852x480视频为例,在将ANativeWindow的格式设置为同样大小后,得到的 window_buffer.stride为 864,则每行需要864*4 = 3456个字节数据。而将视频解码数据转换为RGBA 之后获得的linesize为3408。window 与图像数据的每行数据数不同,所以需要一行行拷贝。

为什么会出现不同? 无论是window的stride还是ffmpeg的linesize只会出现比widget大的情况,这意味 着不可能出现图像数据缺失的情 况,但是为什么会比widget大呢?这是由于字节对齐不同导致的。在编 译FFmpeg时,会在FFmpeg源码根目录下 生成一个config.h文件,这个文件中根据编译目标平台的特性 定义了一些列的宏,其中:

1
2
3
#define HAVE_SIMD_ALIGN_16 0
#define HAVE_SIMD_ALIGN_32 0
#define HAVE_SIMD_ALIGN_64 0

这三个宏表示的就是FFmpeg中数据的以几字节对齐。在目标为android arm架构下,均为0。则 FFmpeg使用8字 节对齐( libavcodec/internal.h )

1
2
3
4
5
6
7
8
#if HAVE_SIMD_ALIGN_64
# define STRIDE_ALIGN 64 /* AVX-512 */
#elif HAVE_SIMD_ALIGN_32
# define STRIDE_ALIGN 32
#elif HAVE_SIMD_ALIGN_16
# define STRIDE_ALIGN 16
#else
# define STRIDE_ALIGN 8 // 注意:会执行此宏,那么就是 8字节对齐 #endif

那么图像宽为852,即数据为852*4=3408的情况下,3408%8=无余数。则不需要占位字节用于对齐,因此 linesize为
3408。

而ANativeWindow中的stride计算出来结果为3456。这是因为ANativeWindow在此处是以64字节对齐, 若stride
为宽度的852,数据为3408的情况下,3408/16=53.25,此时需要占位字节将其补充为54,则 54*64=3456,所以

stride为3456以便于64字节对齐。

计算机字节对齐的原理:(来自百度百科的解释), 总结:(字节对齐可以把算法执行效率最佳化)

***各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存 取。
比如有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节 对齐.

其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对 数据存放进行对齐,会在存取 效率 上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址 开始 的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次 读出的结果的高低字节进行拼凑才能的到该32bit数据

源码地址:https://github.com/jiajunhui/ffmpeg-jjhplayer