diff --git a/src/streamlink/stream/dash.py b/src/streamlink/stream/dash.py index 32008030..efa17536 100644 --- a/src/streamlink/stream/dash.py +++ b/src/streamlink/stream/dash.py @@ -261,8 +261,10 @@ class DASHStream(Stream): # Search for suitable video and audio representations for aset in mpd.periods[0].adaptationSets: if aset.contentProtection: - raise PluginError("{} is protected by DRM".format(url)) + raise PluginError(f"{url} is protected by DRM") for rep in aset.representations: + if rep.contentProtection: + raise PluginError(f"{url} is protected by DRM") if rep.mimeType.startswith("video"): video.append(rep) elif rep.mimeType.startswith("audio"): diff --git a/src/streamlink/stream/dash_manifest.py b/src/streamlink/stream/dash_manifest.py index 5462ea21..a272d607 100644 --- a/src/streamlink/stream/dash_manifest.py +++ b/src/streamlink/stream/dash_manifest.py @@ -803,6 +803,7 @@ class Representation(MPDNode): self.segmentBase = self.only_child(SegmentBase) self.segmentList = self.children(SegmentList) self.segmentTemplate = self.only_child(SegmentTemplate) + self.contentProtection = self.children(ContentProtection) @property def bandwidth_rounded(self) -> float: diff --git a/tests/stream/test_dash.py b/tests/stream/test_dash.py index 315464cd..15de9664 100644 --- a/tests/stream/test_dash.py +++ b/tests/stream/test_dash.py @@ -18,15 +18,14 @@ class TestDASHStream(unittest.TestCase): @patch("streamlink.stream.dash.MPD") def test_parse_manifest_video_only(self, mpdClass): - mpdClass.return_value = Mock(periods=[ - Mock(adaptationSets=[ - Mock(contentProtection=None, - representations=[ - Mock(id=1, mimeType="video/mp4", height=720), - Mock(id=2, mimeType="video/mp4", height=1080), - ]), - ]), - ]) + adaptationset = Mock( + contentProtection=None, + representations=[ + Mock(id="1", contentProtection=None, mimeType="video/mp4", height=720), + Mock(id="2", contentProtection=None, mimeType="video/mp4", height=1080), + ], + ) + mpdClass.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") @@ -35,15 +34,14 @@ class TestDASHStream(unittest.TestCase): @patch("streamlink.stream.dash.MPD") def test_parse_manifest_audio_only(self, mpdClass): - mpdClass.return_value = Mock(periods=[ - Mock(adaptationSets=[ - Mock(contentProtection=None, - representations=[ - Mock(id=1, mimeType="audio/mp4", bandwidth=128.0, lang="en"), - Mock(id=2, mimeType="audio/mp4", bandwidth=256.0, lang="en"), - ]), - ]), - ]) + adaptationset = Mock( + contentProtection=None, + representations=[ + Mock(id="1", contentProtection=None, mimeType="audio/mp4", bandwidth=128.0, lang="en"), + Mock(id="2", contentProtection=None, mimeType="audio/mp4", bandwidth=256.0, lang="en"), + ], + ) + mpdClass.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") @@ -52,16 +50,15 @@ class TestDASHStream(unittest.TestCase): @patch("streamlink.stream.dash.MPD") def test_parse_manifest_audio_single(self, mpdClass): - mpdClass.return_value = Mock(periods=[ - Mock(adaptationSets=[ - Mock(contentProtection=None, - representations=[ - Mock(id=1, mimeType="video/mp4", height=720), - Mock(id=2, mimeType="video/mp4", height=1080), - Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang="en"), - ]), - ]), - ]) + adaptationset = Mock( + contentProtection=None, + representations=[ + Mock(id="1", contentProtection=None, mimeType="video/mp4", height=720), + Mock(id="2", contentProtection=None, mimeType="video/mp4", height=1080), + Mock(id="3", contentProtection=None, mimeType="audio/aac", bandwidth=128.0, lang="en"), + ], + ) + mpdClass.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") @@ -70,17 +67,16 @@ class TestDASHStream(unittest.TestCase): @patch("streamlink.stream.dash.MPD") def test_parse_manifest_audio_multi(self, mpdClass): - mpdClass.return_value = Mock(periods=[ - Mock(adaptationSets=[ - Mock(contentProtection=None, - representations=[ - Mock(id=1, mimeType="video/mp4", height=720), - Mock(id=2, mimeType="video/mp4", height=1080), - Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang="en"), - Mock(id=4, mimeType="audio/aac", bandwidth=256.0, lang="en"), - ]), - ]), - ]) + adaptationset = Mock( + contentProtection=None, + representations=[ + Mock(id="1", contentProtection=None, mimeType="video/mp4", height=720), + Mock(id="2", contentProtection=None, mimeType="video/mp4", height=1080), + Mock(id="3", contentProtection=None, mimeType="audio/aac", bandwidth=128.0, lang="en"), + Mock(id="4", contentProtection=None, mimeType="audio/aac", bandwidth=256.0, lang="en"), + ], + ) + mpdClass.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") @@ -89,17 +85,16 @@ class TestDASHStream(unittest.TestCase): @patch("streamlink.stream.dash.MPD") def test_parse_manifest_audio_multi_lang(self, mpdClass): - mpdClass.return_value = Mock(periods=[ - Mock(adaptationSets=[ - Mock(contentProtection=None, - representations=[ - Mock(id=1, mimeType="video/mp4", height=720), - Mock(id=2, mimeType="video/mp4", height=1080), - Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang="en"), - Mock(id=4, mimeType="audio/aac", bandwidth=128.0, lang="es"), - ]), - ]), - ]) + adaptationset = Mock( + contentProtection=None, + representations=[ + Mock(id="1", contentProtection=None, mimeType="video/mp4", height=720), + Mock(id="2", contentProtection=None, mimeType="video/mp4", height=1080), + Mock(id="3", contentProtection=None, mimeType="audio/aac", bandwidth=128.0, lang="en"), + Mock(id="4", contentProtection=None, mimeType="audio/aac", bandwidth=128.0, lang="es"), + ], + ) + mpdClass.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") @@ -111,17 +106,16 @@ class TestDASHStream(unittest.TestCase): @patch("streamlink.stream.dash.MPD") def test_parse_manifest_audio_multi_lang_alpha3(self, mpdClass): - mpdClass.return_value = Mock(periods=[ - Mock(adaptationSets=[ - Mock(contentProtection=None, - representations=[ - Mock(id=1, mimeType="video/mp4", height=720), - Mock(id=2, mimeType="video/mp4", height=1080), - Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang="eng"), - Mock(id=4, mimeType="audio/aac", bandwidth=128.0, lang="spa"), - ]), - ]), - ]) + adaptationset = Mock( + contentProtection=None, + representations=[ + Mock(id="1", contentProtection=None, mimeType="video/mp4", height=720), + Mock(id="2", contentProtection=None, mimeType="video/mp4", height=1080), + Mock(id="3", contentProtection=None, mimeType="audio/aac", bandwidth=128.0, lang="eng"), + Mock(id="4", contentProtection=None, mimeType="audio/aac", bandwidth=128.0, lang="spa"), + ], + ) + mpdClass.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") @@ -133,16 +127,15 @@ class TestDASHStream(unittest.TestCase): @patch("streamlink.stream.dash.MPD") def test_parse_manifest_audio_invalid_lang(self, mpdClass): - mpdClass.return_value = Mock(periods=[ - Mock(adaptationSets=[ - Mock(contentProtection=None, - representations=[ - Mock(id=1, mimeType="video/mp4", height=720), - Mock(id=2, mimeType="video/mp4", height=1080), - Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang="en_no_voice"), - ]), - ]), - ]) + adaptationset = Mock( + contentProtection=None, + representations=[ + Mock(id="1", contentProtection=None, mimeType="video/mp4", height=720), + Mock(id="2", contentProtection=None, mimeType="video/mp4", height=1080), + Mock(id="3", contentProtection=None, mimeType="audio/aac", bandwidth=128.0, lang="en_no_voice"), + ], + ) + mpdClass.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") @@ -157,17 +150,16 @@ class TestDASHStream(unittest.TestCase): self.session.localization.language.alpha2 = "es" self.session.localization.explicit = True - mpdClass.return_value = Mock(periods=[ - Mock(adaptationSets=[ - Mock(contentProtection=None, - representations=[ - Mock(id=1, mimeType="video/mp4", height=720), - Mock(id=2, mimeType="video/mp4", height=1080), - Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang="en"), - Mock(id=4, mimeType="audio/aac", bandwidth=128.0, lang="es"), - ]), - ]), - ]) + adaptationset = Mock( + contentProtection=None, + representations=[ + Mock(id="1", contentProtection=None, mimeType="video/mp4", height=720), + Mock(id="2", contentProtection=None, mimeType="video/mp4", height=1080), + Mock(id="3", contentProtection=None, mimeType="audio/aac", bandwidth=128.0, lang="en"), + Mock(id="4", contentProtection=None, mimeType="audio/aac", bandwidth=128.0, lang="es"), + ], + ) + mpdClass.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") @@ -178,8 +170,26 @@ class TestDASHStream(unittest.TestCase): assert streams["1080p"].audio_representation.lang == "es" @patch("streamlink.stream.dash.MPD") - def test_parse_manifest_drm(self, mpdClass): - mpdClass.return_value = Mock(periods=[Mock(adaptationSets=[Mock(contentProtection="DRM")])]) + def test_parse_manifest_drm_adaptationset(self, mpdClass): + adaptationset = Mock( + contentProtection="DRM", + representations=[], + ) + mpdClass.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])]) + + with pytest.raises(PluginError): + DASHStream.parse_manifest(self.session, self.test_url) + mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") + + @patch("streamlink.stream.dash.MPD") + def test_parse_manifest_drm_representation(self, mpdClass): + adaptationset = Mock( + contentProtection=None, + representations=[ + Mock(id="1", contentProtection="DRM"), + ], + ) + mpdClass.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])]) with pytest.raises(PluginError): DASHStream.parse_manifest(self.session, self.test_url) @@ -238,17 +248,16 @@ class TestDASHStream(unittest.TestCase): """ Verify the fix for https://github.com/streamlink/streamlink/issues/3365 """ - mpdClass.return_value = Mock(periods=[ - Mock(adaptationSets=[ - Mock(contentProtection=None, - representations=[ - Mock(id=1, mimeType="video/mp4", height=1080, bandwidth=128.0), - Mock(id=2, mimeType="video/mp4", height=1080, bandwidth=64.0), - Mock(id=3, mimeType="video/mp4", height=1080, bandwidth=32.0), - Mock(id=4, mimeType="video/mp4", height=720), - ]), - ]), - ]) + adaptationset = Mock( + contentProtection=None, + representations=[ + Mock(id="1", contentProtection=None, mimeType="video/mp4", height=1080, bandwidth=128.0), + Mock(id="2", contentProtection=None, mimeType="video/mp4", height=1080, bandwidth=64.0), + Mock(id="3", contentProtection=None, mimeType="video/mp4", height=1080, bandwidth=32.0), + Mock(id="4", contentProtection=None, mimeType="video/mp4", height=720), + ], + ) + mpdClass.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") @@ -260,16 +269,15 @@ class TestDASHStream(unittest.TestCase): """ Verify the fix for https://github.com/streamlink/streamlink/issues/4217 """ - mpdClass.return_value = Mock(periods=[ - Mock(adaptationSets=[ - Mock(contentProtection=None, - representations=[ - Mock(id=1, mimeType="video/mp4", height=1080, bandwidth=64.0), - Mock(id=2, mimeType="video/mp4", height=1080, bandwidth=128.0), - Mock(id=3, mimeType="video/mp4", height=1080, bandwidth=32.0), - ]), - ]), - ]) + adaptationset = Mock( + contentProtection=None, + representations=[ + Mock(id="1", contentProtection=None, mimeType="video/mp4", height=1080, bandwidth=64.0), + Mock(id="2", contentProtection=None, mimeType="video/mp4", height=1080, bandwidth=128.0), + Mock(id="3", contentProtection=None, mimeType="video/mp4", height=1080, bandwidth=32.0), + ], + ) + mpdClass.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd")