在前面一节基于FFmpeg进行RTMP推流(一)我们写了最简单的一版推流代码。但细心调试过的兄弟会发现当我们把文件换成mp4后,发现上面的代码在写入文件头时报错。也就是说上一版的代码是有bug的。
问题一 incompatible with output codec id
我们先看下bug提示:
定位之后发现是在这里出错。
//写入头部信息
ret = avformat_write_header(octx, 0);
我们跟踪一下源代码:
原来和这个字段有关。我们看下这个字段的定义:
/**
* Additional information about the codec (corresponds to the AVI FOURCC).
*/
uint32_t codec_tag;
与编码相关的附加信息。在根据源码我们知道这里得设置为0。而代码中我们设置编码器用的avcodec_parameters_copy(out->codecpar, ictx->streams[i]->codecpar)
但我们却设置成了out->codec->codec_tag = 0;
这个根本没有匹配。也就是说AVCodecContext *codec;
和codec->codec_tag
对应。AVCodecParameters *codecpar;
和codecpar->codec_tag
对应。从注释看到AVCodecContext *codec;
已经被弃用。最好使用AVCodecParameters
。同样对应四种方法:
//将输入编解码器上下文信息 copy 给输出编解码器上下文
//ret = avcodec_copy_context(out_stream->codec, in_stream->codec);
ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
//ret = avcodec_parameters_from_context(out_stream->codecpar, in_stream->codec);
//ret = avcodec_parameters_to_context(out_stream->codec, in_stream->codecpar);
最终我们就可以修改我们的代码,也就是将:
for (int i = 0; i < ictx->nb_streams; i++) {
//创建一个新的流到octx中
AVStream *out = avformat_new_stream(octx, ictx->streams[i]->codec->codec);
if (!out) {
return avError(0);
}
//复制配置信息 用于mp4 过时的方法
//ret=avcodec_copy_context(out->codec, ictx->streams[i]->codec);
ret = avcodec_parameters_copy(out->codecpar, ictx->streams[i]->codecpar);
if (ret < 0) {
return avError(ret);
}
out->codec->codec_tag = 0;
}
修改为:
for (i = 0; i < ictx->nb_streams; i++) {
//获取输入视频流
AVStream *in_stream = ictx->streams[i];
//为输出上下文添加音视频流(初始化一个音视频流容器)
AVStream *out_stream = avformat_new_stream(octx, in_stream->codec->codec);
if (!out_stream) {
printf("未能成功添加音视频流\n");
ret = AVERROR_UNKNOWN;
}
//将输入编解码器上下文信息 copy 给输出编解码器上下文
//ret = avcodec_copy_context(out_stream->codec, in_stream->codec);
ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
//ret = avcodec_parameters_from_context(out_stream->codecpar, in_stream->codec);
//ret = avcodec_parameters_to_context(out_stream->codec, in_stream->codecpar);
if (ret < 0) {
printf("copy 编解码器上下文失败\n");
}
out_stream->codecpar->codec_tag = 0;
out_stream->codec->codec_tag = 0;
if (octx->oformat->flags & AVFMT_GLOBALHEADER) {
out_stream->codec->flags = out_stream->codec->flags | CODEC_FLAG_GLOBAL_HEADER;
}
}
//输入流数据的数量循环
for (i = 0; i < ictx->nb_streams; i++) {
if (ictx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
videoindex = i;
break;
}
}
问题二 视频卡顿
解决完上面的问题后,我们发现播放的视频会卡顿。说明我们推流的速度有问题。快了或慢了都会有问题。我们大概可以定位到,我们在计算延时的时候出现了错误。我们先看下计算延时时间的代码:
//计算转换时间戳 pts dts
//获取时间基数
AVRational itime = ictx->streams[avPacket.stream_index]->time_base;
AVRational otime = octx->streams[avPacket.stream_index]->time_base;
avPacket.pts = av_rescale_q_rnd(avPacket.pts, itime, otime, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_NEAR_INF));
avPacket.dts = av_rescale_q_rnd(avPacket.pts, itime, otime, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_NEAR_INF));
//到这一帧时候经历了多长时间
avPacket.duration = av_rescale_q_rnd(avPacket.duration, itime, otime, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_NEAR_INF));
avPacket.pos = -1;
//视频帧推送速度
if (ictx->streams[avPacket.stream_index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
AVRational tb = ictx->streams[avPacket.stream_index]->time_base;
//已经过去的时间
long long now = av_gettime() - startTime;
long long dts = 0;
dts = avPacket.dts * (1000 * 1000 * r2d(tb));
if (dts > now)
av_usleep(dts - now);
else {
cout << "sss";
}
}
我们先做了dts和pts的转化,然后再根据dts获取播放的时间,最终计算和当前时间的时间差。
- 第一个问题。avPacket.dts我们使用的是pts来计算的,这个是有问题的。如果存在B帧的话,这个值就会出错。
- 第二个问题。
dts = avPacket.dts * (1000 * 1000 * r2d(tb));
相当于dts = avPacket.dts * (1000 * 1000 * (tb.num/tb.den));
这个计算也是有问题的。因为在这一版的FFmpeg中。out_stream->time_base
的值为1/9000000。这里我们确以1/1000000计算的,这个当然就错了。有人问,我怎么知道是1/9000000,我们看初始化流时候的源码:
这里设置了time_base。有没有觉得豁然开朗。问题我们找到了,解决当然就简单了。我们这样解决
- 1 就算延时时间差以
AV_TIME_BASE
(1000000)为单位。 - 2 延时后再根据
out_stream->time_base
计算AVPacket的pts和dts。
//延时
if (pkt.stream_index == videoindex) {
AVRational time_base = ictx->streams[videoindex]->time_base;
AVRational time_base_q = { 1,AV_TIME_BASE };
//计算视频播放时间
int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
//计算实际视频的播放时间
int64_t now_time = av_gettime() - start_time;
AVRational avr = ictx->streams[videoindex]->time_base;
cout << avr.num << " " << avr.den << " "<<pkt.dts <<" "<<pkt.pts<<" "<< pts_time <<endl;
if (pts_time > now_time) {
//睡眠一段时间(目的是让当前视频记录的播放时间与实际时间同步)
av_usleep((unsigned int)(pts_time - now_time));
}
}
in_stream = ictx->streams[pkt.stream_index];
out_stream = octx->streams[pkt.stream_index];
//计算延时后,重新指定时间戳
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;
OK以上问题全部解决
这里贴出完整的代码:
#include <iostream>
using namespace std;
//引入头文件
extern "C"
{
#include "libavformat/avformat.h"
//引入时间
#include "libavutil/time.h"
}
//引入库
#pragma comment(lib,"avformat.lib")
//工具库,包括获取错误信息等
#pragma comment(lib,"avutil.lib")
//编解码的库
#pragma comment(lib,"avcodec.lib")
int avError(int errNum);
static double r2d(AVRational r)
{
return r.num == 0 || r.den == 0 ? 0. : (double)r.num / (double)r.den;
}
int main() {
int videoindex = -1;
//所有代码执行之前要调用av_register_all和avformat_network_init
//初始化所有的封装和解封装 flv mp4 mp3 mov。不包含编码和解码
av_register_all();
//初始化网络库
avformat_network_init();
//使用的相对路径,执行文件在bin目录下。test.mp4放到bin目录下即可
const char *inUrl = "hs.mp4";
//输出的地址
const char *outUrl = "rtmp://192.166.11.13/live";
//////////////////////////////////////////////////////////////////
// 输入流处理部分
/////////////////////////////////////////////////////////////////
//打开文件,解封装 avformat_open_input
//AVFormatContext **ps 输入封装的上下文。包含所有的格式内容和所有的IO。如果是文件就是文件IO,网络就对应网络IO
//const char *url 路径
//AVInputFormt * fmt 封装器
//AVDictionary ** options 参数设置
AVFormatContext *ictx = NULL;
AVOutputFormat *ofmt = NULL;
//打开文件,解封文件头
int ret = avformat_open_input(&ictx, inUrl, 0, NULL);
if (ret < 0) {
return avError(ret);
}
cout << "avformat_open_input success!" << endl;
//获取音频视频的信息 .h264 flv 没有头信息
ret = avformat_find_stream_info(ictx, 0);
if (ret != 0) {
return avError(ret);
}
//打印视频视频信息
//0打印所有 inUrl 打印时候显示,
av_dump_format(ictx, 0, inUrl, 0);
//////////////////////////////////////////////////////////////////
// 输出流处理部分
/////////////////////////////////////////////////////////////////
AVFormatContext * octx = NULL;
//如果是输入文件 flv可以不传,可以从文件中判断。如果是流则必须传
//创建输出上下文
ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
if (ret < 0) {
return avError(ret);
}
cout << "avformat_alloc_output_context2 success!" << endl;
ofmt = octx->oformat;
cout << "nb_streams " << ictx->nb_streams << endl;
int i;
//for (i = 0; i < ictx->nb_streams; i++) {
// cout << "i " << i <<" "<< ictx->nb_streams<< endl;
// AVStream *in_stream = ictx->streams[i];
// AVCodec *codec = avcodec_find_decoder(in_stream->codecpar->codec_id);
// AVStream *out_stream = avformat_new_stream(octx, codec);
// if (!out_stream) {
// printf("Failed allocating output stream\n");
// ret = AVERROR_UNKNOWN;
// }
// AVCodecContext *pCodecCtx = avcodec_alloc_context3(codec);
// ret = avcodec_parameters_to_context(pCodecCtx, in_stream->codecpar);
// if (ret < 0) {
// printf("Failed to copy context input to output stream codec context\n");
// }
// pCodecCtx->codec_tag = 0;
// if (octx->oformat->flags & AVFMT_GLOBALHEADER) {
// pCodecCtx->flags |= CODEC_FLAG_GLOBAL_HEADER;
// }
// ret = avcodec_parameters_from_context(out_stream->codecpar, pCodecCtx);
// if (ret < 0) {
// printf("Failed to copy context input to output stream codec context\n");
// }
//}
for (i =