├── CNAME ├── CODE_OF_CONDUCT.md ├── Encoding.md ├── EncodingPipeMore.md ├── FastStart.md ├── LICENSE ├── Makefile ├── README.md ├── _config.yml ├── basic.md ├── benchmarking.md ├── bin └── threefive ├── cc.md ├── cli.md ├── cliencde.md ├── cue.md ├── dash.md ├── examples ├── 35decode │ ├── 35decode │ ├── Makefile │ └── README.md ├── README.md ├── cli.py ├── continuity_counters │ ├── README.md │ └── re_cc.py ├── dtmf │ └── dtmf_descriptor.py ├── encode │ ├── edit_break_duration.py │ ├── encode_time_signal.py │ └── streamedit.py ├── hls │ ├── Readme.md │ ├── hasp.py │ ├── hlsparse.py │ ├── scte35.m3u8 │ └── simple-scte35.m3u8 ├── spliceinsert │ ├── splice_insert.py │ └── splice_insert_too.py ├── splicenull │ └── splice_null.py ├── stream │ ├── cool_decode_http.py │ ├── cue2vtt.py │ ├── cue_list.py │ ├── cue_list_too.py │ ├── decode_http.py │ ├── decode_proxy.py │ ├── preroll.py │ └── scte35packets.py ├── timesignal │ ├── time_signal_blackout_override_program_end.py │ ├── time_signal_placement_opportunity_end.py │ └── time_signal_program_overlap.py └── upid │ ├── custom_upid_handling.py │ ├── multi_upid.py │ ├── threefiveadfr │ ├── upid_combo.py │ └── upid_custom_output.py ├── ffmpegscte35.md ├── ffrewrite.md ├── help.md ├── hlsparse.md ├── issues.md ├── json.txt ├── latest-cli.md ├── libthreefivexml.md ├── mpd_parser.md ├── new2471.md ├── newcli.md ├── oo.md ├── prog.md ├── segment.md ├── setup.py ├── sidecar.md ├── speedtest.md ├── threefive-ffmpeg.md ├── threefive ├── README.md ├── __init__.py ├── base.py ├── bitn.py ├── commands.py ├── crc.py ├── cue.py ├── decode.py ├── descriptors.py ├── encode.py ├── hls.py ├── packetdata.py ├── section.py ├── segment.py ├── segmentation.py ├── showcues.py ├── sixfix.py ├── smoketest.py ├── stream.py ├── streamtypes.py ├── stuff.py ├── superkabuki.py ├── upids.py ├── version.py └── xml.py ├── trigger.md └── xml.md /CNAME: -------------------------------------------------------------------------------- 1 | git.futzu.com 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ### Code of Conduct 2 | > I'm cool, you be cool too. 3 | -------------------------------------------------------------------------------- /EncodingPipeMore.md: -------------------------------------------------------------------------------- 1 | # More on Encoding. 2 | A lot of people have been looking at the encoding page, and I think the encoding is a pretty good explanation 3 | but there is a little more encoding stuff in threefive and rather than complicated the encoding page, I wanted 4 | to put it here. 5 | 6 | ### The Cue class has a lot of stuff I've never really mentioned. I wanted to wait a while and see if anything blew up. A lot of the Cue methods are for encoding. 7 | 8 | ### Cues can be encoded as base64, hex, integers, or bytes 9 | * Docs 10 | ```py3 11 | | encode(self) 12 | | Cue.encode() converts SCTE35 data 13 | | to a base64 encoded string. 14 | | 15 | | encode_as_hex(self) 16 | | encode_as_hex returns self.bites as 17 | | a hex string 18 | | 19 | | encode_as_int(self) 20 | | encode_as_int returns self.bites as an int. 21 | | 22 | ``` 23 | * How to use 24 | ```py3 25 | a@slow:~/SCTE-35-HLS-Sideways$ pypy3 26 | Python 3.9.16 (7.3.11+dfsg-2, Feb 06 2023, 16:52:03) 27 | [PyPy 7.3.11 with GCC 12.2.0] on linux 28 | Type "help", "copyright", "credits" or "license" for more information. 29 | 30 | >>>> from threefive import Cue,TimeSignal 31 | 32 | >>>> cue = Cue() 33 | 34 | >>>> cue.command=TimeSignal() 35 | >>>> cue.command.time_specified_flag=True 36 | >>>> cue.command.pts_time=12345.13 37 | 38 | >>>> cue.encode() 39 | '/DAWAAAAAAAAAP/wBQb+Qjl0xAAAMsxgbg==' 40 | 41 | >>>> cue.encode_as_hex() 42 | '0xfc301600000000000000fff00506fe423974c4000032cc606e' 43 | 44 | >>>> cue.encode_as_int() 45 | 1583008701074197245727019716796221242345910914446764125479022 46 | 47 | ## Note: it's Cue.bites, since bytes() is a builtin python function. 48 | 49 | >>>> cue.bites 50 | b'\xfc0\x16\x00\x00\x00\x00\x00\x00\x00\xff\xf0\x05\x06\xfeB9t\xc4\x00\x002\xcc`n' 51 | 52 | >>>> 53 | 54 | 55 | ``` 56 | ### The Cue class also has methods to load data from a dictionary or JSON for encoding 57 | 58 | ```py3 59 | | load(self, stuff) 60 | | Cue.load loads SCTE35 data for encoding. 61 | | stuff is a dict or json 62 | | with any or all of these keys 63 | | stuff = { 64 | | 'info_section': {dict} , 65 | | 'command': {dict}, 66 | | 'descriptors': [list of {dicts}], 67 | | } 68 | | 69 | | load_command(self, cmd) 70 | | load_command loads data for Cue.command 71 | | cmd should be a dict. 72 | | if 'command_type' is included, 73 | | the command instance will be created. 74 | | 75 | | load_descriptors(self, dlist) 76 | | Load_descriptors loads descriptor data. 77 | | dlist is a list of dicts 78 | | if 'tag' is included in each dict, 79 | | a descriptor instance will be created. 80 | | 81 | | load_info_section(self, isec) 82 | | load_info_section loads data for Cue.info_section 83 | | isec should be a dict. 84 | | if 'splice_command_type' is included, 85 | | an empty command instance will be created for Cue.command 86 | ``` 87 | -------------------------------------------------------------------------------- /FastStart.md: -------------------------------------------------------------------------------- 1 | ### Up and Running in Less Than 7 Seconds. 2 | 3 | #### Step One of Two 4 | 5 | * Estimated time to complete this step : 2.0 - 2.5 seconds 6 | 7 | ```js 8 | pip3 install threefive 9 | ``` 10 | 11 | #### Step Two of Two 12 | 13 | * Estimated time to complete this step : 3.0 - 4.5 seconds 14 | 15 | ```smalltalk 16 | a@fumatica:~$ pypy3 17 | Python 3.8.13 (7.3.9+dfsg-1, Apr 01 2022, 03:05:43) 18 | [PyPy 7.3.9 with GCC 11.2.0] on linux 19 | Type "help", "copyright", "credits" or "license" for more information. 20 | 21 | from threefive import Stream 22 | strm = Stream("https://futzu.com/xaa.ts")` 23 | strm.decode() 24 | ``` 25 | 26 | 27 | 28 | * Ouput looks like this. 29 | ```lua 30 | { 31 | "info_section": { 32 | "table_id": "0xfc", 33 | "section_syntax_indicator": false, 34 | "private": false, 35 | "sap_type": "0x3", 36 | "sap_details": "No Sap Type", 37 | "section_length": 42, 38 | "protocol_version": 0, 39 | "encrypted_packet": false, 40 | "encryption_algorithm": 0, 41 | "pts_adjustment_ticks": 0, 42 | "pts_adjustment": 0.0, 43 | "cw_index": "0xff", 44 | "tier": "0xfff", 45 | "splice_command_length": 15, 46 | "splice_command_type": 5, 47 | "descriptor_loop_length": 10, 48 | "crc": "0x1ed64bdb" 49 | }, 50 | "command": { 51 | "command_length": 15, 52 | "command_type": 5, 53 | "name": "Splice Insert", 54 | "time_specified_flag": true, 55 | "pts_time": 22026.133267, 56 | "pts_time_ticks": 1982351994, 57 | "splice_event_id": 18, 58 | "splice_event_cancel_indicator": false, 59 | "out_of_network_indicator": false, 60 | "program_splice_flag": true, 61 | "duration_flag": false, 62 | "splice_immediate_flag": false, 63 | "unique_program_id": 1, 64 | "avail_num": 18, 65 | "avail_expected": 255 66 | }, 67 | "descriptors": [ 68 | { 69 | "tag": 0, 70 | "descriptor_length": 8, 71 | "name": "Avail Descriptor", 72 | "identifier": "CUEI", 73 | "provider_avail_id": 18 74 | } 75 | ], 76 | "packet_data": { 77 | "pid": "0x41f", 78 | "program": 1050, 79 | "pts_ticks": 1981774196, 80 | "pts": 22019.713289 81 | } 82 | } 83 | 84 | 85 | 86 | ``` 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 The Python Packaging Authority 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PY3 = python3 2 | PIP3 = pip3 3 | PYPY3 = pypy3 4 | 5 | default: install 6 | 7 | fmt: 8 | black threefive/ 9 | black examples/ 10 | black setup.py 11 | 12 | commit: fmt 13 | git pull 14 | git commit $1 15 | git push 16 | 17 | clean: 18 | rm -f dist/* 19 | rm -rf build/* 20 | 21 | pypy3: clean 22 | $(PYPY3) setup.py sdist bdist_wheel 23 | $(PYPY3) setup.py install 24 | 25 | install: clean pkg 26 | $(PY3) setup.py install --user 27 | 28 | pkg: clean 29 | $(PY3) setup.py sdist bdist_wheel 30 | 31 | uninstall: clean 32 | $(PIP3) uninstall threefive 33 | 34 | upload: clean pkg 35 | twine upload dist/* 36 | 37 | upgrade: 38 | $(PIP3) install --upgrade threefive 39 | 40 | cli: 41 | sed -i s/$(PYPY3)/$(PY3)/ cuei 42 | install cuei /usr/local/bin 43 | 44 | pypy3-cli: 45 | sed -i s/$(PY3)/$(PYPY3)/ cuei 46 | install cuei /usr/local/bin 47 | 48 | 49 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /basic.md: -------------------------------------------------------------------------------- 1 | # How to Use threefive.Cue 2 | 3 | ```py3 4 | a@fu:~$ pypy3 5 | Python 3.9.16 (7.3.11+dfsg-2+deb12u2, May 20 2024, 22:08:06) 6 | [PyPy 7.3.11 with GCC 12.2.0] on linux 7 | Type "help", "copyright", "credits" or "license" for more information. 8 | >>>> import threefive 9 | >>>> threefive.version 10 | '2.4.69' 11 | ``` 12 | * __threefive.Cue__ decodes from Base64, Integer, Bytes, Hex string, or Hex literal. 13 | 14 | ```py3 15 | >>>> data ='/DA2AAHOR/nwAAAABQb+PnGRBwAgAh5DVUVJSAAAbH/PAAE1ODcICAAAAAAt86rXNAAAAACwnuYL' 16 | >>>> cue=threefive.Cue(data) 17 | >>>> cue.decode() 18 | True 19 | ``` 20 | ```py3 21 | >>>> data=183300239680257886524837670232596047318548299418173015139317649469035368522983613603709023498560369107202549510766205308206365804540761364 22 | >>>> cue=threefive.Cue(data) 23 | >>>> cue.decode() 24 | True 25 | ``` 26 | ```py3 27 | >>>> data=b'\xfc06\x00\x01\xceG\xf9\xf0\x00\x00\x00\x05\x06\xfe>q\x91\x07\x00 \\x02\x1eCUEIH\x00\x00l\x7f\xcf\x00\x01587\x08\x08\x00\x00\x00\x00-\xf3\xaa\xd74\\x00\x00\x00\x00\xb0\x9e\xe6\x0b' 28 | >>>> cue=threefive.Cue(data) 29 | >>>> cue.decode() 30 | True 31 | ``` 32 | ```py3 33 | >>>> data= '0xfc30360001ce47f9f00000000506fe00a98ac70020021e435545494800006c7fcf00013538370808000000002df3aad73400000000f1e09514' 34 | >>>> cue=threefive.Cue(data) 35 | >>>> cue.decode() 36 | True 37 | ``` 38 | ```py3 39 | >>>> data=0xfc30360001ce47f9f00000000506fe00a98ac70020021e435545494800006c7fcf00013538370808000000002df3aad73400000000f1e09514 40 | >>>> cue=threefive.Cue(data) 41 | >>>> cue.decode() 42 | True 43 | ``` 44 | * Easy conversion from Base64 to Hex. 45 | ```py3 46 | >>>> b64 ='/DA2AAHOR/nwAAAABQb+QZYoVAAgAh5DVUVJSAAAbX/PAAEo0GwICAAAAAAt86rXNAAAAAAfqUZ6' 47 | >>>> cue=threefive.Cue(b64) 48 | >>>> cue.decode() 49 | True 50 | >>>> hexed = cue.encode_as_hex() 51 | >>>> hexed 52 | '0xfc30360001ce47f9f00000000506fe419628540020021e435545494800006d7fcf000128d06c0808000000002df3aad734000000001fa9467a' 53 | >>>> 54 | ``` 55 | * __cue.show()__ displays a decoded cue as JSON. 56 | 57 | ```smalltalk 58 | >>>> cue.show() 59 | { 60 | "info_section": { 61 | "table_id": "0xfc", 62 | "section_syntax_indicator": false, 63 | "private": false, 64 | "sap_type": "0x03", 65 | "sap_details": "No Sap Type", 66 | "section_length": 54, 67 | "protocol_version": 0, 68 | "encrypted_packet": false, 69 | "encryption_algorithm": 0, 70 | "pts_adjustment": 86175.453689, 71 | "cw_index": "0x00", 72 | "tier": "0x00", 73 | "splice_command_length": 5, 74 | "splice_command_type": 6, 75 | "descriptor_loop_length": 32, 76 | "crc": "0xb09ee60b" 77 | }, 78 | "command": { 79 | "command_length": 5, 80 | "command_type": 6, 81 | "name": "Time Signal", 82 | "time_specified_flag": true, 83 | "pts_time": 11640.3343 84 | }, 85 | "descriptors": [ 86 | { 87 | "tag": 2, 88 | "descriptor_length": 30, 89 | "name": "Segmentation Descriptor", 90 | "identifier": "CUEI", 91 | "segmentation_event_id": "0x4800006c", 92 | "segmentation_event_cancel_indicator": false, 93 | "segmentation_event_id_compliance_indicator": true, 94 | "program_segmentation_flag": true, 95 | "segmentation_duration_flag": true, 96 | "delivery_not_restricted_flag": false, 97 | "web_delivery_allowed_flag": false, 98 | "no_regional_blackout_flag": true, 99 | "archive_allowed_flag": true, 100 | "device_restrictions": "No Restrictions", 101 | "segmentation_duration": 225.166833, 102 | "segmentation_message": "Provider Placement Opportunity Start", 103 | "segmentation_upid_type": 8, 104 | "segmentation_upid_type_name": "AiringID", 105 | "segmentation_upid_length": 8, 106 | "segmentation_upid": "0x2df3aad7", 107 | "segmentation_type_id": 52, 108 | "segment_num": 0, 109 | "segments_expected": 0, 110 | "sub_segment_num": 0, 111 | "sub_segments_expected": 0 112 | } 113 | ] 114 | } 115 | ``` 116 | 117 | * Access the Cue instance as a whole or by part. 118 | ```awk 119 | >>>> cue 120 | ``` 121 | ```js 122 | {'bites': b'\xfc06\x00\x01\xceG\xf9\xf0\x00\x00\x00\x05\x06\xfe>q\x91\x07\x00 \x02\x1eCUEIH\x00\x00l\x7f 123 | \xcf\x00\x01587\x08\x08\x00\x00\x00\x00-\xf3\xaa\xd74\x00\x00\x00\x00\xb0\x9e\xe6\x0b', 124 | 'info_section': {'table_id': '0xfc', 125 | 'section_syntax_indicator': False, 'private': False, 'sap_type': '0x03', 'sap_details': 'No Sap Type', 126 | 'section_length': 54, 'protocol_version': 0, 'encrypted_packet': False, 'encryption_algorithm': 0, 127 | 'pts_adjustment': 86175.453689, 'cw_index': '0x00', 'tier': '0x00', 'splice_command_length': 5, 128 | 'splice_command_type': 6, 'descriptor_loop_length': 32, 'crc': '0xb09ee60b'}, 129 | 'command': {'command_length': 5, 'command_type': 6, 'name': 'Time Signal', 130 | 'time_specified_flag': True, 'pts_time': 11640.3343}, 131 | 'descriptors': [{'tag': 2, 'descriptor_length': 30, 'name': 'Segmentation Descriptor', 'identifier': 'CUEI', 132 | 'private_data': None, 'segmentation_event_id': '0x4800006c', 'segmentation_event_cancel_indicator': False, 133 | 'segmentation_event_id_compliance_indicator': True, 'program_segmentation_flag': True, 134 | 'segmentation_duration_flag': True, 'delivery_not_restricted_flag': False, 'web_delivery_allowed_flag': False, 135 | 'no_regional_blackout_flag': True, 'archive_allowed_flag': True, 'device_restrictions': 'No Restrictions', 136 | 'segmentation_duration': 225.166833, 'segmentation_message': 'Provider Placement Opportunity Start', 137 | 'segmentation_upid_type': 8, 'segmentation_upid_type_name': 'AiringID', 'segmentation_upid_length': 8, 138 | 'segmentation_upid': '0x2df3aad7', 'segmentation_type_id': 52, 'segment_num': 0, 'segments_expected': 0, 139 | 'sub_segment_num': 0, 'sub_segments_expected': 0}], 'packet_data': None} 140 | ``` 141 | ```py3 142 | >>>> cue.info_section 143 | ``` 144 | ```js 145 | {'table_id': '0xfc', 'section_syntax_indicator': False, 'private': False, 'sap_type': '0x03', 146 | 'sap_details': 'No Sap Type', 'section_length': 54, 'protocol_version': 0, 'encrypted_packet': False, 147 | 'encryption_algorithm': 0, 'pts_adjustment': 86175.453689, 'cw_index': '0x00', 'tier': '0x00', 148 | 'splice_command_length': 5, 'splice_command_type': 6, 'descriptor_loop_length': 32, 'crc': '0xb09ee60b'} 149 | ``` 150 | ```py3 151 | >>>> cue.command 152 | ``` 153 | ```js 154 | {'command_length': 5, 'command_type': 6, 'name': 'Time Signal', 'time_specified_flag': True, 'pts_time': 11640.3343} 155 | ``` 156 | ``` 157 | >>>> cue.descriptors[0] 158 | ``` 159 | ```js 160 | {'tag': 2, 'descriptor_length': 30, 'name': 'Segmentation Descriptor', 'identifier': 'CUEI', 'private_data': None, 161 | 'segmentation_event_id': '0x4800006c', 'segmentation_event_cancel_indicator': False, 162 | 'segmentation_event_id_compliance_indicator': True, 'program_segmentation_flag': True, 'segmentation_duration_flag': True, 163 | 'delivery_not_restricted_flag': False, 'web_delivery_allowed_flag': True, 'no_regional_blackout_flag': True, 164 | 'archive_allowed_flag': True, 'device_restrictions': 'No Restrictions', 'segmentation_duration': 225.166833, 165 | 'segmentation_message': 'Provider Placement Opportunity Start', 'segmentation_upid_type': 8, 166 | 'segmentation_upid_type_name': 'AiringID', 'segmentation_upid_length': 8, 'segmentation_upid': '0x2df3aad7', 167 | 'segmentation_type_id': 52, 'segment_num': 0, 'segments_expected': 0, 'sub_segment_num': 0, 'sub_segments_expected': 0} 168 | ``` 169 | 170 | * Dot notation works as you expect. 171 | ```py3 172 | >>>> cue.info_section.pts_adjustment 173 | 174 | 86175.453689 175 | 176 | >>>> cue.info_section.pts_adjustment=5.123 177 | 178 | >>>> cue.info_section.pts_adjustmen 179 | 180 | 5.123 181 | 182 | ``` 183 | ```py3 184 | >>>> cue.command.pts_time 185 | 186 | 11640.3343 187 | 188 | >>>> cue.command.pts_time = 123.456789 189 | ``` 190 | ```py3 191 | >>> cue.descriptors[0].segmentation_event_id='0x4800006d' 192 | 193 | >>>> cue.descriptors[0].segmentation_event_id 194 | 195 | '0x4800006d' 196 | ``` 197 | 198 | * threefive objects have the __has()__ method. 199 | ```py3 200 | >>>> cue.has('info_section') 201 | True 202 | ``` 203 | ```py3 204 | >>>> cue.has("fu") 205 | False 206 | ``` 207 | ```py3 208 | >>>> cue.info_section.has('table_id') 209 | True 210 | ``` 211 | ```py3 212 | >>>> cue.command.has('time_specified_flag') 213 | True 214 | ``` 215 | ```py3 216 | >>>> cue.descriptors[0].has("sub_segment_num") 217 | True 218 | ``` 219 | * A Cue instance can be encoded to Base64, Hex, or Integer. 220 | ```py3 221 | >>>> cue.encode() 222 | '/DA2AAHOR/nwAAAABQb+AKmKxwAgAh5DVUVJSAAAbH/PAAE1ODcICAAAAAAt86rXNAAAAADx4JUU' 223 | ``` 224 | ```py3 225 | >>>> cue.encode_as_hex() 226 | '0xfc30360001ce47f9f00000000506fe00a98ac70020021e435545494800006c7fcf00013538370808000000002df3aad73400000000f1e09514' 227 | ``` 228 | ```py3 229 | >>>> cue.encode_as_int() 230 | 183300239680257886524837670232596047318548299418173015139317649469035368522983613603709023498560369107202549510766205308206365804540761364 231 | ``` 232 | * threefive.Cue.bites is the raw bytes of the Cue. 233 | ```py3 234 | >>>> cue.bites 235 | b'\xfc06\x00\x01\xceG\xf9\xf0\x00\x00\x00\x05\x06\xfe\x00\xa9\x8a\xc7\x00 \x02\x1eCUEIH\x00\x00l\x7f\xcf\x00\x01587\x08\x08\x00\x00\x00\x00-\xf3\xaa\xd74\x00\x00\x00\x00\xf1\xe0\x95\x14' 236 | ``` 237 | -------------------------------------------------------------------------------- /benchmarking.md: -------------------------------------------------------------------------------- 1 | ### the video 2 | ```sh 3 | a@debian:~/build/scte35-threefive$ ls -alh ~/mpegts/plp0.ts 4 | -rw-r--r-- 1 a a 3.7G May 21 2020 /home/a/mpegts/plp0.ts 5 | ``` 6 | 7 | ### the code 8 | ```python3 9 | import sys 10 | import threefive 11 | 12 | if __name__ == "__main__": 13 | if len(sys.argv) > 1: 14 | for arg in sys.argv[1:]: 15 | strm = threefive.Stream(arg) 16 | strm.decode() 17 | ``` 18 | ### python 3.10 19 | ```js 20 | a@debian:~/$ time python3 test.py /home/a/mpegts/plp0.ts 21 | 22 | real 0m14.293s 23 | user 0m12.666s 24 | sys 0m1.406s 25 | 26 | ``` 27 | ### PyPy 7.3.9 28 | 29 | ```js 30 | a@debian:~/$ time pypy3 test.py /home/a/mpegts/plp0.ts 31 | 32 | real 0m2.879s <-- Boom goes the dynamite. 33 | user 0m2.576s 34 | sys 0m1.101s 35 | ``` 36 | -------------------------------------------------------------------------------- /bin/threefive: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | threefive command line SCTE35 decoder. 5 | 6 | """ 7 | 8 | 9 | import sys 10 | from new_reader import reader 11 | from threefive import Cue, Stream, print2, decode, version 12 | from threefive.hls import cli as hlscli 13 | from threefive.sixfix import sixfix 14 | from threefive.superkabuki import SuperKabuki 15 | from sideways import cli as sidecli 16 | 17 | REV = "\033[7;1m" 18 | NORM = "\033[27m\033[0m" 19 | NORM = "\033[0m" 20 | BLUE = "\033[36;1;51m" 21 | 22 | B = "\033[7;1m" 23 | U = "\033[m" 24 | 25 | 26 | class SupaStream(Stream): 27 | """ 28 | SupaStream is subclass of Stream used 29 | to print raw SCTE-35 packets. 30 | """ 31 | 32 | def _parse_scte35(self, pkt, pid): 33 | print2(pkt) 34 | print2('') 35 | super()._parse_scte35(pkt, pid) 36 | 37 | 38 | def mk_sidecar(cue): 39 | """ 40 | mk_sidecar generates a sidecar file with the SCTE-35 Cues 41 | """ 42 | pts = 0.0 43 | with open("sidecar.txt", "a") as sidecar: 44 | cue.show() 45 | if cue.packet_data.pts: 46 | pts = cue.packet_data.pts 47 | data = f"{pts},{cue.encode()}\n" 48 | sidecar.write(data) 49 | 50 | 51 | HELP = f""" 52 | threefive {U} 53 | 54 | {B} Default {U} {BLUE}The default action is to read a input and write a SCTE-35 output.{U} 55 | 56 | {BLUE}inputs {U} mpegts, base64, hex, json, xml, and xmlbin{U}. 57 | 58 | {BLUE}outputs{U} base64, bytes, hex, int, json, xml, and xmlbin.{U} 59 | 60 | {BLUE}threefive can read from {U} strings, files, stdin, http(s), multicast, and Udp. 61 | 62 | Input:{U} Output:{U} 63 | {U}{BLUE} mpegts {U} {U}{BLUE} base64 {NORM} threefive https://example.com/video.ts base64 64 | {U}{BLUE} xml {U} {U}{BLUE} bytes {NORM} threefive bytes < xml.xml 65 | {U}{BLUE} base64 {U} {U}{BLUE} hex {NORM} threefive '/DAWAAAAAAAAAP/wBQb+AKmKxwAACzuu2Q==' hex 66 | {U}{BLUE} xmlbin {U} {U}{BLUE} int {NORM} threefive int < xml.xml 67 | {U}{BLUE} mpegts {U} {U}{BLUE} json {NORM} threefive video.ts 68 | {U}{BLUE} json {U} {U}{BLUE} xml {NORM} threefive < json.json xml 69 | {U}{BLUE} hex {U} {U}{BLUE} xmlbin {NORM} threefive 0xfc301600000000000000fff00506fe00a98ac700000b3baed9 xmlbin 70 | 71 | {B} hls {U}{NORM} {BLUE} SCTE-35 hls decode help:{U} threefive hls help 72 | 73 | threefive hls https://example.com/master.m3u8 74 | 75 | {B} hls encode {U} {BLUE} SCTE-35 hls encode help:{U}threefive hls encode help 76 | 77 | threefive hls encode -i https://example.com/master.m3u8 -s sidecar.txt -o output_dir 78 | 79 | {B} inject {U}{NORM}{BLUE} Inject an mpegts stream with a SCTE-35 sidecar file at pid:{NORM} 80 | 81 | threefive inject video.ts with sidecar.txt at 333 82 | 83 | {B} packets {U}{NORM}{BLUE} Print raw SCTE-35 packets from multicast mpegts video:{NORM} 84 | 85 | threefive packets udp://@235.35.3.5:3535 86 | 87 | {B} proxy {U}{NORM}{BLUE} Parse a https stream and write raw video to stdout:{NORM} 88 | 89 | threefive proxy video.ts 90 | 91 | {B} pts {U}{NORM}{BLUE} Print PTS from mpegts video:{NORM} threefive pts video.ts 92 | 93 | {B} sidecar {U}{NORM}{BLUE} Parse a stream, write pts,write SCTE-35 Cues to sidecar.txt:{NORM} 94 | 95 | threefive sidecar video.ts 96 | 97 | {B} sixfix {U}{NORM}{BLUE} Fix SCTE-35 data mangled by ffmpeg:{NORM} threefive sixfix video.ts 98 | 99 | {B} show {U}{NORM}{BLUE} Probe mpegts video:{NORM} threefive show video.ts 100 | 101 | {B} version {U}{NORM}{BLUE} Show version:{NORM} threefive version 102 | 103 | {B} help {U}{NORM} {BLUE}Help:{NORM} threefive help 104 | 105 | """ 106 | 107 | 108 | def read_buff(): 109 | with reader(sys.stdin.buffer) as stuff: 110 | try: 111 | inbuff = stuff.read().decode() 112 | return inbuff 113 | except: 114 | decode(stuff.read()) 115 | 116 | def mk_args(keys): 117 | """ 118 | mk_args generates a list of args for inputs 119 | if no args are present,read from sys.stdin.buffer 120 | """ 121 | args = [arg for arg in sys.argv[1:] if arg not in keys] 122 | if not args: 123 | args.append(read_buff()) 124 | return args 125 | 126 | 127 | # print_map functions 128 | 129 | 130 | def hls(): 131 | sys.argv.remove("hls") 132 | if "encode" in sys.argv: 133 | sidecli() 134 | else: 135 | hlscli() 136 | 137 | 138 | def print_help(): 139 | """ 140 | print_help checks sys.argv for the word help 141 | and displays the help if found 142 | """ 143 | print2(HELP) 144 | sys.exit() 145 | 146 | 147 | def print_version(): 148 | """ 149 | print_version print the threefive version 150 | """ 151 | print2(version) 152 | 153 | 154 | def superkabuki(): 155 | args = {} 156 | if "inject" in sys.argv: 157 | args["input"] = sys.argv[sys.argv.index("inject") + 1] 158 | if "with" in sys.argv: 159 | args["sidecar"] = sys.argv[sys.argv.index("with") + 1] 160 | if "at" in sys.argv: 161 | args["scte35_pid"] = sys.argv[sys.argv.index("at") + 1] 162 | supak = SuperKabuki() 163 | supak.apply_args(args) 164 | supak.encode() 165 | return True 166 | print2("threefive mpegts inject {infile} with {sidecar_file} at {pid}") 167 | return False 168 | 169 | 170 | print_map = { 171 | "hls": hls, 172 | "help": print_help, 173 | "version": print_version, 174 | "inject": superkabuki, 175 | } 176 | 177 | 178 | def chk_print_map(): 179 | """ 180 | chk_print_map checks for print_map.keys() in sys.argv 181 | """ 182 | for k, v in print_map.items(): 183 | if k in sys.argv: 184 | v() 185 | sys.exit() 186 | 187 | 188 | # functions for mpegts_map 189 | 190 | 191 | def packet_chk(this): 192 | """ 193 | packet_chk checks for the packet keyword 194 | and displays SCTE-35 packets if present. 195 | """ 196 | supa = SupaStream(this) 197 | supa.decode() 198 | 199 | 200 | def proxy_chk(this): 201 | """ 202 | proxy_chk checks for the proxy keyword 203 | and proxies the stream to stdout if present. 204 | proxy_chk also writes pts,cue pairs to sidecar.txt 205 | """ 206 | strm = Stream(this) 207 | strm.proxy(func=mk_sidecar) 208 | 209 | 210 | def pts_chk(this): 211 | """ 212 | pts_chk is used to display PTS. 213 | """ 214 | strm = Stream(this) 215 | strm.show_pts() 216 | 217 | 218 | def show_chk(this): 219 | """ 220 | show_chk checks for the show keyword 221 | and displays the streams if present. 222 | """ 223 | strm = Stream(this) 224 | strm.show() 225 | 226 | 227 | def sidecar_chk(this): 228 | """ 229 | sidecar_chk checks for the sidecar keyword and 230 | generates a sidecar file if present. 231 | """ 232 | strm = Stream(this) 233 | strm.decode(func=mk_sidecar) 234 | 235 | 236 | mpegts_map = { 237 | "packets": packet_chk, 238 | "proxy": proxy_chk, 239 | "pts": pts_chk, 240 | "show": show_chk, 241 | "sidecar": sidecar_chk, 242 | "sixfix": sixfix, 243 | } 244 | 245 | 246 | def chk_mpegts_map(): 247 | """ 248 | chk_mpegts_map check sys.argv for mpegts_map keys 249 | """ 250 | m_keys = list(mpegts_map.keys()) 251 | args = mk_args(m_keys) 252 | for key in m_keys: 253 | if key in sys.argv: 254 | for arg in args: 255 | mpegts_map[key](arg) 256 | sys.exit() 257 | 258 | 259 | # functions for funk_map 260 | 261 | 262 | def base64_out(cue): 263 | """ 264 | print SCTE-35 from mpegts as base64 265 | """ 266 | print2(cue.encode()) 267 | 268 | 269 | def bytes_out(cue): 270 | """ 271 | print SCTE-35 from mpegts as base64 272 | """ 273 | print2(cue.bites) 274 | 275 | 276 | def hex_out(cue): 277 | """ 278 | print SCTE-35 from mpegts as hex 279 | """ 280 | print2(cue.encode2hex()) 281 | 282 | 283 | def int_out(cue): 284 | """ 285 | print SCTE-35 from mpegts as int 286 | """ 287 | print2(cue.encode2int()) 288 | 289 | 290 | def json_out(cue): 291 | """ 292 | print SCTE-35 from mpegts as json 293 | """ 294 | cue.show() 295 | 296 | 297 | def xml_out(cue): 298 | """ 299 | xml_out prints cue as xml 300 | """ 301 | print2(cue.xml(xmlbin=False)) 302 | 303 | 304 | def xmlbin_out(cue): 305 | """ 306 | xml_out prints cue as xml 307 | """ 308 | print2(cue.xml()) 309 | 310 | 311 | funk_map = { 312 | "base64": base64_out, 313 | "bytes": bytes_out, 314 | "hex": hex_out, 315 | "int": int_out, 316 | "json": json_out, 317 | "xml": xml_out, 318 | "xmlbin": xmlbin_out, 319 | } 320 | 321 | 322 | def funk(): 323 | """ 324 | return a func 325 | if a key in out_map 326 | is also in sys.argv 327 | """ 328 | func =json_out 329 | for k, v in funk_map.items(): 330 | if k in sys.argv: 331 | func=v 332 | return func 333 | 334 | def to_funk(this): 335 | """ 336 | to_funk prints a cue in a variety of formats. 337 | """ 338 | try: 339 | # mpegts streams handled here. 340 | strm = Stream(this) 341 | func = funk() 342 | strm.decode(func=func) 343 | except: # try to load json or xml 344 | try: 345 | cue=Cue() 346 | cue.load(this) 347 | except: 348 | try: # handle base64, bytes, and hex. 349 | cue = Cue(this) 350 | cue.decode() 351 | except: 352 | pass 353 | if cue: 354 | cue.encode() 355 | func = funk() 356 | func(cue) 357 | 358 | 359 | def chk_funk_map(): 360 | """ 361 | chk_func_map checks for func_map.keys() in sys.argv 362 | """ 363 | funk_keys = list(funk_map.keys()) 364 | args = mk_args(funk_keys) 365 | superfunk = decode 366 | if [fkey for fkey in funk_keys if fkey in sys.argv]: 367 | superfunk = to_funk 368 | else: 369 | superfunk = to_funk 370 | [superfunk(arg) for arg in args] 371 | sys.exit() 372 | 373 | 374 | if __name__ == "__main__": 375 | if len(sys.argv) < 2: 376 | sys.argv.append('json') 377 | chk_print_map() 378 | chk_mpegts_map() 379 | chk_funk_map() 380 | # else: 381 | # decode(sys.stdin.buffer) 382 | -------------------------------------------------------------------------------- /cliencde.md: -------------------------------------------------------------------------------- 1 | # Command Line Encoding 2 | 3 | ### [Encoding from JSON](#encoding) 4 | 5 | ### [Converting SCTE-35 formats](#converting) 6 | 7 | 8 | ## Encoding 9 | 10 | The threefive cli tool can now encode JSON to SCTE-35. The JSON needs to be in threefive format. 11 | 12 | * `a@fu:~$ threefive '/DAWAAAAAAAAAP/wBQb+ABt4xwAAwhCGHw==' 2> json.txt` 13 | 14 | * `cat json.txt` 15 | 16 | ```json 17 | { 18 | "info_section": { 19 | "table_id": "0xfc", 20 | "section_syntax_indicator": false, 21 | "private": false, 22 | "sap_type": "0x03", 23 | "sap_details": "No Sap Type", 24 | "section_length": 22, 25 | "protocol_version": 0, 26 | "encrypted_packet": false, 27 | "encryption_algorithm": 0, 28 | "pts_adjustment": 0.0, 29 | "cw_index": "0x00", 30 | "tier": "0x0fff", 31 | "splice_command_length": 5, 32 | "splice_command_type": 6, 33 | "descriptor_loop_length": 0, 34 | "crc": "0xc210861f" 35 | }, 36 | "command": { 37 | "command_length": 5, 38 | "command_type": 6, 39 | "name": "Time Signal", 40 | "time_specified_flag": true, 41 | "pts_time": 20.004344 42 | }, 43 | "descriptors": [] 44 | } 45 | 46 | ``` 47 | * Change the pts_time 48 | * Here I do it with sed, you can use any editor 49 | 50 | ```js 51 | sed -i 's/20.004344/60.0/' json.txt 52 | ``` 53 | * Re-encode as Base64 54 | ```lua 55 | a@fu:~$ cat json.txt | threefive encode 56 | 57 | /DAWAAAAAAAAAP/wBQb+AFJlwAAAZ1PBRA== 58 | ``` 59 | 60 | * Re-encode as Hex 61 | ```lua 62 | a@fu:~$ cat json.txt | threefive encode hex 63 | 0xfc301600000000000000fff00506fe005265c000006753c144 64 | ``` 65 | 66 | * Re-encode as an integer 67 | ```lua 68 | a@fu:~$ cat json.txt | threefive encode int 69 | 1583008701074197245727019716796221242034694813189400685691204 70 | ``` 71 | * Re-encode as bytes 72 | ```lua 73 | a@fu:~$ cat json.txt | threefive encode bytes 74 | b'\xfc0\x16\x00\x00\x00\x00\x00\x00\x00\xff\xf0\x05\x06\xfe\x00Re\xc0\x00\x00gS\xc1D' 75 | ``` 76 | 77 | ___ 78 | 79 | ## Converting 80 | * Converting involves piping one threefive command into another. 81 | * `From`: 82 | * Base64 83 | * Hex 84 | * `To` 85 | * Base64 86 | * Bytes 87 | * Hex 88 | * Int 89 | 90 | * `Base64` to `hex` 91 | ```js 92 | a@fu:~$ threefive '/DAWAAAAAAAAAP/wBQb+ABt4xwAAwhCGHw==' 2>&1 | threefive encode hex 93 | ``` 94 | ```js 95 | 0xfc301600000000000000fff00506fe001b78c70000c210861f 96 | ``` 97 | * `Hex` to `Integer` 98 | ```js 99 | a@fu:~$ threefive '0xfc301600000000000000fff00506fe001b78c70000c210861f' 2>&1| threefive encode int 100 | ``` 101 | 102 | ```js 103 | 1583008701074197245727019716796221242033681613329959740278303 104 | ``` 105 | 106 | * `Hex` to `Bytes` 107 | ```js 108 | 109 | a@fu:~$ threefive '0xfc301600000000000000fff00506fe001b78c70000c210861f' 2>&1| threefive encode bytes 110 | ``` 111 | ```js 112 | b'\xfc0\x16\x00\x00\x00\x00\x00\x00\x00\xff\xf0\x05\x06\xfe\x00\x1bx\xc7\x00\x00\xc2\x10\x86\x1f' 113 | ``` 114 | -------------------------------------------------------------------------------- /cue.md: -------------------------------------------------------------------------------- 1 | ### A threefive SCTE-35 Cue 2 | 3 | > __1__ ```info_section``` (__Required__) 4 | ```smalltalk 5 | "info_section": { 6 | "table_id": "0xfc", 7 | "section_syntax_indicator": false, 8 | "private": false, 9 | "sap_type": "0x3", 10 | "sap_details": "No Sap Type", 11 | "section_length": 42, 12 | "protocol_version": 0, 13 | "encrypted_packet": false, 14 | "encryption_algorithm": 0, 15 | "pts_adjustment": 0.0, 16 | "cw_index": "0xff", 17 | "tier": "0xfff", 18 | "splice_command_length": 15, 19 | "splice_command_type": 5, 20 | "descriptor_loop_length": 10, 21 | "crc": "0xd7165c79" 22 | }, 23 | 24 | ``` 25 | > __1__ ```command``` (__Required__) 26 | ```smalltalk 27 | "command": { 28 | "command_length": 15, 29 | "command_type": 5, 30 | "name": "Splice Insert", 31 | "time_specified_flag": true, 32 | "pts_time": 23683.480033, 33 | "splice_event_id": 5690, 34 | "splice_event_cancel_indicator": false, 35 | "out_of_network_indicator": true, 36 | "program_splice_flag": true, 37 | "duration_flag": false, 38 | "splice_immediate_flag": false, 39 | "unique_program_id": 0, 40 | "avail_num": 0, 41 | "avail_expected": 0 42 | }, 43 | 44 | ``` 45 | 46 | > __0 or more__ ```descriptors``` (__Optional__) 47 | ```smalltalk 48 | "descriptors": [ 49 | { 50 | "tag": 2, 51 | "descriptor_length": 23, 52 | "name": "Segmentation Descriptor", 53 | "identifier": "CUEI", 54 | "components": [], 55 | "segmentation_event_id": "0x4800000a", 56 | "segmentation_event_cancel_indicator": false, 57 | "program_segmentation_flag": true, 58 | "segmentation_duration_flag": false, 59 | "delivery_not_restricted_flag": false, 60 | "web_delivery_allowed_flag": true, 61 | "no_regional_blackout_flag": true, 62 | "archive_allowed_flag": true, 63 | "device_restrictions": "No Restrictions", 64 | "segmentation_message": "Program Blackout Override", 65 | "segmentation_upid_type": 8, 66 | "segmentation_upid_type_name": "AiringID", 67 | "segmentation_upid_length": 8, 68 | "segmentation_upid": "0x2ca0a1e3", 69 | "segmentation_type_id": 24, 70 | "segment_num": 0, 71 | "segments_expected": 0 72 | }, 73 | { 74 | "tag": 2, 75 | "descriptor_length": 23, 76 | "name": "Segmentation Descriptor", 77 | "identifier": "CUEI", 78 | "components": [], 79 | "segmentation_event_id": "0x48000009", 80 | "segmentation_event_cancel_indicator": false, 81 | "program_segmentation_flag": true, 82 | "segmentation_duration_flag": false, 83 | "delivery_not_restricted_flag": false, 84 | "web_delivery_allowed_flag": true, 85 | "no_regional_blackout_flag": true, 86 | "archive_allowed_flag": true, 87 | "device_restrictions": "No Restrictions", 88 | "segmentation_message": "Program End", 89 | "segmentation_upid_type": 8, 90 | "segmentation_upid_type_name": "AiringID", 91 | "segmentation_upid_length": 8, 92 | "segmentation_upid": "0x2ca0a18a", 93 | "segmentation_type_id": 17, 94 | "segment_num": 0, 95 | "segments_expected": 0 96 | } 97 | ] 98 | 99 | ``` 100 | > ```packet_data``` (__Optional__) 101 | ```smalltalk 102 | "packet_data": { 103 | "pid": "0x40b", 104 | "program": 1030, 105 | "pts": 23678.080033 106 | } 107 | 108 | ``` 109 | -------------------------------------------------------------------------------- /examples/35decode/35decode: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pypy3 2 | """ 3 | 35 decode, command line SCTE35 decoder. 4 | 5 | ( type 'make cli' or 'make pypy3-cli' as root 6 | to install to /usr/local/bin ) 7 | 8 | use like: 9 | 10 | cat myvideo.ts | 35decode 11 | 12 | 35decode myvideo.ts yourvideo.ts someothervideo.ts 13 | 14 | 35decode mpegts_dir/*.ts 15 | 16 | 35decode '/DBZAAAAAAAA///wBQb+AAAAAABDAkFDVUVJAAAACn//AAApMuAPLXVybjp1dWlkOmFhODViYmI2LTVjNDMtNGI2YS1iZWJiLWVlM2IxM2ViNzk5ORAAAFz7UQA=' 17 | """ 18 | 19 | 20 | import sys 21 | import threefive 22 | 23 | if __name__ == '__main__': 24 | if len(sys.argv) > 1: 25 | for arg in sys.argv[1:]: 26 | threefive.decode(arg) 27 | 28 | else: 29 | threefive.decode(sys.stdin.buffer) 30 | -------------------------------------------------------------------------------- /examples/35decode/Makefile: -------------------------------------------------------------------------------- 1 | PY3 = python3 2 | PYPY3 = pypy3 3 | 4 | default: cli 5 | 6 | 7 | cli: 8 | sed -i s/$(PYPY3)/$(PY3)/ 35decode 9 | install 35decode /usr/local/bin 10 | 11 | pypy3-cli: 12 | sed -i s/$(PY3)/$(PYPY3)/ 35decode 13 | install 35decode /usr/local/bin 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/35decode/README.md: -------------------------------------------------------------------------------- 1 | 2 | ### 35 decode, command line SCTE35 decoder. 3 | 4 | >type 'make cli' or 'make pypy3-cli' as root to install to /usr/local/bin 5 | 6 | #### use like: 7 | ```fortran 8 | cat myvideo.ts | 35decode 9 | ``` 10 | ```fortran 11 | 35decode https://futzu.com/xaa.ts 12 | ``` 13 | ```fortran 14 | 35decode ~/myvideo.ts https://futzu.com/xaa.ts someothervideo.ts 15 | ``` 16 | ```fortran 17 | 35decode mpegts_dir/*.ts 18 | ``` 19 | ```fortran 20 | 35decode '/DBZAAAAAAAA///wBQb+AAAAAABDAkFDVUVJAAAACn//AAApMuAPLXVybjp1dWlkOmFhODViYmI2LTVjNDMtNGI2YS1iZWJiLWVlM2IxM2ViNzk5ORAAAFz7UQA=' 21 | ``` 22 | #### Output: 23 | ```fortran 24 | { 25 | "info_section": { 26 | "table_id": "0xfc", 27 | "section_syntax_indicator": false, 28 | "private": false, 29 | "sap_type": "0x3", 30 | "sap_details": "No Sap Type", 31 | "section_length": 171, 32 | "protocol_version": 0, 33 | "encrypted_packet": false, 34 | "encryption_algorithm": 0, 35 | "pts_adjustment_ticks": 0, 36 | "pts_adjustment": 0.0, 37 | "cw_index": "0x0", 38 | "tier": "0xfff", 39 | "splice_command_length": 5, 40 | "splice_command_type": 6, 41 | "descriptor_loop_length": 149, 42 | "crc": "0x8202d03b" 43 | }, 44 | "command": { 45 | "command_length": 5, 46 | "command_type": 6, 47 | "name": "Time Signal", 48 | "time_specified_flag": true, 49 | "pts_time": 17144.101244, 50 | "pts_ticks": 1542969112 51 | }, 52 | "descriptors": [ 53 | { 54 | "tag": 2, 55 | "descriptor_length": 49, 56 | "name": "Segmentation Descriptor", 57 | "identifier": "CUEI", 58 | "components": [], 59 | "segmentation_event_id": "0x970d471", 60 | "segmentation_event_cancel_indicator": false, 61 | "program_segmentation_flag": true, 62 | "segmentation_duration_flag": false, 63 | "delivery_not_restricted_flag": false, 64 | "web_delivery_allowed_flag": true, 65 | "no_regional_blackout_flag": true, 66 | "archive_allowed_flag": true, 67 | "device_restrictions": "No Restrictions", 68 | "segmentation_message": "Chapter End", 69 | "segmentation_upid_type": 1, 70 | "segmentation_upid_type_name": "Deprecated", 71 | "segmentation_upid_length": 34, 72 | "segmentation_upid": "fumatic", 73 | "segmentation_type_id": 33, 74 | "segment_num": 1, 75 | "segments_expected": 1 76 | }, 77 | { 78 | "tag": 2, 79 | "descriptor_length": 49, 80 | "name": "Segmentation Descriptor", 81 | "identifier": "CUEI", 82 | "components": [], 83 | "segmentation_event_id": "0x970d470", 84 | "segmentation_event_cancel_indicator": false, 85 | "program_segmentation_flag": true, 86 | "segmentation_duration_flag": false, 87 | "delivery_not_restricted_flag": false, 88 | "web_delivery_allowed_flag": true, 89 | "no_regional_blackout_flag": true, 90 | "archive_allowed_flag": true, 91 | "device_restrictions": "No Restrictions", 92 | "segmentation_message": "Program End", 93 | "segmentation_upid_type": 1, 94 | "segmentation_upid_type_name": "Deprecated", 95 | "segmentation_upid_length": 34, 96 | "segmentation_upid": "fumatic", 97 | "segmentation_type_id": 17, 98 | "segment_num": 1, 99 | "segments_expected": 1 100 | }, 101 | { 102 | "tag": 2, 103 | "descriptor_length": 19, 104 | "name": "Segmentation Descriptor", 105 | "identifier": "CUEI", 106 | "components": [], 107 | "segmentation_event_id": "0x9710f17", 108 | "segmentation_event_cancel_indicator": false, 109 | "program_segmentation_flag": true, 110 | "segmentation_duration_flag": false, 111 | "delivery_not_restricted_flag": false, 112 | "web_delivery_allowed_flag": true, 113 | "no_regional_blackout_flag": true, 114 | "archive_allowed_flag": true, 115 | "device_restrictions": "No Restrictions", 116 | "segmentation_message": "Program Start", 117 | "segmentation_upid_type": 1, 118 | "segmentation_upid_type_name": "Deprecated", 119 | "segmentation_upid_length": 4, 120 | "segmentation_upid": "fumatic", 121 | "segmentation_type_id": 16, 122 | "segment_num": 1, 123 | "segments_expected": 1 124 | }, 125 | { 126 | "tag": 2, 127 | "descriptor_length": 24, 128 | "name": "Segmentation Descriptor", 129 | "identifier": "CUEI", 130 | "components": [], 131 | "segmentation_event_id": "0x9710f18", 132 | "segmentation_event_cancel_indicator": false, 133 | "program_segmentation_flag": true, 134 | "segmentation_duration_flag": true, 135 | "delivery_not_restricted_flag": false, 136 | "web_delivery_allowed_flag": true, 137 | "no_regional_blackout_flag": true, 138 | "archive_allowed_flag": true, 139 | "device_restrictions": "No Restrictions", 140 | "segmentation_duration": 1343.0, 141 | "segmentation_duration_ticks": 120870000, 142 | "segmentation_message": "Chapter Start", 143 | "segmentation_upid_type": 1, 144 | "segmentation_upid_type_name": "Deprecated", 145 | "segmentation_upid_length": 4, 146 | "segmentation_upid": "fumatic", 147 | "segmentation_type_id": 32, 148 | "segment_num": 1, 149 | "segments_expected": 1 150 | } 151 | ], 152 | "packet_data": { 153 | "pid": "0x36", 154 | "program": 1, 155 | "pcr_ticks": 1542841778, 156 | "pcr": 17142.686422, 157 | "pts_ticks": 1542877610, 158 | "pts": 17143.084556 159 | } 160 | 161 | } 162 | ``` 163 | 164 | 165 | 166 | >I am water. Pour me in a cup, I become the cup. 167 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ### threefive Examples 2 | 3 | * `Segmentation Upids` 4 | * [Custom threefive cli tool with a modified mpu upid for the "Addressable TV" spec](https://github.com/futzu/threefive/blob/master/examples/upid/threefiveadfr) __New 01/11/2024__ 5 | 6 | * [Custom Handling MPU Upid data](https://github.com/futzu/threefive/blob/master/examples/upid/custom_upid_handling.py) 7 | * [Upids with Custom Output](https://github.com/futzu/threefive/blob/master/examples/upid/upid_custom_output.py) 8 | * [Multiple Segmentation Descriptors](https://github.com/futzu/threefive/blob/master/examples/upid/multi_upid.py) 9 | * [Combination Upid Segmentation Descriptor](https://github.com/futzu/threefive/blob/master/examples/upid/upid_combo.py) 10 | 11 | * `Stream` 12 | * [Show raw SCTE-35 packets in a file or stream](https://github.com/futzu/scte35parser-threefive/blob/master/examples/stream/scte35packets.py) __New 02/06/2024__ 13 | * [Return a dict of PTS timestamps and SCTE-35 cue strings from MPEGTS](https://github.com/futzu/scte35-threefive/blob/master/examples/stream/cue_list_too.py) 14 | * [Display SCTE35 info via Webvtt Subtitles](https://github.com/futzu/threefive/blob/master/examples/stream/cue2vtt.py) __Updated 07/01/2023!__ 15 | * [Return a list of SCTE-35 Cues from an MPEGTS file](https://github.com/futzu/threefive/blob/master/examples/stream/cue_list.py) 16 | * [Parsing SCTE35 from MPEGTS over HTTPS](https://github.com/futzu/threefive/blob/master/examples/stream/cool_decode_http.py) 17 | * [Parsing SCTE35 from MPEGTS over HTTPS](https://github.com/futzu/threefive/blob/master/examples/stream/decode_http.py) 18 | * [Stream.decode_proxy() Example](https://github.com/futzu/SCTE35-threefive/blob/master/examples/stream/decode_proxy.py) 19 | * [Show preroll](https://github.com/futzu/threefive/blob/master/examples/stream/preroll.py) 20 | 21 | * `HLS` 22 | * [Using threefive with HLS Manifests](https://github.com/futzu/SCTE35-threefive/tree/master/examples/hls) 23 | * [HASP Hls Aes Scte-35 Parser](https://github.com/futzu/threefive/blob/master/examples/hls/hasp.py) 24 | 25 | * `Time Signal` 26 | * [Time Signal Placement Opportunity End](https://github.com/futzu/threefive/blob/master/examples/timesignal/time_signal_placement_opportunity_end.py) 27 | * [Time Signal Program Overlap](https://github.com/futzu/threefive/blob/master/examples/timesignal/time_signal_program_overlap.py) 28 | * [Time Signal Program Start End](https://github.com/futzu/threefive/blob/master/examples/timesignal/time_signal_blackout_override_program_end.py) 29 | 30 | * `Encode` 31 | * [Edit Break Duration](https://github.com/futzu/scte35-threefive/blob/master/examples/encode/edit_break_duration.py) 32 | * [Encode Time Signal](https://github.com/futzu/scte35-threefive/blob/master/examples/encode/encode_time_signal.py) 33 | * [MPEGTS pass-through SCTE35 Cue Re-Encoding](https://github.com/futzu/scte35-threefive/blob/master/examples/encode/streamedit.py) 34 | 35 | * `Splice Insert` 36 | * [Splice Insert](https://github.com/futzu/SCTE35-threefive/blob/master/examples/spliceinsert/splice_insert.py) 37 | * [Splice Insert Too](https://github.com/futzu/SCTE35-threefive/blob/master/examples/spliceinsert/splice_insert_too.py) 38 | -------------------------------------------------------------------------------- /examples/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | 4 | Usage One: 5 | ---------- 6 | 7 | Pass one or more files, 8 | Base64 encoded strings, 9 | or Hex encoded strings 10 | on the command line. 11 | 12 | pypy3 cli.py ../u.ts \ 13 | /usr/a/35.ts \ 14 | "0xFC301100000000000000FFFFFF0000004F253396" \ 15 | "/DAvAAAAAAAA///wFAVIAACPf+/+c2nALv4AUsz1AAAAAAAKAAhDVUVJAAABNWLbowo=" \ 16 | /usr/a/cue.ts 17 | 18 | 19 | Usage Two: 20 | ---------- 21 | 22 | Pipe data into cli.py 23 | 24 | cat video.ts | pypy3 cli.py 25 | 26 | """ 27 | 28 | import sys 29 | import threefive 30 | 31 | 32 | def do(): 33 | if not sys.argv[1:]: 34 | threefive.decode() 35 | else: 36 | try: 37 | args = sys.argv[1:] 38 | for arg in args: 39 | print(f"Next is {arg}\n\n") 40 | threefive.decode(arg) 41 | except: 42 | pass 43 | 44 | 45 | if __name__ == "__main__": 46 | do() 47 | -------------------------------------------------------------------------------- /examples/continuity_counters/README.md: -------------------------------------------------------------------------------- 1 | ### Clear and reset continuity counters with re_cc.py 2 | 3 | * use like: 4 | ``` 5 | python3 re_cc.py video.ts 6 | 7 | ``` 8 | 9 | ![image](https://user-images.githubusercontent.com/52701496/154814807-5d222b59-ada9-4600-9226-1af297f58314.png) 10 | -------------------------------------------------------------------------------- /examples/continuity_counters/re_cc.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from functools import partial 3 | from threefive import Stream 4 | 5 | 6 | class ResetCC(Stream): 7 | def re_cc(self, proxy=True, fixed_file="re_cced.ts"): 8 | """ 9 | Stream.re_cc resets the continuity counters. 10 | MPEGTS packets are written to stdout for piping. 11 | """ 12 | re_filed = fixed_file 13 | if not self._find_start(): 14 | return False 15 | pcount = 128 16 | outfile = sys.stdout.buffer 17 | if not proxy: 18 | outfile = open(re_filed, "wb+") 19 | for chunk in iter(partial(self._tsdata.read, self._PACKET_SIZE * pcount), b""): 20 | chunky = memoryview(bytearray(chunk)) 21 | chunks = [ 22 | self._set_cc(chunky[i : i + self._PACKET_SIZE]) 23 | for i in range(0, len(chunky), self._PACKET_SIZE) 24 | ] 25 | outfile.write(b"".join(chunks)) 26 | chunky.release() 27 | self._tsdata.close() 28 | if not proxy: 29 | outfile.close() 30 | print(f"\nwrote {re_filed}\n") 31 | return True 32 | 33 | def re_cc_file(self, fixed_file): 34 | return self.re_cc(False, fixed_file) 35 | 36 | def _set_cc(self, pkt): 37 | pid = self._parse_pid(pkt[1], pkt[2]) 38 | if pid == 0x1FFF: 39 | return pkt 40 | new_cc = 0 41 | if pid in self.maps.pid_cc: 42 | last_cc = self.maps.pid_cc[pid] 43 | new_cc = (last_cc + 1) % 16 44 | # print(f"{new_cc}", end='\r') 45 | pkt[3] &= 0xF0 46 | pkt[3] += new_cc 47 | self.maps.pid_cc[pid] = new_cc 48 | return pkt 49 | 50 | 51 | if __name__ == "__main__": 52 | if len(sys.argv) < 3: 53 | print("usage: python3 re_cc.py in_file.ts out_file.ts") 54 | sys.exit() 55 | inarg = sys.argv[1] 56 | outarg = sys.argv[2] 57 | resetter = ResetCC(inarg) 58 | resetter.re_cc_file(outarg) 59 | -------------------------------------------------------------------------------- /examples/dtmf/dtmf_descriptor.py: -------------------------------------------------------------------------------- 1 | """ 2 | SCTE35 DTMF Descriptor Example 3 | 4 | Usage: 5 | pypy3 Dtmf_Descriptor.py 6 | 7 | """ 8 | 9 | 10 | from threefive import decode 11 | 12 | if __name__ == "__main__": 13 | DTMF = "/DAsAAAAAAAAAP/wDwUAAABef0/+zPACTQAAAAAADAEKQ1VFSbGfMTIxIxGolm0=" 14 | decode(DTMF) 15 | -------------------------------------------------------------------------------- /examples/encode/edit_break_duration.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example of Editing a Cue.Command and re-encoding 3 | """ 4 | 5 | import threefive 6 | 7 | BE64 = "/DAvAAAAAAAA///wFAVIAACPf+/+c2nALv4AUsz1AAAAAAAKAAhDVUVJAAABNWLbowo=" 8 | 9 | cue = threefive.Cue(BE64) 10 | cue.decode() 11 | cue.show() 12 | # use dot notation to access values and change them 13 | cue.command.break_duration = 60.0 14 | 15 | # Run cue.encode to generate new base64 string 16 | print(cue.encode()) 17 | 18 | # returns 19 | # '/DAvAAAAAAAA///wFAVIAACPf+/+c2nALv4Ae5igAAAAAAAKAAhDVUVJAAABNVB2fJs=' 20 | 21 | 22 | cue.show() 23 | -------------------------------------------------------------------------------- /examples/encode/encode_time_signal.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example of Encoding a Time Signal Command from Scratch 3 | """ 4 | import threefive 5 | 6 | cmd = threefive.TimeSignal() 7 | # {'command_length': 0, 'command_type': 6, 'bites': None, 'name': 'Time Signal', 'time_specified_flag': None, 'pts_time': None} 8 | 9 | # set the values needed 10 | cmd.time_specified_flag = True 11 | cmd.pts_time = 23000.677777 12 | 13 | # Create an empty Cue 14 | cue = threefive.Cue() 15 | 16 | # set cue.command to the TimeSignal Command cmd 17 | cue.command = cmd 18 | 19 | print(cue.encode()) 20 | # '/DAWAAAAAAAAAP/wBQb+e2KfxwAAN6nTrw==' 21 | 22 | # run cue.show() to check values. 23 | cue.show() 24 | -------------------------------------------------------------------------------- /examples/encode/streamedit.py: -------------------------------------------------------------------------------- 1 | """ 2 | streamedit.py 3 | 4 | An example of how to edit SCTE35 data 5 | in MPEGTS packets on the fly and pass 6 | the modifiied stream to stdout. 7 | 8 | Use like: 9 | 10 | pypy3 streamedit.py https://futzu.com/xaa.ts | pypy3 -c 'import threefive; threefive.decode()' 11 | 12 | """ 13 | 14 | from functools import partial 15 | import sys 16 | from threefive import Stream 17 | 18 | 19 | class Stream2(Stream): 20 | """ 21 | Stream2 is a subclass of threefive.Stream 22 | to demonstrate editing SCTE35 Cues in a MPEGTS stream. 23 | 24 | Use like: 25 | 26 | pypy3 streamedit.py myvideo.ts >> myvideoedited.ts 27 | 28 | 29 | 30 | 31 | BEFORE: 32 | { 33 | "info_section": { 34 | "table_id": "0xfc", 35 | "section_syntax_indicator": false, 36 | "private": false, 37 | "sap_type": "0x3", 38 | "sap_details": "No Sap Type", 39 | "section_length": 42, 40 | "protocol_version": 0, 41 | "encrypted_packet": false, 42 | "encryption_algorithm": 0, 43 | "pts_adjustment": 0.0, 44 | "cw_index": "0xff", 45 | "tier": "0xfff", 46 | "splice_command_length": 15, 47 | "splice_command_type": 5, 48 | "descriptor_loop_length": 10, 49 | "crc": "0xd7165c79" 50 | }, 51 | "command": { 52 | "command_length": 15, 53 | "command_type": 5, 54 | "name": "Splice Insert", 55 | "time_specified_flag": true, 56 | "pts_time": 23683.480033, 57 | "splice_event_id": 5690, 58 | "splice_event_cancel_indicator": false, 59 | "out_of_network_indicator": true, 60 | "program_splice_flag": true, 61 | "duration_flag": false, 62 | "splice_immediate_flag": false, 63 | "unique_program_id": 0, 64 | "avail_num": 0, 65 | "avail_expected": 0 66 | }, 67 | "descriptors": [ 68 | { 69 | "tag": 0, 70 | "descriptor_length": 8, 71 | "name": "Avail Descriptor", 72 | "identifier": "CUEI", 73 | "provider_avail_id": 0 74 | } 75 | ], 76 | "packet_data": { 77 | "pid": "0x40b", 78 | "program": 1030, 79 | "pcr": 23677.003267, 80 | "pts": 23677.030189 81 | } 82 | } 83 | AFTER: 84 | 85 | { 86 | "info_section": { 87 | "table_id": "0xfc", 88 | "section_syntax_indicator": false, 89 | "private": false, 90 | "sap_type": "0x3", 91 | "sap_details": "No Sap Type", 92 | "section_length": 42, 93 | "protocol_version": 0, 94 | "encrypted_packet": false, 95 | "encryption_algorithm": 0, 96 | "pts_adjustment": 109.55, # <-- Changed 97 | "cw_index": "0xff", 98 | "tier": "0xfff", 99 | "splice_command_length": 15, 100 | "splice_command_type": 5, 101 | "descriptor_loop_length": 10, 102 | "crc": "0x82fd33d9" 103 | }, 104 | "command": { 105 | "command_length": 15, 106 | "command_type": 5, 107 | "name": "Splice Insert", 108 | "time_specified_flag": true, 109 | "pts_time": 23683.480033, 110 | "splice_event_id": 5690, 111 | "splice_event_cancel_indicator": false, 112 | "out_of_network_indicator": true, 113 | "program_splice_flag": true, 114 | "duration_flag": false, 115 | "splice_immediate_flag": false, 116 | "unique_program_id": 999, # <-- Changed 117 | "avail_num": 0, 118 | "avail_expected": 0 119 | }, 120 | "descriptors": [ 121 | { 122 | "tag": 0, 123 | "descriptor_length": 8, 124 | "name": "Avail Descriptor", 125 | "identifier": "FUEI", # <-- Changed 126 | "provider_avail_id": 0 127 | } 128 | ], 129 | "packet_data": { 130 | "pid": "0x40b", 131 | "program": 1030, 132 | "pcr": 23677.003267, 133 | "pts": 23677.030189 134 | } 135 | } 136 | 137 | # The Changed cues will be in myvideoedited.ts, 138 | # Check it like this: 139 | 140 | from threefive import decode 141 | 142 | decode('myvideoedited.ts') 143 | 144 | 145 | """ 146 | 147 | _PAD = b"\xff" 148 | 149 | @staticmethod 150 | def edit_cue(cue): 151 | """ 152 | edit_cue sets cue.info_section.pts_adjustment 153 | and then re-encodes the SCTE35 Cue. 154 | """ 155 | print("BEFORE:", file=sys.stderr) 156 | cue.show() 157 | cue.info_section.pts_adjustment = 109.55 # Changed 158 | cue.command.unique_program_id = 999 # Changed 159 | if cue.descriptors: 160 | cue.descriptors[0].identifier = "FUEI" # Changed 161 | cue.encode() 162 | print("AFTER:\n", file=sys.stderr) 163 | cue.show() 164 | 165 | def repack_pkt(self, pkt, cue): 166 | """ 167 | repack_pkt recreates the SCTE35 packet 168 | with the edited SCTE35 Cue as the payload 169 | adds padding as needed and then 170 | writes the new_packet to stdout 171 | """ 172 | pay_idx = pkt.index(cue.bites) 173 | header = pkt[:pay_idx] 174 | self.edit_cue(cue) 175 | new_payload = cue.bites 176 | new_pkt = header + new_payload 177 | new_pkt += (self._PACKET_SIZE - len(new_pkt)) * self._PAD 178 | return new_pkt 179 | 180 | def edit_scte35(self): 181 | """ 182 | edit_scte35 applies Stream2.edit_cue(cue) 183 | on SCTE35 data before writing all packets 184 | to stdout. 185 | """ 186 | if self._find_start(): 187 | for pkt in iter(partial(self._tsdata.read, self._PACKET_SIZE), b""): 188 | cue = self._parse(pkt) 189 | if cue: 190 | pkt = self.repack_pkt(pkt, cue) 191 | sys.stdout.buffer.write(pkt) 192 | self._tsdata.close() 193 | 194 | 195 | if __name__ == "__main__": 196 | args = sys.argv[1:] 197 | for arg in args: 198 | strm = Stream2(arg) 199 | strm.edit_scte35() 200 | -------------------------------------------------------------------------------- /examples/hls/Readme.md: -------------------------------------------------------------------------------- 1 | 2 | #### These are just examples. 3 | ##### If you need a m3u8 parser that speaks SCTE-35, try [m3ufu](https://github.com/futzu/m3ufu) 4 | ##### If you need an HLS segmenter that speaks SCTE-35, try [x9k3](https://github.com/futzu/x9k3) 5 | 6 | ![hasp-action](https://user-images.githubusercontent.com/52701496/139568422-62f016f2-0045-4e94-b2d4-010d54f042c4.png) 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/hls/hasp.py: -------------------------------------------------------------------------------- 1 | """ 2 | hasp.py 3 | 4 | HASP stands for HLS AES SCTE-35 Parser 5 | HASP decodes AES encrypted segments for the 6 | start time of the first segment read. 7 | 8 | Try it with a stream like this: 9 | 10 | pypy3 hasp.py https://phls-vod.cdn.turner.com/cnnngtv/cnn/hls/2018/12/03/urn:ngtv-show:115615/index_1.m3u8 11 | 12 | 13 | """ 14 | 15 | 16 | import os 17 | import sys 18 | import pyaes 19 | import threefive 20 | from new_reader import reader 21 | 22 | 23 | class Stanza: 24 | """ 25 | The Stanza class represents a segment 26 | and associated data 27 | 28 | ex. 29 | 30 | #EXT-X-KEY:METHOD=AES-128,URI="example.com/keys/027a0d992ff6a49a484e6d9b58cfcd0531df924d.key",IV=0x92B99B3F257801E616D08261E3078B19 31 | #EXT-X-ASSET:CAID=0x00000000381B85CE 32 | #EXT-X-CUE-OUT:250.133 33 | #EXT-X-SCTE35:CUE="/DA2AAAAAAAAAAAABQaAUH4T/gAgAh5DVUVJQAVCawDAAAFXgYsICAAAAAA4G4XONAAAAADqLWsS",ID="1074086507" 34 | #EXTINF:6.006, 35 | 1630520255_video_480p-30fps-1836kbps/video_185222.ts 36 | 37 | """ 38 | 39 | def __init__(self, lines, segment, start): 40 | self.lines = lines 41 | self.segment = segment 42 | self.clean_segment() 43 | self.decoded_seg = None 44 | self.pts = None 45 | self.start = start 46 | self.duration = 0 47 | self.cue = False 48 | self.iv = None 49 | self.key = None 50 | self.key_uri = None 51 | self.mode = None 52 | 53 | def clean_segment(self): 54 | if self.segment.startswith("http"): 55 | ss = self.segment.split("/") 56 | while ".." in ss: 57 | i = ss.index("..") 58 | del ss[i] 59 | del ss[i - 1] 60 | self.segment = "/".join(ss) 61 | 62 | def _aes_start(self, line): 63 | if line.startswith("#EXT-X-KEY:METHOD=AES-128"): 64 | try: 65 | self._aes_mode(line) 66 | self._aes_decrypt() 67 | return True 68 | except: 69 | return False 70 | 71 | def _aes_mode(self, line): 72 | self.key_uri = line.split("URI=")[1].split(",")[0] 73 | piv = (line.split("IV=")[1])[2:].split(",")[0] 74 | self.iv = bytes.fromhex(piv) 75 | self._aes_get_key() 76 | self.mode = pyaes.AESModeOfOperationCBC(self.key, iv=self.iv) 77 | 78 | def _aes_decrypt(self): 79 | try: 80 | tmp = self.segment.rsplit("/", 1)[1] 81 | with threefive.reader(self.segment) as infile: 82 | with open(tmp, "wb") as outfile: 83 | pyaes.decrypt_stream(self.mode, infile, outfile) 84 | self._get_pts_start(tmp) 85 | os.unlink(tmp) 86 | except: 87 | pass 88 | 89 | def _aes_get_key(self): 90 | if not self.key_uri.startswith("http"): 91 | head = self.segment[: self.segment.rindex("/") + 1] 92 | self.key_uri = head + self.key_uri 93 | with threefive.reader(self.key_uri) as quay: 94 | self.key = quay.read() 95 | 96 | def _get_pts_start(self, seg): 97 | if not self.start: 98 | pts_start = 0.0 99 | try: 100 | strm = threefive.Stream(seg) 101 | strm.decode() 102 | if len(strm.start.values()) > 0: 103 | pts_start = strm.start.popitem()[1] 104 | self.pts = round(pts_start / 90000.0, 6) 105 | except: 106 | pass 107 | self.start = self.pts 108 | 109 | def _extinf(self, line): 110 | if line.startswith("#EXTINF"): 111 | t = line.split(":")[1].split(",")[0] 112 | t = float(t) 113 | self.duration = round(t, 6) 114 | 115 | def _ext_x_scte35(self, line): 116 | if line.startswith("#EXT-X-SCTE35"): 117 | self.cue = line.split("CUE=")[1].split(",")[0] 118 | if line.startswith("#EXT-OATCLS-SCTE35:"): 119 | self.cue = line.split("#EXT-OATCLS-SCTE35:")[1] 120 | 121 | def _ext_x_daterange(self, line): 122 | if line.startswith("#EXT-X-DATERANGE:"): 123 | for chunk in line.split(","): 124 | k, v = chunk.split("=") 125 | if k.startswith("SCTE35"): 126 | self.cue = v 127 | 128 | def do_cue(self): 129 | """ 130 | do_cue parses a SCTE-35 encoded string 131 | via the threefive.Cue class 132 | """ 133 | if self.cue: 134 | tf = threefive.Cue(self.cue) 135 | tf.decode() 136 | tf.show() 137 | 138 | def decode(self): 139 | for line in self.lines: 140 | if not self.start: 141 | self._aes_start(line) 142 | 143 | print(line) 144 | self._ext_x_scte35(line) 145 | self._extinf(line) 146 | self._ext_x_daterange(line) 147 | if not self.pts: 148 | self._get_pts_start(self.segment) 149 | self.start = self.pts 150 | if not self.start: 151 | self.start = 0.0 152 | return self.start 153 | 154 | 155 | class HASP: 156 | """ 157 | HASP stands for HLS AES SCTE-35 Parser 158 | HASP decodes AES encrypted segments for the 159 | start time of the first segment read. 160 | Try it with a stream like this 161 | 162 | python3 hasp.py https://phls-vod.cdn.turner.com/cnnngtv/cnn/hls/2018/12/03/urn:ngtv-show:115615/index_1.m3u8 163 | """ 164 | 165 | def __init__(self, arg): 166 | self.m3u8 = arg 167 | self.hls_time = 0.0 168 | self.seg_list = [] 169 | self._start = None 170 | self.chunk = [] 171 | self.base_uri = "" 172 | if arg.startswith("http"): 173 | self.base_uri = arg[: arg.rindex("/") + 1] 174 | self.manifest = None 175 | self.target_duration = 0 176 | self.next_expected = 0 177 | 178 | @staticmethod 179 | def _clean_line(line): 180 | if isinstance(line, bytes): 181 | line = line.decode(errors="ignore") 182 | line = ( 183 | line.replace(" ", "").replace('"', "").replace("\n", "").replace("\r", "") 184 | ) 185 | return line 186 | 187 | def show_segment_times(self, stanza): 188 | print(f"Segment: {stanza.segment}") 189 | print(f"Segment Duration : {stanza.duration}") 190 | print(f"Segment Start :{round(self.next_expected,6)}") 191 | print(f"HLS Time: {round(self.hls_time,6)}") 192 | # + f"Segment Duration : {stanza.duration}" 193 | # ) 194 | # if stanza.pts: 195 | # print(f"{rev_text}pts Time :{reset_text} {stanza.pts}\n") 196 | if stanza.cue: 197 | # print(f"Cue:\n") 198 | # stanza.do_cue() 199 | print("\n") 200 | 201 | def do_segment(self, line): 202 | segment = line 203 | if not line.startswith("http"): 204 | segment = self.base_uri + line 205 | if segment not in self.seg_list: 206 | self.seg_list.append(segment) 207 | self.seg_list = self.seg_list[-101:] 208 | stanza = Stanza(self.chunk, segment, self._start) 209 | stanza.decode() 210 | if not self._start: 211 | self._start = stanza.start 212 | self.next_expected = self._start + self.hls_time 213 | self.show_segment_times(stanza) 214 | self.next_expected += round(stanza.duration, 6) 215 | self.hls_time += stanza.duration 216 | self.chunk = [] 217 | 218 | def decode(self): 219 | while True: 220 | with reader(self.m3u8) as self.manifest: 221 | while self.manifest: 222 | line = self.manifest.readline() 223 | if not line: 224 | break 225 | line = self._clean_line(line) 226 | if "ENDLIST" in line: 227 | return False 228 | if "TARGETDURATION" in line: 229 | self.target_duration = int(line.split(":")[1]) >> 1 230 | self.chunk.append(line) 231 | if not line.startswith("#"): 232 | self.do_segment(line) 233 | 234 | 235 | def chk_version(): 236 | min_version = 2305 237 | vn = int(threefive.version().replace(".", "")) 238 | if vn < min_version: 239 | print(f"this script requires threefive.version {min_version} or higher. ") 240 | 241 | sys.exit() 242 | 243 | 244 | if __name__ == "__main__": 245 | chk_version() 246 | arg = sys.argv[1] 247 | HASP(arg).decode() 248 | -------------------------------------------------------------------------------- /examples/hls/hlsparse.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import threefive 3 | 4 | 5 | def do(mesg): 6 | print(f"cue: {mesg}") 7 | tf = threefive.Cue(mesg) 8 | tf.decode() 9 | tf.show() 10 | 11 | 12 | with open(sys.argv[1], "r", encoding="utf-8") as manifest: 13 | hls_time = duration = cue_out = cue_in = 0 14 | while manifest: 15 | l = manifest.readline() 16 | if not l: 17 | break 18 | if l.startswith("#EXT-X-CUE-OUT:"): 19 | cue_out = hls_time 20 | duration = float(l.split(":")[1]) 21 | cue_in = hls_time + duration 22 | print(f"hls time: {hls_time}") 23 | print(f"cue out: {cue_out}") 24 | print(f"duration: {duration}") 25 | print(f"cue in: {cue_in}") 26 | ##EXTINF:4.000000, 27 | if l.startswith("#EXTINF:"): 28 | t = l.split(":")[1].split(",")[0] 29 | t = float(t) 30 | hls_time += t 31 | next_line = manifest.readline()[:-1] 32 | if not (next_line.startswith("#")): 33 | print(f"Segment: {next_line} @ {hls_time}") 34 | # EXT-X-SCTE35:CUE= 35 | if l.startswith("#EXT-X-SCTE35"): 36 | mesg = l.split("CUE=")[1] 37 | do(mesg) 38 | if l.startswith("#EXT-OATCLS-SCTE35:"): 39 | mesg = l.split("#EXT-OATCLS-SCTE35:")[1] 40 | do(mesg) 41 | ##EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11:15:00Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A000829896F50000008700000000 42 | if l.startswith("#EXT-X-DATERANGE:"): 43 | for chunk in l.split(","): 44 | k, v = chunk.split("=") 45 | if k.startswith("SCTE35"): 46 | cue = threefive.Cue(v) 47 | cue.decode() 48 | cue.show() 49 | -------------------------------------------------------------------------------- /examples/hls/scte35.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-TARGETDURATION:12 4 | #EXT-X-MEDIA-SEQUENCE:1 5 | #EXT-X-PLAYLIST-TYPE:VOD 6 | #EXTINF:10.000, 7 | file_60p_1_00001.ts 8 | #EXT-OATCLS-SCTE35:/DAnAAAAAAAAAP/wBQb+AA27oAARAg9DVUVJAAAAAX+HCQA0AAE0xUZn 9 | #EXT-X-CUE-OUT:30.000 10 | #EXTINF:2.000, 11 | file_60p_1_00002.ts 12 | #EXT-X-CUE-OUT-CONT:ElapsedTime=2.000,Duration=30,SCTE35=/DAnAAAAAAAAAP/wBQb+AA27oAARAg9DVUVJAAAAAX+HCQA0AAE0xUZn 13 | #EXTINF:12.000, 14 | file_60p_1_00003.ts 15 | #EXT-X-CUE-OUT-CONT:ElapsedTime=14.000,Duration=30,SCTE35=/DAnAAAAAAAAAP/wBQb+AA27oAARAg9DVUVJAAAAAX+HCQA0AAE0xUZn 16 | #EXTINF:12.000, 17 | file_60p_1_00004.ts 18 | #EXT-X-CUE-OUT-CONT:ElapsedTime=26.000,Duration=30,SCTE35=/DAnAAAAAAAAAP/wBQb+AA27oAARAg9DVUVJAAAAAX+HCQA0AAE0xUZn 19 | #EXTINF:4.000, 20 | file_60p_1_00005.ts 21 | #EXT-OATCLS-SCTE35:/DAnAAAAAAAAAP/wBQb+ADbugAARAg9DVUVJAAAAAX+HCQA1AAA3v5+Q 22 | #EXT-X-CUE-IN 23 | #EXTINF:8.000, 24 | file_60p_1_00006.ts 25 | #EXTINF:12.000, 26 | file_60p_1_00007.ts 27 | #EXTINF:12.000, 28 | file_60p_1_00008.ts 29 | #EXTINF:3.000, 30 | file_60p_1_00009.ts 31 | #EXT-OATCLS-SCTE35:/DAnAAAAAAAAAP/wBQb+AGb/MAARAg9DVUVJAAAAAn+HCQA0AALMua1L 32 | #EXT-X-CUE-OUT:30.000 33 | #EXTINF:9.000, 34 | file_60p_1_00010.ts 35 | #EXT-X-CUE-OUT-CONT:ElapsedTime=9.000,Duration=30,SCTE35=/DAnAAAAAAAAAP/wBQb+AGb/MAARAg9DVUVJAAAAAn+HCQA0AALMua1L 36 | #EXTINF:12.000, 37 | file_60p_1_00011.ts 38 | #EXT-X-CUE-OUT-CONT:ElapsedTime=21.000,Duration=30,SCTE35=/DAnAAAAAAAAAP/wBQb+AGb/MAARAg9DVUVJAAAAAn+HCQA0AALMua1L 39 | #EXTINF:9.000, 40 | file_60p_1_00012.ts 41 | #EXT-OATCLS-SCTE35:/DAnAAAAAAAAAP/wBQb+AJAyEAARAg9DVUVJAAAAAn+HCQA1AABStd4A 42 | #EXT-X-CUE-IN 43 | #EXTINF:3.000, 44 | file_60p_1_00013.ts 45 | #EXTINF:12.000, 46 | file_60p_1_00014.ts 47 | #EXTINF:12.000, 48 | file_60p_1_00015.ts 49 | #EXTINF:3.000, 50 | file_60p_1_00016.ts 51 | #EXTINF:9.000, 52 | file_60p_1_00017.ts 53 | #EXTINF:12.000, 54 | file_60p_1_00018.ts 55 | #EXTINF:12.000, 56 | file_60p_1_00019.ts 57 | #EXTINF:12.000, 58 | file_60p_1_00020.ts 59 | #EXTINF:12.000, 60 | file_60p_1_00021.ts 61 | #EXTINF:12.000, 62 | file_60p_1_00022.ts 63 | #EXTINF:12.000, 64 | file_60p_1_00023.ts 65 | #EXTINF:10.067, 66 | file_60p_1_00024.ts 67 | #EXT-X-ENDLIST 68 | -------------------------------------------------------------------------------- /examples/hls/simple-scte35.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-TARGETDURATION:10 3 | #EXT-X-VERSION:3 4 | #EXT-X-MEDIA-SEQUENCE:0 5 | #EXT-OATCLS-SCTE35:/DAlAAAAAAAAAP/wFAUAAAABf+/+ANgNkv4AFJlwAAEBAQAA5xULLA== 6 | #EXT-X-CUE-OUT:15.000 7 | #EXTINF:8.844, 8 | media0.ts 9 | #EXT-X-CUE-OUT-CONT:ElapsedTime=8.844,Duration=15,SCTE35=/DAlAAAAAAAAAP/wFAUAAAABf+/+ANgNkv4AFJlwAAEBAQAA5xULLA== 10 | #EXTINF:6.156, 11 | media1.ts 12 | #EXT-X-CUE-IN 13 | #EXTINF:2.00, 14 | media2.ts 15 | #EXTINF:2.000, 16 | media3.ts -------------------------------------------------------------------------------- /examples/spliceinsert/splice_insert.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example from the specification 3 | 4 | 14.2. Splice_Insert 5 | 6 | """ 7 | 8 | import threefive 9 | 10 | HEX = "0xFC302F000000000000FFFFF014054800008F7FEFFE7369C02EFE0052CCF500000000000A0008435545490000013562DBA30A" 11 | B64 = "/DAvAAAAAAAA///wFAVIAACPf+/+c2nALv4AUsz1AAAAAAAKAAhDVUVJAAABNWLbowo=" 12 | 13 | # Using Hex 14 | 15 | threefive.decode(HEX) 16 | 17 | 18 | # Using Base64 19 | # (output should be the same as above) 20 | 21 | threefive.decode(B64) 22 | -------------------------------------------------------------------------------- /examples/spliceinsert/splice_insert_too.py: -------------------------------------------------------------------------------- 1 | """ 2 | Another Splice Insert example 3 | """ 4 | 5 | 6 | import threefive 7 | 8 | 9 | HEXED = "0xFC302100000000000000FFF01005000003DB7FEF7F7E0020F580C0000000000019913DA5" 10 | threefive.decode(HEXED) 11 | -------------------------------------------------------------------------------- /examples/splicenull/splice_null.py: -------------------------------------------------------------------------------- 1 | """ 2 | Splice null command example 3 | 4 | """ 5 | 6 | 7 | from threefive import decode 8 | 9 | 10 | decode(0xFC301100000000000000FFFFFF0000004F253396) 11 | -------------------------------------------------------------------------------- /examples/stream/cool_decode_http.py: -------------------------------------------------------------------------------- 1 | """ 2 | Accepts one or more urls to parse for SCTE-35 over http/https. 3 | 4 | Usage: 5 | 6 | python3 cool_decode_http.py https://futzu.com/xaa.ts https://example.com/video.ts 7 | 8 | """ 9 | 10 | import sys 11 | from threefive import Stream 12 | 13 | 14 | if __name__ == "__main__": 15 | Urls = sys.argv[1:] 16 | streams = [Stream(url) for url in Urls] 17 | for stream in streams: 18 | stream.decode() 19 | -------------------------------------------------------------------------------- /examples/stream/cue2vtt.py: -------------------------------------------------------------------------------- 1 | """ 2 | cue2vtt.py 3 | 4 | uses webvtt subtitles to display splice insert cue out and cue In. 5 | 6 | Usage: 7 | 8 | pypy3 cue2vtt.py thevideo.ts | mplayer thevideo.ts -sub - 9 | 10 | """ 11 | 12 | import sys 13 | import time 14 | from iframes import IFramer 15 | from threefive import Stream 16 | 17 | 18 | def first(): 19 | global start_pts 20 | iframer = IFramer(shush=True) 21 | start_pts = iframer.first(sys.argv[1]) 22 | 23 | 24 | def ts_to_vtt(timestamp): 25 | """ 26 | ts_to_vtt converts timestamp into webvtt times 27 | """ 28 | timestamp -= start_pts 29 | hours, seconds = divmod(timestamp, 3600) 30 | mins, seconds = divmod(seconds, 60) 31 | seconds = round(seconds, 3) 32 | return f"{int(hours):02}:{int(mins):02}:{seconds:02}" 33 | 34 | 35 | def scte35_to_vtt(cue): 36 | """ 37 | scte35_to_vtt prints splice insert cue out and cue in via webvtt 38 | """ 39 | cue_start = 0 40 | cue_end = 0 41 | duration = None 42 | pts_time = None 43 | upid = None 44 | seg_mesg = None 45 | now = cue.packet_data.pts 46 | if cue.command.has("pts_time"): 47 | cue_start = cue.command.pts_time + cue.info_section.pts_adjustment 48 | else: 49 | cue_start = now 50 | if cue_start > now: 51 | time.sleep(cue_start - now) 52 | if cue.command.has("break_duration"): 53 | duration = cue.command.break_duration 54 | for d in cue.descriptors: 55 | if d.has("segmentation_duration"): 56 | duration = d.segmentation_duration 57 | if d.has("segmentation_upid"): 58 | upid = d.segmentation_upid 59 | if d.has("segmentation_message"): 60 | seg_mesg = d.segmentation_message 61 | if cue_start: 62 | cue_end = cue_start + 2 63 | ## if cue_end == 0: 64 | ## end = start + 4 65 | 66 | print(f"{ts_to_vtt(cue_start)} --> {ts_to_vtt(cue_end)} ") 67 | 68 | print(f"Cmd: {cue.command.name} ") 69 | pts_time = None 70 | if cue.command.pts_time: 71 | pts_time = cue.command.pts_time + cue.info_section.pts_adjustment 72 | else: 73 | pts_time = now 74 | print(f"PTS: {pts_time} ") 75 | if duration: 76 | print(f"Duration: {round(duration,3)} ") 77 | if seg_mesg: 78 | print(seg_mesg) 79 | if upid: 80 | print(f"Upid: {upid}") 81 | print() 82 | 83 | 84 | if __name__ == "__main__": 85 | arg = sys.argv[1] 86 | first() 87 | print("WEBVTT\n\n\n") 88 | strm = Stream(arg) 89 | strm.decode(func=scte35_to_vtt) 90 | -------------------------------------------------------------------------------- /examples/stream/cue_list.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example using Stream.decode() with a custom function 3 | to return a list of cues from a video 4 | 5 | Usage: 6 | 7 | pypy3 cue_list.py video.ts 8 | 9 | """ 10 | 11 | 12 | import sys 13 | from threefive import Stream, Cue 14 | 15 | # A list to hold cues 16 | CUES=[] 17 | 18 | def cue_func(cue): 19 | """ 20 | threefive.Stream.decode cqn be passed in a 21 | custom function that accepts a Cue as the only arg. 22 | this is an example of a custom function. 23 | 24 | """ 25 | CUES.append(cue) 26 | cue.show() 27 | 28 | 29 | def do(): 30 | """ 31 | do collects a list of Cues from a Stream 32 | """ 33 | arg = sys.argv[1] 34 | strm = Stream(arg) 35 | strm.decode(func=cue_func) # cue_func is the custom function from above 36 | 37 | 38 | if __name__ == "__main__": 39 | do() 40 | [print(cue.command.name,cue.encode()) for cue in CUES] 41 | -------------------------------------------------------------------------------- /examples/stream/cue_list_too.py: -------------------------------------------------------------------------------- 1 | """ 2 | cue_list_too.py 3 | Example to show how to get a 4 | list of SCTE-35 cues as threefive.Cue instances, 5 | and print a list of the pts of the SCTE35 packet with the base64 Cue string. 6 | 7 | Use like: 8 | 9 | python3 cue_list_too.py myvideo.ts 10 | 11 | Output: 12 | 13 | PTS -> Cue 14 | 21940.651167 , /DAvAAAAAAAA///wFAUAAAASf+/+dcFLSn4AZv8wAAES/wAKAAhDVUVJAAAAEuqoRz8= 15 | 21942.658133 , /DAvAAAAAAAA///wFAUAAAASf+/+dcFLSn4AZv8wAAES/wAKAAhDVUVJAAAAEuqoRz8= 16 | 21944.665344 , /DAvAAAAAAAA///wFAUAAAASf+/+dcFLSn4AZv8wAAES/wAKAAhDVUVJAAAAEuqoRz8= 17 | 22015.646556 , /DAqAAAAAAAA///wDwUAAAASf0/+dihKegABEv8ACgAIQ1VFSQAAABIe1kvb 18 | 22017.653522 , /DAqAAAAAAAA///wDwUAAAASf0/+dihKegABEv8ACgAIQ1VFSQAAABIe1kvb 19 | 22019.660733 , /DAqAAAAAAAA///wDwUAAAASf0/+dihKegABEv8ACgAIQ1VFSQAAABIe1kvb 20 | 22508.448011 , /DAvAAAAAAAA///wFAVAAAT2f+/+eMpEWX4A9zFAAAEL/wAKAAhDVUVJAAAACwRZmfY= 21 | 22510.424778 , /DAvAAAAAAAA///wFAVAAAT2f+/+eMpEWX4A9zFAAAEL/wAKAAhDVUVJAAAACwRZmfY= 22 | 22688.424856 , /DAqAAAAAAAA///wDwVAAAT2f0/+ecF1mQABC/8ACgAIQ1VFSQAAAAsuZVlR 23 | 24 | """ 25 | import sys 26 | 27 | from threefive import Segment 28 | 29 | 30 | def no_op(cue): 31 | return 32 | 33 | 34 | if __name__ == "__main__": 35 | 36 | seg = Segment(sys.argv[1]) 37 | seg.decode() 38 | 39 | print("\n\nPTS -> Cue") 40 | pts_cue_map = {cue.packet_data.pts: cue.encode() for cue in seg.cues} 41 | for ts in sorted(pts_cue_map): 42 | print(ts, " , ", pts_cue_map[ts]) 43 | -------------------------------------------------------------------------------- /examples/stream/decode_http.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parse a mpegts video over http or https 3 | 4 | """ 5 | 6 | from threefive import Stream 7 | 8 | VID = "https://futzu.com/xaa.ts" 9 | 10 | strm = Stream(VID) 11 | strm.decode() 12 | -------------------------------------------------------------------------------- /examples/stream/decode_proxy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Stream.proxy example. 3 | writes SCTE-35 data to stderr 4 | writes the MPEG-TS packets to stdout 5 | so you can pipe it. 6 | 7 | Example: 8 | 9 | python3 decode_proxy.py video.ts | mplayer - 10 | 11 | """ 12 | import sys 13 | from threefive import Stream 14 | 15 | 16 | def do(): 17 | """ 18 | do creates a Stream instance with sys.argv[1] 19 | and then calls Stream.decode_proxy() 20 | """ 21 | strm = Stream(sys.argv[1]) 22 | strm.decode_proxy() 23 | 24 | 25 | if __name__ == "__main__": 26 | do() 27 | -------------------------------------------------------------------------------- /examples/stream/preroll.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pre-roll is the difference in time between when a SCTE-35 packet 3 | is inserted in a MPEGTS stream and the splice time of the SCTE-35 Cue. 4 | 5 | """ 6 | 7 | import sys 8 | from threefive import Stream 9 | 10 | 11 | def show_preroll(cue): 12 | """ 13 | Print Splice Time, pre-roll and the base64 encoded SCTE-35 Cue. 14 | """ 15 | if cue.command.pts_time: 16 | if cue.packet_data: 17 | out = f"{cue.command.pts_time}," 18 | if cue.packet_data.pts and cue.command.pts_time: 19 | pts_preroll = cue.command.pts_time - cue.packet_data.pts 20 | out += f"\t{pts_preroll:.6f}," 21 | out +=f"\t{cue.encode()}" 22 | print(out) 23 | 24 | 25 | def do(): 26 | """ 27 | do processes multiple command line args 28 | """ 29 | args = sys.argv[1:] 30 | for arg in args: 31 | print(f"Input: {arg}") 32 | print("Splice Time,\tPre-roll,\tCue") 33 | strm = Stream(arg, show_null=False) 34 | """ 35 | threefive.Stream.decode accepts an optional function 36 | to be used to when a Cue is found. The function must 37 | meet the interface func(cue). 38 | 39 | In this example, we are using the function show_preroll. 40 | """ 41 | strm.decode(func=show_preroll) 42 | 43 | 44 | if __name__ == "__main__": 45 | do() 46 | -------------------------------------------------------------------------------- /examples/stream/scte35packets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | scte35packets.py 5 | 6 | 7 | print out the raw SCTE-35 packets in a stream or file. 8 | 9 | 10 | Use like: 11 | 12 | a@slow:~$ python3 scte35packets.py https://futzu.com/xaa.ts 13 | 14 | This functionality is also available in the threefive command line tool: 15 | 16 | threefive packets https://futzu.com/xaa.ts 17 | 18 | """ 19 | 20 | 21 | import sys 22 | from threefive import Stream 23 | 24 | 25 | 26 | class SupaStream(Stream): 27 | 28 | def _parse_scte35(self,pkt,pid): 29 | print(pkt) 30 | super()._parse_scte35(pkt,pid) 31 | 32 | 33 | if __name__ == '__main__': 34 | args = sys.argv[1:] 35 | for arg in args: 36 | supa = SupaStream(arg) 37 | supa.decode() 38 | -------------------------------------------------------------------------------- /examples/timesignal/time_signal_blackout_override_program_end.py: -------------------------------------------------------------------------------- 1 | """ 2 | 14.6. Time_Signal – Program Blackout Override / Program End 3 | """ 4 | 5 | 6 | from threefive import Cue 7 | 8 | 9 | BE64 = "/DBIAAAAAAAA///wBQb+ky44CwAyAhdDVUVJSAAACn+fCAgAAAAALKCh4xgAAAIXQ1VFSUgAAAl/nwgIAAAAACygoYoRAAC0IX6w" 10 | 11 | three5 = Cue(BE64) 12 | three5.decode() 13 | three5.show() 14 | -------------------------------------------------------------------------------- /examples/timesignal/time_signal_placement_opportunity_end.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example from the specification 3 | 4 | 14.3. Time_Signal–Placement_Opportunity_End 5 | 6 | """ 7 | 8 | from threefive import decode 9 | 10 | B64 = "/DAvAAAAAAAA///wBQb+dGKQoAAZAhdDVUVJSAAAjn+fCAgAAAAALKChijUCAKnMZ1g=" 11 | 12 | decode(B64) 13 | -------------------------------------------------------------------------------- /examples/timesignal/time_signal_program_overlap.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example from the specification 3 | 4 | 14.5. Time_Signal–Program_Overlap_Start 5 | 6 | """ 7 | 8 | 9 | from threefive import decode 10 | 11 | HEXED = "0xFC302F000000000000FFFFF00506FEAEBFFF640019021743554549480000087F9F0808000000002CA56CF5170000951DB0A8" 12 | 13 | decode(HEXED) 14 | -------------------------------------------------------------------------------- /examples/upid/custom_upid_handling.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom UPID handling example for Richard. 3 | Parse Private data and insert the values into the descriptor 4 | under segmentation_upid 5 | 6 | # These are Added 7 | 8 | "private_cni" 9 | "private_version" 10 | "private_transmission_id" 11 | "private_product_code" 12 | "private_web_publication_key" 13 | 14 | """ 15 | import sys 16 | from threefive.bitn import BitBin 17 | from threefive import Cue, Stream 18 | 19 | 20 | SBSB_ID = "0x53425342" 21 | SBSB_MPU = "FC305700000000000000FFF00506FE1E3E6FBE0041023F43554549000000007FBF0C3053425342360C014C12C933BA0000006EFF1000000000007A71507777734567327335000000000000000000000000000001000065F06E76" 22 | 23 | 24 | def chk_sbsb(descptr): 25 | """ 26 | chk_sbsb checks a Splice Descriptor for SBSB_ID 27 | """ 28 | if descptr.tag == 2: 29 | if descptr.segmentation_upid_type == 12: 30 | if descptr.segmentation_upid["format_identifier"] == SBSB_ID: 31 | sbsb = parse_sbsb(descptr.segmentation_upid["private_data"]) 32 | # add values returned from parse_SBSB to descriptor 33 | descptr.segmentation_upid.update(sbsb) 34 | 35 | 36 | def parse_sbsb(pdata): 37 | """ 38 | Parse UPID Private data 39 | """ 40 | bites = bytes.fromhex(pdata[2:]) 41 | bitbin = BitBin(bites) 42 | return { 43 | "private_cni": bitbin.as_hex(16), 44 | "private_version": bitbin.as_int(8), 45 | "private_transmission_id": bitbin.as_int(64), 46 | "private_product_code": bitbin.as_int(64), 47 | "private_web_publication_key": bitbin.as_ascii(200), 48 | } 49 | 50 | 51 | def do_descriptors(cue): 52 | """ 53 | do - process cue.descriptors 54 | """ 55 | for descptr in cue.descriptors: 56 | chk_sbsb(descptr) 57 | cue.show() 58 | 59 | 60 | def do_cue(scte35): 61 | """ 62 | do_cue, decode Cue and process if needed. 63 | """ 64 | mpu_cue = Cue(scte35) 65 | mpu_cue.decode() 66 | do_descriptors(mpu_cue) 67 | 68 | 69 | def do_stream(): 70 | """ 71 | do_stream processes a list of streams on the command line 72 | """ 73 | args = sys.argv[1:] 74 | for arg in args: 75 | print(f"Next File: {arg}") 76 | strm = Stream(arg) 77 | strm.decode(func=do_descriptors) 78 | 79 | 80 | if __name__ == "__main__": 81 | # parse file names if present 82 | if len(sys.argv) > 1: 83 | do_stream() 84 | else: 85 | # parse the encoded string 86 | do_cue(SBSB_MPU) 87 | -------------------------------------------------------------------------------- /examples/upid/multi_upid.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Multiple Segmentation Upid Descriptors Example 4 | 5 | Three AiringID and One MPU 6 | 7 | """ 8 | 9 | import threefive 10 | 11 | 12 | MPU = "/DCSAAAAAAAAAP/wBQb/RgeVUgB8AhdDVUVJbs6+VX+/CAgAAAAABy0IxzELGQIXQ1VFSW7MmIh/vwgIAAABGDayFhE3AQECHENVRUluzw0If/8AABvLoAgIAAAAAActVhIwDBkCKkNVRUluzw02f78MG1JUTE4xSAEAAAAAMTM3NjkyMDI1NDQ5NUgxAAEAAGnbuXg=" 13 | 14 | cuep = threefive.Cue(MPU) 15 | cuep.decode() 16 | cuep.show() 17 | -------------------------------------------------------------------------------- /examples/upid/threefiveadfr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | 5 | Custom threefive cli tool with a modified mpu upid for 6 | 7 | the "Addressable TV" spec (https://www.snptv.org/wp-content/uploads/2020/08/SNPTV-AFMM-Addressable-TV-UK-version-2.0.6.1.pdf) 8 | 9 | 10 | 1) Define a new mpu method 11 | 12 | def adfr_mpu(self): 13 | mpu_data = { 14 | "format_identifier": self.bitbin.as_charset(32), 15 | "version": self.bitbin.as_int(8), 16 | "ChannelID": self.bitbin.as_int(16), 17 | "YYYMMDD": self.bitbin.as_hex(32), 18 | "Ad Code": self.bitbin.as_hex(16), 19 | "Duration": self.bitbin.as_int(24), 20 | } 21 | return mpu_data 22 | 23 | 24 | 25 | 2) clobber the old mpu method 26 | 27 | threefive.upids.UpidDecoder._decode_mpu = adfr_mpu 28 | 29 | 3) use the install command to install it. 30 | 31 | install threefiveafr ~/.local/bin 32 | 33 | """ 34 | 35 | 36 | import sys 37 | import threefive 38 | 39 | def adfr_mpu(self): 40 | mpu_data = { 41 | "format_identifier": self.bitbin.as_charset(32), 42 | "version": self.bitbin.as_int(8), 43 | "ChannelID": self.bitbin.as_int(16), 44 | "YYYMMDD": self.bitbin.as_hex(32), 45 | "Ad Code": self.bitbin.as_hex(16), 46 | "Duration": self.bitbin.as_int(24), 47 | } 48 | return mpu_data 49 | 50 | 51 | 52 | if __name__ == "__main__": 53 | 54 | threefive.upids.UpidDecoder._decode_mpu = adfr_mpu 55 | 56 | if sys.argv and sys.argv[1].lower() in [b'version','version']: 57 | print(f'{threefive.version}') 58 | sys.exit() 59 | if len(sys.argv) > 1: 60 | if sys.argv[1].lower() in [b'pts','pts']: 61 | strm = threefive.Stream(sys.argv[2]) 62 | strm.show_pts() 63 | sys.exit() 64 | if sys.argv[1].lower() in [b'show','show']: 65 | strm = threefive.Stream(sys.argv[2]) 66 | strm.show() 67 | sys.exit() 68 | for arg in sys.argv[1:]: 69 | threefive.decode(arg) 70 | else: 71 | threefive.decode(sys.stdin.buffer) 72 | -------------------------------------------------------------------------------- /examples/upid/upid_combo.py: -------------------------------------------------------------------------------- 1 | """ 2 | Combination Segmentation Upid Example 3 | 4 | """ 5 | 6 | 7 | import threefive 8 | 9 | 10 | COMBO = "/DA9AAAAAAAAAACABQb+0fha8wAnAiVDVUVJSAAAv3/PAAD4+mMNEQ4FTEEzMDkICAAAAAAuU4SBNAAAPIaCPw==" 11 | 12 | cuep = threefive.Cue(COMBO) 13 | cuep.decode() 14 | cuep.show() 15 | -------------------------------------------------------------------------------- /examples/upid/upid_custom_output.py: -------------------------------------------------------------------------------- 1 | """ 2 | Segmentation Upid Examples 3 | """ 4 | 5 | import threefive 6 | 7 | 8 | ADID = "/DA4AAAAAAAA///wBQb+AKpFLgAiAiBDVUVJAAAAA3//AAApPWwDDEFCQ0QwMTIzNDU2SBAAABZE5vg=" 9 | UMID = "/DBDAAAAAAAA///wBQb+AA2QOQAtAitDVUVJAAAAA3+/BCAwNjBhMmIzNC4wMTAxMDEwNS4wMTAxMGQyMC4xEAEBRKI3vg==" 10 | ISAN = "/DA4AAAAAAAA///wBQb+Lom5UgAiAiBDVUVJAAAABn//AAApPWwGDAAAAAA6jQAAAAAAABAAAHGXrpg=" 11 | TID2 = "/DAzAAAAAAAA///wBQb+AAQesAAdAhtDVUVJAAAAA3+/BwxNVjAwMDQxNDY0MDARAAAf8lPm" 12 | AIRID = "/DBhAAAAAAAA///wBQb+qM1E7QBLAhdDVUVJSAAArX+fCAgAAAAALLLXnTUCAAIXQ1VFSUgAACZ/nwgIAAAAACyy150RAAACF0NVRUlIAAAnf58ICAAAAAAsstezEAAAihiGnw==" 13 | ADI = "/DBEAAAAAAAA///wBQb+AFJlwAAuAixDVUVJYgAFin+/CR1TSUdOQUw6My1zUTROZ0ZUME9qUHNHNFdxVVFvdzUAAEukzlg=" 14 | EIDR = "/DA4AAAAAAAA///wBQb+AKpFLgAiAiBDVUVJAAAAA3//AAApPWwKDDB4MTQ3OGY4NWFlMRAAAGXOtak=" 15 | ATSC = "/DA4AAAAAAAA///wBQb+QjoOtgAiAiBDVUVJAAAAA3//AAApPWwLDADx7/9odW1hbjAxMhAAABdHvhE=" 16 | MPU = "/DCSAAAAAAAAAP/wBQb/RgeVUgB8AhdDVUVJbs6+VX+/CAgAAAAABy0IxzELGQIXQ1VFSW7MmIh/vwgIAAABGDayFhE3AQECHENVRUluzw0If/8AABvLoAgIAAAAAActVhIwDBkCKkNVRUluzw02f78MG1JUTE4xSAEAAAAAMTM3NjkyMDI1NDQ5NUgxAAEAAGnbuXg=" 17 | MID = "/DA9AAAAAAAAAACABQb+0fha8wAnAiVDVUVJSAAAv3/PAAD4+mMNEQ4FTEEzMDkICAAAAAAuU4SBNAAAPIaCPw==" 18 | ADS2 = "/DBUAAAAAAAA///wBQb+Lom5UgA+AjxDVUVJAAAAC3+/Di1BRFMtVVBJRDphYTg1YmJiNi01YzQzLTRiNmEtYmViYi1lZTNiMTNlYjc5OTkRAAAO7LCw" 19 | 20 | 21 | 22 | dmesg = [ 23 | ADID, 24 | UMID, 25 | ISAN, 26 | TID2, 27 | AIRID, 28 | ADI, 29 | EIDR, 30 | ATSC, 31 | MPU, 32 | MID, 33 | ADS2, 34 | ] 35 | 36 | ids = [] 37 | 38 | 39 | def stuff(t, upid, uname): 40 | if t not in ids: 41 | ids.append(t) 42 | print(f"\033[92m{hex(t)}\033[0m : {upid} {uname}") 43 | 44 | 45 | for m in dmesg: 46 | tf = threefive.Cue(m) 47 | tf.decode() 48 | print(tf.xml(binary=True)) 49 | _ = [stuff(d.segmentation_upid_type, d.segmentation_upid, d.segmentation_upid_type_name) for d in tf.descriptors] 50 | -------------------------------------------------------------------------------- /ffmpegscte35.md: -------------------------------------------------------------------------------- 1 | # Ffmpeg and threefive and SCTE-35 2 | 3 | ### ffmpeg changes SCTE-35 stream type from 0x86 to 0x6 when transcoding. 4 | * threefive parses both stream type 0x86 and 0x6. 5 | * the threefive cli tool can convert the stream type back to 0x86 6 | ```rebol 7 | threefive sixfix video.ts 8 | ``` 9 | * the output video will be named sixfixed-video.ts 10 | * [threefive can handle ffmpeg rewriting SCTE-35 packets](https://github.com/futzu/SCTE-35/blob/master/ffrewrite.md) 11 | ### When transcoding video containing SCTE-35 wiith ffmpeg make sure to retain the original PTS and iframe locations. 12 | * use -copyts 13 | * use -muxpreload 0 -muxdelay 0 14 | ```rebol 15 | ffmpeg -copyts -i video.ts [other ffmpeg stuff] -muxpreload 0 -muxdelay 0 outvideo.ts 16 | ``` 17 | 18 | ### How to retain SCTE-35 when using ffmpeg to segment ABR HLS Live. 19 | * __This can be done in realtime on live streams__ 20 | * create a sidecar file of the SCTE-35 Cues with `threefive proxy` 21 | * pipe the MPEGTS from threefive to ffmpeg 22 | * Create renditions and master.m3u8 with ffmpeg 23 | 24 | ```smalltalk 25 | threefive mpegts proxy video.ts | ffmpeg -copyts -i - [..ffmpeg stuff..] master.m3u8 26 | ``` 27 | 28 | * Start threefive to inject SCTE-35 back into the HLS 29 | 30 | ```smalltalk 31 | threefive hls encode -i master.m3u8 -s sidecar.txt -o output_dir 32 | ``` 33 | * threefive will process the renditions from the master.m3u8 and add SCTE-35 Cues from the sidecar file live. 34 | * SCTE-35 is translated to HLS tags. __All SCTE-35 HLS Tags are Supported__. 35 | * SCTE-35 is added to new manifest files in output_dir 36 | * Original segments from ffmpeg are used. Segments are split if needed for Cue Outs. 37 | -------------------------------------------------------------------------------- /ffrewrite.md: -------------------------------------------------------------------------------- 1 | 2 | # Ffmpeg rewriting SCTE-35 packets. 3 | ## As of 2.4.55, threefive can parse the packet either way. 4 | 5 |
I love you msnbc, (please don't get mad at me for using your SCTE-35)
6 | 7 |
8 |
9 |
Let me preface this by saying this is not a criticism of ffmpeg.
10 | In my experience, ffmpeg tends to do things the correct way,
11 | and I have no problem adapting to them.
12 | 13 | My goal is to keep threefive as flexible and compatible as possible.
14 |
15 | 16 | #### This is what I usually get for SCTE-35 packets. 17 | 18 | * four byte header 19 | * \x00 20 | * SCTE-35 Cue 21 | * padding to 188 bytes 22 | 23 | 24 | ![image](https://github.com/user-attachments/assets/11bb0424-725a-4701-9cc1-2d8727fede05) 25 | 26 | 27 | 28 | `ffmpeg -copyts -i msnbc-latest.ts -map 0 -c copy msnbc-latest-ffmpeg.ts` 29 | 30 | #### Now ffmpeg changes that packet to look like this. 31 | 32 | * four byte header + AFC 33 | * padding to 188 bytes 34 | * PES header 35 | * SCTE-35 Cue 36 | 37 | 38 | ![image](https://github.com/user-attachments/assets/3e1b096d-bcee-4c94-980c-6e67bf5a2d7c) 39 | 40 | 41 | 42 | 43 | ## As of 2.4.55, threefive can parse the packet either way. 44 | 45 | -------------------------------------------------------------------------------- /hlsparse.md: -------------------------------------------------------------------------------- 1 | ## The coolest new feature in the threefive cli is the HLS SCTE-35 parser. 2 | 3 | 4 | ```rebol 5 | 2024-11-08T12:47:54.38Z SCTE-35 6 | Stream PTS: 70485.651111 7 | PreRoll: 3.300011 8 | Splice Point: 70488.951122 9 | Type: Time Signal 10 | Media: index_2_8638521.ts 11 | 12 | 13 | 2024-11-08T12:47:59.47Z Skipped #EXT-OATCLS-SCTE35:/DBAAAAAAyiYAAAABQb/6+8nkAAqAihDVUVJ/////3/ 14 | /AAFyylgBFG1zbmJjX0VQMDAwMjEzOTAyNTg3IwcLr6+cHw== 15 | 16 | PTS: 70489.651111 17 | Media: index_2_8638523.ts 18 | 19 | 20 | 2024-11-08T12:47:59.47Z #EXT-X-CUE-OUT:60.068 21 | 22 | PTS: 70489.651111 (Splice Point) 23 | Duration: 60.068 24 | Media: index_2_8638523.ts 25 | ``` 26 | 27 | 28 | 29 | 30 | 31 | The fine folks at [__tunein.com__](https://tunein.com) paid for the developement of __threefive hls__ and insisted it remain open and freely available to everyone. 32 | 33 | you run it like this: 34 | 35 | ```awk 36 | threefive hls https://example.com/master.m3u8 37 | ``` 38 | [ __Help__ ] 39 | 40 | To display this help: 41 | ```sed 42 | threefive hls help 43 | ``` 44 | 45 | [ __Input__ ] 46 | 47 | threefive hls takes an m3u8 URI as input. 48 | 49 | __M3U8 formats supported__: 50 | 51 | * master ( When a master.m3u8 used, threefive hls parses the first rendition it finds ) 52 | * rendition 53 | 54 | __Segment types supported__ 55 | 56 | * AAC 57 | * AC3 58 | * MPEGTS 59 | * codecs: 60 | * video 61 | * mpeg2, h.264, h.265 62 | * audio 63 | * mpeg2, aac, ac3, mp3 64 | 65 | __Protocols supported__: 66 | 67 | * file 68 | * http(s) 69 | * UDP 70 | * Multicast 71 | 72 | __Encryption supported__: 73 | 74 | * AES-128 (segments are automatically decrypted) 75 | 76 | [ __SCTE-35__ ] 77 | 78 | threefive hls displays SCTE-35 Embedded Cues as well as SCTE-35 HLS Tags. 79 | 80 | Supported HLS Tags. 81 | 82 | * #EXT-OATCLS-SCTE35 83 | * #EXT-X-CUE-OUT-CONT 84 | * #EXT-X-DATERANGE 85 | * #EXT-X-SCTE35 86 | * #EXT-X-CUE-IN 87 | * #EXT-X-CUE-OUT 88 | 89 | 90 | [__Profiles and .35rc__] 91 | * A lot of companies have multiple SCTE-35 Tags and/or SCTE-35 embedded inthe segments. threefive hls allows you to set what you parse. This is tunable via a file called __.35rc__ 92 | * to generate .35rc run the following 93 | ```js 94 | threefive hls profile 95 | ``` 96 | * it will creat .35rc in the current directory 97 | 98 | ```js 99 | a@fu:~$ cat .35rc 100 | 101 | expand_cues = False 102 | parse_segments = False 103 | parse_manifests = True 104 | hls_tags = #EXT-OATCLS-SCTE35,#EXT-X-CUE-OUT-CONT, 105 | #EXT-X-DATERANGE,#EXT-X-SCTE35,#EXT-X-CUE-IN,#EXT-X-CUE-OUT 106 | command_types = 0x6,0x5 107 | descriptor_tags = 0x2 108 | starts = 0x22,0x30,0x32,0x34,0x36,0x44,0x46 109 | ``` 110 | ( Integers are show in hex (base 16), 111 | base 10 unsigned integers can also be used in .35rc ) 112 | 113 | 114 | __expand_cues:__ `set to True to show cues fully expanded as JSON` 115 | 116 | 117 | __parse_segments:__ `set to true to enable parsing SCTE-35 from MPEGTS.` 118 | 119 | __parse_manifests:__ `set to true to parse the m3u8 file for SCTE-35 HLS Tags.` 120 | 121 | __hls_tags:__ `set which SCTE-35 HLS Tags to parse.` 122 | 123 | __command_types:__ `set which Splice Commands to parse.` 124 | 125 | __descriptor_tags:__ `set which Splice Descriptor Tags to parse.` 126 | 127 | __starts:__ `set which Segmentation Type IDs to use to start breaks.` 128 | 129 | 130 | 131 | Edit the file as needed and then run threefive hls from the same directory. 132 | 133 | 134 | [__Profile Formatting Rules__ ] 135 | 136 | * Values do not need to be quoted. 137 | 138 | * Multiple values are separated by a commas 139 | 140 | * No partial line comments. Comments must be on a separate lines. 141 | 142 | * Comments can be started with a # or // 143 | * Integers can be base 10 or base 16 144 | 145 | * __threefive hls__ genrates a few output files to make it easier to debug live HLS with SCTE-35 146 | 147 | 148 | [__Output Files__] 149 | 150 | * Created in the current working directory 151 | * __Output files aree Clobbered on start of threefive hls__ 152 | * this is done to prevent old files from stacking up. 153 | * If you want to keep a file, rename it before restarting __threefve hls__ 154 | * Profile rules applied to the output: 155 | * __35.m3u8__ - live playable rewrite of the m3u8 156 | * __35.sidecar__ - list of ( pts, HLS SCTE-35 tag ) pairs 157 | 158 | * Profile rules not applied to the output: 159 | * __35.dump__ - all of the HLS SCTE-35 tags read. 160 | * __35.flat__ - every time an m3u8 is reloaded, it's contents are appended to 35.flat. 161 | 162 | 163 | [ Cool Features ] 164 | 165 | * __ALL SCTE-35 HLS tags are supported__. 166 | * SCTE-35 can also be parsed from segments. 167 | 168 | * __Automatic AES Decryption__, you don't have to do anything, __threefive hls __ 169 | __automatically detects and decrypts AES encrypted segments__ on the fly. 170 | 171 | 172 | * Preroll and splice point and diff of the splice point are displayed. 173 | ```js 174 | 175 | 2024-11-08T13:01:49.60Z SCTE-35 176 | Stream PTS: 71317.660444 177 | PreRoll: 4.090678 178 | Splice Point: 71321.751122 179 | Type: Time Signal 180 | Media: index_2_8638662.ts 181 | 182 | 183 | ``` 184 | 185 | * mpegts streams are listed on start ( like ffprobe ) 186 | ```js 187 | Program: 1 188 | 189 | Service: 190 | Provider: 191 | Pid: 480 192 | Pcr Pid: 481 193 | Streams: 194 | Pid Type 195 | 481 [0x1e1] 0x1b H.264 196 | 482 [0x1e2] 0xf ADTS AAC 197 | 483 [0x1e3] 0x86 SCTE-35 198 | 484 [0x1e4] 0xfc KLV 199 | 485 [0x1e5] 0x15 ID 200 | ``` 201 | * profile settings are also displayed on start 202 | ```js 203 | 204 | 205 | Profile: 206 | 207 | expand_cues = False 208 | 209 | parse_segments = True 210 | 211 | parse_manifests = True 212 | 213 | hls_tags = ['#EXT-OATCLS-SCTE35', '#EXT-X-DATERANGE', '#EXT-X-SCTE35', '#EXT-X-CUE-OUT', '#EXT-X-CUE-OUT-CONT', '#EXT-X-CUE-IN'] 214 | 215 | command_types = ['0x5', '0x6'] 216 | 217 | descriptor_tags = ['0x2'] 218 | 219 | starts = ['0x22', '0x30', '0x32', '0x34', '0x36', '0x44', '0x46'] 220 | 221 | seg_type = [''] 222 | ``` 223 | * current wall time and PTS is displayed while threefive hls is parsing. 224 | ```js 225 | 24-11-08T12:39:19.02Z PTS 69935.651111 226 | 227 | ``` 228 | * break duration and break progress are displayed during ad breaks 229 | ```js 230 | 2024-11-08T13:00:43.25Z PTS 71253.384444 Break 203.967 / 270.035 231 | ``` 232 | * PTS is parsed directly from the HLS segments for accuracy. 233 | 234 | * threefive hls can resume when started in the middle of an ad break. 235 | ```js 236 | 2023-10-13T05:59:50.24Z Resuming Ad Break 237 | 2023-10-13T05:59:50.34Z Setting Break Timer to 17.733 238 | 2023-10-13T05:59:50.44Z Setting Break Duration to 60.067 239 | ``` 240 | 241 | [ Example Usage ] 242 | 243 | * Show this help: 244 | ```sed 245 | threefive hls help 246 | ``` 247 | * Generate a new .35rc 248 | ```sed 249 | threefive hls profile 250 | ``` 251 | * parse an m3u8 252 | ```sed 253 | threefive hls https://example.com/out/v1/547e1b8d09444666ac810f6f8c78ca82/index.m3u8 254 | ``` 255 | 256 | 257 | -------------------------------------------------------------------------------- /issues.md: -------------------------------------------------------------------------------- 1 | 2 | # Issues 3 | 4 | 5 | ## I generally resolve issues fairly quuickly, most within hours.
If you open an issue, and ask me for help, I must have the following. 6 | 7 | 8 | ### I need the output. All of the output.
9 | 10 | If it's a four page stack trace, give it to me. I want it all.
11 | __Don't worry about looking stupid__, We all do stupid stuff,
12 | and I am not going to be jackass about it.
13 | _If I see the same stupid mistake several times,
14 | it tells me that it's not you, it's me and
15 | I need to change something so people understand it better._
16 | ___ 17 | ### If it is stream related, I need the stream.
18 | 19 | In most cases, all I need is a few seconds that contain the SCTE-35 in question.
20 | If you cannot show me the stream, I cannot help you.
21 | ___ 22 | ### If it's code related, I need to see your code. 23 | 24 | I don't want a "snippet",
25 | __I need working code.__ Don't be shy abiout it, show me the code. Again,
26 | __I won't be a jackass about it,__ and if I know a better way to do it, I'll show you.
27 | If you have a better way, show me. We both win.
28 | ___ 29 | ### I expect you to provide feedback, and let me know that your problem is resolved. 30 | 31 | Don't just disappear. __You opened the issue, when your problem is resolved, close it.__
32 | It's not about you, it's about the next person. __You have an obligation to make the information
33 | usable to others, that's what makes open source software work.__
34 | ___ 35 | -------------------------------------------------------------------------------- /json.txt: -------------------------------------------------------------------------------- 1 | { 2 | "info_section": { 3 | "table_id": "0xfc", 4 | "section_syntax_indicator": false, 5 | "private": false, 6 | "sap_type": "0x03", 7 | "sap_details": "No Sap Type", 8 | "section_length": 97, 9 | "protocol_version": 0, 10 | "encrypted_packet": false, 11 | "encryption_algorithm": 0, 12 | "pts_adjustment": 0.0, 13 | "cw_index": "0xff", 14 | "tier": "0x0fff", 15 | "splice_command_length": 5, 16 | "splice_command_type": 6, 17 | "descriptor_loop_length": 75, 18 | "crc": "0x8a18869f" 19 | }, 20 | "command": { 21 | "command_length": 5, 22 | "command_type": 6, 23 | "name": "Time Signal", 24 | "time_specified_flag": true, 25 | "pts_time": 31466.942367 26 | }, 27 | "descriptors": [ 28 | { 29 | "tag": 2, 30 | "descriptor_length": 23, 31 | "name": "Segmentation Descriptor", 32 | "identifier": "CUEI", 33 | "segmentation_event_id": "0x480000ad", 34 | "segmentation_event_cancel_indicator": false, 35 | "segmentation_event_id_compliance_indicator": true, 36 | "program_segmentation_flag": true, 37 | "segmentation_duration_flag": false, 38 | "delivery_not_restricted_flag": false, 39 | "web_delivery_allowed_flag": true, 40 | "no_regional_blackout_flag": true, 41 | "archive_allowed_flag": true, 42 | "device_restrictions": "No Restrictions", 43 | "segmentation_message": "Provider Placement Opportunity End", 44 | "segmentation_upid_type": 8, 45 | "segmentation_upid_type_name": "AiringID", 46 | "segmentation_upid_length": 8, 47 | "segmentation_upid": "0x2cb2d79d", 48 | "segmentation_type_id": 53, 49 | "segment_num": 2, 50 | "segments_expected": 0 51 | }, 52 | { 53 | "tag": 2, 54 | "descriptor_length": 23, 55 | "name": "Segmentation Descriptor", 56 | "identifier": "CUEI", 57 | "segmentation_event_id": "0x48000026", 58 | "segmentation_event_cancel_indicator": false, 59 | "segmentation_event_id_compliance_indicator": true, 60 | "program_segmentation_flag": true, 61 | "segmentation_duration_flag": false, 62 | "delivery_not_restricted_flag": false, 63 | "web_delivery_allowed_flag": true, 64 | "no_regional_blackout_flag": true, 65 | "archive_allowed_flag": true, 66 | "device_restrictions": "No Restrictions", 67 | "segmentation_message": "Program End", 68 | "segmentation_upid_type": 8, 69 | "segmentation_upid_type_name": "AiringID", 70 | "segmentation_upid_length": 8, 71 | "segmentation_upid": "0x2cb2d79d", 72 | "segmentation_type_id": 17, 73 | "segment_num": 0, 74 | "segments_expected": 0 75 | }, 76 | { 77 | "tag": 2, 78 | "descriptor_length": 23, 79 | "name": "Segmentation Descriptor", 80 | "identifier": "CUEI", 81 | "segmentation_event_id": "0x48000027", 82 | "segmentation_event_cancel_indicator": false, 83 | "segmentation_event_id_compliance_indicator": true, 84 | "program_segmentation_flag": true, 85 | "segmentation_duration_flag": false, 86 | "delivery_not_restricted_flag": false, 87 | "web_delivery_allowed_flag": true, 88 | "no_regional_blackout_flag": true, 89 | "archive_allowed_flag": true, 90 | "device_restrictions": "No Restrictions", 91 | "segmentation_message": "Program Start", 92 | "segmentation_upid_type": 8, 93 | "segmentation_upid_type_name": "AiringID", 94 | "segmentation_upid_length": 8, 95 | "segmentation_upid": "0x2cb2d7b3", 96 | "segmentation_type_id": 16, 97 | "segment_num": 0, 98 | "segments_expected": 0 99 | } 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /latest-cli.md: -------------------------------------------------------------------------------- 1 | # threefive 2 | ### the cli 3 | * [default](#default) __Basic threefive cli usage.__ _(start here)_ 4 | * [hls](#hls) __HLS SCTE-35 Decoding and Encoding.__ 5 | * [xml](#xml) __Xml__ _output for_ _SCTE-35._ 6 | * [inject](#inject) __Inject SCTE-35__ _packets into mpegts streams._ 7 | * [packets](#packets) _Output raw SCTE-35 packets from mpegts._ 8 | * [proxy](#proxy) _Parse SCTE-35 from mpegts and copy video stream to stdout._ 9 | * [pts](#pts) _Show PTS in realtime from live streams._ 10 | * [sidecar](#sidecar) _Parse MPEGTS and_ __generate a SCTE-35 sidecar file.__ 11 | * [sixfix](#sixfix) __ffmpeg mangles SCTE-35__, _sixfix is how to fix it._ 12 | * [show](#show) _Display stream information for mpegts._ 13 | * [version](#version) _Print threefive version._ 14 | * [help](#help) _Show threefive help._ 15 | --- 16 | ### `default` 17 | * The default action of the threefive cli is to take an input and make it a SCTE-35 output. 18 | * SCTE-35 inputs are auto-detected. 19 | * SCTE-35 output default to json, other types must be specified. 20 | 21 | * These inputs are supported. 22 | * `mpegts` , `base64`, `hex`, `json`, `xml`, `xmlbin` 23 | 24 | * These outputs are supported. 25 | * `base64`, `bytes`, `hex`, `json`, `xml`, `xmlbin` 26 | 27 | ### Example usage 28 | 29 | | Input | Output | Command | 30 | |--------------|---------------|---------------------------------------------------------------------------| 31 | | `mpegts` | `base64` | **threefive** https://example.com/video.ts **base64** | 32 | | `xml` | `bytes` | **threefive** bytes < xml.xml | 33 | | `base64` | `hex` | **threefive** '/DAWAAAAAAAAAP/wBQb+AKmKxwAACzuu2Q==' **hex** | 34 | | `xmlbin` | `int` | **threefive int** < xml.xml | 35 | | `mpegts` | `json` | **threefive** video.ts | 36 | | `json` | `xml` | **threefive** < json.json **xml** | 37 | | `hex` | `xmlbin` | **threefive** 0xfc301600000000000000fff00506fe00a98ac700000b3baed9 **xmlbin** 38 | | 39 | ___ 40 | ### `hls` 41 | * SCTE-35 Decoding and Encoding for HLS 42 | 43 | * HLS SCTE-35 Decoding: 44 | ```asm 45 | threefive hls https://example.com/master.m3u8 46 | ``` 47 | * _read the help for advanced HLS SCTE-35 decoding._ 48 | ```asm 49 | threefive hls help 50 | ``` 51 | * HLS SCTE-35 Encoding: 52 | ```asm 53 | threefive hls -i https://example.com/master.m3u8 -s sidecar.txt -o output_dir 54 | ``` 55 | * _read the help for advanced HLS SCTE-35 encoding._ 56 | ``` asm 57 | threefive hls encode help 58 | ``` 59 | ___ 60 | ### `xml` 61 | * Xml output: 62 | 63 | * Base64 64 | * xml splice info section format 65 | ```asm 66 | threefive xml '/DAsAAAAAAAAAP/wBQb+7YaD1QAWAhRDVUVJAADc8X+/DAVPVkxZSSIAAJ6Gk2Q=' 67 | ``` 68 | * xml+bin format 69 | ```asm 70 | threefive xmlbin '/DAsAAAAAAAAAP/wBQb+7YaD1QAWAhRDVUVJAADc8X+/DAVPVkxZSSIAAJ6Gk2Q=' 71 | ``` 72 | * MPEGTS 73 | * xml splice info section format 74 | ```asm 75 | threefive xml https://example.com/video.ts 76 | ``` 77 | * xml+bin format 78 | ```asm 79 | threefive xmlbin https://example.com/video.ts 80 | ``` 81 | ___ 82 | ### `inject` 83 | * Inject an mpegts stream with a SCTE-35 sidecar file at pid: 84 | ```asm 85 | threefive inject video.ts with sidecar.txt at 333 86 | ``` 87 | ___ 88 | ### `packets` 89 | * Print raw SCTE-35 packets from multicast mpegts video: 90 | ```asm 91 | threefive packets https://example.com/video.ts 92 | ``` 93 | ___ 94 | ### `proxy` 95 | * Parse a https stream and write raw video to stdout: 96 | ```asm 97 | threefive proxy https://example.com/video.ts 98 | ``` 99 | ___ 100 | ### `pts` 101 | * Print PTS from mpegts video: 102 | ```asm 103 | threefive pts video.ts 104 | ``` 105 | ___ 106 | ### `sidecar` 107 | * Parse a stream, write pts,write SCTE-35 Cues to sidecar.txt: 108 | ```asm 109 | threefive sidecar https://example.com/video.ts 110 | ``` 111 | ___ 112 | ### `sixfix` 113 | * Fix SCTE-35 data mangled by ffmpeg: 114 | ```asm 115 | threefive sixfix video.ts 116 | ``` 117 | ___ 118 | ### `show` 119 | * Probe mpegts video: 120 | ```asm 121 | threefive show video.ts 122 | ``` 123 | ___ 124 | ### `version` 125 | ```asm 126 | threefive version 127 | ``` 128 | ___ 129 | ### `help` 130 | ```asm 131 | threefive help 132 | ``` 133 | ___ 134 | -------------------------------------------------------------------------------- /mpd_parser.md: -------------------------------------------------------------------------------- 1 | # Minimal Dash SCTE-35 mpd parser. 2 | 3 | ```py3 4 | #!/usr/bin/env python3 5 | 6 | """ 7 | mpdp.py minimal SCTE-35 mpd parser. 8 | 9 | """ 10 | 11 | 12 | import sys 13 | from new_reader import reader 14 | from threefive.xml import XmlParser 15 | from threefive import Cue 16 | 17 | 18 | def mk_event(exemel, event_num): 19 | """ 20 | mk_event split out Event nodes 21 | """ 22 | event_num += 1 23 | ridx = exemel.index("") 24 | lidx = exemel.index("\n{event}") 27 | exemel = exemel[ridx + 8 :] 28 | return event, exemel,event_num 29 | 30 | 31 | def mk_scte35(event): 32 | """ 33 | mk_scte35 parse xml into a dict 34 | """ 35 | xp = XmlParser() 36 | scte35 = xp.parse(event) 37 | return scte35 38 | 39 | 40 | def show_scte35(cue,event_num): 41 | """ 42 | show_scte35 43 | """ 44 | if cue: 45 | print(f"\n\n") 46 | cue.show() 47 | print( 48 | f"\n\n" 49 | ) 50 | print(cue.xml()) 51 | 52 | 53 | def chk_scte35(scte35,event_num): 54 | """ 55 | chk_scte35 check for SCTE-35 xml 56 | and SCTE-35 xml+bin 57 | """ 58 | cue = False 59 | if "SpliceInfoSection" in scte35: 60 | cue = Cue() 61 | cue.load(scte35) # <-- cue.load() is used with xml. 62 | cue.encode() 63 | if "Binary" in scte35: 64 | cue = Cue( 65 | scte35["Binary"]["binary"] 66 | ) # init with the data for Base64 encoded SCTE-35 67 | cue.decode() 68 | show_scte35(cue, event_num) 69 | 70 | 71 | def parse_mpd(mpd): 72 | """ 73 | parse_mpd parse an mpd 74 | """ 75 | event_num =0 76 | exemel = reader(mpd).read().decode().replace("\n", " \n") 77 | while "" in exemel: 78 | event, exemel, event_num = mk_event(exemel, event_num) 79 | scte35 = mk_scte35(event) 80 | chk_scte35(scte35,event_num) 81 | 82 | 83 | if __name__ == "__main__": 84 | args = sys.argv[1:] 85 | for arg in args: 86 | parse_mpd(arg) 87 | ``` 88 | ### Usage: 89 | ```sh 90 | pypy3 mpdp.py https://demo.unified-streaming.com/k8s/live/stable/scte35-no-splicing.isml/.mpd 91 | 92 | ``` 93 | ### Output: 94 | 95 | ```js 96 | 97 | 98 | 102 | 104 | /DAgAAAAAAAAAP/wDwUA2h/mf//+ADS8AMAAAAAAAORhJCQ= 105 | 106 | 107 | 108 | 109 | 110 | { 111 | "info_section": { 112 | "table_id": "0xfc", 113 | "section_syntax_indicator": false, 114 | "private": false, 115 | "sap_type": "0x03", 116 | "sap_details": "No Sap Type", 117 | "section_length": 32, 118 | "protocol_version": 0, 119 | "encrypted_packet": false, 120 | "encryption_algorithm": 0, 121 | "pts_adjustment": 0.0, 122 | "cw_index": "0x00", 123 | "tier": "0x0fff", 124 | "splice_command_length": 15, 125 | "splice_command_type": 5, 126 | "descriptor_loop_length": 0, 127 | "crc": "0xe4612424" 128 | }, 129 | "command": { 130 | "command_length": 15, 131 | "command_type": 5, 132 | "name": "Splice Insert", 133 | "break_auto_return": true, 134 | "break_duration": 38.4, 135 | "splice_event_id": 14295014, 136 | "splice_event_cancel_indicator": false, 137 | "out_of_network_indicator": true, 138 | "program_splice_flag": true, 139 | "duration_flag": true, 140 | "splice_immediate_flag": true, 141 | "event_id_compliance_flag": true, 142 | "unique_program_id": 49152, 143 | "avail_num": 0, 144 | "avails_expected": 0 145 | }, 146 | "descriptors": [] 147 | } 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 163 | 165 | /DAgAAAAAAAAAP/wDwUA2h/nf//+ADS8AMAAAAAAAORhJCQ= 166 | 167 | 168 | 169 | 170 | 171 | { 172 | "info_section": { 173 | "table_id": "0xfc", 174 | "section_syntax_indicator": false, 175 | "private": false, 176 | "sap_type": "0x03", 177 | "sap_details": "No Sap Type", 178 | "section_length": 32, 179 | "protocol_version": 0, 180 | "encrypted_packet": false, 181 | "encryption_algorithm": 0, 182 | "pts_adjustment": 0.0, 183 | "cw_index": "0x00", 184 | "tier": "0x0fff", 185 | "splice_command_length": 15, 186 | "splice_command_type": 5, 187 | "descriptor_loop_length": 0, 188 | "crc": "0xe4612424" 189 | }, 190 | "command": { 191 | "command_length": 15, 192 | "command_type": 5, 193 | "name": "Splice Insert", 194 | "break_auto_return": true, 195 | "break_duration": 38.4, 196 | "splice_event_id": 14295015, 197 | "splice_event_cancel_indicator": false, 198 | "out_of_network_indicator": true, 199 | "program_splice_flag": true, 200 | "duration_flag": true, 201 | "splice_immediate_flag": true, 202 | "event_id_compliance_flag": true, 203 | "unique_program_id": 49152, 204 | "avail_num": 0, 205 | "avails_expected": 0 206 | }, 207 | "descriptors": [] 208 | } 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | ... 219 | 220 | ``` 221 | 222 | -------------------------------------------------------------------------------- /new2471.md: -------------------------------------------------------------------------------- 1 | New Features in threefive v2.4.71 2 | 3 | # sixfix 4 | * ffmpeg changes SCTE-35 streams types to 0x6 bin data. sixfix will convert the bin data back to SCTE-35. 5 | * The new file name is prefixed with 'fixed'. 6 | * As with all threefive, this works with local, http(s), UDP , and Multicast sources. 7 | 8 | 9 | * cli sixfix 10 | --- 11 | * Input file is sixed.ts 12 | 13 | ```js 14 | a@fu:~/build/SCTE35_threefive$ ffprobe -hide_banner sixed.ts 15 | [mpegts @ 0x55afc5641140] start time for stream 2 is not set in estimate_timings_from_pts 16 | [mpegts @ 0x55afc5641140] start time for stream 3 is not set in estimate_timings_from_pts 17 | Input #0, mpegts, from 'sixed.ts': 18 | Duration: 00:04:56.30, start: 72668.995200, bitrate: 943 kb/s 19 | Program 1 20 | Metadata: 21 | service_name : Service01 22 | service_provider: FFmpeg 23 | Stream #0:0[0x100]: Video: h264 (Main) ([27][0][0][0] / 0x001B), yuv420p(tv, progressive), 640x360 [SAR 1:1 DAR 16:9], 29.97 fps, 29.97 tbr, 90k tbn 24 | Stream #0:1[0x101](und): Audio: aac (LC) ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp, 64 kb/s 25 | Stream #0:2[0x102]: Data: bin_data ([6][0][0][0] / 0x0006) <------ Bin Data 26 | Stream #0:3[0x103]: Data: timed_id3 (ID3 / 0x20334449) 27 | ``` 28 | * Run sixfix 29 | ```js 30 | a@fu:~/build/SCTE35_threefive$ ./threefive sixfix sixed.ts 31 | ``` 32 | * output file is named fixed-sixed.ts 33 | ```js 34 | a@fu:~/build/SCTE35_threefive$ ffprobe -hide_banner fixed-sixed.ts 35 | Input #0, mpegts, from 'fixed-sixed.ts': 36 | Duration: 00:04:53.66, start: 72668.995200, bitrate: 944 kb/s 37 | Program 1 38 | Metadata: 39 | service_name : Service01 40 | service_provider: FFmpeg 41 | Stream #0:0[0x100]: Video: h264 (Main) ([27][0][0][0] / 0x001B), yuv420p(tv, progressive), 640x360 [SAR 1:1 DAR 16:9], 29.97 fps, 29.97 tbr, 90k tbn 42 | Stream #0:1[0x101](und): Audio: aac (LC) ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp, 64 kb/s 43 | Stream #0:2[0x102]: Data: scte_35 <------------ fixed 44 | Stream #0:3[0x103]: Data: timed_id3 (ID3 / 0x20334449) 45 | ``` 46 | 47 | * code 48 | * you call one function with one arg, the mpegts to fix. 49 | 50 | ```py3 51 | a@fu:~/build/SCTE35_threefive$ pypy3 52 | Python 3.9.16 (7.3.11+dfsg-2+deb12u2, May 20 2024, 22:08:06) 53 | [PyPy 7.3.11 with GCC 12.2.0] on linux 54 | Type "help", "copyright", "credits" or "license" for more information. 55 | >>>> from threefive.sixfix import sixfix 56 | >>>> sixfix("sixed.ts") 57 | fixing these pids {258} 58 | Wrote: sixfixed-sixed.ts 59 | ``` 60 | --- 61 | 62 | # xml 63 | * the cli tool now supports generating valid SCTE-35 xml output. 64 | * Input can be Base64, Hex or mpegts video. 65 | ___ 66 | * cli 67 | ```js 68 | a@fu:~/build/SCTE35_threefive$ ./threefive xml '/DBAAAGRZOeYAAAABQb+hJ8vqAAqAihDVUVJ/////3//AAAbX9ABFG1zbmJjX0VQMDQzMTEyMjEwNTU2EQEAbABeoQ==' 69 | ``` 70 | ```xml 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | msnbc_EP043112210556 79 | 80 | 81 | ``` 82 | * code 83 | ```py3 84 | Python 3.9.16 (7.3.11+dfsg-2+deb12u2, May 20 2024, 22:08:06) 85 | [PyPy 7.3.11 with GCC 12.2.0] on linux 86 | Type "help", "copyright", "credits" or "license" for more information. 87 | >>>> from threefive import Cue 88 | >>>> data = '0xfc302500000000000000fff014050000013f7fefffffc3f680fe00a4cb80013f0000000063394f9c' 89 | >>>> cue=Cue(data) 90 | >>>> cue.decode() 91 | True 92 | >>>> print(cue.xml()) 93 | 94 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | >>>> 104 | ``` 105 | 106 | * mpegts streams can be parsed and SCTE-35 output xml 107 | --- 108 | * cli 109 | ```js 110 | a@fu:~/build/SCTE35_threefive$ ./threefive-exp xml fixed-sixed.ts 111 | ``` 112 | * code 113 | * Stream.decode accepts an optional function to be called every time a cue is found. 114 | ```py3 115 | #!/usr/bin/env python3 116 | 117 | import sys 118 | from threefive import Stream,print2 119 | 120 | 121 | def xml_out(cue): 122 | """ 123 | xml_out print a Cue instance as xml 124 | """ 125 | print2(cue.xml()) 126 | 127 | 128 | if __name__=='__main__': 129 | for arg in sys.argv[1:]: 130 | strm = Stream(arg) 131 | strm.decode(func=xml_out) 132 | 133 | ``` 134 | 135 | 136 | ### Loading and Encoding and Conversion 137 | * the threefive cli can load JSON, XML, Base64 or Hex and encode to JSON,XML,Base64,Hex,Int or Bytes 138 | --- 139 | * base64 to hex 140 | ```py3 141 | printf '/DBAAAGRZOeYAAAABQb+hJ8vqAAqAihDVUVJ/////3//AAAbX9ABFG1zbmJjX0VQMDQzMTEyMjEwNTU2EQEAbABeoQ==' |threefive encode hex 142 | ``` 143 | * hex to base64 144 | ```py3 145 | printf 0xfc302500000000000000fff014050000013f7fefffffc3f680fe00a4cb80013f0000000063394f9c| threefive encode 146 | ``` 147 | * xml to base64 148 | ```py3 149 | cat xml.xml | threefive encode 150 | ``` 151 | * xml to hex 152 | ```py3 153 | cat xml.xml | threefive encode hex 154 | ``` 155 | * json to xml 156 | ```py3 157 | cat json.json | threefive encode xml 158 | ``` 159 | * xml to json 160 | ```py3 161 | cat xml.xml | threefive encode json 162 | ``` 163 | * xml to bytes 164 | ```py3 165 | cat xml.xml | threefive encode bytes 166 | 167 | ``` 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /newcli.md: -------------------------------------------------------------------------------- 1 | # threefive 2 | ### the cli 3 | * [default](#default) __Basic threefive cli usage.__ _(start here)_ 4 | * [hls](#hls) __HLS SCTE-35 Decoding and Encoding.__ 5 | * [xml](#xml) __Xml__ _output for_ _SCTE-35._ 6 | * [inject](#inject) __Inject SCTE-35__ _packets into mpegts streams._ 7 | * [packets](#packets) _Output raw SCTE-35 packets from mpegts._ 8 | * [proxy](#proxy) _Parse SCTE-35 from mpegts and copy video stream to stdout._ 9 | * [pts](#pts) _Show PTS in realtime from live streams._ 10 | * [sidecar](#sidecar) _Parse MPEGTS and_ __generate a SCTE-35 sidecar file.__ 11 | * [sixfix](#sixfix) __ffmpeg mangles SCTE-35__, _sixfix is how to fix it._ 12 | * [show](#show) _Display stream information for mpegts._ 13 | * [version](#version) _Print threefive version._ 14 | * [help](#help) _Show threefive help._ 15 | --- 16 | ### `default` 17 | * The default action of the threefive cli is to take an input and make it a SCTE-35 output. 18 | * SCTE-35 inputs are auto-detected. 19 | * SCTE-35 output default to json, other types must be specified. 20 | 21 | * These inputs are supported. 22 | * `mpegts` , `base64`, `hex`, `json`, `xml`, `xmlbin` 23 | 24 | * These outputs are supported. 25 | * `base64`, `bytes`, `hex`, `json`, `xml`, `xmlbin` 26 | 27 | ### Example usage 28 | 29 | | Input | Output | Command | 30 | |--------------|---------------|---------------------------------------------------------------------------| 31 | | `mpegts` | `base64` | **threefive** https://example.com/video.ts **base64** | 32 | | `xml` | `bytes` | **threefive** bytes < xml.xml | 33 | | `base64` | `hex` | **threefive** '/DAWAAAAAAAAAP/wBQb+AKmKxwAACzuu2Q==' **hex** | 34 | | `xmlbin` | `int` | **threefive int** < xml.xml | 35 | | `mpegts` | `json` | **threefive** video.ts | 36 | | `json` | `xml` | **threefive** < json.json **xml** | 37 | | `hex` | `xmlbin` | **threefive** 0xfc301600000000000000fff00506fe00a98ac700000b3baed9 **xmlbin** 38 | | 39 | ___ 40 | ### `hls` 41 | * SCTE-35 Decoding and Encoding for HLS 42 | 43 | * HLS SCTE-35 Decoding: 44 | ```asm 45 | threefive hls https://example.com/master.m3u8 46 | ``` 47 | * _read the help for advanced HLS SCTE-35 decoding._ 48 | ```asm 49 | threefive hls help 50 | ``` 51 | * HLS SCTE-35 Encoding: 52 | ```asm 53 | threefive hls -i https://example.com/master.m3u8 -s sidecar.txt -o output_dir 54 | ``` 55 | * _read the help for advanced HLS SCTE-35 encoding._ 56 | ``` asm 57 | threefive hls encode help 58 | ``` 59 | ___ 60 | ### `xml` 61 | * Xml output: 62 | 63 | * Base64 64 | * xml splice info section format 65 | ```asm 66 | threefive xml '/DAsAAAAAAAAAP/wBQb+7YaD1QAWAhRDVUVJAADc8X+/DAVPVkxZSSIAAJ6Gk2Q=' 67 | ``` 68 | * xml+bin format 69 | ```asm 70 | threefive xmlbin '/DAsAAAAAAAAAP/wBQb+7YaD1QAWAhRDVUVJAADc8X+/DAVPVkxZSSIAAJ6Gk2Q=' 71 | ``` 72 | * MPEGTS 73 | * xml splice info section format 74 | ```asm 75 | threefive xml https://example.com/video.ts 76 | ``` 77 | * xml+bin format 78 | ```asm 79 | threefive xmlbin https://example.com/video.ts 80 | ``` 81 | ___ 82 | ### `inject` 83 | * Inject an mpegts stream with a SCTE-35 sidecar file at pid: 84 | ```asm 85 | threefive inject video.ts with sidecar.txt at 333 86 | ``` 87 | ___ 88 | ### `packets` 89 | * Print raw SCTE-35 packets from multicast mpegts video: 90 | ```asm 91 | threefive packets https://example.com/video.ts 92 | ``` 93 | ___ 94 | ### `proxy` 95 | * Parse a https stream and write raw video to stdout: 96 | ```asm 97 | threefive proxy https://example.com/video.ts 98 | ``` 99 | ___ 100 | ### `pts` 101 | * Print PTS from mpegts video: 102 | ```asm 103 | threefive pts video.ts 104 | ``` 105 | ___ 106 | ### `sidecar` 107 | * Parse a stream, write pts,write SCTE-35 Cues to sidecar.txt: 108 | ```asm 109 | threefive sidecar https://example.com/video.ts 110 | ``` 111 | ___ 112 | ### `sixfix` 113 | * Fix SCTE-35 data mangled by ffmpeg: 114 | ```asm 115 | threefive sixfix video.ts 116 | ``` 117 | ___ 118 | ### `show` 119 | * Probe mpegts video: 120 | ```asm 121 | threefive show video.ts 122 | ``` 123 | ___ 124 | ### `version` 125 | ```asm 126 | threefive version 127 | ``` 128 | ___ 129 | ### `help` 130 | ```asm 131 | threefive help 132 | ``` 133 | ___ 134 | -------------------------------------------------------------------------------- /oo.md: -------------------------------------------------------------------------------- 1 | # threefive: more OO than you know. 2 | 3 | I designed threefive to be predictable,most objects work the same way and have the same methods. 4 | Most classes are derived from SCTE35Base. The Splice Commands, Splice Descriptors, and Cue classes are subclassed from SCTE35Base. 5 | This really helps when making new SCTE-35 Cues. 6 | 7 | I can ramble on about it, but I think it best to show you. 8 | 9 | Let's make a Cue in the python shell 10 | 11 | ```py3 12 | a@fu:~/build/SCTE35_threefive$ pypy3 13 | Python 3.9.16 (7.3.11+dfsg-2+deb12u2, May 20 2024, 22:08:06) 14 | [PyPy 7.3.11 with GCC 12.2.0] on linux 15 | Type "help", "copyright", "credits" or "license" for more information. 16 | 17 | >>>> from threefive import Cue, TimeSignal, AvailDescriptor 18 | ``` 19 | * let's make a TimeSignal 20 | 21 | ```py3 22 | >>>> ts=TimeSignal() 23 | >>>> ts 24 | {'command_length': 0, 'command_type': 6, 'name': 'Time Signal', 'bites': None, 'time_specified_flag': None, 'pts_time': None} 25 | 26 | ```` 27 | * when you encode a Splice Command or Splice Descriptor, threefive will help you get it right. 28 | * When reading a python stack trace, the last line is the error. 29 | ```py3 30 | >>>> ts.encode() 31 | Traceback (most recent call last): 32 | File "", line 1, in 33 | File "/home/a/build/SCTE35_threefive/threefive/commands.py", line 157, in encode 34 | self._encode_splice_time(nbin) 35 | File "/home/a/build/SCTE35_threefive/threefive/commands.py", line 176, in _encode_splice_time 36 | self._chk_var(bool, nbin.add_flag, "time_specified_flag", 1) 37 | File "/home/a/build/SCTE35_threefive/threefive/base.py", line 38, in _chk_var 38 | raise ValueError(err_mesg) 39 | ValueError: time_specified_flag is not set, it should be 1 bit(s) and type <--- the last line is the error 40 | ``` 41 | * set some vars and try to encode again 42 | ```py3 43 | >>>> ts.time_specified_flag = True 44 | >>>> ts.pts_time=12345.6789 45 | 46 | >>>> ts.encode() 47 | 48 | b'\xfeB:5\xbd' <--- encoding returns a byte string when it works 49 | 50 | ``` 51 | 52 | * call some methods 53 | ```py3 54 | >>>> ts.xml() 55 | 56 | 57 | 58 | 59 | >>>> ts.show() 60 | { 61 | "command_length": 0, 62 | "command_type": 6, 63 | "name": "Time Signal", 64 | "time_specified_flag": true, 65 | "pts_time": 12345.6789 66 | } 67 | ``` 68 | * Now let's make a Cue instance and add the TimeSignal to it. 69 | ```py3 70 | >>>> cue =Cue() 71 | >>>> cue.command=ts 72 | >>>> cue.encode() 73 | '/DAWAAAAAAAAAP/wBQb+Qjo1vQAAuwxz9A==' <--- encode returns a Base64 string for a cue 74 | ``` 75 | * Calling encode on an object will recalculate several vars in the object, 76 | * When encoding a Cue, it will generate anything missing, including the info_section. 77 | ```py3 78 | >>>> cue.info_section 79 | {'table_id': '0xfc', 'section_syntax_indicator': False, 'private': False, 'sap_type': '0x03', 'sap_details': 'No Sap Type', 'section_length': 22, 'protocol_version': 0, 'encrypted_packet': False, 'encryption_algorithm': 0, 'pts_adjustment': 0, 'cw_index': '0x0', 'tier': '0xfff', 'splice_command_length': 5, 'splice_command_type': 6, 'descriptor_loop_length': 0, 'crc': '0xbb0c73f4'} 80 | >>>> 81 | ``` 82 | * encode(), decode(), xml(), and show() also work with Cue 83 | ```py3 84 | >>>> cue.xml() 85 | 86 | 87 | 88 | 89 | 90 | 91 | >>>> cue.command.xml() <-- this is the TimeSignal 92 | 93 | 94 | 95 | 96 | >>>> cue.show() 97 | { 98 | "info_section": { 99 | "table_id": "0xfc", 100 | "section_syntax_indicator": false, 101 | "private": false, 102 | "sap_type": "0x03", 103 | "sap_details": "No Sap Type", 104 | "section_length": 22, 105 | "protocol_version": 0, 106 | "encrypted_packet": false, 107 | "encryption_algorithm": 0, 108 | "pts_adjustment": 0, 109 | "cw_index": "0x0", 110 | "tier": "0xfff", 111 | "splice_command_length": 5, 112 | "splice_command_type": 6, 113 | "descriptor_loop_length": 0, 114 | "crc": "0xbb0c73f4" 115 | }, 116 | "command": { 117 | "command_length": 5, 118 | "command_type": 6, 119 | "name": "Time Signal", 120 | "time_specified_flag": true, 121 | "pts_time": 12345.6789 122 | }, 123 | "descriptors": [] 124 | } 125 | >>>> cue.command.show() 126 | { 127 | "command_length": 5, 128 | "command_type": 6, 129 | "name": "Time Signal", 130 | "time_specified_flag": true, 131 | "pts_time": 12345.6789 132 | } 133 | >>>> 134 | ``` 135 | * Splice Descriptors work the same way 136 | ```py3 137 | >>>> ad = AvailDescriptor() 138 | 139 | >>>> ad.encode() 140 | 141 | Traceback (most recent call last): 142 | File "", line 1, in 143 | File "/home/a/build/SCTE35_threefive/threefive/descriptors.py", line 166, in encode 144 | self._chk_var(int, nbin.add_int, "provider_avail_id", 32) 145 | File "/home/a/build/SCTE35_threefive/threefive/base.py", line 38, in _chk_var 146 | raise ValueError(err_mesg) 147 | ValueError: provider_avail_id is not set, it should be 32 bit(s) and type 148 | 149 | >>>> ad.provider_avail_id = 1234 150 | 151 | >>>> ad.encode() 152 | b'CUEI\x00\x00\x04\xd2' <--- encode returns a byte string when it works 153 | >>>> 154 | 155 | ``` 156 | * Add the AvailDescriptor to the Cue. 157 | * Cue.descriptors is a list, so append it. 158 | ```py3 159 | >>>> cue.descriptors.append(ad) 160 | >>>> cue.encode() 161 | '/DAgAAAAAAAAAP/wBQb+Qjo1vQAKAAhDVUVJAAAE0iVuWvA=' <---- Base64 string 162 | ``` 163 | * modify the AvailDescriptor and add another Descriptor 164 | ```py3 165 | >>>> ad.provider_avail_id = 5678 166 | >>>> ad.show() 167 | { 168 | "tag": 0, 169 | "descriptor_length": 8, 170 | "name": "Avail Descriptor", 171 | "identifier": "CUEI", 172 | "provider_avail_id": 5678 173 | } 174 | >>>> cue.descriptors.append(ad) 175 | >>>> len(cue.descriptors) 176 | 2 177 | ``` 178 | * You can also encode Cue to hex, or int. 179 | ```py3 180 | >>>> cue.encode2hex() 181 | '0xfc302a00000000000000fff00506fe423a35bd00140008435545490000162e0008435545490000162e7cf13378' 182 | >>>> cue.encode2int() 183 | 2313572608209932854073470233617156645467305914531507959339025453988506820473289879370790529590385607701508984 184 | ``` 185 | * Everything in a Cue works of dot notation, if you want to change PTS, change it. 186 | ```py3 187 | >>>> cue.command.pts_time 188 | 12345.6789 189 | >>>> cue.command.pts_time=999.9987 190 | >>>> cue.encode() 191 | '/DAqAAAAAAAAAP/wBQb+BV1KCwAUAAhDVUVJAAAWLgAIQ1VFSQAAFi7SdffT' 192 | ``` 193 | -------------------------------------------------------------------------------- /prog.md: -------------------------------------------------------------------------------- 1 |
Mpegts Multicast in three lines of code. 2 | 3 | ```python3 4 | import threefive 5 | 6 | strm = threefive.Stream('udp://@239.35.0.35:1234') 7 | strm.decode() 8 | ```` 9 | _(need an easy multicast server?_ [gumd](https://github.com/futzu/gumd) ) 10 | 11 | --- 12 | 13 |
14 | 15 | 16 |
Mpegts over Https in three lines of code. 17 | 18 | 19 | ```python3 20 | import threefive 21 | strm = threefive.Stream('https://iodisco.com/ch1/ready.ts') 22 | strm.decode() 23 | ``` 24 | 25 | 26 |
27 | 28 | 29 |
Base64 in five lines of code. 30 | 31 | 32 | ```python3 33 | >>> from threefive import Cue 34 | >>> stuff = '/DAvAAAAAAAA///wBQb+dGKQoAAZAhdDVUVJSAAAjn+fCAgAAAAALKChijUCAKnMZ1g=' 35 | >>> cue=Cue(stuff) 36 | >>> cue.decode() 37 | True 38 | >>> cue.show() 39 | 40 | ``` 41 | --- 42 |
43 | 44 |
Bytes in five lines of code. 45 | 46 | ```python3 47 | >>> import threefive 48 | 49 | >>> stuff = b'\xfc0\x11\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\x00\x00\x00O%3\x96' 50 | >>> cue=Cue(stuff) 51 | >>> cue.decode() 52 | True 53 | >>> cue.show() 54 | ``` 55 | --- 56 | 57 | 58 |
59 | 60 | 61 | 62 |
Hex in 4 lines of code. 63 | 64 | 65 | ```python3 66 | import threefive 67 | 68 | cue = threefive.Cue("0XFC301100000000000000FFFFFF0000004F253396") 69 | cue.decode() 70 | cue.show() 71 | ``` 72 | 73 | --- 74 | 75 |
76 | -------------------------------------------------------------------------------- /segment.md: -------------------------------------------------------------------------------- 1 | ### The threefive.Segment Class 2 | The Segment class is a sub class of threefive.Stream,
designed for processing HLS segments. 3 | 4 | 5 | * Segment Class Specific Features: 6 | * Decryption of AES Encrypted MPEGTS. 7 | * Segment.cues a list of SCTE35 cues found in the segment. 8 | --- 9 | * Usage: 10 | 11 | ```lua 12 | 13 | from threefive import Segment 14 | 15 | uri = "https://example.com/1.ts" 16 | seg = Segment(uri) 17 | seg.decode() 18 | 19 | # Make a list comprehension of cues found in the segment. 20 | data = [cue.encode() for cue in seg.cues] 21 | print(data) 22 | 23 | ['/DARAAAAAAAAAP/wAAAAAHpPv/8=', 24 | '/DAvAAAAAAAAAP/wFAUAAAKWf+//4WoauH4BTFYgAAEAAAAKAAhDVUVJAAAAAOv1oqc='] 25 | 26 | ``` 27 | 28 | * For aes encrypted files 29 | 30 | 31 | ```lua 32 | from threefive import Segment 33 | 34 | key = "https://example.com/aes.key" 35 | IV=0x998C575D24F514AEC84EDC5CABCCDB81 36 | uri = "https://example.com/aes-1.ts" 37 | 38 | seg = Segment(uri,key_uri=key, iv=IV) 39 | seg.decode() 40 | 41 | # make a dictionary comprehension of pts and base64 encoded cues 42 | 43 | data = {cue.packet_data.pts:cue.encode() for cue in seg.cues} 44 | 45 | print(data) 46 | 47 | { 89718.451333: '/DARAAAAAAAAAP/wAAAAAHpPv/8=', 48 | 89730.281789: '/DAvAAAAAAAAAP/wFAUAAAKWf+//4WoauH4BTFYgAAEAAAAKAAhDVUVJAAAAAOv1oqc='} 49 | ``` 50 | 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import setuptools 4 | 5 | with open("README.md", "r", encoding="utf-8") as fh: 6 | readme = fh.read() 7 | 8 | with open("threefive/version.py","r", encoding="utf-8") as latest: 9 | version = latest.read().split("'")[1] 10 | 11 | setuptools.setup( 12 | name="threefive", 13 | version=version, 14 | author="Adrian of Doom, and the Fine Folks at Fu Corp.", 15 | author_email="spam@iodisco.com", 16 | description="The Undisputed Heavyweight Champion of SCTE-35. The Belts have been Unified.", 17 | long_description=readme, 18 | long_description_content_type="text/markdown", 19 | url="https://github.com/futzu/threefive", 20 | install_requires=[ 21 | 'iframes >= 0.0.7', 22 | 'm3ufu >= 0.0.89', 23 | 'new_reader >= 0.1.13', 24 | 'sideways >= 0.0.23', 25 | "pyaes", 26 | ], 27 | 28 | scripts=['bin/threefive'], 29 | packages=setuptools.find_packages(), 30 | classifiers=[ 31 | "License :: OSI Approved :: MIT License", 32 | "Operating System :: OS Independent", 33 | "Programming Language :: Python :: 3.6", 34 | "Programming Language :: Python :: Implementation :: PyPy", 35 | "Programming Language :: Python :: Implementation :: CPython", 36 | ], 37 | python_requires=">=3.6", 38 | ) 39 | -------------------------------------------------------------------------------- /sidecar.md: -------------------------------------------------------------------------------- 1 | 2 | ## Sidecar Files 3 | * Sidecar files are an easy way to insert SCTE-35 into MPEGTS streams or HLS manifests. It's a simplier SCTE-104. 4 | ```js 5 | a@debian:~/x9k3$ cat sidecar.txt 6 | 7 | 38103.868589, /DAxAAAAAAAAAP/wFAUAAABdf+/+zHRtOn4Ae6DOAAAAAAAMAQpDVUVJsZ8xMjEqLYemJQ== 8 | 38199.918911, /DAsAAAAAAAAAP/wDwUAAABef0/+zPACTQAAAAAADAEKQ1VFSbGfMTIxIxGolm0= 9 | ``` 10 | * line format for sidecar file is __insert_pts, cue__ , 11 | * like `38103.868589, /DAxAAAAAAAAAP/wFAUAAABdf+/+zHRtOn4Ae6DOAAAAAAAMAQpDVUVJsZ8xMjEqLYemJQ==` 12 | 13 | * pts is the insert time for the cue, cue can be base64,hex, int, or bytes. 14 | 15 | * The __insert_pts has to be valid for the video__, meaning if your insert_pts is 38103.868589, the video PTS has to be 16 | less than 38103.868589 for the cue to be inserted. 17 | 18 | * [adbreak2](https://github.com/futzu/adbreak2) can be used to generate splice inserts for sidecar files. 19 | 20 | * Generate a "CUE-OUT" and "CUE-IN" with adbreak2 21 | ```js 22 | a@fu:~/cuei$ adbreak2 --duration 240 --event-id=1234 --pts 1234.567890 --sidecar my_sidecar.txt 23 | 24 | Writing to sidecar file: my_sidecar.txt 25 | 26 | CUE-OUT PTS:1234.567889 Id:1234 Duration: 240.0 27 | CUE-IN PTS:1474.567889 Id:1235 28 | 29 | a@fu:~/cuei$ cat my_sidecar.txt 30 | 1234.56789,/DAlAAAAAAAAAP/wFAUAAATSf+/+Bp9rxv4BSZcABNIAAAAA9RqQjw== 31 | 1474.56789,/DAgAAAAAAAAAP/wDwUAAATTf0/+B+kCxgTTAAAAALukcWw= 32 | 33 | ``` 34 | 35 | * Sidecar files can also be generated by threefive 36 | * Generate a sidecar file from an existing video 37 | ```py3 38 | threefive sidecar video.ts 39 | ``` 40 | * Will generate a sidecar file named sidecar.txt with the Cues from video.ts 41 | 42 | * Insert SCTE-35 Cues from a sidecar file into an mpegts file. 43 | * threefive inject _[input video]_ with _[sidecar file]_ at _[pid of new SCTE-35 stream]_ 44 | ```py3 45 | a@fu:~/build/SCTE35_threefive$ threefive inject video.ts with ~/sidecar.txt at 777 46 | 47 | Output File: superkabuki-video.ts 48 | PMT Section Length: 60 49 | Program Number: 1 50 | PCR PID: 256 51 | Program Info Length: 6 52 | 53 | Added Registration Descriptor: 54 | b'\x05\x04CUEI' 55 | 56 | Found Streams: 57 | Stream Type: 0x1b PID: 256 EI Len: 0 58 | Stream Type: 0xf PID: 257 EI Len: 6 59 | Stream Type: 0x86 PID: 258 EI Len: 0 60 | Stream Type: 0x15 PID: 259 EI Len: 15 61 | 62 | Added Stream: 63 | Stream Type: 0x86 PID: 777 EI Len: 0 64 | 65 | Inserted Cue: 66 | @72668.9952, /DAgAAAAAAAAAP/wDwUAAAABf//+AA27oAABAAAAANwB3tE= 67 | 68 | Inserted Cue: 69 | @72670.9972, /DAgAAAAAAAAAP/wDwUAAAABf//+AKTLgAABAAAAANaNPVc= 70 | 71 | Inserted Cue: 72 | @72672.9992, /DAgAAAAAAAAAP/wDwUAAAABf//+AFJlwAABAAAAAMOOklg= 73 | 74 | Inserted Cue: 75 | @72675.0012, /DAgAAAAAAAAAP/wDwUAAAABf//+AA27oAABAAAAANwB3tE= 76 | 77 | Inserted Cue: 78 | @72677.0032, /DAgAAAAAAAAAP/wDwUAAAABf//+AKTLgAABAAAAANaNPVc= 79 | 80 | Inserted Cue: 81 | @72679.0052, /DAgAAAAAAAAAP/wDwUAAAABf//+AFJlwAABAAAAAMOOklg= 82 | 83 | Inserted Cue: 84 | @72681.0072, /DAgAAAAAAAAAP/wDwUAAAABf//+AA27oAABAAAAANwB3tE= 85 | a@fu:~/build/SCTE35_threefive$ 86 | ``` 87 | * output filename will be __superkabuki-video.ts__ 88 | 89 | -------------------------------------------------------------------------------- /speedtest.md: -------------------------------------------------------------------------------- 1 | # `threefive + Python3` VS. `threefive + pypy3` VS. `tsduck in C++` VS. `cuei in Go.` 2 | 3 | > Q. Why only use __threefive, tsduck and cuei__? What about other SCTE-35 parsers? 4 | 5 | > A. To be honest, they are very few other parsers that will parse SCTE-35 from MPEGTS, here are three that were excluded and why. 6 | > 7 | 8 | 1. __lib BitStream from the VLC folks__. Fast parser, but incomplete SCTE-35 Data last time I used it, and it requires writing about 500 lines of code to parse SCTE-35. 9 | 2. __gstreamer__ I believe it will parse SCTE-35, but I spent a few hours trying to figure out how to do it and I have no idea how to even run gstreamer, much less parse SCTE-35 with it. 10 | 3. __Comcast GOTS__ has about 13,000 lines of code, and requires you write about 300 more to parse SCTE-35, and I'm not exactly sure how to use it. 11 | 12 | # The Contestants 13 | 14 | 1. the __threefive cli__ tool running on __python3__. 15 | 2. the __threeefive cli__ tool running on pypy3. 16 | 3. the __tsduck cli__ using the __SpliceMonitor__ plugin. 17 | 4. the __cuei library__ and __ten lines of code__ to make a cli tool. 18 | 19 | 20 | # The File 21 | ```rebol 22 | a@slow:~/cuei$ ls -alh ../v2.ts 23 | -rw-r--r-- 1 a a 4.9G Mar 8 04:03 ../v2.ts 24 | ``` 25 | * I used [SuperKabuki](https://github.com/futzu/superkabuki) to insert 14,862 SCTE-35 Time Signals 26 | --- 27 | ### Test 1: `threefive` with `Python 3.11.2` 28 | 29 | ```smalltalk 30 | a@slow:~/cuei$ time threefive ../v2.ts 2>&1 | grep sap_type | wc -l 31 | 14862 32 | 33 | real 0m8.600s 34 | user 0m8.026s 35 | sys 0m0.778s 36 | 37 | ``` 38 | 39 | * We grep 'sap_type' because it appears in every SCTE-35 Cue, and only once, so we can count the number of cues detected. 40 | * 'sap_type' is proper var naming in python. 41 | --- 42 | 43 | ### Test 2: `threefive` with `PyPy 7.3.11` 44 | 45 | ``` smalltalk 46 | a@slow:~/cuei$ time threefivepypy3 ../v2.ts 2>&1 | grep sap_type | wc -l 47 | 14862 48 | 49 | real 0m3.543s 50 | user 0m2.880s 51 | sys 0m0.846s 52 | ``` 53 | * We grep 'sap_type' because it appears in every SCTE-35 Cue, and only once, so we can count the number of cues detected. 54 | 55 | 56 | --- 57 | ### Test 3: `tsduck 3.36-3528` written in `C++` 58 | 59 | ```smalltalk 60 | a@slow:~/cuei$ time tsp -I file ../v2.ts -P splicemonitor -a -O drop | grep "Command type:" | wc -l 61 | 14862 62 | 63 | real 0m1.435s 64 | user 0m2.006s 65 | sys 0m0.861s 66 | ``` 67 | --- 68 | * We grep 'Command type' because tsduck is not up to date with the specification and doesn't have sap type yet. 'Command type' appears in every tsduck SCTE-35 Cue, and only once, so we can count the number of cues detected. 69 | 70 | 71 | ### Test 4: [`cuei v1.1.93 "Junior"`](https://github.com/futzu/cuei) written in `Go` 72 | 73 | ```smalltalk 74 | a@slow:~/cuei$ time ./cuei ../v2.ts | grep SapType | wc -l 75 | 14862 76 | 77 | real 0m1.048s 78 | user 0m0.605s 79 | sys 0m0.509s 80 | 81 | 82 | ``` 83 | * The code used for the test. 84 | ```go 85 | package main // 1 86 | 87 | import ( // 2 88 | "os" // 3 89 | "github.com/futzu/cuei" // 4 90 | ) // 5 91 | 92 | func main(){ // 6 93 | arg := os.Args[1] // 7 94 | stream := cuei.NewStream() // 8 95 | stream.Decode(arg) // 9 96 | } // 10 lines 97 | ``` 98 | * We grep 'SapType' because it appears in every SCTE-35 Cue, and only once, so we can count the nunmber of cues detected. 99 | * 'SapType' is proper Go var naming. 100 | 101 | --- 102 | 103 | # Results: 104 | 105 | | tool | Cues Detected| GB/second | 106 | |---------------------|--------------|---------------| 107 | | threefive w/ python3| 14862 | 0.5697674 | 108 | | threefive w/ pypy3 | 14862 | 1.3830087 | 109 | | tsduck | 14862 | 3.4146341 | 110 | | cuei | 14862 | __4.6755725__| 111 | 112 | 113 | 114 | # Winner: [`cuei v1.1.93 "Junior"`](https://github.com/futzu/cuei) 115 | parsing over 4.6 GigaBytes per second. I haven't even optimized cuei for speed yet, it will go even faster. 116 | 117 | ### ADDENDUM: 118 | threefive didn't win, but even at the slowest, it's parsing over 500 MegaBytes a second, high quality 4K video is about 11 MegaBytes a second. 119 | 120 | -------------------------------------------------------------------------------- /threefive-ffmpeg.md: -------------------------------------------------------------------------------- 1 | ### Two Critical things to remember when using ffmpeg with SCTE-35. 2 | 1) `-copyts` you need to keep the timestamps if you're transcoding. 3 | 2) `-muxpreload 0 -muxdelay 0` needs to be used to keep iframes and SCTE-35 aligned. 4 | 5 | # threefive works very well with ffmpeg. 6 | * __ffmpeg changes the SCTE-35 (0x86) stream type to bin data (0x6)__ 7 | * __threefive is the only SCTE35 tool that parses both SCTE-35 `(0x86)` and bin data `(0x6)` stream types__. 8 | * __the threefive cli tool can convert the stream_type back to `0x86` after ffmpeg changes it. 9 | 10 | ### Example 1 11 | --- 12 | * Transcode mpegts with a SCTE-35 stream and pipe it to threefive to parse SCTE-35 13 | * oldvid.ts looks like this: 14 | ``` 15 | Program 1 16 | Stream #0:0[0x31]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(tv, bt709, progressive), 17 | 1280x720 [SAR 1:1 DAR 16:9], Closed Captions, 59.94 fps, 59.94 tbr, 90k tbn 18 | Stream #0:1[0x32]: Audio: mp2 ([4][0][0][0] / 0x0004), 48000 Hz, stereo, s16p, 256 kb/s 19 | Stream #0:2[0x33]: Audio: mp2 ([4][0][0][0] / 0x0004), 48000 Hz, stereo, s16p, 256 kb/s 20 | Stream #0:3[0x34]: Audio: mp2 ([4][0][0][0] / 0x0004), 48000 Hz, stereo, s16p, 256 kb/s 21 | Stream #0:4[0x35]: Audio: mp2 ([4][0][0][0] / 0x0004), 48000 Hz, stereo, s16p, 256 kb/s 22 | Stream #0:5[0x36]: Data: scte_35 23 | ``` 24 | * transcode 25 | * `-copyts`, keep timestamps 26 | * `-map` keep SCTE-35 stream 27 | 28 | ```sh 29 | ffmpeg -copyts -i oldvid.ts -vcodec libx265 -map 0 -muxpreload 0 -muxdelay 0 -y newvid.ts 30 | ``` 31 | 32 | * newvid.ts looks like this 33 | ``` 34 | Program 1 35 | Stream #0:0[0x100]: Video: hevc (Main) (HEVC / 0x43564548), yuv420p(tv, bt709), 1280x720 36 | [SAR 1:1 DAR 16:9], 59.94 fps, 59.94 tbr, 90k tbn 37 | Stream #0:1[0x101]: Audio: mp2 ([3][0][0][0] / 0x0003), 48000 Hz, stereo, fltp, 384 kb/s 38 | Stream #0:2[0x102]: Audio: mp2 ([3][0][0][0] / 0x0003), 48000 Hz, stereo, fltp, 384 kb/s 39 | Stream #0:3[0x103]: Audio: mp2 ([3][0][0][0] / 0x0003), 48000 Hz, stereo, fltp, 384 kb/s 40 | Stream #0:4[0x104]: Audio: mp2 ([3][0][0][0] / 0x0003), 48000 Hz, stereo, fltp, 384 kb/s 41 | Stream #0:5[0x105]: Data: bin_data ([6][0][0][0] / 0x0006) 42 | ``` 43 | * You can parse it as is with the threefive cli tool 44 | ```js 45 | threefive newvid.ts 46 | ``` 47 | * transcode with ffmpeg and pipe into threefive cli as is 48 | ```js 49 | ffmpeg -copyts -i oldvid.ts -vcodec libx265 -map 0 -muxpreload 0 -muxdelay 0 -f mpegts -| threefive 50 | ``` 51 | * You can also change the stream type from `0x6` back to `0x86` with __threefive sixfix__ so other SCTE-35 parsers can read the SCTE-35. 52 | ```js 53 | threefive mpegts sixfix sixed.ts 54 | fixing these pids {258} 55 | Wrote: sixfixed-newvideo.ts 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /threefive/README.md: -------------------------------------------------------------------------------- 1 | ### [threefive](https://github.com/futzu/SCTE35-threefive/) 2 | 3 | * [base.py](https://github.com/futzu/SCTE35-threefive/blob/master/threefive/base.py) `threefive.SCTE35Base` class is the __Super__ class for all Splice Commands and Descriptors. 4 | * [bitn.py](https://github.com/futzu/SCTE35-threefive/blob/master/threefive/bitn.py) `threefive.BitBin` and `threefive.NBin` classes. 5 | * [commands.py](https://github.com/futzu/SCTE35-threefive/blob/master/threefive/commands.py) All `Splice Command` classes. 6 | * [crc.py](https://github.com/futzu/scte35-threefive/blob/master/threefive/crc.py) `threefive.crc32` function. 7 | * [cue.py ](https://github.com/futzu/SCTE35-threefive/blob/master/threefive/cue.py) `threefive.Cue` class. 8 | * [decode.py](https://github.com/futzu/SCTE35-threefive/blob/master/threefive/decode.py) `threefive.decode` function. 9 | * [descriptors.py](https://github.com/futzu/SCTE35-threefive/blob/master/threefive/descriptors.py) All `Splice Descriptor` classes. 10 | * [encode.py](https://github.com/futzu/threefive/blob/master/threefive/encode.py) `threefive.mk_splice_null`, `threefive.mk_splice_insert`, and `threefive.mk_time_signal` functions. 11 | * [hls.py](https://github.com/futzu/threefive/blob/master/threefive/hls.py) `HLS SCTE-35 Parser classes and cli()` function. Replaces showcues. 12 | * [packetdata.py](https://github.com/futzu/threefive/blob/master/threefive/packetdata.py) `threefive.PacketData` class. 13 | * [section.py](https://github.com/futzu/SCTE35-threefive/blob/master/threefive/section.py) `threefive.SpliceInfoSection` class. 14 | * [segment.py](https://github.com/futzu/SCTE35-threefive/blob/master/threefive/segment.py) `threefive.Segment` class. 15 | * [segmentation.py](https://github.com/futzu/SCTE35-threefive/blob/master/threefive/segmentation.py) `lookup tables` for the `threefive.SegmentationDescriptor` class. 16 | * [showcues.py](https://github.com/futzu/threefive/blob/master/threefive/showcues.py) `showcues HLS SCTE-35 Parser classes and cli()` function. Deprecated in favor of hls.py. 17 | 18 | * [sixfix.py](https://github.com/futzu/threefive/blob/master/threefive/sixfix.py) `threefive.sixfix ` function. 19 | * [smoketest.py](https://github.com/futzu/threefive/blob/master/threefive/smoketest.py) `threefive.smoke` function. 20 | 21 | * [stream.py](https://github.com/futzu/SCTE35-threefive/blob/master/threefive/stream.py) `threefive.Stream` class. 22 | * [streamtypes.py](https://github.com/futzu/SCTE35-threefive/blob/master/threefive/streamtypes.py) `threefive.streamtypes.streamtype_map` is a dict of mpegts stream types. 23 | 24 | * [stuff.py](https://github.com/futzu/SCTE35-threefive/blob/master/threefive/stuff.py) `print2` function. 25 | * [superkabuki.py](https://github.com/futzu/threefive/blob/master/threefive/superkabuki.py) `threefive.SuperKabuki` class. 26 | 27 | * [upids.py](https://github.com/futzu/SCTE35-threefive/blob/master/threefive/upids.py) `Segmentation Upids` classes. 28 | * [version.py](https://github.com/futzu/SCTE35-threefive/blob/master/threefive/version.py) `threefive.version` 29 | * [xml.py](https://github.com/futzu/SCTE35-threefive/blob/master/threefive/xml.py) `threefive.xml.Node` and, `threefive.xml.XmlParser` classes and several xml related functions. 30 | 31 | 32 | -------------------------------------------------------------------------------- /threefive/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | threefive.__init__.py 3 | """ 4 | 5 | from .stuff import print2 6 | from .cue import Cue 7 | from .decode import decode 8 | from .section import SpliceInfoSection 9 | from .segment import Segment 10 | from .smoketest import smoke 11 | from .stream import Stream 12 | from .version import version 13 | 14 | from .commands import ( 15 | TimeSignal, 16 | SpliceInsert, 17 | SpliceNull, 18 | PrivateCommand, 19 | BandwidthReservation, 20 | ) 21 | 22 | from .descriptors import ( 23 | AvailDescriptor, 24 | DVBDASDescriptor, 25 | DtmfDescriptor, 26 | SegmentationDescriptor, 27 | SpliceDescriptor, 28 | TimeDescriptor, 29 | ) 30 | 31 | from .encode import ( 32 | mk_splice_null, 33 | mk_splice_insert, 34 | mk_time_signal, 35 | ) 36 | -------------------------------------------------------------------------------- /threefive/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | threefive.base contains 3 | the class SCTE35Base. 4 | """ 5 | 6 | import json 7 | from .bitn import NBin 8 | from .stuff import print2 9 | from .xml import Node 10 | 11 | 12 | class SCTE35Base: 13 | """ 14 | SCTE35Base is a base class for 15 | SpliceCommand and SpliceDescriptor classes 16 | """ 17 | 18 | ROLLOVER = 8589934591 19 | 20 | def __repr__(self): 21 | return str(self.__dict__) 22 | 23 | @staticmethod 24 | def _chk_nbin(nbin): 25 | if not nbin: 26 | nbin = NBin() 27 | return nbin 28 | 29 | def _chk_var(self, var_type, nbin_method, var_name, bit_count): 30 | """ 31 | _chk_var is used to check var values and types before encoding 32 | """ 33 | var_value = self.__dict__[var_name] 34 | if var_value is None: 35 | err_mesg = f"\033[7m{var_name} is not set, it should be {bit_count} bit(s) and type {var_type}\033[27m" 36 | raise ValueError(err_mesg) 37 | if not isinstance(var_value, var_type): 38 | err_mesg = f' \033[7m{var_name} is "{var_value}", it should be type {var_type}\033[27m and {bit_count} bit(s),\n ' 39 | raise ValueError(err_mesg) 40 | nbin_method(var_value, bit_count) 41 | 42 | @staticmethod 43 | def as_90k(int_time): 44 | """ 45 | ticks to 90k timestamps 46 | """ 47 | return round((int_time / 90000.0), 6) 48 | 49 | @staticmethod 50 | def as_ticks(float_time): 51 | """ 52 | 90k timestamps to ticks 53 | """ 54 | return int(round(float_time * 90000)) 55 | 56 | @staticmethod 57 | def as_hms(secs_of_time): 58 | """ 59 | as_hms converts timestamp to 60 | 00:00:00.000 format 61 | """ 62 | hours, seconds = divmod(secs_of_time, 3600) 63 | mins, seconds = divmod(seconds, 60) 64 | seconds = round(seconds, 3) 65 | output = f"{int(hours):02}:{int(mins):02}:{seconds:02}" 66 | if len(output.split(".")[1]) < 2: 67 | output += "0" 68 | return output 69 | 70 | @staticmethod 71 | def fix_hex(hexed): 72 | """ 73 | fix_hex adds padded zero if needed for byte conversion. 74 | """ 75 | return (hexed.replace("0x", "0x0", 1), hexed)[len(hexed) % 2 == 0] 76 | 77 | def get(self): 78 | """ 79 | Returns instance as a dict 80 | """ 81 | return self.kv_clean() 82 | 83 | def get_json(self): 84 | """ 85 | get_json returns the instance 86 | data as json. 87 | """ 88 | return json.dumps(self.get(), indent=4) 89 | 90 | def json(self): 91 | """ 92 | json returns self as kv_clean'ed json 93 | """ 94 | return self.get_json() 95 | 96 | def show(self): 97 | """ 98 | show prints self as json to stderr (2) 99 | """ 100 | print2(self.get_json()) 101 | 102 | def kv_clean(self): 103 | """ 104 | kv_clean removes items from a dict if the value is None 105 | """ 106 | 107 | def b2l(val): 108 | if isinstance(val, (SCTE35Base)): 109 | val.kv_clean() 110 | if isinstance(val, (list)): 111 | val = [b2l(v) for v in val] 112 | if isinstance(val, (dict)): 113 | val = {k: b2l(v) for k, v in val.items()} 114 | if isinstance(val, (bytes, bytearray)): 115 | val = list(val) 116 | return val 117 | 118 | return {k: b2l(v) for k, v in vars(self).items() if v is not None} 119 | 120 | def has(self, what): 121 | """ 122 | has runs hasattr with self and what 123 | """ 124 | if hasattr(self, what): 125 | if vars(self)[what]: 126 | return True 127 | return False 128 | 129 | def xml(self, ns="scte35"): 130 | """ 131 | xml default xml method will return 132 | all attributes, which is expressly allowed 133 | for SCTE-35 xml. 134 | """ 135 | xml_attrs = self.kv_clean() 136 | iam = type(self) 137 | iam = str(iam).split("'", 1)[1].split("'", 1)[0] 138 | this = Node(iam, attrs=xml_attrs, ns=ns) 139 | return this 140 | 141 | @staticmethod 142 | def idxsplit(stuff, sep): 143 | """ 144 | idxsplit is like split but you keep 145 | the sep 146 | example: 147 | >>> idxsplit('123456789',4) 148 | >>>'456789' 149 | 150 | """ 151 | if sep in stuff: 152 | return stuff[stuff.index(sep) :] 153 | return False 154 | 155 | def load(self, stuff): 156 | """ 157 | load is used to load 158 | data from a dict or json string 159 | """ 160 | if isinstance(stuff, str): 161 | stuff = json.loads(stuff) 162 | if isinstance(stuff, dict): 163 | prevars= vars(self) 164 | for k, v in stuff.items(): 165 | if k in prevars: 166 | self.__dict__[k] = v 167 | #self.__dict__.update(stuff) 168 | -------------------------------------------------------------------------------- /threefive/bitn.py: -------------------------------------------------------------------------------- 1 | """ 2 | The bitn.BitBin and bitn.NBin classes 3 | """ 4 | 5 | from .stuff import print2 6 | 7 | 8 | class BitBin: 9 | """ 10 | bitn.Bitbin takes a byte string and 11 | converts it to a integer, a very large integer 12 | if needed. A 1500 bit integer is no problem. 13 | several methods are available for slicing off bits. 14 | """ 15 | 16 | def __init__(self, bites): 17 | # self.bites = bites 18 | self.bitsize = self.idx = len(bites) << 3 19 | self.bits = int.from_bytes(bites, byteorder="big") 20 | 21 | def as_90k(self, num_bits): 22 | """ 23 | Returns num_bits 24 | of bits as 90k time 25 | """ 26 | ninetyk = self.as_int(num_bits) / 90000.0 27 | return round(ninetyk, 6) 28 | 29 | def as_int(self, num_bits): 30 | """ 31 | Starting at self.idx of self.bits, 32 | slice off num_bits of bits. 33 | """ 34 | if self.idx >= num_bits: 35 | self.idx -= num_bits 36 | return (self.bits >> (self.idx)) & ~(~0 << num_bits) 37 | return False 38 | 39 | def as_hex(self, num_bits): 40 | """ 41 | Returns the hex value 42 | of num_bits of bits 43 | """ 44 | hexed = hex(self.as_int(num_bits)) 45 | return (hexed.replace("0x", "0x0", 1), hexed)[len(hexed) % 2 == 0] 46 | 47 | def as_charset(self, num_bits, charset="ascii"): 48 | """ 49 | Returns num_bits of bits 50 | as bytes decoded as charset 51 | default charset is ascii. 52 | """ 53 | # print(charset) 54 | stuff = self.as_int(num_bits) 55 | wide = num_bits >> 3 56 | if charset is None: 57 | return int.to_bytes(stuff, wide, byteorder="big") 58 | return int.to_bytes(stuff, wide, byteorder="big").decode( 59 | charset, errors="replace" 60 | ) 61 | 62 | def as_bytes(self, num_bits): 63 | """ 64 | Returns num_bits of bits 65 | as bytes 66 | """ 67 | stuff = self.as_int(num_bits) 68 | wide = num_bits >> 3 69 | return int.to_bytes(stuff, wide, byteorder="big") 70 | 71 | def as_flag(self, num_bits=1): 72 | """ 73 | Returns one bit as True or False 74 | """ 75 | return self.as_int(num_bits) & 1 == 1 76 | 77 | def forward(self, num_bits): 78 | """ 79 | Advances the start point 80 | forward by num_bits 81 | """ 82 | self.idx -= num_bits 83 | 84 | def negative_shift(self, num_bits): 85 | """ 86 | negative_shift is called instead of 87 | throwing a negative shift count error. 88 | """ 89 | print2(f"{num_bits} bits requested, but only {self.idx} bits left.") 90 | print2(f"\n bytes remaining: {self.as_bytes(self.idx)} ") 91 | 92 | 93 | class NBin: 94 | """ 95 | bitn.NBin is 96 | the reverse BitBin. 97 | Encodes data to integers 98 | and then bytes 99 | """ 100 | 101 | def __init__(self): 102 | self.nbits = 0 103 | self.idx = 0 104 | self.bites = b"" 105 | 106 | def nbits2bites(self): 107 | """ 108 | nbits2bites converts 109 | the int self.nbits to bytes as self.bites 110 | and sets self.nbits and self.idx to 0 111 | """ 112 | bites_wide = self.idx >> 3 113 | self.bites += int.to_bytes(self.nbits, bites_wide, byteorder="big") 114 | self.nbits = 0 115 | self.idx = 0 116 | 117 | def add_bites(self, plus_bites): 118 | """ 119 | add_bites appends plus_bites 120 | to self.bites 121 | """ 122 | if isinstance(plus_bites, int): 123 | plus_bites = bytes.fromhex(hex(plus_bites)[2:]) 124 | self.bites += plus_bites 125 | 126 | # if self.idx % 8 == 0: 127 | # self.nbits2bites() 128 | 129 | def add_int(self, int_bits, bit_len): 130 | """ 131 | left shift nbits and append new_bits 132 | """ 133 | self.idx += bit_len 134 | self.nbits = (self.nbits << bit_len) | int_bits 135 | if self.idx % 8 == 0: 136 | self.nbits2bites() 137 | 138 | def add_90k(self, pts, bit_len=33): 139 | """ 140 | Converts 90k float timestamps 141 | to an int and appends it to nbits 142 | via self.add_int 143 | """ 144 | ninetyk = int(pts * 90000.0) 145 | self.add_int(ninetyk, bit_len) 146 | 147 | def add_hex(self, hex_str, bit_len): 148 | """ 149 | add_hex converts a 150 | hex encoded string to an int 151 | and appends it to self.nbits 152 | via self.add_int 153 | """ 154 | if isinstance(hex_str, str): 155 | dehexed = int(hex_str, 16) 156 | # just in case hex_str is an int.... 157 | else: 158 | dehexed = hex_str 159 | self.add_int(dehexed, bit_len) 160 | 161 | def add_flag(self, flg, bit_len=1): 162 | """ 163 | add_flag takes a boolean 164 | value and adds it as an integer 165 | to self.nbits via self.add_int 166 | """ 167 | bit_len = 1 168 | self.add_int(flg.real, bit_len) 169 | 170 | def reserve(self, num): 171 | """ 172 | reserve sets 'num' bits to 1 173 | and appends them to self.nbits 174 | via self.add_int 175 | """ 176 | bit_len = 1 177 | while num: 178 | self.add_int(1, bit_len) 179 | num -= 1 180 | 181 | def forward(self, num): 182 | """ 183 | Currently just an alias to reserve 184 | """ 185 | self.reserve(num) 186 | 187 | def zeroed(self, num): 188 | """ 189 | zeroed sets num bits to zero 190 | """ 191 | bit_len = 1 192 | while num: 193 | self.add_int(0, bit_len) 194 | -------------------------------------------------------------------------------- /threefive/crc.py: -------------------------------------------------------------------------------- 1 | """ 2 | crc.py crc32 function for encoding. 3 | """ 4 | 5 | POLY = 0x104C11DB7 6 | INIT_VALUE = 0xFFFFFFFF 7 | ZERO = 0x0 8 | ONE = 0x1 9 | EIGHT = 0x8 10 | TWENTY_FOUR = 0x18 11 | THIRTY_TWO = 0x20 12 | TWO_FIFTY_FIVE = 0xFF 13 | TWO_FIFTY_SIX = 0x100 14 | 15 | 16 | def _bytecrc(crc, poly): 17 | mask = ONE << (THIRTY_TWO - ONE) 18 | i = EIGHT 19 | while i: 20 | crc = (crc << ONE, crc << ONE ^ poly)[crc & mask != ZERO] 21 | i -= ONE 22 | return crc & INIT_VALUE 23 | 24 | 25 | def _mk_table(): 26 | mask = (ONE << THIRTY_TWO) - ONE 27 | poly = POLY & mask 28 | return [_bytecrc((i << TWENTY_FOUR), poly) for i in range(TWO_FIFTY_SIX)] 29 | 30 | 31 | def crc32(data): 32 | """ 33 | generate a 32 bit crc 34 | """ 35 | 36 | table = _mk_table() 37 | crc = INIT_VALUE 38 | for bite in data: 39 | crc = table[bite ^ ((crc >> TWENTY_FOUR) & TWO_FIFTY_FIVE)] ^ ( 40 | (crc << EIGHT) & (INIT_VALUE - TWO_FIFTY_FIVE) 41 | ) 42 | return crc 43 | 44 | 45 | def crc32hex(data): 46 | """ 47 | crc32hex crc32 returned as hex 48 | """ 49 | return hex(crc32(data)) 50 | -------------------------------------------------------------------------------- /threefive/decode.py: -------------------------------------------------------------------------------- 1 | """ 2 | decode.py 3 | 4 | decode is a SCTE-35 decoder function 5 | with input type auto-detection. 6 | 7 | SCTE-35 data can be parsed with just 8 | one function call. 9 | 10 | the arg stuff is the input. 11 | if stuff is not set, decode will attempt 12 | to read mpegts video from sys.stdin.buffer. 13 | 14 | SCTE-35 data is printed in JSON format. 15 | 16 | For more parsing and output control, 17 | see the Cue and Stream classes. 18 | 19 | """ 20 | 21 | import sys 22 | 23 | from .cue import Cue 24 | from .stream import Stream 25 | 26 | 27 | def _read_stuff(stuff): 28 | try: 29 | # Mpegts Video 30 | strm = Stream(stuff) 31 | strm.decode() 32 | return True 33 | except: 34 | try: 35 | cue = Cue(stuff) 36 | cue.decode() 37 | cue.show() 38 | return True 39 | except: 40 | return False 41 | 42 | 43 | def decode(stuff=None): 44 | """ 45 | decode is a SCTE-35 decoder function 46 | with input type auto-detection. 47 | 48 | SCTE-35 data is printed in JSON format. 49 | 50 | Use like: 51 | 52 | # Base64 53 | stuff = '/DAvAAAAAAAA///wBQb+dGKQoAAZAhdDVUVJSAAAjn+fCAgAAAAALKChijUCAKnMZ1g=' 54 | threefive.decode(stuff) 55 | 56 | # Bytes 57 | payload = b"\xfc0\x11\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\x00\x00\x00O%3\x96" 58 | threefive.decode(payload) 59 | 60 | # Hex String 61 | stuff = '0XFC301100000000000000FFFFFF0000004F253396' 62 | threefive.decode(stuff) 63 | 64 | # Hex Literal 65 | threefive.decode(0XFC301100000000000000FFFFFF0000004F253396) 66 | 67 | # Integer 68 | big_int = 1439737590925997869941740173214217318917816529814 69 | threefive.decode(big_int) 70 | 71 | # Mpegts File 72 | threefive.decode('/path/to/mpegts') 73 | 74 | # Mpegts HTTP/HTTPS Streams 75 | threefive.decode('https://futzu.com/xaa.ts') 76 | 77 | """ 78 | if stuff in [None, sys.stdin.buffer]: 79 | # Mpegts stream or file piped in 80 | return Stream(sys.stdin.buffer).decode() 81 | if isinstance(stuff, int): 82 | return _read_stuff(hex(stuff)) 83 | return _read_stuff(stuff) 84 | -------------------------------------------------------------------------------- /threefive/encode.py: -------------------------------------------------------------------------------- 1 | """ 2 | encode.py 3 | 4 | threefive.encode has helper functions for Cue encoding. 5 | 6 | """ 7 | 8 | from .commands import SpliceNull, SpliceInsert, TimeSignal 9 | from .cue import Cue 10 | 11 | 12 | def mk_splice_null(): 13 | """ 14 | mk_splice_null returns a Cue 15 | with a Splice Null 16 | """ 17 | cue = Cue() 18 | sn = SpliceNull() 19 | cue.command = sn 20 | cue.encode() 21 | return cue 22 | 23 | 24 | def mk_time_signal(pts=None): 25 | """ 26 | mk_time_signal returns a Cue 27 | with a Time Signal 28 | 29 | if pts is None: 30 | time_specified_flag False 31 | 32 | if pts IS set: 33 | time_specified_flag True 34 | pts_time pts 35 | 36 | """ 37 | cue = Cue() 38 | ts = TimeSignal() 39 | ts.time_specified_flag = False 40 | if pts: 41 | pts = float(pts) 42 | ts.time_specified_flag = True 43 | ts.pts_time = pts 44 | cue.command = ts 45 | cue.encode() 46 | return cue 47 | 48 | 49 | def mk_splice_insert(event_id, pts=None, duration=None, out=False): 50 | """ 51 | mk_splice_insert returns a Cue with a Splice Insert. 52 | 53 | The args set the SpliceInsert vars. 54 | 55 | splice_event_id = event_id 56 | 57 | if pts is None (default): 58 | splice_immediate_flag True 59 | time_specified_flag False 60 | 61 | if pts: 62 | splice_immediate_flag False 63 | time_specified_flag True 64 | pts_time pts 65 | 66 | If duration is None (default) 67 | duration_flag False 68 | 69 | if duration IS set: 70 | out_of_network_indicator True 71 | duration_flag True 72 | break_auto_return True 73 | break_duration duration 74 | pts_time pts 75 | 76 | if out is True: 77 | out_of_network_indicator True 78 | 79 | if out is False (default): 80 | out_of_network_indicator False 81 | 82 | """ 83 | cue = Cue() 84 | # default is a CUE-IN 85 | sin = SpliceInsert() 86 | sin.splice_event_id = event_id 87 | sin.splice_event_cancel_indicator = False 88 | sin.out_of_network_indicator = out 89 | sin.duration_flag = False 90 | sin.unique_program_id = event_id 91 | sin.avail_num = 0 92 | sin.avails_expected = 0 93 | sin.splice_immediate_flag = True 94 | sin.time_specified_flag = False 95 | sin.program_splice_flag = True 96 | sin.event_id_compliance_flag = True 97 | # pts = None for Splice Immediate 98 | if pts: 99 | sin.splice_immediate_flag = False 100 | sin.time_specified_flag = True 101 | sin.pts_time = float(pts) 102 | else: 103 | sin.component_count = 0 104 | # If we have a duration, set duration 105 | if duration: 106 | duration = float(duration) 107 | sin.break_duration = duration 108 | sin.break_auto_return = True 109 | sin.duration_flag = True 110 | sin.out_of_network_indicator = True 111 | # Add SpliceInsert to the SCTE35 cue 112 | cue.command = sin 113 | cue.encode() 114 | return cue 115 | -------------------------------------------------------------------------------- /threefive/packetdata.py: -------------------------------------------------------------------------------- 1 | """ 2 | packetdata.py 3 | """ 4 | 5 | from .base import SCTE35Base 6 | 7 | 8 | class PacketData(SCTE35Base): 9 | """ 10 | A PacketData instance is used 11 | to hold packet pid, program, pcr and pts 12 | for SCTE-35 packets. 13 | """ 14 | 15 | def __init__(self, pid, prgm): 16 | self.pid = hex(pid) 17 | self.program = prgm 18 | self.pcr = None 19 | self.pts = None 20 | 21 | def mk_pcr(self, table): 22 | """ 23 | calculates and formats pcr 24 | """ 25 | try: 26 | pcr_ticks = table[self.program] 27 | self.pcr = self.as_90k(pcr_ticks) 28 | except: 29 | pass 30 | 31 | def mk_pts(self, table): 32 | """ 33 | mk_pts calculates and formats pts 34 | """ 35 | try: 36 | pts_ticks = table[self.program] 37 | self.pts = self.as_90k(pts_ticks) 38 | except: 39 | pass 40 | -------------------------------------------------------------------------------- /threefive/section.py: -------------------------------------------------------------------------------- 1 | """ 2 | section.py 3 | 4 | SCTE35 Splice Info Section 5 | """ 6 | 7 | from .bitn import BitBin 8 | from .base import SCTE35Base 9 | from .xml import Node 10 | 11 | 12 | sap_map = { 13 | "0x00": "Type 1 Closed GOP with no leading pictures", 14 | "0x01": "Type 2 Closed GOP with leading pictures", 15 | "0x02": "Type 3 Open GOP", 16 | "0x03": "No Sap Type", 17 | } 18 | 19 | 20 | class SpliceInfoSection(SCTE35Base): 21 | """ 22 | The SCTE-35 splice info section 23 | data. 24 | """ 25 | 26 | def __init__(self): 27 | self.table_id = None 28 | self.section_syntax_indicator = None 29 | self.private = None 30 | # sap_type used to be marked reserved. 31 | self.sap_type = None 32 | self.sap_details = None 33 | self.section_length = None 34 | self.protocol_version = None 35 | self.encrypted_packet = None 36 | self.encryption_algorithm = None 37 | self.pts_adjustment = 0 38 | self.cw_index = None 39 | self.tier = None 40 | self.splice_command_length = None 41 | self.splice_command_type = None 42 | self.descriptor_loop_length = 0 43 | self.crc = None 44 | 45 | def decode(self, bites): 46 | """ 47 | InfoSection.decode 48 | """ 49 | bitbin = BitBin(bites) 50 | self.table_id = bitbin.as_hex(8) 51 | if self.table_id != "0xfc": 52 | raise ValueError( 53 | ("splice_info_section.table_id should be 0xfc Not: ", self.table_id) 54 | ) 55 | self.section_syntax_indicator = bitbin.as_flag(1) 56 | self.private = bitbin.as_flag(1) 57 | self.sap_type = bitbin.as_hex(2) 58 | self.sap_details = sap_map[self.sap_type] 59 | self.section_length = bitbin.as_int(12) 60 | self.protocol_version = bitbin.as_int(8) 61 | self.encrypted_packet = bitbin.as_flag(1) 62 | self.encryption_algorithm = bitbin.as_int(6) 63 | pts_adjustment_ticks = bitbin.as_int(33) 64 | self.pts_adjustment = self.as_90k(pts_adjustment_ticks) 65 | self.cw_index = bitbin.as_hex(8) 66 | self.tier = bitbin.as_hex(12) 67 | self.splice_command_length = bitbin.as_int(12) 68 | self.splice_command_type = bitbin.as_int(8) 69 | 70 | def _encode_table_id(self, nbin): 71 | """ 72 | encode SpliceInfoSection.table_id 73 | """ 74 | self.table_id = "0xfc" 75 | nbin.add_hex(self.table_id, 8) 76 | 77 | def _encode_section_syntax_indicator(self, nbin): 78 | """ 79 | encode SpliceInfoSection.section_syntax_indicator 80 | """ 81 | self.section_syntax_indicator = False 82 | nbin.add_flag(self.section_syntax_indicator, 1) 83 | 84 | def _encode_private_flag(self, nbin): 85 | """ 86 | encode SpliceInfoSection.private 87 | """ 88 | self.private = False 89 | nbin.add_flag(self.private, 1) 90 | 91 | def _encode_sap(self, nbin): 92 | """ 93 | the 3 reserved bits now map SAP 94 | """ 95 | if self.sap_type not in sap_map: 96 | self.sap_type = "0x03" 97 | self.sap_details = sap_map[self.sap_type] 98 | nbin.add_hex(self.sap_type, 2) 99 | 100 | def _encode_section_length(self, nbin): 101 | """ 102 | encode SpliceInfoSection.section_length 103 | """ 104 | if not self.section_length: 105 | self.section_length = 11 106 | nbin.add_int(self.section_length, 12) 107 | 108 | def _encode_protocol_version(self, nbin): 109 | """ 110 | encode SpliceInfoSection.protocol_version 111 | """ 112 | if not self.protocol_version: 113 | self.protocol_version = 0 114 | nbin.add_int(self.protocol_version, 8) 115 | 116 | def _encode_encrypted(self, nbin): 117 | """ 118 | encode SpliceInfoSection.encrypted_packet 119 | and SpliceInfoSection.encyption_algorithm 120 | """ 121 | if not self.encrypted_packet: 122 | self.encrypted_packet = False 123 | nbin.add_flag(self.encrypted_packet) 124 | if not self.encryption_algorithm: 125 | self.encryption_algorithm = 0 126 | nbin.add_int(self.encryption_algorithm, 6) 127 | 128 | def _encode_pts_adjustment(self, nbin): 129 | """ 130 | encode SpliceInfoSection.pts_adjustment_ticks 131 | """ 132 | nbin.add_int(self.as_ticks(self.pts_adjustment), 33) 133 | 134 | def _encode_cw_index(self, nbin): 135 | """ 136 | encode SpliceInfoSection.cw_index 137 | """ 138 | if not self.cw_index: 139 | self.cw_index = "0x0" 140 | nbin.add_hex(self.cw_index, 8) 141 | 142 | def _encode_tier(self, nbin): 143 | """ 144 | encode SpliceInfoSection.tier 145 | """ 146 | if not self.tier: 147 | self.tier = "0xfff" 148 | nbin.add_hex(self.tier, 12) 149 | 150 | def _encode_splice_command(self, nbin): 151 | """ 152 | encode Splice.InfoSection.splice_command_length 153 | and Splice.InfoSection.splice_command_type 154 | """ 155 | if not self.splice_command_length: 156 | self.splice_command_length = 0 157 | nbin.add_int(self.splice_command_length, 12) 158 | if not self.splice_command_type: 159 | self.splice_command_type = 0 160 | nbin.add_int(self.splice_command_type, 8) 161 | 162 | def encode(self, nbin=None): 163 | """ 164 | SpliceInfoSection.encode 165 | takes the vars from an instance and 166 | encodes them as bytes. 167 | """ 168 | nbin = self._chk_nbin(nbin) 169 | self._encode_table_id(nbin) 170 | self._encode_section_syntax_indicator(nbin) 171 | self._encode_private_flag(nbin) 172 | self._encode_sap(nbin) 173 | self._encode_section_length(nbin) 174 | self._encode_protocol_version(nbin) 175 | self._encode_encrypted(nbin) 176 | self._encode_pts_adjustment(nbin) 177 | self._encode_cw_index(nbin) 178 | self._encode_tier(nbin) 179 | self._encode_splice_command(nbin) 180 | return nbin.bites 181 | 182 | def xml(self, ns="scte35"): 183 | """ 184 | xml create xml node for splice info section 185 | """ 186 | sis_attrs = { 187 | "xmlns": "https://scte.org/schemas/35", 188 | "pts_adjustment": self.as_ticks(self.pts_adjustment), 189 | "protocol_version": self.protocol_version, 190 | "sap_type": self.sap_type, 191 | "tier": self.tier, 192 | } 193 | sis = Node("SpliceInfoSection", attrs=sis_attrs, ns=ns) 194 | return sis 195 | 196 | def from_xml(self, stuff): 197 | """ 198 | from_xml SpliceInfoSection from Xml 199 | """ 200 | short = stuff["SpliceInfoSection"] 201 | self.load(short) 202 | -------------------------------------------------------------------------------- /threefive/segment.py: -------------------------------------------------------------------------------- 1 | """ 2 | The threefive.Segment class 3 | """ 4 | 5 | import os 6 | from new_reader import reader 7 | from .stream import Stream 8 | 9 | AES = True 10 | try: 11 | import pyaes 12 | except: 13 | AES = False 14 | 15 | 16 | class Segment(Stream): 17 | """ 18 | The Segment class is a Sub Class of threefive.Stream 19 | made for small, fixed size MPEGTS files,like HLS segments. 20 | 21 | Segment Class Specific Features: 22 | 23 | * Decryption of AES Encrypted MPEGTS. 24 | 25 | * Segment.cues a list of SCTE35 cues found in the segment. 26 | 27 | 28 | Example: 29 | 30 | from threefive import Segment 31 | 32 | >>>> uri = "https://example.com/1.ts" 33 | >>>> seg = Segment(uri) 34 | >>>> seg.decode() 35 | >>>> [cue.encode() for cue in seg.cues] 36 | ['/DARAAAAAAAAAP/wAAAAAHpPv/8=', 37 | '/DAvAAAAAAAAAP/wFAUAAAKWf+//4WoauH4BTFYgAAEAAAAKAAhDVUVJAAAAAOv1oqc='] 38 | 39 | 40 | AES Encryption Example: 41 | 42 | from threefive import Segment 43 | 44 | >>>> key = "https://example.com/aes.key" 45 | >>>> IV=0x998C575D24F514AEC84EDC5CABCCDB81 46 | >>>> uri = "https://example.com/aes-1.ts" 47 | 48 | >>>> seg = Segment(uri,key_uri=key, iv=IV) 49 | >>>> seg.decode() 50 | >>>> {cue.packet_data.pts:cue.encode() for cue in seg.cues} 51 | 52 | { 89718.451333: '/DARAAAAAAAAAP/wAAAAAHpPv/8=', 53 | 89730.281789: '/DAvAAAAAAAAAP/wFAUAAAKWf+//4WoauH4BTFYgAAEAAAAKAAhDVUVJAAAAAOv1oqc='} 54 | 55 | """ 56 | 57 | def __init__(self, seg_uri, key_uri=None, iv=None): 58 | self.seg_uri = seg_uri 59 | self.key_uri = key_uri 60 | self.key = None 61 | self.iv = None 62 | self.cues = [] 63 | self.pts_start = None 64 | self.pts_last = None 65 | self.shush = False 66 | self.tmp = None 67 | self.duration = None 68 | if AES: 69 | if iv: 70 | iv = iv.replace("\n", "") 71 | self.iv = int.to_bytes(int(iv, base=16), 16, byteorder="big") 72 | if self.key_uri: 73 | self._aes_get_key() 74 | self._aes_decrypt() 75 | super().__init__(self.seg_uri) 76 | 77 | def __repr__(self): 78 | return str(self.__dict__) 79 | 80 | def _mk_tmp(self): 81 | self.tmp = "tf-" 82 | self.tmp += self.seg_uri.rsplit("/", 1)[-1] 83 | 84 | def _aes_get_key(self): 85 | with reader(self.key_uri) as quay: 86 | self.key = quay.read() 87 | 88 | def _aes_decrypt(self): 89 | mode = pyaes.AESModeOfOperationCBC(self.key, iv=self.iv) 90 | self._mk_tmp() 91 | with open(self.tmp, "wb") as outfile, reader(self.seg_uri) as infile: 92 | pyaes.decrypt_stream(mode, infile, outfile) 93 | self.seg_uri = self.tmp 94 | 95 | def _add_cue(self, cue): 96 | """ 97 | _add_cue is called by a segment instance 98 | to collect SCTE35 cues. 99 | """ 100 | self.cues.append(cue) 101 | 102 | def shushed(self): 103 | """ 104 | shushed sets self.shush to true to suppress 105 | printing SCTE-35 Cue data. 106 | """ 107 | self.shush = True 108 | 109 | def show_cue(self, cue): 110 | """ 111 | show_cue prints SCTE35 Cue data 112 | and calls add_cue to append the cue to 113 | the Segment,cues list. 114 | """ 115 | if not self.shush: 116 | cue.show() 117 | self._add_cue(cue) 118 | 119 | def decode(self, func=None): 120 | """ 121 | decode a mpegts segment. 122 | """ 123 | 124 | super().decode_fu(func=self.show_cue) 125 | try: 126 | self.pts_start = self.as_90k(self.start.popitem()[1]) 127 | except: 128 | pass 129 | try: 130 | self.pts_last = self.as_90k(list(self.maps.prgm_pts.items())[0][1]) 131 | except: 132 | pass 133 | if self.tmp: 134 | os.unlink(self.tmp) 135 | if self.pts_start and self.pts_last: 136 | self.duration = round(self.pts_last - self.pts_start, 6) 137 | -------------------------------------------------------------------------------- /threefive/segmentation.py: -------------------------------------------------------------------------------- 1 | """ 2 | segmentation.py 3 | 4 | SCTE35 Segmentation Descriptor tables. 5 | """ 6 | 7 | table20 = { 8 | 0x00: "Restrict Group 0", 9 | 0x01: "Restrict Group 1", 10 | 0x02: "Restrict Group 2", 11 | 0x03: "No Restrictions", 12 | } 13 | 14 | 15 | table22 = { 16 | 0x00: "Not Indicated", 17 | 0x01: "Content Identification", 18 | 0x02: "Call Ad Server", # Addressable tv stuff 19 | 0x10: "Program Start", 20 | 0x11: "Program End", 21 | 0x12: "Program Early Termination", 22 | 0x13: "Program Breakaway", 23 | 0x14: "Program Resumption", 24 | 0x15: "Program Runover Planned", 25 | 0x16: "Program RunoverUnplanned", 26 | 0x17: "Program Overlap Start", 27 | 0x18: "Program Blackout Override", 28 | 0x19: "Program Start ??? In Progress", 29 | 0x20: "Chapter Start", 30 | 0x21: "Chapter End", 31 | 0x22: "Break Start", 32 | 0x23: "Break End", 33 | 0x30: "Provider Advertisement Start", 34 | 0x31: "Provider Advertisement End", 35 | 0x32: "Distributor Advertisement Start", 36 | 0x33: "Distributor Advertisement End", 37 | 0x34: "Provider Placement Opportunity Start", 38 | 0x35: "Provider Placement Opportunity End", 39 | 0x36: "Distributor Placement Opportunity Start", 40 | 0x37: "Distributor Placement Opportunity End", 41 | 0x38: "Provider Overlay Placement Opportunity Start", 42 | 0x39: "Provider Overlay Placement Opportunity End", 43 | 0x3A: "Distributor Overlay Placement Opportunity Start", 44 | 0x3B: "Distributor Overlay Placement Opportunity End", 45 | 0x3C: "Provider Promo Start", 46 | 0x3D: "Provider Promo End", 47 | 0x3E: "Distributor Promo Start", 48 | 0x3F: "Distributor Promo End", 49 | 0x40: "Unscheduled Event Start", 50 | 0x41: "Unscheduled Event End", 51 | 0x42: "Alternate Content Opportunity Start", 52 | 0x43: "Alternate Content Opportunity End", 53 | 0x44: "Provider Ad Block Start", 54 | 0x45: "Provider Ad Block End", 55 | 0x46: "Distributor Ad Block Start", 56 | 0x47: "Distributor Ad Block End", 57 | 0x50: "Network Start", 58 | 0x51: "Network End", 59 | } 60 | 61 | 62 | """ 63 | DVB Equivalent Segmentation Type 64 | """ 65 | dvb_table2 = { 66 | 0x0: "No Equivalent", 67 | 0x1: "Distributor Placement Opportunity", 68 | 0x2: "Provider Placement Opportunity", 69 | 0x3: "Distributor Advertisement", 70 | 0x4: "Provider Advertisement", 71 | } 72 | -------------------------------------------------------------------------------- /threefive/sixfix.py: -------------------------------------------------------------------------------- 1 | """ 2 | sixfix.py 3 | """ 4 | 5 | import io 6 | import sys 7 | from threefive.crc import crc32 8 | from threefive.bitn import NBin 9 | from threefive.stuff import print2 10 | from threefive.stream import Stream 11 | 12 | 13 | def passed(cue): 14 | """ 15 | passed a no-op function 16 | """ 17 | return cue 18 | 19 | 20 | class PreFix(Stream): 21 | """ 22 | PreFix is used to gather 06 Bin data pids with SCTE-35. 23 | """ 24 | 25 | def decode(self, func=passed): 26 | super().decode(func=passed) 27 | sixed = self.pids.maybe_scte35 28 | if sixed: 29 | print("fixing these pids", sixed) 30 | return sixed 31 | 32 | 33 | class SixFix(Stream): 34 | """ 35 | SixFix class 36 | fixes bin data streams with SCTE-35 to 0x86 SCTE-35 streams 37 | """ 38 | 39 | CUEI_DESCRIPTOR = b"\x05\x04CUEI" 40 | 41 | def __init__(self, tsdata=None): 42 | super().__init__(tsdata) 43 | self.pmt_payload = None 44 | self.con_pids = set() 45 | self.out_file = "sixfixed-" + tsdata.rsplit("/")[-1] 46 | self.in_file = sys.stdin.buffer 47 | 48 | def _parse_by_pid(self, pkt, pid): 49 | if pid in self.pids.tables: 50 | self._parse_tables(pkt, pid) 51 | if pid in self.pids.pmt: 52 | if self.pmt_payload: 53 | pkt = pkt[:4] + self.pmt_payload 54 | return pkt 55 | 56 | def convert_pids(self): 57 | """ 58 | convert_pids 59 | changes the stream type to 0x86 and replaces 60 | the existing PMT as it writes packets to the outfile 61 | """ 62 | active = io.BytesIO() 63 | pkt_count = 0 64 | chunk_size = 2048 65 | if isinstance(self.out_file, str): 66 | self.out_file = open(self.out_file, "wb") 67 | with self.out_file as out_file: 68 | for pkt in self.iter_pkts(): 69 | pid = self._parse_pid(pkt[1], pkt[2]) 70 | pkt = self._parse_by_pid(pkt, pid) 71 | active.write(pkt) 72 | pkt_count = (pkt_count + 1) % chunk_size 73 | if not pkt_count: 74 | out_file.write(active.getbuffer()) 75 | active = io.BytesIO() 76 | 77 | def _regen_pmt(self, n_seclen, pcr_pid, n_proginfolen, n_info_bites, n_streams): 78 | nbin = NBin() 79 | nbin.add_int(2, 8) # 0x02 80 | nbin.add_int(1, 1) # section Syntax indicator 81 | nbin.add_int(0, 1) # 0 82 | nbin.add_int(3, 2) # reserved 83 | nbin.add_int(n_seclen, 12) # section length 84 | nbin.add_int(1, 16) # program number 85 | nbin.add_int(3, 2) # reserved 86 | nbin.add_int(0, 5) # version 87 | nbin.add_int(1, 1) # current_next_indicator 88 | nbin.add_int(0, 8) # section number 89 | nbin.add_int(0, 8) # last section number 90 | nbin.add_int(7, 3) # res 91 | nbin.add_int(pcr_pid, 13) 92 | nbin.add_int(15, 4) # res 93 | nbin.add_int(n_proginfolen, 12) 94 | nbin.add_bites(n_info_bites) 95 | nbin.add_bites(n_streams) 96 | a_crc = crc32(nbin.bites) 97 | nbin.add_int(a_crc, 32) 98 | n_payload = nbin.bites 99 | pad = 187 - (len(n_payload) + 4) 100 | pointer_field = b"\x00" 101 | if pad > 0: 102 | n_payload = pointer_field + n_payload + (b"\xff" * pad) 103 | self.pmt_payload = n_payload 104 | 105 | def _parse_pmt(self, pay, pid): 106 | """ 107 | parse program maps for streams 108 | """ 109 | pay = self._chk_partial(pay, pid, self._PMT_TID) 110 | if not pay: 111 | return False 112 | seclen = self._parse_length(pay[1], pay[2]) 113 | n_seclen = seclen + 6 114 | if self._section_incomplete(pay, pid, seclen): 115 | return False 116 | program_number = self._parse_program(pay[3], pay[4]) 117 | pcr_pid = self._parse_pid(pay[8], pay[9]) 118 | self.pids.pcr.add(pcr_pid) 119 | self.maps.pid_prgm[pcr_pid] = program_number 120 | proginfolen = self._parse_length(pay[10], pay[11]) 121 | idx = 12 122 | n_proginfolen = proginfolen + len(self.CUEI_DESCRIPTOR) 123 | end = idx + proginfolen 124 | info_bites = pay[idx:end] 125 | n_info_bites = info_bites + self.CUEI_DESCRIPTOR 126 | while idx < end: 127 | # d_type = pay[idx] 128 | idx += 1 129 | d_len = pay[idx] 130 | idx += 1 131 | # d_bytes = pay[idx - 2 : idx + d_len] 132 | idx += d_len 133 | si_len = seclen - 9 134 | si_len -= proginfolen 135 | n_streams = self._parse_program_streams(si_len, pay, idx, program_number) 136 | self._regen_pmt(n_seclen, pcr_pid, n_proginfolen, n_info_bites, n_streams) 137 | return True 138 | 139 | def _parse_program_streams(self, si_len, pay, idx, program_number): 140 | """ 141 | parse the elementary streams 142 | from a program 143 | """ 144 | chunk_size = 5 145 | end_idx = (idx + si_len) - 4 146 | start = idx 147 | while idx < end_idx: 148 | pay, stream_type, pid, ei_len = self._parse_stream_type(pay, idx) 149 | idx += chunk_size 150 | idx += ei_len 151 | self.maps.pid_prgm[pid] = program_number 152 | self._set_scte35_pids(pid, stream_type) 153 | streams = pay[start:end_idx] 154 | return streams 155 | 156 | def _parse_stream_type(self, pay, idx): 157 | """ 158 | extract stream pid and type 159 | """ 160 | npay = pay 161 | stream_type = pay[idx] 162 | el_pid = self._parse_pid(pay[idx + 1], pay[idx + 2]) 163 | if el_pid in self.con_pids: 164 | if stream_type == 6: 165 | npay = pay[:idx] + b"\x86" + pay[idx + 1 :] 166 | ei_len = self._parse_length(pay[idx + 3], pay[idx + 4]) 167 | return npay, stream_type, el_pid, ei_len 168 | 169 | 170 | def sixfix(arg): 171 | """ 172 | sixfix converts 0x6 bin data mpegts streams 173 | that contain SCTE-35 data to stream type 0x86 174 | """ 175 | s1 = PreFix(arg) 176 | sixed = s1.decode(func=passed) 177 | if not sixed: 178 | print2("No bin data SCTE-35 streams were found.") 179 | else: 180 | s2 = SixFix(arg) 181 | s2.con_pids = sixed 182 | s2.convert_pids() 183 | print2(f'Wrote: sixfixed-{arg.rsplit("/")[-1]}') 184 | 185 | 186 | if __name__ == "__main__": 187 | sixfix(sys.argv[1]) 188 | -------------------------------------------------------------------------------- /threefive/smoketest.py: -------------------------------------------------------------------------------- 1 | """ 2 | smoke_test.py 3 | """ 4 | 5 | from .decode import decode 6 | from .stuff import print2 7 | 8 | # The format for tests is a dict of { "test_name" : value to pass to threefive.decode} 9 | ten_tests = { 10 | "Base64": "/DAvAAAAAAAA///wBQb+dGKQoAAZAhdDVUVJSAAAjn+fCAgAAAAALKChijUCAKnMZ1g=", 11 | "Bytes": b"\xfc0\x11\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\x00\x00\x00O%3\x96", 12 | "Hex String": "0XFC301100000000000000FFFFFF0000004F253396", 13 | "Hex Literal": 0xFC301100000000000000FFFFFF0000004F253396, 14 | "Integer": 1439737590925997869941740173214217318917816529814, 15 | "HTTP/HTTPS Streams": "https://futzu.com/xaa.ts", 16 | # "Bad" tests are expected to fail. 17 | "Bad Base64 ": "/DAvAf45AA", 18 | "Bad File": "/you/me/fake.file", 19 | "Bad Integer": -0.345, 20 | " Bad String": "your momma", 21 | } 22 | 23 | 24 | def _decode_test(test_name, test_data): 25 | passed = "✔" 26 | failed = "✘" 27 | print2(f"testing {test_name}\n Data: {test_data}\n") 28 | if decode(test_data): 29 | return passed 30 | return failed 31 | 32 | 33 | def smoke(tests=None): 34 | """ 35 | calls threefive.decode using the values in tests. 36 | The format for tests: 37 | { "test_name" : value to pass to threefive.decode} 38 | 39 | example: 40 | 41 | my_tests ={ 42 | "Base64": "/DAvAAAAAAAA///wBQb+dGKQoAAZAhdDVUVJSAAAjn+fCAgAAAAALKChijUCAKnMZ1g=", 43 | "Bytes": b"\xfc0\x11\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\x00\x00\x00O%3\x96" 44 | } 45 | 46 | import threefive 47 | threefive.smoke_test(my_tests) 48 | 49 | """ 50 | if not tests: 51 | tests = ten_tests 52 | results = {k: _decode_test(k, v) for k, v in tests.items()} 53 | print2("Smoke Test\n") 54 | for kay, vee in results.items(): 55 | print2(f"{kay} {vee}") 56 | 57 | 58 | if __name__ == "__main__": 59 | smoke() 60 | -------------------------------------------------------------------------------- /threefive/streamtypes.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | streamtypes.py 4 | 5 | Used by threefive.Stream 6 | On import stream_types generates a complete streamtype_map. 7 | 8 | from .streamtypes import streamtype_map 9 | 10 | """ 11 | 12 | streamtype_map = { 13 | 0x00: "Reserved", 14 | 0x01: "MPEG-1 video", 15 | 0x02: "H.262 video", 16 | 0x03: "MPEG-2 audio", 17 | 0x04: "MPEG-2 halved sample rate audio", 18 | 0x05: "MPEG-2 tabled data", 19 | 0x06: "MPEG-2 binary data", 20 | 0x07: "ISO/IEC 13522 (MHEG)", 21 | 0x08: "H.222 and DSM CC", 22 | 0x09: "H.222 and auxiliary data", 23 | 0x0A: "DSM CC multiprotocol encapsulation", 24 | 0x0B: "DSM CC U-N messages", 25 | 0x0C: "DSM CC stream descriptors", 26 | 0x0D: "DSM CC tabled data", 27 | 0x0E: "auxiliary data", 28 | 0x0F: "ADTS AAC ", 29 | 0x10: "MPEG-4 H.263 based video", 30 | 0x11: "MPEG-4 LOAS multi-format framed audio", 31 | 0x12: "MPEG-4 FlexMux", 32 | 0x13: "MPEG-4 FlexMux", 33 | 0x14: "DSM CC ", 34 | 0x15: "ID3", 35 | 0x16: "Sectioned metadata", 36 | 0x17: "DSM CC Data Carousel metadata", 37 | 0x18: "DSM CC Object Carousel metadata", 38 | 0x19: "Synchronized Download Protocol metadata", 39 | 0x1A: "IPMP", 40 | 0x1B: "H.264", 41 | 0x1C: "MPEG-4 raw audio)", 42 | 0x1D: "MPEG-4 text", 43 | 0x1E: "MPEG-4 auxiliary video", 44 | 0x1F: "SVC (MPEG-4 AVC sub-bitstream)", 45 | 0x20: "MVC (MPEG-4 AVC sub-bitstream)", 46 | 0x21: "JPEG 2000 video", 47 | # 0x22 - 0x23:"Reserved.", 48 | 0x24: " H.265", 49 | # 0x25 - 0x41:"Reserved.", 50 | 0x42: "Chinese Video Standard", 51 | # 0x43 - 0x7e:"Reserved.", 52 | 0x7F: " DRM Trash", 53 | 0x80: "H.262 with DES-64-CBC ", 54 | 0x81: "AC-3", 55 | 0x82: "SCTE subtitle", 56 | 0x83: "Dolby TrueHD audio ", 57 | 0x84: "AC-3", 58 | 0x85: "DTS 8 channel audio for Blu-ray", 59 | 0x86: "SCTE-35", 60 | 0x87: "AC-3 up to 16 channel audio for ATSC", 61 | # 0x88 - 0x89:"Privately defined.", 62 | 0x90: "Blu-ray Presentation Graphic Stream (subtitling)", 63 | 0x91: "ATSC DSM CC Network Resources table", 64 | # 0x92 - 0xBF:"Privately defined.", 65 | 0xC0: "DigiCipher II text", 66 | 0xC1: "AC-3 with AES-128-CBC ", 67 | 0xC2: "ATSC DSM CC synchronous data", 68 | # 0xC3 - 0xCE:"Privately defined.", 69 | 0xCF: "ADTS AAC with AES-128-CBC frame encryption", 70 | 0xD0: "Privately defined.", 71 | 0xD1: "BBC Dirac ", 72 | 0xD2: "AVS2 ", 73 | 0xD3: "AVS3 Audio", 74 | 0xD4: "AVS3 Video ", 75 | # 0xD5 - 0xDA:"Privately defined.", 76 | 0xDB: " H.264 with AES-128-CBC slice encryption", 77 | # 0xDC - 0xE9:"Privately defined.", 78 | 0xEA: "Windows janky media v9", 79 | # 0xEB - 0xFB:"Privately defined.", 80 | 0xFC: "KLV", 81 | } 82 | 83 | 84 | others = { 85 | "Reserved": list(range(0x22, 0x23)) 86 | + list(range(0x25, 0x41)) 87 | + list(range(0x43, 0x7E)), 88 | "Private": list(range(0x88, 0x89)) 89 | + list(range(0x92, 0xBF)) 90 | + list(range(0xC3, 0xCE)) 91 | + list(range(0xD5, 0xDA)) 92 | + list(range(0xDC, 0xE9)) 93 | + list(range(0xEB, 0xFB)) 94 | + list(range(0xFD, 0xFF)), 95 | } 96 | 97 | 98 | def hex_literal(integer): 99 | """ 100 | Return hex literal 101 | """ 102 | return int(hex(integer), base=16) 103 | 104 | 105 | def add_stream_types(alist, streamtype): 106 | """ 107 | add_stream_types dynamically 108 | adds a range of a stream type, 109 | like "Reserved",to streamtype_map 110 | """ 111 | for i in alist: 112 | j = hex_literal(i) 113 | streamtype_map[j] = streamtype 114 | 115 | 116 | def mk_streamtype_map(others): 117 | """ 118 | mk_streamtype_map dynamically adds 119 | stream types to streamtype_map 120 | """ 121 | for k, v in others.items(): 122 | add_stream_types(v, k) 123 | keys = list(streamtype_map.keys()) 124 | keys.sort() 125 | 126 | 127 | mk_streamtype_map(others) 128 | -------------------------------------------------------------------------------- /threefive/stuff.py: -------------------------------------------------------------------------------- 1 | """ 2 | stuff.py functions and such common to threefive. 3 | """ 4 | 5 | from sys import stderr 6 | 7 | 8 | def print2(stuff=b""): 9 | """ 10 | print2 prints to 2 aka stderr. 11 | """ 12 | print(stuff, file=stderr, flush=True) 13 | 14 | 15 | def dbl_split(data, mark): 16 | """ 17 | dbl_split split bytes on mark twice 18 | return mark + split bytes on mark twice. 19 | """ 20 | return mark + data.split(mark)[-1].split(mark)[-1] 21 | -------------------------------------------------------------------------------- /threefive/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | threefive.version 3 | from the cli tool run: threefive version 4 | """ 5 | version='2.4.99' 6 | -------------------------------------------------------------------------------- /trigger.md: -------------------------------------------------------------------------------- 1 | #
Triggering on SCTE-35 Events with threefive.Stream.
  2 | 
  3 | 
  4 | ## `option 1` ` pass in a function`
  5 | ---
  6 | * Most of the Stream methods take an optional arg __func__, a function to be called when a SCTE-35 Cue is found.  
  7 | 
  8 | * The __func__ function __must__ take __only one arg__, and that is  __a threefive.Cue object__.
  9 | 
 10 | ---
 11 | 
 12 | * I use __sidecar__ files for some of my other projects a __sidecar__ file consists
 13 | of __(PTS, Cue)__ pairs, one per line.
 14 | *  A __sidecar  file__ looks this.
 15 | 
 16 |   
 17 | ```js
 18 | a@slow:~$ cat sidecar.txt 
 19 | 
 20 | 
 21 | 41.273211,/DAWAAAAAAAAAP/wBQb+ADi52AAAp77zBQ==
 22 | 42.240844,/DAWAAAAAAAAAP/wBQb+ADoZwgAAaVWytQ==
 23 | 43.275211,/DAWAAAAAAAAAP/wBQb+ADt5rAAAXIo9lg==
 24 | 44.276211,/DAWAAAAAAAAAP/wBQb+ADzZlgAAwI0IyA==
 25 | 45.277211,/DAWAAAAAAAAAP/wBQb+AD45gAAA1tIPCQ==
 26 | 46.244844,/DAWAAAAAAAAAP/wBQb+AD+ZagAAf8zc/g==
 27 | 47.279211,/DAWAAAAAAAAAP/wBQb+AED5VAAAVMj9Dw==
 28 | 48.246844,/DAWAAAAAAAAAP/wBQb+AEJZPgAAypfF7w==
 29 | 49.281211,/DAWAAAAAAAAAP/wBQb+AEO5KAAAB99quQ==
 30 | ```
 31 | 
 32 | One of the ways I use a sidecar file is to exact the SCTE-35 before using ffmpeg to encode to HLS, 
 33 | and then I use the sidecar file to generate HLS tags and insert them into the ffmpeg m3u8 files in real time. 
 34 | 
 35 | * To make a sidecar file, I write a function.
 36 | 
 37 | ```py3
 38 | 
 39 | def mk_sidecar(cue):
 40 |     """
 41 |     mk_sidecar generates a sidecar file with the SCTE-35 Cues
 42 |     """
 43 |     pts = 0.0
 44 |     with open("sidecar.txt", "a") as sidecar:
 45 |         cue.show()
 46 |         if cue.packet_data.pts:
 47 |             pts = cue.packet_data.pts
 48 |         data = f"{pts},{cue.encode()}\n"
 49 |         sidecar.write(data)
 50 | 
 51 | ```
 52 | * The only other step is to call __Stream.decode__ and pass in the mk_sidecar function.
 53 | 
 54 | * Here's the complete example to generate a sidecar file.
 55 | 
 56 | ```py3
 57 | 
 58 | #!/usr/bin/env python3
 59 | 
 60 | import sys
 61 | from threefive import Stream
 62 | 
 63 | 
 64 | def mk_sidecar(cue):
 65 |     """
 66 |     mk_sidecar generates a sidecar file with the SCTE-35 Cues
 67 |     """
 68 |     pts = 0.0
 69 |     with open("sidecar.txt", "a") as sidecar:
 70 |         cue.show()
 71 |         if cue.packet_data.pts:
 72 |             pts = cue.packet_data.pts
 73 |         data = f"{pts},{cue.encode()}\n"
 74 |         sidecar.write(data)
 75 | 
 76 | 
 77 | if __name__ == '__main__':
 78 |         strm = Stream(sys.argv[1])
 79 |         strm.decode(func=mk_sidecar)
 80 | 
 81 | 
 82 | 
 83 | ```
 84 | * Run it like this:
 85 | 
 86 | ```py3
 87 | pypy3 sidecar_gen.py https://example.com/nmx.ts
 88 | ```
 89 | ---
 90 | * If you want to limit your sidecar file to the SCTE-35 in a specific MPEGTS program, call __decode_program__ instead.
 91 | 
 92 | 
 93 | ```py3
 94 | if __name__ == '__main__':
 95 | 	the_program = 3
 96 |         strm = Stream(sys.argv[1])
 97 |         strm.decode_program(the_program, func=mk_sidecar)
 98 | 
 99 | ```
100 | ---
101 | * To limit the sidecar file to SCTE-35 from a list of pids, call __decode_pids__. 
102 | 
103 | ```py3
104 | if __name__ == '__main__':
105 | 	scte35_pids =[259,1023,399]
106 |         strm = Stream(sys.argv[1])
107 |         strm.decode_pids(scte35_pids, func=mk_sidecar)
108 | 
109 | ```
110 | ---
111 | * To parse the video and then pipe it to ffmpeg or another program, __Stream.proxy__ writes the video stream sys.stdout.
112 | 
113 | ```py3
114 | if __name__ == '__main__':
115 | 	scte35_pids =[259,1023,399]
116 |         strm = Stream(sys.argv[1])
117 |         strm.proxy(func=mk_sidecar)
118 | 
119 | ```
120 | 
121 | ---
122 | 
123 | ## `option 2`  `Stream.decode_next`
124 | 
125 | * __Stream.decode_next__ returns the next Cue found.
126 | * This example uses a while loop with __Stream.decode_next__.
127 | * using __Stream.decode_next__ gives you more control over processing the Cue. 128 | ```py3 129 | 130 | import sys 131 | import threefive 132 | from new_reader import reader 133 | 134 | def mk_sidecar(cue): 135 | """ 136 | mk_sidecar generates a sidecar file with the SCTE-35 Cues 137 | """ 138 | pts = 0.0 139 | with open("sidecar.txt", "a") as sidecar: 140 | cue.show() 141 | if cue.packet_data.pts: 142 | pts = cue.packet_data.pts 143 | data = f"{pts},{cue.encode()}\n" 144 | sidecar.write(data) 145 | 146 | 147 | def do(): 148 | arg = sys.argv[1] 149 | with reader(arg) as tsdata: 150 | st = threefive.Stream(tsdata) 151 | while True: 152 | cue = st.decode_next() 153 | if not cue: 154 | return False 155 | if cue: 156 | mk_sidecar(cue) 157 | 158 | 159 | if __name__ == "__main__": 160 | do() 161 | 162 | ``` 163 | -------------------------------------------------------------------------------- /xml.md: -------------------------------------------------------------------------------- 1 | # Xml as of v2.4.95 2 | 3 | __I strongly suggest using the xml+binary format__ for DASH, it is very straight forward, very compact, and the SCTE-35 data is exactly the same as regular SCTE-35. __use xml+bin for DASH__ 4 | 5 | #### xml in the Cli 6 | * [SCTE-35 xml input](#xml-input) 7 | * [SCTE-35 xml output](#xml-output) 8 | * [SCTE-35 xml output from MPEGTS](#xml-output-from-mpegts-video) 9 | 10 | #### xml in the Cue class 11 | * [SCTE-35 xml input](#xml-as-input) 12 | * [SCTE-35 xml output](#the-cue-class-can-return-xml-as-output) 13 | * [removing or changing the scte35 namespace](#removing-or-changing-the-namespace) 14 | 15 | # Cli 16 | 17 | ## Xml input 18 | 19 | * SCTE-35 __xml__ and SCTE-35 __xmlbin__ can be encoded to base64, bytes, hex, int, or json. 20 | 21 | ### xmlbin 22 | 23 | * parse mpegts video and display scte-35 in xmlbin format. 24 | 25 | threefive xmlbin sixed.ts 26 | 27 | 28 | * convert to xmlbin format 29 | 30 | threefive '/DAgAAAAAAAAAP/wDwUAAAABf0/+AFJlwAABAAAAALQOZyE=' xmlbin 31 | ```xml 32 | 33 | /DAgAAAAAAAAAP/wDwUAAAABf0/+AFJlwAABAAAAALQOZyE= 34 | 35 | ``` 36 | 37 | * write to a file in xmlbin format 38 | 39 | a@fu:~$ threefive '/DAgAAAAAAAAAP/wDwUAAAABf0/+AFJlwAABAAAAALQOZyE=' xmlbin 2> xmlbin.xml 40 | 41 | * read from the file and convert to bytes 42 | 43 | a@fu:~$ threefive bytes < xmlbin.xml 44 | b'\xfc0\x00\x00\x00\x00\x00\x00\x00\xff\xf0\x0f\x05\x00\x00\x00\x01\x7fO\xfe\x00Re\xc0\x00\x01\x00\x00\x00\x00\xb4\x0eg!' 45 | 46 | * read from a file and convert to base64 47 | 48 | a@fu:~$ threefive base64 < xmlbin.xml 49 | /DAgAAAAAAAAAP/wDwUAAAABf0/+AFJlwAABAAAAALQOZyE= 50 | 51 | * read from a file and convert to hex 52 | 53 | a@fu:~$ threefive hex < xmlbin.xml 54 | 0xfc302000000000000000fff00f05000000017f4ffe005265c0000100000000b40e6721 55 | 56 | * read from a file and convert to plain xml 57 | 58 | a@fu:~$ threefive xml < xmlbin.xml 59 | ```xml 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ``` 68 | ## Xml 69 | 70 | 71 | * Xml to Base64 72 | 73 | ```js 74 | threefive < xml.xml 75 | 76 | /DAsAAAAAAAAAP/wBQb+7YaD1QAWAhRDVUVJAADc8X+/DAVPVkxZSSIAAJ6Gk2Q= 77 | 78 | ``` 79 | 80 | * Xml to hex 81 | 82 | ```js 83 | threefive hex < xml.xml 84 | 0xfc302c00000000000000fff00506feed8683d500160214435545490000dcf17fbf0c054f564c59492200009e869364 85 | 86 | ``` 87 | 88 | * Xml to int 89 | ```js 90 | threefive int < xml.xml 91 | 151622312799635087445131038116901140411521203255173124307448868984487395583746158940007186416525810106184013091684 92 | ``` 93 | 94 | ### Xml output 95 | * the cli can convert SCTE-35 base64, hex, or json to xml 96 | ```js 97 | a@fu:~/build/SCTE35_threefive$ threefive xml '0xfc302c00000000000000fff00506feed8683d500160214435545490000dcf17fbf0c054f564c59492200009e869364' 98 | ```xml 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 4f564c5949 107 | 108 | 109 | ``` 110 | 111 | ### Xml output from mpegts video 112 | 113 | * MPEGTS streams can be parsed for SCTE-35 and the output encoded in Xml 114 | 115 | ```js 116 | threefive xml sixed.ts 117 | ``` 118 | ```xml 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 10100000 131 | 132 | 133 | 134 | ``` 135 | 136 | * threefive prints output to stderr, stdout is used for piping data. 137 | * To save the output of the cli tool redirect 2. 138 | ```js 139 | threefive xmlbin sixed.ts 2> fu.xml 140 | ``` 141 | 142 | # Xml with the Cue class. 143 | 144 | ### The Cue class can return xml as output 145 | 146 | ```py3 147 | a@fu:~/build/SCTE35_threefive$ pypy3 148 | Python 3.9.16 (7.3.11+dfsg-2+deb12u2, May 20 2024, 22:08:06) 149 | [PyPy 7.3.11 with GCC 12.2.0] on linux 150 | Type "help", "copyright", "credits" or "license" for more information. 151 | >>>> from threefive import Cue 152 | >>>> cue=Cue('/DA4AAAAAAAA///wBQb+AKpFLgAiAiBDVUVJAAAAA3//AAApPWwDDEFCQ0QwMTIzNDU2SBAAABZE5vg=') 153 | >>>> cue.decode() 154 | True 155 | >>>> cue.xml(xmlbin=False) 156 | ``` 157 | ```xml 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | ABCD0123456H 166 | 167 | 168 | ``` 169 | * By default, the Cue class returns xml in the xml+bin format 170 | ```py3 171 | >>>> cue.xml() 172 | ``` 173 | ```xml 174 | 175 | /DA4AAAAAAAA///wBQb+AKpFLgAiAiBDVUVJAAAAA3//AAApPWwDDEFCQ0QwMTIzNDU2SBAAABZE5vg= 176 | 177 | ``` 178 | 179 | ### Xml as input 180 | 181 | * xml can now be passed in to a Cue instance when initialized 182 | * both xml and xml+bin formats can be used. 183 | ```xml 184 | x=" 185 | 186 | /DA4AAAAAAAA///wBQb+AKpFLgAiAiBDVUVJAAAAA3//AAApPWwDDEFCQ0QwMTIzNDU2SBAAABZE5vg= 187 | 188 | " 189 | ``` 190 | ```py3 191 | >>>> cue2=Cue(x) 192 | 193 | >>>> cue2.xml(xmlbin=False) 194 | ``` 195 | ```xml 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | ABCD0123456H 204 | 205 | 206 | ``` 207 | 208 | ### Removing or changing the namespace 209 | 210 | * the namespace can be removed or changed in either the xml or xml+bin format 211 | * to remove the scte35 namespace, set the optional ns arg to an empty string 212 | 213 | ```py3 214 | >>>> cue.xml(xmlbin=False,ns='') 215 | ``` 216 | ```xml 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | ABCD0123456H 225 | 226 | 227 | ``` 228 | 229 | --------------------------------------------------------------------------------