diff --git a/m3u8/model.py b/m3u8/model.py index ed255636..1e121483 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -255,8 +255,6 @@ def dumps(self): if self.target_duration: output.append('#EXT-X-TARGETDURATION:' + int_or_float_to_string(self.target_duration)) - if self.program_date_time is not None: - 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: @@ -313,9 +311,15 @@ class Segment(BasePathMixin): title attribute from EXTINF parameter `program_date_time` - Returns the EXT-X-PROGRAM-DATE-TIME as a datetime + Returns the EXT-X-PROGRAM-DATE-TIME as a datetime. This field is only set + if EXT-X-PROGRAM-DATE-TIME exists for this segment http://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.5 + `current_program_date_time` + Returns a datetime of this segment, either the value of `program_date_time` + when EXT-X-PROGRAM-DATE-TIME is set or a calculated value based on previous + segments' EXT-X-PROGRAM-DATE-TIME and EXTINF values + `discontinuity` 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 @@ -342,15 +346,17 @@ class Segment(BasePathMixin): Key used to encrypt the segment (EXT-X-KEY) ''' - def __init__(self, uri, base_uri, program_date_time=None, duration=None, - title=None, byterange=None, cue_out=False, discontinuity=False, key=None, - scte35=None, scte35_duration=None, keyobject=None): + def __init__(self, uri, base_uri, 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, + keyobject=None): self.uri = uri self.duration = duration self.title = title self.base_uri = base_uri self.byterange = byterange self.program_date_time = program_date_time + self.current_program_date_time = current_program_date_time self.discontinuity = discontinuity self.cue_out = cue_out self.scte35 = scte35 @@ -371,9 +377,9 @@ def dumps(self, last_segment): if self.discontinuity: output.append('#EXT-X-DISCONTINUITY\n') - if self.program_date_time: - output.append('#EXT-X-PROGRAM-DATE-TIME:%s\n' % - format_date_time(self.program_date_time)) + 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: output.append('#EXT-X-CUE-OUT-CONT\n') output.append('#EXTINF:%s,' % int_or_float_to_string(self.duration)) diff --git a/m3u8/parser.py b/m3u8/parser.py index 7bbdf7b9..ccd5696d 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -82,6 +82,7 @@ def parse(content, strict=False, custom_tags_parser=None): if not data.get('program_date_time'): data['program_date_time'] = program_date_time state['current_program_date_time'] = program_date_time + state['program_date_time'] = program_date_time elif line.startswith(protocol.ext_x_discontinuity): state['discontinuity'] = True @@ -200,8 +201,10 @@ def _parse_extinf(line, data, state, lineno, strict): def _parse_ts_chunk(line, data, state): segment = state.pop('segment') + if state.get('program_date_time'): + segment['program_date_time'] = state.pop('program_date_time') if state.get('current_program_date_time'): - segment['program_date_time'] = state['current_program_date_time'] + 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_out'] = state.pop('cue_out', False) diff --git a/tests/playlists.py b/tests/playlists.py index e2981d99..c912a04d 100755 --- a/tests/playlists.py +++ b/tests/playlists.py @@ -538,6 +538,24 @@ ''' +PLAYLIST_WITH_PROGRAM_DATE_TIME_WITHOUT_DISCONTINUITY = ''' +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:6 +#EXT-X-PLAYLIST-TYPE:EVENT +#EXT-X-MEDIA-SEQUENCE:50 +#EXT-X-PROGRAM-DATE-TIME:2019-06-10T00:05:00.000Z +#EXTINF:6.000, +manifest_1_50.ts?m=1559946393 +#EXT-X-PROGRAM-DATE-TIME:2019-06-10T00:05:06.000Z +#EXTINF:6.000, +manifest_1_51.ts?m=1559946393 +#EXT-X-PROGRAM-DATE-TIME:2019-06-10T00:05:12.000Z +#EXTINF:6.000, +manifest_1_52.ts?m=1559946393 +#EXT-X-ENDLIST +''' + CUE_OUT_PLAYLIST = ''' #EXTM3U #EXT-X-TARGETDURATION:10 diff --git a/tests/test_model.py b/tests/test_model.py index a9e94e30..3225d932 100755 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -56,9 +56,16 @@ def test_program_date_time_attribute_for_each_segment(): obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST_WITH_PROGRAM_DATE_TIME) first_program_date_time = datetime.datetime(2014, 8, 13, 13, 36, 33, tzinfo=utc) - for idx, segment in enumerate(obj.segments): - assert segment.program_date_time == first_program_date_time + \ - datetime.timedelta(seconds=idx * 3) + + # first segment contains both program_date_time and current_program_date_time + assert obj.segments[0].program_date_time == first_program_date_time + assert obj.segments[0].current_program_date_time == first_program_date_time + + # other segments contain only current_program_date_time + for idx, segment in enumerate(obj.segments[1:]): + assert segment.program_date_time is None + assert segment.current_program_date_time == first_program_date_time + \ + datetime.timedelta(seconds=(idx+1) * 3) def test_program_date_time_attribute_with_discontinuity(): @@ -69,9 +76,32 @@ def test_program_date_time_attribute_with_discontinuity(): segments = obj.segments + # first segment has EXT-X-PROGRAM-DATE-TIME assert segments[0].program_date_time == first_program_date_time + assert segments[0].current_program_date_time == first_program_date_time + + # second segment does not have EXT-X-PROGRAM-DATE-TIME + assert segments[1].program_date_time is None + assert segments[1].current_program_date_time == first_program_date_time + datetime.timedelta(seconds=3) + + # segment with EXT-X-DISCONTINUITY also has EXT-X-PROGRAM-DATE-TIME assert segments[5].program_date_time == discontinuity_program_date_time - assert segments[6].program_date_time == discontinuity_program_date_time + datetime.timedelta(seconds=3) + assert segments[5].current_program_date_time == discontinuity_program_date_time + + # subsequent segment does not have EXT-X-PROGRAM-DATE-TIME + assert segments[6].current_program_date_time == discontinuity_program_date_time + datetime.timedelta(seconds=3) + assert segments[6].program_date_time is None + + +def test_program_date_time_attribute_without_discontinuity(): + obj = m3u8.M3U8(playlists.PLAYLIST_WITH_PROGRAM_DATE_TIME_WITHOUT_DISCONTINUITY) + + first_program_date_time = datetime.datetime(2019, 6, 10, 0, 5, tzinfo=utc) + + for idx, segment in enumerate(obj.segments): + program_date_time = first_program_date_time + datetime.timedelta(seconds=idx * 6) + assert segment.program_date_time == program_date_time + assert segment.current_program_date_time == program_date_time def test_segment_discontinuity_attribute(): @@ -488,6 +518,16 @@ def test_dump_should_include_segment_level_program_date_time(): # Tag being expected is in the segment level, not the global one assert "#EXT-X-PROGRAM-DATE-TIME:2014-08-13T13:36:55+00:00" in obj.dumps().strip() + +def test_dump_should_include_segment_level_program_date_time_without_discontinuity(): + obj = m3u8.M3U8(playlists.PLAYLIST_WITH_PROGRAM_DATE_TIME_WITHOUT_DISCONTINUITY) + + output = obj.dumps().strip() + assert "#EXT-X-PROGRAM-DATE-TIME:2019-06-10T00:05:00+00:00" in output + assert "#EXT-X-PROGRAM-DATE-TIME:2019-06-10T00:05:06+00:00" in output + assert "#EXT-X-PROGRAM-DATE-TIME:2019-06-10T00:05:12+00:00" in output + + def test_dump_should_include_map_attributes(): obj = m3u8.M3U8(playlists.MAP_URI_PLAYLIST_WITH_BYTERANGE)