Repository: streamlink/streamlink

Issue:
stream.dash: default suggestedPresentationDelay shouldn't be below minBufferTime
### Checklist

- [ ] This is a [plugin issue](https://streamlink.github.io/plugins.html) and not [a different kind of issue](https://github.com/streamlink/streamlink/issues/new/choose)
- [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 plugin issues](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

6.2.1

### Description

I have a MPD without a suggestedPresentationDelay, so it defaults to 3 seconds. The current code starts playback, but after a few seconds, it's halted again, to continue after a few seconds.

I'm not 100% sure how the `segment_timeline` method works, but it seems that the last possible segment is selected, and a new segment isn't available in time, causing the stream to hang for a few seconds after the first segment has played fine. Writing the stream to a file and playing that works fine.

When I manually modify the `suggestedPresentationDelay` to 10, there is no issue. Setting it to 5 can sometimes still cause the stream to hang at the beginning.

```py
        self.suggestedPresentationDelay = self.attr(
            "suggestedPresentationDelay",
            parser=MPDParsers.duration,
            # if there is no delay, use a delay of 3 seconds
            # TODO: add a customizable parameter for this
            default=timedelta(seconds=10),
        )
```

I'm wondering if the `suggestedPresentationDelay` should be at least the value of minBufferTime if it's missing from the manifest? I guess that ideally, it shouldn't pick the latest segment, like with `--hls-live-edge` for HLS streams.

Manifest with some stripped `<S>` tags to decrease the size.
```xml
<?xml version="1.0" encoding="utf-8"?>
<MPD
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns="urn:mpeg:dash:schema:mpd:2011"
 xmlns:ns1="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:cenc="urn:mpeg:cenc:2013"
 xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd"
 profiles="urn:mpeg:dash:profile:isoff-live:2011,http://dashif.org/guidelines/dash-if-simple"
 maxSegmentDuration="PT5S" minBufferTime="PT5S" type="dynamic" publishTime="2023-10-12T13:18:26Z" timeShiftBufferDepth="PT1H0S"  availabilityStartTime="1970-01-01T00:00:00Z" minimumUpdatePeriod="PT0S">
 <Period id="p0" start="PT0S">
  <AdaptationSet mimeType="video/mp4" segmentAlignment="true" startWithSAP="1">
   <SegmentTemplate initialization="sc-gaFECw/$RepresentationID$_init.mp4" media="sc-gaFECw/$RepresentationID$_t$Time$.m4s" timescale="90000">
    <SegmentTimeline>
<S t="152740179303376" d="360000" r="899"/>
    </SegmentTimeline>
   </SegmentTemplate>
   <Representation id="V0" bandwidth="1255000" codecs="avc1.640028" frameRate="25" width="1920" height="1080" sar="1:1"/>
   <Representation id="V1" bandwidth="535000" codecs="avc1.64001f" frameRate="25" width="1280" height="720" sar="1:1"/>
   <Representation id="V2" bandwidth="353000" codecs="avc1.4d401f" frameRate="25" width="960" height="540" sar="1:1"/>
   <Representation id="V3" bandwidth="213000" codecs="avc1.4d401e" frameRate="25" width="768" height="432" sar="1:1"/>
   <Representation id="V4" bandwidth="157000" codecs="avc1.4d401e" frameRate="25" width="640" height="360" sar="1:1"/>
  </AdaptationSet>
  <AdaptationSet mimeType="audio/mp4" lang="nld" segmentAlignment="true" startWithSAP="1">
   <Role schemeIdUri="urn:mpeg:dash:role:2011" value="main"/>
   <SegmentTemplate initialization="sc-gaFECw/$RepresentationID$_init.mp4" media="sc-gaFECw/$RepresentationID$_t$Time$.m4s" timescale="90000">
    <SegmentTimeline>
<S t="152740179304696" d="359040"/>
<S d="360960"/>
<S d="359040"/>
<S d="360960"/>
<S d="359040"/>
<S d="360960"/>
<!-- cut -->
<S d="360960"/>
<S d="359040"/>
<S d="360960"/>
    </SegmentTimeline>
   </SegmentTemplate>
   <Representation id="A0" bandwidth="128000" codecs="mp4a.40.2" audioSamplingRate="48000"><AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/></Representation>
  </AdaptationSet>
 </Period>
</MPD>
```

### Debug log

```text
[cli][debug] OS:         Linux-5.15.123.1-microsoft-standard-WSL2-x86_64-with-glibc2.35
[cli][debug] Python:     3.10.12
[cli][debug] OpenSSL:    OpenSSL 3.0.2 15 Mar 2022
[cli][debug] Streamlink: 6.2.1+23.g0490c4ef.dirty
[cli][debug] Dependencies:
[cli][debug]  certifi: 2023.7.22
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.3
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.19.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  trio: 0.22.2
[cli][debug]  trio-websocket: 0.11.1
[cli][debug]  typing-extensions: 4.8.0
[cli][debug]  urllib3: 2.0.6
[cli][debug]  websocket-client: 1.6.4
[cli][debug] Arguments:
[cli][debug]  url=https://xxx/dash.mpd
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --output=dump.ts
[cli][debug]  --ffmpeg-fout=mpegts
[cli][info] Found matching plugin dash for URL https://xxx/dash.mpd
[plugins.dash][debug] URL=https://xxx/dash.mpd; params={}
[utils.l10n][debug] Language code: nl_BE
[stream.dash][debug] Available languages for DASH audio streams: nld (using: nld)
[cli][info] Available streams: 360p (worst), 432p, 540p, 720p, 1080p (best)
[cli][info] Opening stream: 1080p (dash)
[cli][info] Writing output to
/home/michael/Development/streamlink/dump.ts
[cli][debug] Checking file output
[stream.dash][debug] Opening DASH reader for: ('p0', None, 'V0') - video/mp4
[stream.dash][debug] video/mp4 segment initialization: downloading (1970-01-01T00:00:00.000000Z / 2023-10-12T13:34:03.535015Z)
[stream.dash.manifest][debug] Generating segment timeline for dynamic playlist: ('p0', None, 'V0')
[stream.dash][debug] Opening DASH reader for: ('p0', None, 'A0') - audio/mp4
[stream.dash][debug] Reloading manifest ('p0', None, 'V0')
[stream.dash][debug] audio/mp4 segment initialization: downloading (1970-01-01T00:00:00.000000Z / 2023-10-12T13:34:03.538269Z)
[stream.dash.manifest][debug] Generating segment timeline for dynamic playlist: ('p0', None, 'A0')
[stream.dash][debug] Reloading manifest ('p0', None, 'A0')
[stream.dash][debug] video/mp4 segment 900: waiting 0.4s (2023-10-12T13:34:04.000000Z / 2023-10-12T13:34:03.555758Z)
[stream.dash][debug] video/mp4 segment initialization: completed
[stream.ffmpegmux][debug] ffmpeg version 6.0 Copyright (c) 2000-2023 the FFmpeg developers
[stream.ffmpegmux][debug]  built with gcc 11 (Ubuntu 11.3.0-1ubuntu1~22.04.1)
[stream.ffmpegmux][debug]  configuration: --disable-decoder=amrnb --disable-decoder=libopenjpeg --disable-gnutls --disable-liblensfun --disable-libopencv --disable-podpages --disable-sndio --disable-stripping --enable-avfilter --enable-chromaprint --enable-frei0r --enable-gcrypt --enable-gpl --enable-ladspa --enable-libaom --enable-libaribb24 --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libdc1394 --enable-libdrm --enable-libfdk-aac --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libglslang --enable-libgme --enable-libgsm --enable-libiec61883 --enable-libjack --enable-libkvazaar --enable-libmp3lame --enable-libmysofa --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librist --enable-librsvg --enable-librubberband --enable-libshine --enable-libsmbclient --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libsvtav1 --enable-libtesseract --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvo-amrwbenc --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzimg --enable-libzmq --enable-libzvbi --enable-lv2 --enable-nonfree --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-openssl --enable-postproc --enable-pthreads --enable-shared --enable-version3 --enable-vulkan --incdir=/usr/include/x86_64-linux-gnu --libdir=/usr/lib/x86_64-linux-gnu --prefix=/usr --toolchain=hardened --enable-vaapi --enable-libvpl --enable-libvmaf --enable-libdavs2 --enable-libxavs2 --enable-libilbc --enable-libjxl --disable-altivec --shlibdir=/usr/lib/x86_64-linux-gnu
[stream.ffmpegmux][debug]  libavutil      58.  2.100 / 58.  2.100
[stream.ffmpegmux][debug]  libavcodec     60.  3.100 / 60.  3.100
[stream.ffmpegmux][debug]  libavformat    60.  3.100 / 60.  3.100
[stream.ffmpegmux][debug]  libavdevice    60.  1.100 / 60.  1.100
[stream.ffmpegmux][debug]  libavfilter     9.  3.100 /  9.  3.100
[stream.ffmpegmux][debug]  libswscale      7.  1.100 /  7.  1.100
[stream.ffmpegmux][debug]  libswresample   4. 10.100 /  4. 10.100
[stream.ffmpegmux][debug]  libpostproc    57.  1.100 / 57.  1.100
[utils.named_pipe][info] Creating pipe streamlinkpipe-66036-1-7821
[utils.named_pipe][info] Creating pipe streamlinkpipe-66036-2-9899
[stream.ffmpegmux][debug] ffmpeg command: /usr/bin/ffmpeg -nostats -y -i /tmp/streamlinkpipe-66036-1-7821 -i /tmp/streamlinkpipe-66036-2-9899 -c:v copy -c:a copy -copyts -f mpegts pipe:1
[stream.ffmpegmux][debug] Starting copy to pipe: /tmp/streamlinkpipe-66036-1-7821
[stream.ffmpegmux][debug] Starting copy to pipe: /tmp/streamlinkpipe-66036-2-9899
[cli][debug] Pre-buffering 8192 bytes
[stream.dash][debug] audio/mp4 segment 900: waiting 0.1s (2023-10-12T13:34:04.000000Z / 2023-10-12T13:34:03.912906Z)
[stream.dash][debug] audio/mp4 segment initialization: completed
[stream.dash][debug] video/mp4 segment 900: downloading (2023-10-12T13:34:04.000000Z / 2023-10-12T13:34:04.000367Z)
[stream.dash][debug] audio/mp4 segment 900: downloading (2023-10-12T13:34:04.000000Z / 2023-10-12T13:34:04.001298Z)
[stream.dash][debug] audio/mp4 segment 900: completed
[stream.dash][debug] video/mp4 segment 900: completed
[cli][debug] Writing stream to output
[download] Written 1.42 MiB to dump.ts (4s @ 387.32 KiB/s)
[stream.dash.manifest][debug] Generating segment timeline for dynamic playlist: ('p0', None, 'V0')
[stream.dash][debug] Reloading manifest ('p0', None, 'V0')
[stream.dash.manifest][debug] Generating segment timeline for dynamic playlist: ('p0', None, 'A0')
[stream.dash][debug] Reloading manifest ('p0', None, 'A0')
[download] Written 1.42 MiB to dump.ts (11s @ 127.71 KiB/s)
[stream.dash.manifest][debug] Generating segment timeline for dynamic playlist: ('p0', None, 'V0')
[stream.dash][debug] Reloading manifest ('p0', None, 'V0')
[stream.dash][debug] video/mp4 segment 900: downloading (2023-10-12T13:34:10.000000Z / 2023-10-12T13:34:16.157370Z)
[stream.dash.manifest][debug] Generating segment timeline for dynamic playlist: ('p0', None, 'A0')
[stream.dash][debug] Reloading manifest ('p0', None, 'A0')
[stream.dash][debug] audio/mp4 segment 900: downloading (2023-10-12T13:34:10.000000Z / 2023-10-12T13:34:16.165648Z)
[stream.dash][debug] audio/mp4 segment 900: completed
[download] Written 1.46 MiB to dump.ts (11s @ 129.09 KiB/s)
[stream.dash][debug] video/mp4 segment 900: completed
[download] Written 2.95 MiB to dump.ts (16s @ 184.29 KiB/s)
[stream.dash.manifest][debug] Generating segment timeline for dynamic playlist: ('p0', None, 'V0')
[stream.dash][debug] Reloading manifest ('p0', None, 'V0')
[stream.dash][debug] video/mp4 segment 899: downloading (2023-10-12T13:34:12.000000Z / 2023-10-12T13:34:21.157445Z)
[stream.dash.manifest][debug] Generating segment timeline for dynamic playlist: ('p0', None, 'A0')
[stream.dash][debug] Reloading manifest ('p0', None, 'A0')
[stream.dash][debug] audio/mp4 segment 899: downloading (2023-10-12T13:34:11.989333Z / 2023-10-12T13:34:21.162231Z)
[download] Written 2.95 MiB to dump.ts (16s @ 181.52 KiB/s)
[stream.dash][debug] video/mp4 segment 900: downloading (2023-10-12T13:34:16.000000Z / 2023-10-12T13:34:21.402217Z)
[stream.dash][debug] video/mp4 segment 899: completed
[stream.dash][debug] audio/mp4 segment 900: downloading (2023-10-12T13:34:16.000000Z / 2023-10-12T13:34:21.408679Z)
[stream.dash][debug] audio/mp4 segment 899: completed
[stream.dash][debug] audio/mp4 segment 900: completed
[download] Written 4.20 MiB to dump.ts (17s @ 254.53 KiB/s)
[stream.dash][debug] video/mp4 segment 900: completed
[download] Written 5.84 MiB to dump.ts (18s @ 334.09 KiB/s)
^C
[stream.ffmpegmux][debug] Closing ffmpeg thread
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[stream.ffmpegmux][debug] Pipe copy complete: /tmp/streamlinkpipe-66036-1-7821
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[stream.ffmpegmux][debug] Pipe copy complete: /tmp/streamlinkpipe-66036-2-9899
[stream.ffmpegmux][debug] Closed all the substreams
[cli][info] Stream ended
Interrupted! Exiting...
[cli][info] Closing currently open stream...
```



Discussion:
> I'm not 100% sure how the `segment_timeline` method works, but it seems that the last possible segment is selected

It yields as many segments as needed until the threshold is reached.

https://github.com/streamlink/streamlink/blob/e197337a0ddbfb77e7ecb045e76f583cb921bdc7/src/streamlink/stream/dash/manifest.py#L859-L885

As you already figured out yourself, the suggested presentation delay has a static default value of 3 and it's missing an option for customizing it. I had a look at adding an option a few months ago, but didn't finish it because of some initialization issues I've run into which required more code refactoring of the MPD and DASHStream classes.

Respecting the manifest's min buffer time however sounds reasonable and should be fixed.

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
@@ -77,8 +77,14 @@ def type(mpdtype: Literal["static", "dynamic"]) -> Literal["static", "dynamic"]:
         return mpdtype
 
     @staticmethod
-    def duration(duration: str) -> Union[timedelta, Duration]:
-        return parse_duration(duration)
+    def duration(anchor: Optional[datetime] = None) -> Callable[[str], timedelta]:
+        def duration_to_timedelta(duration: str) -> timedelta:
+            parsed: Union[timedelta, Duration] = parse_duration(duration)
+            if isinstance(parsed, Duration):
+                return parsed.totimedelta(start=anchor or now())
+            return parsed
+
+        return duration_to_timedelta
 
     @staticmethod
     def datetime(dt: str) -> datetime:
@@ -286,6 +292,8 @@ class MPD(MPDNode):
     parent: None  # type: ignore[assignment]
     timelines: Dict[TTimelineIdent, int]
 
+    DEFAULT_MINBUFFERTIME = 3.0
+
     def __init__(self, *args, url: Optional[str] = None, **kwargs) -> None:
         # top level has no parent
         kwargs["root"] = self
@@ -307,19 +315,10 @@ def __init__(self, *args, url: Optional[str] = None, **kwargs) -> None:
             parser=MPDParsers.type,
             default="static",
         )
-        self.minimumUpdatePeriod = self.attr(
-            "minimumUpdatePeriod",
-            parser=MPDParsers.duration,
-            default=timedelta(),
-        )
-        self.minBufferTime: Union[timedelta, Duration] = self.attr(
-            "minBufferTime",
-            parser=MPDParsers.duration,
-            required=True,
-        )
-        self.timeShiftBufferDepth = self.attr(
-            "timeShiftBufferDepth",
-            parser=MPDParsers.duration,
+        self.publishTime = self.attr(
+            "publishTime",
+            parser=MPDParsers.datetime,
+            required=self.type == "dynamic",
         )
         self.availabilityStartTime = self.attr(
             "availabilityStartTime",
@@ -327,26 +326,38 @@ def __init__(self, *args, url: Optional[str] = None, **kwargs) -> None:
             default=EPOCH_START,
             required=self.type == "dynamic",
         )
-        self.publishTime = self.attr(
-            "publishTime",
-            parser=MPDParsers.datetime,
-            required=self.type == "dynamic",
-        )
         self.availabilityEndTime = self.attr(
             "availabilityEndTime",
             parser=MPDParsers.datetime,
         )
+        self.minBufferTime: timedelta = self.attr(  # type: ignore[assignment]
+            "minBufferTime",
+            parser=MPDParsers.duration(self.publishTime),
+            required=True,
+        )
+        self.minimumUpdatePeriod = self.attr(
+            "minimumUpdatePeriod",
+            parser=MPDParsers.duration(self.publishTime),
+            default=timedelta(),
+        )
+        self.timeShiftBufferDepth = self.attr(
+            "timeShiftBufferDepth",
+            parser=MPDParsers.duration(self.publishTime),
+        )
         self.mediaPresentationDuration = self.attr(
             "mediaPresentationDuration",
-            parser=MPDParsers.duration,
+            parser=MPDParsers.duration(self.publishTime),
             default=timedelta(),
         )
         self.suggestedPresentationDelay = self.attr(
             "suggestedPresentationDelay",
-            parser=MPDParsers.duration,
-            # if there is no delay, use a delay of 3 seconds
+            parser=MPDParsers.duration(self.publishTime),
+            # if there is no delay, use a delay of 3 seconds, but respect the manifest's minBufferTime
             # TODO: add a customizable parameter for this
-            default=timedelta(seconds=3),
+            default=timedelta(seconds=max(
+                self.DEFAULT_MINBUFFERTIME,
+                self.minBufferTime.total_seconds(),
+            )),
         )
 
         # parse children
@@ -423,12 +434,12 @@ def __init__(self, *args, **kwargs) -> None:
         )
         self.duration = self.attr(
             "duration",
-            parser=MPDParsers.duration,
+            parser=MPDParsers.duration(self.root.publishTime),
             default=timedelta(),
         )
         self.start = self.attr(
             "start",
-            parser=MPDParsers.duration,
+            parser=MPDParsers.duration(self.root.publishTime),
             default=timedelta(),
         )
 


Test patch:
diff --git a/tests/resources/dash/test_suggested_presentation_delay.mpd b/tests/resources/dash/test_suggested_presentation_delay.mpd
new file mode 100644
--- /dev/null
+++ b/tests/resources/dash/test_suggested_presentation_delay.mpd
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<MPD
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xmlns="urn:mpeg:dash:schema:mpd:2011"
+  xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd"
+  profiles="urn:mpeg:dash:profile:isoff-live:2011,http://dashif.org/guidelines/dash-if-simple"
+  type="dynamic"
+  availabilityStartTime="1970-01-01T00:00:00Z"
+  publishTime="2000-01-01T00:00:00Z"
+  maxSegmentDuration="PT5S"
+  minBufferTime="PT5S"
+>
+ <Period id="p0" start="PT0S">
+  <AdaptationSet mimeType="video/mp4" segmentAlignment="true" startWithSAP="1">
+   <SegmentTemplate initialization="$RepresentationID$_init.mp4" media="$RepresentationID$_t$Time$.m4s" timescale="90000">
+    <SegmentTimeline>
+      <S t="152740179303376" d="360000" r="899"/>
+    </SegmentTimeline>
+   </SegmentTemplate>
+   <Representation id="V0" bandwidth="1255000" codecs="avc1.640028" frameRate="25" width="1920" height="1080" sar="1:1"/>
+   <Representation id="V1" bandwidth="535000" codecs="avc1.64001f" frameRate="25" width="1280" height="720" sar="1:1"/>
+   <Representation id="V2" bandwidth="353000" codecs="avc1.4d401f" frameRate="25" width="960" height="540" sar="1:1"/>
+   <Representation id="V3" bandwidth="213000" codecs="avc1.4d401e" frameRate="25" width="768" height="432" sar="1:1"/>
+   <Representation id="V4" bandwidth="157000" codecs="avc1.4d401e" frameRate="25" width="640" height="360" sar="1:1"/>
+  </AdaptationSet>
+ </Period>
+</MPD>
diff --git a/tests/stream/dash/test_manifest.py b/tests/stream/dash/test_manifest.py
--- a/tests/stream/dash/test_manifest.py
+++ b/tests/stream/dash/test_manifest.py
@@ -7,9 +7,11 @@
 from freezegun import freeze_time
 
 from streamlink.stream.dash.manifest import MPD, DASHSegment, MPDParsers, MPDParsingError, Representation
+from streamlink.utils.times import fromtimestamp
 from tests.resources import xml
 
 
+EPOCH_START = fromtimestamp(0)
 UTC = datetime.timezone.utc
 
 
@@ -69,10 +71,22 @@ def test_type(self):
         assert MPDParsers.type("dynamic") == "dynamic"
         assert MPDParsers.type("static") == "static"
         with pytest.raises(MPDParsingError):
+            # noinspection PyTypeChecker
             MPDParsers.type("other")
 
     def test_duration(self):
-        assert MPDParsers.duration("PT1S") == datetime.timedelta(0, 1)
+        assert MPDParsers.duration()("PT1S") == datetime.timedelta(seconds=1)
+        assert MPDParsers.duration()("P1W") == datetime.timedelta(seconds=7 * 24 * 3600)
+
+        assert MPDParsers.duration(EPOCH_START)("P3M") == datetime.timedelta(days=31 + 28 + 31)
+        assert MPDParsers.duration(EPOCH_START + datetime.timedelta(31))("P3M") == datetime.timedelta(days=28 + 31 + 30)
+
+        assert MPDParsers.duration(EPOCH_START)("P3Y") == datetime.timedelta(days=3 * 365 + 1)
+        assert MPDParsers.duration(EPOCH_START + datetime.timedelta(3 * 365))("P3Y") == datetime.timedelta(days=3 * 365)
+
+        with freeze_time(EPOCH_START):
+            assert MPDParsers.duration()("P3M") == datetime.timedelta(days=31 + 28 + 31)
+            assert MPDParsers.duration()("P3Y") == datetime.timedelta(days=3 * 365 + 1)
 
     def test_datetime(self):
         assert MPDParsers.datetime("2018-01-01T00:00:00Z") == datetime.datetime(2018, 1, 1, 0, 0, 0, tzinfo=UTC)
@@ -98,6 +112,16 @@ def test_range(self):
 
 
 class TestMPDParser:
+    @pytest.mark.parametrize(("min_buffer_time", "expected"), [
+        pytest.param("PT1S", 3.0, id="minBufferTime lower than suggestedPresentationDelay"),
+        pytest.param("PT5S", 5.0, id="minBufferTime greater than suggestedPresentationDelay"),
+    ])
+    def test_suggested_presentation_delay(self, min_buffer_time: str, expected: float):
+        with xml("dash/test_suggested_presentation_delay.mpd") as mpd_xml:
+            mpd_xml.attrib["minBufferTime"] = min_buffer_time
+            mpd = MPD(mpd_xml, base_url="http://test/", url="http://test/manifest.mpd")
+        assert mpd.suggestedPresentationDelay.total_seconds() == expected
+
     def test_no_segment_list_or_template(self):
         with xml("dash/test_no_segment_list_or_template.mpd") as mpd_xml:
             mpd = MPD(mpd_xml, base_url="http://test/", url="http://test/manifest.mpd")