ffmpeg入门学习——文档4:创建线程

指导4:创建线程
1、概要
上一次我们使用SDL的函数来达到支持音频播放的效果。每当SDL需要音频时它会启动一个线程来调用我们提供的回调函数。现在我们对视频进行同样的处理。这样会使程序更加模块化和跟容易协调工作 - 尤其是当我们想往代码里面加入同步功能。那么我们要从哪里开始呢?
首先我们注意到我们的主函数处理太多东西了:它运行着事件循环,读取包和处理视频解码。所以我们将把这些东西分成几个部分:我们会创建一个线程来负责解包;这个包会叫如到队列里面,然后由相关的视频或者音频线程来读取这个包。音频线程之前已经按照我们的想法建立好了;由于我们需要自己来播放视频,因此创建视频线程会有点复杂。我们会把真正播放视频的代码放在主线程。不是仅仅在每次循环时显示视频,而是把视频播放整合到事件循环中。现在的想法是解码视频,把结果保存到另一个队列中,然后创建一个普通事件(FF_REFRESH_EVENT)加入到事件系统中,接着我们的事件循环不断检测这个事件。他将会在这个队列里面播放下一帧。这里有一个图来解释究竟发生了什么事情:

主要目的是通过使用SDL_Delay线程的事件驱动来控制视频的移动,我们可以控制下一帧视频应该在什么时间在屏幕上显示。当我们在下一个教程中添加视频的刷新时间控制代码,就可以使视频速度播放正常了。


2、简化代码

——> 我们同样会清理一些代码。我们有所有这些视频和音频编解码器的信息,我们将会加入队列和缓冲和所有其他的东西。所有致谢东西都是为了一个逻辑单元,也就是视频。所以我们创建一个大结构体来装载这些信息,我们把它叫做VideoState

typedef struct VideoState {
 
AVFormatContext *pFormatCtx;
intvideoStream, 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;
intaudio_pkt_size;
AVStream *video_st;
PacketQueue videoq;
 
VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE];
intpictq_size, pictq_rindex, pictq_windex;
SDL_mutex *pictq_mutex;
SDL_cond *pictq_cond;
SDL_Thread *parse_tid;
SDL_Thread *video_tid;
 
charfilename[1024];
intquit;
} VideoState;
让我们来看一下我们看到了什么。首先,我们看到基本信息 - 视频和音频流的格式和参数,和相应的AVStream对象。然后我们看到我们把以下音频缓冲移动到这个结构体里面。这些音频的有关信息(音频缓冲,缓冲大小等)都在附近。我们已经给视频添加了另一个队列,也为解码的帧(保存为overlay)准备了缓冲(会用来作为队列,我们不需要一个花哨的队列)。VideoPicture是我们创造的(我们将会在以后看看里面有什么东西)。我们同样注意到结构体还分配指针给我们额外创建的线程,退出标志和视频的文件名。

——>现在就让我们回到主函数,看看如何修改我们的代码,首先设置VideoState结构体:

