// ==UserScript==
// @name 网页音量增强
// @namespace https://github.com/Stareven233
// @version 1.6.2
// @description 增强网页视频音量,支持正则表达式匹配网址,仅对匹配页面生效(初始不对任何网页起作用),支持每个网页单独设置video选择器、音量,支持 iframe 跨域视频
// @description Boost web video volume. Supports regex URL matching, custom video selectors, individual volume settings, and cross-origin iframes.
// @author Noetu
// @license CC BY-NC-SA-4.0
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_notification
// @run-at document-end
// @downloadURL https://update.greasyfork.icu/scripts/567482/%E7%BD%91%E9%A1%B5%E9%9F%B3%E9%87%8F%E5%A2%9E%E5%BC%BA.user.js
// @updateURL https://update.greasyfork.icu/scripts/567482/%E7%BD%91%E9%A1%B5%E9%9F%B3%E9%87%8F%E5%A2%9E%E5%BC%BA.meta.js
// ==/UserScript==
(function () {
'use strict';
if(window._n_volumAmpInitialized) {
return;
}
window._n_volumAmpInitialized = true;
const TM_CONFIG_KEY = 'n_volumAmpTMConfig';
const SETTINGS_STORAGE_KEY = 'n_volumAmpSiteSettings';
const AMP_MAX_FACTOR = 6.66
const AMP_STEP = 0.01
// ============ Utility Functions ============
function escapeHTML(str) {
return str.replace(/[&<>"']/g, m => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
}[m]));
}
// ============ Configuration Management ============
function getTMConfig() {
return GM_getValue(TM_CONFIG_KEY, { rules: [], globalSwitch: true });
}
function saveTMConfig(config) {
GM_setValue(TM_CONFIG_KEY, config);
}
function getSiteSettings() {
return GM_getValue(SETTINGS_STORAGE_KEY, {});
}
function saveSiteSettings(settings) {
GM_setValue(SETTINGS_STORAGE_KEY, settings);
}
// ============ Rule Matching ============
function checkMatch() {
const config = getTMConfig();
if(!config.globalSwitch || !config.rules?.length) {
return { matched: false };
}
const currentUrl = window.location.href;
for(const pattern of config.rules) {
try {
if(new RegExp(pattern).test(currentUrl)) {
return { matched: true, rule: pattern };
}
} catch(e) {
if(currentUrl.includes(pattern)) {
return { matched: true, rule: pattern };
}
}
}
return { matched: false };
}
function parseRules(ruleText) {
let rules = ruleText.split('\n').map(s => s.trim()).filter(s => s !== '');
// Limit regular expression length
rules = rules.filter(s => s.length > 0 && s.length < 1000)
// Validate if it's a valid regular expression
rules = rules.filter(s => {
try {
new RegExp(s);
return true;
} catch {
return false;
}
})
return Array.from(new Set(rules));
}
// ============ Custom Management Modal ============
function openRuleManager() {
const config = getTMConfig();
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.7); z-index: 23333;
display: flex; align-items: center; justify-content: center;
backdrop-filter: blur(4px); font-family: sans-serif;
`;
const modal = document.createElement('div');
modal.style.cssText = `
background: #1e1e2e; color: #cdd6f4; width: 90%; max-width: 500px;
padding: 20px; border-radius: 12px; border: 1px solid #45475a;
box-shadow: 0 10px 40px rgba(0,0,0,0.8);
`;
const locationPrefix = window.location.origin + window.location.pathname.replace(/\/[^\/]*\/?$/, '');
modal.innerHTML = `
⚙️ 匹配规则管理
每行输入一个正则表达式。匹配成功则启用网页音量增强。
当前 URL: ${escapeHTML(window.location.href)}
匹配当前 URL 前缀示例: ${escapeHTML(locationPrefix)}/?.*
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
modal.querySelector('#_btnCancel').onclick = () => overlay.remove();
modal.querySelector('#_btnSave').onclick = () => {
config.rules = parseRules(modal.querySelector('#_rulesArea').value);
saveTMConfig(config);
location.reload();
};
}
// ============ Menu Registration ============
function registerMenu() {
const { matched } = checkMatch();
const currentUrl = window.location.href;
// 1. Quick toggle switch
const statusLabel = matched ? '✅ 本页已启用 (点击移除)' : '⚪ 本页未启用 (点击添加)';
GM_registerMenuCommand(statusLabel, () => {
const currentConfig = getTMConfig();
if(matched) {
currentConfig.rules = currentConfig.rules.filter(r => {
try { return !(new RegExp(r).test(currentUrl)); } catch(e) { return r !== currentUrl; }
});
} else {
const safeUrl = currentUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
currentConfig.rules.push(`^${safeUrl}$`);
}
saveTMConfig(currentConfig);
location.reload();
});
// 2. Add regular expression rule
GM_registerMenuCommand('🧪 管理匹配规则', openRuleManager);
}
// ============ Core Functionality ============
let audioCtx, gainNode;
function initAmplifier(video, volume) {
try {
if(video._n_volumAmpBound) {
return true;
}
const AudioContext = window.AudioContext || window.webkitAudioContext;
audioCtx = new AudioContext();
const source = audioCtx.createMediaElementSource(video);
gainNode = audioCtx.createGain();
gainNode.gain.value = volume;
source.connect(gainNode).connect(audioCtx.destination);
video._n_volumAmpBound = true;
const resume = () => { if(audioCtx.state === 'suspended') audioCtx.resume(); };
window.addEventListener('mousedown', resume, { once: true });
window.addEventListener('keydown', resume, { once: true });
return true;
} catch(e) {
console.warn('音量增强初始化失败:', e);
return false;
}
}
function showUI(settings, rule) {
if(document.getElementById('_n_volumAmpPanel')) {
return;
}
const panel = document.createElement('div');
panel.id = '_n_volumAmpPanel';
panel.style.cssText = `
position: fixed; bottom: 20px; right: 20px; z-index: 23333;
background: rgba(30, 30, 46, 0.95); color: #cdd6f4; padding: 15px;
border-radius: 12px; border: 1px solid #45475a; width: 200px;
box-shadow: 0 4px 15px rgba(0,0,0,0.5); font-family: sans-serif;
`;
panel.innerHTML = `
未找到视频 ×
${Math.round(settings.volume * 100)}%
`;
document.body.appendChild(panel);
const slider = panel.querySelector('#ampSlider');
const ampValText = panel.querySelector('#ampVal');
const updateVolume = (v) => {
ampValText.innerText = Math.round(v * 100);
if(gainNode) gainNode.gain.value = v;
// Broadcast to iframe
for(const frame of document.querySelectorAll('iframe')) {
try {
frame.contentWindow.postMessage({ type: '_n_volumAmpVolume', volume: v }, '*');
} catch(_) { }
}
};
slider.oninput = (e) => updateVolume(parseFloat(e.target.value));
panel.querySelector('#ampSave').onclick = () => {
const selector = panel.querySelector('#ampSelector').value.trim() || 'video';
const volume = parseFloat(slider.value);
const allSettings = getSiteSettings();
allSettings[rule] = { selector, volume };
saveSiteSettings(allSettings);
GM_notification({ text: "配置已保存,刷新页面生效", timeout: 2000 });
};
panel.querySelector('#ampClose').onclick = () => panel.remove();
}
// ============ Startup Logic ============
const isTop = window.self === window.top;
if(isTop) registerMenu();
const { matched, rule } = checkMatch();
if(!matched && isTop) return;
const allSettings = getSiteSettings();
const settings = (rule && allSettings[rule]) || { selector: 'video', volume: 1.0 };
if(isTop) {
showUI(settings, rule);
const uiTitle = document.getElementById('ampStatusTitle');
// Listen for iframe reporting
window.addEventListener('message', (e) => {
if(e.data?.type === '_n_volumAmpReady' && uiTitle) {
uiTitle.innerText = '网页音量增强';
}
});
const findVideo = () => {
const video = document.querySelector(settings.selector);
if(video) {
const success = initAmplifier(video, settings.volume)
if(uiTitle) {
console.log('findVideo success', success);
uiTitle.innerText = success ? '网页音量增强' : '音量增强初始化失败,检查是否有其他类似脚本占用