SCTE35Encoder.py
# -*- coding: utf-8 -*-
import bitstring
import sys, traceback
import pprint
import binascii
import pycrc
from crccheck.crc import Crc32Mpeg2, CrcXmodem
from crccheck.checksum import Checksum32
import crcmod.predefined
import base64
import os 

class SCTE35Encoder():

    def __init__(self, _pid, _outfile,triggertype,_pts,_duration,_upid,_ptsadjustment = 0): # constructor
        self.pid =  _pid
        self.outputfilename =  _outfile
        self.pts = _pts
        self.duration = _duration
        print ("UPID: " + _upid)
        _upid = (str(_upid).zfill(32))                                     # pad upid with 0 so we have 38 bytes. the value 32 comes from we prepend LBTY+version+command automatically which is 6 bytes
        if str(triggertype) == "0":
            self.upid = [76,66,84,89,0,0] + [int(d) for d in (_upid)]      # automatically prepends LBTY + Version + command (trigger or pre-trigger) 
        elif str(triggertype) == "1":
            self.upid = [76,66,84,89,0,16] + [int(d) for d in (_upid)]     # automatically prepends LBTY + Version + command (trigger or pre-trigger) 
        else:
            raise Exception('triggertype not recognized: [' + str(triggertype) + ']')
        
        print (_upid)
        self.ptsadjustment = _ptsadjustment
        self.start()
        
    def __add_time_signal(self,bitarray,value):
        bitarray.append(bitstring.BitArray('bool=True'))                #  time_specified_flag
        #if above is true
        bitarray.append(bitstring.BitArray('uint:6=63'))                #  reserved
        bitarray.append(bitstring.BitArray('uint:33=' + str(value)))    #  pts_time
        
    def __add_splice_descriptor(self,duration,upid):                    #  a splice_descriptor encapsulates segmentation descriptor 

        subdesc = self.__get_segmentation_body(duration,upid);
        size_subdesc = len(subdesc.bytes) + 4;                          # +4 because we need to add the bytes of CUEI string to the "size"
        desc = bitstring.BitArray()
        desc.append(bitstring.BitArray('uint:8=2'))                     #  splice_descriptor_tag
        desc.append(bitstring.BitArray('uint:8=' + str(size_subdesc)))  #  splice_descriptor_length
        desc.append(bitstring.BitArray('uint:32='+str(1129661769)))     #  identifier (CUEI = 1129661769)
        desc.append(subdesc)
        return desc
        # TODO: add Content first, then calculate length, then build message

    def __get_segmentation_body(self,duration,upid):

        seg_duration_flag = "True"                                      
        body = bitstring.BitArray()
        body.append(bitstring.BitArray('uint:32=3'))                    #   DYNAMIC segmentation_event_id 
        body.append(bitstring.BitArray('bool=False'))                   #   event_cancel_indicator
        body.append(bitstring.BitArray('uint:7=1'))                     #   reserved 7 bit
        # if cancel indicator == 0
        body.append(bitstring.BitArray('bool=True'))                    #   program_segmentation_flag 
        body.append(bitstring.BitArray('bool=' + seg_duration_flag))    #   segmentation_duration_flag 
        body.append(bitstring.BitArray('bool=True'))                    #   delivery_not_restricted_flag. Make sure to implement sub-fields in case this value is False
        
        # TODO: if delivery_not_restricted_flag == 0, add  web_delivery_allowed_flag no_regional_blackout_flag  archive_allowed_flag  device_restrictions
        
        body.append(bitstring.BitArray('uint:5=1'))                     #   reserved because delivery_not_restricted is true 
        
        # TODO: if program_segmentation_flag == 0, add  component_count and for each component:  component_tag,  reserved,  pts_offset 

        if seg_duration_flag=="True":
            body.append(bitstring.BitArray('uint:40=' + str(duration))) #   DYNAMIC segmentation duration
            
        body.append(bitstring.BitArray('uint:8=12'))                    #   segmentation_upid_type - MPU = 0xC 
        body.append(bitstring.BitArray('uint:8=38'))                    #   segmentation_upid_length - The value of segmentation_upid_length should be in accordance with the value of segmentation_upid_type.
        
        for char in upid:                                      
            body.append(bitstring.BitArray('uint:8=' + str(char)))      #   appends the LBTY part (upid)
        
        body.append(bitstring.BitArray('uint:8=54'))                    #   segmentation_type_id 0x36
        body.append(bitstring.BitArray('uint:8=0'))                     #   segment_num # only if id is 36
        body.append(bitstring.BitArray('uint:8=0'))                     #   segments_expected # only if id is 36
        
        return body

    def dump(self,obj):
        for attr in dir(obj):
            print("obj.%s = %r" % (attr, getattr(obj, attr)))
    # insert package start code and PID

    def __CRC32_from_bitstring(self,bitstring):
        crc32_func = crcmod.predefined.mkCrcFun('crc-32-mpeg')
        buf = crc32_func((bitstring))
        return "%08X" % buf

    def start(self):
        #BASIC PACKET INFO
        o = bitstring.BitArray('uint:8=71')                     # Sync-Byte = 47 decimal or 0x71 hex
        o.append(bitstring.BitArray('bool=False'))              # Transport error indicator
        o.append(bitstring.BitArray('bool=True'))               # Payload start indicator, for SCTE Messages always true
        o.append(bitstring.BitArray('bool=False'))              # Transport Priority, for SCTE Messages always false
        o.append(bitstring.BitArray('uint:13=' + str((int(self.pid,0))))) 
        o.append(bitstring.BitArray('0b00'))                    # 2bit Transport Scrambling Control
        o.append(bitstring.BitArray('0b01'))                    # 2bit Adaptation Field Control - for SCTE Messages only 01 supported (no adaption field, just payload)

        o.append(bitstring.BitArray('uint:4=0'))                # Packet Counter, DYNAMIC
        o.append(bitstring.BitArray('uint:8=0'))                # Adaption Field always 0? Included bits: 1 Bit: discontinuity_indicator,1 Bit: random_access_indicator,1 Bit: elementary_stream_priority_indicator,1 Bit: PCR_flag,1 Bit: OPCR_flag,1 Bit: splicing_point_flag,1 Bit: transport_private_data_flag,1 Bit: adaptation_field_extension_flag

        o.append(bitstring.BitArray('hex:8=0xFC'))              # table_id This is an 8-bit field. Its value shall be 0xFC. 
        o.append(bitstring.BitArray('bool=False'))              # section_syntax_indicator The section_syntax_indicator is a 1-bit field that should always be set to '0' indicating that MPEG short sections are to be used. 
        o.append(bitstring.BitArray('bool=False'))              # private  This is a 1-bit flag that shall be set to 0. 
        o.append(bitstring.BitArray('0b11'))                    # need to add 2 bits with value 1, reason yet unknown 
        
        #START OF SPLICE INFO SECTION as per SCTE2016
        
        # before adding the rest, we need to insert the size of the rest, use the section object to collect everything
        section = bitstring.BitArray();
        section.append(bitstring.BitArray('uint:8=0'))        # protocol_version is an 8 bit unsigned integer field whose function is to allow, in the future, this table type to carry parameters that may be structured differently than those defined in the current protocol. At present, the only valid value for protocol_version is zero. Non-zero values of protocol_version may be used by a future version of this standard to indicate structurally different tables
        section.append(bitstring.BitArray('bool=False'))      # encrypted_packet - When this bit is set to '1'. it indicates that portions of the splice_info_section, starting with splice_command_type and ending with and including E_CRC_32, are encrypted. When this bit is set to '0', no part of this message is encrypted. The potentially encrypted portions of the splice_info_table are indicated by an E in the Encrypted column of Table 5. 
        section.append(bitstring.BitArray('uint:6=0'))        # encryption_algorithm This 6 bit unsigned integer specifies which encryption algorithm was used to encrypt the #current message. When the encrypted_packet bit is zero, this field is present but undefined. Refer to section 11, and specifically Table 26  Encryption algorithmfor details on the use of this field. @encryptionAlgorithm [Conditional Mandatory, xsd:unsignedByte] If the EncryptedPacket Element is present this value shall be provided.  It is intended that the first device that restamps pcr/pts/dts and that passes the cueing message will insert a value into the pts_adjustment field, which is the delta time between this devices input time domain and its output time domain. All subsequent devices, which also restamp pcr/pts/dts, may further alter the pts_adjustment field by adding their delta time to the field’s existing delta time and placing the result back in the pts_adjustment field. Upon each alteration of the pts_adjustment field, the altering device shall recalculate and update the CRC_32 field. The pts_adjustment shall, at all times, be the proper value to use for conversion of the pts_time field to the curent time-base. The conversion is done by adding the two fields. In the presence of a wrap or overflow condition the carry shall be ignored. ptsAdjustment [Optional, PTSType] See section 13.2. 
        section.append(bitstring.BitArray('uint:33=' + str(self.ptsadjustment)))       # pts_adjustment POSSIBLE DYNAMIC A 33 bit unsigned integer that appears in the clear and that shall be used by a splicing device as an offset to be added to the (sometimes) encrypted pts_time field(s) throughout this message to obtain the intended splice time(s). When this field has a zero value, then the pts_time field(s) shall be used without an offset. Normally, the creator of a cueing message will place a zero value into this field. This adjustment value is the means by which an upstream device, which restamps pcr/pts/dts, may convey to the splicing device the means by which to convert the pts_time field of the message to a newly imposed time domain
        section.append(bitstring.BitArray('uint:8=0'))        # cw_index An 8 bit unsigned integer that conveys which control word (key) is to be used to decrypt the message. The splicing device may store up to 256 keys previously provided for this purpose. When the encrypted_packet bit is zero, this field is present but undefined. @cwIndex [Conditional Mandatory, xsd:unsignedByte] If the EncryptedPacket Element is present this value shall be provided 
        section.append(bitstring.BitArray('hex:12=0xFFF'))    # tier A 12-bit value used by the SCTE 35 message provider to assign messages to authorization tiers. This field may take any value between 0x000 and 0xFFF. The value of 0xFFF provides backwards compatibility and shall be ignored by downstream equipment. When using tier, the message provider should keep the entire message in a single transport stream packe
        section.append(bitstring.BitArray('uint:12=05'))      # splice_command_length (basically size of time_signal = 5bytes)  a 12 bit length of the splice command. The length shall represent the number of bytes following the splice_command_type up to but not including the descriptor_loop_length. Devices that are compliant with this version of the standard shall populate this field with the actual length. The value of 0xFFF provides backwards compatibility and shall be ignored by downstream equipment. 
        section.append(bitstring.BitArray('uint:8=06'))       # splice_command_type  An 8-bit unsigned integer which shall be assigned one of the values shown in column labeled splice_command_type value in Table 6. 
        
        # TIME SIGNAL 
        self.__add_time_signal(section,self.pts)                  # time_signal component DYNAMIC
        
        # DESCRIPTOR LOOP
        
        descriptor = (self.__add_splice_descriptor(self.duration,self.upid))                   # adds CUEI descriptor containing LBTY descriptor
        desc_size = len(descriptor.bytes)
        section.append(bitstring.BitArray('uint:16='+str(desc_size)))        # descriptor loop length 
        section.append(descriptor)     
        section_size = len(section.bytes)
        o.append(bitstring.BitArray('uint:12=' + str(section_size)))      # section length This is a 12-bit field specifying the number of remaining bytes in the splice_info_section immediately following the section_length field up to the end of the splice_info_section. The value in this field shall not exceed 4093
        o.append(section)
        
        #write output file
        f = open(self.outputfilename, 'wb')
        o.tofile(f)
        f.close()
        print ("Wrote output file: " + self.outputfilename)
        
        #calculate and write CRC32 
        del o[0:8] # delete first 8 bits, start code 47, they are not included in checksum
        crc = (self.__CRC32_from_bitstring(o.tobytes()));
        f = open(self.outputfilename, 'ab')
        f.write(binascii.a2b_hex(crc))
        f.close()
        print ("Wrote checksum to file: " + crc )

        # output base64 for testing
        f = open(self.outputfilename, 'rb')
        f.seek(5)
        encoded_bytes = base64.b64encode(f.read())
        f.close()
        print ("Base64: " + str(encoded_bytes))
        
        #check filesize and append 0xFF until we have 188bytes
        statinfo = os.stat(self.outputfilename)
        fillsize = 188-statinfo.st_size
        print ("fillsize "+str(fillsize))
        if fillsize < 0:
            raise Exception("Error, outputfile " + self.outputfilename + " got bigger than 188 bytes!")
        f = open(self.outputfilename, 'ab')
        for x in range(0,fillsize):
            print (x)
            f.write(bytes([255]))
        f.close()
        


if __name__ == "__main__":
    SCTE35Encoder("c:\\temp\\file.bin",0,0,0,"abc-123-abc-123-az")