1int main(intargc, char *argv[]) {
2 
3SDL_Event event;
4 
5VideoState *is;
6 
7is = av_mallocz(sizeof(VideoState));
av_mallocz()函数会为我们申请空间而且初始化为全0。

——>然后我们要初始化为视频缓冲准备的锁(pictq)。因为一旦事件驱动调用我们的视频函数 - 视频函数会从pictq抽出预解码帧。同时,我们的视频解码器会把信息放进去 - 我们不知道那个动作会先发生。希望你认识到这是一个经典的竞争条件。所以我们要在开始任何线程前为其分配空间。同时让我们把文件名放到VideoState当中。
1pstrcpy(is->filename, sizeof(is->filename), argv[1]);
2 
3is->pictq_mutex = SDL_CreateMutex();
4is->pictq_cond = SDL_CreateCond();
pstrcpy(已过期)是ffmpeg中的一个函数,其对strncpy作了一些额外的检测

3、我们的第一个线程
让我们启动我们的线程使工作落到实处吧:
schedule_refresh(is, 40);//某个特定的毫秒数后弹出FF_REFRESH_EVENT事件。这将会反过来调用事件队列里的视频刷新函数
is->parse_tid = SDL_CreateThread(decode_thread, is);
if(!is->parse_tid) {
av_free(is);
return-1;
}
schedule_refresh是一个我们将要定义的函数。它的动作是告诉系统在某个特定的毫秒数后弹出FF_REFRESH_EVENT事件。这将会反过来调用事件队列里的视频刷新函数。但是现在,让我们分析一下SDL_CreateThread()。

SDL_CreateThread()做的事情是这样的 - 它生成一个新线程能完全访问原始进程中的内存,启动我们给的线程。它同样会运行用户定义数据的函数。在这种情况下,我们调用decode_thread()并与VideoState结构体连接。上半部分的函数没什么新东西;它的工作就是打开文件和找到视频流和音频流的索引。唯一不同的地方是把格式内容保存到我们的大结构体中。当我们找到流后,我们调用另一个我们将要定义的函数stream_component_open()。这是一个一般的分离的方法,自从我们设置很多相似的视频和音频解码的代码,我们通过编写这个函数来重用它们。

stream_component_open()函数的作用是找到我们的解码器,设置音频参数,保存重要信息到大结构体中,然后启动音频和视频线程。我们还会在这里设置一些其他参数,例如指定编码器而不是自动检测等等,下面就是代码:

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;//从AVFormatContext结构体中得到AVCodecContext
 
if(codecCtx->codec_type == CODEC_TYPE_AUDIO) {//如果是音频,利用AVCodecContext结构体对对SDL_AudioSpec结构体wanted_spec进行设置
// 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) {
caseCODEC_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;
caseCODEC_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;
}
}
这跟以前我们写的代码几乎一样,只不过现在是包括音频和视频。注意到我们建立了大结构体来作为音频回调的用户数据来代替了aCodecCtx。我们同样保存流到audio_st和video_st。像建立音频队列一样,我们也增加了视频队列。主要是运行视频和音频线程。就像如下:
1SDL_PauseAudio(0);
2break;
3 
4/* ...... */
5 
6is->video_tid = SDL_CreateThread(video_thread, is);

我们还记得之前SDL_PauseAudio()的作用,还有SDL_CreateThread()跟以前的用法一样。我们会回到video_thread()函数。

在这之前,让我们回到decode_thread()函数的下半部分。基本上就是一个循环来读取包和把它放到相应的队列中:

01for(;;) {
02if(is->quit) {
03break;
04}
05// seek stuff goes here
06if(is->audioq.size > MAX_AUDIOQ_SIZE ||
07is->videoq.size > MAX_VIDEOQ_SIZE) {
08SDL_Delay(10);
09continue;
10}
11if(av_read_frame(is->pFormatCtx, packet) < 0) {
12if(url_ferror(&pFormatCtx->pb) == 0) {
13SDL_Delay(100);/* no error; wait for user input */
14continue;
15} else{
16break;
17}
18}
19// Is this a packet from the video stream?
20if(packet->stream_index == is->videoStream) {
21packet_queue_put(&is->videoq, packet);
22} elseif(packet->stream_index == is->audioStream) {
23packet_queue_put(&is->audioq, packet);
24} else{
25av_free_packet(packet);
26}
27}

这里没有新的东西,除了我们的音频和视频队列定义了一个最大值,还有我们加入了检测读取错误的函数。格式内容里面有一个叫做pb的ByteIOContext结构体。ByteIOContext十一个保存所有低级文件信息的结构体。url_ferror检测结构体在读取文件时石油出现某些错误。

经过我们的for循环,我们等待程序结束或者通知我们已经结束。这些代码指导我们如何推送事件 - 一些我们以后用来显示视频的东西。

 while(!is->quit) {
 SDL_Delay(100);
 }
  
 fail:
 if(1){
 SDL_Event event;
 event.type = FF_QUIT_EVENT;
 event.user.data1 = is;
 SDL_PushEvent(&event);
 }
