使用FFmpeg读取视频流并保存

最近接触到FFmpeg,需要实现一个将rtsp协议的码流读取并能显示的程序。在网上搬运代码的同时,也写一些对FFmpeg,Qt这些工具的理解。

准备

首先定义宏,其作用是避免‘UINT64_C’ was not declared in this scope的错误。

#ifndef INT64_C 
#define INT64_C(c) (c ## LL) 
#define UINT64_C(c) (c ## ULL) 
#endif

加入FFmpeg和C++头文件

extern "C" {
	/*Include ffmpeg header file*/
#include <libavformat/avformat.h> 
#include <libavcodec/avcodec.h> 
#include <libswscale/swscale.h> 

#include <libavutil/imgutils.h>  
#include <libavutil/opt.h>     
#include <libavutil/mathematics.h>   
#include <libavutil/samplefmt.h>
}

#include <iostream>
using namespace std;

主函数

首先,定义输入输出AVFormatContext结构体,这类结构体存储音视频数据,也就是音视频文件的一种抽象和封装,注意在FFmpeg开发者只能使用指针。随后定义输入输出文件名,输入就是rtsp协议的地址,这里我用的是我自己的海康摄像头地址。输出保存为一个flv文件。avformat_network_init函数顾名思义是初始化网络。

int main(void)
{
	AVFormatContext* ifmt_ctx = NULL, * ofmt_ctx = NULL;
	const char* in_filename, * out_filename;
	in_filename = "rtsp://admin:WY@123456@192.168.0.64/h264/ch1/main/av_stream";
	out_filename = "output.flv";
	
    avformat_network_init();

设置一个配置字典,在FFmpeg中我们用AVDictionary结构体配置。

	AVDictionary* avdic = NULL;
	char option_key[] = "rtsp_transport";
	char option_value[] = "tcp";
	av_dict_set(&avdic, option_key, option_value, 0);
	char option_key2[] = "max_delay";
	char option_value2[] = "5000000";
	av_dict_set(&avdic, option_key2, option_value2, 0);

接下来就是打开输入流了,需要使用AV,我们先看一下它的声明:、

int avformat_open_input(AVFormatContext **ps, const char *url, ff_const59 AVInputFormat *fmt, AVDictionary **options);

该函数有四个参数,首先是一个指向AVFormatContext指针的指针,随后是url的指针。AVInputFormat 是 指定输入的封装格式。一般传NULL,由FFmpeg自行探测。AVDictionary **options其它参数设置,它是一个字典,用于参数传递。

打开输入视频流之前我们再定义几个参数:

	AVPacket pkt;
	AVOutputFormat* ofmt = NULL;
	int video_index = -1;
	int frame_index = 0;
	
    int i;

AVPacket类保存解复用后,解码之前的数据。至于什么是解复用,我们都知道信号有时分复用,频分复用等,音视频信号中经常将视频音频等进行复用,在接收端就得把他们独立分离出来,即Source->Demux->Stream的变化。详见:AVPacket分析

接下来我们打开输入流:

	//打开输入流
	int ret;
	if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, &avdic)) < 0)
	{
		cout<<"Could not open input file."<<endl;
		goto end;
	}
	if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0)
	{
		cout<<"Failed to retrieve input stream information"<<endl;
		goto end;
	}

这里的end代表程序末尾,在end中做一些关闭输入流等收尾工作。如果avformat_open_input返回负值,则输出错误并结束。avformat_open_input的作用是打开输入流并阅读文件头,但不打开解码器。而avformat_find_stream_info则阅读媒体文件中的包,获得流推送的信息。

继续看代码:

	//nb_streams代表有几路流

	for (i = 0; i < ifmt_ctx->nb_streams; i++) 
	{
		if (ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
		{
			//视频流
			video_index = i;
			cout << "get videostream." << endl;
			break;
		}
	}
	av_dump_format(ifmt_ctx, 0, in_filename, 0);

我们用一个for循环,如果第i个流的codecpar参数中的codec_type 为AVMEDIA_TYPE_VIDEO我们就知道获取了视频流了。随后我们将视频流这一支的i赋给video_index,并将"get videostream."信息打印在控制台上。av_dump_format函数打印其他一些流的信息。如下图所示:

接下来,我们打开输出流:

    avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_filename);

	if (!ofmt_ctx)
	{
		printf("Could not create output context\n");
		ret = AVERROR_UNKNOWN;
		goto end;
	}

