MPEG-2 TS 容器封装格式概览

Published: 2020-09-03

Tags: 视音频

本文总阅读量

什么是 TS?

音视频领域,TS 是 MPEG-2 transport stream 的简称,作为一种封装格式,主要应用于传输与存储音视频媒体。

原始的视频、音频数据往往很大,需要进行编码以便压缩其体积,如视频的 h264 / h265,音频的 aac / mp3,都是编码技术,编码后视频音频是独立的,这就需要封装来将视频、音频、字幕等媒体数据“打包”在一起,这样播放器拿到封装好的视频,根据其封装的信息来播放。封装格式很多,例如 MP4、MKV、AVI 等,TS 也是这样的封装格式。

TS 在电视广播,IPTV 等流媒体领域被广泛的使用,近些年越来越火的原因是苹果推出的 HLS 技术方案就使用的 TS 格式,HLS 的优点是苹果设备支持良好,基于 HTTP 协议便于分发且能复用现有 CDN 技术,唯一的缺点就是作为直播方案延时相较于其它方案是最大的。

容器特点比较

回归正题,容器格式很多,此处对比较有代表性的 TS 封装与 MP4 封装进行简单的介绍。

MP4 是常见的视频封装格式,相比于 TS 格式,它的体积会更小一点(例如: 27.5M / 30.3M),MP4 容器格式在播放之前,视频播放器需要将整个视频文件扫描一遍来获取视频信息(编解码,轨道同步信息等),如果一个 MP4 文件的索引信息保存在视频的尾部,浏览器访问视频需要全部加载完成才能播放。

而 TS 封装则不同,它的是众多 TS 包的组合,每个 TS 包 188字节大小,每个 TS 包都携带有媒体元数据保存在包的头部,TS 文件即使损坏一部分,其它部分照样能够播放,它的容错能力是很好的,每个包都携带元数据,封装出的文件比 MP4 格式的大就也是正常显现。

TS 封装格式能够按包来存储数据,在网络流媒体传输时自然受到青睐。

资料汇集

TS 分层结构

TS 分层结构

整体结构

一个 TS 包 188 字节大小,4字节长的固定头部,可选的 adaptation 扩展头,其余为 payload 数据

关于 TS 头部字段讲解的文章不少,这篇:多媒体文件格式(四):TS 格式

本次学习我打算着重了解一些重要字段,忽略部分细节,然后结合二进制工具查看 ts 文件,以便更好的了解整体。

使用 Hex Editor Neo 打开一个 ts 格式文件

左侧橙色框选部分,泾渭分明的 188 字节长度,表示一个 TS 包,首部如协议中规定的 0x47 起始。

在编辑区全选后,右侧绿色框选记录 31816180 字节,除以 188 = 169235 可以整除,说明整个 TS 文件完全是由一个个 TS 包排列而成的,没有问题。

第一个包起始 4 个字节 47 40 11 10 为固定头部,根据头部说明可知 PID 为 0 0000 0001 0001,十进制的 17

再往下边多看一些 TS 包的 PID,接下来的 PID 值为 0、0、256 ... 256,257 ... 257

这个 PID 代表着不同的分组,相同 PID 的 TS 包,解包后 PES 数据会拼接在一起。

上图就是记录的这个过程, ES 原始的码流数据(如:摄像头经过H264编码的数据)会被打包器分组、打包、加入包头信息变为 PES 包(码流),PES 也跟 ES 一样,只携带单一的媒体数据,视频或音频。PES 包再经过复用器二次打包形成 TS 包。

再看回第一个 TS 包,根据头部描述,从包头最后个字节的 10(0001 0000)可以判断 adaptation field control 值为 二进制“01”,根据下表可知,即这个 TS 包后边的 00 42 f0 25 ... ,留个🕳坑,我暂时还不知道这是个什么数据。。。

Adaptation field control 值说明

描述
00 保留值,用于未来ISO/IEC使用。
01 只有 Payload 数据,没有适配域。
10 只有适配域,没有 Payload 数据。
11 有适配域,在其后跟着 Payload 数据。

接下来的两个包我也没分辨出携带的是什么 Payload,前三个包有毒,先跳过一下。

接下来的两个 TS 包就开始有实际的数据了,比如从右侧可以很明显的看到 H.264 / MPEG-4 字样,含有很多参数信息。

根据规则解析看一下,首先的 4 个字节 47 41 00 30 是 TS 头,其中头部最后一个字节二进制为 0011 0000,根据 11 可判断出,这个 TS 包有适配域且适配域后跟着 TS Payload 数据。

适配域的第一个字节就是适配域的长度,此处可知蓝色框选区域的 TS 包,适配域长度为 7 字节(不包含自身 1 字节),其后是 00 00 01 e0 就很熟悉了,在 《海康摄像头PS流格式解析(RTP/PS/H264)》 博文中,已经遇到过它了。

此处再提一下,这里的 00 00 01 固定的 PES 头部,接下来的一个字节(Stream id)值为 e0,代表的这个 PES 包中的数据为视频数据。(音频流取值范围 (0xC0-0xDF), 视频流取值范围 (0xE0-0xEF))

在这里可以查看 PES 结构:https://en.wikipedia.org/wiki/Packetized_elementary_stream

除去细致的了解 TS 包头及适配域字段含义外,再要了解就到了 PES 包的解析了,PES 后续再学习,此处可以用几行代码解析一下这个TS文件,验证一下如上的分析,也代替手工验证一下每个 TS 包携带的内容。

