1
This commit is contained in:
921
catch-script/catch.js
Normal file
921
catch-script/catch.js
Normal file
@@ -0,0 +1,921 @@
|
||||
(function () {
|
||||
class CatCatcher {
|
||||
constructor() {
|
||||
console.log("catch.js Start");
|
||||
|
||||
// 初始化属性
|
||||
this.enable = true; // 捕获开关
|
||||
this.language = navigator.language; // 语言设置
|
||||
this.isComplete = false; // 捕获完成标志
|
||||
this.catchMedia = []; // 捕获的媒体数据
|
||||
this.mediaSize = 0; // 捕获的媒体数据大小
|
||||
this.setFileName = null; // 文件名
|
||||
this.catCatch = null; // UI元素
|
||||
|
||||
// 移动面板相关属性
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
|
||||
// 初始化语言
|
||||
if (window.CatCatchI18n) {
|
||||
if (!window.CatCatchI18n.languages.includes(this.language)) {
|
||||
this.language = this.language.split("-")[0];
|
||||
if (!window.CatCatchI18n.languages.includes(this.language)) {
|
||||
this.language = "en";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化组件
|
||||
// 删除iframe sandbox属性 避免 issues #576
|
||||
this.setupIframeProcessing();
|
||||
|
||||
// 初始化 Trusted Types
|
||||
this.initTrustedTypes();
|
||||
|
||||
// 创建和设置UI
|
||||
this.createUI();
|
||||
|
||||
// 代理MediaSource方法
|
||||
this.proxyMediaSourceMethods();
|
||||
|
||||
// 自动跳转到缓冲尾
|
||||
if (localStorage.getItem("CatCatchCatch_autoToBuffered") == "checked") {
|
||||
const autoToBufferedInterval = setInterval(() => {
|
||||
const videos = document.querySelectorAll('video');
|
||||
if (videos.length > 0 && Array.from(videos).some(video => !video.paused && video.readyState > 2)) {
|
||||
const autoToBufferedElement = this.catCatch.querySelector("#autoToBuffered");
|
||||
if (autoToBufferedElement) {
|
||||
autoToBufferedElement.click();
|
||||
clearInterval(autoToBufferedInterval);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置iframe处理,删除sandbox属性
|
||||
* 解决 issues #576
|
||||
*/
|
||||
setupIframeProcessing() {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const processIframe = (iframe) => {
|
||||
if (iframe && iframe.hasAttribute && iframe.hasAttribute('sandbox')) {
|
||||
const clonedIframe = iframe.cloneNode(true);
|
||||
clonedIframe.removeAttribute('sandbox');
|
||||
if (iframe.parentNode) {
|
||||
iframe.parentNode.replaceChild(clonedIframe, iframe);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelectorAll('iframe').forEach(processIframe);
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'childList') {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node.nodeName === 'IFRAME') {
|
||||
processIframe(node);
|
||||
} else if (node.querySelectorAll) {
|
||||
node.querySelectorAll('iframe').forEach(processIframe);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(document.body || document.documentElement, { childList: true, subtree: true });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 Trusted Types
|
||||
*/
|
||||
initTrustedTypes() {
|
||||
let createHTML = (string) => {
|
||||
try {
|
||||
const fakeDiv = document.createElement('div');
|
||||
fakeDiv.innerHTML = string;
|
||||
createHTML = (string) => string;
|
||||
} catch (e) {
|
||||
if (typeof trustedTypes !== 'undefined') {
|
||||
const policy = trustedTypes.createPolicy('catCatchPolicy', { createHTML: (s) => s });
|
||||
createHTML = (string) => policy.createHTML(string);
|
||||
const _innerHTML = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
|
||||
Object.defineProperty(Element.prototype, 'innerHTML', {
|
||||
set: function (value) {
|
||||
_innerHTML.set.call(this, createHTML(value));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn("trustedTypes不可用,跳过安全策略设置");
|
||||
}
|
||||
}
|
||||
};
|
||||
createHTML("<div></div>");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建UI元素
|
||||
*/
|
||||
createUI() {
|
||||
const buttonStyle = 'style="border:solid 1px #000;margin:2px;padding:2px;background:#fff;border-radius:4px;border:solid 1px #c7c7c780;color:#000;"';
|
||||
const checkboxStyle = 'style="-webkit-appearance: auto;"';
|
||||
|
||||
this.catCatch = document.createElement("div");
|
||||
this.catCatch.setAttribute("id", "CatCatchCatch");
|
||||
const style = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;`;
|
||||
this.catCatch.innerHTML = `<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYBAMAAAASWSDLAAAAKlBMVEUAAADLlROxbBlRAD16GS5oAjWWQiOCIytgADidUx/95gHqwwTx0gDZqwT6kfLuAAAACnRSTlMA/vUejV7kuzi8za0PswAAANpJREFUGNNjwA1YSxkYTEqhnKZLLi6F1w0gnKA1shdvHYNxdq1atWobjLMKCOAyC3etlVrUAOH4HtNZmLgoAMKpXX37zO1FwcZAwMDguGq1zKpFmTNnzqx0Bpp2WvrU7ttn9py+I8JgLn1R8Pad22vurNkjwsBReHv33junzuyRnOnMwNCSeFH27K5dq1SNgcZxFMnuWrNq1W5VkNntihdv7ToteGcT0C7mIkE1qbWCYjJnM4CqEoWKdoslChXuUgXJqIcLebiphSgCZRhaPDhcDFhdmUMCGIgEAFA+Uc02aZg9AAAAAElFTkSuQmCC" style="-webkit-user-drag: none;width: 20px;">
|
||||
<div id="catCatch" style="${style}">
|
||||
<div id="tips"></div>
|
||||
<button id="download" ${buttonStyle} data-i18n="downloadCapturedData">下载已捕获的数据</button>
|
||||
<button id="clean" ${buttonStyle} data-i18n="deleteCapturedData">删除已捕获数据</button>
|
||||
<div><button id="hide" ${buttonStyle} data-i18n="hide">隐藏</button><button id="close" ${buttonStyle} data-i18n="close">关闭</button></div>
|
||||
<label><input type="checkbox" id="autoDown" ${localStorage.getItem("CatCatchCatch_autoDown") || ""} ${checkboxStyle}><span data-i18n="automaticDownload">完成捕获自动下载</span></label>
|
||||
<label><input type="checkbox" id="ffmpeg" ${localStorage.getItem("CatCatchCatch_ffmpeg") || ""} ${checkboxStyle}><span data-i18n="ffmpeg">使用ffmpeg合并</span></label>
|
||||
<label><input type="checkbox" id="autoToBuffered" ${checkboxStyle}><span data-i18n="autoToBuffered">自动跳转缓冲尾</span></label>
|
||||
<label><input type="checkbox" id="checkHead" ${checkboxStyle}>清理多余头部数据</label>
|
||||
<label><input type="checkbox" id="completeClearCache" ${localStorage.getItem("CatCatchCatch_completeClearCache") || ""} ${checkboxStyle}>下载完成后清空数据</label>
|
||||
<details>
|
||||
<summary data-i18n="fileName" id="summary">文件名设置</summary>
|
||||
<div style="font-weight:bold;"><span data-i18n="fileName">文件名</span>: </div><div id="fileName"></div>
|
||||
<div style="font-weight:bold;"><span data-i18n="selector">表达式</span>: </div><div id="selector">Null</div>
|
||||
<div style="font-weight:bold;"><span data-i18n="regular">正则</span>: </div><div id="regular">Null</div>
|
||||
<button id="setSelector" ${buttonStyle} data-i18n="usingSelector">表达式提取</button>
|
||||
<button id="setRegular" ${buttonStyle} data-i18n="usingRegular">正则提取</button>
|
||||
<button id="setFileName" ${buttonStyle} data-i18n="customize">手动填写</button>
|
||||
</details>
|
||||
<details>
|
||||
<summary>test</summary>
|
||||
<button id="test" ${buttonStyle}>test</button>
|
||||
<button id="restart" ${buttonStyle} data-i18n="capturedBeginning">从头捕获</button>
|
||||
<label><input type="checkbox" id="restartAlways" ${localStorage.getItem("CatCatchCatch_restart") || ""} ${checkboxStyle}><span data-i18n="alwaysCapturedBeginning">始终从头捕获</span>(beta)</label>
|
||||
</details>
|
||||
</div>`;
|
||||
this.catCatch.style = `
|
||||
position: fixed;
|
||||
z-index: 999999;
|
||||
top: 10%;
|
||||
left: 90%;
|
||||
background: rgb(255 255 255 / 85%);
|
||||
border: solid 1px #c7c7c7;
|
||||
border-radius: 4px;
|
||||
color: rgb(26, 115, 232);
|
||||
padding: 5px 5px 5px 5px;
|
||||
font-size: 12px;
|
||||
font-family: "Microsoft YaHei", "Helvetica", "Arial", sans-serif;
|
||||
user-select: none;`;
|
||||
|
||||
// 创建 Shadow DOM
|
||||
this.createShadowRoot();
|
||||
|
||||
// 初始化UI元素引用
|
||||
this.tips = this.catCatch.querySelector("#tips");
|
||||
this.fileName = this.catCatch.querySelector("#fileName");
|
||||
this.selector = this.catCatch.querySelector("#selector");
|
||||
this.regular = this.catCatch.querySelector("#regular");
|
||||
|
||||
if (!this.tips || !this.fileName || !this.selector || !this.regular) {
|
||||
console.error("UI元素初始化失败,找不到必要的DOM元素");
|
||||
}
|
||||
|
||||
// 初始化显示
|
||||
this.tips.innerHTML = this.i18n("waiting", "等待视频播放");
|
||||
this.selector.innerHTML = localStorage.getItem("CatCatchCatch_selector") ?? "Null";
|
||||
this.regular.innerHTML = localStorage.getItem("CatCatchCatch_regular") ?? "Null";
|
||||
|
||||
// 绑定事件
|
||||
this.bindEvents();
|
||||
|
||||
// 自动从头捕获设置
|
||||
if (localStorage.getItem("CatCatchCatch_restart") == "checked") {
|
||||
this.setupAutoRestart();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Shadow DOM
|
||||
* 解决 issues #693 安全使用attachShadow 从iframe中获取原生方法
|
||||
*/
|
||||
createShadowRoot() {
|
||||
try {
|
||||
// 解决 issues #693 安全使用attachShadow 从iframe中获取原生方法
|
||||
const createSecureShadowRoot = (element, mode = 'closed') => {
|
||||
const getPristineAttachShadow = () => {
|
||||
try {
|
||||
const iframe = document.createElement('iframe');
|
||||
const parentNode = document.body || document.documentElement;
|
||||
parentNode.appendChild(iframe);
|
||||
const pristineMethod = iframe.contentDocument.createElement('div').attachShadow;
|
||||
iframe.remove();
|
||||
if (pristineMethod) return pristineMethod;
|
||||
} catch (e) {
|
||||
console.log("获取原生attachShadow方法失败:", e);
|
||||
}
|
||||
return Element.prototype.attachShadow;
|
||||
};
|
||||
|
||||
const executor = Element.prototype.attachShadow.toString().includes('[native code]')
|
||||
? Element.prototype.attachShadow.bind(element)
|
||||
: getPristineAttachShadow().bind(element);
|
||||
|
||||
try {
|
||||
return executor({ mode });
|
||||
} catch (e) {
|
||||
console.error('Shadow DOM 创建失败:', e);
|
||||
// 应急处理:降级方案
|
||||
return document.createElement('div');
|
||||
}
|
||||
};
|
||||
|
||||
// 创建 Shadow DOM 放入CatCatch
|
||||
const divShadow = document.createElement('div');
|
||||
const shadowRoot = createSecureShadowRoot(divShadow);
|
||||
shadowRoot.appendChild(this.catCatch);
|
||||
|
||||
// 页面插入Shadow DOM
|
||||
const htmlElement = document.getElementsByTagName('html')[0];
|
||||
if (htmlElement) {
|
||||
htmlElement.appendChild(divShadow);
|
||||
} else {
|
||||
document.appendChild(divShadow);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("创建Shadow DOM失败:", error);
|
||||
// 降级方案:直接添加到body
|
||||
try {
|
||||
const body = document.body || document.documentElement;
|
||||
body.appendChild(this.catCatch);
|
||||
} catch (e) {
|
||||
console.error("降级添加UI也失败:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定事件处理函数
|
||||
*/
|
||||
bindEvents() {
|
||||
// 移动面板相关事件
|
||||
this.catCatch.addEventListener('mousedown', this.handleDragStart.bind(this));
|
||||
|
||||
// 设置选项相关事件
|
||||
const autoDown = this.catCatch.querySelector("#autoDown");
|
||||
if (autoDown) autoDown.addEventListener('change', this.handleAutoDownChange.bind(this));
|
||||
|
||||
const ffmpeg = this.catCatch.querySelector("#ffmpeg");
|
||||
if (ffmpeg) ffmpeg.addEventListener('change', this.handleFfmpegChange.bind(this));
|
||||
|
||||
const restartAlways = this.catCatch.querySelector("#restartAlways");
|
||||
if (restartAlways) restartAlways.addEventListener('change', this.handleRestartAlwaysChange.bind(this));
|
||||
|
||||
// 按钮相关事件
|
||||
const clean = this.catCatch.querySelector("#clean");
|
||||
if (clean) clean.addEventListener('click', this.handleClean.bind(this));
|
||||
|
||||
const download = this.catCatch.querySelector("#download");
|
||||
if (download) download.addEventListener('click', this.handleDownload.bind(this));
|
||||
|
||||
const hide = this.catCatch.querySelector("#hide");
|
||||
if (hide) hide.addEventListener('click', this.handleHide.bind(this));
|
||||
|
||||
const img = this.catCatch.querySelector("img");
|
||||
if (img) img.addEventListener('click', this.handleHide.bind(this));
|
||||
|
||||
const close = this.catCatch.querySelector("#close");
|
||||
if (close) close.addEventListener('click', this.handleClose.bind(this));
|
||||
|
||||
const restart = this.catCatch.querySelector("#restart");
|
||||
if (restart) restart.addEventListener('click', this.handleRestart.bind(this));
|
||||
|
||||
const setFileName = this.catCatch.querySelector("#setFileName");
|
||||
if (setFileName) setFileName.addEventListener('click', this.handleSetFileName.bind(this));
|
||||
|
||||
const test = this.catCatch.querySelector("#test");
|
||||
if (test) test.addEventListener('click', this.handleTest.bind(this));
|
||||
|
||||
const summary = this.catCatch.querySelector("#summary");
|
||||
if (summary) summary.addEventListener('click', this.getFileName.bind(this));
|
||||
|
||||
const completeClearCache = this.catCatch.querySelector("#completeClearCache");
|
||||
if (completeClearCache) completeClearCache.addEventListener('click', this.handleCompleteClearCache.bind(this));
|
||||
|
||||
// 自动跳转到缓冲节点
|
||||
// this.autoToBufferedFlag = true;
|
||||
const autoToBuffered = this.catCatch.querySelector("#autoToBuffered");
|
||||
if (autoToBuffered) autoToBuffered.addEventListener('click', this.handleAutoToBuffered.bind(this));
|
||||
|
||||
// 文件名设置相关事件
|
||||
const setSelector = this.catCatch.querySelector("#setSelector");
|
||||
if (setSelector) setSelector.addEventListener('click', this.handleSetSelector.bind(this));
|
||||
|
||||
const setRegular = this.catCatch.querySelector("#setRegular");
|
||||
if (setRegular) setRegular.addEventListener('click', this.handleSetRegular.bind(this));
|
||||
|
||||
// i18n 处理
|
||||
this.applyI18n();
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用国际化文本
|
||||
*/
|
||||
applyI18n() {
|
||||
if (window.CatCatchI18n) {
|
||||
this.catCatch.querySelectorAll('[data-i18n]').forEach((element) => {
|
||||
if (element && element.dataset && element.dataset.i18n) {
|
||||
element.innerHTML = window.CatCatchI18n[element.dataset.i18n][this.language] || element.innerHTML;
|
||||
}
|
||||
});
|
||||
this.catCatch.querySelectorAll('[data-i18n-outer]').forEach((element) => {
|
||||
if (element && element.dataset && element.dataset.i18nOuter) {
|
||||
element.outerHTML = window.CatCatchI18n[element.dataset.i18nOuter][this.language] || element.outerHTML;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译函数
|
||||
* @param {String} key
|
||||
* @param {String|null} original 原始文本
|
||||
* @returns 翻译后的文本
|
||||
*/
|
||||
i18n(key, original = "") {
|
||||
if (!window.CatCatchI18n || !key || !window.CatCatchI18n[key]) { return original; }
|
||||
return window.CatCatchI18n[key][this.language] || original;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理面板拖动事件
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
handleDragStart(event) {
|
||||
this.x = event.pageX - this.catCatch.offsetLeft;
|
||||
this.y = event.pageY - this.catCatch.offsetTop;
|
||||
|
||||
const moveHandler = this.handleMove.bind(this);
|
||||
document.addEventListener('mousemove', moveHandler);
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
document.removeEventListener('mousemove', moveHandler);
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理面板移动事件
|
||||
* 通过鼠标事件更新面板位置
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
handleMove(event) {
|
||||
if (!this.catCatch) return;
|
||||
this.catCatch.style.left = (event.pageX - this.x) + 'px';
|
||||
this.catCatch.style.top = (event.pageY - this.y) + 'px';
|
||||
}
|
||||
|
||||
handleAutoDownChange(event) {
|
||||
localStorage.setItem("CatCatchCatch_autoDown", event.target.checked ? "checked" : "");
|
||||
}
|
||||
|
||||
handleFfmpegChange(event) {
|
||||
localStorage.setItem("CatCatchCatch_ffmpeg", event.target.checked ? "checked" : "");
|
||||
}
|
||||
|
||||
handleRestartAlwaysChange(event) {
|
||||
localStorage.setItem("CatCatchCatch_restart", event.target.checked ? "checked" : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理清理缓存事件
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
handleClean(event) {
|
||||
if (window.confirm(this.i18n("clearCacheConfirmation", "确认清除缓存?"))) {
|
||||
this.clearCache();
|
||||
const $clean = this.catCatch.querySelector("#clean");
|
||||
if (!$clean) return;
|
||||
|
||||
$clean.innerHTML = this.i18n("cleanupCompleted", "清理完成!");
|
||||
setTimeout(() => {
|
||||
if ($clean) $clean.innerHTML = this.i18n("clearCache", "清理缓存");
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理下载事件
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
handleDownload(event) {
|
||||
try {
|
||||
if (this.isComplete || window.confirm(this.i18n("downloadConfirmation", "提前下载可能会造成数据混乱.确认?"))) {
|
||||
this.catchDownload();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("下载处理失败:", error);
|
||||
alert(this.i18n("downloadError", "下载过程中出错,请查看控制台"));
|
||||
}
|
||||
}
|
||||
|
||||
handleHide(event) {
|
||||
const catCatchElement = this.catCatch.querySelector('#catCatch');
|
||||
if (catCatchElement.style.display === "none") {
|
||||
catCatchElement.style.display = "flex";
|
||||
this.catCatch.style.opacity = "";
|
||||
} else {
|
||||
catCatchElement.style.display = "none";
|
||||
this.catCatch.style.opacity = "0.5";
|
||||
}
|
||||
}
|
||||
|
||||
handleClose(event) {
|
||||
if (this.isComplete || window.confirm(this.i18n("closeConfirmation", "确认关闭?"))) {
|
||||
this.clearCache();
|
||||
this.enable = false;
|
||||
this.catCatch.style.display = "none";
|
||||
window.postMessage({ action: "catCatchToBackground", Message: "script", script: "catch.js", refresh: false });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从头捕获
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
handleRestart(event) {
|
||||
const checkHead = this.catCatch.querySelector("#checkHead");
|
||||
if (checkHead) checkHead.checked = true;
|
||||
|
||||
this.clearCache();
|
||||
document.querySelectorAll("video").forEach((element) => {
|
||||
element.currentTime = 0;
|
||||
element.play();
|
||||
});
|
||||
}
|
||||
|
||||
handleSetFileName(event) {
|
||||
this.setFileName = window.prompt(this.i18n("fileName", "输入文件名, 不包含扩展名"), this.setFileName ?? "");
|
||||
this.getFileName();
|
||||
}
|
||||
|
||||
handleTest(event) {
|
||||
console.log("捕获的媒体数据:", this.catchMedia);
|
||||
}
|
||||
|
||||
handleCompleteClearCache(event) {
|
||||
localStorage.setItem("CatCatchCatch_completeClearCache", event.target.checked ? "checked" : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动缓冲尾
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
handleAutoToBuffered(event) {
|
||||
// if (!this.autoToBufferedFlag) return;
|
||||
// this.autoToBufferedFlag = false;
|
||||
|
||||
const $autoToBuffered = this.catCatch.querySelector("#autoToBuffered");
|
||||
if (!$autoToBuffered) return;
|
||||
|
||||
localStorage.setItem("CatCatchCatch_autoToBuffered", event.target.checked ? "checked" : "");
|
||||
|
||||
const videos = document.querySelectorAll("video");
|
||||
for (let video of videos) {
|
||||
video.addEventListener("progress", (event) => {
|
||||
try {
|
||||
if (video.buffered && video.buffered.length > 0) {
|
||||
const bufferedEnd = video.buffered.end(0);
|
||||
if ($autoToBuffered.checked && bufferedEnd < video.duration) {
|
||||
video.currentTime = bufferedEnd - 5;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("处理缓冲进度失败:", error);
|
||||
}
|
||||
});
|
||||
|
||||
video.addEventListener("ended", () => {
|
||||
$autoToBuffered.checked = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS选择器 提取文件名
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
handleSetSelector(event) {
|
||||
const result = window.prompt("Selector", localStorage.getItem("CatCatchCatch_selector") ?? "");
|
||||
if (result == null) return;
|
||||
|
||||
if (result == "") {
|
||||
this.clearFileName("selector");
|
||||
return;
|
||||
}
|
||||
|
||||
let title;
|
||||
try {
|
||||
title = document.querySelector(result);
|
||||
} catch (e) {
|
||||
this.clearFileName("selector", this.i18n("fileNameError", "选择器语法错误!"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (title && title.innerHTML) {
|
||||
this.selector.innerHTML = this.stringModify(result);
|
||||
localStorage.setItem("CatCatchCatch_selector", result);
|
||||
this.getFileName();
|
||||
} else {
|
||||
this.clearFileName("selector", this.i18n("fileNameError", "表达式错误, 无法获取或内容为空!"));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 正则 提取文件名
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
handleSetRegular(event) {
|
||||
let result = window.prompt(this.i18n("regular", "文件名获取正则"), localStorage.getItem("CatCatchCatch_regular") ?? "");
|
||||
if (result == null) return;
|
||||
|
||||
if (result == "") {
|
||||
this.clearFileName("regular");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
new RegExp(result);
|
||||
this.regular.innerHTML = this.stringModify(result);
|
||||
localStorage.setItem("CatCatchCatch_regular", result);
|
||||
this.getFileName();
|
||||
} catch (e) {
|
||||
this.clearFileName("regular", this.i18n("fileNameError", "正则表达式错误"));
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心函数 代理MediaSource方法
|
||||
*/
|
||||
proxyMediaSourceMethods() {
|
||||
// 代理 addSourceBuffer 方法
|
||||
window.MediaSource.prototype.addSourceBuffer = new Proxy(window.MediaSource.prototype.addSourceBuffer, {
|
||||
apply: (target, thisArg, argumentsList) => {
|
||||
try {
|
||||
const result = Reflect.apply(target, thisArg, argumentsList);
|
||||
|
||||
// 标题获取
|
||||
setTimeout(() => { this.getFileName(); }, 2000);
|
||||
this.tips.innerHTML = this.i18n("capturingData", "捕获数据中...");
|
||||
|
||||
this.catchMedia.push({ mimeType: argumentsList[0], bufferList: [] });
|
||||
const index = this.catchMedia.length - 1;
|
||||
|
||||
// 代理 appendBuffer 方法
|
||||
result.appendBuffer = new Proxy(result.appendBuffer, {
|
||||
apply: (target, thisArg, argumentsList) => {
|
||||
Reflect.apply(target, thisArg, argumentsList);
|
||||
|
||||
if (this.enable && argumentsList[0]) {
|
||||
this.mediaSize += argumentsList[0].byteLength || 0;
|
||||
if (this.tips) {
|
||||
this.tips.innerHTML = this.i18n("capturingData", "捕获数据中...") + ": " + this.byteToSize(this.mediaSize);
|
||||
}
|
||||
this.catchMedia[index].bufferList.push(argumentsList[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("addSourceBuffer 代理错误:", error);
|
||||
return Reflect.apply(target, thisArg, argumentsList);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 代理 endOfStream 方法
|
||||
window.MediaSource.prototype.endOfStream = new Proxy(window.MediaSource.prototype.endOfStream, {
|
||||
apply: (target, thisArg, argumentsList) => {
|
||||
try {
|
||||
Reflect.apply(target, thisArg, argumentsList);
|
||||
|
||||
if (this.enable) {
|
||||
this.isComplete = true;
|
||||
if (this.tips) {
|
||||
this.tips.innerHTML = this.i18n("captureCompleted", "捕获完成");
|
||||
}
|
||||
|
||||
if (localStorage.getItem("CatCatchCatch_autoDown") == "checked") {
|
||||
setTimeout(() => this.catchDownload(), 500);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("endOfStream 代理错误:", error);
|
||||
return Reflect.apply(target, thisArg, argumentsList);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动从头捕获
|
||||
* 监控DOM变化,自动重置视频播放位置
|
||||
*/
|
||||
setupAutoRestart() {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('video').forEach((video) => this.resetVideoPlayback(video));
|
||||
|
||||
// 监控 DOM
|
||||
const observer = new MutationObserver(mutations => {
|
||||
mutations.forEach(mutation => {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
try {
|
||||
if (node.tagName === 'VIDEO') {
|
||||
this.resetVideoPlayback(node);
|
||||
} else if (node.querySelectorAll) {
|
||||
node.querySelectorAll('video').forEach(video => this.resetVideoPlayback(video));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("处理新添加的视频节点失败:", error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.body || document.documentElement, { childList: true, subtree: true });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置视频播放位置
|
||||
* @param {Object} video
|
||||
*/
|
||||
resetVideoPlayback(video) {
|
||||
if (!video) return;
|
||||
const timer = setInterval(() => {
|
||||
if (!video.paused) {
|
||||
video.currentTime = 0;
|
||||
const checkHead = this.catCatch.querySelector("#checkHead");
|
||||
if (checkHead) checkHead.checked = true;
|
||||
this.clearCache();
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// 5秒后如果还没有检测到播放,就清除定时器
|
||||
setTimeout(() => clearInterval(timer), 5000);
|
||||
|
||||
video.addEventListener('play', () => {
|
||||
if (!video.isResetCatCatch) {
|
||||
video.isResetCatCatch = true;
|
||||
video.currentTime = 0;
|
||||
const checkHead = this.catCatch.querySelector("#checkHead");
|
||||
if (checkHead) checkHead.checked = true;
|
||||
this.clearCache();
|
||||
}
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载捕获的数据
|
||||
*/
|
||||
catchDownload() {
|
||||
if (this.catchMedia.length == 0) {
|
||||
alert(this.i18n("noData", "没抓到有效数据"));
|
||||
return;
|
||||
}
|
||||
|
||||
let downloadWithFFmpeg = this.catchMedia.length >= 2 && localStorage.getItem("CatCatchCatch_ffmpeg") == "checked";
|
||||
|
||||
/**
|
||||
* 检查文件
|
||||
* 检查是否有头部文件 没有头部文件则提示 不使用ffmpeg合并
|
||||
* 检查是否有多个头部文件 根据用户选项 是否清理多于头部数据
|
||||
*/
|
||||
const checkHead = this.catCatch.querySelector("#checkHead");
|
||||
// 仅确认一次是否清除多余头部数据
|
||||
let userConfirmedHeadChoice = false;
|
||||
|
||||
for (let key in this.catchMedia) {
|
||||
if (!this.catchMedia[key]?.bufferList || this.catchMedia[key].bufferList.length <= 1) continue;
|
||||
let lastHeaderIndex = -1;
|
||||
|
||||
// 遍历所有 buffer 寻找最后一个头部
|
||||
for (let i = 0; i < this.catchMedia[key].bufferList.length; i++) {
|
||||
const data = new Uint8Array(this.catchMedia[key].bufferList[i]);
|
||||
|
||||
// 检查MP4格式的头部 (ftyp)
|
||||
if (data.length > 8 &&
|
||||
data[4] === 0x66 && // 'f'
|
||||
data[5] === 0x74 && // 't'
|
||||
data[6] === 0x79 && // 'y'
|
||||
data[7] === 0x70) // 'p'
|
||||
{
|
||||
lastHeaderIndex = i; // 持续更新直到找到最后一个头部
|
||||
}
|
||||
// 检查WebM格式的头部 (1A 45 DF A3)
|
||||
else if (data.length > 4 &&
|
||||
data[0] === 0x1A &&
|
||||
data[1] === 0x45 &&
|
||||
data[2] === 0xDF &&
|
||||
data[3] === 0xA3) {
|
||||
lastHeaderIndex = i; // 持续更新直到找到最后一个WebM头部
|
||||
}
|
||||
}
|
||||
if (lastHeaderIndex == -1) {
|
||||
alert(this.i18n("noHead", "没有检测到视频头部数据, 请使用本地工具处理"));
|
||||
downloadWithFFmpeg = false; // 没有头部数据则不使用ffmpeg合并
|
||||
}
|
||||
if (lastHeaderIndex > 0) {
|
||||
// 只有第一次遇到多余头部且用户尚未选择时才提示
|
||||
if (!userConfirmedHeadChoice && !checkHead.checked) {
|
||||
checkHead.checked = window.confirm(this.i18n("headData", "检测到多余头部数据, 是否清除?"));
|
||||
userConfirmedHeadChoice = true; // 标记已经询问过用户
|
||||
}
|
||||
|
||||
if (checkHead.checked) {
|
||||
this.catchMedia[key].bufferList.splice(0, lastHeaderIndex); // 移除最后一个头部之前的所有元素
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
downloadWithFFmpeg ? this.downloadWithFFmpeg() : this.downloadDirect();
|
||||
|
||||
if (this.isComplete) {
|
||||
if (localStorage.getItem("CatCatchCatch_completeClearCache") == "checked") { this.clearCache(); }
|
||||
if (this.tips) {
|
||||
this.tips.innerHTML = this.i18n("downloadCompleted", "下载完毕...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用FFmpeg合并下载捕获的数据
|
||||
*/
|
||||
downloadWithFFmpeg() {
|
||||
const media = [];
|
||||
for (let item of this.catchMedia) {
|
||||
if (!item || !item.bufferList || item.bufferList.length === 0) continue;
|
||||
|
||||
const mime = (item.mimeType && item.mimeType.split(';')[0]) || 'video/mp4';
|
||||
const fileBlob = new Blob(item.bufferList, { type: mime });
|
||||
const type = mime.split('/')[0] || 'video';
|
||||
|
||||
media.push({
|
||||
data: (typeof chrome == "object") ? URL.createObjectURL(fileBlob) : fileBlob,
|
||||
type: type
|
||||
});
|
||||
}
|
||||
|
||||
if (media.length === 0) {
|
||||
alert(this.i18n("noData", "没有有效数据可下载"));
|
||||
return;
|
||||
}
|
||||
|
||||
const title = this.fileName ? this.fileName.innerHTML.trim() : document.title;
|
||||
|
||||
window.postMessage({
|
||||
action: "catCatchFFmpeg",
|
||||
use: "catchMerge",
|
||||
files: media,
|
||||
title: title,
|
||||
output: title,
|
||||
quantity: media.length
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 直接下载捕获的数据
|
||||
*/
|
||||
downloadDirect() {
|
||||
const a = document.createElement('a');
|
||||
let downloadCount = 0;
|
||||
|
||||
for (let item of this.catchMedia) {
|
||||
if (!item || !item.bufferList || item.bufferList.length === 0) continue;
|
||||
|
||||
const mime = (item.mimeType && item.mimeType.split(';')[0]) || 'video/mp4';
|
||||
const type = mime.split('/')[0] == "video" ? "mp4" : "mp3";
|
||||
const fileBlob = new Blob(item.bufferList, { type: mime });
|
||||
|
||||
a.href = URL.createObjectURL(fileBlob);
|
||||
a.download = `${this.fileName ? this.fileName.innerHTML.trim() : document.title}.${type}`;
|
||||
a.click();
|
||||
|
||||
// 释放URL对象以避免内存泄漏
|
||||
setTimeout(() => URL.revokeObjectURL(a.href), 100);
|
||||
downloadCount++;
|
||||
}
|
||||
|
||||
a.remove();
|
||||
|
||||
if (downloadCount === 0) {
|
||||
alert(this.i18n("noData", "没有有效数据可下载"));
|
||||
}
|
||||
}
|
||||
|
||||
clearFileName(obj = "selector", warning = "") {
|
||||
localStorage.removeItem("CatCatchCatch_" + obj);
|
||||
const element = obj == "selector" ? this.selector : this.regular;
|
||||
if (element) element.innerHTML = this.i18n("notSet", "未设置");
|
||||
this.getFileName();
|
||||
if (warning) alert(warning);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存
|
||||
*/
|
||||
clearCache() {
|
||||
this.mediaSize = 0;
|
||||
if (this.isComplete) {
|
||||
this.catchMedia = [];
|
||||
this.isComplete = false;
|
||||
return;
|
||||
}
|
||||
|
||||
for (let key in this.catchMedia) {
|
||||
const media = this.catchMedia[key];
|
||||
if (media && media.bufferList && media.bufferList.length > 0) {
|
||||
// 保留第一个buffer块,清除其余的
|
||||
const firstBuffer = media.bufferList[0];
|
||||
media.bufferList = [firstBuffer];
|
||||
this.mediaSize += firstBuffer ? (firstBuffer.byteLength || 0) : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
byteToSize(byte) {
|
||||
if (!byte || byte < 1024) return "0KB";
|
||||
if (byte < 1024 * 1024) {
|
||||
return (byte / 1024).toFixed(1) + "KB";
|
||||
} else if (byte < 1024 * 1024 * 1024) {
|
||||
return (byte / 1024 / 1024).toFixed(1) + "MB";
|
||||
} else {
|
||||
return (byte / 1024 / 1024 / 1024).toFixed(1) + "GB";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件名
|
||||
*/
|
||||
getFileName() {
|
||||
try {
|
||||
if (!this.fileName) return;
|
||||
|
||||
if (this.setFileName) {
|
||||
this.fileName.innerHTML = this.stringModify(this.setFileName);
|
||||
return;
|
||||
}
|
||||
|
||||
let name = "";
|
||||
const selectorKey = localStorage.getItem("CatCatchCatch_selector");
|
||||
if (selectorKey) {
|
||||
const title = document.querySelector(selectorKey);
|
||||
if (title && title.innerHTML) {
|
||||
name = title.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
const regularKey = localStorage.getItem("CatCatchCatch_regular");
|
||||
if (regularKey) {
|
||||
const str = name == "" ? document.documentElement.outerHTML : name;
|
||||
const reg = new RegExp(regularKey, "g");
|
||||
let result = str.match(reg);
|
||||
if (result) {
|
||||
result = result.filter((item) => item !== "");
|
||||
name = result.join("_");
|
||||
}
|
||||
}
|
||||
|
||||
this.fileName.innerHTML = name ? this.stringModify(name) : this.stringModify(document.title);
|
||||
} catch (error) {
|
||||
console.error("获取文件名失败:", error);
|
||||
if (this.fileName) this.fileName.innerHTML = this.stringModify(document.title);
|
||||
}
|
||||
}
|
||||
|
||||
stringModify(str) {
|
||||
if (!str) return "untitled";
|
||||
|
||||
return str.replace(/['\\:\*\?"<\/>\|~]/g, function (m) {
|
||||
return {
|
||||
"'": ''',
|
||||
'\\': '\',
|
||||
'/': '/',
|
||||
':': ':',
|
||||
'*': '*',
|
||||
'?': '?',
|
||||
'"': '"',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'|': '|',
|
||||
'~': '_'
|
||||
}[m];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建并启动CatCatcher实例
|
||||
const catCatcher = new CatCatcher();
|
||||
})();
|
||||
Reference in New Issue
Block a user