分享

用FFMPEG SDK进行视频转码压缩时解决音视频不同步问题的方法(转)

 开花结果 2016-05-05
用FFMPEG SDK进行视频转码压缩的时候,转码成功后去看视频的内容,发现音视频是不同步的。这个的确是一个恼火的事情。我在用FFMPEG SDK做h264格式的FLV文件编码Filter的时候就碰到了这个问题。

经过研究发现,FFMPEG SDK写入视频的时候有两个地方用来控制写入的时间戳,一个是AvPacket, 一个是AvFrame。 在调用avcodec_encode_video的时候需要传入AvFrame的对象指针,也就是传入一帧未压缩的视频进行压缩处理,AvFrame包含一个pts的参数,这个参数就是当前帧将来在还原播放的时候的时间戳。而AvPacket里面也有pts,还有dts。说起这个就必须要说明一下I,P,B三种视频压缩帧。I帧就是关键帧,不依赖于其他视频帧,P帧是向前预测的帧,只依赖于前面的视频帧,而B帧是双向预测视频帧,依赖于前后视频帧。由于B帧的存在,因为它是双向的,必须知道前面的视频帧和后面的视频帧的详细内容后,才能知道本B帧最终该呈现什么图像。而pts和dts两个参数就是用来控制视频帧的显示和解码的顺序。

pts就是帧显示的顺序。

dts就是帧被读取进行解码的顺序。

如果没有B帧存在,dts和pts是相同的。反之,则是不相同的。关于这个的详细介绍可以参考一下mpeg的原理。

再说说AvPacket中包含的pts和dts两个到底该设置什么值?

pts和dts需要设置的就是视频帧解码和显示的顺序。每增加一帧就加一,并不是播放视频的时间戳。
但是实践证明经过rmvb解码的视频有时候并不是固定帧率的,而是变帧率的,这样,如果每压缩一帧,pts和dts加一的方案为导致音视频不同步。

那怎么来解决音视频同步的问题呢?

请看如下代码段。

lTimeStamp 是通过directshow 获取的当前的视频帧的时间戳。

m_llframe_index为当前已经经过压缩处理的帧的数量。

首先av_rescale计算得到当前压缩处理已经需要处理什么时间戳的视频帧,如果该时间戳尚未到达directshow当前提供的视频帧的时间戳,则将该帧丢弃掉。

否则进行压缩操作。并设置AVPacket的pts和dts。这里假设B帧不存在。

因为在将来播放的时候视频以我们设定的固定播放帧率进行播放,所以需要根据设定的播放帧率计算得到的视频帧时间戳和directshow提供的当前视频帧的时间戳进行比较,设定是否需要进行实施延缓播放的策略。如果需要延缓播放,则将pts增加步长2,否则以普通速度播放,则设置为1.dts与之相同。
__int64 x =av_rescale(m_llframe_index,AV_TIME_BASE*(int64_t)c-<time_base.num,c-<time_base.den);
if( x < lTimeStamp )
{
return TRUE;
}
m_pVideoFrame2-<pts = lTimeStamp;
m_pVideoFrame2-<pict_type = 0;

int out_size = avcodec_encode_video( c, m_pvideo_outbuf, video_outbuf_size,m_pVideoFrame2 );
/* if zero size, it means the image was buffered */
if (out_size < 0)
{
AVPacket pkt;
av_init_packet(&pkt);

if( x < lTimeStamp )
{
pkt.pts = pkt.dts = m_llframe_index;
pkt.duration = 0;
}
else
{
pkt.duration = (lTimeStamp - x)*c-<time_base.den/1000000 + 1;
pkt.pts = m_llframe_index;
pkt.dts = pkt.pts;
m_llframe_index += pkt.duration;
}

//pkt.pts = lTimeStamp * (__int64)frame_rate.den / 1000;
if( c-<coded_frame && c-<coded_frame-<key_frame )
{
pkt.flags |= PKT_FLAG_KEY;
}

pkt.stream_index= m_pVideoStream-<index;
pkt.data= m_pvideo_outbuf;
pkt.size= out_size;

/* write the compressed frame in the media file */
ret = av_interleaved_write_frame( m_pAvFormatContext, &pkt );
}
else
{
ret = 0;
}
请问avcodec_decode_video解码的帧为什么后面的比前面的pts小呢?

