Detect browser audio codec support, use opus for Firefox

Firefox on Linux lacks mp3 decoders. Detect browser capabilities
at load time: use mp3 for Chrome, opus (ogg) for Firefox.
MediaSource streaming only works with mp3 (Chrome); Firefox
falls back to buffered playback with opus format.
Sends response_format parameter to TTS server.
This commit is contained in:
russell@unturf.com 2026-01-27 08:19:24 -05:00
parent 3019fd0e15
commit 74a1ea581c

View file

@ -19,6 +19,24 @@ export function setTTSMode(mode) {
}
}
// Detect best audio format for this browser
// Chrome: mp3 works everywhere, MediaSource supports audio/mpeg
// Firefox: mp3 may lack decoders on Linux; opus (ogg) always works
// For MediaSource streaming: only Chrome supports audio/mpeg, nobody supports audio/ogg in MSE
function detectAudioFormat() {
const audio = document.createElement('audio');
// Check if the browser can play mp3 at all
const canMp3 = audio.canPlayType('audio/mpeg') === 'probably' || audio.canPlayType('audio/mpeg') === 'maybe';
if (canMp3) {
return { format: 'mp3', mime: 'audio/mpeg', mseMime: 'audio/mpeg' };
}
// Fallback to opus (ogg container) - Firefox always supports this
console.log("Browser lacks mp3 support, using opus format");
return { format: 'opus', mime: 'audio/ogg', mseMime: null }; // MSE doesn't support ogg
}
const AUDIO_FORMAT = detectAudioFormat();
// Simple JavaScript text cleaning for TTS
function cleanTextForTTS(text) {
return text
@ -55,6 +73,7 @@ export async function speakText(text, voice = "alloy", rate = 0.9, model = "tts-
model: model,
voice: voice,
input: spokenText,
response_format: AUDIO_FORMAT.format,
}),
});
@ -97,6 +116,7 @@ export async function speakTextDirect(text, voice = "alloy", rate = 0.9, model =
model: model,
voice: voice,
input: cleanedText,
response_format: AUDIO_FORMAT.format,
}),
});
@ -128,6 +148,15 @@ export async function speakTextStreaming(text, voice = "alloy", rate = 0.9, mode
const cleanedText = cleanTextForTTS(text);
console.log("TTS streaming input preview:", cleanedText.substring(0, 100) + "...");
// Determine if we can use MediaSource for true streaming
const mseMime = AUDIO_FORMAT.mseMime;
const canStream = mseMime && window.MediaSource && MediaSource.isTypeSupported(mseMime);
// For streaming via MSE, request mp3 (only format MSE supports well)
// For buffered fallback, use detected format (opus for Firefox)
const requestFormat = canStream ? 'mp3' : AUDIO_FORMAT.format;
console.log(`TTS streaming: canStream=${canStream}, format=${requestFormat}, mseMime=${mseMime}`);
const response = await fetch(TTS_API_URL, {
method: "POST",
headers: {
@ -138,6 +167,7 @@ export async function speakTextStreaming(text, voice = "alloy", rate = 0.9, mode
model: model,
voice: voice,
input: cleanedText,
response_format: requestFormat,
}),
});
@ -145,9 +175,9 @@ export async function speakTextStreaming(text, voice = "alloy", rate = 0.9, mode
throw new Error(`HTTP error! status: ${response.status}`);
}
// Check if MediaSource is supported and can handle audio/mpeg
if (!window.MediaSource || !MediaSource.isTypeSupported('audio/mpeg')) {
console.warn("MediaSource API doesn't support audio/mpeg, falling back to buffered mode");
// If MSE isn't available for this format, fall back to buffered playback
if (!canStream) {
console.warn(`MediaSource can't stream ${requestFormat}, using buffered playback with ${AUDIO_FORMAT.format}`);
const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
@ -169,7 +199,7 @@ export async function speakTextStreaming(text, voice = "alloy", rate = 0.9, mode
mediaSource.addEventListener('sourceopen', async () => {
let sourceBuffer;
try {
sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');
sourceBuffer = mediaSource.addSourceBuffer(mseMime);
} catch (e) {
console.error("Failed to create SourceBuffer:", e);
reject(e);
@ -237,7 +267,7 @@ export async function speakTextStreaming(text, voice = "alloy", rate = 0.9, mode
console.log(`TTS streaming complete: ${totalBytes} bytes`);
// Build blob for download/storage
const audioBlob = new Blob(chunks, { type: 'audio/mpeg' });
const audioBlob = new Blob(chunks, { type: mseMime });
setLastTTS(text, { audio, blob: audioBlob });
resolve({ blob: audioBlob, blobUrl: audioUrl });