return0;
我们通过SDL定义的一个宏来获取用户事件的值。第一个用户事件应该分配给SDL_USEREVENT,下一个分配给SDL_USEREVENT + 1,如此类推。FF_QUIT_EVENT在SDL_USEREVENT + 2中定义。如果我们喜欢,我们同样可以传递用户事件,这里我们把我们的指针传递给了一个大结构体。最后我们调用SDL_PushEvent()。在我们的循环分流中,我们只是把SDL_QUIT_EVENT部分放进去。我们还会看到事件循环的更多细节;现在,只是保证当我们推送FF_QUIT_EVENT时,我们会得到它和quit值变为1。


4、获得帧:视频线程
准备好解码后,我们开启视频线程:从视频队列里面读取包,把视频解码为帧,然后调用queue_picture函数来把帧放进picture队列:

int video_thread( void *arg)
{
VideoState *is = (VideoState *)arg;
AVPacket pkt1, *packet = &pkt1;
intlen1, frameFinished;
AVFrame *pFrame;
 
pFrame = avcodec_alloc_frame();
 
for (;;)
{
if (packet_queue_get(&is->videoq, packet, 1) < 0)
{
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);
return0;
}
大部分函数在这点上应该是相似的。我们已经把avcodec_decode_video函数移动到这里,只是替换了一些参数;例如,我们的大结构体里面有AVStream,所以我们从那里得到我们的编解码器。我们持续地从视频队列里面取包,知道某人告诉我们该结束或者我们遇到错误。


5、帧排队

一起来看看我们picture队列里面用来存储我们解码帧的函数pFrame。由于我们的picture队列是SDL overlay(大概是为了视频显示尽量少的计算),我们需要把转换的帧存储在picture队列里面 :

1typedef struct VideoPicture {
2SDL_Overlay *bmp;
3intwidth, height; /* source height & width */
4intallocated;
5} VideoPicture;
我们的大结构体有缓冲来存储他们。然而,我们需要自己分配SDL_Overlay(注意到allocated标志用来标示我们是否已经分配了内存)。

使用这个队列我们需要两个指针 - 写索引和读索引。我们同样记录着缓冲里面实际上有多少图片。为了写队列,我们第一次要等待缓冲清空以保证有空间存储VideoPicture。然后我们检测我们是否为写索引申请了overlay。如果没有,我们需要申请一些空间。如果窗口的大小改变了,我们同样需要重新申请缓冲。然而,为了避免锁问题,我们不会在这里申请(我还不太确定为什么;我相信要避免在不同线程调用SDL overlay函数。)

int queue_picture(VideoState *is, AVFrame *pFrame)
{
VideoPicture *vp;
intdst_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;
}
}
当我们想退出时,退出机制就像我们之前看到的那样处理。我们已经定义了FF_ALLOC_EVENT为SDL_USEREVENT。我们推送事件然后等待条件变量分配函数运行。

