视频录屏
当前为
// ==UserScript==
// @name VideoRecorder
// @namespace Recorder
// @version 0.0.3
// @description 视频录屏
// @author zenfanxing
// @license MIT
// @match *://pan.baidu.com/disk/home*
// @match *://yun.baidu.com/disk/home*
// @match *://pan.baidu.com/disk/main*
// @match *://yun.baidu.com/disk/main*
// @match *://pan.baidu.com/s*
// @match *://yun.baidu.com/s*
// @match *://*.youku.com/*
// @match *://*.iqiyi.com/*
// @match *://*.iq.com/*
// @match *://*.le.com/*
// @match *://v.qq.com/*
// @match *://m.v.qq.com/*
// @match *://*.tudou.com/*
// @match *://*.mgtv.com/*
// @match *://tv.sohu.com/*
// @match *://film.sohu.com/*
// @match *://*.1905.com/*
// @match *://*.bilibili.com/*
// @match *://*.pptv.com/*
// @match *://item.taobao.com/*
// @match *://s.taobao.com/*
// @match *://chaoshi.detail.tmall.com/*
// @match *://detail.tmall.com/*
// @match *://detail.tmall.hk/*
// @match *://item.jd.com/*
// @match *://*.yiyaojd.com/*
// @match *://npcitem.jd.hk/*
// @match *://*.liangxinyao.com/*
// @match *://music.163.com/*
// @match *://y.qq.com/*
// @match *://*.kugou.com/*
// @match *://*.kuwo.cn/*
// @match *://*.ximalaya.com/*
// @match *://*.zhihu.com/*
// @match *://*.douyin.com/*
// @match *://*.kuaishou.com/*
// @match *://*.ixigua.com/*
// @match *://*.youtube.com/*
// ==/UserScript==
(function () {
// const videoType = 'video/mp4'
const videoType = 'video/webm'
const html = `
<style>
*:not(#recorder-videos)>video:hover {
outline: 4px solid greenyellow;
}
video.recorder-selected {
outline: 4px solid orange;
}
.recorder {
display: none;
}
.recorder.show {
display: block;
}
.recorder * {
box-sizing: border-box;
}
.recorder-btn {
position: fixed;
right: 60px;
bottom: 60px;
width: 60px;
height: 60px;
padding: 4px;
padding-bottom: 56px;
opacity: 0.4;
transform: all 0.3s;
background-color: #00B2FF;
border-radius: 30px;
overflow: hidden;
color: white;
z-index: 999;
}
.recorder-btn button {
display: none;
width: 52px;
height: 52px;
background-color: #0091FF;
border-radius: 50%;
margin-bottom: 10px;
border: none;
cursor: pointer;
color: inherit;
font-size: 12px;
}
.recorder-btn #recorder-btn-list {
display: block;
}
.recorder-btn button.show {
display: block;
}
.recorder-btn:hover {
height: auto;
opacity: 1;
}
.recorder-modal {
display: none;
}
.recorder-modal.show {
display: block;
}
.recorder-mask {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
z-index: 9999;
}
.recorder-videos {
position: fixed;
right: 0;
top: 0;
background-color: #fff;
width: 300px;
height: 100%;
padding: 10px;
z-index: 9999;
overflow-y: auto;
}
.recorder-videos::before {
content: 'CTRL + T 快捷键快速设置录屏结束点';
margin-bottom: 10px;
font-size: 12px;
}
.recorder-videos video {
width: 280px;
}
.recorder-videos a {
display: block;
color: white;
background-color: #00B2FF;
padding: 6px;
margin-bottom: 10px;
text-align: center;
text-decoration: none;
}
.recorder-btn-handle {
display: flex;
position: absolute;
left: 0;
bottom: 0;
width: 60px;
height: 60px;
justify-content: center;
align-items: center;
font-size: 40px;
background-color: #00B2FF;
cursor: pointer;
}
#recorder-time {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: red;
display: none;
justify-content: center;
align-items: center;
text-align: center;
font-size: 12px;
}
#recorder-time.show {
display: flex;
}
</style>
<div class="recorder" id="recorder">
<div class="recorder-btn">
<button id="recorder-btn-list">列表</button>
<button id="recorder-btn-pause">暂停</button>
<button id="recorder-btn-resume">恢复</button>
<button id="recorder-btn-stop">停止</button>
<button id="recorder-btn-start" class="show">开始</button>
<div class="recorder-btn-handle">
<svg width="1em" height="1em" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.25 10.607v5.321h1.5v-5.321h5.321v-1.5H10.75V3.786h-1.5v5.32H3.93v1.5h5.32Z"
fill="currentColor"></path>
</svg>
<div id="recorder-time"></div>
</div>
</div>
<div id="recorder-modal" class="recorder-modal">
<div id="recorder-mask" class="recorder-mask"></div>
<div id="recorder-videos" class="recorder-videos"></div>
</div>
</div>
`
// 创建html
const createHTML = () => {
const con = document.createElement('div');
con.innerHTML = html;
document.body.appendChild(con)
}
// 时间格式化
const timeFormat = (t, hasHours = true) => {
let h, m
if (hasHours) {
h = parseInt(t / 3600000) + ''
m = parseInt((t % 3600000) / 60000) + ''
} else {
m = parseInt(t / 60000) + ''
}
const s = parseInt(t % 60000 / 1000) + ''
return hasHours ? `${h}:${m.padStart(2, '0')}:${s.padStart(2, '0')}` : `${m}:${s.padStart(2, '0')}`
}
// 状态
const STATUS = {
PAUSE: 'pause',
RECORDING: 'recording',
STOP: 'stop'
}
class Recorder {
constructor() {
this._urls = []; // 录屏视频链接
this._status = STATUS.STOP; // 初始状态
this._time = 0; // 录制时常
this._realtime = 0; // 实时显示时长
this._targetTime = 0; // 自动停止时间点
this.init(); // 初始话html
// dom
this._recorderEle = document.getElementById('recorder');
this._modalEle = document.getElementById('recorder-modal');
this._maskEle = document.getElementById('recorder-mask');
this._videos = document.getElementById('recorder-videos');
this._startEle = document.getElementById('recorder-btn-start');
this._pauseEle = document.getElementById('recorder-btn-pause');
this._resumeEle = document.getElementById('recorder-btn-resume');
this._stopEle = document.getElementById('recorder-btn-stop');
this._listEle = document.getElementById('recorder-btn-list');
this._timeEle = document.getElementById('recorder-time');
this._video = document.querySelector('video');
// 自动选择页面第一个视频节点
if (this._video) {
this._video.classList.add("recorder-selected");
this._recorderEle.classList.add('show');
this._video.preload = 'auto';
}
this.listen();
}
init() {
createHTML(html);
}
// 监听元素点击事件
listen() {
this._startEle.onclick = () => {
this.start();
}
this._pauseEle.onclick = () => {
this.pause();
}
this._resumeEle.onclick = () => {
this.resume();
}
this._stopEle.onclick = () => {
this.stop();
}
this._listEle.onclick = () => {
this._modalEle.classList.add('show');
}
this._maskEle.onclick = () => {
this._modalEle.classList.remove('show');
}
if (this._video) {
this.videoAddListener();
}
// 快捷键
window.addEventListener('keydown', (e) => {
// ctrl + T
if (this._video && e.ctrlKey && e.keyCode === 84) {
if (this._status === STATUS.STOP && this._video.currentTime && this._targetTime !==
this._video.currentTime && confirm(
`设置视频的${timeFormat(this._video.currentTime * 1000)}下次录制的结束时间点`)) {
this._targetTime = this._video.currentTime;
} else if (this._targetTime && alert('清除录制结束点')) {
this._targetTime = 0;
}
}
})
// 切换视频节点
document.body.addEventListener('click', (e) => {
if (e.target.nodeName === 'VIDEO' && e.target.parentNode.id !== 'recorder-videos' &&
this._status === STATUS.STOP && this._video !== e.target) {
this._recorderEle.classList.add('show');
this._video && this._video.classList.remove('recorder-selected');
this._video && this.videoRemoveListener();
this._video = e.target;
this._video.preload = 'auto';
this.videoAddListener();
this._video.classList.add("recorder-selected");
}
})
}
// 视频节点事件监听
videoAddListener() {
if (!this._video) return;
this._videoHandlePlay = () => {
if (this._status === STATUS.PAUSE) {
this.setStatus(STATUS.RECORDING);
}
}
this._videoHandlePause = () => {
if (this._status === STATUS.RECORDING) {
this.setStatus(STATUS.PAUSE);
}
}
this._videoHandleEnded = () => {
if (this._status !== STATUS.STOP) {
this.setStatus(STATUS.STOP);
}
}
this._videoHandleTimeUpdate = () => {
if (this._targetTime && this._status === STATUS.RECORDING && this._video.currentTime >= this
._targetTime) {
this.stop();
}
if (this._status === STATUS.RECORDING && this._video.buffered && this._video.buffered.length && (this._video.buffered.end(this._video.buffered.length - 1) - this._video.currentTime < 3)) {
this._bufferTimer && clearInterval(this._bufferTimer);
this._bufferTimer = null;
this.pause();
this._video.pause();
console.log('buffered 小于 3s, 暂停录制等待缓存', this._video.buffered.end(this._video.buffered.length - 1) - this._video.currentTime)
this._bufferTimer = setInterval(() => {
if (this._video.buffered.end(this._video.buffered.length - 1) - this._video.currentTime > 6) {
this._bufferTimer && clearInterval(this._bufferTimer);
this._bufferTimer = null;
this.resume();
this._video.play();
console.log('buffered 大于 6s, 恢复录制')
}
}, 500)
}
}
this._video.addEventListener('play', this._videoHandlePlay);
this._video.addEventListener('pause', this._videoHandlePause);
this._video.addEventListener('waiting', this._videoHandlePause);
this._video.addEventListener('playing', this._videoHandlePlay);
this._video.addEventListener('ended', this._videoHandleEnded);
this._video.addEventListener('timeupdate', this._videoHandleTimeUpdate);
}
// 视频节点事件移除
videoRemoveListener() {
if (!this._video) return;
this._video.removeEventListener('play', this._videoHandlePlay);
this._video.removeEventListener('pause', this._videoHandlePause);
this._video.removeEventListener('waiting', this._videoHandlePause);
this._video.removeEventListener('playing', this._videoHandlePlay);
this._video.removeEventListener('ended', this._videoHandleEnded);
this._video.addEventListener('timeupdate', this._videoHandleTimeUpdate);
}
// 更新状态
setStatus(status) {
const now = Date.now();
switch (status) {
case STATUS.RECORDING:
// 视频可播放下一帧才开始录制
if (this._status === STATUS.PAUSE) {
this._recorder.state !== 'inactive' && this._recorder.resume(); // 恢复录制
} else {
this._recorder.start(); // 开始录制
}
this._video.play();
this.timing();
// 更新ui
this._timeEle.classList.add('show');
this._startEle.classList.remove('show');
this._resumeEle.classList.remove('show');
this._pauseEle.classList.add('show');
this._stopEle.classList.add('show');
this._bufferTimer && clearInterval(this._bufferTimer);
this._bufferTimer = null;
break;
case STATUS.PAUSE:
this.timingEnd();
this._recorder.state !== 'inactive' && this._recorder.pause(); // 暂停录制
// 更新ui
this._pauseEle.classList.remove('show');
this._resumeEle.classList.add('show');
break;
case STATUS.STOP:
this.timingEnd();
this._bufferTimer && clearInterval(this._bufferTimer);
this._bufferTimer = null;
this._time = 0;
this._targetTime = 0;
this._recorder.state !== 'inactive' && this._recorder.stop(); // 停止录制
this._video.pause(); // 暂停视频
// 更新ui
this._timeEle.classList.remove('show');
this._startEle.classList.add('show');
this._pauseEle.classList.remove('show');
this._resumeEle.classList.remove('show');
this._stopEle.classList.remove('show');
break;
}
// 更新状态
this._status = status;
}
// 录制前初始化
startInit() {
if (this._targetTime && this._video.currentTime >= this._targetTime) {
this._targetTime = 0;
}
this._time = 0;
this._realtime = 0;
this._recorder = null;
this._currentStartTime = null;
clearInterval(this._timer);
this._timer = null;
clearInterval(this._bufferTimer)
this._bufferTimer = null;
}
// 开始录制
start() {
if (!this._video || !this._video.isConnected) {
alert('请点击重新选中播放器');
return;
}
if (this._video.ended) {
alert('视频已经播完');
return;
}
if (this._video.readyState !== 4) {
alert('视频还未准备好');
return
}
if (this._status === STATUS.STOP) {
this.startInit(); // 初始化
this._recorder = new MediaRecorder(this._video.captureStream(), {
mimeType: 'video/webm;codecs=h264' // 视频编码格式
});
const blobs = [];
// 处理剪辑的数据
this._recorder.ondataavailable = (event) => {
if (event.data.size > 0) blobs.push(event.data);
};
this._recorder.onstop = () => {
this._urls.push(URL.createObjectURL(new Blob(blobs, {
type: videoType
})));
// 更新列表
this.renderList();
}
this.setStatus(STATUS.RECORDING);
}
}
// 停止
stop() {
if (this._recorder && this._status !== STATUS.STOP) {
this.setStatus(STATUS.STOP);
}
}
// 暂停
pause() {
if (this._recorder && this._status === STATUS.RECORDING) {
this.setStatus(STATUS.PAUSE);
}
}
// 恢复
resume() {
if (this._recorder && this._status === STATUS.PAUSE) {
this.setStatus(STATUS.RECORDING);
}
}
// 开始计时
timing() {
this._currentStartTime = Date.now();
const run = () => {
this._realtime = this._time + (Date.now() - this._currentStartTime)
this._timeEle.innerText =
`${timeFormat(this._realtime, false)}${this._targetTime ? `\n(${timeFormat(this._targetTime * 1000)})`: ''}`;
}
run();
this._timer = setInterval(run, 500);
}
// 结束计时
timingEnd() {
if (this._currentStartTime) {
clearInterval(this._timer);
this._time += (Date.now() - this._currentStartTime);
this._currentStartTime = null;
this._realtime = 0;
}
}
// 更新录制列表
renderList() {
const htmlList = this._urls.map((url) =>
`<video src=${url} controls></video><a download href="${url}">下载</a>`);
this._videos.innerHTML = htmlList.join('');
}
}
window.__videoRecorder = new Recorder()
})();