Repository: streamlink/streamlink

Issue:
plugins.bilibili: Some live streams cannot be obtained
### 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.1

### Description

Some live broadcast browsers on station B can be watched, but cannot be obtained using streamlink. Some even have a black screen when viewed through browsers. I guess it may be a CDN problem.

### Debug log

```text
streamlink https://live.bilibili.com/24022841 best --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.1
[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/24022841
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][info] Found matching plugin bilibili for URL https://live.bilibili.com/24022841
error: Unable to validate JSON: ValidationError(dict):
  Unable to validate value of key 'data'
  Context(AnySchema):
    ValidationError(equality):
      <{'current_quality': 0, 'accept_quality': None, 'current...> does not equal None
    ValidationError(dict):
      Key 'durl' not found in <{'current_quality': 0, 'accept_quality': None, 'current...>
```



Discussion:
The plugin requires a full rewrite...

The stream URLs are now embedded as JSON on the live URL itself. I have already a working re-implementation of the plugin in my local git repo. However, they only appear to have low-quality streams for regular users. The streamer's input stream and re-encoded high-quality streams all require a login, and creating a new account requires a phone number or some Chinese social media accounts, so this is a dead end for me.

Resolving patch:
diff --git a/src/streamlink/plugins/bilibili.py b/src/streamlink/plugins/bilibili.py
--- a/src/streamlink/plugins/bilibili.py
+++ b/src/streamlink/plugins/bilibili.py
@@ -4,94 +4,104 @@
 $type live
 """
 
-import logging
 import re
-from urllib.parse import urlparse
 
 from streamlink.plugin import Plugin, pluginmatcher
 from streamlink.plugin.api import validate
-from streamlink.stream.http import HTTPStream
-
-
-log = logging.getLogger(__name__)
-
-API_URL = "https://api.live.bilibili.com/room/v1/Room/playUrl"
-ROOM_API = "https://api.live.bilibili.com/room/v1/Room/room_init?id={}"
-SHOW_STATUS_OFFLINE = 0
-SHOW_STATUS_ONLINE = 1
-SHOW_STATUS_ROUND = 2
-STREAM_WEIGHTS = {
-    "source": 1080,
-}
-
-_room_id_schema = validate.Schema(
-    {
-        "data": validate.any(None, {
-            "room_id": int,
-            "live_status": int,
-        }),
-    },
-    validate.get("data"),
-)
-
-_room_stream_list_schema = validate.Schema(
-    {
-        "data": validate.any(None, {
-            "durl": [{"url": validate.url()}],
-        }),
-    },
-    validate.get("data"),
-)
+from streamlink.stream.hls import HLSStream
 
 
 @pluginmatcher(re.compile(
     r"https?://live\.bilibili\.com/(?P<channel>[^/]+)",
 ))
 class Bilibili(Plugin):
-    @classmethod
-    def stream_weight(cls, stream):
-        if stream in STREAM_WEIGHTS:
-            return STREAM_WEIGHTS[stream], "Bilibili"
-
-        return Plugin.stream_weight(stream)
+    SHOW_STATUS_OFFLINE = 0
+    SHOW_STATUS_ONLINE = 1
+    SHOW_STATUS_ROUND = 2
 
     def _get_streams(self):
-        self.session.http.headers.update({"Referer": self.url})
-        channel = self.match.group("channel")
-        res_room_id = self.session.http.get(ROOM_API.format(channel))
-        room_id_json = self.session.http.json(res_room_id, schema=_room_id_schema)
-        room_id = room_id_json["room_id"]
-        if room_id_json["live_status"] != SHOW_STATUS_ONLINE:
+        schema_stream = validate.all(
+            [{
+                "protocol_name": str,
+                "format": validate.all(
+                    [{
+                        "format_name": str,
+                        "codec": validate.all(
+                            [{
+                                "codec_name": str,
+                                "base_url": str,
+                                "url_info": [{
+                                    "host": validate.url(),
+                                    "extra": str,
+                                }],
+                            }],
+                            validate.filter(lambda item: item["codec_name"] == "avc"),
+                        ),
+                    }],
+                    validate.filter(lambda item: item["format_name"] == "fmp4"),
+                ),
+            }],
+            validate.filter(lambda item: item["protocol_name"] == "http_hls"),
+        )
+
+        data = self.session.http.get(
+            self.url,
+            schema=validate.Schema(
+                validate.parse_html(),
+                validate.xml_xpath_string(".//script[contains(text(),'window.__NEPTUNE_IS_MY_WAIFU__={')][1]/text()"),
+                validate.none_or_all(
+                    validate.transform(str.replace, "window.__NEPTUNE_IS_MY_WAIFU__=", ""),
+                    validate.parse_json(),
+                    {
+                        "roomInitRes": {
+                            "data": {
+                                "live_status": int,
+                                "playurl_info": {
+                                    "playurl": {
+                                        "stream": schema_stream,
+                                    },
+                                },
+                            },
+                        },
+                        "roomInfoRes": {
+                            "data": {
+                                "room_info": {
+                                    "live_id": int,
+                                    "title": str,
+                                    "area_name": str,
+                                },
+                                "anchor_info": {
+                                    "base_info": {
+                                        "uname": str,
+                                    },
+                                },
+                            },
+                        },
+                    },
+                    validate.union_get(
+                        ("roomInfoRes", "data", "room_info", "live_id"),
+                        ("roomInfoRes", "data", "anchor_info", "base_info", "uname"),
+                        ("roomInfoRes", "data", "room_info", "area_name"),
+                        ("roomInfoRes", "data", "room_info", "title"),
+                        ("roomInitRes", "data", "live_status"),
+                        ("roomInitRes", "data", "playurl_info", "playurl", "stream"),
+                    ),
+                ),
+            ),
+        )
+        if not data:
             return
 
-        params = {
-            "cid": room_id,
-            "quality": "4",
-            "platform": "web",
-        }
-        res = self.session.http.get(API_URL, params=params)
-        room = self.session.http.json(res, schema=_room_stream_list_schema)
-        if not room:
+        self.id, self.author, self.category, self.title, live_status, streams = data
+        if live_status != self.SHOW_STATUS_ONLINE:
             return
 
-        for stream_list in room["durl"]:
-            name = "source"
-            url = stream_list["url"]
-            # check if the URL is available
-            log.trace("URL={0}".format(url))
-            r = self.session.http.get(url,
-                                      retries=0,
-                                      timeout=3,
-                                      stream=True,
-                                      acceptable_status=(200, 403, 404, 405))
-            p = urlparse(url)
-            if r.status_code != 200:
-                log.error("Netloc: {0} with error {1}".format(p.netloc, r.status_code))
-                continue
-
-            log.debug("Netloc: {0}".format(p.netloc))
-            stream = HTTPStream(self.session, url)
-            yield name, stream
+        for stream in streams:
+            for stream_format in 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)
 
 
 __plugin__ = Bilibili


Test patch: