您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
支持大部分网页视频、直播錄影 / 视频录制 / 录制视频
// ==UserScript== // @name 文文錄影机 // @namespace moe.moekai.aya.videorecorder // @version 2.5 // @description 支持大部分网页视频、直播錄影 / 视频录制 / 录制视频 // @author YIU // @include * // @icon https://any.moest.top/monkeydoc/res/ayavrec.ico // @run-at document-start // @grant unsafeWindow // @grant GM_registerMenuCommand // @grant GM_openInTab // @grant GM_getValue // @grant GM_setValue // @require https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js // @license GPL-3.0 // @compatible chrome 76+ // @compatible firefox 70+ // @supportURL https://github.com/usaginya/mkAppUpInfo/tree/master/monkeyjs // @homepageURL https://github.com/usaginya/mkAppUpInfo/tree/master/monkeyjs // ==/UserScript== //-- 以下格式转换方式仅供参考、推荐使用小丸工具箱等其它转换工具 //- 可以使用下面的ffmpeg命令直接转换格式为mp4(非标准mp4) //ffmpeg -i WebVideo.webm -strict -2 -c copy output.mp4 //- 转为一般恒定mp4(二次转换,-r限制帧率避免爆帧,-crf数值越小体积越大质量越好,建议为21左右) //ffmpeg -i WebVideo.webm -r 60 -crf 21 output.mp4 (function ($) { 'use strict'; //VV 全局变量定义 --- let initialIsDone; let gmMenuUiId; let selectedMimeTypeId; let supportedMimeTypes; let buttonShowMode; //## 注册脚本菜单 -- if (!gmMenuUiId) { gmMenuUiId = GM_registerMenuCommand('设置 · Settings', gmMenuUiEvent); } //## 脚本菜单事件 - 创建菜单界面 function gmMenuUiEvent() { // 切换编码类型菜单 if (!supportedMimeTypes) { createSupportedMimeType(); } if (!initialIsDone) { selectedMimeTypeId = parseInt(GM_getValue('MimeTypeId')); } let menuMimeTypeItems = []; for (let id in supportedMimeTypes) { let item = { id: id, group: 'gmayavrradiobtn-mimetype', title: supportedMimeTypes[id].tips ? (id < 1 ? supportedMimeTypes[id].type : supportedMimeTypes[id].tips) : supportedMimeTypes[id].type, tips: supportedMimeTypes[id].tips ? (id < 1 ? supportedMimeTypes[id].tips : supportedMimeTypes[id].type) : null, selected: (selectedMimeTypeId && selectedMimeTypeId == id || !selectedMimeTypeId && id < 1), isLast: id < 1, onSelected: () => { selectedMimeTypeId = id; forwardCommandToIframe('changemimetypeid', selectedMimeTypeId); } }; menuMimeTypeItems.push(item); } // 切换錄影按钮菜单 if (!initialIsDone) { loadSiteButtonShowMode(); } let btnModes = [ {id: 0 , title: '悬停显示', tips: '鼠标指针在视频上时显示'}, {id: 1 , title: '总是显示'}, {id: 2 , title: '不显示'}, {group: 'gmayavrradiobtn-bsmlayer', id: 10 , title: '内层', tips: '按钮在影视同一层'}, {group: 'gmayavrradiobtn-bsmlayer', id: 11 , title: '中层', tips: '按钮在影视相同的区域'}, {group: 'gmayavrradiobtn-bsmlayer', id: 12 , title: '外层', tips: '按钮在影视区域外层、被什么遮挡的话可以尝试选择'} ]; let menuBottomShowModeItems = []; btnModes.forEach((mode) => { let item = { group: mode.group, id: mode.id, title: mode.title, tips: mode.tips, selected: () => { if (mode.group != 'gmayavrradiobtn-bsmlayer') { return buttonShowMode.mode && buttonShowMode.mode == mode.id || !buttonShowMode.mode && mode.id < 1; } return buttonShowMode.layer && buttonShowMode.layer == mode.id || !buttonShowMode.layer && mode.id < 11; }, onSelected: () => { let btnSM = { mode: buttonShowMode.mode, layer: buttonShowMode.layer }; let newBtnSM = { mode: mode.group != 'gmayavrradiobtn-bsmlayer' ? mode.id : btnSM.mode, layer: mode.group === 'gmayavrradiobtn-bsmlayer' ? mode.id : btnSM.layer }; // 改变层之前先移除按钮 if (mode.group === 'gmayavrradiobtn-bsmlayer') { buttonShowMode.mode = 2; initialization(); } // 等待删除后再绑定按钮 setTimeout(() => { buttonShowMode.mode = newBtnSM.mode; buttonShowMode.layer = newBtnSM.layer; initialization(); saveSiteButtonShowMode(); // 向子窗口页面发送重新绑定指令,必须延迟发送,否则保存设置有冲突 forwardCommandToIframe('rebind', newBtnSM); }, 300); } }; menuBottomShowModeItems.push(item); }); // 构建菜单参数 let menu = { title: { href: 'https://greasyfork.org/scripts/430752' }, tabs: { 'ButtonShowMode': { title: '錄影按钮显示', content: { radioButton: { column: 3, items: menuBottomShowModeItems } } }, 'MimeType': { title: '视频编码类型', content: { radioButton: { configName: 'MimeTypeId', column: 4, items: menuMimeTypeItems } } } //- tabs end - }, }; gmAyaUiCreate(menu); } //## 载入当前网站錄影按钮显示方式 function loadSiteButtonShowMode() { if (!buttonShowMode) { buttonShowMode = { host: location.host, mode: 0, layer: 10 }; } let siteButtonShowMode = GM_getValue('siteButtonShowMode'); siteButtonShowMode = !siteButtonShowMode ? [] : siteButtonShowMode; siteButtonShowMode = siteButtonShowMode.filter((btnsm) => btnsm.host == buttonShowMode.host); buttonShowMode = siteButtonShowMode.length > 0 ? siteButtonShowMode[0] : buttonShowMode; } //## 保存当前网站錄影按钮显示方式 function saveSiteButtonShowMode() { if (!buttonShowMode || !buttonShowMode.host) { return; } let siteButtonShowMode = GM_getValue('siteButtonShowMode'); siteButtonShowMode = !siteButtonShowMode ? [] : siteButtonShowMode; if (siteButtonShowMode == []) { siteButtonShowMode.push(buttonShowMode); GM_setValue('siteButtonShowMode', siteButtonShowMode); return; } siteButtonShowMode = siteButtonShowMode.filter((btnsm) => btnsm.host != buttonShowMode.host); if (buttonShowMode.mode > 0 || buttonShowMode.layer > 10){ siteButtonShowMode.push(buttonShowMode); } GM_setValue('siteButtonShowMode', siteButtonShowMode); } /** 格式化编码类型 * @param {array} type 被格式化的编码类型(webm/vp9)[1:编码格式(webm..), 2:编码类型(vp9..)] */ function formatSupportedMimeType(type) { return /^(.*?)\/(.*?)$/gi.exec(type); } //## 创建支持的编码类型 -- function createSupportedMimeType() { let types = [ { id: 0, type: 'Default', tips: 'webm'}, { id: 1, type: 'webm/vp9' },{ id: 2, type: 'webm/vp8' }, { id: 3, type: 'webm/h265' },{ id: 4, type: 'webm/h264' }, { id: 5, type: 'webm/av1' },{ id: 6, type: 'webm/avc1' }, { id: 7, type: 'x-matroska/vp9', tips: 'mkv/vp9' },{ id: 8, type: 'x-matroska/vp8', tips: 'mkv/vp8' }, { id: 9, type: 'x-matroska/h265', tips: 'mkv/h265' },{ id: 10, type: 'x-matroska/h264', tips: 'mkv/h264' }, { id: 11, type: 'x-matroska/av1', tips: 'mkv/av1' },{ id: 12, type: 'x-matroska/avc1', tips: 'mkv/avc1' }, ]; supportedMimeTypes = {}; types.forEach(function(v){ let type = formatSupportedMimeType(v.type); type = v.id < 1 ? '/webm' : `/${type[1]}\;codecs=${type[2]},opus`; if (MediaRecorder.isTypeSupported(`video${type}`)) { supportedMimeTypes[v.id] = v; } }); } //## 获取当前的编码类型字符串 function getSelectedMimeTypeString() { let selectedMimeType = 'video/webm'; if (!supportedMimeTypes) { createSupportedMimeType(); } if (!selectedMimeTypeId || selectedMimeTypeId < 1 || !supportedMimeTypes[selectedMimeTypeId]) { return selectedMimeType; } selectedMimeType = formatSupportedMimeType(supportedMimeTypes[selectedMimeTypeId].type); return `video/${selectedMimeType[1]}\;codecs=${selectedMimeType[2]},opus`; } /** ====== 文文GM设置界面窗口 ====== * @param {object} menu.title { <string>text: 窗口标题(可选), <string>href: 链接(可选) } * @param {function} menu.onCloseing 窗口被关闭时执行的回调方法(可选) * @param {object} menu.tabs 选项卡页面组 * @param {objectName} menu.tabs.tabId 选项卡页面索引(only) * @param {string} menu.tabs.tabId.title 选项卡标题文字 * @param {string} menu.tabs.tabId.content 选项卡内容 * -- 选项卡内容对象 ---------------------------- * -- 单选按钮组 -------------------------- * radioButton: { * <Array> items: [{ * <int> id: 选项索引, * <string> group: 选项分组(可选), * <string> title: 选项标题, * <string> tips: 选项提示(可选), * <bool|int|function> selected: 选项是否选中(only/可选), * <bool> isLast: 选项是否排在最后(only/可选), * <function> onSelected: 选项被选中时执行的回调方法(可选) * }], * <int> column: 每行选项显示个数(1~5)(可选), * <string> configName: 存储设置名 \ 将会根据 items[i].id 索引保存(可选) * } */ function gmAyaUiCreate(menu) { if (!menu || !menu.tabs) { return; } if ($('#gmayaui').length > 0) { gmAyaUiRemove(() => gmAyaUiCreate(menu)); return; } let uiDom = $(`<div class="gmayauibg"><div id="gmayaui"><style> .gmayauibg{position:fixed;display:flex!important;width:100%;height:100%;top:0;left:0;right:0;bottom:0; align-content:center;justify-content:center;flex-wrap:wrap;background-color:#fff1;z-index:666666!important} #gmayaui{margin:0 2vh;min-width:300px;min-height:300px;box-shadow:0 0 16px #2bf6;background-color:#fffc;display:none; border-radius:5px;backdrop-filter:blur(6px);padding:12px;user-select:none;-webkit-user-select:none; box-sizing:unset;-moz-user-select:none;-moz-box-sizing:unset;z-index:6} #gmayaui,#gmayaui div,#gmayaui label,#gmayaui li,#gmayaui span{outline:0!important;text-align:center!important; font-weight:400!important;font-family:'Microsoft YaHei',Helvetica,'宋体',Tahoma,Arial,sans-serif!important; font-size:12pt!important;border:0!important} #gmayaui a{color:unset!important;text-decoration:none!important;transition:color .3s} #gmayaui a:hover{color:#08a5ef!important} #gmayaui .head{position:relative;display:inline-block;width:100%} #gmayaui .head .title{margin:0 4vh;color:#666!important;font-size:14pt!important} #gmayaui .close{position:absolute;display:inline-block;width:18px;height:18px;right:2px;overflow:hidden} #gmayaui .close::before{-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);transform:rotate(45deg)} #gmayaui .close::after{-webkit-transform:rotate(-45deg);-moz-transform:rotate(-45deg);transform:rotate(-45deg)} #gmayaui .close::after,#gmayaui .close::before{content:'';position:absolute;height:6px;width:100%;top:50%;left:0; margin-top:-3px;background:#91989FCC;border-radius:4px 0;transition:background .5s} #gmayaui .close:focus::after,#gmayaui .close:focus::before,#gmayaui .close:hover::after, #gmayaui .close:hover::before{background:#08a5ef;transition:background .5s} #gmayaui .body{margin-top:2vh} #gmayaui .wrap{position:relative;width:auto!important;height:auto!important;margin:5px;flex:1 0 50%} #gmayaui .wrap.w2{flex:1 0 40%}#gmayaui .wrap.w3{flex:1 0 30%} #gmayaui .wrap.w4{flex:1 0 20%}#gmayaui .wrap.w5{flex:1 0 10%} #gmayaui .item{color:#fff!important;background-color:#91989F77;position:relative;box-shadow:0 0 0 5px #0000; padding:5px 8px;border-radius:5px;transition:.5s;cursor:pointer} #gmayaui .item:focus,#gmayaui .item:hover{background-color:#30547777} #gmayaui label{display:unset;margin:unset;padding:unset} #gmayaui input[type=radio]{display:none!important} #gmayaui input:checked+label .item{box-shadow:0 0 3px 1px #88ceff;background-color:#08a5ef} #gmayaui .content,#gmayaui .contenttips{line-height:normal!important} #gmayaui .contenttips::after{content:attr(tooltip);top:0;left:50%;width:100%;background-color:#ffffffe6; border-radius:8px;color:#e07a22!important;padding:10px;position:absolute;text-align:center;z-index:66; backdrop-filter:blur(2px);font-size:10pt!important;white-space:pre-wrap;box-shadow:0 0 8px #e827;opacity:0; transition:.5s;-webkit-transform:translate(-50%,calc(-100% - 10px)); transform:translate(-50%,calc(-100% - 10px));pointer-events:none} #gmayaui .contenttips::before{content:'';position:absolute;display:none;top:0;left:50%;background-color:#0000; width:0;height:0;z-index:66;backdrop-filter:blur(2px);border-left:solid 10px #0000;border-bottom:solid 10px #fffd; -webkit-transform:translate(-50%,calc(-100% - 5px)) rotate(45deg); transform:translate(-50%,calc(-100% - 5px)) rotate(45deg)} #gmayaui .contenttips:focus::after,#gmayaui .contenttips:focus::before,#gmayaui .contenttips:hover::after, #gmayaui .contenttips:hover::before{opacity:1;transition:.5s} #gmayavruibgclose{position:absolute;width:100%;height:100%} #gmayaui .tabs{position:relative;margin:0 auto;width:100%;left:0!important;top:0!important;right:0!important; bottom:0!important;padding:unset!important;display:block!important} #gmayaui .tabs nav{background:none!important;box-shadow:none!important;position: relative!important} #gmayaui .tabs nav ul{position:relative!important;display:flex!important;margin:0 auto!important;padding:0!important;list-style:none!important; flex-flow:row wrap!important;justify-content:center!important} #gmayaui .tabs nav ul li{position:relative!important;display:block!important;color:#999!important;margin:0 .5em;flex:1;line-height:2.5; -webkit-transition:color .3s;transition:color .3s} #gmayaui .tabs nav ul li:focus,#gmayaui .tabs nav ul li:hover{color:#779} #gmayaui .tabs nav ul li::before{content:'';position:absolute!important;top:0;left:0;z-index:-1;width:100%;height:100%; background:#fff6;clip-path:inset(92% 0 0 0);-webkit-transition:background-color .3s; transition:background-color .3s} #gmayaui .tabs nav ul li:focus::before,#gmayaui .tabs nav ul li:hover::before{background:#aab} #gmayaui .tabs nav ul li::after{content:'';position:absolute;left:48%;bottom:-2px;width:0;height:0; margin-bottom:5px;z-index:-1;background:linear-gradient(135deg,#08a5ef 0,#08a5ef 50%,transparent 50%,transparent 100%); transform:rotate(225deg);-webkit-transition:bottom .3s,width .3s,height .3s;transition:bottom .3s,width .3s,height .3s} #gmayaui .tabs nav ul li.tab-current,#gmayaui .tabs nav ul li.tab-current:focus, #gmayaui .tabs nav ul li.tab-current:hover{color:#08a5ef} #gmayaui .tabs nav ul li.tab-current::before{background:#08a5ef} #gmayaui .tabs nav ul li.tab-current::after{bottom:-8px;width:10px;height:10px} #gmayaui .content-wrap section{display:none;margin:0 auto;padding-top:1em;text-align:center} #gmayaui .content-wrap section.content-current{display:block;animation:gmayauiani-show-tab-content ease .5s} #gmayaui .content-wrap{position:relative} #gmayaui .tab-content{display:flex!important;flex-wrap:wrap;flex-direction:row} @keyframes gmayauiani-show-tab-content{0%{opacity:0;clip-path:inset(0 0 60% 0)}100%{opacity:1;clip-path:inset(0)}} </style> <div class="head"><div class="title"> <a href="${menu.title && menu.title.href ? menu.title.href : 'javascript:;'}" target="_blank"> ${menu.title && menu.title.text ? menu.title.text : GM_info.script.name} </a><span class="close" tabindex="0"></span></div></div> <div class="body"><div class="tabs"><nav><ul></ul></nav><div class="content-wrap"></div></div></div> </div><div id="gmayavruibgclose"></div></div>`); // 绑定窗口事件 $([uiDom.find('#gmayavruibgclose'), uiDom.find('.close')]).each(function() { this.click(() => { gmAyaUiRemove(); if (menu.onCloseing) { menu.onCloseing(); } }); }); // 构建选项卡页内容 let fastTabId = Object.keys(menu.tabs)[0]; for (let tabId in menu.tabs) { if (!menu.tabs.hasOwnProperty(tabId)){ continue; } // 选项卡栏 let tabli = `<li${ fastTabId && tabId === fastTabId ? ' class="tab-current"' : '' }> ${menu.tabs[tabId].title}</li>`; tabli = $(tabli); tabli.click(function () { uiDom.find('.tabs li.tab-current').removeClass('tab-current'); $(this).addClass('tab-current'); uiDom.find('section.content-current').removeClass('content-current'); uiDom.find(`section#gmayaui-${tabId}`).addClass('content-current'); }); uiDom.find('.tabs>nav>ul').append(tabli); // 选项卡内容框架 let tabSection = `<section id="gmayaui-${tabId}"`; if (fastTabId && tabId === fastTabId) { tabSection += ` class="content-current"`; fastTabId = undefined; } tabSection += `><div class="tab-content"></div></section>` tabSection = $(tabSection); // 生成选项卡内容 for (let contentKey in menu.tabs[tabId].content) { let content = menu.tabs[tabId].content; if (!content.hasOwnProperty(contentKey)){ continue; } content = content[contentKey]; // 单选按钮组 if (/^radioButton$/i.test(contentKey)){ let column = content.column; let configName = content.configName; let itemDom = undefined; let itemLastDom = undefined; let items = content.items; for (let i in items) { let item = items[i]; let itemGroup = items[i].group ? items[i].group : 'gmayaui-radiobutton'; let itemBtn = $(`<input type="radio" name="${itemGroup}" id="${itemGroup}${i}" />`); itemBtn.click(function () { if (item.onSelected) { item.onSelected(); } if (configName) { GM_setValue(configName, item.id); } }); let itemBtnContent = $(`<label for="${itemGroup}${i}"> <div class="item ${item.tips ? 'contenttips' : 'content'}" ${item.tips ? `tooltip="${item.tips}"` : ''}>${item.title}</div></label>`); if (item.selected) { if (/boolean|number/i.test(typeof(item.selected))) { itemBtn.prop('checked', item.selected); } else if (/function/i.test(typeof(item.selected))) { itemBtn.prop('checked', item.selected()); } } itemDom = $(`<div class="wrap${ column > 1 && column < 6 ? ` w${column}` : '' }"></div>`); itemDom.append(itemBtn).append(itemBtnContent); if (item.isLast) { itemLastDom = itemDom; continue; } tabSection.find('.tab-content').append(itemDom); } tabSection.find('.tab-content').append(itemLastDom); } //- radioButton end - } //- 生成选项卡内容 end - // 装载选项卡内容 uiDom.find('.content-wrap').append(tabSection); } // 显示界面 $('body').append(uiDom); uiDom.children(':first').fadeIn('fast'); } /** 移除设置界面窗口 * @param {function} callback 关闭窗口后执行的回调方法 */ function gmAyaUiRemove(callback) { $('#gmayaui').fadeOut('fast', function(){ $(this).parent().remove(); if (callback) { callback(); } }); } //====== 文文GM设置界面窗口 END ====== //## Catch error event function catchErrorEvent(err, videoObj){ if (/NotSupportedError/gi.test(err.toString())) { alert(`${GM_info.script.name} - 錄影不支持\n请尝试在脚本设置中切换「视频编码类型」`); return; } if (/SecurityError/gi.test(err.toString())) { alert(`${GM_info.script.name} - 錄影权限不足\n无法对跨域的视频进行錄影`); if (!videoObj) { return; } let testVideoUri = videoObj.src; let testVideoSourceDom = $(videoObj).find('source:first')[0]; if (!testVideoUri && testVideoSourceDom) { testVideoUri = testVideoSourceDom.src; } if (!testVideoUri || /^blob:/i.test(testVideoUri)) { return; } if (confirm(`${GM_info.script.name}\n发现源地址\n要尝试在新页面打开吗?`)) { let openUri = /\.m3u8$/gi.test(testVideoUri) ? `https://any.moest.top/m3u8get/?source=${testVideoUri}` : testVideoUri; openUrl(openUri); } setTimeout(() => videoObj.pause(), 100); return; } console.error('Aya Video Recorder', err); alert(`${GM_info.script.name} - 发生意外错误\n${err}`); } //## Video recording extension method function ExtensionVideoRecorder() { unsafeWindow.HTMLVideoElement.prototype.record = async function (duration_seconds = 60, btnDom = null) { let video; try { video = this instanceof unsafeWindow.HTMLVideoElement ? this : document.querySelector('video'); video.captureStream = video.captureStream || video.mozCaptureStream; let stream = video.captureStream(60); let mimeType = getSelectedMimeTypeString(); const recOption = { mimeType: mimeType }; let recorder = new MediaRecorder(stream, recOption); let stopRecord = () => { if (recorder.state === 'recording' || recorder.state === 'paused') { recorder.stop(); } }; let pauseRecord = (setResume) => { if(!setResume && recorder.state === 'recording') { recorder.pause(); return; } if(setResume && recorder.state === 'paused') { recorder.resume(); } }; let formatSeconds = (second) => { let h = Math.floor(second / 3600) let m = Math.floor(second / 60 % 60); let s = Math.floor(second % 60); return `${h < 10 ? `0${h}` : h}:${m < 10 ? `0${m}` : m}:${s < 10 ? `0${s}` : s}`; }; if (btnDom) { btnDom[0].recS = 0; btnChangeState(btnDom, 1); btnDom[0].recTimeCalc = setInterval(() => { if (recorder.state === 'paused') { btnChangeState(btnDom, 1, 1, video.recordIsMuted ? '由于静音錄影被迫暂停' : `已暂停 ${formatSeconds(btnDom[0].recS)}` ); return; } btnDom[0].recS++; btnChangeState(btnDom, 1, 0,`停止 ${formatSeconds(btnDom[0].recS)}`); }, 1000); //-- listen video ended btnDom[0].videoEnded = () => { stopRecord(); video.removeEventListener('ended', btnDom[0].videoEnded); btnDom[0].videoEnded = 6; }; video.addEventListener('ended', btnDom[0].videoEnded); btnDom[0].recStop = () => { stopRecord(); video.removeEventListener('ended', btnDom[0].videoEnded); btnDom[0].videoEnded = undefined; }; } //-- listen video events video.recordPause = () => pauseRecord(); video.recordResume = () => pauseRecord(1); video.videoVolumeChange = () => { if (video.muted || video.volume <=0) { pauseRecord(); video.recordIsMuted = 1; return; } if (video.recordIsMuted) { pauseRecord(1); video.recordIsMuted = undefined; } } //- pause video.addEventListener('pause', video.recordPause); //- waiting video.addEventListener('waiting', video.recordPause); //- playing video.addEventListener('playing', video.recordResume); //- volumechange video.addEventListener('volumechange', video.videoVolumeChange); let blobs = []; await new Promise((resolve, reject) => { recorder.onstop = resolve; recorder.onerror = reject; recorder.ondataavailable = (event) => blobs.push(event.data); try { // Save the stream into memory every second to reduce the jam recorder.start(1000); return true; } catch(err) { // In FireFox if (btnDom) { clearInterval(btnDom[0].recTimeCalc); buttonAddOrDel(btnDom, btnDom[0].video, 1); } catchErrorEvent(err, video); return false; } }); // Recording stopped video.removeEventListener('pause', video.recordPause); video.removeEventListener('waiting', video.recordPause); video.removeEventListener('playing', video.recordResume); video.removeEventListener('volumechange', video.videoVolumeChange); video.recordPause = video.recordResume = video.videoVolumeChange = undefined; if (btnDom) { btnDom[0].vblob = new Blob(blobs, { type: mimeType }); btnDom[0].dlurl = URL.createObjectURL(btnDom[0].vblob); clearInterval(btnDom[0].recTimeCalc); btnChangeState(btnDom); if (btnDom[0].autoDL && btnDom[0].videoEnded > 5) { btnDom[0].videoEnded = undefined; createDownload(btnDom[0].dlurl); } } blobs = stream = recorder = undefined; return true; } catch(err) { catchErrorEvent(err, video); return false; } } } //## 新页面打开链接 function openUrl(url){ GM_openInTab(url, { active: true, insert: true, setParent :true }); } //## 创建下载(blob链接, 下载后是否释放) function createDownload(dlurl, revoke) { let defaultFileName = `WebVideo${new Date().toLocaleString().replace(/\\|\/|:|\*|\?|\"|<|>|\|/ig, '')}`; let filename = ($('title').length > 0 ? $('title').text() : defaultFileName) + '.webm'; let a = document.createElement('a'); a.href = dlurl; a.download = filename; a.click(); if (revoke) { window.URL.revokeObjectURL(dlurl); } } //## 向子窗口发送指令 function sendCommandToWindow(winDom, command, parameter) { if (!winDom || !command) { return; } winDom.postMessage({ gm : GM_info.script.namespace, action : command, value : parameter }, '*'); } //## 转发指令 --------------- function forwardCommandToIframe(command, parameter) { $('iframe').each(function () { sendCommandToWindow(this.contentWindow, command, parameter); }); } //-- 监听接收指令 -------------- window.addEventListener('message', function(e) { if (!e.data || !e.data.gm || e.data.gm != GM_info.script.namespace || !e.data.action) { return; } switch (e.data.action) { case 'rebind' : if (!e.data.value) { break; } e.data.value.host = location.host; reBindVideoEvent(e.data.value, 1); break; case 'changemimetypeid' : if (!e.data.value) { break; } selectedMimeTypeId = e.data.value; GM_setValue('MimeTypeId', selectedMimeTypeId); break; } forwardCommandToIframe(e.data.action, e.data.value) }); //-- 初始化 ------------------------------- window.onload = function () { // 载入设置 selectedMimeTypeId = parseInt(GM_getValue('MimeTypeId')); loadSiteButtonShowMode(); // 5s尝试初始化 let tryCount = 0; let timerInit = setInterval(() => { initialization(); if (tryCount > 4 || $('style:contains(gmAyaRecBtn)').length > 0) { clearInterval(timerInit); tryCount = timerInit = undefined; return; } tryCount++; }, 1000); initialIsDone = !0; }; //## 退出全屏时重新绑定 -------------- $(window).resize(function () { let isFull = document.fullScreen || document.webkitIsFullScreen || document.mozFullScreen; if (isFull === undefined || !isFull) { initialization(); } }); //## 初始化过程 -------------- function initialization() { if ($('video').length < 1) { return; } if ($('style:contains(gmAyaRecBtn)').length < 1) { $('head').append($(`<style> .gmAyaRecBtn{position:absolute;left:0;top:0;display:inline-block;border-radius:4px; background-color:#ff7728bb;border:none;color:#fff;text-align:center;font-size:12pt;padding:5px 10px; cursor:pointer;margin:5px;font-family:"Microsoft YaHei",Arial,sans-serif;z-index:998!important; transition:.5s!important;line-height:1!important} .gmAyaRecBtn:hover{background-color:#ff5520} .gmAyaRecBtn.dl{background-color:#56bb2cbb;padding-right:18px;transition:.5s} .gmAyaRecBtn.dl:hover{background-color:#2cbb80;transition:.5s} .gmAyaRecBtn span{display:inline-block;cursor:pointer;position:relative;color:#fff;transition:.5s} .gmAyaRecBtn span:after{content:attr(data-content-after);font-size:19pt;position:absolute;opacity:0; top:-6px;margin-left:5px;color:#fff;transition:.5s} .gmAyaRecBtn span.rec{padding-right:18px;transition:.5s} .gmAyaRecBtn span.rec:after{animation:twinkle .5s infinite alternate} .gmAyaRecBtn span.dl,.gmAyaRecBtn span.pause{padding-right:12px;transition:.5s} .gmAyaRecBtn span.dl:after{font-size:12pt} .gmAyaRecBtn span.pause:after{font-size:10pt;font-weight:bold} .gmAyaRecBtn span.dl:after,.gmAyaRecBtn span.pause:after{opacity:1;top:0;animation:none} @keyframes twinkle{0%{opacity:.5}100%{opacity:1}} </style>`)); } if (!unsafeWindow.HTMLVideoElement.prototype.record) { ExtensionVideoRecorder(); } if (buttonShowMode.mode > 0) { bindVideoEvent(changeButtonShowMode); return; } bindVideoEvent(); } //## 绑定video hover事件 function bindVideoEventHover(videoDom) { videoDom.gmayavrhover = function () { switchButton($(videoDom)); } videoDom.gmayavrunhover = function () { switchButton($(videoDom), 1); } videoDom.addEventListener('mouseenter', videoDom.gmayavrhover) videoDom.addEventListener('mouseleave', videoDom.gmayavrunhover); switchButton($(videoDom), 1); } //## 解除绑定video hover事件 function unBindVideoEventHover(videoDom) { if (videoDom.gmayavrhover) { videoDom.removeEventListener('mouseenter', videoDom.gmayavrhover); videoDom.gmayavrhover = undefined; } if (videoDom.gmayavrunhover) { videoDom.removeEventListener('mouseleave', videoDom.gmayavrunhover); videoDom.gmayavrunhover = undefined; } } //## 绑定video事件(每绑定一个video都会回调传入video jQuery dom) function bindVideoEvent(callback) { let video = $('video'); if (video.length > 0) { if (buttonShowMode.mode < 1) { video.each(function () { unBindVideoEventHover(this); bindVideoEventHover(this); }); return; } if (buttonShowMode.mode > 0 && callback) { callback(video); } } } /*## 重新绑定video事件 * @param {object} newButtonShowMode 新绑定的按钮模式对象 * @param {bool} needToSave 保存按钮模式到配置 */ function reBindVideoEvent(newButtonShowMode, needToSave) { if (!newButtonShowMode) { return; } // 移除旧按钮 if (buttonShowMode) { buttonShowMode.mode = 2; initialization(); } // 等待删除后重新绑定 setTimeout(() => { buttonShowMode = newButtonShowMode; initialization(); if (needToSave) { saveSiteButtonShowMode(); } }, 300); } //## 定位按钮容器返回 jq dom function positionButtonContainer(videoDom) { let inDom = videoDom[0].parentNode; if (buttonShowMode.layer < 11) { return $(inDom); } let videoWidth = videoDom[0].clientWidth, videoHeight = videoDom[0].clientHeight; if (!videoWidth || !videoHeight) { return; } while (inDom && !/body|html/i.test(inDom.tagName)){ if (inDom.clientWidth > videoWidth || inDom.clientHeight > videoHeight) { break; } inDom = inDom.parentNode; } inDom = buttonShowMode.layer > 11 ? (inDom.parentNode ? inDom.parentNode : inDom) : inDom; return $(inDom); } //## 显示或隐藏按钮 function switchButton(videoDom, hide) { if (!videoDom) { return; } let inDom = positionButtonContainer(videoDom); if (!inDom) { return; } let gmbtn = inDom.find('.gmAyaRecBtn'); if (hide) { if (gmbtn.length < 1 || gmbtn[0].isRec || gmbtn[0].dlurl){ return; } setTimeout(() => buttonAddOrDel(gmbtn, undefined, buttonShowMode.mode > 1), 100); return; } buttonAddOrDel(0, videoDom); } //## 改变按钮显示方式 function changeButtonShowMode(videoDom) { switch(buttonShowMode.mode) { case 1: videoDom.each(function(){ switchButton($(this)); }); break; case 2: videoDom.each(function(){ switchButton($(this), 1); }); break; default: initialization(); videoDom.each(function(){ switchButton($(this), 1); }); } } //## 添加或删除按钮(添加:无btnDom 有videoDom, 删除:有btnDom 无videoDom, 重新添加) function buttonAddOrDel(btnDom, videoDom, reAdd) { // 删除 if (!videoDom || reAdd) { if (!reAdd && (!btnDom || btnDom[0].hovered || btnDom[0].isRec || btnDom[0].dlurl || buttonShowMode.mode === 1)) { return false; } btnDom.remove(); btnDom = undefined; // 删除后再添加 if (reAdd && buttonShowMode.mode === 1) { buttonAddOrDel(0, videoDom) } return false; } //== 添加 //- 定位按钮容器jq dom let inDom = positionButtonContainer(videoDom); if (!inDom || inDom.find('.gmAyaRecBtn').length > 0 || buttonShowMode.mode > 1) { return false; } let newBtn = $(`<a class="gmAyaRecBtn" href="javascript:;"><span>錄影</span></a>`); newBtn[0].video = videoDom; newBtn.hover(function () { this.hovered = 1; }, function () { this.hovered = 0; }); newBtn.click(function () { //---- 下载 if (this.dlurl) { if (confirm('要下载錄影吗?')) { createDownload(this.dlurl); return false; } if (!confirm('要重新开始錄影吗?')) { return false; } window.URL.revokeObjectURL(this.dlurl); buttonAddOrDel($(this), videoDom, 1); return false; } //---- 錄影 let videoObj = videoDom[0]; if (this.isRec) { //停止錄影 videoObj.pause(); this.recStop(); return false; } //开始錄影 let durs = videoObj.duration; if (!durs) { alert('无法取得视频长度'); return false; } let videoIsPaused = videoObj.paused; videoObj.pause(); if (!confirm('要开始錄影吗?')) { if (!videoIsPaused) { //延迟播放避免某些网站播放器逻辑冲突 setTimeout(() => videoObj.play(), 800); } return false; } if (videoObj.duration != Infinity) { if (videoObj.currentTime > 0 && videoObj.currentTime <= videoObj.duration && confirm('要从头开始錄影吗?')) { videoObj.currentTime = 0; } else { durs -= videoObj.currentTime; } newBtn[0].autoDL = confirm('当錄影结束时弹出下载?'); } if (videoObj.muted || videoObj.volume <= 0) { videoObj.muted = false; videoObj.volume = 0.0001; } let promise = videoObj.record(durs, newBtn); let promiseReturn = true; promise.then((result) => { promiseReturn = result; }); setTimeout(() => { if (!promiseReturn) { if (!videoIsPaused) { setTimeout(() => videoObj.play(), 800); } return false; } videoObj.play(); }, 100); return false; }); inDom.append(newBtn); return false; } //## 改变按钮状态(按钮dom, 是否正在錄影, 錄影是否已暂停, 状态标题) function btnChangeState(btnDom, isRecording, isPaused , title) { if (!btnDom) { return; } let btnSpan = btnDom.children(':first'); //錄影暂停 if (isPaused && btnDom[0].isRec > 0) { if (btnSpan.hasClass('pause')) { return; } btnSpan.text(title); btnSpan.attr('data-content-after', '||'); btnDom.addClass('pause'); btnSpan.removeClass('rec').addClass('pause'); return; } //錄影状态 if (isRecording) { btnDom[0].isRec = 1; btnSpan.text(title ? title : '錄影已开始'); if (btnSpan.hasClass('rec')) { return; } btnSpan.attr('data-content-after', '●'); btnDom.removeClass('pause'); btnSpan.removeClass('pause').addClass('rec'); return; } //停止錄影状态 btnDom[0].isRec = 0; btnSpan.removeClass('rec').removeClass('pause'); if (btnDom[0].dlurl) { btnSpan.text('下载錄影'); btnSpan.attr('data-content-after', '▼'); btnDom.addClass('dl'); btnSpan.addClass('dl'); return; } } })(jQuery);