Skip to content

Commit f8f935b

Browse files
committed
refactor: 重构媒体嗅探系统为策略模式并优化视频预览
主要改动: - 将嗅探逻辑从 content.ts 重构为独立的策略类(7种策略) - 采用策略模式提升代码可维护性和可扩展性 - 移除视频截图生成封面逻辑,改用 video 标签 preload="metadata" 原生预览 - 删除 thumbnailGenerated 消息监听和后台封面生成 - 新增策略系统文档和 7 个独立策略实现 - 优化 content.ts 代码结构,减少 400+ 行冗余代码 - 更新视频预览样式(video-thumbnail-wrapper → video-preview-wrapper) 策略清单: 1. ImageTag - 扫描图片标签 2. BackgroundImage - 扫描 CSS 背景图 3. VideoTag - 扫描视频标签 4. AudioTag - 扫描音频标签 5. PerformanceAPI - 扫描动态加载资源 6. CustomAttributes - 扫描自定义属性 7. ScriptExtraction - 从脚本提取视频 URL 技术优势: - 策略独立可插拔,易于添加新嗅探方式 - 错误隔离,单个策略失败不影响整体 - 优先级控制,优化执行顺序 - 统一去重机制,避免资源重复 - 使用浏览器原生能力,减少计算和存储开销
1 parent 54ce2bf commit f8f935b

File tree

15 files changed

+776
-428
lines changed

15 files changed

+776
-428
lines changed

extensions/chrome-extension/src/content.ts

Lines changed: 13 additions & 401 deletions
Large diffs are not rendered by default.

extensions/chrome-extension/src/popup.css

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -588,21 +588,20 @@ body {
588588
object-fit: cover;
589589
}
590590

