1 | import { EventEmitter, Subscription, Platform } from '@unimodules/core';
|
2 | import { PermissionResponse, PermissionStatus } from 'unimodules-permissions-interface';
|
3 |
|
4 | import {
|
5 | _DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS,
|
6 | AVPlaybackStatus,
|
7 | AVPlaybackStatusToSet,
|
8 | } from '../AV';
|
9 | import ExponentAV from '../ExponentAV';
|
10 | import { isAudioEnabled, throwIfAudioIsDisabled } from './AudioAvailability';
|
11 | import { Sound } from './Sound';
|
12 |
|
13 | export type RecordingOptions = {
|
14 | android: {
|
15 | extension: string;
|
16 | outputFormat: number;
|
17 | audioEncoder: number;
|
18 | sampleRate?: number;
|
19 | numberOfChannels?: number;
|
20 | bitRate?: number;
|
21 | maxFileSize?: number;
|
22 | };
|
23 | ios: {
|
24 | extension: string;
|
25 | outputFormat?: string | number;
|
26 | audioQuality: number;
|
27 | sampleRate: number;
|
28 | numberOfChannels: number;
|
29 | bitRate: number;
|
30 | bitRateStrategy?: number;
|
31 | bitDepthHint?: number;
|
32 | linearPCMBitDepth?: number;
|
33 | linearPCMIsBigEndian?: boolean;
|
34 | linearPCMIsFloat?: boolean;
|
35 | };
|
36 | };
|
37 |
|
38 |
|
39 | export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_DEFAULT = 0;
|
40 | export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_THREE_GPP = 1;
|
41 | export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG_4 = 2;
|
42 | export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AMR_NB = 3;
|
43 | export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AMR_WB = 4;
|
44 | export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AAC_ADIF = 5;
|
45 | export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AAC_ADTS = 6;
|
46 | export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_RTP_AVP = 7;
|
47 | export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG2TS = 8;
|
48 | export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_WEBM = 9;
|
49 |
|
50 | export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_DEFAULT = 0;
|
51 | export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AMR_NB = 1;
|
52 | export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AMR_WB = 2;
|
53 | export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC = 3;
|
54 | export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_HE_AAC = 4;
|
55 | export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC_ELD = 5;
|
56 |
|
57 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_LINEARPCM = 'lpcm';
|
58 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AC3 = 'ac-3';
|
59 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_60958AC3 = 'cac3';
|
60 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_APPLEIMA4 = 'ima4';
|
61 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC = 'aac ';
|
62 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4CELP = 'celp';
|
63 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4HVXC = 'hvxc';
|
64 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4TWINVQ = 'twvq';
|
65 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MACE3 = 'MAC3';
|
66 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MACE6 = 'MAC6';
|
67 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ULAW = 'ulaw';
|
68 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ALAW = 'alaw';
|
69 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_QDESIGN = 'QDMC';
|
70 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_QDESIGN2 = 'QDM2';
|
71 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_QUALCOMM = 'Qclp';
|
72 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEGLAYER1 = '.mp1';
|
73 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEGLAYER2 = '.mp2';
|
74 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEGLAYER3 = '.mp3';
|
75 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_APPLELOSSLESS = 'alac';
|
76 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_HE = 'aach';
|
77 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_LD = 'aacl';
|
78 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_ELD = 'aace';
|
79 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_ELD_SBR = 'aacf';
|
80 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_ELD_V2 = 'aacg';
|
81 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_HE_V2 = 'aacp';
|
82 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_SPATIAL = 'aacs';
|
83 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AMR = 'samr';
|
84 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AMR_WB = 'sawb';
|
85 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AUDIBLE = 'AUDB';
|
86 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ILBC = 'ilbc';
|
87 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_DVIINTELIMA = 0x6d730011;
|
88 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MICROSOFTGSM = 0x6d730031;
|
89 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AES3 = 'aes3';
|
90 | export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ENHANCEDAC3 = 'ec-3';
|
91 |
|
92 | export const RECORDING_OPTION_IOS_AUDIO_QUALITY_MIN = 0;
|
93 | export const RECORDING_OPTION_IOS_AUDIO_QUALITY_LOW = 0x20;
|
94 | export const RECORDING_OPTION_IOS_AUDIO_QUALITY_MEDIUM = 0x40;
|
95 | export const RECORDING_OPTION_IOS_AUDIO_QUALITY_HIGH = 0x60;
|
96 | export const RECORDING_OPTION_IOS_AUDIO_QUALITY_MAX = 0x7f;
|
97 |
|
98 | export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_CONSTANT = 0;
|
99 | export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_LONG_TERM_AVERAGE = 1;
|
100 | export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_VARIABLE_CONSTRAINED = 2;
|
101 | export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_VARIABLE = 3;
|
102 |
|
103 |
|
104 |
|
105 | export const RECORDING_OPTIONS_PRESET_HIGH_QUALITY: RecordingOptions = {
|
106 | android: {
|
107 | extension: '.m4a',
|
108 | outputFormat: RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG_4,
|
109 | audioEncoder: RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC,
|
110 | sampleRate: 44100,
|
111 | numberOfChannels: 2,
|
112 | bitRate: 128000,
|
113 | },
|
114 | ios: {
|
115 | extension: '.caf',
|
116 | audioQuality: RECORDING_OPTION_IOS_AUDIO_QUALITY_MAX,
|
117 | sampleRate: 44100,
|
118 | numberOfChannels: 2,
|
119 | bitRate: 128000,
|
120 | linearPCMBitDepth: 16,
|
121 | linearPCMIsBigEndian: false,
|
122 | linearPCMIsFloat: false,
|
123 | },
|
124 | };
|
125 |
|
126 | export const RECORDING_OPTIONS_PRESET_LOW_QUALITY: RecordingOptions = {
|
127 | android: {
|
128 | extension: '.3gp',
|
129 | outputFormat: RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_THREE_GPP,
|
130 | audioEncoder: RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AMR_NB,
|
131 | sampleRate: 44100,
|
132 | numberOfChannels: 2,
|
133 | bitRate: 128000,
|
134 | },
|
135 | ios: {
|
136 | extension: '.caf',
|
137 | audioQuality: RECORDING_OPTION_IOS_AUDIO_QUALITY_MIN,
|
138 | sampleRate: 44100,
|
139 | numberOfChannels: 2,
|
140 | bitRate: 128000,
|
141 | linearPCMBitDepth: 16,
|
142 | linearPCMIsBigEndian: false,
|
143 | linearPCMIsFloat: false,
|
144 | },
|
145 | };
|
146 |
|
147 |
|
148 |
|
149 | export type RecordingStatus = {
|
150 | canRecord: boolean;
|
151 | isRecording: boolean;
|
152 | isDoneRecording: boolean;
|
153 | durationMillis: number;
|
154 | };
|
155 |
|
156 | export { PermissionResponse, PermissionStatus };
|
157 |
|
158 | let _recorderExists: boolean = false;
|
159 | const eventEmitter = Platform.OS === 'android' ? new EventEmitter(ExponentAV) : null;
|
160 |
|
161 | export async function getPermissionsAsync(): Promise<PermissionResponse> {
|
162 | return ExponentAV.getPermissionsAsync();
|
163 | }
|
164 |
|
165 | export async function requestPermissionsAsync(): Promise<PermissionResponse> {
|
166 | return ExponentAV.requestPermissionsAsync();
|
167 | }
|
168 |
|
169 | export class Recording {
|
170 | _subscription: Subscription | null = null;
|
171 | _canRecord: boolean = false;
|
172 | _isDoneRecording: boolean = false;
|
173 | _finalDurationMillis: number = 0;
|
174 | _uri: string | null = null;
|
175 | _onRecordingStatusUpdate: ((status: RecordingStatus) => void) | null = null;
|
176 | _progressUpdateTimeoutVariable: number | null = null;
|
177 | _progressUpdateIntervalMillis: number = _DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS;
|
178 | _options: RecordingOptions | null = null;
|
179 |
|
180 | // Internal methods
|
181 |
|
182 | _cleanupForUnloadedRecorder = async (finalStatus: RecordingStatus) => {
|
183 | this._canRecord = false;
|
184 | this._isDoneRecording = true;
|
185 |
|
186 | this._finalDurationMillis = finalStatus.durationMillis;
|
187 | _recorderExists = false;
|
188 | if (this._subscription) {
|
189 | this._subscription.remove();
|
190 | this._subscription = null;
|
191 | }
|
192 | this._disablePolling();
|
193 | return await this.getStatusAsync();
|
194 | };
|
195 |
|
196 | _pollingLoop = async () => {
|
197 | if (isAudioEnabled() && this._canRecord && this._onRecordingStatusUpdate != null) {
|
198 | this._progressUpdateTimeoutVariable = setTimeout(
|
199 | this._pollingLoop,
|
200 | this._progressUpdateIntervalMillis
|
201 | ) as any;
|
202 | try {
|
203 | await this.getStatusAsync();
|
204 | } catch (error) {
|
205 | this._disablePolling();
|
206 | }
|
207 | }
|
208 | };
|
209 |
|
210 | _disablePolling() {
|
211 | if (this._progressUpdateTimeoutVariable != null) {
|
212 | clearTimeout(this._progressUpdateTimeoutVariable);
|
213 | this._progressUpdateTimeoutVariable = null;
|
214 | }
|
215 | }
|
216 |
|
217 | _enablePollingIfNecessaryAndPossible() {
|
218 | if (isAudioEnabled() && this._canRecord && this._onRecordingStatusUpdate != null) {
|
219 | this._disablePolling();
|
220 | this._pollingLoop();
|
221 | }
|
222 | }
|
223 |
|
224 | _callOnRecordingStatusUpdateForNewStatus(status: RecordingStatus) {
|
225 | if (this._onRecordingStatusUpdate != null) {
|
226 | this._onRecordingStatusUpdate(status);
|
227 | }
|
228 | }
|
229 |
|
230 | async _performOperationAndHandleStatusAsync(
|
231 | operation: () => Promise<RecordingStatus>
|
232 | ): Promise<RecordingStatus> {
|
233 | throwIfAudioIsDisabled();
|
234 | if (this._canRecord) {
|
235 | const status = await operation();
|
236 | this._callOnRecordingStatusUpdateForNewStatus(status);
|
237 | return status;
|
238 | } else {
|
239 | throw new Error('Cannot complete operation because this recorder is not ready to record.');
|
240 | }
|
241 | }
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 | getStatusAsync = async (): Promise<RecordingStatus> => {
|
248 |
|
249 | if (this._canRecord) {
|
250 | return this._performOperationAndHandleStatusAsync(() => ExponentAV.getAudioRecordingStatus());
|
251 | }
|
252 | const status = {
|
253 | canRecord: false,
|
254 | isRecording: false,
|
255 | isDoneRecording: this._isDoneRecording,
|
256 | durationMillis: this._finalDurationMillis,
|
257 | };
|
258 | this._callOnRecordingStatusUpdateForNewStatus(status);
|
259 | return status;
|
260 | };
|
261 |
|
262 | setOnRecordingStatusUpdate(onRecordingStatusUpdate: ((status: RecordingStatus) => void) | null) {
|
263 | this._onRecordingStatusUpdate = onRecordingStatusUpdate;
|
264 | if (onRecordingStatusUpdate == null) {
|
265 | this._disablePolling();
|
266 | } else {
|
267 | this._enablePollingIfNecessaryAndPossible();
|
268 | }
|
269 | this.getStatusAsync();
|
270 | }
|
271 |
|
272 | setProgressUpdateInterval(progressUpdateIntervalMillis: number) {
|
273 | this._progressUpdateIntervalMillis = progressUpdateIntervalMillis;
|
274 | this.getStatusAsync();
|
275 | }
|
276 |
|
277 | // Record API
|
278 |
|
279 | async prepareToRecordAsync(
|
280 | options: RecordingOptions = RECORDING_OPTIONS_PRESET_LOW_QUALITY
|
281 | ): Promise<RecordingStatus> {
|
282 | throwIfAudioIsDisabled();
|
283 |
|
284 | if (_recorderExists) {
|
285 | throw new Error('Only one Recording object can be prepared at a given time.');
|
286 | }
|
287 |
|
288 | if (this._isDoneRecording) {
|
289 | throw new Error('This Recording object is done recording; you must make a new one.');
|
290 | }
|
291 |
|
292 | if (!options || !options.android || !options.ios) {
|
293 | throw new Error(
|
294 | 'You must provide recording options for android and ios in order to prepare to record.'
|
295 | );
|
296 | }
|
297 |
|
298 | const extensionRegex = /^\.\w+$/;
|
299 | if (
|
300 | !options.android.extension ||
|
301 | !options.ios.extension ||
|
302 | !extensionRegex.test(options.android.extension) ||
|
303 | !extensionRegex.test(options.ios.extension)
|
304 | ) {
|
305 | throw new Error(`Your file extensions must match ${extensionRegex.toString()}.`);
|
306 | }
|
307 |
|
308 | if (!this._canRecord) {
|
309 | if (eventEmitter) {
|
310 | this._subscription = eventEmitter.addListener(
|
311 | 'Expo.Recording.recorderUnloaded',
|
312 | this._cleanupForUnloadedRecorder
|
313 | );
|
314 | }
|
315 |
|
316 | const {
|
317 | uri,
|
318 | status,
|
319 | }: {
|
320 | uri: string;
|
321 | // status is of type RecordingStatus, but without the canRecord field populated
|
322 | status: Pick<RecordingStatus, Exclude<keyof RecordingStatus, 'canRecord'>>;
|
323 | } = await ExponentAV.prepareAudioRecorder(options);
|
324 |
|
325 | _recorderExists = true;
|
326 | this._uri = uri;
|
327 | this._options = options;
|
328 | this._canRecord = true;
|
329 |
|
330 | const currentStatus = { ...status, canRecord: true };
|
331 | this._callOnRecordingStatusUpdateForNewStatus(currentStatus);
|
332 | this._enablePollingIfNecessaryAndPossible();
|
333 | return currentStatus;
|
334 | } else {
|
335 | throw new Error('This Recording object is already prepared to record.');
|
336 | }
|
337 | }
|
338 |
|
339 | async startAsync(): Promise<RecordingStatus> {
|
340 | return this._performOperationAndHandleStatusAsync(() => ExponentAV.startAudioRecording());
|
341 | }
|
342 |
|
343 | async pauseAsync(): Promise<RecordingStatus> {
|
344 | return this._performOperationAndHandleStatusAsync(() => ExponentAV.pauseAudioRecording());
|
345 | }
|
346 |
|
347 | async stopAndUnloadAsync(): Promise<RecordingStatus> {
|
348 | if (!this._canRecord) {
|
349 | if (this._isDoneRecording) {
|
350 | throw new Error('Cannot unload a Recording that has already been unloaded.');
|
351 | } else {
|
352 | throw new Error('Cannot unload a Recording that has not been prepared.');
|
353 | }
|
354 | }
|
355 | // We perform a separate native API call so that the state of the Recording can be updated with
|
356 | // the final duration of the recording. (We cast stopStatus as Object to appease Flow)
|
357 | const finalStatus = await ExponentAV.stopAudioRecording();
|
358 | await ExponentAV.unloadAudioRecorder();
|
359 | return this._cleanupForUnloadedRecorder(finalStatus);
|
360 | }
|
361 |
|
362 | // Read API
|
363 |
|
364 | getURI(): string | null {
|
365 | return this._uri;
|
366 | }
|
367 |
|
368 | async createNewLoadedSound(
|
369 | initialStatus: AVPlaybackStatusToSet = {},
|
370 | onPlaybackStatusUpdate: ((status: AVPlaybackStatus) => void) | null = null
|
371 | ): Promise<{ sound: Sound; status: AVPlaybackStatus }> {
|
372 | console.warn(
|
373 | `createNewLoadedSound is deprecated in favor of createNewLoadedSoundAsync, which has the same API aside from the method name`
|
374 | );
|
375 | return this.createNewLoadedSoundAsync(initialStatus, onPlaybackStatusUpdate);
|
376 | }
|
377 |
|
378 | async createNewLoadedSoundAsync(
|
379 | initialStatus: AVPlaybackStatusToSet = {},
|
380 | onPlaybackStatusUpdate: ((status: AVPlaybackStatus) => void) | null = null
|
381 | ): Promise<{ sound: Sound; status: AVPlaybackStatus }> {
|
382 | if (this._uri == null || !this._isDoneRecording) {
|
383 | throw new Error('Cannot create sound when the Recording has not finished!');
|
384 | }
|
385 | return Sound.createAsync(
|
386 |
|
387 | { uri: this._uri },
|
388 | initialStatus,
|
389 | onPlaybackStatusUpdate,
|
390 | false
|
391 | );
|
392 | }
|
393 | }
|
394 |
|
\ | No newline at end of file |