这个函数的作用是根据文件名寻找合适的AVFormatContext管理结构。接下来写文件头到输出文件:

    //写文件头到输出文件
    ret = avformat_write_header(ofmt_ctx, NULL);
    if (ret < 0)
    {
    	printf("Error occured when opening output URL\n");
    	goto end;
    }

接下来就是把数据存入视频文件啦。我们使用一个while循环:

	while (1)
	{
		AVStream* in_stream, * out_stream;
		//从输入流获取一个数据包
		ret = av_read_frame(ifmt_ctx, &pkt);//读一帧并放到pkt中去
		if (ret < 0)
			break;//读取失败

		in_stream = ifmt_ctx->streams[pkt.stream_index];
		out_stream = ofmt_ctx->streams[pkt.stream_index];
		//copy packet
		//转换 PTS/DTS 时序
		pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (enum AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
		pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (enum AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
		//printf("pts %d dts %d base %d\n",pkt.pts,pkt.dts, in_stream->time_base);
		pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
		pkt.pos = -1;

		//此while循环中并非所有packet都是视频帧,当收到视频帧时记录一下,仅此而已
		if (pkt.stream_index == video_index)
		{
			printf("Receive %8d video frames from input URL\n", frame_index);
			frame_index++;
		}

		//将包数据写入到文件。
		ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
		if (ret < 0)
		{
			if (ret == -22) {
				continue;
			}
			else {
				printf("Error muxing packet.error code %d\n", ret);
				break;
			}
		}

对网络视频文件的播放需要经过以下步骤:解协议,解封装,解码视音频,视音频同步。这其中,接协议后的输出就是AVStream类。我们首先用av_read_frame读取一帧的数据。紧接着,我们用指针in_stream指向packet中某一个流的数据,out_stream指向另一个流的数据。获取流的指针后,进行pts/dts转换。pts,dts分别是视频播放和解码时间戳,下面这篇文章叙述地比较详细:

pts,dts的概念——作者:SamirChen

若收到视频帧,则打印到控制台:

		if (pkt.stream_index == video_index)
		{
			printf("Receive %8d video frames from input URL\n", frame_index);
			frame_index++;
		}

最后,调用av_interleaved_write_frame写数据,av_interleaved_write_frame函数相较于av_write_frame提供了对 packet 进行缓存和 pts 检查的功能。

		//将包数据写入到文件。
		ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
		if (ret < 0)
		{
			if (ret == -22) {
				continue;
			}
			else {
				printf("Error muxing packet.error code %d\n", ret);
				break;
			}
		}
				av_packet_unref(&pkt);
	}

最后,我们写文件尾和对之前的错误进行后续处理:

	//写文件尾
	av_write_trailer(ofmt_ctx);

end:
	av_dict_free(&avdic);
	avformat_close_input(&ifmt_ctx);
	//Close input
	if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
		avio_close(ofmt_ctx->pb);
	avformat_free_context(ofmt_ctx);
	if (ret < 0 && ret != AVERROR_EOF)
	{
		cout<<"Error occured."<<endl;
		return -1;
	}

	return 0;
}

在使用visual studio运行时,需要关闭SDL检查,否则会因为版本原因报错

总结

第一次使用FFmpeg进行多媒体开发,虽然代码是搬运的,但过程还是很有趣的,以后有时间会继续学习这一块。
原文链接:
FFmpeg从rtsp抓取流
完整项目代码:

#ifndef INT64_C 
#define INT64_C(c) (c ## LL) 
#define UINT64_C(c) (c ## ULL) 
#endif 

extern "C" {
	/*Include ffmpeg header file*/
#include <libavformat/avformat.h> 
#include <libavcodec/avcodec.h> 
#include <libswscale/swscale.h> 

#include <libavutil/imgutils.h>  
#include <libavutil/opt.h>     
#include <libavutil/mathematics.h>   
#include <libavutil/samplefmt.h>
}

#include <iostream>
using namespace std;

int main(void)
{
	AVFormatContext* ifmt_ctx = NULL, * ofmt_ctx = NULL;
	const char* in_filename, * out_filename;
	in_filename = "rtsp://admin:WY@123456@192.168.0.64/h264/ch1/main/av_stream";
	out_filename = "output.flv";

	avformat_network_init();

	AVDictionary* avdic = NULL;
	char option_key[] = "rtsp_transport";
	char option_value[] = "tcp";
	av_dict_set(&avdic, option_key, option_value, 0);
	char option_key2[] = "max_delay";
	char option_value2[] = "5000000";
	av_dict_set(&avdic, option_key2, option_value2, 0);

	AVPacket pkt;
	AVOutputFormat* ofmt = NULL;
	int video_index = -1;
	int frame_index = 0;

	int i;

	//打开输入流
	int ret;
	if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, &avdic)) < 0)
	{
		cout<<"Could not open input file."<<endl;
		goto end;
	}
	if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0)
	{
		cout<<"Failed to retrieve input stream information"<<endl;
		goto end;
	}


	//nb_streams代表有几路流

	for (i = 0; i < ifmt_ctx->nb_streams; i++) 
	{
		if (ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
		{
			//视频流
			video_index = i;
			cout << "get videostream." << endl;
			break;
		}
	}

	av_dump_format(ifmt_ctx, 0, in_filename, 0);

	//打开输出流
	avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_filename);

	if (!ofmt_ctx)
	{
		printf("Could not create output context\n");
		ret = AVERROR_UNKNOWN;
		goto end;
	}
	ofmt = ofmt_ctx->oformat;

	for (i = 0; i < ifmt_ctx->nb_streams; i++)
	{
		AVStream* in_stream = ifmt_ctx->streams[i];
		AVStream* out_stream = avformat_new_stream(ofmt_ctx, in_stream->codec->codec);

		if (!out_stream)
		{
			printf("Failed allocating output stream.\n");
			ret = AVERROR_UNKNOWN;
			goto end;
		}

		//将输出流的编码信息复制到输入流
		ret = avcodec_copy_context(out_stream->codec, in_stream->codec);
		if (ret < 0)
		{
			printf("Failed to copy context from input to output stream codec context\n");
			goto end;
		}

		out_stream->codec->codec_tag = 0;

		if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
			out_stream->codec->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

	}

	av_dump_format(ofmt_ctx, 0, out_filename, 1);

	if (!(ofmt->flags & AVFMT_NOFILE))
	{
		ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
		if (ret < 0)
		{
			printf("Could not open output URL '%s'", out_filename);
			goto end;
		}
	}

	//写文件头到输出文件
	ret = avformat_write_header(ofmt_ctx, NULL);
	if (ret < 0)
	{
		printf("Error occured when opening output URL\n");
		goto end;
	}


	//while循环中持续获取数据包,不管音频视频都存入文件
	while (1)
	{
		AVStream* in_stream, * out_stream;
		//从输入流获取一个数据包
		ret = av_read_frame(ifmt_ctx, &pkt);
		if (ret < 0)
			break;

		in_stream = ifmt_ctx->streams[pkt.stream_index];
		out_stream = ofmt_ctx->streams[pkt.stream_index];
		//copy packet
		//转换 PTS/DTS 时序
		pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (enum AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
		pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (enum AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
		//printf("pts %d dts %d base %d\n",pkt.pts,pkt.dts, in_stream->time_base);
		pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
		pkt.pos = -1;

		//此while循环中并非所有packet都是视频帧,当收到视频帧时记录一下
		if (pkt.stream_index == video_index)
		{
			printf("Receive %8d video frames from input URL\n", frame_index);
			frame_index++;
		}

		//将包数据写入到文件。
		ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
		if (ret < 0)
		{
			if (ret == -22) {
				continue;
			}
			else {
				printf("Error muxing packet.error code %d\n", ret);
				break;
			}

		}

		//av_free_packet(&pkt); //此句在新版本中已deprecated 由av_packet_unref代替
		av_packet_unref(&pkt);
	}


	//写文件尾
	av_write_trailer(ofmt_ctx);

end:
	av_dict_free(&avdic);
	avformat_close_input(&ifmt_ctx);
	//Close input
	if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
		avio_close(ofmt_ctx->pb);
	avformat_free_context(ofmt_ctx);
	if (ret < 0 && ret != AVERROR_EOF)
	{
		cout<<"Error occured."<<endl;
		return -1;
	}

	return 0;
}

原文 使用FFmpeg读取视频流并保存_ffmpeg保存视频流_豹纹法克米的博客-CSDN博客

★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