You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

300 lines
11 KiB
HTML

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!--
如果录音功能是以一个弹窗或者切换页形式打开,
请注意在离开页面的时候一定要主动触发停止录音功能mediaRecorder.stop();
并且销毁mediaRecorder实例
否则会导致
1. 内存泄漏
2. 浏览器卡顿或者崩溃
3. 麦克风被持续占用(无法被其他应用或页面使用)
什么情况下会导致该问题,比如录音功能是一个弹窗页面,点击开始录音之后,未点击结束录音直接关闭弹窗就会导致上述问题
-->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1"
/>
<title>录音与播放功能</title>
<style>
body {
font-family: 'Segoe UI', sans-serif;
background-color: #f5f7fa;
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background-color: #fff;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 30px;
width: 100%;
max-width: 500px;
text-align: center;
}
h1 {
color: #2c3e50;
margin-bottom: 20px;
}
.controls {
margin-bottom: 20px;
}
button {
background-color: #3498db;
color: #fff;
border: none;
padding: 12px 24px;
font-size: 16px;
border-radius: 8px;
cursor: pointer;
margin: 0 8px;
transition: background-color 0.3s;
}
button:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
button:hover {
background-color: #2980b9;
}
.timer {
font-size: 18px;
color: #e74c3c;
margin: 10px 0;
font-weight: 700;
}
.file-info {
margin-top: 10px;
color: #7f8c8d;
font-size: 14px;
font-weight: 400;
}
.upload-status {
margin-top: 10px;
font-size: 14px;
font-weight: 400;
}
.audio-player {
margin-top: 20px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
audio {
width: 100%;
height: 50px;
border: none;
background-color: #f0f0f0;
}
.status {
margin-top: 10px;
color: #7f8c8d;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<h1>录音与播放</h1>
<div class="controls">
<button id="startBtn">开始录音</button>
<button
id="stopBtn"
disabled="disabled"
>
结束录音
</button>
<button
id="uploadBtn"
disabled="disabled"
>
上传到服务器
</button>
</div>
<div
class="timer"
id="timer"
>
00:00
</div>
<div
class="file-info"
id="fileInfo"
>
文件大小0 KB
</div>
<div
class="upload-status"
id="uploadStatus"
></div>
<div
class="status"
id="status"
>
准备就绪
</div>
<div class="audio-player">
<audio
id="audioPlayer"
controls
></audio>
</div>
</div>
<script>
// 获取 DOM 元素
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const uploadBtn = document.getElementById('uploadBtn');
const timerEl = document.getElementById('timer');
const fileInfoEl = document.getElementById('fileInfo');
const uploadStatusEl = document.getElementById('uploadStatus');
const statusEl = document.getElementById('status');
const audioPlayer = document.getElementById('audioPlayer');
let mediaRecorder;
let audioChunks = [];
let recordingTime = 0;
const MAX_RECORDING_TIME = 60; // 最大60秒
const AUTO_STOP_TIME = 10; // 超过10秒自动停止
let timerInterval;
let audioBlob = null; // 存储录音结果,用于上传
// 模拟上传接口地址(可替换为真实后端地址)
const UPLOAD_URL = 'https://httpbin.org/post'; // 测试用接口,可上传文件
// 开始录音
startBtn.addEventListener('click', async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus',
});
audioChunks = [];
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data);
}
};
mediaRecorder.onstop = () => {
// 生成 Blob
audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
const audioUrl = URL.createObjectURL(audioBlob);
audioPlayer.src = audioUrl;
// 计算文件大小KB
const fileSizeKB = (audioBlob.size / 1024).toFixed(2);
fileInfoEl.textContent = `文件大小:${fileSizeKB} KB`;
statusEl.textContent = `录音完成,大小:${fileSizeKB} KB可播放`;
uploadBtn.disabled = false; // 启用上传按钮
stopBtn.disabled = true;
startBtn.disabled = false;
clearInterval(timerInterval);
timerEl.textContent = '00:00';
};
mediaRecorder.start();
startBtn.disabled = true;
stopBtn.disabled = false;
statusEl.textContent = '正在录音...';
// 启动计时器
recordingTime = 0;
timerInterval = setInterval(() => {
recordingTime++;
timerEl.textContent = formatTime(recordingTime);
// ✅ 超过10秒自动停止录音
if (recordingTime >= AUTO_STOP_TIME) {
statusEl.textContent = `⚠️ 已超过 ${AUTO_STOP_TIME} 秒,自动停止录音`;
stopRecording();
}
// 也检查最大时长(防止意外)
if (recordingTime >= MAX_RECORDING_TIME) {
statusEl.textContent = `⚠️ 已达到最大录音时长 ${MAX_RECORDING_TIME} 秒,自动停止`;
stopRecording();
}
}, 1000);
} catch (err) {
alert('无法访问麦克风:' + err.message);
console.error('录音失败:', err);
}
});
// 结束录音
stopBtn.addEventListener('click', stopRecording);
function stopRecording() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
}
if (mediaRecorder && mediaRecorder.stream) {
mediaRecorder.stream.getTracks().forEach((track) => track.stop());
}
}
// 上传录音文件
uploadBtn.addEventListener('click', async () => {
if (!audioBlob) {
alert('没有可上传的录音文件');
return;
}
uploadStatusEl.textContent = '正在上传...';
uploadBtn.disabled = true;
const formData = new FormData();
formData.append('audio', audioBlob, 'recording.webm'); // 文件名自定义
try {
const response = await fetch(UPLOAD_URL, {
method: 'POST',
body: formData,
});
if (response.ok) {
const result = await response.json();
uploadStatusEl.textContent = '✅ 上传成功!';
console.log('上传成功:', result);
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (err) {
uploadStatusEl.textContent = '❌ 上传失败:' + err.message;
console.error('上传失败:', err);
} finally {
uploadBtn.disabled = false;
}
});
// 格式化时间:秒转为 mm:ss
function formatTime(seconds) {
const mins = Math.floor(seconds / 60)
.toString()
.padStart(2, '0');
const secs = (seconds % 60).toString().padStart(2, '0');
return `${mins}:${secs}`;
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
stopBtn.disabled = true;
uploadBtn.disabled = true;
statusEl.textContent = '点击“开始录音”开始';
});
</script>
</body>
</html>