FFPLAY的原理(六)

显示视频
这就是我们的视频线程。现在我们看过了几乎所有的线程除了一个--记得我们调用schedule_refresh()函数吗?让我们看一下实际中是如何做的:

1 static void schedule_refresh(VideoState *is, int delay) { 
2 SDL_AddTimer(delay, sdl_refresh_timer_cb, is); 
3 } 

函数SDL_AddTimer()是SDL中的一个定时(特定的毫秒)执行用户定义的回调函数(可以带一些参数user data)的简单函数。我们将用这个函数来定时刷新视频--每次我们调用这个函数的时候,它将设置一个定时器来触发定时事件来把一帧从图像队列中显示到屏 幕上。
但是,让我们先触发那个事件。

1 static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) { 
2 SDL_Event event; 
3 event.type = FF_REFRESH_EVENT; 
4 event.user.data1 = opaque; 
5 SDL_PushEvent(&event); 
6 return 0; 
7 } 

这里向队列中写入了一个现在很熟悉的事件。FF_REFRESH_EVENT被定义成SDL_USEREVENT+1。要注意的一件事是当返回0的时候,SDL停止定时器,于是回调就不会再发生。
现在我们产生了一个FF_REFRESH_EVENT事件,我们需要在事件循环中处理它:

1 for(;;) { 
2 SDL_WaitEvent(&event); 
3 switch(event.type) { 
4 case FF_REFRESH_EVENT: 
5 video_refresh_timer(event.user.data1); 
6 break; 

于是我们就运行到了这个函数,在这个函数中会把数据从图像队列中取出:

 1 void video_refresh_timer(void *userdata) { 
 2 VideoState *is = (VideoState *)userdata; 
 3 VideoPicture *vp; 
 4 if(is->video_st) { 
 5 if(is->pictq_size == 0) { 
 6 schedule_refresh(is, 1); 
 7 } else { 
 8 vp = &is->pictq[is->pictq_rindex]; 
 9 schedule_refresh(is, 80); 
10 video_display(is); 
11 if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) { 
12 is->pictq_rindex = 0; 
13 } 
14 SDL_LockMutex(is->pictq_mutex); 
15 is->pictq_size--; 
16 SDL_CondSignal(is->pictq_cond); 
17 SDL_UnlockMutex(is->pictq_mutex); 
18 } 
19 } else { 
20 schedule_refresh(is, 100); 
21 } 
22 } 

现在,这只是一个极其简单的函数:当队列中有数据的时候,他从其中获得数据,为下一帧设置定时器,调用video_display函数来真正显示图像到屏 幕上,然后把队列读索引值加1,并且把队列的尺寸size减1。你可能会注意到在这个函数中我们并没有真正对vp做一些实际的动作,原因是这样的:我们将 在后面处理。我们将在后面同步音频和视频的时候用它来访问时间信息。你会在这里看到这个注释信息“timing密码here”。那里我们将讨论什么时候显 示下一帧视频,然后把相应的值写入到schedule_refresh()函数中。现在我们只是随便写入一个值80。从技术上来讲,你可以猜测并验证这个 值,并且为每个电影重新编译程序,但是:1)过一段时间它会漂移;2)这种方式是很笨的。我们将在后面来讨论它。
我们几乎做完了;我们仅仅剩了最后一件事:显示视频!下面就是video_display函数:

 1 void video_display(VideoState *is) { 
 2 SDL_Rect rect; 
 3 VideoPicture *vp; 
 4 AVPicture pict; 
 5 float aspect_ratio; 
 6 int w, h, x, y; 
 7 int i; 
 8 vp = &is->pictq[is->pictq_rindex]; 
 9 if(vp->bmp) { 
10 if(is->video_st->codec->sample_aspect_ratio.num == 0) { 
11 aspect_ratio = 0; 
12 } else { 
13 aspect_ratio = av_q2d(is->video_st->codec->sample_aspect_ratio) * 
14 is->video_st->codec->width / is->video_st->codec->height; 
15 } 
16 if(aspect_ratio <= 0.0) { 
17 aspect_ratio = (float)is->video_st->codec->width / 
18 (float)is->video_st->codec->height; 
19 } 
20 h = screen->h; 
21 w = ((int)rint(h * aspect_ratio)) & -3; 
22 if(w > screen->w) { 
23 w = screen->w; 
24 h = ((int)rint(w / aspect_ratio)) & -3; 
25 } 
26 x = (screen->w - w) / 2; 
27 y = (screen->h - h) / 2; 
28 rect.x = x; 
29 rect.y = y; 
30 rect.w = w; 
31 rect.h = h; 
32 SDL_DisplayYUVOverlay(vp->bmp, &rect); 
33 } 
34 } 

