// ==UserScript== // @name 虎牙清晰度解锁(真30M / 原画切换) // @description 解锁虎牙网页直播清晰度,修正30M档位映射,保留主播原画选择,支持可选的自动最高画质。 // @author snowlove12 // @version 2.0.0 // @namespace snowlove12/huya-quality // @license GPL-3.0-or-later // @icon https://scriptcat.org/api/v2/resource/image/1Jb7c7tYccYHz82u // @tag 虎牙 // @tag 直播 // @tag 视频 // @tag 清晰度 // @match *://www.huya.com/* // @run-at document-end // @grant unsafeWindow // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @noframes // ==/UserScript== /* * 修改部分 Copyright (C) 2026 snowlove12。依 GPL-3.0-or-later 分发。 * * 来源与署名: * - 原作「虎牙免登录观看」,作者 (σ`д′)σ: * https://greasyfork.org/zh-CN/scripts/477947 * - 基于 Li「虎牙解锁蓝光20M清晰度(去扫码角标版)」1.2.0 修改。 * - 取流与档位核验思路参考「纯粹直播(Pure Live)」: * https://github.com/liuchuancong/pure_live * 最终实现使用虎牙网页原生 SDK 的签名及令牌更新机制。 * * 网页把部分「蓝光30M」的 iBitRate 设成 0(主播源流)。本版修正为 30000, * 并单独保留 0 档。只修正明确标为 30M 的项,不猜测其他原画/4K/20M 项的码率。 * 虎牙原生 SDK 已实现 fm/seqid/wsSecret 计算及令牌更新,直接复用其签名生命周期; * ratio、codec、sdkPcdn 不参与 wsSecret 签名。30M 使用 H.264 HTTP-FLV 直连。 * 不缓存一份固定签名 URL,不需要额外播放器或本地服务器。 */ (() => { 'use strict'; const W = typeof unsafeWindow === 'object' ? unsafeWindow : window; if (W.__huyaTrue30Installed) return; W.__huyaTrue30Installed = true; const TRUE30 = /(?:^|[^\d])30\s*M(?:\b|$)/i; const SOURCE = '原画(主播源流)'; let player = null, timer = null, stopped = false, initPending = false; let autoDone = '', autoTries = 0, bootTries = 0; let autoSelectHighRes = false; try { autoSelectHighRes = !!GM_getValue('autoSelectHighRes', false); } catch (_) {} const $ = el => W.jQuery ? W.jQuery(el) : null; const dataOf = el => $(el)?.data('data'); const is30 = d => !!d && TRUE30.test(d.sDisplayName || ''); function message(text) { console.info('[虎牙真30M]', text); const toast = document.createElement('div'); toast.textContent = text; toast.style.cssText = 'position:fixed;top:80px;left:50%;transform:translateX(-50%);z-index:2147483647;background:#222e;color:#fff;padding:10px 18px;border-radius:6px;pointer-events:none'; (document.body || document.documentElement).appendChild(toast); setTimeout(() => toast.remove(), 4500); } // SDK 会把原生 CDN 请求改成 PCDN。去掉分流参数;签名和 UID 仍由 SDK 提供。 function direct30(raw) { try { const u = new W.URL(raw, location.href); if (!u.hostname.endsWith('.flv.huya.com') || !u.pathname.endsWith('.flv') || u.searchParams.get('ratio') !== '30000') return raw; u.searchParams.delete('sdkPcdn'); u.searchParams.set('codec', '264'); return u.href; } catch (_) { return raw; } } function hookRequests() { const fetch0 = W.fetch; if (typeof fetch0 === 'function') { W.fetch = function (input, ...args) { if (input instanceof W.Request) { const url = direct30(input.url); if (url !== input.url) input = new W.Request(url, input); } else input = direct30(input); return fetch0.call(this, input, ...args); }; } const open0 = W.XMLHttpRequest.prototype.open; W.XMLHttpRequest.prototype.open = function (method, url, ...args) { return open0.call(this, method, direct30(url), ...args); }; } function hookPlayer(vp) { const h = vp.vcore?.h5player; if (!h || typeof h.start !== 'function') return false; const start0 = h.start; h.start = function (info, ...args) { if (info && Number(info.iBitRate) === 30000 && Number(info.liveType) === 1) { info = { ...info, type: W.H5Player.FLV, iBitRate: 30000, curBitrate: 30000, iCodecType: 0, iHEVCBitRate: -1, iIsHEVCSupport: 0, iIsP2PSupport: 0, sP2pUrl: '', isAuto: false, hasAutoBitrate: false }; this.setConfig(100, ''); this.setConfig(101, false); } return start0.call(this, info, ...args); }; hookRequests(); return true; } function tierLists() { const g = player.G; return [g.getBitRateList?.(), player.ENV?.stream?.vMultiStreamInfo, g.livingInfo?.tNotice?.vMultiStreamInfo?.value, g.livingInfo?.vStreamGearInfo?.value].filter(Array.isArray); } function patchTier(d) { d.iBitRate = 30000; // 原画 HEVC 的 0 不代表 30M HEVC;已验证的 30M 转码使用 H.264。 d.iHEVCBitRate = -1; d.iCodecType = 0; } function unlocked(d) { return Number(d.iEnable) === 1 || Number(d.iEnableMethod) === 4 || Number(d.iEnableMethod) === 0; } function select(el) { if (!el || !player) return false; const d = dataOf(el); if (!d) return false; if (Number(player.G.curBitRate) !== Number(d.iBitRate) || !el.classList.contains('on')) { el.classList.remove('on'); el.click(); } return true; } function patchMenu() { if (!player || !W.jQuery || player.G.isReplay) return; const list = document.querySelector('#player-ctrl-wrap .player-videotype-list'); if (!list) return; const models = tierLists(); for (const model of models) { const d = model.find(is30); if (!d) continue; if (Number(d.iBitRate) === 0 && !model.some(x => x.sDisplayName === SOURCE)) { model.push({ ...d, sDisplayName: SOURCE, iEnable: 1, iEnableMethod: 0, sTagText: '', sTagUrl: '', sEnableUrl: '' }); } patchTier(d); } let trueItem = null, sourceItem = null, replaceSelected = false; for (const el of [...list.children]) { const d = dataOf(el); if (!d) continue; if (is30(d)) { trueItem = el; if (Number(d.iBitRate) === 0) { replaceSelected = el.classList.contains('on'); if (![...list.children].some(e => dataOf(e)?.sDisplayName === SOURCE)) { // 复制原生点击处理器,保持播放、清晰度标题、菜单状态一起更新。 const clone = $(el).clone(true); clone.data('data', { ...d, sDisplayName: SOURCE, status: 0, iEnable: 1, iEnableMethod: 0, sTagText: '', sTagUrl: '', sEnableUrl: '' }); clone.removeClass('on'); clone.find('span').first().text(SOURCE); clone.insertAfter(el); } } patchTier(d); el.title = '真正的30M转码;实际码率会随画面内容变化'; } if (unlocked(d)) d.status = 0; if (d.sDisplayName === SOURCE) sourceItem = el; } if (sourceItem && !replaceSelected && Number(player.G.curBitRate) === 0) { for (const el of list.children) el.classList.toggle('on', el === sourceItem); } if (replaceSelected && trueItem) select(trueItem); // 自动最高画质只在进房时选择一次,之后用户仍能手动降档。 const roomKey = String(player.G.presenterUid) + ':' + String(player.G.streamInfo?.sStreamName || ''); if (autoSelectHighRes && autoDone !== roomKey) { const target = trueItem || [...list.children].find(el => { const d = dataOf(el); return d && d.isAuto !== 1 && unlocked(d); }); if (target && ++autoTries <= 8 && select(target)) autoDone = roomKey; } } try { GM_registerMenuCommand(`自动切换最高画质:${autoSelectHighRes ? '开启' : '关闭'}`, () => { GM_setValue('autoSelectHighRes', !autoSelectHighRes); location.reload(); }); GM_registerMenuCommand('立即切换真正30M', () => { patchMenu(); const el = [...document.querySelectorAll('.player-videotype-list li')].find(e => is30(dataOf(e))); if (!select(el)) message('这个房间目前没有蓝光30M档位'); }); GM_registerMenuCommand('切换主播原画', () => { patchMenu(); const el = [...document.querySelectorAll('.player-videotype-list li')].find(e => Number(dataOf(e)?.iBitRate) === 0); if (!select(el)) message('这个房间目前没有原画选项'); }); } catch (_) {} const style = document.createElement('style'); style.textContent = '#player-ctrl-wrap .bitrate-right-btn{display:none!important;visibility:hidden!important;pointer-events:none!important;}'; (document.head || document.documentElement).appendChild(style); function tick() { if (stopped) return; if (!player && !initPending && W.TT_ROOM_PLAYER?.initComplete) { initPending = true; W.TT_ROOM_PLAYER.initComplete(vp => { initPending = false; if (stopped || !vp || player) return; if (hookPlayer(vp)) player = vp; }); } try { patchMenu(); } catch (e) { console.warn('[虎牙真30M]', e); } if (player || ++bootTries < 120) timer = setTimeout(tick, 1000); } window.addEventListener('pagehide', () => { stopped = true; clearTimeout(timer); }); window.addEventListener('pageshow', e => { if (e.persisted && stopped) { stopped = false; tick(); } }); tick(); })();