From 5a2a3a9f8869cafd30a3527c4518a5f42590ea3d Mon Sep 17 00:00:00 2001 From: alexb Date: Wed, 8 Nov 2017 15:13:45 -0400 Subject: [PATCH 1/8] Added protocol and used _parse_key method --- m3u8/parser.py | 3 +++ m3u8/protocol.py | 1 + 2 files changed, 4 insertions(+) diff --git a/m3u8/parser.py b/m3u8/parser.py index a5c8779c..9e5801ed 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -141,6 +141,9 @@ def parse(content, strict=False): segment_map_info = _parse_attribute_list(protocol.ext_x_map, line, quoted_parser) data['segment_map'] = segment_map_info + elif line.startswith(protocol.ext_x_start): + key = _parse_key(line) + # Comments and whitespace elif line.startswith('#'): # comment diff --git a/m3u8/protocol.py b/m3u8/protocol.py index 5d2e678d..6ede5070 100644 --- a/m3u8/protocol.py +++ b/m3u8/protocol.py @@ -26,3 +26,4 @@ ext_x_cue_end = '#EXT-X-CUE-IN' ext_x_cue_span = '#EXT-X-CUE-SPAN' ext_x_map = '#EXT-X-MAP' +ext_x_start = '#EXT-X-START' From 60b23068f12dc2709dfb82d3680d573c3984c1d8 Mon Sep 17 00:00:00 2001 From: alexb Date: Wed, 8 Nov 2017 15:30:04 -0400 Subject: [PATCH 2/8] Added start to simple parameters and added it to dumps logic --- m3u8/model.py | 5 ++++- m3u8/parser.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/m3u8/model.py b/m3u8/model.py index 0ee22bb2..6f4ef745 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -131,7 +131,8 @@ class M3U8(object): ('is_independent_segments', 'is_independent_segments'), ('version', 'version'), ('allow_cache', 'allow_cache'), - ('playlist_type', 'playlist_type') + ('playlist_type', 'playlist_type'), + ('start', 'start') ) def __init__(self, content=None, base_path=None, base_uri=None, strict=False): @@ -251,6 +252,8 @@ def dumps(self): output.append('#EXT-X-PROGRAM-DATE-TIME:' + format_date_time(self.program_date_time)) if not (self.playlist_type is None or self.playlist_type == ''): output.append('#EXT-X-PLAYLIST-TYPE:%s' % str(self.playlist_type).upper()) + if self.start: + output.append('#EXT-X-START:' + self.start) if self.is_i_frames_only: output.append('#EXT-X-I-FRAMES-ONLY') if self.is_variant: diff --git a/m3u8/parser.py b/m3u8/parser.py index 9e5801ed..07871707 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -52,6 +52,7 @@ def parse(content, strict=False): 'iframe_playlists': [], 'media': [], 'keys': [], + 'start': None } state = { @@ -142,7 +143,7 @@ def parse(content, strict=False): data['segment_map'] = segment_map_info elif line.startswith(protocol.ext_x_start): - key = _parse_key(line) + _parse_simple_parameter(line, data) # Comments and whitespace elif line.startswith('#'): From 3a305dfe152eb3a9d14ca07a52820fe7cdcf2ee3 Mon Sep 17 00:00:00 2001 From: alexb Date: Wed, 8 Nov 2017 15:36:11 -0400 Subject: [PATCH 3/8] Uppercased start param --- m3u8/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/m3u8/model.py b/m3u8/model.py index 6f4ef745..0cc9e2ab 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -253,7 +253,7 @@ def dumps(self): if not (self.playlist_type is None or self.playlist_type == ''): output.append('#EXT-X-PLAYLIST-TYPE:%s' % str(self.playlist_type).upper()) if self.start: - output.append('#EXT-X-START:' + self.start) + output.append('#EXT-X-START:%s' % self.start.upper()) if self.is_i_frames_only: output.append('#EXT-X-I-FRAMES-ONLY') if self.is_variant: From 77475250ca57fae08063bd533b5359633e6bb71a Mon Sep 17 00:00:00 2001 From: alexb Date: Wed, 8 Nov 2017 16:04:11 -0400 Subject: [PATCH 4/8] Replaced '_' by '-' when dumping playlist --- m3u8/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/m3u8/model.py b/m3u8/model.py index 0cc9e2ab..7c3967b4 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -253,7 +253,7 @@ def dumps(self): if not (self.playlist_type is None or self.playlist_type == ''): output.append('#EXT-X-PLAYLIST-TYPE:%s' % str(self.playlist_type).upper()) if self.start: - output.append('#EXT-X-START:%s' % self.start.upper()) + output.append('#EXT-X-START:%s' % self.start.upper().replace('_', '-')) if self.is_i_frames_only: output.append('#EXT-X-I-FRAMES-ONLY') if self.is_variant: From 235055e5afa4e704f3fca2a122964e77e915bb71 Mon Sep 17 00:00:00 2001 From: alexb Date: Wed, 8 Nov 2017 17:35:11 -0400 Subject: [PATCH 5/8] Refactored start logic --- m3u8/model.py | 24 +++++++++++++++++++++--- m3u8/parser.py | 8 +++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/m3u8/model.py b/m3u8/model.py index 7c3967b4..394d3b77 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -8,6 +8,7 @@ import errno import math +from m3u8 import protocol from m3u8.parser import parse, format_date_time from m3u8.mixins import BasePathMixin, GroupedBasePathMixin @@ -131,8 +132,7 @@ class M3U8(object): ('is_independent_segments', 'is_independent_segments'), ('version', 'version'), ('allow_cache', 'allow_cache'), - ('playlist_type', 'playlist_type'), - ('start', 'start') + ('playlist_type', 'playlist_type') ) def __init__(self, content=None, base_path=None, base_uri=None, strict=False): @@ -179,6 +179,8 @@ def _initialize_attributes(self): ) self.segment_map = self.data.get('segment_map') + self.start = Start(**self.data.get('start', {})) + def __unicode__(self): return self.dumps() @@ -253,7 +255,7 @@ def dumps(self): if not (self.playlist_type is None or self.playlist_type == ''): output.append('#EXT-X-PLAYLIST-TYPE:%s' % str(self.playlist_type).upper()) if self.start: - output.append('#EXT-X-START:%s' % self.start.upper().replace('_', '-')) + output.append(str(self.start)) if self.is_i_frames_only: output.append('#EXT-X-I-FRAMES-ONLY') if self.is_variant: @@ -679,6 +681,22 @@ def __str__(self): return '\n'.join(output) +class Start(object): + + def __init__(self, time_offset, precise=None): + self.time_offset = float(time_offset) + self.precise = precise + + def __str__(self): + output = [ + 'TIME-OFFSET=' + str(self.time_offset) + ] + if self.precise and self.precise in ['YES', 'NO']: + output.append('PRECISE=' + str(self.precise)) + + return protocol.ext_start + ':' + ','.join(output) + + def find_key(keydata, keylist): if not keydata: return None diff --git a/m3u8/parser.py b/m3u8/parser.py index 07871707..07ede965 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -51,8 +51,7 @@ def parse(content, strict=False): 'segments': [], 'iframe_playlists': [], 'media': [], - 'keys': [], - 'start': None + 'keys': [] } state = { @@ -143,7 +142,10 @@ def parse(content, strict=False): data['segment_map'] = segment_map_info elif line.startswith(protocol.ext_x_start): - _parse_simple_parameter(line, data) + attribute_parser = {} + attribute_parser["time_offset"] = lambda x: float(x) + start_info = _parse_attribute_list(protocol.ext_x_start, line, attribute_parser) + data['start'] = start_info # Comments and whitespace elif line.startswith('#'): From 865de12016af5091dd39d0bab71d4451421008ec Mon Sep 17 00:00:00 2001 From: alexb Date: Wed, 8 Nov 2017 17:41:36 -0400 Subject: [PATCH 6/8] Protected M3U8 _initialize_attributes if not start is detected --- m3u8/model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/m3u8/model.py b/m3u8/model.py index 394d3b77..5b876d12 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -179,7 +179,8 @@ def _initialize_attributes(self): ) self.segment_map = self.data.get('segment_map') - self.start = Start(**self.data.get('start', {})) + start = self.data.get('start', None) + self.start = start and Start(**start) def __unicode__(self): return self.dumps() From 6b5d78b811c604e2978d35ce995a31efb24592d6 Mon Sep 17 00:00:00 2001 From: alexb Date: Wed, 8 Nov 2017 17:49:22 -0400 Subject: [PATCH 7/8] Fixed import issue --- m3u8/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/m3u8/model.py b/m3u8/model.py index 5b876d12..f677b648 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -8,7 +8,7 @@ import errno import math -from m3u8 import protocol +from m3u8.protocol import ext_x_start from m3u8.parser import parse, format_date_time from m3u8.mixins import BasePathMixin, GroupedBasePathMixin @@ -695,7 +695,7 @@ def __str__(self): if self.precise and self.precise in ['YES', 'NO']: output.append('PRECISE=' + str(self.precise)) - return protocol.ext_start + ':' + ','.join(output) + return ext_x_start + ':' + ','.join(output) def find_key(keydata, keylist): From d6ec551f7d3c7be10ee1bed2240795ec54a3089e Mon Sep 17 00:00:00 2001 From: alexb Date: Thu, 9 Nov 2017 12:24:13 -0400 Subject: [PATCH 8/8] Added unit tests --- m3u8/parser.py | 5 +++-- tests/playlists.py | 18 ++++++++++++++++++ tests/test_model.py | 18 ++++++++++++++++++ tests/test_parser.py | 12 ++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/m3u8/parser.py b/m3u8/parser.py index 07ede965..ab407160 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -142,8 +142,9 @@ def parse(content, strict=False): data['segment_map'] = segment_map_info elif line.startswith(protocol.ext_x_start): - attribute_parser = {} - attribute_parser["time_offset"] = lambda x: float(x) + attribute_parser = { + "time_offset": lambda x: float(x) + } start_info = _parse_attribute_list(protocol.ext_x_start, line, attribute_parser) data['start'] = start_info diff --git a/tests/playlists.py b/tests/playlists.py index 20a4c32d..026367a9 100755 --- a/tests/playlists.py +++ b/tests/playlists.py @@ -15,6 +15,24 @@ #EXT-X-ENDLIST ''' +SIMPLE_PLAYLIST_WITH_START_NEGATIVE_OFFSET = ''' +#EXTM3U +#EXT-X-TARGETDURATION:5220 +#EXT-X-START:TIME-OFFSET=-2.0 +#EXTINF:5220, +http://media.example.com/entire.ts +#EXT-X-ENDLIST +''' + +SIMPLE_PLAYLIST_WITH_START_PRECISE = ''' +#EXTM3U +#EXT-X-TARGETDURATION:5220 +#EXT-X-START:TIME-OFFSET=10.5,PRECISE=YES +#EXTINF:5220, +http://media.example.com/entire.ts +#EXT-X-ENDLIST +''' + SIMPLE_PLAYLIST_FILENAME = abspath( join(dirname(__file__), 'playlists/simple-playlist.m3u8')) diff --git a/tests/test_model.py b/tests/test_model.py index 4a310093..dc8b812d 100755 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -8,6 +8,9 @@ import arrow import datetime + +from m3u8.protocol import ext_x_start + import m3u8 import playlists from m3u8.model import Segment, Key, Media @@ -712,6 +715,21 @@ def test_segment_map_uri_attribute_with_byterange(): obj = m3u8.M3U8(playlists.MAP_URI_PLAYLIST_WITH_BYTERANGE) assert obj.segment_map['uri'] == "main.mp4" + +def test_start_with_negative_offset(): + obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST_WITH_START_NEGATIVE_OFFSET) + assert obj.start.time_offset == -2.0 + assert obj.start.precise is None + assert ext_x_start + ':TIME-OFFSET=-2.0\n' in obj.dumps() + + +def test_start_with_precise(): + obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST_WITH_START_PRECISE) + assert obj.start.time_offset == 10.5 + assert obj.start.precise == 'YES' + assert ext_x_start + ':TIME-OFFSET=10.5,PRECISE=YES\n' in obj.dumps() + + # custom asserts diff --git a/tests/test_parser.py b/tests/test_parser.py index 3ac1349e..df20426c 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -275,3 +275,15 @@ def test_should_parse_empty_uri_with_base_path(): assert media.uri is None assert media.base_path is None assert 'base_uri/' == media.base_uri + + +def test_should_parse_start_with_negative_time_offset(): + data = m3u8.parse(playlists.SIMPLE_PLAYLIST_WITH_START_NEGATIVE_OFFSET) + assert data['start']['time_offset'] == -2.0 + assert not hasattr(data['start'], 'precise') + + +def test_should_parse_start_with_precise(): + data = m3u8.parse(playlists.SIMPLE_PLAYLIST_WITH_START_PRECISE) + assert data['start']['time_offset'] == 10.5 + assert data['start']['precise'] == 'YES'