mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(app): record voice messages in a widely playable container (#1973)
This commit is contained in:
1 parent
bf11445299
commit
fdd12194b1
1 file changed
+54
-6
@@ -6,7 +6,11 @@ import styles from '@app/features/channel/components/VoiceMessageRecorder.module
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {usePortalHost} from '@app/features/ui/overlay/PortalHostContext';
|
||||
import {getReducedMotionProps} from '@app/features/ui/utils/ReducedMotionAnimation';
|
||||
import {prepareVoiceMessageWav} from '@app/features/voice/utils/VoiceMessageRecordingUtils';
|
||||
import {
|
||||
computeVoiceWaveformFromAudioBuffer,
|
||||
type PreparedVoiceMessage,
|
||||
prepareVoiceMessageWav,
|
||||
} from '@app/features/voice/utils/VoiceMessageRecordingUtils';
|
||||
import {sendVoiceMessage} from '@app/features/voice/utils/VoiceMessageSendUtils';
|
||||
import {autoUpdate, FloatingPortal, flip, offset, shift, useFloating} from '@floating-ui/react';
|
||||
import {
|
||||
@@ -67,6 +71,43 @@ const SEND_VOICE_MESSAGE_DESCRIPTOR = msg({
|
||||
comment: 'Accessible label and tooltip for the send button after recording a voice message.',
|
||||
});
|
||||
const logger = new Logger('VoiceMessageRecorder');
|
||||
const UNIVERSALLY_PLAYABLE_RECORDING_MIME = 'audio/mp4';
|
||||
const OPUS_RECORDING_MIME_CANDIDATES = ['audio/ogg;codecs=opus', 'audio/webm;codecs=opus'] as const;
|
||||
|
||||
export function selectRecordingMimeType(isTypeSupported: (mimeType: string) => boolean): string | undefined {
|
||||
if (isTypeSupported(UNIVERSALLY_PLAYABLE_RECORDING_MIME)) {
|
||||
return UNIVERSALLY_PLAYABLE_RECORDING_MIME;
|
||||
}
|
||||
return OPUS_RECORDING_MIME_CANDIDATES.find((candidate) => isTypeSupported(candidate));
|
||||
}
|
||||
|
||||
export function isUniversallyPlayableContainer(mimeType: string): boolean {
|
||||
return mimeType.split(';')[0].trim().toLowerCase() === UNIVERSALLY_PLAYABLE_RECORDING_MIME;
|
||||
}
|
||||
|
||||
async function prepareUniversalUpload(blob: Blob, filename: string): Promise<PreparedVoiceMessage> {
|
||||
const contextClass =
|
||||
window.AudioContext || (window as typeof window & {webkitAudioContext?: typeof AudioContext}).webkitAudioContext;
|
||||
if (!contextClass) {
|
||||
throw new Error('AudioContext is unavailable; cannot measure voice message');
|
||||
}
|
||||
const audioContext = new contextClass();
|
||||
try {
|
||||
const audioBuffer = await audioContext.decodeAudioData(await blob.arrayBuffer());
|
||||
const {duration, waveform} = computeVoiceWaveformFromAudioBuffer(audioBuffer);
|
||||
return {file: new File([blob], filename, {type: blob.type}), duration, waveform};
|
||||
} finally {
|
||||
audioContext.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareVoiceMessageUpload(blob: Blob): Promise<PreparedVoiceMessage> {
|
||||
const stamp = Date.now();
|
||||
if (isUniversallyPlayableContainer(blob.type)) {
|
||||
return prepareUniversalUpload(blob, `voice-message-${stamp}.m4a`);
|
||||
}
|
||||
return prepareVoiceMessageWav(blob, `voice-message-${stamp}.wav`);
|
||||
}
|
||||
|
||||
type StopAction = 'send' | 'discard';
|
||||
|
||||
@@ -322,13 +363,13 @@ export default function VoiceMessageRecorder({channelId, disabled, tooltipAnchor
|
||||
resetState();
|
||||
return;
|
||||
}
|
||||
const blob = new Blob(recordedChunks, {type: recorderMime || 'audio/ogg'});
|
||||
const blob = new Blob(recordedChunks, {type: recorderMime || OPUS_RECORDING_MIME_CANDIDATES[0]});
|
||||
setIsSending(true);
|
||||
setIsRecording(false);
|
||||
setIsLocked(false);
|
||||
setLockPreview(false);
|
||||
try {
|
||||
const {file, duration, waveform} = await prepareVoiceMessageWav(blob, `voice-message-${Date.now()}.wav`);
|
||||
const {file, duration, waveform} = await prepareVoiceMessageUpload(blob);
|
||||
await sendVoiceMessage({
|
||||
channelId,
|
||||
file,
|
||||
@@ -351,11 +392,15 @@ export default function VoiceMessageRecorder({channelId, disabled, tooltipAnchor
|
||||
showTooltip(i18n._(VOICE_RECORDING_IS_NOT_SUPPORTED_IN_THIS_BROWSER_DESCRIPTOR));
|
||||
return;
|
||||
}
|
||||
const preferredType = selectRecordingMimeType((candidate) => MediaRecorder.isTypeSupported(candidate));
|
||||
if (preferredType === undefined) {
|
||||
showTooltip(i18n._(VOICE_RECORDING_IS_NOT_SUPPORTED_IN_THIS_BROWSER_DESCRIPTOR));
|
||||
return;
|
||||
}
|
||||
let acquiredStream: MediaStream | null = null;
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({audio: true});
|
||||
const preferredType = MediaRecorder.isTypeSupported('audio/ogg;codecs=opus')
|
||||
? 'audio/ogg;codecs=opus'
|
||||
: 'audio/webm;codecs=opus';
|
||||
acquiredStream = stream;
|
||||
const recorder = new MediaRecorder(stream, {mimeType: preferredType});
|
||||
chunksRef.current = [];
|
||||
recorder.addEventListener('dataavailable', handleDataAvailable);
|
||||
@@ -379,6 +424,9 @@ export default function VoiceMessageRecorder({channelId, disabled, tooltipAnchor
|
||||
startWaveform(stream);
|
||||
} catch (error) {
|
||||
logger.error('Failed to start voice recording', error);
|
||||
if (acquiredStream !== null && streamRef.current !== acquiredStream) {
|
||||
acquiredStream.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
showTooltip(i18n._(UNABLE_TO_START_RECORDING_PLEASE_ALLOW_MICROPHONE_ACCESS_DESCRIPTOR));
|
||||
}
|
||||
}, [
|
||||
|
||||
Reference in new issue
Block a user