| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>前端录屏并保存视频到本地</title> |
| <style> |
| canvas, |
| video { |
| width: 640px; |
| |
| aspect-ratio: 16 / 9; |
| border: 1px solid black; |
| box-sizing: border-box; |
| } |
| </style> |
| </head> |
| <body> |
| <h3>前端录屏并保存视频到本地</h3> |
| <button type="button" id="share-screen">开启屏幕共享</button> |
| <button type="button" hidden id="close-screen">关闭屏幕共享</button> |
| <button type="button" hidden id="record-start">录屏</button> |
| <button type="button" hidden id="record-pause">暂停录屏</button> |
| <button type="button" hidden id="record-resume">继续录屏</button> |
| <button type="button" hidden id="record-stop">停止录屏</button> |
| <button type="button" hidden id="record-download">下载录屏</button> |
| <hr /> |
| |
| <script type="module"> |
| |
| import { setWebmDuration } from '../node_modules/@ozean/set-webm-duration/dist/set-webm-duration.esm.js'; |
| |
| const shareScreen = document.getElementById('share-screen'); |
| const closeScreen = document.getElementById('close-screen'); |
| const recordStart = document.getElementById('record-start'); |
| const recordPause = document.getElementById('record-pause'); |
| const recordResume = document.getElementById('record-resume'); |
| const recordStop = document.getElementById('record-stop'); |
| const recordDownload = document.getElementById('record-download'); |
| |
| const constraints = { video: true, audio: true }; |
| |
| let recorder = null, |
| videoDuration = 0; |
| const chunks = []; |
| |
| shareScreen.addEventListener('click', shareScreenHandler); |
| closeScreen.addEventListener('click', closeScreenHandler); |
| |
| recordStart.addEventListener('click', recordStartHandler); |
| recordStop.addEventListener('click', recordStopHandler); |
| recordPause.addEventListener('click', recordPauseHandler); |
| recordResume.addEventListener('click', recordResumeHandler); |
| |
| recordDownload.addEventListener('click', downloadVideo); |
| |
| |
| function recordStartHandler() { |
| if (!recorder) throw new Error('未启动录屏'); |
| |
| recorder.start(); |
| recordStart.setAttribute('hidden', true); |
| recordStop.removeAttribute('hidden'); |
| recordPause.removeAttribute('hidden'); |
| console.log('开始录屏'); |
| } |
| |
| function recordStopHandler() { |
| if (!recorder) throw new Error('未启动录屏'); |
| |
| recorder.stop(); |
| recordStop.setAttribute('hidden', true); |
| recordPause.setAttribute('hidden', true); |
| recordResume.setAttribute('hidden', true); |
| closeScreen.setAttribute('hidden', true); |
| recordDownload.removeAttribute('hidden'); |
| } |
| |
| function recordPauseHandler() { |
| if (!recorder) throw new Error('未启动录屏'); |
| |
| recorder.pause(); |
| recordPause.setAttribute('hidden', true); |
| recordResume.removeAttribute('hidden'); |
| } |
| |
| function recordResumeHandler() { |
| if (!recorder) throw new Error('未启动录屏'); |
| |
| recorder.resume(); |
| recordPause.removeAttribute('hidden'); |
| recordResume.setAttribute('hidden', true); |
| } |
| |
| |
| let closed = false; |
| function closeScreenHandler() { |
| if (!recorder) throw new Error('屏幕共享未开启'); |
| |
| closed = true; |
| |
| |
| if (recorder.state === 'inactive') { |
| recordStart.setAttribute('hidden', true); |
| _close(); |
| return; |
| } |
| |
| const result = confirm('关闭屏幕共享后,录制的内容有可能丢失,确定要关闭屏幕共享吗?'); |
| if (result) { |
| recordStop.setAttribute('hidden', true); |
| recordPause.setAttribute('hidden', true); |
| recordResume.setAttribute('hidden', true); |
| recordDownload.setAttribute('hidden', true); |
| _close(); |
| } |
| } |
| |
| function _close() { |
| closeScreen.setAttribute('hidden', true); |
| removeVideo(); |
| recorder.stream.getTracks().forEach((track) => track.stop()); |
| recorder = null; |
| } |
| |
| function removeVideo() { |
| [...document.querySelectorAll('video')].forEach((dom) => dom.remove()); |
| } |
| |
| |
| async function shareScreenHandler() { |
| try { |
| removeVideo(); |
| closed = false; |
| |
| let stream = await navigator.mediaDevices.getDisplayMedia(constraints); |
| |
| let video = document.createElement('video'); |
| video.playsInline = true; |
| video.autoplay = true; |
| video.muted = true; |
| video.id = 'share-video'; |
| document.body.appendChild(video); |
| |
| |
| video.srcObject = stream; |
| |
| closeScreen.removeAttribute('hidden'); |
| recordStart.removeAttribute('hidden'); |
| recordDownload.setAttribute('hidden', true); |
| |
| recorder = new MediaRecorder(stream, { |
| mimeType: 'video/webm', |
| }); |
| |
| recorder.addEventListener('dataavailable', (event) => { |
| chunks.push(event.data); |
| }); |
| recorder.addEventListener('stop', () => { |
| if (closed) return; |
| |
| const blob = new Blob(chunks, { type: 'video/webm' }); |
| const url = URL.createObjectURL(blob); |
| console.log('录制结束'); |
| |
| const video2 = document.createElement('video'); |
| video2.width = parseInt(getComputedStyle(video).width); |
| video2.height = parseInt(getComputedStyle(video).height); |
| video.playsInline = true; |
| video2.muted = true; |
| video2.id = 'record-video'; |
| video2.loop = true; |
| |
| video2.addEventListener('durationchange', setVideoDuration.bind(video2)); |
| |
| video2.src = url; |
| video2.currentTime = 24 * 60 * 60; |
| |
| |
| |
| video.srcObject = null; |
| video.style.display = 'none'; |
| stream.getTracks().forEach((track) => track.stop()); |
| video = stream = recorder = null; |
| |
| document.body.appendChild(video2); |
| |
| |
| }); |
| |
| |
| |
| |
| |
| |
| } catch (err) { |
| if (recorder) { |
| recorder.stream.getTracks().forEach((track) => track.stop()); |
| recorder = null; |
| } |
| console.log(err.name + ' => ' + err.message); |
| } |
| } |
| |
| |
| async function setVideoDuration() { |
| const video2 = this; |
| if (video2.duration !== Infinity) { |
| video2.currentTime = 0; |
| |
| videoDuration = video2.duration; |
| } |
| } |
| |
| |
| async function downloadVideo() { |
| if (chunks.length === 0) throw new Error('录屏内容为空,请先录制屏幕共享内容'); |
| |
| console.log('正在解析视频中,马上开始下载录屏'); |
| |
| |
| const blob = new Blob(chunks, { type: 'video/webm' }); |
| const buffer = await blob.arrayBuffer(); |
| const newBuffer = setWebmDuration(buffer, videoDuration * 1000); |
| const newBlob = new Blob([newBuffer]); |
| const url = URL.createObjectURL(newBlob); |
| |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = 'video.webm'; |
| a.click(); |
| URL.revokeObjectURL(url); |
| } |
| |
| |
| function drawVideo(video) { |
| const canvas = document.createElement('canvas'); |
| const ctx = canvas.getContext('2d'); |
| |
| const width = video.offsetWidth || video.style.width || video.width, |
| height = video.offsetHeight || video.style.height || video.height; |
| |
| canvas.id = 'record-canvas'; |
| canvas.width = width * devicePixelRatio; |
| canvas.height = height * devicePixelRatio; |
| canvas.style.width = width + 'px'; |
| canvas.style.height = height + 'px'; |
| |
| document.body.appendChild(canvas); |
| |
| requestAnimationFrame(function _draw() { |
| ctx.drawImage(video, 0, 0, canvas.width, canvas.height); |
| requestAnimationFrame(_draw); |
| }); |
| } |
| </script> |
| </body> |
| </html> |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了