使用的ffmpeg版本为5.0.1,SDL的版本为2.022。c++环境为vs2017。
先上最简易的整体代码,初步实现了SDL和ffmpeg的结合。
#include#include #include extern "C" { #include "libavformat/avformat.h" //头文件不仅要在项目中鼠标点击配置,在代码中也要引入 #include "include/libavformat/avformat.h" #include "include/libswscale/swscale.h" #include "include/libavdevice/avdevice.h" #include"libavcodec/avcodec.h" } using namespace std; int main(int argc, char *argv[]) { int ret = 1; const char *file = "ds.mov"; // 初始化ffmpeg组件 AVFormatContext *pFormatCtx = nullptr; //视频文件上下文 int videostream; //视频流标识 AVCodecParameters *pCodeParameters = nullptr; //解码器相关参数 const AVCodec *pCodec = nullptr; //解码器 AVCodecContext *pCodecCtx = nullptr; // 解码器上下文 AVFrame *pFrame = nullptr; //解码后的帧 AVPacket packet; // 解码前的帧 //初始化SDL组件 SDL_Rect rect; //渲染显示面积 SDL_Window *window = NULL; // 窗口 SDL_Renderer *renderer = NULL; // 渲染 SDL_Texture *texture = NULL; // 纹理 Uint32 pixformat; //视频分辨率 int w_width = 640; int w_height = 480; //SDL初始化 if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) { cout << "can not initialize SDL" << endl; return ret; } //FFMPEG 视频文件读取 if (avformat_open_input(&pFormatCtx, file, nullptr, nullptr) != 0) { cout << "can not open the video file" << endl; goto __FAIL; } //FFMPEG 寻找视频流 videostream = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1,nullptr,0); if (videostream == -1) { cout << "can not open a video stream" << endl; goto __FAIL; } //FFMPEG 寻找合适的解码器 pCodeParameters = pFormatCtx->streams[videostream]->codecpar; pCodec = avcodec_find_decoder(pCodeParameters->codec_id); if (pCodec == nullptr) { cout << "can not find a codec" << endl; goto __FAIL; } //FFMPEG 解码器信息配置 pCodecCtx = avcodec_alloc_context3(pCodec); if (avcodec_parameters_to_context(pCodecCtx, pCodeParameters) != 0) { cout << "can not copy codec context" << endl; goto __FAIL; } //FFMPEG 解码器启动 if (avcodec_open2(pCodecCtx, pCodec, nullptr) < 0) { cout << " can not open the decoder" << endl; goto __FAIL; } //FFMPEG 初始化解码的帧 pFrame = av_frame_alloc(); //SDL 获得显示的视频画面的长度与宽度 w_width = pCodecCtx->width; w_height = pCodecCtx->height; //SDL 窗口初始化 window = SDL_CreateWindow("MEDIA PLAYER", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, w_width, w_height, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); if (!window) { cout << "can not create window" << endl; goto __FAIL; } //SDL 渲染器初始化 renderer = SDL_CreateRenderer(window, -1, 0); if (!renderer) { cout << "can not create renderer!" << endl; goto __FAIL; } //SDL 视频格式与纹理初始化 pixformat = SDL_PIXELFORMAT_IYUV; texture = SDL_CreateTexture(renderer, pixformat, SDL_TEXTUREACCESS_STREAMING, w_width, w_height); //主循环 while (av_read_frame(pFormatCtx,&packet)>= 0) {//FFMPEG 如果已经读到了一个帧 if (packet.stream_index == videostream) {//并且该帧是视频帧 avcodec_send_packet(pCodecCtx,&packet); while (avcodec_receive_frame(pCodecCtx, pFrame) == 0) { //SDL 刷新纹理 SDL_UpdateYUVTexture(texture, NULL, pFrame->data[0], pFrame->linesize[0], pFrame->data[1], pFrame->linesize[1], pFrame->data[2], pFrame->linesize[2]); rect.x = 0;//SDL设置渲染目标的显示区域 rect.y = 0; rect.w = pCodecCtx->width; rect.h = pCodecCtx->height; SDL_RenderClear(renderer);//SDL 清空渲染器内容 SDL_RenderCopy(renderer, texture, NULL, &rect);//SDL 将纹理复制到渲染器 SDL_RenderPresent(renderer);//SDL 渲染 } } av_packet_unref(&packet); SDL_Event event;// SDL事件 SDL_PollEvent(&event);// 轮询事件 switch (event.type) { case SDL_QUIT: //如果窗口被关闭 goto __QUIT; default: break; } } __QUIT: ret = 0; __FAIL: if (pFrame) { av_frame_free(&pFrame); pFrame = nullptr; } if (pCodecCtx) { avcodec_close(pCodecCtx); pCodecCtx = nullptr; pCodec = nullptr; } if (pCodeParameters) { avcodec_parameters_free(&pCodeParameters); pCodeParameters = nullptr; } if (pFormatCtx) { avformat_free_context(pFormatCtx); pFormatCtx = nullptr; } if (texture) { SDL_DestroyTexture(texture); texture = nullptr; } if (renderer) { SDL_DestroyRenderer(renderer); renderer = nullptr; } if (window) { SDL_DestroyWindow(window); window = nullptr; } SDL_Quit(); cout << "succeed!" << endl; return ret; }
以上代码在关键函数的注释中都已经注明了其属于哪个模块,有助于拆解学习。
以上代码的大部分内容在之前的文章都有所介绍,这里主要讲解新出现的知识。
av_read_frame(pFormatCtx,&packet)在对视频的帧进行解码时,首先我们需要将帧从视频文件中提取出来,注意当前的帧是未解压缩的,属于packet。复习一下,packet是未解码的帧,frame是解码完成了的帧。
av_read_frame()实现的正是此功能,该函数确切的说是从视频文件中读取一个packet出来。并且该函数可以保证读取的帧是完整的。
而在之前的文章中提到,一个packet只含有一个视频帧,但是可以包含多个音频帧。
对于音频,如果每个帧具有已知的固定大小(例如PCM或ADPCM数据),则它包含整数帧数。 如果每个帧的大小可变(MPEG),则包含一个帧。
通过将此函数作为主循环的工作条件,可以保证在从视频文件中读到多媒体帧后就工作,读不到多媒体帧就不工作。
avcodec_send_packet(pCodecCtx,&packet)该函数正是ffmpeg对视频进行解码操作的函数,该函数的工作内容和名字一样,将packet数据传入到codec进行解码,得到解码后的frame数据。
avcodec_receive_frame(pCodecCtx, pFrame)该函数是与解码操作配合的函数,在codec解码器将packet解码为frame后使用该函数将frame读取出来,frame变量就是当前的解码的帧。
这里注意一下,avcodec_receive_frame(pCodecCtx, pFrame)与avcodec_send_packet(pCodecCtx,&packet)这两个函数是一一对应的么?其实应该不是,因为上文提过,一个packet可能解出多个frame出来(在音频帧的情况下),因此send函数应该是对应多个receive函数的。
当我们不知道一个函数会执行多少次时,我们可以将此函数放入while()循环的判断语句中,当函数的返回值发生变化,说明该函数不需要再被执行,循环内的相关处理语句也不会再执行。
SDL与ffmpeg的结合点通过之前文章可以知道,SDL是通过将视频帧“刷”到纹理上,之后实现渲染播放的,我们将ffmpeg解码后的frame刷到纹理上,就实现了两者的结合。
SDL_UpdateYUVTexture(texture, NULL, pFrame->data[0], pFrame->linesize[0], pFrame->data[1], pFrame->linesize[1], pFrame->data[2], pFrame->linesize[2]);
刷新纹理的函数中,分别对y、u、v三个通道的数据进行了加载,等于是刷进了纹理,之后的操作就和 SDL播放视频没有什么区别了。



