diff --git a/README.rst b/README.rst index e9c2540c..ac5b0973 100644 --- a/README.rst +++ b/README.rst @@ -120,11 +120,11 @@ the same thing. .. _issue 1: https://github.com/globocom/m3u8/issues/1 .. _variant streams: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-6.2.4 .. _example here: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-8.5 -.. _#EXT-X-STREAM-INF: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.4.10 +.. _#EXT-X-STREAM-INF: https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-4.3.4.2 .. _issue 4: https://github.com/globocom/m3u8/issues/4 -.. _I-frame playlists: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.4.12 +.. _I-frame playlists: https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-4.3.4.3 .. _Apple's documentation: https://developer.apple.com/library/ios/technotes/tn2288/_index.html#//apple_ref/doc/uid/DTS40012238-CH1-I_FRAME_PLAYLIST .. _Alternative audio: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-8.7 -.. _#EXT-X-MEDIA: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.4.9 +.. _#EXT-X-MEDIA: https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-4.3.4.1 .. _VOD: https://developer.apple.com/library/mac/technotes/tn2288/_index.html#//apple_ref/doc/uid/DTS40012238-CH1-TNTAG2 .. _EVENT: https://developer.apple.com/library/mac/technotes/tn2288/_index.html#//apple_ref/doc/uid/DTS40012238-CH1-EVENT_PLAYLIST diff --git a/m3u8/model.py b/m3u8/model.py index c3065057..b72f6f17 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -5,7 +5,6 @@ from collections import namedtuple import os -import posixpath import errno import math @@ -457,7 +456,8 @@ class Playlist(BasePathMixin): Attributes: `stream_info` is a named tuple containing the attributes: `program_id`, - `bandwidth`,`resolution`, `codecs` and `resolution` which is a a tuple (w, h) of integers + `bandwidth`, `average_bandwidth`, `resolution`, `codecs` and `resolution` + which is a a tuple (w, h) of integers `media` is a list of related Media entries. @@ -474,10 +474,13 @@ def __init__(self, uri, stream_info, media, base_uri): else: resolution_pair = None - self.stream_info = StreamInfo(bandwidth=stream_info['bandwidth'], - program_id=stream_info.get('program_id'), - resolution=resolution_pair, - codecs=stream_info.get('codecs')) + self.stream_info = StreamInfo( + bandwidth=stream_info['bandwidth'], + average_bandwidth=stream_info.get('average_bandwidth'), + program_id=stream_info.get('program_id'), + resolution=resolution_pair, + codecs=stream_info.get('codecs') + ) self.media = [] for media_type in ('audio', 'video', 'subtitles'): group_id = stream_info.get(media_type) @@ -492,6 +495,9 @@ def __str__(self): stream_inf.append('PROGRAM-ID=%d' % self.stream_info.program_id) if self.stream_info.bandwidth: stream_inf.append('BANDWIDTH=%d' % self.stream_info.bandwidth) + if self.stream_info.average_bandwidth: + stream_inf.append('AVERAGE-BANDWIDTH=%d' % + self.stream_info.average_bandwidth) if self.stream_info.resolution: res = str(self.stream_info.resolution[0]) + 'x' + str(self.stream_info.resolution[1]) stream_inf.append('RESOLUTION=' + res) @@ -530,6 +536,7 @@ def __init__(self, base_uri, uri, iframe_stream_info): self.iframe_stream_info = StreamInfo( bandwidth=iframe_stream_info.get('bandwidth'), + average_bandwidth=None, program_id=iframe_stream_info.get('program_id'), resolution=resolution_pair, codecs=iframe_stream_info.get('codecs') @@ -555,7 +562,10 @@ def __str__(self): return '#EXT-X-I-FRAME-STREAM-INF:' + ','.join(iframe_stream_inf) -StreamInfo = namedtuple('StreamInfo', ['bandwidth', 'program_id', 'resolution', 'codecs']) +StreamInfo = namedtuple( + 'StreamInfo', + ['bandwidth', 'average_bandwidth', 'program_id', 'resolution', 'codecs'] +) class Media(BasePathMixin): ''' diff --git a/m3u8/parser.py b/m3u8/parser.py index 17b5cc92..b20751a7 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -184,6 +184,7 @@ def _parse_stream_inf(line, data, state): atribute_parser = remove_quotes_parser('codecs', 'audio', 'video', 'subtitles') atribute_parser["program_id"] = int atribute_parser["bandwidth"] = int + atribute_parser["average_bandwidth"] = int state['stream_info'] = _parse_attribute_list(protocol.ext_x_stream_inf, line, atribute_parser) def _parse_i_frame_stream_inf(line, data): diff --git a/tests/playlists.py b/tests/playlists.py index 38b5a2d6..6bb45fc1 100755 --- a/tests/playlists.py +++ b/tests/playlists.py @@ -67,6 +67,18 @@ http://example.com/audio-only.m3u8 ''' +VARIANT_PLAYLIST_WITH_AVERAGE_BANDWIDTH = ''' +#EXTM3U +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1252345 +http://example.com/low.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2466570 +http://example.com/mid.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=7680000,AVERAGE-BANDWIDTH=7560423 +http://example.com/hi.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=65000,AVERAGE-BANDWIDTH=63005,CODECS="mp4a.40.5,avc1.42801e" +http://example.com/audio-only.m3u8 +''' + VARIANT_PLAYLIST_WITH_IFRAME_PLAYLISTS = ''' #EXTM3U #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=800000,RESOLUTION=624x352,CODECS="avc1.4d001f, mp4a.40.5" @@ -105,7 +117,6 @@ IFRAME_PLAYLIST = ''' #EXTM3U -#EXT-X-MEDIA-SEQUENCE:0 #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXT-X-PLAYLIST-TYPE:VOD @@ -143,7 +154,6 @@ PLAYLIST_USING_BYTERANGES = ''' #EXTM3U -#EXT-X-MEDIA-SEQUENCE:0 #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:11 #EXTINF:10, diff --git a/tests/test_model.py b/tests/test_model.py index 3025b3d4..b838da0b 100755 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -60,10 +60,11 @@ def test_segment_discontinuity_attribute(): def test_segment_cue_out_attribute(): obj = m3u8.M3U8(playlists.CUE_OUT_PLAYLIST) segments = obj.segments + print segments[0].__dict__ - assert segments[0].cue_out == True assert segments[1].cue_out == True assert segments[2].cue_out == True + assert segments[3].cue_out == True def test_key_attribute(): obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST) @@ -515,7 +516,7 @@ def test_0_media_sequence_added_to_file(): obj = m3u8.M3U8() obj.media_sequence = 0 result = obj.dumps() - expected = '#EXTM3U\n#EXT-X-MEDIA-SEQUENCE:0\n' + expected = '#EXTM3U\n' assert result == expected def test_none_media_sequence_gracefully_ignored(): diff --git a/tests/test_parser.py b/tests/test_parser.py index 6522b97f..9eeb50da 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -86,6 +86,18 @@ def test_should_parse_variant_playlist(): assert 65000 == playlists_list[-1]['stream_info']['bandwidth'] assert 'mp4a.40.5,avc1.42801e' == playlists_list[-1]['stream_info']['codecs'] +def test_should_parse_variant_playlist_with_average_bandwidth(): + data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_AVERAGE_BANDWIDTH) + playlists_list = list(data['playlists']) + assert 1280000 == playlists_list[0]['stream_info']['bandwidth'] + assert 1252345 == playlists_list[0]['stream_info']['average_bandwidth'] + assert 2560000 == playlists_list[1]['stream_info']['bandwidth'] + assert 2466570 == playlists_list[1]['stream_info']['average_bandwidth'] + assert 7680000 == playlists_list[2]['stream_info']['bandwidth'] + assert 7560423 == playlists_list[2]['stream_info']['average_bandwidth'] + assert 65000 == playlists_list[3]['stream_info']['bandwidth'] + assert 63005 == playlists_list[3]['stream_info']['average_bandwidth'] + def test_should_parse_variant_playlist_with_iframe_playlists(): data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_IFRAME_PLAYLISTS) iframe_playlists = list(data['iframe_playlists']) diff --git a/tests/test_variant_m3u8.py b/tests/test_variant_m3u8.py index 44836d8e..811a9541 100644 --- a/tests/test_variant_m3u8.py +++ b/tests/test_variant_m3u8.py @@ -90,3 +90,38 @@ def test_create_a_variant_m3u8_with_two_playlists_and_two_iframe_playlists(): CODECS="avc1.4d001f",URI="video-1200k-iframes.m3u8" """ assert expected_content == variant_m3u8.dumps() + + +def test_variant_playlist_with_average_bandwidth(): + variant_m3u8 = m3u8.M3U8() + + low_playlist = m3u8.Playlist( + 'http://example.com/low.m3u8', + stream_info={'bandwidth': 1280000, + 'average_bandwidth': 1257891, + 'program_id': 1, + 'subtitles': 'subs'}, + media=[], + base_uri=None + ) + high_playlist = m3u8.Playlist( + 'http://example.com/high.m3u8', + stream_info={'bandwidth': 3000000, + 'average_bandwidth': 2857123, + 'program_id': 1, + 'subtitles': 'subs'}, + media=[], + base_uri=None + ) + + variant_m3u8.add_playlist(low_playlist) + variant_m3u8.add_playlist(high_playlist) + + expected_content = """\ +#EXTM3U +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1257891 +http://example.com/low.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3000000,AVERAGE-BANDWIDTH=2857123 +http://example.com/high.m3u8 +""" + assert expected_content == variant_m3u8.dumps()