Skip to content

Commit 931c13f

Browse files
authored
Merge pull request #286 from dozro/fix/voice-message-mess
Fix audio codec handling and enhance voice recorder functionality
2 parents 85b0da3 + 166c8f4 commit 931c13f

7 files changed

Lines changed: 250 additions & 27 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: patch
3+
---
4+
5+
fix of compatibility of voice messages with element clients and style misshaps

src/app/features/room/AudioMessageRecorder.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type AudioMessageRecorderProps = {
88
onRequestClose: () => void;
99
onWaveformUpdate: (waveform: number[]) => void;
1010
onAudioLengthUpdate: (length: number) => void;
11+
onAudioCodecUpdate?: (codec: string) => void;
1112
};
1213

1314
// We use a react voice recorder library to handle the recording of audio messages, as it provides a simple API and handles the complexities of recording audio in the browser.
@@ -19,6 +20,7 @@ export function AudioMessageRecorder({
1920
onRequestClose,
2021
onWaveformUpdate,
2122
onAudioLengthUpdate,
23+
onAudioCodecUpdate,
2224
}: AudioMessageRecorderProps) {
2325
const containerRef = useRef<HTMLDivElement>(null);
2426
const isDismissedRef = useRef(false);
@@ -50,7 +52,7 @@ export function AudioMessageRecorder({
5052
borderRadius: config.radii.R400,
5153
boxShadow: config.shadow.E200,
5254
padding: config.space.S400,
53-
minWidth: 300,
55+
width: 300,
5456
}}
5557
>
5658
<Text size="H4">Audio Message Recorder</Text>
@@ -60,16 +62,20 @@ export function AudioMessageRecorder({
6062
audioFile,
6163
waveform,
6264
audioLength,
65+
audioCodec,
6366
}: {
6467
audioFile: Blob;
6568
waveform: number[];
6669
audioLength: number;
70+
audioCodec: string;
6771
}) => {
6872
if (isDismissedRef.current) return;
6973
// closes the recorder and sends the audio file back to the parent component to be uploaded and sent as a message
7074
onRecordingComplete(audioFile);
7175
onWaveformUpdate(waveform);
7276
onAudioLengthUpdate(audioLength);
77+
// Pass the audio codec to the parent component
78+
if (onAudioCodecUpdate) onAudioCodecUpdate(audioCodec);
7379
}}
7480
buttonBackgroundColor={color.SurfaceVariant.Container}
7581
buttonHoverBackgroundColor={color.SurfaceVariant.ContainerHover}