请问如下代码:
while( av_read_frame(pFormatCtxSource,&packet)<=0 )
{
if( packet.stream_index==videoStream )
{
int out_size = avcodec_decode_video(pCodecCtxSource,pFrameSource, &bFrameFinished, packet.data, packet.size); // Decode fromsource frame

if( bFrameFinished )
{
pFrameSource-<pts =av_rescale_q(packet.pts, pCodecCtxSource-<time_base,pStCodec-<time_base);
int out_size =avcodec_encode_video(pStCodec, video_buffer, 200000, pFrameSource); // Encodeto output
if( out_size<0 )
{
// ...
}
}
}

av_free_packet(&packet);

}

在我Decode的时候,第一帧得到的 pFrameSource-<pts 是96,再解第二帧的时候,pFrameSource-<pts 计算完后就成了80几,后几帧也是比96小,过一会又会解出来一个100多的,接下来又是比100多小的,这是为什么?在Encode的时候,先Encode一个pts=96的,再去Encode比96小的帧就返回-1了,直到找到一个比96大的。

另外,我计算pts的方法正确吗?


答复:

Because you have B - Frame

for example:

the Inputsequence for video encoder
1  2  3   4   5    6   7
I   B   B   P  B   B   I

Let's take1,2,3.. as PTS for simplification

the out sequencefor video encoder ( this equals the decoder sequence)
1  4  2    3   7   5   6
I  P    B   B   I    B   B

you will get aPTS sequence as following:

1  4  2  3  7  5  6 

7  5 6sequence will be same as your question


问:

哦,那是不是我的pts不能这么算呢?而是要每次+1,对吗?那么,packet中的pts和dts要用在什么地方呢?我这样按存储顺序进行解码的话,显示之前是不是要自己进行缓存呢?谢谢!


另外,还有个问题,既然解码的时候,不一定是按照pts递增的顺序得到的解码后的画面,那我在编码图像的时候,是应该按照解码出来的帧顺序进行编码吗?还是把帧先缓存起来,最后严格接照图像的显示顺序来编码呢?用代码来表示,就是:
方法一:
while(av_read_frame )

解码; 
pts+1; 
编码; 
输出;
}

方法二:
while(av_read_frame )
{
解码;
if( pts
{
缓存;
}
else
{
编码缓存的帧并写入文件;
}
}

这两个方法,哪个是正确的呢?因为我看到网上的代码都用的是方法一,但是我觉得方法二是对的呀?


答:

the output of decoderis the right order for display because I/P frames will be cacheduntil next I/P



理解:


Decoder 后output的pts 是按正常的顺序,即显示的顺序输出的,如果有B帧,decoder会缓存。

但encoder后,输出的是按dts输出的。

Pts,dts并不是时间戳,而更应该理解为frame的顺序序列号。由于每帧frame的帧率并不一定是一致的,可能会变化的。转换为时间戳的话,应该是(pts*帧率)。为加深理解

可以将pts比做是第pts帧frame,假设每帧的帧率不变的话,则显示的时间戳为(pts*帧率),如果考虑帧率变化的,则要想办法将(pts*当前的帧率)累加到后面。


在tutorial5中在decode 下增加trace后打印情况:

len1 = avcodec_decode_video(is-<video_st-<codec,pFrame, &frameFinished,

packet-<data,packet-<size);

printf("-----------------------------------------------------------------------------\n");

printf("avcodec_decode_videopacket-<pts:%x,packet-<dts:%x\n",packet-<pts,packet-<dts);

printf("avcodec_decode_videopFrame-<pkt_pts:%x,pFrame-<pkt_dts:%x,pFrame-<pts:%x\n",pFrame-<pkt_pts,pFrame-<pkt_dts,pFrame-<pts);

if(pFrame-<opaque)

printf("avcodec_decode_video*(uint64_t *)pFrame-<opaque:%x\n",*(uint64_t *)pFrame-<opaque);


其中播一个mp4文件的打印情况:

-----------------------------------------------------------------------------

avcodec_decode_video packet-<pts:1ae,packet-<dts:0

avcodec_decode_videopFrame-<pkt_pts:0,pFrame-<pkt_dts:80000000,pFrame-<pts:0

avcodec_decode_video *(uint64_t *)pFrame-<opaque:1ae

-----------------------------------------------------------------------------

avcodec_decode_video packet-<pts:1af,packet-<dts:0

avcodec_decode_videopFrame-<pkt_pts:0,pFrame-<pkt_dts:80000000,pFrame-<pts:0

avcodec_decode_video *(uint64_t *)pFrame-<opaque:1af

-----------------------------------------------------------------------------

avcodec_decode_video packet-<pts:24c,packet-<dts:0

avcodec_decode_videopFrame-<pkt_pts:0,pFrame-<pkt_dts:80000000,pFrame-<pts:0

avcodec_decode_video *(uint64_t *)pFrame-<opaque:1ac

-----------------------------------------------------------------------------

avcodec_decode_video packet-<pts:24d,packet-<dts:0

avcodec_decode_videopFrame-<pkt_pts:0,pFrame-<pkt_dts:80000000,pFrame-<pts:0

avcodec_decode_video *(uint64_t *)pFrame-<opaque:24d

-----------------------------------------------------------------------------

avcodec_decode_video packet-<pts:24e,packet-<dts:0

avcodec_decode_videopFrame-<pkt_pts:0,pFrame-<pkt_dts:80000000,pFrame-<pts:0

avcodec_decode_video*(uint64_t *)pFrame-<opaque:24e


以下为播放rm文件的情况:

-----------------------------------------------------------------------------

avcodec_decode_videopacket-<pts:1831b,packet-<dts:0

avcodec_decode_videopFrame-<pkt_pts:0,pFrame-<pkt_dts:80000000,pFrame-<pts:0

avcodec_decode_video *(uint64_t *)pFrame-<opaque:1831b

-----------------------------------------------------------------------------

avcodec_decode_videopacket-<pts:18704,packet-<dts:0

avcodec_decode_videopFrame-<pkt_pts:0,pFrame-<pkt_dts:80000000,pFrame-<pts:0

avcodec_decode_video *(uint64_t *)pFrame-<opaque:18704

-----------------------------------------------------------------------------

avcodec_decode_videopacket-<pts:18aed,packet-<dts:0

avcodec_decode_videopFrame-<pkt_pts:0,pFrame-<pkt_dts:80000000,pFrame-<pts:0

avcodec_decode_video *(uint64_t *)pFrame-<opaque:18aed

-----------------------------------------------------------------------------

avcodec_decode_videopacket-<pts:18ed6,packet-<dts:0

avcodec_decode_videopFrame-<pkt_pts:0,pFrame-<pkt_dts:80000000,pFrame-<pts:0

avcodec_decode_video *(uint64_t *)pFrame-<opaque:18ed6

-----------------------------------------------------------------------------

avcodec_decode_videopacket-<pts:192bf,packet-<dts:0

avcodec_decode_videopFrame-<pkt_pts:0,pFrame-<pkt_dts:80000000,pFrame-<pts:0

avcodec_decode_video *(uint64_t *)pFrame-<opaque:192bf

-----------------------------------------------------------------------------

avcodec_decode_videopacket-<pts:196a8,packet-<dts:0

avcodec_decode_videopFrame-<pkt_pts:0,pFrame-<pkt_dts:80000000,pFrame-<pts:0

avcodec_decode_video *(uint64_t *)pFrame-<opaque:196a8



可以看出有的pts是+1 累加,有的是加了很多,但都是按顺序累加的。当传人decoder前的packet有pts时,则decoder后获取的frame将会赋值packet的pts;当传人的packet 只是一帧的部分数据或是B帧,由于decoder出来的frame要按正常的pts顺序输出,有可能decoder不会获取到frame ,或decoder内部会缓存也不会输出frame,即frame的pts会为空。Frame pts(即opaque) 为空的话则会看frame-<dts,dts都没有的话才认为frame-<pts为0.

对于:

pts *= av_q2d(is-<video_st-<time_base);////即pts*帧率


// Did we get avideo frame?

if(frameFinished) {

pts =synchronize_video(is, pFrame, pts);

///// synchronize_video考虑了3中情况:

1.    pts拿到的话就用该pts

2.    pts没有拿到的话就用前一帧的pts时间

3.    如果该帧要重复显示,则将显示的数量*帧率,再加到前面的pts中。

if(queue_picture(is, pFrame, pts) < 0 decodershowp>



static double synchronize_video(VideoState *is, AVFrame*src_frame, double pts) {


doubleframe_delay;


if(pts != 0) {

/* if we havepts, set video clock to it */

is-<video_clock = pts;

} else {

/* if we aren'tgiven a pts, set it to the clock */

pts =is-<video_clock;

}

/* update thevideo clock */

/////很关键:前面传进来的pts已经是时间戳了,是当前frame开始播放的时间戳,

/////下面frame_delay是该帧显示完将要花费的时间,(pts+frame_delay)也即是/////预测的下一帧将要播放的时间戳。

frame_delay =av_q2d(is-<video_st-<codec-<time_base);

/* if we arerepeating a frame, adjust clock accordingly */


//////重复多帧的话要累加上

frame_delay +=src_frame-<repeat_pict * (frame_delay * 0.5);

is-<video_clock += frame_delay;

return pts;/////此时返回的值即为下一帧将要开始显示的时间戳。

}

///////开定时器去显示帧队列中的已经decode过的数据,按前面的分析我们已经知道帧队列中的数据已经是按pts顺序插入到队列中的。Timer的作用就是有帧率不一致及重复帧的情况造成时间戳不是线性的,有快有慢,从而tutorial5才有timer的方式来播放:追赶

以下是一个网友很直观浅显的例子解释:

ccq(183892517) 17:05:21 if(packet-<dts ==AV_NOPTS_VALUE 是不是就是没有获取到dts的情况?

David Cen(3727567) 17:06:44 就是有一把尺子 一只蚂蚁跟着一个标杆走  David Cen(3727567) 17:06:58 标杆是匀速的 蚂蚁或快或慢 DavidCen(3727567) 17:07:18 慢了你就抽它 让他跑起来 快了就拽它  David Cen(3727567) 17:07:38 这样音(标杆)视频(蚂蚁)就能同步了 DavidCen(3727567) 17:08:00 这里最大的问题就是音频是匀速的 视频是非线性的


另外:此时vp–<pts获取到的pts已经转化为时间戳了,这个时间戳为就是当前帧显示结束的时间戳,也即是下一帧将显示的预测时间戳。

static void video_refresh_timer(void *userdata) {


VideoState *is = (VideoState*)userdata;

VideoPicture *vp;

double actual_delay, delay,sync_threshold, ref_clock, diff;


if(is-<video_st) {

if(is-<pictq_size == 0) {

schedule_refresh(is, 1);

} else {

vp =&is-<pictq[is-<pictq_rindex];

delay = vp-<pts -is-<frame_last_pts; /* the pts from last time */  ////这是当前要显示的frame和下一副                                                        //////将要显示的frame的间隔时间

if(delay < delay>= 1.0) {

/* if incorrect delay, useprevious one */

delay =is-<frame_last_delay;

}

/* save for next time */

is-<frame_last_delay =delay;

is-<frame_last_pts =vp-<pts;


/* update delay to sync toaudio */

ref_clock = get_audio_clock(is);/////获取到声音当前播放的时间戳。

diff = vp-<pts -ref_clock;////// vp-<pts实际上是预测的下一帧将要播放的开始时间,


//////////也就是说在diff这段时间中声音是匀速发生的,但是在delay这段时间frame的显示可能就会有快//////////慢的区别。

/* Skip or repeat the frame.Take delay into account

FFPlay still doesn't "know if this is thebest guess." */

sync_threshold = (delay <AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;


if(fabs(diff) < AV NOSYNC_THRESHOLD p>

if(diff < -sync_threshold p>

delay = 0;//////下一帧画面显示的时间和当前的声音很近的话加快显示下一帧(即后面video_display显示完当前帧后开启定时器很快去显示下一帧)

} else if(diff <=sync_threshold) {

delay = 2 * delay;//////下一帧开始显示的时间和当前声音的时间隔的比较长则延缓,即两帧画面间话的显示的时间长度大于两帧画面间的声音播放的时间,则我们将两帧画显示的时候加倍拖长点,比如帧1和帧2的时间显示间隔为40ms,但帧1和帧2的声音播放时间为55ms,怎么办呢?我们不可能去打乱声音的质量的,则我们采用的方法是:将两帧画面的播放间隔加大,本来是过30ms就要开始播下一帧的,我们改成60ms后才播下一帧。

}

}/////

////当然如果diff大于AV_NOSYNC_THRESHOLD,即快进的模式了,画面跳动太大,不存在音视频同步的问题了。

is-<frame_timer += delay;

/* computer the REAL delay*/

actual_delay =is-<frame_timer - (av_gettime() / 1000000.0);

if(actual_delay < 0 p>

/* Really it should skipthe picture instead */

actual_delay = 0.010;

}

schedule_refresh(is,(int)(actual_delay * 1000 + 0.5));////开定时器去显示下一帧

/* show the picture! */

video_display(is);////立马显示当前帧


/* update queue for nextpicture! */

if(++is-<pictq_rindex ==VIDEO_PICTURE_QUEUE_SIZE) {

is-<pictq_rindex = 0;

}

SDL_LockMutex(is-<pictq_mutex);

is-<pictq_size--;

SDL_CondSignal(is-<pictq_cond);

SDL_UnlockMutex(is-<pictq_mutex);

}

} else {

schedule_refresh(is, 100);

}

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多