// ==UserScript== // @name 媒体流捕获 // @namespace https://github.com/Momo707577045/media-source-extract // @version 0.2 // @description https://github.com/Momo707577045/media-source-extract 配套插件 // @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 // @downloadURL none // ==/UserScript== (() => { 'use strict'; if (document.getElementById('media-source-capture')) { return; } doMediaSource(window.MediaSource); doMediaSource(window.BwpMediaSource); 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 = `
`; // 16倍速播放 function tenRatePlay() { setTimeout(() => { let $domList = document.querySelectorAll('video,bwp-video'); for (let i = 0, length = $domList.length; i < length; i++) { const $dom = $domList[i]; $dom.playbackRate = 16; $dom.muted = true; } }); } // 下载资源 function download() { setTimeout(() => { const date = new Date(); for (const target of sourceBufferList) { const mime = target.mime.split(';')[0]; const type = mime.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')}.${type[1]}`; a.href = URL.createObjectURL(fileBlob); a.style.display = 'none'; document.body.appendChild(a); a.click(); a.remove(); if (isEndOfStream === true) { sourceBufferList = []; isEndOfStream = false; } } }); } // BWP(Bilibili Web Player) 是哔哩哔哩为实现软解 HEVC 而通过 Shadow DOM API 封装了一个 标签,并实现了一套 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; showTip(`已捕获到终点,请下载`); $downloadNum.innerHTML = `已捕获到终点,请下载`; 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', tenRatePlay); $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); } })(); function showTip(msg, style = ``) { // 该函数需要在top内运行,否则可能显示异常 let root = document.querySelector(`:root`); if (window === top) { let tip = document.querySelector(`:root > tip`); if (tip && tip.nodeType === 1) { // 防止中途新的showTip事件创建多个tip造成卡顿 root.removeChild(tip); } 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 = `\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); setTimeout(() => { try { root.removeChild(tip); } catch (e) { // 排除root没有找到tip } }, time * 1000); } else { top.showTip(msg, style); } }