Repository: streamlink/streamlink

Issue:
plugins.steam: DASH segment requests missing token query-string parameter, sometimes leading to 403 error
### Checklist

- [X] This is a plugin issue 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 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

Latest build from the master branch

### Description

Segment requests on the webplayer on steamcommunity.com always include a `token` query-string parameter with the user's IP and an hmac. The Streamlink plugin does not include that, and requests can fail with a 403, but only sometimes.

Webplayer request with `token`
```
https://steambroadcast.akamaized.net/broadcast/76561199177801954/3374786983679056684/segment/video/1/init/?broadcast_origin=ext2-mad1.steamserver.net&token=ip=REDACTED~exp=1678046973~acl=/broadcast/76561199177801954/3374786983679056684/*~hmac=98dabb3a60f2656bb9224234df1bffaa6d44e9b9
```

### Debug log

```text
$ streamlink -l debug 'https://steamcommunity.com/broadcast/watch/76561199177801954' best
[cli][debug] OS:         Linux-6.1.12-1-git-x86_64-with-glibc2.37
[cli][debug] Python:     3.11.1
[cli][debug] Streamlink: 5.3.1+12.g41108844
[cli][debug] Dependencies:
[cli][debug]  certifi: 2022.12.7
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.2
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.17
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.28.2
[cli][debug]  urllib3: 1.26.14
[cli][debug]  websocket-client: 1.5.1
[cli][debug] Arguments:
[cli][debug]  url=https://steamcommunity.com/broadcast/watch/76561199177801954
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --player=mpv
[plugins.steam][debug] Restored cookies: REDACTED
[cli][info] Found matching plugin steam for URL https://steamcommunity.com/broadcast/watch/76561199177801954
[plugins.steam][debug] Getting broadcast stream: sessionid=REDACTED
[utils.l10n][debug] Language code: en_US
[stream.dash][debug] Available languages for DASH audio streams: und (using: und)
[cli][info] Available streams: 360p (worst), 480p, 720p (best)
[cli][info] Opening stream: 720p (dash)
[cli][info] Starting player: mpv
[stream.dash][debug] Opening DASH reader for: ('1', 'video', '1') - video/mp4
[stream.dash][debug] Opening DASH reader for: ('1', 'audio', '1') - audio/mp4
[stream.dash_manifest][debug] Generating segment numbers for dynamic playlist: ('1', 'video', '1')
[stream.dash_manifest][debug] Stream start: 2023-03-05 09:15:06+00:00
[stream.dash_manifest][debug] Current time: 2023-03-05 17:07:21.152912+00:00
[stream.dash_manifest][debug] Availability: 2023-03-05 17:07:14+00:00
[stream.dash_manifest][debug] presentationTimeOffset: 0:00:00; suggestedPresentationDelay: 0:00:03; minBufferTime: 0:00:03
[stream.dash_manifest][debug] segmentDuration: 2.0; segmentStart: 1; segmentOffset: 14165 (28329.152912s)
[stream.dash_manifest][debug] Generating segment numbers for dynamic playlist: ('1', 'audio', '1')
[stream.dash_manifest][debug] Stream start: 2023-03-05 09:15:06+00:00
[stream.dash_manifest][debug] Current time: 2023-03-05 17:07:21.155688+00:00
[stream.dash_manifest][debug] Availability: 2023-03-05 17:07:14+00:00
[stream.dash_manifest][debug] presentationTimeOffset: 0:00:00; suggestedPresentationDelay: 0:00:03; minBufferTime: 0:00:03
[stream.dash_manifest][debug] segmentDuration: 2.0; segmentStart: 1; segmentOffset: 14165 (28329.155688s)
[stream.dash][error] Failed to open video/mp4 segment https://steambroadcast.akamaized.net/broadcast/76561199177801954/3374786983679056684/segment/video/1/init/?broadcast_origin=ext2-mad1.steamserver.net: Unable to open URL: https://steambroadcast.akamaized.net/broadcast/76561199177801954/3374786983679056684/segment/video/1/init/?broadcast_origin=ext2-mad1.steamserver.net (403 Client Error: Forbidden for url: https://steambroadcast.akamaized.net/broadcast/76561199177801954/3374786983679056684/segment/video/1/init/?broadcast_origin=ext2-mad1.steamserver.net)
[stream.ffmpegmux][debug] ffmpeg version n5.1.2 Copyright (c) 2000-2022 the FFmpeg developers
[stream.ffmpegmux][debug]  built with gcc 12.2.0 (GCC)
[stream.ffmpegmux][debug]  configuration: --prefix=/usr --disable-debug --disable-static --disable-stripping --enable-amf --enable-avisynth --enable-cuda-llvm --enable-lto --enable-fontconfig --enable-gmp --enable-gnutls --enable-gpl --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libdav1d --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libiec61883 --enable-libjack --enable-libmfx --enable-libmodplug --enable-libmp3lame --enable-libopencore_amrnb --enable-libopencore_amrwb --enable-libopenjpeg --enable-libopus --enable-libpulse --enable-librav1e --enable-librsvg --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libtheora --enable-libv4l2 --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxcb --enable-libxml2 --enable-libxvid --enable-libzimg --enable-nvdec --enable-nvenc --enable-opencl --enable-opengl --enable-shared --enable-version3 --enable-vulkan
[stream.ffmpegmux][debug]  libavutil      57. 28.100 / 57. 28.100
[stream.ffmpegmux][debug]  libavcodec     59. 37.100 / 59. 37.100
[stream.ffmpegmux][debug]  libavformat    59. 27.100 / 59. 27.100
[stream.ffmpegmux][debug]  libavdevice    59.  7.100 / 59.  7.100
[stream.ffmpegmux][debug]  libavfilter     8. 44.100 /  8. 44.100
[stream.ffmpegmux][debug]  libswscale      6.  7.100 /  6.  7.100
[stream.ffmpegmux][debug]  libswresample   4.  7.100 /  4.  7.100
[stream.ffmpegmux][debug]  libpostproc    56.  6.100 / 56.  6.100
[utils.named_pipe][info] Creating pipe streamlinkpipe-574031-1-7242
[utils.named_pipe][info] Creating pipe streamlinkpipe-574031-2-2694
[stream.ffmpegmux][debug] ffmpeg command: /usr/bin/ffmpeg -nostats -y -i /tmp/streamlinkpipe-574031-1-7242 -i /tmp/streamlinkpipe-574031-2-2694 -c:v copy -c:a copy -copyts -f matroska pipe:1
[stream.ffmpegmux][debug] Starting copy to pipe: /tmp/streamlinkpipe-574031-1-7242
[stream.ffmpegmux][debug] Starting copy to pipe: /tmp/streamlinkpipe-574031-2-2694
[cli][debug] Pre-buffering 8192 bytes
[stream.dash][error] Failed to open video/mp4 segment https://steambroadcast.akamaized.net/broadcast/76561199177801954/3374786983679056684/segment/video/1/init/?broadcast_origin=ext2-mad1.steamserver.net: Unable to open URL: https://steambroadcast.akamaized.net/broadcast/76561199177801954/3374786983679056684/segment/video/1/init/?broadcast_origin=ext2-mad1.steamserver.net (403 Client Error: Forbidden for url: https://steambroadcast.akamaized.net/broadcast/76561199177801954/3374786983679056684/segment/video/1/init/?broadcast_origin=ext2-mad1.steamserver.net)
...
```



