参考ISO13818-1,EN-300743,从TS流解析开始,结合MS6328的code将DVB Subtitle的解析过程简单梳理了一下。
一、TS PAT PMT PES (参考ISO13818-1)
- 由TS包中取出PAT表
从搜台开始pat 解析:
MW_DVB_SI_PSI_Parser ::_ScanStar()下m_pPatParser->Start(PAT_SCAN_TIMEOUT)
这里mapi_demux_section_filter *m_pSecFilter = m_pDemux->AllocateSectionFilter()返回一个demux section filter 指针
这个section指定了:
stSecConfig.u16PID = PID_PAT;
stSecConfig.au8MatchByte[0] = TID_PAS; //table id
指定pat相应的section
(//这里的section,用来分段传输psi中各个table信息的结构,是去掉了TS 包头以后的TS payload 数据)
SI以及MPEG-2 PSI tables在被插入TS 包之前会被分段成一个或多个section。Section是可变长度的,每个table的section被限制在1024 bytes,除了EIT 表的section为4096bytes。Refer to《en_300468》
这里是取TS中PSI数据,区别于PES数据)
注意:有效负载(payload):指包中跟在包头后面的bytes。比如:TS 包的有效负载包括一个PES_packet_header 和PES_packtet_data_bytes; 或者,一个指针域(pointer_field)和 PSI_sections 或者 私有数据。
一个PES 包的有效负载只包含PES_packtet_data_bytes。另外,TS 包的包头和调整区域(adaptation fields)不是有效负载。
…
m_pSecFilter->Init(stSecConfig)
m_pSecFilter->Start()
…
解析完pat,得到有用的信息,兵存储于Pat Table结构中:
mapi_si_PAT_parser::Parse() { PatTbl.ServiceIDInfo[u16ServiceIndex].u16ServiceID PatTbl.ServiceIDInfo[u16ServiceIndex].u16PmtPID u16ServiceIndex++; PatTbl.u16ServiceCount = u16ServiceIndex }
- 由PAT得到对应的PMT
在_PatReady()下会将对应的从pat解析时得到的PmtPID存储如pCurProg 信息中 (serviceIID,OriginalNetworkId,TransportStream_id,NetworkId),PMT 会根据curprog.u16PmtPID来解析。
_PMT_Monitor()下sm_pCurPmtParser->Start(xx, PMTPID, xx)
mapi_si_PMT_parser::Start()
stSecConfig.u16PID = u16PMTPid;
stSecConfig.au8MatchByte[0] = TID_PMS; //table id
底层就会解析出相应PID的section数据,存储到pmt table中。
每次pmt的刷新,会将信息存到curprog中,相应的信息会传递给subtitle service结构体,MW_DTV_Subtitle::m_Services,通过队列存储起来:u16Sub_Pid,u8subtitling_type。
- 根据PMT得出subtitle相关信息
有了PMT信息,就可以得出subtitle 的信息了,所以当打开一个相应的subtitle时,就可以通过索引找到存储起来的subtitle信息。
….
if(m_pDVBSubtitle_decoder->Connect() == TRUE) { m_pDVBSubtitle_decoder->SetCA_PageID(u16composition_page_id,u16ancillary_page_id); //composion_pageid ancillary_pageid m_pDVBSubtitle_decoder->Open(u16PID, bFileIn); m_enOpenType = EN_OPEN_SUBTITLE_DVB; bSuccess = TRUE; }
注:ancillary_page_id只对于一个subtitle流中复合多个subtitle services(eg,不同语言,或者normal/hearing impaired)才有用,否则与composition_page_id一样
根据subtitle的PID(u16PID)就开始了subtitle的解析。
二、Subtitle 解析
- 下面是PES的处理
MW_DVBSubtitleDecoder::Connect()
这里创建subtitle的render相关,并且会create 一个线程(__sbt_thread())来处理subittle的数据。
__sbt_thread():
MW_DVBSubtitleDecoder::__process_PES()
<1>.在MW_DTV_Subtite::Init()下会有m_pDVBSubtitle_decoder->AllocatePESFilter()会分配一个PESFilter,注意与上面的SectionFilter区别。
<2>. m_pPES_Filter->Open(u16PID) 这里会传递需要解析的PID给相应的Pesfilter,然后再根据pes包头取得数据存处于buffer(一个完整的PES packet)。
这里 Stream_id必须是0xBD,表示private_stream_1(具体可查看iso13818-1 Table2-18)
<3>__compute_PTS(buffer)
取得PTS_DTS_flags(2bit)(pu8bff[7] & 0xc0 == 0x80 或者 0xc0)
当此值为‘10’,表示PTS fields存在与PES 包头
当此值为‘11’,表示PTS fields和DTS fields都存在与PES 包头
当此值为‘00’,表示PTS或者DTS fields都不会存在与PES包头
‘01’是禁止的
PTS 33bit,分3部分,DTS 也是33bit,同样是由3部分组成,这里我们只取PTS,用不到DTS(具体可以参考iso13818-1关于PES packet结构的spec)
if(((pu8bff[7] & 0xc0) == 0x80) || ((pu8bff[7] & 0xc0) == 0xc0)) { u32tmp = pu8bff[9]; u32PTSLow = (u32tmp & 0x06); u32PTSLow <<= 29; u32tmp = pu8bff[10]; u32tmp <<= 22; u32PTSLow |= u32tmp; u32tmp = pu8bff[11] & 0xfe; u32tmp <<= 14; u32PTSLow |= u32tmp; u32tmp = pu8bff[12]; u32tmp <<= 7; u32PTSLow |= u32tmp; u32tmp = pu8bff[13] & 0xfe; u32tmp >>= 1; u32PTSLow |= u32tmp; }
(当然,还有PTS的特殊情况,这里就用到了system time clock(STC)的概念
理想情况下:如果解码器的时钟频率与编码器匹配,那么解码速率就与编码时一致。这时候任何正确的 PCR(由TS取得)就可以用作解码器的STC。但实际情况是不可能匹配的。一种方法是通过phase-locked loop(PLL)来强制解码器的clock与收到的数据流一致。)
然后会将取得的一个完整的PES:buffer数据以及PTS压入一个队列DataQueue,_push(pu8Buf, u16Len, u32PTS)
mapi_dvb_subtitle_decoder::Main(MAPI_U32 u32STC)
…
pSubtitle->__GetCurrentPTS(&u32STC);
mapi_interface::Get_mapi_dvb_subtitle()->Main(u32STC);
…
这个main函数就是处理DataQueue中的数据
void mapi_dvb_subtitle_decoder::Main(MAPI_U32 u32STC) { if(DataQueue_CheckPTS(u32STC)) //查找DataQueue中所有PTS,若有一小于当前STC,则满足条件 { QueueElement qeCur; if(_pop(&qeCur) == MAPI_TRUE) { __decode(&qeCur); _freeQueueElement(&qeCur); } } }
到这里就进入了subtitle的decode部分了
- __decode()
Mapi_dvb_subtitle_decoder.cpp (参考EN_300743)
从code中mapi_dvb_subtitle_decoder::__decode(QueueElement *qe){}可见解析流程:
__decode_PES_Header()->__decode_Segment()->__render_Display()->__resetObject()
(这段可参考pes_packet结构)
<1>pes prefix,并取得pes length
m_sbt_stream.SkipLen(4);
m_sbt_stream.GetWord(&m_u16_PES_length);
指针向后移动6个字节
<2>__decode_PES_Header()
这里取得pes headerlen,做一些校验,并根据headerlen 将PES header剔除
找出PES header的界限?
<3> __decode_CheckStreamID()
这里开始就是剔除了PES header的数据,开始解析subtitling segment数据
当是DVB subtitle stream时,PES_packet_data_bytes的结构如下:
end_of_PES_data_field_marker 这个占不占空间呢? 貌似不占
data_identifier 必须是0x20,subtitle_steam_id必须是0x00
<4> __decode_Segment()
这里取得page_id,segment_length,
还记得上面open subtitle之前
m_pDVBSubtitle_decoder->SetCA_PageID(u16composition_page_id,u16ancillary_page_id)这个接口吗?
这是从PMT中取得的compositon_page_id,ancillary_page_id.
在这里需要判断此segment的pageid是否等于上面之一,否则就跳过这个segment
if((u16_page_id != m_u16CPID) && (u16_page_id != m_u16APID)) { // skip this segment, not whole PES m_sbt_stream.SkipLen(u16_segment_length); m_u16_PES_length -= u16_segment_length; }
Segment_length 是指segment_data_field()的数据bytes,其具体的数据结构是由segment_type指定
Segment的第二个字节表示segment_type,,指定了这个segment 数据的类型
1) case 0x14 display definition segment
这里是假定显示的宽度是720pixels 显示的高度是576lines
Segment 结构如下:
定义了display definition的一些相关 具体可参考(en300743 7.2.1)
2)case 0x10 page composition segment
这个类型的sgement结构如下:
__decode_Page():
取得regioncount = u16_segment_length / 6 (page segment的长度是6个字节)
通过循环,取得所有有效region的region id,以及该region显示的位置坐标(h,v)
存处于一个m_page的结构体中
page_time_out:表示page instance存在的时间。主要是为了防止错误发生时(比如page instance其time out没有被重新定义或没有删除)导致page instance 一直显示
page state:
0x00:normal case,用于page update,仅显page instance有改变的subtitle elements
此case下说明region_id已经存在,需要判断是否已经存在。当然一开始page state不会是nomal case的情况。????
0x01:acquisition point 用于page refresh ,显示下一个page instance要用到的所有的subtitle 元素
这里主要执行__resetObject() 重置object的索引,version等
0x10 :mode change,用于new page,显示new page需要的所有subtitle 元素
这里需reset region,reset object,CLUT索引,screen buffer等
3)case 0x11 region compositon segment
__decode_Region()
Region_depth指定了pixel depth
CLUT_id 指定了用于这个region的CLUTS
Object_id 指定了这个region内显示的object(通过循环取得所有的object)
Region_fill_flag定义为1后,说明该region需要用背景色填充,填充的颜色由region_8-bit_pixel-code,region_4-bit_pixel-code,region_2-bit_pixel-code指定;
region_8-bit_pixel-code:指定8bit的CLUT表的入口,但仅仅当该region_depth是8 bit的时候才有效;
region_4-bit_pixel-code:指定4 bit的CLUT表入口。当region depth是4bit可用;或者region depth是8bit,且region_level_of_compatibility是4bit的时候才有用;
region_2-bit_pixel-code:指定2bit的CLUT表入口。单region depth为2bit,或者region_level_of_compatibility为2bit的时候可用。
region_level_of_compatibility:表示解码器满足的最小CLUT类型(2bit、4bit、8bit)
如果page state是normal case的情况,说明region_id并没有改变(与已经解析出的region信息作比较),无需重新创建region
(但是这里如果新来的region width/height 与已经解析出来的region的width/height不一样,需要替换并重新创建这个region)
如果 page state是mode change或acquisition point,需要重新解析region_id,重新创建region
这里解出的region_width region_height,不能超过上面DDS 段解除的display width,display height
根据取出的region_id,region_width,region_heigth,pixel_depth,来创建__createRegionBuffer(),最终通过DirectFB 来绘制一个window
__createRegionBuffer()最终是通过IDirectFB*做了两件事CreateWindow、SetPallete。
也就是说一个新的region,就对应了一个新的IDirectFBWindow*
大致情况如下:
If (可以显示了) //这个应该是表示这种surface是最终要显示出来的。 一般只需要创建一个可以显示的surface就可以了
{
- 通过IDirectFB*取得IDirectFBDisplayLayer*,存为m_pDFBLayer;这样就可以用过m_pDFBLayer做进一步的操作了。
- m_pDFBLayer-> SetCooperativeLevel (m_pDFBLayer, DLSCL_ADMINISTRATIVE)//设置合作级别。这里应该是表示管理权限,枚举出window,并管理它们
- m_pDFBLayer->GetConfiguration(m_pDFBLayer,&config)//获得当前layer的配置信息,如width、height、pixelformat等(这里的width、height来自与region_width、region_height)
当然获取config是为了重新设置之,设置好后,需调用m_pDFBLayer->SetConfiguration()
- m_pDFBLayer->GetScreen(m_pDFBLayer,&p_screen);//取得screen指针
然后再根据screen的方法p_screen->GetSize(p_screen,&screenwidth,&screenheight),取得screen的宽和高。
获取screen的宽和高,主要是为了判断与当前video的宽和高的关系(MS6328这段代码没看懂?),然后通过
m_pDFBLayer-> SetScreenLocation (m_pDFBLayer,x,y,width,height)//来设置screen的位置
- m_pDFBLayer->Create(m_pDFBLayer,&desc,&m_pDFBWindow)//由layer来创建window,其中desc是一些配置信息
- m_pDFBWindow->GetSurface(m_pDFBWindow,&m_Windows[u8WinID].pSurface)//
每次创建一个window,都获取一个surface,并存储到window数组中,所以一个region就对应一个window的数组元素,也对应一个surface
- m_pDFBWindow->SetOpacity(m_pDFBWindow->,0/0xff)
0表示全透明,隐藏window,0xff表示不透明
}
else //这种方式创建的surface不可以显示到屏幕上
{
//也就是创建一个region时,同时创建一个surface(由超级接口IDirectFB插口创建),并存储入数组里(宽高像素格式pitch等)
直接由IDirectFB创建一个surface,存储到数组中
m_pDFB->CreateSurface(m_pDFB, &surDesc,&m_Windows[u8WinID].pSurface)
surDesc指定了一些配置信息
在官网的例子中,看到surDesc.caps = DSCAPS_PRIMARY | DSCAPS_FLIPPING;,设置为primary surface,应该同上面是一样的功能???
pixelformat:使用的是DSPF_LUT8
width:region_width
height:region_heigth
}
在取得surface之后,设置surface的调色板
- m_Windows[u8WinID].pSurface->GetPalette(m_Windows[u8WinID].pSurface,&palette)//这里为什么无需SetPalette?Surface创建后就能直接得到GetPalette??
- 接着palette->SetEntries(palette,DFBColor*color,num_colors,offset)
接着palette->Release(palette)//这个接口在官网的接口定义里没有找到?
Region segment解析完后得到了一系列索引信息,object索引,CLUT索引等;具体的内容
和颜色还需进一步解析相关的segment。
我们知道surface代表一块预留的内存,用来保存像素数据,那么像素数据从哪里来呢?
在解析object segment的时候,会将像素数据存储到surface对应的内存地址中。(具体见解析object)
4)case 0x12 CLUT definition segment
循环解析出所有的cult信息,用YCbCrT 颜色系统存储,需转换成ARGB
按公式将YCbCrT的信息转换成ARGB相关,T直接转换为A(透明度),Y=0表示透明
(略写)
5)case 0x13 object data segment
object_coidng_method ==‘0x00’ 编码方式是像素编码,这种是graphical object(一般是这种情况)
object_coding_method==’0x01’ 编码方式是字符编码,这种是字符格式的字幕
一个object 是由top field 和bottom field交错组成。
每一个对象,用于top filed 的pixel-data sub-block 和用于 bottom field的 pixel-data sub-block必须存储在相同的object_data_segment内。 如果bottom field没有数据,则取top field的数据作为bottom field的数据。
对于pixel-data sub_block的结构可参考 EN_300743 7.2.5,详细定义了pixel data的编码规则。
在new region的时候创建了surface,那么这个时候可以获取到surface对应的存储像素数据的内存地址,往该内存地址写入数据即可。
m_Windows[u8WinID].pSurface->Lock(m_Windows[u8WinID].pSurface, DSLF_WRITE, (void **) pu8Buffer, &u32pitch) //DSLF_WIRTE,表示该地址可写的权限、
pu8Buffer是数据指针,u32pitch是行距
写入像素数据的起始地址是这么计算的:pu8Buffer+ object_horizontal_position(region中取得)+ object_vertical_position*pitch
这里涉及到图像压缩算法 run length encoding(行程编码):将一扫描行中的颜色值相同的相邻像素用一个计算值和那些像素值的颜色值来代替。比如aaabccccccddeee,用3a1b6c2d3e来代替
比如
run_lenght_3-10指示了2—bit_pixel_code重复的次数。。
m_Windows[u8WinID].pSurface->Unlock(m_Windows[u8WinID].pSurface);//记得执行这步
到这一步,surface的内存中已经有像素数据了,但是并不会立即显示出来。
1.surface是单buffer的(这个怎么判断呢?)
将每个region对应的surface,用StretchBlit()输入到OFFSCREEN特殊的surface:
m_Windows[u8DstWinID].pSurface->StretchBlit(DstSurface,OFFSCREEN , pSrcDFBRect, pDstDFBRect)
当然要使用StretchBlit之前,还需先用SetBlittingFlags(sur*,DSBLIT_NOFX)、SetRenderOptions(sur*,DSRO_SMOOTH_UPSCALE)这两个接口设置一下。
- surface是double buffer
pSurface->GetCapabilities(m_Windows[u8DstWinID].pSurface, &surfaceCaps)//取得该surface的相关属性
if(DSCAPS_DOUBLE == (surfaceCaps & DSCAPS_DOUBLE))//来判断是否是double buffer
pSurface->Flip(m_Windows[u8DstWinID].pSurface, NULL, (DFBSurfaceFlipFlags)DSFLIP_NONE)//用Flip将数据强行送到屏幕显示
最后别忘了pSurface->ReleaseSource(pSurface)//把该surface可能的引用全部释放掉
尤其在用了StretchBlit()方法后
PS:
MS6328创建了两个特殊的surface:
- CreateWindow(ONSCREEN_GWIN_ID, &rect, MAPI_TRUE, m_pCallbackArgument);//表示这个surface是可以显示出来的,具体的步骤可见上述region 解析部分
- CreateWindow(OFFSCREEN_GWIN_ID, &rect, MAPI_FALSE, m_pCallbackArgument);//这个surface是不可以显示出来的作为转存数据使用。(上面每次new region创建的surface也是无需显示的,可显示的只要一个即可)
6)0x80 end of display set segment
提供一种模糊的指示给解码器,表示display set已经传输完成。虽然没有什么实际用处,但这个segment是一定需要的。
上面解析出来的相关数据,通过DirectFB就可以显示出subtitle了。
三、 DVB subtitle system Overview
CLUT color look up table 颜色索引表,CLUT_id存在于每个region中,用于将object中的颜色伪码转换成实际显示的颜色。
objects 的使用和配置是由region compositon segment定义的
regions的使用和配置 是由 page compositon segment定义的,page compositon segment 包含了一系列供显示的region,每个都有特定的位置。此外,region可以被定义而不被使用。
同一时间可以显示不止一个region。
一个subtitle stream可以传输多个subtitle service(eg,不同语言,或者normal/hearing impaired)。
在这种情况下,每个特定的subtitle service是用page-id来区分的。
在同一个subtitle stream中不同subtitle service之间可以共享subtitling data。
当然更多的情况是用不同的PID区分不同的subtitle service。
Figure 2: Example of two ways of conveying dual language subtitles (one using shared data)
图表2的情况是没有ancillary page的,看来只有同一个stream中有多个service时才会用到此辅助页面。
注意:
一个单独sbutitle stream是不能同时有高清(包含display_definition_segment)和标清的sbutitle service.
在一个subtitle stream中,一个page id 是分配给每个segment。Segments可以只包含特定的service 的数据,也可以包含用于共享service的数据。故,一个stream中的数据段最多有两种paige id values;
一种page id value 指定的数据段用于特定数据 (composioning page id) 这是必选
另一种paage id value 指定共享数据段 (ancillary page id),可选(包含log数据)。 可参考上图。
Display set
一个subtitle service中具有相同PTS的整套segments就成为 display set。 Display set中的最后一个段必须跟着一个 end_of-display-set 段,表示没有更多的与此PTS相关的subtitle 数据了。
Segment type
1.display definition segment 可选,用户定义display size 针对高清subtitle
2.page composiont segment 定义接下来显示的pages,每个page包含的regions,time-out信息 和页面状态
3.region composition segment 一个region里有一个或多个objects,但只有一个CLUT(由CLUT-id指定);带有region的配置,region的属性:水平,垂直的size,背景色,
4.CLUT definition segment
5.object data segment 有两种类型的object:绘图objects和文本objects
6.end of display set segment
段数据中的 page id value应当与 subtitle descriptor中的compositon_page_id 或者ancillary_page_id相等。
Page compostions 是不能由复合subtitle services所共享的
每个page composition 段中的page id 应当与compositon_page_id相等
总的来说,subtitle system中的数据层次结构如下:
- Transport Stream (TS)
- Transport packets with the same PID
- PES packets,with PTSs providing timing information
- Subtitle service
- Segments signalled by the compositon page id and optionally the ancillary page id
- Where appropriate,a display definition segment
Subtitle data,containing information on page composition,region composition,CLUTs,objects and end of display set.