default··约 19 分钟读完

M3U8 下载助手油猴脚本 - 技术实现解析

M3U8下载助手技术解析 该油猴脚本通过注入拦截脚本实现对M3U8链接的全面捕获。核心架构包括: 请求拦截层:劫持XHR、fetch和WebSocket请求 DOM监听层:通过MutationObserver监控动态元素变化 跨域通信:iframe间使用postMessage传递数据 UI展示层:提供浮动按钮等交互界面 关键技术点: 使用document-start确保早期注入 通过保存原始方法实现API劫持 在send时而非open时检测URL 全面覆盖各种属性(src/href/data-src等) 脚
前端javascriptwindows

M3U8 下载助手油猴脚本 - 技术实现解析

概述

本文深入解析 M3U8 下载助手油猴脚本的技术实现,包括 M3U8 链接检测机制、iframe 跨域通信、DOM 监听等核心技术。


架构设计

整体架构

┌─────────────────────────────────────────────────────────────┐ │ 浏览器页面 │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ 注入的拦截脚本 │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ XHR │ │ fetch │ │WebSocket│ │ DOM │ │ │ │ │ │ 劫持 │ │ 劫持 │ │ 劫持 │ │ 监听 │ │ │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ │ │ │ │ │ └────────────┴─────┬──────┴────────────┘ │ │ │ │ │ │ │ │ │ 收集 M3U8 链接 │ │ │ │ │ │ │ │ │ ┌───────────┴───────────┐ │ │ │ │ │ window.__m3u8Links │ │ │ │ │ │ (Set 集合) │ │ │ │ │ └───────────┬───────────┘ │ │ │ └──────────────────────────┼────────────────────────────┘ │ │ │ │ │ postMessage (如果是 iframe) │ │ │ │ │ ┌──────────────────────────┴────────────────────────────┐ │ │ │ 顶层窗口 (油猴脚本) │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ 监听 message 事件,收集来自所有 iframe 的链接 │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ 创建浮动按钮、弹窗等 UI 组件 │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ GM_xmlhttpRequest 调用后端 API │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ │ HTTP/WebSocket ▼ ┌─────────────────────────────────────────────────────────────┐ │ M3U8 下载服务 (Rust) │ └─────────────────────────────────────────────────────────────┘

核心技术详解

1. 脚本注入机制

油猴脚本使用 @run-at document-start 在页面最早期执行,但为了不阻塞页面加载,只注入一个轻量级的 script 元素:

const injectScript = ` (function() { // 拦截逻辑 })(); `; const s = document.createElement('script'); s.textContent = injectScript; (document.head || document.documentElement).appendChild(s); s.remove();

为什么要注入到页面上下文?

油猴脚本运行在隔离的沙箱环境中,无法直接访问页面的 window 对象。通过注入 script 元素,可以让拦截代码在页面上下文中执行,从而劫持页面的原生 API。

2. 网络请求拦截

2.1 XMLHttpRequest 劫持

// 保存原始方法 const xhrOpen = XMLHttpRequest.prototype.open; const xhrSend = XMLHttpRequest.prototype.send; // 劫持 open 方法,保存 URL XMLHttpRequest.prototype.open = function(method, url) { this._url = url; return xhrOpen.apply(this, arguments); }; // 劫持 send 方法,检测 URL XMLHttpRequest.prototype.send = function(body) { if (this._url) { add(this._url); // 添加到检测集合 } return xhrSend.apply(this, arguments); };

为什么要在 send 时检测?

因为 open 只是设置参数,send 才真正发起请求。而且有些代码会在 open 之后修改 URL。

2.2 fetch 劫持

