VLC 播放的音视频数据处理流水线搭建
- 音视频流播放处理循环
- 音频输出处理流水线
VLC 用
input_thread_t
对象直接或间接管理音视频播放有关的各种资源,包括
Access,
Demux,
Decode,
Output,
Filter 等,这个类型定义 (位于
vlc-3.0.16/include/vlc_input.h) 如下:
struct input_thread_t
{VLC_COMMON_MEMBERS
};
input_thread_t
是个抽象类型,VLC 中这个类型的具体实现为 input_thread_private_t
,后者定义 (位于 vlc-3.0.16/src/input/input_internal.h) 如下:
#define INPUT_CONTROL_FIFO_SIZE 100/* input_source_t: gathers all information per input source */
typedef struct
{VLC_COMMON_MEMBERSdemux_t *p_demux; /**< Demux object (most downstream) *//* Title infos for that input */bool b_title_demux; /* Titles/Seekpoints provided by demux */int i_title;input_title_t **title;int i_title_offset;int i_seekpoint_offset;int i_title_start;int i_title_end;int i_seekpoint_start;int i_seekpoint_end;/* Properties */bool b_can_pause;bool b_can_pace_control;bool b_can_rate_control;bool b_can_stream_record;bool b_rescale_ts;double f_fps;/* */int64_t i_pts_delay;bool b_eof; /* eof of demuxer */} input_source_t;typedef struct
{int i_type;vlc_value_t val;
} input_control_t;/** Private input fields */
typedef struct input_thread_private_t
{struct input_thread_t input;/* Global properties */bool b_preparsing;bool b_can_pause;bool b_can_rate_control;bool b_can_pace_control;/* Current state */int i_state;bool is_running;bool is_stopped;bool b_recording;int i_rate;/* Playtime configuration and state */int64_t i_start; /* :start-time,0 by default */int64_t i_stop; /* :stop-time, 0 if none */int64_t i_time; /* Current time */bool b_fast_seek;/* :input-fast-seek *//* Output */bool b_out_pace_control; /* XXX Move it ot es_sout ? */sout_instance_t *p_sout; /* Idem ? */es_out_t *p_es_out;es_out_t *p_es_out_display;vlc_viewpoint_t viewpoint;bool viewpoint_changed;vlc_renderer_item_t *p_renderer;/* Title infos FIXME multi-input (not easy) ? */int i_title;const input_title_t **title;int i_title_offset;int i_seekpoint_offset;/* User bookmarks FIXME won't be easy with multiples input */seekpoint_t bookmark;int i_bookmark;seekpoint_t **pp_bookmark;/* Input attachment */int i_attachment;input_attachment_t **attachment;const demux_t **attachment_demux;/* Main input properties *//* Input item */input_item_t *p_item;/* Main source */input_source_t *master;/* Slave sources (subs, and others) */int i_slave;input_source_t **slave;/* Resources */input_resource_t *p_resource;input_resource_t *p_resource_private;/* Stats counters */struct {counter_t *p_read_packets;counter_t *p_read_bytes;counter_t *p_input_bitrate;counter_t *p_demux_read;counter_t *p_demux_bitrate;counter_t *p_demux_corrupted;counter_t *p_demux_discontinuity;counter_t *p_decoded_audio;counter_t *p_decoded_video;counter_t *p_decoded_sub;counter_t *p_sout_sent_packets;counter_t *p_sout_sent_bytes;counter_t *p_sout_send_bitrate;counter_t *p_played_abuffers;counter_t *p_lost_abuffers;counter_t *p_displayed_pictures;counter_t *p_lost_pictures;vlc_mutex_t counters_lock;} counters;/* Buffer of pending actions */vlc_mutex_t lock_control;vlc_cond_t wait_control;int i_control;input_control_t control[INPUT_CONTROL_FIFO_SIZE];vlc_thread_t thread;vlc_interrupt_t interrupt;
} input_thread_private_t;
播放 VLC 播放列表中的一个音视频流的时候,音视频流的播放通过 PlayItem()
函数起动,这个函数被调用的调用栈如下:
Thread 3 "vlc" hit Breakpoint 14, PlayItem (p_item=<optimized out>, p_playlist=0xaaaaaab37980) at playlist/thread.c:227
227 if( input_Start( p_input_thread ) )
(gdb) bt
#0 PlayItem (p_item=<optimized out>, p_playlist=0xaaaaaab37980) at playlist/thread.c:227
#1 Next (p_playlist=0xaaaaaab37980) at playlist/thread.c:474
#2 Thread (data=0xaaaaaab37980) at playlist/thread.c:499
#3 0x0000fffff7e4d5c8 in start_thread (arg=0x0) at ./nptl/pthread_create.c:442
PlayItem()
函数定义 (位于 vlc-3.0.16/src/playlist/thread.c) 如下:
static bool PlayItem( playlist_t *p_playlist, playlist_item_t *p_item )
{playlist_private_t *p_sys = pl_priv(p_playlist);input_item_t *p_input = p_item->p_input;vlc_renderer_item_t *p_renderer;PL_ASSERT_LOCKED;msg_Dbg( p_playlist, "creating new input thread" );p_item->i_nb_played++;set_current_status_item( p_playlist, p_item );p_renderer = p_sys->p_renderer;/* Retain the renderer now to avoid it to be released by* playlist_SetRenderer when we exit the locked scope. If the last reference* was to be released, we would use a dangling pointer */if( p_renderer )vlc_renderer_item_hold( p_renderer );assert( p_sys->p_input == NULL );PL_UNLOCK;libvlc_MetadataCancel( p_playlist->obj.libvlc, p_item );input_thread_t *p_input_thread = input_Create( p_playlist, p_input, NULL,p_sys->p_input_resource,p_renderer );if( p_renderer )vlc_renderer_item_release( p_renderer );if( likely(p_input_thread != NULL) ){var_AddCallback( p_input_thread, "intf-event",InputEvent, p_playlist );if( input_Start( p_input_thread ) ){var_DelCallback( p_input_thread, "intf-event",InputEvent, p_playlist );vlc_object_release( p_input_thread );p_input_thread = NULL;}}/* TODO store art policy in playlist private data */char *psz_arturl = input_item_GetArtURL( p_input );/* p_input->p_meta should not be null after a successful CreateThread */bool b_has_art = !EMPTY_STR( psz_arturl );if( !b_has_art || strncmp( psz_arturl, "attachment://", 13 ) ){PL_DEBUG( "requesting art for new input thread" );libvlc_ArtRequest( p_playlist->obj.libvlc, p_input, META_REQUEST_OPTION_NONE );}free( psz_arturl );PL_LOCK;p_sys->p_input = p_input_thread;PL_UNLOCK;var_SetAddress( p_playlist, "input-current", p_input_thread );PL_LOCK;return p_input_thread != NULL;
}
PlayItem()
函数接收 playlist_item_t
类型的 p_item
参数,这个类型定义 (位于 vlc-3.0.16/include/vlc_playlist.h) 如下:
/** playlist item / node */
struct playlist_item_t
{input_item_t *p_input; /**< Linked input item */playlist_item_t **pp_children; /**< Children nodes/items */playlist_item_t *p_parent; /**< Item parent */int i_children; /**< Number of children, -1 if not a node */unsigned i_nb_played; /**< Times played */int i_id; /**< Playlist item specific id */uint8_t i_flags; /**< Flags \see playlist_item_flags_e */
};
playlist_item_t
类型包含一个类型为 input_item_t
的字段 p_input
,其中包含要播放的音视频流的各种信息,如 URI,名称,基础流的个数及格式等,input_item_t
类型的详细定义 (位于 vlc-3.0.16/include/vlc_input_item.h) 如下:
struct info_t
{char *psz_name; /**< Name of this info */char *psz_value; /**< Value of the info */
};struct info_category_t
{char *psz_name; /**< Name of this category */int i_infos; /**< Number of infos in the category */struct info_t **pp_infos; /**< Pointer to an array of infos */
};/*** Describes an input and is used to spawn input_thread_t objects.*/
struct input_item_t
{char *psz_name; /**< text describing this item */char *psz_uri; /**< mrl of this item */int i_options; /**< Number of input options */char **ppsz_options; /**< Array of input options */uint8_t *optflagv; /**< Some flags of input options */unsigned optflagc;input_item_opaque_t *opaques; /**< List of opaque pointer values */mtime_t i_duration; /**< Duration in microseconds */int i_categories; /**< Number of info categories */info_category_t **pp_categories; /**< Pointer to the first info category */int i_es; /**< Number of es format descriptions */es_format_t **es; /**< Es formats */input_stats_t *p_stats; /**< Statistics */vlc_meta_t *p_meta;int i_epg; /**< Number of EPG entries */vlc_epg_t **pp_epg; /**< EPG entries */int64_t i_epg_time; /** EPG timedate as epoch time */const vlc_epg_t *p_epg_table; /** running/selected program cur/next EPG table */int i_slaves; /**< Number of slaves */input_item_slave_t **pp_slaves; /**< Slave entries that will be loaded bythe input_thread */vlc_event_manager_t event_manager;vlc_mutex_t lock; /**< Lock for the item */uint8_t i_type; /**< Type (file, disc, ... see input_item_type_e) */bool b_net; /**< Net: always true for TYPE_STREAM, itdepends for others types */bool b_error_when_reading;/**< Error When Reading */int i_preparse_depth; /**< How many level of sub items can be preparsed:-1: recursive, 0: none, >0: n levels */bool b_preparse_interact; /**< Force interaction with the user whenpreparsing.*/
};
这里的几个类型的结构关系大概如下图所示:
PlayItem()
函数主要是调用 input_Create()
函数创建 input_thread_t
/input_thread_private_t
对象,并调用 input_Start()
函数启动对音视频流的处理。
input_Create()
和 input_Start()
函数函数定义 (位于 vlc-3.0.16/src/input/input.c) 如下:
input_thread_t *input_Create( vlc_object_t *p_parent,input_item_t *p_item,const char *psz_log, input_resource_t *p_resource,vlc_renderer_item_t *p_renderer )
{return Create( p_parent, p_item, psz_log, false, p_resource, p_renderer );
}. . . . . .
/*** Start a input_thread_t created by input_Create.** You must not start an already running input_thread_t.** \param the input thread to start*/
int input_Start( input_thread_t *p_input )
{input_thread_private_t *priv = input_priv(p_input);void *(*func)(void *) = Run;if( priv->b_preparsing )func = Preparse;assert( !priv->is_running );/* Create thread and wait for its readiness. */priv->is_running = !vlc_clone( &priv->thread, func, priv,VLC_THREAD_PRIORITY_INPUT );if( !priv->is_running ){input_ChangeState( p_input, ERROR_S );msg_Err( p_input, "cannot create input thread" );return VLC_EGENERIC;}return VLC_SUCCESS;
}. . . . . .static input_thread_t *Create( vlc_object_t *p_parent, input_item_t *p_item,const char *psz_header, bool b_preparsing,input_resource_t *p_resource,vlc_renderer_item_t *p_renderer )
{/* Allocate descriptor */input_thread_private_t *priv;priv = vlc_custom_create( p_parent, sizeof( *priv ), "input" );if( unlikely(priv == NULL) )return NULL;input_thread_t *p_input = &priv->input;. . . . . .}
static void *Run( void *data )
{input_thread_private_t *priv = data;input_thread_t *p_input = &priv->input;vlc_interrupt_set(&priv->interrupt);if( !Init( p_input ) ){if( priv->b_can_pace_control && priv->b_out_pace_control ){/* We don't want a high input priority here or we'll* end-up sucking up all the CPU time */vlc_set_priority( priv->thread, VLC_THREAD_PRIORITY_LOW );}MainLoop( p_input, true ); /* FIXME it can be wrong (like with VLM) *//* Clean up */End( p_input );}input_SendEventDead( p_input );return NULL;
}
除了 b_preparsing
参数传 false
外,input_Create()
函数将接收的各个参数传给 Create()
,通过后者创建 input_thread_private_t
对象。input_Start()
函数启动一个新的线程处理音视频流,默认情况下,新起的线程跑 Run()
函数,但如果设置了 b_preparsing
,新起的线程跑 Preparse()
函数。
播放列表中各个音频流的信息,也通过 input_thread_t
获取。当我们往 VLC 的播放列表拖入一个文件时,UI 事件向播放列表添加一个输入项,输入项最终被放入播放列表预解析器的待解析队列中,这个调用过程如下面的调用栈所示:
#0 background_worker_Push (worker=0xaaaaaaae2e40, entity=entity@entry=0xffffc885fec0, id=id@entry=0xffffc87f5ac0, timeout=timeout@entry=-1) at misc/background_worker.c:211
#1 0x0000fffff7ced04c in playlist_preparser_Push (preparser=0xaaaaaaadbe00, item=item@entry=0xffffc885fec0, i_options=META_REQUEST_OPTION_NONE, timeout=-1, id=0xffffc87f5ac0)at playlist/preparser.c:178
#2 0x0000fffff7cca748 in vlc_MetadataRequest(libvlc=0xaaaaaaab58e0, item=item@entry=0xffffc885fec0, i_options=i_options@entry=META_REQUEST_OPTION_NONE, timeout=timeout@entry=-1, id=id@entry=0xffffc87f5ac0) at libvlc.c:504
#3 0x0000fffff7ceee5c in playlist_Preparse (p_item=0xffffc87f5ac0, p_playlist=0xaaaaaab37980) at playlist/item.c:750
#4 playlist_NodeAddInput (p_playlist=p_playlist@entry=0xaaaaaab37980, p_input=p_input@entry=0xffffc885fec0, p_parent=<optimized out>, i_pos=i_pos@entry=-1) at playlist/item.c:541
#5 0x0000fffff7ceef20 in playlist_AddInput (p_playlist=p_playlist@entry=0xaaaaaab37980, p_input=p_input@entry=0xffffc885fec0, play_now=play_now@entry=false, b_playlist=b_playlist@entry=true)at playlist/item.c:504
#6 0x0000fffff7ceefe4 in playlist_AddExt(p_playlist=0xaaaaaab37980, psz_uri=<optimized out>, psz_name=<optimized out>, play_now=false, i_options=0, ppsz_options=0x0, i_option_flags=2, b_playlist=true) at playlist/item.c:483
#7 0x0000ffffe51d4440 in Open::openMRLwithOptions(intf_thread_t*, QString const&, QStringList*, bool, bool, char const*)(p_intf=0xaaaaaad8e8d0, mrl=..., options=<optimized out>, b_start=false, b_playlist=true, title=0x0) at /usr/include/aarch64-linux-gnu/qt5/QtCore/qarraydata.h:61
#8 0x0000ffffe51ba768 in MainInterface::dropEventPlay(QDropEvent*, bool, bool) (this=0xffffc81475f0, event=0xffffe319d020, b_play=<optimized out>, b_playlist=true)at gui/qt/main_interface.cpp:1580
#9 0x0000ffffe4b96378 in QWidget::event(QEvent*) () at /usr/lib/aarch64-linux-gnu/libQt5Widgets.so.5
#10 0x0000ffffe4b52ac0 in QApplicationPrivate::notify_helper(QObject*, QEvent*) () at /usr/lib/aarch64-linux-gnu/libQt5Widgets.so.5
#11 0x0000ffffe4b5a2e0 in QApplication::notify(QObject*, QEvent*) () at /usr/lib/aarch64-linux-gnu/libQt5Widgets.so.5
#12 0x0000ffffe40acb90 in QCoreApplication::notifyInternal2(QObject*, QEvent*) () at /usr/lib/aarch64-linux-gnu/libQt5Core.so.5
其中 input_item_t
对象在 playlist_AddExt()
函数中创建,它所属的 playlist_item_t
对象则在 playlist_AddInput()
函数中通过 playlist_NodeAddInput()
创建。VLC 起一个后台线程来预解析音视频流,VLC 调用 PreparserOpenInput()
函数预解析音视频流,这个函数被调用的调用栈如下:
Thread 32 "vlc" hit Breakpoint 7, PreparserOpenInput (preparser_=0xaaaaaaadbe00, item_=0xffffc8730830, out=0xffffb021e790) at playlist/preparser.c:55
55 playlist_preparser_t* preparser = preparser_;
(gdb) bt
#0 PreparserOpenInput (preparser_=0xaaaaaaadbe00, item_=0xffffc8730830, out=0xffffb021e790) at playlist/preparser.c:55
#1 0x0000fffff7d43c1c in Thread (data=0xaaaaaaae2e40) at misc/background_worker.c:115
#2 0x0000fffff7e4d5c8 in start_thread (arg=0x0) at ./nptl/pthread_create.c:442
PreparserOpenInput()
函数定义 (位于 vlc-3.0.16/src/playlist/preparser.c) 如下:
static int PreparserOpenInput( void* preparser_, void* item_, void** out )
{playlist_preparser_t* preparser = preparser_;input_thread_t* input = input_CreatePreparser( preparser->owner, item_ );if( !input ){input_item_SignalPreparseEnded( item_, ITEM_PREPARSE_FAILED );return VLC_EGENERIC;}var_AddCallback( input, "intf-event", InputEvent, preparser->worker );if( input_Start( input ) ){var_DelCallback( input, "intf-event", InputEvent, preparser->worker );input_Close( input );input_item_SignalPreparseEnded( item_, ITEM_PREPARSE_FAILED );return VLC_EGENERIC;}*out = input;return VLC_SUCCESS;
}
与前面看到的 PlayItem()
函数有些类似,但它调用 input_CreatePreparser()
函数创建 input_thread_private_t
对象,同时由于配置了 b_preparsing
,input_Start()
函数起的线程将运行 Preparse()
函数,且创建的 input_thread_private_t
对象被配置了 OBJECT_FLAGS_QUIET | OBJECT_FLAGS_NOINTERACT
,这意味着无法通过 VLC 的日志机制为这个对象输出日志。input_CreatePreparser()
和 Preparse()
函数定义 (位于 vlc-3.0.16/src/input/input.c) 如下:
static void *Preparse( void *data )
{input_thread_private_t *priv = data;input_thread_t *p_input = &priv->input;vlc_interrupt_set(&priv->interrupt);if( !Init( p_input ) ){ /* if the demux is a playlist, call Mainloop that will call* demux_Demux in order to fetch sub items */bool b_is_playlist = false;if ( input_item_ShouldPreparseSubItems( priv->p_item )&& demux_Control( priv->master->p_demux, DEMUX_IS_PLAYLIST,&b_is_playlist ) )b_is_playlist = false;if( b_is_playlist )MainLoop( p_input, false );End( p_input );}input_SendEventDead( p_input );return NULL;
}. . . . . .
input_thread_t *input_CreatePreparser( vlc_object_t *parent,input_item_t *item )
{return Create( parent, item, NULL, true, NULL, NULL );
}
input_CreatePreparser()
函数也是 Create()
函数的包装,只是它的 b_preparsing
参数传的是 true
。Preparse()
函数与 Run()
函数类似,只是它不运行音视频流的 Demux 循环。预解析处理有超时限制,VLC 并不总是会等预解析完成。预解析成功完成时,可以在 VLC 的播放列表中看到通过 demuxer 获得的音视频流的时长等信息,没能成功完成时,则只能看到文件名作为标题。
音视频流播放处理循环
VLC 中,input_Start()
函数启动一个新的线程跑 Run()
函数来播放音视频流,这个函数调用 Init()
函数初始化音视频流播放处理过程,初始化前面通过 input_Create()
函数创建的 input_thread_private_t
对象,随后调用 MainLoop()
函数跑一个处理循环,最后调用 End()
函数结束处理。
音视频流播放处理过程初始化,需要完成许多事情,如为要播放的音视频流创建 access 以便于获取数据,创建 demuxer,创建 decoder 等。具体来看 Init()
函数的定义 (位于 vlc-3.0.16/src/input/input.c) 如下:
static int Init( input_thread_t * p_input )
{input_thread_private_t *priv = input_priv(p_input);input_source_t *master;if( var_Type( p_input->obj.parent, "meta-file" ) ){msg_Dbg( p_input, "Input is a meta file: disabling unneeded options" );var_SetString( p_input, "sout", "" );var_SetBool( p_input, "sout-all", false );var_SetString( p_input, "input-slave", "" );var_SetInteger( p_input, "input-repeat", 0 );var_SetString( p_input, "sub-file", "" );var_SetBool( p_input, "sub-autodetect-file", false );}InitStatistics( p_input );
#ifdef ENABLE_SOUTif( InitSout( p_input ) )goto error;
#endif/* Create es out */priv->p_es_out = input_EsOutTimeshiftNew( p_input, priv->p_es_out_display,priv->i_rate );if( priv->p_es_out == NULL )goto error;/* */input_ChangeState( p_input, OPENING_S );input_SendEventCache( p_input, 0.0 );/* */master = InputSourceNew( p_input, priv->p_item->psz_uri, NULL, false );if( master == NULL )goto error;priv->master = master;InitTitle( p_input );/* Load master infos *//* Init length */mtime_t i_length;if( demux_Control( master->p_demux, DEMUX_GET_LENGTH, &i_length ) )i_length = 0;if( i_length <= 0 )i_length = input_item_GetDuration( priv->p_item );input_SendEventLength( p_input, i_length );input_SendEventPosition( p_input, 0.0, 0 );if( !priv->b_preparsing ){StartTitle( p_input );SetSubtitlesOptions( p_input );LoadSlaves( p_input );InitPrograms( p_input );double f_rate = var_InheritFloat( p_input, "rate" );if( f_rate != 0.0 && f_rate != 1.0 ){vlc_value_t val = { .i_int = INPUT_RATE_DEFAULT / f_rate };input_ControlPush( p_input, INPUT_CONTROL_SET_RATE, &val );}}if( !priv->b_preparsing && priv->p_sout ){priv->b_out_pace_control = priv->p_sout->i_out_pace_nocontrol > 0;msg_Dbg( p_input, "starting in %ssync mode",priv->b_out_pace_control ? "a" : "" );}vlc_meta_t *p_meta = vlc_meta_New();if( p_meta != NULL ){/* Get meta data from users */InputMetaUser( p_input, p_meta );/* Get meta data from master input */InputSourceMeta( p_input, master, p_meta );/* And from slave */for( int i = 0; i < priv->i_slave; i++ )InputSourceMeta( p_input, priv->slave[i], p_meta );es_out_ControlSetMeta( priv->p_es_out, p_meta );vlc_meta_Delete( p_meta );}msg_Dbg( p_input, "`%s' successfully opened",input_priv(p_input)->p_item->psz_uri );/* initialization is complete */input_ChangeState( p_input, PLAYING_S );return VLC_SUCCESS;error:input_ChangeState( p_input, ERROR_S );if( input_priv(p_input)->p_es_out )es_out_Delete( input_priv(p_input)->p_es_out );es_out_SetMode( input_priv(p_input)->p_es_out_display, ES_OUT_MODE_END );if( input_priv(p_input)->p_resource ){if( input_priv(p_input)->p_sout )input_resource_RequestSout( input_priv(p_input)->p_resource,input_priv(p_input)->p_sout, NULL );input_resource_SetInput( input_priv(p_input)->p_resource, NULL );if( input_priv(p_input)->p_resource_private )input_resource_Terminate( input_priv(p_input)->p_resource_private );}if( !priv->b_preparsing && libvlc_stats( p_input ) ){
#define EXIT_COUNTER( c ) do { if( input_priv(p_input)->counters.p_##c ) \stats_CounterClean( input_priv(p_input)->counters.p_##c );\input_priv(p_input)->counters.p_##c = NULL; } while(0)EXIT_COUNTER( read_bytes );EXIT_COUNTER( read_packets );EXIT_COUNTER( demux_read );EXIT_COUNTER( input_bitrate );EXIT_COUNTER( demux_bitrate );EXIT_COUNTER( demux_corrupted );EXIT_COUNTER( demux_discontinuity );EXIT_COUNTER( played_abuffers );EXIT_COUNTER( lost_abuffers );EXIT_COUNTER( displayed_pictures );EXIT_COUNTER( lost_pictures );EXIT_COUNTER( decoded_audio );EXIT_COUNTER( decoded_video );EXIT_COUNTER( decoded_sub );if( input_priv(p_input)->p_sout ){EXIT_COUNTER( sout_sent_packets );EXIT_COUNTER( sout_sent_bytes );EXIT_COUNTER( sout_send_bitrate );}
#undef EXIT_COUNTER}/* Mark them deleted */input_priv(p_input)->p_es_out = NULL;input_priv(p_input)->p_sout = NULL;return VLC_EGENERIC;
}
Init()
函数做的主要的事情如下:
- 初始化统计计数器。
- 创建 es out,es 指 Elementary streams,即基本流。后面更详细地对 es 作解释。
- 调用
InputSourceNew()
函数创建 input source。VLC 的 input source 指解码之前,对音视频流数据做处理的组件,这包括 access 组件,access 组件后面执行缓存策略的 stream_filter 组件,再后面自动的 stream_filter 组件,再后面更上层指定的其它 stream_filter 组件,再后面的 demux 组件。或者对于类型的流,可以将 demux 和数据获取合并的情况,为 access_demux 组件。demux 组件或 access_demux 组件后面的是若干个更上层指定的 demux_filter 组件。所有这些由input_source_t
对象描述,它的demux_t
类型的字段