-
Notifications
You must be signed in to change notification settings - Fork 88
/
Copy pathtutorial05.tex
318 lines (255 loc) · 17.7 KB
/
tutorial05.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
\chapter{同步视频}
\label{ch5}
\section{如何同步视频}
前面整个的一段时间,我们有了一个几乎无用的电影播放器。当然,它能播放视频,也能播放音频,但是它还不能被确切地称之为一部\emph{电影}。那么我们还要做什么呢?
\section{PTS和DTS}
幸运的是,音频和视频流都有一些关于以多快速度和什么时间来播放它们的信息在里面。音频流有采样,视频流有每秒的帧率。然而,如果我们只是简单的通过数帧和乘以帧率的方式来同步视频,那么就很有可能会失去同步。于是作为一种补充,在流中的包有种叫做\textbf{解码时间戳}(DTS)和\textbf{显示时间戳}(PTS)的机制。为了理解这两个参数,你需要了解电影的存储方式。像MPEG等格式,使用被叫做B 帧(B代表“bidrectional”)的方式。另外两种帧被叫做I帧和P帧(I代表"intra",P代表“predicted”)。I帧包含了某个特定的完整图像。P 帧依赖于前面的I帧和P帧并且使用比较或者差分的方式来编码。B帧与P帧有点类似,但是它是依赖于\emph{前面}和\emph{后面}的帧的信息的。这也就解释了为什么我们可能在调用avcodec_decode_video 以后会得不到一帧图像。
所以对于一个电影,帧是这样来显示的:I B B P。现在我们需要在显示B帧之前知道P帧中的信息。因此,帧可能会按照这样的方式来存储:IPBB。这就是为什么我们会有一个解码时间戳和一个显示时间戳的原因。解码时间戳告诉我们什么时候需要解码,显示时间戳告诉我们什么时候需要显示。所以,在这种情况下,我们的流可以是这样的:
\begin{verbatim}
PTS: 1 4 2 3
DTS: 1 2 3 4
Stream: I P B B
\end{verbatim}
通常PTS和DTS只有在流中有B帧的时候会不同。
当我们调用av_read_frame()得到一个包的时候,PTS 和DTS 的信息也会保存在包中。但是我们真正想要的PTS 是我们刚刚解码出来的原始帧的PTS,这样我们才能知道什么时候来显示它。然而,我们从avcodec_decode_video()函数中得到的帧只是一个AVFrame,其中并 没有包含有用的PTS值(注意:AVFrame确实包含一个pts变量,但并不总是我们得到帧的时候想要的值)。然而,ffmpeg 重新排序包以便于被avcodec_decode_video()函数处理的包的DTS可以\emph{总是}与其返回的PTS相同。但是,另外的一个警告是:我们也并不是总能得到这个信息。
不用担心,因为有另外一种办法可以找到帧的PTS,我们可以让程序自己来重新排序包。我们保存一帧的第一个包的PTS:这将作为整个这一帧的PTS。当流没有提供DTS的时候,我们可以使用这个保存下来的PTS。我们可以通过函数avcodec_decode_video()来计算出哪个包是一帧的第一个包。怎样实现呢?任何时候当一个包开始一帧的时候,avcodec_decode_video()将调用一个函数来为一帧申请一个缓冲。当然,ffmpeg 允许我们重新定义那个分配内存的函数。所以我们写了一个新的函数来保存一个包的显示时间戳。
当然,尽管那样,我们可能还是得不到一个正确的时间戳。我们将在后面处理这个问题。
\section{同步}
真不错,现在知道了什么时候来显示一个视频帧,但是我们怎样来实际操作呢?这里有个主意:当我们显示了一帧以后,我们计算出下一帧显示的时间。然后我们简单的设置一个新的定时器用于在那个时间之后刷新视频。正如你可能猜到的,我们检查下一帧的PTS值而不是系统时钟来计算超时时长。这种方式可以工作,但是有两种情况要处理。
首先,要知道下一个PTS是什么时候。现在你应该能想到可以添加视频速率到我们的PTS中——对,很接近了!然而,有些视频需要帧重复。这意味着我们要重复播放当前的帧。这将导致程序显示下一帧太快了。所以我们需要处理它们。
第二,正如程序现在这样,视频和音频播放很欢快,一点也不受同步的影响。如果一切都工作得很好的话,我们不必担心。但是,你的电脑并不是最好的,很多视频文件也不是完好的。所以,我们有三种选择:同步音频到视频,同步视频到音频,或者都同步到外部时钟(例如你的电脑时钟)。从现在开始,我们将同步视频到音频。
\section{编写代码:获得帧的时间戳}
现在让我们到代码中来做这些事情。我们将需要为我们的大结构体添加一些成员,但是我们会只加必要的。先看一下视频线程。记住,在这里我们得到了解码线程输出到队列中的包。这里我们需要的是通过avcodec_decode_video 函数来得到帧的时间戳。我们说的第一种方式是从上次处理的包中得到DTS,这很容易:
\begin{lstlisting}
double pts;
for(;;) {
if(packet_queue_get(&is->videoq, packet, 1) < 0) {
// means we quit getting packets
break;
}
pts = 0;
// Decode video frame
len1 = avcodec_decode_video(is->video_st->codec,
pFrame, &frameFinished,
packet->data, packet->size);
if(packet->dts != AV_NOPTS_VALUE) {
pts = packet->dts;
} else {
pts = 0;
}
pts *= av_q2d(is->video_st->time_base);
\end{lstlisting}
如果我们得不到PTS 就把它设置为0。
好,那是很容易的。但是如我们之前所说的,如果包的DTS不能帮到我们,我们需要使用所解码的那一帧的第一个包的PTS。我们通过让ffmpeg 使用我们自己的申请帧程序来实现。下面的是函数的原型:
\begin{lstlisting}
int get_buffer(struct AVCodecContext *c, AVFrame *pic);
void release_buffer(struct AVCodecContext *c, AVFrame *pic);
\end{lstlisting}
申请函数没有告诉我们关于包的任何事情,所以我们要自己每次在得到一个包的时候把PTS保存到一个全局变量中去。我们自己以读到它。然后,我们把值保存到AVFrame结构体的opaque变量中去,这是一个自定义变量,我们可以拿来想干什么就干什么。先来看一下我们的函数:
\begin{lstlisting}
uint64_t global_video_pkt_pts = AV_NOPTS_VALUE;
/* These are called whenever we allocate a frame
* buffer. We use this to store the global_pts in
* a frame at the time it is allocated.
*/
int our_get_buffer(struct AVCodecContext *c, AVFrame *pic) {
int ret = avcodec_default_get_buffer(c, pic);
uint64_t *pts = av_malloc(sizeof(uint64_t));
*pts = global_video_pkt_pts;
pic->opaque = pts;
return ret;
}
void our_release_buffer(struct AVCodecContext *c, AVFrame *pic) {
if(pic) av_freep(&pic->opaque);
avcodec_default_release_buffer(c, pic);
}
\end{lstlisting}
函数avcodec_default_get_buffer 和avcodec_default_release_buffer 是ffmpeg中默认的申请缓冲的函数。函数av_freep是一个内存管理函数,它不但把内存释放而且把指针设置为NULL。
现在到了我们打开流的函数(stream_component_open),我们添加这几行来告诉ffmpeg 如何去做:
\begin{lstlisting}
codecCtx->get_buffer = our_get_buffer;
codecCtx->release_buffer = our_release_buffer;
\end{lstlisting}
现在我们必需添加代码来保存PTS到全局变量中,然后在需要的时候来使用它。我们的代码现在看起来应该是这样子:
\begin{lstlisting}
for(;;) {
if(packet_queue_get(&is->videoq, packet, 1) < 0) {
// means we quit getting packets
break;
}
pts = 0;
// Save global pts to be stored in pFrame in first call
global_video_pkt_pts = packet->pts;
// Decode video frame
len1 = avcodec_decode_video(is->video_st->codec, pFrame, &frameFinished,
packet->data, packet->size);
if(packet->dts == AV_NOPTS_VALUE
&& pFrame->opaque && *(uint64_t*)pFrame->opaque != AV_NOPTS_VALUE) {
pts = *(uint64_t *)pFrame->opaque;
} else if(packet->dts != AV_NOPTS_VALUE) {
pts = packet->dts;
} else {
pts = 0;
}
pts *= av_q2d(is->video_st->time_base);
\end{lstlisting}
技术提示:你可能已经注意到我们使用int64 来表示PTS。这是因为PTS是以整型来保存的。这个值是一个时间戳相当于时间的度量,用来以流的time_base为单位进行时间度量。例如,如果一个流是24帧每秒,值为42的PTS 表示这一帧应该排在第42 个帧的位置如果我们每秒有24 帧(这里 并不完全正确)。
我们可以通过除以帧率来把这个值转化为秒。流中的time_base 值表示1/framerate(对于固定帧率来说),所以得到了以秒为单位的PTS,我们需要乘以time_base。
\section{编写代码:使用PTS来同步}
现在我们得到了PTS。我们要注意前面讨论到的两个同步问题。我们将定义一个函数叫做synchronize_video,它可以更新同步的PTS。这个函数也能最终处理我们得不到PTS的情况。同时我们要知道下一帧的时间以便于正确设置刷新速率。我们可以使用内部的反映当前视频已经播放时间的时钟video_clock来完成这个功能。我们把这些值添加到大结构体中。
\begin{lstlisting}
typedef struct VideoState {
double video_clock; //<pts of last decoded frame / predicted pts of next decoded frame
\end{lstlisting}
下面的是函数synchronize_video,它可以很好的自我注释:
\begin{lstlisting}
double synchronize_video(VideoState *is, AVFrame *src_frame, double pts) {
double frame_delay;
if(pts != 0) {
/* if we have pts, set video clock to it */
is->video_clock = pts;
} else {
/* if we aren't given a pts, set it to the clock */
pts = is->video_clock;
}
/* update the video clock */
frame_delay = av_q2d(is->video_st->codec->time_base);
/* if we are repeating a frame, adjust clock accordingly */
frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);
is->video_clock += frame_delay;
return pts;
}
\end{lstlisting}
你也许注意到了,我们也计算了重复的帧。
现在让我们得到正确的PTS 并且使用queue_picture来队列化帧,添加一个新的时间戳参数pts:
\begin{lstlisting}
// Did we get a video frame?
if(frameFinished) {
pts = synchronize_video(is, pFrame, pts);
if(queue_picture(is, pFrame, pts) < 0) {
break;
}
}
\end{lstlisting}
对于queue_picture来说唯一改变的事情就是我们把时间戳值pts保存到VideoPicture结构体中,我们我们必需添加一个时间戳变量到结构体中并且添加一行代码:
\begin{lstlisting}
typedef struct VideoPicture {
...
double pts;
}
int queue_picture(VideoState *is, AVFrame *pFrame, double pts) {
... stuff ...
if(vp->bmp) {
... convert picture ...
vp->pts = pts;
... alert queue ...
}
\end{lstlisting}
现在我们的图像队列中的所有图像都有了正确的时间戳值,所以让我们看一下视频刷新函数。你会记得上次我们用80ms的刷新时间来应付它。那么,现在我们将会算出实际的值。
我们的策略是通过简单计算前一帧和现在这一帧的时间戳来预测出下一个时间戳的时间。同时,我们需要同步视频到音频。我们将设置一个\textbf{音频时钟}(audio clock);一个内部值记录了我们正在播放的音频的位置。就像从任意的mp3播放器中读出来的数字一样。既然我们把视频同步到音频,视频线程使用这个值来算出是否太快还是太慢。
我们将在后面来实现这些代码;现在我们假设我们已经有一个可以给我们音频时钟的函数get_audio_clock。一旦我们有了这个值,我们在音频和视频失去同步的时候应该做些什么呢?简单而有点笨的办法是试着用跳过正确帧或者其它的方式来解决。作为一种替代的手段,我们会调整下次刷新的值;如果时间戳太落后于音频时间,我们加倍计算延迟。如果时间戳太领先于音频时间,我们将尽可能快的刷新。既然我们有了调整过的时间和\textbf{延迟},我们将把它和我们通过frame_timer 计算出来的系统时钟进行比较。这个帧计时器将会统计出电影播放中所有的延时。换句话说,这个 frame_timer 就是指我们什么时候来显示下一帧。我们简单的添加新的帧定时器延时,把它和电脑的系统时间进行比较,然后使用那个值来调度下一次刷新。这可能有点难以理解,所以请认真研究代码:
\begin{lstlisting}
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 */
if(delay <= 0 || delay >= 1.0) {
/* if incorrect delay, use previous 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 to audio */
ref_clock = get_audio_clock(is);
diff = vp->pts - ref_clock;
/* Skip or repeat the frame. Take delay into account
FFPlay still doesn't "know if this is the best guess." */
sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
if(fabs(diff) < AV_NOSYNC_THRESHOLD) {
if(diff <= -sync_threshold) {
delay = 0;
} else if(diff >= sync_threshold) {
delay = 2 * delay;
}
}
is->frame_timer += delay;
/* computer the REAL delay */
actual_delay = is->frame_timer - (av_gettime() / 1000000.0);
if(actual_delay < 0.010) {
/* Really it should skip the picture instead */
actual_delay = 0.010;
}
schedule_refresh(is, (int)(actual_delay * 1000 + 0.5));
/* show the picture! */
video_display(is);
/* update queue for next picture! */
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);
}
}
\end{lstlisting}
我们在这里做了很多检查:首先,我们保证现在的时间戳和上一个时间戳之间的处以delay是有意义的。如果不是的话,我们就猜测着用上次的延迟。接着,我们有一个同步阈值,因为在同步的时候事情并不总是那么完美的。在ffplay中使用0.01 作为它的值。我们也保证阈值不会比时间戳之间的间隔短。最后,我们把最小的刷新值设置为10毫秒。
\marginpar{\rule[-8mm]{0.4mm}{5mm}}{\textbf{注:}事实上这里我们应该跳过这一帧,但是我们不想为此而烦恼。}
我们给大结构体添加了很多的变量,所以不要忘记检查一下代码。同时也不要忘记在函数streame_component_open中初始化帧时间frame_timer和前面的帧延迟frame delay:
\begin{lstlisting}
is->frame_timer = (double)av_gettime() / 1000000.0;
is->frame_last_delay = 40e-3;
\end{lstlisting}
\section{同步:音频时钟}
现在让我们看一下怎样得到音频时钟。我们可以在音频解码函数audio_decode_frame中更新时钟时间。现在,请记住我们并不是每次调用这个函数的时候都在处理新的包,所以有我们要在两个地方更新时钟。第一个地方是我们得到新的包的时候:我们简单的设置音频时钟为这个包的时间戳。然后,如果一个包里有许多帧,我们通过样本数和采样率来计算,所以当我们得到包的时候:
\begin{lstlisting}
/* if update, update the audio clock w/pts */
if(pkt->pts != AV_NOPTS_VALUE) {
is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts;
}
\end{lstlisting}
然后当我们处理这个包的时候:
\begin{lstlisting}
/* Keep audio_clock up-to-date */
pts = is->audio_clock;
*pts_ptr = pts;
n = 2 * is->audio_st->codec->channels;
is->audio_clock += (double)data_size /
(double)(n * is->audio_st->codec->sample_rate);
\end{lstlisting}
一点细节:临时函数被改成包含pts_ptr,所以要保证你已经改了那些。这时的pts_ptr是一个用来通知audio_callback函数当前音频包的时间戳的指针。这将在下次用来同步音频和视频。
现在我们可以最后来实现我们的get_audio_clock 函数。它并不像得到is->audio_clock 值那样简单。注意我们会在每次处理 它的时候设置音频时间戳,但是如果你看了audio_callback 函数,它花费了时间来把数据从音频包中移到我们的输出缓冲区中,这意味着我们音频时钟中记录的时间比实际的要早太多。所以我们必须要检查一下我们还有多少没有写入。下面是完整的代码:
\begin{lstlisting}
double get_audio_clock(VideoState *is) {
double pts;
int hw_buf_size, bytes_per_sec, n;
pts = is->audio_clock; /* maintained in the audio thread */
hw_buf_size = is->audio_buf_size - is->audio_buf_index;
bytes_per_sec = 0;
n = is->audio_st->codec->channels * 2;
if(is->audio_st) {
bytes_per_sec = is->audio_st->codec->sample_rate * n;
}
if(bytes_per_sec) {
pts -= (double)hw_buf_size / bytes_per_sec;
}
return pts;
}
\end{lstlisting}
你应该知道为什么这个函数可以正常工作了;)
这就是了!让我们编译它:
\begin{lstlisting}
gcc -o tutorial05 tutorial05.c -lavutil -lavformat -lavcodec -lz
-lm`sdl-config --cflags --libs`
\end{lstlisting}
最后,你可以使用我们自己的电影播放器来看电影了。下次我们将看一下音频同步,然后接下来的教程我们会讨论定位(seeking)。