Decin's blogs

one dream


  • Home

  • About

  • Tags

  • Categories

  • Archives

  • Search

iOS交叉编译环境搭建(ubuntu16.04 server)

Posted on 2019-04-12 | Edited on 2019-06-18 | In system

介绍

在linux环境下搭建ios编译环境, 主要需要llvm, clang, cctools-port和ios_sdk, 以及一些编译必备工具
以下是在Ubuntu 16.04.6 server下实践llvm-4.0.1, cfe-4.0.1(clang), iPhoneOS10.0.sdk

所需工具

  • Ubuntu 16.04.6服务器(64位)(http://ftp.sjtu.edu.cn/ubuntu-cd/16.04.6/ubuntu-16.04.6-desktop-amd64.iso)
  • llvm和clang(4.0.1)(http://releases.llvm.org/download.html#5.0.0)
  • openssl(https://github.com/openssl/openssl)
  • automake(http://ftp.gnu.org/gnu/automake/)
  • cmake(https://cmake.org/download/)
  • autogen(http://ftp.gnu.org/gnu/autogen/)
  • libtool(http://ftp.gnu.org/gnu/libtool/)
  • autoconf(http://ftp.gnu.org/gnu/autoconf/)
  • libssl-dev(https://pkgs.org/download/libssl-dev)
  • cctools-port(https://github.com/tpoechtrager/cctools-port)
  • iOS-SDK(http://resources.airnativeextensions.com/ios/)

尝试通过apt安装这些必备工具
$ sudo apt update
$ sudo apt install git gcc cmake libssl-dev libtool autoconf automake clang-4.0

安装失败则通过手动安装, wget下载源码后安装, 也可以通过下载.deb文件dpkg -i安装

注意:如果源无法访问, 请考虑翻墙, 或者更换apt安装源(类似于:https://www.cnblogs.com/gabin/p/6519352.html)

安装完clang看看位置如果不是usr/bin
执行下面命令制作软链接,只为保险不是必需的….
$ sudo ln -s /usr/bin/clang-4.0 /usr/bin/clang
$ sudo ln -s /usr/bin/clang++-4.0 /usr/bin/clang++

安装llvm和clang

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#下载llvm源码 
$ wget http://llvm.org/releases/4.0.1/llvm-4.0.1.src.tar.xz
$ tar xf llvm-4.0.1.src.tar.xz
$ mv llvm-4.0.1.src llvm

#下载clang源码
$ cd llvm/tools
$ wget http://llvm.org/releases/4.0.1/cfe-4.0.1.src.tar.xz
$ tar xf cfe-4.0.1.src.tar.xz
$ mv cfe-4.0.1.src clang
$ cd ../..

#下载clang-tools-extra源码 可选
$ cd llvm/tools/clang/tools
$ wget http://llvm.org/releases/4.0.1/clang-tools-extra-4.0.1.src.tar.xz
$ tar xf clang-tools-extra-4.0.1.src.tar.xz
$ mv clang-tools-extra-4.0.1.src extra
$ cd ../../../..

#下载compiler-rt源码 可选
$ cd llvm/projects
$ wget http://llvm.org/releases/4.0.1/compiler-rt-4.0.1.src.tar.xz
$ tar xf compiler-rt-4.0.1.src.tar.xz
$ mv compiler-rt-4.0.1.src compiler-rt
$ cd ../..


$ mkdir llvmbuild
$ cd llvmbuild


#正常套路安装
#设置配置
#–prefix=directory — 设置llvm编译的安装路径(default/usr/local).
#–enable-optimized — 是否选择优化(defaultis NO),yes是指安装一个Release版本.
#–enable-assertions — 是否断言检查(default is YES).
$ ../llvm/configure --enable-optimized --enable-targets=host-only --prefix=/usr/bin
#如果不能通过configure按照提示使用cmake $ cmake -G "Unix Makefiles" -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DCLANG_DEFAULT_CXX_STDLIB=libc++ -DCMAKE_BUILD_TYPE="Release" ../llvm

#构建
$ cmake ../llvm make

#安装
$ sudo make install

打包ios sdk

在Mac中打包ios sdk, tmp为临时目录, 可以在任意位置, 此步骤参考https://github.com/tpoechtrager/cctools-port/tree/master/usage_examples/ios_toolchain

1
2
3
4
5
6
7
8
9
$ SDK=$(ls -l /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs | grep " -> iPhoneOS.sdk" | head -n1 | awk '{print $9}')
$ cp -r /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk /tmp/$SDK 1>/dev/null
$ cp -r /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1 /tmp/$SDK/usr/include/c++ 1>/dev/null

$ pushd /tmp
$ tar -cvzf $SDK.tar.gz $SDK
$ rm -rf $SDK
$ mv $SDK.tar.gz ~ #此时最好放在~目录下, 由于cctools-port中usage_examples/ios_toolchain/build.sh可能会出问题
$ popd

还可以在http://resources.airnativeextensions.com/ios/下载ios sdk, 但是和上面的略有不同, 不包括拷贝的/c++/v1头文件

制作工具链

参考(https://github.com/tpoechtrager/cctools-port/tree/master/usage_examples/ios_toolchain)

iOS arm64工具链

1
2
$ cd cctools-port
$ IPHONEOS_DEPLOYMENT_TARGET=9.0 usage_examples/ios_toolchain/build.sh ~/iPhoneOS10.0.sdk.tar.gz arm64

制作工具链成功后会提示 all done
将生成的工具链移到 /usr/local/ 目录并更名为ios-arm64

1
$ sudo mv usage_examples/ios_toolchain/target /usr/local/ios-arm64

使用rename命令重命名前缀以与armv7区分开来把arm-前缀改为aarch64-前缀(苹果的arm64叫aarch64)

1
$ rename 's/arm-/aarch64-/' /usr/local/ios-arm64/bin/*

将库文件拷贝一份,放进公共库/usr/lib

1
2
3
$ sudo cp /usr/local/ios-arm64/lib/libtapi.so /usr/lib
最后将工具链的bin目录加入PATH,方便调用
$ export PATH=$PATH:/usr/local/ios-arm64/bin

iOS armv7工具链

1
$ IPHONEOS_DEPLOYMENT_TARGET=9.0 usage_examples/ios_toolchain/build.sh ~/iPhoneOS10.0.sdk.tar.gz armv7

制作工具链成功后会提示 all done
将生成的工具链移到 /usr/local/ 目录并更名为ios-armv7

1
2
3
4
5
$ sudo mv usage_examples/ios_toolchain/target /usr/local/ios-armv7
将库文件拷贝一份,放进公共库/usr/lib
$ sudo cp /usr/local/ios-armv7/lib/libtapi.so /usr/lib
最后将工具链的bin目录加入PATH,方便调用
$ export PATH=$PATH:/usr/local/ios-armv7/bin

也可以合并工具链, 读者可以自行尝试

到这里已经可以通过工具链打包库给ios端用了
类似通过clang和ar打包静态库在真机上测试, 下面是一个大概例子

1
2
$ arm-apple-darwin11-clang -c ts_scan_program.c ts_scanner.c ../queue/ns_variable_queue.c -I ../queue/ -I ../libdvbpsi-1.3.2/src/ -I ../libdvbpsi-1.3.2/src/tables/ -I ../libdvbpsi-1.3.2/src/descriptors
$ arm-apple-darwin11-ar -cr libtsscan_m.a ./*.o

还可以模仿苹果的xcodebuild, 直接在linux环境下编译xcode项目, https://github.com/facebook/xcbuild

参考:
https://blog.csdn.net/fyf786452470/article/details/79160670
https://www.jianshu.com/p/d99995927527

遇到的问题

  • * building apple-libtapi *
    …
    Scanning dependencies of target install-libtapi
    …
    gcc: error trying to exec ‘cc1obj’: execvp: No such file or directory
    Makefile:558: recipe for target ‘libobjc_la-NSBlocks.lo’ failed
    make[1]: * [libobjc_la-NSBlocks.lo] Error 1
    make[1]: *
    Waiting for unfinished jobs….
    libtool: compile: gcc -DPACKAGE_NAME="cctools" -DPACKAGE_TARNAME="cctools" -DPACKAGE_VERSION="895" “-DPACKAGE_STRING="cctools 895"“ -DPACKAGE_BUGREPORT="t.poechtrager@gmail.com" -DPACKAGE_URL="" -DSTDC_HEADERS=1 -DHAVE_SYS_TYPES_H=1 -DHAVE_SYS_STAT_H=1 -DHAVE_STDLIB_H=1 -DHAVE_STRING_H=1 -DHAVE_MEMORY_H=1 -DHAVE_STRINGS_H=1 -DHAVE_INTTYPES_H=1 -DHAVE_STDINT_H=1 -DHAVE_UNISTD_H=1 -DHAVE_DLFCN_H=1 -DLT_OBJDIR=".libs/" -DEMULATED_HOST_CPU_TYPE=12 -DEMULATED_HOST_CPU_SUBTYPE=0 -D__STDC_LIMIT_MACROS=1 -D__STDC_CONSTANT_MACROS=1 -DHAVE_EXECINFO_H=1 -I. -I../../../../../cctools/libobjc2 -DTYPE_DEPENDENT_DISPATCH -DGNUSTEP -D__OBJC_RUNTIME_INTERNAL__=1 -D_XOPEN_SOURCE=500 -D__BSD_VISIBLE=1 -D_DEFAULT_SOURCE=1 -DNO_SELECTOR_MISMATCH_WARNINGS -isystem /home/shifttime/ios/build_en/cctools-port/usage_examples/ios_toolchain/target/include -std=gnu99 -Wall -O3 -c ../../../../../cctools/libobjc2/Protocol2.m -fPIC -DPIC -o .libs/libobjc_la-Protocol2.o
    gcc: error trying to exec ‘cc1obj’: execvp: No such file or directory
    Makefile:561: recipe for target ‘libobjc_la-Protocol2.lo’ failed
    make[1]: * [libobjc_la-Protocol2.lo] Error 1
    make[1]: Leaving directory ‘/home/shifttime/ios/build_en/cctools-port/usage_examples/ios_toolchain/tmp/cctools/libobjc2’
    Makefile:414: recipe for target ‘all-recursive’ failed
    make: *
    [all-recursive] Error 1
    * checking toolchain *
    cannot invoke compiler!

  • linux terminal 输入命令有历史记录 $ history

  • ~/.bash_history HISTSIZE=1000(default)

  • Ubuntu缺省情况下,并没有提供C/C++的编译环境,因此还需要手动安装。但是如果单独安装gcc以及g++比较麻烦,幸运的是,Ubuntu提供了一个build-essential软件包。查看该软件包的依赖关系:
    apt-cache depends build-essential
    gcc: error trying to exec ‘cc1obj’: execvp: No such file or directory
    apt Install gobjc (http://security.ubuntu.com/ubuntu/pool/universe/g/gcc-5/) https://www.kubuntuforums.net/showthread.php/35193-gcc-4-2-error-trying-to-exec-cc1obj-execvp-No-such-file-or-directory

  • $ apt install git
    出现如下错误
    E: Unmet dependencies. Try ‘apt-get -f install’ with no packages…
    原因:
    在新版的Ubuntu下,例如Ubuntu 14.04或者16.04一般是不会出现broken dependencies,或者出现unmet dependencies, 但是如果我们使用dpkg强制安装了某些deb包,或者在build-dep的是否手动更改了某些Packages的文件和版本时, 那么在再次使用apt-get install或者build-dep来安装library和packages的时就很可能出现问题.
    按照提示输入 $ apt-get -f install

  • LLVM最新的4.0.1版本已经不能通过configure/make来编译安装了,它只支持CMake编译。
    $ ../llvm/configure –enable-optimized –enable-targets=host-only CC=gcc CXX=g++
    需要通过cmake编译, 例如:
    $ cmake -G “Unix Makefiles” -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DCLANG_DEFAULT_CXX_STDLIB=libc++ -DCMAKE_BUILD_TYPE=”Release” ../llvm
    参考:https://typecodes.com/linux/cmakellvmclang4.html

PSI/SI解析

Posted on 2019-04-08 | Edited on 2019-06-18 | In audio/video

psi/si介绍

TS流基本结构

如大家所知在开发过程中, ts流基本结构如下:

g48276d5fbhd05e658960347bb156472.png

e7884014298ac7d6cc726abe6467cff4.png

具体的包字段:

fd8276daf9d05e658960347bb1139685.png

一般我们在解析节目的时候,
会从PAT->PMT/NIT->SDT得到节目的这个节目名称/供应商和存储音视频数据的包的PID等信息,
再根据该节目的音视频PID去过滤码流, 获得音视频包,
最后获得音视频压缩数据供解码器解码播放.具体结构如下:

8bc3a1b442d3f39fff75e9ab95eb28f7.png

一般我们称获得PSI/SI信息为扫描节目,
平常最常用的是基本节目信息和电子节目指南(EPG, 由EIT和TOT/TDT等组成)

PSI(节目特定信息, Program Specific Information)了解

节目特定信息(PSI)包括 ITU-T H.222.0 建议书| ISO/IEC 13818-1
正式数据和专用数据两部分,以使节目的多路分解能够由解码器完成。节目由一个或多个基本流组成,每个流有一个
PID 签标。节目、基本流
或者它们之中的若干部分可以加扰供有条件访问使用。然而,节目特定信息应不加扰。

传输流中,节目特定信息分成为 6 种表构造,如表 2-28
所示。尽管这些结构可以看作为简单的表,但
它们将被分割成若干分段并插入到传输流包中,一些分段具有预定的
PID,其余的分段具有用户自选的 PID。

a34c8cd647fb46b37f18b3fb315e4593.png

ITU-T H.222.0 建议书| ISO/IEC 13818-1 规定的 PSI
表应分割成一个或多个分段
在传输包内承载。每个分 段为一个句法构造,用于提供把每个
ITU-T H.222.0 建议书| ISO/IEC 13818-1 规定的 PSI 表映射成传输流包。

与 ITU-T H.222.0 建议书| ISO/IEC 13818-1 规定的 PSI
表一道,承载专用数据表也是可能的。传输流包内承载专用信息的方法不由本规范定义。同样的构造可以用于提供承载
ITU-T H.222.0 建议书| ISO/IEC
13818-1规定的PSI表,以致映射该专用数据的句法与映射ITU-T H.222.0建议书| ISO/IEC
13818-1规定的 PSI
表所使用的句法相同。出于此目的,规定专用分段。若承载专用数据的传输流包与承载节目映射表的传
输流包具有相同的 PID 值(如节目相关表中所标识的),则应使用 private_section
子句法和语义。 private_data_bytes 中承载的数据可以加扰。然而,private_section
的其他字段应无任何必要加扰。此 private_section
允许所传输的数据具有最小的结构。当不使用此结构时,传输流包内专用数据的映射不由本
建议书|国际标准规定。

分段长度可变。分段的起始端由传输流包有效载荷内的 pointer_field
指示。该字段的句法在表 2-29 中 指定。

自适应字段可在承载 PSI 分段的传输流包中出现。

传输流内,值为 0xFF 的包填充字节仅可在分段最后字节之后承载 PSI 和/或
private_sections 的传输流
包的有效载荷中发现。在此情况中,直至传输流包结束的所有字节也应是值为 0xFF
的填充字节。这些字节 可以被解码器丢弃。在这样的情况中,具有相同 PID
值的下一个传输流包的有效载荷必须随着值为 0x00 的 pointer_field
开始指示自此以后的下一个分段立即起始。

每个传输流必须包含一个或多个具有 PID 值 0x0000
的传输流包。这些传输流包一起应包含完整的节目
相关表,提供传输流内所有节目的完整目录一览。最近传输的具有
current_next_indicator 设置为值‘1’的
该表的版本必定总是适用于传输流中的当前数据。传输流内承载的节目中的任何变化必然在具有
PID 值 0x0000 的传输流包中承载的节目相关表的更新版本中描述。这些分段都应使用
table _id 值 0x00。仅具有此 table_id 值的分段才被容许在具有 PID 值 0x0000
的传输流包内存在。对于 PAT 的新版本生效而言,具有新 版本号并具有
current_next_indicator 设置为‘1’的所有分段(如 last_section_number
中所指示的)必须退 出 T-STD 中规定的 Bsys(参阅
2.4.2)。当所需要的该分段最后字节完成此表退出 Bsys 时,PAT 方始生效。

每当传输流内一个或多个基本流被加扰时,应传输包含完整有条件访问表的具有 PID 值
0x0001 的传 输流包,该有条件访问表包括同加扰流有关的 CA_descriptors
。传输的所有传输流包应一起组成有条件访 问表的一个完整版本。最近传输的具有
current_next_indicator 设置为值‘1’的该表的版本必定总是适用于
传输流中的当前数据。使得现存表格无效或不完整的加扰中的任何变化必须在该有条件访问表的更新版本
中描述。这些分段都将使用 table_id 值 0x01。仅具有此 table_id
值的分段才被容许在具有 PID 值 0x0001 的传输流包内存在。对于 CAT
的新版本生效而言,具有新版本号并具有 current_next_indicator 设置为‘1’
的所有分段(如 last_section_number 中所指示的)必须退出
Bsys。当所需要的该分段的最后字节完成此表 退出Bsys时,CAT方始生效。

每个传输流必须包含具有 PID 值的一个或多个传输流包,它们在节目相关表内签标为包含
TS_program_map_section 的传输流包。节目相关表中所罗列的每个节目必须在唯一的 TS
节目映射分段中描
述。任何一个节目必须在传输流自身内被完全定义。在适当的节目映射表分段中具备相关
elementary_PID 字
段的专用数据应是该节目的一部分。其他专用数据可在未列入节目映射表分段的传输流中存在。最近传输
的具有 current_next_indicator 设置为值‘1’的 TS_program_map_section
的版本必定总是适用于传输流内的
当前数据。传输流内承载的任何节目的定义中的任何变化必须在具有 PID
值的传输流包中所承载的节目映 射表相应分段的更新版本中描述,该 PID
值标识为那个特定节目的 program_map_PID 。承载给定 TS_program_map_section
的所有传输流包必须具有相同的 PID 值。节目延续存在期间,包括其所有相关事
件,program_map_PID 都应保持不变。节目限定应不跨越多于一个的
TS_program_map_section。当具有新 version_number 并具有 current_next_indicator
值设置为‘1’的那个分段的最后字节退出 Bsys 时,TS 节目映
射分段的新的版本方始生效。

具有 table_id 值 0x02 的分段应包含节目映射表信息。这样的分段可在具有不同 PID
值的传输流包中承载。

网络信息表为任选的并且其内容为专用。若存在,它将在具有相同 PID
值的传输流包内承载,该 PID
称之为网络PID。network_PID值由用户定义,并且只要存在,应在保留program_number
0x0000考虑的节 目相关表中出现。若网络信息表存在,它必须取一个或多个
private_sections 的形式。

42 ITU-T H.222.0建议书 (05/2006)

ISO/IEC 13818-1:2007(C) PSI 表规定的 ITU-T H.222.0 建议书| ISO/IEC 13818-1
分段中,最大字节数为 1 024 字节。private_section

中最大字节数为 4 096 字节。

传输流描述表为任选的。若存在,传输流描述在具有 PID 值 0x0002
的传输流包内承载,如表 2-28 所
指示的,并应适用于整个传输流。传输流描述的分段必须使用 table_id 值 0x03,如表
2-31 所指示的,并且 其内容受限于表 2-45
中指定的描述符。当要求的该分段的最后字节完成此表退出 Bsys 时,
TS_description_section 方始生效。

在起始码、同步字节或 PSI
数据中的其他比特模式出现的地方不存在任何限制,无论是本建议书|国际
标准数据流还是专用数据流。

节目相关表 (PAT)

7f0a184a2100d5af127519d1df25db62.png

2f80e6a1c3e6b5d4458003fb05b90811.png

936be18f27aa39be8af00d890e9d14ab.png

每个传输流必须包含一个完整有效的节目相关表。节目相关表给出 program_number
与携带那个节目定 义的传输流包的 PID(PMT_PID)之间的对应。PAT
映射成传输流包之前,它可以被分割成最多 255 个分 段。每个分段承载该全部 PAT
的一个部分。为在误差条件下极小化数据丢失,此分割或许是合适的。即,
包丢失或比特差错可以局限于该 PAT
的较小分段,这样允许其他的分段仍旧被接收和正确解码。若所有 PAT
信息放置在一个分段,则例如,引发 table_id 中一个变化的比特差错将引起整个 PAT
的丢失。然而,只要 该分段未扩展超出 1024 字节的最大长度限制,这仍旧是容许的。

节目 0(零)被保留并用于指定网络 PID。这是指向承载网络信息表的传输流包的指针。

传输节目相关表无须加密。

节目映射表 (PMT)

节目映射表提供节目编号与构成它的节目元之间的映射。此表在具有一个或多个专门选择的
PID 值的 传输流包中存在。这些传输流包可以包含由 table_id
字段所规定的其他专用构造。可能具有这样的 TS PMT 分段,它涉及具有公共 PID
值的传输流包中所承载的不同节目。

本建议书|国际标准要求一个最小的节目标识:节目编号、PCRPID、流类型以及节目元
PID。节目或基 本流的另外信息可通过使用描述符()句法来传送。参阅 C.8.6。

专用数据也可以在作为承载 TS 节目映射表分段所指明的传输流包中发送。通过使用
private_section() 来实现这一过程。在 private_section()中,该应用决定
version_number 和 current_next_indicator 是否代表单一
分段的这些字段的值或者它们是否适用于作为一个较大专用表的一个部分的诸多分段。

b3ed6947f981c953b8fb951be40c4c54.png

注 1 — 传输包含节目映射表的传输流包不加密。

注 2 —
在TS_program_map_section()字段内承载的专用描述符中的事件上传输信息是可能的。

C.8.3 有条件访问表 (CAT)

有条件访问(CA)表给出一个或多个 CA 系统、它们的 EMM
流和与它们有关的任何特定参数之间的 关系。

注 — 包含EMM和CA参数的传输流包的(专用)内容只要存在,一般将加密(加扰)。

908f1095764203f858498741e692c033.png

C.8.4 网络信息表 (NIT)

NIT 的内容为专用且不由本建议书|国际标准指定。一般而言,它将包括的具有
transport_stream_id、信
道频率、卫星转发器个数、调制解调特性等的用户自选业务的映射。

C.8.5 Private_section()

Private_sections()能够以两种基本格式出现:短版本(仅包含直至并包括 section_length
字段在内的所有 字段)或长版本(直至并包括 last_section_number
字段在内的所有字段均存在,并且专用数据字节之后 CRC_32 字段也存在)。

Private_section()能够在被称为 PMT_PID 的 PID 中出现,或在唯一包含
private_sections()的具有其他 PID 值的传输流包中出现,该 PID 值包括分配给 NIT 的
PID。若承载专用分段()的传输流包的 PID 被标识为承 载专用分段的 PID(stream_type
赋值为 0x05),则仅 private_sections 可在那个 PID 值的传输流包中出现。该
分段可以是长版本类型或短版本类型。

C.8.6 描述符

本建议书|国际标准中存在若干规定的标准描述符。许多额外的专用描述符也可定义。所有描述符均有
一个共同的格式:{标记,长度,数据}。任何专门定义的描述符必须遵从此格式。这些专用描述符的数据
部分专门定义。

当一个描述符(CA_descriptor())在TS
PMT分段中出现时,它用于指示与节目元有关的ECM数据的 位置(传输包的 PID 值)。当在
CA 分段中出现时,它涉及 EMM 数据的位置。

为了扩展有效的 private_descriptors 的个数,可能使用以下机制:可以专门定义专用
descriptor_tag 作为 一个复合描述符来构造。这需要专门规定一个更深层的
sub_descriptor 作为该专用描述符的专用数据字节的
首字段。所描述的结构分别在下两个表中指示。

9fcd06ce5cc0686e64d08580f298512c.png

ce91628a91556c6c84bb37d333d6ad50.png

SI(节目特定信息, Service Information)了解

GB/T 17975.1-2000 中的业务信息被称为节目特定信息(PSI)。PSI
数据提供了使能够接收机自动配置的信息,用于对复用流中的不同节目流进行解复用和解码。
PSI 信息由四种类型表组成。每类表按段传输。

1)节目关联表(PAT):

 针对复用的每一路业务,PAT 提供了相应的节目映射表(PMT)的位置(传输流(TS)包
的包标识符(PID)的值),同时还提供网络信息表(NIT)的位置。

2)条件接收表(CAT):
条件接收表提供了在复用流中条件接收系统的有关信息。这些信息属于专用数据(未在本

标准中定义),并依赖于条件接收系统。当有 EMM 时,它还包括了 EMM 流的位置。

3)节目映射表(PMT):

 节目映射表标识并指示了组成每路业务的流的位置,及每路业务的节目时钟参考(PCR)
字段的位置。

4)网络信息表(NIT): 本标准定义的 NIT 表的位置符合 GB/T 17975.1-2000
规范,但数据格式已超出了 GB/T

17975.1-2000 的范围,这是为了提供更多的有关物理网络的信息。本标准中还定义了网

络信息表的语法及语义。除了 PSI
信息,还需要为用户提供有关业务和事件的识别信息。本标准定义了这些数据的编码。PSI

中的 PAT、CAT、PMT
只提供了它所在的复用流(现行符复用流)的信息,在本标准中,业务信息还提供
了其他复用流中的业务和事件信息。这些数据由以下九个表构成:

1)业务群关联表(BAT):
业务群关联表提供了业务群相关的信息,给出了业务群的名称以及每个业务群中的业务列

表。

2)业务描述表(SDT):

 业务描述表包含了描述系统中业务的数据,例如业务名称、业务提供者等。

3)事件信息表(EIT):

 事件信息表包含了与事件或节目相关的数据,例如事件名称、起始时间、持续时间等。

 不同的描述符用于不同类型的事件信息的传输,例如不同的业务类型。

4)运行状态表(RST):

 运行状态表给出了事件的状态(运行/非运行)。运行状态表更新这些信息,允许自动适
时切换事件。

5)时间和日期表(TDT):
时间和日期表给出了与当前的时间和日期相关的信息。由于这些信息频繁更新,所以需要

使用一个单独的表。

6)时间偏移表(TOT):

 时间偏移表给出了与当前的时间、日期和本地时间偏移相关的信息。由于时间信息频繁更
新,所以需要使用一个单独的表。

7)填充表(ST): 填充表用于使现有的段无效,例如在一个传输系统的边界。

8)选择信息表(SIT):
选择信息表仅用于码流片段(例如,记录的一段码流)中,它包含了描述该码流片段的业

务信息的概要数据。 9)间断信息表(DIT):

 间断信息表仅用于码流片段(例如,记录的一段码流)中,它将插入到码流片段业务信息
间断的地方。

当应用这些标识符时,允许灵活地组织这些表,并允许将来兼容性扩展。

总体结构如下:

067f7ca1c80e6ef941904da2b8494786.png

本标准中的业务信息(SI)表与 MPEG-2 中的 PSI
表,都被分成为一个或若干个段表示,然后插入

到TS包中。第 4 部分中所列的表是概念性的,在 IRD 中无需以特定的形式重新生成。除了
EIT 表外,业务信息

表在传送过程中不能被加扰,但如果需要,EIT 表可以加扰(见
5.1.5)。段是一种用来把在所有的 MPEG-2 表和本标准中规定的 SI 表映射成 TS
包的语法结构。这些业务信

息语法结构符合 GB/T 17975.1-2000 定义的专用段语法结构。 5.1.1 说明

段的长度是可变的。除 EIT 表外,每个表中的段限长为 1024 字节,但 EIT 中的段限长
4096 字节。 每一个段由以下元素的组合唯一标识:a) 表标识符(table_id):

- 表标识符标识段所属的表;- 一些表标识符已分别被 ISO 和 ETSI
定义。表标识符的其它值可以由用户根据特定目的自行分

配。表标识符值的列表见表 2。b) 表标识符扩展(table_id_extentsion):

- 表标识符扩展用于标识子表;

- 子表的解释见 5.2。 c) 段号(section_number):

-
段号字段用于解码器将特定子表的段以原始顺序重新组合。本标准建议段按顺序传输,除非某
些子表的段需要比其它的段更频繁地传输,例如出于随机存取的考虑;

- 在本标准中指定的各种业务信息表,段编号也适用于子表。 d)
版本号(version_number):

-
当本标准中规定的业务信息所描述的传输流特征发生变化时(例如:新事件开始,给定业务的
组成的基本流发生变化),应发送更新了的业务信息数据。新版本的业务信息以传送一子表为
标志,它与前子表具有相同的标识符,但版本号改为下一值;

- 本标准中规定的业务信息表,版本号适用于一个子表的所有段。 e)
当前后续指示符(current_next_indicator):

- 每一段都要标以“当前”有效或“后续”有效。它使得新的 SI
版本可以在传输流特征发生变
化之前传输,让解码器能够为变化做准备。然而,一个段的下一个版本的提前传输不是必需的,
但如果被传输,它将成为该段的下一个正确版本。

5.1.2 段到传输流(TS)包的映射

段可直接映射到 TS 包中。段可能起始于 TS
包有效负载的起始处,但这并不是必需的,因为 TS 包

的有效负载的第一个段的起始位置是由 pointer_field 字段指定的。一个 TS
包内决不允许存在多余一
个的pointer_field字段,其余段的起始位置均可从第一个段及其后各段的长度中计算出来,这是因为
语法规定一个传输码流的段之间不能有空隙。

在任一 PID 值的 TS
包中,一个段必须在下一个段允许开始之前结束,否则就无法识别数据属于哪
个段标题。若一个段在 TS
包的末尾前结束了,但又不便打开另一个段,则提供一种填充机制来填满剩
余空间。该机制对包中剩下的每个字节均填充为 0xFF。这样 table_id 就不允许取值为
0xFF,以免与填 充相混淆。一旦一个段的末尾出现了字节 0xFF,该 TS
包的剩余字节必然都被填充为 0xFF,从而允许解 码器丢弃 TS
包的剩余部分。填充也可用一般的 adaptation_field 机制实现。

段在传输流中的映射机制及功能,2.4.4 节,附录 C 及 GB/T 17975.1-2000
有更详尽的描述。

5.1.3 PID及表标识符字段编码

下表列出了用于传送业务信息段的 TS 包的 PID 值。

4493ee33a982de03bd1526677e945582.png

强化section的理解

PSI和SI中都有section的概念,刚开始学习MPEG-2 TS流解析时,看ISO/IEC
13818-1的文档上面的PAT,PMT表:program_association_section()和TS_program_map_section()时,很容易就以为可以直接从188字节的TS
packet中取数据填到各个字段中,网上也可以搜到这样类似的程序:

void ParsePat(psi_pat *p_pat, uint8_t *p_packet)

{

// declare variables and initial

…

// get payload_unit_start_indicator

b_pusi = p_packet[1] & 0x40;

b_adaptation = p_packet[3] & 0x20;

// calculate the skip to skip to payload field

if(b_adaptation)

{

skip = 5 + p_packet[4];

}

else

{

skip = 4;

}

// skip pointer_field

if(b_pusi)

{

skip = skip + p_packet[skip] + 1;

}

// now skip to payload field

p_payload = p_packet + skip;

// now parse table

p_pat->table_id = p_payload[0];

…

}

这样在解析PAT和PMT表时往往也能得到正确的答案,这是因为PAT,PMT表数据量比较小的缘故。其实在TS流中还有一个重要的概念——section。文档中program_association_section()和TS_program_map_section()指的就是PAT
section和PMT
section。它们中都有一个字段:section_length,文档告诉我们这个字段值的是此字段之后section的字节数,除了EIT
section的最大字节数是4096外,其他section最大字节数是1024字节。 而TS
packet最大只有188字节,因此这里需要考虑section到packet的映射问题,基本上存在三种情况:

1、section对应一个packet

2、section太长,在几个连续的packet中

3、在一个packet的负载中结束上一个section之后马上开始了一个新的section。

c35e21b8afa95122ecf0349446d8f881.png

下面将以EIT表举例

EIT表

事件信息表 EIT(见下表)按时间顺序提供每一个业务所包含的事件的信息。按照不同
table_id,有四类 EIT:

1) 现行传输流,当前/后续事件信息= table_id = “0x4E”;

2) 其它传输流,当前/后续事件信息= table_id = “0x4F”;

3) 现行传输流,事件时间表信息= table_id = “0x50” 至 “0x5F”;

4) 其它传输流,事件时间表信息= table_id = “0x60” 至 “0x6F”。

现行传输流的所有 EIT 子表都有相同的 transport_stream_id 和 original_network_id。
除准视频点播(NVOD)业务之外,当前/后续表中只包含在现行传输流或其他传输流中指定业务的

当前事件和按时间顺序排列的后续事件的信息,因为 NVOD
业务可能包含两个以上的事件描述。无论是
对现行传输流还是其他传输流,事件时间表都包含了以时间表的形式出现的事件列表,这些事件包括下
一个事件之后的一些事件。EIT 时间表是可选的,事件信息按时间顺序排列。

按照下表语法,EIT 表被切分成事件信息段。任何构成 EIT 表的段,都要由 PID 为
0x0012 的 TS 包 传输。

2e7f0b3c6f8e245de2dcbff709108ae6.png

baae4abf67289ef5247c08d78941c9a4.png

对于当前事件有如下规定:

1.同一时刻最多只有一个当前事件。

2.当存在一个当前事件时,该事件应该被描述在EIT Present/Following的section0中。

3.当前事件中的running_status应当被给出。

4.在同一时刻,最多有一个following event.

5.如果following event存在,该事件应当在EIT Present/Following的section1中。

6.如果following event不存在,则传输一个section1为空的EIT Present/Following。

7.Following event的running_status应当给出。

事件的持续时间和EIT持续事件一样,必须包含事件被设置为“not
running”或者“pausing”。事件的开始时间和EIT
start_time一样,应当是整个事件的开始事件,而不是从pause恢复后的时间。

注意:一个事件的开始时间加上它的持续时间可能比following
event的开始时间要小。换句话说,允许事件之间有间隔。在这种情况下,following
event被看作是间隔后的事件,这个事件应当编在EIT Present/Following的secting1中。

注意:开始时间和持续时间都是预定的,一些广播服务提供商可能会更新这些信息,而另外一些则更愿意保持开始时间不变。例如为了避免名为“8点新闻”的事件被误解,把信息中的开始事件从8:01:23改为8:00:00。

EIT Schedule结构:

一、EIT Schedule结构遵从如下规则:

1、EIT/Schedule分配了16个table_id,0x50-0x5F给当前TS,0x60-0x6F给其它TS,这些id按照时间顺序排列;

2、子表下的256个section被分为32段(segment),每8个section一个字段(segment).
segment #1、从section0到7、segment #2、从section8到15等等

3、每段包含三个小时内开始的事件信息;

4、段内事件信息按照事件排列;

5、如果一个段(segment)有n节(section),而n<8,这个信息必须放在段前n个节中,还要显示指明最后一节的位置:S0+n-1(S0是段中第一节),这个值在EIT的segment_last_section_number中。例如,第二段只有两节,那么segment_last_section_number包含值8+2-1=9;

6、如果段中有节的话,段的segment_last_section_number应当有值s0+7;

7、完全空的段通过空节(不包含loop
over事件)表示,段的vsegment_last_section_number值为s0+0

8、段中事件的安排遵从一个时间t0.

T0是通过时间坐标(Universal Time Coordinated(UTC))的“last midnight”。

举个例子:UTC-6的下午5点,就是UTC-0的下午11点,即从:last
midnight”算起23小时。因此对于UTC-6,t0就是前一天的下午6点;

table_id
0x50(对于其它TS是0x60)的第0段,包含从午夜(UTC时间)到“今天”02:59:59(UTC时间)(三个小时)的事件信息。第一段包含从03:00:00到05:59:59(UTC时间)的事件信息。依此类推,这就意味着第一个子表包含“今天”UTC午夜时间算起4天的信息;

9、last_section_number用来指明子表的结束位置;

10、last_table_id用来指明整个EIT/Schedule结构的结束位置;

11、与过去相关的段可以用空段代替,参见7规则;

12、EIT/Schedule包含的时间定义中的running_status应当设为“为定义”即0x00;

13、EIT/Schedule表不适用于NVOD涉及的服务,因为这些服务带有未定义开始时间的事件;

二、EIT加密

EIT Schedule表格可以被加密,为了与条件接入相联系,必须分配一个service_id(=MPEG-2
program_number)来描述加密的EIT Schedule
Tables,这个service_id在PSI中。EIT在PMT中定义,service_id看成由一个private
steam组成的各种电视节目(The EIT is identified in the Program Map
Table(PMT)section for this service_id as a programme consisting of one private
steam),PMT包含一个或者多个CA_descriptor来验证相关的CA码流。为达到这个目的,在DVB应用程序中service_id的值0xFFFF被保留。

在EIT
present/following表中,每一事件都用一个event_id来标识它,每一个事件的顺序关系(当前正在发生的事件/后续发生的事件)就由EIT
present/following来描述。

那么如何来识别当前正在发生的事情和后续发生的事情呢?那是通过event_id来标识的,如图5所示。图中event_id=0x49表示当前正在发生的事件;event_id=0x4A表示后续发生的事件。那么在当前事件完成进入后续事件时,此时的后续事件变成当前事件,后续事件将由一个新的事件代替。这一变化是使用version_number来加以描述的。

例如:

当前播出 19:00-19:30 新闻联播 event_id=0x49;

后续播出 19:31-20:00 动画片 event_id=0x4A,此version_number=0

设新的后续 21:01-21:45 曲艺节目。当新闻联播完成后,则变化为:

当前播出 19:31-20:00 动画片 event_id=0x49;

后续播出 21:01-21:45 曲艺节目 event_id=0x4A,此version_number=1

libdvbpsi 源代码学习

认识libdvbpsi

libdvbpsi是vlc中的一个解码库。它能解码或解析出所有的节目专用信息(PSI)以及MPEG2
TS流或DVB流中的描述符(descriptor)。

目前能解析的PSI/SI表包括( BAT,CAT,EIT,NIT,PAT,PMT,SDT,SIS,TOT,TDT).

BAT:Bouquet Association Table 业务关联表

CAT:Conditional Access Table 条件接入表

EIT:Event Information Table 事件信息表(EPG)

NIT: Network Information Table 网络信息表

PAT: Program Association Table 节目关联表

PMT: Program Map Table 节目映射表

SDT: Service Description Table 业务描述表

libdvbpsi主页: http://www.videolan.org/developers/libdvbpsi.html

其中涉及到的DVB规范如下:

系统: ISO/IEC 13818-1

视频编码: ISO/IEC 13818-2

音频编码。 ISO/IEC 13818-3

DVB官网:http://www.dvb.org/

此处笔者使用的是version 1.3.2.

架构分析

libdvbpsi 1.3.2源码目录结构如下:

.

├── demux.c 解复用器

├── descriptor.c 各种描述符数据的抽象

├── descriptors 各种描述符的解析

│ ├── dr.h

│ ├── dr_02.c

│ ├── dr_02.h

│ ├── …

├── dvbpsi.c 抽象成DVB/PSI decoders,封装出接口,供应用层调用。

├── psi.c psi section structure

└── tables 各种psi子表解析的具体实现

├── eit.c eit表结构

├── pat.c pat表结构

├── pmt.c pmt表结构

每个解码器被划分成两个实体:即the PSI decoder和the specific
decoder。之所以如此划分的原因是,每个psi表的section都有相同的格式。

解码器结构如图1所示:

图1:解码器结构

PSI解码器:主要任务就是获取应用层提供的ts流数据包(
STB则是根据底层的解码器芯片获取ts流),并将完整的psi
section(段)发送给专用的解码器解析。对于不连续的ts流,PSI解码器也必须稳定可靠的工作,并将ts流交给专用的解码器处理。

专用解码器(specific decoder):主要任务就是根据psi解码器提供的psi
sections,重建完整的表(PSI/SI)并将
他们返回给应用层处理(STB通常是存入相应的database),同时还要根据psi
decoder的指示去检查ts的完整性(作CRC校验)。如果不完整,则返回错误。

PSI
decoder可理解为对每个具体专用解码器相同特征或行为的抽象,也就是抽象出一个类:decoder,而每个具体的decoder则是具体的类的对象或实例,所以要具体实现。用C语言的解释就是抽象出decoder的接口(Interface),要使用哪个解码器则传入不同的回调函数/函数指针(callback)。

(注:从这里可以看到,函数指针是实现多态的手段,而多态就是隔离变化的秘诀)

dvbpsi_packet_push函数分析

在1.4节中, section到packet的映射问题, 存在三种情况可以在libdvbpsi源码的bool
dvbpsi_packet_push(dvbpsi_t *p_dvbpsi, uint8_t*
p_data)函数中找到。明白了这三种情况,再对照函数的注释,应该就不难看懂这个函数了。

/\*******************************************************************

  • * dvbpsi_PushPacket *

  • ****************************************************************** *

  • * Injection of a TS packet into a PSI decoder. *

  • ****************************************************************/ *

bool dvbpsi_packet_push(dvbpsi_t \p_dvbpsi, uint8_t* p_data)*

*{ *

  • uint8_t i_expected_counter; /* Expected continuity counter */ *

  • dvbpsi_psi_section_t* p_section; /* Current section */ *

  • uint8_t* p_payload_pos; /* Where in the TS packet */ *

  • uint8_t* p_new_pos = NULL; /* Beginning of the new section, *

  • updated to NULL when the new *

  • section is handled */ *

  • int i_available; /* Byte count available in the *

  • packet */ *

  • /* 本次放进来的packet的负载字节数 */ *

  • /* TS start code */ *

  • if(p_data[0] != 0x47) *

  • { *

  • DVBPSI_ERROR(“PSI decoder”, “not a TS packet”); *

  • return; *

  • } *

  • /* Continuity check */ *

  • i_expected_counter = (h_dvbpsi->i_continuity_counter + 1) & 0xf; *

  • h_dvbpsi->i_continuity_counter = p_data[3] & 0xf; *

  • if(i_expected_counter == ((h_dvbpsi->i_continuity_counter + 1) & 0xf) *

  • && !h_dvbpsi->b_discontinuity) *

  • { *

  • DVBPSI_ERROR_ARG(“PSI decoder”, *

  • “TS duplicate (received %d, expected %d) for PID %d”, *

  • h_dvbpsi->i_continuity_counter, i_expected_counter, *

  • ((uint16_t)(p_data[1] & 0x1f) << 8) | p_data[2]); *

  • return; *

  • } *

  • if(i_expected_counter != h_dvbpsi->i_continuity_counter) *

  • { *

  • DVBPSI_ERROR_ARG(“PSI decoder”, *

  • “TS discontinuity (received %d, expected %d) for PID %d”, *

  • h_dvbpsi->i_continuity_counter, i_expected_counter, *

  • ((uint16_t)(p_data[1] & 0x1f) << 8) | p_data[2]); *

  • h_dvbpsi->b_discontinuity = 1; *

  • if(h_dvbpsi->p_current_section) *

  • { *

  • dvbpsi_DeletePSISections(h_dvbpsi->p_current_section); *

  • h_dvbpsi->p_current_section = NULL; *

  • } *

  • } *

  • /* Return if no payload in the TS packet */ *

  • if(!(p_data[3] & 0x10)) *

  • { *

  • return; *

  • } *

  • /* Skip the adaptation_field if present */ *

  • if(p_data[3] & 0x20) *

  • p_payload_pos = p_data + 5 + p_data[4]; *

  • else *

  • p_payload_pos = p_data + 4; *

  • /* Unit start -> skip the pointer_field and a new section begins */ *

  • /* payload_unit_start_indicator 为1时不仅有一个pointer_field指针,更意味着一个新的分段section开始 */ *

  • if(p_data[1] & 0x40) *

  • { *

  • p_new_pos = p_payload_pos + *p_payload_pos + 1; *

  • p_payload_pos += 1; *

  • } *

  • /* 用来判断上个分段是否结束,为NULL表示上个分段结束了并已被收集 */ *

  • p_section = h_dvbpsi->p_current_section; *

  • /* If the psi decoder needs a begginning of section and a new section *

  • begins in the packet then initialize the dvbpsi_psi_section_t structure */ *

  • /* 上个分段结束,一般就开始新的分段(payload_unit_start_indicator为1),否则错误直接返回 */ *

  • if(p_section == NULL) *

  • { *

  • if(p_new_pos) *

  • { *

  • /* Allocation of the structure */ *

  • h_dvbpsi->p_current_section *

  • = p_section *

  • = dvbpsi_NewPSISection(h_dvbpsi->i_section_max_size); *

  • /* Update the position in the packet */ *

  • p_payload_pos = p_new_pos; *

  • /* New section is being handled */ *

  • p_new_pos = NULL; *

  • /* Just need the header to know how long is the section */ *

  • /* 开始新的分段,先读分段的header,到section_length刚好3字节,主要是要先读section_length用来确定此分段有多少字节。 */ *

  • h_dvbpsi->i_need = 3; *

  • h_dvbpsi->b_complete_header = 0; *

  • } *

  • else *

  • { *

  • /* No new section => return */ *

  • return; *

  • } *

  • } *

  • /* Remaining bytes in the payload */ *

  • /* p_payload_pos = p_data + offset, 其中offset是section包头的长度 */ *

  • /* 所以,i_available = 188 - offset */ *

  • i_available = 188 + p_data - p_payload_pos; *

  • while(i_available > 0) *

  • { *

  • /* h_dvbpsi->i_need 用来记录该分段还差多少个字节才结束分段 */ *

  • /* 此包的负载足够填充当前分段,需要考虑: *

  • * 1、如果已经填充过分段的header,则将此包的负载追加到分段数据的末尾即结束此分段,并调用h_dvbpsi->pf_callback收集当前section以完成一个子表 *

  • * 这个时候还要判断,如果此包的负载还有剩余,并且不是填充字段(0xff)则意味着直接开始了一个新的分段。 *

  • * 2、如果还没有填充过分段的header,则分析段的header,主要是section_length字段用来更新h_dvbpsi->i_need */ *

  • if(i_available >= h_dvbpsi->i_need) *

  • { *

  • /* There are enough bytes in this packet to complete the *

  • header/section */ *

  • memcpy(p_section->p_payload_end, p_payload_pos, h_dvbpsi->i_need); *

  • p_payload_pos += h_dvbpsi->i_need; *

  • p_section->p_payload_end += h_dvbpsi->i_need; *

  • i_available -= h_dvbpsi->i_need; *

  • if(!h_dvbpsi->b_complete_header) *

  • { *

  • /* Header is complete */ *

  • h_dvbpsi->b_complete_header = 1; *

  • /* Compute p_section->i_length and update h_dvbpsi->i_need */ *

  • /* 分段已经读了3个字节也即读到section_length字段,恰恰还需要section_length个字节 */ *

  • h_dvbpsi->i_need = p_section->i_length *

  • = ((uint16_t)(p_section->p_data[1] & 0xf)) << 8 *

  • | p_section->p_data[2]; *

  • /* Check that the section isn’t too long */ *

  • if(h_dvbpsi->i_need > h_dvbpsi->i_section_max_size - 3) *

  • { *

  • DVBPSI_ERROR(“PSI decoder”, “PSI section too long”); *

  • dvbpsi_DeletePSISections(p_section); *

  • h_dvbpsi->p_current_section = NULL; *

  • /* If there is a new section not being handled then go forward *

  • in the packet */ *

  • if(p_new_pos) *

  • { *

  • h_dvbpsi->p_current_section *

  • = p_section *

  • = dvbpsi_NewPSISection(h_dvbpsi->i_section_max_size); *

  • p_payload_pos = p_new_pos; *

  • p_new_pos = NULL; *

  • h_dvbpsi->i_need = 3; *

  • h_dvbpsi->b_complete_header = 0; *

  • i_available = 188 + p_data - p_payload_pos; *

  • } *

  • else *

  • { *

  • i_available = 0; *

  • } *

  • } *

  • } *

  • else /* 分段准备结束并调用回调函数收集section */ *

  • { *

  • /* PSI section is complete */ *

  • p_section->b_syntax_indicator = p_section->p_data[1] & 0x80; *

  • p_section->b_private_indicator = p_section->p_data[1] & 0x40; *

  • /* Update the end of the payload if CRC_32 is present */ *

  • if(p_section->b_syntax_indicator) *

  • p_section->p_payload_end -= 4; *

  • /* 主要是CRC校验 */ *

  • if(p_section->p_data[0] != 0x72 && dvbpsi_ValidPSISection(p_section)) *

  • { *

  • /* PSI section is valid */ *

  • p_section->i_table_id = p_section->p_data[0]; *

  • if(p_section->b_syntax_indicator) *

  • { *

  • p_section->i_extension = (p_section->p_data[3] << 8) *

  • | p_section->p_data[4]; *

  • p_section->i_version = (p_section->p_data[5] & 0x3e) >> 1; *

  • p_section->b_current_next = p_section->p_data[5] & 0x1; *

  • p_section->i_number = p_section->p_data[6]; *

  • p_section->i_last_number = p_section->p_data[7]; *

  • p_section->p_payload_start = p_section->p_data + 8; *

  • } *

  • else *

  • { *

  • p_section->i_extension = 0; *

  • p_section->i_version = 0; *

  • p_section->b_current_next = 1; *

  • p_section->i_number = 0; *

  • p_section->i_last_number = 0; *

  • p_section->p_payload_start = p_section->p_data + 3; *

  • } *

  • /* 收集当前section */ *

  • h_dvbpsi->pf_callback(h_dvbpsi, p_section); *

  • /* 结束当前section */ *

  • h_dvbpsi->p_current_section = NULL; *

  • } *

  • else *

  • { *

  • /* PSI section isn’t valid => trash it */ *

  • dvbpsi_DeletePSISections(p_section); *

  • h_dvbpsi->p_current_section = NULL; *

  • } *

  • /* A TS packet may contain any number of sections, only the first *

  • * new one is flagged by the pointer_field. If the next payload *

  • * byte isn’t 0xff then a new section starts. */ *

  • /* 负载还有剩余,并且不是填充字段,则新的分段开始了,这种情况就是一个packet中有2个分段。 */ *

  • if(p_new_pos == NULL && i_available && *p_payload_pos != 0xff) *

  • p_new_pos = p_payload_pos; *

  • /* If there is a new section not being handled then go forward *

  • in the packet */ *

  • if(p_new_pos) *

  • { *

  • h_dvbpsi->p_current_section *

  • = p_section *

  • = dvbpsi_NewPSISection(h_dvbpsi->i_section_max_size); *

  • p_payload_pos = p_new_pos; *

  • p_new_pos = NULL; *

  • h_dvbpsi->i_need = 3; *

  • h_dvbpsi->b_complete_header = 0; *

  • i_available = 188 + p_data - p_payload_pos; *

  • } *

  • else *

  • { *

  • i_available = 0; *

  • } *

  • } *

  • } *

  • /* 此包的负载不够填充当前分段,则直接把此包的负载追加到当前分段的分段数据末尾上。 *

  • * 这种情况就是一个分段在多个packet中。 */ *

  • else *

  • { *

  • /* There aren’t enough bytes in this packet to complete the *

  • header/section */ *

  • memcpy(p_section->p_payload_end, p_payload_pos, i_available); *

  • p_section->p_payload_end += i_available; *

  • h_dvbpsi->i_need -= i_available; *

  • i_available = 0; *

  • } *

  • } *

*} *

引自———————

作者:xujf_12

来源:CSDN

原文:https://blog.csdn.net/xujf_12/article/details/5804169

PSI decocder详细分析

由上一篇
libdvbpsi源码分析(二)main函数,简单分析了demo程序中main函数的执行流程。现在将对具体的PSI表作详细解析。主要是对main函数中的libdvbpsi_init和dvbpsi_new以及相关的dvbpsi_pat_attach作相关分析。

1.创建PSI decoders

ts_stream_t \libdvbpsi_init(int debug, ts_stream_log_cb pf_log, void
*cb_data)*

将每张table表的数据各自抽象成specific
decoder(ts_xxx_t),最后封装成通用的universal decoder即:ts_stream_t

struct ts_stream_t

{

/\ Program Association Table */*

ts_pat_t pat;

/\ Program Map Table */*

int i_pmt;

ts_pmt_t \pmt;*

/\ Conditional Access Table */*

ts_cat_t cat;

#ifdef TS_USE_SCTE_SIS

/\ Splice Information Section */*

ts_sis_t sis;

#endif

ts_rst_t rst;

/\ Subbtables */*

ts_sdt_t sdt;

ts_eit_t eit;

ts_tdt_t tdt;

/\ pid */*

ts_pid_t pid[8192];

enum dvbpsi_msg_level level;

/\ statistics */*

uint64_t i_packets;

uint64_t i_null_packets;

uint64_t i_lost_bytes;

/\ logging */*

ts_stream_log_cb pf_log;

void \cb_data;*

};

其中,libdvbpsi_init创建了一个整体的decoder,它由各个具体的specific
decoder构成。代码展开如下:

ts_stream_t \libdvbpsi_init(int debug, ts_stream_log_cb pf_log, void
*cb_data)*

{

ts_stream_t \stream = (ts_stream_t *)calloc(1, sizeof(ts_stream_t));*

if (stream == NULL)

return NULL;

if (pf_log)

{

stream->pf_log = pf_log;

stream->cb_data = cb_data;

}

/\ print PSI tables debug anyway, unless no debug is wanted at all */*

switch (debug)

{

case 0: stream->level = DVBPSI_MSG_NONE; break;

case 1: stream->level = DVBPSI_MSG_ERROR; break;

case 2: stream->level = DVBPSI_MSG_WARN; break;

case 3: stream->level = DVBPSI_MSG_DEBUG; break;

}

/\ PAT */*

/\创建dvbpsi handle structure*/*

stream->pat.handle = dvbpsi_new(&dvbpsi_message, stream->level);

if (stream->pat.handle == NULL)

goto error;

/\初始化PAT decoder 并且将handle_PAT(callback) 绑定到pat decoder*/*

if (!dvbpsi_pat_attach(stream->pat.handle, handle_PAT, stream))

{

dvbpsi_delete(stream->pat.handle);

stream->pat.handle = NULL;

goto error;

}

/\ CAT */*

stream->cat.handle = dvbpsi_new(&dvbpsi_message, stream->level);

if (stream->cat.handle == NULL)

goto error;

if (!dvbpsi_cat_attach(stream->cat.handle, handle_CAT, stream))

{

dvbpsi_delete(stream->cat.handle);

stream->cat.handle = NULL;

goto error;

}

/\ SDT demuxer */*

stream->sdt.handle = dvbpsi_new(&dvbpsi_message, stream->level);

if (stream->sdt.handle == NULL)

goto error;

if (!dvbpsi_AttachDemux(stream->sdt.handle, handle_subtable, stream))

{

dvbpsi_delete(stream->sdt.handle);

stream->sdt.handle = NULL;

goto error;

}

/\ RST */*

stream->rst.handle = dvbpsi_new(&dvbpsi_message, stream->level);

if (stream->rst.handle == NULL)

goto error;

if (!dvbpsi_rst_attach(stream->rst.handle, handle_RST, stream))

{

dvbpsi_delete(stream->rst.handle);

stream->rst.handle = NULL;

goto error;

}

/\ EIT demuxer */*

stream->eit.handle = dvbpsi_new(&dvbpsi_message, stream->level);

if (stream->eit.handle == NULL)

goto error;

if (!dvbpsi_AttachDemux(stream->eit.handle, handle_subtable, stream))

{

dvbpsi_delete(stream->eit.handle);

stream->eit.handle = NULL;

goto error;

}

/\ TDT demuxer */*

stream->tdt.handle = dvbpsi_new(&dvbpsi_message, stream->level);

if (stream->tdt.handle == NULL)

goto error;

if (!dvbpsi_AttachDemux(stream->tdt.handle, handle_subtable, stream))

{

dvbpsi_delete(stream->tdt.handle);

stream->tdt.handle = NULL;

goto error;

}

stream->pat.pid = &stream->pid[0x00];

stream->cat.pid = &stream->pid[0x01];

stream->sdt.pid = &stream->pid[0x11];

stream->eit.pid = &stream->pid[0x12];

stream->rst.pid = &stream->pid[0x13];

stream->tdt.pid = &stream->pid[0x14];

return stream;

error:

if (dvbpsi_decoder_present(stream->pat.handle))

dvbpsi_pat_detach(stream->pat.handle);

if (dvbpsi_decoder_present(stream->cat.handle))

dvbpsi_cat_detach(stream->cat.handle);

if (dvbpsi_decoder_present(stream->sdt.handle))

dvbpsi_DetachDemux(stream->sdt.handle);

if (dvbpsi_decoder_present(stream->eit.handle))

dvbpsi_DetachDemux(stream->eit.handle);

if (dvbpsi_decoder_present(stream->rst.handle))

dvbpsi_rst_detach(stream->rst.handle);

if (dvbpsi_decoder_present(stream->tdt.handle))

dvbpsi_DetachDemux(stream->tdt.handle);

if (stream->pat.handle)

dvbpsi_delete(stream->pat.handle);

if (stream->cat.handle)

dvbpsi_delete(stream->cat.handle);

if (stream->sdt.handle)

dvbpsi_delete(stream->sdt.handle);

if (stream->rst.handle)

dvbpsi_delete(stream->rst.handle);

if (stream->eit.handle)

dvbpsi_delete(stream->eit.handle);

if (stream->tdt.handle)

dvbpsi_delete(stream->tdt.handle);

free(stream);

return NULL;

}

2.PAT表的解析

由于每个表的具体解析流程都是相似的,所以选取其中一个做典型分析。比如PAT。

/\ PAT */*

/\创建dvbpsi handle structure*/*

stream->pat.handle = dvbpsi_new(&dvbpsi_message, stream->level);

if (stream->pat.handle == NULL)

goto error;

/\初始化PAT decoder 并且将handle_PAT(callback) 绑定到pat decoder*/*

if (!dvbpsi_pat_attach(stream->pat.handle, handle_PAT, stream))

{

dvbpsi_delete(stream->pat.handle);

stream->pat.handle = NULL;

goto error;

}

2.1.pat表的sepecfic decoder的抽象(也就是它的struct设计)

ts_pat_t:

typedef struct

{

dvbpsi_t \handle;*

int i_pat_version;

int i_ts_id;

ts_pid_t \pid;*

} ts_pat_t;

dvbpsi_t:

struct dvbpsi_s

{

dvbpsi_decoder_t \p_decoder; /*!< private pointer to*

specific decoder \/*

/\ Messages callback */*

dvbpsi_message_cb pf_message; /\!< Log message callback */*

enum dvbpsi_msg_level i_msg_level; /\!< Log level */*

/\ private data pointer for use by caller, not by libdvbpsi itself ! */*

void \p_sys; /*!< pointer to private data*

from caller. Do not use

from inside libdvbpsi. It

will crash any application. \/*

};

dvbpsi_decoder_t:

struct dvbpsi_decoder_s

{

DVBPSI_DECODER_COMMON

};

/*实质上宏替换展开,则dvbpsi_decoder_t如下所示*/

dvbpsi_decoder_t

{

uint8_t i_magic[3]; /\!< Reserved magic value */*

bool b_complete_header; /\!< Flag for header completion */*

bool b_discontinuity; /\!< Discontinuity flag */*

bool b_current_valid; /\!< Current valid indicator */*

uint8_t i_continuity_counter; /\!< Continuity counter */*

uint8_t i_last_section_number;/\!< Last received section number */*

dvbpsi_psi_section_t \p_current_section; /*!< Current section */*

dvbpsi_psi_section_t \p_sections; /*!< List of received PSI sections */*

dvbpsi_callback_gather_t pf_gather;/\!< PSI decoder’s callback */*

int i_section_max_size; /\!< Max size of a section for this decoder */*

int i_need; /\!< Bytes needed */*

}

dvbpsi_psi_section_t:

struct dvbpsi_psi_section_s

{

/\ non-specific section data */*

uint8_t i_table_id; /\!< table_id */*

bool b_syntax_indicator; /\!< section_syntax_indicator */*

bool b_private_indicator; /\!< private_indicator */*

uint16_t i_length; /\!< section_length */*

/\ used if b_syntax_indicator is true */*

uint16_t i_extension; /\!< table_id_extension */*

/\!< transport_stream_id for a*

PAT section \/*

uint8_t i_version; /\!< version_number */*

bool b_current_next; /\!< current_next_indicator */*

uint8_t i_number; /\!< section_number */*

uint8_t i_last_number; /\!< last_section_number */*

/\ non-specific section data */*

/\ the content is table-specific */*

uint8_t \ p_data; /*!< complete section */*

uint8_t \ p_payload_start; /*!< payload start */*

uint8_t \ p_payload_end; /*!< payload end */*

/\ used if b_syntax_indicator is true */*

uint32_t i_crc; /\!< CRC_32 */*

/\ list handling */*

struct dvbpsi_psi_section_s \ p_next; /*!< next element of*

the list \/*

};

dvbpsi_psi_section_t结构体的设计,是根据标准ISO/IEC 13818-1 section
2.4.4.11协议如下表 private section所示:

table1: private section

由上述可知,用面向对象的思想来看,其继承关系是:

dvbpsi_psi_section_t<–dvbpsi_pat_decoder_t<–dvbpsi_decoder_t<–dvbpsi_t<–ts_pat_t<–ts_stream_t.

使用libdvbpsi

从上一节我们知道了psi decoder的构建过程。本章将延续上文
以PAT表详细解析为例,由点及面的概述libdvbpsi的实现。

下面详细分析pat decoder解码器的创建过程:

a、首先通过dvbpsi_new创建一个通用的decoder,但还未真正的实例化为pat
decoder,即pat decoder还未初始化。

dvbpsi_t \dvbpsi_new(dvbpsi_message_cb callback, enum dvbpsi_msg_level
level)*

{

dvbpsi_t \p_dvbpsi = calloc(1, sizeof(dvbpsi_t));*

if (p_dvbpsi == NULL)

return NULL;

p_dvbpsi->p_decoder = NULL; //赋值为NULL,pat decoder还未初始化

p_dvbpsi->pf_message = callback;

p_dvbpsi->i_msg_level = level;

return p_dvbpsi;

}

b、初始化PAT decoder
并且绑定pat表的解析handle动作。其中dvbpsi_pat_attach中dvbpsi_decoder_new创建了
真正的pat decoder实例,并通过p_dvbpsi->p_decoder =
DVBPSI_DECODER(p_pat_decoder)将其赋值给了抽象的decoder。

bool dvbpsi_pat_attach(dvbpsi_t \p_dvbpsi, dvbpsi_pat_callback pf_callback,
void* p_cb_data)*

{

assert(p_dvbpsi);

assert(p_dvbpsi->p_decoder == NULL);

/\ PSI decoder configuration and initial state */*

dvbpsi_pat_decoder_t \p_pat_decoder;*

/\创建真正的pat decoder*/*

p_pat_decoder = (dvbpsi_pat_decoder_t\) dvbpsi_decoder_new(&dvbpsi_pat_sections_gather, 1024, true,
sizeof(dvbpsi_pat_decoder_t));*

if (p_pat_decoder == NULL)

return false;

/\ PAT decoder information */*

p_pat_decoder->pf_pat_callback = pf_callback;

p_pat_decoder->p_cb_data = p_cb_data;

p_pat_decoder->p_building_pat = NULL;

/\将pat decoder赋值给p_decoder(类型的强转dvbpsi_pat_decoder_t*
转成dvbpsi_decoder_t*)*/*

p_dvbpsi->p_decoder = DVBPSI_DECODER(p_pat_decoder);

return true;

}

【总结a,b】

dvbpsi_new是一个接口,创建抽象的decoder。但具体的要创建哪种specific
decoder需要caller自己去初始化,赋予其实例化的属性。通过以下接口去实例化decoder。每个表的解析动作放置在tables/目录下的各个.c文件中。

{

dvbpsi_pat_attach,(tables/pat.c)

dvbpsi_bat_attach,(tables/bat.c)

…

}

c、PAT表中section的聚集

创建pat decoder实例时,传入了callback函数dvbpsi_pat_sections_gather, 用于gather
PAT表中的所有section。
只有等待表中所有section收集完整并进行CRC校验后,才开始进行表的解析或重建。

void dvbpsi_pat_sections_gather(dvbpsi_t\ p_dvbpsi, dvbpsi_psi_section_t*
p_section)*

{

dvbpsi_pat_decoder_t\ p_pat_decoder;s*

assert(p_dvbpsi);

assert(p_dvbpsi->p_decoder);

if (!dvbpsi_CheckPSISection(p_dvbpsi, p_section, 0x00, “PAT decoder”))

{

dvbpsi_DeletePSISections(p_section);

return;

}

/\ Now we have a valid PAT section */*

p_pat_decoder = (dvbpsi_pat_decoder_t \)p_dvbpsi->p_decoder;*

/\ TS discontinuity check */*

if (p_pat_decoder->b_discontinuity)

{

dvbpsi_ReInitPAT(p_pat_decoder, true);

p_pat_decoder->b_discontinuity = false;

}

else

{

if (p_pat_decoder->p_building_pat)

{

if (dvbpsi_CheckPAT(p_dvbpsi, p_section))

dvbpsi_ReInitPAT(p_pat_decoder, true);

}

else

{

if( (p_pat_decoder->b_current_valid)

&& (p_pat_decoder->current_pat.i_version == p_section->i_version)

&& (p_pat_decoder->current_pat.b_current_next ==

p_section->b_current_next))

{

/\ Don’t decode since this version is already decoded */*

dvbpsi_debug(p_dvbpsi, “PAT decoder”,

*”ignoring already decoded section %d”,*

p_section->i_number);

dvbpsi_DeletePSISections(p_section);

return;

}

}

}

/\ Add section to PAT */*

if (!dvbpsi_AddSectionPAT(p_dvbpsi, p_pat_decoder, p_section))

{

dvbpsi_error(p_dvbpsi, “PAT decoder”, “failed decoding section %d”,

p_section->i_number);

dvbpsi_DeletePSISections(p_section);

return;

}

/\ Check if we have all the sections */*

if (dvbpsi_decoder_psi_sections_completed(DVBPSI_DECODER(p_pat_decoder)))

{

assert(p_pat_decoder->pf_pat_callback);

/\ Save the current information */*

p_pat_decoder->current_pat = \p_pat_decoder->p_building_pat;*

p_pat_decoder->b_current_valid = true;

/\ Decode the sections */*

dvbpsi_pat_sections_decode(p_pat_decoder->p_building_pat,

p_pat_decoder->p_sections);

/\ Delete the sections */*

dvbpsi_DeletePSISections(p_pat_decoder->p_sections);

p_pat_decoder->p_sections = NULL;

/\ signal the new PAT */*

p_pat_decoder->pf_pat_callback(p_pat_decoder->p_cb_data,

p_pat_decoder->p_building_pat);

/\ Reinitialize the structures */*

dvbpsi_ReInitPAT(p_pat_decoder, false);

}

}

d、PAT表及PMT表的解析

紧接着对pat table进行解析或重建。 解析的动作实质就是绑定到pat
decoder的handle(callback)。

(两者含义其实一样,知道了table表的所有细节,可以说成解析了table,也可以说成重建了table)

PAT表的协议根据标准 ISO/IEC 13818-1 section 2.4.4.3如下表所示:

源码解析如下:

/\*****************************************************************************

\ handle_PAT*

\****************************************************************************/*

static void handle_PAT(void\ p_data, dvbpsi_pat_t* p_pat)*

{

dvbpsi_pat_program_t\ p_program = p_pat->p_first_program;*

ts_stream_t\ p_stream = (ts_stream_t*) p_data;*

p_stream->pat.i_pat_version = p_pat->i_version;

p_stream->pat.i_ts_id = p_pat->i_ts_id;

printf(“\n”);

printf(“ PAT: Program Association Table\n”);

printf(“\tTransport stream id : %d\n”, p_pat->i_ts_id);

printf(“\tVersion number : %d\n”, p_pat->i_version);

printf(“\tCurrent next : %s\n”, p_pat->b_current_next ? “yes” : “no”);

if (p_stream->pat.pid->i_prev_received > 0)

printf(“\tLast received : %”PRId64” ms ago\n”,

(mtime_t)(p_stream->pat.pid->i_received -
p_stream->pat.pid->i_prev_received));

printf(“\t\t| program_number @ [NIT|PMT]_PID\n”);

while (p_program)

{

/\ Attach new PMT decoder */*

ts_pmt_t \p_pmt = calloc(1, sizeof(ts_pmt_t));*

if (p_pmt)

{

/\ PMT */*

p_pmt->handle = dvbpsi_new(&dvbpsi_message, p_stream->level);

if (p_pmt->handle == NULL)

{

fprintf(stderr, “dvbinfo: Failed attach new PMT decoder\n”);

free(p_pmt);

break;

}

p_pmt->i_number = p_program->i_number;

p_pmt->pid_pmt = &p_stream->pid[p_program->i_pid];

p_pmt->pid_pmt->i_pid = p_program->i_pid;

p_pmt->p_next = NULL;

/\创建PMT Decoder*/*

if (!dvbpsi_pmt_attach(p_pmt->handle, p_program->i_number, handle_PMT,
p_stream))

{

fprintf(stderr, “dvbinfo: Failed to attach new pmt decoder\n”);

break;

}

/\ insert at start of list */*

p_pmt->p_next = p_stream->pmt;

p_stream->pmt = p_pmt;

p_stream->i_pmt++;

assert(p_stream->pmt);

}

else

fprintf(stderr, “dvbinfo: Failed create new PMT decoder\n”);

printf(“\t\t| %14d @ pid: 0x%x (%d)\n”,

p_program->i_number, p_program->i_pid, p_program->i_pid);

p_program = p_program->p_next;

}

printf(“\tActive : %s\n”, p_pat->b_current_next ? “yes” : “no”);

dvbpsi_pat_delete(p_pat);

}

PAT表中含有PMT表的节目号,所以需要解析PMT表。

PMT表的协议根据标准ISO/IEC 13818-1 section 2.4.4.8如下表所示:

源码解析如下:

/\*****************************************************************************

\ handle_PMT*

\****************************************************************************/*

static void handle_PMT(void\ p_data, dvbpsi_pmt_t* p_pmt)*

{

dvbpsi_pmt_es_t\ p_es = p_pmt->p_first_es;*

ts_stream_t\ p_stream = (ts_stream_t*) p_data;*

/\ Find signalled PMT */*

ts_pmt_t \p = p_stream->pmt;*

while (p)

{

if (p->i_number == p_pmt->i_program_number)

break;

p = p->p_next;

}

assert(p);

p->i_pmt_version = p_pmt->i_version;

p->pid_pcr = &p_stream->pid[p_pmt->i_pcr_pid];

p_stream->pid[p_pmt->i_pcr_pid].b_pcr = true;

printf(“\n”);

printf(“ PMT: Program Map Table\n”);

printf(“\tProgram number : %d\n”, p_pmt->i_program_number);

printf(“\tVersion number : %d\n”, p_pmt->i_version);

printf(“\tPCR_PID : 0x%x (%d)\n”, p_pmt->i_pcr_pid, p_pmt->i_pcr_pid);

printf(“\tCurrent next : %s\n”, p_pmt->b_current_next ? “yes” : “no”);

DumpDescriptors(“\t ]”, p_pmt->p_first_descriptor);

printf(“\t| type @ elementary_PID : Description\n”);

while(p_es)

{

printf(“\t| 0x%02x @ pid 0x%x (%d): %s\n”,

p_es->i_type, p_es->i_pid, p_es->i_pid,

GetTypeName(p_es->i_type) );

DumpDescriptors(“\t| ]”, p_es->p_first_descriptor);

p_es = p_es->p_next;

}

dvbpsi_pmt_delete(p_pmt);

}

至此,PAT表的解析分析完成了。具体在代码调试过程中,可借助码流分析工具(TS
expert等),详细的查看各个表的信息,并结合dvb规范,详细查询每个descriptor的描述,完成解码工作。

VLC源码分析

Posted on 2019-01-12 | Edited on 2019-06-18 | In vlc

VLC源码分析

Decin Jan 12th, 2018 VLC

0x00 前置信息

VLC是一个非常庞大的工程,我从它的架构及流程入手进行分析,涉及到一些很细的概念先搁置一边,日后详细分析。

0x01 源码结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
.
├── AUTHORS
├── COPYING
├── COPYING.LIB
├── INSTALL
├── NEWS
├── README
├── THANKS
├── aclocal.m4
├── autom4te.cache
│   ├── output.0
│   ├── output.1
│   ├── output.2
│   ├── output.3
│   ├── requests
│   ├── traces.0
│   ├── traces.1
│   ├── traces.2
│   └── traces.3
├── autotools
│   ├── compile
│   ├── config.guess
│   ├── config.rpath
│   ├── config.sub
│   ├── depcomp
│   ├── install-sh
│   ├── ltmain.sh
│   ├── missing
│   ├── test-driver
│   └── ylwrap
├── bin
│   ├── breakpad.cpp
│   ├── cachegen.c
│   ├── darwinvlc.m
│   ├── override.c
│   ├── rootwrap.c
│   ├── vlc.c
│   ├── vlc_win32_rc.rc.in
│   └── winvlc.c
├── bootstrap
├── build-iPhoneOS
│   ├── arm64
│   ├── armv7
│   └── armv7s
├── build-iPhoneSimulator
│   ├── i386
│   └── x86_64
├── compat 存放可能使用的函数
├── contrib
│   ├── bootstrap 第三库的编译脚本, bootstrap?
│   ├── iPhone-aarch64-apple-darwin14-aarch64
│   ├── iPhone-armv7-apple-darwin14-armv7
│   ├── iPhone-armv7s-apple-darwin14-armv7s
│   ├── iPhone-i386-apple-darwin14-i386
│   ├── iPhone-x86_64-apple-darwin14-x86_64
│   ├── iPhoneOS-aarch64
│   ├── iPhoneOS-armv7
│   ├── iPhoneOS-armv7s
│   ├── iPhoneSimulator-i386
│   ├── iPhoneSimulator-x86_64
│   ├── src 第三方库源码包的校验和, 和补丁(可以在此处对第三库进行打补丁)
│   └── tarballs 第三方库源码包(tar.bz2)
├── doc
├── extras
│   ├── analyser
│   ├── breakpad
│   ├── buildsystem
│   ├── misc
│   ├── package
│   └── tools
├── include
│   ├── vlc
│   ├── vlc_access.h
│   ├── vlc_actions.h
│   ├── ...
├── install-iPhone
│   ├── contrib
│   ├── core
│   └── plugins
├── install-iPhoneOS
│   ├── arm64
│   ├── armv7
│   └── armv7s
├── install-iPhoneSimulator
│   ├── i386
│   └── x86_64
├── lib libvlc接口层, 对src/封装播放器接口
│   ├── audio.c
│   ├── core.c
│   ├── dialog.c
│   ├── error.c
│   ├── event.c 事件管理, play, stop, pause, bufferring等事件
│   ├── libvlc.sym ib的接口列表, 具体作用?
│   ├── libvlc_internal.h
│   ├── log.c
│   ├── media.c 构建输入源, 一个源代表一个media(http://..., file://..)
│   ├── media_discoverer.c
│   ├── media_internal.h
│   ├── media_library.c
│   ├── media_list.c 构建多个输入源
│   ├── media_list_internal.h
│   ├── media_list_path.h
│   ├── media_list_player.c
│   ├── media_player.c 播放器play, stop, pause, seek等接口
│   ├── media_player_internal.h
│   ├── playlist.c
│   ├── renderer_discoverer.c
│   ├── renderer_discoverer_internal.h
│   ├── video.c record, snapshot等接口
│   └── vlm.c
├── m4 autoMake和autoconf的宏文件
│   ├── ax_append_compile_flags.m4
│   ├── ax_append_flag.m4
│   ├── ...
├── make-alias
├── modules
│   ├── access access模块
│   ├── access_output
│   ├── arm_neon
│   ├── audio_filter
│   ├── audio_mixer
│   ├── audio_output 频输出模块, 连接硬件, 如audiounit_ios.m, opensles_android.c
│   ├── codec 编解码器模块
│   ├── control
│   ├── demux 解复用模块, 解析音视频流等
│   ├── gui 各平台的UI
│   ├── hw
│   ├── keystore
│   ├── list.sh
│   ├── logger
│   ├── lua
│   ├── meta_engine
│   ├── misc 被libvlc其他部分使用杂项, 如线程系统, 消息队列, CPU探测, 对象查询系统
│   ├── mux 复用模块, 流的打包
│   ├── notify
│   ├── packetizer
│   ├── services_discovery
│   ├── spu 字画面(subtitle, epg osd).
│   ├── stream_extractor
│   ├── stream_filter
│   ├── stream_out
│   ├── text_renderer
│   ├── video_chroma
│   ├── video_filter
│   ├── video_output 视频输出模块, 连接硬件, 如ios.m, android/目录下
│   ├── video_splitter
│   └── visualization
├── share 图标、脚本
├── src vlc架构核心代码
│   ├── check_headers/
│   ├── check_symbols/
│   ├── config/ 从命令行和配置文件加载配置,提供功能模块的读取和写入配置.
│   ├── extras/ 平台特殊性相关代码。
│   ├── interface/ 提供代码中可以调用的接口中,如按键后硬件作出反应。
│   ├── libvlc-module.c 热键, 设置界面显示文字等
│   ├── libvlc.c
│   ├── libvlc.h
│   ├── libvlccore.sym
│   ├── misc/ 各种混合, 包含picture_fifo.c, fifo.c, variables.c, libvlc使用的其他部分功能,如线程系统,消息队列,CPU的检测,对象查找系统,或平台的特定代码。
│   ├── modules/ 模块管理, 模块选择, 模块加载。
│   ├── network/ 提供网络接口, tcp, udp等。
│   ├── playlist/ 管理播放功能,如停止,播放,下一首,随机播放等。
│   ├── revision.c
│   ├── revision.txt
│   ├── version.c
│   ├── test/ src的测试代码。
│   ├── text/ 字符集。
│   ├── input/ 显示(output)前的大部分流程, access, demux, es_out, decoder.
│   ├── stream_output/ 输出音频流和视频流到网络。
│   ├── audio_output/ 从解码后的数据队列中获得的数据后播放.
│   ├── video_output/ 从解码后的数据队列中获得的数据后播放, 子画面生成(subtitle, epg osd)等。
│   ├── darwin/ 苹果系统相关, 文件系统相关, posix thread的vlc封装等.
│   ├── os2/ os2平台相关, 同上.
│   ├── posix/ posix规范, 同上.
│   ├── linux/ linux系统相关, 同上.
│   ├── android/ android系统相关, 同上.
│   └── win32/ 类似同上.
└── test 测试代码.

0x02 基础概念

  • 对于一个视频的播放,播放器的执行步骤大致如下:
    1. 读取原始数据
    2. 解复用(demux)
    3. 解码(decode)
    4. 显示(render)
  • VLC在包含以上概念的基础上,又抽象出几个其他概念,先列出VLC中抽象出来的重要概念:
    • playlist: playlist表示播放列表,VLC在启动后,即创建一个playlist thread,用户输入后,动态创建input。
    • input: input表示输入,当用户通过界面输入一个文件或者流地址时,input thread 被动态创建,该线程的生命周期直到本次播放结束。
    • access: access表示访问,是VLC抽象的一个层,该层向下直接使用文件或网络IO接口(http, rstp等),向上为stream层服务,提供IO接口。
    • stream: stream表示流,是VLC抽象的一个层,该层向下直接使用access层提供的IO接口,向上为demux层服务,提供IO接口。
    • demux: demux表示解复用,是视频技术中的概念,该层向下直接使用stream层提供的IO接口,数据出来后送es_out。
    • es_out: es_out表示输出,是VLC抽象的一个层,该层获取demux后的数据,送decode解码。
    • decode: decode表示解码,是视频技术中的概念,获取es_out出来的数据(通过一个fifo交互),解码后送output。
    • output: output表示输出,获取从decode出来的数据,送readerer。
    • render: render表示显示,获取从output出来的数据(通过一个fifo交互),然后显示。

下图显示了这些抽象的概念的关系,其中蓝色表示VLC抽象的概念。
modules_structure.png

0x04 架构综述

VLC的整体框架是设计成一套module的管理机制,将功能分类并抽象成modules。

VLC main: player的main。初始化libVLC 并加载用户界面。
libVLCcore:libvlc的核心,抽象出了一个libvlc_instance_t 对象,提供modules的装载/卸载机制。
modules: modules提供具体的功能,比如上面的access,demux,decode就是以一个模块的形式存在。
External libraries:外部开源库。

模块管理

模块位于 modules/子目录,在运行时被加载。每一个模块提供不同的特征适应特定的文件的环境。另外,大量的不断编写的可移植功能位于audio_output/,vidco_output/ 和 gui/模块,以支持新的平台(如:BeoS Mae OS X)。

模块中的插件被位于 src/misc/modules.c 和 include/modules*.h 中的函数动态加载和卸载。写模块的 API 描述如下,共 3 种:

  • (1)模块描述宏:声明模块具有哪种优先级的能力(接口,demux2 等等),还有GUI模块的实现参数,特定模块的配置变量,快捷方式,子模块等等;
  • (2)Open(vlc_object_t* p_object):被 VLC 调用初始化这个模块,它被模块描述宏赋值给了 结构体 module_t 中的 pf_activate 函数指针,被 module_need 调用;
  • (3)Close(vlc_object_t* p_object):被 VLC 调用负初始化这个模块,保证消耗 Open 分配的所 有资源。它被模块描述宏赋值给了结构体 module_t 中的 pf_deactivate 函数指针,被 module_unneed 调用。

用 libvlc 写的模块能够直接被编译进 vlc,因为有的 OS 不支持动态加载代码。被静态编译进 vlc 的模块叫做内置模块。

模块的加载方式:
首先模块先将自身注册到VLC中,代码片段如:

1
2
3
vlc_module_begin()
...
vlc_module_end()

然后在需要加载模块的时候,调用module_need接口,去找到合适的模块。找到合适的模块后,会执行注册中设置的回调方法,诸如Open*名字的方法。
同样自己可以实现模块,只需要按照VLC模块的标准即可。VLC中很多模块就是通过外部的开源库实现的。

vlc中模块大致分类:
1364359619_6154.jpg

线程管理

VLC 是一个密集的多线程应用。由于解码器必须预先清空和播放工序必须预先做好流程
(比如说解码器和输出必须被分开使用,否则无法保证在要求的时间里播放文件),因此 VLC 不采用单线程方法。目前不支持单线程的客户端,多线程的解码器通常就意味着更多的开销 (各线程共享内存的问题等),进程间的通信也会比较复杂。

VLC 的线程结构基于 pthreads 线程模型。为了可移植的目的,没有直接使用 pthreads 函数,而是做了一系列类似的包裹函数:vlc_thread_create,vlc_thread_exit,vlc_thread_join, vlc_mutex_init , vlc_mutex_lock , vlc_mutex_unlock , vlc_mutex_destroy , vlc_cond_init , vlc_cond_signal , vlc_cond_broadcast , vlc_cond_wait , vlc_cond_destroy 和 类 似 结 构:vlc_thread_t,vlc_mutex_t,and vlc_cond_t。

线程同步

VLC 的另一个关键特征就是解码和播放是异步的:解码由一个解码器线程工作,播放由音
频输出线程或者视频输出线程工作。这个设计的主要目的是不会阻塞任何解码器线程,能够 及时播放正确的音频帧或者视频帧。这样实现也导致产生了在接口,输入,解码器和输出之 间的一个复杂的通讯结构。

虽然当前接口并不允许,但是让若干个输入和视频输出线程在同一时刻读取多个文件是 可行的(这是 VLC 未来改进的主要方向)。现在的客户端就是用这种思想实现的,这就意味着 如果没有用到全局锁的话那么一个不能重入的库是不能被使用的(尤其是 liba52 库)。

VLC 输出的流里包含时间戳,被传递给解码器,所有有时间戳标记的流也均被记录,这
样输出层可以正确及时的播放这些流。时间 mtime_t 是一个有符号的 64-bit 整形变量,单位 是百万分之一秒,是从 1970 年 7 月 1 日以来的绝对时间。

当前时间能够被 mdate()函数恢复。一个线程可以被阻塞到 mwait(mtime_t date)等到一 个确定的时间才被执行。也可以用 msleep(mtime_t delay)休眠一段时间。如果有重要的事情 要处理的话,那么应该在正常时间到来之前被唤醒(如色度变换)。例如在 modules\codec\synchro.c 中,通常的解码时间被记录,保证图像被即时解码。

0x05 流程分析

程序通过用户配置的方式来加载不同的模块,以下主要对ts流播放流程进行跟踪:
首先,给出流程图,参照该图,再继续下面的流程分析,绿色线表示打开VLC后的执行操作;黑色线表示用户输入一个视频后的执行操作;蓝色线从红色圈开始,表示开始播放输入流后的数据流向。
vlc_flowpath_analysis.png

(1) main函数(vlc/bin/vlc.c)

  • 1.参数信号处理相关,不详分析。
  • 2.调用libvlc_new()初始化一个libvlc_instance_t实例。(libvlc_instance_t is opaque. It represents a libvlc instance)
    • 2.1 调用libvlc_InternalCreate创建一个libvlc_int_t。(This structure is a LibVLC instance, for use by libvlc core and plugins.)
    • 2.2 调用libvlc_InternalInit初始化libvlc_int_t实例。
    • 2.3 初始化libvlc_instance_t其他成员。
  • 3.调用libvlc_add_intf添加模块。
    • 3.1 获取playlist,如果为空,则调用playlist_Create创建一个playlist结构,并调用playlist_Activate创建新的playlist线程Thread(src/playlist/thread.c)。
    • 3.2 调用intf_Create创建一个默认的interface。
    • 3.2.1 调用vlc_custom_create创建一个vlc object(intf_thread_t)。
    • 3.2.2 注册一个添加interface的回调方法。
    • 3.2.3 调用module_need加载一个interface模块。

调用libvlc_playlist_play,如果播放列表不为空,并且被设置为自动播放,则播放播放列表内容。
信号处理相关,不详分析。

(2) 创建一个输入

  • 1 通过libvlc_media_list_player控制播放, libvlc_media_list_player_play_item
    • 1.1 初始化成功后,程序运行在playlist的线程Thread(src/playlist/thread.c)中,循环接收界面输入的请求。
    • 1.2 当输入一个新的文件或者流地址,在PlaylistVAControl获得信号,并发送该信号。
    • 1.3 Thread接收到播放请求后,在LoopRequest中调用PlayItem方法。
      • 1.3.1 调用input_Create创建一个input(input_thread_t *p_input_thread, 注意这里并不是线程指针而是自定义结构体, input线程的相关信息)结构,并初始化各种成员,其中包括调用input_EsOutNew创建p_es_out_display(es_out)。
      • 1.3.2 调用input_Start创建一个input线程, 入口函数为Run(src/input/input.c)。
  • 2 通过libvlc_media_player控制播放, 调用libvlc_media_player_play
    • 2.1 input_Create创建(同1.3.1)
    • 2.2 同1.3.2

(3) 初始化输入

  • 1.调用Run(src/input/input.c)中的Init方法,开始初始化。

  • 2.调用input_EsOutTimeshiftNew新建一个50M的Timeshift(暂停缓存),包括创建并初始化p_es_out(es_out),与后续步骤9相关。
    设置input的状态为OPENING_S。

  • 3.调用InputSourceNew, 调用input_SplitMRL分解输入uri, 调用InputDemuxNew创建输入Demux模块

    • 3.1 以stream形参为NULL调用demux_NewAdvanced加载”access_demux”模块(first, try to create an access demux)。如:rtsp://源则有access_demux模块, 而不需要走以下步骤分步创建.

    • 3.2 如果没有合适的”access_demux”模块,则调用stream_AccessNew创建一个实际的access模块(stream_t* p_stream)。(not an access-demux: create the underlying access stream)

      • 3.2.1 调用vlc_stream_CommonNew创建stream_t结构体(作为access)。
      • 3.2.2 调用module_need加载合适的access模块。
      • 3.2.3 调用access模块的Open*方法,以avio模块为例。
        • 3.2.3.1 调用vlc_init_avformat初始化VLC即avformat环境。
        • 3.2.3.2 调用avio_open2打开该uri。
        • 3.2.3.3 设置access的IO方法指针。
    • 3.3 通过stream_FilterChainNew添加明确的stream_filter.(attach explicit stream filters to stream)

    • 3.4 以stream形参为p_stream调用demux_NewAdvanced创建常规的demux模块.(create a regular demux with the access stream created)

      • 3.4.1 调用vlc_custom_create创建demux_priv_t结构体, 并初始化(demux_priv_t *)priv->demux。
      • 3.4.2 调用vlc_module_load加载合适的demux模块。
      • 3.4.3 调用demux模块的Open*方法,以avformat/demux模块为例。
        • 3.4.3.1 调用stream_Peek从stream层获取数据,用于分析输入的文件格式。
        • 3.4.3.2 调用av_probe_input_format分析输入的文件格式。
        • 3.4.3.3 设置demux_sys_t结构体部分变量的值。
        • 3.4.3.4 调用avformat_alloc_context分配AVFormatContext结构体。
        • 3.4.3.5 调用avio_alloc_context设置AVFormatContext结构体的AVIOContext类型成员pb,并设置read和seek方法指针。
        • 3.4.3.6 调用avformat_open_input打开一个输入,这里的input与VLC中的input不是一个概念,关于avformat_open_input的分析详见我的另一篇文章《avformat_open_input详细分析》链接地址。
        • 3.4.3.7 调用avformat_find_stream_info分析流信息,该方法通过读取数据初始化流以及流解码信息。
        • 3.4.3.8 根据分析的流信息,设置fmt变量,并调用es_out_Add。
        • 3.4.3.9 实际调用EsOutAdd(src/input/es_out.c),添加一个es_out,有几个流就做几次es_out_Add操作,比如该输入中有一个视频流和一个音频流,则作两次es_out_Add操作。
        • 3.4.3.10 nb_chapters相关未详细分析。
    • 3.5 设置record相关。

    • 3.6 调用demux_Control设置demux pts delay。

    • 3.7 调用demux_Control设置fps。

  • 4.调用demux_Control获取输入的长度。

  • 5.调用StartTitle显示标题。

  • 6.调用LoadSubtitles加载字幕。

  • 7.调用LoadSlaves,含义不详。

  • 8.调用InitPrograms,设置es_out和decoder相关。

    • 8.1 调用UpdatePtsDelay计算正确的pts_delay值。
    • 8.2 sout相关可选,暂不分析。
    • 8.3 调用es_out_SetMode,设置es_out的mode为ES_OUT_MODE_AUTO。
    • 8.4 以DEMUX_SET_GROUP指令调用demux_Control,DEMUX_SET_GROUP/SET_ES only a hint for demuxer (mainly DVB) to allow not reading everything。
  • 9.实际调用EsOutControlLocked进入case ES_OUT_SET_MODE分支。

    • 9.1 设置es_out_sys_t 的b_active和i_mode。
    • 9.2 调用EsOutSelect方法,根据指定模块选择一个es_out。
    • 9.3 在EsOutSelect方法中进入ES_OUT_MODE_AUTO分支,进一步调用EsSelect方法,再进一步调用EsCreateDecoder方法创建decoder。
      • 9.3.1 调用input_DecoderNew创建一个新的decoder。
      • 9.3.2 如果需要缓存,调用input_DecoderStartWait发送信号,开始线程等待。
      • 9.3.3 调用EsOutDecoderChangeDelay设置decode delay。
  • 10.续9.3.1进入decoder_New方法。

    • 10.1 调用CreateDecoder创建decoder配置结构体。
    • 10.1.1 调用vlc_custom_create创建一个vlc object(decoder_t)。
    • 10.1.2 新建decode fifo。
    • 10.1.3 调用module_need加载适配的解码模块。
      • 10.1.3.1 调用decode模块的OpenDecoder方法,以codec/avcodec模块为例。
      • 10.1.3.2 调用GetFfmpegCodec方法 determine codec type(源码描述)。
      • 10.1.3.3 调用vlc_init_avcodec方法初始化解码环境。
      • 10.1.3.4 调用avcodec_find_decoder设置AVCodec。
      • 10.1.3.5 调用avcodec_alloc_context3分配一个AVCodecContext。
      • 10.1.3.6 调用Init*Dec系列初始化解码环境。
      • 10.1.4 初始化decoder_t结构体其他成员。
    • 10.2 调用vlc_clone创建解码线程DecoderThread。
  • 12.续10.1.3.5,以InitVideoDec为例。

    • 12.1 为decoder_sys_t结构分配内存。
    • 12.2 设置相关回调方法。
    • 12.3 设置解码线程类型。
    • 12.4 调用ffmpeg_InitCodec初始化extradata相关数据。
    • 12.5 调用OpenVideoCodec方法,设置解码的长宽及采用率,进一步调用avcodec_open2打开codec。
    • 根据需要,设置线程优先级。
    • 设置meta相关。
    • 初始化完成,设置该input的状态为PLAYING_S。

(4) 播放输入
MainLoop(src/input/input.c)

  • 1.调用MainLoopDemux访问demuxer去demux数据。

  • 2.进一步调用在加载demux模块时设置的demux方法,同样以avformat/demux模块为例,实际调用Demux方法(module/demux/avformat/demux.c)。

    • 2.1 调用av_read_frame读取一帧数据。
    • 2.2 判断读取无误时,则为block_t结构分配内存,并将这一帧从AVPacket中拷贝至block_t结构中。
    • 2.3 如果该帧是I帧,则设置I帧标致位。
    • 2.4 时间戳处理相关,未深入分析。
    • 2.5 根据需要调用es_out_Control设置PCR,未深入分析。
    • 2.6 调用es_out_Send将这一帧数据发送给es_out。
    • 2.7 调用av_free_packet释放这一帧数据。
  • 3.调用es_out_Send后,实际调用EsOutSend(src/input/es_out.c)方法。

    • 3.1 调用stats_Update更新相关状态,具体未详分析。
    • 3.2 设置预读相关,如果需要预读,并且到的数据的pts小于预读需要的时间,则设置BLOCK_FLAG_PREROLL标志位。
    • 3.3 检查sout mode,具体有sync 和async mode,异同未详细分析。
    • 3.4 如果设置record,将数据dup后送decoder。
    • 3.5 调用input_DecoderDecode将block_t的数据送至decode fifo中。
      • 3.5.1 判断控制速度线程等待相关信息,具体未详细分析。
      • 3.5.2 如果decode fifo超过最大长度,则清空重置decode fifo。
      • 3.5.3 调用block_FifoPut将该block_t的数据压入decode fifo,并通知读取线程。
    • 3.6 格式变化判断处理相关,未详细分析。
    • 3.7 字幕处理相关,未详细分析。
  • 4.续3.5进入decode read thread,即DecoderThread(src/input/decoder.c)。

    • 4.1 调用block_FifoGet方法,从decode fifo中获取数据。
    • 4.2 基于某些条件,发送停止等待消息给其他线程,未详细分析。
    • 4.3 调用DecoderProcess方法开始decode a block。
    • 4.4 判断输入流的格式,调用不同的方法,这里以视频流为例,调用DecoderProcessVideo方法。
    • 4.5 packetizer相关为深入分析,在DecoderProcessVideo方法中进一步调用DecoderDecodeVideo方法。
  • 5.续4.5调用pf_decode_video,这里以avcodec模块的decoder为例,即DecodeVideo(modules/codec/avcodec/video.c)方法,在该方法中,开始真正的解码。

    • 5.1 如果在Demux中获取的流信息中包含新的extradata,并且原来的extradata数据为空,则调用ffmpeg_InitCodec初始化codec,如果b_delayed_open为true,则调用OpenVideoCodec重新打开codec。
    • 5.2 调用av_init_packet初始化解码数据包。
    • 5.3 调用avcodec_decode_video2解码数据。
    • 5.4 调用av_free_packet释放内存。
    • 5.5 计算pts值,返回解码后的数据。
    • 5.6 如果opaque为空,则调用ffmpeg_NewPictBuf方法创建一个新的picture buffer。具体调用回调指针pf_vout_buffer_new指向的vout_new_buffer,进一步调用input_resource_RequestVout最终调用VoutCreate。
      • 5.6.1 调用vlc_custom_create创建一个vlc object(vout_thread_t)。
      • 5.6.2 调用spu_Create初始化sub picture unit。
      • 5.6.3 调用vlc_clone创建一个output线程Thread(src/video_output/video_output.c)。
      • 5.6.4 output线程循环调用vout_control_Pop,首次进入ThreadControl方法中,执行ThreadStart方向,创建picture fifo(p->decoder_fifo)。
  • 6.pf_decode_video返回后,解码后的数据保存在p_pic中,进一步调用DecoderPlayVideo方法,在该方法中调用vout_PutPicture将解码后的数据压入picture fifo中。

  • 7.当picture fifo中有数据后,vout线程调用ThreadDisplayPicture中的ThreadDisplayPreparePicture方法。

    • 7.1 调用picture_fifo_Pop从picture fifo中获取解码后的数据。
    • 7.2 如果延迟太大,并且设置延迟丢帧,则丢掉该帧数据。
  • 8.调用ThreadDisplayRenderPicture显示图像。

0x06 总结

对VLC的流程分析,主要通过跟踪数据流向的方式展开。对于最后显示部分的分析还不足,另外很多细节尚未深入。
附一张图:
VLC流程图.png

参考:

  • http://blog.csdn.net/tx3344/article/details/8517062
  • http://my.oschina.net/xiaot99/blog/197555
Decin

Decin

3 posts
3 categories
3 tags
GitHub E-Mail
0%
© 2019 Decin
Powered by Hexo v3.8.0
|
Theme – NexT.Gemini v7.1.2