[HTML] 纯文本查看 复制代码 <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>录屏工具</title>
</head>
<body>
<div>
<h3>录屏工具</h3>
<div>
<label><input type="checkbox" id="sysAudio" checked>系统声音</label>
<label style="margin-left:10px;"><input type="checkbox" id="micAudio" checked>麦克风</label>
</div>
<div id="status">点击开始</div>
<div id="timer">00:00</div>
<button id="start">开始录制</button>
<button id="stop" disabled>停止录制</button>
</div>
<script>
const state = { rec: null, screen: null, stream: null, aCtx: null, writer: null, timerId: null, sec: 0 };
const el = Object.fromEntries(['start', 'stop', 'sysAudio', 'micAudio', 'status', 'timer'].map(id => [id, document.getElementById(id)]));
const fmt = sec => `${(sec / 60 | 0).toString().padStart(2, 0)}:${(sec % 60).toString().padStart(2, 0)}`;
const reset = () => {
el.start.disabled = el.sysAudio.disabled = el.micAudio.disabled = false;
el.stop.disabled = true;
el.timer.textContent = '00:00';
state.aCtx?.close();
Object.keys(state).forEach(k => state[k] = null);
state.sec = 0;
};
el.start.addEventListener('click', async () => {
try {
el.start.disabled = el.sysAudio.disabled = el.micAudio.disabled = true;
el.stop.disabled = false;
el.status.textContent = '选择路径...';
state.writer = await (await showSaveFilePicker({ suggestedName: '录屏.webm', types: [{ description: 'WebM', accept: { 'video/webm': ['.webm'] } }] })).createWritable();
el.status.textContent = '准备录制...';
state.screen = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: el.sysAudio.checked });
const audioStreams = [
...(el.micAudio.checked ? [await navigator.mediaDevices.getUserMedia({ audio: true })] : []),
...(el.sysAudio.checked && state.screen.getAudioTracks().length ? [new MediaStream(state.screen.getAudioTracks())] : [])
];
state.stream = audioStreams.length
? (() => {
state.aCtx = new AudioContext();
const destination = state.aCtx.createMediaStreamDestination();
audioStreams.forEach(stream => { state.aCtx.createMediaStreamSource(stream).connect(destination); });
return new MediaStream([...state.screen.getVideoTracks(), ...destination.stream.getAudioTracks()]);
})()
: state.screen;
state.rec = new MediaRecorder(state.stream, { mimeType: 'video/webm; codecs=vp8,opus' });
state.rec.ondataavailable = e => { if (e.data.size) { state.writer.write(e.data); } };
state.rec.onstart = () => {
el.status.textContent = '正在录制...';
const startTime = performance.now();
state.timerId = requestAnimationFrame(function updateTimer() {
const currentTime = Math.floor((performance.now() - startTime) / 1000);
if (currentTime !== state.sec) { state.sec = currentTime; el.timer.textContent = fmt(state.sec); }
state.timerId = requestAnimationFrame(updateTimer);
});
};
(state.screen.getVideoTracks()[0] || {}).onended = stopRec;
state.rec.start(5000);
} catch (err) {
alert(err.message || '已取消');
el.status.textContent = '已取消';
reset();
}
});
const stopRec = async () => {
try {
if (state.rec?.state === 'inactive') return;
state.rec.stop();
cancelAnimationFrame(state.timerId);
const duration = fmt(state.sec);
[state.stream, state.screen].forEach(stream => { if (stream) { stream.getTracks().forEach(track => track.stop()); } });
await state.writer?.close();
el.status.textContent = `录制完成!${duration}`;
alert(`已保存\n时长:${duration}`);
} catch (err) {
el.status.textContent = '操作出错';
console.error(err);
} finally {
reset();
}
};
el.stop.addEventListener('click', stopRec);
</script>
</body>
</html>
|