最近接触到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;
}