<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>麦克风和摄像头测试</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.camera-container {
position: relative;
width: 100%;
max-width: 640px;
margin: 0 auto;
}
.audio-level-bar {
height: 20px;
background-color: #e5e7eb;
border-radius: 10px;
overflow: hidden;
margin-top: 10px;
}
.audio-level-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #10b981);
border-radius: 10px;
transition: width 0.1s ease;
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.status-active {
background-color: #10b981;
box-shadow: 0 0 8px #10b981;
}
.status-inactive {
background-color: #ef4444;
}
.device-item {
transition: all 0.3s ease;
}
.device-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
</style>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto px-4 py-8">
<div class="text-center mb-10">
<h1 class="text-3xl font-bold text-gray-800 mb-2">麦克风和摄像头测试</h1>
<p class="text-gray-600">测试您的设备音频和视频功能</p>
</div>
<!-- 设备状态区域 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-10">
<!-- 麦克风状态 -->
<div class="bg-white rounded-xl shadow-lg p-6 device-item">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-800 flex items-center">
<span class="status-indicator status-inactive" id="mic-status"></span>
麦克风
</h2>
<span class="text-sm text-gray-500" id="mic-status-text">未检测到</span>
</div>
<div class="mb-4">
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>音量强度</span>
<span id="mic-volume-value">0%</span>
</div>
<div class="audio-level-bar">
<div class="audio-level-fill" id="mic-level-bar" style="width: 0%"></div>
</div>
</div>
<div class="space-y-2">
<button id="start-mic-btn" class="w-full bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded-lg transition duration-200">
开始测试麦克风
</button>
<button id="stop-mic-btn" class="w-full bg-red-500 hover:bg-red-600 text-white py-2 px-4 rounded-lg transition duration-200 hidden">
停止测试
</button>
</div>
</div>
<!-- 摄像头状态 -->
<div class="bg-white rounded-xl shadow-lg p-6 device-item">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-800 flex items-center">
<span class="status-indicator status-inactive" id="camera-status"></span>
摄像头
</h2>
<span class="text-sm text-gray-500" id="camera-status-text">未检测到</span>
</div>
<div class="camera-container">
<video id="camera-preview" autoplay muted playsinline class="w-full h-64 object-cover rounded-lg bg-gray-200 border border-gray-300"></video>
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
<div id="camera-placeholder" class="text-gray-500 text-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-2 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<p>摄像头预览</p>
</div>
</div>
</div>
<div class="mt-4 space-y-2">
<button id="start-camera-btn" class="w-full bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded-lg transition duration-200">
开始测试摄像头
</button>
<button id="stop-camera-btn" class="w-full bg-red-500 hover:bg-red-600 text-white py-2 px-4 rounded-lg transition duration-200 hidden">
停止测试
</button>
</div>
</div>
</div>
<!-- 设备列表 -->
<div class="bg-white rounded-xl shadow-lg p-6 mb-10">
<h2 class="text-xl font-semibold text-gray-800 mb-4">可用设备</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h3 class="font-medium text-gray-700 mb-2">音频输入设备</h3>
<select id="audio-input-select" class="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="">选择音频输入设备</option>
</select>
</div>
<div>
<h3 class="font-medium text-gray-700 mb-2">视频输入设备</h3>
<select id="video-input-select" class="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="">选择视频输入设备</option>
</select>
</div>
</div>
</div>
<!-- 测试结果 -->
<div class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-xl font-semibold text-gray-800 mb-4">测试结果</h2>
<div id="test-results" class="prose max-w-none">
<p class="text-gray-600">点击上方按钮开始测试您的设备。</p>
</div>
</div>
</div>
<script>
// 全局变量
let mediaStream = null;
let audioContext = null;
let analyser = null;
let microphone = null;
let animationFrameId = null;
// DOM 元素
const startMicBtn = document.getElementById('start-mic-btn');
const stopMicBtn = document.getElementById('stop-mic-btn');
const startCameraBtn = document.getElementById('start-camera-btn');
const stopCameraBtn = document.getElementById('stop-camera-btn');
const cameraPreview = document.getElementById('camera-preview');
const micLevelBar = document.getElementById('mic-level-bar');
const micVolumeValue = document.getElementById('mic-volume-value');
const micStatus = document.getElementById('mic-status');
const micStatusText = document.getElementById('mic-status-text');
const cameraStatus = document.getElementById('camera-status');
const cameraStatusText = document.getElementById('camera-status-text');
const testResults = document.getElementById('test-results');
const audioInputSelect = document.getElementById('audio-input-select');
const videoInputSelect = document.getElementById('video-input-select');
const cameraPlaceholder = document.getElementById('camera-placeholder');
// 初始化设备列表
async function initDeviceList() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const audioInputs = devices.filter(device => device.kind === 'audioinput');
const videoInputs = devices.filter(device => device.kind === 'videoinput');
// 清空现有选项
audioInputSelect.innerHTML = '<option value="">选择音频输入设备</option>';
videoInputSelect.innerHTML = '<option value="">选择视频输入设备</option>';
// 添加音频输入设备
audioInputs.forEach(device => {
const option = document.createElement('option');
option.value = device.deviceId;
option.textContent = device.label || `麦克风 ${audioInputSelect.children.length}`;
audioInputSelect.appendChild(option);
});
// 添加视频输入设备
videoInputs.forEach(device => {
const option = document.createElement('option');
option.value = device.deviceId;
option.textContent = device.label || `摄像头 ${videoInputSelect.children.length}`;
videoInputSelect.appendChild(option);
});
} catch (error) {
console.error('获取设备列表失败:', error);
testResults.innerHTML = `<p class="text-red-500">无法获取设备列表: ${error.message}</p>`;
}
}
// 更新麦克风音量显示
function updateMicLevel() {
if (!analyser) return;
const dataArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(dataArray);
// 计算平均值
let sum = 0;
for (let i = 0; i < dataArray.length; i++) {
sum += dataArray[i];
}
const average = sum / dataArray.length;
// 更新UI
const volumePercent = Math.round(average / 2.55); // 转换为百分比
micVolumeValue.textContent = `${volumePercent}%`;
micLevelBar.style.width = `${volumePercent}%`;
// 递归调用以持续更新
animationFrameId = requestAnimationFrame(updateMicLevel);
}
// 开始麦克风测试
async function startMicrophoneTest() {
try {
// 停止之前的流
stopMicrophoneTest();
// 获取设备ID
const deviceId = audioInputSelect.value || undefined;
const constraints = {
audio: deviceId ? { deviceId: { exact: deviceId } } : true,
video: false
};
// 获取媒体流
mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
// 创建音频上下文和分析器
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
microphone = audioContext.createMediaStreamSource(mediaStream);
microphone.connect(analyser);
// 启动音量监控
updateMicLevel();
// 更新UI状态
micStatus.className = 'status-indicator status-active';
micStatusText.textContent = '正在使用';
startMicBtn.classList.add('hidden');
stopMicBtn.classList.remove('hidden');
testResults.innerHTML = `
<div class="text-green-600">
<p><strong>✓ 麦克风测试成功</strong></p>
<p>已成功访问麦克风设备</p>
${deviceId ? `<p>设备: ${audioInputSelect.options[audioInputSelect.selectedIndex].text}</p>` : ''}
</div>
`;
} catch (error) {
console.error('麦克风测试失败:', error);
testResults.innerHTML = `
<div class="text-red-600">
<p><strong>✗ 麦克风测试失败</strong></p>
<p>${error.message}</p>
</div>
`;
// 恢复按钮状态
startMicBtn.classList.remove('hidden');
stopMicBtn.classList.add('hidden');
}
}
// 停止麦克风测试
function stopMicrophoneTest() {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
if (mediaStream) {
mediaStream.getTracks().forEach(track => track.stop());
mediaStream = null;
}
if (microphone) {
microphone.disconnect();
microphone = null;
}
if (analyser) {
analyser.disconnect();
analyser = null;
}
if (audioContext) {
audioContext.close();
audioContext = null;
}
// 更新UI状态
micStatus.className = 'status-indicator status-inactive';
micStatusText.textContent = '未检测到';
startMicBtn.classList.remove('hidden');
stopMicBtn.classList.add('hidden');
micVolumeValue.textContent = '0%';
micLevelBar.style.width = '0%';
}
// 开始摄像头测试
async function startCameraTest() {
try {
// 停止之前的流
stopCameraTest();
// 获取设备ID
const deviceId = videoInputSelect.value || undefined;
const constraints = {
audio: false,
video: deviceId ? { deviceId: { exact: deviceId } } : true
};
// 获取媒体流
mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
// 设置视频预览
cameraPreview.srcObject = mediaStream;
cameraPlaceholder.classList.add('hidden');
// 更新UI状态
cameraStatus.className = 'status-indicator status-active';
cameraStatusText.textContent = '正在使用';
startCameraBtn.classList.add('hidden');
stopCameraBtn.classList.remove('hidden');
testResults.innerHTML = `
<div class="text-green-600">
<p><strong>✓ 摄像头测试成功</strong></p>
<p>已成功访问摄像头设备</p>
${deviceId ? `<p>设备: ${videoInputSelect.options[videoInputSelect.selectedIndex].text}</p>` : ''}
</div>
`;
} catch (error) {
console.error('摄像头测试失败:', error);
testResults.innerHTML = `
<div class="text-red-600">
<p><strong>✗ 摄像头测试失败</strong></p>
<p>${error.message}</p>
</div>
`;
// 恢复按钮状态
startCameraBtn.classList.remove('hidden');
stopCameraBtn.classList.add('hidden');
}
}
// 停止摄像头测试
function stopCameraTest() {
if (mediaStream) {
mediaStream.getTracks().forEach(track => track.stop());
mediaStream = null;
}
// 清除视频预览
cameraPreview.srcObject = null;
cameraPlaceholder.classList.remove('hidden');
// 更新UI状态
cameraStatus.className = 'status-indicator status-inactive';
cameraStatusText.textContent = '未检测到';
startCameraBtn.classList.remove('hidden');
stopCameraBtn.classList.add('hidden');
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', async () => {
// 初始化设备列表
await initDeviceList();
// 绑定事件监听器
startMicBtn.addEventListener('click', startMicrophoneTest);
stopMicBtn.addEventListener('click', stopMicrophoneTest);
startCameraBtn.addEventListener('click', startCameraTest);
stopCameraBtn.addEventListener('click', stopCameraTest);
// 设备选择变化时清除测试状态
audioInputSelect.addEventListener('change', () => {
if (mediaStream && mediaStream.active) {
stopMicrophoneTest();
}
});
videoInputSelect.addEventListener('change', () => {
if (mediaStream && mediaStream.active) {
stopCameraTest();
}
});
// 显示初始信息
testResults.innerHTML = '<p class="text-gray-600">点击上方按钮开始测试您的设备。</p>';
});
// 处理页面可见性变化(防止后台运行导致的问题)
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// 页面隐藏时停止测试
if (mediaStream && mediaStream.active) {
stopMicrophoneTest();
stopCameraTest();
}
}
});
</script>
</body>
</html>
index.html
index.html