Repository: streamlink/streamlink

Issue:
stream.dash: incorrect base_url (BaseURL) handling after fetching manifest
### Checklist

- [x] This is a bug report 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 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)

### Collaboration

- [x] [I will provide feedback should a pull request be opened with a fix for the problem](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#pull-request-feedback)

### Streamlink version

7.0.0+31.gfac5d8f7

### Description

## The Issue

Hi! Recently I'm testing MPEG-DASH with streamlink. After encountering a bunch of HTTP 404 errors when downloading segments, I've started to read the code, and then fortunately I think I've found the solution.

My command lines:

```console
$ ffmpeg -i test.mkv -c copy -f dash test.mpd

$ ffprobe test.mkv
ffprobe version n6.1.1-7-ga267d4ad4c-20240222 Copyright (c) 2007-2023 the FFmpeg developers
  built with gcc 13.2.0 (crosstool-NG 1.25.0.232_c175b21)
  configuration: ...
  ...
Input #0, matroska,webm, from 'test.mkv':
  Metadata:
    COMPATIBLE_BRANDS: isomiso2avc1mp41
    MAJOR_BRAND     : isom
    MINOR_VERSION   : 512
    ENCODER         : Lavf60.16.100
  Duration: 00:05:00.17, start: 0.000000, bitrate: 1620 kb/s
  Stream #0:0: Video: h264 (Main), yuv420p(progressive), 1920x1080, 30 fps, 30 tbr, 1k tbn (default)
    Metadata:
      HANDLER_NAME    : VideoHandler
      VENDOR_ID       : [0][0][0][0]
      DURATION        : 00:05:00.166000000
  Stream #0:1: Audio: aac (LC), 44100 Hz, stereo, fltp (default)
    Metadata:
      HANDLER_NAME    : SoundHandler
      VENDOR_ID       : [0][0][0][0]
      DURATION        : 00:05:00.019000000
```

The MPD file:

<details>
<summary>the content of "test.mpd"</summary>

```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:xlink="http://www.w3.org/1999/xlink"
	xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
	profiles="urn:mpeg:dash:profile:isoff-live:2011"
	type="static"
	mediaPresentationDuration="PT5M0.1S"
	maxSegmentDuration="PT5.0S"
	minBufferTime="PT12.0S">
	<ProgramInformation>
	</ProgramInformation>
	<ServiceDescription id="0">
	</ServiceDescription>
	<Period id="0" start="PT0.0S">
		<AdaptationSet id="0" contentType="video" startWithSAP="1" segmentAlignment="true" bitstreamSwitching="true" frameRate="30/1" maxWidth="1920" maxHeight="1080" par="16:9">
			<Representation id="0" mimeType="video/mp4" codecs="avc1.4d4029" bandwidth="1428902" width="1920" height="1080" sar="1:1">
				<SegmentTemplate timescale="10000000" initialization="init-stream$RepresentationID$.m4s" media="chunk-stream$RepresentationID$-$Number%05d$.m4s" startNumber="1">
					<SegmentTimeline>
						<S t="0" d="60000000" r="26" />
						<S d="59990000" />
						<S d="60000000" r="21" />
						<S d="1670000" />
					</SegmentTimeline>
				</SegmentTemplate>
			</Representation>
		</AdaptationSet>
		<AdaptationSet id="1" contentType="audio" startWithSAP="1" segmentAlignment="true" bitstreamSwitching="true">
			<Representation id="1" mimeType="audio/mp4" codecs="mp4a.40.2" bandwidth="194151" audioSamplingRate="44100">
				<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2" />
				<SegmentTemplate timescale="44100" initialization="init-stream$RepresentationID$.m4s" media="chunk-stream$RepresentationID$-$Number%05d$.m4s" startNumber="1">
					<SegmentTimeline>
						<S t="750" d="221174" />
						<S d="221184" r="57" />
						<S d="180224" />
					</SegmentTimeline>
				</SegmentTemplate>
			</Representation>
		</AdaptationSet>
	</Period>
</MPD>

```

</details>

## My Solution

https://github.com/streamlink/streamlink/blob/fac5d8f740952485ce2c094e2826ed358fbe90fd/src/streamlink/stream/dash/dash.py#L256-L260

```diff
-        urlp[2], _ = urlp[2].rsplit("/", 1)
+        urlp[2] = urlp[2].rsplit("/", 1)[0] + "/"
```

And also add a correspoding test:

```Python
    def test_baseurl_urljoin(self):
        with xml("dash/test_baseurl_urljoin.mpd") as mpd_xml:
            mpd = MPD(mpd_xml, base_url="https://foo/bar", url="https://foo/bar/manifest.mpd")
        # ...                                           ^
        #                             No trailing slash |

        assert segment_urls == [
            [
                ("empty-baseurl", "absolute-segments", "https://foo/absolute/init_video_5000kbps.m4s"),
                ("empty-baseurl", "absolute-segments", "https://foo/absolute/media_video_5000kbps-1.m4s"),
            ],
            [
                ("empty-baseurl", "relative-segments", "https://foo/relative/init_video_5000kbps.m4s"),
                ("empty-baseurl", "relative-segments", "https://foo/relative/media_video_5000kbps-1.m4s"),

```

END.

### Debug log

```text
$ python -m streamlink --loglevel debug http://127.0.0.1:8080/mpeg-dash-test/vod/test.mpd best
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.11.9
[cli][debug] OpenSSL:    OpenSSL 3.0.13 30 Jan 2024
[cli][debug] Streamlink: 7.0.0+31.gfac5d8f7
[cli][debug] Dependencies:
[cli][debug]  certifi: 2024.6.2
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.4
[cli][debug]  pycountry: 24.6.1
[cli][debug]  pycryptodome: 3.20.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.32.3
[cli][debug]  trio: 0.25.1
[cli][debug]  trio-websocket: 0.11.1
[cli][debug]  typing-extensions: 4.12.2
[cli][debug]  urllib3: 2.2.1
[cli][debug]  websocket-client: 1.8.0
[cli][debug] Arguments:
[cli][debug]  url=http://127.0.0.1:8080/mpeg-dash-test/vod/test.mpd
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][info] Found matching plugin dash for URL http://127.0.0.1:8080/mpeg-dash-test/vod/test.mpd
[plugins.dash][debug] URL=http://127.0.0.1:8080/mpeg-dash-test/vod/test.mpd; params={}
[utils.l10n][debug] Language code: en_US
[stream.dash][debug] Available languages for DASH audio streams: NONE (using: n/a)
[cli][info] Available streams: 1080p (worst, best)
[cli][info] Opening stream: 1080p (dash)
[cli][info] Starting player: C:\Program Files\VideoLAN\VLC\vlc.exe
[stream.dash][debug] Opening DASH reader for: ('0', '0', '0') - video/mp4
[stream.dash][debug] Opening DASH reader for: ('0', '1', '1') - audio/mp4
[stream.ffmpegmux][debug] ffmpeg version n6.1.1-7-ga267d4ad4c-20240222 Copyright (c) 2000-2023 the FFmpeg developers
built with gcc 13.2.0 (crosstool-NG 1.25.0.232_c175b21)
configuration: ...
...
[stream.dash.manifest][debug] Generating segment timeline for static playlist: ('0', '0', '0')
[stream.dash][debug] video/mp4 segment initialization: downloading (1970-01-01T00:00:00.000000Z / 2024-12-14T14:13:11.643145Z)
[utils.named_pipe][info] Creating pipe streamlinkpipe-247056-1-9449
[utils.named_pipe][info] Creating pipe streamlinkpipe-247056-2-3498
[stream.dash.manifest][debug] Generating segment timeline for static playlist: ('0', '1', '1')
[stream.ffmpegmux][debug] ffmpeg command: B:\dev\ffmpeg-n6.1.1-7-ga267d4ad4c-win64-gpl-6.1\bin\ffmpeg.EXE -y -nostats -loglevel info -i \\.\pipe\streamlinkpipe-247056-1-9449 -i \\.\pipe\streamlinkpipe-247056-2-3498 -c:v copy -c:a copy -copyts -f matroska pipe:1
[stream.dash][debug] audio/mp4 segment initialization: downloading (1970-01-01T00:00:00.000000Z / 2024-12-14T14:13:11.648832Z)
[stream.ffmpegmux][debug] Starting copy to pipe: \\.\pipe\streamlinkpipe-247056-1-9449
[stream.ffmpegmux][debug] Starting copy to pipe: \\.\pipe\streamlinkpipe-247056-2-3498
[cli][debug] Pre-buffering 8192 bytes
[stream.dash][error] video/mp4 segment initialization: failed (Unable to open URL: http://127.0.0.1:8080/mpeg-dash-test/init-stream0.m4s (404 Client Error: Not Found for url: http://127.0.0.1:8080/mpeg-dash-test/init-stream0.m4s))
[stream.dash][debug] video/mp4 segment 1: downloading (1970-01-01T00:00:00.000000Z / 2024-12-14T14:13:13.762230Z)
[stream.dash][error] audio/mp4 segment initialization: failed (Unable to open URL: http://127.0.0.1:8080/mpeg-dash-test/init-stream1.m4s (404 Client Error: Not Found for url: http://127.0.0.1:8080/mpeg-dash-test/init-stream1.m4s))
[stream.dash][debug] audio/mp4 segment 1: downloading (1970-01-01T00:00:00.000000Z / 2024-12-14T14:13:13.768768Z)
[stream.dash][error] video/mp4 segment 1: failed (Unable to open URL: http://127.0.0.1:8080/mpeg-dash-test/chunk-stream0-00001.m4s (404 Client Error: Not Found for url: http://127.0.0.1:8080/mpeg-dash-test/chunk-stream0-00001.m4s))
[stream.dash][debug] video/mp4 segment 2: downloading (1970-01-01T00:00:00.000000Z / 2024-12-14T14:13:15.883276Z)
[stream.dash][error] audio/mp4 segment 1: failed (Unable to open URL: http://127.0.0.1:8080/mpeg-dash-test/chunk-stream1-00001.m4s (404 Client Error: Not Found for url: http://127.0.0.1:8080/mpeg-dash-test/chunk-stream1-00001.m4s))
...
```


Discussion:
After doing bisect, I found this issue is introduced by #6328 (53ac068d1c1eeaf574f0cc3041824891a7c663d2). To make `urllib.parse.urljoin` work correctly, we need to provide a "correct" base URL. The correctness is, for example, if we treat the parent URL of MPD as `BaseURL`, we need to make it end with a slash.

In https://github.com/streamlink/streamlink/issues/6327#issuecomment-2522739927, @bastimeyer said:

> I have no idea why the following was ever implemented, because it seems totally wrong to me and a simple `urljoin(url, other)` would suffice:
> ```py
>             if not parts[2].endswith("/"):
>                 parts[2] += "/"
> ```
> https://github.com/streamlink/streamlink/blame/7.0.0/src/streamlink/stream/dash/manifest.py#L398-L410

I guess the above lines may be related to the path processing we are talking here.

<br>

In addition, I've searched "BaseURL" in [DASH-IF-IOP-v4.3.pdf](https://dashif.org/docs/DASH-IF-IOP-v4.3.pdf) and didn't see any statement talking about the rule of the URL, so we can add the `base_url="https://foo/bar"` test to keep the maximum compatibility.
Thanks for the bug report. The BaseURL merging/joining was just updated in #6328. Your DASH manifest however does not include any `BaseURL` nodes, so the "document" BaseURL depends on the input URL. The code you're referencing from `DASHStream.fetch_manifest()` is incorrect and also need to be updated. I forgot about that in #6328. I'll have a quick look later today.



Patch:
diff --git a/src/streamlink/stream/dash/dash.py b/src/streamlink/stream/dash/dash.py
index e90df7394c8..35e1352060b 100644
--- a/src/streamlink/stream/dash/dash.py
+++ b/src/streamlink/stream/dash/dash.py
@@ -9,7 +9,6 @@
 from datetime import datetime
 from time import time
 from typing import Any
-from urllib.parse import urlparse, urlunparse
 
 from requests import Response
 
@@ -253,11 +252,7 @@ def fetch_manifest(session: Streamlink, url_or_manifest: str, **request_args) ->
         manifest: str = res.text
         url: str = res.url
 
-        urlp = list(urlparse(url))
-        urlp[2], _ = urlp[2].rsplit("/", 1)
-        base_url: str = urlunparse(urlp)
-
-        return manifest, dict(url=url, base_url=base_url)
+        return manifest, dict(url=url, base_url=url)
 
     @staticmethod
     def parse_mpd(manifest: str, mpd_params: Mapping[str, Any]) -> MPD:


Test patch:
diff --git a/tests/stream/dash/test_dash.py b/tests/stream/dash/test_dash.py
index db17ae468e3..e54f73efa28 100644
--- a/tests/stream/dash/test_dash.py
+++ b/tests/stream/dash/test_dash.py
@@ -72,7 +72,7 @@ def test_video_only(self, session: Streamlink, mpd: Mock):
         mpd.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])])
 
         streams = DASHStream.parse_manifest(session, "http://test/manifest.mpd")
