您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
获取第三方推流码
// ==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); })();