// ==UserScript==
// @name 全网视频增强助手 - 多平台去广告·倍速·净化
// @namespace https://github.com/video-enhancer
// @version 2.6.0
// @description 支持 iflix / iQIYI海外版(iq.com) / 优酷海外版(youku.tv) / 优酷国内(youku.com) / 1905电影网 / PPTV(PP视频) / 土豆 / 哔哩哔哩 / 西瓜视频 去广告、倍速突破、界面净化、播放增强
// @author VideoEnhancer
// @icon https://cdn-icons-png.flaticon.com/512/1384/1384060.png
// @match *://www.iflix.com/*
// @match *://*.iflix.com/*
// @match *://www.iq.com/*
// @match *://*.iq.com/*
// @match *://www.youku.tv/*
// @match *://*.youku.tv/*
// @match *://www.youku.com/*
// @match *://*.youku.com/*
// @match *://www.1905.com/*
// @match *://*.1905.com/*
// @match *://www.pptv.com/*
// @match *://*.pptv.com/*
// @match *://v.pptv.com/*
// @match *://www.tudou.com/*
// @match *://*.tudou.com/*
// @match *://www.bilibili.com/*
// @match *://*.bilibili.com/*
// @match *://www.ixigua.com/*
// @match *://*.ixigua.com/*
// @match *://www.ixigua.net/*
// @match *://*.ixigua.net/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_notification
// @grant GM_setClipboard
// @grant GM_download
// @grant GM_xmlhttpRequest
// @connect *
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ============================================================
// 全局配置
// ============================================================
const CONFIG = {
version: '2.6.0',
debug: GM_getValue('debug', false),
// 广告屏蔽规则
blockSelectors: {
// 通用广告选择器
global: [
'[id*="ad-"]', '[id*="advert"]', '[id*="guanggao"]',
'[class*="ad-wrap"]', '[class*="ad-banner"]', '[class*="ad-slot"]',
'[class*="advert"]', '[class*="guanggao"]', '[class*="sidebar-ad"]',
'[class*="popup-ad"]', '[class*="float-ad"]', '[class*="overlay-ad"]',
'[class*="banner-ad"]', '[class*="video-ad"]',
'iframe[src*="ad"]', 'iframe[src*="doubleclick"]',
'div[data-ad]', 'div[aria-label="广告"]',
'.ad-container', '.ad-player', '.ad-layer',
'.iqp-player-videoad', '.iqp-ad', '.skippable-after',
],
// 爱奇艺海外版 (iq.com)
iq: [
'.iqp-banner', '.iqp-side-ad', '.m-iqp-ad',
'[class*="iqp"][class*="ad"]',
'.twelve-adv', '.mod-ad', '.m-slider-ad',
'.iqp-pause-ad',
],
// iflix
iflix: [
'.ad-overlay', '[class*="preroll"]', '[class*="midroll"]',
'[class*="postroll"]', '[class*="sponsor"]',
'[data-testid*="ad"]',
],
// 优酷
youku: [
'.youku-layer-logo', // 优酷logo水印
'#module-basic-advert', '.adv-area', '.pause-adv',
'.control-advert', '.corner-mark', '.yk-ad',
'[class*="advert-"]', '.recommend-ad',
],
// 1905电影网
moive1905: [
'.adWrap', '.ad-box', '#ad_', '.side-ad',
'.top-ad', '.banner-ad',
],
// PPTV / PP视频
pptv: [
'.pp-ad', '.ad-wrap', '.ad-box', '.ad-layer',
'.pp-player-ad', '.suspend-ad',
'[id*="ad"]', '[class*="ad-"]',
],
// 土豆
tudou: [
'.td-ad', '.advert', '.ad-module',
'.sidebar-promo',
],
// 哔哩哔哩
bili: [
'.ad-report', '.bili-ad', '#slide_ad',
'.video-page-game-card', '.rec-section',
'.pop-live', '[class*="ad-"]',
'.bili-dyn-ads', '.video-card-ad',
'.activity-m', '.vcd', // 视频卡片广告
'.bangumi-pgc-video-card', // 大会员推广
],
// 西瓜视频
xigua: [
'.ad-container', '[class*="xigua-ad"]',
'.video-feed-ad', '.inline-ad',
],
},
// 倍速控制
speedOptions: [0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0],
defaultSpeed: GM_getValue('defaultSpeed', 1.0),
// 自动跳过片头片尾 (秒)
skipIntro: GM_getValue('skipIntro', 0),
skipOutro: GM_getValue('skipOutro', 0),
// 界面净化
removePopup: GM_getValue('removePopup', true), // 去除弹窗
removeFloat: GM_getValue('removeFloat', true), // 去除悬浮元素
removeSidebar: GM_getValue('removeSidebar', false), // 去除侧边栏
autoFullscreen: GM_getValue('autoFullscreen', false),
autoPlay: GM_getValue('autoPlay', true),
};
// ============================================================
// 工具函数
// ============================================================
const Utils = {
log(...args) {
if (CONFIG.debug) {
console.log(`[视频增强 v${CONFIG.version}]`, ...args);
}
},
info(...args) {
console.log(`%c[视频增强 v${CONFIG.version}]%c ${args.join(' ')}`,
'background:#f60;color:#fff;border-radius:3px;padding:0 4px', 'color:#f60');
},
currentSite() {
const h = location.hostname;
const map = {
'iflix': /iflix\.com/,
'iq': /iq\.com/,
'youku': /youku\.(com|tv)/,
'movie1905': /1905\.com/,
'pptv': /pptv\.com/,
'tudou': /tudou\.com/,
'bili': /bilibili\.com/,
'xigua': /ixigua\.(com|net)/,
};
for (const [site, re] of Object.entries(map)) {
if (re.test(h)) return site;
}
return 'unknown';
},
// 安全移除元素
remove(el) {
if (!el) return;
try { el.remove(); } catch { try { el.parentNode.removeChild(el); } catch {} }
},
// 隐藏元素
hide(el) {
if (!el) return;
el.style.setProperty('display', 'none', 'important');
el.style.setProperty('visibility', 'hidden', 'important');
el.style.setProperty('height', '0', 'important');
el.style.setProperty('overflow', 'hidden', 'important');
el.style.setProperty('pointer-events', 'none', 'important');
},
// 观察DOM变化
observe(target, callback, opts = {}) {
if (!target) return;
const observer = new MutationObserver(callback);
observer.observe(target, {
childList: true,
subtree: true,
...opts,
});
return observer;
},
// 等待元素出现
waitFor(selector, timeout = 10000, parent = document) {
return new Promise((resolve, reject) => {
const el = parent.querySelector(selector);
if (el) return resolve(el);
const observer = new MutationObserver(() => {
const el = parent.querySelector(selector);
if (el) { observer.disconnect(); resolve(el); }
});
observer.observe(parent, { childList: true, subtree: true });
setTimeout(() => { observer.disconnect(); reject(new Error(`超时: ${selector}`)); }, timeout);
});
},
// 防抖
debounce(fn, delay = 300) {
let timer;
return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); };
},
};
// ============================================================
// 模块一:广告拦截
// ============================================================
const AdBlocker = {
removedCount: 0,
init() {
const site = Utils.currentSite();
this.removeStaticAds(site);
this.observeDynamicAds(site);
this.interceptRequests(site);
Utils.info(`广告拦截已启用 [${site}]`);
},
// 移除静态广告
removeStaticAds(site) {
const selectors = [
...CONFIG.blockSelectors.global,
...(CONFIG.blockSelectors[site] || []),
];
selectors.forEach(sel => {
try {
document.querySelectorAll(sel).forEach(el => {
// 安全检查:避免误删播放器本身
if (this.isPlayerElement(el)) return;
Utils.hide(el);
this.removedCount++;
});
} catch {}
});
},
// 动态广告监控
observeDynamicAds(site) {
const debouncedClean = Utils.debounce(() => {
const selectors = [
...CONFIG.blockSelectors.global,
...(CONFIG.blockSelectors[site] || []),
];
let count = 0;
selectors.forEach(sel => {
try {
document.querySelectorAll(sel).forEach(el => {
if (this.isPlayerElement(el)) return;
if (el.offsetParent !== null || el.style.display !== 'none') {
Utils.hide(el);
count++;
}
});
} catch {}
});
if (count > 0) {
this.removedCount += count;
Utils.log(`动态清理 ${count} 个广告元素`);
}
}, 500);
Utils.observe(document.body, debouncedClean);
},
// 拦截广告请求
interceptRequests(site) {
// 拦截广告相关的脚本和资源加载
const adPatterns = [
/doubleclick\.net/, /googlesyndication\.com/,
/ad\.iqiyi\.com/, /ad\.youku\.com/,
/ad\.bilibili\.com/, /cm\.bilibili\.com/,
/static\.ad\.pptv\.com/,
/tanx\.com/, /mm\.taobao\.com/,
/adnxs\.com/, /adsrvr\.org/,
/analytics\./, /tracker\./,
/collect\./, /log\./,
/beacon\./,
];
const origOpen = XMLHttpRequest.prototype.open;
const origFetch = window.fetch;
// 拦截 XHR
XMLHttpRequest.prototype.open = function (method, url, ...args) {
if (adPatterns.some(p => p.test(url))) {
Utils.log(`拦截广告请求: ${url}`);
return; // 不执行请求
}
return origOpen.call(this, method, url, ...args);
};
// 拦截 Fetch
window.fetch = function (url, options) {
const urlStr = typeof url === 'string' ? url : (url && url.url) || '';
if (adPatterns.some(p => p.test(urlStr))) {
Utils.log(`拦截广告Fetch: ${urlStr}`);
return Promise.resolve(new Response('', { status: 200 }));
}
return origFetch.call(this, url, options);
};
// 注入 CSS 隐藏策略
const hideCSS = adPatterns.map(p => {
try { return p.source; } catch { return ''; }
}).join(',');
if (hideCSS) {
GM_addStyle(`
/* 全局广告屏蔽 */
[id*="ad-"], [id*="advert"], [id*="guanggao"],
[class*="ad-wrap"], [class*="ad-banner"], [class*="ad-slot"],
[class*="advert"], [class*="guanggao"], [class*="sidebar-ad"],
[class*="popup-ad"], [class*="float-ad"], [class*="overlay-ad"],
.ad-container, .ad-player, .ad-layer,
.iqp-player-videoad, .iqp-ad, .skippable-after,
.twelve-adv, .mod-ad, .pause-adv,
.ad-overlay, [class*="preroll"], [class*="midroll"],
#module-basic-advert, .adv-area, .pause-adv, .control-advert,
.yk-ad, .recommend-ad,
.adWrap, .ad-box, .side-ad, .top-ad,
.pp-ad, .ad-wrap, .ad-box, .ad-layer, .pp-player-ad,
.td-ad, .advert, .ad-module,
.ad-report, .bili-ad, #slide_ad, .video-page-game-card,
.rec-section, .pop-live, .vcd, .bili-dyn-ads, .video-card-ad,
.activity-m, .bangumi-pgc-video-card,
.video-feed-ad, .inline-ad,
[data-ad], div[aria-label="广告"], [data-testid*="ad"] {
display: none !important;
visibility: hidden !important;
height: 0 !important;
overflow: hidden !important;
pointer-events: none !important;
position: absolute !important;
z-index: -9999 !important;
}
/* 去除弹窗遮罩 */
[class*="modal"][class*="ad"],
[class*="popup"][class*="ad"],
[class*="dialog"][class*="ad"],
[class*="layer"][class*="ad"] {
display: none !important;
}
`);
}
},
// 判断是否为播放器核心元素 (避免误删)
isPlayerElement(el) {
const tag = el.tagName;
const cls = el.className || '';
const id = el.id || '';
// video/audio 标签绝对不能删
if (tag === 'VIDEO' || tag === 'AUDIO' || tag === 'SOURCE') return true;
// 播放器容器不能删
if (/player|video-box|player-container|xgplayer|bilibili-player/i.test(cls + id)) {
// 但如果class同时含有ad,则可能是广告层
if (/ad/i.test(cls + id)) return false;
return true;
}
return false;
},
};
// ============================================================
// 模块二:视频播放增强
// ============================================================
const VideoEnhancer = {
currentVideo: null,
init() {
this.findVideo();
this.observeVideo();
Utils.info('视频增强已启用');
},
findVideo() {
const video = document.querySelector('video');
if (video && video !== this.currentVideo) {
this.currentVideo = video;
this.applyEnhancements(video);
}
},
observeVideo() {
// 持续寻找 video 元素
const findVideo = Utils.debounce(() => this.findVideo(), 1000);
Utils.observe(document.body, findVideo);
// 定时检查 (某些SPA页面)
setInterval(findVideo, 3000);
},
applyEnhancements(video) {
if (!video) return;
Utils.log('找到视频播放器,正在应用增强...');
// 倍速设置
if (CONFIG.defaultSpeed > 1.0) {
video.playbackRate = CONFIG.defaultSpeed;
}
// 自动播放
if (CONFIG.autoPlay) {
video.play().catch(() => {});
}
// 去除播放限制
this.removePlaybackRestrictions(video);
// 创建控制面板
this.createControlPanel(video);
// 自动跳过片头
this.setupSkipIntro(video);
// 监听广告插入
this.monitorAdInsertion(video);
},
removePlaybackRestrictions(video) {
// 去除禁止快进
const origPlay = HTMLMediaElement.prototype.play;
video.play = function () {
return origPlay.call(this).catch(() => {});
};
// 允许任意倍速
try {
Object.defineProperty(video, 'playbackRate', {
get() { return this._playbackRate || 1; },
set(v) {
this._playbackRate = v;
// 绕过某些平台的倍速检查
try { this._playbackRate = v; } catch {}
},
configurable: true,
});
} catch {}
// 禁止暂停广告自动恢复播放
video.addEventListener('pause', (e) => {
setTimeout(() => {
// 如果是因为广告导致的暂停,3秒后尝试恢复
if (video.paused && !document.hidden) {
const overlay = document.querySelector(
'[class*="ad"][class*="playing"], [class*="ad"][class*="pause"]'
);
if (!overlay) {
// video.play().catch(() => {});
}
}
}, 3000);
});
},
// 监控广告插入并自动跳过
monitorAdInsertion(video) {
let adDetected = false;
// 检测广告:通常广告视频时长很短(<30秒)且不可跳过
const checkAd = () => {
if (!video.duration || video.duration > 60) {
if (adDetected) {
adDetected = false;
Utils.log('广告结束,恢复正常播放');
}
return;
}
// 检测可能的广告
const indicators = [
document.querySelector('[class*="ad-skip"]'),
document.querySelector('[class*="skip-btn"]'),
document.querySelector('[class*="countdown"]'),
document.querySelector('[class*="ad-time"]'),
];
const adOverlay = document.querySelector(
'[class*="ad-layer"], [class*="video-ad"], [class*="player-ad"]'
);
if (adOverlay || (video.duration < 30 && video.currentTime < video.duration)) {
if (!adDetected) {
adDetected = true;
Utils.log('检测到广告,尝试跳过...');
this.trySkipAd(video);
}
}
};
video.addEventListener('timeupdate', Utils.debounce(checkAd, 1000));
video.addEventListener('loadedmetadata', checkAd);
},
trySkipAd(video) {
// 方法1:寻找跳过按钮并点击
const skipSelectors = [
'[class*="skip"]', '[class*="Skip"]',
'button[class*="close"]', '[class*="ad-close"]',
'[class*="ad-skip-btn"]', '[class*="skipAd"]',
'.iqp-player-videoad-close',
'.skippable-after',
'[class*="countdown"]', // 倒计时结束后的跳过
];
const clickSkip = () => {
skipSelectors.forEach(sel => {
document.querySelectorAll(sel).forEach(btn => {
if (btn.offsetParent !== null) {
btn.click();
Utils.log('点击跳过按钮:', sel);
}
});
});
};
// 立即尝试
clickSkip();
// 持续尝试
const interval = setInterval(clickSkip, 500);
setTimeout(() => clearInterval(interval), 15000); // 最多尝试15秒
},
setupSkipIntro(video) {
if (CONFIG.skipIntro <= 0) return;
video.addEventListener('timeupdate', () => {
if (video.currentTime >= CONFIG.skipIntro && video.currentTime < CONFIG.skipIntro + 1) {
video.currentTime = CONFIG.skipIntro;
Utils.info(`已跳过片头 ${CONFIG.skipIntro}s`);
}
});
},
createControlPanel(video) {
if (document.getElementById('ve-control-panel')) return;
const panel = document.createElement('div');
panel.id = 've-control-panel';
panel.innerHTML = `
`;
document.body.appendChild(panel);
this.injectPanelCSS();
this.bindPanelEvents(panel, video);
this.makePanelDraggable(panel);
},
makePanelDraggable(panel) {
const header = panel.querySelector('.ve-panel-header');
let isDragging = false, startX, startY, origX, origY;
header.addEventListener('mousedown', (e) => {
if (e.target.closest('.ve-toggle')) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = panel.getBoundingClientRect();
origX = rect.left;
origY = rect.top;
panel.style.transition = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
panel.style.left = (origX + e.clientX - startX) + 'px';
panel.style.top = (origY + e.clientY - startY) + 'px';
panel.style.right = 'auto';
});
document.addEventListener('mouseup', () => { isDragging = false; });
// 收起/展开
const toggle = panel.querySelector('.ve-toggle');
toggle.addEventListener('click', () => {
const body = panel.querySelector('.ve-panel-body');
const isHidden = body.style.display === 'none';
body.style.display = isHidden ? 'block' : 'none';
toggle.textContent = isHidden ? '−' : '+';
});
},
injectPanelCSS() {
GM_addStyle(`
#ve-control-panel {
position: fixed;
top: 20px;
right: 20px;
z-index: 2147483647;
background: rgba(20, 20, 30, 0.92);
color: #e0e0e0;
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0,0,0,0.5);
font-family: -apple-system, "Microsoft YaHei", sans-serif;
font-size: 13px;
min-width: 280px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.1);
user-select: none;
transition: opacity 0.3s;
}
#ve-control-panel:hover { opacity: 1 !important; }
.ve-panel-header {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: move;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.ve-title { font-weight: 600; font-size: 14px; color: #fff; }
.ve-site { margin-left: 8px; color: #888; font-size: 11px; }
.ve-toggle {
margin-left: auto;
background: none; border: none;
color: #aaa; font-size: 18px;
cursor: pointer; padding: 0 4px;
}
.ve-toggle:hover { color: #fff; }
.ve-panel-body { padding: 8px 12px; }
.ve-section {
display: flex; align-items: center;
margin: 6px 0; gap: 6px;
}
.ve-section-compact { flex-wrap: wrap; }
.ve-section label { color: #bbb; font-size: 12px; white-space: nowrap; }
.ve-section select, .ve-section input[type="number"] {
background: rgba(255,255,255,0.1); color: #fff;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 4px; padding: 2px 6px;
font-size: 12px; outline: none;
}
.ve-section select:focus, .ve-section input:focus {
border-color: #4fc3f7;
}
.ve-speed-slider { flex: 1; accent-color: #4fc3f7; height: 3px; }
.ve-speed-display, .ve-volume-display {
font-size: 11px; color: #4fc3f7; min-width: 36px; text-align: right;
}
.ve-volume-boost { flex: 1; accent-color: #ff9800; height: 3px; }
.ve-pip {
background: rgba(79, 195, 247, 0.2);
border: 1px solid #4fc3f7; color: #4fc3f7;
border-radius: 4px; padding: 2px 10px; font-size: 12px;
cursor: pointer; outline: none;
}
.ve-pip:hover { background: rgba(79, 195, 247, 0.3); }
.ve-screenshot, .ve-download, .ve-copy-url {
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.15);
color: #ccc; border-radius: 4px;
padding: 4px 8px; font-size: 12px;
cursor: pointer; outline: none;
}
.ve-screenshot:hover, .ve-download:hover, .ve-copy-url:hover {
background: rgba(255,255,255,0.15); color: #fff;
}
.ve-section input[type="checkbox"] {
accent-color: #4fc3f7;
}
/* 通知Toast */
.ve-toast {
position: fixed; top: 50%; left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.85); color: #fff;
padding: 12px 24px; border-radius: 8px;
font-size: 14px; z-index: 2147483647;
animation: ve-fadeIn 0.3s, ve-fadeOut 0.3s 1.7s forwards;
pointer-events: none;
}
@keyframes ve-fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes ve-fadeOut { from { opacity: 1; } to { opacity: 0; } }
/* 视频下载列表 */
.ve-download-area {
margin-top: 8px;
border-top: 1px solid rgba(255,255,255,0.1);
padding-top: 8px;
max-height: 300px;
overflow-y: auto;
}
.ve-download-area::-webkit-scrollbar { width: 4px; }
.ve-download-area::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 2px; }
.ve-dl-title { color: #4fc3f7; font-weight: 600; font-size: 12px; }
.ve-url-list { display: flex; flex-direction: column; gap: 6px; }
.ve-url-row {
background: rgba(255,255,255,0.05);
border-radius: 6px;
padding: 6px 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
.ve-url-info { display: flex; align-items: baseline; gap: 6px; }
.ve-url-label { color: #fff; font-size: 12px; font-weight: 500; }
.ve-url-meta { color: #888; font-size: 10px; }
.ve-url-actions { display: flex; gap: 4px; flex-wrap: wrap; }
.ve-url-actions button {
background: rgba(79,195,247,0.15);
border: 1px solid rgba(79,195,247,0.3);
color: #4fc3f7; border-radius: 3px;
padding: 2px 6px; font-size: 11px;
cursor: pointer; outline: none;
}
.ve-url-actions button:hover { background: rgba(79,195,247,0.3); }
.ve-url-actions .ve-btn-dl-m3u8 {
background: rgba(255,152,0,0.15);
border-color: rgba(255,152,0,0.3);
color: #ff9800;
}
.ve-url-actions .ve-btn-dl-m3u8:hover { background: rgba(255,152,0,0.3); }
`);
},
bindPanelEvents(panel, video) {
// 倍速控制
const speedSelect = panel.querySelector('.ve-speed');
const speedSlider = panel.querySelector('.ve-speed-slider');
const speedDisplay = panel.querySelector('.ve-speed-display');
const setSpeed = (val) => {
const speed = parseFloat(val);
video.playbackRate = speed;
speedDisplay.textContent = speed + 'x';
GM_setValue('defaultSpeed', speed);
};
speedSelect.addEventListener('change', (e) => {
setSpeed(e.target.value);
speedSlider.value = e.target.value;
});
speedSlider.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
video.playbackRate = val;
speedDisplay.textContent = val + 'x';
});
speedSlider.addEventListener('change', (e) => {
GM_setValue('defaultSpeed', parseFloat(e.target.value));
});
// 画中画
panel.querySelector('.ve-pip').addEventListener('click', () => {
if (document.pictureInPictureElement) {
document.exitPictureInPicture().catch(() => {});
panel.querySelector('.ve-pip').textContent = '开启';
} else if (video.requestPictureInPicture) {
video.requestPictureInPicture().then(() => {
panel.querySelector('.ve-pip').textContent = '关闭';
}).catch(() => {
this.showToast('画中画不可用');
});
}
});
// 音量增益
const volumeSlider = panel.querySelector('.ve-volume-boost');
const volumeDisplay = panel.querySelector('.ve-volume-display');
volumeSlider.addEventListener('input', (e) => {
const vol = parseInt(e.target.value);
video.volume = Math.min(vol / 100, 1);
volumeDisplay.textContent = vol + '%';
// 使用Web Audio API实现音量增益
this.applyVolumeBoost(video, vol / 100);
});
// 自动播放
panel.querySelector('.ve-auto-play').addEventListener('change', (e) => {
GM_setValue('autoPlay', e.target.checked);
CONFIG.autoPlay = e.target.checked;
});
// 自动全屏
panel.querySelector('.ve-auto-fs').addEventListener('change', (e) => {
GM_setValue('autoFullscreen', e.target.checked);
CONFIG.autoFullscreen = e.target.checked;
if (e.target.checked) {
this.enterFullscreen(video);
}
});
// 跳过片头片尾
panel.querySelector('.ve-skip-intro').addEventListener('change', (e) => {
CONFIG.skipIntro = parseInt(e.target.value) || 0;
GM_setValue('skipIntro', CONFIG.skipIntro);
});
panel.querySelector('.ve-skip-outro').addEventListener('change', (e) => {
CONFIG.skipOutro = parseInt(e.target.value) || 0;
GM_setValue('skipOutro', CONFIG.skipOutro);
if (CONFIG.skipOutro > 0) {
this.setupSkipOutro(video);
}
});
// 截图
panel.querySelector('.ve-screenshot').addEventListener('click', () => {
this.takeScreenshot(video);
});
// 检测视频源 & 真正下载
panel.querySelector('.ve-detect-urls').addEventListener('click', () => {
VideoDownloader.detect(video, panel);
});
// 复制地址
panel.querySelector('.ve-copy-url').addEventListener('click', () => {
GM_setClipboard(location.href, 'text');
this.showToast('已复制当前页面地址');
});
// 双击视频全屏
video.addEventListener('dblclick', () => {
this.enterFullscreen(video);
});
},
applyVolumeBoost(video, boost) {
if (boost <= 1) return;
if (!this._audioCtx) {
this._audioCtx = new (window.AudioContext || window.webkitAudioContext)();
this._source = this._audioCtx.createMediaElementSource(video);
this._gain = this._audioCtx.createGain();
this._source.connect(this._gain);
this._gain.connect(this._audioCtx.destination);
}
this._gain.gain.value = boost;
},
enterFullscreen(video) {
const container = video.closest('[class*="player"]') || video.closest('div');
const el = container || video;
try {
(el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen).call(el);
} catch {}
},
setupSkipOutro(video) {
video.addEventListener('timeupdate', () => {
if (CONFIG.skipOutro > 0 && video.duration) {
const remaining = video.duration - video.currentTime;
if (remaining <= CONFIG.skipOutro && remaining > CONFIG.skipOutro - 1) {
Utils.info(`已跳过片尾 ${CONFIG.skipOutro}s`);
video.currentTime = video.duration;
video.pause();
}
}
});
},
takeScreenshot(video) {
try {
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0);
const link = document.createElement('a');
link.download = `screenshot_${Date.now()}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
this.showToast('截图已保存');
} catch (e) {
this.showToast('截图失败(可能有跨域限制)');
}
},
showVideoInfo(video) {
const info = [
`当前地址: ${location.href}`,
`视频时长: ${this.formatTime(video.duration)}`,
`当前时间: ${this.formatTime(video.currentTime)}`,
`播放速率: ${video.playbackRate}x`,
`分辨率: ${video.videoWidth}x${video.videoHeight}`,
];
GM_setClipboard(info.join('\n'), 'text');
this.showToast('视频信息已复制到剪贴板');
},
formatTime(sec) {
if (!sec || isNaN(sec)) return '00:00';
const h = Math.floor(sec / 3600);
const m = Math.floor((sec % 3600) / 60);
const s = Math.floor(sec % 60);
return h > 0
? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
: `${m}:${String(s).padStart(2, '0')}`;
},
showToast(msg) {
const toast = document.createElement('div');
toast.className = 've-toast';
toast.textContent = msg;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2000);
},
};
// ============================================================
// 模块二B:视频源检测与真实下载
// ============================================================
const VideoDownloader = {
capturedURLs: [],
init() {
this.hookMediaRequests();
Utils.info('视频源拦截已启用');
},
// 在 document-start 阶段 hook,拦截所有视频/音频请求
hookMediaRequests() {
const self = this;
const videoExts = /\.(mp4|m3u8|m4s|ts|flv|webm|mkv|avi|mov|mp3|aac|m4a|ogg|wav)(\?|$)/i;
const videoContentTypes = /(video|audio)\/(mp4|mpeg|ogg|webm|x-flv|mp2t|apple-mpegurl|x-mpegURL|octet-stream|quicktime)/i;
const cdnPatterns = [
// B站
/bilivideo\.com/, /cn-[a-z]+-[a-z]\d*\.bilivideo\.com/,
/upos-sz-[a-z]+\.bilivideo\.com/, /akamaihd\.net/,
// 优酷
/youku\.com\/.*\.(mp4|m3u8)/, /kugou\.com/,
/vali\.cpm\.youku\.com/, /a1\.ykimg\.com/,
// 爱奇艺
/iqiyi\.com\/.*\.(mp4|m3u8)/, /iq\.com\/.*\.(mp4|m3u8)/,
/qiyi\.com/, /71\.am\.com/,
// PPTV
/pptv\.com\/.*\.(mp4|m3u8)/, /ppimg\.com/,
// 1905
/1905\.com\/.*\.(mp4|m3u8)/,
// 西瓜
/ixigua\.com\/.*\.(mp4|m3u8)/, /pstatp\.com/,
/snssdk\.com/, /byteimg\.com/,
// iflix
/iflix\.com\/.*\.(mp4|m3u8)/,
// 土豆
/tudou\.com\/.*\.(mp4|m3u8)/,
// 通用CDN
/vod\.|video\.|media\.|stream\.|cdn\./,
/cloudfront\.net/, /akamaized\.net/,
/llnwi\.net/, /vod-cdn\./,
/m3u8/, /\.mp4/, /\.flv/,
];
const isVideoURL = (url) => {
if (!url || typeof url !== 'string') return false;
return videoExts.test(url) || cdnPatterns.some(p => p.test(url));
};
// Hook XMLHttpRequest
const origXHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url, ...args) {
if (isVideoURL(url)) {
self.captureURL(url, 'XHR');
}
return origXHROpen.call(this, method, url, ...args);
};
// Hook Fetch
const origFetch = window.fetch;
window.fetch = function (urlOrReq, options) {
const url = typeof urlOrReq === 'string' ? urlOrReq : (urlOrReq && urlOrReq.url) || '';
if (isVideoURL(url)) {
self.captureURL(url, 'Fetch');
}
return origFetch.call(this, urlOrReq, options);
};
// Hook