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")