您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
整合版:声道控制+自动切换+镜像同步
// ==UserScript== // @name B站终极声道控制Pro // @namespace http://tampermonkey.net/ // @version 4.0 // @description 整合版:声道控制+自动切换+镜像同步 // @author QAQ // @match https://www.bilibili.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_notification // @run-at document-end // ==/UserScript== (function() { 'use strict'; // 增强配置 const CONFIG = { MODES: { ORIGINAL: { id: 0, name: '原声', icon: '🔊', color: '#00a1d6' }, SWAPPED: { id: 1, name: '镜像', icon: '🔄', color: '#ff6050' } }, SYNC: { INTERVAL: 250, // 同步频率 TIMEOUT: 3000 // 同步超时 }, AUDIO: { SAMPLE_RATE: 48000 // 专业音频采样率 } }; // 全局状态 let state = { currentMode: GM_getValue('audioMode', CONFIG.MODES.ORIGINAL.id), autoSwitch: { enabled: false, interval: GM_getValue('autoInterval', 5) }, mirror: { window: null, isMaster: true, syncTimer: null } }; let audioNodes = new WeakMap(); // 专业级样式 GM_addStyle(` .bpx-audio-pro-panel { position: fixed; left: 20px; bottom: 100px; z-index: 2147483647; background: rgba(16,18,20,0.98); border-radius: 12px; padding: 16px; width: 300px; box-shadow: 0 8px 32px rgba(0,0,0,0.3); backdrop-filter: blur(16px); border: 1px solid rgba(255,255,255,0.15); font-family: system-ui, sans-serif; } .mode-section { margin-bottom: 16px; } .mode-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } .mode-btn { padding: 12px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); border-radius: 8px; color: white; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; justify-content: center; gap: 6px; } .mode-btn.active { background: var(--active-color); box-shadow: 0 4px 16px var(--active-glow); } .auto-control { margin: 12px 0; } .time-input { width: 100%; padding: 10px; background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; color: white; margin-bottom: 8px; } .toggle-btn { width: 100%; padding: 10px; border-radius: 6px; border: none; color: white; cursor: pointer; transition: opacity 0.2s; } .auto-active { background: linear-gradient(135deg, #00c853 0%, #009624 100%); } .auto-inactive { background: linear-gradient(135deg, #ff6050 0%, #ff3b30 100%); } .mirror-control { margin-top: 12px; background: linear-gradient(135deg, #6200ea 0%, #3700b3 100%); } `); // 主初始化 function initCoreSystem() { checkInstanceType(); injectControlPanel(); setupAudioSystem(); setupStateSync(); registerMenuCommands(); } // 实例类型检测 function checkInstanceType() { const urlParams = new URLSearchParams(window.location.search); state.mirror.isMaster = !urlParams.has('mirror'); if (!state.mirror.isMaster) document.title = `[镜像] ${document.title}`; } // 注入控制面板 function injectControlPanel() { const panel = document.createElement('div'); panel.className = 'bpx-audio-pro-panel'; panel.innerHTML = ` <div class="mode-section"> <div class="mode-grid"> ${Object.values(CONFIG.MODES).map(mode => ` <button class="mode-btn" data-mode="${mode.id}" style="--active-color: ${mode.color}; --active-glow: ${mode.color}40"> ${mode.icon} ${mode.name} </button> `).join('')} </div> </div> <div class="auto-control"> <input type="number" class="time-input" placeholder="自动切换间隔(秒)" min="1" value="${state.autoSwitch.interval}"> <button class="toggle-btn ${state.autoSwitch.enabled ? 'auto-active' : 'auto-inactive'}"> ${state.autoSwitch.enabled ? '停止自动切换' : '启动自动切换'} </button> </div> ${state.mirror.isMaster ? '<button class="toggle-btn mirror-control">${state.mirror.window ? "关闭镜像" : "启动镜像窗口"}</button>' : '<div class="mirror-status">镜像模式已激活</div>' } `; const container = document.querySelector('.bpx-player-container') || document.body; container.prepend(panel); bindPanelEvents(panel); updateUIState(); } // 事件绑定 function bindPanelEvents(panel) { // 模式切换 panel.querySelectorAll('.mode-btn').forEach(btn => { btn.addEventListener('click', () => handleModeChange(parseInt(btn.dataset.mode))); }); // 自动切换 const autoBtn = panel.querySelector('.toggle-btn:not(.mirror-control)'); autoBtn.addEventListener('click', toggleAutoSwitch); // 镜像控制 const mirrorBtn = panel.querySelector('.mirror-control'); if (mirrorBtn) { mirrorBtn.addEventListener('click', handleMirrorOperation); } // 输入验证 panel.querySelector('.time-input').addEventListener('input', function(e) { this.value = Math.max(1, parseInt(this.value) || 5); }); } // 处理模式切换 function handleModeChange(modeId) { const newMode = Object.values(CONFIG.MODES).find(m => m.id === modeId); state.currentMode = newMode; applyAudioSettings(); GM_setValue('audioMode', modeId); updateUIState(); if (state.mirror.window) { state.mirror.window.postMessage({ type: 'modeChange', modeId }, '*'); } } // 自动切换控制 function toggleAutoSwitch() { state.autoSwitch.enabled = !state.autoSwitch.enabled; this.classList.toggle('auto-active'); this.classList.toggle('auto-inactive'); this.textContent = state.autoSwitch.enabled ? '停止自动切换' : '启动自动切换'; if (state.autoSwitch.enabled) { const interval = parseInt(document.querySelector('.time-input').value) * 1000; state.autoSwitch.timer = setInterval(() => { const newModeId = (state.currentMode.id + 1) % 2; handleModeChange(newModeId); }, interval); } else { clearInterval(state.autoSwitch.timer); } GM_setValue('autoInterval', document.querySelector('.time-input').value); } // 镜像窗口管理 function handleMirrorOperation() { const btn = this; btn.disabled = true; btn.textContent = '初始化中...'; if (state.mirror.window) { state.mirror.window.close(); state.mirror.window = null; btn.textContent = '启动镜像窗口'; btn.disabled = false; return; } const success = createMirrorWindow(); if (success) { btn.textContent = '同步中...'; handleModeChange(CONFIG.MODES.SWAPPED.id); } else { GM_notification({ title: '镜像创建失败', text: '请允许弹出窗口', highlight: true }); btn.textContent = '启动镜像窗口'; } btn.disabled = false; } // 创建镜像窗口 function createMirrorWindow() { try { const video = document.querySelector('video'); if (!video) return false; const mirrorUrl = window.location.href.replace(/#.*$/, '') + '?mirror=1'; state.mirror.window = window.open(mirrorUrl, '_blank', ` width=${window.innerWidth}, height=${window.innerHeight}, location=no, menubar=no, toolbar=no `); setupMasterSync(video); return true; } catch (e) { console.error('镜像创建失败:', e); return false; } } // 主窗口同步逻辑 function setupMasterSync(video) { let lastUpdate = 0; const syncHandler = () => { const currentTime = Date.now(); if (currentTime - lastUpdate < CONFIG.SYNC.INTERVAL) return; GM_setValue('syncPacket', { time: video.currentTime, paused: video.paused, rate: video.playbackRate, timestamp: currentTime }); lastUpdate = currentTime; }; video.addEventListener('timeupdate', syncHandler); video.addEventListener('ratechange', syncHandler); video.addEventListener('play', syncHandler); video.addEventListener('pause', syncHandler); } // 状态同步系统 function setupStateSync() { if (!state.mirror.isMaster) { state.mirror.syncTimer = setInterval(() => { const packet = GM_getValue('syncPacket'); if (!packet || packet.timestamp <= (video.lastSync || 0)) return; const video = document.querySelector('video'); if (video) { video.lastSync = packet.timestamp; video.currentTime = packet.time; video.playbackRate = packet.rate; packet.paused ? video.pause() : video.play(); } }, CONFIG.SYNC.INTERVAL); } // 跨窗口通信 window.addEventListener('message', (e) => { if (e.data.type === 'modeChange') { handleModeChange(e.data.modeId); } }); } // 音频系统 function setupAudioSystem() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeName === 'VIDEO') initAudioContext(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); // 初始化现有视频 document.querySelectorAll('video').forEach(initAudioContext); } // 初始化音频处理 function initAudioContext(video) { if (audioNodes.has(video)) return; try { const ctx = new AudioContext({ sampleRate: CONFIG.AUDIO.SAMPLE_RATE }); const source = ctx.createMediaElementSource(video); const splitter = ctx.createChannelSplitter(2); const merger = ctx.createChannelMerger(2); source.connect(splitter); updateAudioRouting(splitter, merger); merger.connect(ctx.destination); audioNodes.set(video, { splitter, merger }); // 自动恢复上下文 video.addEventListener('play', () => ctx.resume()); video.addEventListener('volumechange', () => { if (!state.mirror.isMaster) video.muted = true; }); } catch (error) { console.error('音频初始化失败:', error); } } // 更新音频路由 function updateAudioRouting(splitter, merger) { splitter.disconnect(); if (state.currentMode.id === CONFIG.MODES.SWAPPED.id) { splitter.connect(merger, 0, 1); splitter.connect(merger, 1, 0); } else { splitter.connect(merger, 0, 0); splitter.connect(merger, 1, 1); } } // 应用音频设置 function applyAudioSettings() { document.querySelectorAll('video').forEach(video => { const nodes = audioNodes.get(video); if (nodes) updateAudioRouting(nodes.splitter, nodes.merger); }); } // 更新UI状态 function updateUIState() { document.querySelectorAll('.mode-btn').forEach(btn => { const isActive = parseInt(btn.dataset.mode) === state.currentMode.id; btn.classList.toggle('active', isActive); }); const mirrorBtn = document.querySelector('.mirror-control'); if (mirrorBtn) { mirrorBtn.textContent = state.mirror.window ? "关闭镜像" : "启动镜像窗口"; } } // 注册菜单命令 function registerMenuCommands() { GM_registerMenuCommand('⚙️ 打开/关闭控制台', () => { const panel = document.querySelector('.bpx-audio-pro-panel'); panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; }); } // 清理资源 window.addEventListener('beforeunload', () => { clearInterval(state.autoSwitch.timer); clearInterval(state.mirror.syncTimer); GM_setValue('syncPacket', null); if (state.mirror.window) state.mirror.window.close(); }); // 启动系统 if (document.readyState === 'complete') { initCoreSystem(); } else { window.addEventListener('load', initCoreSystem); } })();