fix(webui): detect Chrome voice recording support (#5027)

This commit is contained in:
chengyongru
2026-07-22 14:10:09 +08:00
committed by GitHub
parent 3748f664b2
commit 63bc6e98a7
13 changed files with 119 additions and 22 deletions
+27 -8
View File
@@ -29,6 +29,7 @@ const VOICE_MIME_CANDIDATES = [
export type VoiceRecorderState = "idle" | "recording" | "transcribing";
export type VoiceRecorderErrorKey =
| "failed"
| "noDevice"
| "noInput"
| "notConfigured"
| "permission"
@@ -181,14 +182,17 @@ export function useVoiceRecorder({
const startRecording = useCallback(async () => {
if (!onTranscribeAudio || state !== "idle" || startPendingRef.current) return;
if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === "undefined") {
onClearError();
const mediaDevices = navigator.mediaDevices;
const MediaRecorderCtor = mediaRecorderConstructor();
if (!mediaDevices?.getUserMedia || !MediaRecorderCtor) {
onError("unsupported");
return;
}
startPendingRef.current = true;
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const recorder = new MediaRecorder(stream, mediaRecorderOptions());
const stream = await mediaDevices.getUserMedia({ audio: true });
const recorder = new MediaRecorderCtor(stream, mediaRecorderOptions(MediaRecorderCtor));
chunksRef.current = [];
streamRef.current = stream;
mediaRecorderRef.current = recorder;
@@ -251,10 +255,10 @@ export function useVoiceRecorder({
noInputHintVisibleRef.current = true;
onError("noInput");
}, VOICE_NO_INPUT_HINT_MS);
} catch {
} catch (error) {
cleanupRecording();
setState("idle");
onError("permission");
onError(recordingErrorKey(error));
}
}, [
cleanupRecording,
@@ -370,12 +374,21 @@ function clearTimer(ref: { current: ReturnType<typeof setTimeout> | null }) {
}
}
function mediaRecorderOptions(): MediaRecorderOptions | undefined {
if (typeof MediaRecorder === "undefined") return undefined;
const mimeType = VOICE_MIME_CANDIDATES.find((type) => MediaRecorder.isTypeSupported(type));
function mediaRecorderOptions(MediaRecorderCtor: MediaRecorderConstructor): MediaRecorderOptions | undefined {
const mimeType = VOICE_MIME_CANDIDATES.find((type) => MediaRecorderCtor.isTypeSupported?.(type));
return mimeType ? { mimeType } : undefined;
}
type MediaRecorderConstructor = typeof MediaRecorder;
function mediaRecorderConstructor(): MediaRecorderConstructor | undefined {
if (typeof window === "undefined") return undefined;
const browserWindow = window as Window & {
MediaRecorder?: MediaRecorderConstructor;
};
return browserWindow.MediaRecorder;
}
function formatVoiceElapsed(ms: number): string {
const seconds = Math.max(0, Math.floor(ms / 1000));
const minutes = Math.floor(seconds / 60);
@@ -510,3 +523,9 @@ function transcriptionErrorKey(error: unknown): VoiceRecorderErrorKey {
if (detail === "duration") return "tooLong";
return "failed";
}
function recordingErrorKey(error: unknown): VoiceRecorderErrorKey {
const name = error instanceof Error ? error.name : "";
if (name === "NotFoundError") return "noDevice";
return "permission";
}