目录
一、抛砖
二、引玉
1、ffmpeg调试方法
2、上代码(伪代码)
(1)直接看transcode_step()
(2)分解输入-process_input()
(3)分解输出-reap_filters()
三、回首掏
1、解决问题
2、时间戳日志示例
最近在测试拉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的源码,也是一件不容易的事。
这里先说一下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)>代表打印的地方
如果不想看,可以直接跳到这一小节,直接看解决问题。
int transcode_step() { ... ret = process_input(ist->file_index);//主要是拉流,解码,送入过滤器 ... return reap_filters(0);//主要是从过滤器取帧,编码,写流 }
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()函数取出,这里不深入了, }
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居然自己留了一手,虽然我之前一直也用类似方式。
仔细查看命令行的时间戳日志,发现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:居然是递增的!
结合上面我们看的源码,我受到了‘惊吓’,不,我们能知道其实送到编码器其实是自己内部递增的,跟你传的啥值一点问题都没有。
我仿照改动我的程序之后,成功解决了之前的问题。
直接看我括号后的参数就行,这里面的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。