设备检测(Web)

最近更新时间:2026-06-09 11:59:45

我的收藏
本篇文档将指导您如何在用户加入房间前提供设备预览与测试功能。通过 Atomicx 提供的进房前测试接口,您可以引导用户检查并调整音视频设备状态,确保以最佳状态进入多人音视频房间。

功能介绍

设备预览是保障音视频房间顺利运行的重要环节。Atomicx 专为进房前场景提供了一套独立的测试接口,其核心能力包括:
视频画面预览:在不推流的情况下启动摄像头,验证画面清晰度与光线。
麦克风检测:通过实时音量反馈,确认麦克风采集是否正常。
扬声器校准:通过播放测试音,确认输出设备及其音量设置。

前提条件

用户已经通过 useLoginState 完成登录鉴权,请参考 接入概览

实现设备检测功能

步骤1:摄像头测试

使用 startCameraTest 方法启动摄像头测试。该方法仅用于本地预览,不会产生网络流量。
代码示例:
Vue3
React
<template>
<div class="preview-container">
<!-- 摄像头预览区域 -->
<div id="camera-preview" class="camera-preview">
<!-- 加载状态 -->
<div v-if="isCameraTestLoading" class="loading-overlay">
<div class="loading-spinner" />
<span>正在启动摄像头...</span>
</div>
</div>

<!-- 摄像头选择 -->
<div class="camera-selector">
<label>选择摄像头</label>
<select
:value="currentCamera?.deviceId"
@change="handleCameraChange"
:disabled="isCameraTestLoading"
>
<option
v-for="camera in cameraList"
:key="camera.deviceId"
:value="camera.deviceId"
>
{{ camera.deviceName }}
</option>
</select>
</div>

<!-- 控制按钮 -->
<div class="control-buttons">
<button @click="getCameraList">获取摄像头列表</button>
<button
@click="toggleCameraTest"
:disabled="isCameraTestLoading"
>
{{ isCameraTesting ? '停止预览' : '开始预览' }}
</button>
</div>
</div>
</template>

<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { IconWarning } from '@tencentcloud/uikit-base-component-vue3';
import {
useLoginState,
useDeviceState,
DeviceStatus,
DeviceError,
} from 'tuikit-atomicx-vue3/room';

const {
isCameraTesting,
isCameraTestLoading,
cameraList,
currentCamera,
startCameraTest,
stopCameraTest,
getCameraList,
setCurrentCamera
} = useDeviceState();

onUnmounted(() => {
if (isCameraTesting.value) {
stopCameraTest();
}
});

const toggleCameraTest = async () => {
if (isCameraTesting.value) {
await stopCameraTest();
} else {
const previewElement = document.getElementById('camera-preview');
if (previewElement) {
await startCameraTest({ view: previewElement });
}
}
};

const handleCameraChange = async (event: Event) => {
const deviceId = (event.target as HTMLSelectElement).value;
try {
await setCurrentCamera({ deviceId });
} catch (error) {
console.error('切换摄像头失败:', error);
}
};
</script>

