FFmpeg入门教程10.05:保存视频帧

系列索引:FFmpeg入门系列索引

上一篇:FFmpeg入门教程10.04:解码视频流过程

上一篇介绍了解码的基本流程,获取了视频帧数,但是没有视频每一帧数据的解码操作。

我们从视频中取出每一帧进行操作,我们已经分配了AVFrame内存,当我们转换它颜色空间时仍然需要一个位置来放置原始数据。我们使用av_image_get_buffer_size来获得我们需要的大小,并手动分配空间:

1
2
3
//一帧图像数据大小
int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB32, codecCtx->width, codecCtx->height, 1);
unsigned char *out_buffer = (unsigned char *)av_malloc(numBytes * sizeof(unsigned char));

av_malloc()是ffmpeg的malloc,它只是malloc的一个简单包装器,它确保内存地址是对齐的。但是它不会保护您免受内存泄漏、双重释放或其他malloc问题的影响。现在我们使用av_image_fill_arrays()将AVFrame与新分配的缓冲区关联起来。关于AVPicture的转换:AVPicture结构是AVFrame结构的子集-AVFrame结构的开头与AVPicture结构相同。

1
2
//会将pFrameRGB的数据按RGB格式自动"关联"到buffer,即pFrameRGB中的数据改变了,out_buffer中的数据也会相应的改变
av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, out_buffer, AV_PIX_FMT_RGB32,codecCtx->width, codecCtx->height,1);

我们从out_buffer中取我们想要的数据,但是还需要有将yuv格式转换为RGB格式的操作。

1
2
3
4
5
//================================ 设置数据转换参数 ================================//
struct SwsContext *img_ctx = sws_getContext(
codecCtx->width, codecCtx->height, codecCtx->pix_fmt, //源地址长宽以及数据格式
codecCtx->width, codecCtx->height, AV_PIX_FMT_RGB32, //目的地址长宽以及数据格式
SWS_BICUBIC, NULL, NULL, NULL); //算法类型 AV_PIX_FMT_YUVJ420P AV_PIX_FMT_BGR24

首先构建一个源和目标图片的格式、长宽的对应关系。然后将对象的转换关系写明:

1
sws_scale(img_ctx, yuvFrame->data, yuvFrame->linesize, 0, codecCtx->height,rgbFrame->data, rgbFrame->linesize);

接下来进行帧解码并且保存为图片。

解码流程图为:

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

完整测试代码(根据官方代码和上一篇代码修改):

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#include <stdio.h>
#include <stdlib.h>
#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"

//将FFmpeg解码后的数据保存到本地文件
void saveFrame(AVFrame *pFrame, int width, int height, int iFrame)
{
FILE *pFile;
char szFilename[32];
int y;

// 打开文件
sprintf(szFilename, "frame%d.ppm", iFrame);
pFile = fopen(szFilename, "wb");
if (pFile == NULL)
return;

// 写入文件头
fprintf(pFile, "P6\n%d %d\n255\n", width, height);

// 写入像素数据
for (y = 0; y < height; y++)
fwrite(pFrame->data[0] + y * pFrame->linesize[0], 1, width * 3, pFile);

// 关闭文件
fclose(pFile);
}

int main() {
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 *rgbFrame = 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;
}

//================================ 设置数据转换参数 ================================//
struct SwsContext *img_ctx = sws_getContext(
codecCtx->width, codecCtx->height, codecCtx->pix_fmt, //源地址长宽以及数据格式
codecCtx->width, codecCtx->height, AV_PIX_FMT_RGB32, //目的地址长宽以及数据格式
SWS_BICUBIC, NULL, NULL, NULL); //算法类型 AV_PIX_FMT_YUVJ420P AV_PIX_FMT_BGR24

//==================================== 分配空间 ==================================//
//一帧图像数据大小
int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB32, codecCtx->width, codecCtx->height, 1);
unsigned char *out_buffer = (unsigned char *)av_malloc(numBytes * sizeof(unsigned char));

//=========================== 分配AVPacket结构体 ===============================//
int i = 0;//用于帧计数
pkt = av_packet_alloc(); //分配一个packet
av_new_packet(pkt, codecCtx->width * codecCtx->height); //调整packet的数据

//会将pFrameRGB的数据按RGB格式自动"关联"到buffer 即pFrameRGB中的数据改变了
//out_buffer中的数据也会相应的改变
av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, out_buffer, AV_PIX_FMT_RGB32,
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){
if (++i <= 500 && i >= 455){
sws_scale(img_ctx,
(const uint8_t* const*)yuvFrame->data,
yuvFrame->linesize,
0,
codecCtx->height,
rgbFrame->data,
rgbFrame->linesize);
saveFrame(rgbFrame, codecCtx->width, codecCtx->height, i);
}
}
}
}
av_packet_unref(pkt);//重置pkt的内容
}
printf("There are %d frames int total.\n", i);
}while(0);
//===========================释放所有指针===============================//
av_packet_free(&pkt);
avcodec_close(codecCtx);
avformat_close_input(&fmtCtx);
avformat_free_context(fmtCtx);
av_frame_free(&yuvFrame);
av_frame_free(&rgbFrame);

return ret;
}

原图(视频中的某一帧,近似)是:

decode

保存的ppm图片是:

decode

离远看差不多。

GitHub项目地址(源代码):ffmpeg_beginner中的10.05.video_decode_save

下一篇:FFmpeg入门教程10.06:解码视频并保存为YUV格式文件(YUV420P/YUV420SP)


FFmpeg入门教程10.05:保存视频帧
https://blog.jackeylea.com/ffmpeg/ffmpeg-save-video-frame/
作者
JackeyLea
发布于
2020年7月13日
许可协议