在上一篇文章中,对FFmpeg的视频解码过程作了一个总结。因为才接触FFmpeg,仍是挺陌生的,这里就解码过程再作一个总结。
本文的总结分为如下两个部分:html
在学习的过程主要参考的是dranger tutorial,因此跟着教程在本文的最后使用SDL2.0将解码后的数据输出到屏幕上。git
一个多媒体文件包含有多个流(视频流 video stream,音频流 audio stream,字幕等);流是一种抽象的概念,表示一连串的数据元素;
流中的数据元素称为帧Frame。也就是说多媒体文件中,主要有两种数据:流Stream 及其数据元素 帧Frame,在FFmpeg天然有与这两种数据相对应的抽象:AVStream和AVPacket。github
使用FFmpeg的解码,数据的传递过程可概括以下:缓存
avformat_open_input
打开流,将信息填充到AVFormatContext
中av_read_frame
从流中读取数据帧到 AVPacket
,AVPacket
保存仍然是未解码的数据。avcodec_decode_video2
将AVPacket
的数据解码,并将解码后的数据填充到AVFrame
中,AVFrame
中保存的是解码后的原始数据。上述过程可使用下图表示:
ide
FFmpeg并无垃圾回收机制,所分配的空间都须要本身维护。而因为视频处理过程当中数据量是很是大,对于动态内存的使用更要谨慎。
本小节主要介绍解码过程使用到的结构体存储空间的分配与释放。函数
AVFormatContext 在FFmpeg中有很重要的做用,描述一个多媒体文件的构成及其基本信息,存放了视频编解码过程当中的大部分信息。一般该结构体由avformat_open_input
分配
存储空间,在最后调用avformat_input_close
关闭。学习
av_read_frame
建立和填充AVPacket中的数据缓冲区,av_free_apcket
释放这块缓冲区。av_frame_alloc
来建立,经过av_frame_free
来释放。和AVPacket相似,AVFrame中也有一块数据缓存空间,av_frame_alloc
的时候并不会为这块缓存区域分配空间,须要使用其余的方法。在解码的过程使用了两个AVFrame,这两个AVFrame分配缓存空间的方法也不相同
AVFrame* pFrameYUV; pFrameYUV = av_frame_alloc(); // 手动为 pFrameYUV分配数据缓存空间 int numBytes = avpicture_get_size(AV_PIX_FMT_YUV420P,pCodecCtx->widht,pCodecCtx->width); uint8_t* buffer = (uint8_t*)av_malloc(numBytes * sizeof(uint8_t)); // 将分配的数据缓存空间和AVFrame关联起来 avpicture_fill((AVPicture *)pFrameYUV, buffer, AV_PIX_FMT_YUV420P,pCodecCtx->width, pCodecCtx->height)
首先计算须要缓存空间大小,调用av_malloc
分配缓存空间,最后调用avpicture_fill
将分配的缓存空间和AVFrame关联起来。
调用av_frame_free
来释放AVFrame,该函数不止释放AVFrame自己的空间,还会释放掉包含在其内的其余对象动态申请的空间,例如上面的缓存空间。ui
av_malloc
只是在申请内存空间的时候会考虑到内存对齐(2字节,4字节对齐),av_free
释放。avformat_close_input
关闭打开的流avformat_find_stream_info
进一步获得流的信息。经过上面的三个函数已经获取了对多媒体文件进行解码的所须要信息,下面要作的就是根据这些信息获得相应的解码器。
结构体AVCodecContext
描述了编解码器的上下文信息,包含了流中所使用的关于编解码器的全部信息,能够经过 AVFormatContext->AVStream->AVCodecContext
来获得,在有了AVCodecContext后,能够经过codec_id来找到相应的解码器,具体代码以下:指针
AVCodec* pCodec = nullptr; pCodecCtxOrg = pFormatCtx->streams[videoStream]->codec; // codec context // 找到video stream的 decoder pCodec = avcodec_find_decoder(pCodecCtxOrg->codec_id);
通过以上的过程,AVFrame中的数据缓存中存放的就是解码后的原始数据了。整个流程梳理以下:code
使用SDL2.0,dranger tutorial中的显示视频部分的代码就不是很适用了,须要作一些修改。不过,SDL2.0显示图像仍是挺简单的。
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER); SDL_Window* window = SDL_CreateWindow("FFmpeg Decode", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, pCodecCtx->width, pCodecCtx->height, SDL_WINDOW_OPENGL | SDL_WINDOW_MAXIMIZED); SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, 0); SDL_Texture* bmp = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, pCodecCtx->width, pCodecCtx->height); SDL_Rect rect; rect.x = 0; rect.y = 0; rect.w = pCodecCtx->width; rect.h = pCodecCtx->height; SDL_Event event;
上述代码为初始化后SDL显示图像所须要的环境,在使用FFmpeg解码数据后
int frameFinished = 0; avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet); if (frameFinished) { sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize); SDL_UpdateTexture(bmp, &rect, pFrameRGB->data[0], pFrameRGB->linesize[0]); SDL_RenderClear(renderer); SDL_RenderCopy(renderer, bmp, &rect, &rect); SDL_RenderPresent(renderer); }
上面代码就将解码获得的图像帧使用SDL显示了出来。不过,这里真的只是显示而已,以可以解码速度快速的将整个视频的图像帧显示一遍。