|
""" |
|
MIT License |
|
|
|
Copyright (C) 2021 ROCKY4546 |
|
https://github.com/rocky4546 |
|
|
|
This file is part of Cabernet |
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software |
|
and associated documentation files (the "Software"), to deal in the Software without restriction, |
|
including without limitation the rights to use, copy, modify, merge, publish, distribute, |
|
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software |
|
is furnished to do so, subject to the following conditions: |
|
|
|
The above copyright notice and this permission notice shall be included in all copies or |
|
substantial portions of the Software. |
|
""" |
|
|
|
import binascii |
|
import struct |
|
import datetime |
|
import logging |
|
import lib.common.utils as utils |
|
from lib.common.algorithms import Crc |
|
from lib.common.models import CrcModels |
|
|
|
ATSC_EXTENDED_CHANNEL_DESCR_TAG = b'\xA0' |
|
ATSC_SERVICE_LOCATION_DESCR_TAG = b'\xA1' |
|
ATSC_VIRTUAL_CHANNEL_TABLE_TAG = b'\xC8' |
|
ATSC_MASTER_GUIDE_TABLE_TAG = b'\xC7' |
|
ATSC_SERVICE_DESCR_TABLE_TAG = b'\x42' |
|
MPEG2_PROGRAM_SYSTEM_TIME_TABLE_TAG = b'\xCD' |
|
MPEG2_PROGRAM_ASSOCIATION_TABLE_TAG = b'\x00' |
|
MPEG2_CONDITIONAL_ACCESS_TABLE_TAG = b'\x01' |
|
MPEG2_PROGRAM_MAP_TABLE_TAG = b'\x02' |
|
|
|
ATSC_MSG_LEN = 188 |
|
LEAP_SECONDS_1980 = 19 |
|
LEAP_SECONDS_2021 = 37 |
|
|
|
|
|
class ATSCMsg: |
|
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self): |
|
self.logger = logging.getLogger(__name__) |
|
models = CrcModels() |
|
crc32_mpeg_model = models.get_params('crc-32-mpeg') |
|
self.crc_width = crc32_mpeg_model['width'] |
|
self.crc_poly = crc32_mpeg_model['poly'] |
|
self.crc_reflect_in = crc32_mpeg_model['reflect_in'] |
|
self.crc_xor_in = crc32_mpeg_model['xor_in'] |
|
self.crc_reflect_out = crc32_mpeg_model['reflect_out'] |
|
self.crc_xor_out = crc32_mpeg_model['xor_out'] |
|
self.crc_table_idx_width = 8 |
|
self.atsc_blank_section = b'\x47\x1f\xff\x10\x00'.ljust(ATSC_MSG_LEN, b'\xff') |
|
self.type_strings = [] |
|
self.msg_counter = {} |
|
|
|
def gen_crc_mpeg(self, _msg): |
|
alg = Crc( |
|
width=self.crc_width, |
|
poly=self.crc_poly, |
|
reflect_in=self.crc_reflect_in, |
|
xor_in=self.crc_xor_in, |
|
reflect_out=self.crc_reflect_out, |
|
xor_out=self.crc_xor_out, |
|
table_idx_width=8, |
|
) |
|
crc_int = alg.bit_by_bit(_msg) |
|
crc = struct.pack('>I', crc_int) |
|
return crc |
|
|
|
def gen_header(self, _pid): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if _pid not in self.msg_counter.keys(): |
|
self.msg_counter[_pid] = 0 |
|
|
|
sync = 0x47400000 |
|
pid_shifted = _pid << 8 |
|
msg_int = sync | pid_shifted | (self.msg_counter[_pid] + 16) |
|
msg = struct.pack('>I', msg_int) + b'\x00' |
|
self.msg_counter[_pid] += 1 |
|
if self.msg_counter[_pid] > 15: |
|
self.msg_counter[_pid] = 0 |
|
return msg |
|
|
|
def gen_multiple_string_structure(self, _names): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
msg = utils.set_u8(len(_names)) |
|
for name in _names: |
|
lang = self.gen_lang(b'eng') |
|
segment_len = utils.set_u8(1) |
|
compress_mode = utils.set_u16(0) |
|
name_bytes = utils.set_str(name.encode(), False) |
|
msg += lang + segment_len + compress_mode + name_bytes |
|
return msg |
|
|
|
def gen_channel_longnames(self, names): |
|
|
|
|
|
|
|
|
|
|
|
|
|
long_name = self.gen_multiple_string_structure(names) |
|
return ATSC_EXTENDED_CHANNEL_DESCR_TAG + utils.set_u8(len(long_name)) + long_name |
|
|
|
def gen_vct_channel_descriptor(self, xxx): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pass |
|
|
|
def gen_pid(self, _prog_number): |
|
|
|
|
|
|
|
|
|
|
|
pid_lookup = [0x00, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, |
|
0x130, 0x140, 0x150, 0x160, 0x170, 0x180, 0x190, 0x230, 0x240] |
|
return pid_lookup[_prog_number] |
|
|
|
def gen_lang(self, _name): |
|
return struct.pack('%ds' % (len(_name)), _name) |
|
|
|
def update_sdt_names(self, _video, _service_provider, _service_name): |
|
if _video.data is None: |
|
return |
|
i = 0 |
|
video_len = len(_video.data) |
|
msg = None |
|
while True: |
|
if i + ATSC_MSG_LEN > video_len: |
|
break |
|
packet = _video.data[i:i + ATSC_MSG_LEN] |
|
program_fields = self.decode_ts_packet(packet) |
|
if program_fields is None: |
|
i += ATSC_MSG_LEN |
|
continue |
|
if program_fields['transport_error_indicator']: |
|
i += ATSC_MSG_LEN |
|
continue |
|
if program_fields['pid'] == 0x0011: |
|
descr = b'\x01' \ |
|
+ utils.set_str(_service_provider, False) \ |
|
+ utils.set_str(_service_name, False) |
|
descr = b'\x48' + utils.set_u8(len(descr)) + descr |
|
msg = packet[8:20] + utils.set_u8(len(descr)) + descr |
|
length = utils.set_u16(len(msg) + 4 + 0xF000) |
|
msg = ATSC_SERVICE_DESCR_TABLE_TAG + length + msg |
|
crc = self.gen_crc_mpeg(msg) |
|
msg = packet[:5] + msg + crc |
|
msg = msg.ljust(len(packet), b'\xFF') |
|
_video.data = b''.join([ |
|
_video.data[:i], |
|
msg, |
|
_video.data[i + ATSC_MSG_LEN:] |
|
]) |
|
i += ATSC_MSG_LEN |
|
if msg is None: |
|
self.logger.debug('Missing ATSC SDT Msg in stream, unable to update provider and service name') |
|
else: |
|
self.logger.debug('Updating ATSC SDT with service info {} {}' \ |
|
.format(_service_provider, _service_name)) |
|
|
|
def gen_sld(self, _base_pid, _elements): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elem_len = utils.set_u8(len(_elements) + 1) |
|
video_pid = utils.set_u16(_base_pid + 1 + 57344) |
|
stream_type = b'\x02' |
|
lang_0 = b'\x00\x00\x00' |
|
msg = stream_type + video_pid + lang_0 |
|
stream_type = b'\x81' |
|
audio_pid_int = _base_pid + 3 + 57344 |
|
for lang in _elements: |
|
audio_pid_int += 1 |
|
audio_pid = utils.set_u16(audio_pid_int) |
|
lang_msg = struct.pack('%ds' % (len(lang)), |
|
lang.encode()) |
|
msg += stream_type + audio_pid + lang_msg |
|
msg = video_pid + elem_len + msg |
|
length = utils.set_u8(len(msg)) |
|
return ATSC_SERVICE_LOCATION_DESCR_TAG + length + msg |
|
|
|
def gen_vct_channel(self, _tsid, _short_name, _channel): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
u16name = b'' |
|
short_name7 = _short_name.ljust(7, '\x00') |
|
|
|
for ch in short_name7: |
|
u16name += utils.set_u16(ord(ch)) |
|
ch_num = _channel['chnum_maj'] << 10 |
|
ch_num |= 15728640 |
|
ch_num |= _channel['chnum_min'] |
|
u3bch_num = utils.set_u32(ch_num)[1:] |
|
mod_mode = b'\x04' |
|
freq = b'\x00\x00\x00\x00' |
|
prog_num = utils.set_u16(_channel['prog_num']) |
|
pid = self.gen_pid(_channel['prog_num']) |
|
misc_bits = b'\x0d\xc2' |
|
source_id = prog_num |
|
descr = _channel['descr'] |
|
descr_msg = b'' |
|
for key in descr.keys(): |
|
if key == 'long_names': |
|
descr_msg += self.gen_channel_longnames(descr[key]) |
|
elif key == 'lang': |
|
descr_msg += self.gen_sld(pid, descr[key]) |
|
descr_len = utils.set_u16(len(descr_msg) + 0xFC00) |
|
|
|
return u16name + u3bch_num + mod_mode + freq + _tsid + prog_num + misc_bits + \ |
|
source_id + descr_len + descr_msg |
|
|
|
def gen_pat_channels(self, _channels): |
|
|
|
|
|
|
|
|
|
msg = b'' |
|
for i in range(1, len(_channels) + 1): |
|
pid = utils.set_u16(self.gen_pid(i) + 57344) |
|
msg += utils.set_u16(i) + pid |
|
return msg |
|
|
|
def gen_pat(self, _mux_stream): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tsid = _mux_stream['tsid'] |
|
ver_sect = b'\xc3\x00\x00' |
|
channels_len = utils.set_u8(len(_mux_stream['channels'])) |
|
for i in range(len(_mux_stream['channels'])): |
|
pid = self.gen_pid(i) |
|
msg = tsid + ver_sect + self.gen_pat_channels(_mux_stream['channels']) |
|
length = utils.set_u16(len(msg) + 4 + 0xB000) |
|
msg = MPEG2_PROGRAM_ASSOCIATION_TABLE_TAG + length + msg |
|
crc = self.gen_crc_mpeg(msg) |
|
msg = self.gen_header(0) + msg + crc |
|
return self.format_video_packets([msg]) |
|
|
|
def gen_vct(self, _mux_stream): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
msg = b'' |
|
tsid = _mux_stream['tsid'] |
|
ver_sect_last_sect_proto = b'\xc3\x00\x00\x00' |
|
channels_len = utils.set_u8(len(_mux_stream['channels'])) |
|
for short_name in _mux_stream['channels'].keys(): |
|
msg += self.gen_vct_channel(tsid, short_name, _mux_stream['channels'][short_name]) |
|
extra_empty_descr = b'\xfc\x00' |
|
msg = tsid + ver_sect_last_sect_proto + channels_len + msg + extra_empty_descr |
|
length = utils.set_u16(len(msg) + 4 + 0xF000) |
|
|
|
msg = ATSC_VIRTUAL_CHANNEL_TABLE_TAG + length + msg |
|
crc = self.gen_crc_mpeg(msg) |
|
msg = self.gen_header(0x1ffb) + msg + crc |
|
|
|
|
|
return self.format_video_packets([msg]) |
|
|
|
def gen_stt(self): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
table_id_ext = b'\x00\x00' |
|
ver_sect_proto = b'\xc1\x00\x00\x00' |
|
|
|
time_gps = datetime.datetime.utcnow() - datetime.datetime(1980, 1, 6) \ |
|
- datetime.timedelta(seconds=LEAP_SECONDS_2021 - LEAP_SECONDS_1980) |
|
time_gps_sec = int(time_gps.total_seconds()) |
|
system_time = utils.set_u32(time_gps_sec) |
|
delta_time = utils.set_u8(LEAP_SECONDS_2021 - LEAP_SECONDS_1980) |
|
daylight_savings = b'\x60' |
|
|
|
msg = table_id_ext + ver_sect_proto + system_time + \ |
|
delta_time + daylight_savings + b'\x00' |
|
length = utils.set_u16(len(msg) + 4 + 0xF000) |
|
msg = MPEG2_PROGRAM_SYSTEM_TIME_TABLE_TAG + length + msg |
|
crc = self.gen_crc_mpeg(msg) |
|
msg = self.gen_header(0x1ffb) + msg + crc |
|
return self.format_video_packets([msg]) |
|
|
|
def gen_pmt(self, _channels): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
msgs = [] |
|
prog_num_int = 0 |
|
for short_name in _channels.keys(): |
|
prog_num_int += 1 |
|
prog_num_bytes = utils.set_u16(prog_num_int) |
|
ver_sect = b'\xc1\x00\x00' |
|
base_pid_int = self.gen_pid(prog_num_int) |
|
pid_video_int = base_pid_int + 1 |
|
pid_video = utils.set_u16(pid_video_int + 0xE000) |
|
pid_audio_int = pid_video_int + 3 |
|
pid_audio = utils.set_u16(pid_audio_int + 0xE000) |
|
descr_prog = b'\xf0\x00' |
|
descr_video = b'\x02' + pid_video + b'\xF0\x00' |
|
descr_audio = b'\x81' + pid_audio + b'\xF0\x00' |
|
msg = prog_num_bytes + ver_sect + pid_video + descr_prog + descr_video + descr_audio |
|
length = utils.set_u16(len(msg) + 4 + 0xB000) |
|
msg = MPEG2_PROGRAM_MAP_TABLE_TAG + length + msg |
|
crc = self.gen_crc_mpeg(msg) |
|
msgs.append(self.gen_header(base_pid_int) + msg + crc) |
|
return [self.format_video_packets(msgs)] |
|
|
|
def gen_mgt(self, _mux_stream): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
msg = ATSC_MASTER_GUIDE_TABLE_TAG |
|
return msg |
|
|
|
def gen_cat(self): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return b'\x00\x01\xb0\x09\xff\xff\xc3\x00\x00\xd5\xdc\xfb\x4c' |
|
|
|
def update_continuity_counter(self, section): |
|
pid = self.get_pid(section) |
|
if pid is None: |
|
return section |
|
|
|
if pid not in self.msg_counter.keys(): |
|
self.msg_counter[pid] = 0 |
|
|
|
s_int = section[3] |
|
s_top = s_int & 0xf0 |
|
|
|
s_int = s_top + self.msg_counter[pid] |
|
sect_ba = bytearray(section) |
|
sect_ba[3] = s_int |
|
sect_bytes = bytes(sect_ba) |
|
|
|
self.msg_counter[pid] += 1 |
|
if self.msg_counter[pid] > 15: |
|
self.msg_counter[pid] = 0 |
|
|
|
return sect_bytes |
|
|
|
|
|
def format_video_packets(self, _msgs=None): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sections = [ |
|
self.update_continuity_counter(self.atsc_blank_section), |
|
self.update_continuity_counter(self.atsc_blank_section), |
|
self.update_continuity_counter(self.atsc_blank_section), |
|
self.update_continuity_counter(self.atsc_blank_section), |
|
self.update_continuity_counter(self.atsc_blank_section), |
|
self.update_continuity_counter(self.atsc_blank_section), |
|
self.update_continuity_counter(self.atsc_blank_section), |
|
] |
|
|
|
if _msgs is None: |
|
return b''.join(sections) |
|
|
|
|
|
if len(_msgs) > 7: |
|
self.logger.error('ATSC: TOO MANY MESSAGES={}'.format(len(_msgs))) |
|
return None |
|
for i in range(len(_msgs)): |
|
if len(_msgs[i]) > ATSC_MSG_LEN: |
|
self.logger.error('ATSC: MESSAGE LENGTH TOO LONG={}'.format(len(_msgs[i]))) |
|
return None |
|
else: |
|
sections[i] = self.update_continuity_counter(_msgs[i].ljust(ATSC_MSG_LEN, b'\xff')) |
|
|
|
return b''.join(sections) |
|
|
|
|
|
def extract_psip(self, _video_data): |
|
packet_list = [] |
|
if _video_data is None: |
|
return |
|
i = 0 |
|
video_len = len(_video_data) |
|
prev_pid = -1 |
|
pmt_pids = None |
|
pat_found = False |
|
pmt_found = False |
|
seg_counter = 0 |
|
|
|
while True: |
|
if i + ATSC_MSG_LEN > video_len: |
|
break |
|
packet = _video_data[i:i + ATSC_MSG_LEN] |
|
i += ATSC_MSG_LEN |
|
program_fields = self.decode_ts_packet(packet) |
|
|
|
seg_counter += 1 |
|
if seg_counter > 7: |
|
|
|
break |
|
|
|
if program_fields is None: |
|
continue |
|
if program_fields['transport_error_indicator']: |
|
continue |
|
|
|
|
|
if program_fields['pid'] == 0 \ |
|
or program_fields['pid'] == 4096: |
|
packet_list.append(packet) |
|
|
|
seg_counter += 1 |
|
if seg_counter > 7: |
|
|
|
break |
|
|
|
continue |
|
|
|
|
|
if program_fields['pid'] == 0x0000: |
|
pmt_pids = self.decode_pat(program_fields['payload']) |
|
|
|
if not pat_found: |
|
packet_list.append(packet) |
|
pat_found = True |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
prev_pid = program_fields['pid'] |
|
return packet_list |
|
|
|
def sync_audio_video(self, _video_data): |
|
""" |
|
Trims the audio or video to sync the PTS for both |
|
and return the video data with the removed parts |
|
""" |
|
packet_list = [] |
|
if _video_data is None: |
|
return |
|
i = 0 |
|
video_len = len(_video_data) |
|
prev_pid = -1 |
|
pmt_pids = None |
|
pat_found = False |
|
pmt_found = False |
|
seg_counter = 0 |
|
|
|
|
|
|
|
|
|
|
|
|
|
while True: |
|
if i + ATSC_MSG_LEN > video_len: |
|
break |
|
packet = _video_data[i:i + ATSC_MSG_LEN] |
|
i += ATSC_MSG_LEN |
|
program_fields = self.decode_ts_packet(packet) |
|
|
|
seg_counter += 1 |
|
if seg_counter > 7: |
|
|
|
break |
|
else: |
|
packet_list.append(packet) |
|
continue |
|
|
|
if program_fields is None: |
|
continue |
|
if program_fields['transport_error_indicator']: |
|
continue |
|
|
|
if program_fields['pid'] == 0x0000: |
|
pmt_pids = self.decode_pat(program_fields['payload']) |
|
|
|
if not pat_found: |
|
packet_list.append(packet) |
|
pat_found = True |
|
if pmt_pids and program_fields['pid'] in pmt_pids.keys(): |
|
program = pmt_pids[program_fields['pid']] |
|
self.decode_pmt(program_fields['pid'], program, program_fields['payload']) |
|
if not pmt_found: |
|
|
|
packet_list.append(packet) |
|
pmt_found = True |
|
continue |
|
elif program_fields['pid'] == 0x1ffb: |
|
self.logger.info('Packet Table indicator 0x1ffb, not implemented {}'.format(i)) |
|
continue |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
prev_pid = program_fields['pid'] |
|
return packet_list |
|
|
|
|
|
def get_pid(self, _packet_188): |
|
word = struct.unpack('!I', _packet_188[0:4])[0] |
|
sync = (word & 0xff000000) >> 24 |
|
if sync != 0x47: |
|
return None |
|
|
|
|
|
pid = (word & 0x1fff00) >> 8 |
|
return pid |
|
|
|
def decode_ts_packet(self, _packet_188): |
|
fields = {} |
|
word = struct.unpack('!I', _packet_188[0:4])[0] |
|
sync = (word & 0xff000000) >> 24 |
|
if sync != 0x47: |
|
return None |
|
|
|
fields['transport_error_indicator'] = (word & 0x800000) != 0 |
|
|
|
|
|
|
|
|
|
fields['payload_unit_start_indicator'] = (word & 0x400000) != 0 |
|
|
|
|
|
fields['transport_priority'] = (word & 0x200000) != 0 |
|
|
|
|
|
fields['pid'] = (word & 0x1fff00) >> 8 |
|
|
|
|
|
|
|
|
|
|
|
fields['scrambling_control'] = (word & 0xc0) >> 6 |
|
|
|
|
|
|
|
|
|
|
|
fields['adaptation_field_control'] = (word & 0x30) >> 4 |
|
|
|
if fields['adaptation_field_control'] == 1: |
|
has_adapt = False |
|
has_payload = True |
|
elif fields['adaptation_field_control'] == 2: |
|
has_adapt = True |
|
has_payload = False |
|
elif fields['adaptation_field_control'] == 3: |
|
has_adapt = True |
|
has_payload = True |
|
else: |
|
|
|
|
|
|
|
|
|
has_adapt = False |
|
has_payload = True |
|
fields['corrupted_adaption_control_field'] = True |
|
|
|
|
|
|
|
fields['cont_counter'] = word & 0xf |
|
|
|
payload_start = 5 |
|
if has_adapt: |
|
adapt_length = struct.unpack('b', bytes([_packet_188[5]]))[0] |
|
if 6 + adapt_length > len(_packet_188): |
|
return None |
|
|
|
fields['adapt'] = _packet_188[6:6 + adapt_length] |
|
payload_start = 6 + adapt_length |
|
|
|
if has_payload: |
|
fields['payload'] = _packet_188[payload_start:] |
|
else: |
|
|
|
extra = _packet_188[payload_start:] |
|
if len(extra) != 0: |
|
fields['corrupt_payload'] = extra |
|
|
|
return fields |
|
|
|
def decode_pmt(self, pid, program, payload): |
|
t = binascii.b2a_hex(payload) |
|
if t not in self.type_strings: |
|
self.type_strings.append(t) |
|
|
|
pcr_pid = struct.unpack("!H", payload[8:10])[0] |
|
reserved = (pcr_pid & 0xe000) >> 13 |
|
pcr_pid &= 0x1fff |
|
desc1 = payload[12:] |
|
|
|
|
|
|
|
def decode_pat(self, payload): |
|
t = binascii.b2a_hex(payload) |
|
if t not in self.type_strings: |
|
self.type_strings.append(t) |
|
|
|
|
|
|
|
section_length = (payload[1] & 0xf << 8) | payload[2] |
|
program_map_pids = {} |
|
|
|
|
|
program_count = (section_length - 5) / 4 - 1 |
|
|
|
if section_length > 20: |
|
return program_map_pids |
|
|
|
for i in range(0, int(program_count)): |
|
at = 8 + (i * 4) |
|
program_number = struct.unpack("!H", payload[at:at + 2])[0] |
|
if at + 2 > len(payload): |
|
break |
|
program_map_pid = struct.unpack("!H", payload[at + 2:at + 2 + 2])[0] |
|
|
|
|
|
reserved = (program_map_pid & 0xe000) >> 13 |
|
program_map_pid &= 0x1fff |
|
|
|
program_map_pids[program_map_pid] = program_number |
|
i += 1 |
|
return program_map_pids |
|
|