|
|
<!--
|
|
|
如果录音功能是以一个弹窗或者切换页形式打开,
|
|
|
请注意在离开页面的时候一定要主动触发停止录音功能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>
|