// ==UserScript==
// @name 音频延迟调节器 | 音画同步修复(毫秒级精度+智能检测)
// @name:en Audio Delay Adjuster | A/V Sync Fix (ms Precision + Auto Detection)
// @namespace https://github.com/1683343576Hua
// @version 4.1.1
// @description 专门解决H5视频音频比画面快的问题,支持0.001秒毫秒级精度调节,支持手都检测延迟,全网站兼容,支持iframe内嵌视频,自动记忆用户设置
// @description:en Fix H5 video audio ahead of picture, support 0.001s millisecond precision adjustment, auto delay detection, full site compatible, iframe video support, auto save user settings
// @author 1683343576Hua
// @match *://*/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// @run-at document-start
// @allFrames true
// @homepageURL https://github.com/1683343576Hua/h5-video-av-sync-fix
// @supportURL https://github.com/1683343576Hua/h5-video-av-sync-fix/issues
// @downloadURL https://update.greasyfork.icu/scripts/572947/%E9%9F%B3%E9%A2%91%E5%BB%B6%E8%BF%9F%E8%B0%83%E8%8A%82%E5%99%A8%20%7C%20%E9%9F%B3%E7%94%BB%E5%90%8C%E6%AD%A5%E4%BF%AE%E5%A4%8D%EF%BC%88%E6%AF%AB%E7%A7%92%E7%BA%A7%E7%B2%BE%E5%BA%A6%2B%E6%99%BA%E8%83%BD%E6%A3%80%E6%B5%8B%EF%BC%89.user.js
// @updateURL https://update.greasyfork.icu/scripts/572947/%E9%9F%B3%E9%A2%91%E5%BB%B6%E8%BF%9F%E8%B0%83%E8%8A%82%E5%99%A8%20%7C%20%E9%9F%B3%E7%94%BB%E5%90%8C%E6%AD%A5%E4%BF%AE%E5%A4%8D%EF%BC%88%E6%AF%AB%E7%A7%92%E7%BA%A7%E7%B2%BE%E5%BA%A6%2B%E6%99%BA%E8%83%BD%E6%A3%80%E6%B5%8B%EF%BC%89.meta.js
// ==/UserScript==
(function() {
'use strict';
// ========== 核心配置 ==========
const MAX_DELAY = 10; // 最大延迟10秒,可自行修改
const STEP_FINE = 0.001; // 微调步长:1毫秒
const STEP_MID = 0.01; // 中调步长:10毫秒
const STEP_COARSE = 0.1; // 粗调步长:100毫秒
const DEFAULT_DELAY = GM_getValue('audio_delay_sec', 0);
// ========== 全局状态 ==========
let audioCtx = null;
let globalDelayNode = null;
let hookedVideos = new Map();
let currentDelay = DEFAULT_DELAY;
let isPanelCollapsed = false;
// 检测延迟相关状态
let detectionState = 'idle'; // idle, waiting_sound, waiting_video
let soundTime = 0;
let videoTime = 0;
// ========== 1. 样式注入 ==========
GM_addStyle(`
#audio-delay-master-panel {
position: fixed;
top: 20px;
right: 20px;
background: rgba(15, 15, 15, 0.95);
color: #ffffff;
padding: 18px;
border-radius: 12px;
z-index: 2147483647;
font-family: system-ui, -apple-system, sans-serif;
box-shadow: 0 4px 25px rgba(0, 0, 0, 0.7);
width: 260px;
transition: all 0.2s ease;
user-select: none;
}
#audio-delay-master-panel.collapsed {
width: 70px;
height: 45px;
padding: 10px;
overflow: hidden;
}
#audio-delay-master-panel .panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
cursor: move;
}
#audio-delay-master-panel .panel-title {
font-size: 15px;
font-weight: 600;
white-space: nowrap;
}
#audio-delay-master-panel .panel-actions {
display: flex;
gap: 10px;
align-items: center;
}
#audio-delay-master-panel .action-btn {
cursor: pointer;
color: #aaaaaa;
font-size: 18px;
line-height: 1;
transition: color 0.2s;
}
#audio-delay-master-panel .action-btn:hover {
color: #ffffff;
}
#audio-delay-master-panel .delay-display {
text-align: center;
font-size: 36px;
font-weight: 700;
color: #00ccff;
margin: 12px 0;
letter-spacing: 1px;
font-variant-numeric: tabular-nums;
}
#audio-delay-master-panel .delay-slider {
width: 100%;
margin: 10px 0;
height: 6px;
border-radius: 3px;
background: #333333;
outline: none;
-webkit-appearance: none;
}
#audio-delay-master-panel .delay-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #00ccff;
cursor: pointer;
transition: all 0.2s;
}
#audio-delay-master-panel .delay-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
background: #00eeff;
}
#audio-delay-master-panel .btn-group {
display: flex;
gap: 6px;
margin: 10px 0;
}
#audio-delay-master-panel .control-btn {
flex: 1;
background: #2a2a2a;
border: 1px solid #444444;
color: #ffffff;
padding: 8px 0;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: background 0.2s;
font-weight: 500;
}
#audio-delay-master-panel .control-btn:hover {
background: #404040;
}
#audio-delay-master-panel .control-btn.primary {
background: #0066cc;
border-color: #0088ff;
}
#audio-delay-master-panel .control-btn.danger {
background: #aa2200;
border-color: #ff4400;
}
#audio-delay-master-panel .control-btn.success {
background: #00aa44;
border-color: #00cc55;
}
#audio-delay-master-panel .status-tip {
font-size: 11px;
color: #888888;
text-align: center;
margin-top: 10px;
line-height: 1.5;
}
#audio-delay-master-panel .detection-tip {
font-size: 12px;
color: #ffcc00;
text-align: center;
margin: 10px 0;
padding: 8px;
background: rgba(255, 204, 0, 0.1);
border-radius: 6px;
border: 1px solid rgba(255, 204, 0, 0.3);
min-height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
#audio-delay-master-panel .collapsed-tip {
display: none;
text-align: center;
font-size: 14px;
font-weight: 600;
color: #00ccff;
line-height: 25px;
}
#audio-delay-master-panel.collapsed .panel-content,
#audio-delay-master-panel.collapsed .panel-title {
display: none;
}
#audio-delay-master-panel.collapsed .collapsed-tip {
display: block;
}
`);
// ========== 2. 音频引擎核心初始化 ==========
function initAudioContext() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
globalDelayNode = audioCtx.createDelay(MAX_DELAY);
globalDelayNode.delayTime.value = currentDelay;
globalDelayNode.connect(audioCtx.destination);
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
return { audioCtx, globalDelayNode };
}
// ========== 3. 视频Hook核心逻辑 ==========
function hookVideo(videoElement) {
if (hookedVideos.has(videoElement)) return;
if (!videoElement.crossOrigin) {
videoElement.crossOrigin = 'anonymous';
}
const onVideoPlay = async () => {
try {
const { audioCtx, globalDelayNode } = initAudioContext();
if (hookedVideos.has(videoElement)) return;
const sourceNode = audioCtx.createMediaElementSource(videoElement);
sourceNode.connect(globalDelayNode);
hookedVideos.set(videoElement, {
source: sourceNode,
playListener: onVideoPlay
});
updateStatusTip(`已连接 ${hookedVideos.size} 个视频`);
} catch (error) {
console.warn('音频延迟Hook失败:', error);
updateStatusTip('视频连接失败,尝试刷新页面重试');
}
};
videoElement.addEventListener('play', onVideoPlay, { once: false });
videoElement.addEventListener('ended', () => unhookVideo(videoElement));
videoElement.addEventListener('remove', () => unhookVideo(videoElement));
}
function unhookVideo(videoElement) {
const data = hookedVideos.get(videoElement);
if (data) {
data.source.disconnect();
videoElement.removeEventListener('play', data.playListener);
hookedVideos.delete(videoElement);
}
}
// ========== 4. 延迟更新核心函数 ==========
function updateDelay(newDelay) {
newDelay = Math.max(0, Math.min(MAX_DELAY, newDelay));
currentDelay = newDelay;
document.getElementById('delay-value-display').textContent = `${currentDelay.toFixed(3)}s`;
document.getElementById('delay-slider').value = currentDelay;
if (globalDelayNode) {
globalDelayNode.delayTime.value = currentDelay;
}
GM_setValue('audio_delay_sec', currentDelay);
initAudioContext();
}
// ========== 5. 智能检测延迟功能 ==========
function startDetection() {
detectionState = 'waiting_sound';
soundTime = 0;
videoTime = 0;
updateDetectionTip('请播放视频,找到一个明显的同步点(如拍手、枪响)
当听到声音时,点击下方「标记声音」');
document.getElementById('btn-detect-sound').style.display = 'block';
document.getElementById('btn-detect-video').style.display = 'none';
document.getElementById('btn-detect-apply').style.display = 'none';
document.getElementById('btn-detect-start').style.display = 'none';
}
function markSound() {
soundTime = performance.now();
detectionState = 'waiting_video';
updateDetectionTip('好的!现在当你看到对应画面动作时,点击「标记画面」');
document.getElementById('btn-detect-sound').style.display = 'none';
document.getElementById('btn-detect-video').style.display = 'block';
}
function markVideo() {
videoTime = performance.now();
detectionState = 'idle';
// 计算延迟:画面时间 - 声音时间 = 需要的音频延迟
const calculatedDelay = (videoTime - soundTime) / 1000;
if (calculatedDelay > 0) {
updateDetectionTip(`检测完成!
计算出的延迟值:${calculatedDelay.toFixed(3)}秒
点击「应用延迟」使用此值`);
document.getElementById('btn-detect-video').style.display = 'none';
document.getElementById('btn-detect-apply').style.display = 'block';
document.getElementById('btn-detect-apply').dataset.delay = calculatedDelay;
} else {
updateDetectionTip(`检测结果异常(延迟值为负)
请确保先听到声音,再看到画面
点击「重新检测」再试一次`);
document.getElementById('btn-detect-video').style.display = 'none';
document.getElementById('btn-detect-start').textContent = '重新检测';
document.getElementById('btn-detect-start').style.display = 'block';
}
}
function applyCalculatedDelay() {
const delay = parseFloat(document.getElementById('btn-detect-apply').dataset.delay);
updateDelay(delay);
updateDetectionTip('延迟已应用!你可以继续微调以获得最佳效果');
document.getElementById('btn-detect-apply').style.display = 'none';
document.getElementById('btn-detect-start').textContent = '重新检测';
document.getElementById('btn-detect-start').style.display = 'block';
}
function updateDetectionTip(html) {
const tipEl = document.getElementById('detection-tip');
if (tipEl) tipEl.innerHTML = html;
}
// ========== 6. UI构建与交互 ==========
function buildPanel() {
if (document.getElementById('audio-delay-master-panel')) return;
const panel = document.createElement('div');
panel.id = 'audio-delay-master-panel';
panel.innerHTML = `