Repository: streamlink/streamlink

Issue:
stream.dash: range attribute in initialization segment
### Checklist

- [X] This is a bug report and not a different kind of issue
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed bug reports](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22bug%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

Latest stable release

### Description

It seems that `dash_manifest.py` needs to support range attribute of initialization segment.

I played mpd made with Bento4 tools: `mp4dash -f -o bento4 --mpd-name=manifest.mpd --no-split --use-segment-list video-frag.mp4 audio-frag.mp4`
Video and audio play normally once, but it won't stop there. Playback continues, video stops motion with errors and audio plays.

This is the content of mpd:
```
<?xml version="1.0" ?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" profiles="urn:mpeg:dash:profile:isoff-live:2011" minBufferTime="PT10.01S" mediaPresentationDuration="PT30.097S" type="static">
  <!-- Created with Bento4 mp4-dash.py, VERSION=2.0.0-639 -->
  <Period>
    <!-- Video -->
    <AdaptationSet mimeType="video/mp4" segmentAlignment="true" startWithSAP="1" maxWidth="768" maxHeight="432">
      <Representation id="video-avc1" codecs="avc1.4D401E" width="768" height="432" scanType="progressive" frameRate="30000/1001" bandwidth="699597">
        <SegmentList timescale="1000" duration="10010">
          <Initialization sourceURL="video-frag.mp4" range="36-746"/>
          <SegmentURL media="video-frag.mp4" mediaRange="747-876117"/>
          <SegmentURL media="video-frag.mp4" mediaRange="876118-1466913"/>
          <SegmentURL media="video-frag.mp4" mediaRange="1466914-1953954"/>
          <SegmentURL media="video-frag.mp4" mediaRange="1953955-1994652"/>
        </SegmentList>
      </Representation>
    </AdaptationSet>
    <!-- Audio -->
    <AdaptationSet mimeType="audio/mp4" startWithSAP="1" segmentAlignment="true">
      <Representation id="audio-und-mp4a.40.2" codecs="mp4a.40.2" bandwidth="98808" audioSamplingRate="48000">
        <AudioChannelConfiguration schemeIdUri="urn:mpeg:mpegB:cicp:ChannelConfiguration" value="2"/>
        <SegmentList timescale="1000" duration="10010">
          <Initialization sourceURL="audio-frag.mp4" range="32-623"/>
          <SegmentURL media="audio-frag.mp4" mediaRange="624-124199"/>
          <SegmentURL media="audio-frag.mp4" mediaRange="124200-250303"/>
          <SegmentURL media="audio-frag.mp4" mediaRange="250304-374365"/>
          <SegmentURL media="audio-frag.mp4" mediaRange="374366-374836"/>
        </SegmentList>
      </Representation>
    </AdaptationSet>
  </Period>
</MPD>
```
I tried this mpd directly with mpv: `mpv http://127.0.0.1:8888/bento4/manifest.mpd`
Warnings are reported many times (`[ffmpeg] Invalid return value 0 for stream protocol`), but video and audio seem to play fine, just once then stop.


### Debug log

```text
streamlink -l debug http://127.0.0.1:8888/bento4/manifest.mpd best
[cli][debug] OS:         macOS 10.12.6
[cli][debug] Python:     3.9.9
[cli][debug] Streamlink: 3.0.2
[cli][debug] Requests(2.26.0), Socks(1.7.1), Websocket(1.2.1)
[cli][debug] Arguments:
[cli][debug]  url=http://127.0.0.1:8888/bento4/manifest.mpd
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --player=mpv
[cli][debug]  --verbose-player=True
[cli][debug]  --player-no-close=True
[cli][info] Found matching plugin dash for URL http://127.0.0.1:8888/bento4/manifest.mpd
[plugins.dash][debug] Parsing MPD URL: http://127.0.0.1:8888/bento4/manifest.mpd
[utils.l10n][debug] Language code: en_US
[stream.dash][debug] Available languages for DASH audio streams: NONE (using: n/a)
[cli][info] Available streams: 432p (worst, best)
[cli][info] Opening stream: 432p (dash)
[stream.dash][debug] Opening DASH reader for: video-avc1 (video/mp4)
[stream.dash][debug] Opening DASH reader for: audio-und-mp4a.40.2 (audio/mp4)
[utils.named_pipe][info] Creating pipe streamlinkpipe-55925-1-9651
[utils.named_pipe][info] Creating pipe streamlinkpipe-55925-2-8564
[stream.ffmpegmux][debug] ffmpeg command: ffmpeg -nostats -y -i /var/tmp/streamlinkpipe-55925-1-9651 -i /var/tmp/streamlinkpipe-55925-2-8564 -c:v copy -c:a copy -copyts -f matroska pipe:1
[stream.ffmpegmux][debug] Starting copy to pipe: /var/tmp/streamlinkpipe-55925-1-9651
[stream.ffmpegmux][debug] Starting copy to pipe: /var/tmp/streamlinkpipe-55925-2-8564
[cli][debug] Pre-buffering 8192 bytes
[stream.dash][debug] Download of segment: http://127.0.0.1:8888/bento4/audio-frag.mp4 complete
[stream.dash][debug] Download of segment: http://127.0.0.1:8888/bento4/audio-frag.mp4 complete
[stream.dash][debug] Download of segment: http://127.0.0.1:8888/bento4/video-frag.mp4 complete
[stream.dash][debug] Download of segment: http://127.0.0.1:8888/bento4/audio-frag.mp4 complete
[cli][info] Starting player: mpv
[cli.output][debug] Opening subprocess: mpv --force-media-title=http://127.0.0.1:8888/bento4/manifest.mpd -
[stream.dash][debug] Download of segment: http://127.0.0.1:8888/bento4/audio-frag.mp4 complete
[stream.dash][debug] Download of segment: http://127.0.0.1:8888/bento4/video-frag.mp4 complete
[stream.dash][debug] Download of segment: http://127.0.0.1:8888/bento4/audio-frag.mp4 complete
[stream.dash][debug] Download of segment: http://127.0.0.1:8888/bento4/video-frag.mp4 complete
[stream.dash][debug] Download of segment: http://127.0.0.1:8888/bento4/video-frag.mp4 complete
[stream.dash][debug] Download of segment: http://127.0.0.1:8888/bento4/video-frag.mp4 complete
[file] Reading from stdin...
[cli][debug] Writing stream to output
 (+) Video --vid=1 (*) (h264 768x432 29.970fps)
 (+) Audio --aid=1 (*) (aac 2ch 48000Hz)
[vo/gpu] opengl cocoa backend is deprecated, use vo=libmpv instead
AO: [coreaudio] 48000Hz stereo 2ch floatp
VO: [gpu] 768x432 yuv420p
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[stream.segmented][debug] Closing writer thread
[stream.ffmpegmux][debug] Pipe copy complete: /var/tmp/streamlinkpipe-55925-2-8564
[stream.ffmpegmux][debug] Pipe copy complete: /var/tmp/streamlinkpipe-55925-1-9651
[stream.ffmpegmux][debug] Closing ffmpeg thread
[stream.ffmpegmux][debug] Closed all the substreams
[cli][info] Stream ended
AV: 00:00:30 / 00:00:30 (100%) A-V: -0.411
Invalid video timestamp: 30.163000 -> 29.996000
AV: 00:00:30 / 00:00:30 (100%) A-V: -0.244
Invalid video timestamp: 29.996000 -> 29.996000
AV: 00:00:29 / 00:00:30 (99%) A-V: -0.294 Dropped: 1
Invalid video timestamp: 29.996000 -> 29.996000
AV: 00:00:29 / 00:00:30 (99%) A-V: -0.294 Dropped: 2
*** snipped ***
Invalid video timestamp: 29.996000 -> 29.996000
AV: 00:00:29 / 00:00:30 (99%) A-V: -0.294 Dropped: 892
Invalid video timestamp: 29.996000 -> 29.996000
AV: 00:00:29 / 00:00:30 (99%) A-V:  0.000 ct: -0.104 Dropped: 896

Exiting... (End of file)
[cli][info] Closing currently open stream...
```



Discussion:

Resolving patch:
diff --git a/src/streamlink/stream/dash_manifest.py b/src/streamlink/stream/dash_manifest.py
--- a/src/streamlink/stream/dash_manifest.py
+++ b/src/streamlink/stream/dash_manifest.py
@@ -356,6 +356,10 @@ class Initialization(MPDNode):
     def __init__(self, node, root=None, parent=None, *args, **kwargs):
         super().__init__(node, root, parent, *args, **kwargs)
         self.source_url = self.attr("sourceURL")
+        self.range = self.attr(
+            "range",
+            parser=MPDParsers.range,
+        )
 
 
 class SegmentURL(MPDNode):
@@ -407,6 +411,7 @@ def segments(self):
                 duration=0,
                 init=True,
                 content=False,
+                byterange=self.initialization.range,
             )
         for n, segment_url in enumerate(self.segment_urls, self.start_number):
             yield Segment(


Test patch:
diff --git a/tests/resources/dash/test_segments_byterange.mpd b/tests/resources/dash/test_segments_byterange.mpd
new file mode 100644
--- /dev/null
+++ b/tests/resources/dash/test_segments_byterange.mpd
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<MPD
+    xmlns="urn:mpeg:dash:schema:mpd:2011"
+    profiles="urn:mpeg:dash:profile:isoff-live:2011"
+    minBufferTime="PT10.01S"
+    mediaPresentationDuration="PT30.097S"
+    type="static"
+>
+  <Period>
+    <AdaptationSet mimeType="video/mp4" segmentAlignment="true" startWithSAP="1" maxWidth="768" maxHeight="432">
+      <Representation id="video-avc1" codecs="avc1.4D401E" width="768" height="432" scanType="progressive" frameRate="30000/1001" bandwidth="699597">
+        <SegmentList timescale="1000" duration="10010">
+          <Initialization sourceURL="video-frag.mp4" range="36-746"/>
+          <SegmentURL media="video-frag.mp4" mediaRange="747-876117"/>
+          <SegmentURL media="video-frag.mp4" mediaRange="876118-1466913"/>
+          <SegmentURL media="video-frag.mp4" mediaRange="1466914-1953954"/>
+          <SegmentURL media="video-frag.mp4" mediaRange="1953955-1994652"/>
+        </SegmentList>
+      </Representation>
+    </AdaptationSet>
+    <AdaptationSet mimeType="audio/mp4" startWithSAP="1" segmentAlignment="true">
+      <Representation id="audio-und-mp4a.40.2" codecs="mp4a.40.2" bandwidth="98808" audioSamplingRate="48000">
+        <SegmentList timescale="1000" duration="10010">
+          <Initialization sourceURL="audio-frag.mp4" range="32-623"/>
+          <SegmentURL media="audio-frag.mp4" mediaRange="624-124199"/>
+          <SegmentURL media="audio-frag.mp4" mediaRange="124200-250303"/>
+          <SegmentURL media="audio-frag.mp4" mediaRange="250304-374365"/>
+          <SegmentURL media="audio-frag.mp4" mediaRange="374366-374836"/>
+        </SegmentList>
+      </Representation>
+    </AdaptationSet>
+  </Period>
+</MPD>
diff --git a/tests/stream/test_dash_parser.py b/tests/stream/test_dash_parser.py
--- a/tests/stream/test_dash_parser.py
+++ b/tests/stream/test_dash_parser.py
@@ -261,3 +261,29 @@ def test_segments_static_periods_duration(self):
             mpd = MPD(mpd_xml, base_url="http://test.se/", url="http://test.se/manifest.mpd")
             duration = mpd.periods[0].duration.total_seconds()
             assert duration == pytest.approx(204.32)
+
+    def test_segments_byterange(self):
+        with xml("dash/test_segments_byterange.mpd") as mpd_xml:
+            mpd = MPD(mpd_xml, base_url="http://test/", url="http://test/manifest.mpd")
+        assert [
+            [
+                (seg.url, seg.init, seg.byterange)
+                for seg in adaptationset.representations[0].segments()
+            ]
+            for adaptationset in mpd.periods[0].adaptationSets
+        ] == [
+            [
+                ("http://test/video-frag.mp4", True, (36, 711)),
+                ("http://test/video-frag.mp4", False, (747, 875371)),
+                ("http://test/video-frag.mp4", False, (876118, 590796)),
+                ("http://test/video-frag.mp4", False, (1466914, 487041)),
+                ("http://test/video-frag.mp4", False, (1953955, 40698)),
+            ],
+            [
+                ("http://test/audio-frag.mp4", True, (32, 592)),
+                ("http://test/audio-frag.mp4", False, (624, 123576)),
+                ("http://test/audio-frag.mp4", False, (124200, 126104)),
+                ("http://test/audio-frag.mp4", False, (250304, 124062)),
+                ("http://test/audio-frag.mp4", False, (374366, 471)),
+            ],
+        ]