openm3u8 7.0.0__cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openm3u8/__init__.py +113 -0
- openm3u8/_m3u8_parser.abi3.so +0 -0
- openm3u8/_m3u8_parser.c +3122 -0
- openm3u8/httpclient.py +36 -0
- openm3u8/mixins.py +52 -0
- openm3u8/model.py +1694 -0
- openm3u8/parser.py +786 -0
- openm3u8/protocol.py +47 -0
- openm3u8/version_matching.py +37 -0
- openm3u8/version_matching_rules.py +108 -0
- openm3u8-7.0.0.dist-info/METADATA +122 -0
- openm3u8-7.0.0.dist-info/RECORD +15 -0
- openm3u8-7.0.0.dist-info/WHEEL +6 -0
- openm3u8-7.0.0.dist-info/licenses/LICENSE +13 -0
- openm3u8-7.0.0.dist-info/top_level.txt +1 -0
openm3u8/parser.py
ADDED
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
# Copyright 2014 Globo.com Player authors. All rights reserved.
|
|
2
|
+
# Modifications Copyright (c) 2026 Wurl.
|
|
3
|
+
# Use of this source code is governed by a MIT License
|
|
4
|
+
# license that can be found in the LICENSE file.
|
|
5
|
+
|
|
6
|
+
import itertools
|
|
7
|
+
import re
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from backports.datetime_fromisoformat import MonkeyPatch
|
|
12
|
+
|
|
13
|
+
MonkeyPatch.patch_fromisoformat()
|
|
14
|
+
except ImportError:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from openm3u8 import protocol, version_matching
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.2
|
|
22
|
+
http://stackoverflow.com/questions/2785755/how-to-split-but-ignore-separators-in-quoted-strings-in-python
|
|
23
|
+
"""
|
|
24
|
+
ATTRIBUTELISTPATTERN = re.compile(r"""((?:[^,"']|"[^"]*"|'[^']*')+)""")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cast_date_time(value):
|
|
28
|
+
return datetime.fromisoformat(value)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def format_date_time(value, **kwargs):
|
|
32
|
+
return value.isoformat(**kwargs)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ParseError(Exception):
|
|
36
|
+
def __init__(self, lineno, line):
|
|
37
|
+
self.lineno = lineno
|
|
38
|
+
self.line = line
|
|
39
|
+
|
|
40
|
+
def __str__(self):
|
|
41
|
+
return "Syntax error in manifest on line %d: %s" % (self.lineno, self.line)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse(content, strict=False, custom_tags_parser=None):
|
|
45
|
+
"""
|
|
46
|
+
Given a M3U8 playlist content returns a dictionary with all data found
|
|
47
|
+
"""
|
|
48
|
+
data = {
|
|
49
|
+
"media_sequence": 0,
|
|
50
|
+
"is_variant": False,
|
|
51
|
+
"is_endlist": False,
|
|
52
|
+
"is_i_frames_only": False,
|
|
53
|
+
"is_independent_segments": False,
|
|
54
|
+
"is_images_only": False,
|
|
55
|
+
"playlist_type": None,
|
|
56
|
+
"playlists": [],
|
|
57
|
+
"segments": [],
|
|
58
|
+
"iframe_playlists": [],
|
|
59
|
+
"image_playlists": [],
|
|
60
|
+
"tiles": [],
|
|
61
|
+
"media": [],
|
|
62
|
+
"keys": [],
|
|
63
|
+
"rendition_reports": [],
|
|
64
|
+
"skip": {},
|
|
65
|
+
"part_inf": {},
|
|
66
|
+
"session_data": [],
|
|
67
|
+
"session_keys": [],
|
|
68
|
+
"segment_map": [],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
state = {
|
|
72
|
+
"expect_segment": False,
|
|
73
|
+
"expect_playlist": False,
|
|
74
|
+
"current_key": None,
|
|
75
|
+
"current_segment_map": None,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
lines = string_to_lines(content)
|
|
79
|
+
if strict:
|
|
80
|
+
found_errors = version_matching.validate(lines)
|
|
81
|
+
|
|
82
|
+
if len(found_errors) > 0:
|
|
83
|
+
raise Exception(found_errors)
|
|
84
|
+
|
|
85
|
+
for lineno, line in enumerate(lines, 1):
|
|
86
|
+
line = line.strip()
|
|
87
|
+
parse_kwargs = {
|
|
88
|
+
"line": line,
|
|
89
|
+
"lineno": lineno,
|
|
90
|
+
"data": data,
|
|
91
|
+
"state": state,
|
|
92
|
+
"strict": strict,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Blank lines are ignored.
|
|
96
|
+
if not line:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
# Call custom parser if needed
|
|
100
|
+
if line.startswith("#") and callable(custom_tags_parser):
|
|
101
|
+
go_to_next_line = custom_tags_parser(line, lineno, data, state)
|
|
102
|
+
|
|
103
|
+
# Do not try to parse other standard tags on this line if custom_tags_parser
|
|
104
|
+
# function returns `True`
|
|
105
|
+
if go_to_next_line:
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
# Fast-path: dispatch based on tag token up to first ':' (or full tag if none)
|
|
109
|
+
if line.startswith("#"):
|
|
110
|
+
tag = line.split(":", 1)[0]
|
|
111
|
+
handler = DISPATCH.get(tag)
|
|
112
|
+
if handler is not None:
|
|
113
|
+
handler(**parse_kwargs)
|
|
114
|
+
continue
|
|
115
|
+
# #EXTM3U should be present; ignore if seen
|
|
116
|
+
if tag == protocol.ext_m3u:
|
|
117
|
+
continue
|
|
118
|
+
# In strict mode, unrecognized tags are illegal
|
|
119
|
+
if strict:
|
|
120
|
+
raise ParseError(lineno, line)
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
# Lines that don't start with # are either segments or playlists.
|
|
124
|
+
if state["expect_segment"]:
|
|
125
|
+
_parse_ts_chunk(**parse_kwargs)
|
|
126
|
+
elif state["expect_playlist"]:
|
|
127
|
+
_parse_variant_playlist(**parse_kwargs)
|
|
128
|
+
# In strict mode, any other content is illegal
|
|
129
|
+
elif strict:
|
|
130
|
+
raise ParseError(lineno, line)
|
|
131
|
+
|
|
132
|
+
# Handle remaining partial segments.
|
|
133
|
+
if "segment" in state:
|
|
134
|
+
data["segments"].append(state.pop("segment"))
|
|
135
|
+
|
|
136
|
+
return data
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _parse_key(line, data, state, **kwargs):
|
|
140
|
+
params = ATTRIBUTELISTPATTERN.split(line.replace(protocol.ext_x_key + ":", ""))[
|
|
141
|
+
1::2
|
|
142
|
+
]
|
|
143
|
+
key = {}
|
|
144
|
+
for param in params:
|
|
145
|
+
name, value = param.split("=", 1)
|
|
146
|
+
key[normalize_attribute(name)] = remove_quotes(value)
|
|
147
|
+
|
|
148
|
+
state["current_key"] = key
|
|
149
|
+
if key not in data["keys"]:
|
|
150
|
+
data["keys"].append(key)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _parse_extinf(line, state, lineno, strict, **kwargs):
|
|
154
|
+
chunks = line.replace(protocol.extinf + ":", "").split(",", 1)
|
|
155
|
+
if len(chunks) == 2:
|
|
156
|
+
duration, title = chunks
|
|
157
|
+
elif len(chunks) == 1:
|
|
158
|
+
if strict:
|
|
159
|
+
raise ParseError(lineno, line)
|
|
160
|
+
else:
|
|
161
|
+
duration = chunks[0]
|
|
162
|
+
title = ""
|
|
163
|
+
if "segment" not in state:
|
|
164
|
+
state["segment"] = {}
|
|
165
|
+
state["segment"]["duration"] = float(duration)
|
|
166
|
+
state["segment"]["title"] = title
|
|
167
|
+
state["expect_segment"] = True
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _parse_ts_chunk(line, data, state, **kwargs):
|
|
171
|
+
segment = state.pop("segment")
|
|
172
|
+
if state.get("program_date_time"):
|
|
173
|
+
segment["program_date_time"] = state.pop("program_date_time")
|
|
174
|
+
if state.get("current_program_date_time"):
|
|
175
|
+
segment["current_program_date_time"] = state["current_program_date_time"]
|
|
176
|
+
state["current_program_date_time"] += timedelta(seconds=segment["duration"])
|
|
177
|
+
segment["uri"] = line
|
|
178
|
+
segment["cue_in"] = state.pop("cue_in", False)
|
|
179
|
+
segment["cue_out"] = state.pop("cue_out", False)
|
|
180
|
+
segment["cue_out_start"] = state.pop("cue_out_start", False)
|
|
181
|
+
segment["cue_out_explicitly_duration"] = state.pop(
|
|
182
|
+
"cue_out_explicitly_duration", False
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
scte_op = state.get if segment["cue_out"] else state.pop
|
|
186
|
+
segment["scte35"] = scte_op("current_cue_out_scte35", None)
|
|
187
|
+
segment["oatcls_scte35"] = scte_op("current_cue_out_oatcls_scte35", None)
|
|
188
|
+
segment["scte35_duration"] = scte_op("current_cue_out_duration", None)
|
|
189
|
+
segment["scte35_elapsedtime"] = scte_op("current_cue_out_elapsedtime", None)
|
|
190
|
+
segment["asset_metadata"] = scte_op("asset_metadata", None)
|
|
191
|
+
|
|
192
|
+
segment["discontinuity"] = state.pop("discontinuity", False)
|
|
193
|
+
if state.get("current_key"):
|
|
194
|
+
segment["key"] = state["current_key"]
|
|
195
|
+
else:
|
|
196
|
+
# For unencrypted segments, the initial key would be None
|
|
197
|
+
if None not in data["keys"]:
|
|
198
|
+
data["keys"].append(None)
|
|
199
|
+
if state.get("current_segment_map"):
|
|
200
|
+
segment["init_section"] = state["current_segment_map"]
|
|
201
|
+
segment["dateranges"] = state.pop("dateranges", None)
|
|
202
|
+
segment["gap_tag"] = state.pop("gap", None)
|
|
203
|
+
segment["blackout"] = state.pop("blackout", None)
|
|
204
|
+
data["segments"].append(segment)
|
|
205
|
+
state["expect_segment"] = False
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _parse_attribute_list(prefix, line, attribute_parser, default_parser=None):
|
|
209
|
+
params = ATTRIBUTELISTPATTERN.split(line.replace(prefix + ":", ""))[1::2]
|
|
210
|
+
|
|
211
|
+
attributes = {}
|
|
212
|
+
if not line.startswith(prefix + ":"):
|
|
213
|
+
return attributes
|
|
214
|
+
|
|
215
|
+
for param in params:
|
|
216
|
+
param_parts = param.split("=", 1)
|
|
217
|
+
if len(param_parts) == 1:
|
|
218
|
+
name = ""
|
|
219
|
+
value = param_parts[0]
|
|
220
|
+
else:
|
|
221
|
+
name, value = param_parts
|
|
222
|
+
|
|
223
|
+
name = normalize_attribute(name)
|
|
224
|
+
if name in attribute_parser:
|
|
225
|
+
value = attribute_parser[name](value)
|
|
226
|
+
elif default_parser is not None:
|
|
227
|
+
value = default_parser(value)
|
|
228
|
+
|
|
229
|
+
attributes[name] = value
|
|
230
|
+
|
|
231
|
+
return attributes
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _parse_stream_inf(line, data, state, **kwargs):
|
|
235
|
+
state["expect_playlist"] = True
|
|
236
|
+
data["is_variant"] = True
|
|
237
|
+
data["media_sequence"] = None
|
|
238
|
+
state["stream_info"] = _parse_attribute_list(
|
|
239
|
+
protocol.ext_x_stream_inf, line, STREAM_INF_ATTRIBUTE_PARSER
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _parse_i_frame_stream_inf(line, data, **kwargs):
|
|
244
|
+
iframe_stream_info = _parse_attribute_list(
|
|
245
|
+
protocol.ext_x_i_frame_stream_inf, line, IFRAME_STREAM_INF_ATTRIBUTE_PARSER
|
|
246
|
+
)
|
|
247
|
+
iframe_playlist = {
|
|
248
|
+
"uri": iframe_stream_info.pop("uri"),
|
|
249
|
+
"iframe_stream_info": iframe_stream_info,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
data["iframe_playlists"].append(iframe_playlist)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _parse_image_stream_inf(line, data, **kwargs):
|
|
256
|
+
image_stream_info = _parse_attribute_list(
|
|
257
|
+
protocol.ext_x_image_stream_inf, line, IMAGE_STREAM_INF_ATTRIBUTE_PARSER
|
|
258
|
+
)
|
|
259
|
+
image_playlist = {
|
|
260
|
+
"uri": image_stream_info.pop("uri"),
|
|
261
|
+
"image_stream_info": image_stream_info,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
data["image_playlists"].append(image_playlist)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _parse_is_images_only(line, data, **kwargs):
|
|
268
|
+
data["is_images_only"] = True
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _parse_tiles(line, data, state, **kwargs):
|
|
272
|
+
tiles_info = _parse_attribute_list(
|
|
273
|
+
protocol.ext_x_tiles, line, TILES_ATTRIBUTE_PARSER
|
|
274
|
+
)
|
|
275
|
+
data["tiles"].append(tiles_info)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _parse_media(line, data, **kwargs):
|
|
279
|
+
media = _parse_attribute_list(protocol.ext_x_media, line, MEDIA_ATTRIBUTE_PARSER)
|
|
280
|
+
data["media"].append(media)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _parse_variant_playlist(line, data, state, **kwargs):
|
|
284
|
+
playlist = {"uri": line, "stream_info": state.pop("stream_info")}
|
|
285
|
+
data["playlists"].append(playlist)
|
|
286
|
+
state["expect_playlist"] = False
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _parse_bitrate(state, **kwargs):
|
|
290
|
+
if "segment" not in state:
|
|
291
|
+
state["segment"] = {}
|
|
292
|
+
state["segment"]["bitrate"] = _parse_simple_parameter(cast_to=int, **kwargs)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _parse_byterange(line, state, **kwargs):
|
|
296
|
+
if "segment" not in state:
|
|
297
|
+
state["segment"] = {}
|
|
298
|
+
state["segment"]["byterange"] = line.replace(protocol.ext_x_byterange + ":", "")
|
|
299
|
+
state["expect_segment"] = True
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _parse_targetduration(**parse_kwargs):
|
|
303
|
+
return _parse_simple_parameter(cast_to=int, **parse_kwargs)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _parse_media_sequence(**parse_kwargs):
|
|
307
|
+
return _parse_simple_parameter(cast_to=int, **parse_kwargs)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _parse_discontinuity_sequence(**parse_kwargs):
|
|
311
|
+
return _parse_simple_parameter(cast_to=int, **parse_kwargs)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _parse_program_date_time(line, state, data, **parse_kwargs):
|
|
315
|
+
_, program_date_time = _parse_simple_parameter_raw_value(
|
|
316
|
+
line, cast_to=cast_date_time, **parse_kwargs
|
|
317
|
+
)
|
|
318
|
+
if not data.get("program_date_time"):
|
|
319
|
+
data["program_date_time"] = program_date_time
|
|
320
|
+
state["current_program_date_time"] = program_date_time
|
|
321
|
+
state["program_date_time"] = program_date_time
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _parse_discontinuity(state, **parse_kwargs):
|
|
325
|
+
state["discontinuity"] = True
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _parse_cue_in(state, **parse_kwargs):
|
|
329
|
+
state["cue_in"] = True
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _parse_cue_span(state, **parse_kwargs):
|
|
333
|
+
state["cue_out"] = True
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _parse_version(**parse_kwargs):
|
|
337
|
+
return _parse_simple_parameter(cast_to=int, **parse_kwargs)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _parse_allow_cache(**parse_kwargs):
|
|
341
|
+
return _parse_simple_parameter(cast_to=str, **parse_kwargs)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _parse_playlist_type(line, data, **kwargs):
|
|
345
|
+
return _parse_simple_parameter(line, data)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _parse_x_map(line, data, state, **kwargs):
|
|
349
|
+
segment_map_info = _parse_attribute_list(
|
|
350
|
+
protocol.ext_x_map, line, X_MAP_ATTRIBUTE_PARSER
|
|
351
|
+
)
|
|
352
|
+
state["current_segment_map"] = segment_map_info
|
|
353
|
+
data["segment_map"].append(segment_map_info)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _parse_start(line, data, **kwargs):
|
|
357
|
+
start_info = _parse_attribute_list(
|
|
358
|
+
protocol.ext_x_start, line, START_ATTRIBUTE_PARSER
|
|
359
|
+
)
|
|
360
|
+
data["start"] = start_info
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _parse_gap(state, **kwargs):
|
|
364
|
+
state["gap"] = True
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _parse_blackout(line, state, **kwargs):
|
|
368
|
+
# Store the full tag content to pass through unmodified
|
|
369
|
+
# Extract everything after "#EXT-X-BLACKOUT"
|
|
370
|
+
if ":" in line:
|
|
371
|
+
# Tag has parameters: #EXT-X-BLACKOUT:params
|
|
372
|
+
blackout_data = line.split(":", 1)[1]
|
|
373
|
+
else:
|
|
374
|
+
# Tag has no parameters, just store True
|
|
375
|
+
blackout_data = True
|
|
376
|
+
state["blackout"] = blackout_data
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _parse_simple_parameter_raw_value(line, cast_to=str, normalize=False, **kwargs):
|
|
380
|
+
param, value = line.split(":", 1)
|
|
381
|
+
param = normalize_attribute(param.replace("#EXT-X-", ""))
|
|
382
|
+
if normalize:
|
|
383
|
+
value = value.strip().lower()
|
|
384
|
+
return param, cast_to(value)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _parse_and_set_simple_parameter_raw_value(
|
|
388
|
+
line, data, cast_to=str, normalize=False, **kwargs
|
|
389
|
+
):
|
|
390
|
+
param, value = _parse_simple_parameter_raw_value(line, cast_to, normalize)
|
|
391
|
+
data[param] = value
|
|
392
|
+
return data[param]
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _parse_simple_parameter(line, data, cast_to=str, **kwargs):
|
|
396
|
+
return _parse_and_set_simple_parameter_raw_value(line, data, cast_to, True)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _parse_i_frames_only(data, **kwargs):
|
|
400
|
+
data["is_i_frames_only"] = True
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _parse_is_independent_segments(data, **kwargs):
|
|
404
|
+
data["is_independent_segments"] = True
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _parse_endlist(data, **kwargs):
|
|
408
|
+
data["is_endlist"] = True
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _parse_cueout_cont(line, state, **kwargs):
|
|
412
|
+
state["cue_out"] = True
|
|
413
|
+
|
|
414
|
+
elements = line.split(":", 1)
|
|
415
|
+
if len(elements) != 2:
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
# EXT-X-CUE-OUT-CONT:ElapsedTime=10,Duration=60,SCTE35=... style
|
|
419
|
+
cue_info = _parse_attribute_list(
|
|
420
|
+
protocol.ext_x_cue_out_cont,
|
|
421
|
+
line,
|
|
422
|
+
CUEOUT_CONT_ATTRIBUTE_PARSER,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# EXT-X-CUE-OUT-CONT:2.436/120 style
|
|
426
|
+
progress = cue_info.get("")
|
|
427
|
+
if progress:
|
|
428
|
+
progress_parts = progress.split("/", 1)
|
|
429
|
+
if len(progress_parts) == 1:
|
|
430
|
+
state["current_cue_out_duration"] = progress_parts[0]
|
|
431
|
+
else:
|
|
432
|
+
state["current_cue_out_elapsedtime"] = progress_parts[0]
|
|
433
|
+
state["current_cue_out_duration"] = progress_parts[1]
|
|
434
|
+
|
|
435
|
+
duration = cue_info.get("duration")
|
|
436
|
+
if duration:
|
|
437
|
+
state["current_cue_out_duration"] = duration
|
|
438
|
+
|
|
439
|
+
scte35 = cue_info.get("scte35")
|
|
440
|
+
if duration:
|
|
441
|
+
state["current_cue_out_scte35"] = scte35
|
|
442
|
+
|
|
443
|
+
elapsedtime = cue_info.get("elapsedtime")
|
|
444
|
+
if elapsedtime:
|
|
445
|
+
state["current_cue_out_elapsedtime"] = elapsedtime
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _parse_cueout(line, state, **kwargs):
|
|
449
|
+
state["cue_out_start"] = True
|
|
450
|
+
state["cue_out"] = True
|
|
451
|
+
if "DURATION" in line.upper():
|
|
452
|
+
state["cue_out_explicitly_duration"] = True
|
|
453
|
+
|
|
454
|
+
elements = line.split(":", 1)
|
|
455
|
+
if len(elements) != 2:
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
cue_info = _parse_attribute_list(
|
|
459
|
+
protocol.ext_x_cue_out,
|
|
460
|
+
line,
|
|
461
|
+
CUEOUT_ATTRIBUTE_PARSER,
|
|
462
|
+
)
|
|
463
|
+
cue_out_scte35 = cue_info.get("cue")
|
|
464
|
+
cue_out_duration = cue_info.get("duration") or cue_info.get("")
|
|
465
|
+
|
|
466
|
+
current_cue_out_scte35 = state.get("current_cue_out_scte35")
|
|
467
|
+
state["current_cue_out_scte35"] = cue_out_scte35 or current_cue_out_scte35
|
|
468
|
+
state["current_cue_out_duration"] = cue_out_duration
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _parse_server_control(line, data, **kwargs):
|
|
472
|
+
data["server_control"] = _parse_attribute_list(
|
|
473
|
+
protocol.ext_x_server_control, line, SERVER_CONTROL_ATTRIBUTE_PARSER
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _parse_part_inf(line, data, **kwargs):
|
|
478
|
+
data["part_inf"] = _parse_attribute_list(
|
|
479
|
+
protocol.ext_x_part_inf, line, PART_INF_ATTRIBUTE_PARSER
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _parse_rendition_report(line, data, **kwargs):
|
|
484
|
+
rendition_report = _parse_attribute_list(
|
|
485
|
+
protocol.ext_x_rendition_report, line, RENDITION_REPORT_ATTRIBUTE_PARSER
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
data["rendition_reports"].append(rendition_report)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _parse_part(line, state, **kwargs):
|
|
492
|
+
part = _parse_attribute_list(protocol.ext_x_part, line, PART_ATTRIBUTE_PARSER)
|
|
493
|
+
|
|
494
|
+
# this should always be true according to spec
|
|
495
|
+
if state.get("current_program_date_time"):
|
|
496
|
+
part["program_date_time"] = state["current_program_date_time"]
|
|
497
|
+
state["current_program_date_time"] += timedelta(seconds=part["duration"])
|
|
498
|
+
|
|
499
|
+
part["dateranges"] = state.pop("dateranges", None)
|
|
500
|
+
part["gap_tag"] = state.pop("gap", None)
|
|
501
|
+
|
|
502
|
+
if "segment" not in state:
|
|
503
|
+
state["segment"] = {}
|
|
504
|
+
segment = state["segment"]
|
|
505
|
+
if "parts" not in segment:
|
|
506
|
+
segment["parts"] = []
|
|
507
|
+
|
|
508
|
+
segment["parts"].append(part)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _parse_skip(line, data, **parse_kwargs):
|
|
512
|
+
data["skip"] = _parse_attribute_list(
|
|
513
|
+
protocol.ext_x_skip, line, SKIP_ATTRIBUTE_PARSER
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _parse_session_data(line, data, **kwargs):
|
|
518
|
+
session_data = _parse_attribute_list(
|
|
519
|
+
protocol.ext_x_session_data, line, SESSION_DATA_ATTRIBUTE_PARSER
|
|
520
|
+
)
|
|
521
|
+
data["session_data"].append(session_data)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _parse_session_key(line, data, **kwargs):
|
|
525
|
+
params = ATTRIBUTELISTPATTERN.split(
|
|
526
|
+
line.replace(protocol.ext_x_session_key + ":", "")
|
|
527
|
+
)[1::2]
|
|
528
|
+
key = {}
|
|
529
|
+
for param in params:
|
|
530
|
+
name, value = param.split("=", 1)
|
|
531
|
+
key[normalize_attribute(name)] = remove_quotes(value)
|
|
532
|
+
data["session_keys"].append(key)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _parse_preload_hint(line, data, **kwargs):
|
|
536
|
+
data["preload_hint"] = _parse_attribute_list(
|
|
537
|
+
protocol.ext_x_preload_hint, line, PRELOAD_HINT_ATTRIBUTE_PARSER
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _parse_daterange(line, state, **kwargs):
|
|
542
|
+
parsed = _parse_attribute_list(
|
|
543
|
+
protocol.ext_x_daterange, line, DATERANGE_ATTRIBUTE_PARSER
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
if "dateranges" not in state:
|
|
547
|
+
state["dateranges"] = []
|
|
548
|
+
|
|
549
|
+
state["dateranges"].append(parsed)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _parse_content_steering(line, data, **kwargs):
|
|
553
|
+
data["content_steering"] = _parse_attribute_list(
|
|
554
|
+
protocol.ext_x_content_steering, line, CONTENT_STEERING_ATTRIBUTE_PARSER
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _parse_oatcls_scte35(line, state, **kwargs):
|
|
559
|
+
scte35_cue = line.split(":", 1)[1]
|
|
560
|
+
state["current_cue_out_oatcls_scte35"] = scte35_cue
|
|
561
|
+
if not state.get("current_cue_out_scte35"):
|
|
562
|
+
state["current_cue_out_scte35"] = scte35_cue
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _parse_asset(line, state, **kwargs):
|
|
566
|
+
# EXT-X-ASSET attribute values may or may not be quoted, and need to be URL-encoded.
|
|
567
|
+
# They are preserved as-is here to prevent loss of information.
|
|
568
|
+
state["asset_metadata"] = _parse_attribute_list(
|
|
569
|
+
protocol.ext_x_asset, line, {}, default_parser=str
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def string_to_lines(string):
|
|
574
|
+
return string.strip().splitlines()
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def remove_quotes_parser(*attrs):
|
|
578
|
+
return dict(zip(attrs, itertools.repeat(remove_quotes)))
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def remove_quotes(string):
|
|
582
|
+
"""
|
|
583
|
+
Remove quotes from string.
|
|
584
|
+
|
|
585
|
+
Ex.:
|
|
586
|
+
"foo" -> foo
|
|
587
|
+
'foo' -> foo
|
|
588
|
+
'foo -> 'foo
|
|
589
|
+
|
|
590
|
+
"""
|
|
591
|
+
quotes = ('"', "'")
|
|
592
|
+
if string.startswith(quotes) and string.endswith(quotes):
|
|
593
|
+
return string[1:-1]
|
|
594
|
+
return string
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def normalize_attribute(attribute):
|
|
598
|
+
return attribute.replace("-", "_").lower().strip()
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def get_segment_custom_value(state, key, default=None):
|
|
602
|
+
"""
|
|
603
|
+
Helper function for getting custom values for Segment
|
|
604
|
+
Are useful with custom_tags_parser
|
|
605
|
+
"""
|
|
606
|
+
if "segment" not in state:
|
|
607
|
+
return default
|
|
608
|
+
if "custom_parser_values" not in state["segment"]:
|
|
609
|
+
return default
|
|
610
|
+
return state["segment"]["custom_parser_values"].get(key, default)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def save_segment_custom_value(state, key, value):
|
|
614
|
+
"""
|
|
615
|
+
Helper function for saving custom values for Segment
|
|
616
|
+
Are useful with custom_tags_parser
|
|
617
|
+
"""
|
|
618
|
+
if "segment" not in state:
|
|
619
|
+
state["segment"] = {}
|
|
620
|
+
|
|
621
|
+
if "custom_parser_values" not in state["segment"]:
|
|
622
|
+
state["segment"]["custom_parser_values"] = {}
|
|
623
|
+
|
|
624
|
+
state["segment"]["custom_parser_values"][key] = value
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
# Attribute parser constants (built once)
|
|
628
|
+
STREAM_INF_ATTRIBUTE_PARSER = remove_quotes_parser(
|
|
629
|
+
"codecs",
|
|
630
|
+
"audio",
|
|
631
|
+
"video",
|
|
632
|
+
"video_range",
|
|
633
|
+
"subtitles",
|
|
634
|
+
"pathway_id",
|
|
635
|
+
"stable_variant_id",
|
|
636
|
+
)
|
|
637
|
+
STREAM_INF_ATTRIBUTE_PARSER.update(
|
|
638
|
+
{
|
|
639
|
+
"program_id": int,
|
|
640
|
+
"bandwidth": lambda x: int(float(x)),
|
|
641
|
+
"average_bandwidth": int,
|
|
642
|
+
"frame_rate": float,
|
|
643
|
+
"hdcp_level": str,
|
|
644
|
+
}
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
IFRAME_STREAM_INF_ATTRIBUTE_PARSER = remove_quotes_parser(
|
|
648
|
+
"codecs", "uri", "pathway_id", "stable_variant_id"
|
|
649
|
+
)
|
|
650
|
+
IFRAME_STREAM_INF_ATTRIBUTE_PARSER.update(
|
|
651
|
+
{
|
|
652
|
+
"program_id": int,
|
|
653
|
+
"bandwidth": int,
|
|
654
|
+
"average_bandwidth": int,
|
|
655
|
+
"hdcp_level": str,
|
|
656
|
+
}
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
IMAGE_STREAM_INF_ATTRIBUTE_PARSER = remove_quotes_parser(
|
|
660
|
+
"codecs", "uri", "pathway_id", "stable_variant_id"
|
|
661
|
+
)
|
|
662
|
+
IMAGE_STREAM_INF_ATTRIBUTE_PARSER.update(
|
|
663
|
+
{
|
|
664
|
+
"program_id": int,
|
|
665
|
+
"bandwidth": int,
|
|
666
|
+
"average_bandwidth": int,
|
|
667
|
+
"resolution": str,
|
|
668
|
+
}
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
MEDIA_ATTRIBUTE_PARSER = remove_quotes_parser(
|
|
672
|
+
"uri",
|
|
673
|
+
"group_id",
|
|
674
|
+
"language",
|
|
675
|
+
"assoc_language",
|
|
676
|
+
"name",
|
|
677
|
+
"instream_id",
|
|
678
|
+
"characteristics",
|
|
679
|
+
"channels",
|
|
680
|
+
"stable_rendition_id",
|
|
681
|
+
"thumbnails",
|
|
682
|
+
"image",
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
X_MAP_ATTRIBUTE_PARSER = remove_quotes_parser("uri", "byterange")
|
|
686
|
+
|
|
687
|
+
START_ATTRIBUTE_PARSER = {"time_offset": lambda x: float(x)}
|
|
688
|
+
|
|
689
|
+
SERVER_CONTROL_ATTRIBUTE_PARSER = {
|
|
690
|
+
"can_block_reload": str,
|
|
691
|
+
"hold_back": lambda x: float(x),
|
|
692
|
+
"part_hold_back": lambda x: float(x),
|
|
693
|
+
"can_skip_until": lambda x: float(x),
|
|
694
|
+
"can_skip_dateranges": str,
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
PART_INF_ATTRIBUTE_PARSER = {"part_target": lambda x: float(x)}
|
|
698
|
+
|
|
699
|
+
RENDITION_REPORT_ATTRIBUTE_PARSER = remove_quotes_parser("uri")
|
|
700
|
+
RENDITION_REPORT_ATTRIBUTE_PARSER.update({"last_msn": int, "last_part": int})
|
|
701
|
+
|
|
702
|
+
PART_ATTRIBUTE_PARSER = remove_quotes_parser("uri")
|
|
703
|
+
PART_ATTRIBUTE_PARSER.update(
|
|
704
|
+
{"duration": lambda x: float(x), "independent": str, "gap": str, "byterange": str}
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
SKIP_ATTRIBUTE_PARSER = remove_quotes_parser("recently_removed_dateranges")
|
|
708
|
+
SKIP_ATTRIBUTE_PARSER.update({"skipped_segments": int})
|
|
709
|
+
|
|
710
|
+
SESSION_DATA_ATTRIBUTE_PARSER = remove_quotes_parser(
|
|
711
|
+
"data_id", "value", "uri", "language"
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
PRELOAD_HINT_ATTRIBUTE_PARSER = remove_quotes_parser("uri")
|
|
715
|
+
PRELOAD_HINT_ATTRIBUTE_PARSER.update(
|
|
716
|
+
{"type": str, "byterange_start": int, "byterange_length": int}
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
DATERANGE_ATTRIBUTE_PARSER = remove_quotes_parser(
|
|
720
|
+
"id", "class", "start_date", "end_date"
|
|
721
|
+
)
|
|
722
|
+
DATERANGE_ATTRIBUTE_PARSER.update(
|
|
723
|
+
{
|
|
724
|
+
"duration": float,
|
|
725
|
+
"planned_duration": float,
|
|
726
|
+
"end_on_next": str,
|
|
727
|
+
"scte35_cmd": str,
|
|
728
|
+
"scte35_out": str,
|
|
729
|
+
"scte35_in": str,
|
|
730
|
+
}
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
CONTENT_STEERING_ATTRIBUTE_PARSER = remove_quotes_parser("server_uri", "pathway_id")
|
|
734
|
+
|
|
735
|
+
CUEOUT_CONT_ATTRIBUTE_PARSER = remove_quotes_parser("duration", "elapsedtime", "scte35")
|
|
736
|
+
|
|
737
|
+
CUEOUT_ATTRIBUTE_PARSER = remove_quotes_parser("cue")
|
|
738
|
+
|
|
739
|
+
TILES_ATTRIBUTE_PARSER = remove_quotes_parser("uri")
|
|
740
|
+
TILES_ATTRIBUTE_PARSER.update({"resolution": str, "layout": str, "duration": float})
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
# Single token-to-handler dispatch to avoid a long startswith chain
|
|
744
|
+
DISPATCH = {
|
|
745
|
+
protocol.ext_x_byterange: _parse_byterange,
|
|
746
|
+
protocol.ext_x_bitrate: _parse_bitrate,
|
|
747
|
+
protocol.ext_x_targetduration: _parse_targetduration,
|
|
748
|
+
protocol.ext_x_media_sequence: _parse_media_sequence,
|
|
749
|
+
protocol.ext_x_discontinuity_sequence: _parse_discontinuity_sequence,
|
|
750
|
+
protocol.ext_x_program_date_time: _parse_program_date_time,
|
|
751
|
+
protocol.ext_x_discontinuity: _parse_discontinuity,
|
|
752
|
+
protocol.ext_x_cue_out_cont: _parse_cueout_cont,
|
|
753
|
+
protocol.ext_x_cue_out: _parse_cueout,
|
|
754
|
+
protocol.ext_oatcls_scte35: _parse_oatcls_scte35,
|
|
755
|
+
protocol.ext_x_asset: _parse_asset,
|
|
756
|
+
protocol.ext_x_cue_in: _parse_cue_in,
|
|
757
|
+
protocol.ext_x_cue_span: _parse_cue_span,
|
|
758
|
+
protocol.ext_x_version: _parse_version,
|
|
759
|
+
protocol.ext_x_allow_cache: _parse_allow_cache,
|
|
760
|
+
protocol.ext_x_key: _parse_key,
|
|
761
|
+
protocol.extinf: _parse_extinf,
|
|
762
|
+
protocol.ext_x_stream_inf: _parse_stream_inf,
|
|
763
|
+
protocol.ext_x_i_frame_stream_inf: _parse_i_frame_stream_inf,
|
|
764
|
+
protocol.ext_x_media: _parse_media,
|
|
765
|
+
protocol.ext_x_playlist_type: _parse_playlist_type,
|
|
766
|
+
protocol.ext_i_frames_only: _parse_i_frames_only,
|
|
767
|
+
protocol.ext_is_independent_segments: _parse_is_independent_segments,
|
|
768
|
+
protocol.ext_x_endlist: _parse_endlist,
|
|
769
|
+
protocol.ext_x_map: _parse_x_map,
|
|
770
|
+
protocol.ext_x_start: _parse_start,
|
|
771
|
+
protocol.ext_x_server_control: _parse_server_control,
|
|
772
|
+
protocol.ext_x_part_inf: _parse_part_inf,
|
|
773
|
+
protocol.ext_x_rendition_report: _parse_rendition_report,
|
|
774
|
+
protocol.ext_x_part: _parse_part,
|
|
775
|
+
protocol.ext_x_skip: _parse_skip,
|
|
776
|
+
protocol.ext_x_session_data: _parse_session_data,
|
|
777
|
+
protocol.ext_x_session_key: _parse_session_key,
|
|
778
|
+
protocol.ext_x_preload_hint: _parse_preload_hint,
|
|
779
|
+
protocol.ext_x_daterange: _parse_daterange,
|
|
780
|
+
protocol.ext_x_gap: _parse_gap,
|
|
781
|
+
protocol.ext_x_content_steering: _parse_content_steering,
|
|
782
|
+
protocol.ext_x_image_stream_inf: _parse_image_stream_inf,
|
|
783
|
+
protocol.ext_x_images_only: _parse_is_images_only,
|
|
784
|
+
protocol.ext_x_tiles: _parse_tiles,
|
|
785
|
+
protocol.ext_x_blackout: _parse_blackout,
|
|
786
|
+
}
|