-        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test")]
+        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test/manifest.mpd")]
         assert sorted(streams.keys()) == sorted(["720p", "1080p"])
 
     def test_audio_only(self, session: Streamlink, mpd: Mock):
@@ -86,7 +86,7 @@ def test_audio_only(self, session: Streamlink, mpd: Mock):
         mpd.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])])
 
         streams = DASHStream.parse_manifest(session, "http://test/manifest.mpd")
-        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test")]
+        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test/manifest.mpd")]
         assert sorted(streams.keys()) == sorted(["a128k", "a256k"])
 
     @pytest.mark.parametrize(
@@ -143,7 +143,7 @@ def test_with_videoaudio_only(
             with_video_only=with_video_only,
             with_audio_only=with_audio_only,
         )
-        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test")]
+        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test/manifest.mpd")]
         assert list(streams.keys()) == expected
 
     def test_audio_single(self, session: Streamlink, mpd: Mock):
@@ -158,7 +158,7 @@ def test_audio_single(self, session: Streamlink, mpd: Mock):
         mpd.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])])
 
         streams = DASHStream.parse_manifest(session, "http://test/manifest.mpd")
-        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test")]
+        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test/manifest.mpd")]
         assert sorted(streams.keys()) == sorted(["720p", "1080p"])
 
     def test_audio_multi(self, session: Streamlink, mpd: Mock):
