经过《浅析mpeg-ts封装结构》与《biTStream功能介绍与应用》,我们了解了mpegts的基本结构及一款简单易用的解析库。今天要介绍的是基于biTStream实现的一款精悍好用的工具:DVBlast。
首先说明一下DVB的概念。DVB全称是Digital Video Broadcasting,数字视频广播。当然,它根据传输方式分为好几种,例如家庭有线电视的cable线、卫星信号、地面广播等。我们最常见的应该还是家庭有线电视,看电视时需要通过一个盒子一端连接墙上的有线孔,一端连接在电视机上,初次使用需要有一个搜索电视台的过程。搜索结束之后就可以长期观看电视节目了。
那个电视盒子就是一个调制解调设备,它从特定的频点上接收信号然后将信号还原为数字格式输出。在发送端往往会将所有节目流复用到一路TS流中,这样只需要占用一个频点就可以传输所有节目,频点资源是有限的,但一个频点的带宽是非常大的。一般的是电视盒子是linux系统,因此dvb设备会有专门的驱动,设备被映射为tty类型的节点。读取数据时就可以通过标准的系统调用打开设备,不断读取数据向外输出。
在其官网说明中特地提到一个场景就是在计算性能比较弱的环境(嵌入式系统、低功耗设备)上也能顺畅地接收复合流并进行解复用之后按节目分别输出到不同的地址端口上。
官网:http://www.videolan.org/projects/dvblast.html
工程源码:https://get.videolan.org/dvblast/3.1/dvblast-3.1.tar.bz2
Dvblast处理的是mpegts流,关于其结构请参考《浅析mpeg-ts封装结构》。
一个程序无论其功能多复杂,总是有输入、处理、输出这三个过程。下面分别对dvblast
的编译以及这三方面进行说明。
依赖了两个库:
Libev:一个多路事件处理框架,可以高效地处理IO事件。需要预先安装,并将其头文件与动态库置于可访问的路径内。方法可以是安装在系统目录下,也可以是修改Makefile指向其安装后路径。
Bitstream:由若干组C语言头文件构成的库,关于其说明可参考《biTStream功能介绍与应用》。其无需编译,只要将其置于可访问路径内即可。例如整体复制至/usr/include目录内。或者修改Makefile,将其路径添加至CFLAGS变量内。
配置安装好上述两个库之后的编译方式就很简单了,只需要在源码目录内执行make即可。编译成功会生成dvblast与dvblastctl两个可执行程序。
Dvblast可支持两种方式的输入,一是读取dvb设备的数据;二是读取udp传输的数据,包括单播与组播的方式。
我们先看打开dvb设备的方法:
dvb.c : void dvb_open(void) 打开设备,为读取数据作准备。
sprintf( psz_tmp, "/dev/dvb/adapter%d/frontend%d", i_adapter, i_fenum );
if( (i_frontend = open(psz_tmp, O_RDWR | O_NONBLOCK)) < 0 )
sprintf( psz_tmp, "/dev/dvb/adapter%d/dvr%d", i_adapter, i_fenum );
if( (i_dvr = open(psz_tmp, O_RDONLY | O_NONBLOCK)) < 0 )
if ( ioctl( i_dvr, DMX_SET_BUFFER_SIZE, i_dvr_buffer_size ) < 0 )
ev_io_init(&dvr_watcher, DVRRead, i_dvr, EV_READ);
ev_io_start(event_loop, &dvr_watcher);
读取dvb数据:
dvb.c :
static void DVRRead(struct ev_loop *loop, struct ev_io *w, int revents)
for ( i = 0; i < MAX_READ_ONCE; i++ )
{
if ( (*pp_current) == NULL ) *pp_current = block_New();
p_iov[i].iov_base = (*pp_current)->p_ts;
p_iov[i].iov_len = TS_SIZE;
pp_current = &(*pp_current)->p_next;
}
if ( (i_len = readv(i_dvr, p_iov, MAX_READ_ONCE)) < 0 )
创建udp接收套接字:
udp.c :
void udp_Open( void )
if ( (i_handle = socket( i_family, SOCK_DGRAM, IPPROTO_UDP )) < 0 )
setsockopt( i_handle, SOL_SOCKET, SO_REUSEADDR, (void *) &i, sizeof( i ) );
/* Increase the receive buffer size to 1/2MB (8Mb/s during 1/2s) to avoid
* packet loss caused by scheduling problems */
i = 0x80000;
setsockopt( i_handle, SOL_SOCKET, SO_RCVBUF, (void *) &i, sizeof( i ) );
if ( bind( i_handle, p_bind_ai->ai_addr, p_bind_ai->ai_addrlen ) < 0 )
if ( setsockopt( i_handle, IPPROTO_IP, IP_ADD_MEMBERSHIP,
(char *)&imr, sizeof(struct ip_mreq) ) == -1 )
ev_io_init(&udp_watcher, udp_Read, i_handle, EV_READ);
ev_io_start(event_loop, &udp_watcher);
收取udp数据:
udp.c :
static void udp_Read(struct ev_loop *loop, struct ev_io *w, int revents)
struct sockaddr_storage addr;
struct msghdr mh = {
.msg_name = &addr,
.msg_namelen = sizeof(addr),
.msg_iov = NULL,
.msg_iovlen = 0,
.msg_control = NULL,
.msg_controllen = 0,
.msg_flags = 0
};
recvmsg( i_handle, &mh, MSG_DONTWAIT | MSG_PEEK )
TS流数据从dvb设备或者网络收到之后下一步即是进行分流处理。分流依据的是配置文件中指定的以一组pid构成一路节目,而这路节目输出至一个udp单播地址或者组播地址。
demux.c :
static void demux_Handle( block_t *p_ts )
if ( !ts_validate( p_ts->p_ts ) )
if ( i_pid != PADDING_PID && p_pid->i_last_cc != -1
&& !ts_check_duplicate( i_cc, p_pid->i_last_cc )
&& ts_check_discontinuity( i_cc, p_pid->i_last_cc ) )
if ( ts_get_transporterror( p_ts->p_ts ) )
uint8_t *p_payload = ts_payload( p_ts->p_ts );
if ( p_payload + 3 < p_ts->p_ts + TS_SIZE )
i_pes_status = pes_validate( p_payload ) ? 1 : 0;
2.4 数据输出
数据被处理完成即要被输出至网络。
output.c : void output_Put( output_t *p_output, block_t *p_block )
if ( ts_has_adaptation( p_block->p_ts )
&& ts_get_adaptation( p_block->p_ts )
&& tsaf_has_pcr( p_block->p_ts ) )
p_packet->i_dts = p_block->i_dts;
p_packet = output_PacketNew( p_output );
i_next_send = p_packet->i_dts + p_output->config.i_output_latency;
ev_timer_stop(event_loop, &output_watcher);
ev_timer_set(&output_watcher, (i_next_send - i_wallclock) / 1000000., 0);
ev_timer_start(event_loop, &output_watcher);
Dvblast命令行程序也已经具备了比较强大的功能,可以满足一般需求。如果有定制化的需求也可以通过修改dvblast工程源码的方式去解决。本人曾经遇到过的一个问题就是dvb设备驱动未遵循标准方式对dev设备命名,因此只能在2.2.1节所示的源码中进行修改。还有一种业务场景是收下来的TS流要保存为符合HLS协议的切片文件,这个也可以在2.4节所示代码处加入切片存储并生成m3u8的功能。
Dvblast通过读取一个配置文件的方式来驱动工作,配置文件有一个特殊的格式,现在对其内容进行说明。
既然大名都叫dvblast,那必定要先从dvb设备读取来说了。
命令行:-a 3
释义:就是打开设备/dev/dvb/adapter3
Dvblast只支持读取udp数据,当然,看过源码之后也是可以改成读取tcp数据或者其他协议的,无非是对应用层的协议进行一次解析而已。
命令行: -D [[:]@][:][/]*
示例:-D 239.255.0.2:1234/udp/ifindex=1
数据输出是dvblast配置中比较复杂的部分,所以它强制从一个磁盘文件读取输出参数。我以一个最常用的示例来说明主要配置项。
配置:[:][@[:]][/]* [,]*
示例: 239.255.0.1:1234 1 0 0,128,1234,1235
说明:各项元素之间以若干空格分开,其中第一项元素是输出的地址和端口,示例中是一个组播地址;第二项元素是是否启用标志,可能有时候配置项过期但又不想从配置文件删除,就可以设置此项为0;第三项是将对应SID的数据输出;第四项则是归属于同一路的pid集合,这需要管理员事先就清楚哪些pid是属于同一路的。