将全站视频自动混音为单声道,并支持白名单例外,优化淡入体验无割裂感,排除了哔哩哔哩直播
// ==UserScript==
// @name 全站单声道
// @namespace http://tampermonkey.net/
// @version 1.4
// @description 将全站视频自动混音为单声道,并支持白名单例外,优化淡入体验无割裂感,排除了哔哩哔哩直播
// @author You
// @match *://*/*
// @exclude *://live.bilibili.com/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const whitelist = ['music.', 'twitch.tv', 'spotify'];
function isWhitelisted() {
return whitelist.some(site => window.location.hostname.includes(site));
}
if (isWhitelisted()) {
console.log('[单声道脚本] 当前站点在白名单中,已跳过。');
return;
}
let processedVideos = new WeakSet();
function setupMono(video) {
if (processedVideos.has(video)) return;
processedVideos.add(video);
try {
const context = new (window.AudioContext || window.webkitAudioContext)();
const source = context.createMediaElementSource(video);
const splitter = context.createChannelSplitter(2);
const merger = context.createChannelMerger(2);
splitter.connect(merger, 0, 0);
splitter.connect(merger, 0, 1);
const gainNode = context.createGain();
gainNode.gain.value = 1; // 默认直接 1
source.connect(splitter);
merger.connect(gainNode);
gainNode.connect(context.destination);
// 如果已经在播放 → 快速淡入 (0.05s)
if (!video.paused) {
gainNode.gain.setValueAtTime(0, context.currentTime);
gainNode.gain.linearRampToValueAtTime(1, context.currentTime + 0.05);
}
console.log('[单声道脚本] 已应用单声道:', video);
} catch (e) {
console.warn('[单声道脚本] 处理视频失败:', e);
}
}
// 使用 MutationObserver 动态监听 video
const observer = new MutationObserver(() => {
document.querySelectorAll('video').forEach(video => {
if (!processedVideos.has(video)) {
video.addEventListener('loadedmetadata', () => setupMono(video), { once: true });
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
// 初始检查
document.querySelectorAll('video').forEach(video => {
video.addEventListener('loadedmetadata', () => setupMono(video), { once: true });
});
})();