现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

VLC流程的分析

2017-01-24 06:40 工业·编程 ⁄ 共 9216字 ⁄ 字号 暂无评论

模块的加载

模块的加载分为两部分:模块的初始化,模块的加载。

模块的初始化

libvlc_new 函数初始化

libvlc_InternalInit函数调用module_InitBank ()初始化一个成员为module_t的结构体链表。

libvlc_InternalInit 调用module_LoadPlugins会加载进现有的modules。

module_LoadPlugins 调用module_InitStaticModules函数从vlc_static_modules数组中开始加载,vlc_static_modules保存的是每个modules的入口的函数指针。

vlc_static_modules的取得,不同的平台取得的方法不同,以IOS为例,通过aggregateStaticPlugin.sh脚本遍历modules目录下的静态库,生成一个函数指针列表,如vlc-plugins.h中的int vlc_entry__http (int ()(void , void , int, …), void ); 然后该脚本还会根据这个列表生成vlc_static_modules数组,里面的成员都是这种函数指针的声明符,如下,这只是代码片段

const void * vlc_static_modules [] = {

vlc_entry__http,

vlc_entry__httplive,

NULL

};

有了例如vlc_entry__http 这样的声明符,如何与每个module模块对应起来呢?首先,像vlc_entry__http这样的只是声明符,并没有实际的地址,所以,需要对应的模块定义一个与该名字相同的函数名,这样就能调用了。在例如http.c文件中都有一个vlc_module_begin ()宏,该宏的具体定义在vlc_plugin.h中,它的定义了每个模块对应一个入口函数。

module_InitStaticModules函数会遍历vlc_static_modules数组,调用每个模块的入口函数,加载每个模块的相关信息。值得注意的信息有,会加载模块的描述信息,简称,以及注册回调函数(open,close)。

模块的调用

模块的调用,只有在需要的时候才调用,不需要以后就可以移除掉了。下面我们以http模块的调用为例,介绍它的流程。

1. 视频首次播放时调用libvlc_media_player_play(),然后调用Input_create()以及input_start()。

2. 在input_start()里会调用vlc_clone()创建一个子线程来管理播放器的流程控制,子线程的入口是run()函数,run()会调用init()进行初始化,init()会调用InputSourceInit()函数。

3. 在InputSourceInit函数中,首先调用input_SplitMRL()函数解析出视频URL的获取方法如file,或者http,域名,文件路径等。然后调用access_New(),初始化一个access的对象进入到获取视频的阶段。

4. 在access_New()中,会调用module_need()函数开始加载模块,参数会传入http字符串代表模块的名字。

