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_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 gen_channel_longnames(self, names): # Table 6.28 Extended Channel Name Descriptor # channel name for the virtual channel containing this descriptor # allows for upto 245 character strings # 8bit = tag = 0xA0 # 8bit = description length # long_channel_name_text = gen_multiple_string_structure() long_name = self.gen_multiple_string_structure(names) return ATSC_EXTENDED_CHANNEL_DESCR_TAG + utils.set_u8(len(long_name)) + long_name
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_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_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_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 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