// ==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/* // @downloadURL https://update.greasyfork.icu/scripts/461956/VideoRecorder.user.js // @updateURL https://update.greasyfork.icu/scripts/461956/VideoRecorder.meta.js // ==/UserScript== (function () { // const videoType = 'video/mp4' const videoType = 'video/webm' const html = `
` // 创建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) => `下载`); this._videos.innerHTML = htmlList.join(''); } } window.__videoRecorder = new Recorder() })();