在前一篇中的解封装的最后,把音视频的压缩数据分别放入了视频和音频的队列中,接下来就是在视频播放部分从队列中取出压缩数据进行解码播放。
主要的结构体和函数释义 结构体 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:释放上下文空间
解码播放流程 两个队列
存放压缩数据的队列 queue<AVPacket *>
存放解码后原始数据的队列 queue<AVFrame *>
两个线程
第一个线程是取出队列的压缩包 进行解码,解码后的原始包 丢到frame对别中去
第二个线程是从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 ); pthread_create (&pid_video_decode, nullptr , task_video_decode, this ); 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 ; } res = avcodec_send_packet (codecContext, packet); if (res){ LOGE ("video channel decode AVPacket fail !!!" ) releaseAVPacket (&packet); break ; } AVFrame *frame = av_frame_alloc (); res = avcodec_receive_frame (codecContext, frame); if (res == AVERROR (EAGAIN)){ 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 ]; int dst_lineSize[4 ]; av_image_alloc (dest_data, dst_lineSize, codecContext->width, codecContext->height, AV_PIX_FMT_RGBA, 1 ); 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); 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); ANativeWindow_Buffer windowBuffer; if (ANativeWindow_lock (window, &windowBuffer, nullptr )){ LOGE ("window is locked!!!" ); ANativeWindow_release (window); window = nullptr ; pthread_mutex_unlock (&mutex); return ; } 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); } 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);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 #elif HAVE_SIMD_ALIGN_32 # define STRIDE_ALIGN 32 #elif HAVE_SIMD_ALIGN_16 # define STRIDE_ALIGN 16 #else # define STRIDE_ALIGN 8
那么图像宽为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