Repository: streamlink/streamlink

Issue:
plugins.bilibili: support for higher stream qualities
### Checklist

- [X] 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.4.2

### Description

You can only get a fixed cookie if you can only get 480P without Cookie. Even if the user does not enter the cookie, it can still be used. This can ensure anonymity

### Debug log

```text
streamlink https://live.bilibili.com/545068 best -p mpv --loglevel=debug
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.11.6
[cli][debug] OpenSSL:    OpenSSL 3.0.11 19 Sep 2023
[cli][debug] Streamlink: 6.4.2
[cli][debug] Dependencies:
[cli][debug]  certifi: 2023.11.17
[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.23.1
[cli][debug]  trio-websocket: 0.11.1
[cli][debug]  typing-extensions: 4.8.0
[cli][debug]  urllib3: 2.1.0
[cli][debug]  websocket-client: 1.6.4
[cli][debug] Arguments:
[cli][debug]  url=https://live.bilibili.com/545068
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --player=mpv
[cli][info] Found matching plugin bilibili for URL https://live.bilibili.com/545068
[cli][info] Available streams: live_alt, live (worst, best)
[cli][info] Opening stream: live (hls)
[cli][info] Starting player: mpv
[stream.hls][debug] Reloading playlist
[cli][debug] Pre-buffering 8192 bytes
```


Discussion:
user00036: Has this change been done, I found out that v1 may not be able to play some live streams, I recommend v1 and v2 to get the live streams at the same time and take the one with the highest stream quality
bastimeyer: https://github.com/streamlink/streamlink/issues/6331#issuecomment-2531859781

Patch:
diff --git a/src/streamlink/plugins/bilibili.py b/src/streamlink/plugins/bilibili.py
index 1a5a1121..cebd7482 100644
--- a/src/streamlink/plugins/bilibili.py
+++ b/src/streamlink/plugins/bilibili.py
@@ -10,6 +10,7 @@ import re
 from streamlink.exceptions import NoStreamsError
 from streamlink.plugin import Plugin, pluginmatcher
 from streamlink.plugin.api import validate
+from streamlink.stream import HTTPStream
 from streamlink.stream.hls import HLSStream
 
 
@@ -17,17 +18,64 @@ log = logging.getLogger(__name__)
 
 
 @pluginmatcher(
-    re.compile(r"https?://live\.bilibili\.com/(?P<channel>[^/]+)"),
+    re.compile(r"https?://live\.bilibili\.com/(?P<channel>[^/?#]+)"),
 )
 class Bilibili(Plugin):
-    _URL_API_PLAYINFO = "https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo"
+    _URL_API_V1_PLAYURL = "https://api.live.bilibili.com/room/v1/Room/playUrl"
+    _URL_API_V2_PLAYINFO = "https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo"
 
     SHOW_STATUS_OFFLINE = 0
     SHOW_STATUS_ONLINE = 1
     SHOW_STATUS_ROUND = 2
 
-    @staticmethod
-    def _schema_streams():
+    @classmethod
+    def stream_weight(cls, stream):
+        offset = 1 if "_alt" in stream else 0
+        if stream.startswith("httpstream"):
+            return 4 - offset, stream
+        if stream.startswith("hls"):
+            return 2 - offset, stream
+        return super().stream_weight(stream)
+
+    def _get_api_v1_playurl(self, room_id):
+        return self.session.http.get(
+            self._URL_API_V1_PLAYURL,
+            params={
+                "cid": room_id,
+                "platform": "web",
+                "quality": "4",
+            },
+            schema=validate.Schema(
+                validate.parse_json(),
+                validate.any(
+                    validate.all(
+                        {
+                            "code": 0,
+                            "data": {
+                                "durl": [
+                                    validate.all(
+                                        {
+                                            "url": validate.url(),
+                                        },
+                                        validate.get("url"),
+                                    ),
+                                ],
+                            },
+                        },
+                        validate.get(("data", "durl")),
+                    ),
+                    validate.all(
+                        {
+                            "code": int,
+                        },
+                        validate.transform(lambda _: []),
+                    ),
+                ),
+            ),
+        )
+
+    @property
+    def _schema_v2_streams(self):
         return validate.all(
             [
                 {
@@ -60,9 +108,9 @@ class Bilibili(Plugin):
             validate.filter(lambda item: item["protocol_name"] == "http_hls"),
         )
 
-    def _get_api_playinfo(self, room_id):
+    def _get_api_v2_playinfo(self, room_id):
         return self.session.http.get(
-            self._URL_API_PLAYINFO,
+            self._URL_API_V2_PLAYINFO,
             params={
                 "room_id": room_id,
                 "no_playurl": 0,
@@ -83,7 +131,7 @@ class Bilibili(Plugin):
                         "playurl_info": validate.none_or_all(
                             {
                                 "playurl": {
-                                    "stream": self._schema_streams(),
+                                    "stream": self._schema_v2_streams,
                                 },
                             },
                             validate.get(("playurl", "stream")),
@@ -110,7 +158,7 @@ class Bilibili(Plugin):
                                 "playurl_info": validate.none_or_all(
                                     {
                                         "playurl": {
-                                            "stream": self._schema_streams(),
+                                            "stream": self._schema_v2_streams,
                                         },
                                     },
                                     validate.get(("playurl", "stream")),
@@ -154,17 +202,23 @@ class Bilibili(Plugin):
         return streams
 
     def _get_streams(self):
-        streams = self._get_page_playinfo()
-        if not streams:
-            log.debug("Falling back to _get_api_playinfo()")
-            streams = self._get_api_playinfo(self.match["channel"])
-
-        for stream in streams or []:
-            for stream_format in stream["format"]:
+        http_streams = self._get_api_v1_playurl(self.match["channel"])
+        for http_stream in http_streams:
+            if self.session.http.head(http_stream, raise_for_status=False).status_code >= 400:
+                continue
+            yield "httpstream", HTTPStream(self.session, http_stream)
+
+        hls_streams = self._get_page_playinfo()
+        if not hls_streams:
+            log.debug("Falling back to _get_api_v2_playinfo()")
+            hls_streams = self._get_api_v2_playinfo(self.match["channel"])
+
+        for hls_stream in hls_streams or []:
+            for stream_format in hls_stream["format"]:
                 for codec in stream_format["codec"]:
                     for url_info in codec["url_info"]:
                         url = f"{url_info['host']}{codec['base_url']}{url_info['extra']}"
-                        yield "live", HLSStream(self.session, url)
+                        yield "hls", HLSStream(self.session, url)
 
 
 __plugin__ = Bilibili


Test patch:
diff --git a/tests/plugins/test_bilibili.py b/tests/plugins/test_bilibili.py
index 0ac77acf..69b65c6f 100644
--- a/tests/plugins/test_bilibili.py
+++ b/tests/plugins/test_bilibili.py
@@ -6,5 +6,5 @@ class TestPluginCanHandleUrlBilibili(PluginCanHandleUrl):
     __plugin__ = Bilibili
 
     should_match_groups = [
-        ("https://live.bilibili.com/CHANNEL", {"channel": "CHANNEL"}),
+        ("https://live.bilibili.com/CHANNEL?live_from=78001", {"channel": "CHANNEL"}),
     ]