这是油猴脚本开发里一个很经典的难题,核心矛盾在于"播放中"这个状态太容易误判。给你系统梳理一套可靠的筛选策略:
核心思路:多条件评分,而不是单一判断
不要试图用一个条件找到"那个视频",而是给每个 <video> 元素打分,分最高的就是目标。
第一层:基础状态过滤(必须满足)
function getCandidates() {
return [...document.querySelectorAll('video')].filter(v => {
if (v.readyState < 2) return false; // 还没加载到可播放
if (v.error) return false; // 出错的排除
if (!v.src && !v.currentSrc) return false; // 没有源的排除
return true;
});
}
第二层:排除"伪装者"的关键特征
排除悬停预览视频
预览视频的典型特征:
function isPreviewVideo(v) {
// 预览通常静音、且 duration 很短或是循环小片段
if (v.muted && v.loop && v.duration < 15) return true;
// 检查 CSS 类名/父元素关键词
const keywords = ['preview', 'hover', 'thumb', 'thumbnail', 'mute'];
const context = (v.className + v.id + (v.closest('[class]')?.className || '')).toLowerCase();
if (keywords.some(k => context.includes(k))) return true;
// 预览视频通常尺寸很小
const rect = v.getBoundingClientRect();
if (rect.width < 200 || rect.height < 120) return true;
return false;
}
排除首页轮播/广告
function isBannerVideo(v) {
// 轮播通常:自动播放 + 静音 + 没有 controls
if (v.autoplay && v.muted && !v.controls) return true;
// 检查是否在广告/banner容器内
const adKeywords = ['ad', 'banner', 'carousel', 'swiper', 'hero'];
let el = v.parentElement;
for (let i = 0; i < 5; i++) { // 往上找5层
if (!el) break;
const ctx = (el.className + el.id).toLowerCase();
if (adKeywords.some(k => ctx.includes(k))) return true;
el = el.parentElement;
}
return false;
}
排除直播流(如果你不需要控制直播)
function isLiveStream(v) {
// 直播的 duration 是 Infinity
if (v.duration === Infinity) return true;
// 或检查 URL 特征
if (/\.m3u8|\/live\//i.test(v.currentSrc)) return true;
return false;
}
第三层:打分系统(核心)
function scoreVideo(v) {
let score = 0;
const rect = v.getBoundingClientRect();
const viewH = window.innerHeight;
const viewW = window.innerWidth;
// ✅ 正在播放(最重要)
if (!v.paused && !v.ended) score += 100;
// ✅ 有声音(用户主动看的视频通常不静音)
if (!v.muted && v.volume > 0) score += 50;
// ✅ 有进度(说明用户真的在看)
if (v.currentTime > 3) score += 30;
// ✅ 尺寸大(主视频通常占据大面积)
const area = rect.width * rect.height;
score += Math.min(area / 1000, 60); // 最多加60分
// ✅ 在视口内且居中
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const distFromCenter = Math.hypot(centerX - viewW/2, centerY - viewH/2);
score += Math.max(0, 40 - distFromCenter / 20);
// ✅ 有 controls 属性(主播放器通常有)
if (v.controls) score += 20;
// ❌ 静音扣分
if (v.muted) score -= 30;
// ❌ 在视口外扣分(用户看不到的)
if (rect.bottom < 0 || rect.top > viewH) score -= 80;
return score;
}
完整的捕获函数
function getTargetVideo() {
const candidates = getCandidates()
.filter(v => !isPreviewVideo(v))
.filter(v => !isBannerVideo(v));
// 如果不需要排直播就注释掉下一行
// .filter(v => !isLiveStream(v));
if (candidates.length === 0) return null;
if (candidates.length === 1) return candidates[0];
// 多个候选时打分
return candidates.sort((a, b) => scoreVideo(b) - scoreVideo(a))[0];
}
终极补充:用户交互信号
如果上面还不够准,加上用户行为监听是最可靠的方案:
let userPickedVideo = null;
// 用户点击过的视频,直接记为目标
document.addEventListener('click', e => {
const v = e.target.closest('video') || e.composedPath().find(el => el.tagName === 'VIDEO');
if (v) userPickedVideo = v;
}, true);
function getTargetVideo() {
// 优先用用户点击过的(且还在播放的)
if (userPickedVideo && !userPickedVideo.paused) return userPickedVideo;
// 否则走评分逻辑
return getScoredVideo();
}
总结
| 场景 |
主要克制手段 |
| 悬停预览 |
尺寸小 + muted+loop+短时长 + 类名关键词 |
| 首页轮播 |
autoplay+muted+无controls + 父元素关键词 |
| 直播 |
duration === Infinity |
| 多视频并存 |
打分系统(有声 > 尺寸大 > 居中 > 有进度) |
| 兜底 |
监听用户点击,直接锁定目标 |
实际写脚本时,打分 + 用户点击监听组合基本能覆盖 99% 的情况,单靠任何一个条件都容易翻车。