// ==UserScript== // @name MSE_Video_Catcher // @name:zh-CN MSE视频缓存工具 // @namespace https://greasyfork.org/zh-CN/users/135090 // @version 0.3.3 // @description 智能拦截MSE明文分片,缓存完自动下载 // @author zwb // @match https://*.ipanda.com/*V*.shtml* // @match https://*.cctv.cn/*V*.shtml* // @match https://*.cctv.com/*V*.shtml* // @match https://*.12371.cn/*V*.shtml* // @match https://*.bilibili.com/video/* // @match https://*.acfun.cn/v/ac* // @match https://www.douyin.com/video/* // @grant none // @run-at document-start // @license LGPL-3 // @downloadURL https://update.greasyfork.icu/scripts/438368/MSE_Video_Catcher.user.js // @updateURL https://update.greasyfork.icu/scripts/438368/MSE_Video_Catcher.meta.js // ==/UserScript== (function () { 'use strict'; const S = { mode: null, chunks: [], totalBytes: 0, isCaching: false, isDone: false, hasMSE: false, videoEl: null, audioSrcCreated: false, recorder: null, recChunks: [], startTime: 0, origRate: 1, origVol: 1, origMuted: false, hasAudioStream: false, audioChunks: [], audioTotalBytes: 0, audioMimeType: null, }; const fmtB = b => b < 1024 ? b + ' B' : b < 1048576 ? (b / 1024).toFixed(1) + ' KB' : b < 1073741824 ? (b / 1048576).toFixed(2) + ' MB' : (b / 1073741824).toFixed(2) + ' GB'; function bufPct(v) { if (!v || !isFinite(v.duration) || v.duration <= 0) return 0; try { const b = v.buffered; if (!b.length) return 0; let t = 0; for (let i = 0; i < b.length; i++) t += b.end(i) - b.start(i); return Math.min(100, t / v.duration * 100); } catch (e) { console.info(e); return 0;} } function parseBoxesWithSidx(u8) { const out = []; let off = 0; const end = u8.length; while (off < end) { if (off + 8 > end) { out.push({ type: '_partial', data: u8.slice(off) }); break; } const dv = new DataView(u8.buffer, u8.byteOffset + off, Math.min(end - off, 16)); let sz = dv.getUint32(0); const type = String.fromCharCode(u8[off + 4], u8[off + 5], u8[off + 6], u8[off + 7]); if (sz === 1) { if (off + 16 > end) { out.push({ type: '_partial', data: u8.slice(off) }); break; } sz = dv.getUint32(8) * 4294967296 + new DataView(u8.buffer, u8.byteOffset + off + 12, 4).getUint32(0); } else if (sz === 0) { sz = end - off; } if (sz < 8 || off + sz > end) { out.push({ type: '_partial', data: u8.slice(off) }); break; } out.push({ type, data: u8.slice(off, off + sz), offset: off, size: sz }); off += sz; } return out; } function makeFtyp() { return new Uint8Array([0,0,0,24,0x66,0x74,0x79,0x70,0x69,0x73,0x6F,0x6D,0,0,0,0,0x69,0x73,0x6F,0x6D,0x69,0x73,0x6F,0x32,0x6D,0x70,0x34,0x31]); } function rebuildAudioFile(audioChunks, mimeType) { if (!audioChunks || !audioChunks.length) return null; let fileExtension = 'm4a', blobType = 'audio/mp4'; if (mimeType.includes('mp3')) { fileExtension = 'mp3'; blobType = 'audio/mpeg'; } else if (mimeType.includes('aac')) { fileExtension = 'aac'; blobType = 'audio/aac'; } else if (mimeType.includes('webm')) { fileExtension = 'weba'; blobType = 'audio/webm'; } else if (mimeType.includes('ogg')) { fileExtension = 'ogg'; blobType = 'audio/ogg'; } const totalSize = audioChunks.reduce((s, c) => s + c.length, 0); const result = new Uint8Array(totalSize); let offset = 0; for (const chunk of audioChunks) { result.set(chunk, offset); offset += chunk.length; } return { blob: new Blob([result], { type: blobType }), extension: fileExtension }; } function rebuildAudioMP4WithSidx(audioChunks) { return rebuildAudioFile(audioChunks, 'audio/mp4'); } function rebuildMP4WithSidx(chunks) { let ftyp = null, moov = null, tail = null; const frags = [], sidxBoxes = []; for (let i = 0; i < chunks.length; i++) { let data = chunks[i]; if (tail) { const m = new Uint8Array(tail.length + data.length); m.set(tail, 0); m.set(data, tail.length); data = m; tail = null; } const boxes = parseBoxesWithSidx(data); for (const b of boxes) { if (b.type === '_partial') { tail = b.data; continue; } switch (b.type) { case 'ftyp': if (!ftyp) ftyp = b.data; break; case 'moov': if (!moov) moov = b.data; break; case 'moof': case 'mdat': case 'emsg': case 'styp': frags.push(b); break; case 'sidx': sidxBoxes.push({ data: b.data }); break; } } } if (!ftyp) ftyp = makeFtyp(); const parts = [ftyp]; if (moov) parts.push(moov); for (const sidx of sidxBoxes) parts.push(sidx.data); for (const frag of frags) parts.push(frag.data); const total = parts.reduce((s, p) => s + p.length, 0); if (total < 1024) return null; return new Blob(parts, { type: 'video/mp4' }); } // MSE拦截器 try { const origAddSB = MediaSource.prototype.addSourceBuffer; MediaSource.prototype.addSourceBuffer = function (mime) { S.hasMSE = true; if (mime.includes('audio') || mime.includes('Audio')) { S.hasAudioStream = true; S.audioMimeType = mime; S.audioChunks = S.audioChunks || []; S.audioTotalBytes = S.audioTotalBytes || 0; } const sb = origAddSB.call(this, mime); const origEOS = MediaSource.prototype.endOfStream; this.endOfStream = function (arg) { schedDL('mse', 'MediaSource.endOfStream'); return origEOS.call(this, arg); }; const origApp = sb.appendBuffer; const origAA = sb.appendBufferAsync; const isAudio = mime.includes('audio') || mime.includes('Audio'); sb.appendBuffer = function (buf) { if (buf && buf.byteLength > 0) { if (isAudio) { S.audioChunks.push(new Uint8Array(buf).slice(0)); S.audioTotalBytes += buf.byteLength; } else { S.chunks.push(new Uint8Array(buf).slice(0)); S.totalBytes += buf.byteLength; } S.isCaching = true; } return origApp.call(this, buf); }; if (origAA) { sb.appendBufferAsync = function (buf) { if (buf && buf.byteLength > 0) { if (isAudio) { S.audioChunks.push(new Uint8Array(buf).slice(0)); S.audioTotalBytes += buf.byteLength; } else { S.chunks.push(new Uint8Array(buf).slice(0)); S.totalBytes += buf.byteLength; } S.isCaching = true; } return origAA.call(this, buf); }; } return sb; }; } catch (e) { console.info(e); } let _at = null; function schedDL(mode, reason) { if (S.isDone) return; clearTimeout(_at); _at = setTimeout(() => autoDL(mode, reason), 1000); } function autoDL(mode, reason) { if (S.isDone) return; S.isDone = true; S.isCaching = false; if (location.hostname.includes("cctv") || location.hostname.includes("12371") ){ document.querySelectorAll("video").forEach(video => video.pause()); } notify(reason+'.缓存完成,正在自动下载…'); refreshUI(); setTimeout(() => { finishDL(mode); restorePlay(); }, 400); } function bindEvents(v) { v.addEventListener('ended', () => { if (S.isCaching && !S.isDone) schedDL(S.mode || 'mse', '播放结束'); }, { once: true }); let lp = 0; const pid = setInterval(() => { if (S.isDone || !S.isCaching) { clearInterval(pid); return; } const p = bufPct(v); if (p > lp) lp = p; if (p >= 99.5 && isFinite(v.duration) && v.duration > 0) { schedDL(S.mode || 'mse', '缓冲完成'); clearInterval(pid); } }, 1000); } function restorePlay() { const v = S.videoEl; if (!v) return; try { v.pause(); v.volume = S.origVol; v.muted = true; v.playbackRate = S.origRate; v.pause(); } catch (e) { v.pause();console.info(e); } } function createPanel() { if (document.getElementById('tm-p')) return; const d = document.createElement('div'); d.id = 'tm-p'; d.innerHTML = `