Repository: streamlink/streamlink

Issue:
stream.hls: BANDWIDTH=0 raises ValueError: math domain error
### 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)

### Streamlink version

6.2.1

### Description

We have noticed that Streamlink does not work with Twitch Highlight videos. It fails to locate a 'playable stream.' VODs and clips, on the other hand, work without any issues.

Example Link: https://www.twitch.tv/videos/1946928063

### Debug log

```text
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.11.5
[cli][debug] OpenSSL:    OpenSSL 3.0.9 30 May 2023
[cli][debug] Streamlink: 6.2.1
[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.3
[cli][debug] Arguments:
[cli][debug]  url=https://www.twitch.tv/videos/1946928063
[cli][debug]  --loglevel=debug
[cli][debug]  --ffmpeg-ffmpeg=C:\Program Files\Streamlink\ffmpeg\ffmpeg.exe
[cli][info] Found matching plugin twitch for URL https://www.twitch.tv/videos/1946928063
[plugins.twitch][debug] Getting HLS streams for video ID 1946928063
[utils.l10n][debug] Language code: en_US
error: No playable streams found on this URL: https://www.twitch.tv/videos/1946928063
```



Discussion:

Resolving patch:
diff --git a/src/streamlink/stream/hls/m3u8.py b/src/streamlink/stream/hls/m3u8.py
--- a/src/streamlink/stream/hls/m3u8.py
+++ b/src/streamlink/stream/hls/m3u8.py
@@ -156,8 +156,11 @@ def __init__(self, base_uri: Optional[str] = None):
     def create_stream_info(cls, streaminf: Mapping[str, Optional[str]], streaminfoclass=None):
         program_id = streaminf.get("PROGRAM-ID")
 
-        _bandwidth = streaminf.get("BANDWIDTH")
-        bandwidth = 0 if not _bandwidth else round(int(_bandwidth), 1 - int(math.log10(int(_bandwidth))))
+        try:
+            bandwidth = int(streaminf.get("BANDWIDTH") or 0)
+            bandwidth = round(bandwidth, 1 - int(math.log10(bandwidth)))
+        except ValueError:
+            bandwidth = 0
 
         _resolution = streaminf.get("RESOLUTION")
         resolution = None if not _resolution else cls.parse_resolution(_resolution)


Test patch:
diff --git a/tests/resources/hls/test_multivariant_bandwidth.m3u8 b/tests/resources/hls/test_multivariant_bandwidth.m3u8
new file mode 100644
--- /dev/null
+++ b/tests/resources/hls/test_multivariant_bandwidth.m3u8
@@ -0,0 +1,19 @@
+#EXTM3U
+#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="chunked",NAME="1080p60",AUTOSELECT=NO,DEFAULT=NO
+#EXT-X-STREAM-INF:BANDWIDTH=0,CODECS="avc1.64002A,mp4a.40.2",RESOLUTION=1920x1080,VIDEO="chunked",FRAME-RATE=59.993
+chunked.m3u8
+#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="audio_only",NAME="Audio Only",AUTOSELECT=NO,DEFAULT=NO
+#EXT-X-STREAM-INF:CODECS="mp4a.40.2",VIDEO="audio_only"
+audio_only.m3u8
+#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="720p60",NAME="720p60",AUTOSELECT=YES,DEFAULT=YES
+#EXT-X-STREAM-INF:BANDWIDTH=3000000,CODECS="avc1.4D0020,mp4a.40.2",RESOLUTION=1280x720,VIDEO="720p60",FRAME-RATE=59.993
+720p60.m3u8
+#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="480p30",NAME="480p",AUTOSELECT=YES,DEFAULT=YES
+#EXT-X-STREAM-INF:BANDWIDTH=1500000,CODECS="avc1.4D001F,mp4a.40.2",RESOLUTION=852x480,VIDEO="480p30",FRAME-RATE=29.996
+480p30.m3u8
+#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="360p30",NAME="360p",AUTOSELECT=YES,DEFAULT=YES
+#EXT-X-STREAM-INF:BANDWIDTH=700000,CODECS="avc1.4D001E,mp4a.40.2",RESOLUTION=640x360,VIDEO="360p30",FRAME-RATE=29.996
+360p30.m3u8
+#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="160p30",NAME="160p",AUTOSELECT=YES,DEFAULT=YES
+#EXT-X-STREAM-INF:BANDWIDTH=300000,CODECS="avc1.4D000C,mp4a.40.2",RESOLUTION=284x160,VIDEO="160p30",FRAME-RATE=29.996
+160p30.m3u8
diff --git a/tests/stream/hls/test_m3u8.py b/tests/stream/hls/test_m3u8.py
--- a/tests/stream/hls/test_m3u8.py
+++ b/tests/stream/hls/test_m3u8.py
@@ -4,9 +4,11 @@
 import pytest
 
 from streamlink.stream.hls import (
+    M3U8,
     ByteRange,
     DateRange,
     ExtInf,
+    HLSPlaylist,
     HLSSegment,
     M3U8Parser,
     Media,
@@ -551,3 +553,16 @@ def test_parse_date(self) -> None:
                == [None, True, True, True, False, False, False, True, True, None]
         assert [playlist.is_date_in_daterange(playlist.segments[3].date, daterange) for daterange in playlist.dateranges] \
                == [None, True, True, True, False, False, False, False, False, None]
+
+    def test_parse_bandwidth(self) -> None:
+        with text("hls/test_multivariant_bandwidth.m3u8") as m3u8_fh:
+            playlist: M3U8[HLSSegment, HLSPlaylist] = parse_m3u8(m3u8_fh.read(), "http://mocked/", parser=M3U8Parser)
+
+        assert [(pl.stream_info.video, pl.stream_info.bandwidth) for pl in playlist.playlists] == [
+            ("chunked", 0),
+            ("audio_only", 0),
+            ("720p60", 3000000),
+            ("480p30", 1500000),
+            ("360p30", 700000),
+            ("160p30", 300000),
+        ]