@@ -174,7 +174,7 @@ def test_audio_multi(self, session: Streamlink, mpd: Mock):
         mpd.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])])
 
         streams = DASHStream.parse_manifest(session, "http://test/manifest.mpd")
-        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test")]
+        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test/manifest.mpd")]
         assert sorted(streams.keys()) == sorted(["720p+a128k", "1080p+a128k", "720p+a256k", "1080p+a256k"])
 
     def test_audio_multi_lang(self, session: Streamlink, mpd: Mock):
@@ -190,7 +190,7 @@ def test_audio_multi_lang(self, session: Streamlink, mpd: Mock):
         mpd.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])])
 
         streams = DASHStream.parse_manifest(session, "http://test/manifest.mpd")
-        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test")]
+        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test/manifest.mpd")]
         assert sorted(streams.keys()) == sorted(["720p", "1080p"])
         assert getattr(streams["720p"].audio_representation, "lang", None) == "en"
         assert getattr(streams["1080p"].audio_representation, "lang", None) == "en"
@@ -208,7 +208,7 @@ def test_audio_multi_lang_alpha3(self, session: Streamlink, mpd: Mock):
         mpd.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])])
 
         streams = DASHStream.parse_manifest(session, "http://test/manifest.mpd")
-        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test")]
+        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test/manifest.mpd")]
         assert sorted(streams.keys()) == sorted(["720p", "1080p"])
         assert getattr(streams["720p"].audio_representation, "lang", None) == "eng"
         assert getattr(streams["1080p"].audio_representation, "lang", None) == "eng"
@@ -225,7 +225,7 @@ def test_audio_invalid_lang(self, session: Streamlink, mpd: Mock):
         mpd.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])])
 
         streams = DASHStream.parse_manifest(session, "http://test/manifest.mpd")
-        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test")]
+        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test/manifest.mpd")]
         assert sorted(streams.keys()) == sorted(["720p", "1080p"])
         assert getattr(streams["720p"].audio_representation, "lang", None) == "en_no_voice"
         assert getattr(streams["1080p"].audio_representation, "lang", None) == "en_no_voice"
@@ -245,7 +245,7 @@ def test_audio_multi_lang_locale(self, monkeypatch: pytest.MonkeyPatch, session:
         mpd.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])])
 
         streams = DASHStream.parse_manifest(session, "http://test/manifest.mpd")
-        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test")]
+        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test/manifest.mpd")]
         assert sorted(streams.keys()) == sorted(["720p", "1080p"])
         assert getattr(streams["720p"].audio_representation, "lang", None) == "es"
         assert getattr(streams["1080p"].audio_representation, "lang", None) == "es"
@@ -264,7 +264,7 @@ def test_duplicated_resolutions(self, session: Streamlink, mpd: Mock):
         mpd.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])])
 
         streams = DASHStream.parse_manifest(session, "http://test/manifest.mpd")
