├── .gitignore ├── LICENSE ├── README.md ├── common.py ├── requirements.txt ├── setup.sh ├── ts.py └── ts_inspect.py /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /include 3 | /lib 4 | /lib64 5 | __pycache__ 6 | .idea 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, CableLabs, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MPEG-TS Inspector 2 | 3 | This is a debugging tool that parses and outputs the contents of MPEG-TS files. Currently, it understands TS packets, PAT sections, PMT sections, and PES packets. 4 | 5 | ## Setup 6 | 7 | You will need `git` and `python3` installed. 8 | 9 | git clone https://github.com/brendanlong/mpeg-ts-inspector.git 10 | cd mpeg-ts-inspector 11 | ./setup.sh # installs a Python 3 virtualenv with bitstring and crcmod 12 | 13 | ## Usage 14 | 15 | Activate the virtualenv with: 16 | 17 | source bin/activate 18 | 19 | See current options with `./ts_inspect.py -h`. 20 | 21 | ## Examples 22 | 23 | ### Show all TS packets, PES packets, PAT sections and PMT sections 24 | 25 | ./ts_inspect.py --show-ts --show-pes --show-pat --show-pmt somefile.ts 26 | 27 | ### Show TS and PES packets for PID's 33 and 34 28 | 29 | ./ts_inspect.py --show-ts --show-pes --filter 33,34 somefile.ts 30 | -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from base64 import b64encode 4 | 5 | 6 | class ReprType(object): 7 | """An object that should be inherited from to avoid being converted to a 8 | JSON dict.""" 9 | 10 | # http://stackoverflow.com/a/4256027/212555 11 | def del_none(o): 12 | """ 13 | Delete keys with the value ``None`` in a dictionary, recursively. 14 | 15 | This alters the input so you may wish to ``copy`` the dict first. 16 | """ 17 | if isinstance(o, dict): 18 | d = o.copy() 19 | else: 20 | d = o.__dict__.copy() 21 | for key, value in list(d.items()): 22 | if value is None: 23 | del d[key] 24 | elif isinstance(value, dict): 25 | del_none(value) 26 | return d 27 | 28 | 29 | def _to_json_dict(o): 30 | if isinstance(o, bytes): 31 | try: 32 | return o.decode("ASCII") 33 | except UnicodeError: 34 | return b64encode(o) 35 | if isinstance(o, set): 36 | return list(o) 37 | if isinstance(o, ReprType): 38 | return repr(o) 39 | return o.__dict__ 40 | 41 | 42 | def to_json(o): 43 | return json.dumps(del_none(o), default=_to_json_dict, indent=4) 44 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bitstring 2 | crcmod 3 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | virtualenv -p python3 . 3 | source bin/activate 4 | pip install -r requirements.txt 5 | 6 | echo "You probably want to 'source bin/activate' now." 7 | -------------------------------------------------------------------------------- /ts.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from itertools import chain, count 3 | import logging 4 | import struct 5 | import zlib 6 | 7 | import bitstring 8 | from bitstring import BitArray, BitStream 9 | from common import ReprType, to_json 10 | import crcmod 11 | 12 | 13 | crc32 = crcmod.predefined.mkCrcFun("crc-32-mpeg") 14 | 15 | 16 | class read_ts(object): 17 | def __init__(self, file_name): 18 | self.file = open(file_name, "rb") 19 | self.byte_offset = 0 20 | 21 | def __iter__(self): 22 | return self 23 | 24 | def __next__(self): 25 | try: 26 | ts_data = self.file.read(TSPacket.SIZE) 27 | if not ts_data: 28 | raise StopIteration() 29 | return TSPacket.parse(ts_data, self.byte_offset) 30 | finally: 31 | self.byte_offset += TSPacket.SIZE 32 | 33 | 34 | def read_pes(media_segment, initialization_segment=None): 35 | pmt_pids = set() 36 | pes_readers = {} 37 | for segment in initialization_segment, media_segment: 38 | if not segment: 39 | continue 40 | for ts_packet in chain(read_ts(segment), [None]): 41 | if ts_packet.pid == ProgramAssociationTable.PID: 42 | pat = ProgramAssociationTable(ts_packet.payload) 43 | pmt_pids.update(pat.programs.values()) 44 | 45 | elif ts_packet.pid == pmt_pid: 46 | pmt = ProgramMapTable(ts_packet.payload) 47 | for pid in pmt.streams: 48 | if pid not in pes_readers: 49 | pes_readers[pid] = PESReader() 50 | 51 | elif ts_packet.pid in pes_readers: 52 | pes_packet = pes_readers[ts_packet.pid].add_ts_packet(ts_packet) 53 | if pes_packet: 54 | yield pes_packet 55 | 56 | 57 | def read_timestamp(name, data): 58 | timestamp = data.read("uint:3") 59 | if not data.read("bool"): 60 | raise Exception("First marker bit in {} section of header is not " 61 | "1.".format(name)) 62 | timestamp = (timestamp << 15) + data.read("uint:15") 63 | if not data.read("bool"): 64 | raise Exception("Second marker bit in {} section of header is not " 65 | "1.".format(name)) 66 | timestamp = (timestamp << 15) + data.read("uint:15") 67 | if not data.read("bool"): 68 | raise Exception("Third marker bit in {} section of header is not " 69 | "1.".format(name)) 70 | return timestamp 71 | 72 | 73 | class TSPacket(object): 74 | SYNC_BYTE = 0x47 75 | SIZE = size = 188 76 | 77 | def __init__(self, pid): 78 | self.transport_error_indicator = False 79 | self.payload_unit_start_indicator = False 80 | self.transport_priority = False 81 | self.pid = pid 82 | self.scrambling_control = 0 83 | self.continuity_counter = 0 84 | 85 | self.discontinuity_indicator = False 86 | self.random_access_indicator = False 87 | self.elementary_stream_priority_indicator = False 88 | 89 | self.program_clock_reference_base = None 90 | self.program_clock_reference_extension = None 91 | self.original_program_clock_reference_base = None 92 | self.original_program_clock_reference_extension = None 93 | self.splice_countdown = None 94 | self.private_data = None 95 | self.ltw_valid_flag = None 96 | self.ltw_offset = None 97 | self.piecewise_rate = None 98 | self.splice_type = None 99 | self.dts_next_au = None 100 | self.payload = None 101 | 102 | @staticmethod 103 | def parse(data, byte_offset): 104 | ts = TSPacket(None) 105 | ts.byte_offset = byte_offset 106 | 107 | data = BitStream(data) 108 | sync_byte = data.read("uint:8") 109 | if sync_byte != TSPacket.SYNC_BYTE: 110 | raise Exception( 111 | "First byte of TS packet at offset {} is not a sync byte." 112 | .format(byte_offset)) 113 | 114 | ts.transport_error_indicator = data.read("bool") 115 | ts.payload_unit_start_indicator = data.read("bool") 116 | ts.transport_priority = data.read("bool") 117 | ts.pid = data.read("uint:13") 118 | ts.scrambling_control = data.read("uint:2") 119 | 120 | # adaptation_field_control 121 | has_adaptation_field = data.read("bool") 122 | has_payload = data.read("bool") 123 | 124 | ts.continuity_counter = data.read("uint:4") 125 | if has_adaptation_field: 126 | adaptation_field_length = data.read("uint:8") 127 | if adaptation_field_length: 128 | ts.discontinuity_indicator = data.read("bool") 129 | ts.random_access_indicator = data.read("bool") 130 | ts.elementary_stream_priority_indicator = data.read("bool") 131 | pcr_flag = data.read("bool") 132 | opcr_flag = data.read("bool") 133 | splicing_point_flag = data.read("bool") 134 | transport_private_data_flag = data.read("bool") 135 | adaptation_field_extension_flag = data.read("bool") 136 | 137 | if pcr_flag: 138 | ts.program_clock_reference_base = data.read("uint:33") 139 | data.read(6) # reserved 140 | ts.program_clock_reference_extension = data.read("uint:9") 141 | 142 | if opcr_flag: 143 | ts.original_program_clock_reference_base = data.read( 144 | "uint:33") 145 | data.read(6) # reserved 146 | ts.original_program_clock_reference_extension = data.read( 147 | "uint:9") 148 | 149 | if splicing_point_flag: 150 | ts.splice_countdown = data.read("uint:8") 151 | 152 | if transport_private_data_flag: 153 | transport_private_data_length = data.read("uint:8") 154 | ts.private_data = data.read( 155 | transport_private_data_length * 8).bytes 156 | 157 | if adaptation_field_extension_flag: 158 | adaptation_field_extension_length = data.read("uint:8") 159 | ltw_flag = data.read("bool") 160 | piecewise_rate_flag = data.read("bool") 161 | seamless_splice_flag = data.read("bool") 162 | data.read(5) # reserved 163 | 164 | if ltw_flag: 165 | ts.ltw_valid_flag = data.read("bool") 166 | ts.ltw_offset = data.read("uint:15") 167 | 168 | if piecewise_rate_flag: 169 | data.read(2) # reserved 170 | ts.piecewise_rate = data.read("uint:22") 171 | 172 | if seamless_splice_flag: 173 | ts.splice_type = data.read("uint:4") 174 | ts.dts_next_au = read_timestamp("DTS_next_AU", data) 175 | 176 | # Skip the rest of the header and padding bytes 177 | data.bytepos = adaptation_field_length + 5 178 | 179 | if has_payload: 180 | ts.payload = data.read("bytes") 181 | return ts 182 | 183 | @property 184 | def bytes(self): 185 | adaptation_field_extension_length = 1 186 | if self.ltw_valid_flag is not None: 187 | adaptation_field_extension_length += 2 188 | if self.piecewise_rate is not None: 189 | adaptation_field_extension_length += 3 190 | if self.splice_type is not None: 191 | adaptation_field_extension_length += 5 192 | 193 | # adaptation field stuffing bytes 194 | if self.payload is not None: 195 | adaptation_field_length = 188 - len(self.payload) - 5 196 | else: 197 | adaptation_field_length = 0 198 | if self.program_clock_reference_base is not None: 199 | adaptation_field_length += 6 200 | if self.original_program_clock_reference_base is not None: 201 | adaptation_field_length += 6 202 | if self.splice_countdown is not None: 203 | adaptation_field_length += 1 204 | if self.private_data is not None: 205 | adaptation_field_length += len(self.private_data) 206 | if adaptation_field_extension_length > 1: 207 | adaptation_field_length += adaptation_field_extension_length 208 | 209 | if adaptation_field_length > 0: 210 | adaptation_field_length += 1 211 | 212 | binary = bitstring.pack( 213 | "uint:8, bool, bool, bool, uint:13, uint:2, bool, bool, uint:4", 214 | self.SYNC_BYTE, self.transport_error_indicator, 215 | self.payload_unit_start_indicator, self.transport_priority, 216 | self.pid, self.scrambling_control, adaptation_field_length >= 0, 217 | self.payload is not None, self.continuity_counter) 218 | 219 | if adaptation_field_length >= 0: 220 | binary.append(bitstring.pack( 221 | "uint:8", 222 | adaptation_field_length)) 223 | 224 | if adaptation_field_length > 0: 225 | binary.append(bitstring.pack( 226 | "bool, bool, bool, bool, bool, bool, bool, bool", 227 | self.discontinuity_indicator, 228 | self.random_access_indicator, 229 | self.elementary_stream_priority_indicator, 230 | self.program_clock_reference_base is not None, 231 | self.original_program_clock_reference_base is not None, 232 | self.splice_countdown is not None, 233 | self.private_data is not None, 234 | adaptation_field_extension_length > 1)) 235 | 236 | if self.program_clock_reference_base is not None: 237 | binary.append(bitstring.pack( 238 | "uint:33, pad:6, uint:9", 239 | self.program_clock_reference_base, 240 | self.program_clock_reference_extension)) 241 | 242 | if self.original_program_clock_reference_base: 243 | binary.append(bitstring.pack( 244 | "uint:33, pad:6, uint:9", 245 | self.original_program_clock_reference_base, 246 | self.original_program_clock_reference_extension)) 247 | 248 | if self.splice_countdown: 249 | binary.append(bitstring.pack("uint:8", self.splice_coundown)) 250 | 251 | if self.private_data is not None: 252 | binary.append(bitstring.pack( 253 | "uint:8, bytes", 254 | len(self.private_data), self.private_data)) 255 | 256 | if adaptation_field_extension_length > 1: 257 | binary.append(bitstring.pack( 258 | "uint:8, bool, bool, bool, pad:5", 259 | adaptation_field_extension_length, 260 | self.ltw_valid_flag is not None, 261 | self.piecewise_rate is not None, 262 | self.splice_type is not None)) 263 | 264 | if self.ltw_valid_flag is not None: 265 | binary.append(bitstring.pack( 266 | "bool, uint:15", 267 | self.ltw_valid_flag, self.ltw_offset)) 268 | 269 | if self.piecewise_rate is not None: 270 | binary.append(bitstring.pack( 271 | "pad:2, uint:22", self.piecewise_rate)) 272 | 273 | if self.splice_type is not None: 274 | binary.append(bitstring.pack( 275 | "uint:4, uint:3, bool, uint:15, bool, uint:15, bool", 276 | self.splice_type, 277 | self.dts_next_au >> 30, 1, 278 | (self.dts_next_au >> 15) & 0x7FFF, 1, 279 | self.dts_next_au & 0x7FFF, 1)) 280 | self.splice_type = data.read("uint:4") 281 | self.dts_next_au = read_timestamp("DTS_next_AU", data) 282 | 283 | while (len(binary) / 8) < adaptation_field_length + 5: 284 | binary.append(bitstring.pack("uint:8", 0xFF)) 285 | 286 | if self.payload is not None: 287 | binary.append(self.payload) 288 | 289 | if (len(binary) / 8) != 188: 290 | raise Exception( 291 | "TS Packet is %s bytes long, but should be exactly 188 bytes." \ 292 | % (binary.bytelen)) 293 | return binary.bytes 294 | 295 | def __repr__(self): 296 | return to_json(self) 297 | 298 | 299 | class ProgramAssociationTable(object): 300 | PID = 0x00 301 | TABLE_ID = 0x00 302 | 303 | def __init__(self, data): 304 | data = BitStream(data) 305 | pointer_field = data.read("uint:8") 306 | if pointer_field: 307 | data.read(pointer_field) 308 | 309 | self.table_id = data.read("uint:8") 310 | if self.table_id != self.TABLE_ID: 311 | raise Exception( 312 | "table_id for PAT is {} but should be {}".format( 313 | self.table_id, self.TABLE_ID)) 314 | self.section_syntax_indicator = data.read("bool") 315 | self.private_indicator = data.read("bool") 316 | data.read(2) # reserved 317 | section_length = data.read("uint:12") 318 | self.transport_stream_id = data.read("uint:16") 319 | data.read(2) # reserved 320 | self.version_number = data.read("uint:5") 321 | self.current_next_indicator = data.read("bool") 322 | self.section_number = data.read("uint:8") 323 | self.last_section_number = data.read("uint:8") 324 | 325 | num_programs = (section_length - 9) // 4 326 | self.programs = OrderedDict() 327 | for _ in range(num_programs): 328 | program_number = data.read("uint:16") 329 | data.read(3) # reserved 330 | pid = data.read("uint:13") 331 | self.programs[program_number] = pid 332 | data.read("uint:32") # crc 333 | calculated_crc = crc32(data.bytes[pointer_field + 1:data.bytepos]) 334 | if calculated_crc != 0: 335 | raise Exception( 336 | "CRC of entire PAT should be 0, but saw %s." \ 337 | % (calculated_crc)) 338 | 339 | while data.bytepos < len(data.bytes): 340 | padding_byte = data.read("uint:8") 341 | if padding_byte != 0xFF: 342 | raise Exception("Padding byte at end of PAT was 0x{:X} but " 343 | "should be 0xFF".format(padding_byte)) 344 | 345 | def __repr__(self): 346 | return to_json(self) 347 | 348 | def __eq__(self, other): 349 | return isinstance(other, ProgramAssociationTable) \ 350 | and self.__dict__ == other.__dict__ 351 | 352 | 353 | class Descriptor(object): 354 | TAG_CA_DESCRIPTOR = 9 355 | 356 | def __init__(self, tag): 357 | self.tag = tag 358 | self.contents = b"" 359 | 360 | @staticmethod 361 | def parse(data): 362 | desc = Descriptor(data.read("uint:8")) 363 | length = data.read("uint:8") 364 | if desc.tag == desc.TAG_CA_DESCRIPTOR: 365 | desc.ca_system_id = data.read("bytes:2") 366 | data.read(3) # reserved 367 | desc.ca_pid = data.read("uint:13") 368 | desc.private_data_bytes = data.read((length - 4) * 8).bytes 369 | else: 370 | desc.contents = data.read(length * 8).bytes 371 | return desc 372 | 373 | @property 374 | def length(self): 375 | if self.tag == self.TAG_CA_DESCRIPTOR: 376 | return 4 + len(self.private_data_bytes) 377 | else: 378 | return len(self.contents) 379 | 380 | @property 381 | def size(self): 382 | return 2 + self.length 383 | 384 | @property 385 | def bytes(self): 386 | binary = bitstring.pack("uint:8, uint:8", self.tag, self.length) 387 | if self.tag == self.TAG_CA_DESCRIPTOR: 388 | binary.append(bitstring.pack( 389 | "bytes:2, pad:3, uint:13, bytes", 390 | self.ca_system_id, self.ca_pid, self.private_data_bytes)) 391 | else: 392 | binary.append(self.contents) 393 | assert(len(binary) / 8 == self.size) 394 | return binary.bytes 395 | 396 | def __repr__(self): 397 | return to_json(self) 398 | 399 | def __eq__(self, other): 400 | return isinstance(other, Descriptor) \ 401 | and self.__dict__ == other.__dict__ 402 | 403 | @staticmethod 404 | def read_descriptors(data, size): 405 | total = 0 406 | descriptors = [] 407 | while total < size: 408 | descriptor = Descriptor.parse(data) 409 | descriptors.append(descriptor) 410 | total += descriptor.size 411 | if total != size: 412 | raise Exception("Excepted {} bytes of descriptors, but got " 413 | "{} bytes of descriptors.".format(size, total)) 414 | return descriptors 415 | 416 | 417 | class StreamType(ReprType): 418 | def __init__(self, num): 419 | self.num = int(num) 420 | 421 | def long(self): 422 | if self.num == 0x00: 423 | return "ITU-T | ISO/IEC Reserved" 424 | elif self.num == 0x01: 425 | return "ISO/IEC 11172 Video" 426 | elif self.num == 0x02: 427 | return "ITU-T Rec. H.262 | ISO/IEC 13818-2 Video or " \ 428 | "ISO/IEC 11172-2 constrained parameter video stream" 429 | elif self.num == 0x03: 430 | return "ISO/IEC 11172 Audio" 431 | elif self.num == 0x04: 432 | return "ISO/IEC 13818-3 Audio" 433 | elif self.num == 0x05: 434 | return "ITU-T Rec. H.222.0 | ISO/IEC 13818-1 private_sections" 435 | elif self.num == 0x06: 436 | return "ITU-T Rec. H.222.0 | ISO/IEC 13818-1 PES packets " \ 437 | "containing private data" 438 | elif self.num == 0x07: 439 | return "ISO/IEC 13522 MHEG" 440 | elif self.num == 0x08: 441 | return "ITU-T Rec. H.222.0 | ISO/IEC 13818-1 Annex A DSM-CC" 442 | elif self.num == 0x09: 443 | return "ITU-T Rec. H.222.1" 444 | elif self.num == 0x0A: 445 | return "ISO/IEC 13818-6 type A" 446 | elif self.num == 0x0B: 447 | return "ISO/IEC 13818-6 type B" 448 | elif self.num == 0x0C: 449 | return "ISO/IEC 13818-6 type C" 450 | elif self.num == 0x0D: 451 | return "ISO/IEC 13818-6 type D" 452 | elif self.num == 0x0E: 453 | return "ITU-T Rec. H.222.0 | ISO/IEC 13818-1 auxiliary" 454 | elif self.num == 0x0F: 455 | return "ISO/IEC 13818-7 Audio with ADTS transport syntax" 456 | elif self.num == 0x10: 457 | return "ISO/IEC 14496-2 Visual" 458 | elif self.num == 0x11: 459 | return "ISO/IEC 14496-3 Audio with the LATM transport " \ 460 | "syntax as defined in ISO/IEC 14496-3 / AMD 1" 461 | elif self.num == 0x12: 462 | return "ISO/IEC 14496-1 SL-packetized stream or FlexMux " \ 463 | "stream carried in PES packets" 464 | elif self.num == 0x13: 465 | return "ISO/IEC 14496-1 SL-packetized stream or FlexMux " \ 466 | "stream carried in ISO/IEC14496_sections" 467 | elif self.num == 0x14: 468 | return "ISO/IEC 13818-6 Synchronized Download Protocol" 469 | elif self.num >= 0x15 and self.num <= 0x7F: 470 | return "ITU-T Rec. H.222.0 | ISO/IEC 13818-1 Reserved" 471 | else: 472 | return "User Private" 473 | 474 | def __int__(self): 475 | return self.num 476 | 477 | def __repr__(self): 478 | return "0x{num:02x} ({long})".format(num=self.num, long=self.long()) 479 | 480 | 481 | class Stream(object): 482 | def __init__(self, data): 483 | self.stream_type = StreamType(data.read("uint:8")) 484 | data.read(3) # reserved 485 | self.elementary_pid = data.read("uint:13") 486 | data.read(4) # reserved 487 | es_info_length = data.read("uint:12") 488 | self.descriptors = Descriptor.read_descriptors(data, es_info_length) 489 | 490 | @property 491 | def size(self): 492 | total = 5 493 | for descriptor in self.descriptors: 494 | total += descriptor.size 495 | return total 496 | 497 | @property 498 | def bytes(self): 499 | es_info_length = 0 500 | for descriptor in self.descriptors: 501 | es_info_length += descriptor.size 502 | binary = bitstring.pack( 503 | "uint:8, pad:3, uint:13, pad:4, uint:12", 504 | self.stream_type, self.elementary_pid, es_info_length) 505 | for descriptor in self.descriptors: 506 | binary.append(descriptor.bytes) 507 | return binary.bytes 508 | 509 | def __eq__(self, other): 510 | return isinstance(other, Stream) \ 511 | and self.__dict__ == other.__dict__ 512 | 513 | def __repr__(self): 514 | return to_json(self.__dict__) 515 | 516 | 517 | class ProgramMapTable(object): 518 | TABLE_ID = 0x02 519 | 520 | def __init__(self, data): 521 | data = BitStream(data) 522 | pointer_field = data.read("uint:8") 523 | if pointer_field: 524 | data.read(pointer_field) 525 | 526 | self.table_id = data.read("uint:8") 527 | if self.table_id != self.TABLE_ID: 528 | raise Exception( 529 | "table_id for PMT is {} but should be {}".format( 530 | self.table_id, self.TABLE_ID)) 531 | self.section_syntax_indicator = data.read("bool") 532 | self.private_indicator = data.read("bool") 533 | data.read(2) # reserved 534 | section_length = data.read("uint:12") 535 | 536 | self.program_number = data.read("uint:16") 537 | data.read(2) # reserved 538 | self.version_number = data.read("uint:5") 539 | self.current_next_indicator = data.read("bool") 540 | self.section_number = data.read("uint:8") 541 | self.last_section_number = data.read("uint:8") 542 | 543 | data.read(3) # reserved 544 | self.pcr_pid = data.read("uint:13") 545 | 546 | data.read(4) # reserved 547 | program_info_length = data.read("uint:12") 548 | self.descriptors = Descriptor.read_descriptors( 549 | data, program_info_length) 550 | 551 | self.streams = OrderedDict() 552 | while data.bytepos < section_length + 3 - 4: 553 | stream = Stream(data) 554 | if stream.elementary_pid in self.streams: 555 | raise Exception( 556 | "PMT contains the same elementary PID more than once.") 557 | self.streams[stream.elementary_pid] = stream 558 | 559 | data.read("uint:32") # crc 560 | calculated_crc = crc32(data.bytes[pointer_field + 1:data.bytepos]) 561 | if calculated_crc != 0: 562 | raise Exception( 563 | "CRC of entire PMT should be 0, but saw %s." \ 564 | % (calculated_crc)) 565 | 566 | while data.bytepos < len(data.bytes): 567 | padding_byte = data.read("uint:8") 568 | if padding_byte != 0xFF: 569 | raise Exception("Padding byte at end of PMT was 0x{:02X} but " 570 | "should be 0xFF".format(padding_byte)) 571 | 572 | @property 573 | def bytes(self): 574 | binary = bitstring.pack( 575 | "pad:8, uint:8, bool, bool, pad:2", 576 | self.TABLE_ID, self.section_syntax_indicator, 577 | self.private_indicator) 578 | 579 | program_info_length = 0 580 | for descriptor in self.descriptors: 581 | program_info_length += descriptor.size 582 | 583 | length = 13 + program_info_length 584 | for stream in self.streams.values(): 585 | length += stream.size 586 | 587 | binary.append(bitstring.pack( 588 | "uint:12, uint:16, pad:2, uint:5, bool, uint:8, uint:8, pad:3," + 589 | "uint:13, pad:4, uint:12", 590 | length, self.program_number, self.version_number, 591 | self.current_next_indicator, self.section_number, 592 | self.last_section_number, self.pcr_pid, program_info_length)) 593 | 594 | for descriptor in self.descriptors: 595 | binary.append(descriptor.bytes) 596 | for stream in self.streams.values(): 597 | binary.append(stream.bytes) 598 | 599 | binary.append(bitstring.pack("uint:32", crc32(binary.bytes[1:]))) 600 | return binary.bytes 601 | 602 | def __repr__(self): 603 | return to_json(self) 604 | 605 | def __eq__(self, other): 606 | return isinstance(other, ProgramMapTable) \ 607 | and self.__dict__ == other.__dict__ 608 | 609 | 610 | class PESReader(object): 611 | def __init__(self): 612 | self.ts_packets = [] 613 | self.data = [] 614 | 615 | def add_ts_packet(self, ts_packet): 616 | pes_packet = None 617 | if self.ts_packets and ts_packet.payload_unit_start_indicator: 618 | try: 619 | pes_packet = PESPacket(bytes(self.data), self.ts_packets) 620 | except Exception as e: 621 | logging.warning(e) 622 | 623 | self.ts_packets = [] 624 | self.data = [] 625 | 626 | if ts_packet is not None: 627 | self.ts_packets.append(ts_packet) 628 | if ts_packet.payload: 629 | self.data.extend(ts_packet.payload) 630 | 631 | return pes_packet 632 | 633 | 634 | class StreamID(object): 635 | PROGRAM_STREAM_MAP = 0xBC 636 | PADDING = 0xBE 637 | PRIVATE_2 = 0xBF 638 | ECM = 0xF0 639 | EMM = 0xF1 640 | PROGRAM_STREAM_DIRECTORY = 0xFF 641 | DSMCC = 0xF2 642 | H222_1_TYPE_E = 0xF8 643 | 644 | @staticmethod 645 | def has_pes_header(sid): 646 | return sid != StreamID.PROGRAM_STREAM_MAP \ 647 | and sid != StreamID.PADDING \ 648 | and sid != StreamID.PRIVATE_2 \ 649 | and sid != StreamID.ECM \ 650 | and sid != StreamID.EMM \ 651 | and sid != StreamID.PROGRAM_STREAM_DIRECTORY \ 652 | and sid != StreamID.DSMCC \ 653 | and sid != StreamID.H222_1_TYPE_E 654 | 655 | 656 | class PESPacket(object): 657 | def __init__(self, data, ts_packets): 658 | self.bytes = data 659 | first_ts = ts_packets[0] 660 | self.pid = first_ts.pid 661 | self.byte_offset = first_ts.byte_offset 662 | self.size = len(ts_packets) * TSPacket.SIZE 663 | self.random_access = first_ts.random_access_indicator 664 | 665 | self.ts_packets = ts_packets 666 | data = BitStream(data) 667 | 668 | start_code = data.read("uint:24") 669 | if start_code != 0x000001: 670 | raise Exception("packet_start_code_prefix is 0x{:06X} but should " 671 | "be 0x000001".format(start_code)) 672 | 673 | self.stream_id = data.read("uint:8") 674 | pes_packet_length = data.read("uint:16") 675 | 676 | if StreamID.has_pes_header(self.stream_id): 677 | bits = data.read("uint:2") 678 | if bits != 2: 679 | raise Exception("First 2 bits of a PES header should be 0x2 " 680 | "but saw 0x{:02X}'".format(bits)) 681 | 682 | self.pes_scrambling_control = data.read("uint:2") 683 | self.pes_priority = data.read("bool") 684 | self.data_alignment_indicator = data.read("bool") 685 | self.copyright = data.read("bool") 686 | self.original_or_copy = data.read("bool") 687 | pts_dts_flags = data.read("uint:2") 688 | escr_flag = data.read("bool") 689 | es_rate_flag = data.read("bool") 690 | dsm_trick_mode_flag = data.read("bool") 691 | additional_copy_info_flag = data.read("bool") 692 | pes_crc_flag = data.read("bool") 693 | pes_extension_flag = data.read("bool") 694 | pes_header_data_length = data.read("uint:8") 695 | 696 | if pts_dts_flags & 2: 697 | bits = data.read("uint:4") 698 | if bits != pts_dts_flags: 699 | raise Exception( 700 | "2 bits before PTS should be 0x{:02X} but saw 0x{" 701 | ":02X}".format(pts_dts_flags, bits)) 702 | self.pts = read_timestamp("PTS", data) 703 | 704 | if pts_dts_flags & 1: 705 | bits = data.read("uint:4") 706 | if bits != 0x1: 707 | raise Exception("2 bits before DTS should be 0x1 but saw " 708 | "0x{:02X}".format(bits)) 709 | self.dts = read_timestamp("DTS", data) 710 | 711 | # skip the rest of the header and stuffing bytes 712 | data.bytepos = pes_header_data_length + 9 713 | if self.stream_id == StreamID.PADDING: 714 | self.payload = None 715 | else: 716 | self.payload = data.read("bytes") 717 | 718 | def __repr__(self): 719 | d = self.__dict__.copy() 720 | del d["bytes"] 721 | del d["ts_packets"] 722 | return to_json(d) 723 | -------------------------------------------------------------------------------- /ts_inspect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import logging 4 | from ts import * 5 | import sys 6 | 7 | 8 | class OmniSet(object): 9 | def __contains__(self, x): 10 | return True 11 | 12 | 13 | if __name__ == "__main__": 14 | parser = argparse.ArgumentParser() 15 | parser.add_argument("mpeg_ts_file", help="The file to read") 16 | parser.add_argument( 17 | "--show-ts", help="Output TS packets.", action="store_true", 18 | default=False) 19 | parser.add_argument( 20 | "--show-pes", help="Output PES packets.", action="store_true", 21 | default=False) 22 | parser.add_argument( 23 | "--show-pat", help="Output PAT sections.", action="store_true", 24 | default=False) 25 | parser.add_argument( 26 | "--show-pmt", help="Output PMT sections.", action="store_true", 27 | default=False) 28 | parser.add_argument( 29 | "--as-c-array", action="store_true", default=False, 30 | help="Output sections as C arrays instead of pretty-printing.") 31 | parser.add_argument( 32 | "--filter", type=lambda x: list(map(int, x.split(","))), 33 | default=OmniSet(), 34 | help="Only show output for PIDs in this comma-separated list.") 35 | parser.add_argument( 36 | "--no-wait", help="Don't want for input after output", 37 | action="store_true", default=False) 38 | parser.add_argument( 39 | "--verbose", "-v", action="store_true", default=False, 40 | help="Enable verbose output.") 41 | args = parser.parse_args() 42 | 43 | logging.basicConfig( 44 | format='%(levelname)s: %(message)s', 45 | level=logging.DEBUG if args.verbose else logging.INFO) 46 | 47 | def wait(): 48 | if args.no_wait: 49 | pass 50 | else: 51 | input() 52 | 53 | def output(o): 54 | print(o) 55 | if args.as_c_array: 56 | print("uint8_t %s_bytes[] = {%s};" % 57 | (type(o).__name__.lower(), ", ".join(map(str, o.bytes)))) 58 | 59 | pmt_pids = set() 60 | pes_readers = {} 61 | ts_reader = read_ts(args.mpeg_ts_file) 62 | while True: 63 | try: 64 | ts_packet = next(ts_reader) 65 | except StopIteration: 66 | break 67 | except Exception as e: 68 | print("Error reading TS packet: %s" % e) 69 | continue 70 | 71 | if args.show_ts and ts_packet.pid in args.filter: 72 | output(ts_packet) 73 | wait() 74 | if ts_packet.pid == ProgramAssociationTable.PID: 75 | try: 76 | pat = ProgramAssociationTable(ts_packet.payload) 77 | if args.show_pat and ts_packet.pid in args.filter: 78 | output(pat) 79 | wait() 80 | pmt_pids.update(pat.programs.values()) 81 | except Exception as e: 82 | print("Error reading PAT: %s" % e) 83 | 84 | elif ts_packet.pid in pmt_pids: 85 | try: 86 | pmt = ProgramMapTable(ts_packet.payload) 87 | if args.show_pmt and ts_packet.pid in args.filter: 88 | output(pmt) 89 | wait() 90 | for pid in pmt.streams: 91 | if pid not in pes_readers: 92 | pes_readers[pid] = PESReader() 93 | except Exception as e: 94 | print("Error reading PMT: %s" % e) 95 | 96 | elif args.show_pes and ts_packet.pid in pes_readers: 97 | try: 98 | pes_packet = pes_readers[ts_packet.pid].add_ts_packet(ts_packet) 99 | if pes_packet and ts_packet.pid in args.filter: 100 | output(pes_packet) 101 | wait() 102 | except Exception as e: 103 | print("Error reading PES packet: %s" % e) 104 | --------------------------------------------------------------------------------