目录
一、抛砖
最近在测试拉rtmp转码推rtmp,使用的源是一个文件循环推出来的rtmp流,当源从文件尾跳到文件头的时候,会出现x264的错误,之后我的程序就会出现推流失败,查看了原因,应该是送入推流的时间戳错误。
[h264 @ 0x7f8f2c001200] Frame num change from 37 to 38
[h264 @ 0x7f8f2c001200] decode_slice_header error
...
[h264 @ 0x7f8f2c001200] Frame num change from 37 to 49
[h264 @ 0x7f8f2c001200] decode_slice_header error
[h264 @ 0x7f8f2c001200] Frame num change from 37 to 0
[h264 @ 0x7f8f2c001200] decode_slice_header error
avcodec_send_packet ret=0
avcodec_receive_frame ret=-11
avcodec_send_packet ret=0
avcodec_receive_frame ret=-11
avcodec_send_packet ret=0
avcodec_receive_frame ret=-11
avcodec_send_packet ret=0
avcodec_receive_frame ret=-11
avcodec_send_packet ret=0
avcodec_receive_frame ret=-11
avcodec_send_packet ret=0
avcodec_receive_frame ret=0 ====OK!
avcodec_send_packet ret=0
avcodec_receive_frame ret=0 ====OK!
avcodec_send_packet ret=0
avcodec_receive_frame ret=0 ====OK!
查看了日志时间戳真的不对,后面avcodec_receive_frame 成功之后,会解码解出2个相同的时间戳,出现了大时间戳先于小时间戳写入,很奇怪。但是我认为遇到这种源从文件尾跳到文件头的时候,的确应该是要做一些手段来处理的。
然后我用命令行测,发现ffmpeg也会出现x264的错误,但是ffmpeg能顺利继续,不出错。然后决定去翻一下源码来解决,并且也记录下吧。以前看ffplay的时间戳感觉设置的贼乱,的确,看了ffmpeg的源码,更乱了,但是最近看的也挺多的,能接受看ffmpeg的源码,也是一件不容易的事。
二、引玉
1、ffmpeg调试方法
这里先说一下ffmpeg命令行的调试,可以用gdb等调试,找到热点、关键的函数,然后我建议还是加上打印信息来查时间戳是最好的。ffmpeg也知道查时间戳很头疼,其实已经把时间戳日志给准备好了,你只要在命令行加上-debug_ts,就能打开查时间戳的日志,十分方便。
demuxer -> ist_index:1 type:video next_dts:80000 next_dts_time:0.08 next_pts:80000 next_pts_time:0.08 pkt_pts:80 pkt_pts_time:0.08 pkt_dts:80 pkt_dts_time:0.08 off:0 off_time:0
demuxer+ffmpeg -> ist_index:1 type:video pkt_pts:80 pkt_pts_time:0.08 pkt_dts:80 pkt_dts_time:0.08 off:0 off_time:0
decoder -> ist_index:1 type:video frame_pts:80 frame_pts_time:0.08 best_effort_ts:80 best_effort_ts_time:0.08 keyframe:0 frame_type:2 time_base:1/1000
filter -> pts:2 pts_time:0.08 exact:2.000008 time_base:1/25
encoder <- type:video frame_pts:2 frame_pts_time:0.08 time_base:1/25
encoder -> type:video pkt_pts:2 pkt_pts_time:0.08 pkt_dts:2 pkt_dts_time:0.08
muxer <- type:video pkt_pts:80 pkt_pts_time:0.08 pkt_dts:80 pkt_dts_time:0.08 size:50
可以看到关键步骤的流程基本都有,可见ffmpeg(´థ౪థ),文末会对重要时间戳解释下
在下面代码中我就用<av_log("demuxer+ffmpeg)>代表打印的地方
2、上代码(伪代码)
如果不想看,可以直接跳到这一小节,直接看解决问题。
(1)直接看transcode_step()
int transcode_step()
{
...
ret = process_input(ist->file_index);//主要是拉流,解码,送入过滤器
...
return reap_filters(0);//主要是从过滤器取帧,编码,写流
}
(2)分解输入-process_input()
int process_input()
{
ret = get_input_packet(ifile, &pkt);//里面调用了av_read_frame
<av_log("demuxer)>
...
<av_log("demuxer+ffmpeg)>
process_input_packet(ist, &pkt, 0);//往下分解
discard_packet:
av_packet_unref(&pkt);
return 0;
}
void process_input_packet()
{
...//有大量对pts的操作
if (pkt && pkt->dts != AV_NOPTS_VALUE) {
ist->next_dts = ist->dts = av_rescale_q(pkt->dts, ist->st->time_base, AV_TIME_BASE_Q);//这里很关键,time_base=1/1000
if (ist->dec_ctx->codec_type != AVMEDIA_TYPE_VIDEO || !ist->decoding_needed)
ist->next_pts = ist->pts = ist->dts;
}
...
decode_video()//解码视频,往下分解
...
}
void decode_video()
{
...
if (ist->dts != AV_NOPTS_VALUE)
dts = av_rescale_q(ist->dts, AV_TIME_BASE_Q, ist->st->time_base);
...
decode()//具体解码,不深入了
...
if(best_effort_timestamp != AV_NOPTS_VALUE) {//这里注意给 decoded_frame->pts = best_effort_timestamp赋值了
int64_t ts = av_rescale_q(decoded_frame->pts = best_effort_timestamp, ist->st->time_base, AV_TIME_BASE_Q);
if (ts != AV_NOPTS_VALUE)
ist->next_pts = ist->pts = ts;
}
...
<av_log("decoder ->)>
...
send_frame_to_filters(decoded_frame);//送去给过滤器,后面可以通过reap_filters()里的
av_buffersink_get_frame_flags()函数取出,这里不深入了,
}
(3)分解输出-reap_filters()
void reap_filters()
{
for (i = 0; i < nb_output_streams; i++) {
while (1) {
ret=av_buffersink_get_frame_flags()//取出过滤器里的数据,一般是已经缩放好的frame
if (ret < 0) ...//如果没数据就不会去编码,不用太在意
if (filtered_frame->pts != AV_NOPTS_VALUE) ...//如果没数据就不会去编码,对过滤好的frame计算时间戳,一般也可以不看
switch (av_buffersink_get_type(filter)) {
case AVMEDIA_TYPE_VIDEO:
...
<av_log("filter ->)>
do_video_out(of, ost, filtered_frame, float_pts);//编码输出 往下分解
case AVMEDIA_TYPE_AUDIO:
...
}
}
}
}
void do_video_out(...)
{
...
switch (format_video_sync) {
case VSYNC_VSCFR:
...
case VSYNC_VFR://默认会进到这里
...
}
...//以上都是针对影响时间戳的配置,对时间戳做配置,比如vsync或者强制关键帧等配置,用的少
for (i = 0; i < nb_frames; i++) {
in_picture->pts = ost->sync_opts;
...
avcodec_send_frame()//编码
while (1) {
avcodec_receive_packet()
<av_log("encoder ->)>
av_packet_rescale_ts(&pkt, enc->time_base, ost->mux_timebase);
<av_log("encoder ->>>//自己加的
}
ost->sync_opts++;//给编码器的时间!!
}
}
可以看到,一般情况下,送给编码器的时间居然是ost->sync_opts++;!!!
一直单纯的以为,时间戳从拉传到推,能处理好的才是最标准的。没想到ffmpeg居然自己留了一手,虽然我之前一直也用类似方式。
三、回首掏
1、解决问题
仔细查看命令行的时间戳日志,发现ffmpeg也出现解码解出2个相同的时间戳,但是为什么它能成功呢?让我们看一下相同时间戳的主要日志:
filter -> pts:586 pts_time:23.44 exact:585.625008 time_base:1/25
encoder -> type:video pkt_pts:586 pkt_pts_time:23.44 pkt_dts:586 pkt_dts_time:23.44
filter -> pts:586 pts_time:23.44 exact:586.375008 time_base:1/25
encoder -> type:video pkt_pts:587 pkt_pts_time:23.48 pkt_dts:587 pkt_dts_time:23.48
实际可以看到filter -> pts:586 在2次都不变的,但是encoder -> type:video pkt_pts:居然是递增的!
结合上面我们看的源码,我受到了‘惊吓’,不,我们能知道其实送到编码器其实是自己内部递增的,跟你传的啥值一点问题都没有。
我仿照改动我的程序之后,成功解决了之前的问题。
2、时间戳日志示例
直接看我括号后的参数就行,这里面的encoder ->>是我自己在源码里加的,其实不加也行,直接看muxer ->也是一样的
demuxer -> ist_index:1 type:video next_dts:NOPTS next_dts_time:NOPTS next_pts:NOPTS next_pts_time:NOPTS (从read获取的pts)pkt_pts:40 pkt_pts_time:0.04 pkt_dts:40 pkt_dts_time:0.04 off:0 off_time:0
demuxer+ffmpeg -> ist_index:1 type:video (ffmpeg简单调整后)pkt_pts:40 pkt_pts_time:0.04 pkt_dts:40 pkt_dts_time:0.04 off:0 off_time:0
(解码后) decoder -> ist_index:1 type:video (解码后的时间戳)frame_pts:40 frame_pts_time:0.04 best_effort_ts:40 best_effort_ts_time:0.04 keyframe:1 frame_type:1 (输入源的AVStream的时间基)time_base:1/1000
(过滤后) filter -> (过滤后的时间戳)pts:1 pts_time:0.04 exact:1.000008 (过滤器的时间基)time_base:1/25
(编码前) encoder <- type:video (过滤后的时间戳)frame_pts:1 frame_pts_time:0.04 (编码器AVCodec的时间基)time_base:1/25
(编码后) encoder -> type:video (过滤后的时间戳)pkt_pts:1 pkt_pts_time:0.04 pkt_dts:1 pkt_dts_time:0.04
(编码转时间后) encoder ->> type:video (转换后的时间戳)pkt_pts:40 pkt_pts_time:0.04 pkt_dts:40 pkt_dts_time:0.04
可以看到ffmpeg拉流后把时间戳转换为内部时间基,然后解码再计算,送给了过滤器,从过滤器处理再计算,送给了编码器,编码后根据你写的流的时间基进行转换,然后送去推流。
在我这里rtmp也就是flv,时间基是1000;帧率25,pts间隔是40。拉流后把时间戳转换为40000,解码后计算为40,过滤器转换后,变成了1,编码后再转成40。