src/app/features/room/RoomInput.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ import { usePowerLevelsContext } from '$hooks/usePowerLevels';
151151
import { useRoomCreators } from '$hooks/useRoomCreators';
152152
import { useRoomPermissions } from '$hooks/useRoomPermissions';
153153
import { AutocompleteNotice } from '$components/editor/autocomplete/AutocompleteNotice';
154+
import { getSupportedAudioExtension } from '$plugins/voice-recorder-kit/supportedCodec';
154155
import { SchedulePickerDialog } from './schedule-send';
155156
import * as css from './schedule-send/SchedulePickerDialog.css';
156157
import {
@@ -1131,7 +1132,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
11311132
onRecordingComplete={(audioBlob) => {
11321133
const file = new File(
11331134
[audioBlob],
1134-
`sable-audio-message-${Date.now()}.ogg`,
1135+
`sable-audio-message-${Date.now()}.${getSupportedAudioExtension(audioBlob.type)}`,
11351136
{
11361137
type: audioBlob.type,
11371138
}

src/app/features/room/msgContent.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,28 +155,60 @@ export const getAudioMsgContent = (
155155
audioLength?: number
156156
): AudioMsgContent => {
157157
const { file, encInfo } = item;
158-
const content: IContent = {
158+
let content: IContent = {
159159
msgtype: MsgType.Audio,
160160
filename: file.name,
161-
body: file.name,
161+
body: item.body && item.body.length > 0 ? item.body : 'a voice message',
162162
format: 'org.matrix.custom.html',
163-
formatted_body: file.name,
163+
formatted_body: item.body && item.body.length > 0 ? item.body : '<em>a voice message</em>',
164164
info: {
165165
mimetype: file.type,
166166
size: file.size,
167+
duration: item.metadata.markedAsSpoiler || !audioLength ? 0 : audioLength * 1000,
167168
},
169+
170+
// Element-compatible unstable extensible-event keys
168171
'org.matrix.msc1767.audio': {
169172
waveform: waveform?.map((v) => Math.round(v * 1024)), // scale waveform values to fit in 10 bits (0-1024) for more efficient storage, as per MSC1767 spec
170173
duration: item.metadata.markedAsSpoiler || !audioLength ? 0 : audioLength * 1000, // if marked as spoiler, set duration to 0 to hide it in clients that support msc1767
171174
},
175+
'org.matrix.msc1767.text': item.body && item.body.length > 0 ? item.body : 'a voice message',
176+
'org.matrix.msc3245.voice.v2': {
177+
duration: !audioLength ? 0 : audioLength,
178+
waveform: waveform?.map((v) => Math.round(v * 1024)),
179+
},
180+
// for element compat
181+
'org.matrix.msc3245.voice': {},
172182
};
173183
if (encInfo) {
174184
content.file = {
175185
...encInfo,
176186
url: mxc,
177187
};
188+
content = {
189+
...content,
190+
191+
// Element-compatible unstable extensible-event keys
192+
'org.matrix.msc1767.file': {
193+
name: file.name,
194+
mimetype: file.type,
195+
size: file.size,
196+
file: content.file,
197+
},
198+
};
178199
} else {
179200
content.url = mxc;
201+
content = {
202+
...content,
203+
204+
// Element-compatible unstable extensible-event keys
205+
'org.matrix.msc1767.file': {
206+
name: file.name,
207+
mimetype: file.type,
208+
size: file.size,
209+
url: content.url,
210+
},
211+
};
180212
}
181213
if (item.body && item.body.length > 0) content.body = item.body;
182214
if (item.formatted_body && item.formatted_body.length > 0) {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const safariPreferredCodecs = [
2+
// Safari works best with MP4/AAC but fails when strict codecs are defined on iOS.
3+
// Prioritize the plain container to avoid NotSupportedError during MediaRecorder initialization.
4+
'audio/mp4',
5+
'audio/mp4;codecs=mp4a.40.2',
6+
'audio/mp4;codecs=mp4a.40.5',
7+
'audio/mp4;codecs=aac',
8+
'audio/aac',
9+
// Fallbacks
10+
'audio/wav;codecs=1',
11+
'audio/wav',
12+
'audio/mpeg',
13+
];
14+
15+
const defaultPreferredCodecs = [
16+
// Chromium / Firefox stable path.
17+
'audio/webm;codecs=opus',
18+
'audio/webm',
19+
// Firefox
20+
'audio/ogg;codecs=opus',
21+
'audio/ogg;codecs=vorbis',
22+
'audio/ogg',
23+
// Fallbacks
24+
'audio/wav;codecs=1',
25+
'audio/wav',
26+
'audio/mpeg',
27+
// Keep MP4/AAC as late fallback for non-Safari browsers.
28+
'audio/mp4;codecs=mp4a.40.2',
29+
'audio/mp4;codecs=mp4a.40.5',
30+
'audio/mp4;codecs=aac',
31+
'audio/mp4',
32+
'audio/aac',
33+
'audio/ogg;codecs=speex',
34+
'audio/webm;codecs=vorbis',
35+
];
36+
37+
/**
38+
* Checks for supported audio codecs in the current browser and returns the first supported codec.
39+
* If no supported codec is found, it returns null.
40+
*/
41+
export function getSupportedAudioCodec(): string | null {
42+
if (!('MediaRecorder' in globalThis) || !globalThis.MediaRecorder) {
43+
return null;
44+
}
45+
46+
const userAgent = globalThis.navigator?.userAgent ?? '';
47+
const isIOS =
48+
/iPad|iPhone|iPod/.test(userAgent) ||
49+
// eslint-disable-next-line @typescript-eslint/no-deprecated
50+
(globalThis.navigator?.platform === 'MacIntel' && globalThis.navigator?.maxTouchPoints > 1);
51+
const isSafari = /^((?!chrome|android|crios|fxios|edgios).)*safari/i.test(userAgent) || isIOS;
52+
53+
const preferredCodecs = isSafari ? safariPreferredCodecs : defaultPreferredCodecs;
54+
const supportedCodec = preferredCodecs.find((codec) => MediaRecorder.isTypeSupported(codec));
55+
return supportedCodec || null;
56+
}
57+
58+
/**
59+
* Returns the appropriate file extension for a given audio codec.
60+
* This is used to ensure that the recorded audio file has the correct extension based on the codec used for recording.
61+
*/
62+
export function getSupportedAudioExtension(codec: string): string {
63+
const baseType = codec.split(';')[0].trim();
64+
switch (baseType) {
65+
case 'audio/ogg':
66+
return 'ogg';
67+
case 'audio/webm':
68+
return 'webm';
69+
case 'audio/mp4':
70+
return 'm4a';
71+
case 'audio/mpeg':
72+
return 'mp3';
73+
case 'audio/wav':
74+
return 'wav';
75+
case 'audio/aac':
76+
return 'aac';
77+
default:
78+
return 'dat'; // default extension for unknown codecs
79+
}
80+
}

src/app/plugins/voice-recorder-kit/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type VoiceRecorderStopPayload = {
77
audioUrl: string;
88
waveform: number[];
99
audioLength: number;
10+
audioCodec: string;
1011
};
1112

1213
export type UseVoiceRecorderOptions = {

0 commit comments

Comments
 (0)