-
Notifications
You must be signed in to change notification settings - Fork 88
/
Copy pathtutorial04.tex
543 lines (434 loc) · 23.6 KB
/
tutorial04.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
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
\chapter{创建线程}
\label{ch4}
\section{概述}
上面通过SDL的音频功能添加了音频支持,SDL启动一个线程监听音频回调函数,视频显示也将如此。这使得代码更加模块化,更容易协调,尤其是当我们想要添加同步时。现在从哪开始呢?
首先注意到我们的主函数要处理的太多了:事件循环、读取数据包、解码视频。所以我们要做的是分开它们:创建一个负责解码数据包的线程;然后将数据包添加到队列中,并由相应的音频和视频的线程读取。我们已经这样设置了音频线程;视频线程将会复杂一点,因为我们要自己显示视频。我们将在主循环中添加显示代码。我们将把视频显示和事件循环放在一起,而不是每次循环显示视频。思路是这样的,解码视频,把产生的帧存到\emph{另一个}队列里,然后创建一个自定义事件 (FF_REFRESH_EVENT)并添加到事件系统里。那么当事件循环看到这个事件时,它将显示队列中的下一帧。下面是手绘的 ASCII字符图,指明流程:
\newpage
\begin{verbatim}
________ audio _______ _____
| | pkts | | | | to spkr
| DECODE |----->| AUDIO |--->| SDL |-->
|________| |_______| |_____|
| video _______
| pkts | |
+---------->| VIDEO |
________ |_______| _______
| | | | |
| EVENT | +------>| VIDEO | to mon.
| LOOP |----------------->| DISP. |-->
|_______|<---FF_REFRESH----|_______|
\end{verbatim}
移动视频循环到事件里的主要意图是使用SDL_Delay线程,让我们可以控制在屏幕是显示下一帧的确切时间。当我们最终在下一教程中同步视频时,会在其中添加代码,用来控制下一帧的刷新,这样让恰当的图像在恰当的时间显示。
\section{简化代码}
下面我们将清理一下代码。我们有音频和视频编解码器的全部信息,也将添加队列和缓冲区,并且难说不会再添其它什么东西。所有这些都是一个逻辑单元,即电影。我们将创建一个大的结构体,名为VideoState,包含所有的信息。
\begin{lstlisting}
typedef struct VideoState {
AVFormatContext *pFormatCtx;
int videoStream, audioStream;
AVStream *audio_st;
PacketQueue audioq;
uint8_t audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];
unsigned int audio_buf_size;
unsigned int audio_buf_index;
AVPacket audio_pkt;
uint8_t *audio_pkt_data;
int audio_pkt_size;
AVStream *video_st;
PacketQueue videoq;
VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE];
int pictq_size, pictq_rindex, pictq_windex;
SDL_mutex *pictq_mutex;
SDL_cond *pictq_cond;
SDL_Thread *parse_tid;
SDL_Thread *video_tid;
char filename[1024];
int quit;
} VideoState;
\end{lstlisting}
这里可以看出我们下一步工作的一些眉目。首先,是基础信息,包括格式上下文(format contex),音视频流标志(indices),及相应的AVStream 对象。可以看到,我们把一些音频缓冲区(buffers)信息放到结构体里了。这些(audio_buf、 audio_buf_size 等)都是与存在(或不在)缓冲区里的音频有关的信息\footnote{原文:These (audio_buf, audio_buf_size, etc.) were all for information about audio that was still lying around (or the lack thereof).}。我们为视频添加了另一个队列,和解码帧(存成一个覆盖)缓冲区(它将用作队列,不需要什么花哨的数据结构)。VideoPicture 是我们自定义的结构(当使用它的时侯,你会知道它的含义)。我们也注意到为额外创建的两个线程分配了指针,还有退出标志,电影文件名。
下面回到主函数,看程序里的变化。让我们初始化VideoState结构:
\begin{lstlisting}
int main(int argc, char *argv[]) {
SDL_Event event;
VideoState *is;
is = av_mallocz(sizeof(VideoState));
\end{lstlisting}
av_mallocz()是一个很好的函数,分配内存并初始化为0。
然后我们会初始化显示缓冲区pictq 的锁,因为事件循环会调用显示函数,记住,显示函数将从pictq里获取预解码的帧。同时,我们视频解码器将在pictq里放入它的信息——我们不知道谁将先发生。希望你已经意识到这是一个典型的\textbf{竞态条件}(race condition)。所以开始任何线程之前,先初始化分配它。让我们把电影的文件名复制到VideoState里。
\begin{lstlisting}
pstrcpy(is->filename, sizeof(is->filename), argv[1]);
is->pictq_mutex = SDL_CreateMutex();
is->pictq_cond = SDL_CreateCond();
\end{lstlisting}
av_strlcpy是ffmpeg里添加了边界检查,比strncpy更好的函数。
\section{我们的第一个线程 }
让我们最终启动线程,完成实际的工作:
\begin{lstlisting}
schedule_refresh(is, 40);
is->parse_tid = SDL_CreateThread(decode_thread, is);
if(!is->parse_tid) {
av_free(is);
return -1;
}
\end{lstlisting}
我们稍后将会定义schedule_refresh函数。它所做的基本上是告诉系统在指定的毫秒数后发送FF_REFRESH_EVENT事件。当它在实际队列里时,会回调视频刷新函数。让我们先看一下SDL_CreateThread()。
SDL_CreateThread(),可以创建一个和原始线程一样拥有完全访问权限的新线程,并运行传入的函数。在这里,我们调用了decode_thread(),还有VideoState结构作为参数。函数的上半部分没有有什么新的,仅完成打开文件和查找的音视频流索引的工作。唯一不同的是,在我们大结构里保存格式上下文。我们找到了流标志后,将调用另一个我们定义的函数,stream_component_open()。 大事化小是很自然的事,我们做了很多类似的事来设置视频和音频编解码器,我们将把它封装为函数,以重用代码。
我们使用stream_component_open() 函数查找解码器,初始化音频选项,将重要的信息保存到我们大的结构,并启动音频和视频的线程。在这个函数中我们还做了一些其它的,如强制指定而不是自动检测的编解码器种类等。代码如下:
\begin{lstlisting}
int stream_component_open(VideoState *is, int stream_index) {
AVFormatContext *pFormatCtx = is->pFormatCtx;
AVCodecContext *codecCtx;
AVCodec *codec;
SDL_AudioSpec wanted_spec, spec;
if(stream_index < 0 || stream_index >= pFormatCtx->nb_streams) {
return -1;
}
// Get a pointer to the codec context for the video stream
codecCtx = pFormatCtx->streams[stream_index]->codec;
if(codecCtx->codec_type == CODEC_TYPE_AUDIO) {
// Set audio settings from codec info
wanted_spec.freq = codecCtx->sample_rate;
/* .... */
wanted_spec.callback = audio_callback;
wanted_spec.userdata = is;
if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
return -1;
}
}
codec = avcodec_find_decoder(codecCtx->codec_id);
if(!codec || (avcodec_open(codecCtx, codec) < 0)) {
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
switch(codecCtx->codec_type) {
case CODEC_TYPE_AUDIO:
is->audioStream = stream_index;
is->audio_st = pFormatCtx->streams[stream_index];
is->audio_buf_size = 0;
is->audio_buf_index = 0;
memset(&is->audio_pkt, 0, sizeof(is->audio_pkt));
packet_queue_init(&is->audioq);
SDL_PauseAudio(0);
break;
case CODEC_TYPE_VIDEO:
is->videoStream = stream_index;
is->video_st = pFormatCtx->streams[stream_index];
packet_queue_init(&is->videoq);
is->video_tid = SDL_CreateThread(video_thread, is);
break;
default:
break;
}
}
\end{lstlisting}
除了现在通用于到音频和视频之外,这和我们之前的代码几乎相同。注意,我们使用大结构作为音频回调的userdata,而不是aCodecCtx。 我们也把流存为audio_st和video_st,并像设置音频队列一样添加了视频队列。该启动视频和音频线程了,代码如下:
\begin{lstlisting}
SDL_PauseAudio(0);
break;
/* ...... */
is->video_tid = SDL_CreateThread(video_thread, is);
\end{lstlisting}
记得我们上次用过SDL_PauseAudio(),SDL_CreateThread()的使用和上次完全一样。我们即将回到video_thread()函数。
在此之前,让我们回到我们的decode_thread()函数的下半。它基本上只是 for 循环,读取数据包中并把它放在正确的队列:
\begin{lstlisting}
for(;;) {
if(is->quit) {
break;
}
// seek stuff goes here
if(is->audioq.size > MAX_AUDIOQ_SIZE ||
is->videoq.size > MAX_VIDEOQ_SIZE) {
SDL_Delay(10);
continue;
}
if(av_read_frame(is->pFormatCtx, packet) < 0) {
if(url_ferror(&pFormatCtx->pb) == 0) {
SDL_Delay(100); /* no error; wait for user input */
continue;
} else {
break;
}
}
// Is this a packet from the video stream?
if(packet->stream_index == is->videoStream) {
packet_queue_put(&is->videoq, packet);
} else if(packet->stream_index == is->audioStream) {
packet_queue_put(&is->audioq, packet);
} else {
av_free_packet(packet);
}
}
\end{lstlisting}
这里没有什么新东西,除了我们给音频和视频队列限定了一个最大值并且我们添加一个检测读错误的函数。格式上下文里面有一个叫做pb的ByteIOContext类型结构体。这个结构体是用来保存一些低级的文件信息。函数url_ferror 用来检测结构体并发现是否有些读取文件错误。
在循环以后,我们的代码是用等待其余的程序结束和提示我们已经结束的。这些代码是有用的,因为它指示出了如何驱动事件——后面我们显示视频会用到。
\begin{lstlisting}
while(!is->quit) {
SDL_Delay(100);
}
fail:
if(1){
SDL_Event event;
event.type = FF_QUIT_EVENT;
event.user.data1 = is;
SDL_PushEvent(&event);
}
return 0;
\end{lstlisting}
我们使用SDL 常量SDL_USEREVENT来从用户事件中得到值。第一个用户事件的值应当是SDL_USEREVENT,下一个是 SDL_USEREVENT +1 并且依此类推。在我们的程序中FF_QUIT_EVENT 被定义成SDL_USEREVENT+2。如果喜欢,我们也可以传递用户数据,在这里我们传递的是大结构体的指针。最后我们调用SDL_PushEvent()函数。在我们的事件分支中,我们只是像以前放入SDL_QUIT_EVENT部分一样。我们将在自己的事件队列中详细讨论,现在只是确保我们正确放入了FF_QUIT_EVENT 事件,我们将在后面捕捉到它并且设置我们的退出标志quit。
\section{得到帧:video_thread}
当我们准备好解码器后,我们开始视频线程。这个线程从视频队列中读取包,把它解码成视频帧,然后调用queue_picture 函数把处理好的帧放入到图像队列中:
\begin{lstlisting}
int video_thread(void *arg) {
VideoState *is = (VideoState *)arg;
AVPacket pkt1, *packet = &pkt1;
int len1, frameFinished;
AVFrame *pFrame;
pFrame = avcodec_alloc_frame();
for(;;) {
if(packet_queue_get(&is->videoq, packet, 1) < 0) {
// means we quit getting packets
break;
}
// Decode video frame
len1 = avcodec_decode_video(is->video_st->codec, pFrame, &frameFinished,
packet->data, packet->size);
// Did we get a video frame?
if(frameFinished) {
if(queue_picture(is, pFrame) < 0) {
break;
}
}
av_free_packet(packet);
}
av_free(pFrame);
return 0;
}
\end{lstlisting}
这里的很多函数应该很熟悉吧。我们把avcodec_decode_video 函数移到了这里,替换了一些参数,例如:我们把AVStream 保存在我们自己的大结构体中,所以我们可以从那里得到编解码器的信息。我们仅仅是不断地从视频队列中取包一直到有人告诉我们要停止或者出错为止。
\section{把帧队列化}
让我们看一下保存解码后的帧pFrame到图像队列中去的函数。因为我们的图像队列是SDL覆盖的集合(基本上不用让视频显示函数再做计算了),我们需要把帧转换成相应的格式。我们保存到图像队列中的数据是我们自己做的一个结构体。
\begin{lstlisting}
typedef struct VideoPicture {
SDL_Overlay *bmp;
int width, height; /* source height & width */
int allocated;
} VideoPicture;
\end{lstlisting}
我们的大结构体有一个可以保存这些的缓冲区。然而,我们需要自己来申请SDL_Overlay(注意:allocated 标志会指明我们是否已经做了这个申请的动作)。
为了使用这个队列,我们有两个指针——写入指针和读取指针。我们还记录实际有多少图像在缓冲区。要写入到队列中,我们先要等待缓冲清空以便于有位置来保存我们的VideoPicture。然后我们检查我们是否已经申请到了一个可以写入覆盖的索引号。如果没有,我们要申请一段空间。我们也要重新申请缓冲如果窗口的大小已经改变。然而,为了避免被锁定,尽量避免在这里申请(我现在还不太清楚原因;我相信是为了避免在其它线程中调用SDL覆盖函数的原因)。
\begin{lstlisting}
int queue_picture(VideoState *is, AVFrame *pFrame) {
VideoPicture *vp;
int dst_pix_fmt;
AVPicture pict;
/* wait until we have space for a new pic */
SDL_LockMutex(is->pictq_mutex);
while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE &&
!is->quit) {
SDL_CondWait(is->pictq_cond, is->pictq_mutex);
}
SDL_UnlockMutex(is->pictq_mutex);
if(is->quit)
return -1;
// windex is set to 0 initially
vp = &is->pictq[is->pictq_windex];
/* allocate or resize the buffer! */
if(!vp->bmp ||
vp->width != is->video_st->codec->width ||
vp->height != is->video_st->codec->height) {
SDL_Event event;
vp->allocated = 0;
/* we have to do it in the main thread */
event.type = FF_ALLOC_EVENT;
event.user.data1 = is;
SDL_PushEvent(&event);
/* wait until we have a picture allocated */
SDL_LockMutex(is->pictq_mutex);
while(!vp->allocated && !is->quit) {
SDL_CondWait(is->pictq_cond, is->pictq_mutex);
}
SDL_UnlockMutex(is->pictq_mutex);
if(is->quit) {
return -1;
}
}
\end{lstlisting}
这里的事件机制与前面我们想要退出的时候看到的一样。我们已经定义了事件FF_ALLOC_EVENT 作为SDL_USEREVENT。我们把事件发到事 件队列中然后等待申请内存的函数设置好条件变量。
让我们来看一看如何来修改事件循环:
\begin{lstlisting}
for(;;) {
SDL_WaitEvent(&event);
switch(event.type) {
/* ... */
case FF_ALLOC_EVENT:
alloc_picture(event.user.data1);
break;
\end{lstlisting}
记住event.user.data1 是我们的大结构体。就这么简单。让我们看一下alloc_picture()函数:
\begin{lstlisting}
void alloc_picture(void *userdata) {
VideoState *is = (VideoState *)userdata;
VideoPicture *vp;
vp = &is->pictq[is->pictq_windex];
if(vp->bmp) {
// we already have one make another, bigger/smaller
SDL_FreeYUVOverlay(vp->bmp);
}
// Allocate a place to put our YUV image on that screen
vp->bmp = SDL_CreateYUVOverlay(is->video_st->codec->width,
is->video_st->codec->height,
SDL_YV12_OVERLAY,
screen);
vp->width = is->video_st->codec->width;
vp->height = is->video_st->codec->height;
SDL_LockMutex(is->pictq_mutex);
vp->allocated = 1;
SDL_CondSignal(is->pictq_cond);
SDL_UnlockMutex(is->pictq_mutex);
}
\end{lstlisting}
你可以看到我们把SDL_CreateYUVOverlay函数从主循环中移到了这里。这段代码应该完全可以自我注释。记住我们把高度和宽度保存到VideoPicture结构体中因为我们需要保存我们的视频的大小没有因为某些原因而改变。好,我们几乎已经全部解决并且可以申请到YUV覆盖和准备好接收图像。让我们回顾一下queue_picture并看一个拷贝帧到覆盖的代码。你应该能认出其中的一部分:
\begin{lstlisting}
int queue_picture(VideoState *is, AVFrame *pFrame) {
/* Allocate a frame if we need it... */
/* ... */
/* We have a place to put our picture on the queue */
if(vp->bmp) {
SDL_LockYUVOverlay(vp->bmp);
dst_pix_fmt = PIX_FMT_YUV420P;
/* point pict at the queue */
pict.data[0] = vp->bmp->pixels[0];
pict.data[1] = vp->bmp->pixels[2];
pict.data[2] = vp->bmp->pixels[1];
pict.linesize[0] = vp->bmp->pitches[0];
pict.linesize[1] = vp->bmp->pitches[2];
pict.linesize[2] = vp->bmp->pitches[1];
// Convert the image into YUV format that SDL uses
img_convert(&pict, dst_pix_fmt,
(AVPicture *)pFrame, is->video_st->codec->pix_fmt,
is->video_st->codec->width, is->video_st->codec->height);
SDL_UnlockYUVOverlay(vp->bmp);
/* now we inform our display thread that we have a pic ready */
if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {
is->pictq_windex = 0;
}
SDL_LockMutex(is->pictq_mutex);
is->pictq_size++;
SDL_UnlockMutex(is->pictq_mutex);
}
return 0;
}
\end{lstlisting}
这部分代码和前面用到的一样,主要是简单的用我们的帧来填充YUV 覆盖。最后一点只是简单的给队列加1。这个队列在写的时候会一直写入到满为止,在读的时候会一直读空为止。因此所有的都依赖于is->pictq_size值,这要求我们必需要锁定它。这里我们做的是增加写指针(在必要的时候采用轮转的方式),然后锁定队列并且增加尺寸。现在我们的读函数将会知道队列中有了更多的信息,当队列满的时候,我们的写函数也会知道。
\section{显示视频}
上面就是我们的视频线程要做的。现在我们封装了几乎所有的散乱的线程,还剩下一个——记得我们前面调用schedule_refresh()函数吗?让我们看一下实际中是如何做的:
\begin{lstlisting}
/* schedule a video refresh in 'delay' ms */
static void schedule_refresh(VideoState *is, int delay) {
SDL_AddTimer(delay, sdl_refresh_timer_cb, is);
}
\end{lstlisting}
SDL_AddTimer()是一个SDL函数,按照给定的毫秒来调用(也可以带一些用户数据参数)用户特定的函数。我们将用这个函数来定时刷新视频——每次我们调用这个函数的时候,它将设置一个定时器来触发定时事件来把一帧从图像队列中显示到屏幕上。哈!
但是,让我们先触发那个事件:
\begin{lstlisting}
static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) {
SDL_Event event;
event.type = FF_REFRESH_EVENT;
event.user.data1 = opaque;
SDL_PushEvent(&event);
return 0; /* 0 means stop timer */
}
\end{lstlisting}
这里向队列中写入了一个现在很熟悉的事件。FF_REFRESH_EVENT 被定义成SDL_USEREVENT+1。要注意的一件事是当返回0 的时候,SDL停止定时器,于是回调就不会再发生。
现在我们产生了一个FF_REFRESH_EVENT事件,我们需要在事件循环中处理它:
\begin{lstlisting}
for(;;) {
SDL_WaitEvent(&event);
switch(event.type) {
/* ... */
case FF_REFRESH_EVENT:
video_refresh_timer(event.user.data1);
break;
\end{lstlisting}
于是我们就运行到了这个函数,在这个函数中会把数据从图像队列中取出:
\begin{lstlisting}
void video_refresh_timer(void *userdata) {
VideoState *is = (VideoState *)userdata;
VideoPicture *vp;
if(is->video_st) {
if(is->pictq_size == 0) {
schedule_refresh(is, 1);
} else {
vp = &is->pictq[is->pictq_rindex];
/* Timing code goes here */
schedule_refresh(is, 80);
/* 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}
现在,这只是一个极其简单的函数:当队列中有数据的时候,它从其中获得数据,为下一帧设置定时器,调用video_display 函数来真正显示图像到屏幕上,然后把队列读索引值加1,并且把队列的大小减1。你可能会注意到在这个函数中我们并没有真正对vp 做一些实际的动作,原因是这样的:我们将在后面处理。我们将在后面同步音频和视频的时候用它来访问时间信息。你会在这里看到这个注释信息“timing code here”。那里我们将讨论什么时候显示下一帧视频,然后把相应的值写入到schedule_refresh()函数中。现在我们只是随便写入一个值 80。从技术上来讲,你可以猜测并验证这个值,并且为每个电影重新编译程序,但是:1)过一段时间它会漂移;2)这种方式是很笨的。我们将在后面来讨论它。
我们几乎做完了;我们仅仅剩了最后一件事:显示视频!下面就是video_display函数:
\begin{lstlisting}
void video_display(VideoState *is) {
SDL_Rect rect;
VideoPicture *vp;
AVPicture pict;
float aspect_ratio;
int w, h, x, y;
int i;
vp = &is->pictq[is->pictq_rindex];
if(vp->bmp) {
if(is->video_st->codec->sample_aspect_ratio.num == 0) {
aspect_ratio = 0;
} else {
aspect_ratio = av_q2d(is->video_st->codec->sample_aspect_ratio) *
is->video_st->codec->width / is->video_st->codec->height;
}
if(aspect_ratio <= 0.0) {
aspect_ratio = (float)is->video_st->codec->width /
(float)is->video_st->codec->height;
}
h = screen->h;
w = ((int)rint(h * aspect_ratio)) & -3;
if(w > screen->w) {
w = screen->w;
h = ((int)rint(w / aspect_ratio)) & -3;
}
x = (screen->w - w) / 2;
y = (screen->h - h) / 2;
rect.x = x;
rect.y = y;
rect.w = w;
rect.h = h;
SDL_DisplayYUVOverlay(vp->bmp, &rect);
}
}
\end{lstlisting}
因为我们的屏幕可以是任意尺寸(我们设置为640x480并且用户可以自己来改变尺寸),我们需要动态计算出我们显示的图像的矩形大小。所以一开始我们需要计算出电影的\textbf{纵横比}(aspect ratio),表示方式为宽度除以高度。某些编解码器会有奇数\textbf{采样纵横比}(sample aspect ration),只是简单表示了一个像素或者一个采样的宽度除以高度的比例。因为宽度和高度在我们的编解码器中是用像素为单位的,所以实际的纵横比与纵横比乘以采样纵横比相同。某些编解码器会显示纵横比为0,这表示每个像素的纵横比为1x1。然后我们把电影缩放到适合屏幕的尽可能大的尺寸。这里的\& -3表示与-3做与运算,把值舍入到最为接近的4的倍数。\footnote{ 实际上是让它们4 字节对齐。——译者注}然后我们把电影移到中心位置,接着调用SDL_DisplayYUVOverlay() 函数。
结果是什么?我们做完了吗?嗯,我们仍然要重新改写音频部分的代码来使用新的VideoStruct结构体,但是那些改变不大,你可以参考一下示例代码。最后我们要做的是改变ffmpeg提供的默认退出回调函数为我们的退出回调函数。
\begin{lstlisting}
VideoState *global_video_state;
int decode_interrupt_cb(void) {
return (global_video_state && global_video_state->quit);
}
\end{lstlisting}
我们在主函数中为大结构体设置了global_video_state。
这就好了!让我们编译它:
\begin{lstlisting}
gcc -o tutorial04 tutorial04.c -lavutil -lavformat -lavcodec -lz -lm \
`sdl-config --cflags --libs`
\end{lstlisting}
请享受一下没有经过同步的电影!下次我们将编译一个可以最终工作的电影播放器。