Skip to content

Commit 732db95

Browse files
authored
[CLNP: 2977] fix: m4a format audio file playing in Safari (#1123)
Fixes https://sendbird.atlassian.net/browse/CLNP-2997 #### Problem Statement Voice audio files created on platforms other than the web, such as Android/iOS, are in M4A format. When attempting to convert these M4A files to `audio/mp3` or `audio/mpeg` format for playback on the web, Safari browser cannot find the correct codec and throws an error: `Unhandled Promise Rejection: NotSupportedError: The operation is not supported.` This results in the file not being playable. #### Solution For Safari browser only, if the message file type is M4A, ensure it is played in this format. However, the file must be parsed as `audio/x-m4a` instead of `audio/m4a` to be correctly played in the Safari browser. #### Test Cases Checked | Browser | .m4a file(created in iOS / Android UIKit) | .mp3 File(created in React UIKit) | |----------|----------|----------| | Safari | ✅ | ✅ | | Chrome | ✅ | ✅ | | Firefox | ✅ | ✅ | > A testable account credential for .m4a can be found in the linked Jira ticket
1 parent 83c6dd5 commit 732db95

File tree

6 files changed

+95
-4
lines changed

6 files changed

+95
-4
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {
2+
VOICE_MESSAGE_FILE_NAME,
3+
VOICE_MESSAGE_FILE_NAME__XM4A,
4+
VOICE_MESSAGE_MIME_TYPE,
5+
VOICE_MESSAGE_MIME_TYPE__XM4A,
6+
} from '../../../utils/consts';
7+
import { isSafari } from '../../../utils/browser';
8+
import { getParsedVoiceAudioFileInfo } from '../utils';
9+
10+
describe('getParsedVoiceAudioFileInfo', () => {
11+
const originalUserAgent = navigator.userAgent;
12+
13+
afterEach(() => {
14+
Object.defineProperty(navigator, 'userAgent', {
15+
value: originalUserAgent,
16+
writable: false,
17+
});
18+
});
19+
20+
it('should return VOICE_MESSAGE_MIME_TYPE__XM4A for Safari and m4a MIME type', () => {
21+
const safariUserAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15';
22+
23+
Object.defineProperty(navigator, 'userAgent', {
24+
value: safariUserAgent,
25+
writable: true,
26+
});
27+
28+
expect(isSafari(safariUserAgent)).toBe(true);
29+
expect(getParsedVoiceAudioFileInfo('audio/mp3').mimeType).toBe(VOICE_MESSAGE_MIME_TYPE);
30+
expect(getParsedVoiceAudioFileInfo('audio/m4a').mimeType).toBe(VOICE_MESSAGE_MIME_TYPE__XM4A);
31+
expect(getParsedVoiceAudioFileInfo('audio/mp3').name).toBe(VOICE_MESSAGE_FILE_NAME);
32+
expect(getParsedVoiceAudioFileInfo('audio/m4a').name).toBe(VOICE_MESSAGE_FILE_NAME__XM4A);
33+
});
34+
35+
it('should return VOICE_MESSAGE_MIME_TYPE for non-Safari browser and m4a MIME type', () => {
36+
const chromeUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36';
37+
Object.defineProperty(navigator, 'userAgent', {
38+
value: chromeUserAgent,
39+
writable: true,
40+
});
41+
42+
expect(isSafari(chromeUserAgent)).toBe(false);
43+
expect(getParsedVoiceAudioFileInfo('audio/mp3').mimeType).toBe(VOICE_MESSAGE_MIME_TYPE);
44+
expect(getParsedVoiceAudioFileInfo('audio/m4a').mimeType).toBe(VOICE_MESSAGE_MIME_TYPE);
45+
expect(getParsedVoiceAudioFileInfo('audio/mp3').name).toBe(VOICE_MESSAGE_FILE_NAME);
46+
expect(getParsedVoiceAudioFileInfo('audio/m4a').name).toBe(VOICE_MESSAGE_FILE_NAME);
47+
});
48+
});

src/hooks/VoicePlayer/index.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import {
1515
SET_CURRENT_PLAYER,
1616
} from './dux/actionTypes';
1717
import {
18-
VOICE_MESSAGE_FILE_NAME,
1918
VOICE_MESSAGE_MIME_TYPE,
2019
VOICE_PLAYER_AUDIO_ID,
2120
VOICE_PLAYER_ROOT_ID,
2221
} from '../../utils/consts';
2322
import useSendbirdStateContext from '../useSendbirdStateContext';
23+
import { getParsedVoiceAudioFileInfo } from './utils';
2424

2525
// VoicePlayerProvider interface
2626
export interface VoicePlayerProps {
@@ -30,6 +30,7 @@ export interface VoicePlayerPlayProps {
3030
groupKey: string;
3131
audioFile?: File;
3232
audioFileUrl?: string;
33+
audioFileMimeType?: string;
3334
}
3435
export interface VoicePlayerContext {
3536
play: (props: VoicePlayerPlayProps) => void;
@@ -91,6 +92,7 @@ export const VoicePlayerProvider = ({
9192
groupKey,
9293
audioFile,
9394
audioFileUrl = '',
95+
audioFileMimeType = VOICE_MESSAGE_MIME_TYPE,
9496
}: VoicePlayerPlayProps): void => {
9597
if (groupKey !== currentGroupKey) {
9698
pause(currentGroupKey);
@@ -126,9 +128,9 @@ export const VoicePlayerProvider = ({
126128
fetch(audioFileUrl)
127129
.then((res) => res.blob())
128130
.then((blob) => {
129-
const audioFile = new File([blob], VOICE_MESSAGE_FILE_NAME, {
131+
const audioFile = new File([blob], getParsedVoiceAudioFileInfo(audioFileMimeType).name, {
130132
lastModified: new Date().getTime(),
131-
type: VOICE_MESSAGE_MIME_TYPE,
133+
type: getParsedVoiceAudioFileInfo(audioFileMimeType).mimeType,
132134
});
133135
resolve(audioFile);
134136
logger.info('VoicePlayer: Get the audioFile from URL.');

src/hooks/VoicePlayer/useVoicePlayer.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect } from 'react';
22
import { useVoicePlayerContext } from '.';
3-
import { VOICE_PLAYER_AUDIO_ID } from '../../utils/consts';
3+
import { VOICE_PLAYER_AUDIO_ID, VOICE_MESSAGE_MIME_TYPE } from '../../utils/consts';
44
import { useVoiceRecorderContext } from '../VoiceRecorder';
55

66
import { AudioUnitDefaultValue, VoicePlayerStatusType } from './dux/initialState';
@@ -11,6 +11,7 @@ export interface UseVoicePlayerProps {
1111
channelUrl?: string;
1212
audioFile?: File;
1313
audioFileUrl?: string;
14+
audioFileMimeType?: string;
1415
}
1516

1617
export interface UseVoicePlayerContext {
@@ -27,6 +28,7 @@ export const useVoicePlayer = ({
2728
channelUrl = '',
2829
audioFile,
2930
audioFileUrl = '',
31+
audioFileMimeType = VOICE_MESSAGE_MIME_TYPE,
3032
}: UseVoicePlayerProps): UseVoicePlayerContext => {
3133
const groupKey = generateGroupKey(channelUrl, key);
3234
const {
@@ -44,6 +46,7 @@ export const useVoicePlayer = ({
4446
groupKey,
4547
audioFile,
4648
audioFileUrl,
49+
audioFileMimeType,
4750
});
4851
}
4952
};

src/hooks/VoicePlayer/utils.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,36 @@
1+
import { isSafari } from '../../utils/browser';
2+
import {
3+
VOICE_MESSAGE_MIME_TYPE,
4+
VOICE_MESSAGE_MIME_TYPE__XM4A,
5+
VOICE_MESSAGE_FILE_NAME,
6+
VOICE_MESSAGE_FILE_NAME__XM4A,
7+
} from '../../utils/consts';
8+
19
export type GroupKey = string;
210
export const generateGroupKey = (channelUrl = '', key = ''): GroupKey => (`${channelUrl}-${key}`);
11+
12+
/**
13+
* Parses and returns the correct MIME type based on the browser.
14+
* If the browser is Safari and the file type is m4a, use 'audio/x-m4a' for the audio player.
15+
* Safari doesn't support 'audio/mp3' well.
16+
* Also, 'audio/m4a' should be converted to 'audio/x-m4a' to be correctly played in Safari.
17+
* @link: https://sendbird.atlassian.net/browse/CLNP-2997
18+
*
19+
* @param mimeType - The original MIME type.
20+
* @returns Converted file name and MIME type.
21+
*/
22+
export const getParsedVoiceAudioFileInfo = (mimeType: string): {
23+
name: string,
24+
mimeType: string,
25+
} => {
26+
if (isSafari(navigator.userAgent) && mimeType.includes('m4a')) {
27+
return {
28+
name: VOICE_MESSAGE_FILE_NAME__XM4A,
29+
mimeType: VOICE_MESSAGE_MIME_TYPE__XM4A,
30+
};
31+
}
32+
return {
33+
name: VOICE_MESSAGE_FILE_NAME,
34+
mimeType: VOICE_MESSAGE_MIME_TYPE,
35+
};
36+
};

src/ui/VoiceMessageItemBody/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const VoiceMessageItemBody = ({
4141
channelUrl,
4242
key: `${message?.messageId}`,
4343
audioFileUrl: message?.url,
44+
audioFileMimeType: message?.type,
4445
});
4546

4647
useEffect(() => {

src/utils/consts.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ export const VOICE_PLAYER_AUDIO_ID = 'sendbird-global-audio-player-id';
2424

2525
// voice message file
2626
export const VOICE_MESSAGE_FILE_NAME = 'Voice_message.mp3';
27+
export const VOICE_MESSAGE_FILE_NAME__XM4A = 'Voice_message.m4a';
28+
2729
export const VOICE_MESSAGE_MIME_TYPE = 'audio/mp3;sbu_type=voice';
30+
export const VOICE_MESSAGE_MIME_TYPE__XM4A = 'audio/x-m4a;sbu_type=voice';
2831

2932
// meta array
3033
export const META_ARRAY_VOICE_DURATION_KEY = 'KEY_VOICE_MESSAGE_DURATION';

0 commit comments

Comments
 (0)