偶遇FFMpeg(四)-FFmpeg PC端推流
2018-05-02 本文已影响135人
deep_sadness
开编
之前在Android集成FFmpeg。主要还是基于命令行的方式进行操作。刚刚好最近又在研究推流相关的东西。看了一些博文。和做了一些实践。
就希望通过本文记录袭来。
本文的大体结构如下
目录.png
FFMPEG 开发环境搭建
笔者是在 Windows10 64+Visual Studio2017的环境下开发的
下载和安装VisualStudio2017
去官网下载和安装就可以
在项目中配置FFMPEG
- 下载FFMPEG相关的文件和解压
从FFMPEG WINDOW BUILD中下载dev
和shared
两个部分的内容
下载示例图.png
-
dev_package.pngdev
压缩包内
-
shared_package.pngshared
压缩包内
- 创建VisualStudio项目和配置FFMPEG
-
创建控制台项目
创建VisualStudio项目.png -
在项目中配置依赖项(重点)
-
在左上角,点击项目。最后一下的弹出框中进行配置。
项目相关配置.png -
然后将dll的文件复制到当前的目录下。
文件复制到当前.png -
将Window编译调试,选择到正确的x64
正确的x64.png -
处理一些错误。让程序跑起来
-
错误1:
av_register_all
过时。
解决方法: 暂时没有什么更好的办法,只能去头文件里面。把attribute_deprecated
注释掉了
推流代码
大致先了解一下结构体和结构体之间的关系
结构体关系
结构体关系.png结构体
-
AVFormatContext
AVFormatContext
是格式封装的上下文对象。
在这里,会比较熟悉的常用的成员变量有:-
AVIOContext *pb
:用来合成音频和视频,或者分解的AVIOContext -
unsigned int nb_streams
:视音频流的个数 -
AVStream **streams
:视音频流 -
char filename[1024]
:文件名 -
AVDictionary *metadata
:存储视频元信息的metadata对象。
-
-
AVDictionaryEntry
每一条元数据分为key
和value
两个属性。
typedef struct AVDictionaryEntry {
char *key;
char *value;
} AVDictionaryEntry;
可以根据下面代码。取出这些数据
AVFormatContext *fmt_ctx = NULL;
AVDictionaryEntry *tag = NULL;
int ret;
if ((ret = avformat_open_input(&fmt_ctx, argv[1], NULL, NULL)))
return ret;
while ((tag = av_dict_get(fmt_ctx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX)))
printf("%s=%s\n", tag->key, tag->value);
avformat_close_input(&fmt_ctx);
-
AVRational
表示媒体信息的一些分数,是分母和分子的结构。计算过程中,会多次使用这样的数据结构
typedef struct AVRational{
int num; ///< Numerator
int den; ///< Denominator
} AVRational;
-
AVPacket
AVPacket
是存储压缩编码数据相关信息的结构体。-
uint8_t *data
:压缩编码的数据。
例如对于H.264来说。1个AVPacket的data通常对应一个NAL!
注意:在这里只是对应,而不是一模一样。他们之间有微小的差别:使用FFMPEG类库分离出多媒体文件中的H.264码流
因此在使用FFMPEG进行视音频处理的时候,常常可以将得到的AVPacket的data数据直接写成文件,从而得到视音频的码流文件。
-int size
:
data的大小 -
int64_t pts
:
显示时间戳
-int64_t dts
:
解码时间戳
-int stream_index
:
标识该AVPacket所属的视频/音频流。
-
FFMPEG推流的套路
套路图如下:
FFMPEG推流的套路.png
整个方法的流向:
copy from leixiaohua.png首先,我们先来熟悉一下这个整体的套路。其实推流的过程。我的理解是,经过解封装,按照原来的数据结构,提取和转成目标数据结构进行发送。
因为FFmpeg做好了封装,我们只要对其调用方法就可以了。
按照套路图,我们知道,使用FFmpeg的话
- 第一步是得到整体封装的输入和输出的上下文对象
AVFormatContext
。
//注册所有的
av_register_all();
//初始化网络
avformat_network_init();
//配置输入和输出
const char *inUrl = "dongfengpo.flv";
const char *outUrl = "rtmp://localhost/live/test";
AVFormatContext *ictx = NULL;
//得到输入的上下文
int ret = avformat_open_input(&ictx, inUrl, NULL, NULL);
if (ret < 0)
{
return avError2(ret);
}
cout << " avformat_open_input success! " << endl;
//去打印结果
ret = avformat_find_stream_info(ictx, NULL);
if (ret < 0)
{
return avError2(ret);
}
//将AVFormat打印出来
av_dump_format(ictx, 0, inUrl, 0);
//开始处理输出流
int videoIndex = 0;
//0.先得到AVFormat
AVFormatContext *octx;
AVOutputFormat *ofmt = NULL;
ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
if (ret < 0)
{
return avError2(ret);
}
cout << "avformat_alloc_output_context2 success!" << endl;
ofmt = octx->oformat;
- 再创建输出的
AVStream
,并从输入AVFormatContext
的其中取得AVStream
,将对应的参数(主要是编码器信息)copy
到其中。
//开始遍历流,进行对应stream的创建
for (int i = 0; i < ictx->nb_streams; i++)
{
//这里开始要创建一个新的AVStream
AVStream *stream = ictx->streams[i];
//判断是否是videoIndex。这里先记录下视频流。后面会对这个流进行操作
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
videoIndex = i;
}
//创建输出流
AVCodec *c = avcodec_find_decoder(stream->codecpar->codec_id);
AVStream *os = avformat_new_stream(octx, c);
//应该将编解码器的参数从input中复制过来
// 这里要注意的是,因为 os->codec这样的取法,已经过时了。所以使用codecpar
ret = avcodec_parameters_copy(os->codecpar, stream->codecpar);
if (ret < 0)
{
return avError2(ret);
}
cout << "avcodec_parameters_copy success!" << endl;
cout << "avcodec_parameters_copy success! in stream codec tag" << stream->codecpar->codec_tag << endl;
cout << "avcodec_parameters_copy success! out stream codec tag" << os->codecpar->codec_tag << endl;
//复制成功之后。还需要设置 codec_tag(编码器的信息?)
os->codecpar->codec_tag = 0;
}
//检查一遍我们的输出
av_dump_format(octx, 0, outUrl, 1);
- 因为是推流,所以第三部,就是通过
avio_open
链接网址,做好推流的准备
//开始使用io进行推流
//通过AVIO_FLAG_WRITE这个标记位,打开输出的AVFormatContext->AVIOContext
ret = avio_open(&octx->pb, outUrl, AVIO_FLAG_WRITE);
if (ret < 0)
{
return avError2(ret);
}
cout << "avio_open success!" << endl;
- 推流的过程。首先通过
avformat_write_header
写入头部信息。接着是通过av_read_frame
函数读取输入的frame
的数据,写入到AVPakcet
当中。处理每一帧的pts
和dts
。再通过av_interleaved_write_frame
将这一个帧发送出去。最后,通过av_packet_unref
释放AVPacket
//先写头
ret = avformat_write_header(octx, 0);
if (ret < 0)
{
return avError2(ret);
}
//取得到每一帧的数据,写入
AVPacket pkt;
//为了让我们的代码发送流的速度,相当于整个视频播放的数据。需要记录程序开始的时间
//后面再根据,每一帧的时间。做适当的延迟,防止我们的代码发送的太快了
long long start_time = av_gettime();
//记录视频帧的index,用来计算pts
long long frame_index = 0;
while (true)
{
//输入输出视频流
AVStream *in_stream, *out_stream;
//从输入流中读取数据 frame到AVPacket当中
ret = av_read_frame(ictx, &pkt);
if (ret < 0)
{
break;
}
//没有显示时间的时候,才会进入计算和校验
//没有封装格式的裸流(例如H.264裸流)是不包含PTS、DTS这些参数的。在发送这种数据的时候,需要自己计算并写入AVPacket的PTS,DTS,duration等参数。如果没有pts,则进行计算
if (pkt.pts == AV_NOPTS_VALUE)
{
//AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间。
//先得到流中的time_base
AVRational time_base = ictx->streams[videoIndex]->time_base;
//开始校对pts和 dts.通过time_base和dts转成真正的时间
//得到的是每一帧的时间
/*
r_frame_rate 基流帧速率 。取得是时间戳内最小的帧的速率 。每一帧的时间就是等于 time_base/r_frame_rate
av_q2d 转化为double类型
*/
int64_t calc_duration = (double)AV_TIME_BASE / av_q2d(ictx->streams[videoIndex]->r_frame_rate);
//配置参数 这些时间,都是通过 av_q2d(time_base) * AV_TIME_BASE 来转成实际的参数
pkt.pts = (double)(frame_index * calc_duration) / (double)av_q2d(time_base) * AV_TIME_BASE;
//一个GOP中,如果存在B帧的话,只有I帧的dts就不等于pts
pkt.dts = pkt.pts;
pkt.duration = (double)calc_duration / (double)av_q2d(time_base) * AV_TIME_BASE;
}
//开始处理延迟.只有等于视频的帧,才会处理
if (pkt.stream_index == videoIndex)
{
//需要计算当前处理的时间和开始处理时间之间的间隔??
//0.先取时间基数
AVRational time_base = ictx->streams[videoIndex]->time_base;
//AV_TIME_BASE_Q 用小数表示的时间基数。等于时间基数的倒数
AVRational time_base_r = { 1, AV_TIME_BASE };
//计算视频播放的时间. 公式等于 pkt.dts * time_base / time_base_r`
//.其实就是 stream中的time_base和定义的time_base直接的比例
int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_r);
//计算实际视频的播放时间。 视频实际播放的时间=代码处理的时间??
int64_t now_time = av_gettime() - start_time;
cout << time_base.num << " " << time_base.den << " " << pkt.dts << " " << pkt.pts << " " << pts_time << endl;
//如果显示的pts time 比当前的时间迟,就需要手动让程序睡一会,再发送出去,保持当前的发送时间和pts相同
if (pts_time > now_time)
{
//睡眠一段时间(目的是让当前视频记录的播放时间与实际时间同步)
av_usleep((unsigned int)(pts_time - now_time));
}
}
//重新计算一次pts和dts.主要是通过 in_s的time_base 和 out_s的time_base进行计算和校对
//先取得stream
in_stream = ictx->streams[pkt.stream_index];
out_stream = octx->streams[pkt.stream_index];
//重新开始指定时间戳
//计算延时后,重新指定时间戳。 这次是根据 in_stream 和 output_stream之间的比例
//计算dts时,不再直接用pts,因为如有有B帧,就会不同
//pts,dts,duration都也相同
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (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, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt.duration = (int)av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
//再次标记字节流的位置,-1表示不知道字节流的位置
pkt.pos = -1;
//如果当前的帧是视频帧,则将我们定义的frame_index往后推
if (pkt.stream_index == videoIndex)
{
printf("Send %8d video frames to output URL\n", frame_index);
frame_index++;
}
//发送!!!
ret = av_interleaved_write_frame(octx, &pkt);
if (ret < 0)
{
printf("发送数据包出错\n");
break;
}
//使用完了,记得释放
av_packet_unref(&pkt);
}
//写文件尾(Write file trailer)
av_write_trailer(octx);
avformat_close_input(&ictx);
/* close output */
if (ictx && !(octx->flags & AVFMT_NOFILE))
avio_close(octx->pb);
avformat_free_context(octx);
if (ret < 0 && ret != AVERROR_EOF) {
printf("Error occurred.\n");
return -1;
}
参考
基于FFmpeg进行RTMP推流
最简单的基于FFmpeg的推流器(以推送RTMP为例)
FFMPEG中最关键的结构体之间的关系
FFMPEG结构体分析:AVPacket