Greasy Fork is available in English.

文文錄影机

支持大部分网页视频、直播錄影 / 视频录制 / 录制视频

// ==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);