Skip to content

Commit

Permalink
Add support for EXT-OATCLS-SCTE35
Browse files Browse the repository at this point in the history
  • Loading branch information
bbayles authored and mauricioabreu committed Aug 5, 2022
1 parent c99440e commit 69aee28
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 24 deletions.
16 changes: 13 additions & 3 deletions m3u8/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
import os
import errno

from m3u8.protocol import ext_x_start, ext_x_key, ext_x_session_key, ext_x_map
from m3u8.protocol import (
ext_x_key,
ext_x_map,
ext_oatcls_scte35,
ext_x_session_key,
ext_x_start,
)
from m3u8.parser import parse, format_date_time
from m3u8.mixins import BasePathMixin, GroupedBasePathMixin

Expand Down Expand Up @@ -443,8 +449,8 @@ class Segment(BasePathMixin):
def __init__(self, uri=None, base_uri=None, program_date_time=None, current_program_date_time=None,
duration=None, title=None, bitrate=None, byterange=None, cue_out=False,
cue_out_start=False, cue_in=False, discontinuity=False, key=None, scte35=None,
scte35_duration=None, scte35_elapsedtime=None, keyobject=None, parts=None,
init_section=None, dateranges=None, gap_tag=None, custom_parser_values=None):
oatcls_scte35=None, scte35_duration=None, scte35_elapsedtime=None, keyobject=None,
parts=None, init_section=None, dateranges=None, gap_tag=None, custom_parser_values=None):
self.uri = uri
self.duration = duration
self.title = title
Expand All @@ -458,6 +464,7 @@ def __init__(self, uri=None, base_uri=None, program_date_time=None, current_prog
self.cue_out = cue_out
self.cue_in = cue_in
self.scte35 = scte35
self.oatcls_scte35 = oatcls_scte35
self.scte35_duration = scte35_duration
self.scte35_elapsedtime = scte35_elapsedtime
self.key = keyobject
Expand Down Expand Up @@ -506,6 +513,9 @@ def dumps(self, last_segment, timespec='milliseconds'):
output.append(str(self.dateranges))
output.append('\n')

if self.oatcls_scte35:
output.append(f'{ext_oatcls_scte35}:{self.oatcls_scte35}\n')

if self.cue_out_start:
output.append('#EXT-X-CUE-OUT{}\n'.format(
(':' + self.scte35_duration) if self.scte35_duration else ''))
Expand Down
38 changes: 19 additions & 19 deletions m3u8/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,13 @@ def parse(content, strict=False, custom_tags_parser=None):
state['cue_out'] = True

elif line.startswith(protocol.ext_x_cue_out):
_parse_cueout(line, state, string_to_lines(content)[lineno - 2])
_parse_cueout(line, state)
state['cue_out_start'] = True
state['cue_out'] = True

elif line.startswith(f'{protocol.ext_oatcls_scte35}:'):
_parse_oatcls_scte35(line, state)

elif line.startswith(protocol.ext_x_cue_in):
state['cue_in'] = True

Expand Down Expand Up @@ -269,6 +272,7 @@ def _parse_ts_chunk(line, data, state):
segment['cue_out_start'] = state.pop('cue_out_start', False)
scte_op = state.pop if segment['cue_in'] else state.get
segment['scte35'] = scte_op('current_cue_out_scte35', None)
segment['oatcls_scte35'] = scte_op('current_cue_out_oatcls_scte35', None)
segment['scte35_duration'] = scte_op('current_cue_out_duration', None)
segment['scte35_elapsedtime'] = scte_op('current_cue_out_elapsedtime', None)
segment['discontinuity'] = state.pop('discontinuity', False)
Expand Down Expand Up @@ -397,15 +401,7 @@ def _cueout_no_duration(line):
if line == protocol.ext_x_cue_out:
return (None, None)

def _cueout_elemental(line, state, prevline):
param, value = line.split(':', 1)
res = re.match('.*EXT-OATCLS-SCTE35:(.*)$', prevline)
if res:
return (res.group(1), value)
else:
return None

def _cueout_envivio(line, state, prevline):
def _cueout_envivio(line, state):
param, value = line.split(':', 1)
res = re.match('.*DURATION=(.*),.*,CUE="(.*)"', value)
if res:
Expand All @@ -414,31 +410,28 @@ def _cueout_envivio(line, state, prevline):
return None

def _cueout_duration(line):
# this needs to be called after _cueout_elemental
# as it would capture those cues incompletely
# This was added separately rather than modifying "simple"
param, value = line.split(':', 1)
res = re.match(r'DURATION=(.*)', value)
if res:
return (None, res.group(1))

def _cueout_simple(line):
# this needs to be called after _cueout_elemental
# as it would capture those cues incompletely
param, value = line.split(':', 1)
res = re.match(r'^(\d+(?:\.\d)?\d*)$', value)
if res:
return (None, res.group(1))

def _parse_cueout(line, state, prevline):
def _parse_cueout(line, state):
_cueout_state = (_cueout_no_duration(line)
or _cueout_elemental(line, state, prevline)
or _cueout_envivio(line, state, prevline)
or _cueout_envivio(line, state)
or _cueout_duration(line)
or _cueout_simple(line))
if _cueout_state:
state['current_cue_out_scte35'] = _cueout_state[0]
state['current_cue_out_duration'] = _cueout_state[1]
cue_out_scte35, cue_out_duration = _cueout_state
current_cue_out_scte35 = state.get('current_cue_out_scte35')
state['current_cue_out_scte35'] = cue_out_scte35 or current_cue_out_scte35
state['current_cue_out_duration'] = cue_out_duration

def _parse_server_control(line, data, state):
attribute_parser = {
Expand Down Expand Up @@ -552,6 +545,13 @@ def _parse_content_steering(line, data, state):
protocol.ext_x_content_steering, line, attribute_parser
)


def _parse_oatcls_scte35(line, state):
scte35_cue = line.split(':', 1)[1]
state['current_cue_out_oatcls_scte35'] = scte35_cue
state['current_cue_out_scte35'] = scte35_cue


def string_to_lines(string):
return string.strip().splitlines()

Expand Down
2 changes: 1 addition & 1 deletion m3u8/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
ext_x_cue_out_cont = '#EXT-X-CUE-OUT-CONT'
ext_x_cue_in = '#EXT-X-CUE-IN'
ext_x_cue_span = '#EXT-X-CUE-SPAN'
ext_x_scte35 = '#EXT-OATCLS-SCTE35'
ext_oatcls_scte35 = '#EXT-OATCLS-SCTE35'
ext_is_independent_segments = '#EXT-X-INDEPENDENT-SEGMENTS'
ext_x_map = '#EXT-X-MAP'
ext_x_start = '#EXT-X-START'
Expand Down
9 changes: 9 additions & 0 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,15 @@ def test_segment_cue_out_cont_attributes_dumps():
)
assert expected in result

