22 * 课堂音频自动恢复 hook(静音诊断 / 设备变更重启)
33 *
44 * @ai -context: 从 useClassroomAudio 拆出的独立恢复层。两条恢复路径:
5- * ①输出设备不匹配诊断——系统环回只录默认输出设备,视频声音若输出到其他
6- * 设备(HDMI/蓝牙)会表现为"音频块正常但持续静音",检出后提示用户核对;
7- * ②设备变更重启——默认输出设备切换后环回仍绑定旧设备,devicechange 时
8- * 自动 stop→start 重新绑定。
5+ * ①静音诊断——音频块正常但持续静音,成因随音频源不同(见下);
6+ * ②设备变更重启——仅端点环回需要(它绑定系统默认输出设备),进程环回
7+ * 与输出设备无关,切设备时不必重启。
8+ * @ai -context: 诊断文案必须按 sourceKind 分支(ADR-001)——对进程环回说
9+ * "请检查默认输出设备"是误导,它根本不受输出设备与系统音量影响,此时
10+ * 真实成因是目标窗口没在发声或声音来自别的应用。
911 * @ai -context: 仅依赖 refs 的稳定回调,重启经 restartingRef 互斥防并发。
1012 */
1113import { useEffect , useRef , useCallback } from 'react' ;
1214import type { CaptureMode , SessionStatus } from '@/lib/capture' ;
15+ import type { AudioSourceKind } from '@/lib/capture/audioSourceStrategy' ;
16+ import { getAudioSourcePreference } from '@/lib/capture/audioSourcePreference' ;
1317import {
1418 computeChunkRms , SilenceTracker ,
1519 getDefaultOutputDeviceLabel , subscribeDeviceChange ,
@@ -25,17 +29,25 @@ interface UseAudioRecoveryOptions {
2529 mode : CaptureMode ;
2630 /** 会话选中窗口的源 ID,重启时沿用同一源 */
2731 audioSourceId ?: string | null ;
32+ /** 本次会话实际生效的音频源(由 audio_capture_start 回传) */
33+ sourceKind ?: AudioSourceKind | null ;
2834 onNotify : ( type : 'warning' | 'error' , message : string ) => void ;
2935}
3036
31- export function useAudioRecovery ( { status, mode, audioSourceId, onNotify } : UseAudioRecoveryOptions ) {
37+ export function useAudioRecovery ( {
38+ status, mode, audioSourceId, sourceKind, onNotify,
39+ } : UseAudioRecoveryOptions ) {
3240 const notifyRef = useRef ( onNotify ) ;
3341 notifyRef . current = onNotify ;
3442 const effectiveSourceRef = useRef < string | undefined > ( audioSourceId ?? undefined ) ;
3543 const silenceTrackerRef = useRef ( new SilenceTracker ( ) ) ;
3644 const restartingRef = useRef ( false ) ;
45+ // 生效源用 ref 桥接:静音诊断的监听器依赖数组不含它,避免重订阅丢失计数
46+ const sourceKindRef = useRef < AudioSourceKind | null > ( sourceKind ?? null ) ;
47+ sourceKindRef . current = sourceKind ?? null ;
3748
3849 const audioEnabled = status === 'capturing' && ( mode === 'audio' || mode === 'mixed' ) ;
50+ const isProcessSource = sourceKind === 'process_loopback' ;
3951
4052 // 会话开始时重置恢复状态(audioSourceId 取会话启动瞬间的快照)
4153 useEffect ( ( ) => {
@@ -53,7 +65,7 @@ export function useAudioRecovery({ status, mode, audioSourceId, onNotify }: UseA
5365 await window . electronAPI . invoke ( 'audio_capture_stop' ) ;
5466 await new Promise ( ( r ) => setTimeout ( r , RESTART_CLEANUP_DELAY_MS ) ) ;
5567 const result = await window . electronAPI . invoke ( 'audio_capture_start' , {
56- ...AUDIO_START_OPTIONS , sourceId,
68+ ...AUDIO_START_OPTIONS , sourceId, preference : getAudioSourcePreference ( ) ,
5769 } ) as { success : boolean ; error ?: string } ;
5870 if ( result . success ) silenceTrackerRef . current . reset ( ) ;
5971 else console . warn ( '[useAudioRecovery] 音频捕获重启失败:' , result . error ) ;
@@ -66,12 +78,20 @@ export function useAudioRecovery({ status, mode, audioSourceId, onNotify }: UseA
6678 }
6779 } , [ ] ) ;
6880
69- // 静音诊断:音频块正常但持续无声 → 提示核对系统默认输出设备
81+ // 静音诊断:音频块正常但持续无声 → 按生效源给出对应成因
7082 useEffect ( ( ) => {
7183 if ( ! audioEnabled || ! window . electronAPI ) return ;
7284 const off = window . electronAPI . on ( 'audio_capture_chunk' , ( ...args : unknown [ ] ) => {
7385 const chunk = args [ 0 ] as { audioBuffer : ArrayBuffer } ;
7486 if ( ! silenceTrackerRef . current . push ( computeChunkRms ( chunk . audioBuffer ) ) ) return ;
87+
88+ if ( sourceKindRef . current === 'process_loopback' ) {
89+ // 进程环回不受系统音量/输出设备影响,成因只能是目标窗口没在发声
90+ notifyRef . current ( 'warning' ,
91+ '持续收到静音音频:当前只采集目标窗口的声音,请确认该窗口正在播放,' +
92+ '或在设置中改为采集「系统全部声音」' ) ;
93+ return ;
94+ }
7595 void getDefaultOutputDeviceLabel ( ) . then ( ( label ) => {
7696 notifyRef . current ( 'warning' ,
7797 `持续收到静音音频:请确认视频声音正在播放,且输出到系统默认设备${ label ? `「${ label } 」` : '' } (系统音频捕获只能录到默认输出设备的声音)` ) ;
@@ -80,9 +100,9 @@ export function useAudioRecovery({ status, mode, audioSourceId, onNotify }: UseA
80100 return off ;
81101 } , [ audioEnabled ] ) ;
82102
83- // 设备变更自动重启:重新绑定新的默认输出设备
103+ // 设备变更自动重启:仅端点环回需要(它绑定系统默认输出设备)
84104 useEffect ( ( ) => {
85- if ( ! audioEnabled || ! window . electronAPI ) return ;
105+ if ( ! audioEnabled || ! window . electronAPI || isProcessSource ) return ;
86106 const unsubscribe = subscribeDeviceChange ( ( ) => {
87107 console . info ( '[useAudioRecovery] 检测到音频设备变更,自动重启音频捕获' ) ;
88108 void restartCapture ( effectiveSourceRef . current ) . then ( ( ok ) => {
@@ -91,5 +111,5 @@ export function useAudioRecovery({ status, mode, audioSourceId, onNotify }: UseA
91111 } ) ;
92112 } ) ;
93113 return unsubscribe ;
94- } , [ audioEnabled , restartCapture ] ) ;
114+ } , [ audioEnabled , isProcessSource , restartCapture ] ) ;
95115}
0 commit comments