Skip to content

Commit dd8c879

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents 2eec656 + cc3cec4 commit dd8c879

1 file changed

Lines changed: 56 additions & 17 deletions

File tree

wiktionary_pron/scripts/tts.js

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)