您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
捕获HTML视频流,并下载的实现。理论上可以捕获任意在线视频或直播的媒体流
当前为
// ==UserScript== // @name 媒体流捕获 // @namespace https://github.com/Momo707577045/media-source-extract // @version 0.2.4 // @description 捕获HTML视频流,并下载的实现。理论上可以捕获任意在线视频或直播的媒体流 // @license AGPL-3.0 // @author Momo707577045 // @include * // @exclude http://blog.luckly-mjw.cn/tool-show/media-source-extract/player/player.html // @grant none // @run-at document-start // ==/UserScript== (() => { 'use strict'; if (document.getElementById('media-source-capture')) { return; } let isClose = false, isEndOfStream = false; let sourceBufferList = []; let $btnDownload = document.createElement('div'); let $downloadNum = document.createElement('div'); let $tenRate = document.createElement('div'); // 16倍速播放 let $closeBtn = document.createElement('div'); // 关闭 $closeBtn.innerHTML = ` <div style="margin-top: 4px; height: 34px; width: 34px; line-height: 34px; display: inline-block; border-radius: 50px; background-color: rgba(0, 0, 0, 0.5);" id="m3u8-close"> <img style="padding-top: 4px; width: 24px; cursor: pointer;" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAk1BMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ROyVeAAAAMHRSTlMA1Sq7gPribxkJx6Ey8onMsq+GTe10QF8kqJl5WEcvIBDc0sHAkkk1FgO2ZZ+dj1FHfPqwAAACNElEQVRIx6VW6ZqqMAwtFlEW2Rm3EXEfdZa+/9PdBEvbIVXu9835oW1yjiQlTWQE/iYPuTObOTzMNz4bQFRlY2FgnFXRC/o01mytiafP+BPvQZk56bcLSOXem1jpCy4QgXvRtlEVCARfUP65RM/hp29/+0R7eSbhoHlnffZ8h76e6x1tyw9mxXaJ3nfTVLd89hQr9NfGceJxfLIXmONh6eNNYftNSESRmgkHlEOjmhgBbYcEW08FFQN/ro6dvAczjhgXEdQP76xHEYxM+igQq259gLrCSlwbD3iDtTMy+A4Yuk0B6zV8c+BcO2OgFIp/UvJdG4o/Rp1JQYXeZFflPEFMfvugiFGFXN587YtgX7C8lRGFXPCGGYCCzlkoxJ4xqmi/jrIcdYYh5pwxiwI/gt7lDDFrcLiMKhBJ//W78ENsJgVUsV8wKpjZBXshM6cCW0jbRAilICFxIpgGMmmiWGHSIR6ViY+DPFaqSJCbQ5mbxoZLIlU0Al/cBj6N1uXfFI0okLppi69StmumSFQRP6oIKDedFi3vRDn3j6KozCZlu0DdJb3AupJXNLmqkk9+X9FEHLt1Jq8oi1H5n01AtRlvwQZQl9hmtPY4JEjMDs5ftWJN4Xr4lLrV2OHiUDHCPgvA/Tn/hP4zGUBfjZ3eLJ+NIOfHxi8CMoAQtYfmw93v01O0e7VlqqcCsXML3Vsu94cxnb4c7ML5chG8JIP9b38dENGaj3+x+TpiA/AL/fen8In7H8l3ZjdJQt2TAAAAAElFTkSuQmCC"> </div>`; doMediaSource(window.MediaSource); doMediaSource(window.BwpMediaSource); // 16倍速播放 function tenRatePlay(rate) { setTimeout(() => { let $domList = document.querySelectorAll('video,bwp-video'); for (let i = 0, length = $domList.length; i < length; i++) { const $dom = $domList[i]; $dom.playbackRate = rate; $dom.muted = rate !== 1; } }); } // 下载资源 function download() { setTimeout(() => { const date = new Date(); for (const target of sourceBufferList) { const mime = target.mime.split(';'); console.log(target); const type = mime[0].split("/"); const fileBlob = new Blob(target.bufferList, {type: mime}); // 创建一个Blob对象,并设置文件的 MIME 类型 const a = document.createElement('a'); a.download = `${type[0]}_${date.getFullYear().toString().padStart(4, '0')}${date.getMonth().toString().padStart(2, '0')}${date.getDay().toString().padStart(2, '0')}_${date.getHours().toString().padStart(2, '0')}${date.getMinutes().toString().padStart(2, '0')}${date.getSeconds().toString().padStart(2, '0')}_${mime[1]}.${type[1]}`; a.href = URL.createObjectURL(fileBlob); a.style.display = 'none'; document.body.appendChild(a); a.click(); a.remove(); if (isEndOfStream === true) { sourceBufferList = []; $downloadNum.innerHTML = `已捕获 0 个片段`; isEndOfStream = false; showTip('视频已下载'); } } }); } // BWP(Bilibili Web Player) 是哔哩哔哩为实现软解 HEVC 而通过 Shadow DOM API 封装了一个 <bwp-video> 标签,并实现了一套 BWP MSE API 作者:哔哩哔哩技术 https://www.bilibili.com/read/cv16257864 出处:bilibili function doMediaSource(MediaSource) { if (MediaSource) { let endOfStream = MediaSource.prototype.endOfStream; MediaSource.prototype.endOfStream = function () { if (!isClose) { isEndOfStream = true; $downloadNum.innerHTML = `已捕获到终点,请下载`; showTip('已捕获到终点,请下载'); endOfStream.call(this); } } let addSourceBuffer = MediaSource.prototype.addSourceBuffer MediaSource.prototype.addSourceBuffer = function (mime) { if (!isClose) { if (isEndOfStream) { if (confirm('检测到新的视频流,是否下载已捕获?\n点击“确定”下载已捕获\n点击“取消”重新捕获')) { download(); } } appendDom(); let sourceBuffer = addSourceBuffer.call(this, mime); let append = sourceBuffer.appendBuffer; let bufferList = []; sourceBufferList.push({ mime, bufferList, }); sourceBuffer.appendBuffer = function (buffer) { $downloadNum.innerHTML = `已捕获 ${sourceBufferList[0].bufferList.length} 个片段`; bufferList.push(buffer); append.call(this, buffer); } return sourceBuffer; } } } } // 添加操作的 dom function appendDom() { if (document.getElementById('media-source-capture')) { return; } const baseStyle = `position: fixed; top: 50px; right: 50px; height: 40px; padding: 0 20px; z-index: 99999; color: white; cursor: pointer; font-size: 16px; font-weight: bold; line-height: 40px; text-align: center; border-radius: 4px; background-color: #3498db; box-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.3);`; $tenRate.innerHTML = '16倍速捕获(静音)'; $downloadNum.innerHTML = `已捕获 0 个片段`; $btnDownload.innerHTML = '下载已捕获片段'; $btnDownload.id = 'media-source-capture'; $tenRate.style = baseStyle + `top: 150px;`; $btnDownload.style = baseStyle + `top: 100px;`; $downloadNum.style = baseStyle; $closeBtn.style = `position: fixed; top: 200px; right: 50px; text-align: center; z-index: 99999; cursor: pointer;`; $btnDownload.addEventListener('click', download); $tenRate.addEventListener('click', function () { if ($tenRate.innerHTML === '16倍速捕获(静音)') { tenRatePlay(16); $tenRate.innerHTML = '恢复正常倍速音量'; } else { tenRatePlay(1); $tenRate.innerHTML = '16倍速捕获(静音)'; } }); $closeBtn.addEventListener('click', function () { $btnDownload.remove(); $downloadNum.remove(); $closeBtn.remove(); $tenRate.remove(); sourceBufferList = []; isClose = true; }); let $html = document.querySelector("html"), $head = document.querySelector('head'); $html.insertBefore($tenRate, $head); $html.insertBefore($downloadNum, $head); $html.insertBefore($btnDownload, $head); $html.insertBefore($closeBtn, $head); } if (window === top) { window.addEventListener("message", event => { if (event.source !== window) { try { let sql = event.data.split("\x00"); if (sql[0] === "showTip" && sql[1].constructor === String) { if (sql[2]) showTip(sql[1], sql[2]); } } catch (e) { // 排除 下标越界错误 及 指令处理错误 } } }); } function showTip(msg, style = ``) { // 该函数需要在top内运行,否则可能显示异常 let root = document.querySelector(`:root`); if (window === top) { let tip = document.querySelector(`:root > tip`); if (tip && tip.nodeType === 1) { clearInterval(parseInt(tip.getAttribute("interval"))); // 防止中途新的showTip事件创建多个tip造成卡顿 tip.innerHTML = `<style>@keyframes showTip {0%{opacity: 1;} 66.67%{opacity: 1;} 100%{opacity: 0;}}</style>\n` + msg; let time = msg.replace(/\s/, ``).length / 1.6; // TODO 2个字/秒 // cubic-bezier(起始点, 起始点偏移量, 结束点偏移量, 结束点),这里的 cubic-bezier函数 表示动画速度的变化规律 tip.style.animation = `showTip ` + (time > 2.5 ? time : 2.5) + `s cubic-bezier(0,` + ((time - 1) > 0 ? (time - 1) / time : 0) + `,` + (1 - ((time - 1) > 0 ? (time - 1) / time : 0)) + `,1) 1 normal`; } else { tip = document.createElement(`tip`); // pointer-events: none; 禁用鼠标事件,input标签使用 disabled='disabled' 禁用input标签 tip.style = style + `pointer-events: none; opacity: 0; background-color: #222a; color: #fff; font-family: 微软雅黑,黑体,Droid Serif,Arial,sans-serif; font-size: 20px; text-align: center; padding: 6px; border-radius: 16px; position: fixed; transform: translate(-50%, -50%); left: 50%; bottom: 15%; z-index: 2147483647;`; tip.innerHTML = `<style>@keyframes showTip {0%{opacity: 0;} 33.34%{opacity: 1;} 66.67%{opacity: 1;} 100%{opacity: 0;}}</style>\n` + msg; let time = msg.replace(/\s/, ``).length / 2; // TODO 2个字/秒 // cubic-bezier(起始点, 起始点偏移量, 结束点偏移量, 结束点),这里的 cubic-bezier函数 表示动画速度的变化规律 tip.style.animation = `showTip ` + (time > 2 ? time : 2) + `s cubic-bezier(0,` + ((time - 1) > 0 ? (time - 1) / time : 0) + `,` + (1 - ((time - 1) > 0 ? (time - 1) / time : 0)) + `,1) 1 normal`; root.appendChild(tip); } tip.setAttribute("interval", String(setTimeout(() => { try { root.removeChild(tip); } catch (e) { // 排除root没有找到tip } }, time * 1000))); } else { top.postMessage("showTip\x00" + msg + "\x00" + style, "*"); } } })();