[记录一个bug]ffmpeg转码时间戳-伪代码版流程要点小记[已解决]

目录

一、抛砖

二、引玉

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的源码,也是一件不容易的事。

二、引玉

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。

上一篇:炫“库”行动-人大金仓有奖征文-Kingbase DTS数据迁移工具


下一篇:如何在网页上隐藏你的Email邮件地址