Discussion:

Resolving patch:
diff --git a/src/streamlink/plugins/steam.py b/src/streamlink/plugins/steam.py
--- a/src/streamlink/plugins/steam.py
+++ b/src/streamlink/plugins/steam.py
@@ -137,7 +137,7 @@ def dologin(self, email, password, emailauth="", emailsteamid="", captchagid="-1
                 if not captcha_text:
                     return False
             else:
-                # If the user must enter the code that was emailed to them
+                # Whether the user must enter the code that was emailed to them
                 if resp.get("emailauth_needed"):
                     if emailauth:
                         raise SteamLoginFailed("Email auth key error")
@@ -148,7 +148,7 @@ def dologin(self, email, password, emailauth="", emailsteamid="", captchagid="-1
                     if not emailauth:
                         return False
 
-                # If the user must enter a two factor auth code
+                # Whether the user must enter a two-factor auth code
                 if resp.get("requires_twofactor"):
                     try:
                         twofactorcode = self.input_ask("Two factor auth code required")
@@ -186,12 +186,11 @@ def _get_broadcast_stream(self, steamid, viewertoken=0, sessionid=None):
             schema=validate.Schema(
                 validate.parse_json(),
                 {
-                    "success": validate.any("ready", "unavailable", "waiting", "waiting_to_start", "waiting_for_start"),
-                    "retry": int,
-                    "broadcastid": validate.any(str, int),
-                    validate.optional("url"): validate.url(),
-                    validate.optional("viewertoken"): str,
+                    "success": str,
+                    "url": validate.url(),
+                    "cdn_auth_url_parameters": validate.any(str, None),
                 },
+                validate.union_get("success", "url", "cdn_auth_url_parameters"),
             ),
         )
 
@@ -232,20 +231,17 @@ def _get_streams(self):
         res = self.session.http.get(self.url)  # get the page to set some cookies
         sessionid = res.cookies.get("sessionid")
 
-        streamdata = None
-        while streamdata is None or streamdata["success"] in ("waiting", "waiting_for_start"):
-            streamdata = self._get_broadcast_stream(steamid, sessionid=sessionid)
+        success, url, cdn_auth = self._get_broadcast_stream(steamid, sessionid=sessionid)
+        if success != "ready":
+            log.error("This stream is currently unavailable")
+            return
 
-            if streamdata["success"] == "ready":
-                return DASHStream.parse_manifest(self.session, streamdata["url"])
+        # CDN auth data is only required at certain times of the day, and it makes
+        # segment requests fail if missing (the DASH manifest still works regardless).
+        # Remove leading ampersand and pass the params as a string, to avoid URL quoting.
+        params = re.sub(r"^&", "", cdn_auth) if cdn_auth else None
 
-            if streamdata["success"] == "unavailable":
-                log.error("This stream is currently unavailable")
-                return
-
-            r = streamdata["retry"] / 1000.0
-            log.info(f"Waiting for stream, will retry again in {r:.1f} seconds...")
-            time.sleep(r)
+        return DASHStream.parse_manifest(self.session, url, params=params)
 
 
 __plugin__ = SteamBroadcastPlugin


Test patch: