@@ -234,41 +234,80 @@ class StreamingTTS {
234234 }
235235
236236 stop ( ) {
237+ // 1. Completely and safely dismantle the WebSocket connection.
237238 if ( this . #currentSocket) {
238- this . #currentSocket. close ( ) ;
239- this . #currentSocket = null ;
240- }
241- if ( this . #mediaSource && this . #mediaSource. readyState === "open" ) {
239+ // Nullify ALL event handlers to prevent any lingering callbacks from firing.
240+ this . #currentSocket. onopen = null ;
241+ this . #currentSocket. onmessage = null ;
242+ this . #currentSocket. onerror = null ;
243+ this . #currentSocket. onclose = null ;
244+
245+ // Defensively close the socket only if it's in an active state.
242246 try {
243- this . #mediaSource. endOfStream ( ) ;
247+ const state = this . #currentSocket. readyState ;
248+ if ( state === WebSocket . OPEN || state === WebSocket . CONNECTING ) {
249+ // Use the spec-compliant close method with a normal closure code.
250+ this . #currentSocket. close ( 1000 , "client stop" ) ;
251+ }
244252 } catch ( e ) {
245- /* Ignore */
253+ // This can happen in rare cases; it's safe to ignore.
254+ console . debug ( "Error while closing WebSocket, ignoring:" , e ) ;
246255 }
256+
257+ this . #currentSocket = null ;
247258 }
248- this . #audioPlayer . pause ( ) ;
249- this . #audioPlayer . removeAttribute ( "src" ) ;
259+
260+ // 2. Clear internal state.
250261 this . #audioQueue = [ ] ;
251262 this . #isAppending = false ;
263+
264+ // 3. Gracefully end the MediaSource stream.
265+ this . #finalizeStream( ) ;
266+
267+ // 4. Fully and safely reset the <audio> element.
268+ this . #audioPlayer. pause ( ) ;
269+
270+ // Revoke any object URL to prevent memory leaks.
271+ if ( this . #audioPlayer. src && this . #audioPlayer. src . startsWith ( "blob:" ) ) {
272+ URL . revokeObjectURL ( this . #audioPlayer. src ) ;
273+ }
274+
275+ // Remove the source and call load() to force the element to reset.
276+ this . #audioPlayer. removeAttribute ( "src" ) ;
277+ try {
278+ this . #audioPlayer. load ( ) ;
279+ } catch ( e ) {
280+ // This can fail in some browsers/states; it's safe to ignore.
281+ console . debug ( "Error while resetting audio element, ignoring:" , e ) ;
282+ }
252283 }
253284
254285 // --- Private Helper Methods ---
255286 #finalizeStream( ) {
287+ // This is the more robust version that prevents Firefox warnings and is generally safer.
288+ if ( ! this . #mediaSource || this . #mediaSource. readyState !== "open" ) {
289+ return ;
290+ }
291+
256292 const end = ( ) => {
257- if ( this . #mediaSource && this . #mediaSource . readyState === "open" ) {
293+ if ( this . #mediaSource. readyState === "open" ) {
258294 try {
259295 this . #mediaSource. endOfStream ( ) ;
260296 } catch ( e ) {
261- console . warn ( "MediaSource already ended." ) ;
297+ console . warn (
298+ "Error calling endOfStream, stream likely already closed." ,
299+ e ,
300+ ) ;
262301 }
263302 }
264303 } ;
265- if ( this . #isAppending || this . #audioQueue . length > 0 ) {
266- const interval = setInterval ( ( ) => {
267- if ( ! this . #isAppending && this . #audioQueue . length === 0 ) {
268- clearInterval ( interval ) ;
269- end ( ) ;
270- }
271- } , 50 ) ;
304+
305+ if ( this . #sourceBuffer && this . #sourceBuffer . updating ) {
306+ const onUpdateEnd = ( ) => {
307+ this . #sourceBuffer . removeEventListener ( "updateend" , onUpdateEnd ) ;
308+ end ( ) ;
309+ } ;
310+ this . #sourceBuffer . addEventListener ( "updateend" , onUpdateEnd ) ;
272311 } else {
273312 end ( ) ;
274313 }
0 commit comments