def gen_pat_channels(self, _channels): # for each section (sids): # 2bytes = program number (non-zero) 1..n # 3bits = 111 static bits # 13bits = multiples of 10 30,40,50,60,130,140,150,160,230,... msg = b'' for i in range(1, len(_channels) + 1): pid = utils.set_u16(self.gen_pid(i) + 57344) # E000 msg += utils.set_u16(i) + pid return msg
def gen_vct_channel(self, _tsid, _short_name, _channel): # Channel part of Table 6.4 Terrestrial Virtual Channel Table # 7*2byte characters short name = dict key # 1111 static bits # 10bits major channel number # 10bits minor channel number # 1byte modulation_mode = 0x04 # 4bytes carrier_freq = 0 # 2bytes channel_tsid = same as VCT TSID # 2bytes program number = index from 1 to n # 2bits ETM_location = 00 (no location) # 1bit access_control = 0 # 1bit hidden = 0 # 11 static bits # 1bit hide_guide = 0 # 111 static bits # 6bits service_type = 000010 # 2bytes source_id = index from 1 to n (same as prog_num) # 111111 static bits # 10bits description_length = long channel name length # descriptor() = gen_extended_channel_descriptor(names) # 111111 static bits # 10bits additional_description_length = long channel name length = 0 # no additional descriptions are used # chnum_maj # chnum_min # prog_num # long_name 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 # 0xf00000 static bits 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' # 0000 1101 1100 0010 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_vct(self, _mux_stream): # Table 6.4 Terrestrial Virtual Channel Table # 1byte table_id = 0xc8 # 1111 static bits # 12bits length including crc # 2bytes TSID (transport stream id) = 0x0b21 (how is this generated?) # 11 static bits # 5bits version_no = 1 # 1bit current_next_indicator = 1 # 1byte section_number = 0 (only one section) # 1bytes last_section_number = 0 (only one section) # 1byte protocol_version = 0 # 1byte number of channels # gen_vct_channel(channel) # CRC_32 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 # channels is a dict with the key being the primary channel name (short_name) return self.format_video_packets([msg])
def update_sdt_names(self, _sdt_msg, _service_provider, _service_name): # must start with x474011 (pid for SDT is x0011 # update the full msg length 2 bytes with first 4 bits as xF at position 7-8 (includes crc) # update descriptor length lower 12 bits at position 20-21 (remaining bytes - crc) # x48 service descriptor tag # 1 byte length of service provider # byte string of the service provider # 1 byte length of service name # byte string of the service name # crc if _sdt_msg[:3] != b'\x47\x40\x11': self.logger.info('WRONG ATSC MSG {}'.format(bytes(_sdt_msg[:20]).hex())) return _sdt_msg 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 = _sdt_msg[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 = _sdt_msg[:5] + msg + crc msg = msg.ljust(len(_sdt_msg), b'\xFF') return msg
def gen_err_response(self, _frame_type, _tag, _text): # This is a tag type of HDHOMERUN_ERROR_MESSAGE # does not include the crc msg = msgs[_tag].format(*_text).encode() tag = utils.set_u8(HDHOMERUN_ERROR_MESSAGE) err_resp = utils.set_str(msg, True) msg_len = utils.set_u16(len(tag) + len(err_resp)) response = _frame_type + msg_len + tag + err_resp return response
def gen_sld(self, _base_pid, _elements): # Table 6.29 Service Location Descriptor # Appears in each channel in the VCT # 8bit = tag = 0xA1 # 8bit = description length # 3bits = 111 static bits # 13bits = PCR_PID = 0x1FFF or the program ID value found in the TS_prog_map # 8bits = number of elements # for each element # 8bits = stream_type = 0x02 (video stream) or 0x81 (audio stream) # 3bits = 111 static bits # 13bits = PCR_PID = (same as abovefor video stream) unique for audio # 8*3bytes = lang (spa, eng, mul, null for video stream # # element is an array of languages # the PID is based on the program number. # Base_PID = prog_num << 4 # Video Stream Element = Base_PID + 1 with the 12th bit set # Audio Stream Elements = Vide Stream PID + 4 with the 12th bit set, then +1 for each additional lang elem_len = utils.set_u8(len(_elements) + 1) video_pid = utils.set_u16(_base_pid + 1 + 57344) # E000 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 # E000 (starts at 0x34 and increments by 1) 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 datagram_received(self, _data, _host_port): """Handle a received multicast datagram.""" (host, port) = _host_port if self.config['hdhomerun']['udp_netmask'] is None: is_allowed = True else: net = IPv4Network(self.config['hdhomerun']['udp_netmask']) is_allowed = IPv4Address(host) in net if is_allowed: self.logger.debug('UDP: from {}:{}'.format(host, port)) try: (frame_type, msg_len, device_type, sub_dt_len, sub_dt, device_id, sub_did_len, sub_did) = \ struct.unpack('>HHBBIBBI', _data[0:-4]) (crc, ) = struct.unpack('<I', _data[-4:]) except ValueError as err: self.logger.error('UDP: {}'.format(err)) return if frame_type != HDHOMERUN_TYPE_DISCOVER_REQ: self.logger.error( 'UDP: Unknown from type = {}'.format(frame_type)) else: msg_type = bytes.fromhex('0003') header = bytes.fromhex('010400000001') device_id = bytes.fromhex('0204' + self.config['hdhomerun']['hdhr_id']) base_url = 'http://' + \ self.config['main']['plex_accessible_ip'] + \ ':' + str(self.config['main']['web_admin_port']) base_url_msg = b'\x2a' + utils.set_str(base_url.encode(), False) tuner_count = b'\x10\x01' + utils.set_u8( self.config['main']['tuner_count']) lineup_url = base_url + '/lineup.json' lineup_url = b'\x27' + utils.set_str(lineup_url.encode(), False) msg = header + device_id + base_url_msg + tuner_count + lineup_url msg_len = utils.set_u16(len(msg)) response = msg_type + msg_len + msg x = zlib.crc32(response) crc = struct.pack('<I', x) response += crc self.logger.debug('UDP Response={} {}'.format( _host_port, response)) self.sock_multicast.sendto(response, _host_port)
def gen_stt(self): # Table 6.1 System Time Table # 1byte table_id = 0xcd # 1111 static bits # 12bits length including crc # 2bytes table id extension = 0x0000 # 2bits = 11 static bits # 5bits version_no = 1 # 1bit current_next_indicator = 1 # 1byte section_number = 0 (only one section) # 1byte last_section_number = 0 (only one section) # 1byte protocol_version = 0 # 4bytes system time = time since 1980 (GPS) # 1byte GPS_UTC_offset = 12 (last checked 2021) # 2bytes daylight_saving = 0x60 # 1bit ds_status # 2bits 11 static bits # 5bits DS day of month # 8bits DS hour # CRC_32 # 475f fb17 00cd f011 0000 c100 0000 ..G_............ # 0x01b0: 4d3c e809 1060 00f3 30ca 76 # 1295837193 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_multiple_string_structure(self, _names): # Table 6.39 Multiple String Structure # event titles, long channel names, the ETT messages, and RRT text items # allows for upto 255 character strings # 8bit = array size # for each name # 3byte ISO_639_language_string = 'eng' # 1byte segments = normally 0x01 # for each segment # 1byte compression = 0x00 (not compression) # 1byte mode = 0x00 (used with unicode 2 byte letters, assume ASCII) # 1byte length in bytes = calculated # string in byte format. 1 byte per character 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_pat(self, _mux_stream): # Table Program Association Table : MPEG-2 protocol # 1byte table_id = 0x00 # 1011 static bits # 12bits length including crc # 2bytes = tsid (0000 11xx xxxx xxxx) # 2bits = 11 static bits # 5bits version_no = 1 # 1bit current_next_indicator = 1 # 1byte section_number = 0 (only one section) # 1byte last_section_number = 0 (only one section) # gen_pat_channels() # crc 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_pmt(self, _channels): # Table Program Map Table : MPEG-2 protocol # # DATA EXAMPLE # 0001 b009 ffff c300 00d5 dcfb 4c # 1byte table_id = 0x02 # 1011 static bits # 12bits length including crc # 2bytes = program number (like 6) # 2bits = 11 static bits # 5bits version_no = 1 # 1bit current_next_indicator = 1 # 1byte section_number = 0 (only one section) # 1byte last_section_number = 0 (only one section) # 3bits = 111 static bits # 13bits = PCR_PID (like for prog_num 3 = 61. seems to always end in a 1) # 4bits = 1111 static bits # 12bits = program_info_length # for loop of descriptors # 05 name of the channel (GA94) # # 1bytes = stream_type = x02 # 3bits = 111 static bits # 13bits = elem_PID # 4bits = 1111 static bits # 12bits = num of descr # for each descr # crc # NOTE: all transmissions had a zero sections transmission # search 0x0020.*0001 ... # there is one pmt per channel # returns an array of msgs to send. one per channel. # 4740 5016 0002 .....[.,..G@P... # 0x0030: b042 0003 c300 00e0 51f0 1610 06c0 0271 .B......Q......q # 0x0040: c000 0087 06c1 0101 00f2 0005 0447 4139 .............GA9 # 0x0050: 3402 e051 f003 0601 0281 e054 f012 0504 4..Q.......T.... # 0x0060: 4143 2d33 810a 0828 05ff 0f00 bf65 6e67 AC-3...(.....eng # 0x0070: 47ab 58d1 # descriptors # 10 smoothing_buffer_descriptor # f0 1610 06c0 0271 c000 0087 06c1 0101 00f2 0005 0447 4139 34 KDTN-DT KPTD-LD KDTN-ES # f0 0810 06c0 bd62 c008 00 KUVN-DT Bounce ESCAPE LAFF KSTR-DT GRIT # f0 12 0a04 656e 6700 810a e828 05ff 0f01 bf65 6e67 # f0 0810 06c0 bd62 c008 00 KTVT-DT StartTV DABL FAVE # f0 0605 0447 4139 34 KTXT-DT COMET CHARGE TBD SBN (31) # f0 1f05 0447 4139 3487 17c1 0102 00f3 04f1 0f01 656e 6701 0000 0754 562d 5047 2d56 KTXT-DT COMET CHARGE TBD SBN (51) # f0 1a05 0447 4139 3487 12c1 0101 00f2 0c01 656e 6701 0000 0454 562d 47 KTXT-DT COMET CHARGE TBD SBN (71) # f0 0605 0447 4139 34 KTXT-DT COMET CHARGE TBD SBN (61) # f0 3c05 0447 4139 3487 34c2 0101 00f3 0d01 656e 6701 0000 # 0554 562d 5047 0201 00f4 1c01 656e 6701 0000 1450 4720 # 2853 7572 762e 2070 6172 656e 7461 6c65 29 KTXT-DT COMET CHARGE TBD SBN (41) # f0 00 TBN_HD Hilsng SMILE Enlace POSITIV (31 41 51 61 71) # f0 00 ION qubo IONPlus Shop QVC HSN (31 41 51 61 71 81) # f0 0810 06c0 bd62 c008 00 KXAS-DT COZI-TV NBCLX (31 41 51) # f0 0810 06c0 bd62 c008 00 KDAF-DT Antenna Court CHARGE (31 41 51 61) # f0 0810 06c0 bd62 c008 00 KTXA-DT MeTV ThisTV Circle HSN (31 41 51 61 71) # f0 1610 06c0 0271 c000 0087 06c1 0101 00f2 0005 0447 4139 34 KXDA-LD (EBETV) KXDA-LD (ALCANCE) KXDA-LD KXDA-LD (BIBLIATV) (31 41 51 61 71) # f0 0810 06c0 bd62 c008 00 DECADES KDFW-DT KDFW_D3 GetTV (31 41 51 61) # f0 1a87 12c1 0101 00f2 0c01 656e 6701 0000 0454 562d 4705 0447 4139 34 KERA-HD4 kids Create (31 41 51) # f0 00 KFWD BizTV 52_4 SBN JTV CRTV AChurch (31 41 51 61 71 81 91) # # Assume no base descriptors \xf0 \x00 # Assume no video stream type descriptor 02 E0 xx F0 00 # Assume no audio stream type descriptor 81 E0 xx F0 00 # # 1bytes = stream_type = x02 # 3bits = 111 static bits # 13bits = elem_PID # 4bits = 1111 static bits # 12bits = num of descr # for each descr # # 02 E0 31 F0 00 (31 is the PID) # 02 E0 51 F0 05 # 02 E0 91 F0 0E # 02 E0 51 F0 03 06 01 02 # 02 E0 31 F0 12 06 01 02 # 02 E0 31 F0 12 06 01 02 86 0D E2 65 6E 67 C1 FF FF 65 6E 67 7E FF FF (x86 CCT) # Audio PIDs 34 35 36 44 54 # 81 E0 74 F0 00 (a3 descr) # 81 E0 94 F0 00 # 05 04 41 43 2D 33 (reg descr optional) # 81 E0 35 F0 18 # 81 0A 08 28 05 FF 37 01 BF 73 70 61 # 0A 04 73 70 61 00 # all audio is x4-x9 where x is the video PID so 31 is 34 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 create_getset_response(self, _req_dict, _address): (host, port) = _address frame_type = utils.set_u16(HDHOMERUN_TYPE_GETSET_RSP) name = _req_dict[HDHOMERUN_GETSET_NAME] name_str = name.decode('utf-8') if HDHOMERUN_GETSET_VALUE in _req_dict.keys(): value = _req_dict[HDHOMERUN_GETSET_VALUE] else: value = None if name == b'/sys/model': # required to id the device name_resp = utils.set_u8(HDHOMERUN_GETSET_NAME) + utils.set_str( name, True) value_resp = utils.set_u8(HDHOMERUN_GETSET_VALUE) + utils.set_str( b'hdhomerun4_atsc', True) msg_len = utils.set_u16(len(name_resp) + len(value_resp)) response = frame_type + msg_len + name_resp + value_resp x = zlib.crc32(response) crc = struct.pack('<I', x) response += crc return response elif name_str.startswith('/tuner'): tuner_index = int(name_str[6]) if name_str.endswith('/lockkey'): self.logger.error( 'TCP: NOT IMPLEMENTED GETSET LOCKKEY MSG REQUEST: {} '. format(_req_dict)) response = self.gen_err_response(frame_type, 'lockedErrMsg', [host]) x = zlib.crc32(response) crc = struct.pack('<I', x) response += crc return response elif name_str.endswith('/status'): tuner_status = self.tuners[tuner_index]['status'] if tuner_status == 'Scan': response = self.gen_err_response(frame_type, 'scanErrMsg', [host]) else: value_resp = utils.set_u8(HDHOMERUN_GETSET_VALUE) \ + utils.set_str(tuner_status_msg[tuner_status], True) name_resp = utils.set_u8( HDHOMERUN_GETSET_NAME) + utils.set_str(name, True) msg_len = utils.set_u16(len(name_resp) + len(value_resp)) response = frame_type + msg_len + name_resp + value_resp x = zlib.crc32(response) crc = struct.pack('<I', x) response += crc return response elif name_str.endswith('/vchannel'): tuner_status = self.tuners[tuner_index]['status'] if tuner_status == 'Stream': value_resp = utils.set_u8(HDHOMERUN_GETSET_VALUE) \ + utils.set_str(self.tuners[tuner_index]['channel'].encode(), True) else: value_resp = utils.set_u8(HDHOMERUN_GETSET_VALUE) \ + utils.set_str('none', True) name_resp = utils.set_u8( HDHOMERUN_GETSET_NAME) + utils.set_str(name, True) msg_len = utils.set_u16(len(name_resp) + len(value_resp)) response = frame_type + msg_len + name_resp + value_resp x = zlib.crc32(response) crc = struct.pack('<I', x) response += crc return response else: self.logger.error( 'TCP: NOT IMPLEMENTED GETSET MSG REQUEST: {} '.format( _req_dict)) return None else: self.logger.error( 'TCP: 3 UNKNOWN GETSET MSG REQUEST: {} '.format(_req_dict)) return None