
简介一款专为TS流结构学习与广电故障排查设计的码流分析软件以树形视图完整呈现节目关联表PAT、节目映射表PMT、业务描述表SDT、事件信息表EIT及字幕PES包的结构节点与SI各表字段一一对应适合初学者对照实例理解TS封装原理。工具内置仿真搜台与EPG演示可双击Subtitle节点即时显示字幕图片双击EIT表导出网页报告点击HTML按钮一键保存常用表解析结果支持任意大小码流文件分析。资源共30个文件包括可执行程序、C语言源码、网页组件JS/HTML和操作演示图片压缩包仅161KB轻量易用。已有1911人学习下载。相比同类工具多解析了LCN与Subtitle数据附带的C源码可参考研究TS解析与PSI/SI过滤逻辑配合演示图片能直观理解各表的数据关系适合数字电视开发新人快速上手。1. 码流分析软件与 ts analysis tool它到底在帮你拆什么VLC 能播、ExoPlayer 黑屏或者同一段 .ts 在机顶盒上偶尔卡顿、在电脑上却正常——这类问题最常见的根因不在编解码器而在 TS 结构本身。码流分析软件ts analysis tool就是用来把这层黑匣子打开的它把传输流里的 PID、PAT、PMT、PCR、PTS 这些散落的结构信息汇总成可读的报告让你在五分钟内判断一条码流是“能播”还是“健康”。对做 OTT、DVR、Android 播放器或者数字电视开发的工程师掌握 TS 结构不是面试题里的背诵项而是排障时实打实的基本功。这篇文章从结构讲起再落到可复现的命令、参数和踩坑记录。2. 先看懂 TS 结构再上手工具PAT、PMT、PCR 和四个关键字段2.1 先把“.ts文件”说清楚分片与完整传输流的区别标题里的“ts 结构”实际对应两类常见实物。一类是 HLS 切片产生的 .ts 分片比如 m3u8 列表里的segment_00001.ts每个分片几秒到十几秒不等播放器按列表顺序拉取另一类是数字电视、DVB、卫星接收机里的完整传输流可能是几百 MB 的录播文件也可能是 UDP 组播流里的实时数据。两者的底层容器规范完全一样都是 MPEG-2 Transport StreamISO/IEC 13818-1但使用场景不同分析侧重点也不同。分片分析重点看“分片边界对不对”分片是否从 IDR 帧开始、PAT/PMT 是否在分片头部出现。完整传输流分析重点看“时钟和表是否稳定”PCR 间隔、continuity counter 是否连续、PSI 表重复周期是否合规。对一个 ts analysis tool 来说这两种场景都要覆盖但你先得清楚自己手里的是哪一种否则容易拿分片的标准去要求整条流得出错误结论。TS 对比 MP4 这类文件容器最大的差异是“无索引、无目录”。MP4 有 moov 盒子告诉你每帧在哪、时长多少TS 没有。播放器必须通过 PAT 找到 PMT再通过 PMT 找到音视频 PID最后靠 PES 头里的 PTS/DTS 和 adaptation field 里的 PCR 重建时间轴。这套“先找表、再找流、再对时钟”的过程就是整个 TS 结构分析的骨架。2.2 188 字节传输包ts analysis tool 的分析起点TS 的基本单位是传输包标准长度 188 字节。开头是 4 字节包头后面 184 字节是有效载荷或 adaptation field。所有分析工具的第一步都是先按 188 字节切包、找同步字节 0x47再把包头里的字段解出来。这步做对了后面的表解析才有意义。包头里需要掌握的关键字段不多ts analysis tool 的报表里反复出现的也就这几个字段位宽所在位置作用分析时看什么sync_byte8 bit第 1 字节恒为 0x47用于同步是否出现非 0x47 的错位字节transport_error_indicator1 bit第 2 字节最高位前级设备标记错误包为 1 时说明链路层有丢包/误码payload_unit_start_indicator1 bit第 2 字节次高位为 1 时本包负载是一个 PSI/Section 或 PES 包的起始分析 PAT/PMT 时靠它定位PID13 bit第 2 字节低 5 位 第 3 字节包的唯一标识0x0000 是 PAT其余 PID 由表映射adaptation_field_control2 bit第 4 字节高 2 位01 仅负载10 仅 adaptation11 两者都有决定负载起始位置continuity_counter4 bit第 4 字节低 4 位同一 PID 包序号0~15 循环递增跳变即丢包continuity_counter 是排障时最容易见效的字段。同一 PID 的包序号应当依次递增0,1,2,…,15,0,1…。如果中间跳了两位以上说明这段码流在传输过程中丢了包。这里有个特例adaptation_field_control 为 10包内只有 adaptation field没有有效载荷时不参与递增分析工具需要把这个判断写对否则会误报。另外虽然标准是 188 字节DVB 广播链路里常见 204 字节的包——多了 16 字节里德-所罗门前向纠错码。分析工具一般能自动识别但如果你在抓包或者从 UDP 组播里落盘时把 204 字节的流直接喂给按 188 切包的工具会出现大面积“同步字节丢失”的误报。遇到这种情况先确认工具是否支持--rs或类似开关让它按 204 字节对齐。2.3 PAT、PMT 和 PCR结构是否健康看这三处就够TS 结构之所以让新手觉得难不是规范复杂而是表和数据包之间是引用关系必须一层层查。第一层是 PATProgram Association Table固定承载在 PID 0 上。它本身不包含具体流信息只告诉你“这个传输流里有哪几个节目每个节目的 PMT 在哪个 PID 上”。第二层是 PMTProgram Map Table承载在 PAT 指出的 PID 上。PMT 里有两类关键信息PCR_PID 指向该节目用于时钟重建的包以及一张 ES 流列表。每个 ES 条目包含 stream_type编码类型和 elementary_PID该段流实际的 PID。例如 stream_type 0x1B 是 H.264/AVC0x0F 是 AAC ADTS0x24 是 HEVC。播放器就是靠这张表知道“去哪个 PID 找视频、去哪个 PID 找音频、用什么解码器”。第三层是 PCRProgram Clock Reference存在 adaptation field 里由 33 位 90 kHz 基值加 9 位 27 MHz 扩展组成。播放器用它重建系统时钟再结合 PES 头上的 PTS/DTS 做音视频同步。标准要求 PCR 至少每 100 ms 出现一次实际复用器通常按更短间隔插入。分析工具会统计 PCR 间隔的最大值、抖动情况这两项直接决定播放器会不会出现音画不同步。看一条 TS 是否健康就按这个顺序查PAT 能不能解析出 PMT PIDPMT 能不能解析出 PCR_PID 和 ES 列表PCR 间隔是否合规最后再看 PTS 是否按 90 kHz 时基单调递增。把这串跑通剩下的细节都是在这些结构上的叠加。3. 用一个 ts analysis tool 拆真实码流ffprobe 与 tsduck 的组合打法3.1 最小命令组合先用 ffprobe 看全貌再用 tsduck 看细节工具不在多两个就够。ffprobe 看全貌tsduck 看细节。前者几乎人人都有跟 FFmpeg 一起装的适合快速确认容器格式、流数量、编码类型后者专注于 TS/PSI 深度分析能输出 PAT、PMT、连续性错误、PCR 指标这些 ffprobe 看不到的东西。第一步用 ffprobe 确认“这条流大体上是什么”ffprobe -show_format -show_streams -of json input.ts这段命令会输出容器信息和所有流信息每条流的 index、codec_name、profile、宽高、帧率、time_base、duration 等。拿到这些先核对三件事编码类型是否符合预期视频 PID 数量是否为 1是否有字幕/私有流混在里面。如果 ffprobe 在这里就报错或者读不出流大概率文件本身不完整不用继续往下分析了。第二步用 tsduck 的 tsp 工具拆 PSI 表并统计异常tsp -I file input.ts -P analyze -O droptsp 是 tsduck 的命令行处理工具-I file指定输入文件-P analyze把 PSI/SI 表的解析结果打印到终端-O drop表示分析完成后不输出文件、直接丢弃。跑完你会看到 PAT、PMT、SDT、EIT 等表格的详细内容包括每个表的版本号、重复间隔、PID 分配。如果你装的版本里-P analyze不可用老的 tsduck 发行版里也有独立的 tsanalyze 可执行文件作用相同。第三步如果要针对性排查丢包和时钟问题把插件换成下面这条流水线tsp -I file input.ts -P continuity -P pcr -O drop-P continuity会逐 PID 检查 continuity_counter 的递增关系并输出跳变统计-P pcr统计 PCR 间隔和抖动。这两组输出是判断“这条流传输过程有没有掉包、时钟稳不稳”的直接证据。3.2 读懂分析报告哪几行决定播放命运分析工具输出一大堆表格新手容易被信息淹没。其实只需要盯住几行。PAT 报告里看两点program_number 有没有为 0 的保留项PMT PID 是否指向非 0 PID。如果 PMT PID 为 0 或者 PAT 根本解析不出来播放器会直接判定“找不到节目”表现是黑屏或一直转圈。PMT 报告里看三点PCR_PID 是否指向实际存在的 PIDstream_type 与真实编码是否一致ES 列表是否包含视频和音频。常见翻车是 stream_type 写错比如实际是 H.264 却标成 0x02MPEG-2 Video有些播放器会按错误类型初始化解码器然后报 format 不支持另一些宽容的播放器能自动探测于是同一个文件在不同设备上表现完全不同。连续性报告里看一行有没有 PID 的 lost packet 计数持续增长。如果有说明源端或者传输链路在丢包。至于丢包发生在复用器输出、网络传输还是录制环节需要结合抓包或者播放器日志确认。PCR 报告看两个值最大间隔和抖动。间隔超过 100 ms播放器缓存会被耗尽导致卡顿抖动大音频和视频轨的时钟基准不一致表现为音画逐渐错位。这个指标是硬指标没有灰色地带。3.3 参数与常见误用分析时长、过滤 PID、输出重定向实测中最容易踩的两个参数坑一是分析范围太大二是忘了过滤无关 PID。对大文件全量跑 analyze 浪费时间。tsduck 支持在输入加偏移量常见做法是只分析前一段tsp -I file input.ts -P analyze --max-input-packets 50000 -O drop--max-input-packets限制最多处理 50000 个 TS 包约 9.4 MB 数据足够覆盖几十个 PAT/PMT 周期适合快速确认结构。注意这个参数是限制包数不是时长写脚本时要换算一下。对 UDP 组播流tsp 的输入换成-I ip即可边收边分析。但线上流容易脏建议先 tcpdump 落盘再分析否则重传窗口不够反而漏掉关键表。落盘命令很简单关键是在抓包层不要做包重组保持原始 UDP 负载tsp -I ip 239.1.1.1:1234 -P continuity -O file live_ts.ts误用方面最常见的是拿 ffprobe 的-show_packets输出当 PSI 分析依据。它确实能看到包级别信息但当 TS 包有 adaptation field 时负载起始位置要按 adaptation_field_length 跳过ffprobe 对这些细节的处理不像专门的 TS 工具那么直观。另一个误用是把 HTTP 拉流直接curl存成 .ts 再分析——如果服务端做过分片传输或者 chunked 编码文件头部可能有 HTTP 头残留工具对齐同步字节会翻车。解决办法是分析前用ffmpeg -i重写一遍输出让容器干净起来。4. 把分析结果落到播放链路ts分片与 Media3 ExoPlayer 调试4.1 ts分片是怎么被生成的从半截开始的常见病HLS 的 ts 分片一般由 ffmpeg 的 hls muxer 或商业切片器生成。规范要求每个分片从关键帧开始这样播放器从任意分片切入都能即刻解码。但实际切片器在分片边界的行为差异很大有的在每个分片头部都写 PAT/PMT有的只在流启动时写一次。这就会造成一种典型病症第一个分片能正常播放从第二个分片开始播放器不停向服务器要 PAT/PMT 或者干脆黑屏。用 ts analysis tool 看第二个分片你会发现在分片开头几百个包里找不到 PID 0 的 PAT。原因就是切片器认为“复用器不会重启没必要重复插入 PSI”。这种分片在 VLC 里可能没事因为 VLC 可以跨分片保留之前解析到的表但在 ExoPlayer 或自研播放器里分片是独立加载的可能表的生命周期不共享于是翻车。开发调试时从缓存目录把正在播的 ts 分片捞出来单独分析是个常用手法。步骤不复杂让播放器跑起来从应用缓存目录找到对应分片文件先看文件头有没有 PAT再看分片第一个关键帧离文件起点有多远。用 ffprobe 快速定位ffprobe -show_packets -select_streams v:0 seg_00002.ts | head -30输出里找第一个flagsK的包对比pos字段和文件总长度。如果关键帧离起点超过几百 KB说明这个分片前面塞了大量非视频数据网络带宽会被浪费。这属于结构层面的问题播放器侧很难绕过去。4.2 Media3 ExoPlayer 播放 ts 的关键配置Android 端从 Media3 的 ExoPlayer 播放 ts 分片分两种情况。播 HLS 列表里的 ts 分片用 HlsMediaSource直接播单个 ts 文件用 ProgressiveMediaSource。区别在于前者会解析 m3u8 并按分片加载后者把单个 ts 文件当作连续流从头播到尾。Media3 1.x 的 Kotlin 实现大概是下面这个样子val dataSourceFactory DefaultDataSource.Factory(context) // 场景一播放 HLS 里的 ts 分片 val hlsSource HlsMediaSource.Factory(dataSourceFactory) .setAllowChunklessPreparation(false) .createMediaSource(MediaItem.fromUri(playlistUrl)) // 场景二播放单个 .ts 文件 val progressiveSource ProgressiveMediaSource.Factory(dataSourceFactory) .setTag(ts_local) .createMediaSource(MediaItem.fromUri(fileUri)) player.setMediaSource(hlsSource) player.prepare() player.playWhenReady truesetAllowChunklessPreparation(false)在 Media3 里表示不做无分片预加载强制先读 m3u8 再取分片。如果你的 ts 分片结构不健康、PAT/PMT 出现晚关掉这个选项反而更稳因为它让播放器按标准流程先拿分片再解析表。ProgressiveMediaSource.Factory的setTag只是打个标记便于日志排查不影响播放行为。播放失败时ExoPlayer 的错误回调会给你一个PlaybackException错误码常见两类ERROR_CODE_IO_BAD_HTTP_STATUS说明拉流的问题ERROR_CODE_DECODER_INIT_FAILED说明码流结构和解码器期望不符。后一种情况十有八九要回到 PMT 的 stream_type 上找原因。分析工具里看到 stream_type 是 0x1BH.264但实际 ES 里是 HEVC播放器按 H.264 初始化解码器必然失败。4.3 用分析结论反推播放器行为分析工具给出的结论最终要映射到播放器的表现上才有效。我自己的排查习惯是列一张对应表PAT 缺失对应“找不到节目无限 loading”PMT 里的 PCR_PID 指向不存在的 PID 对应“偶尔卡顿但无报错”continuity counter 跳变对应“画面花屏、断音”PCR 间隔超标对应“长时间播放后音画不同步”。举个例子。有一次排查线上一个 DVR 回放服务用户反馈播放 20 分钟后音画开始错位。抓回录制的 ts 文件用 tsp 统计 PCR发现 PCR 最大间隔 800 ms远超规范要求。原因在于录制服务器的转封装组件对 PCR 做了重映射但复用了源流的 PTS导致播放器时钟基准漂移。这个结论用 ffprobe 看不出来因为流格式完全是合法的只有针对 PCR 的专项统计能暴露问题。另一次排查中PMT 里有两个音频 PID一个 AAC 一个 AC-3播放器默认选了 AAC结果声音断断续续。用分析工具列出两个 PID 的包数量后发现 AC-3 轨的包间隔均匀、AAC 轨丢包严重于是改播放器的音轨选择策略。这类问题不看结构数据就只能靠猜而猜的效率太低了。5. TS 分析的常见翻车现场五分钟排查清单与三个玄学问题5.1 现象到原因一张排查对照表分析 TS 时遇到的现象和根因之间通常存在稳定的对应关系。整理成一张表排查时按图索骥比反复试播放器快得多。现象可能原因验证手段播放器一直转圈不进入播放状态PAT 缺失或 PMT PID 指向错误tsp analyze 看 PID 0 是否有完整 PAT有声音无画面 / 有画面无声音PMT 里 stream_type 与实际编码不符ffprobe 比对 codec_name 与 PMT 表播 10 分钟开始音画不同步PCR 间隔超标或 PCR 抖动过大tsp pcr 查看 max interval / jitter画面花屏、声音断续但不报错传输链路丢包continuity counter 跳变tsp continuity 查看 lost packets 统计同一个文件 VLC 能播、ExoPlayer 黑屏PSI 表生命周期管理策略不同分片头部缺 PAT检查每个分片前 512 字节是否含 0x47 和 PAT拉流时文件能播落盘后打不开录制时包头错位非 188 字节对齐检查文件头是否有非 0x47 字节残留这张表不是万能药但能覆盖日常 80% 的 TS 播放问题。剩下 20% 需要组合多个工具交叉验证比如同时看 continuity 和 pcr 才能定位到“丢包导致 PCR 丢失”这种二次故障。5.2 四步定位法先看表、再看包、再看时间戳、最后再看流碰到一个表现异常的 ts 文件我按固定四步走顺序不能乱。第一步看表。跑tsp -P analyze确认 PAT/PMT 都能解析PMT 里的 PID 都在实际码流中找得到。这一步过滤掉“结构不完整”的最常见病因。第二步看包。跑tsp -P continuity统计各 PID 的连续性错误。重点看视频 PID 和 PCR_PID如果这两个 PID 的包都有跳变几乎可以断定问题出在传输或录制环节而不是复用器。第三步看时间戳。用 tsp 的 pcr 插件或手动解析 PES 头里的 PTS。把相邻 PTS 的差值换算成实际时间正常应该是均匀递增的。如果出现回退或者大幅跳变说明封装端时间戳处理有 bug。第四步看流。这一步回到 ffprobe确认 ES 的编码层本身没有损坏关键帧是否完整、SPS/PPS 是否在关键帧前。前面的表、包、时间戳都正常但播放仍然失败最后才会怀疑到编码层。按这个顺序排查绝大多数问题在第前两步就能定位。5.3 三个容易误判的“玄学”问题第一个玄学本地播放器都正常换到目标设备就黑屏。本地播放器通常实现了错误恢复丢了 PAT 会再等下一个周期甚至能跨文件缓存 PSI 表嵌入式设备播放器往往只解析一次表错过就不再重试。对策是不要用本地播放器“能播”来证明码流没问题只信分析工具的报表。第二个玄学同一码流用 A 工具分析报 PCR 抖动用 B 工具报正常。这类冲突通常源于工具对 PCR 的采样方式不同。A 工具统计的是 PCR 与 PTS 的偏差B 工具只统计 PCR 间隔。两者衡量的是不同指标不是互相矛盾。看清楚报告里“统计口径”一栏再下结论。第三个玄学continuity counter 有跳变但图像完全正常。可能原因有两个跳变发生在私有 PID 或空包 PID 上不影响音视频或者跳变发生在 adaptation field 为 10 的包上按规范本就不参与递增工具没过滤导致误报。前者看丢包 PID 列表就能排除后者需要确认工具是否正确处理了 adaptation_field_control。这两个坑我都踩过第一次误判成“播放器能容忍丢包”其实是工具统计口径问题。6. 给自己写一个最小 TS 侦查脚本把结构规则变成可复现工具工欲善其事必先利其器。与其依赖图形界面工具不如写一个能打印 PID 分布、解析 PAT/PMT、统计 continuity 错误的最小脚本。这既是验证你对结构的理解也是将来排查时的第一道防线。脚本思路很简单按 188 字节切包解包头遇到 PID 0 就解析 PAT从 PAT 拿到 PMT PID 后继续解析 PMT最后汇总每个 PID 的包数和连续性错误。完整代码不算长可以放进自己的工具库import sys from collections import defaultdict TS_SIZE 188 SYNC 0x47 def packet_info(pkt): pusi (pkt[1] 0x40) ! 0 pid ((pkt[1] 0x1F) 8) | pkt[2] afc (pkt[3] 4) 0x03 cc pkt[3] 0x0F return pusi, pid, afc, cc def payload_start(pkt, afc): if afc 1: return 4 if afc 3: return 5 pkt[4] return -1 def parse_pat(sec): # sec[0] table_id, sec[1:3] section_length, sec[3:5] tsid # sec[5] version, sec[6] sec_num, sec[7] last_sec res [] pos 8 while pos 4 len(sec): prog (sec[pos] 8) | sec[pos1] pid ((sec[pos2] 0x1F) 8) | sec[pos3] if prog ! 0: res.append((prog, pid)) pos 4 return res def parse_pmt(sec): pcr_pid ((sec[8] 0x1F) 8) | sec[9] info_len ((sec[10] 0x0F) 8) | sec[11] streams [] pos 12 info_len while pos 5 len(sec): st sec[pos] epid ((sec[pos1] 0x1F) 8) | sec[pos2] es_len ((sec[pos3] 0x0F) 8) | sec[pos4] streams.append((st, epid)) pos 5 es_len return pcr_pid, streams def main(): if len(sys.argv) 2: print(usage: python ts_inspect.py input.ts) return data open(sys.argv[1], rb).read() start data.find(bytes([SYNC])) pkt_cnt, pmt_pids 0, set() pid_counter, last_cc, cc_err defaultdict(int), {}, defaultdict(int) streams, pcr_pids set(), set() pos start while pos TS_SIZE len(data) and data[pos] SYNC: pkt data[pos:posTS_SIZE] pusi, pid, afc, cc packet_info(pkt) pkt_cnt 1 pid_counter[pid] 1 if pid in last_cc and last_cc[pid] ! cc and cc ! (last_cc[pid] 1) % 16: cc_err[pid] 1 if afc 1 or afc 3: last_cc[pid] cc off payload_start(pkt, afc) if off 0 or not pusi: pos TS_SIZE continue # PSI section 可能是跨包切开的这里只解析单包完整的 section if pid 0: sec_len ((pkt[off1] 0x0F) 8) | pkt[off2] for prog, pmt_pid in parse_pat(pkt[off:off3sec_len]): pmt_pids.add(pmt_pid) print(f[PAT] program{prog} pmt_pid0x{pmt_pid:04x}) elif pid in pmt_pids: sec_len ((pkt[off1] 0x0F) 8) | pkt[off2] pcr_pid, stream_list parse_pmt(pkt[off:off3sec_len]) pcr_pids.add(pcr_pid) for st, epid in stream_list: streams.add((st, epid)) print(f[PMT] pcr_pid0x{pcr_pid:04x} stream_type0x{st:02x} es_pid0x{epid:04x}) pos TS_SIZE print(f[packets] total{pkt_cnt}) for pid in sorted(pid_counter): tag if pid 0: tag PAT elif pid in pmt_pids: tag PMT elif pid in pcr_pids: tag PCR print(f0x{pid:04x}: {pid_counter[pid]} packets{tag} cc_errors{cc_err[pid]}) print(f[streams] {[(hex(st), hex(epid)) for st, epid in sorted(streams)]}) if __name__ __main__: main()脚本里有个已知边界PSI section 可能被 TS 包切在中间单包内不一定装得下完整 section。真实广播流里 PAT 很短几乎总在一个包内完成但 PMT 在 ES 信息较多时可能跨包。碰到解析不出来的情况不要慌换成熟的工具做交叉验证即可。这个脚本的价值在于把包头解析、表映射、计数检查这三层逻辑串起来跑通了你对 TS 结构的理解就从“看过文章”变成了“能动手复现”。验证脚本对不对拿两条已知码流测试一条是 ffprobe 能正常读出的标准文件脚本输出的 PID 汇总应与 ffprobe 的流信息一致另一条是你故意截断的文件脚本应该在连续性错误计数上给出变化。我现在拿到陌生 ts 文件的第一反应就是先跑一遍这个最小脚本再决定要不要上 tsduck 全套。希望这个习惯和这篇文章能帮到你。本文还有配套的精品资源点击获取