diff --git a/m3u8/model.py b/m3u8/model.py index 54478193..19b7af0b 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -788,7 +788,8 @@ def __init__(self, uri, stream_info, media, base_uri): resolution=resolution_pair, codecs=stream_info.get('codecs'), frame_rate=stream_info.get('frame_rate'), - video_range=stream_info.get('video_range') + video_range=stream_info.get('video_range'), + hdcp_level=stream_info.get('hdcp_level') ) self.media = [] for media_type in ('audio', 'video', 'subtitles'): @@ -820,8 +821,8 @@ class IFramePlaylist(BasePathMixin): Attributes: `iframe_stream_info` is a named tuple containing the attributes: - `program_id`, `bandwidth`, `average_bandwidth`, `codecs`, `video_range` and - `resolution` which is a tuple (w, h) of integers + `program_id`, `bandwidth`, `average_bandwidth`, `codecs`, `video_range`, + `hdcp_level` and `resolution` which is a tuple (w, h) of integers More info: http://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.13 ''' @@ -850,6 +851,7 @@ def __init__(self, base_uri, uri, iframe_stream_info): resolution=resolution_pair, codecs=iframe_stream_info.get('codecs'), video_range=iframe_stream_info.get('video_range'), + hdcp_level=iframe_stream_info.get('hdcp_level'), frame_rate=None ) @@ -874,6 +876,9 @@ def __str__(self): if self.iframe_stream_info.video_range: iframe_stream_inf.append('VIDEO-RANGE=%s' % self.iframe_stream_info.video_range) + if self.iframe_stream_info.hdcp_level: + iframe_stream_inf.append('HDCP-LEVEL=%s' % + self.iframe_stream_info.hdcp_level) if self.uri: iframe_stream_inf.append('URI=' + quoted(self.uri)) @@ -892,6 +897,7 @@ class StreamInfo(object): subtitles = None frame_rate = None video_range = None + hdcp_level = None def __init__(self, **kwargs): self.bandwidth = kwargs.get("bandwidth") @@ -905,6 +911,7 @@ def __init__(self, **kwargs): self.subtitles = kwargs.get("subtitles") self.frame_rate = kwargs.get("frame_rate") self.video_range = kwargs.get("video_range") + self.hdcp_level = kwargs.get("hdcp_level") def __str__(self): stream_inf = [] @@ -927,6 +934,8 @@ def __str__(self): stream_inf.append('CODECS=' + quoted(self.codecs)) if self.video_range is not None: stream_inf.append('VIDEO-RANGE=%s' % self.video_range) + if self.hdcp_level is not None: + stream_inf.append('HDCP-LEVEL=%s' % self.hdcp_level) return ",".join(stream_inf) diff --git a/m3u8/parser.py b/m3u8/parser.py index fea4987e..0cd3490d 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -295,6 +295,7 @@ def _parse_stream_inf(line, data, state): atribute_parser["average_bandwidth"] = int atribute_parser["frame_rate"] = float atribute_parser["video_range"] = str + atribute_parser["hdcp_level"] = str state['stream_info'] = _parse_attribute_list(protocol.ext_x_stream_inf, line, atribute_parser) @@ -304,6 +305,7 @@ def _parse_i_frame_stream_inf(line, data): atribute_parser["bandwidth"] = int atribute_parser["average_bandwidth"] = int atribute_parser["video_range"] = str + atribute_parser["hdcp_level"] = str iframe_stream_info = _parse_attribute_list(protocol.ext_x_i_frame_stream_inf, line, atribute_parser) iframe_playlist = {'uri': iframe_stream_info.pop('uri'), 'iframe_stream_info': iframe_stream_info} diff --git a/tests/playlists.py b/tests/playlists.py index c0f09f08..d4f766af 100755 --- a/tests/playlists.py +++ b/tests/playlists.py @@ -161,6 +161,16 @@ http://example.com/hdr.m3u8 ''' +VARIANT_PLAYLIST_WITH_HDCP_LEVEL = ''' +#EXTM3U +#EXT-X-STREAM-INF:PROGRAM-ID=1,HDCP-LEVEL=NONE" +http://example.com/none.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,HDCP-LEVEL=TYPE-0" +http://example.com/type0.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,HDCP-LEVEL=TYPE-1" +http://example.com/type1.m3u8 +''' + VARIANT_PLAYLIST_WITH_BANDWIDTH_FLOAT = ''' #EXTM3U #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=1280000.0 @@ -1092,11 +1102,11 @@ VARIANT_PLAYLIST_WITH_IFRAME_VIDEO_RANGE = ''' #EXTM3U -#EXT-X-STREAM-INF:PROGRAM-ID=1,VIDEO-RANGE=SDR" +#EXT-X-STREAM-INF:PROGRAM-ID=1,VIDEO-RANGE=SDR http://example.com/sdr.m3u8 -#EXT-X-STREAM-INF:PROGRAM-ID=1,VIDEO-RANGE=PQ" +#EXT-X-STREAM-INF:PROGRAM-ID=1,VIDEO-RANGE=PQ http://example.com/hdr-pq.m3u8 -#EXT-X-STREAM-INF:PROGRAM-ID=1,VIDEO-RANGE=HLG" +#EXT-X-STREAM-INF:PROGRAM-ID=1,VIDEO-RANGE=HLG http://example.com/hdr-hlg.m3u8 #EXT-X-I-FRAME-STREAM-INF:VIDEO_RANGE=SDR,URI="http://example.com/sdr-iframes.m3u8" #EXT-X-I-FRAME-STREAM-INF:VIDEO_RANGE=PQ,URI="http://example.com/hdr-pq-iframes.m3u8" @@ -1104,6 +1114,20 @@ #EXT-X-I-FRAME-STREAM-INF:URI="http://example.com/unknown-iframes.m3u8" ''' +VARIANT_PLAYLIST_WITH_IFRAME_HDCP_LEVEL = ''' +#EXTM3U +#EXT-X-STREAM-INF:PROGRAM-ID=1,HDCP-LEVEL=NONE +http://example.com/none.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,HDCP-LEVEL=TYPE-0 +http://example.com/type0.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,HDCP-LEVEL=TYPE-1 +http://example.com/type1.m3u8 +#EXT-X-I-FRAME-STREAM-INF:HDCP-LEVEL=NONE,URI="http://example.com/none-iframes.m3u8" +#EXT-X-I-FRAME-STREAM-INF:HDCP-LEVEL=TYPE-0,URI="http://example.com/type0-iframes.m3u8" +#EXT-X-I-FRAME-STREAM-INF:HDCP-LEVEL=TYPE-1,URI="http://example.com/type1-iframes.m3u8" +#EXT-X-I-FRAME-STREAM-INF:URI="http://example.com/unknown-iframes.m3u8" +''' + DELTA_UPDATE_SKIP_DATERANGES_PLAYLIST = '''#EXTM3U #EXT-X-VERSION:10 #EXT-X-TARGETDURATION:6 diff --git a/tests/test_parser.py b/tests/test_parser.py index 9f0d4702..877d8abe 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -179,6 +179,13 @@ def test_should_parse_variant_playlist_with_video_range(): assert 'SDR' == playlists_list[0]['stream_info']['video_range'] assert 'PQ' == playlists_list[1]['stream_info']['video_range'] +def test_should_parse_variant_playlist_with_hdcp_level(): + data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_HDCP_LEVEL) + playlists_list = list(data['playlists']) + assert 'NONE' == playlists_list[0]['stream_info']['hdcp_level'] + assert 'TYPE-0' == playlists_list[1]['stream_info']['hdcp_level'] + assert 'TYPE-1' == playlists_list[2]['stream_info']['hdcp_level'] + # This is actually not according to specification but as for example Twitch.tv # is producing master playlists that have bandwidth as floats (issue 72) # this tests that this situation does not break the parser and will just @@ -533,6 +540,23 @@ def test_should_parse_variant_playlist_with_iframe_with_video_range(): assert 'http://example.com/unknown-iframes.m3u8' == iframe_playlists[3]['uri'] assert 'video_range' not in iframe_playlists[3]['iframe_stream_info'] +def test_should_parse_variant_playlist_with_iframe_with_hdcp_level(): + data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_IFRAME_HDCP_LEVEL) + iframe_playlists = list(data['iframe_playlists']) + + assert True == data['is_variant'] + + assert 4 == len(iframe_playlists) + + assert 'http://example.com/none-iframes.m3u8' == iframe_playlists[0]['uri'] + assert 'NONE' == iframe_playlists[0]['iframe_stream_info']['hdcp_level'] + assert 'http://example.com/type0-iframes.m3u8' == iframe_playlists[1]['uri'] + assert 'TYPE-0' == iframe_playlists[1]['iframe_stream_info']['hdcp_level'] + assert 'http://example.com/type1-iframes.m3u8' == iframe_playlists[2]['uri'] + assert 'TYPE-1' == iframe_playlists[2]['iframe_stream_info']['hdcp_level'] + assert 'http://example.com/unknown-iframes.m3u8' == iframe_playlists[3]['uri'] + assert 'hdcp_level' not in iframe_playlists[3]['iframe_stream_info'] + def test_delta_playlist_daterange_skipping(): data = m3u8.parse(playlists.DELTA_UPDATE_SKIP_DATERANGES_PLAYLIST) assert data['skip']['recently_removed_dateranges'] == "1" diff --git a/tests/test_variant_m3u8.py b/tests/test_variant_m3u8.py index cd6d759b..7c52b4d3 100644 --- a/tests/test_variant_m3u8.py +++ b/tests/test_variant_m3u8.py @@ -160,6 +160,49 @@ def test_variant_playlist_with_video_range(): """ assert expected_content == variant_m3u8.dumps() +def test_variant_playlist_with_hdcp_level(): + variant_m3u8 = m3u8.M3U8() + + none_playlist = m3u8.Playlist( + 'http://example.com/none.m3u8', + stream_info={'bandwidth': 1280000, + 'hdcp_level': 'NONE', + 'program_id': 1}, + media=[], + base_uri=None + ) + type0_playlist = m3u8.Playlist( + 'http://example.com/type0.m3u8', + stream_info={'bandwidth': 3000000, + 'hdcp_level': 'TYPE-0', + 'program_id': 1}, + media=[], + base_uri=None + ) + type1_playlist = m3u8.Playlist( + 'http://example.com/type1.m3u8', + stream_info={'bandwidth': 4000000, + 'hdcp_level': 'TYPE-1', + 'program_id': 1}, + media=[], + base_uri=None + ) + + variant_m3u8.add_playlist(none_playlist) + variant_m3u8.add_playlist(type0_playlist) + variant_m3u8.add_playlist(type1_playlist) + + expected_content = """\ +#EXTM3U +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1280000,HDCP-LEVEL=NONE +http://example.com/none.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3000000,HDCP-LEVEL=TYPE-0 +http://example.com/type0.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=4000000,HDCP-LEVEL=TYPE-1 +http://example.com/type1.m3u8 +""" + assert expected_content == variant_m3u8.dumps() + def test_variant_playlist_with_multiple_media(): variant_m3u8 = m3u8.loads(playlists.MULTI_MEDIA_PLAYLIST) assert variant_m3u8.dumps() == playlists.MULTI_MEDIA_PLAYLIST @@ -242,3 +285,39 @@ def test_create_a_variant_m3u8_with_iframe_with_video_range_playlists(): #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=3000000,VIDEO-RANGE=HLG,URI="video-HLG-iframes.m3u8" """ assert expected_content == variant_m3u8.dumps() + + +def test_create_a_variant_m3u8_with_iframe_with_hdcp_level_playlists(): + variant_m3u8 = m3u8.M3U8() + + for hdcplv in ['NONE', 'TYPE-0', 'TYPE-1']: + playlist = m3u8.Playlist( + uri='video-%s.m3u8' % hdcplv, + stream_info={'bandwidth': 3000000, + 'hdcp_level': hdcplv}, + media=[], + base_uri='http://example.com/%s' % hdcplv + ) + iframe_playlist = m3u8.IFramePlaylist( + uri='video-%s-iframes.m3u8' % hdcplv, + iframe_stream_info={'bandwidth': 3000000, + 'hdcp_level': hdcplv}, + base_uri='http://example.com/%s' % hdcplv + ) + + variant_m3u8.add_playlist(playlist) + variant_m3u8.add_iframe_playlist(iframe_playlist) + + expected_content = """\ +#EXTM3U +#EXT-X-STREAM-INF:BANDWIDTH=3000000,HDCP-LEVEL=NONE +video-NONE.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=3000000,HDCP-LEVEL=TYPE-0 +video-TYPE-0.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=3000000,HDCP-LEVEL=TYPE-1 +video-TYPE-1.m3u8 +#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=3000000,HDCP-LEVEL=NONE,URI="video-NONE-iframes.m3u8" +#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=3000000,HDCP-LEVEL=TYPE-0,URI="video-TYPE-0-iframes.m3u8" +#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=3000000,HDCP-LEVEL=TYPE-1,URI="video-TYPE-1-iframes.m3u8" +""" + assert expected_content == variant_m3u8.dumps()