FFmpeg版本:4.1.3
原理
从前面的文章,我们可以知道,实现推流客户端需要执行的下面几个步骤:
- 采集
- 编码
- 封装
- 推流
本文实现的是将本地的文件推送到服务器的过程,因此,不存在采集和编码过程。只有封装和推流的过程。
代码分析
源代码在 :https://github.com/WaPonX/FFmpegDemo
源代码中用到的一些关键的FFmpeg函数解释可以看:直播入门(附录二)FFmpeg关键函数一览表
环境的配置和播放,我们在直播入门(三)动手实现一个简单的直播中讲过的,这里不再赘述了。
初始化
av_register_all()
这个函数在FFmpeg 4.0 中已经被标记为废弃了。因此不再需要该函数进行初始化。
另外也不用调用avformat_network_init()
函数进行初始化了。
bool Pusher::Initialize()
{
int ret = 0;
/// 为输入分配一个AVFormatContext对象
m_inputContext = avformat_alloc_context();
if(nullptr == m_inputContext)
{
return false;
}
/// 打开输入流,注意调用了这个函数之后,m_inputContext才的iformat才会被分配对象
/// 否则会被设置为空
/// FFmpeg的说明中,也强调了AVFormatContext中的iformat应该由该函数来分配对象,不能手动赋值。
ret = avformat_open_input(&m_inputContext, m_input.c_str(), nullptr, nullptr);
if (ret < 0)
{
LOG("Could not open input file. error code is %d", ret);
avformat_close_input(&m_inputContext);
return false;
}
LOG("Input format %s, duration %lld us", m_inputContext->iformat->long_name, m_inputContext->duration);
/// 从上下文中解析流数据
ret = avformat_find_stream_info(m_inputContext, nullptr);
if (ret < 0)
{
LOG("Failed to retrieve input stream information. error code is %d", ret);
avformat_close_input(&m_inputContext);
}
av_dump_format(m_inputContext, 0, m_input.c_str(), 0);
/// 为输出流分配一个上下文对象,指定输出流的格式
avformat_alloc_output_context2(&m_outputContext, nullptr, "flv", m_output.c_str());
LOG("Input format %s, duration %lld us", m_outputContext->oformat->long_name, m_outputContext->duration);
if (m_outputContext == nullptr)
{
LOG("Could not create output context\n");
CloseContext(m_inputContext, m_outputContext);
return false;
}
/// 从输入流中复制AVStream对象。
for (uint32_t index = 0; index < m_inputContext->nb_streams; ++index)
{
//根据输入流创建输出流
AVStream *inStream = m_inputContext->streams[index];
AVStream *outStream = avformat_new_stream(m_outputContext, inStream->codec->codec);
if (nullptr == outStream)
{
LOG("Failed allocating output/input stream\n");
CloseContext(m_inputContext, m_outputContext);
return false;
}
//复制AVCodecContext的设置
ret = avcodec_copy_context(outStream->codec, inStream->codec);
if (ret < 0)
{
LOG("Failed to copy context from input to output stream codec context\n");
CloseContext(m_inputContext, m_outputContext);
return false;
}
outStream->codec->codec_tag = 0;
if (m_outputContext->oformat->flags & AV_CODEC_FLAG_GLOBAL_HEADER)
{
outStream->codec->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
}
av_dump_format(m_outputContext, 0, m_output.c_str(), 1);
/// 打开输出端,建立连接,访问URL。
if (!(m_outputContext->oformat->flags & AVFMT_NOFILE))
{
/// 使用写标记打开
int ret = avio_open(&m_outputContext->pb, m_output.c_str(), AVIO_FLAG_WRITE);
if (ret < 0)
{
LOG("Could not open output URL '%s', Error Code is %d", m_output, ret);
CloseContext(m_inputContext, m_outputContext);
return false;
}
}
/// 记录视频流和音频流的StreamID
ret = ParseVideoAndAudioStreamIndex();
if (ret != 0)
{
return false;
}
m_isInit = true;
return true;
}
初始化的操作,主要就是从输入流中创建输出流,并且记录一些信息。
推送数据
推送的主要实现参考的是雷神的代码:最简单的基于FFmpeg的推流器(以推送RTMP为例)
int32_t Pusher::Push()
{
//写文件头
int ret = avformat_write_header(m_outputContext, NULL);
if (ret < 0)
{
LOG("Error occurred when opening output URL, Error Code is %d\n", ret);
CloseContext(m_inputContext, m_outputContext);
}
AVPacket packet;
uint32_t videoWriteFrameCount = 0;
int64_t start_time = av_gettime();
while (true)
{
AVStream *inStream, *outStream;
//获取一个数据包
ret = av_read_frame(m_inputContext, &packet);
if (ret < 0)
{
LOG("faild to read one packet from input, Error Code is %d\n", ret);
break;
}
//FIX:No PTS (Example: Raw H.264)
//Simple Write PTS
if (packet.pts == AV_NOPTS_VALUE)
{
//Write PTS
AVRational time_base1 = m_inputContext->streams[m_videoStreamIndex[0]]->time_base;
//Duration between 2 frames (us)
int64_t calc_duration =
(double) AV_TIME_BASE / av_q2d(m_inputContext->streams[m_videoStreamIndex[0]]->r_frame_rate);
//Parameters
packet.pts = (double) (videoWriteFrameCount * calc_duration) / (double) (av_q2d(time_base1) * AV_TIME_BASE);
packet.dts = packet.pts;
packet.duration = (double) calc_duration / (double) (av_q2d(time_base1) * AV_TIME_BASE);
}
/// 延迟发送,否则会出错
Delay(packet, start_time);
inStream = m_inputContext->streams[packet.stream_index];
outStream = m_outputContext->streams[packet.stream_index];
/* copy packet */
//转换PTS/DTS(Convert PTS/DTS)
packet.pts = av_rescale_q_rnd(packet.pts, inStream->time_base, outStream->time_base,
(AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
packet.dts = av_rescale_q_rnd(packet.dts, inStream->time_base, outStream->time_base,
(AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
packet.duration = av_rescale_q(packet.duration, inStream->time_base, outStream->time_base);
packet.pos = -1;
//Print to Screen
if (packet.stream_index == m_videoStreamIndex[0])
{
LOG("Send %8d video frames to output URL\n", videoWriteFrameCount);
++videoWriteFrameCount;
}
ret = av_interleaved_write_frame(m_outputContext, &packet);
if (ret < 0)
{
LOG("Error muxing packet\n");
av_free_packet(&packet);
break;
}
av_free_packet(&packet);
}
//写文件尾
av_write_trailer(m_outputContext);
return 0;
}
值得注意的是,如果不执行延迟的操作,数据会过快发送给服务器,在播放的时候,画面一闪而过就没有了。
延迟的操作,尚且看懂,但是时间戳的计算方法就我现在还没搞懂,有时间再研究一下。