Skip to content

Commit

Permalink
SCTE35 CUE-OUT and CUE-IN support
Browse files Browse the repository at this point in the history
  • Loading branch information
davemevans authored and mauricioabreu committed Nov 15, 2019
1 parent 13b3fd7 commit 4eddce1
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 24 deletions.
5 changes: 2 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,10 @@ Supported tags
* `#EXT-X-DISCONTINUITY`_
* #EXT-X-CUE-OUT
* #EXT-X-CUE-OUT-CONT
* `#EXT-X-INDEPENDENT-SEGMENTS`_
* #EXT-OATCLS-SCTE35
* #EXT-X-CUE-OUT
* #EXT-X-CUE-IN
* #EXT-X-CUE-SPAN
* #EXT-OATCLS-SCTE35
* `#EXT-X-INDEPENDENT-SEGMENTS`_
* `#EXT-X-MAP`_
* `#EXT-X-START`_
* #EXT-X-SERVER-CONTROL
Expand Down
23 changes: 20 additions & 3 deletions m3u8/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,17 @@ class Segment(BasePathMixin):
Returns a boolean indicating if a EXT-X-DISCONTINUITY tag exists
http://tools.ietf.org/html/draft-pantos-http-live-streaming-13#section-3.4.11
`cue_out_start`
Returns a boolean indicating if a EXT-X-CUE-OUT tag exists
`cue_out`
Returns a boolean indicating if a EXT-X-CUE-OUT-CONT tag exists
Note: for backwards compatibility, this will be True when cue_out_start
is True, even though this tag did not exist in the input, and
EXT-X-CUE-OUT-CONT will not exist in the output
`cue_in`
Returns a boolean indicating if a EXT-X-CUE-IN tag exists
`scte35`
Base64 encoded SCTE35 metadata if available
Expand All @@ -399,8 +408,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, byterange=None, cue_out=False,
discontinuity=False, key=None, scte35=None, scte35_duration=None,
duration=None, title=None, byterange=None, cue_out=False, cue_out_start=False,
cue_in=False, discontinuity=False, key=None, scte35=None, scte35_duration=None,
keyobject=None, parts=None):
self.uri = uri
self.duration = duration
Expand All @@ -410,7 +419,9 @@ def __init__(self, uri=None, base_uri=None, program_date_time=None, current_prog
self.program_date_time = program_date_time
self.current_program_date_time = current_program_date_time
self.discontinuity = discontinuity
self.cue_out_start = cue_out_start
self.cue_out = cue_out
self.cue_in = cue_in
self.scte35 = scte35
self.scte35_duration = scte35_duration
self.key = keyobject
Expand Down Expand Up @@ -439,8 +450,14 @@ def dumps(self, last_segment):
if self.program_date_time:
output.append('#EXT-X-PROGRAM-DATE-TIME:%s\n' %
format_date_time(self.program_date_time))
if self.cue_out:

if self.cue_out_start:
output.append('#EXT-X-CUE-OUT{}\n'.format(
(':' + self.scte35_duration) if self.scte35_duration else ''))
elif self.cue_out:
output.append('#EXT-X-CUE-OUT-CONT\n')
if self.cue_in:
output.append('#EXT-X-CUE-IN\n')

if self.parts:
output.append(str(self.parts))
Expand Down
41 changes: 31 additions & 10 deletions m3u8/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,20 @@ def parse(content, strict=False, custom_tags_parser=None):
elif line.startswith(protocol.ext_x_discontinuity):
state['discontinuity'] = True

elif line.startswith(protocol.ext_x_cue_out):
_parse_cueout(line, state)
elif line.startswith(protocol.ext_x_cue_out_cont):
_parse_cueout_cont(line, state)
state['cue_out'] = True
state['cue_start'] = True

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

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

elif line.startswith(protocol.ext_x_cue_span):
state['cue_out'] = True
state['cue_start'] = True

elif line.startswith(protocol.ext_x_version):
_parse_simple_parameter(line, data, int)
Expand Down Expand Up @@ -237,9 +238,12 @@ def _parse_ts_chunk(line, data, state):
segment['current_program_date_time'] = state['current_program_date_time']
state['current_program_date_time'] += datetime.timedelta(seconds=segment['duration'])
segment['uri'] = line
segment['cue_in'] = state.pop('cue_in', False)
segment['cue_out'] = state.pop('cue_out', False)
segment['cue_out_start'] = state.pop('cue_out_start', False)
if state.get('current_cue_out_scte35'):
segment['scte35'] = state['current_cue_out_scte35']
if state.get('current_cue_out_duration'):
segment['scte35_duration'] = state['current_cue_out_duration']
segment['discontinuity'] = state.pop('discontinuity', False)
if state.get('current_key'):
Expand Down Expand Up @@ -325,13 +329,19 @@ def _parse_simple_parameter(line, data, cast_to=str):
return _parse_and_set_simple_parameter_raw_value(line, data, cast_to, True)


def _parse_cueout(line, state):
def _parse_cueout_cont(line, state):
param, value = line.split(':', 1)
res = re.match('.*Duration=(.*),SCTE35=(.*)$', value)
if res:
state['current_cue_out_duration'] = res.group(1)
state['current_cue_out_scte35'] = res.group(2)

def _cueout_no_duration(line):
# this needs to be called first since line.split in all other
# parsers will throw a ValueError if passed just this tag
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)
Expand All @@ -348,8 +358,19 @@ def _cueout_envivio(line, state, prevline):
else:
return None