def test_segment_oatcls_scte35_dumps():
obj = m3u8.M3U8(playlists.CUE_OUT_ELEMENTAL_PLAYLIST)

result = obj.dumps()
expected = (
'#EXT-OATCLS-SCTE35:/DAlAAAAAAAAAP/wFAUAAAABf+//wpiQkv4ARKogAAEBAQAAQ6sodg==\n'
)
assert expected in result

def test_segment_cue_out_start_dumps():
obj = m3u8.M3U8(playlists.CUE_OUT_WITH_DURATION_PLAYLIST)

Expand Down
3 changes: 2 additions & 1 deletion tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ def test_should_parse_program_date_time_from_playlist():
def test_should_parse_scte35_from_playlist():
data = m3u8.parse(playlists.CUE_OUT_ELEMENTAL_PLAYLIST)
assert not data['segments'][2]['cue_out']
assert data['segments'][3]['scte35']
assert data['segments'][3]['scte35'] == '/DAlAAAAAAAAAP/wFAUAAAABf+//wpiQkv4ARKogAAEBAQAAQ6sodg=='
assert data['segments'][3]['oatcls_scte35'] == '/DAlAAAAAAAAAP/wFAUAAAABf+//wpiQkv4ARKogAAEBAQAAQ6sodg=='
assert data['segments'][3]['cue_out']
assert '/DAlAAAAAAAAAP/wFAUAAAABf+//wpiQkv4ARKogAAEBAQAAQ6sodg==' == data['segments'][4]['scte35']
assert '50' == data['segments'][4]['scte35_duration']
Expand Down

0 comments on commit 69aee28

Please sign in to comment.