UNPKG

14.4 kBPlain TextView Raw
1import { EventEmitter, Subscription, Platform } from '@unimodules/core';
2import { PermissionResponse, PermissionStatus } from 'unimodules-permissions-interface';
3
4import {
5 _DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS,
6 AVPlaybackStatus,
7 AVPlaybackStatusToSet,
8} from '../AV';
9import ExponentAV from '../ExponentAV';
10import { isAudioEnabled, throwIfAudioIsDisabled } from './AudioAvailability';
11import { Sound } from './Sound';
12
13export 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// TODO: consider changing these to enums
39export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_DEFAULT = 0;
40export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_THREE_GPP = 1;
41export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG_4 = 2;
42export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AMR_NB = 3;
43export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AMR_WB = 4;
44export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AAC_ADIF = 5;
45export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AAC_ADTS = 6;
46export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_RTP_AVP = 7;
47export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG2TS = 8;
48export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_WEBM = 9;
49
50export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_DEFAULT = 0;
51export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AMR_NB = 1;
52export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AMR_WB = 2;
53export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC = 3;
54export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_HE_AAC = 4;
55export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC_ELD = 5;
56
57export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_LINEARPCM = 'lpcm';
58export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AC3 = 'ac-3';
59export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_60958AC3 = 'cac3';
60export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_APPLEIMA4 = 'ima4';
61export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC = 'aac ';
62export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4CELP = 'celp';
63export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4HVXC = 'hvxc';
64export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4TWINVQ = 'twvq';
65export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MACE3 = 'MAC3';
66export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MACE6 = 'MAC6';
67export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ULAW = 'ulaw';
68export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ALAW = 'alaw';
69export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_QDESIGN = 'QDMC';
70export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_QDESIGN2 = 'QDM2';
71export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_QUALCOMM = 'Qclp';
72export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEGLAYER1 = '.mp1';
73export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEGLAYER2 = '.mp2';
74export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEGLAYER3 = '.mp3';
75export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_APPLELOSSLESS = 'alac';
76export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_HE = 'aach';
77export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_LD = 'aacl';
78export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_ELD = 'aace';
79export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_ELD_SBR = 'aacf';
80export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_ELD_V2 = 'aacg';
81export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_HE_V2 = 'aacp';
82export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_SPATIAL = 'aacs';
83export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AMR = 'samr';
84export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AMR_WB = 'sawb';
85export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AUDIBLE = 'AUDB';
86export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ILBC = 'ilbc';
87export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_DVIINTELIMA = 0x6d730011;
88export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MICROSOFTGSM = 0x6d730031;
89export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AES3 = 'aes3';
90export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ENHANCEDAC3 = 'ec-3';
91
92export const RECORDING_OPTION_IOS_AUDIO_QUALITY_MIN = 0;
93export const RECORDING_OPTION_IOS_AUDIO_QUALITY_LOW = 0x20;
94export const RECORDING_OPTION_IOS_AUDIO_QUALITY_MEDIUM = 0x40;
95export const RECORDING_OPTION_IOS_AUDIO_QUALITY_HIGH = 0x60;
96export const RECORDING_OPTION_IOS_AUDIO_QUALITY_MAX = 0x7f;
97
98export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_CONSTANT = 0;
99export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_LONG_TERM_AVERAGE = 1;
100export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_VARIABLE_CONSTRAINED = 2;
101export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_VARIABLE = 3;
102
103// TODO : maybe make presets for music and speech, or lossy / lossless.
104
105export 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
126export 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// TODO: For consistency with PlaybackStatus, should we include progressUpdateIntervalMillis here as
148// well?
149export type RecordingStatus = {
150 canRecord: boolean;
151 isRecording: boolean;
152 isDoneRecording: boolean;
153 durationMillis: number;
154};
155
156export { PermissionResponse, PermissionStatus };
157
158let _recorderExists: boolean = false;
159const eventEmitter = Platform.OS === 'android' ? new EventEmitter(ExponentAV) : null;
160
161export async function getPermissionsAsync(): Promise<PermissionResponse> {
162 return ExponentAV.getPermissionsAsync();
163}
164
165export async function requestPermissionsAsync(): Promise<PermissionResponse> {
166 return ExponentAV.requestPermissionsAsync();
167}
168
169export 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 // $FlowFixMe(greg): durationMillis is not always defined
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(); // Automatically calls onRecordingStatusUpdate for the final state.
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 // Note that all calls automatically call onRecordingStatusUpdate as a side effect.
244
245 // Get status API
246
247 getStatusAsync = async (): Promise<RecordingStatus> => {
248 // Automatically calls onRecordingStatusUpdate.
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 // $FlowFixMe: Flow can't distinguish between this literal and Asset
387 { uri: this._uri },
388 initialStatus,
389 onPlaybackStatusUpdate,
390 false
391 );
392 }
393}
394
\No newline at end of file