Fix progressive playback: add error handling, response.body check
Progressive playback was silently failing if response.body was null or if the reader threw. Now falls back to full buffer if body isn't streamable, logs chunk progress, and properly unblocks callers on error instead of hanging forever.
This commit is contained in:
parent
cc1d21ade0
commit
03147f028d
1 changed files with 41 additions and 33 deletions
|
|
@ -178,14 +178,30 @@ export async function speakTextStreaming(text, voice = "alloy", rate = 0.9, mode
|
|||
// If MSE isn't available, use progressive playback:
|
||||
// Start playing after first chunk arrives instead of waiting for full download
|
||||
if (!canStream) {
|
||||
console.log(`Using progressive playback (no MSE for ${requestFormat})`);
|
||||
console.log(`Progressive playback mode (no MSE for ${requestFormat})`);
|
||||
|
||||
// Check if ReadableStream is available
|
||||
if (!response.body) {
|
||||
console.warn("Response body not streamable, falling back to full buffer");
|
||||
const audioBlob = await response.blob();
|
||||
const audioUrl = URL.createObjectURL(audioBlob);
|
||||
const audio = new Audio(audioUrl);
|
||||
audio.playbackRate = rate;
|
||||
setLastTTS(text, { audio, blob: audioBlob });
|
||||
return { audio, blob: audioBlob, blobUrl: audioUrl, streamed: false, done: Promise.resolve({ blob: audioBlob, blobUrl: audioUrl }) };
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const chunks = [];
|
||||
let totalBytes = 0;
|
||||
let audio = null;
|
||||
let currentBlobUrl = null;
|
||||
let audioReady = null;
|
||||
let audioReadyResolve = null;
|
||||
|
||||
const done = new Promise(async (resolve, reject) => {
|
||||
// Promise that resolves when audio element is created
|
||||
audioReady = new Promise((resolve) => { audioReadyResolve = resolve; });
|
||||
|
||||
const done = (async () => {
|
||||
try {
|
||||
while (true) {
|
||||
const { done: readerDone, value } = await reader.read();
|
||||
|
|
@ -193,16 +209,17 @@ export async function speakTextStreaming(text, voice = "alloy", rate = 0.9, mode
|
|||
|
||||
chunks.push(value);
|
||||
totalBytes += value.byteLength;
|
||||
console.log(`Progressive: received ${totalBytes} bytes`);
|
||||
|
||||
// Start playback after accumulating some data (~8KB)
|
||||
// This gives the decoder enough to parse headers and begin
|
||||
// Start playback after accumulating enough data for decoder headers
|
||||
if (!audio && totalBytes > 8192) {
|
||||
const partialBlob = new Blob(chunks, { type: AUDIO_FORMAT.mime });
|
||||
currentBlobUrl = URL.createObjectURL(partialBlob);
|
||||
audio = new Audio(currentBlobUrl);
|
||||
const partialUrl = URL.createObjectURL(partialBlob);
|
||||
audio = new Audio(partialUrl);
|
||||
audio.playbackRate = rate;
|
||||
audio.play().catch(e => console.warn("Auto-play blocked:", e.message));
|
||||
console.log(`Progressive playback started at ${totalBytes} bytes`);
|
||||
audioReadyResolve(audio);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -211,12 +228,14 @@ export async function speakTextStreaming(text, voice = "alloy", rate = 0.9, mode
|
|||
const fullBlobUrl = URL.createObjectURL(fullBlob);
|
||||
|
||||
if (!audio) {
|
||||
// Very short audio - never hit the threshold
|
||||
// Short audio — never hit the 8KB threshold
|
||||
audio = new Audio(fullBlobUrl);
|
||||
audio.playbackRate = rate;
|
||||
audio.play().catch(e => console.warn("Auto-play blocked:", e.message));
|
||||
console.log(`Progressive: short audio, playing full ${totalBytes} bytes`);
|
||||
audioReadyResolve(audio);
|
||||
} else {
|
||||
// Replace with complete audio to enable seeking
|
||||
// Swap to complete blob for seeking/duration support
|
||||
const currentTime = audio.currentTime;
|
||||
const wasPlaying = !audio.paused;
|
||||
audio.src = fullBlobUrl;
|
||||
|
|
@ -225,40 +244,29 @@ export async function speakTextStreaming(text, voice = "alloy", rate = 0.9, mode
|
|||
if (wasPlaying) audio.play().catch(() => {});
|
||||
}
|
||||
|
||||
if (currentBlobUrl && currentBlobUrl !== fullBlobUrl) {
|
||||
URL.revokeObjectURL(currentBlobUrl);
|
||||
}
|
||||
|
||||
console.log(`Progressive playback complete: ${totalBytes} bytes`);
|
||||
setLastTTS(text, { audio, blob: fullBlob });
|
||||
resolve({ blob: fullBlob, blobUrl: fullBlobUrl });
|
||||
return { blob: fullBlob, blobUrl: fullBlobUrl };
|
||||
} catch (error) {
|
||||
console.error("Progressive playback error:", error);
|
||||
reject(error);
|
||||
// If audio was never created, unblock the waiter
|
||||
if (!audio) audioReadyResolve(null);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// Return immediately — audio will be set once first chunks arrive
|
||||
// Use a proxy object so callers get the audio once it's ready
|
||||
const result = { audio: null, blob: null, blobUrl: null, streamed: true, done };
|
||||
// Wait for audio element to be created (first ~8KB)
|
||||
const readyAudio = await audioReady;
|
||||
if (!readyAudio) {
|
||||
throw new Error("Progressive playback failed to create audio");
|
||||
}
|
||||
|
||||
const result = { audio: readyAudio, blob: null, blobUrl: null, streamed: true, done };
|
||||
|
||||
done.then(({ blob, blobUrl }) => {
|
||||
result.blob = blob;
|
||||
result.blobUrl = blobUrl;
|
||||
}).catch(() => {});
|
||||
|
||||
// Wait for audio element to be created (first chunks)
|
||||
await new Promise((resolve) => {
|
||||
const check = () => {
|
||||
if (audio) {
|
||||
result.audio = audio;
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(check, 50);
|
||||
}
|
||||
};
|
||||
check();
|
||||
});
|
||||
}).catch((err) => console.error("Progressive done error:", err));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue