Xbai 发表于 2026-2-25 13:45:55

如何精准捕获播放的<video>元素?

本帖最后由 Xbai 于 2026-2-25 14:05 编辑

最近想用AI帮我写一个符合自己使用习惯的视频控制脚本,功能很好实现,就是这个核心功能"精准捕获播放的那个视频”一直不满意,怎么才能完美排除其他视频(比如悬停预览、首页轮播、直播之类的),只捕获播放的那个视频用于控制呢?

Xbai 发表于 2026-2-25 13:49:07

最近想用AI帮我写一个符合自己使用习惯的视频控制脚本,功能很好实现,就是这个核心功能"精准捕获播放的那个视频”一直不满意,怎么才能完美排除其他视频(比如悬停预览、首页轮播、直播之类的),只捕获播放的那个视频用于控制呢?

cyfung1031 发表于 2026-2-25 14:36:25

本帖最后由 cyfung1031 于 2026-2-25 14:37 编辑

这个很难
要结合大量不同领域的技术
video 本身的画面大小
video 有没有声音
video 是不是被静音
video 是不是被用户点击过
video 有没有触发 IntersectionObserver
video 的大小占了画面多大比例
游标停留在video的时间长度
video 有没有相应的控制项
video 的ancestor 有没有 absolute 广告层

然后结合所有做一个统计评分
最高分那一个 + 基本条件符合 就是你要的那一个

---

找一些現有的改腳本吧
例如 https://greasyfork.org/zh-CN/scripts/4870-maximize-video

Xbai 发表于 2026-3-1 19:15:41

cyfung1031 发表于 2026-2-25 14:36
这个很难
要结合大量不同领域的技术
video 本身的画面大小


好吧🫡

卢梭 发表于 2026-3-1 21:30:29

这是油猴脚本开发里一个很经典的难题,核心矛盾在于"播放中"这个状态太容易误判。给你系统梳理一套可靠的筛选策略:

---

## 核心思路:多条件评分,而不是单一判断

不要试图用一个条件找到"那个视频",而是给每个 `<video>` 元素打分,分最高的就是目标。

---

## 第一层:基础状态过滤(必须满足)

```javascript
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;
});
}
```

---

## 第二层:排除"伪装者"的关键特征

### 排除悬停预览视频
预览视频的典型特征:
```javascript
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('')?.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;
}
```

### 排除首页轮播/广告
```javascript
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;
}
```

### 排除直播流(如果你不需要控制直播)
```javascript
function isLiveStream(v) {
// 直播的 duration 是 Infinity
if (v.duration === Infinity) return true;
// 或检查 URL 特征
if (/\.m3u8|\/live\//i.test(v.currentSrc)) return true;
return false;
}
```

---

## 第三层:打分系统(核心)

```javascript
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;
}
```

---

## 完整的捕获函数

```javascript
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;

// 多个候选时打分
return candidates.sort((a, b) => scoreVideo(b) - scoreVideo(a));
}
```

---

## 终极补充:用户交互信号

如果上面还不够准,加上**用户行为监听**是最可靠的方案:

```javascript
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% 的情况,单靠任何一个条件都容易翻车。

卢梭 发表于 2026-3-1 21:30:54

这是油猴脚本开发里一个很经典的难题,核心矛盾在于"播放中"这个状态太容易误判。给你系统梳理一套可靠的筛选策略:

---

## 核心思路:多条件评分,而不是单一判断

不要试图用一个条件找到"那个视频",而是给每个 `<video>` 元素打分,分最高的就是目标。

---

## 第一层:基础状态过滤(必须满足)

```javascript
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;
});
}
```

---

## 第二层:排除"伪装者"的关键特征

### 排除悬停预览视频
预览视频的典型特征:
```javascript
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('')?.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;
}
```

### 排除首页轮播/广告
```javascript
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;
}
```

### 排除直播流(如果你不需要控制直播)
```javascript
function isLiveStream(v) {
// 直播的 duration 是 Infinity
if (v.duration === Infinity) return true;
// 或检查 URL 特征
if (/\.m3u8|\/live\//i.test(v.currentSrc)) return true;
return false;
}
```

---

## 第三层:打分系统(核心)

```javascript
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;
}
```

---

## 完整的捕获函数

```javascript
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;

// 多个候选时打分
return candidates.sort((a, b) => scoreVideo(b) - scoreVideo(a));
}
```

---

## 终极补充:用户交互信号

如果上面还不够准,加上**用户行为监听**是最可靠的方案:

```javascript
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% 的情况,单靠任何一个条件都容易翻车。
页: [1]
查看完整版本: 如何精准捕获播放的<video>元素?