因为我们的屏幕可以是任意尺寸(我们设置为640x480并且用户可以自己来改变尺寸),我们需要动态计算出我们显示的图像的矩形大小。所以一开始我们需 要计算出电影的纵横比aspect ratio,表示方式为宽度除以高度。某些编解码器会有奇数采样纵横比,只是简单表示了一个像素或者一个采样的宽度除以高度的比例。因为宽度和高度在我们 的编解码器中是用像素为单位的,所以实际的纵横比与纵横比乘以样本纵横比相同。某些编解码器会显示纵横比为0,这表示每个像素的纵横比为1x1。然后我们 把电影缩放到适合屏幕的尽可能大的尺寸。这里的& -3表示与-3做与运算,实际上是让它们4字节对齐。然后我们把电影移到中心位置,接着调用SDL_DisplayYUVOverlay()函数。
结果是什么?我们做完了吗?嗯,我们仍然要重新改写声音部分的代码来使用新的VideoStruct结构体,但是那些只是尝试着改变,你可以看一下那些参考示例代码。最后我们要做的是改变ffmpeg提供的默认退出回调函数为我们的退出回调函数。

1 VideoState *global_video_state; 
2 int decode_interrupt_cb(void) { 
3 return (global_video_state && global_video_state->quit); 
4 } 

我们在主函数中为大结构体设置了global_video_state。
这就是了!让我们编译它:

gcc -o tutorial04 tutorial04.c -lavutil -lavformat -lavcodec -lz -lm \ 
`sdl-config --cflags --libs` 

请享受一下没有经过同步的电影!下次我们将编译一个可以最终工作的电影播放器。

