@@ -79,6 +79,8 @@ export class AudioCapture {
7979 private readonly onChunk : ( chunk : AudioChunk ) => void ;
8080 private capturing = false ;
8181 private disposed = false ;
82+ /** 启动中互斥锁:守卫与 capturing 置位间存在 await 空隙,同步上锁防并发双启动 */
83+ private starting = false ;
8284
8385 /** 当前活跃的 Provider */
8486 private provider : AudioSourceProvider | null = null ;
@@ -127,40 +129,47 @@ export class AudioCapture {
127129 sourceId ?: string ,
128130 extras ?: AudioCaptureStartExtras ,
129131 ) : Promise < void > {
130- if ( this . capturing || this . disposed ) return ;
132+ if ( this . capturing || this . starting || this . disposed ) return ;
131133
132- const resolvedSourceId = sourceId ?? null ;
133- // 应急总闸关闭时能力恒为不可用,选源退回端点环回
134- const processAvailable = isProcessLoopbackEnabled ( ) && isProcessLoopbackAvailable ( ) ;
135- this . decision = selectAudioSource ( {
136- capabilities : { processLoopbackAvailable : processAvailable } ,
137- sourceId : resolvedSourceId ,
138- preference : extras ?. preference ,
139- microphone : extras ?. microphone ,
140- } ) ;
134+ // 进入即同步上锁,finally 释放:堵住守卫检查与状态翻转之间的 await 空隙,
135+ // 防止并发双启动导致 provider 被重复创建/降级链交叉触发
136+ this . starting = true ;
137+ try {
138+ const resolvedSourceId = sourceId ?? null ;
139+ // 应急总闸关闭时能力恒为不可用,选源退回端点环回
140+ const processAvailable = isProcessLoopbackEnabled ( ) && isProcessLoopbackAvailable ( ) ;
141+ this . decision = selectAudioSource ( {
142+ capabilities : { processLoopbackAvailable : processAvailable } ,
143+ sourceId : resolvedSourceId ,
144+ preference : extras ?. preference ,
145+ microphone : extras ?. microphone ,
146+ } ) ;
141147
142- logger . info (
143- `[AudioCapture] 选源: ${ this . decision . kind } (${ this . decision . reason } )` +
144- `${ this . decision . fallback ? `, 降级目标=${ this . decision . fallback } ` : '' } ` ,
145- ) ;
148+ logger . info (
149+ `[AudioCapture] 选源: ${ this . decision . kind } (${ this . decision . reason } )` +
150+ `${ this . decision . fallback ? `, 降级目标=${ this . decision . fallback } ` : '' } ` ,
151+ ) ;
146152
147- try {
148- await this . startWithKind ( this . decision . kind , win , resolvedSourceId ) ;
149- } catch ( err ) {
150- const fallback = this . decision . fallback ;
151- if ( ! fallback ) throw err ;
153+ try {
154+ await this . startWithKind ( this . decision . kind , win , resolvedSourceId ) ;
155+ } catch ( err ) {
156+ const fallback = this . decision . fallback ;
157+ if ( ! fallback ) throw err ;
152158
153- const message = err instanceof Error ? err . message : String ( err ) ;
154- logger . warn ( `[AudioCapture] ${ this . decision . kind } 启动失败(${ message } ),降级到 ${ fallback } ` ) ;
155- this . decision = {
156- kind : fallback ,
157- reason : `${ this . decision . kind } 启动失败后降级:${ message } ` ,
158- fallback : null ,
159- } ;
160- await this . startWithKind ( fallback , win , resolvedSourceId ) ;
161- }
159+ const message = err instanceof Error ? err . message : String ( err ) ;
160+ logger . warn ( `[AudioCapture] ${ this . decision . kind } 启动失败(${ message } ),降级到 ${ fallback } ` ) ;
161+ this . decision = {
162+ kind : fallback ,
163+ reason : `${ this . decision . kind } 启动失败后降级:${ message } ` ,
164+ fallback : null ,
165+ } ;
166+ await this . startWithKind ( fallback , win , resolvedSourceId ) ;
167+ }
162168
163- this . capturing = true ;
169+ this . capturing = true ;
170+ } finally {
171+ this . starting = false ;
172+ }
164173 }
165174
166175 /** 按源类型创建并启动 Provider */
0 commit comments