11/**
2- * 课堂音频自动恢复 hook(静音诊断 / 窗口源回退 / 设备变更重启)
2+ * 课堂音频自动恢复 hook(静音诊断 / 设备变更重启)
33 *
4- * @ai -context: 从 useClassroomAudio 拆出的独立恢复层。三条恢复路径:
5- * ①窗口源静音回退——Windows 上窗口源(window:xxx)getUserMedia 可能"成功但
6- * 给出纯静音轨"(不抛错,异常降级不会触发),SilenceTracker 检出连续静音后
7- * 自动 stop→start 以环回源重启(每会话仅回退一次);
8- * ②输出设备不匹配提示——已在环回源仍持续静音,说明视频声音输出到了
9- * 非默认设备,提示用户核对默认输出设备;
10- * ③设备变更重启——默认输出设备切换后环回仍绑定旧设备,devicechange 时
11- * 自动重启重新绑定。
12- * @ai -context: 仅依赖 refs 的稳定回调,重启经 restartingRef 互斥防止并发。
4+ * @ai -context: 从 useClassroomAudio 拆出的独立恢复层。两条恢复路径:
5+ * ①输出设备不匹配诊断——系统环回只录默认输出设备,视频声音若输出到其他
6+ * 设备(HDMI/蓝牙)会表现为"音频块正常但持续静音",检出后提示用户核对;
7+ * ②设备变更重启——默认输出设备切换后环回仍绑定旧设备,devicechange 时
8+ * 自动 stop→start 重新绑定。
9+ * @ai -context: 仅依赖 refs 的稳定回调,重启经 restartingRef 互斥防并发。
1310 */
1411import { useEffect , useRef , useCallback } from 'react' ;
1512import type { CaptureMode , SessionStatus } from '@/lib/capture' ;
@@ -26,19 +23,16 @@ const RESTART_CLEANUP_DELAY_MS = 500;
2623interface UseAudioRecoveryOptions {
2724 status : SessionStatus ;
2825 mode : CaptureMode ;
29- /** 方案A: 会话选中窗口的源 ID(回退环回后置空) */
26+ /** 会话选中窗口的源 ID,重启时沿用同一源 */
3027 audioSourceId ?: string | null ;
3128 onNotify : ( type : 'warning' | 'error' , message : string ) => void ;
3229}
3330
3431export function useAudioRecovery ( { status, mode, audioSourceId, onNotify } : UseAudioRecoveryOptions ) {
3532 const notifyRef = useRef ( onNotify ) ;
3633 notifyRef . current = onNotify ;
37- /** 当前生效的音频源(窗口源回退环回后变为 undefined) */
3834 const effectiveSourceRef = useRef < string | undefined > ( audioSourceId ?? undefined ) ;
3935 const silenceTrackerRef = useRef ( new SilenceTracker ( ) ) ;
40- /** 本会话是否已执行过窗口源→环回回退 */
41- const fellBackRef = useRef ( false ) ;
4236 const restartingRef = useRef ( false ) ;
4337
4438 const audioEnabled = status === 'capturing' && ( mode === 'audio' || mode === 'mixed' ) ;
@@ -47,7 +41,6 @@ export function useAudioRecovery({ status, mode, audioSourceId, onNotify }: UseA
4741 useEffect ( ( ) => {
4842 if ( status !== 'capturing' ) return ;
4943 effectiveSourceRef . current = audioSourceId ?? undefined ;
50- fellBackRef . current = false ;
5144 silenceTrackerRef . current . reset ( ) ;
5245 // eslint-disable-next-line react-hooks/exhaustive-deps -- 仅会话开始时快照 audioSourceId
5346 } , [ status ] ) ;
@@ -73,31 +66,19 @@ export function useAudioRecovery({ status, mode, audioSourceId, onNotify }: UseA
7366 }
7467 } , [ ] ) ;
7568
76- // 静音诊断:窗口源先自动回退环回;已是环回仍静音则提示核对输出设备
69+ // 静音诊断:音频块正常但持续无声 → 提示核对系统默认输出设备
7770 useEffect ( ( ) => {
7871 if ( ! audioEnabled || ! window . electronAPI ) return ;
7972 const off = window . electronAPI . on ( 'audio_capture_chunk' , ( ...args : unknown [ ] ) => {
8073 const chunk = args [ 0 ] as { audioBuffer : ArrayBuffer } ;
8174 if ( ! silenceTrackerRef . current . push ( computeChunkRms ( chunk . audioBuffer ) ) ) return ;
82-
83- const src = effectiveSourceRef . current ;
84- if ( src ?. startsWith ( 'window:' ) && ! fellBackRef . current ) {
85- // 方案A 漏洞补偿:窗口源"成功但静音"不抛错,只能靠静音检测触发回退
86- fellBackRef . current = true ;
87- effectiveSourceRef . current = undefined ;
88- console . warn ( '[useAudioRecovery] 窗口音频源持续静音,自动回退系统环回' ) ;
89- void restartCapture ( undefined ) . then ( ( ok ) => {
90- if ( ok ) notifyRef . current ( 'warning' , '窗口音频源持续静音,已自动切换为系统环回捕获' ) ;
91- } ) ;
92- return ;
93- }
9475 void getDefaultOutputDeviceLabel ( ) . then ( ( label ) => {
9576 notifyRef . current ( 'warning' ,
96- `持续收到静音音频:请确认视频声音正在播放,且输出到系统默认设备${ label ? `「${ label } 」` : '' } (音频捕获只能录到默认输出设备的声音 )` ) ;
77+ `持续收到静音音频:请确认视频声音正在播放,且输出到系统默认设备${ label ? `「${ label } 」` : '' } (系统音频捕获只能录到默认输出设备的声音 )` ) ;
9778 } ) ;
9879 } ) ;
9980 return off ;
100- } , [ audioEnabled , restartCapture ] ) ;
81+ } , [ audioEnabled ] ) ;
10182
10283 // 设备变更自动重启:重新绑定新的默认输出设备
10384 useEffect ( ( ) => {
0 commit comments