如何同步视频
PTS和DTS
幸运的是,音频和视频流都有一些关于以多快速度和什么时间来播放它们的信息在里面。音频流有采样,视频流有每秒的帧率。然而,如果我们只是简单的通过数帧 和乘以帧率的方式来同步视频,那么就很有可能会失去同步。于是作为一种补充,在流中的包有种叫做DTS(解码时间戳)和PTS(显示时间戳)的机制。为了 这两个参数,你需要了解电影存放的方式。像MPEG等格式,使用被叫做B帧(B表示双向bidrectional)的方式。另外两种帧被叫做I帧和P帧 (I表示关键帧,P表示预测帧)。I帧包含了某个特定的完整图像。P帧依赖于前面的I帧和P帧并且使用比较或者差分的方式来编码。B帧与P帧有点类似,但 是它是依赖于前面和后面的帧的信息的。这也就解释了为什么我们可能在调用avcodec_decode_video以后会得不到一帧图像。
所以对于一个电影,帧是这样来显示的:I B B P。现在我们需要在显示B帧之前知道P帧中的信息。因此,帧可能会按照这样的方式来存储:IPBB。这就是为什么我们会有一个解码时间戳和一个显示时间戳 的原因。解码时间戳告诉我们什么时候需要解码,显示时间戳告诉我们什么时候需要显示。所以,在这种情况下,我们的流可以是这样的:
PTS: 1 4 2 3
DTS: 1 2 3 4
Stream: I P B B
通常PTS和DTS只有在流中有B帧的时候会不同。
当我们调用av_read_frame()得到一个包的时候,PTS和DTS的信息也会保存在包中。但是我们真正想要的PTS是我们刚刚解码出来的原始帧 的PTS,这样我们才能知道什么时候来显示它。然而,我们从avcodec_decode_video()函数中得到的帧只是一个AVFrame,其中并 没有包含有用的PTS值(注意:AVFrame并没有包含时间戳信息,但当我们等到帧的时候并不是我们想要的样子)。然而,ffmpeg重新排序包以便于 被avcodec_decode_video()函数处理的包的DTS可以总是与其返回的PTS相同。但是,另外的一个警告是:我们也并不是总能得到这个 信息。
不用担心,因为有另外一种办法可以找到帧的PTS,我们可以让程序自己来重新排序包。我们保存一帧的第一个包的PTS:这将作为整个这一帧的 PTS。我们 可以通过函数avcodec_decode_video()来计算出哪个包是一帧的第一个包。怎样实现呢?任何时候当一个包开始一帧的时 候,avcodec_decode_video()将调用一个函数来为一帧申请一个缓冲。当然,ffmpeg允许我们重新定义那个分配内存的函数。所以我 们制作了一个新的函数来保存一个包的时间戳。
当然,尽管那样,我们可能还是得不到一个正确的时间戳。我们将在后面处理这个问题。
同步
现在,知道了什么时候来显示一个视频帧真好,但是我们怎样来实际操作呢?这里有个主意:当我们显示了一帧以后,我们计算出下一帧显示的时间。然后我们简单 的设置一个新的定时器来。你可能会想,我们检查下一帧的PTS值而不是系统时钟来看超时是否会到。这种方式可以工作,但是有两种情况要处理。
首先,要知道下一个PTS是什么。现在我们能添加视频速率到我们的PTS中--太对了!然而,有些电影需要帧重复。这意味着我们重复播放当前的帧。这将导致程序显示下一帧太快了。所以我们需要计算它们。
第二,正如程序现在这样,视频和音频播放很欢快,一点也不受同步的影响。如果一切都工作得很好的话,我们不必担心。但是,你的电脑并不是最好的,很多视频 文件也不是完好的。所以,我们有三种选择:同步音频到视频,同步视频到音频,或者都同步到外部时钟(例如你的电脑时钟)。从现在开始,我们将同步视频到音 频。
写代码:获得帧的时间戳
现在让我们到代码中来做这些事情。我们将需要为我们的大结构体添加一些成员,但是我们会根据需要来做。首先,让我们看一下视频线程。记住,在这里我们得到 了解码线程输出到队列中的包。这里我们需要的是从avcodec_decode_video函数中得到帧的时间戳。我们讨论的第一种方式是从上次处理的包 中得到DTS,这是很容易的:

 1 double pts; 
 2 for(;;) { 
 3 if(packet_queue_get(&is->videoq, packet, 1) < 0) { 
 4 // means we quit getting packets 
 5 break; 
 6 } 
 7 pts = 0; 
 8 // Decode video frame 
 9 len1 = avcodec_decode_video(is->video_st->codec, 
10 pFrame, &frameFinished, 
11 packet->data, packet->size); 
12 if(packet->dts != AV_NOPTS_VALUE) { 
13 pts = packet->dts; 
14 } else { 
15 pts = 0; 
16 } 
17 pts *= av_q2d(is->video_st->time_base); 

如果我们得不到PTS就把它设置为0。
好,那是很容易的。但是我们所说的如果包的DTS不能帮到我们,我们需要使用这一帧的第一个包的PTS。我们通过让ffmpeg使用我们自己的申请帧程序来实现。下面的是函数的格式:
int get_buffer(struct AVCodecContext *c, AVFrame *pic);
void release_buffer(struct AVCodecContext *c, AVFrame *pic);
申请函数没有告诉我们关于包的任何事情,所以我们要自己每次在得到一个包的时候把PTS保存到一个全局变量中去。我们自己以读到它。然后,我们把值保存到AVFrame结构体难理解的变量中去。所以一开始,这就是我们的函数:

 1 uint64_t global_video_pkt_pts = AV_NOPTS_VALUE; 
 2 int our_get_buffer(struct AVCodecContext *c, AVFrame *pic) { 
 3 int ret = avcodec_default_get_buffer(c, pic); 
 4 uint64_t *pts = av_malloc(sizeof(uint64_t)); 
 5 *pts = global_video_pkt_pts; 
 6 pic->opaque = pts; 
 7 return ret; 
 8 } 
 9 void our_release_buffer(struct AVCodecContext *c, AVFrame *pic) { 
10 if(pic) av_freep(&pic->opaque); 
11 avcodec_default_release_buffer(c, pic); 
12 } 

函数avcodec_default_get_buffer和avcodec_default_release_buffer是ffmpeg中默认的申请缓冲的函数。函数av_freep是一个内存管理函数,它不但把内存释放而且把指针设置为NULL。
现在到了我们流打开的函数(stream_component_open),我们添加这几行来告诉ffmpeg如何去做:

1 codecCtx->get_buffer = our_get_buffer; 
2 codecCtx->release_buffer = our_release_buffer; 

现在我们必需添加代码来保存PTS到全局变量中,然后在需要的时候来使用它。我们的代码现在看起来应该是这样子:

 1 for(;;) { 
 2 if(packet_queue_get(&is->videoq, packet, 1) < 0) { 
 3 // means we quit getting packets 
 4 break; 
 5 } 
 6 pts = 0; 
 7 // Save global pts to be stored in pFrame in first call 
 8 global_video_pkt_pts = packet->pts; 
 9 // Decode video frame 
10 len1 = avcodec_decode_video(is->video_st->codec, pFrame, &frameFinished, 
11 packet->data, packet->size); 
12 if(packet->dts == AV_NOPTS_VALUE 
13 && pFrame->opaque && *(uint64_t*)pFrame->opaque != AV_NOPTS_VALUE) { 
14 pts = *(uint64_t *)pFrame->opaque; 
15 } else if(packet->dts != AV_NOPTS_VALUE) { 
16 pts = packet->dts; 
17 } else { 
18 pts = 0; 
19 } 
20 pts *= av_q2d(is->video_st->time_base); 

技术提示:你可能已经注意到我们使用int64来表示PTS。这是因为PTS是以整型来保存的。这个值是一个时间戳相当于时间的度量,用来以流的 time_base为单位进行时间度量。例如,如果一个流是24帧每秒,值为42的PTS表示这一帧应该排在第42个帧的位置如果我们每秒有24帧(这里 并不完全正确)。
我们可以通过除以帧率来把这个值转化为秒。流中的time_base值表示1/framerate(对于固定帧率来说),所以得到了以秒为单位的PTS,我们需要乘以time_base。
写代码:使用PTS来同步
现在我们得到了PTS。我们要注意前面讨论到的两个同步问题。我们将定义一个函数叫做synchronize_video,它可以更新同步的 PTS。这个函数也能最终处理我们得不到PTS的情况。同时我们要知道下一帧的时间以便于正确设置刷新速率。我们可以使用内部的反映当前视频已经播放时间 的时钟 video_clock来完成这个功能。我们把这些值添加到大结构体中。
typedef struct VideoState {
double video_clock; ///
下面的是函数synchronize_video,它可以很好的自我注释:

 1 double synchronize_video(VideoState *is, AVFrame *src_frame, double pts) { 
 2 double frame_delay; 
 3 if(pts != 0) { 
 4 is->video_clock = pts; 
 5 } else { 
 6 pts = is->video_clock; 
 7 } 
 8 frame_delay = av_q2d(is->video_st->codec->time_base); 
 9 frame_delay += src_frame->repeat_pict * (frame_delay * 0.5); 
10 is->video_clock += frame_delay; 
11 return pts; 
12 } 

你也会注意到我们也计算了重复的帧。
现在让我们得到正确的PTS并且使用queue_picture来队列化帧,添加一个新的时间戳参数pts:

1 // Did we get a video frame? 
2 if(frameFinished) { 
3 pts = synchronize_video(is, pFrame, pts); 
4 if(queue_picture(is, pFrame, pts) < 0) { 
5 break; 
6 } 
7 } 

对于queue_picture来说唯一改变的事情就是我们把时间戳值pts保存到VideoPicture结构体中,我们必需添加一个时间戳变量到结构体中并且添加一行代码:

 1 typedef struct VideoPicture { 
 2 ... 
 3 double pts; 
 4 } 
 5 int queue_picture(VideoState *is, AVFrame *pFrame, double pts) { 
 6 ... stuff ... 
 7 if(vp->bmp) { 
 8 ... convert picture ... 
 9 vp->pts = pts; 
10 ... alert queue ... 
11 } 

现在我们的图像队列中的所有图像都有了正确的时间戳值,所以让我们看一下视频刷新函数。你会记得上次我们用80ms的刷新时间来欺骗它。那么,现在我们将会算出实际的值。
我们的策略是通过简单计算前一帧和现在这一帧的时间戳来预测出下一个时间戳的时间。同时,我们需要同步视频到音频。我们将设置一个音频时间 audio clock;一个内部值记录了我们正在播放的音频的位置。就像从任意的mp3播放器中读出来的数字一样。既然我们把视频同步到音频,视频线程使用这个值来 算出是否太快还是太慢。

我们将在后面来实现这些代码;现在我们假设我们已经有一个可以给我们音频时间的函数get_audio_clock。一旦我们有了这个值,我们在音频和视 频失去同步的时候应该做些什么呢?简单而有点笨的办法是试着用跳过正确帧或者其它的方式来解决。作为一种替代的手段,我们会调整下次刷新的值;如果时间戳 太落后于音频时间,我们加倍计算延迟。如果时间戳太领先于音频时间,我们将尽可能快的刷新。既然我们有了调整过的时间和延迟,我们将把它和我们通过 frame_timer计算出来的时间进行比较。这个帧时间frame_timer将会统计出电影播放中所有的延时。换句话说,这个 frame_timer就是指我们什么时候来显示下一帧。我们简单的添加新的帧定时器延时,把它和电脑的系统时间进行比较,然后使用那个值来调度下一次刷 新。这可能有点难以理解,所以请认真研究代码:

 1 void video_refresh_timer(void *userdata) { 
 2 VideoState *is = (VideoState *)userdata; 
 3 VideoPicture *vp; 
 4 double actual_delay, delay, sync_threshold, ref_clock, diff; 
 5 if(is->video_st) { 
 6 if(is->pictq_size == 0) { 
 7 schedule_refresh(is, 1); 
 8 } else { 
 9 vp = &is->pictq[is->pictq_rindex]; 
10 delay = vp->pts - is->frame_last_pts; 
11 if(delay <= 0 || delay >= 1.0) { 
12 delay = is->frame_last_delay; 
13 } 
14 is->frame_last_delay = delay; 
15 is->frame_last_pts = vp->pts; 
16 ref_clock = get_audio_clock(is); 
17 diff = vp->pts - ref_clock; 
18 sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD; 
19 if(fabs(diff) < AV_NOSYNC_THRESHOLD) { 
20 if(diff <= -sync_threshold) { 
21 delay = 0; 
22 } else if(diff >= sync_threshold) { 
23 delay = 2 * delay; 
24 } 
25 } 
26 is->frame_timer += delay; 
27 actual_delay = is->frame_timer - (av_gettime() / 1000000.0); 
28 if(actual_delay < 0.010) { 
29 actual_delay = 0.010; 
30 } 
31 schedule_refresh(is, (int)(actual_delay * 1000 + 0.5)); 
32 video_display(is); 
33 if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) { 
34 is->pictq_rindex = 0; 
35 } 
36 SDL_LockMutex(is->pictq_mutex); 
37 is->pictq_size--; 
38 SDL_CondSignal(is->pictq_cond); 
39 SDL_UnlockMutex(is->pictq_mutex); 
40 } 
41 } else { 
42 schedule_refresh(is, 100); 
43 } 
44 } 

我们在这里做了很多检查:首先,我们保证现在的时间戳和上一个时间戳之间的处以delay是有意义的。如果不是的话,我们就猜测着用上次的延迟。接着,我 们有一个同步阈值,因为在同步的时候事情并不总是那么完美的。在ffplay中使用0.01作为它的值。我们也保证阈值不会比时间戳之间的间隔短。最后, 我们把最小的刷新值设置为10毫秒。
(这句不知道应该放在哪里)事实上这里我们应该跳过这一帧,但是我们不想为此而烦恼。
我们给大结构体添加了很多的变量,所以不要忘记检查一下代码。同时也不要忘记在函数streame_component_open中初始化帧时间frame_timer和前面的帧延迟frame delay:

1 is->frame_timer = (double)av_gettime() / 1000000.0; 
2 is->frame_last_delay = 40e-3; 

同步:声音时钟
现在让我们看一下怎样来得到声音时钟。我们可以在声音解码函数audio_decode_frame中更新时钟时间。现在,请记住我们并不是每次调用这个 函数的时候都在处理新的包,所以有我们要在两个地方更新时钟。第一个地方是我们得到新的包的时候:我们简单的设置声音时钟为这个包的时间戳。然后,如果一 个包里有许多帧,我们通过样本数和采样率来计算,所以当我们得到包的时候:

1 if(pkt->pts != AV_NOPTS_VALUE) { 
2 is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts; 
3 } 

然后当我们处理这个包的时候:

1 pts = is->audio_clock; 
2 *pts_ptr = pts; 
3 n = 2 * is->audio_st->codec->channels; 
4 is->audio_clock += (double)data_size / 
5 (double)(n * is->audio_st->codec->sample_rate); 

一点细节:临时函数被改成包含pts_ptr,所以要保证你已经改了那些。这时的pts_ptr是一个用来通知audio_callback函数当前声音包的时间戳的指针。这将在下次用来同步声音和视频。
现在我们可以最后来实现我们的get_audio_clock函数。它并不像得到is->audio_clock值那样简单。注意我们会在每次处理 它的时候设置声音时间戳,但是如果你看了audio_callback函数,它花费了时间来把数据从声音包中移到我们的输出缓冲区中。这意味着我们声音时 钟中记录的时间比实际的要早太多。所以我们必须要检查一下我们还有多少没有写入。下面是完整的代码:

 1 double get_audio_clock(VideoState *is) { 
 2 double pts; 
 3 int hw_buf_size, bytes_per_sec, n; 
 4 pts = is->audio_clock; 
 5 hw_buf_size = is->audio_buf_size - is->audio_buf_index; 
 6 bytes_per_sec = 0; 
 7 n = is->audio_st->codec->channels * 2; 
 8 if(is->audio_st) { 
 9 bytes_per_sec = is->audio_st->codec->sample_rate * n; 
10 } 
11 if(bytes_per_sec) { 
12 pts -= (double)hw_buf_size / bytes_per_sec; 
13 } 
14 return pts; 
15 } 

你应该知道为什么这个函数可以正常工作了;)
这就是了!让我们编译它:

gcc -o tutorial05 tutorial05.c -lavutil -lavformat -lavcodec -lz -lm`sdl-config --cflags --libs` 

最后,你可以使用我们自己的电影播放器来看电影了。下次我们将看一下声音同步,然后接下来的指导我们会讨论查询。

posted @ 2013-10-31 16:05  Djzny  阅读(457)  评论(0编辑  收藏  举报