系列索引:FFmpeg入门系列索引
上一篇:FFmpeg入门教程04.03:保存视频帧
上一篇我们解码并保存了其中的几帧确保解码过程和结果是对的。本篇我们将解码整个视频并保存为标准的YUV格式(YUV格式具体信息详见YUV格式介绍),我们就选YUV420P(I420)作为输出格式。
保存文件需要对本地文件进行读写操作,那么首先要有文件操作指针,C为FILE,C++为iostream。
以C为例。
1FILE *fp = fopen("result.yuv","w+b");扩展名任意,只要数据格式对就可以了,最好是把数据格式标识出来,比如:1280x720_yuv420p.yuv。
与上一篇文章相比,就是多了将像素值写入文件的部分。
解码流程图为:
flowchart TB
F --Yes--> I
K --下一帧--> F
I --No--> F
subgraph init
direction TB
A(开始) --> B[打开文件]
B --> C[查找流信息]
C --> D[查找对应解码器]
D --> E[打开解码器]
E --> F{读取帧}
F --No--> G(结束)
end
subgraph decode
direction TB
I{是视频帧?} --Yes--> J[发送帧给解码器]
J --> K[从解码器获取结果]
end
flowchart TB
F --Yes--> I
K --Next Frame--> F
I --No--> F
subgraph init
direction TB
A(Start) --> B[avformat_open_input]
B --> C[av_find_stream_info]
C --> D[avcodec_find_decoder]
D --> E[avcodec_open]
E --> F{av_read_frame}
F --No--> G(End)
end
subgraph decode
direction TB
I{Video Packet?} --Yes--> J[avcodec_send_packet]
J --> K[avcodec_receive_frame]
end
YUV420P格式YUV420P像素分为三个部分:Y/U/V,Y部分长度为width * height,U为width * height /4 ,V部分和U部分长度一样。(为什么会是这样,见YUV格式介绍)。
测试代码:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115#include #include #include "libavcodec/avcodec.h"#include "libavfilter/avfilter.h"#include "libavformat/avformat.h"#include "libavutil/avutil.h"#include "libavutil/ffversion.h"#include "libswresample/swresample.h"#include "libswscale/swscale.h"#include "libpostproc/postprocess.h"int main() { FILE *fp=fopen("result.yuv","w+b"); if(fp==NULL){ printf("Cannot open file.\n"); return -1; } char filePath[] = "/home/jackey/Videos/Sample.mkv";//文件地址 int videoStreamIndex = -1;//视频流所在流序列中的索引 int ret=0;//默认返回值 //需要的变量名并初始化 AVFormatContext *fmtCtx=NULL; AVPacket *pkt =NULL; AVCodecContext *codecCtx=NULL; AVCodecParameters *avCodecPara=NULL; const AVCodec *codec=NULL; AVFrame *yuvFrame = av_frame_alloc(); do{ //=========================== 创建AVFormatContext结构体 ===============================// //分配一个AVFormatContext,FFMPEG所有的操作都要通过这个AVFormatContext来进行 fmtCtx = avformat_alloc_context(); //==================================== 打开文件 ======================================// if ((ret=avformat_open_input(&fmtCtx, filePath, NULL, NULL)) != 0) { printf("cannot open video file\n"); break; } //=================================== 获取视频流信息 ===================================// if ((ret=avformat_find_stream_info(fmtCtx, NULL)) < 0) { printf("cannot retrive video info\n"); break; } //循环查找视频中包含的流信息,直到找到视频类型的流 //便将其记录下来 保存到videoStreamIndex变量中 for (unsigned int i = 0; i < fmtCtx->nb_streams; i++) { if (fmtCtx->streams[ i ]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { videoStreamIndex = i; break;//找到视频流就退出 } } //如果videoStream为-1 说明没有找到视频流 if (videoStreamIndex == -1) { printf("cannot find video stream\n"); break; } //打印输入和输出信息:长度 比特率 流格式等 av_dump_format(fmtCtx, 0, filePath, 0); //================================= 查找解码器 ===================================// avCodecPara = fmtCtx->streams[ videoStreamIndex ]->codecpar; codec = avcodec_find_decoder(avCodecPara->codec_id); if (codec == NULL) { printf("cannot find decoder\n"); break; } //根据解码器参数来创建解码器内容 codecCtx = avcodec_alloc_context3(codec); avcodec_parameters_to_context(codecCtx, avCodecPara); if (codecCtx == NULL) { printf("Cannot alloc context."); break; } //================================ 打开解码器 ===================================// if ((ret=avcodec_open2(codecCtx, codec, NULL)) < 0) { // 具体采用什么解码器ffmpeg经过封装 我们无须知道 printf("cannot open decoder\n"); break; } int w=codecCtx->width;//视频宽度 int h=codecCtx->height;//视频高度 //=========================== 分配AVPacket结构体 ===============================// pkt = av_packet_alloc(); //分配一个packet av_new_packet(pkt, codecCtx->width * codecCtx->height); //调整packet的数据 //=========================== 读取视频信息 ===============================// while (av_read_frame(fmtCtx, pkt) >= 0) { //读取的是一帧视频 数据存入一个AVPacket的结构中 if (pkt->stream_index == videoStreamIndex){ if (avcodec_send_packet(codecCtx, pkt) == 0){ while (avcodec_receive_frame(codecCtx, yuvFrame) == 0){ fwrite(yuvFrame->data[0],1,w*h,fp);//y fwrite(yuvFrame->data[1],1,w*h/4,fp);//u fwrite(yuvFrame->data[2],1,w*h/4,fp);//v } } } av_packet_unref(pkt);//重置pkt的内容 } }while(0); //===========================释放所有指针===============================// av_packet_free(&pkt); avcodec_close(codecCtx); avformat_close_input(&fmtCtx); avformat_free_context(fmtCtx); av_frame_free(&yuvFrame); return ret;}因为FFmpeg软解后的帧格式为YUV420P,也就不用进行格式转换了,直接将解码后的数据写入本地就行了。
解码结果为:
12345-rw-r--r-- 1 jackey jackey 64928 4月 5 19:45 main.o-rw-r--r-- 1 jackey jackey 40882 4月 5 19:34 Makefile-rw-r--r-- 1 jackey jackey 277850880 4月 5 19:45 result.yuv-rw-r--r-- 1 jackey jackey 1657362 8月 18 2017 Sample.mkv-rwxr-xr-x 1 jackey jackey 66976 4月 5 19:45 video_decode_mp42yuvmkv格式视频大小为1.7MB,解码后的YUV格式视频大小为278MB。
我使用ffplay播放YUV格式视频:
1ffplay -pixel_format yuv420p -video_size 1280x534 result.yuv和源视频打开效果一样就是解码正常。
YUV420SP格式因为FFmpeg使用CPU软解后的YUV格式为YUV420P,本部分在CPU软解码之后,我们将其转换为YUV420SP并写入本地文件。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142#include #include #include "libavcodec/avcodec.h"#include "libavfilter/avfilter.h"#include "libavformat/avformat.h"#include "libavutil/avutil.h"#include "libavutil/ffversion.h"#include "libswresample/swresample.h"#include "libswscale/swscale.h"#include "libpostproc/postprocess.h"int main() { FILE *fp=fopen("result.yuv","w+b"); if(fp==NULL){ printf("Cannot open file.\n"); return -1; } char filePath[] = "/home/jackey/Videos/Sample.mkv";//文件地址 int videoStreamIndex = -1;//视频流所在流序列中的索引 int ret=0;//默认返回值 //需要的变量名并初始化 AVFormatContext *fmtCtx=NULL; AVPacket *pkt =NULL; AVCodecContext *codecCtx=NULL; AVCodecParameters *avCodecPara=NULL; const AVCodec *codec=NULL; AVFrame *yuvFrame = av_frame_alloc(); AVFrame *nv12Frame = av_frame_alloc(); unsigned char *out_buffer; do{ //=========================== 创建AVFormatContext结构体 ===============================// //分配一个AVFormatContext,FFMPEG所有的操作都要通过这个AVFormatContext来进行 fmtCtx = avformat_alloc_context(); //==================================== 打开文件 ======================================// if ((ret=avformat_open_input(&fmtCtx, filePath, NULL, NULL)) != 0) { printf("cannot open video file\n"); break; } //=================================== 获取视频流信息 ===================================// if ((ret=avformat_find_stream_info(fmtCtx, NULL)) < 0) { printf("cannot retrive video info\n"); break; } //循环查找视频中包含的流信息,直到找到视频类型的流 //便将其记录下来 保存到videoStreamIndex变量中 for (unsigned int i = 0; i < fmtCtx->nb_streams; i++) { if (fmtCtx->streams[ i ]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { videoStreamIndex = i; break;//找到视频流就退出 } } //如果videoStream为-1 说明没有找到视频流 if (videoStreamIndex == -1) { printf("cannot find video stream\n"); break; } //打印输入和输出信息:长度 比特率 流格式等 av_dump_format(fmtCtx, 0, filePath, 0); //================================= 查找解码器 ===================================// avCodecPara = fmtCtx->streams[ videoStreamIndex ]->codecpar; codec = avcodec_find_decoder(avCodecPara->codec_id); if (codec == NULL) { printf("cannot find decoder\n"); break; } //根据解码器参数来创建解码器内容 codecCtx = avcodec_alloc_context3(codec); avcodec_parameters_to_context(codecCtx, avCodecPara); if (codecCtx == NULL) { printf("Cannot alloc context."); break; } //================================ 打开解码器 ===================================// if ((ret=avcodec_open2(codecCtx, codec, NULL)) < 0) { // 具体采用什么解码器ffmpeg经过封装 我们无须知道 printf("cannot open decoder\n"); break; } int w=codecCtx->width;//视频宽度 int h=codecCtx->height;//视频高度 //================================ 设置数据转换参数 ================================// struct SwsContext *img_ctx = sws_getContext( codecCtx->width, codecCtx->height, codecCtx->pix_fmt, //源地址长宽以及数据格式 codecCtx->width, codecCtx->height, AV_PIX_FMT_NV12, //目的地址长宽以及数据格式 SWS_BICUBIC, NULL, NULL, NULL); //算法类型 AV_PIX_FMT_YUVJ420P AV_PIX_FMT_BGR24 //==================================== 分配空间 ==================================// //一帧图像数据大小 int numBytes = av_image_get_buffer_size(AV_PIX_FMT_NV12, codecCtx->width, codecCtx->height, 1); out_buffer = (unsigned char *)av_malloc(numBytes * sizeof(unsigned char)); //=========================== 分配AVPacket结构体 ===============================// pkt = av_packet_alloc(); //分配一个packet av_new_packet(pkt, codecCtx->width * codecCtx->height); //调整packet的数据 //会将pFrameRGB的数据按RGB格式自动"关联"到buffer 即nv12Frame中的数据改变了 //out_buffer中的数据也会相应的改变 av_image_fill_arrays(nv12Frame->data, nv12Frame->linesize, out_buffer, AV_PIX_FMT_NV12, codecCtx->width, codecCtx->height, 1); //=========================== 读取视频信息 ===============================// while (av_read_frame(fmtCtx, pkt) >= 0) { //读取的是一帧视频 数据存入一个AVPacket的结构中 if (pkt->stream_index == videoStreamIndex){ if (avcodec_send_packet(codecCtx, pkt) == 0){ while (avcodec_receive_frame(codecCtx, yuvFrame) == 0){ sws_scale(img_ctx, (const uint8_t* const*)yuvFrame->data, yuvFrame->linesize, 0, h, nv12Frame->data, nv12Frame->linesize); fwrite(nv12Frame->data[0],1,w*h,fp);//y fwrite(nv12Frame->data[1],1,w*h/2,fp);//uv } } } av_packet_unref(pkt);//重置pkt的内容 } }while(0); //===========================释放所有指针===============================// av_packet_free(&pkt); avcodec_close(codecCtx); avformat_close_input(&fmtCtx); avformat_free_context(fmtCtx); av_frame_free(&yuvFrame); av_frame_free(&nv12Frame); av_free(out_buffer); return ret;}获得的结果也是278MB,使用ffplayer播放
1ffplay -pixel_format nv12 -video_size 1280x534 result.yuv如果显示结果和源视频文件一样,就表示解码正常。
GitHub项目地址(源代码):ffmpeg_beginner中的10.06.1video_decode_mp42yuv420p和10.06.2video_decode_mp42yuv420sp
下一篇:FFmpeg入门教程04.05:软解并使用QtWidget播放视频(YUV420P->RGB32)