ts_parser.py

#!/bin/env python3
# -*- coding: utf-8 -*-

import time
import binascii

READ_BYTES_LEN = 188

def hex2bin(hex):
    # two hex len is one bytes, then *4 it's bits len
    if hex == '':
        return '00000000'
    else:
        return bin(int(hex, 16)).lstrip('0b').zfill(len(hex) * 4)

def hex2int(hex):
    return int(hex, 16)

def parser_pid(hex):

    pid_hex = hex[2: 6].decode("utf-8")
    if pid_hex == '':
        return "no-pid"
    pid_bin = bin(int(pid_hex, 16)).lstrip('0b')
    pid_int = int(pid_bin[-13:], 2)
    return pid_int

def parser_af(hex):

    af_hex = hex[6: 8].decode("utf-8")
    bit_string = hex2bin(af_hex)
    return bit_string[2: 4]

def parser_pes_prefix(hex):
    if hex[0:6] == b"000001":
        fst_pes = True
    else:
        fst_pes = False

    stream_id = hex[6: 8] if fst_pes == True else '__'
    return [fst_pes, stream_id]

def run(filename):

    pid_cnt = {}
    with open(filename, "rb") as f:
        bytes = True
        pkg_idx = 1
        while bytes:
            bytes = f.read(READ_BYTES_LEN)
            hex = binascii.hexlify(bytes)

            # 获取PID
            pid = parser_pid(hex)

            if pid in pid_cnt:
                pid_cnt[pid] += 1
            else:
                pid_cnt[pid] = 1

            # 获取适配域标识
            af = parser_af(hex)

            # 适配域长度
            if af in ['10', '11']:
                af_len = hex2int(hex[8: 10])
            else:
                af_len = 0

            # 获取Payload部分
            af_real_len = af_len + 1 if af_len else 0
            payload = hex[8 + af_real_len * 2: ]

            # 判断TS包是否包含PES包头及其携带的媒体类型
            fst_pes, stream_id = parser_pes_prefix(payload)

            # if fst_pes:
            #     print("idx:", pkg_idx, "pid:", pid, "af:", af, "af_len:", af_len, "fst_pes:", fst_pes, "stream_id:", stream_id)
            print("idx:", pkg_idx, "pid:", pid, "af:", af, "af_len:", af_len, "fst_pes:", fst_pes, "stream_id:", stream_id)

            # time.sleep(0.1)
            pkg_idx += 1

    print(pid_cnt)


if __name__ == "__main__":

    filename = "faded.ts"
    run(filename)

运行后得到这样的输出:

idx: 1 pid: 17 af: 01 af_len: 0 fst_pes: False stream_id: __
idx: 2 pid: 0 af: 01 af_len: 0 fst_pes: False stream_id: __
idx: 3 pid: 4096 af: 01 af_len: 0 fst_pes: False stream_id: __
idx: 4 pid: 256 af: 11 af_len: 7 fst_pes: True stream_id: b'e0'
idx: 5 pid: 256 af: 01 af_len: 0 fst_pes: False stream_id: __
idx: 6 pid: 256 af: 01 af_len: 0 fst_pes: False stream_id: __
idx: 7 pid: 256 af: 01 af_len: 0 fst_pes: False stream_id: __
idx: 8 pid: 256 af: 01 af_len: 0 fst_pes: False stream_id: __
idx: 9 pid: 256 af: 01 af_len: 0 fst_pes: False stream_id: __
idx: 10 pid: 256 af: 01 af_len: 0 fst_pes: False stream_id: __

...

idx: 169228 pid: 257 af: 01 af_len: 0 fst_pes: False stream_id: __
idx: 169229 pid: 257 af: 01 af_len: 0 fst_pes: False stream_id: __
idx: 169230 pid: 257 af: 11 af_len: 124 fst_pes: False stream_id: __
idx: 169231 pid: 257 af: 11 af_len: 1 fst_pes: True stream_id: b'c0'
idx: 169232 pid: 257 af: 01 af_len: 0 fst_pes: False stream_id: __
idx: 169233 pid: 257 af: 01 af_len: 0 fst_pes: False stream_id: __
idx: 169234 pid: 257 af: 01 af_len: 0 fst_pes: False stream_id: __
idx: 169235 pid: 257 af: 11 af_len: 139 fst_pes: False stream_id: __
idx: 169236 pid: no-pid af: 00 af_len: 0 fst_pes: False stream_id: __
{17: 802, 0: 4068, 4096: 4068, 256: 140647, 257: 19650, 'no-pid': 1}

idx 最后为 169236,因为代码计数从1开始的,减去1,跟最开始用 Hex Editer 查看并计算出的 169235 是吻合的。

同时可以看到,PID 为 256 的有 140647个视频包(e0),PID 为 257 的有 19650 个音频包(c0)。

不过还是有一些暂时还不知道内容,比如有一个包没有 PID,并且 PID 为 17、0、4096 的包,它们保存的是什么数据,因为是 Payload 内容,所以后续还是要先学习 PES 包,才可能解开这些疑问。

TS 概览,先到这里。

参考

  1. https://tsduck.io/download/docs/mpegts-introduction.pdf
  2. https://www.cnblogs.com/renhui/p/10362640.html
  3. https://www.jianshu.com/p/73f005a7d406
  4. https://en.wikipedia.org/wiki/Packetized_elementary_stream