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

390 lines
15 KiB

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.

<!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: 50px auto;
padding: 20px;
}
.test-section {
margin: 20px 0;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
button {
padding: 10px 20px;
margin: 5px;
cursor: pointer;
}
.log {
background: #f5f5f5;
padding: 10px;
margin: 10px 0;
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
.success { color: green; }
.error { color: red; }
.info { color: blue; }
</style>
</head>
<body>
<h1>🎤 语音功能测试工具</h1>
<div class="test-section">
<h2>1. 浏览器支持检测</h2>
<button onclick="checkBrowserSupport()">检测浏览器支持</button>
<div id="browserSupport" class="log"></div>
</div>
<div class="test-section">
<h2>2. 麦克风权限测试</h2>
<button onclick="testMicrophonePermission()">测试麦克风权限</button>
<div id="micPermission" class="log"></div>
</div>
<div class="test-section">
<h2>3. 录音测试</h2>
<button onclick="startRecording()" id="recordBtn">开始录音</button>
<button onclick="stopRecording()" id="stopBtn" disabled>停止录音</button>
<button onclick="playRecording()" id="playBtn" disabled>播放录音</button>
<div id="recordLog" class="log"></div>
<audio id="audioPlayer" controls style="width: 100%; margin-top: 10px;"></audio>
</div>
<div class="test-section">
<h2>4. WebSocket 连接测试</h2>
<input type="text" id="wsUrl" value="ws://localhost:8080" style="width: 300px;">
<button onclick="testWebSocket()">测试连接</button>
<div id="wsLog" class="log"></div>
</div>
<div class="test-section">
<h2>5. 完整流程测试</h2>
<p>用户名: <input type="text" id="username" value="TestUser"></p>
<p>接收者: <input type="text" id="receiver" value="OtherUser"></p>
<button onclick="connectAndLogin()">连接并登录</button>
<button onclick="sendTestVoice()" id="sendVoiceBtn" disabled>发送测试语音</button>
<div id="fullTestLog" class="log"></div>
</div>
<script>
let mediaRecorder = null;
let audioChunks = [];
let recordedBlob = null;
let ws = null;
let stream = null;
function log(elementId, message, type = 'info') {
const element = document.getElementById(elementId);
const time = new Date().toLocaleTimeString();
const className = type;
element.innerHTML += `<div class="${className}">[${time}] ${message}</div>`;
element.scrollTop = element.scrollHeight;
}
function checkBrowserSupport() {
const logId = 'browserSupport';
document.getElementById(logId).innerHTML = '';
log(logId, '开始检测浏览器支持...', 'info');
const features = {
'WebSocket': typeof WebSocket !== 'undefined',
'MediaDevices': !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
'MediaRecorder': typeof MediaRecorder !== 'undefined',
'FileReader': typeof FileReader !== 'undefined',
'Blob': typeof Blob !== 'undefined',
'Promise': typeof Promise !== 'undefined'
};
for (const [feature, supported] of Object.entries(features)) {
if (supported) {
log(logId, `${feature}: 支持`, 'success');
} else {
log(logId, `${feature}: 不支持`, 'error');
}
}
// 检查安全上下文
const isSecure = window.isSecureContext ||
location.protocol === 'https:' ||
location.hostname === 'localhost' ||
location.hostname === '127.0.0.1';
if (isSecure) {
log(logId, '✅ 安全上下文: 是', 'success');
} else {
log(logId, '❌ 安全上下文: 否(需要 HTTPS 或 localhost', 'error');
}
log(logId, `协议: ${location.protocol}`, 'info');
log(logId, `主机: ${location.hostname}`, 'info');
// 检查支持的 MIME 类型
if (typeof MediaRecorder !== 'undefined') {
const mimeTypes = [
'audio/webm;codecs=opus',
'audio/webm',
'audio/ogg;codecs=opus',
'audio/mp4'
];
log(logId, '支持的音频格式:', 'info');
mimeTypes.forEach(type => {
if (MediaRecorder.isTypeSupported(type)) {
log(logId, `${type}`, 'success');
} else {
log(logId, `${type}`, 'error');
}
});
}
}
async function testMicrophonePermission() {
const logId = 'micPermission';
document.getElementById(logId).innerHTML = '';
log(logId, '请求麦克风权限...', 'info');
try {
const testStream = await navigator.mediaDevices.getUserMedia({ audio: true });
log(logId, '✅ 麦克风权限已授予', 'success');
const tracks = testStream.getAudioTracks();
log(logId, `音轨数量: ${tracks.length}`, 'info');
tracks.forEach((track, index) => {
log(logId, `音轨 ${index + 1}:`, 'info');
log(logId, ` 标签: ${track.label}`, 'info');
log(logId, ` 状态: ${track.readyState}`, 'info');
log(logId, ` 启用: ${track.enabled}`, 'info');
});
// 列出所有音频设备
const devices = await navigator.mediaDevices.enumerateDevices();
const audioInputs = devices.filter(d => d.kind === 'audioinput');
log(logId, `可用麦克风设备: ${audioInputs.length}`, 'info');
audioInputs.forEach((device, index) => {
log(logId, ` ${index + 1}. ${device.label || '未命名设备'}`, 'info');
});
testStream.getTracks().forEach(track => track.stop());
log(logId, '测试完成,已释放麦克风', 'success');
} catch (error) {
log(logId, `❌ 错误: ${error.name}`, 'error');
log(logId, `消息: ${error.message}`, 'error');
if (error.name === 'NotAllowedError') {
log(logId, '解决方法: 点击地址栏的锁图标,允许麦克风权限', 'info');
} else if (error.name === 'NotFoundError') {
log(logId, '解决方法: 检查麦克风是否已连接', 'info');
}
}
}
async function startRecording() {
const logId = 'recordLog';
document.getElementById(logId).innerHTML = '';
try {
log(logId, '请求麦克风访问...', 'info');
stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
log(logId, '✅ 麦克风访问成功', 'success');
let mimeType = 'audio/webm';
if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
mimeType = 'audio/webm;codecs=opus';
}
log(logId, `使用格式: ${mimeType}`, 'info');
mediaRecorder = new MediaRecorder(stream, { mimeType: mimeType });
audioChunks = [];
const startTime = Date.now();
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data);
log(logId, `收到数据块: ${event.data.size} 字节`, 'info');
}
};
mediaRecorder.onstop = () => {
const duration = Math.floor((Date.now() - startTime) / 1000);
log(logId, `录音结束,时长: ${duration}`, 'success');
recordedBlob = new Blob(audioChunks, { type: mimeType });
log(logId, `音频大小: ${recordedBlob.size} 字节`, 'info');
const url = URL.createObjectURL(recordedBlob);
document.getElementById('audioPlayer').src = url;
document.getElementById('playBtn').disabled = false;
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start(100);
log(logId, '🔴 开始录音...', 'success');
document.getElementById('recordBtn').disabled = true;
document.getElementById('stopBtn').disabled = false;
} catch (error) {
log(logId, `❌ 错误: ${error.name} - ${error.message}`, 'error');
}
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
document.getElementById('recordBtn').disabled = false;
document.getElementById('stopBtn').disabled = true;
}
}
function playRecording() {
document.getElementById('audioPlayer').play();
}
function testWebSocket() {
const logId = 'wsLog';
const url = document.getElementById('wsUrl').value;
document.getElementById(logId).innerHTML = '';
log(logId, `连接到: ${url}`, 'info');
try {
const testWs = new WebSocket(url);
testWs.onopen = () => {
log(logId, '✅ WebSocket 连接成功', 'success');
testWs.close();
};
testWs.onerror = (error) => {
log(logId, '❌ WebSocket 连接失败', 'error');
log(logId, '请确保服务器正在运行', 'info');
};
testWs.onclose = (event) => {
log(logId, `连接关闭, code: ${event.code}`, 'info');
};
} catch (error) {
log(logId, `❌ 错误: ${error.message}`, 'error');
}
}
function connectAndLogin() {
const logId = 'fullTestLog';
const url = document.getElementById('wsUrl').value;
const username = document.getElementById('username').value;
document.getElementById(logId).innerHTML = '';
log(logId, `连接到: ${url}`, 'info');
ws = new WebSocket(url);
ws.onopen = () => {
log(logId, '✅ WebSocket 连接成功', 'success');
const loginMsg = {
type: 'LOGIN',
sender: username
};
ws.send(JSON.stringify(loginMsg));
log(logId, `发送登录消息: ${username}`, 'info');
};
ws.onmessage = (event) => {
log(logId, `收到消息: ${event.data}`, 'info');
try {
const message = JSON.parse(event.data);
if (message.type === 'LOGIN_SUCCESS') {
log(logId, '✅ 登录成功', 'success');
document.getElementById('sendVoiceBtn').disabled = false;
} else if (message.type === 'VOICE') {
log(logId, '✅ 收到语音消息!', 'success');
log(logId, ` 发送者: ${message.sender}`, 'info');
log(logId, ` 文件名: ${message.fileName}`, 'info');
log(logId, ` 时长: ${message.duration}`, 'info');
log(logId, ` 大小: ${message.fileSize} 字节`, 'info');
}
} catch (e) {
log(logId, `解析消息失败: ${e.message}`, 'error');
}
};
ws.onerror = (error) => {
log(logId, '❌ WebSocket 错误', 'error');
};
ws.onclose = () => {
log(logId, 'WebSocket 连接关闭', 'info');
document.getElementById('sendVoiceBtn').disabled = true;
};
}
async function sendTestVoice() {
const logId = 'fullTestLog';
const receiver = document.getElementById('receiver').value;
const username = document.getElementById('username').value;
if (!recordedBlob) {
log(logId, '❌ 请先录制语音', 'error');
return;
}
log(logId, '准备发送语音消息...', 'info');
const reader = new FileReader();
reader.onload = function(e) {
const fileData = e.target.result;
const base64Data = fileData.split(',')[1];
const message = {
type: 'VOICE',
sender: username,
receiver: receiver,
fileName: `test_voice_${Date.now()}.webm`,
fileType: 'audio/webm',
fileSize: recordedBlob.size,
fileData: base64Data,
duration: 5
};
log(logId, `发送语音消息到: ${receiver}`, 'info');
log(logId, `数据大小: ${base64Data.length} 字符`, 'info');
ws.send(JSON.stringify(message));
log(logId, '✅ 语音消息已发送', 'success');
};
reader.readAsDataURL(recordedBlob);
}
</script>
</body>
</html>