<style scoped>
.preview-container { position: relative; width: 100%; max-width: 640px; margin: 0 auto;}.camera-preview { width: 100%; position: relative; aspect-ratio: 16 / 9; background-color: #000; border-radius: 8px; overflow: hidden;}.loading-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; background-color: rgba(0, 0, 0, 0.7); color: white; border-radius: 8px;}.loading-spinner { width: 40px; height: 40px; border: 4px solid rgba(255, 255, 255, 0.3); border-top-color: white; border-radius: 50%; animation: spin 0.8s linear infinite;}@keyframes spin { to { transform: rotate(360deg); }}.error-message { display: flex; align-items: center; gap: 8px; padding: 12px; margin-top: 12px; background-color: #ffebee; color: #c62828; border-radius: 4px; font-size: 14px;}.camera-selector { margin-top: 16px; display: flex; align-items: center; gap: 12px;}.camera-selector label { font-weight: 500; min-width: 80px;}.camera-selector select { flex: 1; padding: 8px 12px; border: 1px solid #e0e0e0; border-radius: 4px; font-size: 14px;}.control-buttons { margin-top: 16px; display: flex; gap: 12px;}.control-buttons button { flex: 1; padding: 12px 24px; background-color: #1976d2; color: white; border: none; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; transition: background-color 0.3s;}.control-buttons button:hover:not(:disabled) { background-color: #1565c0;}.control-buttons button:disabled { background-color: #bdbdbd; cursor: not-allowed;}
</style>
import React, { useEffect } from 'react';
import {
useDeviceState,
DeviceStatus,
DeviceError,
} from 'tuikit-atomicx-react/room';

const CameraTest: React.FC = () => {
const {
isCameraTesting,
isCameraTestLoading,
cameraList,
currentCamera,
startCameraTest,
stopCameraTest,
getCameraList,
setCurrentCamera
} = useDeviceState();

useEffect(() => {
return () => {
if (isCameraTesting) {
stopCameraTest();
}
};
}, []);

const toggleCameraTest = async () => {
if (isCameraTesting) {
await stopCameraTest();
} else {
const previewElement = document.getElementById('camera-preview');
if (previewElement) {
await startCameraTest({ view: previewElement });
}
}
};

const handleCameraChange = async (event: React.ChangeEvent<HTMLSelectElement>) => {
const deviceId = event.target.value;
try {
await setCurrentCamera({ deviceId });
} catch (error) {
console.error('切换摄像头失败:', error);
}
};

return (
<div className="preview-container">
{/* 摄像头预览区域 */}
<div id="camera-preview" className="camera-preview">
{/* 加载状态 */}
{isCameraTestLoading && (
<div className="loading-overlay">
<div className="loading-spinner" />
<span>正在启动摄像头...</span>
</div>
)}
</div>

{/* 摄像头选择 */}
<div className="camera-selector">
<label>选择摄像头</label>
<select
value={currentCamera?.deviceId}
onChange={handleCameraChange}
disabled={isCameraTestLoading}
>
{cameraList.map(camera => (
<option key={camera.deviceId} value={camera.deviceId}>
{camera.deviceName}
</option>
))}
</select>
</div>

{/* 控制按钮 */}
<div className="control-buttons">
<button onClick={getCameraList}>获取摄像头列表</button>
<button
onClick={toggleCameraTest}
disabled={isCameraTestLoading}
>
{isCameraTesting ? '停止预览' : '开始预览'}
</button>
</div>
</div>
);
};

export default CameraTest;

步骤2:麦克风测试

通过 startMicrophoneTest 启动麦克风测试,并利用 testingMicVolume 实现 UI 音量条的实时波动。
代码示例:
Vue3
React
<template>
<div class="microphone-test">
<!-- 音量显示 -->
<div class="volume-display">
<div class="volume-label">麦克风音量</div>
<div class="volume-bar-container">
<div
class="volume-bar"
:style="{ width: `${testingMicVolume}%` }"
:class="volumeClass"
/>
</div>
<div class="volume-value">{{ testingMicVolume }}</div>
</div>

<!-- 麦克风选择 -->
<div class="microphone-selector">
<label>选择麦克风</label>
<select
:value="currentMicrophone?.deviceId"
@change="handleMicrophoneChange"
:disabled="isMicrophoneTesting"
>
<option
v-for="mic in microphoneList"
:key="mic.deviceId"
:value="mic.deviceId"
>
{{ mic.deviceName }}
</option>
</select>
</div>

<!-- 控制按钮 -->
<div class="control-buttons">
<button
@click="getMicrophoneList"
>
获取设备
</button>
<button
@click="toggleMicrophoneTest"
>
{{ isMicrophoneTesting ? '停止测试' : '开始测试' }}
</button>
</div>

<!-- 测试提示 -->
<div v-if="isMicrophoneTesting" class="test-tip">
<span>请对着麦克风说话,查看音量变化</span>
</div>
</div>
</template>

<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import {
useDeviceState,
DeviceError,
} from 'tuikit-atomicx-vue3/room';

const {
isMicrophoneTesting,
testingMicVolume,
microphoneList,
currentMicrophone,
startMicrophoneTest,
stopMicrophoneTest,
getMicrophoneList,
setCurrentMicrophone
} = useDeviceState();

// 音量条样式类
const volumeClass = computed(() => {
const volume = testingMicVolume.value;
if (volume === 0) return 'volume-zero';
if (volume < 30) return 'volume-low';
if (volume < 70) return 'volume-medium';
return 'volume-high';
});

// 清理:停止测试
onUnmounted(() => {
if (isMicrophoneTesting.value) {
stopMicrophoneTest();
}
});

// 切换麦克风测试
const toggleMicrophoneTest = async () => {
if (isMicrophoneTesting.value) {
await stopMicrophoneTest();
} else {
try {
await startMicrophoneTest({ interval: 200 });
} catch (error) {
console.error('启动麦克风测试失败:', error);
}
}
};

// 切换麦克风设备
const handleMicrophoneChange = async (event: Event) => {
const deviceId = (event.target as HTMLSelectElement).value;
try {
await setCurrentMicrophone({ deviceId });
} catch (error) {
console.error('切换麦克风失败:', error);
}
};
</script>

<style scoped>
.microphone-test { width: 100%; max-width: 640px; margin: 0 auto;}.volume-display { padding: 24px; background-color: #f5f5f5; border-radius: 8px; margin-bottom: 16px;}.volume-label { font-size: 14px; font-weight: 500; color: #666; margin-bottom: 12px;}.volume-bar-container { width: 100%; height: 24px; background-color: #e0e0e0; border-radius: 12px; overflow: hidden; margin-bottom: 8px;}.volume-bar { height: 100%; transition: width 0.1s ease; border-radius: 12px;}.volume-bar.volume-zero { background-color: #9e9e9e;}.volume-bar.volume-low { background-color: #ff9800;}.volume-bar.volume-medium { background-color: #4caf50;}.volume-bar.volume-high { background-color: #2196f3;}.volume-value { text-align: center; font-size: 18px; font-weight: 600; color: #333;}.microphone-selector { display: flex; align-items: center; gap: 12px; margin-bottom: 16px;}.microphone-selector label { font-weight: 500; min-width: 80px;}.microphone-selector select { flex: 1; padding: 8px 12px; border: 1px solid #e0e0e0; border-radius: 4px; font-size: 14px;}.error-message { display: flex; align-items: center; gap: 8px; padding: 12px; margin-bottom: 16px; background-color: #ffebee; color: #c62828; border-radius: 4px; font-size: 14px;}.control-buttons { margin-bottom: 12px; display: flex; gap: 8px; }.control-buttons button { flex: 1; width: 100%; padding: 12px 24px; background-color: #1976d2; color: white; border: none; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; transition: background-color 0.3s;}.control-buttons button:hover:not(:disabled) { background-color: #1565c0;}.control-buttons button:disabled { background-color: #bdbdbd; cursor: not-allowed;}.test-tip { display: flex; align-items: center; gap: 8px; padding: 12px; background-color: #e3f2fd; color: #1565c0; border-radius: 4px; font-size: 13px;}
</style>
import React, { useMemo, useEffect } from 'react';
import {
useDeviceState,
DeviceError,
} from 'tuikit-atomicx-react/room';

const MicrophoneTest: React.FC = () => {
const {
isMicrophoneTesting,
testingMicVolume,
microphoneList,
currentMicrophone,
startMicrophoneTest,
stopMicrophoneTest,
getMicrophoneList,
setCurrentMicrophone
} = useDeviceState();

// Volume bar style class
const volumeClass = useMemo(() => {
const volume = testingMicVolume;
if (volume === 0) return 'volume-zero';
if (volume < 30) return 'volume-low';
if (volume < 70) return 'volume-medium';
return 'volume-high';
}, [testingMicVolume]);

// Cleanup: stop test
useEffect(() => {
return () => {
if (isMicrophoneTesting) {
stopMicrophoneTest();
}
};
}, []);

// Toggle microphone test
const toggleMicrophoneTest = async () => {
if (isMicrophoneTesting) {
await stopMicrophoneTest();
} else {
try {
await startMicrophoneTest({ interval: 200 });
} catch (error) {
console.error('启动麦克风测试失败:', error);
}
}
};

// Switch microphone device
const handleMicrophoneChange = async (event: React.ChangeEvent<HTMLSelectElement>) => {
const deviceId = event.target.value;
try {
await setCurrentMicrophone({ deviceId });
} catch (error) {
console.error('切换麦克风失败:', error);
}
};

return (
<div className="microphone-test">
{/* 音量显示 */}
<div className="volume-display">
<div className="volume-label">麦克风音量</div>
<div className="volume-bar-container">
<div
className={`volume-bar ${volumeClass}`}
style={{ width: `${testingMicVolume}%` }}
/>
</div>
<div className="volume-value">{testingMicVolume}</div>
</div>

{/* 麦克风选择 */}
<div className="microphone-selector">
<label>选择麦克风</label>
<select
value={currentMicrophone?.deviceId}
onChange={handleMicrophoneChange}
disabled={isMicrophoneTesting}
>
{microphoneList.map(mic => (
<option key={mic.deviceId} value={mic.deviceId}>
{mic.deviceName}
</option>
))}
</select>
</div>

{/* 控制按钮 */}
<div className="control-buttons">
<button onClick={getMicrophoneList}>
获取设备
</button>
<button onClick={toggleMicrophoneTest}>
{isMicrophoneTesting ? '停止测试' : '开始测试'}
</button>
</div>

{/* 测试提示 */}
{isMicrophoneTesting && (
<div className="test-tip">
<span>请对着麦克风说话,查看音量变化</span>
</div>
)}
</div>
);
};

export default MicrophoneTest;

步骤3:扬声器测试

使用 startSpeakerTest 方法播放一段指定的音频文件来确认扬声器是否工作正常。
代码示例:
Vue3
React
<template>
<div class="speaker-test">
<!-- 扬声器选择 -->
<div class="speaker-selector">
<label>选择扬声器</label>
<select
:value="currentSpeaker?.deviceId"
@change="handleSpeakerChange"
:disabled="isSpeakerTesting"
>
<option
v-for="speaker in speakerList"
:key="speaker.deviceId"
:value="speaker.deviceId"
>
{{ speaker.deviceName }}
</option>
</select>
</div>

<!-- 控制按钮 -->
<div class="control-buttons">
<button @click="getSpeakerList">获取扬声器列表</button>
<button
@click="toggleSpeakerTest"
:disabled="isSpeakerTesting"
>
{{ isSpeakerTesting ? '正在播放...' : '播放测试音频' }}
</button>
<button
v-if="isSpeakerTesting"
@click="stopSpeakerTest"
>
停止播放
</button>
</div>

<!-- 测试提示 -->
<div v-if="isSpeakerTesting" class="test-tip">
<span>正在播放测试音频,请确认是否能听到声音</span>
</div>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { useDeviceState } from 'tuikit-atomicx-vue3/room';

const {
isSpeakerTesting,
speakerList,
currentSpeaker,
startSpeakerTest,
stopSpeakerTest,
getSpeakerList,
setCurrentSpeaker,
} = useDeviceState();

// 测试音频文件路径
// 注意:此 URL 仅供开发测试使用,生产环境请使用您自己的音频文件,建议使用 3-5 秒的 MP3 格式音频文件
const testAudioPath = 'https://web.sdk.qcloud.com/trtc/electron/download/resources/media/TestSpeaker.mp3';

// 清理:停止测试
onUnmounted(() => {
if (isSpeakerTesting.value) {
stopSpeakerTest();
}
});

// 切换扬声器测试
const toggleSpeakerTest = async () => {
if (isSpeakerTesting.value) {
await stopSpeakerTest();
} else {
try {
await startSpeakerTest({ filePath: testAudioPath });
} catch (error) {
console.error('启动扬声器测试失败:', error);
}
}
};

// 切换扬声器设备
const handleSpeakerChange = async (event: Event) => {
const deviceId = (event.target as HTMLSelectElement).value;
try {
await setCurrentSpeaker({ deviceId });
} catch (error) {
console.error('切换扬声器失败:', error);
}
};
</script>

<style scoped>
.speaker-test { width: 100%; max-width: 640px; margin: 0 auto;}.speaker-selector { display: flex; align-items: center; gap: 12px; margin-bottom: 24px;}.speaker-selector label { font-weight: 500; min-width: 80px;}.speaker-selector select { flex: 1; padding: 8px 12px; border: 1px solid #e0e0e0; border-radius: 4px; font-size: 14px;}.volume-slider-container { display: flex; align-items: center; gap: 12px;}.volume-slider { flex: 1; height: 6px; border-radius: 3px; background: #e0e0e0; outline: none; -webkit-appearance: none;}.volume-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: #1976d2; cursor: pointer;}.volume-slider::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: #1976d2; cursor: pointer; border: none;}.volume-value { min-width: 50px; text-align: right; font-weight: 500; color: #333;}.control-buttons { display: flex; gap: 12px; margin-bottom: 12px;}button { flex: 1; padding: 12px 24px; background-color: #1976d2; color: white; border: none; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; transition: background-color 0.3s;}.control-buttons button:hover:not(:disabled) { background-color: #1565c0;}.control-buttons button:disabled { background-color: #bdbdbd; cursor: not-allowed;}.test-tip { display: flex; align-items: center; gap: 8px; padding: 12px; background-color: #e3f2fd; color: #1565c0; border-radius: 4px; font-size: 13px;}
</style>
import React, { useEffect } from 'react';
import { useDeviceState } from 'tuikit-atomicx-react/room';

const SpeakerTest: React.FC = () => {
const {
isSpeakerTesting,
speakerList,
currentSpeaker,
startSpeakerTest,
stopSpeakerTest,
getSpeakerList,
setCurrentSpeaker,
} = useDeviceState();

// Test audio file path
// Note: This URL is for development testing only. In production, use your own audio file. Recommended: 3-5 second MP3 format.
const testAudioPath = 'https://web.sdk.qcloud.com/trtc/electron/download/resources/media/TestSpeaker.mp3';

// Cleanup: stop test
useEffect(() => {
return () => {
if (isSpeakerTesting) {
stopSpeakerTest();
}
};
}, []);

// Toggle speaker test
const toggleSpeakerTest = async () => {
if (isSpeakerTesting) {
await stopSpeakerTest();
} else {
try {
await startSpeakerTest({ filePath: testAudioPath });
} catch (error) {
console.error('启动扬声器测试失败:', error);
}
}
};

// Switch speaker device
const handleSpeakerChange = async (event: React.ChangeEvent<HTMLSelectElement>) => {
const deviceId = event.target.value;
try {
await setCurrentSpeaker({ deviceId });
} catch (error) {
console.error('切换扬声器失败:', error);
}
};

return (
<div className="speaker-test">
{/* 扬声器选择 */}
<div className="speaker-selector">
<label>选择扬声器</label>
<select
value={currentSpeaker?.deviceId}
onChange={handleSpeakerChange}
disabled={isSpeakerTesting}
>
{speakerList.map(speaker => (
<option key={speaker.deviceId} value={speaker.deviceId}>
{speaker.deviceName}
</option>
))}
</select>
</div>

{/* 控制按钮 */}
<div className="control-buttons">
<button onClick={getSpeakerList}>获取扬声器列表</button>
<button
onClick={toggleSpeakerTest}
disabled={isSpeakerTesting}
>
{isSpeakerTesting ? '正在播放...' : '播放测试音频'}
</button>
{isSpeakerTesting && (
<button onClick={stopSpeakerTest}>
停止播放
</button>
)}
</div>

{/* 测试提示 */}
{isSpeakerTesting && (
<div className="test-tip">
<span>正在播放测试音频,请确认是否能听到声音</span>
</div>
)}
</div>
);
};

export default SpeakerTest;

API 文档

State/Component
功能描述
API 文档
useDeviceState
包含音视频设备状态,音视频设备列表及操作接口。

常见问题

为什么摄像头预览没有画面?

可能的原因包括:
权限被拒绝:检查浏览器是否允许摄像头权限。
设备被占用:关闭其他使用摄像头的应用。
设备未连接:检查摄像头是否正确连接。
浏览器不支持:使用 Chrome、Edge 等现代浏览器。

麦克风测试时音量一直为 0?

可能的原因包括:
麦克风权限未授予:检查浏览器权限设置。
麦克风被静音:检查系统麦克风设置。
设备选择错误:切换到正确的麦克风设备。