让我们来看看我们是怎么改变事件循环的:
1for(;;) {
2SDL_WaitEvent(&event);
3switch(event.type) {
4/* ... */
5caseFF_ALLOC_EVENT:
6alloc_picture(event.user.data1);
7break;
记住event.user.data1就是我们的大结构体。这已经足够简单了。让我们来看看alloc_picture()函数:
01void alloc_picture(void*userdata) {
02 
03VideoState *is = (VideoState *)userdata;
04VideoPicture *vp;
05 
06vp = &is->pictq[is->pictq_windex];
07if(vp->bmp) {
08// we already have one make another, bigger/smaller
09SDL_FreeYUVOverlay(vp->bmp);
10}
11// Allocate a place to put our YUV image on that screen
12vp->bmp = SDL_CreateYUVOverlay(is->video_st->codec->width,
13is->video_st->codec->height,
14SDL_YV12_OVERLAY,
15screen);
16vp->width = is->video_st->codec->width;
17vp->height = is->video_st->codec->height;
18
19SDL_LockMutex(is->pictq_mutex);
20vp->allocated = 1;
21SDL_CondSignal(is->pictq_cond);
22SDL_UnlockMutex(is->pictq_mutex);
23}
你应该认识到我们已经把SDL_CreateYUVOverlay移动到这里。此代码现在应该是相当不言自明。记住我们把宽度和高度保存到VideoPicture里面,因为由于某些原因我们不想改变视频的尺寸。

好了,我们解决了所有东西,现在我们的YUV overlay已经分配好内存,准备接收图片了。让我们回到queue_picture来看看把帧复制到overlay当中,你应该记得这部分内容的:

01int queue_picture(VideoState *is, AVFrame *pFrame) {
02 
03/* Allocate a frame if we need it... */
04/* ... */
05/* We have a place to put our picture on the queue */
06 
07if(vp->bmp) {
08 
09SDL_LockYUVOverlay(vp->bmp);
10
11dst_pix_fmt = PIX_FMT_YUV420P;
12/* point pict at the queue */
13 
14pict.data[0] = vp->bmp->pixels[0];
15pict.data[1] = vp->bmp->pixels[2];
16pict.data[2] = vp->bmp->pixels[1];
17
18pict.linesize[0] = vp->bmp->pitches[0];
19pict.linesize[1] = vp->bmp->pitches[2];
20pict.linesize[2] = vp->bmp->pitches[1];
21
22// Convert the image into YUV format that SDL uses
23img_convert(&pict, dst_pix_fmt,
24(AVPicture *)pFrame, is->video_st->codec->pix_fmt,
25is->video_st->codec->width, is->video_st->codec->height);
26
27SDL_UnlockYUVOverlay(vp->bmp);
28/* now we inform our display thread that we have a pic ready */
29if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {
30is->pictq_windex = 0;
31}
32SDL_LockMutex(is->pictq_mutex);
33is->pictq_size++;
34SDL_UnlockMutex(is->pictq_mutex);
35}
36return0;
37}
这部分的主要功能就是我们之前所用的简单地把帧填充到YUV overlay。最后把值加到队列当中。队列的工作是持续添加直到满,和里面有什么就读取什么。因此所有东西都基于is->pictq_size这个值,需要我们来锁住它。所以现在工作是增加写指针(有需要的话翻转它),然后锁住队列增加其大小。现在我们的读索引知道队列里面有更多的信息,如果队列满了,我们的写索引会知道的。

6、播放视频

这就是我们的视频线程!现在我们已经包裹起所有松散的线程,除了这个 - 还记得我们调用schedule_refresh()函数吗?让我们来看看它实际上做了什么工作:

1/* schedule a video refresh in 'delay' ms */
2static void schedule_refresh(VideoState *is, intdelay) {
3SDL_AddTimer(delay, sdl_refresh_timer_cb, is);
4}
SDL_AddTimer()是一个SDL函数,在一个特定的毫秒数里它简单地回调了用户指定函数(可选择携带一些用户数据)。我们用这个函数来计划视频的更新 - 每次我们调用这个函数,它会设定一个时间,然后会触发一个事件,然后我们的主函数会调用函数来从picture队列里拉出一帧然后显示它!

不过首先,让我们来触发事件。它会发送:
1static Uint32 sdl_refresh_timer_cb(Uint32 interval, void*opaque) {
2SDL_Event event;
3event.type = FF_REFRESH_EVENT;
4event.user.data1 = opaque;
5SDL_PushEvent(&event);
6return0; /* 0 means stop timer */
7}
这里就是相似的事件推送。FF_REFRESH_EVENT在这里的定义是SDL_USEREVENT + 1。有一个地方需要注意的是当我们返回0时,SDL会停止计时器,回调将不再起作用。

现在我们推送FF_REFRESH_EVENT,我们需要在事件循环中处理它:
1for(;;) {
2 
3SDL_WaitEvent(&event);
4switch(event.type) {
5/* ... */
6caseFF_REFRESH_EVENT:
7video_refresh_timer(event.user.data1);
8break;
然后调用这个函数,将会把数据从picture队列里面拉出来:
01void video_refresh_timer(void*userdata) {
02 
03VideoState *is = (VideoState *)userdata;
04VideoPicture *vp;
05
06if(is->video_st) {
07if(is->pictq_size == 0) {
08schedule_refresh(is, 1);
09} else{
10vp = &is->pictq[is->pictq_rindex];
11/* Timing code goes here */
12 
13schedule_refresh(is, 80);
14
15/* show the picture! */
16video_display(is);
17
18/* update queue for next picture! */
19if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
20is->pictq_rindex = 0;
21}
22SDL_LockMutex(is->pictq_mutex);
23is->pictq_size--;
24SDL_CondSignal(is->pictq_cond);
25SDL_UnlockMutex(is->pictq_mutex);
26}
27} else{
28schedule_refresh(is, 100);
29}
30}
现在,这个函数就非常简单明了了:它会从队列里面拉出数据,设置下一帧播放时间,调用vidoe_display来使视频显示到屏幕中,队列计数值加1,然后减小它的尺寸。你会注意到我们没有对vp做任何动作,这里解析为什么:在之后,我们会使用访问时序信息来同步视频和音频。看看那个“这里的时序代码”的地方,我们会找到我们应该以读快的速度来播放视频的下一帧,然后把值放到schedule_refresh()函数里面。现在我们只是设了一个固定值80。技术上,你可以猜测和检验这个值,然后重编你想看的所有电影,但是 1.过一段时间它会变 2.这是很笨的方法。之后我们会回到这个地方。

我们已经差不多完成了;我们还剩下最后一样东西要做:播放视频!这里就是视频播放的函数:
01void video_display(VideoState *is) {
02 
03SDL_Rect rect;
04VideoPicture *vp;
05AVPicture pict;
06floataspect_ratio;
07intw, h, x, y;
08inti;
09 
10vp = &is->pictq[is->pictq_rindex];
11if(vp->bmp) {
12if(is->video_st->codec->sample_aspect_ratio.num == 0) {
13aspect_ratio = 0;
14} else{
15aspect_ratio = av_q2d(is->video_st->codec->sample_aspect_ratio) *
16is->video_st->codec->width / is->video_st->codec->height;
17}
18if(aspect_ratio <= 0.0) {
19aspect_ratio = (float)is->video_st->codec->width /
20(float)is->video_st->codec->height;
21}
22h = screen->h;
23w = ((int)rint(h * aspect_ratio)) & -3;
24if(w > screen->w) {
25w = screen->w;
26h = ((int)rint(w / aspect_ratio)) & -3;
27}
28x = (screen->w - w) / 2;
29y = (screen->h - h) / 2;
30
31rect.x = x;
32rect.y = y;
33rect.w = w;
34rect.h = h;
35SDL_DisplayYUVOverlay(vp->bmp, &rect);
36}
37}
由于我们的屏幕尺寸可能为任何尺寸(我们设置为640x480,用户有方法可以重新设置尺寸),我们需要动态指出我们需要多大的一个矩形区域。所以首先我们需要指定我们视频的长宽比,也就是宽除以高的值。一些编解码器会有一个奇样本长宽比,也就是一个像素或者一个样本的宽高比。由于我们的编解码的长宽值是按照像素来计算的,所以实际的宽高比等于样本宽高比某些编解码器的宽高比为0,表示每个像素的宽高比为 1x1。然后我们把视频缩放到尽可能大的尺寸。这里的 & -3表示与 -3做与运算,实际上是让他们4字节对齐。然后我们把电影居中,然后调用SDL_DisplayYUVOverlay()。

那么结果怎样?我们做完了吗?我们仍然要重写音频代码来使用我们新的VideoStruct,但那只是琐碎的改变,你可以参考示例代码。最后我们需啊哟做的事情是改变ffmpeg内部的退出回调函数变为我们自己的退出回调函数。
VideoState *global_video_state;
 
int decode_interrupt_cb(void) {
return(global_video_state && global_video_state->quit);
}
我们在主函数里面设置global_video_state这个大结构体。
这就是了!让我们来编译它:
1sdl-config --cflags --libs
2gcc -o tutorial04 tutorial04.c -lavutil -lavformat -lavcodec -lswscale -lSDL -lz -lm

享受你的未同步电影吧!下一节我们会使视频播放器真正地工作起来。


  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值