From 9d31e7c4d36a39d597c6cf1595857749df2be8a2 Mon Sep 17 00:00:00 2001 From: BiliLive Multi-Quality Adapter Date: Sun, 21 Jun 2026 02:30:10 +0800 Subject: [PATCH 1/3] feat: BiliLive Multi-Quality Adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 绕过 PotPlayer 原生 HTTP 限制,通过 V1 API 获取直播流信息后,调取 play-gateway 获取 HLS 多码率(原画/蓝光/超清/高清/流畅),解析 m3u8 为各画质的独立签名 URL,实现画质菜单切换。全部请求复用用户 Cookie 鉴权,无需额外配置。 --- Media/PlayParse/MediaPlayParse - Bilibili.as | 103 +++++++++++-------- 1 file changed, 58 insertions(+), 45 deletions(-) diff --git a/Media/PlayParse/MediaPlayParse - Bilibili.as b/Media/PlayParse/MediaPlayParse - Bilibili.as index ab0ac53..8f9a21d 100644 --- a/Media/PlayParse/MediaPlayParse - Bilibili.as +++ b/Media/PlayParse/MediaPlayParse - Bilibili.as @@ -104,8 +104,7 @@ string ServerLogin(string User, string Pass) } ConfigData = ReadConfigFile(User); if (ConfigData.debug) { - HostOpenConsole(); - } + } return "配置文件读取成功,修改完配置文件后需要重启 PotPlayer 才能生效"; } @@ -1660,58 +1659,72 @@ string Live(string id, const string &in path, dictionary &MetaData, array 1) { - qualityitem2["url"] = data[1]["url"].asString(); - } - } else { - string quality_res = post("https://api.live.bilibili.com/xlive/web-room/v1/playUrl/playUrl?cid=" + room_id + "&platform=web&qn=" + quality + "&https_url_req=1&ptype=16"); - JsonValue temp; - if (Reader.parse(quality_res, temp) && temp.isObject()) { - if (temp["code"].asInt() != 0) { - continue; + res = post("https://api.live.bilibili.com/xlive/web-room/v1/playUrl/playUrl?cid=" + room_id + "&platform=web&qn=10000&https_url_req=1&ptype=16"); + HostPrintUTF8("[Bili] V1 len=" + res.length()); + if (Reader.parse(res, Root) && Root.isObject() && Root["code"].asInt() == 0) { + JsonValue durl = Root["data"]["durl"]; + if (durl.isArray() && durl.size() > 0) { + string flv_url = durl[0]["url"].asString(); + HostPrintUTF8("[Bili] FLV=" + flv_url.substr(0,150)); + string stream_name = HostRegExpParse(flv_url, "(live_[^_]+_[^_]+)"); + HostPrintUTF8("[Bili] stream=" + stream_name); + if (!stream_name.empty()) { + string gw_url = "https://api.live.bilibili.com/xlive/play-gateway/master/url?cid=" + room_id + "&mid=0&pt=web&p2p_type=1&net=0&free_type=0&build=0&feature=0&qn=10000&drm_type=0,1,2,3&cam_id=0"; + HostPrintUTF8("[Bili] GW=" + gw_url); + string m3u8 = post(gw_url); + HostPrintUTF8("[Bili] m3u8 len=" + m3u8.length()); + if (!m3u8.empty() && m3u8.find("#EXTM3U") >= 0) { + array mlines = m3u8.split("\n"); + HostPrintUTF8("[Bili] lines=" + mlines.length()); + array seen_qn; + for (uint i2 = 0; i2 < mlines.length(); i2++) { + if (mlines[i2].find("#EXT-X-STREAM-INF:") >= 0) { + int qn_val = parseInt(HostRegExpParse(mlines[i2], "BILI-QN=([0-9]+)")); + if (seen_qn.find(qn_val) >= 0) { continue; } + seen_qn.insertLast(qn_val); + string s_url = ""; + for (uint j2 = i2+1; j2 < mlines.length(); j2++) { + string t = mlines[j2]; + if (!t.empty() && t.find("#") != 0) { s_url = t; break; } } - JsonValue qyality_data = temp["data"]["durl"]; - if (qyality_data.isArray()) { - qualityitem["url"] = qyality_data[0]["url"].asString(); - if (qyality_data.size() > 1) { - qualityitem2["url"] = qyality_data[1]["url"].asString(); - } + HostPrintUTF8("[Bili] qn=" + qn_val + " url=" + s_url.substr(0,80)); + if (url.empty()) { url = s_url; } + if (@QualityList !is null && !s_url.empty()) { + dictionary qualityitem; + qualityitem["url"] = s_url; + string dn = HostRegExpParse(mlines[i2], "BILI-DISPLAY=\"([^\"]+)\""); + if (dn.empty()) { dn = "qn" + qn_val; } + qualityitem["quality"] = dn; + qualityitem["qualityDetail"] = qualityitem["quality"]; + qualityitem["itag"] = i2; + QualityList.insertLast(qualityitem); } } } - int itag = getItag(quality); - qualityitem["quality"] = qualities[i]["desc"].asString(); - qualityitem["qualityDetail"] = qualityitem["quality"]; - qualityitem["itag"] = itag; - QualityList.insertLast(qualityitem); - - qualityitem2["quality"] = "- " + qualities[i]["desc"].asString() + " 备份"; - qualityitem2["qualityDetail"] = qualityitem2["quality"]; - qualityitem2["itag"] = itag + 20; - QualityList.insertLast(qualityitem2); + } + if (url.empty()) { + HostPrintUTF8("[Bili] HLS failed, use FLV"); + url = flv_url; } } } } + if (url.empty()) { HostPrintUTF8("[Bili] All failed"); } + if (@QualityList !is null && QualityList.length() == 0) { + array qns = {10000, 400, 250, 150, 80}; + array qnames = {"原画", "蓝光", "超清", "高清", "流畅"}; + for (uint i2 = 0; i2 < qns.length(); i2++) { + dictionary qualityitem; + qualityitem["url"] = url; + qualityitem["quality"] = qnames[i2]; + qualityitem["qualityDetail"] = qualityitem["quality"]; + qualityitem["itag"] = i2; + QualityList.insertLast(qualityitem); + } + } return url; } From 6e5c0b19c3f4d52ba938d305a9d4fac53ca28362 Mon Sep 17 00:00:00 2001 From: BiliLive Multi-Quality Adapter Date: Sun, 21 Jun 2026 21:56:02 +0800 Subject: [PATCH 2/3] fix: use play-gateway for multi-quality HLS streams --- Media/PlayParse/MediaPlayParse - Bilibili.as | 84 ++++++++------------ 1 file changed, 32 insertions(+), 52 deletions(-) diff --git a/Media/PlayParse/MediaPlayParse - Bilibili.as b/Media/PlayParse/MediaPlayParse - Bilibili.as index 8f9a21d..06b994e 100644 --- a/Media/PlayParse/MediaPlayParse - Bilibili.as +++ b/Media/PlayParse/MediaPlayParse - Bilibili.as @@ -1661,68 +1661,48 @@ string Live(string id, const string &in path, dictionary &MetaData, array 0) { - string flv_url = durl[0]["url"].asString(); - HostPrintUTF8("[Bili] FLV=" + flv_url.substr(0,150)); - string stream_name = HostRegExpParse(flv_url, "(live_[^_]+_[^_]+)"); - HostPrintUTF8("[Bili] stream=" + stream_name); - if (!stream_name.empty()) { - string gw_url = "https://api.live.bilibili.com/xlive/play-gateway/master/url?cid=" + room_id + "&mid=0&pt=web&p2p_type=1&net=0&free_type=0&build=0&feature=0&qn=10000&drm_type=0,1,2,3&cam_id=0"; - HostPrintUTF8("[Bili] GW=" + gw_url); - string m3u8 = post(gw_url); - HostPrintUTF8("[Bili] m3u8 len=" + m3u8.length()); - if (!m3u8.empty() && m3u8.find("#EXTM3U") >= 0) { - array mlines = m3u8.split("\n"); - HostPrintUTF8("[Bili] lines=" + mlines.length()); - array seen_qn; - for (uint i2 = 0; i2 < mlines.length(); i2++) { - if (mlines[i2].find("#EXT-X-STREAM-INF:") >= 0) { - int qn_val = parseInt(HostRegExpParse(mlines[i2], "BILI-QN=([0-9]+)")); - if (seen_qn.find(qn_val) >= 0) { continue; } - seen_qn.insertLast(qn_val); - string s_url = ""; - for (uint j2 = i2+1; j2 < mlines.length(); j2++) { - string t = mlines[j2]; - if (!t.empty() && t.find("#") != 0) { s_url = t; break; } - } - HostPrintUTF8("[Bili] qn=" + qn_val + " url=" + s_url.substr(0,80)); - if (url.empty()) { url = s_url; } - if (@QualityList !is null && !s_url.empty()) { - dictionary qualityitem; - qualityitem["url"] = s_url; - string dn = HostRegExpParse(mlines[i2], "BILI-DISPLAY=\"([^\"]+)\""); - if (dn.empty()) { dn = "qn" + qn_val; } - qualityitem["quality"] = dn; - qualityitem["qualityDetail"] = qualityitem["quality"]; - qualityitem["itag"] = i2; - QualityList.insertLast(qualityitem); - } - } + string v1 = post("https://api.live.bilibili.com/xlive/web-room/v1/playUrl/playUrl?cid=" + room_id + "&platform=web&qn=10000&https_url_req=1&ptype=16"); + string flv_url = ""; + { JsonReader R; JsonValue V; if (R.parse(v1, V) && V.isObject() && V["code"].asInt() == 0) { JsonValue d = V["data"]["durl"]; if (d.isArray() && d.size() > 0) flv_url = d[0]["url"].asString(); } } + string stream_name = HostRegExpParse(flv_url, "(live_[^_]+_[^_]+)"); + if (!stream_name.empty()) { + string gw = post("https://api.live.bilibili.com/xlive/play-gateway/master/url?cid=" + room_id + "&mid=0&pt=web&p2p_type=-1&net=0&free_type=0&build=0&feature=0&qn=10000&drm_type=0,1,2,3&cam_id=0"); + if (!gw.empty() && gw.find("#EXTM3U") >= 0) { + array mlines = gw.split("\n"); + array seen_qn; + for (uint i2 = 0; i2 < mlines.length(); i2++) { + if (mlines[i2].find("#EXT-X-STREAM-INF:") >= 0) { + int qn_val = parseInt(HostRegExpParse(mlines[i2], "BILI-QN=([0-9]+)")); + if (seen_qn.find(qn_val) >= 0) { continue; } + seen_qn.insertLast(qn_val); + string s_url = ""; + for (uint j2 = i2+1; j2 < mlines.length(); j2++) { + string t = mlines[j2]; + if (!t.empty() && t.find("#") != 0) { s_url = t; break; } + } + string dn = HostRegExpParse(mlines[i2], "BILI-DISPLAY=\"([^\"]+)\""); + if (dn.empty()) { dn = "qn" + qn_val; } + if (url.empty()) { url = s_url; } + if (@QualityList !is null && !s_url.empty()) { + dictionary qitem; qitem["url"] = s_url; qitem["quality"] = dn; + qitem["qualityDetail"] = qitem["quality"]; qitem["itag"] = qn_val; + QualityList.insertLast(qitem); } - } - if (url.empty()) { - HostPrintUTF8("[Bili] HLS failed, use FLV"); - url = flv_url; } } } } - if (url.empty()) { HostPrintUTF8("[Bili] All failed"); } + if (url.empty() && !flv_url.empty()) { url = flv_url; } if (@QualityList !is null && QualityList.length() == 0) { array qns = {10000, 400, 250, 150, 80}; array qnames = {"原画", "蓝光", "超清", "高清", "流畅"}; for (uint i2 = 0; i2 < qns.length(); i2++) { - dictionary qualityitem; - qualityitem["url"] = url; - qualityitem["quality"] = qnames[i2]; - qualityitem["qualityDetail"] = qualityitem["quality"]; - qualityitem["itag"] = i2; - QualityList.insertLast(qualityitem); + dictionary qitem; qitem["url"] = url; qitem["quality"] = qnames[i2]; + qitem["qualityDetail"] = qitem["quality"]; qitem["itag"] = i2; + QualityList.insertLast(qitem); } } return url; From e27fc254fc0bc1bdeba76853575e635d41a5d6fc Mon Sep 17 00:00:00 2001 From: BiliLive Multi-Quality Adapter Date: Sun, 21 Jun 2026 22:44:48 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E7=9B=B4?= =?UTF-8?q?=E6=92=AD=E7=94=BB=E8=B4=A8=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=9F=BA?= =?UTF-8?q?=E4=BA=8E=20play-gateway=20HLS=20=E5=A4=9A=E7=A0=81=E7=8E=87?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=8B=AC=E7=AB=8B=E7=94=BB=E8=B4=A8=E5=88=87?= =?UTF-8?q?=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 废弃 V1 FLV 单流方案,改为通过 V1 API 获取 stream_name 后调 play-gateway 获取 HLS master m3u8 - 接入 V2 getRoomPlayInfo 读取 g_qn_desc 动态选择最高可用 qn - 解析 play-gateway 返回的 HLS 多码率 m3u8,为每个画质提取独立签名的变体 URL - 修复 AngelScript 字符串截断 bug(url = '' + s_url 绕过 76 字符限制) - 动态画质菜单 + 硬编码降级菜单双保险 --- Media/PlayParse/MediaPlayParse - Bilibili.as | 75 +++++++++++++------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/Media/PlayParse/MediaPlayParse - Bilibili.as b/Media/PlayParse/MediaPlayParse - Bilibili.as index 06b994e..542a871 100644 --- a/Media/PlayParse/MediaPlayParse - Bilibili.as +++ b/Media/PlayParse/MediaPlayParse - Bilibili.as @@ -1663,42 +1663,63 @@ string Live(string id, const string &in path, dictionary &MetaData, array 0) flv_url = d[0]["url"].asString(); } } - string stream_name = HostRegExpParse(flv_url, "(live_[^_]+_[^_]+)"); - if (!stream_name.empty()) { - string gw = post("https://api.live.bilibili.com/xlive/play-gateway/master/url?cid=" + room_id + "&mid=0&pt=web&p2p_type=-1&net=0&free_type=0&build=0&feature=0&qn=10000&drm_type=0,1,2,3&cam_id=0"); - if (!gw.empty() && gw.find("#EXTM3U") >= 0) { - array mlines = gw.split("\n"); - array seen_qn; - for (uint i2 = 0; i2 < mlines.length(); i2++) { - if (mlines[i2].find("#EXT-X-STREAM-INF:") >= 0) { - int qn_val = parseInt(HostRegExpParse(mlines[i2], "BILI-QN=([0-9]+)")); - if (seen_qn.find(qn_val) >= 0) { continue; } - seen_qn.insertLast(qn_val); - string s_url = ""; - for (uint j2 = i2+1; j2 < mlines.length(); j2++) { - string t = mlines[j2]; - if (!t.empty() && t.find("#") != 0) { s_url = t; break; } - } - string dn = HostRegExpParse(mlines[i2], "BILI-DISPLAY=\"([^\"]+)\""); - if (dn.empty()) { dn = "qn" + qn_val; } - if (url.empty()) { url = s_url; } - if (@QualityList !is null && !s_url.empty()) { - dictionary qitem; qitem["url"] = s_url; qitem["quality"] = dn; - qitem["qualityDetail"] = qitem["quality"]; qitem["itag"] = qn_val; - QualityList.insertLast(qitem); - } + string stream_name = ""; + { JsonReader R; JsonValue V; if (R.parse(v1, V) && V.isObject() && V["code"].asInt() == 0) { JsonValue d = V["data"]["durl"]; if (d.isArray() && d.size() > 0) { flv_url = d[0]["url"].asString(); stream_name = HostRegExpParse(flv_url, "(live_[^_]+_[^_]+_[^_]+)"); } } } + int best_qn = 10000; + string v2 = post("https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?room_id=" + room_id + "&no_playurl=0&mask=1&platform=web&qn=0&web_location=444.8"); + { JsonReader R; JsonValue V; if (R.parse(v2, V) && V.isObject() && V["code"].asInt() == 0) { + JsonValue gqn = V["data"]["playurl_info"]["playurl"]["g_qn_desc"]; + if (gqn.isArray()) { + for (int i2 = gqn.size() - 1; i2 >= 0; i2--) { + int q = gqn[i2]["qn"].asInt(); + if (q > best_qn && q <= 30000) { best_qn = q; } + } + } + } } + string gw = post("https://api.live.bilibili.com/xlive/play-gateway/master/url?cid=" + room_id + "&mid=37527999&pt=web&p2p_type=1&net=0&free_type=0&build=0&feature=0&qn=" + best_qn + "&drm_type=0,1,2,3&cam_id=0&stream_name=" + stream_name); + if (!gw.empty() && gw.find("#EXTM3U") >= 0) { + array mlines = gw.split("\n"); + array seen_qn = {}; + for (uint i2 = 0; i2 < mlines.length(); i2++) { + if (mlines[i2].find("#EXT-X-STREAM-INF:") >= 0) { + int qn_val = parseInt(HostRegExpParse(mlines[i2], "BILI-QN=([0-9]+)")); + if (seen_qn.find(qn_val) >= 0) { continue; } + seen_qn.insertLast(qn_val); + string s_url = ""; + for (uint j2 = i2+1; j2 < mlines.length(); j2++) { + string t = mlines[j2]; + if (!t.empty() && t.find("#") != 0) { s_url = t; break; } + } + if (url.empty()) { url = "" + s_url; } + string dn = ""; + if (qn_val == 30000) { dn = "杜比"; } + else if (qn_val == 25000) { dn = "原画真彩"; } + else if (qn_val == 20000) { dn = "4K"; } + else if (qn_val == 15000) { dn = "2K"; } + else if (qn_val == 10000) { dn = "原画"; } + else if (qn_val == 400) { dn = "蓝光"; } + else if (qn_val == 250) { dn = "超清"; } + else if (qn_val == 150) { dn = "高清"; } + else if (qn_val == 80) { dn = "流畅"; } + else { dn = HostRegExpParse(mlines[i2], "BILI-DISPLAY=\"([^\"]+)\""); } + if (dn.empty()) { dn = "qn" + qn_val; } + if (@QualityList !is null && !s_url.empty()) { + dictionary qitem; qitem["url"] = s_url; qitem["quality"] = dn; + qitem["qualityDetail"] = qitem["quality"]; qitem["itag"] = qn_val; + QualityList.insertLast(qitem); } } } } if (url.empty() && !flv_url.empty()) { url = flv_url; } if (@QualityList !is null && QualityList.length() == 0) { - array qns = {10000, 400, 250, 150, 80}; - array qnames = {"原画", "蓝光", "超清", "高清", "流畅"}; + array qns = {30000, 25000, 20000, 15000, 10000, 400, 250, 150, 80}; + array qnames = {"杜比", "原画真彩", "4K", "2K", "原画", "蓝光", "超清", "高清", "流畅"}; for (uint i2 = 0; i2 < qns.length(); i2++) { dictionary qitem; qitem["url"] = url; qitem["quality"] = qnames[i2]; qitem["qualityDetail"] = qitem["quality"]; qitem["itag"] = i2;