-        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test")]
+        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test/manifest.mpd")]
         assert sorted(streams.keys()) == sorted(["720p", "1080p", "1080p_alt", "1080p_alt2"])
 
     # Verify the fix for https://github.com/streamlink/streamlink/issues/4217
@@ -280,7 +280,7 @@ def test_duplicated_resolutions_sorted_bandwidth(self, session: Streamlink, mpd:
         mpd.return_value = Mock(periods=[Mock(adaptationSets=[adaptationset])])
 
         streams = DASHStream.parse_manifest(session, "http://test/manifest.mpd")
-        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test")]
+        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test/manifest.mpd")]
         assert getattr(streams["1080p"].video_representation, "bandwidth", None) == pytest.approx(128.0)
         assert getattr(streams["1080p_alt"].video_representation, "bandwidth", None) == pytest.approx(64.0)
         assert getattr(streams["1080p_alt2"].video_representation, "bandwidth", None) == pytest.approx(32.0)
@@ -319,10 +319,10 @@ def test_string(self, session: Streamlink, mpd: Mock, parse_xml: Mock):
     #       This test currently achieves nothing... (manifest fixture added in 7aada92)
     def test_segments_number_time(self, session: Streamlink, mpd: Mock):
         with xml("dash/test_9.mpd") as mpd_xml:
-            mpd.return_value = MPD(mpd_xml, base_url="http://test", url="http://test/manifest.mpd")
+            mpd.return_value = MPD(mpd_xml, base_url="http://test/manifest.mpd", url="http://test/manifest.mpd")
 
         streams = DASHStream.parse_manifest(session, "http://test/manifest.mpd")
-        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test")]
+        assert mpd.call_args_list == [call(ANY, url="http://test/manifest.mpd", base_url="http://test/manifest.mpd")]
         assert list(streams.keys()) == ["480p"]
 
 
diff --git a/tests/stream/dash/test_manifest.py b/tests/stream/dash/test_manifest.py
index 77543a76651..d82fd3aefa8 100644
--- a/tests/stream/dash/test_manifest.py
+++ b/tests/stream/dash/test_manifest.py
@@ -552,7 +552,55 @@ def test_segments_byterange(self):
             ],
         ]
 