const origFetch = window.fetch; window.fetch = function(input) { // input 可以是字符串或 Request 对象 add(typeof input === 'string' ? input : input.url); return origFetch.apply(this, arguments); };

2.3 WebSocket 劫持

const origWS = window.WebSocket; window.WebSocket = function(url) { add(url); return new origWS(url); };

3. DOM 变化监听

使用 MutationObserver 监听 DOM 变化,捕获动态添加的视频元素:

new MutationObserver(mutations => { mutations.forEach(mutation => { // 属性变化 if (mutation.type === 'attributes') { const attrName = mutation.attributeName; if (attrName === 'src' || attrName === 'href') { add(mutation.target[attrName]); } } // 子节点变化 else if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { // Element 节点 // 检查各种可能包含 URL 的属性 ['src', 'href', 'data-src', 'data-href'].forEach(attr => { const value = node.getAttribute(attr); if (value) add(value); }); // 特别处理 video/audio 元素 if (node.tagName === 'VIDEO' || node.tagName === 'AUDIO') { if (node.src) add(node.src); if (node.currentSrc) add(node.currentSrc); } } }); } }); }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'href'] });

优化点:

  • 使用 attributeFilter 只监听 srchref 属性,减少回调次数
  • 只检查 nodeType === 1 的元素节点

4. iframe 跨域通信

当视频在 iframe 中加载时,需要跨 iframe 传递检测到的链接。

4.1 iframe 内部:发送消息

// 在 iframe 中检测到链接后 if (top === self) { // 是顶层窗口,直接添加 window.__m3u8Links.add(url); } else { // 是 iframe,发送消息给顶层 top.postMessage({ type: 'M3U8_LINK_FOUND', url: url }, '*'); }

4.2 顶层窗口:接收消息

if (realWin.top === realWin.self) { realWin.addEventListener('message', e => { if (e.data?.type === 'M3U8_LINK_FOUND' && e.data.url) { if (!realWin.__m3u8Links.has(e.data.url)) { realWin.__m3u8Links.add(e.data.url); // 更新 UI if (realWin.__m3u8Links.size === 1) { ensureButton(); } } } }); }

5. URL 标准化

检测到的 URL 需要标准化处理:

const add = (url) => { if (!url) return; try { // 转换为绝对 URL let abs = new URL(url, location.href).href; // 验证是否是 M3U8 链接 if (abs.includes('.m3u8') && /^https?:/.test(abs)) { // 只在顶层窗口存储 if (top === self) { window.__m3u8Links.add(abs); } else { top.postMessage({ type: 'M3U8_LINK_FOUND', url: abs }, '*'); } } } catch(e) { // URL 解析失败,忽略 } };

关键点:

  • 使用 new URL(url, location.href) 处理相对路径
  • 正则验证必须是 HTTP(S) 协议
  • 使用 Set 自动去重

6. 文件名处理

自动清理文件名中的非法字符:

const sanitize = name => { if (!name) return ''; return name .replace(/[\\/:*?"<>|]/g, '_') // 替换非法字符为下划线 .trim(); };

智能默认文件名生成:

const defaultFileName = (link) => { // 优先使用网页标题 let title = document.title; if (title?.trim()) return sanitize(title); // 其次使用 URL 中的文件名 try { let last = new URL(link).pathname .split('/') .pop() .replace(/\.m3u8$/i, ''); return sanitize(decodeURIComponent(last)) || 'video'; } catch (e) { return 'm3u8_video'; } };

7. GM_xmlhttpRequest

使用油猴提供的 GM_xmlhttpRequest 发送跨域请求:

const startDownload = (url, name) => new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: `${BACKEND_URL}/api/download`, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ url, name }), onload: resp => { if (resp.status === 200 || resp.status === 201) { try { resolve(JSON.parse(resp.responseText).id); } catch (e) { reject('解析失败'); } } else { reject(`状态码 ${resp.status}`); } }, onerror: err => reject(`网络错误: ${err}`) }); });

为什么不用原生 fetch?

因为油猴脚本运行在隔离环境中,原生 fetch 会受到同源策略限制。GM_xmlhttpRequest 是油猴提供的特权 API,可以绕过这些限制。

8. WebSocket 进度监控

下载任务创建后,通过 WebSocket 实时监控进度:

const monitorTask = (taskId, taskName, url) => { const ui = createProgressModal(taskId, taskName, url); const connect = () => { const ws = new WebSocket( `${BACKEND_URL.replace('http', 'ws')}/api/tasks/${taskId}/ws` ); ws.onmessage = e => { const task = JSON.parse(e.data); // 更新进度 if (task.progress !== undefined) { ui.updateProgress(task.progress); } // 更新状态 if (task.status) { ui.updateStatus(task.status, task.error); } // 完成或失败时关闭 if (task.status === 'completed') { finalize(true); } else if (task.status === 'failed') { finalize(false, task.error); } }; }; connect(); };

性能优化

1. 防抖处理

对于频繁触发的事件,使用防抖减少处理次数:

let updateTimeout = null; const debouncedUpdate = () => { clearTimeout(updateTimeout); updateTimeout = setTimeout(() => { // 实际处理逻辑 }, 100); };

2. Set 去重

使用 Set 自动去重,避免重复存储相同链接:

window.__m3u8Links = new Set();

3. 定时扫描优化

定时扫描只在检测到链接后才创建浮动按钮,避免空操作:

setInterval(() => { if (realWin.__m3u8Links?.size && !document.getElementById('tm-m3u8-btn')) { ensureButton(); } }, 1500);

4. 条件注入

检查是否已注入,避免重复注入:

if (realWin.__m3u8InterceptorInstalled) return; realWin.__m3u8InterceptorInstalled = true;

安全考虑

1. URL 验证

只处理 HTTP(S) 协议的 URL:

if (!/^https?:/.test(abs)) return;

2. XSS 防护

用户输入的文件名需要转义:

const escapeHtml = (text) => { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; };

3. 作用域隔离

使用 IIFE 避免污染全局作用域:

(function() { 'use strict'; // 脚本代码 })();

调试技巧

1. 查看检测到的链接

在浏览器控制台中执行:

// 在页面上下文中执行 console.log(Array.from(window.__m3u8Links));

2. 手动触发按钮显示

// 强制显示按钮 ensureButton();

3. 查看注入是否成功

console.log(window.__m3u8InterceptorInstalled); // 应该是 true

总结

M3U8 下载助手油猴脚本综合运用了多种浏览器技术:

技术用途
XHR/fetch 劫持拦截网络请求
MutationObserver监听 DOM 变化
postMessageiframe 跨域通信
GM_xmlhttpRequest跨域 API 调用
WebSocket实时进度监控
URL APIURL 标准化

这些技术的组合使得脚本能够全面、高效地检测网页中的 M3U8 链接,并提供良好的用户体验。