591-
/* 视频封面样式 */
592-
.video-thumbnail-wrapper {
591+
/* 视频预览样式 */
592+
.video-preview-wrapper {
593593
width: 100%;
594594
height: 100%;
595595
position: relative;
596596
}
597597

598-
.video-thumbnail-wrapper img {
598+
.video-preview-wrapper video {
599599
width: 100%;
600600
height: 100%;
601601
object-fit: cover;
602602
}
603603

604-
.video-thumbnail-wrapper::after {
605-
content: '▶';
604+
.video-play-icon {
606605
position: absolute;
607606
top: 50%;
608607
left: 50%;

extensions/chrome-extension/src/popup.tsx

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -80,21 +80,7 @@ const Popup: React.FC = () => {
8080
}
8181
}
8282

83-
// 监听后台生成的封面更新
84-
const handleThumbnailUpdate = (message: any) => {
85-
if (message.action === 'thumbnailGenerated') {
86-
setSniffedResources((prevResources) =>
87-
prevResources.map((resource) =>
88-
resource.url === message.url && !resource.thumbnail
89-
? { ...resource, thumbnail: message.thumbnail }
90-
: resource
91-
)
92-
)
93-
}
94-
}
95-
9683
chrome.storage.onChanged.addListener(handleStorageChange)
97-
chrome.runtime.onMessage.addListener(handleThumbnailUpdate)
9884

9985
// 每 5 秒刷新当前 tab 的任务状态
10086
const interval = setInterval(() => {
@@ -103,7 +89,6 @@ const Popup: React.FC = () => {
10389

10490
return () => {
10591
chrome.storage.onChanged.removeListener(handleStorageChange)
106-
chrome.runtime.onMessage.removeListener(handleThumbnailUpdate)
10792
clearInterval(interval)
10893
}
10994
}, [activeTab])
@@ -660,13 +645,16 @@ const Popup: React.FC = () => {
660645
<img src={resource.url} alt={resource.alt || 'Image'} loading="lazy" />
661646
)}
662647
{resource.type === 'video' && (
663-
resource.thumbnail ? (
664-
<div className="video-thumbnail-wrapper">
665-
<img src={resource.thumbnail} alt="Video thumbnail" />
666-
</div>
667-
) : (
668-
<span className="resource-icon">🎬</span>
669-
)
648+
<div className="video-preview-wrapper">
649+
<video
650+
src={resource.url}
651+
poster={resource.thumbnail}
652+
preload="metadata"
653+
muted
654+
playsInline
655+
/>
656+
<div className="video-play-icon"></div>
657+
</div>
670658
)}
671659
{resource.type === 'audio' && <span className="resource-icon">🎵</span>}
672660
</div>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// 媒体资源接口
2+
export interface MediaResource {
3+
url: string
4+
type: 'image' | 'video' | 'audio'
5+
size?: number // 文件大小(字节)
6+
width?: number // 图片/视频宽度
7+
height?: number // 图片/视频高度
8+
alt?: string
9+
thumbnail?: string // 视频封面(base64 或 URL)
10+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// 导出基础类型
2+
export { MediaResource } from './base'
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
# 通用嗅探策略系统
2+
3+
这个目录包含了用于嗅探页面中通用媒体资源的策略模式实现。
4+
5+
## 架构设计
6+
7+
### 策略模式 (Strategy Pattern)
8+
9+
策略模式允许我们将不同的嗅探算法封装成独立的策略类,使代码更加模块化和可维护。
10+
11+
```
12+
┌─────────────────────┐
13+
│ SniffStrategy │ (抽象基类)
14+
│ --------------- │
15+
│ + name │
16+
│ + priority │
17+
│ + sniff() │
18+
│ + sniffSafely() │
19+
└─────────────────────┘
20+
21+
│ 继承
22+
23+
┌─────┴─────┬─────────┬─────────┐
24+
│ │ │ │
25+
┌───┴────┐ ┌────┴───┐ ┌──┴──┐ ┌───┴───┐
26+
│Image │ │Video │ │Audio│ │... │
27+
│Tag │ │Tag │ │Tag │ │ │
28+
└────────┘ └────────┘ └─────┘ └───────┘
29+
```
30+
31+
## 已实现的策略
32+
33+
| 优先级 | 策略名称 | 文件 | 描述 |
34+
|-------|---------|------|------|
35+
| 1 | ImageTag | `image-tag.ts` | 扫描所有 `<img>` 标签 |
36+
| 2 | BackgroundImage | `background-image.ts` | 扫描 CSS 背景图片 |
37+
| 3 | VideoTag | `video-tag.ts` | 扫描所有 `<video>` 标签及其 `<source>` 子元素 |
38+
| 4 | AudioTag | `audio-tag.ts` | 扫描所有 `<audio>` 标签及其 `<source>` 子元素 |
39+
| 5 | PerformanceAPI | `performance-api.ts` | 从 Performance API 获取动态加载的资源 |
40+
| 6 | CustomAttributes | `custom-attributes.ts` | 扫描自定义 data 属性(兜底方案) |
41+
| 7 | ScriptExtraction | `script-extraction.ts` | 从页面脚本中提取视频 URL(针对抖音等平台) |
42+
43+
## 如何使用
44+
45+
### 在 content.ts 中自动加载
46+
47+
策略系统在 `content.ts` 中自动加载和执行:
48+
49+
```typescript
50+
import { sniffStrategies } from './sniffers/strategies'
51+
52+
// 执行所有通用嗅探策略
53+
sniffStrategies.forEach(strategy => {
54+
const strategyResources = strategy.sniffSafely(seenUrls)
55+
resources.push(...strategyResources)
56+
})
57+
```
58+
59+
### 策略执行顺序
60+
61+
策略按 `priority` 属性从小到大执行,确保:
62+
1. 最常见的资源类型先被嗅探(如图片、视频)
63+
2. 性能影响小的策略先执行
64+
3. 兜底策略最后执行
65+
66+
## 如何添加新策略
67+
68+
### 步骤 1: 创建策略类
69+
70+
`strategies/` 目录下创建新文件,例如 `my-strategy.ts`
71+
72+
```typescript
73+
import { MediaResource } from '../base'
74+
import { SniffStrategy } from './base'
75+
76+
export class MyStrategy extends SniffStrategy {
77+
get name(): string {
78+
return 'MyStrategy'
79+
}
80+
81+
get priority(): number {
82+
return 8 // 设置优先级
83+
}
84+
85+
sniff(seenUrls: Set<string>): MediaResource[] {
86+
const resources: MediaResource[] = []
87+
88+
// 实现你的嗅探逻辑
89+
// 注意:seenUrls 用于去重,添加资源时需要检查和更新
90+
91+
return resources
92+
}
93+
}
94+
```
95+
96+
### 步骤 2: 注册策略
97+
98+
`index.ts` 中导入并注册:
99+
100+
```typescript
101+
import { MyStrategy } from './my-strategy'
102+
103+
export const sniffStrategies: SniffStrategy[] = [
104+
// ... 现有策略
105+
new MyStrategy() // 添加新策略
106+
].sort((a, b) => a.priority - b.priority)
107+
```
108+
109+
### 步骤 3: 测试
110+
111+
重新构建扩展并测试:
112+
113+
```bash
114+
pnpm build
115+
```
116+
117+
## 设计原则
118+
119+
### 1. 单一职责
120+
121+
每个策略只负责一种嗅探方法,使代码易于理解和维护。
122+
123+
### 2. 去重机制
124+
125+
所有策略共享 `seenUrls` 集合:
126+
127+
```typescript
128+
if (!seenUrls.has(url) && this.isValidUrl(url)) {
129+
seenUrls.add(url)
130+
resources.push({ url, type: 'image' })
131+
}
132+
```
133+
134+
### 3. 错误隔离
135+
136+
使用 `sniffSafely()` 包装执行,确保单个策略失败不影响其他策略:
137+
138+
```typescript
139+
public sniffSafely(seenUrls: Set<string>): MediaResource[] {
140+
try {
141+
return this.sniff(seenUrls)
142+
} catch (error) {
143+
console.debug(`[Strategy:${this.name}] Sniffing error:`, error)
144+
return []
145+
}
146+
}
147+
```
148+
149+
### 4. 优先级控制
150+
151+
通过 `priority` 属性控制执行顺序:
152+
- 1-3: 高优先级(基本 DOM 元素)
153+
- 4-6: 中优先级(API 和属性)
154+
- 7+: 低优先级(兜底和特殊情况)
155+
156+
## 性能优化
157+
158+
### 1. 惰性执行
159+
160+
策略只在需要时执行,不会预加载。
161+
162+
### 2. 早期退出
163+
164+
在策略内部尽早检查和退出:
165+
166+
```typescript
167+
if (!seenUrls.has(url) && this.isValidUrl(url)) {
168+
// 只有在通过所有检查后才进行复杂操作
169+
seenUrls.add(url)
170+
resources.push(resource)
171+
}
172+
```
173+
174+
### 3. DOM 查询优化
175+
176+
使用高效的选择器:
177+
178+
```typescript
179+
// 推荐
180+
const images = document.querySelectorAll('img')
181+
182+
// 避免
183+
const images = document.querySelectorAll('*')
184+
.filter(el => el.tagName === 'IMG')
185+
```
186+
187+
## 与平台嗅探器的关系
188+
189+
- **平台嗅探器**`sniffers/youtube.ts` 等):针对特定平台的优化嗅探
190+
- **通用策略**`sniffers/strategies/`):适用于所有页面的通用嗅探
191+
192+
执行顺序:
193+
1. 先执行平台嗅探器(如果匹配)
194+
2. 再执行通用策略
195+
3. 所有结果合并并去重
196+
197+
## 故障排除
198+
199+
### 策略没有执行?
200+
201+
检查控制台日志:
202+
```
203+
[Sniff] Starting general strategies...
204+
[Sniff] Running strategy: ImageTag
205+
[Sniff] Strategy ImageTag found X resources
206+
```
207+
208+
### 资源重复?
209+
210+
确保使用 `seenUrls` 去重:
211+
```typescript
212+
if (!seenUrls.has(url)) {
213+
seenUrls.add(url)
214+
// ...
215+
}
216+
```
217+
218+
### 性能问题?
219+
220+
- 检查策略的 priority,确保高开销策略后执行
221+
- 使用 `console.time()` 测量策略执行时间
222+
- 考虑添加防抖或节流
223+
224+
## 未来扩展
225+
226+
可以考虑添加的策略:
227+
- **IframeStrategy**: 扫描 iframe 中的资源
228+
- **SVGStrategy**: 扫描 SVG 图片
229+
- **WebPStrategy**: 专门处理 WebP 图片
230+
- **M3U8Strategy**: HLS 视频流专门处理
231+
- **DataURLStrategy**: 处理 data URL 格式的资源
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { MediaResource } from '../base'
2+
import { SniffStrategy } from './base'
3+
4+
/**
5+
* 策略4:扫描所有 <audio> 标签
6+
*/
7+
export class AudioTagStrategy extends SniffStrategy {
8+
get name(): string {
9+
return 'AudioTag'
10+
}
11+
12+
get priority(): number {
13+
return 4
14+
}
15+
16+
sniff(seenUrls: Set<string>): MediaResource[] {
17+
const resources: MediaResource[] = []
18+
19+
const audios = document.querySelectorAll('audio')
20+
audios.forEach((audio) => {
21+
const url = audio.src || audio.currentSrc || audio.dataset.src || audio.getAttribute('data-src')
22+
23+
if (url && !seenUrls.has(url) && this.isValidUrl(url)) {
24+
seenUrls.add(url)
25+
resources.push({
26+
url,
27+
type: 'audio',
28+
alt: audio.title || ''
29+
})
30+
}
31+
32+
// 扫描 audio 标签内的 source 元素
33+
const sources = audio.querySelectorAll('source')
34+
sources.forEach((source) => {
35+
const url = source.src || source.dataset.src || source.getAttribute('data-src')
36+
37+
if (url && !seenUrls.has(url) && this.isValidUrl(url)) {
38+
seenUrls.add(url)
39+
resources.push({
40+
url,
41+
type: 'audio'
42+
})
43+
}
44+
})
45+
})
46+
47+
return resources
48+
}
49+
}

0 commit comments

Comments
 (0)