<!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;
/* height: 300px; */
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">
// 下載依賴 npm i @ozean/set-webm-duration
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 };
/** @type {MediaRecorder} */
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;
// video2.play();
// 錄製結束,關閉錄屏、track
video.srcObject = null;
video.style.display = 'none';
stream.getTracks().forEach((track) => track.stop());
video = stream = recorder = null;
document.body.appendChild(video2);
// 繪製錄屏內容 (到 canvas)
// drawVideo(video2);
});
// drawVideo(video);
// window.addEventListener('resize', () => {
// document.getElementById('record-canvas').remove();
// drawVideo(video);
// })
} 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;
// console.log("video2.duration => ", video2.duration)
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>