@@ -33,6 +33,8 @@ type ToolBuffer = {
3333 args : string ;
3434} ;
3535
36+ const cancelledSetup = Symbol ( "cancelledSetup" ) ;
37+
3638export class WebRtcSdpRealtimeTalkTransport implements RealtimeTalkTransport {
3739 private peer : RTCPeerConnection | null = null ;
3840 private channel : RTCDataChannel | null = null ;
@@ -58,27 +60,44 @@ export class WebRtcSdpRealtimeTalkTransport implements RealtimeTalkTransport {
5860 throw new Error ( "Realtime Talk requires browser WebRTC and microphone access" ) ;
5961 }
6062 this . closed = false ;
61- this . peer = new RTCPeerConnection ( ) ;
63+ const peer = new RTCPeerConnection ( ) ;
64+ this . peer = peer ;
6265 this . audio = document . createElement ( "audio" ) ;
6366 this . audio . autoplay = true ;
6467 this . audio . style . display = "none" ;
6568 document . body . append ( this . audio ) ;
66- this . peer . addEventListener ( "track" , ( event ) => {
69+ peer . addEventListener ( "track" , ( event ) => {
6770 if ( this . audio ) {
6871 this . audio . srcObject = event . streams [ 0 ] ;
6972 }
7073 } ) ;
71- this . media = await navigator . mediaDevices . getUserMedia ( { audio : true } ) ;
72- for ( const track of this . media . getAudioTracks ( ) ) {
73- this . peer . addTrack ( track , this . media ) ;
74+ const media = await this . awaitSetupStep (
75+ peer ,
76+ navigator . mediaDevices . getUserMedia ( { audio : true } ) ,
77+ ) ;
78+ if ( media === cancelledSetup ) {
79+ return ;
80+ }
81+ if ( ! this . isCurrentPeer ( peer ) ) {
82+ media . getTracks ( ) . forEach ( ( track ) => track . stop ( ) ) ;
83+ return ;
84+ }
85+ this . media = media ;
86+ for ( const track of media . getAudioTracks ( ) ) {
87+ peer . addTrack ( track , media ) ;
7488 }
75- this . channel = this . peer . createDataChannel ( "oai-events" ) ;
76- this . channel . addEventListener ( "open" , ( ) => {
89+ const channel = peer . createDataChannel ( "oai-events" ) ;
90+ if ( ! this . isCurrentPeer ( peer ) ) {
91+ channel . close ( ) ;
92+ return ;
93+ }
94+ this . channel = channel ;
95+ channel . addEventListener ( "open" , ( ) => {
7796 this . ctx . callbacks . onStatus ?.( "listening" ) ;
7897 this . emitTalkEvent ( { type : "session.ready" } ) ;
7998 } ) ;
80- this . channel . addEventListener ( "message" , ( event ) => this . handleRealtimeEvent ( event . data ) ) ;
81- this . peer . addEventListener ( "connectionstatechange" , ( ) => {
99+ channel . addEventListener ( "message" , ( event ) => this . handleRealtimeEvent ( event . data ) ) ;
100+ peer . addEventListener ( "connectionstatechange" , ( ) => {
82101 if ( this . closed ) {
83102 return ;
84103 }
@@ -87,24 +106,73 @@ export class WebRtcSdpRealtimeTalkTransport implements RealtimeTalkTransport {
87106 }
88107 } ) ;
89108
90- const offer = await this . peer . createOffer ( ) ;
91- await this . peer . setLocalDescription ( offer ) ;
92- const sdp = await fetch ( this . session . offerUrl ?? "https://api.openai.com/v1/realtime/calls" , {
93- method : "POST" ,
94- body : offer . sdp ,
95- headers : {
96- ...this . session . offerHeaders ,
97- Authorization : `Bearer ${ this . session . clientSecret } ` ,
98- "Content-Type" : "application/sdp" ,
99- } ,
100- } ) ;
109+ const offer = await this . awaitSetupStep ( peer , peer . createOffer ( ) ) ;
110+ if ( offer === cancelledSetup ) {
111+ return ;
112+ }
113+ if ( ! this . isCurrentPeer ( peer ) ) {
114+ return ;
115+ }
116+ const localDescriptionResult = await this . awaitSetupStep ( peer , peer . setLocalDescription ( offer ) ) ;
117+ if ( localDescriptionResult === cancelledSetup ) {
118+ return ;
119+ }
120+ if ( ! this . isCurrentPeer ( peer ) ) {
121+ return ;
122+ }
123+ const sdp = await this . awaitSetupStep (
124+ peer ,
125+ fetch ( this . session . offerUrl ?? "https://api.openai.com/v1/realtime/calls" , {
126+ method : "POST" ,
127+ body : offer . sdp ,
128+ headers : {
129+ ...this . session . offerHeaders ,
130+ Authorization : `Bearer ${ this . session . clientSecret } ` ,
131+ "Content-Type" : "application/sdp" ,
132+ } ,
133+ } ) ,
134+ ) ;
135+ if ( sdp === cancelledSetup ) {
136+ return ;
137+ }
138+ if ( ! this . isCurrentPeer ( peer ) ) {
139+ return ;
140+ }
101141 if ( ! sdp . ok ) {
102142 throw new Error ( `Realtime WebRTC setup failed (${ sdp . status } )` ) ;
103143 }
104- await this . peer . setRemoteDescription ( {
105- type : "answer" ,
106- sdp : await sdp . text ( ) ,
107- } ) ;
144+ const answerSdp = await this . awaitSetupStep ( peer , sdp . text ( ) ) ;
145+ if ( answerSdp === cancelledSetup ) {
146+ return ;
147+ }
148+ if ( ! this . isCurrentPeer ( peer ) ) {
149+ return ;
150+ }
151+ await this . awaitSetupStep (
152+ peer ,
153+ peer . setRemoteDescription ( {
154+ type : "answer" ,
155+ sdp : answerSdp ,
156+ } ) ,
157+ ) ;
158+ }
159+
160+ private isCurrentPeer ( peer : RTCPeerConnection ) : boolean {
161+ return ! this . closed && this . peer === peer ;
162+ }
163+
164+ private async awaitSetupStep < T > (
165+ peer : RTCPeerConnection ,
166+ promise : Promise < T > ,
167+ ) : Promise < T | typeof cancelledSetup > {
168+ try {
169+ return await promise ;
170+ } catch ( error ) {
171+ if ( ! this . isCurrentPeer ( peer ) ) {
172+ return cancelledSetup ;
173+ }
174+ throw error ;
175+ }
108176 }
109177
110178 stop ( ) : void {
0 commit comments