前端代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>录音功能</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.recording-container {
text-align: center;
margin-top: 50px;
}
.controls {
margin: 20px 0;
}
button {
padding: 10px 20px;
margin: 0 10px;
font-size: 16px;
cursor: pointer;
}
#recordingsList {
margin-top: 30px;
text-align: left;
}
audio {
width: 100%;
margin: 10px 0;
}
.upload-status {
margin-top: 10px;
padding: 10px;
border-radius: 5px;
}
.upload-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.upload-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.upload-progress {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
</style>
</head>
<body>
<div class="recording-container">
<h1>录音功能演示</h1>
<div class="controls">
<button id="recordButton">开始录音</button>
<button id="stopButton" disabled>停止录音</button>
</div>
<div id="recordingStatus">
<p>点击"开始录音"按钮开始录音</p>
</div>
<div id="recordingsList">
<h2>录音列表</h2>
<div id="recordings"></div>
</div>
</div>
<script>
let mediaRecorder;
let recordedChunks = [];
let recordingCount = 0;
const recordButton = document.getElementById('recordButton');
const stopButton = document.getElementById('stopButton');
const recordingStatus = document.getElementById('recordingStatus');
const recordings = document.getElementById('recordings');
// 检查浏览器是否支持录音功能
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
console.log('录音功能可用');
} else {
recordingStatus.innerHTML = '<p style="color: red;">您的浏览器不支持录音功能</p>';
}
// 开始录音
recordButton.addEventListener('click', async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// 使用浏览器支持的最佳音频格式
const options = getSupportedMimeTypes();
mediaRecorder = new MediaRecorder(stream, options);
recordedChunks = [];
mediaRecorder.start();
recordingStatus.innerHTML = '<p style="color: green;">正在录音...</p>';
recordButton.disabled = true;
stopButton.disabled = false;
mediaRecorder.addEventListener('dataavailable', event => {
if (event.data.size > 0) {
recordedChunks.push(event.data);
}
});
mediaRecorder.addEventListener('stop', () => {
debugger
const mimeType = mediaRecorder.mimeType || 'audio/mp3';
const fileExtension = getFileExtension(mimeType);
const blob = new Blob(recordedChunks, { type: mimeType });
// 生成文件名
const fileName = `recording${recordingCount + 1}.${fileExtension}`;
// 创建本地播放元素
createRecordingElement(blob, fileName);
// 自动上传到服务器
uploadRecording(blob, fileName);
recordingStatus.innerHTML = '<p>录音已停止,正在上传...</p>';
recordButton.disabled = false;
stopButton.disabled = true;
});
} catch (error) {
console.error('录音失败:', error);
recordingStatus.innerHTML = '<p style="color: red;">录音失败: ' + error.message + '</p>';
}
});
// 获取浏览器支持的MIME类型
function getSupportedMimeTypes() {
const possibleTypes = [
'audio/mp3',
'audio/webm',
'audio/ogg',
'audio/wav'
];
for (let type of possibleTypes) {
if (MediaRecorder.isTypeSupported(type)) {
return { mimeType: type };
}
}
// 如果都不支持,使用默认配置
return {};
}
// 根据MIME类型获取文件扩展名
function getFileExtension(mimeType) {
const mimeToExtension = {
'audio/mp3': 'mp3',
'audio/mpeg': 'mp3',
'audio/webm': 'webm',
'audio/ogg': 'ogg',
'audio/wav': 'wav',
'audio/wave': 'wav'
};
return mimeToExtension[mimeType] || 'mp3';
}
// 创建录音播放元素
function createRecordingElement(blob, fileName) {
const url = URL.createObjectURL(blob);
recordingCount++;
const recordingElement = document.createElement('div');
recordingElement.innerHTML = `
<h3>录音 ${recordingCount}</h3>
<audio controls>
<source src="${url}" type="${blob.type}">
您的浏览器不支持音频播放。
</audio>
<a href="${url}" download="${fileName}">下载录音</a>
<div id="uploadStatus${recordingCount}" class="upload-status"></div>
`;
recordings.prepend(recordingElement);
return recordingCount;
}
// 上传录音文件到服务器
function uploadRecording(blob, fileName) {
const formData = new FormData();
formData.append('audio', blob, fileName);
// 创建当前录音的上传状态元素
const currentUploadStatusId = `uploadStatus${recordingCount + 1}`;
// 更新上传状态显示
const updateUploadStatus = (message, className) => {
const statusElement = document.getElementById(currentUploadStatusId);
if (statusElement) {
statusElement.textContent = message;
statusElement.className = `upload-status ${className}`;
}
};
// 发送上传请求
fetch('https://170.170.10.82:8443/api/upload', {
method: 'POST',
body: formData
}).then(response => {
if (response.ok) {
updateUploadStatus('上传成功!', 'upload-success');
console.log('录音文件上传成功');
} else {
throw new Error(`上传失败: ${response.status} ${response.statusText}`);
}
}).catch(error => {
console.error('上传失败:', error);
updateUploadStatus(`上传失败: ${error.message}`, 'upload-error');
});
}
// 停止录音
stopButton.addEventListener('click', () => {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
// 停止所有音轨
mediaRecorder.stream.getTracks().forEach(track => track.stop());
}
});
</script>
</body>
</html>
java代码
package com.et.webrtc.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.http.ResponseEntity; import org.springframework.http.HttpStatus;
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.UUID;
@RestController @CrossOrigin(origins = "*") @RequestMapping("/api") public class AudioUploadController {
@Value("${upload.dir:uploads/}")
private String uploadDir;
@PostMapping("/upload")
public ResponseEntity<?> uploadAudio(@RequestParam("audio") MultipartFile file) {
try {
// 检查文件是否为空
if (file.isEmpty()) {
return ResponseEntity.badRequest().body(createResponse("error", "文件为空", null, null));
}
// 创建上传目录
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String fileExtension = getFileExtension(originalFilename);
String uniqueFilename = System.currentTimeMillis() + "_" + UUID.randomUUID().toString() + "." + fileExtension;
// 保存文件
Path filePath = uploadPath.resolve(uniqueFilename);
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
// 返回成功响应
return ResponseEntity.ok()
.body(createResponse("success", "文件上传成功", uniqueFilename, filePath.toString()));
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(createResponse("error", "文件上传失败: " + e.getMessage(), null, null));
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(createResponse("error", "上传过程中发生错误: " + e.getMessage(), null, null));
}
}
// 获取文件扩展名
private String getFileExtension(String filename) {
if (filename == null || filename.lastIndexOf(".") == -1) {
return "audio";
}
return filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
}
// 创建响应对象
private UploadResponse createResponse(String status, String message, String filename, String filepath) {
return new UploadResponse(status, message, filename, filepath);
}
// 响应数据类
public static class UploadResponse {
private String status;
private String message;
private String filename;
private String filepath;
public UploadResponse(String status, String message, String filename, String filepath) {
this.status = status;
this.message = message;
this.filename = filename;
this.filepath = filepath;
}
// Getters and Setters
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getFilename() { return filename; }
public void setFilename(String filename) { this.filename = filename; }
public String getFilepath() { return filepath; }
public void setFilepath(String filepath) { this.filepath = filepath; }
}
}