From 815a1d4751efc436c2058019f9185d889fd79689 Mon Sep 17 00:00:00 2001 From: birme Date: Fri, 2 Sep 2016 11:35:43 +0200 Subject: [PATCH 1/6] Revert cue-out fix that was commited to master --- m3u8/parser.py | 2 +- m3u8/protocol.py | 1 - tests/test_parser.py | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/m3u8/parser.py b/m3u8/parser.py index f7c30f01..a0b7f5d0 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -75,7 +75,7 @@ def parse(content, strict=False): elif line.startswith(protocol.ext_x_discontinuity): state['discontinuity'] = True - elif line.startswith(protocol.ext_x_cue_out) or line.startswith(protocol.ext_x_cue_out_start): + elif line.startswith(protocol.ext_x_cue_out): _parse_cueout(line, state) state['cue_out'] = True state['cue_start'] = True diff --git a/m3u8/protocol.py b/m3u8/protocol.py index 15365a73..bfa59929 100644 --- a/m3u8/protocol.py +++ b/m3u8/protocol.py @@ -18,7 +18,6 @@ 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' diff --git a/tests/test_parser.py b/tests/test_parser.py index d21af03c..76754c48 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -176,8 +176,7 @@ def test_should_parse_program_date_time_from_playlist(): def test_should_parse_scte35_from_playlist(): data = m3u8.parse(playlists.CUE_OUT_WITH_SCTE35_PLAYLIST) - assert not data['segments'][2]['cue_out'] - assert data['segments'][3]['cue_out'] + assert not data['segments'][3]['cue_out'] assert '/DAlAAAAAAAAAP/wFAUAAAABf+//wpiQkv4ARKogAAEBAQAAQ6sodg==' == data['segments'][4]['scte35'] assert '50' == data['segments'][4]['scte35_duration'] From 3c50bf4a224dfbe5bd231a53724885d1f8d2b718 Mon Sep 17 00:00:00 2001 From: birme Date: Wed, 5 Oct 2016 15:09:41 +0200 Subject: [PATCH 2/6] Missing ext_x_cue_out_start --- m3u8/protocol.py | 1 + 1 file changed, 1 insertion(+) diff --git a/m3u8/protocol.py b/m3u8/protocol.py index bfa59929..15365a73 100644 --- a/m3u8/protocol.py +++ b/m3u8/protocol.py @@ -18,6 +18,7 @@ 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' From 5a94b06333ddc120efbe9fcf68a1973891f01c36 Mon Sep 17 00:00:00 2001 From: birme Date: Wed, 5 Oct 2016 15:39:41 +0200 Subject: [PATCH 3/6] Handle Envivio flavor of cue markers --- m3u8/parser.py | 15 ++++++++++++--- m3u8/protocol.py | 1 + tests/playlists.py | 28 ++++++++++++++++++++++++++++ tests/test_parser.py | 8 ++++++++ 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/m3u8/parser.py b/m3u8/parser.py index 906fb56f..447c338c 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -94,6 +94,10 @@ def parse(content, strict=False): state['cue_out'] = True state['cue_start'] = 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) @@ -280,15 +284,20 @@ def _parse_cueout(line, state): state['current_cue_out_duration'] = res.group(1) state['current_cue_out_scte35'] = res.group(2) - def _parse_cueout_start(line, state, prevline): param, value = line.split(':', 1) state['current_cue_out_duration'] = value + # Try Elemental flavor first res = re.match('.*EXT-OATCLS-SCTE35:(.*)$', prevline) if res: state['current_cue_out_scte35'] = res.group(1) - - + else: + # Then try Envivio flavor + res2 = re.match('.*DURATION=(.*),.*,CUE="(.*)"', value) + if res2: + state['current_cue_out_duration'] = res2.group(1) + state['current_cue_out_scte35'] = res2.group(2) + def string_to_lines(string): return string.strip().replace('\r\n', '\n').split('\n') diff --git a/m3u8/protocol.py b/m3u8/protocol.py index 15365a73..2fb3be56 100644 --- a/m3u8/protocol.py +++ b/m3u8/protocol.py @@ -24,3 +24,4 @@ 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_span = '#EXT-X-CUE-SPAN' diff --git a/tests/playlists.py b/tests/playlists.py index 651b7f1a..b04d5124 100755 --- a/tests/playlists.py +++ b/tests/playlists.py @@ -511,6 +511,34 @@ master2500_47233.ts ''' +CUE_OUT_ENVIVIO_PLAYLIST = ''' +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:11 +#EXT-X-MEDIA-SEQUENCE:399703 +#EXTINF:10.0000, +20160914T080055-master804-199/1703.ts +#EXTINF:10.0000, +20160914T080055-master804-199/1704.ts +#EXTINF:5.1200, +20160914T080055-master804-199/1705.ts +#EXT-X-CUE-OUT:DURATION=366,ID=16777323,CUE="/DAlAAAENOOQAP/wFAUBAABrf+//N25XDf4B9p/gAAEBAQAAxKni9A==" +#EXTINF:10.0000, +20160914T080055-master804-199/1706.ts +#EXT-X-CUE-SPAN:TIMEFROMSIGNAL=PT10S,ID=16777323 +#EXTINF:10.0000, +20160914T080055-master804-199/1707.ts +#EXT-X-CUE-SPAN:TIMEFROMSIGNAL=PT20S,ID=16777323 +#EXTINF:10.0000, +20160914T080055-master804-199/1708.ts +#EXT-X-CUE-SPAN:TIMEFROMSIGNAL=PT30S,ID=16777323 +#EXTINF:10.0000, +20160914T080055-master804-199/1709.ts +#EXT-X-CUE-IN:ID=16777323 +#EXTINF:10.0000, +20160914T080055-master804-199/1710.ts +''' + 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 diff --git a/tests/test_parser.py b/tests/test_parser.py index 0ca6cfda..cedfe8bc 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -196,6 +196,14 @@ def test_should_parse_scte35_from_playlist(): assert '/DAlAAAAAAAAAP/wFAUAAAABf+//wpiQkv4ARKogAAEBAQAAQ6sodg==' == data['segments'][4]['scte35'] assert '50' == data['segments'][4]['scte35_duration'] +def test_should_parse_envivio_cue_playlist(): + data = m3u8.parse(playlists.CUE_OUT_ENVIVIO_PLAYLIST) + assert data['segments'][3]['scte35'] + assert data['segments'][3]['cue_out'] + assert '/DAlAAAENOOQAP/wFAUBAABrf+//N25XDf4B9p/gAAEBAQAAxKni9A==' == data['segments'][3]['scte35'] + assert '366' == data['segments'][3]['scte35_duration'] + assert data['segments'][4]['cue_out'] + def test_parse_simple_playlist_messy(): data = m3u8.parse(playlists.SIMPLE_PLAYLIST_MESSY) assert 5220 == data['targetduration'] From 972337e1bf96cfb3d4afc5060a645a43500b8289 Mon Sep 17 00:00:00 2001 From: birme Date: Wed, 5 Oct 2016 16:07:33 +0200 Subject: [PATCH 4/6] Add more tests for Envivio cue markers --- tests/test_model.py | 7 +++++++ tests/test_parser.py | 2 ++ 2 files changed, 9 insertions(+) diff --git a/tests/test_model.py b/tests/test_model.py index 2d3b6829..5352b0dc 100755 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -80,6 +80,13 @@ def test_segment_scte35_attribute(): assert segments[9].cue_out == False assert segments[4].scte35 == '/DAlAAAAAAAAAP/wFAUAAAABf+//wpiQkv4ARKogAAEBAQAAQ6sodg==' +def test_segment_envivio_scte35_attribute(): + obj = m3u8.M3U8(playlists.CUE_OUT_ENVIVIO_PLAYLIST) + segments = obj.segments + assert segments[3].cue_out == True + assert segments[4].scte35 == '/DAlAAAENOOQAP/wFAUBAABrf+//N25XDf4B9p/gAAEBAQAAxKni9A==' + assert segments[5].scte35 == '/DAlAAAENOOQAP/wFAUBAABrf+//N25XDf4B9p/gAAEBAQAAxKni9A==' + assert segments[7].cue_out == False def test_keys_on_clear_playlist(): obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST) diff --git a/tests/test_parser.py b/tests/test_parser.py index cedfe8bc..82274913 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -203,6 +203,8 @@ def test_should_parse_envivio_cue_playlist(): assert '/DAlAAAENOOQAP/wFAUBAABrf+//N25XDf4B9p/gAAEBAQAAxKni9A==' == data['segments'][3]['scte35'] assert '366' == data['segments'][3]['scte35_duration'] assert data['segments'][4]['cue_out'] + assert '/DAlAAAENOOQAP/wFAUBAABrf+//N25XDf4B9p/gAAEBAQAAxKni9A==' == data['segments'][4]['scte35'] + assert '/DAlAAAENOOQAP/wFAUBAABrf+//N25XDf4B9p/gAAEBAQAAxKni9A==' == data['segments'][5]['scte35'] def test_parse_simple_playlist_messy(): data = m3u8.parse(playlists.SIMPLE_PLAYLIST_MESSY) From 0c8cea3183bf4771b58a14a523b3c61627f9837d Mon Sep 17 00:00:00 2001 From: birme Date: Wed, 5 Oct 2016 17:11:34 +0200 Subject: [PATCH 5/6] Made a more clear separation between Elemental and Envivio implementations --- m3u8/parser.py | 25 +++++++++++++++++-------- tests/playlists.py | 2 +- tests/test_model.py | 4 ++-- tests/test_parser.py | 2 +- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/m3u8/parser.py b/m3u8/parser.py index 447c338c..c37cceec 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -284,19 +284,28 @@ def _parse_cueout(line, state): state['current_cue_out_duration'] = res.group(1) state['current_cue_out_scte35'] = res.group(2) -def _parse_cueout_start(line, state, prevline): +def _cueout_elemental(line, state, prevline): param, value = line.split(':', 1) - state['current_cue_out_duration'] = value - # Try Elemental flavor first res = re.match('.*EXT-OATCLS-SCTE35:(.*)$', prevline) if res: state['current_cue_out_scte35'] = res.group(1) + state['current_cue_out_duration'] = value + return True else: - # Then try Envivio flavor - res2 = re.match('.*DURATION=(.*),.*,CUE="(.*)"', value) - if res2: - state['current_cue_out_duration'] = res2.group(1) - state['current_cue_out_scte35'] = res2.group(2) + return False + +def _cueout_envivio(line, state, prevline): + param, value = line.split(':', 1) + res = re.match('.*DURATION=(.*),.*,CUE="(.*)"', value) + if res: + state['current_cue_out_duration'] = res.group(1) + state['current_cue_out_scte35'] = res.group(2) + return True + else: + return False + +def _parse_cueout_start(line, state, prevline): + _cueout_elemental(line, state, prevline) or _cueout_envivio(line, state, prevline) def string_to_lines(string): return string.strip().replace('\r\n', '\n').split('\n') diff --git a/tests/playlists.py b/tests/playlists.py index b04d5124..bad5da0b 100755 --- a/tests/playlists.py +++ b/tests/playlists.py @@ -476,7 +476,7 @@ ''' -CUE_OUT_WITH_SCTE35_PLAYLIST = ''' +CUE_OUT_ELEMENTAL_PLAYLIST = ''' #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 diff --git a/tests/test_model.py b/tests/test_model.py index 5352b0dc..02d0f1d6 100755 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -73,8 +73,8 @@ def test_segment_cue_out_attribute(): assert segments[3].cue_out == False -def test_segment_scte35_attribute(): - obj = m3u8.M3U8(playlists.CUE_OUT_WITH_SCTE35_PLAYLIST) +def test_segment_elemental_scte35_attribute(): + obj = m3u8.M3U8(playlists.CUE_OUT_ELEMENTAL_PLAYLIST) segments = obj.segments assert segments[4].cue_out == True assert segments[9].cue_out == False diff --git a/tests/test_parser.py b/tests/test_parser.py index 82274913..b5355bbc 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -189,7 +189,7 @@ def test_should_parse_program_date_time_from_playlist(): assert cast_date_time('2014-08-13T13:36:33+00:00') == data['program_date_time'] def test_should_parse_scte35_from_playlist(): - data = m3u8.parse(playlists.CUE_OUT_WITH_SCTE35_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]['cue_out'] From e130c0f85c16ec088c4730a3d40913048a6d34b0 Mon Sep 17 00:00:00 2001 From: birme Date: Wed, 5 Oct 2016 19:04:19 +0200 Subject: [PATCH 6/6] A more functional style implementation --- m3u8/parser.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/m3u8/parser.py b/m3u8/parser.py index c37cceec..21e6207a 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -288,24 +288,23 @@ def _cueout_elemental(line, state, prevline): param, value = line.split(':', 1) res = re.match('.*EXT-OATCLS-SCTE35:(.*)$', prevline) if res: - state['current_cue_out_scte35'] = res.group(1) - state['current_cue_out_duration'] = value - return True + return (res.group(1), value) else: - return False + return None def _cueout_envivio(line, state, prevline): param, value = line.split(':', 1) res = re.match('.*DURATION=(.*),.*,CUE="(.*)"', value) if res: - state['current_cue_out_duration'] = res.group(1) - state['current_cue_out_scte35'] = res.group(2) - return True + return (res.group(2), res.group(1)) else: - return False + return None def _parse_cueout_start(line, state, prevline): - _cueout_elemental(line, state, prevline) or _cueout_envivio(line, state, prevline) + _cueout_state = _cueout_elemental(line, state, prevline) or _cueout_envivio(line, state, prevline) + if _cueout_state: + state['current_cue_out_scte35'] = _cueout_state[0] + state['current_cue_out_duration'] = _cueout_state[1] def string_to_lines(string): return string.strip().replace('\r\n', '\n').split('\n')