145 lines
4.3 KiB
JavaScript
145 lines
4.3 KiB
JavaScript
// 资源嗅探脚本:在安装页面播放器脚本之前接管 fetch / XMLHttpRequest / createObjectURL,
|
||
// 把真正被下载的媒体地址(mp4 / m3u8 等)回报给宿主程序,用于"下载视频"按钮。
|
||
(function () {
|
||
if (window.__mediaSniffInstalled) return;
|
||
window.__mediaSniffInstalled = true;
|
||
|
||
window.__mediaUrls = [];
|
||
window.__m3u8Blobs = [];
|
||
|
||
var MEDIA_EXT =
|
||
/\.(mp4|flv|m3u8|mpd|mkv|mov|m4v|webm|f4v|3gp|mpg|mpeg|ts)(\?|#|$)/i;
|
||
var PLAIN_EXT =
|
||
/\.(jpg|jpeg|png|gif|webp|ico|svg|bmp|css|js|json|woff2?|ttf|map|txt)(\?|#|$)/i;
|
||
|
||
function isMediaUrl(u) {
|
||
if (!u) return false;
|
||
u = String(u);
|
||
if (u.indexOf("blob:") === 0 || u.indexOf("data:") === 0) return false;
|
||
if (PLAIN_EXT.test(u)) return false;
|
||
return MEDIA_EXT.test(u) || /\/(video|vod|playback|media)[\/-]/i.test(u);
|
||
}
|
||
|
||
function absUrl(u) {
|
||
try {
|
||
return new URL(String(u), location.href).href;
|
||
} catch (e) {
|
||
return String(u);
|
||
}
|
||
}
|
||
|
||
function post(msg) {
|
||
try {
|
||
if (
|
||
window.chrome &&
|
||
window.chrome.webview &&
|
||
window.chrome.webview.postMessage
|
||
) {
|
||
window.chrome.webview.postMessage(JSON.stringify(msg));
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
function remember(text) {
|
||
if (!text || typeof text !== "string") return;
|
||
if (text.indexOf("#EXTM3U") < 0) return;
|
||
if (window.__m3u8Blobs.indexOf(text) >= 0) return;
|
||
window.__m3u8Blobs.push(text);
|
||
post({ type: "hlsText", text: text });
|
||
}
|
||
|
||
function report(u) {
|
||
try {
|
||
var full = absUrl(u);
|
||
if (!isMediaUrl(full)) return;
|
||
if (window.__mediaUrls.indexOf(full) >= 0) return;
|
||
window.__mediaUrls.push(full);
|
||
// base 是发出这个请求的文档地址:播放器常在跨域 iframe 里,
|
||
// 宿主必须用它当 Referer,用顶层页面地址会被防盗链判为非法来源
|
||
post({ type: "media", url: full, base: location.href });
|
||
} catch (e) {}
|
||
}
|
||
|
||
window.__reportMedia = report;
|
||
window.__rememberHlsText = remember;
|
||
|
||
// 抖音等站点会用 atob(base64) 把播放地址还原出来,这里顺手抓一份解码结果
|
||
try {
|
||
var origAtob = window.atob;
|
||
if (origAtob) {
|
||
window.atob = function (s) {
|
||
var r = origAtob.apply(this, arguments);
|
||
try {
|
||
if (r && typeof r === "string") {
|
||
if (r.indexOf("#EXTM3U") >= 0) remember(r);
|
||
else if (isMediaUrl(r)) report(r);
|
||
}
|
||
} catch (e) {}
|
||
return r;
|
||
};
|
||
}
|
||
} catch (e) {}
|
||
|
||
try {
|
||
var origFetch = window.fetch;
|
||
if (origFetch) {
|
||
window.fetch = function (input, init) {
|
||
try {
|
||
var u =
|
||
typeof input === "string" || input instanceof URL
|
||
? input
|
||
: input && input.url;
|
||
if (u) report(u);
|
||
} catch (e) {}
|
||
return origFetch.apply(this, arguments);
|
||
};
|
||
}
|
||
} catch (e) {}
|
||
|
||
try {
|
||
var xhrOpen = XMLHttpRequest.prototype.open;
|
||
var xhrSend = XMLHttpRequest.prototype.send;
|
||
XMLHttpRequest.prototype.open = function (method, u) {
|
||
try {
|
||
if (u) report(u);
|
||
} catch (e) {}
|
||
return xhrOpen.apply(this, arguments);
|
||
};
|
||
XMLHttpRequest.prototype.send = function () {
|
||
var xhr = this;
|
||
try {
|
||
xhr.addEventListener("load", function () {
|
||
try {
|
||
if (xhr.responseType === "" || xhr.responseType === "text") {
|
||
remember(xhr.responseText);
|
||
} else if (
|
||
xhr.responseType === "blob" &&
|
||
xhr.response &&
|
||
xhr.response.text
|
||
) {
|
||
xhr.response.text().then(remember);
|
||
}
|
||
} catch (e) {}
|
||
});
|
||
} catch (e) {}
|
||
return xhrSend.apply(this, arguments);
|
||
};
|
||
} catch (e) {}
|
||
|
||
// MSE 播放时 video.src 只能是 blob:,抓不到 http 地址,所以靠上面的分片嗅探兜底;
|
||
// 这里额外记录 createObjectURL 的调用,方便识别"确实在用 MSE"。
|
||
try {
|
||
var ocu = window.URL && window.URL.createObjectURL;
|
||
if (ocu) {
|
||
window.URL.createObjectURL = function (obj) {
|
||
var u = ocu.apply(this, arguments);
|
||
try {
|
||
window.__usesMse = true;
|
||
if (obj && typeof obj.text === "function") obj.text().then(remember);
|
||
} catch (e) {}
|
||
return u;
|
||
};
|
||
}
|
||
} catch (e) {}
|
||
})();
|