VideoRecorder

视频录屏

  1. // ==UserScript==
  2. // @name VideoRecorder
  3. // @namespace Recorder
  4. // @version 0.0.3
  5. // @description 视频录屏
  6. // @author zenfanxing
  7. // @license MIT
  8. // @match *://pan.baidu.com/disk/home*
  9. // @match *://yun.baidu.com/disk/home*
  10. // @match *://pan.baidu.com/disk/main*
  11. // @match *://yun.baidu.com/disk/main*
  12. // @match *://pan.baidu.com/s*
  13. // @match *://yun.baidu.com/s*
  14. // @match *://*.youku.com/*
  15. // @match *://*.iqiyi.com/*
  16. // @match *://*.iq.com/*
  17. // @match *://*.le.com/*
  18. // @match *://v.qq.com/*
  19. // @match *://m.v.qq.com/*
  20. // @match *://*.tudou.com/*
  21. // @match *://*.mgtv.com/*
  22. // @match *://tv.sohu.com/*
  23. // @match *://film.sohu.com/*
  24. // @match *://*.1905.com/*
  25. // @match *://*.bilibili.com/*
  26. // @match *://*.pptv.com/*
  27. // @match *://item.taobao.com/*
  28. // @match *://s.taobao.com/*
  29. // @match *://chaoshi.detail.tmall.com/*
  30. // @match *://detail.tmall.com/*
  31. // @match *://detail.tmall.hk/*
  32. // @match *://item.jd.com/*
  33. // @match *://*.yiyaojd.com/*
  34. // @match *://npcitem.jd.hk/*
  35. // @match *://*.liangxinyao.com/*
  36. // @match *://music.163.com/*
  37. // @match *://y.qq.com/*
  38. // @match *://*.kugou.com/*
  39. // @match *://*.kuwo.cn/*
  40. // @match *://*.ximalaya.com/*
  41. // @match *://*.zhihu.com/*
  42. // @match *://*.douyin.com/*
  43. // @match *://*.kuaishou.com/*
  44. // @match *://*.ixigua.com/*
  45. // @match *://*.youtube.com/*
  46. // ==/UserScript==
  47.  
  48. (function () {
  49. // const videoType = 'video/mp4'
  50. const videoType = 'video/webm'
  51. const html = `
  52. <style>
  53. *:not(#recorder-videos)>video:hover {
  54. outline: 4px solid greenyellow;
  55. }
  56. video.recorder-selected {
  57. outline: 4px solid orange;
  58. }
  59. .recorder {
  60. display: none;
  61. }
  62. .recorder.show {
  63. display: block;
  64. }
  65. .recorder * {
  66. box-sizing: border-box;
  67. }
  68. .recorder-btn {
  69. position: fixed;
  70. right: 60px;
  71. bottom: 60px;
  72. width: 60px;
  73. height: 60px;
  74. padding: 4px;
  75. padding-bottom: 56px;
  76. opacity: 0.4;
  77. transform: all 0.3s;
  78. background-color: #00B2FF;
  79. border-radius: 30px;
  80. overflow: hidden;
  81. color: white;
  82. z-index: 999;
  83. }
  84. .recorder-btn button {
  85. display: none;
  86. width: 52px;
  87. height: 52px;
  88. background-color: #0091FF;
  89. border-radius: 50%;
  90. margin-bottom: 10px;
  91. border: none;
  92. cursor: pointer;
  93. color: inherit;
  94. font-size: 12px;
  95. }
  96. .recorder-btn #recorder-btn-list {
  97. display: block;
  98. }
  99. .recorder-btn button.show {
  100. display: block;
  101. }
  102. .recorder-btn:hover {
  103. height: auto;
  104. opacity: 1;
  105. }
  106. .recorder-modal {
  107. display: none;
  108. }
  109. .recorder-modal.show {
  110. display: block;
  111. }
  112. .recorder-mask {
  113. position: fixed;
  114. left: 0;
  115. top: 0;
  116. width: 100%;
  117. height: 100%;
  118. background-color: rgba(0, 0, 0, 0.3);
  119. z-index: 9999;
  120. }
  121. .recorder-videos {
  122. position: fixed;
  123. right: 0;
  124. top: 0;
  125. background-color: #fff;
  126. width: 300px;
  127. height: 100%;
  128. padding: 10px;
  129. z-index: 9999;
  130. overflow-y: auto;
  131. }
  132. .recorder-videos::before {
  133. content: 'CTRL + T 快捷键快速设置录屏结束点';
  134. margin-bottom: 10px;
  135. font-size: 12px;
  136. }
  137. .recorder-videos video {
  138. width: 280px;
  139. }
  140. .recorder-videos a {
  141. display: block;
  142. color: white;
  143. background-color: #00B2FF;
  144. padding: 6px;
  145. margin-bottom: 10px;
  146. text-align: center;
  147. text-decoration: none;
  148. }
  149. .recorder-btn-handle {
  150. display: flex;
  151. position: absolute;
  152. left: 0;
  153. bottom: 0;
  154. width: 60px;
  155. height: 60px;
  156. justify-content: center;
  157. align-items: center;
  158. font-size: 40px;
  159. background-color: #00B2FF;
  160. cursor: pointer;
  161. }
  162. #recorder-time {
  163. position: absolute;
  164. top: 0;
  165. left: 0;
  166. width: 100%;
  167. height: 100%;
  168. border-radius: 50%;
  169. background-color: red;
  170. display: none;
  171. justify-content: center;
  172. align-items: center;
  173. text-align: center;
  174. font-size: 12px;
  175. }
  176. #recorder-time.show {
  177. display: flex;
  178. }
  179. </style>
  180. <div class="recorder" id="recorder">
  181. <div class="recorder-btn">
  182. <button id="recorder-btn-list">列表</button>
  183. <button id="recorder-btn-pause">暂停</button>
  184. <button id="recorder-btn-resume">恢复</button>
  185. <button id="recorder-btn-stop">停止</button>
  186. <button id="recorder-btn-start" class="show">开始</button>
  187. <div class="recorder-btn-handle">
  188. <svg width="1em" height="1em" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
  189. <path d="M9.25 10.607v5.321h1.5v-5.321h5.321v-1.5H10.75V3.786h-1.5v5.32H3.93v1.5h5.32Z"
  190. fill="currentColor"></path>
  191. </svg>
  192. <div id="recorder-time"></div>
  193. </div>
  194. </div>
  195. <div id="recorder-modal" class="recorder-modal">
  196. <div id="recorder-mask" class="recorder-mask"></div>
  197. <div id="recorder-videos" class="recorder-videos"></div>
  198. </div>
  199. </div>
  200. `
  201. // 创建html
  202. const createHTML = () => {
  203. const con = document.createElement('div');
  204. con.innerHTML = html;
  205. document.body.appendChild(con)
  206. }
  207. // 时间格式化
  208. const timeFormat = (t, hasHours = true) => {
  209. let h, m
  210. if (hasHours) {
  211. h = parseInt(t / 3600000) + ''
  212. m = parseInt((t % 3600000) / 60000) + ''
  213. } else {
  214. m = parseInt(t / 60000) + ''
  215. }
  216. const s = parseInt(t % 60000 / 1000) + ''
  217. return hasHours ? `${h}:${m.padStart(2, '0')}:${s.padStart(2, '0')}` : `${m}:${s.padStart(2, '0')}`
  218. }
  219. // 状态
  220. const STATUS = {
  221. PAUSE: 'pause',
  222. RECORDING: 'recording',
  223. STOP: 'stop'
  224. }
  225. class Recorder {
  226. constructor() {
  227. this._urls = []; // 录屏视频链接
  228. this._status = STATUS.STOP; // 初始状态
  229. this._time = 0; // 录制时常
  230. this._realtime = 0; // 实时显示时长
  231. this._targetTime = 0; // 自动停止时间点
  232. this.init(); // 初始话html
  233. // dom
  234. this._recorderEle = document.getElementById('recorder');
  235. this._modalEle = document.getElementById('recorder-modal');
  236. this._maskEle = document.getElementById('recorder-mask');
  237. this._videos = document.getElementById('recorder-videos');
  238. this._startEle = document.getElementById('recorder-btn-start');
  239. this._pauseEle = document.getElementById('recorder-btn-pause');
  240. this._resumeEle = document.getElementById('recorder-btn-resume');
  241. this._stopEle = document.getElementById('recorder-btn-stop');
  242. this._listEle = document.getElementById('recorder-btn-list');
  243. this._timeEle = document.getElementById('recorder-time');
  244. this._video = document.querySelector('video');
  245. // 自动选择页面第一个视频节点
  246. if (this._video) {
  247. this._video.classList.add("recorder-selected");
  248. this._recorderEle.classList.add('show');
  249. this._video.preload = 'auto';
  250. }
  251. this.listen();
  252. }
  253. init() {
  254. createHTML(html);
  255. }
  256. // 监听元素点击事件
  257. listen() {
  258. this._startEle.onclick = () => {
  259. this.start();
  260. }
  261. this._pauseEle.onclick = () => {
  262. this.pause();
  263. }
  264. this._resumeEle.onclick = () => {
  265. this.resume();
  266. }
  267. this._stopEle.onclick = () => {
  268. this.stop();
  269. }
  270. this._listEle.onclick = () => {
  271. this._modalEle.classList.add('show');
  272. }
  273. this._maskEle.onclick = () => {
  274. this._modalEle.classList.remove('show');
  275. }
  276. if (this._video) {
  277. this.videoAddListener();
  278. }
  279. // 快捷键
  280. window.addEventListener('keydown', (e) => {
  281. // ctrl + T
  282. if (this._video && e.ctrlKey && e.keyCode === 84) {
  283. if (this._status === STATUS.STOP && this._video.currentTime && this._targetTime !==
  284. this._video.currentTime && confirm(
  285. `设置视频的${timeFormat(this._video.currentTime * 1000)}下次录制的结束时间点`)) {
  286. this._targetTime = this._video.currentTime;
  287. } else if (this._targetTime && alert('清除录制结束点')) {
  288. this._targetTime = 0;
  289. }
  290. }
  291. })
  292. // 切换视频节点
  293. document.body.addEventListener('click', (e) => {
  294. if (e.target.nodeName === 'VIDEO' && e.target.parentNode.id !== 'recorder-videos' &&
  295. this._status === STATUS.STOP && this._video !== e.target) {
  296. this._recorderEle.classList.add('show');
  297. this._video && this._video.classList.remove('recorder-selected');
  298. this._video && this.videoRemoveListener();
  299. this._video = e.target;
  300. this._video.preload = 'auto';
  301. this.videoAddListener();
  302. this._video.classList.add("recorder-selected");
  303. }
  304. })
  305. }
  306. // 视频节点事件监听
  307. videoAddListener() {
  308. if (!this._video) return;
  309. this._videoHandlePlay = () => {
  310. if (this._status === STATUS.PAUSE) {
  311. this.setStatus(STATUS.RECORDING);
  312. }
  313. }
  314. this._videoHandlePause = () => {
  315. if (this._status === STATUS.RECORDING) {
  316. this.setStatus(STATUS.PAUSE);
  317. }
  318. }
  319. this._videoHandleEnded = () => {
  320. if (this._status !== STATUS.STOP) {
  321. this.setStatus(STATUS.STOP);
  322. }
  323. }
  324. this._videoHandleTimeUpdate = () => {
  325. if (this._targetTime && this._status === STATUS.RECORDING && this._video.currentTime >= this
  326. ._targetTime) {
  327. this.stop();
  328. }
  329. 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)) {
  330. this._bufferTimer && clearInterval(this._bufferTimer);
  331. this._bufferTimer = null;
  332. this.pause();
  333. this._video.pause();
  334. console.log('buffered 小于 3s, 暂停录制等待缓存', this._video.buffered.end(this._video.buffered.length - 1) - this._video.currentTime)
  335. this._bufferTimer = setInterval(() => {
  336. if (this._video.buffered.end(this._video.buffered.length - 1) - this._video.currentTime > 6) {
  337. this._bufferTimer && clearInterval(this._bufferTimer);
  338. this._bufferTimer = null;
  339. this.resume();
  340. this._video.play();
  341. console.log('buffered 大于 6s, 恢复录制')
  342. }
  343. }, 500)
  344. }
  345. }
  346. this._video.addEventListener('play', this._videoHandlePlay);
  347. this._video.addEventListener('pause', this._videoHandlePause);
  348. this._video.addEventListener('waiting', this._videoHandlePause);
  349. this._video.addEventListener('playing', this._videoHandlePlay);
  350. this._video.addEventListener('ended', this._videoHandleEnded);
  351. this._video.addEventListener('timeupdate', this._videoHandleTimeUpdate);
  352. }
  353. // 视频节点事件移除
  354. videoRemoveListener() {
  355. if (!this._video) return;
  356. this._video.removeEventListener('play', this._videoHandlePlay);
  357. this._video.removeEventListener('pause', this._videoHandlePause);
  358. this._video.removeEventListener('waiting', this._videoHandlePause);
  359. this._video.removeEventListener('playing', this._videoHandlePlay);
  360. this._video.removeEventListener('ended', this._videoHandleEnded);
  361. this._video.addEventListener('timeupdate', this._videoHandleTimeUpdate);
  362. }
  363. // 更新状态
  364. setStatus(status) {
  365. const now = Date.now();
  366. switch (status) {
  367. case STATUS.RECORDING:
  368. // 视频可播放下一帧才开始录制
  369. if (this._status === STATUS.PAUSE) {
  370. this._recorder.state !== 'inactive' && this._recorder.resume(); // 恢复录制
  371. } else {
  372. this._recorder.start(); // 开始录制
  373. }
  374. this._video.play();
  375. this.timing();
  376. // 更新ui
  377. this._timeEle.classList.add('show');
  378. this._startEle.classList.remove('show');
  379. this._resumeEle.classList.remove('show');
  380. this._pauseEle.classList.add('show');
  381. this._stopEle.classList.add('show');
  382. this._bufferTimer && clearInterval(this._bufferTimer);
  383. this._bufferTimer = null;
  384. break;
  385. case STATUS.PAUSE:
  386. this.timingEnd();
  387. this._recorder.state !== 'inactive' && this._recorder.pause(); // 暂停录制
  388. // 更新ui
  389. this._pauseEle.classList.remove('show');
  390. this._resumeEle.classList.add('show');
  391. break;
  392. case STATUS.STOP:
  393. this.timingEnd();
  394. this._bufferTimer && clearInterval(this._bufferTimer);
  395. this._bufferTimer = null;
  396. this._time = 0;
  397. this._targetTime = 0;
  398. this._recorder.state !== 'inactive' && this._recorder.stop(); // 停止录制
  399. this._video.pause(); // 暂停视频
  400. // 更新ui
  401. this._timeEle.classList.remove('show');
  402. this._startEle.classList.add('show');
  403. this._pauseEle.classList.remove('show');
  404. this._resumeEle.classList.remove('show');
  405. this._stopEle.classList.remove('show');
  406. break;
  407. }
  408. // 更新状态
  409. this._status = status;
  410. }
  411. // 录制前初始化
  412. startInit() {
  413. if (this._targetTime && this._video.currentTime >= this._targetTime) {
  414. this._targetTime = 0;
  415. }
  416. this._time = 0;
  417. this._realtime = 0;
  418. this._recorder = null;
  419. this._currentStartTime = null;
  420. clearInterval(this._timer);
  421. this._timer = null;
  422. clearInterval(this._bufferTimer)
  423. this._bufferTimer = null;
  424. }
  425. // 开始录制
  426. start() {
  427. if (!this._video || !this._video.isConnected) {
  428. alert('请点击重新选中播放器');
  429. return;
  430. }
  431. if (this._video.ended) {
  432. alert('视频已经播完');
  433. return;
  434. }
  435. if (this._video.readyState !== 4) {
  436. alert('视频还未准备好');
  437. return
  438. }
  439. if (this._status === STATUS.STOP) {
  440. this.startInit(); // 初始化
  441. this._recorder = new MediaRecorder(this._video.captureStream(), {
  442. mimeType: 'video/webm;codecs=h264' // 视频编码格式
  443. });
  444. const blobs = [];
  445. // 处理剪辑的数据
  446. this._recorder.ondataavailable = (event) => {
  447. if (event.data.size > 0) blobs.push(event.data);
  448. };
  449. this._recorder.onstop = () => {
  450. this._urls.push(URL.createObjectURL(new Blob(blobs, {
  451. type: videoType
  452. })));
  453. // 更新列表
  454. this.renderList();
  455. }
  456. this.setStatus(STATUS.RECORDING);
  457. }
  458. }
  459. // 停止
  460. stop() {
  461. if (this._recorder && this._status !== STATUS.STOP) {
  462. this.setStatus(STATUS.STOP);
  463. }
  464. }
  465. // 暂停
  466. pause() {
  467. if (this._recorder && this._status === STATUS.RECORDING) {
  468. this.setStatus(STATUS.PAUSE);
  469. }
  470. }
  471. // 恢复
  472. resume() {
  473. if (this._recorder && this._status === STATUS.PAUSE) {
  474. this.setStatus(STATUS.RECORDING);
  475. }
  476. }
  477. // 开始计时
  478. timing() {
  479. this._currentStartTime = Date.now();
  480. const run = () => {
  481. this._realtime = this._time + (Date.now() - this._currentStartTime)
  482. this._timeEle.innerText =
  483. `${timeFormat(this._realtime, false)}${this._targetTime ? `\n(${timeFormat(this._targetTime * 1000)})`: ''}`;
  484. }
  485. run();
  486. this._timer = setInterval(run, 500);
  487. }
  488. // 结束计时
  489. timingEnd() {
  490. if (this._currentStartTime) {
  491. clearInterval(this._timer);
  492. this._time += (Date.now() - this._currentStartTime);
  493. this._currentStartTime = null;
  494. this._realtime = 0;
  495. }
  496. }
  497. // 更新录制列表
  498. renderList() {
  499. const htmlList = this._urls.map((url) =>
  500. `<video src=${url} controls></video><a download href="${url}">下载</a>`);
  501. this._videos.innerHTML = htmlList.join('');
  502. }
  503. }
  504. window.__videoRecorder = new Recorder()
  505. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址