Repository: streamlink/streamlink
Pull request #1846 (2018-06-25 22:45:20)

stream.dash: use the ID and mime-type to identify a representation
This is a work-around for some DASH streams that have duplicate IDs for representations. By using the mimeType as well as the ID, we can better identify the different streams. Naughty Valve. 


Patch:
diff --git a/src/streamlink/stream/dash.py b/src/streamlink/stream/dash.py
index 562190a1..aa0a72fa 100644
--- a/src/streamlink/stream/dash.py
+++ b/src/streamlink/stream/dash.py
@@ -60,10 +60,10 @@ class DASHStreamWorker(SegmentedStreamWorker):
         self.period = self.stream.period
 
     @staticmethod
-    def get_representation(mpd, representation_id):
+    def get_representation(mpd, representation_id, mime_type):
         for aset in mpd.periods[0].adaptationSets:
             for rep in aset.representations:
-                if rep.id == representation_id:
+                if rep.id == representation_id and rep.mimeType == mime_type:
                     return rep
 
     def iter_segments(self):
@@ -71,7 +71,7 @@ class DASHStreamWorker(SegmentedStreamWorker):
         back_off_factor = 1
         while not self.closed:
             # find the representation by ID
-            representation = self.get_representation(self.mpd, self.reader.representation_id)
+            representation = self.get_representation(self.mpd, self.reader.representation_id, self.reader.mime_type)
             refresh_wait = max(self.mpd.minimumUpdatePeriod.total_seconds(),
                                self.mpd.periods[0].duration.total_seconds()) or 5
             with sleeper(refresh_wait * back_off_factor):
@@ -96,7 +96,7 @@ class DASHStreamWorker(SegmentedStreamWorker):
             return
 
         self.reader.buffer.wait_free()
-        log.debug("Reloading manifest ({0})".format(self.reader.representation_id))
+        log.debug("Reloading manifest ({0}:{1})".format(self.reader.representation_id, self.reader.mime_type))
         res = self.session.http.get(self.mpd.url, exception=StreamError)
 
         new_mpd = MPD(self.session.http.xml(res, ignore_ns=True),
@@ -104,7 +104,7 @@ class DASHStreamWorker(SegmentedStreamWorker):
                       url=self.mpd.url,
                       timelines=self.mpd.timelines)
 
-        new_rep = self.get_representation(new_mpd, self.reader.representation_id)
+        new_rep = self.get_representation(new_mpd, self.reader.representation_id, self.reader.mime_type)
         with freeze_timeline(new_mpd):
             changed = len(list(itertools.islice(new_rep.segments(), 1))) > 0
 
@@ -118,10 +118,11 @@ class DASHStreamReader(SegmentedStreamReader):
     __worker__ = DASHStreamWorker
     __writer__ = DASHStreamWriter
 
-    def __init__(self, stream, representation_id, *args, **kwargs):
+    def __init__(self, stream, representation_id, mime_type, *args, **kwargs):
         SegmentedStreamReader.__init__(self, stream, *args, **kwargs)
+        self.mime_type = mime_type
         self.representation_id = representation_id
-        log.debug("Opening DASH reader for: {0}".format(self.representation_id))
+        log.debug("Opening DASH reader for: {0} ({1})".format(self.representation_id, self.mime_type))
 
 
 
@@ -198,11 +199,11 @@ class DASHStream(Stream):
 
     def open(self):
         if self.video_representation:
-            video = DASHStreamReader(self, self.video_representation.id)
+            video = DASHStreamReader(self, self.video_representation.id, self.video_representation.mimeType)
             video.open()
 
         if self.audio_representation:
-            audio = DASHStreamReader(self, self.audio_representation.id)
+            audio = DASHStreamReader(self, self.audio_representation.id, self.audio_representation.mimeType)
             audio.open()
 
         if self.video_representation and self.audio_representation:


