Example #1
0
 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
Example #2
0
    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
Example #3
0
    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])
Example #4
0
    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
Example #5
0
 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
Example #6
0
    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
Example #7
0
    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)
Example #8
0
    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])
Example #9
0
 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
Example #10
0
 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])
Example #11
0
    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)]
Example #12
0
    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