[SimplePlayer] 1. 从视频文件中提取图像
在开始之前,我们需要了解视频文件的格式。视频文件的格式众多,无法三言两语就能详细分析其结构,尽管如此,ffmpeg却很好地提取了各类视频文件的共同特性,并对其进行了抽象描述。
视频文件格式,统称为container。它包含一个描述视频信息的头部,以及内含实际的音视频编码数据的packets。当然,这里的头部以及packet部分只是个抽象描述,实际的视频格式的描述信息可能不是存放在视频文件的起始位置,可能是由分散于视频文件的各个位置的多个部分组成;数据包有可能是由头部以及尾部进行分割的传统数据包形式,也有可能是一大块数据区域,由索引进行各个数据包的分割。
视频文件中的packets最主要的就是视频以及音频packets,demux的过程就是解析container的header来获取视频信息,所得到的视频信息能帮助我们区分packet是音频或者视频。同样属性的packets会被称为stream。
packet中存储的数据就是音视频编码后的数据,通过解码器进行decode后就能得到视频图像或者音频帧。其中需要注意的一点是,一个packet不一定对应一帧,packet的顺序也不一定是实际的播放顺序,而通过ffmpeg解码出来的frame的顺序就是实际的播放顺序。
Demux
首先需要一个用于存储视频文件信息的结构体。
1 | pFormatCtx = avformat_alloc_context(); |
读取视频文件,并对该文件进行demux,所得到的视频信息存储于刚刚所构建的结构体当中
1 2 3 4 | if (avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0){ fprintf(stderr, "open input failed\n" ); return -1; } |
如果pFormatCtx=NULL,那么avformat_open_input也能自动为pFormatCtx分配存储空间。
对于有些视频格式,单单通过demux并不能获得所有的视频信息,为了获得这些信息,还需要读取并尝试解码该视频几个最前端packets(通常会解码每个stream第一个packet)。所读取的这几个packets会被缓存以供后续处理。
1 2 3 4 | if (avformat_find_stream_info(pFormatCtx, NULL)<0){ fprintf(stderr, "find stream info failed\n" ); return -1; } |
从所获得的信息当中得到video stream序号,后续可以通过stream序号来对packet进行筛选。
1 | videoStream = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); |
Decode
创建一个用于存储以及维护解码信息结构体。
1 | pCodecCtx = avcodec_alloc_context3(NULL); |
把demux时所获得的视频相关信息传递到解码结构体中。
1 2 3 4 | if (avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[videoStream]->codecpar)<0){ fprintf(stderr, "copy param from format context to codec context failed\n" ); return -1; } |
根据解码器id来寻找对应的解码器
1 2 3 4 5 6 7 | pCodec = avcodec_find_decoder(pCodecCtx->codec_id); if (pCodec==NULL){ fprintf(stderr, "Unsupported codec,codec id %d\n" , pCodecCtx->codec_id); return -1; } else { fprintf(stdout, "codec id is %d\n" , pCodecCtx->codec_id); } |
打开该解码器,主要目的是对解码器进行初始化
1 2 3 4 | if (avcodec_open2(pCodecCtx, pCodec, NULL)<0){ fprintf(stderr, "open codec failed\n" ); return -1; } |
创建一个用于维护所读取的packet的结构体,一个用于维护解码所得的frame的结构体
1 2 3 4 5 6 | pPacket = av_packet_alloc(); pFrame = av_frame_alloc(); if (pFrame == NULL||pPacket == NULL){ fprintf(stderr, "cannot get buffer of frame or packet\n" ); return -1; } |
从视频文件中读取packet,如果所读取的packet是video,则进行解码,解码所得的帧由pFrame进行维护。当然,并不是每次调用avcodec_decode_video2都会返回一帧,因为也可能会有需要多个packet才能解码出一帧的情况,因此只有当指示一帧是否解码完成的frameFinished为1才能对这一帧进行后续处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | while (av_read_frame(pFormatCtx, pPacket)>=0){ //Only deal with the video stream of the type "videoStream" if (pPacket->stream_index==videoStream){ //Decode video frame avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, pPacket); //fprintf(stdout, "Frame : %d ,pts=%lld, timebase=%lf\n", i, pFrame->pts, av_q2d(pFormatCtx->streams[videoStream]->time_base)); if (frameFinished){ if (i>=START_FRAME && i<=END_FRAME){ SaveFrame2YUV(pFrame, pCodecCtx->width, pCodecCtx->height, i); i++; } else { i++; continue ; } } } av_packet_unref(pPacket); } |
当一个packet被解码后就可以调用av_packet_unref来释放该packet所占用的空间了。
Store
视频文件解码出来后通常都是YUV格式,Y、U、V三路分量分别存储在AVFrame的data[0]、data[1]、data[2]所指向的内存区域。linesize[0]、linesize[1]、linesize[2]分别指示了Y、U、V一行所占用的字节数。下面把解码所得的帧保存为YUV Planar格式。
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 | void SaveFrame2YUV(AVFrame *pFrame, int width, int height, int iFrame){ static FILE *pFile; char szFilename[32]; int y; //Open file if (iFrame==START_FRAME){ sprintf(szFilename, "Video.yuv" ); pFile = fopen(szFilename, "wb" ); if (pFile==NULL) return ; } //Write YUV Data, Only support YUV420 //Y for (y=0; y<height; y++){ fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, pFrame->linesize[0], pFile); } //U for (y=0; y<(height+1)/2; y++){ fwrite(pFrame->data[1]+y*pFrame->linesize[1], 1, pFrame->linesize[1], pFile); } //V for (y=0; y<(height+1)/2; y++){ fwrite(pFrame->data[2]+y*pFrame->linesize[2], 1, pFrame->linesize[2], pFile); } //Close FIle if (iFrame==END_FRAME){ fclose(pFile); } } |
最后就是释放内存,关闭decoder,关闭demuxer
1 2 3 4 | av_free(pPacket); av_free(pFrame); avcodec_close(pCodecCtx); avformat_close_input(&pFormatCtx); |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架