Test patch:
diff --git a/tests/streams/test_dash.py b/tests/streams/test_dash.py
index ff489f7e..3b462004 100644
--- a/tests/streams/test_dash.py
+++ b/tests/streams/test_dash.py
@@ -107,39 +107,39 @@ class TestDASHStream(unittest.TestCase):
     @patch('streamlink.stream.dash.DASHStreamReader')
     @patch('streamlink.stream.dash.FFMPEGMuxer')
     def test_stream_open_video_only(self, muxer, reader):
-        stream = DASHStream(self.session, Mock(), Mock(id=1))
+        stream = DASHStream(self.session, Mock(), Mock(id=1, mimeType="video/mp4"))
         open_reader = reader.return_value = Mock()
 
         stream.open()
 
-        reader.assert_called_with(stream, 1)
+        reader.assert_called_with(stream, 1, "video/mp4")
         open_reader.open.assert_called_with()
         muxer.assert_not_called()
 
     @patch('streamlink.stream.dash.DASHStreamReader')
     @patch('streamlink.stream.dash.FFMPEGMuxer')
     def test_stream_open_video_audio(self, muxer, reader):
-        stream = DASHStream(self.session, Mock(), Mock(id=1), Mock(id=2))
+        stream = DASHStream(self.session, Mock(), Mock(id=1, mimeType="video/mp4"), Mock(id=2, mimeType="audio/mp3"))
         open_reader = reader.return_value = Mock()
 
         stream.open()
 
-        self.assertSequenceEqual(reader.mock_calls, [call(stream, 1),
+        self.assertSequenceEqual(reader.mock_calls, [call(stream, 1, "video/mp4"),
                                                      call().open(),
-                                                     call(stream, 2),
+                                                     call(stream, 2, "audio/mp3"),
                                                      call().open()])
         self.assertSequenceEqual(muxer.mock_calls, [call(self.session, open_reader, open_reader, copyts=True),
                                                     call().open()])
 
 
 class TestDASHStreamWorker(unittest.TestCase):
-
     @patch("streamlink.stream.dash_manifest.time.sleep")
     @patch('streamlink.stream.dash.MPD')
     def test_dynamic_reload(self, mpdClass, sleep):
         reader = MagicMock()
         worker = DASHStreamWorker(reader)
         reader.representation_id = 1
+        reader.mime_type = "video/mp4"
 
         representation = Mock(id=1, mimeType="video/mp4", height=720)
         segments = [Mock(url="init_segment"), Mock(url="first_segment"), Mock(url="second_segment")]
@@ -173,6 +173,7 @@ class TestDASHStreamWorker(unittest.TestCase):
         reader = MagicMock()
         worker = DASHStreamWorker(reader)
         reader.representation_id = 1
+        reader.mime_type = "video/mp4"
 
         representation = Mock(id=1, mimeType="video/mp4", height=720)
         segments = [Mock(url="init_segment"), Mock(url="first_segment"), Mock(url="second_segment")]
@@ -195,6 +196,29 @@ class TestDASHStreamWorker(unittest.TestCase):
         self.assertSequenceEqual(list(worker.iter_segments()), segments)
         representation.segments.assert_called_with(init=True)
 
+    @patch("streamlink.stream.dash_manifest.time.sleep")
+    def test_duplicate_rep_id(self, sleep):
+        representation_vid = Mock(id=1, mimeType="video/mp4", height=720)
+        representation_aud = Mock(id=1, mimeType="audio/aac")
+
+        mpd = Mock(dynamic=False,
+                   publishTime=1,
+                   periods=[
+                       Mock(adaptationSets=[
+                           Mock(contentProtection=None,
+                                representations=[
+                                    representation_vid
+                                ]),
+                           Mock(contentProtection=None,
+                                representations=[
+                                    representation_aud
+                                ])
+                       ])
+                   ])
+
+        self.assertEqual(representation_vid, DASHStreamWorker.get_representation(mpd, 1, "video/mp4"))
+        self.assertEqual(representation_aud, DASHStreamWorker.get_representation(mpd, 1, "audio/aac"))
+
 
 if __name__ == "__main__":
     unittest.main()