Bläddra i källkod

✨ feat(rtc): add audio device management capabilities

- add audio device enumeration with getAudioDevices() method
- implement microphone switching functionality with switchMicrophone()
- add speaker switching capability with switchSpeaker()
- add audioDeviceChanged event for device change notifications
- add audio device selection support in createTracks() method

✨ feat(umd-test): enhance test page with audio device controls

- add microphone selection dropdown
- add speaker selection dropdown
- implement device refresh functionality
- add audio device status display
- add RTC manager initialization controls
- update test page to use direct SDK import instead of UMD

✅ test(rtc): add audio device management test cases

- add getAudioDevices() test scenarios
- add switchMicrophone() test cases
- implement switchSpeaker() test scenarios
- add error handling tests for device operations
- add test mocks for MediaDevices API
yourname 3 månader sedan
förälder
incheckning
a1bfd76f45

+ 62 - 1
packages/stt-sdk-core/src/managers/rtc-manager-adapter.ts

@@ -90,7 +90,7 @@ export class RtcManagerAdapter extends AGEventEmitter<RtcEventMap> implements IR
     }
   }
 
-  async createTracks(): Promise<void> {
+  async createTracks(audioDeviceId?: string): Promise<void> {
     if (!this._joined) {
       throw new SttError(
         'NOT_JOINED',
@@ -102,6 +102,7 @@ export class RtcManagerAdapter extends AGEventEmitter<RtcEventMap> implements IR
       // 创建麦克风和摄像头轨道
       const tracks = await AgoraRTC.createMicrophoneAndCameraTracks({
         AGC: false,
+        microphoneId: audioDeviceId,
       })
 
       this._localTracks.audioTrack = tracks[0]
@@ -114,6 +115,66 @@ export class RtcManagerAdapter extends AGEventEmitter<RtcEventMap> implements IR
     }
   }
 
+  // 音频设备管理方法
+  async getAudioDevices(): Promise<{
+    microphones: MediaDeviceInfo[]
+    speakers: MediaDeviceInfo[]
+  }> {
+    try {
+      // 请求麦克风权限以获取设备列表
+      await navigator.mediaDevices.getUserMedia({ audio: true })
+
+      const devices = await navigator.mediaDevices.enumerateDevices()
+      const microphones = devices.filter((device) => device.kind === 'audioinput')
+      const speakers = devices.filter((device) => device.kind === 'audiooutput')
+
+      return { microphones, speakers }
+    } catch (error) {
+      this.emit('error', error as Error)
+      throw error
+    }
+  }
+
+  async switchMicrophone(deviceId: string): Promise<void> {
+    if (!this._localTracks.audioTrack) {
+      throw new SttError('NO_AUDIO_TRACK', 'No audio track available to switch')
+    }
+
+    try {
+      // 关闭当前音频轨道
+      this._localTracks.audioTrack.close()
+
+      // 使用新设备创建音频轨道
+      const newAudioTrack = await AgoraRTC.createMicrophoneAudioTrack({
+        microphoneId: deviceId,
+        AGC: false,
+      })
+
+      this._localTracks.audioTrack = newAudioTrack
+
+      // 如果已发布,重新发布新轨道
+      if (this._joined && this._client) {
+        await this._client.unpublish([this._localTracks.audioTrack])
+        await this._client.publish([this._localTracks.audioTrack])
+      }
+
+      this.emit('audioDeviceChanged', { deviceId, deviceType: 'microphone' })
+    } catch (error) {
+      this.emit('error', error as Error)
+      throw error
+    }
+  }
+
+  async switchSpeaker(deviceId: string): Promise<void> {
+    try {
+      await AgoraRTC.setPlaybackDevice(deviceId)
+      this.emit('audioDeviceChanged', { deviceId, deviceType: 'speaker' })
+    } catch (error) {
+      this.emit('error', error as Error)
+      throw error
+    }
+  }
+
   async publish(): Promise<void> {
     if (!this._joined) {
       throw new SttError(

+ 8 - 1
packages/stt-sdk-core/src/types/index.ts

@@ -122,6 +122,7 @@ export interface RtcEventMap {
   remoteUserChanged: (user: any) => void
   networkQuality: (quality: any) => void
   textstreamReceived: (textstream: ITextstream) => void
+  audioDeviceChanged: (data: { deviceId: string; deviceType: 'microphone' | 'speaker' }) => void
   destroying: () => void
   destroyed: () => void
 }
@@ -152,9 +153,15 @@ export interface RtmChannelMetadata {
 // SDK 主类接口
 export interface IRtcManagerAdapter {
   join(config: RtcManagerConfig): Promise<void>
-  createTracks(): Promise<void>
+  createTracks(audioDeviceId?: string): Promise<void>
   publish(): Promise<void>
   destroy(): Promise<void>
+  getAudioDevices(): Promise<{
+    microphones: MediaDeviceInfo[]
+    speakers: MediaDeviceInfo[]
+  }>
+  switchMicrophone(deviceId: string): Promise<void>
+  switchSpeaker(deviceId: string): Promise<void>
   isJoined: boolean
   config?: RtcManagerConfig
   userId: string | number

+ 131 - 0
packages/stt-sdk-core/tests/rtc-manager-adapter.test.ts

@@ -16,9 +16,30 @@ vi.mock('agora-rtc-sdk-ng', () => ({
     createMicrophoneAndCameraTracks: vi.fn(() =>
       Promise.resolve([{ close: vi.fn(), isPlaying: false, play: vi.fn() }, { close: vi.fn() }])
     ),
+    createMicrophoneAudioTrack: vi.fn(() =>
+      Promise.resolve({ close: vi.fn(), isPlaying: false, play: vi.fn() })
+    ),
+    setPlaybackDevice: vi.fn(() => Promise.resolve()),
   },
 }))
 
+// 模拟 MediaDevices API
+Object.defineProperty(global, 'navigator', {
+  value: {
+    mediaDevices: {
+      getUserMedia: vi.fn(() => Promise.resolve({})),
+      enumerateDevices: vi.fn(() =>
+        Promise.resolve([
+          { deviceId: 'mic1', kind: 'audioinput', label: '麦克风 1' },
+          { deviceId: 'mic2', kind: 'audioinput', label: '麦克风 2' },
+          { deviceId: 'speaker1', kind: 'audiooutput', label: '扬声器 1' },
+          { deviceId: 'speaker2', kind: 'audiooutput', label: '扬声器 2' },
+        ])
+      ),
+    },
+  },
+})
+
 // 模拟 token 工具
 vi.mock('../src/utils/token-utils', () => ({
   generateAgoraToken: vi.fn(() => Promise.resolve('mock-token')),
@@ -229,4 +250,114 @@ describe('RtcManagerAdapter', () => {
       }
     })
   })
+
+  describe('audio device management', () => {
+    describe('getAudioDevices', () => {
+      it('should get audio devices successfully', async () => {
+        const devices = await manager.getAudioDevices()
+
+        expect(devices).toEqual({
+          microphones: [
+            { deviceId: 'mic1', kind: 'audioinput', label: '麦克风 1' },
+            { deviceId: 'mic2', kind: 'audioinput', label: '麦克风 2' },
+          ],
+          speakers: [
+            { deviceId: 'speaker1', kind: 'audiooutput', label: '扬声器 1' },
+            { deviceId: 'speaker2', kind: 'audiooutput', label: '扬声器 2' },
+          ],
+        })
+      })
+
+      it('should handle errors when getting devices', async () => {
+        // 模拟 getUserMedia 失败
+        const mockNavigator = global.navigator as any
+        mockNavigator.mediaDevices.getUserMedia = vi.fn(() =>
+          Promise.reject(new Error('Permission denied'))
+        )
+
+        await expect(manager.getAudioDevices()).rejects.toThrow('Permission denied')
+      })
+    })
+
+    describe('switchMicrophone', () => {
+      it('should switch microphone successfully', async () => {
+        const config = { channel: 'test-channel', userId: 'test-user' }
+        await manager.join(config)
+        await manager.createTracks()
+
+        const audioDeviceChangedSpy = vi.fn()
+        manager.on('audioDeviceChanged', audioDeviceChangedSpy)
+
+        await manager.switchMicrophone('mic2')
+
+        expect(audioDeviceChangedSpy).toHaveBeenCalledWith({
+          deviceId: 'mic2',
+          deviceType: 'microphone',
+        })
+      })
+
+      it('should throw error when no audio track available', async () => {
+        await expect(manager.switchMicrophone('mic1')).rejects.toThrow(SttError)
+      })
+
+      it('should throw error when not joined', async () => {
+        const config = { channel: 'test-channel', userId: 'test-user' }
+        await manager.join(config)
+        await manager.createTracks()
+
+        // 模拟 createMicrophoneAudioTrack 失败
+        const mockAgoraRTC = await import('agora-rtc-sdk-ng')
+        mockAgoraRTC.default.createMicrophoneAudioTrack = vi.fn(() =>
+          Promise.reject(new Error('Device not found'))
+        )
+
+        await expect(manager.switchMicrophone('invalid-device')).rejects.toThrow('Device not found')
+      })
+    })
+
+    describe('switchSpeaker', () => {
+      it('should switch speaker successfully', async () => {
+        const audioDeviceChangedSpy = vi.fn()
+        manager.on('audioDeviceChanged', audioDeviceChangedSpy)
+
+        await manager.switchSpeaker('speaker2')
+
+        expect(audioDeviceChangedSpy).toHaveBeenCalledWith({
+          deviceId: 'speaker2',
+          deviceType: 'speaker',
+        })
+      })
+
+      it('should handle errors when switching speaker', async () => {
+        // 模拟 setPlaybackDevice 失败
+        const mockAgoraRTC = await import('agora-rtc-sdk-ng')
+        mockAgoraRTC.default.setPlaybackDevice = vi.fn(() =>
+          Promise.reject(new Error('Device not found'))
+        )
+
+        await expect(manager.switchSpeaker('invalid-device')).rejects.toThrow('Device not found')
+      })
+    })
+
+    describe('createTracks with device selection', () => {
+      it('should create tracks with specific microphone device', async () => {
+        const config = { channel: 'test-channel', userId: 'test-user' }
+        await manager.join(config)
+
+        const localUserChangedSpy = vi.fn()
+        manager.on('localUserChanged', localUserChangedSpy)
+
+        await manager.createTracks('mic2')
+
+        expect(localUserChangedSpy).toHaveBeenCalled()
+
+        // 验证 createMicrophoneAndCameraTracks 被调用时传入了正确的参数
+        const mockAgoraRTC = await import('agora-rtc-sdk-ng')
+        expect(mockAgoraRTC.default.createMicrophoneAndCameraTracks).toHaveBeenCalledWith({
+          AGC: false,
+          microphoneId: 'mic2',
+        })
+      })
+    })
+  })
 })

+ 2 - 1
src/pages/login/index.tsx

@@ -82,7 +82,8 @@ const LoginPage = () => {
   }
 
   const onClickUmdDemo = () => {
-    window.open("/umd-demo.html", "_blank")
+    // window.open("/umd-demo.html", "_blank")
+    nav("/umd-test")
   }
 
   return (

+ 4 - 2
src/pages/umd-test/index.module.scss

@@ -1,7 +1,8 @@
 .umdTestPage {
   padding: 24px;
-  min-height: 100vh;
+  height: 100vh;
   background: #f5f5f5;
+  overflow-y: auto;
 
   .header {
     text-align: center;
@@ -41,7 +42,8 @@
   .testCard,
   .logCard,
   .infoCard,
-  .captionCard {
+  .captionCard,
+  .audioCard {
     :global(.ant-card-head) {
       border-bottom: 1px solid #f0f0f0;
     }

+ 293 - 69
src/pages/umd-test/index.tsx

@@ -1,5 +1,5 @@
 import { useState, useEffect } from "react"
-import { Card, Button, Input, Form, message, Space, Typography, Divider, Alert } from "antd"
+import { Card, Button, Input, Form, message, Space, Typography, Divider, Alert, Select } from "antd"
 import {
   InfoCircleOutlined,
   PlayCircleOutlined,
@@ -12,70 +12,61 @@ import { useNavigate } from "react-router-dom"
 
 import styles from "./index.module.scss"
 
-const { Title, Text } = Typography
+// 导入SDK核心模块
+import { SttSdk } from "../../../packages/stt-sdk-core/src"
+import type {
+  ISttManagerAdapter,
+  IRtmManagerAdapter,
+  IRtcManagerAdapter,
+} from "../../../packages/stt-sdk-core/src/types"
 
-// UMD格式SDK的全局变量声明
-declare global {
-  interface Window {
-    SttSdkCore: any
-  }
-}
+const { Title, Text } = Typography
 
 const UmdTestPage = () => {
   const nav = useNavigate()
   const [messageApi, contextHolder] = message.useMessage()
 
   const [form] = Form.useForm()
-  const [sdk, setSdk] = useState<any>(null)
-  const [sttManager, setSttManager] = useState<any>(null)
-  const [rtmManager, setRtmManager] = useState<any>(null)
+  const [sdk, setSdk] = useState<SttSdk | null>(null)
+  const [sttManager, setSttManager] = useState<ISttManagerAdapter | null>(null)
+  const [rtmManager, setRtmManager] = useState<IRtmManagerAdapter | null>(null)
+  const [rtcManager, setRtcManager] = useState<IRtcManagerAdapter | null>(null)
   const [isSdkInitialized, setIsSdkInitialized] = useState(false)
   const [isSttManagerInitialized, setIsSttManagerInitialized] = useState(false)
+  const [isRtcManagerInitialized, setIsRtcManagerInitialized] = useState(false)
   const [isTranscriptionActive, setIsTranscriptionActive] = useState(false)
   const [transcriptionStatus, setTranscriptionStatus] = useState<string>("idle")
   const [testResults, setTestResults] = useState<string[]>([])
   const [sttData, setSttData] = useState<any>({})
   const [captionVisible, setCaptionVisible] = useState(true)
-  const [umdLoaded, setUmdLoaded] = useState(false)
+  const [sdkLoaded, setSdkLoaded] = useState(false)
+
+  // 音频设备相关状态
+  const [audioDevices, setAudioDevices] = useState<{
+    microphones: MediaDeviceInfo[]
+    speakers: MediaDeviceInfo[]
+  }>({ microphones: [], speakers: [] })
+  const [selectedMicrophone, setSelectedMicrophone] = useState<string>("")
+  const [selectedSpeaker, setSelectedSpeaker] = useState<string>("")
+  const [isAudioDevicesLoaded, setIsAudioDevicesLoaded] = useState(false)
 
   // 添加测试日志
   const addTestLog = (log: string) => {
     setTestResults((prev) => [...prev, `${new Date().toLocaleTimeString()}: ${log}`])
   }
 
-  // 动态加载UMD格式的SDK
-  const loadUmdSdk = async () => {
+  // 加载SDK核心模块
+  const loadSdk = async () => {
     try {
-      addTestLog("开始加载UMD格式SDK...")
-
-      // 检查是否已加载
-      if (window.SttSdkCore) {
-        addTestLog("✅ UMD SDK已加载")
-        setUmdLoaded(true)
-        return
-      }
-
-      // 动态创建script标签加载UMD SDK
-      const script = document.createElement("script")
-      script.src = "/packages/stt-sdk-core/dist/index.umd.js"
-      script.type = "text/javascript"
-      script.async = true
-
-      script.onload = () => {
-        addTestLog("✅ UMD SDK加载成功")
-        setUmdLoaded(true)
-        messageApi.success("UMD SDK加载成功")
-      }
-
-      script.onerror = () => {
-        addTestLog("❌ UMD SDK加载失败")
-        messageApi.error("UMD SDK加载失败,请检查文件路径")
-      }
+      addTestLog("开始加载SDK核心模块...")
 
-      document.head.appendChild(script)
+      // SDK已通过import直接导入,标记为已加载
+      setSdkLoaded(true)
+      addTestLog("✅ SDK核心模块已加载")
+      messageApi.success("SDK核心模块加载成功")
     } catch (error) {
-      addTestLog(`❌ UMD SDK加载异常: ${error}`)
-      messageApi.error(`UMD SDK加载异常: ${error}`)
+      addTestLog(`❌ SDK核心模块加载异常: ${error}`)
+      messageApi.error(`SDK核心模块加载异常: ${error}`)
     }
   }
 
@@ -109,7 +100,27 @@ const UmdTestPage = () => {
     }
   }
 
-  // 初始化SDK(使用UMD格式)
+  // 监听RTC文本流事件
+  const onTextstreamReceived = (textstream: any) => {
+    console.log("[UMD Test] textstreamReceived:", textstream)
+
+    // 记录文本流数据到日志
+    addTestLog(`📡 收到文本流数据: ${JSON.stringify(textstream)}`)
+
+    // 更新实时显示
+    if (textstream.dataType === "transcribe" && textstream.words?.length > 0) {
+      const transcript = textstream.words.map((word: any) => word.text).join(" ")
+      addTestLog(`🎙️ 实时转录: ${transcript}`)
+
+      // 更新实时转录显示
+      setSttData((prev: any) => ({
+        ...prev,
+        transcribe1: transcript,
+      }))
+    }
+  }
+
+  // 初始化SDK(使用直接导入的SDK)
   const initializeSdk = async (values: {
     appId: string
     certificate: string
@@ -117,16 +128,15 @@ const UmdTestPage = () => {
     userName: string
   }) => {
     try {
-      if (!window.SttSdkCore) {
-        addTestLog("❌ UMD SDK未加载,请先加载SDK")
-        messageApi.error("UMD SDK未加载,请先加载SDK")
+      if (!sdkLoaded) {
+        addTestLog("❌ SDK未加载,请先加载SDK")
+        messageApi.error("SDK未加载,请先加载SDK")
         return
       }
 
-      addTestLog("开始初始化UMD SDK...")
+      addTestLog("开始初始化SDK...")
 
-      // 使用UMD格式的SDK
-      const SttSdk = window.SttSdkCore.default || window.SttSdkCore.SttSdk
+      // 使用直接导入的SDK
       const newSdk = new SttSdk()
 
       await newSdk.initialize({
@@ -137,23 +147,28 @@ const UmdTestPage = () => {
 
       setSdk(newSdk)
       setIsSdkInitialized(true)
-      addTestLog("✅ UMD SDK初始化成功")
+      addTestLog("✅ SDK初始化成功")
 
       // 创建管理器
       const rtmManager = newSdk.createRtmManager()
       const sttManager = newSdk.createSttManager(rtmManager)
+      const rtcManager = newSdk.createRtcManager()
 
       // 监听RTM管理器的事件
       rtmManager.on("sttDataChanged", onSttDataChanged)
 
+      // 监听RTC管理器的文本流事件
+      rtcManager.on("textstreamReceived", onTextstreamReceived)
+
       setSttManager(sttManager)
       setRtmManager(rtmManager)
-      addTestLog("✅ STT和RTM管理器创建成功")
+      setRtcManager(rtcManager)
+      addTestLog("✅ STT、RTM和RTC管理器创建成功")
 
-      messageApi.success("UMD SDK初始化成功")
+      messageApi.success("SDK初始化成功")
     } catch (error) {
-      addTestLog(`❌ UMD SDK初始化失败: ${error}`)
-      messageApi.error(`UMD SDK初始化失败: ${error}`)
+      addTestLog(`❌ SDK初始化失败: ${error}`)
+      messageApi.error(`SDK初始化失败: ${error}`)
     }
   }
 
@@ -183,6 +198,34 @@ const UmdTestPage = () => {
     }
   }
 
+  // 初始化RTC管理器
+  const initializeRtcManager = async () => {
+    if (!rtcManager || !form) return
+
+    try {
+      const values = form.getFieldsValue()
+      addTestLog("开始初始化RTC管理器...")
+
+      // 生成随机用户ID
+      const genRandomUserId = () => Math.floor(Math.random() * 1000000)
+
+      await rtcManager.join({
+        channel: values.channel,
+        userId: genRandomUserId(),
+      })
+
+      // 使用选定的麦克风设备创建轨道
+      await rtcManager.createTracks(selectedMicrophone || undefined)
+
+      setIsRtcManagerInitialized(true)
+      addTestLog("✅ RTC管理器初始化成功(已加入RTC频道)")
+      messageApi.success("RTC管理器初始化成功")
+    } catch (error) {
+      addTestLog(`❌ RTC管理器初始化失败: ${error}`)
+      messageApi.error(`RTC管理器初始化失败: ${error}`)
+    }
+  }
+
   // 开始转录
   const startTranscription = async () => {
     if (!sttManager) return
@@ -264,6 +307,13 @@ const UmdTestPage = () => {
         setRtmManager(null)
       }
 
+      if (rtcManager) {
+        rtcManager.off("textstreamReceived", onTextstreamReceived)
+        await rtcManager.destroy()
+        setRtcManager(null)
+        setIsRtcManagerInitialized(false)
+      }
+
       if (sdk) {
         await sdk.destroy()
         setSdk(null)
@@ -290,9 +340,88 @@ const UmdTestPage = () => {
     nav("/home")
   }
 
-  // 组件挂载时尝试加载UMD SDK
+  // 获取音频设备列表
+  const getAudioDevices = async () => {
+    try {
+      addTestLog("开始获取音频设备列表...")
+
+      // 请求麦克风权限
+      await navigator.mediaDevices.getUserMedia({ audio: true })
+
+      const devices = await navigator.mediaDevices.enumerateDevices()
+      const microphones = devices.filter((device) => device.kind === "audioinput")
+      const speakers = devices.filter((device) => device.kind === "audiooutput")
+
+      setAudioDevices({ microphones, speakers })
+
+      // 设置默认设备
+      if (microphones.length > 0) {
+        setSelectedMicrophone(microphones[0].deviceId)
+      }
+      if (speakers.length > 0) {
+        setSelectedSpeaker(speakers[0].deviceId)
+      }
+
+      setIsAudioDevicesLoaded(true)
+      addTestLog(`✅ 音频设备加载完成: ${microphones.length}个麦克风, ${speakers.length}个扬声器`)
+    } catch (error) {
+      addTestLog(`❌ 获取音频设备失败: ${error}`)
+      messageApi.error(`获取音频设备失败: ${error}`)
+    }
+  }
+
+  // 选择麦克风设备
+  const selectMicrophone = async (deviceId: string) => {
+    try {
+      setSelectedMicrophone(deviceId)
+
+      // 如果RTC管理器已初始化,切换麦克风设备
+      if (rtcManager && isRtcManagerInitialized) {
+        await rtcManager.switchMicrophone(deviceId)
+        addTestLog(
+          `🎤 已切换麦克风: ${audioDevices.microphones.find((d) => d.deviceId === deviceId)?.label || deviceId}`,
+        )
+      } else {
+        addTestLog(
+          `🎤 已选择麦克风: ${audioDevices.microphones.find((d) => d.deviceId === deviceId)?.label || deviceId}`,
+        )
+      }
+
+      messageApi.success("麦克风设备已切换")
+    } catch (error) {
+      addTestLog(`❌ 选择麦克风失败: ${error}`)
+      messageApi.error(`选择麦克风失败: ${error}`)
+    }
+  }
+
+  // 选择扬声器设备
+  const selectSpeaker = async (deviceId: string) => {
+    try {
+      setSelectedSpeaker(deviceId)
+
+      // 如果RTC管理器已初始化,切换扬声器设备
+      if (rtcManager && isRtcManagerInitialized) {
+        await rtcManager.switchSpeaker(deviceId)
+        addTestLog(
+          `🔊 已切换扬声器: ${audioDevices.speakers.find((d) => d.deviceId === deviceId)?.label || deviceId}`,
+        )
+      } else {
+        addTestLog(
+          `🔊 已选择扬声器: ${audioDevices.speakers.find((d) => d.deviceId === deviceId)?.label || deviceId}`,
+        )
+      }
+
+      messageApi.success("扬声器设备已切换")
+    } catch (error) {
+      addTestLog(`❌ 选择扬声器失败: ${error}`)
+      messageApi.error(`选择扬声器失败: ${error}`)
+    }
+  }
+
+  // 组件挂载时尝试加载SDK和音频设备
   useEffect(() => {
-    loadUmdSdk()
+    loadSdk()
+    getAudioDevices()
   }, [])
 
   // 组件卸载时清理资源
@@ -309,8 +438,8 @@ const UmdTestPage = () => {
       {contextHolder}
 
       <div className={styles.header}>
-        <Title level={2}>🌐 UMD 格式 SDK 测试页面</Title>
-        <Text type="secondary">测试 UMD 格式 SDK 在 AMD 环境中的使用</Text>
+        <Title level={2}>🌐 SDK 核心模块测试页面</Title>
+        <Text type="secondary">测试直接导入的 SDK 核心模块功能</Text>
       </div>
 
       <div className={styles.content}>
@@ -318,13 +447,13 @@ const UmdTestPage = () => {
           <Card title="📦 SDK 加载状态" className={styles.loadCard}>
             <Space direction="vertical" style={{ width: "100%" }}>
               <Alert
-                message={umdLoaded ? "✅ UMD SDK 已加载" : "⏳ 正在加载 UMD SDK..."}
-                type={umdLoaded ? "success" : "info"}
+                message={sdkLoaded ? "✅ SDK 核心模块已加载" : "⏳ 正在加载 SDK 核心模块..."}
+                type={sdkLoaded ? "success" : "info"}
                 showIcon
               />
 
-              <Button onClick={loadUmdSdk} disabled={umdLoaded}>
-                {umdLoaded ? "✅ 已加载" : "重新加载 SDK"}
+              <Button onClick={loadSdk} disabled={sdkLoaded}>
+                {sdkLoaded ? "✅ 已加载" : "重新加载 SDK"}
               </Button>
             </Space>
           </Card>
@@ -377,7 +506,7 @@ const UmdTestPage = () => {
                   <Button
                     type="primary"
                     onClick={() => initializeSdk(form.getFieldsValue())}
-                    disabled={!umdLoaded || isSdkInitialized}
+                    disabled={!sdkLoaded || isSdkInitialized}
                   >
                     {isSdkInitialized ? "✅ 已初始化" : "初始化 SDK"}
                   </Button>
@@ -387,6 +516,50 @@ const UmdTestPage = () => {
                   </Button>
                 </Space>
               </Form.Item>
+
+              <Divider />
+
+              <Form.Item label="🎤 麦克风设备">
+                <Space direction="vertical" style={{ width: "100%" }}>
+                  <Select
+                    value={selectedMicrophone}
+                    onChange={selectMicrophone}
+                    placeholder="选择麦克风设备"
+                    disabled={!isAudioDevicesLoaded}
+                    style={{ width: "100%" }}
+                  >
+                    {audioDevices.microphones.map((device) => (
+                      <Select.Option key={device.deviceId} value={device.deviceId}>
+                        {device.label || `麦克风 ${device.deviceId.slice(0, 8)}`}
+                      </Select.Option>
+                    ))}
+                  </Select>
+                  <Button
+                    type="link"
+                    size="small"
+                    onClick={getAudioDevices}
+                    icon={<ReloadOutlined />}
+                  >
+                    刷新设备列表
+                  </Button>
+                </Space>
+              </Form.Item>
+
+              <Form.Item label="🔊 扬声器设备">
+                <Select
+                  value={selectedSpeaker}
+                  onChange={selectSpeaker}
+                  placeholder="选择扬声器设备"
+                  disabled={!isAudioDevicesLoaded}
+                  style={{ width: "100%" }}
+                >
+                  {audioDevices.speakers.map((device) => (
+                    <Select.Option key={device.deviceId} value={device.deviceId}>
+                      {device.label || `扬声器 ${device.deviceId.slice(0, 8)}`}
+                    </Select.Option>
+                  ))}
+                </Select>
+              </Form.Item>
             </Form>
           </Card>
 
@@ -400,6 +573,14 @@ const UmdTestPage = () => {
                 {isSttManagerInitialized ? "✅ STT管理器已初始化" : "初始化STT管理器"}
               </Button>
 
+              <Button
+                onClick={initializeRtcManager}
+                disabled={!isSdkInitialized || isRtcManagerInitialized}
+                block
+              >
+                {isRtcManagerInitialized ? "✅ RTC管理器已初始化" : "初始化RTC管理器"}
+              </Button>
+
               <Divider />
 
               <Button
@@ -503,20 +684,20 @@ const UmdTestPage = () => {
           <Card title="💡 使用说明" className={styles.infoCard}>
             <Space direction="vertical" style={{ width: "100%" }}>
               <Text type="secondary">
-                <InfoCircleOutlined /> UMD格式测试说明:
+                <InfoCircleOutlined /> SDK核心模块测试说明:
               </Text>
               <ol>
-                <li>UMD SDK通过动态script标签加载</li>
-                <li>SDK作为全局变量 window.SttSdkCore 可用</li>
-                <li>支持AMD/CommonJS/全局变量使用方式</li>
-                <li>功能与ES模块版本完全一致</li>
+                <li>SDK通过直接import导入核心模块</li>
+                <li>支持TypeScript类型检查和智能提示</li>
+                <li>便于调试和开发时使用</li>
+                <li>功能与UMD版本完全一致</li>
               </ol>
 
               <Text type="secondary">
                 <InfoCircleOutlined /> 测试步骤:
               </Text>
               <ol>
-                <li>等待UMD SDK加载完成</li>
+                <li>等待SDK核心模块加载完成</li>
                 <li>填写 App ID 和 Certificate</li>
                 <li>点击"初始化 SDK"</li>
                 <li>初始化 STT 管理器</li>
@@ -530,6 +711,49 @@ const UmdTestPage = () => {
             </Space>
           </Card>
 
+          <Card title="🎧 音频设备状态" className={styles.audioCard}>
+            <Space direction="vertical" style={{ width: "100%" }}>
+              <Alert
+                message={
+                  isAudioDevicesLoaded
+                    ? `✅ 音频设备已加载 (${audioDevices.microphones.length}个麦克风, ${audioDevices.speakers.length}个扬声器)`
+                    : "⏳ 正在加载音频设备..."
+                }
+                type={isAudioDevicesLoaded ? "success" : "info"}
+                showIcon
+              />
+
+              <div style={{ fontSize: "12px", color: "#666" }}>
+                <div>
+                  <strong>当前麦克风:</strong>{" "}
+                  {selectedMicrophone
+                    ? audioDevices.microphones.find((d) => d.deviceId === selectedMicrophone)
+                        ?.label || "默认设备"
+                    : "未选择"}
+                </div>
+                <div style={{ marginTop: 4 }}>
+                  <strong>当前扬声器:</strong>{" "}
+                  {selectedSpeaker
+                    ? audioDevices.speakers.find((d) => d.deviceId === selectedSpeaker)?.label ||
+                      "默认设备"
+                    : "未选择"}
+                </div>
+                <div style={{ marginTop: 8, paddingTop: 8, borderTop: "1px solid #f0f0f0" }}>
+                  <strong>RTC状态:</strong> {isRtcManagerInitialized ? "✅ 已连接" : "❌ 未连接"}
+                </div>
+              </div>
+
+              <Button
+                type="primary"
+                size="small"
+                onClick={getAudioDevices}
+                icon={<ReloadOutlined />}
+              >
+                刷新设备列表
+              </Button>
+            </Space>
+          </Card>
+
           <Card
             title={
               <Space>