@@ -30,8 +30,13 @@ class Stream {
3030class Mixer {
3131 static instances : Mixer [ ] = [ ] ;
3232 readonly inputs : Track [ ] = [ ] ;
33+ readonly connections : unknown [ ] = [ ] ;
3334 readonly output = new Track ( "audio" ) ;
3435 readonly destination = { stream : new Stream ( [ this . output ] ) } ;
36+ readonly mix = {
37+ gain : { value : 1 } ,
38+ connect : mock ( ( ) => { } ) ,
39+ } ;
3540 resume = mock ( async ( ) => { } ) ;
3641 close = mock ( async ( ) => { } ) ;
3742
@@ -43,16 +48,28 @@ class Mixer {
4348 return this . destination ;
4449 }
4550
51+ createGain ( ) {
52+ return this . mix ;
53+ }
54+
4655 createMediaStreamSource ( stream : Stream ) {
4756 return {
48- connect : ( ) => {
57+ connect : ( destination : unknown ) => {
4958 this . inputs . push ( ...stream . getAudioTracks ( ) ) ;
59+ this . connections . push ( destination ) ;
5060 } ,
5161 } ;
5262 }
5363}
5464
55- const globals = [ "MediaStream" , "AudioContext" , "MediaRecorder" , "navigator" ] ;
65+ const globals = [
66+ "MediaStream" ,
67+ "AudioContext" ,
68+ "MediaRecorder" ,
69+ "navigator" ,
70+ "setTimeout" ,
71+ "clearTimeout" ,
72+ ] ;
5673const descriptors = globals . map (
5774 ( name ) => [ name , Object . getOwnPropertyDescriptor ( globalThis , name ) ] as const ,
5875) ;
@@ -129,6 +146,9 @@ test("records both audio sources through one track and releases the mixer", asyn
129146 expect ( input ?. getVideoTracks ( ) ) . toEqual ( [ video ] ) ;
130147 expect ( input ?. getAudioTracks ( ) ) . toEqual ( [ mixer ?. output ] ) ;
131148 expect ( mixer ?. inputs ) . toEqual ( [ system , mic ] ) ;
149+ expect ( mixer ?. connections ) . toEqual ( [ mixer ?. mix , mixer ?. mix ] ) ;
150+ expect ( mixer ?. mix . gain . value ) . toBe ( 0.5 ) ;
151+ expect ( mixer ?. mix . connect ) . toHaveBeenCalledWith ( mixer ?. destination ) ;
132152 } finally {
133153 recorder . destroy ( ) ;
134154 }
@@ -157,6 +177,48 @@ test.each([
157177 } ,
158178) ;
159179
180+ test ( "times out pending audio startup and releases captured tracks" , async ( ) => {
181+ const { video, system, mic } = capture ( true , true ) ;
182+ setGlobal (
183+ "AudioContext" ,
184+ class extends Mixer {
185+ resume = mock ( ( ) => new Promise < void > ( ( ) => { } ) ) ;
186+ } ,
187+ ) ;
188+ let deadline : ( ( ) => void ) | undefined ;
189+ const timeout = mock ( ( callback : ( ) => void , delay : number ) => {
190+ expect ( delay ) . toBe ( 10_000 ) ;
191+ deadline = callback ;
192+ return 7 ;
193+ } ) ;
194+ const clear = mock ( ( ) => { } ) ;
195+ setGlobal ( "setTimeout" , timeout ) ;
196+ setGlobal ( "clearTimeout" , clear ) ;
197+ const recorder = new CapRecorder ( {
198+ publicKey : "test-public-key" ,
199+ mode : "tab" ,
200+ camera : { enabled : false } ,
201+ systemAudio : true ,
202+ } ) ;
203+ try {
204+ const starting = recorder . start ( ) ;
205+ await new Promise < void > ( ( resolve ) => setImmediate ( resolve ) ) ;
206+ expect ( deadline ) . toBeDefined ( ) ;
207+ deadline ?.( ) ;
208+ await expect ( starting ) . rejects . toThrow ( "Recording audio startup timed out" ) ;
209+ expect ( recorder . phase ) . toBe ( "error" ) ;
210+ expect ( clear ) . toHaveBeenCalledWith ( 7 ) ;
211+ expect ( video . stop ) . toHaveBeenCalled ( ) ;
212+ expect ( system . stop ) . toHaveBeenCalled ( ) ;
213+ expect ( mic . stop ) . toHaveBeenCalled ( ) ;
214+ const mixer = Mixer . instances [ 0 ] ;
215+ expect ( mixer ?. output . stop ) . toHaveBeenCalled ( ) ;
216+ expect ( mixer ?. close ) . toHaveBeenCalledTimes ( 1 ) ;
217+ } finally {
218+ recorder . destroy ( ) ;
219+ }
220+ } ) ;
221+
160222test . each ( [ "encoder" , "audio" ] as const ) (
161223 "cleans up both audio sources when %s initialization fails" ,
162224 async ( failure ) => {
0 commit comments