-    def test_baseurl_urljoin(self):
+    def test_baseurl_urljoin_no_trailing_slash(self):
+        with xml("dash/test_baseurl_urljoin.mpd") as mpd_xml:
+            mpd = MPD(mpd_xml, base_url="https://foo/bar", url="https://test/manifest.mpd")
+
+        segment_urls = [
+            [
+                (period.id, adaptationset.id, segment.uri)
+                for segment in itertools.islice(representation.segments(), 2)
+            ]
+            for period in mpd.periods
+            for adaptationset in period.adaptationSets
+            for representation in adaptationset.representations
+        ]  # fmt: skip
+        assert segment_urls == [
+            [
+                ("empty-baseurl", "absolute-segments", "https://foo/absolute/init_video_5000kbps.m4s"),
+                ("empty-baseurl", "absolute-segments", "https://foo/absolute/media_video_5000kbps-1.m4s"),
+            ],
+            [
+                ("empty-baseurl", "relative-segments", "https://foo/relative/init_video_5000kbps.m4s"),
+                ("empty-baseurl", "relative-segments", "https://foo/relative/media_video_5000kbps-1.m4s"),
+            ],
+            [
+                ("baseurl-with-scheme", "absolute-segments", "https://host/absolute/init_video_5000kbps.m4s"),
+                ("baseurl-with-scheme", "absolute-segments", "https://host/absolute/media_video_5000kbps-1.m4s"),
+            ],
+            [
+                ("baseurl-with-scheme", "relative-segments", "https://host/path/relative/init_video_5000kbps.m4s"),
+                ("baseurl-with-scheme", "relative-segments", "https://host/path/relative/media_video_5000kbps-1.m4s"),
+            ],
+            [
+                ("absolute-baseurl", "absolute-segments", "https://foo/absolute/init_video_5000kbps.m4s"),
+                ("absolute-baseurl", "absolute-segments", "https://foo/absolute/media_video_5000kbps-1.m4s"),
+            ],
+            [
+                ("absolute-baseurl", "relative-segments", "https://foo/path/relative/init_video_5000kbps.m4s"),
+                ("absolute-baseurl", "relative-segments", "https://foo/path/relative/media_video_5000kbps-1.m4s"),
+            ],
+            [
+                ("relative-baseurl", "absolute-segments", "https://foo/absolute/init_video_5000kbps.m4s"),
+                ("relative-baseurl", "absolute-segments", "https://foo/absolute/media_video_5000kbps-1.m4s"),
+            ],
+            [
+                ("relative-baseurl", "relative-segments", "https://foo/path/relative/init_video_5000kbps.m4s"),
+                ("relative-baseurl", "relative-segments", "https://foo/path/relative/media_video_5000kbps-1.m4s"),
+            ],
+        ]
+
+    def test_baseurl_urljoin_with_trailing_slash(self):
         with xml("dash/test_baseurl_urljoin.mpd") as mpd_xml:
             mpd = MPD(mpd_xml, base_url="https://foo/bar/", url="https://test/manifest.mpd")
 
@@ -600,6 +648,7 @@ def test_baseurl_urljoin(self):
             ],
         ]
 
+    def test_baseurl_urljoin_empty(self):
         with xml("dash/test_baseurl_urljoin.mpd") as mpd_xml:
             mpd = MPD(mpd_xml, base_url="", url="https://test/manifest.mpd")
 
@@ -647,7 +696,7 @@ def test_baseurl_urljoin(self):
             ],
         ]
 
-    def test_nested_baseurls(self):
+    def test_baseurl_nested(self):
         with xml("dash/test_baseurl_nested.mpd") as mpd_xml:
             mpd = MPD(mpd_xml, base_url="https://foo/", url="https://test/manifest.mpd")