def _parse_cueout_start(line, state, prevline):
_cueout_state = _cueout_elemental(line, state, prevline) or _cueout_envivio(line, state, prevline)
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('^(\d+(?:\.\d)?\d*)$', value)
if res:
return (None, res.group(1))

def _parse_cueout(line, state, prevline):
_cueout_state = (_cueout_no_duration(line)
or _cueout_elemental(line, state, prevline)
or _cueout_envivio(line, state, prevline)
or _cueout_simple(line))
if _cueout_state:
state['current_cue_out_scte35'] = _cueout_state[0]
state['current_cue_out_duration'] = _cueout_state[1]
Expand Down
11 changes: 5 additions & 6 deletions m3u8/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@
ext_x_byterange = '#EXT-X-BYTERANGE'
ext_x_i_frame_stream_inf = '#EXT-X-I-FRAME-STREAM-INF'
ext_x_discontinuity = '#EXT-X-DISCONTINUITY'
ext_x_cue_out_start = '#EXT-X-CUE-OUT'
ext_x_cue_out = '#EXT-X-CUE-OUT-CONT'
ext_is_independent_segments = '#EXT-X-INDEPENDENT-SEGMENTS'
ext_x_scte35 = '#EXT-OATCLS-SCTE35'
ext_x_cue_start = '#EXT-X-CUE-OUT'
ext_x_cue_end = '#EXT-X-CUE-IN'
ext_x_cue_out = '#EXT-X-CUE-OUT'
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_is_independent_segments = '#EXT-X-INDEPENDENT-SEGMENTS'
ext_x_map = '#EXT-X-MAP'
ext_x_start = '#EXT-X-START'
ext_x_server_control = '#EXT-X-SERVER-CONTROL'
Expand Down
24 changes: 24 additions & 0 deletions tests/playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,30 @@
0.aac
'''

CUE_OUT_NO_DURATION_PLAYLIST = '''#EXTM3U
#EXT-X-TARGETDURATION:6
#EXT-X-CUE-OUT
#EXTINF:5.76,
0.aac
#EXTINF:5.76,
1.aac
#EXT-X-CUE-IN
#EXTINF:5.76,
2.aac
'''

CUE_OUT_WITH_DURATION_PLAYLIST = '''#EXTM3U
#EXT-X-TARGETDURATION:6
#EXT-X-CUE-OUT:11.52
#EXTINF:5.76,
0.aac
#EXTINF:5.76,
1.aac
#EXT-X-CUE-IN
#EXTINF:5.76,
2.aac
'''

MULTI_MEDIA_PLAYLIST = '''#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA:URI="chinese/ed.ttml",TYPE=SUBTITLES,GROUP-ID="subs",LANGUAGE="zho",NAME="Chinese",AUTOSELECT=YES,FORCED=NO
Expand Down
39 changes: 37 additions & 2 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,42 @@ def test_segment_cue_out_attribute():
assert segments[2].cue_out == True
assert segments[3].cue_out == False

def test_segment_cue_out_dumps():
def test_segment_cue_out_start_attribute():
obj = m3u8.M3U8(playlists.CUE_OUT_WITH_DURATION_PLAYLIST)

assert obj.segments[0].cue_out_start == True

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

assert obj.segments[2].cue_in == True

def test_segment_cue_out_cont_dumps():
obj = m3u8.M3U8(playlists.CUE_OUT_PLAYLIST)

result = obj.dumps()
expected = '#EXT-X-CUE-OUT'
expected = '#EXT-X-CUE-OUT-CONT\n'
assert expected in result

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

result = obj.dumps()
expected = '#EXT-X-CUE-OUT:11.52\n'
assert expected in result

def test_segment_cue_out_start_no_duration_dumps():
obj = m3u8.M3U8(playlists.CUE_OUT_NO_DURATION_PLAYLIST)

result = obj.dumps()
expected = '#EXT-X-CUE-OUT\n'
assert expected in result

def test_segment_cue_out_in_dumps():
obj = m3u8.M3U8(playlists.CUE_OUT_NO_DURATION_PLAYLIST)

result = obj.dumps()
expected = '#EXT-X-CUE-IN\n'
assert expected in result

def test_segment_elemental_scte35_attribute():
Expand All @@ -152,6 +182,11 @@ def test_segment_unknown_scte35_attribute():
assert obj.segments[0].scte35 == None
assert obj.segments[0].scte35_duration == None

def test_segment_cue_out_no_duration():
obj = m3u8.M3U8(playlists.CUE_OUT_NO_DURATION_PLAYLIST)
assert obj.segments[0].cue_out_start == True
assert obj.segments[2].cue_in == True

def test_keys_on_clear_playlist():
obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST)

Expand Down
5 changes: 5 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,11 @@ def test_should_parse_envivio_cue_playlist():
assert '/DAlAAAENOOQAP/wFAUBAABrf+//N25XDf4B9p/gAAEBAQAAxKni9A==' == data['segments'][4]['scte35']
assert '/DAlAAAENOOQAP/wFAUBAABrf+//N25XDf4B9p/gAAEBAQAAxKni9A==' == data['segments'][5]['scte35']

def test_should_parse_no_duration_cue_playlist():
data = m3u8.parse(playlists.CUE_OUT_NO_DURATION_PLAYLIST)
assert data['segments'][0]['cue_out_start']
assert data['segments'][2]['cue_in']

def test_parse_simple_playlist_messy():
data = m3u8.parse(playlists.SIMPLE_PLAYLIST_MESSY)
assert 5220 == data['targetduration']
Expand Down

0 comments on commit 4eddce1

Please sign in to comment.