|
|
<!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>
|