5. 调用vlc_module_load(obj, cap, name, strict, generic_start, obj),在这里多传入了一个generic_start函数指针,在vlc_module_load函数中通过模块名字的简称进行匹配,查找加载什么模块,查找的方法,就是遍历链表。查找到以后就调用module_load (obj, cand, probe, args)这里的probe就是前面的generic参数,在该函数中会调用probe也就是前面的generic将模块的m->pf_activate激活,而每个模块的m->pf_activate就是每个模块的注册时(vlc_module_set (VLC_MODULE_CB_OPEN, activate)都是注册的open函数指针,就这样,就加载进了http模块。

视频的播放流程

视频的播放流程主要分为两部分:程序启动的初始化,具体视频的播放。

程序启动初始化

程序的初始化会相继调用两个函数libvlc_new(),libvlc_media_player_new()。后者必须以前者的实例为基础。

先来看看libvlc_new(),它在core.c中,返回的是一个libvlc_instance_t的实例。它会调用libvlc_InternalInit()进行初始化和静态模块的注册,主要进行的初始化有:系统环境的初始化,modules的初始化,日志系统的初始化,modules的加载,然后创建了很多变量,大多数与vlc除了播放以外的相关。

再看看libvlc_media_player_new()函数,首先以前面返回的libvlc_instance_t为基础创建一个对象,然后再创建很多与播放相关的variable_t类型的变量。这种变量贯穿了整个程序,variable_t是一个结构体,里面包含了一个callback。用于变量变化的时候调用,后面基本上所有的状态的改变都是以改变这种变量从而调用callback实现的。然后继续初始化eventManager。用于event的处理。最后调用register_event 注册消息。

视频的播放控制

播放

播放的流程比较特殊,分为首次播放和暂停后的播放。因为视频开始播放后,会另起一个线程来控制对象的状态,主线程将直接返回,去接收另外的信息。

播放是调用libvlc_media_player_play(),在该函数中,首先判断input.p_thread是否为空,如果为空表示是首次播放,如果不为空,直接调用input_Control()播放视频。因为目前视频是第一次播放,所以必为空,那么将调用input_Create()函数,它实际是调用input_thread_t *Create()函数,来创建一个input_thread_t的对象,并初始化其中的对象,值得注意的是p_input->p->p_es_out_display = input_EsOutNew( p_input, p_input->p->i_rate );该p_es_out_display将在后面作为真正传入数据到decoder的容器。

调用var_AddCallback注册播放过程中一些必要的回调函数。然后调用input_Start()开始播放流程。

input_Start()会新起一个线程,来处理轮循播放,原线程返回。子线程的入口在run()。Run函数首先调用init()函数初始化线程对象input_thread_t *p_input。在init()函数中,会初始化跟视频相关的信息如视频meta, InitStatistics()会初始化统计数据等等,值得注意的是,会调用InputSourceInit()函数,在前面模块的加载中讲到,在该函数中会分析视频url,并加载http模块,开始下载数据。然后进入了无线循环模式,MainLoop函数。

4.在MainLoop函数中,每次循环都会首先会判断是否暂停了,如果没暂停且没有到视频末尾,就调用MainLoopDemux()函数进入解封装。

5.在MainLoopDemux()函数中调用demux_Demux( p_input->p->input.p_demux );开始解封装,其中p_input->p->input.p_demux的创建是在前面InputSourceInit中创建的

in->p_demux = demux_New( p_input, p_input, psz_access, psz_demux,

p_stream->psz_path ? p_stream->psz_path : psz_path,

p_stream, p_input->p->p_es_out,

p_input->b_preparsing );参数中涉及到p_stream也是在InputSourceInit函数中创建的

stream_t *p_stream = stream_AccessNew( p_access, ppsz_input_list );

p_access的创建也是在这时创建的access_t *p_access = access_New( p_input, p_input,

psz_access, psz_demux, psz_path );。

6.此时问题来了,视频demux需要的数据到底从何而来呢?

7.此时我们进入access_New()函数中,该函数首先创建了一个stream_t对象,然后注册一些流控制的回调函数,调用了AStreamPrebufferStream( s );在该函数中调用了AReadStream()函数,在这个函数中调用了p_access->pf_read( p_access, p_read, i_read )函数,这是p_access的一个读取数据的回调函数。p_access->pf_read在access_New的时候会被初始化为null。它具体的赋值是在加载具体的modules后赋值的,如http.c中p_access->pf_read = ReadCompressed。也就是说再stream.c中的AReadStream()函数会调用http.c中的ReadCompressed来读取数据。

8.前面说了access_New()中会调用AStreamPrebufferStream( s )从http模块读取数据,也就是说stream *s中就存有数据了。现在回到解封装函数demux_Demux()中,在该函数中实际调用的是p_demux->pf_demux( p_demux ),又是一个函数指针,它的赋值与access->pf_read的赋值极其相似,在创建的时候demux_New()中赋值为NULL。它的具体的赋值是在具体的模块加载的时候赋值的,具体的加载时在demux_New()中,它会根据url的后缀名,具体加载哪个模块,假设是mp4文件,则加载mp4模块。在mp4.c的open()函数中会有p_demux->pf_demux = Demux;那么demux_Demux()函数会从input.c进入到mp4.c文件中。那么现在调用了mp4.c中的demux()函数了MP4_Block_Send()函数,它实际又调用了es_out_Send( p_demux->out, p_track->p_es, p_block )函数,es_out_send实际调用了out->pf_send( out, id, p_block )函数,这又是一个函数指针。

out->pf_send函数指针,它具体的值到底是什么呢,这又得一步一步回溯,首先传入得参数是p_demux->out,p_demux->out的赋值是在demux_new函数中,在该函数中p_demux->out = out;它的实参是在实际调用时传入的p_input->p->p_es_out。我们知道在p_input结构体中有两个es_out类型的结构体p_es_out和p_es_out_display。它们两个的构建和左右都不相同,具体怎么不同呢,后面就能看出来了,回到原来的地方,在demux_new中,我们传入的是p_es_out这个参数,它的构造是调用的p_input->p->p_es_out = input_EsOutTimeshiftNew( p_input, p_input->p->p_es_out_display, p_input->p->i_rate );从代码可以看出它是在es_out_Timeshift.c中EsOutTimeshiftNew构造的,并传入了p_es_out_display作为参数。在该函数中,给p_es_out的pf_send具体赋值了,p_out->pf_send= Send,所以回到前面es_out_send调用的out->pf_send( out, id, p_block )函数,它的函数指针是es_out_timeshift.c文件中的静态函数send()。那我们在深入到send()函数中,它调用了CmdExecuteSend( p_sys->p_out, &cmd)函数,继而又调用了es_out_Send( p_out, p_cmd->u.send.p_es->p_es, p_block )函数,我们知道es_out_send实际调用的是p_out->pf_send函数,这又是一个函数指针。

那么这个pf_send具体又是哪个函数,我们利用前面的回溯方法,继续向前搜索会发现,这个参数p_out是前面的传入的p_es_out_display,而p_es_out_display它的初始化是在input.c里面的create函数中p_es_out_display = input_EsOutNew( p_input, p_input->p->i_rate ),input_esOutNew是es_out.c文件中,在该函数赋值了out->pf_send = EsOutSend。所以回到前面CmdExecuteSend( p_sys->p_out, &cmd)实际调用了es_out.c文件中的esOutSend函数,在esOutSend函数中,调用了input_DecoderDecode()函数开始解码流程。

在进入input_DecoderDecode()函数后,我们发现并没有看到解码的操作,只有一个block_FifoPut(),该函数的左右是将一个block数据添加到一个FIFO队列末尾。那到底哪里才是解码操作呢,此时我们看看input_DecoderDecode( es->p_dec, p_block,p_input->p->b_out_pace_control );函数的参数,p_dec是一个decoder的结构体,它的构建在前面,我们继续追溯。发现在mp4.c文件中,open()函数会调用MP4_TrackCreate()函数,继而调用TrackCreateES()函数,它会调用es_out_Add,它会经过es_time_shif.c最终调用es_out.c的EsOutAdd函数,会通过EsOutSelect-> EsSelect-> EsCreateDecoder-> input_DecoderNew。input_DecoderNew是decoder.c里面的函数,它实际调用的是decoder_New()函数,它会调用CreateDecoder函数创建一个decoder_t的结构体并根据解码的需要加载具体的解码模块,然后开启一个线程处理解码的操作,线程的入口是DecoderThread()函数,它调用DecoderProcess函数,该函数会根据数据是什么类型的数据分别调用不同的解码流程,我们以视频数据为例,它会调用DecoderProcessVideo函数,继而调用DecoderDecodeVideo函数,它又会调用p_dec->pf_decode_video函数进行解码,这又是一个函数指针,它的赋值是在具体的解码模块中赋值的。具体解码后的数据流向,暂时还没有调研。

经过前面11个步骤的说明,现在可以小结一下了,视频数据的整体流向已经很清晰了,首先通过access的模块如http从网络上下载数据经过stream.c的处理传递给demux模块做解封装的处理,解封装后的数据最终会发送给es_out.c处理,并发给你decoder模块做解码操作。具体解码后的数据的流向,暂时没有了解,留到后面再了解。

暂停

解析完播放,我们再来看看暂停是如何控制的,从暂停操作,就可以看出所有的控制流程的框架。

1. 暂停的控制首先在media_player.c文件中调用libvlc_media_player_set_pause,该函数首先会判断是否正在播放或正在缓冲,然后调用input_Control( p_input_thread, INPUT_SET_STATE, PAUSE_S )函数进行控制,该函数是在control.c中,它会根据传入的控制命令,调用return var_SetInteger( p_input, “state”, i_int );后就直接返回了。从input_control函数代码中可以看出,几乎所有的控制,状态的取得都是通过get和set p_input的变量实现。首先需要找到这些变量的创建在什么地方,它是在input.c文件中的create()函数中的input_ControlVarInit()函数中创建的var_Create( p_input, “state”, VLC_VAR_INTEGER );它创建了一个variable_t类型的结构体,里面有一个callback的成员,会调用InputAddCallbacks( p_input, p_input_callbacks );函数将state变量与StateCallback回调函数绑定起来。

现在回到var_SetInteger( p_input, “state”, i_int )函数,它会调用var_SetChecked函数,从而调用TriggerCallback函数触发回调函数StateCallback。该函数会调用input.c文件中的input_ControlPush函数。这里需要提到的是在input.c中的控制的实现是使用FIFO的方式来管理控制命令的,又input_controlpush将控制命令压入FIFO队列中,然后由mainloop循环调用controlpop函数,将控制队列的函数弹出来。

controlpop调用后,如果队列中有命令执行,就会调用control函数,该函数中会根据不同的命令调用不同的处理函数,对于暂停命令,就会调用ControlPause函数,同时还会调用input_ChangeState函数改变state的状态反馈给上层。同时改变p_input->p->i_state为pause状态,那么mainloop的循环就不会再调用demux模块的解封装函数了,就不会再有视频数据流向es_out。在controlPause函数中,会首先调用stream_Control在stream.c,调用相应的access模块暂停,然后调用es_out_SetPauseState在es_out.c层面,同时通知decoder.c的层面的控制。

到此为止,暂停操作的流程可以总结一下。暂停的操作,首先通过改变state变量来触发callback函数,同时改变p_input->p->state状态,让主循环来暂停解封装的操作,停止数据继续解封装和解码,同时调用access,stream,es_out,decoder各个层面的control让暂停操作。其余的控制,流程大致跟暂停差不多。

线程的管理

线程的创建

我们假设libvlc的创建和启动是在主线程里,那么后面新起的线程都是子线程,下面介绍每个线程的具体任务。

主线程:负责创建libvlc对象,初始化,同时主线程也会接收来自上层的所有控制指令play,pause,将指令调用controlpush函数压入到FIFO队列中等。

子线程1,可以简称input线程,它是由主线程在第一次play的时候创建的,在Input_start函数里,会创建一个input线程,入口是run函数,它的任务主要是不断的循环,调用解封装函数,促进数据的流动,让数据从网络到内存,再解封装,解码。同时不断的反馈具体的状态信息到主线程上。

子线程2. 可以简称hls线程,在httplive.c里面创建,vlc_clone(&p_sys->thread, hls_Thread, s, VLC_THREAD_PRIORITY_INPUT)。主要任务,没有详细了解,我猜想应该是负责解析m3u8文件,读取具体的ts流。

子线程3.简称decoder线程,它是Input线程在decoder_New函数中创建的,它也是一个无限循环,从一个FIFO的队列中不断的取出数据送去解码。这就是它的主要任务。

线程的同步

目前为止,有了4个线程,必然涉及到对共享变量的同步互斥机制,下面进行简单的分析。

采用的同步互斥主要有两种方法,锁和条件变量

锁,vlc_mutex_lock( &p_input->p->p_item->lock );

因为这几个线程共享的一个对象是p_input。它是input_thread_t类型的结构体,所以每次更改状态时都会锁住它。

条件变量:vlc_cond_timedwait和vlc_cond_signal。这是同步的时候需要用到的,比如controlpush和controlpop命令的压入和弹出的时候,

小结

当然,整个vlc流程中,不仅仅只有这几个线程,其实还有很多线程,目前分析的是从数据的输入到解码这部分的线程,具体解码后的线程,暂时还没有去详细分析,留到后面补充。

Stream.c模块的分析

1.首先是创建一个stream的对象,调用的是stream_AccessNew函数,在该函数中会设置pf_read,pf_peek,pf_readdir,pf_control。并且,在stream_AccessNew中,会给stream.p_buffer分配3个track一共12MB的空间,每个tack拥有4MB的空间,就目前分析来看,实际上只用到了1个track也就是4MB的空间,至于其他两个track到底有什么用还不清楚,并初始化一次读取数据的量stream.i_read_size为1024。并且还会调用AStreamPrebufferStream( s )函数,对stream进行预填充,下面进行详细的分析AStreamPrebufferStream( s )加载数据的机制。

2. AstreamPrebufferStream函数中,该函数会循环调用AreadStream函数读取最小128字节的数据,不过默认第一次是读取i_read_size的数据,看实际的读取量而定,这是预填充的数据。

2.在流程分析中提到,mainloop里面有个无线循环会不断的调用demux函数,而在每个demux函数中会不断的调用MP4_Block_Read,从而调用stream_Block,再调用stream_Read,它实际上是调用pf_read函数,它是指向AstreamReadStream函数。

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

作者:baohonglai

给我留言

留言无头像?