-
Notifications
You must be signed in to change notification settings - Fork 138
/
Copy pathindex.tsx
155 lines (139 loc) · 5.6 KB
/
index.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { useLocalization } from '../../lib/LocalizationContext';
import Modal from '../../ui/Modal';
import {
BROWSER_SUPPORT_MIME_TYPE_LIST,
VOICE_MESSAGE_FILE_NAME,
VOICE_MESSAGE_MIME_TYPE,
VOICE_RECORDER_AUDIO_BIT_RATE,
} from '../../utils/consts';
import { type WebAudioUtils } from './WebAudioUtils';
import { noop } from '../../utils/utils';
import useSendbird from '../../lib/Sendbird/context/hooks/useSendbird';
// Input props of VoiceRecorder
export interface VoiceRecorderProps {
children: React.ReactElement;
}
export interface VoiceRecorderEventHandler {
onRecordingStarted?: () => void;
onRecordingEnded?: (props: null | File) => void;
}
// Output of VoiceRecorder
export interface VoiceRecorderContext {
start: (eventHandler?: VoiceRecorderEventHandler) => void;
stop: () => void;
isRecordable: boolean;
}
const Context = createContext<VoiceRecorderContext>({
start: noop,
stop: noop,
isRecordable: false,
});
export const VoiceRecorderProvider = (props: VoiceRecorderProps): React.ReactElement => {
const { children } = props;
const { state } = useSendbird();
const { config } = state;
const { logger, groupChannel } = config;
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(null);
const [isRecordable, setIsRecordable] = useState<boolean>(false);
const [permissionWarning, setPermissionWarning] = useState<boolean>(false);
const { stringSet } = useLocalization();
const isVoiceMessageEnabled = groupChannel.enableVoiceMessage;
const [webAudioUtils, setWebAudioUtils] = useState<WebAudioUtils | null>(null);
const browserSupportMimeType = BROWSER_SUPPORT_MIME_TYPE_LIST.find((mimeType) => MediaRecorder.isTypeSupported(mimeType)) ?? '';
if (isVoiceMessageEnabled && !browserSupportMimeType) {
logger.error('VoiceRecorder: Browser does not support mimeType', { mimmeTypes: BROWSER_SUPPORT_MIME_TYPE_LIST });
}
useEffect(() => {
if (isVoiceMessageEnabled && !webAudioUtils) {
import('./WebAudioUtils').then((module) => setWebAudioUtils(module));
}
}, [isVoiceMessageEnabled, webAudioUtils]);
const start = useCallback((eventHandler?: VoiceRecorderEventHandler): void => {
if (isVoiceMessageEnabled && !webAudioUtils) {
logger.error('VoiceRecorder: Recording audio processor is being loaded.');
return;
}
const checkPermission = () => {
try {
// Type '"microphone"' is not assignable to type 'PermissionName'.ts(2322)
// this is typescript issue
// https://github.com/microsoft/TypeScript/issues/33923
// @ts-expect-error
navigator.permissions.query({ name: 'microphone' }).then((result) => {
if (result.state === 'denied') {
logger.warning('VoiceRecorder: Permission denied.');
setPermissionWarning(true);
}
});
} catch (error) {
logger.warning('VoiceRecorder: Failed to check permission.', error);
}
};
logger.info('VoiceRecorder: Start recording.');
if (mediaRecorder) {
stop();
logger.info('VoiceRecorder: Previous mediaRecorder is stopped.');
}
checkPermission();
navigator?.mediaDevices?.getUserMedia?.({ audio: true })
.then((stream) => {
logger.info('VoiceRecorder: Succeeded getting media stream.', stream);
setIsRecordable(true);
const mediaRecorder = new MediaRecorder(stream, {
mimeType: browserSupportMimeType,
audioBitsPerSecond: VOICE_RECORDER_AUDIO_BIT_RATE,
});
// when recording stops
mediaRecorder.ondataavailable = (e) => {
logger.info('VoiceRecorder: Succeeded getting an available data.', e.data);
const audioFile = new File([e.data], VOICE_MESSAGE_FILE_NAME, {
lastModified: new Date().getTime(),
type: VOICE_MESSAGE_MIME_TYPE,
});
webAudioUtils?.downsampleToWav(audioFile, (buffer) => {
const mp3Buffer = webAudioUtils?.encodeMp3(buffer);
const mp3blob = new Blob(mp3Buffer, { type: VOICE_MESSAGE_MIME_TYPE });
const convertedAudioFile = new File([mp3blob], VOICE_MESSAGE_FILE_NAME, {
lastModified: new Date().getTime(),
type: VOICE_MESSAGE_MIME_TYPE,
});
eventHandler?.onRecordingEnded?.(convertedAudioFile);
logger.info('VoiceRecorder: Succeeded converting audio file.', convertedAudioFile);
});
const tracks = stream.getAudioTracks();
tracks.forEach((track) => track.stop());
setIsRecordable(false);
};
mediaRecorder.onstart = eventHandler?.onRecordingStarted ?? noop;
mediaRecorder?.start();
setMediaRecorder(mediaRecorder);
})
.catch((err) => {
logger.error('VoiceRecorder: Failed getting media stream.', err);
setMediaRecorder(null);
});
}, [mediaRecorder, webAudioUtils]);
const stop = useCallback((): void => {
// Stop recording
mediaRecorder?.stop();
setMediaRecorder(null);
setIsRecordable(false);
logger.info('VoiceRecorder: Stop recording.');
}, [mediaRecorder]);
return (
<Context.Provider value={{ start, stop, isRecordable }}>
{children}
{permissionWarning && (
<Modal hideFooter onClose={() => setPermissionWarning(false)}>
<>{stringSet.VOICE_RECORDING_PERMISSION_DENIED}</>
</Modal>
)}
</Context.Provider>
);
};
export const useVoiceRecorderContext = (): VoiceRecorderContext => useContext(Context);
export default {
VoiceRecorderProvider,
useVoiceRecorderContext,
};