ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SRT推流黑屏排查:MPEG-TS中PAT与PMT表的原理和工程实践

SRT推流黑屏排查:MPEG-TS中PAT与PMT表的原理和工程实践 有一次帮客户排查SRT推流间歇性黑屏抓包看了半天网络丢包率只有0.2%SRT重传也正常可拉流端就是动不动黑一下。最后把PID 0x0000的包统计出来发现PAT每5秒才发一次PMT更离谱经常被切片切断。这不是网络问题是推流端对MPEG-TS里PAT表和PMT表的生成策略出了问题。SRT协议本身只是一个可靠的“货运通道”它不关心你运的是H.264还是AAC而MPEG-TS里负责告诉解码器“货在哪儿”的正是PAT和PMT这两张PSI表。这篇文章就把SRT封装MPEG-TS时最基础也最关键的PAT/PMT结构从头到尾拆一遍从字段定义、解析实例到抓包验证最后把我在实际项目中踩过的四个坑完整复盘。适合做SRT推流、拉流端开发、转封装网关的工程师也适合被TS黑屏问题折磨的运维同学。1. 搞SRT传输为什么要先死磕PAT和PMT1.1 SRT只负责把货送到不负责解释货SRTSecure Reliable Transport本质上是一个基于UDP的高可靠传输协议相比传统TCP它在丢包环境下表现更好延时更低相比纯UDP裸传它又有ACK重传、流量控制、加密等机制。但有一点很容易被忽略SRT对上层载荷的语义完全透明它不管载荷是TS、FLV还是裸H.264也没有能力告诉你“这个流里有几个节目、视频PID是多少”。在实际工程里SRT最常见的载荷就是MPEG-TS。原因很简单TS是广播时代就定型的封装格式包长固定一般是188字节支持多路节目复用编解码器、硬件编码器、CDN、播放器的兼容性都极好。所以你会发现很多推流软件、硬件编码器、SRT网关最终输出都是“SRT MPEG-TS”的组合。这时候问题就来了既然SRT不解释货那接收端要怎么从一堆TS包里还原出画面答案就是TS流里的PSIProgram Specific Information表其中最核心的两张就是PAT和PMT。任何MPEG-TS解复用器第一步一定是找PAT第二步是找PMT第三步才轮到真正的音视频PES数据。如果这两张表有问题后面所有解码流程都无从谈起。1.2 接收端靠PAT/PMT完成“寻址”可以这么理解SRT是快递车MPEG-TS是货箱PAT和PMT就是货箱外面的总清单和箱子内部的细目表。接收端的处理流程是这样的在所有TS包里找PID等于0x0000的包这里面装的是PAT。解析PAT得到整个TS里所有节目的program_number以及每个节目对应的PMT_PID。如果用户要看节目1就去订阅PMT_PID对应的TS包拿到PMT。解析PMT得到这个节目内部所有流的stream_type和elementary_PID。根据音视频PID订阅对应TS包组成PES送解码器解码。也就是说PAT是“总目录”PMT是“某本书的目录”。没有PAT接收端连有几个节目都不知道没有PMT就算PID在哗哗地传也无法知道哪个PID是视频、哪个PID是音频、各自是什么编码格式。我为什么强调“死磕”这两个表因为太多黑屏、花屏、切换频道卡顿的问题最后定位到根因都是PSI表没做好。很多自研推流器只会在启动时发一次PAT/PMT或者把PAT里的PMT_PID写错又或者PMT里的stream_type与真实编码不符。这些问题在局域网低延迟测试时可能不明显一旦走到SRT跨网传输、丢包、延迟波动、接收端随时加入的场景立刻暴露无遗。2. 拆开PAT节目号到PMT PID的映射表2.1 从TS包里把PAT捞出来MPEG-TS的最小单位是TS包固定188字节。所有TS包都以同步字节0x47开头紧接着是PID等信息。判断一个TS包是不是PAT只需要看它的PID是不是0x0000。一个TS包的前4字节结构是这样的位域长度含义sync_byte8 bit固定0x47transport_error_indicator1 bit传输错误标记payload_unit_start_indicator1 bitPUSI为1表示本包包含PES或PSI section的起始transport_priority1 bit优先级PID13 bit包标识PAT固定为0x0000transport_scrambling_control2 bit加扰控制adaptation_field_control2 bit适配字段控制01表示仅载荷11表示有适配字段和载荷continuity_counter4 bit连续计数所以代码里判断PAT非常简单先读5个字节含sync取PID ((data[1] 0x1F) 8) | data[2]如果等于0x0000且payload_unit_start_indicator等于1就说明这个TS包里装着PAT section的起始部分。但这里有个坑PAT section可能被切分到多个TS包也可能在一个TS包的payload里已经包含了pointer_field。每个PSI/PMT包的payload第一个字节一定是pointer_field表示从下一个字节开始有多少个字节是填充数据。绝大多数情况下pointer_field为0意味着section紧跟着就开始。解析时不要忘记跳过它。2.2 PAT section字段逐个解释拿到PAT的section数据后结构如下字段长度含义table_id8 bitPAT固定为0x00section_syntax_indicator1 bit固定为1保留位1 bit通常为0保留位2 bit通常为11section_length12 bit从transport_stream_id到CRC之前的总长度transport_stream_id16 bit传输流标识用于区分TS版本号/当前标记1 byte高5位是version_number低1位是current_next_indicatorsection_number8 bit当前section号last_section_number8 bit最后一个section号节目循环4字节每条每个节目对应一项见下面CRC3232 bit对整段section做校验节目循环里的每一条是4字节前16 bit是program_number表示节目号。program_number为0时比较特殊表示后面的PID不是PMT而是NIT的network_PID一般点对点直播用不到。后16 bit中高3位是保留位低13位是program_map_PID即PMT所在的PID。举个例子假设我抓到一个PAT section的payload如下不含TS头、不含pointer_field00 B0 0D 00 01 C1 00 00 00 01 E1 00 1A 2B 3C 4D逐个看00table_id0x00确实是PAT。B0 0Dsection_syntax_indicator为1section_length0x00D即从transport_stream_id开始一共13个字节。00 01transport_stream_id1。C1version_number0current_next_indicator1。00section_number0。00last_section_number0。00 01program_number1。E1 00高3位保留为111低13位是0x0100即PMT_PID0x0100。最后的4字节是CRC32。所以从这个PAT可以读出来的信息是TS里有一个节目节目号是1它的PMT在PID 0x0100上。2.3 PAT里那些容易被忽略的细节version_number这个字段特别容易踩坑。PAT的内容一旦发生变化比如增删节目、改变PMT_PID必须把version_number加1。接收端会缓存当前版本的PAT如果检测到version_number没变即使你实际发了新的PAT很多解复用器也会因为“版本没变”而直接丢弃或沿用旧缓存。这个特性本身是协议设计上的优化但也是很多诡异故障的根源。section_number和last_section_number用于多section的PAT。PAT的总长度不能超过1021字节section_length是12位但一个节目条目只占4字节所以在绝大多数SRT场景下PAT都是单sectionsection_number和last_section_number都是0。如果哪天你看到一个PAT的last_section_number大于0说明这个TS里节目多到爆了接收端必须把多个section全部收齐才能拿到完整节目列表。还有一点PAT作为一个独立PID它自己的continuity_counter必须严格按照0-15循环递增。有些自研复用器不重视PSI包的连续性导致接收端误以为PAT包丢失进而影响PAT重建。这个细节后面还会再提。3. 拆开PMT节目内部的“音视频流菜单”3.1 PMT section格式和stream_type找到PMT所在PID后会用和解析PAT类似的方式组装PMT section。PMT的table_id是0x02section整体结构和PAT类似但节目循环变成了“流描述循环”。字段长度含义table_id8 bitPMT固定为0x02section_length12 bit后面数据长度program_number16 bit关联的节目号必须与PAT里的program_number一致版本号/当前标记1 byte版本号current_next_indicatorsection_number8 bitsection编号last_section_number8 bit最后一个section编号PCR_PID13 bit承载PCR的TS包PIDprogram_info_length12 bit节目级描述符长度流循环每条至少5字节每个基本流描述CRC3232 bit校验流循环里每个ES都有如下字段stream_type8 bit表示编码类型。elementary_PID13 bit这个流的数据在哪个PID。ES_info_length12 bit描述符长度。ES_info描述符数据。常见的stream_type值stream_type含义0x01MPEG-1 Video0x02MPEG-2 Video0x03MPEG-1 Audio0x04MPEG-2 Audio0x0FAAC音频ADTS0x1BH.264/AVC视频0x24HEVC/H.265视频0x06私有数据AC-3/E-AC-3/字幕等常挂这里3.2 一个H.264 AAC节目的PMT实例假设有一个SRT推流里面只有一路节目编码是H.264视频和AAC音频。设计PID如下PAT PID0x0000PMT PID0x0100视频ES PID0x0101音频ES PID0x0102PCR_PID0x0101也就是视频PIDPMT section的解析结果应该是字段值program_number1PCR_PID0x0101stream_1 type0x1BH.264elementary_PID_10x0101stream_2 type0x0FAACelementary_PID_20x0102接收端拿到这个PMT后就知道要同时订阅PID 0x0101和0x0102并根据stream_type分别送入视频和音频解码器。PCR_PID这个字段很多人不理解。PCRProgram Clock Reference是TS流中的参考时钟解码器需要它来同步音视频、恢复27MHz时钟。PCR并不是单独一个PID在发而是放在某个TS包的adaptation_field里。PCR_PID告诉接收端“你要去哪个PID上找PCR”。最常见做法是让PCR_PID等于视频ES PID也就是PCR随着视频包一起发送。如果PCR_PID指向一个不存在的PID或者指向的PID上根本不放PCR解码器会出现音画不同步、画面卡顿等问题。3.3 PMT的版本和节目切换机制PMT的version_number规则和PAT一样内容一变就加1。在SRT场景下最常见的情况是音视频PID不变但编码参数变了比如分辨率从1080p切到720p。严格来说这种编码参数变化不一定要改PMT但如果stream_type变了比如从H.264换成HEVC就一定要升版本否则接收端旧缓存里的PMT会指示解码器用H.264处理HEVC码流画面必然崩。再说节目切换。如果这个TS流里有多个节目PAT里会有多条program_number到PMT_PID的映射每个节目对应一个独立的PMT甚至每个节目的PCR_PID可能都不同。播放器切换节目时做的事情就是重新查一遍PAT和PMT然后改变订阅的ES PID集合。SRT点对点直播一般只传一个节目但当你做SRT转播网关、多节目复用器时这套逻辑就是核心。4. SRT封装TS时PAT/PMT的插入策略和边界问题4.1 SRT包与TS包不是天然对齐的SRT协议本身不规定载荷如何切片但绝大多数现成实现会把多个完整的TS包顺序塞进一个SRT包以尽量贴近UDP MTU。比如7个188字节的TS包正好是1316字节再加上SRT包头通常在1500字节以内这样就不会触发IP分片。但你在写解析器时不能想当然。抓包时常见的情况有几种一个SRT包里放了固定数量的TS包比如4个、5个、7个。一个SRT包里只有一个TS包适合极低码率或要求极低延时的场景。部分硬件设备在SRT payload里先放一个自定义扩展头比如带时间戳、序列号然后才是TS数据。所以解析SRT载荷里的TS流第一件事不是直接按188字节切而是先确认起始位置。最务实的做法是在SRT payload里扫描0x47找到后开始按188字节切如果尾部不足188字节缓存下来和下一个SRT包拼接再继续解析。这个过程我会在第5章用代码展示。反过来说好的SRT封装实现应该保证一个TS包不能横跨两个SRT包否则任何一个SRT包丢失即使有重传接收端也需要跨包重组逻辑复杂度会高很多。实际我用过的主流SRT库和硬件编码器都不会让TS包跨SRT包。4.2 PAT/PMT多久插一次才合理这是整个SRT封装TS流里最容易出问题的点。很多跑单播的人会有一种思维接收端一开始就连接了我只要在开头发一次PAT/PMT不就行了不行。SRT虽然是面向连接的协议但接收端可能随时加入也可能在传输过程中因为网络闪断触发SRT重连重连后它必须重新获取PSI。就算不重连UDP丢包也可能导致一次PAT/PMT直接丢了。我的习惯是PAT和PMT的重复周期控制在100毫秒到200毫秒之间。DVB标准要求通常是几百毫秒以内但SRT常用于低延迟场景接收端要秒开所以100毫秒比较稳。你可以自己算一下100ms内即使PATPMT加起来200字节1秒也才2KB在几Mbps码率里完全可以忽略。具体实现上我喜欢在复用器里维护两个计时器而不是简单数TS包。因为TS包发送速率和码率有关码率低的时候包少可能1秒都凑不齐固定包数。更稳的做法是每100ms强制插一个PAT section紧接着的下一组插入PMT section两者也可以都在同一个TS包周期内各发一次。如果担心PES包被打断可以在两个section发送之间间隔几个TS包但间隔不要太大。另外提醒一句PAT和PMT不要做成只在关键帧前后插入。那样一旦接收端错过了I帧区域可能要等下一个GOP才能拿到PSI秒开率会很差。4.3 PID规划和PCR频率封装TS流之前先做PID规划。不要把音视频PID随便设。例如用途PIDPAT0x0000PMT0x0100视频ES0x0101音频ES0x0102空包0x1FFF这个规划的好处是所有PID都是0x01xx在wireshark里一眼就能认出来而且避开了0x0001CAT、0x0002等保留常用段。PMT PID其实可以选任何非0值但不要用0x1FFF空包也不要用0x0000PAT。PCR发送频率要结合你的编码器时钟。PCR必须放在adaptation_field里且PCR_PID指向的PID要有足够密的包来承载它。我见过的很多实现是视频帧的每个TS包都携带PCR或者每几十个TS包带一次。对于低延迟直播PCR间隔建议在几十毫秒量级太稀疏会导致解码器时钟抖动太密集会白白占用包内空间。5. 实战抓包并用Python逐字节验证PAT/PMT5.1 抓包准备先用Wireshark抓SRT端口比如推流端发到9000端口抓包Filter就写udp.port 9000抓到包之后选中几个SRT数据包不要只看Wireshark帮你解析好的信息那会把问题藏掉。右键选择“导出分组字节流”或“Export Packet Bytes”把UDP payload原始数据保存成二进制文件存下来后面用Python读。如果抓到的数据是SRT包payload的前面是SRT包头不是0x47。不要慌这正是验证封装方式的好机会。自己先看一眼文件开头如果能直接看到连续0x47说明设备没有额外扩展头如果前几个字节是杂数据后面才出现0x47说明有自定义扩展头或SRT头还没剥掉。5.2 一个可直接跑的TS解析脚本下面这段Python代码可以读取包含原始UDP payload的文件假设文件里有一串TS包可能带也可能不带SRT头自动扫描0x47按188字节切包解析出PAT和PMT并打印节目列表和流列表。import struct import sys def read_ts_packets(data): packets [] i 0 # 扫描第一个0x47 while i len(data) - 188: if data[i] 0x47: # 粗略校验如果这真的是TS包那么188字节后大概率也是0x47 if i 188 len(data) and data[i 188] 0x47: packets.append(data[i:i188]) i 188 continue i 1 return packets def parse_ts_header(pkt): if pkt[0] ! 0x47: return None pid ((pkt[1] 0x1F) 8) | pkt[2] pusi (pkt[1] 0x40) ! 0 afc (pkt[3] 0x30) 4 return pid, pusi, afc def ts_payload(pkt, afc): if afc 0x01: # 仅payload return pkt[4:] elif afc 0x03: # 有适配字段和payload af_len pkt[4] return pkt[5af_len:] elif afc 0x02: # 仅适配字段 return b else: return b def parse_pat(section): if section[0] ! 0x00: return None section_length ((section[1] 0x0F) 8) | section[2] tsid struct.unpack(H, section[3:5])[0] version (section[5] 1) 0x1F print(fPAT: transport_stream_id{tsid}, version{version}, section_length{section_length}) idx 8 end 3 section_length - 4 # 去掉CRC while idx end: program_number struct.unpack(H, section[idx:idx2])[0] pmt_pid ((section[idx2] 0x1F) 8) | section[idx3] if program_number 0: print(f network_PID0x{pmt_pid:04X}) else: print(f Program {program_number} - PMT PID 0x{pmt_pid:04X}) idx 4 def parse_pmt(section): if section[0] ! 0x02: return None section_length ((section[1] 0x0F) 8) | section[2] program_number struct.unpack(H, section[3:5])[0] version (section[5] 1) 0x1F pcr_pid ((section[8] 0x1F) 8) | section[9] prog_info_len ((section[10] 0x0F) 8) | section[11] print(fPMT: program_number{program_number}, version{version}, PCR_PID0x{pcr_pid:04X}) idx 12 prog_info_len end 3 section_length - 4 while idx end: stream_type section[idx] es_pid ((section[idx1] 0x1F) 8) | section[idx2] es_info_len ((section[idx3] 0x0F) 8) | section[idx4] print(f stream_type0x{stream_type:02X}, PID0x{es_pid:04X}, es_info_len{es_info_len}) idx 5 es_info_len if __name__ __main__: data open(sys.argv[1], rb).read() ts_packets read_ts_packets(data) print(ffound {len(ts_packets)} ts packets) pat_sections [] pmt_sections [] for pkt in ts_packets: hdr parse_ts_header(pkt) if not hdr: continue pid, pusi, afc hdr payload ts_payload(pkt, afc) if not payload: continue # 跳过pointer_field pointer payload[0] section_start 1 pointer if pusi and section_start len(payload): if pid 0x0000: pat_sections.append(payload[section_start:]) # PMT PID需要动态获取这里先收集所有PID为0x0100的PSI包 elif pid 0x0100: pmt_sections.append(payload[section_start:]) for s in pat_sections: parse_pat(s) for s in pmt_sections: parse_pmt(s)这段脚本是演示性的PMT PID在脚本里写死成0x0100实际使用时应该先解析PAT拿到PMT PID然后动态收集。但作为验证工具足够直观。跑一下如果能看到类似输出found 1423 ts packets PAT: transport_stream_id1, version0, section_length13 Program 1 - PMT PID 0x0100 PMT: program_number1, version0, PCR_PID0x0101 stream_type0x1B, PID0x0101, es_info_len5 stream_type0x0F, PID0x0102, es_info_len0说明PAT/PMT层面是完全正常的黑屏问题基本可以排除PSI的原因可以继续查PES或解码器。5.3 用ffprobe做快速验证如果你不想写代码可以直接用ffprobe验证SRT流ffprobe -v trace -i srt://127.0.0.1:9000?modelistener -show_programs -show_streams或者用ffplay拉流看控制台是否打印出正确的Program 1、Stream #0:0等。ffprobe的优势是能直接从SRT流里解复用TS并把PAT/PMT的信息解析成可读格式。如果ffprobe都打不出stream信息那PSI基本就是有问题的。另外dvbsnoop也是解析PSI的老牌工具。它可以读原始TS文件直接打印PAT/PMT字段很适合在嵌入式环境里做交叉验证。5.4 验证PAT/PMT是否健康的四个检查点我每次收到一个SRT流会先按下面四条过一遍sync字节0x47是否连续间隔是否是188字节。如果不是先排查封装对齐问题。PID 0x0000是否周期性出现频率是否在100ms到500ms之间。PAT里的PMT PID和实际PMT包是否对应program_number是否和你预期一致。PMT里所有ES PID在抓包文件里是否都有实际数据并且stream_type和编解码器输出一致。只要这四条都通过我基本可以放心地说这个SRT流的“寻址”没有问题后续问题大概率在编码参数、时间戳或解码端。6. 因为PAT/PMT踩过的四个坑与完整排查链路6.1 黑屏但抓包里有大量TS数据PAT插入太稀疏这个场景我就开篇说的案例。现象是接收端黑屏但Wireshark里能看到视频PID在持续传数据。当时我第一反应是编码器有问题结果拿到抓包文件后统计了各PID的包数发现PID 0x0000的包在整个30秒文件里只出现了6次。算下来PAT每5秒才一次。为什么这会导致黑屏因为SRT接收端可能在任意时刻开始解TS。如果接收端启动的一瞬间没有等到PAT就必须一直等下一个PAT。5秒一次的PAT意味着用户拉流后平均要等2.5秒才可能开始解析加上网络抖动、解码器Buffer黑屏时间会更长。更极端的情况是如果某个PAT包在SRT重传之前被上层丢弃接收端又要多等5秒。修复方法很简单把PAT/PMT重复周期强制改到100ms。我也顺便把PMT的插入逻辑从“关键帧后插入”改成了“独立定时器插入”改完后再抓包黑屏问题彻底消失。这个坑再次说明一个道理PSI表不是发一次就完事的它在流媒体协议里的角色相当于“心跳”必须持续存在。6.2 多路节目合流后PID冲突另一个项目里我要把两路SRT源合并成一路TS输出给下游。两路源分别是两个不同地区的编码器编码器产出的TS流视频PID恰好都是0x0101。合流时如果直接复制TS包虽然PAT里给每个节目分配了不同的PMT PID但两个节目的ES PID却相同接收端就乱了。当时的表现是切换到节目1正常切到节目2后画面会偶发花屏再切回节目1也可能花屏。抓包看到两个节目里的PID 0x0101都对应H.264视频但显然不是同一路内容。解复用器订阅PID 0x0101后两个节目的视频包混在一起普通播放器完全没有能力区分。排查链路先打印PAT确认两个节目的PMT PID确实不同再打印两个PMT发现节目2的PMT里也写着视频PID 0x0101。这时候必须做PID重映射把节目2的视频PID改成0x0201音频改成0x0202并同步更新PMT里的elementary_PID和PCR_PID。改完后花屏消失。教训就是任何合流、转封装、PID过滤操作都必须全量检查所有PID的唯一性不能默认当前源就是规范的。6.3 SRT payload前面有自定义扩展头按188字节对齐全失败某次对接一个硬件编码器对方说支持SRT协议但我的拉流端解析出来的TS全是乱码找不到连续0x47。我一开始怀疑是SRT包被切得很碎一个TS包被拆分到两个SRT包里但统计后发现每个SRT包长度都一样很有规律。排查方法很土把第一个SRT包的完整payload用十六进制打开从偏移0开始逐个位置找0x47。结果在偏移12字节的位置看到了0x47再往后每188字节都出现0x47。这说明SRT头之后还有一个12字节的自定义扩展头随后才是标准TS包。为什么这个坑很容易踩因为很多SRT封装实现并为严格按Haivision参考设计的裸TS载荷而是在TS前面附加了扩展时间戳、帧序号、加密标记等信息。你的代码如果写死“从SRT payload偏移0开始就是TS”遇到这类设备必然失败。正确处理是解析时先做“0x47滑动窗口扫描”。从UDP payload的不同偏移位置试着切188字节如果能连续多包命中0x47就认为找到TS起始偏移。一旦确认这个偏移后面就固定下来。不要每次解析都全量扫描那样性能太差。6.4 改了音频PID但解码器一直用旧参数这个坑更隐蔽。有一回我们在推流端把音频PID从0x0102改成0x0112同时同步改了PMT里的elementary_PID。改完后重新推流测试拉流端发现只有画面没有声音日志里显示音频PID仍然订阅0x0102。我一开始以为是新配置没生效抓包看了PMT发现网上发的PMT里确实已经写成了0x0112。那问题出在哪后来仔细看PMT的version_number发现推流器在修改PMT内容时没有递增版本号。接收端在内存里已经缓存了旧version的PMT当它收到新PMT时看到version还是原样直接认为是重复section丢弃了于是继续沿用旧PMT导致一直订阅旧的音频PID。修复很简单把PMT version_number加1同时PAT version_number也加1虽然PAT内容没变但有些严格接收端要求PAT/PMT版本同步更新。改完后音频立刻恢复。这个坑提醒我们PAT/PMT的解析不能只看内容还要监控version_number。做推流器时PSI表生成逻辑里必须把“版本管理”当成一等公民不能只改内容不升版本。7. 最后留几个PAT/PMT相关的小技巧如果你也被SRTTS的黑屏、卡顿问题折腾过分享几个我自己的调试习惯。上线前我会在推流端启动后立刻抓前1秒左右的SRT包用类似上面的脚本做一个“三秒预检”把PAT/PMT打印出来看一眼。重点确认PAT/PMT周期够不够密PMT_PID对不对stream_type有没有和编码器配置对应。这一步成本极低但能挡掉90%的PSI低级问题。故障排查时优先在Wireshark里看两个统计PID 0x0000的出现频率以及实际音视频PID的连续性。如果PAT频率正常但拉流端还是黑屏就去查PMT如果PMT正常再去查PES的时间戳和PCR基本能把问题锁定在一个很小的范围里。还有一个很多人不知道的小技巧有些解码器在黑屏时会给日志比如“no PMT found”或者“PAT timeout”。这类日志已经帮你定向到了PSI层直接抓包看PAT/PMT不要浪费时间在编码器上。SRT把TS流当成一个可靠管道里的字节流可靠性由SRT保证但“节目清单”必须由PAT/PMT自己负责。这个关系理解透了那些看似诡异的黑屏问题其实都逃不过两张表的手掌心。
返回列表