FFmpeg4入门系列教程27:保存视频流数据至本地(rtsp转mp4)

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

上一篇:FFmpeg4入门系列教程26:捕获摄像头编码h264并推流

推流部分介绍结束后,本系列的最后一篇介绍如何将流捕获并保存为本地文件。

简单来说,就是将rtsp流中的h264视频流在没解码之前获取下来,并保存到本地文件mp4中的h264流中,h264->mp4。之前在FFmpeg4入门系列教程13:h264编码为mp4介绍过将本地h264文件编码为mp4文件。本文基于此代码修改。

转换流程图为:

flow

以下代码来自FFmpeg4入门系列教程13:h264编码为mp4,只是修改了输入输出文件地址。

获取输入数据

就是打开地址,查找视频等等操作。

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
AVFormatContext *inVFmtCtx=NULL,*outFmtCtx=NULL;

int frame_index=0;//统计帧数
int inVStreamIndex=-1,outVStreamIndex=-1;//输入输出视频流在文件中的索引位置
const char *inVFileName = "rtsp://192.168.1.103/test";
const char *outFileName = "result.mp4";

//======================输入部分============================//

//打开输入文件
if(avformat_open_input(&inVFmtCtx,inVFileName,NULL,NULL)<0){
printf("Cannot open input file.\n");
return -1;
}

//查找输入文件中的流
if(avformat_find_stream_info(inVFmtCtx,NULL)<0){
printf("Cannot find stream info in input file.\n");
return -1;
}

//查找视频流在文件中的位置
for(size_t i=0;i<inVFmtCtx->nb_streams;i++){
if(inVFmtCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_VIDEO){
inVStreamIndex=(int)i;
break;
}
}

AVCodecParameters *codecPara = inVFmtCtx->streams[inVStreamIndex]->codecpar;//输入视频流的编码参数

printf("===============Input information========>\n");
av_dump_format(inVFmtCtx, 0, inVFileName, 0);
printf("===============Input information========<\n");

打开本地文件

创建一个本地文件.mp4,然后在此文件中插入一条h264视频流。

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
//=====================输出部分=========================//
//打开输出文件并填充格式数据
if(avformat_alloc_output_context2(&outFmtCtx,NULL,NULL,outFileName)<0){
printf("Cannot alloc output file context.\n");
return -1;
}

//打开输出文件并填充数据
if(avio_open(&outFmtCtx->pb,outFileName,AVIO_FLAG_READ_WRITE)<0){
printf("output file open failed.\n");
return -1;
}

//在输出的mp4文件中创建一条视频流
AVStream *outVStream = avformat_new_stream(outFmtCtx,NULL);
if(!outVStream){
printf("Failed allocating output stream.\n");
return -1;
}
outVStream->time_base.den=25;
outVStream->time_base.num=1;
outVStreamIndex=outVStream->index;

//查找编码器
AVCodec *outCodec = avcodec_find_encoder(codecPara->codec_id);
if(outCodec==NULL){
printf("Cannot find any encoder.\n");
return -1;
}

//从输入的h264编码器数据复制一份到输出文件的编码器中
AVCodecContext *outCodecCtx=avcodec_alloc_context3(outCodec);
AVCodecParameters *outCodecPara = outFmtCtx->streams[outVStream->index]->codecpar;
if(avcodec_parameters_copy(outCodecPara,codecPara)<0){
printf("Cannot copy codec para.\n");
return -1;
}
if(avcodec_parameters_to_context(outCodecCtx,outCodecPara)<0){
printf("Cannot alloc codec ctx from para.\n");
return -1;
}
outCodecCtx->time_base.den=25;
outCodecCtx->time_base.num=1;

//打开输出文件需要的编码器
if(avcodec_open2(outCodecCtx,outCodec,NULL)<0){
printf("Cannot open output codec.\n");
return -1;
}

printf("============Output Information=============>\n");
av_dump_format(outFmtCtx,0,outFileName,1);
printf("============Output Information=============<\n");

保存每一帧数据

和h264->MP4一样,没有编解码过程,只是修改时间戳。

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
//写入文件头
if(avformat_write_header(outFmtCtx,NULL)<0){
printf("Cannot write header to file.\n");
return -1;
}

//===============编码部分===============//

AVPacket *pkt=av_packet_alloc();
AVStream *inVStream = inVFmtCtx->streams[inVStreamIndex];

int i=0;
while(av_read_frame(inVFmtCtx,pkt)>=0 && i<1000){//循环读取每一帧直到读完
if(pkt->stream_index==inVStreamIndex){//确保处理的是视频流
++i;
//FIXME:No PTS (Example: Raw H.264)
//Simple Write PTS
//如果当前处理帧的显示时间戳为0或者没有等等不是正常值
if(pkt->pts==AV_NOPTS_VALUE){
printf("frame_index:%d\n", frame_index);
//Write PTS
AVRational time_base1 = inVStream->time_base;
//Duration between 2 frames (us)
int64_t calc_duration = (double)AV_TIME_BASE / av_q2d(inVStream->r_frame_rate);
//Parameters
pkt->pts = (double)(frame_index*calc_duration) / (double)(av_q2d(time_base1)*AV_TIME_BASE);
pkt->dts = pkt->pts;
pkt->duration = (double)calc_duration / (double)(av_q2d(time_base1)*AV_TIME_BASE);
frame_index++;
}
//Convert PTS/DTS
pkt->pts = av_rescale_q_rnd(pkt->pts, inVStream->time_base, outVStream->time_base, (enum AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt->dts = av_rescale_q_rnd(pkt->dts, inVStream->time_base, outVStream->time_base, (enum AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt->duration = av_rescale_q(pkt->duration, inVStream->time_base, outVStream->time_base);
pkt->pos = -1;
pkt->stream_index = outVStreamIndex;
printf("Write 1 Packet. size:%5d\tpts:%ld\n", pkt->size, pkt->pts);
//Write
if (av_interleaved_write_frame(outFmtCtx, pkt) < 0) {
printf("Error muxing packet\n");
break;
}
av_packet_unref(pkt);
}
}

av_write_trailer(outFmtCtx);

结果

代码中保存1000帧,每秒25帧,共计40秒。

直接双击打开mp4文件。

result

完整代码在ffmpeg_Beginner中的27.video_dump_stream_to_local_file中。

到此,入门应该是结束了,本系列至此结束,没有下一篇了。

接下来是参数优化和源码分析了。