FFmpeg4入门系列教程7:解码视频并保存为YUV格式文件(YUV420P or YUV420SP)

索引地址:系列教程索引地址

上一篇:FFmpeg4入门系列教程6:保存视频帧

上一篇我们解码并保存了其中的几帧确保解码过程和结果是对的。本篇我们将解码整个视频并保存为标准的YUV格式(YUV格式具体信息详见YUV格式介绍),我们就选YUV420P(I420)作为输出格式。

保存文件需要对本地文件进行读写操作,那么首先要有文件操作指针,C为FILE,C++为iostream。

以C为例。

1
FILE *fp = fopen("result.yuv","w+b");

扩展名任意,只要数据格式对就可以了,最好是把数据格式标识出来,比如:1280x720_yuv420p.yuv

与上一篇文章相比,就是多了将像素值写入文件的部分。

解码流程图为:

flow

函数调用流程图为:

flow

YUV420P格式

YUV420P像素分为三个部分:Y/U/V,Y部分长度为width * height,U为width * height /4 ,V部分和U部分长度一样。(为什么会是这样,见YUV格式介绍)。

测试代码:

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
#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"

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;
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);
avcodec_parameters_free(&avCodecPara);
//avformat_close_input(&fmtCtx);
//avformat_free_context(fmtCtx);
av_frame_free(&yuvFrame);

return ret;
}

因为FFmpeg软解后的帧格式为YUV420P,也就不用进行格式转换了,直接将解码后的数据写入本地就行了。

解码结果为:

1
2
3
4
5
-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_mp42yuv

mkv格式视频大小为1.7MB,解码后的YUV格式视频大小为278MB。

我使用ffplay播放YUV格式视频:

1
ffplay -pixel_format yuv420p -s 1280x534 result.yuv

和源视频打开效果一样就是解码正常。

YUV420SP格式

因为FFmpeg使用CPU软解后的YUV格式为YUV420P,本部分在CPU软解码之后,我们将其转换为YUV420SP并写入本地文件。

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
#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"

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;
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);
avcodec_parameters_free(&avCodecPara);
//avformat_close_input(&fmtCtx);
//avformat_free_context(fmtCtx);
av_frame_free(&yuvFrame);
av_frame_free(&nv12Frame);

av_free(out_buffer);

return ret;
}

获得的结果也是278MB,使用ffplayer播放

1
ffplay -pixel_format nv12 -s 1280x534 result.yuv

如果显示结果和源视频文件一样,就表示解码正常。

result

GitHub项目地址(源代码):ffmpeg_Beginner中的7.1video_decode_mp42yuv420p和7.2video_decode_mp42yuv420sp

下一篇:FFmpeg4入门系列教程8:软解并使用QtWidget播放视频