// ==UserScript==
// @name B站推流码获取工具
// @namespace https://github.com/smathsp
// @version 1.14
// @description 获取第三方推流码
// @author smathsp
// @license GPL-3.0
// @match *://*.bilibili.com/*
// @icon https://www.bilibili.com/favicon.ico
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @grant GM_notification
// @connect api.live.bilibili.com
// @connect passport.bilibili.com
// @require https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js
// @run-at document-end
// ==/UserScript==
(function () {
"use strict";
// 存储键名常量
const STORAGE_KEYS = {
LAST_ROOM_ID: "bili_last_roomid",
DARK_MODE: "bili_dark_mode",
IS_LIVE_STARTED: "isLiveStarted",
STREAM_INFO: "streamInfo",
LAST_GROUP_ID: "bili_last_groupid",
LAST_AREA_ID: "bili_last_areaid",
AREA_LIST_TIME: "bili_area_list_time",
AREA_LIST: "bili_area_list",
USER_MID: "bili_user_mid",
LAST_TITLE: "bili_last_title",
};
// API URL Constants
const API_URL_AREA_LIST =
"https://api.live.bilibili.com/room/v1/Area/getList?show_pinyin=1";
const API_URL_START_LIVE =
"https://api.live.bilibili.com/room/v1/Room/startLive";
const API_URL_UPDATE_ROOM =
"https://api.live.bilibili.com/room/v1/Room/update";
const API_URL_STOP_LIVE =
"https://api.live.bilibili.com/room/v1/Room/stopLive";
const API_URL_FACE_AUTH =
"https://api.live.bilibili.com/xlive/app-blink/v1/preLive/IsUserIdentifiedByFaceAuth";
// 将 GM_xmlhttpRequest Promise 化
function gmRequest(options) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
...options,
onload: resolve,
onerror: reject,
ontimeout: reject,
onabort: reject,
});
});
}
// SVG 图标常量
const SUN_SVG =
'<svg viewBox="0 0 24 24" width="20" height="20"><circle cx="12" cy="12" r="5" fill="#FFD600"/><g stroke="#FFD600" stroke-width="2"><line x1="12" y1="1" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="6.34" y2="6.34"/><line x1="17.66" y1="17.66" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="6.34" y2="17.66"/><line x1="17.66" y1="6.34" x2="19.78" y2="4.22"/></g></svg>';
const MOON_SVG =
'<svg viewBox="0 0 24 24" width="20" height="20"><path d="M21 12.79A9 9 0 0 1 12.21 3c-.55 0-.66.71-.19.93A7 7 0 1 0 20.07 12c.22.47-.38.74-.87.79z" fill="#888"/></svg>';
const CLOSE_SVG =
'<svg viewBox="0 0 1024 1024" width="16" height="16"><path d="M512 421.49 331.09 240.58c-24.74-24.74-64.54-24.71-89.28 0.03-24.74 24.74-24.72 64.54 0.03 89.28L422.75 510.8 241.84 691.71c-24.74 24.74-24.72 64.54 0.03 89.33 24.74 24.74 64.54 24.71 89.28-0.03L512 600.1l180.91 180.91c24.74 24.74 64.54 24.71 89.28-0.03 24.74-24.74 24.72-64.54-0.03-89.28L601.25 510.8 782.16 329.89c24.74-24.74 24.72-64.54-0.03-89.33-24.74-24.74-64.54-24.71-89.28 0.03L512 421.49z" fill="#888888"></path></svg>';
// 插入全局样式表,统一亮暗色模式
function insertGlobalStyle() {
if (document.getElementById("bili-stream-global-style")) return;
const style = document.createElement("style");
style.id = "bili-stream-global-style";
style.innerHTML = `
:root {
--bili-bg: rgba(255, 255, 255, 0.8);
--bili-fg: #222;
--bili-panel-shadow: 0 8px 32px rgba(0,0,0,0.15);
--bili-border: rgba(238, 238, 238, 0.6);
--bili-input-bg: rgba(255, 255, 255, 0.9);
--bili-input-fg: #222;
--bili-input-border: rgba(221, 221, 221, 0.8);
--bili-tip-bg: rgba(254, 240, 241, 0.9);
--bili-tip-fg: #d92b46;
--bili-tip-border: #fb7299;
--bili-btn-main: #fb7299;
--bili-btn-main-hover: #fc8bab;
--bili-btn-main-disabled: #bfbfbf;
--bili-btn-stop: #ff4b4b;
--bili-btn-stop-hover: #d9363e;
--bili-btn-stop-disabled: #999;
--bili-btn-text: #fff;
--bili-title-color: #fb7299;
--bili-label-color: #666;
--bili-tip-yellow-bg: rgba(255, 251, 230, 0.9);
--bili-tip-yellow-border: #faad14;
--bili-tip-yellow-fg: #faad14;
--bili-tip-green-bg: rgba(230, 255, 237, 0.9);
--bili-tip-green-border: #52c41a;
--bili-tip-green-fg: #389e0d;
}
.bili-dark-mode {
--bili-bg: rgba(35, 35, 36, 0.8);
--bili-fg: #eee;
--bili-panel-shadow: 0 8px 32px rgba(0,0,0,0.4);
--bili-border: rgba(68, 68, 68, 0.6);
--bili-input-bg: rgba(24, 24, 26, 0.9);
--bili-input-fg: #eee;
--bili-input-border: rgba(68, 68, 68, 0.8);
--bili-tip-bg: rgba(45, 35, 38, 0.9);
--bili-tip-fg: #ffb6c1;
--bili-tip-border: #fb7299;
--bili-btn-main: #fb7299;
--bili-btn-main-hover: #fc8bab;
--bili-btn-main-disabled: #bfbfbf;
--bili-btn-stop: #ff4b4b;
--bili-btn-stop-hover: #d9363e;
--bili-btn-stop-disabled: #999;
--bili-btn-text: #fff;
--bili-title-color: #fb7299;
--bili-label-color: #aaa;
--bili-tip-yellow-bg: rgba(58, 45, 26, 0.9);
--bili-tip-yellow-border: #faad14;
--bili-tip-yellow-fg: #ffd666;
--bili-tip-green-bg: rgba(30, 43, 34, 0.9);
--bili-tip-green-border: #52c41a;
--bili-tip-green-fg: #b7eb8f;
}
#bili-stream-code-panel {
background-color: var(--bili-bg) !important;
color: var(--bili-fg) !important;
box-shadow: var(--bili-panel-shadow) !important;
border-radius: 12px;
padding: 12px;
font-family: "Microsoft YaHei", sans-serif;
max-width: 280px;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--bili-border);
}
#bili-result {
background-color: var(--bili-bg) !important;
color: var(--bili-fg) !important;
border: 1px solid var(--bili-border) !important;
border-radius: 8px;
margin-top: 15px;
padding: 10px;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.bili-input {
background: var(--bili-input-bg) !important;
color: var(--bili-input-fg) !important;
border: 1px solid var(--bili-input-border) !important;
border-radius: 4px;
padding: 6px 8px;
font-size: 13px;
width: 100%;
box-sizing: border-box;
}
.bili-select {
background: var(--bili-input-bg) !important;
color: var(--bili-input-fg) !important;
border: 1px solid var(--bili-input-border) !important;
border-radius: 4px;
padding: 6px 8px;
font-size: 13px;
width: 100%;
box-sizing: border-box;
}
#bili-room-id, #bili-title, #server-addr, #stream-code {
background: var(--bili-input-bg) !important;
color: var(--bili-input-fg) !important;
border: 1px solid var(--bili-input-border) !important;
border-radius: 4px;
padding: 8px;
font-size: 14px;
}
#bili-area-group, #bili-area {
background: var(--bili-input-bg) !important;
color: var(--bili-input-fg) !important;
border: 1px solid var(--bili-input-border) !important;
border-radius: 4px;
padding: 8px;
font-size: 14px;
}
.bili-important-tip {
background-color: var(--bili-tip-bg) !important;
color: var(--bili-tip-fg) !important;
border-left: 4px solid var(--bili-tip-border) !important;
border-radius: 6px;
margin-top: 8px;
padding: 8px;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.bili-tip-yellow {
background: var(--bili-tip-yellow-bg);
border-left: 4px solid var(--bili-tip-yellow-border);
color: var(--bili-tip-yellow-fg);
border-radius: 6px;
margin-top: 8px;
padding: 8px;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.bili-tip-green {
background: var(--bili-tip-green-bg);
border-left: 4px solid var(--bili-tip-green-border);
color: var(--bili-tip-green-fg);
border-radius: 6px;
margin-top: 8px;
padding: 8px;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
#bili-qr-container {
background-color: var(--bili-bg) !important;
color: var(--bili-fg) !important;
border: 1px solid var(--bili-border) !important;
}
#bili-qr-container h3 {
color: var(--bili-title-color) !important;
}
#bili-qr-container p {
color: var(--bili-label-color) !important;
}
.bili-btn-main {
background: var(--bili-btn-main);
color: var(--bili-btn-text);
border: none;
border-radius: 4px;
padding: 10px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s, opacity 0.3s;
}
.bili-btn-main:hover:not(:disabled) {
background: var(--bili-btn-main-hover);
}
.bili-btn-main:disabled {
background: var(--bili-btn-main-disabled);
opacity: 0.5;
cursor: not-allowed;
}
.bili-btn-stop {
background: var(--bili-btn-stop);
color: var(--bili-btn-text);
border: none;
border-radius: 4px;
padding: 10px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s, opacity 0.3s;
}
.bili-btn-stop:hover:not(:disabled) {
background: var(--bili-btn-stop-hover);
}
.bili-btn-stop:disabled {
background: var(--bili-btn-stop-disabled);
opacity: 0.5;
cursor: not-allowed;
}
.bili-title {
color: var(--bili-title-color) !important;
font-size: 18px;
margin: 0;
}
.bili-title:hover {
opacity: 0.8;
text-decoration: underline !important;
}
.bili-label {
color: var(--bili-label-color);
font-size: 14px;
}
.bili-copy-btn {
margin-left: 5px;
background: var(--bili-btn-main);
color: var(--bili-btn-text);
border: none;
border-radius: 4px;
padding: 6px 8px;
cursor: pointer;
transition: background 0.3s;
font-size: 12px;
flex-shrink: 0;
}
.bili-copy-btn:disabled {
background: var(--bili-btn-main-disabled);
cursor: not-allowed;
}
.bili-copy-btn:hover:not(:disabled) {
background: var(--bili-btn-main-hover);
}
.bili-message {
color: var(--bili-fg);
font-size: 15px;
margin: 0;
}
.bili-message-error {
color: red;
}
.bili-status-success {
background: #d4edda !important;
color: #155724 !important;
border: 1px solid #c3e6cb !important;
}
.bili-status-error {
background: #f8d7da !important;
color: #721c24 !important;
border: 1px solid #f5c6cb !important;
}
`;
document.head.appendChild(style);
}
// 全局变量
let roomId = null; // 当前房间ID
let csrf = null; // CSRF令牌
let startLiveButton = null; // "开始直播"按钮引用
let stopLiveButton = null; // "结束直播"按钮引用
let editTitleButton = null; // "修改标题"按钮引用
let editAreaButton = null; // "修改分区"按钮引用
let isLiveStarted = GM_getValue(STORAGE_KEYS.IS_LIVE_STARTED, false); // 直播状态
let streamInfo = GM_getValue(STORAGE_KEYS.STREAM_INFO, null); // 推流信息缓存
// 请求头
const headers = {
accept: "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
origin: "https://link.bilibili.com",
referer: "https://link.bilibili.com/p/center/index",
"user-agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
};
// 开始直播数据模板
const startData = {
room_id: "",
platform: "pc_link", //感谢bilibili Z-Lake提供
area_v2: "",
backup_stream: "0",
csrf_token: "",
csrf: "",
};
// 停止直播数据模板
const stopData = {
room_id: "",
platform: "pc_link", //感谢bilibili Z-Lake提供
csrf_token: "",
csrf: "",
};
// 修改直播标题数据模板
const titleData = {
room_id: "",
platform: "pc_link", //感谢bilibili Z-Lake提供
title: "",
csrf_token: "",
csrf: "",
};
// 修改直播分区数据模板
const areaData = {
room_id: "",
platform: "pc_link", //感谢bilibili Z-Lake提供
area_id: "",
csrf_token: "",
csrf: "",
};
// 人脸验证数据模板
const faceAuthData = {
room_id: "",
face_auth_code: "60024",
csrf_token: "",
csrf: "",
visit_id: "",
};
// 初始化入口
function init() {
try {
insertGlobalStyle(); // 插入全局样式
removeExistingComponents(); // 清理旧组件
createUI(); // 创建UI(只创建一次主面板)
restoreLiveState(); // 恢复直播状态
} catch (error) {
console.error("B站推流码获取工具初始化失败:", error);
}
}
// 移除已存在的组件
function removeExistingComponents() {
const existingPanel = document.getElementById("bili-stream-code-panel");
if (existingPanel) {
// 清理事件监听器
if (existingPanel._clickOutsideHandler) {
document.removeEventListener(
"click",
existingPanel._clickOutsideHandler,
true
);
}
existingPanel.remove();
}
const existingButton = document.getElementById("bili-stream-float-button");
if (existingButton) existingButton.remove();
// 清空按钮引用,防止旧引用干扰
startLiveButton = null;
stopLiveButton = null;
editTitleButton = null;
editAreaButton = null;
}
// 创建UI(只创建一次主面板)
function createUI() {
// 若主面板已存在则不再重复创建
if (!document.getElementById("bili-stream-code-panel")) {
const panel = createPanel();
panel.style.display = "none";
}
// 浮动按钮可重复创建(防止丢失)
createFloatButton();
}
// 创建面板
function createPanel() {
const panel = document.createElement("div");
panel.id = "bili-stream-code-panel";
panel.style.cssText = `
position: fixed;
top: 70px;
right: 10px;
width: 240px;
z-index: 10000;
display: none;
`;
// 头部区域
const header = createPanelHeader();
panel.appendChild(header);
// 表单区域
const form = createPanelForm();
panel.appendChild(form);
// 结果区域
const resultArea = document.createElement("div");
resultArea.id = "bili-result";
resultArea.style.cssText = `
margin-top: 15px;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
background-color: #f9f9f9;
display: none;
`;
panel.appendChild(resultArea);
document.body.appendChild(panel);
// 新增:设置点击外部隐藏面板
setupClickOutsideHandler(panel);
return panel;
}
// 新增:设置点击外部隐藏面板的处理器
function setupClickOutsideHandler(panel) {
function handleClickOutside(event) {
// 只在面板可见时处理
if (panel.style.display === "none") return;
// 检查点击目标
const floatButton = document.getElementById("bili-stream-float-button");
const isClickInPanel = panel.contains(event.target);
const isClickOnFloatButton =
floatButton && floatButton.contains(event.target);
// 点击面板外且不是浮动按钮时隐藏面板
if (!isClickInPanel && !isClickOnFloatButton) {
panel.style.display = "none";
}
}
// 使用捕获阶段监听,确保在其他事件处理器之前执行
document.addEventListener("click", handleClickOutside, true);
// 存储处理器引用,便于清理
panel._clickOutsideHandler = handleClickOutside;
}
// 创建面板头部
function createPanelHeader() {
const header = document.createElement("div");
header.style.cssText =
"display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;";
// 标题 - 改为可点击的链接
const title = document.createElement("a");
title.textContent = "推流码获取";
title.className = "bili-title";
title.href = "https://github.com/smathsp/UserScript";
title.target = "_blank";
title.style.cssText = "text-decoration: none; cursor: pointer;";
title.title = "访问 GitHub 仓库";
// 亮暗模式切换按钮
const modeBtn = document.createElement("button");
modeBtn.id = "bili-mode-toggle";
modeBtn.style.cssText =
"width: 28px; height: 28px; border: none; background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center; margin-left: 8px;";
// SVG 图标
let isDarkMode = GM_getValue(STORAGE_KEYS.DARK_MODE, false);
modeBtn.innerHTML = isDarkMode ? MOON_SVG : SUN_SVG;
modeBtn.title = isDarkMode ? "切换为亮色模式" : "切换为暗色模式";
modeBtn.onclick = function () {
isDarkMode = !isDarkMode;
GM_setValue(STORAGE_KEYS.DARK_MODE, isDarkMode);
modeBtn.innerHTML = isDarkMode ? MOON_SVG : SUN_SVG;
modeBtn.title = isDarkMode ? "切换为亮色模式" : "切换为暗色模式";
applyColorMode(isDarkMode);
};
// 首次渲染时应用模式
setTimeout(() => applyColorMode(isDarkMode), 0);
// 关闭按钮
const closeButton = document.createElement("button");
closeButton.innerHTML = CLOSE_SVG;
closeButton.style.cssText =
"width: 24px; height: 24px; border: none; background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center;";
closeButton.onclick = () => {
document.getElementById("bili-stream-code-panel").style.display = "none";
};
// 头部右侧按钮组
const rightBtns = document.createElement("div");
rightBtns.style.cssText = "display: flex; align-items: center; gap: 4px;";
rightBtns.appendChild(modeBtn);
rightBtns.appendChild(closeButton);
header.appendChild(title);
header.appendChild(rightBtns);
return header;
}
// 亮暗模式应用函数
function applyColorMode(isDark) {
// 只切换 class,不再手动设置 style
const root = document.documentElement;
if (isDark) {
root.classList.add("bili-dark-mode");
} else {
root.classList.remove("bili-dark-mode");
}
}
// 创建面板表单
function createPanelForm() {
const form = document.createElement("div");
form.style.cssText = "display: flex; flex-direction: column; gap: 10px;";
// 房间ID输入
form.appendChild(createRoomIdInput());
// 分区选择
const areaSelectionElement = createAreaSelection();
form.appendChild(areaSelectionElement);
if (areaSelectionElement.loadAndBindAreaListPromise) {
areaSelectionElement.loadAndBindAreaListPromise
.then(() => {
autoFillRoomId();
})
.catch((error) => {
console.error(
"Error during area list loading, or in autoFillRoomId:",
error
);
setTimeout(autoFillRoomId, 300);
});
} else {
console.warn(
"loadAndBindAreaListPromise not found, falling back for autoFillRoomId."
);
setTimeout(autoFillRoomId, 300);
}
// 标题输入
form.appendChild(createTitleInput());
// 按钮组
form.appendChild(createButtonGroup());
return form;
}
// 创建房间ID输入
function createRoomIdInput() {
const container = document.createElement("div");
container.style.cssText =
"display: flex; flex-direction: column; gap: 5px;";
const label = document.createElement("label");
label.textContent = "房间ID:";
label.className = "bili-label";
const input = document.createElement("input");
input.type = "text";
input.id = "bili-room-id";
input.placeholder = "请输入你的房间ID";
input.className = "bili-input";
// 新增:输入时保存
input.addEventListener("blur", function () {
GM_setValue(STORAGE_KEYS.LAST_ROOM_ID, input.value.trim());
});
container.appendChild(label);
container.appendChild(input);
return container;
}
// 创建分区选择
function createAreaSelection() {
const container = document.createElement("div");
container.id = "bili-area-selection-container";
container.style.cssText =
"display: flex; flex-direction: column; gap: 5px;";
const label = document.createElement("label");
label.textContent = "直播分区:";
label.className = "bili-label";
// 加载指示器
const loading = document.createElement("div");
loading.id = "bili-area-loading";
loading.textContent = "正在加载分区列表...";
loading.style.cssText =
"padding: 8px; color: #666; font-size: 14px; text-align: center; cursor: pointer;";
// 分区组选择器
const groupSelect = document.createElement("select");
groupSelect.id = "bili-area-group";
groupSelect.className = "bili-select";
groupSelect.style.cssText = "margin-bottom: 8px; display: none;";
// 子分区选择器
const areaSelect = document.createElement("select");
areaSelect.id = "bili-area";
areaSelect.className = "bili-select";
areaSelect.style.cssText = "display: none;";
// 统一事件绑定
groupSelect.addEventListener("change", function () {
const areaList = getCachedAreaList() || [];
const selectedIndex = this.options[this.selectedIndex].dataset.index;
GM_setValue(STORAGE_KEYS.LAST_GROUP_ID, groupSelect.value);
updateAreaSelectors(
areaList,
Number(selectedIndex),
groupSelect,
areaSelect
);
});
areaSelect.addEventListener("change", function () {
GM_setValue(STORAGE_KEYS.LAST_AREA_ID, areaSelect.value);
GM_setValue(STORAGE_KEYS.LAST_GROUP_ID, groupSelect.value);
});
loading.onclick = function () {
if (
loading.style.color === "rgb(255, 75, 75)" ||
loading.style.color === "#ff4b4b"
) {
loadAndBindAreaList();
}
};
container.appendChild(label);
container.appendChild(loading);
container.appendChild(groupSelect);
container.appendChild(areaSelect);
// 合并后的分区刷新函数
function updateAreaSelectors(
areaList,
groupIdx = 0,
groupSel = groupSelect,
areaSel = areaSelect
) {
groupSel.innerHTML = "";
areaSel.innerHTML = "";
areaList.forEach((group, idx) => {
const option = document.createElement("option");
option.value = group.id;
option.textContent = group.name;
option.dataset.index = idx;
groupSel.appendChild(option);
});
// 恢复上次大类
const lastGroupId = GM_getValue(STORAGE_KEYS.LAST_GROUP_ID);
if (lastGroupId) {
for (let i = 0; i < groupSel.options.length; i++) {
if (groupSel.options[i].value == lastGroupId) {
groupSel.selectedIndex = i;
groupIdx = i;
break;
}
}
}
if (areaList[groupIdx] && areaList[groupIdx].list) {
areaList[groupIdx].list.forEach((area) => {
const option = document.createElement("option");
option.value = area.id;
option.textContent = area.name;
areaSel.appendChild(option);
});
}
// 恢复上次分区id
const lastAreaId = GM_getValue(STORAGE_KEYS.LAST_AREA_ID);
if (lastAreaId && areaSel.options.length > 0) {
for (let i = 0; i < areaSel.options.length; i++) {
if (areaSel.options[i].value == lastAreaId) {
areaSel.selectedIndex = i;
break;
}
}
}
// 显示选择器
loading.style.display = "none";
groupSel.style.display = "block";
areaSel.style.display = "block";
}
// 加载分区数据
function loadAndBindAreaList() {
return new Promise(async (resolve, reject) => {
// 返回 Promise
loading.style.display = "block";
groupSelect.style.display = "none";
areaSelect.style.display = "none";
loading.textContent = "正在加载分区列表...";
loading.style.color = "#666";
const cachedList = getCachedAreaList();
if (cachedList) {
updateAreaSelectors(cachedList, 0, groupSelect, areaSelect);
resolve(); // 解析 Promise
return;
}
try {
const response = await gmRequest({
method: "GET",
url: API_URL_AREA_LIST,
headers: headers,
});
const result = JSON.parse(response.responseText);
if (result.code === 0) {
cacheAreaList(result.data);
updateAreaSelectors(result.data, 0, groupSelect, areaSelect);
resolve(); // 解析 Promise
} else {
console.error("Area list API error:", result);
showAreaLoadError();
reject(new Error("Failed to load area list")); // 拒绝 Promise
}
} catch (errorResponse) {
console.error("Area list request error:", errorResponse);
showAreaLoadError();
reject(errorResponse); // 拒绝 Promise
}
});
}
// 将 Promise 附加到容器元素,以便在 createUI 中访问
container.loadAndBindAreaListPromise = loadAndBindAreaList();
return container;
}
// 创建标题输入
function createTitleInput() {
const container = document.createElement("div");
container.style.cssText =
"display: flex; flex-direction: column; gap: 5px;";
const label = document.createElement("label");
label.textContent = "直播标题:";
label.className = "bili-label";
const input = document.createElement("input");
input.type = "text";
input.id = "bili-title";
input.placeholder = "请输入直播标题";
input.className = "bili-input";
// 新增:输入时保存
input.addEventListener("blur", function () {
GM_setValue(STORAGE_KEYS.LAST_TITLE, input.value.trim());
});
container.appendChild(label);
container.appendChild(input);
return container;
}
// 创建按钮组
function createButtonGroup() {
const container = document.createElement("div");
container.style.cssText = "display: flex; flex-direction: column; gap: 8px; margin-top: 10px;";
// 修改按钮组(直播中显示)
const editButtonsContainer = document.createElement("div");
editButtonsContainer.id = "bili-edit-buttons";
editButtonsContainer.style.cssText = "display: none; gap: 8px;";
// 修改标题按钮
editTitleButton = document.createElement("button");
editTitleButton.textContent = "修改标题";
editTitleButton.className = "bili-btn-main";
editTitleButton.style.cssText = "flex: 1; font-size: 13px; padding: 8px;";
editTitleButton.onclick = editLiveTitle;
// 修改分区按钮
editAreaButton = document.createElement("button");
editAreaButton.textContent = "修改分区";
editAreaButton.className = "bili-btn-main";
editAreaButton.style.cssText = "flex: 1; font-size: 13px; padding: 8px;";
editAreaButton.onclick = editLiveArea;
editButtonsContainer.appendChild(editTitleButton);
editButtonsContainer.appendChild(editAreaButton);
// 主按钮组
const mainButtonsContainer = document.createElement("div");
mainButtonsContainer.style.cssText = "display: flex; gap: 10px;";
// 开始直播按钮
startLiveButton = document.createElement("button");
startLiveButton.textContent = "获取推流码并开始直播";
startLiveButton.className = "bili-btn-main";
startLiveButton.style.flex = "1";
startLiveButton.onclick = startLive;
// 结束直播按钮
stopLiveButton = document.createElement("button");
stopLiveButton.textContent = "结束直播";
stopLiveButton.className = "bili-btn-stop";
stopLiveButton.style.flex = "1";
stopLiveButton.disabled = true;
stopLiveButton.onclick = stopLive;
mainButtonsContainer.appendChild(startLiveButton);
mainButtonsContainer.appendChild(stopLiveButton);
container.appendChild(editButtonsContainer);
container.appendChild(mainButtonsContainer);
return container;
}
// 创建浮动按钮
function createFloatButton() {
const button = document.createElement("div");
button.id = "bili-stream-float-button";
button.innerHTML =
'<svg viewBox="0 0 1024 1024" width="24" height="24"><path d="M718.3 183.7H305.7c-122 0-221 99-221 221v214.6c0 122 99 221 221 221h412.6c122 0 221-99 221-221V404.7c0-122-99-221-221-221z m159.1 435.6c0 87.6-71.5 159.1-159.1 159.1H305.7c-87.6 0-159.1-71.5-159.1-159.1V404.7c0-87.6 71.5-159.1 159.1-159.1h412.6c87.6 0 159.1 71.5 159.1 159.1v214.6z" fill="#FFFFFF"></path><path d="M415.5 532.2v-131c0-7.1 3.8-13.6 10-17.1 6.2-3.5 13.7-3.5 19.9 0l131 75.1c6.2 3.5 10 10.1 10 17.1 0 7.1-3.8 13.6-10 17.1l-131 65.5c-6.2 3.5-13.7 3.5-19.9 0-6.2-3.5-10-10.1-10-17.1v-9.6z" fill="#FFFFFF"></path></svg>';
button.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
width: 48px;
height: 48px;
border-radius: 50%;
background-color: #fb7299;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 10001;
transition: transform 0.3s;
`;
button.onmouseover = function () {
this.style.transform = "scale(1.1)";
};
button.onmouseout = function () {
this.style.transform = "scale(1)";
};
button.onclick = togglePanel;
document.body.appendChild(button);
return button;
}
// 检查实际直播状态
async function checkActualLiveStatus() {
if (!roomId || !csrf) return;
try {
const response = await gmRequest({
method: "POST",
url: "https://api.live.bilibili.com/room/v1/Room/get_info",
headers: headers,
data: new URLSearchParams({ room_id: roomId }).toString(),
});
const result = JSON.parse(response.responseText);
if (result.code === 0 && result.data) {
const actualLiveStatus = result.data.live_status; // 0=未开播,1=直播中,2=轮播中
// 检查本地状态与实际状态是否一致
if (isLiveStarted && actualLiveStatus !== 1) {
// 本地认为在直播,但实际未直播 - 状态不一致
console.log("检测到直播状态不一致:本地显示直播中,但服务器显示未直播");
// 更新本地状态
isLiveStarted = false;
streamInfo = null;
GM_setValue(STORAGE_KEYS.IS_LIVE_STARTED, false);
GM_setValue(STORAGE_KEYS.STREAM_INFO, null);
// 更新UI
updateButtonsForLive(false);
// 清除结果区域或显示提示
const resultArea = document.getElementById("bili-result");
if (resultArea) {
resultArea.innerHTML = `<div class="bili-tip-yellow"><span style="font-weight:bold;">提示:</span>检测到该房间直播已被结束,已自动更新状态</div>`;
resultArea.style.display = "block";
}
// 显示通知
GM_notification({
text: "检测到直播已结束,状态已同步",
title: "B站推流码获取工具",
timeout: 5000,
});
} else if (!isLiveStarted && actualLiveStatus === 1) {
// 本地认为未直播,但实际在直播 - 可能是其他地方开启的直播
console.log("检测到可能存在其他地方开启的直播");
const resultArea = document.getElementById("bili-result");
if (resultArea) {
resultArea.innerHTML = `<div class="bili-tip-yellow"><span style="font-weight:bold;">提示:</span>检测到该房间正在直播中,可能是通过其他方式开启的</div>`;
resultArea.style.display = "block";
}
}
}
} catch (error) {
console.error("检查直播状态失败:", error);
// 静默失败,不影响正常使用
}
}
// 显示/隐藏面板
async function togglePanel() {
const panel = document.getElementById("bili-stream-code-panel");
if (!panel) return; // 理论上不会发生
const isShowing = panel.style.display === "none" || !panel.style.display;
panel.style.display = isShowing ? "block" : "none";
// 如果是显示面板,检查直播状态
if (isShowing) {
// 延迟一点检查,确保面板已显示
setTimeout(() => {
checkActualLiveStatus();
}, 100);
}
}
// 检查浮动按钮
function checkFloatButton() {
if (!document.getElementById("bili-stream-float-button")) {
createFloatButton();
}
}
// 显示分区加载错误信息
function showAreaLoadError() {
const loading = document.getElementById("bili-area-loading");
if (loading) {
loading.textContent = "无法加载分区列表,请稍后刷新重试";
loading.style.color = "#ff4b4b";
}
// 显示通知
GM_notification({
text: "无法加载直播分区列表,请检查网络连接或登录状态",
title: "B站推流码获取工具",
timeout: 5000,
});
}
// 更新分区选择器
function updateAreaSelectors(areaList) {
const loading = document.getElementById("bili-area-loading");
const groupSelect = document.getElementById("bili-area-group");
const areaSelect = document.getElementById("bili-area");
// 防止 loading 取不到时报错
if (!loading || !groupSelect || !areaSelect) return;
// 隐藏加载提示
loading.style.display = "none";
// 显示选择器
groupSelect.style.display = "block";
areaSelect.style.display = "block";
// 清空选择器
groupSelect.innerHTML = "";
areaSelect.innerHTML = "";
// 添加分区大类
areaList.forEach((group, index) => {
const option = document.createElement("option");
option.value = group.id;
option.textContent = group.name;
option.dataset.index = index;
groupSelect.appendChild(option);
});
// 默认显示第一个分区大类的子分区
if (areaList.length > 0 && areaList[0].list) {
areaList[0].list.forEach((area) => {
const option = document.createElement("option");
option.value = area.id;
option.textContent = area.name;
areaSelect.appendChild(option);
});
}
// 分区大类变更事件
groupSelect.addEventListener("change", function () {
const selectedIndex = this.options[this.selectedIndex].dataset.index;
const selectedGroup = areaList[selectedIndex];
// 清空子分区
areaSelect.innerHTML = "";
if (selectedGroup && selectedGroup.list) {
selectedGroup.list.forEach((area) => {
const option = document.createElement("option");
option.value = area.id;
option.textContent = area.name;
areaSelect.appendChild(option);
});
}
});
}
// 获取缓存的分区列表
function getCachedAreaList() {
const timeStamp = GM_getValue(STORAGE_KEYS.AREA_LIST_TIME);
if (!timeStamp) return null;
const now = new Date().getTime();
const oneDay = 24 * 60 * 60 * 1000;
// 超过一天则认为过期
if (now - timeStamp > oneDay) return null;
const listStr = GM_getValue(STORAGE_KEYS.AREA_LIST);
if (!listStr) return null;
try {
return JSON.parse(listStr);
} catch (e) {
return null;
}
}
// 缓存分区列表
function cacheAreaList(areaList) {
GM_setValue(STORAGE_KEYS.AREA_LIST, JSON.stringify(areaList));
GM_setValue(STORAGE_KEYS.AREA_LIST_TIME, new Date().getTime());
}
// 拆分 autoFillRoomId 内部逻辑
function getRoomIdFromHistory() {
return GM_getValue(STORAGE_KEYS.LAST_ROOM_ID);
}
function getCsrfToken() {
const csrfCookie = document.cookie
.split("; ")
.find((row) => row.startsWith("bili_jct="));
return csrfCookie ? csrfCookie.split("=")[1] : null;
}
function autoFillRoomId() {
const lastRoomId = GM_getValue(STORAGE_KEYS.LAST_ROOM_ID);
const lastAreaId = GM_getValue(STORAGE_KEYS.LAST_AREA_ID);
const lastTitle = GM_getValue(STORAGE_KEYS.LAST_TITLE);
if (streamInfo && streamInfo.roomId) {
document.getElementById("bili-room-id").value = streamInfo.roomId;
roomId = streamInfo.roomId;
if (document.getElementById("bili-title") && streamInfo.title) {
document.getElementById("bili-title").value = streamInfo.title;
}
} else {
// 仅使用历史记录获取房间ID,取消自动获取功能
const foundRoomId = getRoomIdFromHistory();
if (foundRoomId) {
document.getElementById("bili-room-id").value = foundRoomId;
roomId = foundRoomId;
} else if (lastRoomId) {
document.getElementById("bili-room-id").value = lastRoomId;
roomId = lastRoomId;
}
}
if (document.getElementById("bili-title") && lastTitle) {
document.getElementById("bili-title").value = lastTitle;
}
// 移除 setTimeout,因为现在依赖 Promise
// setTimeout(() => {
if (lastAreaId) {
const areaSelect = document.getElementById("bili-area");
if (areaSelect) {
for (let i = 0; i < areaSelect.options.length; i++) {
if (areaSelect.options[i].value == lastAreaId) {
areaSelect.selectedIndex = i;
break;
}
}
}
}
// }, 500);
csrf = getCsrfToken();
}
// 恢复直播状态
function restoreLiveState() {
if (isLiveStarted && streamInfo) {
setTimeout(() => {
const panel = document.getElementById("bili-stream-code-panel");
if (panel) {
// 更新按钮状态
updateButtonsForLive(true);
// 恢复推流信息
restoreStreamInfo();
}
}, 500);
}
}
// 推流信息区输入框和按钮也用 class
function restoreStreamInfo() {
if (!streamInfo) return;
const resultArea = document.getElementById("bili-result");
if (!resultArea) return;
const rtmpAddr = streamInfo.rtmpAddr;
const rtmpCode = streamInfo.rtmpCode;
const resultHTML = `
<div style="display: flex; flex-direction: column; gap: 8px;">
<h3 class="bili-title" style="font-size: 16px;">推流信息 (进行中)</h3>
<div>
<p style="margin: 0; font-weight: bold;">服务器地址:</p>
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<input id="server-addr" readonly value="${rtmpAddr}" title="${rtmpAddr}" class="bili-input" />
<button id="copy-addr" class="bili-copy-btn">复制</button>
</div>
<p style="margin: 0; font-weight: bold;">推流码:</p>
<div style="display: flex; align-items: center;">
<input id="stream-code" readonly value="${rtmpCode}" title="${rtmpCode}" class="bili-input" />
<button id="copy-code" class="bili-copy-btn">复制</button>
</div>
</div>
<div class="bili-important-tip">
<p style="margin: 0; font-weight: bold;">重要提示:</p>
<p style="margin: 3px 0 0; font-size: 13px;">1. 长时间无信号会自动关闭直播</p>
<p style="margin: 3px 0 0; font-size: 13px;">2. 推流码如果变动会有提示</p>
</div>
</div>
`;
resultArea.innerHTML = resultHTML;
resultArea.style.display = "block";
// 添加复制按钮事件
const copyAddrBtn = document.getElementById("copy-addr");
if (copyAddrBtn) {
copyAddrBtn.addEventListener("click", function () {
copyToClipboardWithButton(rtmpAddr, copyAddrBtn);
});
}
const copyCodeBtn = document.getElementById("copy-code");
if (copyCodeBtn) {
copyCodeBtn.addEventListener("click", function () {
copyToClipboardWithButton(rtmpCode, copyCodeBtn);
});
}
// 新增:推流信息区插入后,重新应用颜色模式
const isDarkMode = GM_getValue("bili_dark_mode", false);
applyColorMode(isDarkMode);
}
// 更新按钮状态(开始/结束直播)
function updateButtonsForLive(isLive) {
if (startLiveButton) {
startLiveButton.disabled = isLive;
}
if (stopLiveButton) {
stopLiveButton.disabled = !(roomId && String(roomId).trim() !== "");
}
// 控制修改按钮的显示
const editButtonsContainer = document.getElementById("bili-edit-buttons");
if (editButtonsContainer) {
if (isLive) {
editButtonsContainer.style.display = "flex";
} else {
editButtonsContainer.style.display = "none";
}
}
}
// 恢复直播状态
function restoreLiveState() {
if (isLiveStarted && streamInfo) {
setTimeout(() => {
const panel = document.getElementById("bili-stream-code-panel");
if (panel) {
// 更新按钮状态
updateButtonsForLive(true);
// 恢复推流信息
restoreStreamInfo();
}
}, 500);
}
}
// 推流信息区输入框和按钮也用 class
function restoreStreamInfo() {
if (!streamInfo) return;
const resultArea = document.getElementById("bili-result");
if (!resultArea) return;
const rtmpAddr = streamInfo.rtmpAddr;
const rtmpCode = streamInfo.rtmpCode;
const resultHTML = `
<div style="display: flex; flex-direction: column; gap: 8px;">
<h3 class="bili-title" style="font-size: 16px;">推流信息 (进行中)</h3>
<div>
<p style="margin: 0; font-weight: bold;">服务器地址:</p>
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<input id="server-addr" readonly value="${rtmpAddr}" title="${rtmpAddr}" class="bili-input" />
<button id="copy-addr" class="bili-copy-btn">复制</button>
</div>
<p style="margin: 0; font-weight: bold;">推流码:</p>
<div style="display: flex; align-items: center;">
<input id="stream-code" readonly value="${rtmpCode}" title="${rtmpCode}" class="bili-input" />
<button id="copy-code" class="bili-copy-btn">复制</button>
</div>
</div>
<div class="bili-important-tip">
<p style="margin: 0; font-weight: bold;">重要提示:</p>
<p style="margin: 3px 0 0; font-size: 13px;">1. 长时间无信号会自动关闭直播</p>
<p style="margin: 3px 0 0; font-size: 13px;">2. 推流码如果变动会有提示</p>
</div>
</div>
`;
resultArea.innerHTML = resultHTML;
resultArea.style.display = "block";
// 添加复制按钮事件
const copyAddrBtn = document.getElementById("copy-addr");
if (copyAddrBtn) {
copyAddrBtn.addEventListener("click", function () {
copyToClipboardWithButton(rtmpAddr, copyAddrBtn);
});
}
const copyCodeBtn = document.getElementById("copy-code");
if (copyCodeBtn) {
copyCodeBtn.addEventListener("click", function () {
copyToClipboardWithButton(rtmpCode, copyCodeBtn);
});
}
// 新增:推流信息区插入后,重新应用颜色模式
const isDarkMode = GM_getValue("bili_dark_mode", false);
applyColorMode(isDarkMode);
}
// 修改直播标题
async function editLiveTitle() {
if (!isLiveStarted || !roomId) {
showMessage("请先开始直播", true);
return;
}
// 直接使用UI输入框的值
const newTitle = document.getElementById("bili-title").value.trim();
if (!newTitle) {
showMessage("请输入直播标题", true);
return;
}
try {
const success = await updateLiveTitle(roomId, newTitle);
if (success) {
// 更新本地存储的标题信息
if (streamInfo) {
streamInfo.title = newTitle;
GM_setValue(STORAGE_KEYS.STREAM_INFO, streamInfo);
}
GM_setValue(STORAGE_KEYS.LAST_TITLE, newTitle);
showMessage("直播标题修改成功");
} else {
showMessage("修改直播标题失败,请检查权限或网络连接", true);
}
} catch (error) {
console.error("修改标题错误:", error);
showMessage("修改直播标题时发生错误", true);
}
}
// 修改直播分区
async function editLiveArea() {
if (!isLiveStarted || !roomId) {
showMessage("请先开始直播", true);
return;
}
const areaSelect = document.getElementById("bili-area");
const newAreaId = areaSelect.value;
try {
// 直接更新分区,不重新开播
const success = await updateLiveArea(roomId, newAreaId);
if (success) {
// 更新本地存储的分区信息
if (streamInfo) {
streamInfo.areaId = newAreaId;
GM_setValue(STORAGE_KEYS.STREAM_INFO, streamInfo);
}
GM_setValue(STORAGE_KEYS.LAST_AREA_ID, newAreaId);
showMessage("分区修改成功,直播继续进行中");
} else {
showMessage("修改分区失败,请检查权限或网络连接", true);
}
} catch (error) {
console.error("修改分区错误:", error);
showMessage("修改分区时发生错误", true);
}
}
// 开始直播
async function startLive() {
// 获取输入值
roomId = document.getElementById("bili-room-id").value.trim();
const areaId = document.getElementById("bili-area").value;
const liveTitle = document.getElementById("bili-title").value.trim();
// 验证输入
if (!roomId) {
showMessage("请输入房间ID", true);
return;
}
if (!liveTitle) {
showMessage("请输入直播标题", true);
return;
}
if (!csrf) {
showMessage("无法获取CSRF令牌,请确保已登录B站", true);
return;
}
try {
const titleUpdated = await updateLiveTitle(roomId, liveTitle);
if (!titleUpdated) {
showMessage(
"设置直播标题失败,请确认是否已登录或有权限修改此直播间",
true
);
return;
}
// 设置请求参数
startData.room_id = roomId;
startData.csrf_token = csrf;
startData.csrf = csrf;
startData.area_v2 = areaId;
// 获取推流码
showMessage("正在获取推流码...");
const startLiveResponse = await gmRequest({
method: "POST",
url: API_URL_START_LIVE,
headers: headers,
data: new URLSearchParams(startData).toString(),
});
const startLiveResult = JSON.parse(startLiveResponse.responseText);
if (startLiveResult.code === 0) {
// 成功获取
handleStartLiveSuccess(startLiveResult.data, liveTitle, areaId);
} else if (startLiveResult.code === 60024 || (startLiveResult.data && startLiveResult.data.qr)) {
// 需要人脸验证
console.log("需要人脸验证:", startLiveResult);
showMessage("需要人脸验证,正在显示二维码...");
await handleFaceAuth(startLiveResult.data.qr, roomId, liveTitle, areaId);
} else {
console.error("Start live API error:", startLiveResult);
showMessage(
`获取推流码失败: ${startLiveResult.message || "未知错误"} (${startLiveResult.code})`,
true
);
}
} catch (errorResponse) {
console.error("API request failed in startLive:", errorResponse);
let errorMessage = "网络请求失败或解析错误";
if (errorResponse && errorResponse.responseText) {
try {
const parsedError = JSON.parse(errorResponse.responseText);
errorMessage = `API错误: ${parsedError.message || "未知API错误"}`;
} catch (e) {}
} else if (errorResponse instanceof Error) {
errorMessage = `请求错误: ${errorResponse.message}`;
}
showMessage(errorMessage, true);
}
}
// 处理人脸验证
async function handleFaceAuth(qrUrl, roomId, title, areaId) {
if (!qrUrl) {
showMessage("未获取到人脸验证二维码", true);
return;
}
try {
// 显示二维码
showQRCode(qrUrl);
// 设置人脸验证数据
faceAuthData.room_id = roomId;
faceAuthData.csrf_token = csrf;
faceAuthData.csrf = csrf;
// 轮询验证状态
let isVerified = false;
let attempts = 0;
const maxAttempts = 60; // 最多等待60秒
showMessage("请使用B站客户端扫描二维码进行人脸验证...");
while (!isVerified && attempts < maxAttempts) {
try {
const response = await gmRequest({
method: "POST",
url: API_URL_FACE_AUTH,
headers: headers,
data: new URLSearchParams(faceAuthData).toString(),
});
const result = JSON.parse(response.responseText);
if (result.code === 0 && result.data && result.data.is_identified) {
isVerified = true;
hideQRCode();
showMessage("人脸验证成功,正在重新获取推流码...");
// 验证成功后重新开始直播
await retryStartLive(roomId, title, areaId);
return;
}
// 等待1秒后重试
await new Promise(resolve => setTimeout(resolve, 1000));
attempts++;
} catch (error) {
console.error("人脸验证状态检查失败:", error);
attempts++;
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
// 超时处理
hideQRCode();
showMessage("人脸验证超时,请重试", true);
} catch (error) {
console.error("人脸验证处理失败:", error);
hideQRCode();
showMessage("人脸验证处理失败,请重试", true);
}
}
// 显示二维码
function showQRCode(qrUrl) {
// 移除已存在的二维码容器和遮罩
const existingContainer = document.getElementById('bili-qr-container');
const existingOverlay = document.getElementById('bili-qr-overlay');
if (existingContainer) {
existingContainer.remove();
}
if (existingOverlay) {
existingOverlay.remove();
}
// 创建遮罩层
const overlay = document.createElement('div');
overlay.id = 'bili-qr-overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 10001;
display: flex;
align-items: center;
justify-content: center;
`;
// 创建二维码容器
const qrContainer = document.createElement('div');
qrContainer.id = 'bili-qr-container';
qrContainer.style.cssText = `
position: relative;
background: rgba(255, 255, 255, 0.98);
border: 1px solid #ddd;
border-radius: 12px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
text-align: center;
min-width: 300px;
max-width: 90vw;
max-height: 90vh;
`;
const title = document.createElement('h3');
title.textContent = '人脸验证';
title.style.cssText = `
margin: 0 0 15px 0;
color: #fb7299;
font-size: 18px;
font-weight: bold;
`;
const instruction = document.createElement('p');
instruction.textContent = '请使用B站客户端扫描二维码进行人脸验证';
instruction.style.cssText = `
margin: 0 0 20px 0;
color: #666;
font-size: 14px;
`;
const qrDiv = document.createElement('div');
qrDiv.id = 'bili-qr-code';
qrDiv.style.cssText = `
margin: 20px auto;
display: flex;
justify-content: center;
align-items: center;
`;
const closeBtn = document.createElement('button');
closeBtn.textContent = '取消';
closeBtn.style.cssText = `
background: #ff4b4b;
color: white;
border: none;
border-radius: 5px;
padding: 10px 20px;
cursor: pointer;
margin-top: 20px;
font-size: 14px;
`;
closeBtn.onmouseover = () => closeBtn.style.background = '#d9363e';
closeBtn.onmouseout = () => closeBtn.style.background = '#ff4b4b';
closeBtn.onclick = () => {
hideQRCode();
showMessage("已取消人脸验证", true);
};
qrContainer.appendChild(title);
qrContainer.appendChild(instruction);
qrContainer.appendChild(qrDiv);
qrContainer.appendChild(closeBtn);
// 将二维码容器添加到遮罩层中
overlay.appendChild(qrContainer);
document.body.appendChild(overlay);
// 点击遮罩层外部关闭二维码
overlay.addEventListener('click', function(e) {
if (e.target === overlay) {
hideQRCode();
showMessage("已取消人脸验证", true);
}
});
// 生成二维码
try {
if (typeof QRCode !== 'undefined') {
console.log('QRCode库已加载');
console.log('qrUrl:', qrUrl);
console.log('QRCode.CorrectLevel:', QRCode.CorrectLevel);
// 创建二维码 - 使用更兼容的方式
const qrcode = new QRCode(qrDiv, qrUrl);
// 等待DOM更新后检查是否成功生成
setTimeout(() => {
if (qrDiv.innerHTML.trim() === '') {
console.log('⚠️ 二维码DOM为空,尝试备用方法');
// 备用方法:手动创建二维码
try {
qrDiv.innerHTML = '';
const qrcode2 = new QRCode(qrDiv, {
text: qrUrl,
width: 200,
height: 200,
colorDark: "#000000",
colorLight: "#ffffff"
});
// 再次检查
setTimeout(() => {
if (qrDiv.innerHTML.trim() === '') {
console.log('⚠️ 备用方法也失败,显示链接');
qrDiv.innerHTML = `
<p style="color: #666; font-size: 14px; line-height: 1.5; text-align: center;">
二维码生成失败,请手动访问:<br>
<a href="${qrUrl}" target="_blank" style="color: #fb7299; text-decoration: none; word-break: break-all;">${qrUrl}</a>
</p>
`;
} else {
console.log('✅ 备用方法生成二维码成功');
}
}, 500);
} catch (backupError) {
console.error('备用二维码生成失败:', backupError);
qrDiv.innerHTML = `
<p style="color: #666; font-size: 14px; line-height: 1.5; text-align: center;">
二维码生成失败,请手动访问:<br>
<a href="${qrUrl}" target="_blank" style="color: #fb7299; text-decoration: none; word-break: break-all;">${qrUrl}</a>
</p>
`;
}
} else {
console.log('✅ 二维码生成成功');
}
}, 500);
} else {
console.log('⚠️ QRCode库未加载,显示链接');
// 如果QRCode库未加载,显示链接
qrDiv.innerHTML = `
<p style="color: #666; font-size: 14px; line-height: 1.5; text-align: center;">
二维码库未加载,请手动访问:<br>
<a href="${qrUrl}" target="_blank" style="color: #fb7299; text-decoration: none; word-break: break-all;">${qrUrl}</a>
</p>
`;
}
} catch (error) {
console.error('生成二维码失败:', error);
qrDiv.innerHTML = `
<p style="color: #ff4b4b; font-size: 14px; line-height: 1.5; text-align: center;">
生成二维码失败,请手动访问:<br>
<a href="${qrUrl}" target="_blank" style="color: #fb7299; text-decoration: none; word-break: break-all;">${qrUrl}</a>
</p>
`;
}
}
// 隐藏二维码
function hideQRCode() {
const qrContainer = document.getElementById('bili-qr-container');
const qrOverlay = document.getElementById('bili-qr-overlay');
if (qrContainer) {
qrContainer.remove();
}
if (qrOverlay) {
qrOverlay.remove();
}
}
// 重试开始直播
async function retryStartLive(roomId, title, areaId) {
try {
// 重新设置开始直播数据
startData.room_id = roomId;
startData.csrf_token = csrf;
startData.csrf = csrf;
startData.area_v2 = areaId;
const response = await gmRequest({
method: "POST",
url: API_URL_START_LIVE,
headers: headers,
data: new URLSearchParams(startData).toString(),
});
const result = JSON.parse(response.responseText);
if (result.code === 0) {
handleStartLiveSuccess(result.data, title, areaId);
} else {
console.error("重试开始直播失败:", result);
showMessage(`重试开始直播失败: ${result.message || "未知错误"} (${result.code})`, true);
}
} catch (error) {
console.error("重试开始直播请求失败:", error);
showMessage("重试开始直播请求失败", true);
}
}
// 处理开始直播成功
function handleStartLiveSuccess(data, title, areaId) {
const rtmpAddr = data.rtmp.addr;
const rtmpCode = data.rtmp.code;
// 新增:保存本次推流信息到本地用于下次对比
GM_setValue("bili_last_rtmp_addr", rtmpAddr);
GM_setValue("bili_last_rtmp_code", rtmpCode);
// 检查上次推流信息是否有变动
let changeTip = "";
const prevAddr = GM_getValue("bili_prev_rtmp_addr");
const prevCode = GM_getValue("bili_prev_rtmp_code");
if (prevAddr && prevCode) {
if (prevAddr !== rtmpAddr || prevCode !== rtmpCode) {
changeTip = `<div class=\"bili-tip-yellow\"><span style=\"font-weight:bold;\">注意:</span>本次推流信息与上次不同,请确认已更新到OBS等推流软件!</div>`;
} else {
changeTip = `<div class=\"bili-tip-green\"><span style=\"font-weight:bold;\">推流信息没有变动 🎉🎉</span></div>`;
}
}
// 更新本地上次推流信息为本次
GM_setValue("bili_prev_rtmp_addr", rtmpAddr);
GM_setValue("bili_prev_rtmp_code", rtmpCode);
// 显示推流信息
const resultHTML = `
<div style="display: flex; flex-direction: column; gap: 8px;">
<h3 class="bili-title" style="font-size: 16px;">推流信息</h3>
<div>
<p style="margin: 0; font-weight: bold;">服务器地址:</p>
<div style="display: flex; align-items: center; margin-bottom: 8px;">
<input id="server-addr" readonly value="${rtmpAddr}" title="${rtmpAddr}" class="bili-input" />
<button id="copy-addr" class="bili-copy-btn">复制</button>
</div>
<p style="margin: 0; font-weight: bold;">推流码:</p>
<div style="display: flex; align-items: center;">
<input id="stream-code" readonly value="${rtmpCode}" title="${rtmpCode}" class="bili-input" />
<button id="copy-code" class="bili-copy-btn">复制</button>
</div>
</div>
${changeTip}
<div class="bili-important-tip">
<p style="margin: 0; font-weight: bold;">重要提示:</p>
<p style="margin: 3px 0 0; font-size: 13px;">1. 长时间无信号会自动关闭直播</p>
<p style="margin: 3px 0 0; font-size: 13px;">2. 推流码如果变动会有提示</p>
</div>
</div>
`;
const resultArea = document.getElementById("bili-result");
resultArea.innerHTML = resultHTML;
resultArea.style.display = "block";
// 添加复制按钮事件
const copyAddrBtn = document.getElementById("copy-addr");
if (copyAddrBtn) {
copyAddrBtn.addEventListener("click", function () {
copyToClipboardWithButton(rtmpAddr, copyAddrBtn);
});
}
const copyCodeBtn = document.getElementById("copy-code");
if (copyCodeBtn) {
copyCodeBtn.addEventListener("click", function () {
copyToClipboardWithButton(rtmpCode, copyCodeBtn);
});
}
// 新增:推流信息区插入后,重新应用颜色模式
const isDarkMode = GM_getValue("bili_dark_mode", false);
applyColorMode(isDarkMode);
// 更新按钮状态
updateButtonsForLive(true);
// 保存直播状态
isLiveStarted = true;
streamInfo = {
rtmpAddr,
rtmpCode,
roomId,
areaId,
title,
};
GM_setValue("isLiveStarted", true);
GM_setValue("streamInfo", streamInfo);
// 显示通知
GM_notification({
text: "已成功获取推流码并开始直播",
title: "B站推流码获取工具",
timeout: 5000,
});
}
// 更新直播标题
async function updateLiveTitle(roomId, title) {
titleData.room_id = roomId;
titleData.title = title;
titleData.csrf_token = csrf;
titleData.csrf = csrf;
try {
const response = await gmRequest({
method: "POST",
url: API_URL_UPDATE_ROOM,
headers: headers,
data: new URLSearchParams(titleData).toString(),
});
const result = JSON.parse(response.responseText);
if (result.code !== 0) {
console.error("Update title API error:", result);
}
return result.code === 0;
} catch (errorResponse) {
console.error("Update title request error:", errorResponse);
return false;
}
}
// 更新直播分区
async function updateLiveArea(roomId, areaId) {
areaData.room_id = roomId;
areaData.area_id = areaId;
areaData.csrf_token = csrf;
areaData.csrf = csrf;
try {
const response = await gmRequest({
method: "POST",
url: API_URL_UPDATE_ROOM,
headers: headers,
data: new URLSearchParams(areaData).toString(),
});
const result = JSON.parse(response.responseText);
if (result.code !== 0) {
console.error("Update area API error:", result);
}
return result.code === 0;
} catch (errorResponse) {
console.error("Update area request error:", errorResponse);
return false;
}
}
// 停止直播
async function stopLive() {
if (!isLiveStarted) return;
if (!confirm("确定要结束直播吗?")) return;
// 设置请求参数
stopData.room_id = roomId;
stopData.csrf_token = csrf;
stopData.csrf = csrf;
try {
const response = await gmRequest({
method: "POST",
url: API_URL_STOP_LIVE,
headers: headers,
data: new URLSearchParams(stopData).toString(),
});
const result = JSON.parse(response.responseText);
if (result.code === 0) {
// 成功结束直播
showMessage("直播已成功结束");
// 更新按钮状态
updateButtonsForLive(false);
// 清除直播状态
isLiveStarted = false;
streamInfo = null;
GM_setValue("isLiveStarted", false);
GM_setValue("streamInfo", null);
// 隐藏推流信息区域,回到未开播状态
const resultArea = document.getElementById("bili-result");
if (resultArea) {
setTimeout(() => {
resultArea.style.display = "none";
}, 2000); // 2秒后隐藏,让用户看到结束成功的消息
}
} else {
console.error("Stop live API error:", result);
showMessage(`结束直播失败: ${result.message || "未知错误"}`, true);
}
} catch (errorResponse) {
console.error("Stop live request error:", errorResponse);
let errorMessage = "网络请求失败或解析错误";
if (errorResponse && errorResponse.responseText) {
try {
const parsedError = JSON.parse(errorResponse.responseText);
errorMessage = `API错误: ${parsedError.message || "未知API错误"}`;
} catch (e) {}
} else if (errorResponse instanceof Error) {
errorMessage = `请求错误: ${errorResponse.message}`;
}
showMessage(errorMessage, true);
}
}
// 显示消息(优化版:不覆盖推流信息)
function showMessage(message, isError = false) {
const resultArea = document.getElementById("bili-result");
if (resultArea && isLiveStarted && streamInfo) {
// 如果已开播,在推流信息区域内添加状态提示
showStatusInStreamInfo(message, isError);
} else if (resultArea) {
// 未开播时,正常显示消息
resultArea.innerHTML = `<p class="bili-message${
isError ? " bili-message-error" : ""
}">${message}</p>`;
resultArea.style.display = "block";
}
// 始终显示Toast通知
GM_notification({
text: message,
title: isError ? "错误" : "B站推流码获取工具",
timeout: 5000,
});
}
// 在推流信息区域显示状态提示
function showStatusInStreamInfo(message, isError = false) {
const resultArea = document.getElementById("bili-result");
if (!resultArea) return;
// 查找或创建状态栏
let statusBar = document.getElementById("bili-status-bar");
if (!statusBar) {
statusBar = document.createElement("div");
statusBar.id = "bili-status-bar";
statusBar.style.cssText = `
margin-bottom: 10px;
padding: 8px 12px;
border-radius: 4px;
font-size: 13px;
text-align: center;
transition: all 0.3s ease;
`;
// 将状态栏插入到推流信息的前面
resultArea.insertBefore(statusBar, resultArea.firstChild);
}
// 设置状态栏样式和内容
statusBar.className = isError ? "bili-status-error" : "bili-status-success";
statusBar.textContent = message;
statusBar.style.display = "block";
// 3秒后自动隐藏状态栏
clearTimeout(statusBar._hideTimer);
statusBar._hideTimer = setTimeout(() => {
if (statusBar) {
statusBar.style.display = "none";
}
}, 3000);
}
// 复制到剪贴板
function copyToClipboard(text) {
GM_setClipboard(text);
showMessage("已复制到剪贴板");
}
// 复制到剪贴板(按钮变✅,不弹窗)
function copyToClipboardWithButton(text, btn) {
GM_setClipboard(text);
if (!btn) return;
const oldText = btn.textContent;
btn.textContent = "✅";
btn.disabled = true;
btn.classList.add("bili-copy-btn");
setTimeout(() => {
btn.textContent = oldText;
btn.disabled = false;
btn.classList.add("bili-copy-btn");
}, 2000);
}
// 页面导航事件监听
window.addEventListener("popstate", init);
window.addEventListener("hashchange", init);
// 监听页面可见性变化,页面可见时检查浮动按钮
document.addEventListener("visibilitychange", function () {
if (document.visibilityState === "visible") {
checkFloatButton();
}
});
// 使用MutationObserver监听DOM变化,动态检查浮动按钮
const observer = new MutationObserver(function () {
checkFloatButton();
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
// 初始加载
setTimeout(init, 500);
})();