UNPKG

23.6 kBJavaScriptView Raw
1// @flow
2
3import { NativeModules } from 'react-native';
4
5import {
6 _DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS,
7 _COMMON_AV_PLAYBACK_METHODS,
8 _getURIAndFullInitialStatusForLoadAsync,
9 _throwErrorIfValuesOutOfBoundsInStatus,
10 _getUnloadedStatus,
11 type PlaybackSource,
12 type PlaybackStatus,
13 type PlaybackStatusToSet,
14} from './AV';
15
16export type RecordingOptions = {
17 android: {
18 extension: string,
19 outputFormat: number,
20 audioEncoder: number,
21 sampleRate?: number,
22 numberOfChannels?: number,
23 bitRate?: number,
24 maxFileSize?: number,
25 },
26 ios: {
27 extension: string,
28 outputFormat?: string | number,
29 audioQuality: number,
30 sampleRate: number,
31 numberOfChannels: number,
32 bitRate: number,
33 bitRateStrategy?: number,
34 bitDepthHint?: number,
35 linearPCMBitDepth?: number,
36 linearPCMIsBigEndian?: boolean,
37 linearPCMIsFloat?: boolean,
38 },
39};
40
41export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_DEFAULT = 0;
42export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_THREE_GPP = 1;
43export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG_4 = 2;
44export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AMR_NB = 3;
45export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AMR_WB = 4;
46export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AAC_ADIF = 5;
47export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AAC_ADTS = 6;
48export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_RTP_AVP = 7;
49export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG2TS = 8;
50export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_WEBM = 9;
51
52export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_DEFAULT = 0;
53export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AMR_NB = 1;
54export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AMR_WB = 2;
55export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC = 3;
56export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_HE_AAC = 4;
57export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC_ELD = 5;
58export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_VORBIS = 6;
59
60export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_LINEARPCM = 'lpcm';
61export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AC3 = 'ac-3';
62export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_60958AC3 = 'cac3';
63export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_APPLEIMA4 = 'ima4';
64export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC = 'aac ';
65export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4CELP = 'celp';
66export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4HVXC = 'hvxc';
67export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4TWINVQ = 'twvq';
68export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MACE3 = 'MAC3';
69export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MACE6 = 'MAC6';
70export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ULAW = 'ulaw';
71export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ALAW = 'alaw';
72export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_QDESIGN = 'QDMC';
73export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_QDESIGN2 = 'QDM2';
74export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_QUALCOMM = 'Qclp';
75export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEGLAYER1 = '.mp1';
76export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEGLAYER2 = '.mp2';
77export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEGLAYER3 = '.mp3';
78export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_APPLELOSSLESS = 'alac';
79export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_HE = 'aach';
80export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_LD = 'aacl';
81export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_ELD = 'aace';
82export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_ELD_SBR = 'aacf';
83export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_ELD_V2 = 'aacg';
84export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_HE_V2 = 'aacp';
85export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_SPATIAL = 'aacs';
86export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AMR = 'samr';
87export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AMR_WB = 'sawb';
88export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AUDIBLE = 'AUDB';
89export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ILBC = 'ilbc';
90export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_DVIINTELIMA = 0x6d730011;
91export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MICROSOFTGSM = 0x6d730031;
92export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AES3 = 'aes3';
93export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ENHANCEDAC3 = 'ec-3';
94
95export const RECORDING_OPTION_IOS_AUDIO_QUALITY_MIN = 0;
96export const RECORDING_OPTION_IOS_AUDIO_QUALITY_LOW = 0x20;
97export const RECORDING_OPTION_IOS_AUDIO_QUALITY_MEDIUM = 0x40;
98export const RECORDING_OPTION_IOS_AUDIO_QUALITY_HIGH = 0x60;
99export const RECORDING_OPTION_IOS_AUDIO_QUALITY_MAX = 0x7f;
100
101export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_CONSTANT = 0;
102export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_LONG_TERM_AVERAGE = 1;
103export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_VARIABLE_CONSTRAINED = 2;
104export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_VARIABLE = 3;
105
106// TODO : maybe make presets for music and speech, or lossy / lossless.
107
108export const RECORDING_OPTIONS_PRESET_HIGH_QUALITY: RecordingOptions = {
109 android: {
110 extension: '.m4a',
111 outputFormat: RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG_4,
112 audioEncoder: RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC,
113 sampleRate: 44100,
114 numberOfChannels: 2,
115 bitRate: 128000,
116 },
117 ios: {
118 extension: '.caf',
119 audioQuality: RECORDING_OPTION_IOS_AUDIO_QUALITY_MAX,
120 sampleRate: 44100,
121 numberOfChannels: 2,
122 bitRate: 128000,
123 linearPCMBitDepth: 16,
124 linearPCMIsBigEndian: false,
125 linearPCMIsFloat: false,
126 },
127};
128
129export const RECORDING_OPTIONS_PRESET_LOW_QUALITY: RecordingOptions = {
130 android: {
131 extension: '.3gp',
132 outputFormat: RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_THREE_GPP,
133 audioEncoder: RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AMR_NB,
134 sampleRate: 44100,
135 numberOfChannels: 2,
136 bitRate: 128000,
137 },
138 ios: {
139 extension: '.caf',
140 audioQuality: RECORDING_OPTION_IOS_AUDIO_QUALITY_MIN,
141 sampleRate: 44100,
142 numberOfChannels: 2,
143 bitRate: 128000,
144 linearPCMBitDepth: 16,
145 linearPCMIsBigEndian: false,
146 linearPCMIsFloat: false,
147 },
148};
149
150// TODO For consistency with PlaybackStatus, should we include progressUpdateIntervalMillis here as well?
151export type RecordingStatus =
152 | {
153 canRecord: false,
154 isDoneRecording: false,
155 }
156 | {
157 canRecord: true,
158 isRecording: boolean,
159 durationMillis: number,
160 }
161 | {
162 canRecord: false,
163 isDoneRecording: true,
164 durationMillis: number,
165 };
166
167export type AudioMode = {
168 allowsRecordingIOS: boolean,
169 interruptionModeIOS: number,
170 playsInSilentModeIOS: boolean,
171 interruptionModeAndroid: boolean,
172 shouldDuckAndroid: boolean,
173};
174
175export const INTERRUPTION_MODE_IOS_MIX_WITH_OTHERS = 0;
176export const INTERRUPTION_MODE_IOS_DO_NOT_MIX = 1;
177export const INTERRUPTION_MODE_IOS_DUCK_OTHERS = 2;
178
179export const INTERRUPTION_MODE_ANDROID_DO_NOT_MIX = 1;
180export const INTERRUPTION_MODE_ANDROID_DUCK_OTHERS = 2;
181
182let _enabled: boolean = true;
183let _recorderExists: boolean = false;
184const _DISABLED_ERROR: Error = new Error(
185 'Cannot complete operation because audio is not enabled.'
186);
187
188// Returns true if value is in validValues, and false if not.
189const _isValueValid = (value: any, validValues: Array<any>): boolean => {
190 return validValues.filter(validValue => validValue === value).length > 0;
191};
192
193// Returns array of missing keys in object. Returns an empty array if no missing keys are found.
194const _findMissingKeys = (
195 object: Object,
196 requiredKeys: Array<any>
197): Array<any> => {
198 return requiredKeys.filter(requiredKey => !(requiredKey in object));
199};
200
201export async function setIsEnabledAsync(value: boolean): Promise<void> {
202 _enabled = value;
203 await NativeModules.ExponentAV.setAudioIsEnabled(value);
204 // TODO : We immediately pause all players when disabled, but we do not resume all shouldPlay
205 // players when enabled. Perhaps for completeness we should allow this; the design of the
206 // enabling API is for people to enable / disable this audio library, but I think that it should
207 // intuitively also double as a global pause/resume.
208}
209
210export async function setAudioModeAsync(mode: AudioMode): Promise<void> {
211 const missingKeys = _findMissingKeys(mode, [
212 'allowsRecordingIOS',
213 'interruptionModeIOS',
214 'playsInSilentModeIOS',
215 'interruptionModeAndroid',
216 'shouldDuckAndroid',
217 ]);
218 if (missingKeys.length > 0) {
219 throw new Error(
220 `Audio mode attempted to be set without the required keys: ${JSON.stringify(
221 missingKeys
222 )}`
223 );
224 }
225 if (
226 !_isValueValid(mode.interruptionModeIOS, [
227 INTERRUPTION_MODE_IOS_MIX_WITH_OTHERS,
228 INTERRUPTION_MODE_IOS_DO_NOT_MIX,
229 INTERRUPTION_MODE_IOS_DUCK_OTHERS,
230 ])
231 ) {
232 throw new Error(`"interruptionModeIOS" was set to an invalid value.`);
233 }
234 if (
235 !_isValueValid(mode.interruptionModeAndroid, [
236 INTERRUPTION_MODE_ANDROID_DO_NOT_MIX,
237 INTERRUPTION_MODE_ANDROID_DUCK_OTHERS,
238 ])
239 ) {
240 throw new Error(`"interruptionModeAndroid" was set to an invalid value.`);
241 }
242 if (
243 typeof mode.allowsRecordingIOS !== 'boolean' ||
244 typeof mode.playsInSilentModeIOS !== 'boolean' ||
245 typeof mode.shouldDuckAndroid !== 'boolean'
246 ) {
247 throw new Error(
248 '"allowsRecordingIOS", "playsInSilentModeIOS", and "shouldDuckAndroid" must be booleans.'
249 );
250 }
251 await NativeModules.ExponentAV.setAudioMode(mode);
252}
253
254export class Sound {
255 _loaded: boolean;
256 _loading: boolean;
257 _key: number;
258 _onPlaybackStatusUpdate: ?(status: PlaybackStatus) => void;
259
260 constructor() {
261 this._loaded = false;
262 this._loading = false;
263 this._key = -1;
264 this._onPlaybackStatusUpdate = null;
265 }
266
267 static create = async (
268 source: PlaybackSource,
269 initialStatus: PlaybackStatusToSet = {},
270 onPlaybackStatusUpdate: ?(status: PlaybackStatus) => void = null,
271 downloadFirst: boolean = true
272 ): Promise<{ sound: Sound, status: PlaybackStatus }> => {
273 const sound: Sound = new Sound();
274 sound.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);
275 const status: PlaybackStatus = await sound.loadAsync(
276 source,
277 initialStatus,
278 downloadFirst
279 );
280 return { sound, status };
281 };
282
283 // Internal methods
284
285 _callOnPlaybackStatusUpdateForNewStatus(status: PlaybackStatus) {
286 if (this._onPlaybackStatusUpdate != null) {
287 this._onPlaybackStatusUpdate(status);
288 }
289 }
290
291 async _performOperationAndHandleStatusAsync(
292 operation: () => Promise<PlaybackStatus>
293 ): Promise<PlaybackStatus> {
294 if (!_enabled) {
295 throw _DISABLED_ERROR;
296 }
297 if (this._loaded) {
298 const status = await operation();
299 this._callOnPlaybackStatusUpdateForNewStatus(status);
300 return status;
301 } else {
302 throw new Error('Cannot complete operation because sound is not loaded.');
303 }
304 }
305
306 _internalStatusUpdateCallback = (status: PlaybackStatus) => {
307 this._callOnPlaybackStatusUpdateForNewStatus(status);
308 this._setInternalStatusUpdateCallback(); // Callbacks are only called once and then released.
309 };
310
311 // TODO: We can optimize by only using time observer on native if (this._onPlaybackStatusUpdate).
312 _setInternalStatusUpdateCallback() {
313 if (this._loaded) {
314 NativeModules.ExponentAV.setStatusUpdateCallbackForSound(
315 this._key,
316 this._internalStatusUpdateCallback
317 );
318 }
319 }
320
321 _errorCallback = (error: string) => {
322 this._loaded = false;
323 this._key = -1;
324 this._callOnPlaybackStatusUpdateForNewStatus(_getUnloadedStatus(error));
325 };
326
327 // ### Unified playback API ### (consistent with Video.js)
328 // All calls automatically call onPlaybackStatusUpdate as a side effect.
329
330 // Get status API
331
332 getStatusAsync = async (): Promise<PlaybackStatus> => {
333 if (this._loaded) {
334 return this._performOperationAndHandleStatusAsync(() =>
335 NativeModules.ExponentAV.getStatusForSound(this._key)
336 );
337 }
338 const status: PlaybackStatus = _getUnloadedStatus();
339 this._callOnPlaybackStatusUpdateForNewStatus(status);
340 return status;
341 };
342
343 setOnPlaybackStatusUpdate(
344 onPlaybackStatusUpdate: ?(status: PlaybackStatus) => void
345 ) {
346 this._onPlaybackStatusUpdate = onPlaybackStatusUpdate;
347 this.getStatusAsync();
348 }
349
350 // DEPRECATED -- WILL BE REMOVED IN SDK21:
351 setCallback = (callback: ?(status: PlaybackStatus) => void) => {
352 console.warn(
353 `'Sound.setCallback()' is deprecated and will be removed in SDK21. Use 'Sound.setOnPlaybackStatusUpdate()' instead.`
354 );
355 this.setOnPlaybackStatusUpdate(callback);
356 };
357
358 // Loading / unloading API
359
360 async loadAsync(
361 source: PlaybackSource,
362 initialStatus: PlaybackStatusToSet = {},
363 downloadFirst: boolean = true
364 ): Promise<PlaybackStatus> {
365 if (!_enabled) {
366 throw _DISABLED_ERROR;
367 }
368 if (this.loading) {
369 throw new Error('The Sound is already loading.');
370 }
371 if (!this._loaded) {
372 this._loading = true;
373
374 const {
375 uri,
376 fullInitialStatus,
377 } = await _getURIAndFullInitialStatusForLoadAsync(
378 source,
379 initialStatus,
380 downloadFirst
381 );
382
383 // This is a workaround, since using load with resolve / reject seems to not work.
384 return new Promise(
385 function(resolve, reject) {
386 const loadSuccess = (key: number, status: PlaybackStatus) => {
387 this._key = key;
388 this._loaded = true;
389 this._loading = false;
390 NativeModules.ExponentAV.setErrorCallbackForSound(
391 this._key,
392 this._errorCallback
393 );
394 this._setInternalStatusUpdateCallback();
395 this._callOnPlaybackStatusUpdateForNewStatus(status);
396 resolve(status);
397 };
398 const loadError = (error: string) => {
399 this._loading = false;
400 reject(new Error(error));
401 };
402 NativeModules.ExponentAV.loadForSound(
403 uri,
404 fullInitialStatus,
405 loadSuccess,
406 loadError
407 );
408 }.bind(this)
409 );
410 } else {
411 throw new Error('The Sound is already loaded.');
412 }
413 }
414
415 async unloadAsync(): Promise<PlaybackStatus> {
416 if (this._loaded) {
417 this._loaded = false;
418 const key = this._key;
419 this._key = -1;
420 const status = await NativeModules.ExponentAV.unloadForSound(key);
421 this._callOnPlaybackStatusUpdateForNewStatus(status);
422 return status;
423 } else {
424 return this.getStatusAsync(); // Automatically calls onPlaybackStatusUpdate.
425 }
426 }
427
428 // Set status API (only available while isLoaded = true)
429
430 async setStatusAsync(status: PlaybackStatusToSet): Promise<PlaybackStatus> {
431 _throwErrorIfValuesOutOfBoundsInStatus(status);
432 return this._performOperationAndHandleStatusAsync(() =>
433 NativeModules.ExponentAV.setStatusForSound(this._key, status)
434 );
435 }
436
437 // Additional convenience methods on top of setStatusAsync are set via _COMMON_AV_PLAYBACK_METHODS.
438 playAsync: () => Promise<PlaybackStatus>;
439 playFromPositionAsync: (positionMillis: number) => Promise<PlaybackStatus>;
440 pauseAsync: () => Promise<PlaybackStatus>;
441 stopAsync: () => Promise<PlaybackStatus>;
442 setPositionAsync: (positionMillis: number) => Promise<PlaybackStatus>;
443 setRateAsync: (
444 rate: number,
445 shouldCorrectPitch: boolean
446 ) => Promise<PlaybackStatus>;
447 setVolumeAsync: (volume: number) => Promise<PlaybackStatus>;
448 setIsMutedAsync: (isMuted: boolean) => Promise<PlaybackStatus>;
449 setIsLoopingAsync: (isLooping: boolean) => Promise<PlaybackStatus>;
450 setProgressUpdateIntervalAsync: (
451 progressUpdateIntervalMillis: number
452 ) => Promise<PlaybackStatus>;
453}
454
455Object.assign(Sound.prototype, _COMMON_AV_PLAYBACK_METHODS);
456
457export class Recording {
458 _canRecord: boolean;
459 _isDoneRecording: boolean;
460 _finalDurationMillis: number;
461 _uri: ?string;
462 _onRecordingStatusUpdate: ?(status: RecordingStatus) => void;
463 _progressUpdateTimeoutVariable: ?number;
464 _progressUpdateIntervalMillis: number;
465 _options: ?RecordingOptions;
466
467 constructor() {
468 this._canRecord = false;
469 this._isDoneRecording = false;
470 this._finalDurationMillis = 0;
471 this._uri = null;
472 this._progressUpdateTimeoutVariable = null;
473 this._progressUpdateIntervalMillis = _DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS;
474 this._options = null;
475 }
476
477 // Internal methods
478
479 _cleanupForUnloadedRecorder = async (finalStatus: RecordingStatus) => {
480 this._canRecord = false;
481 this._isDoneRecording = true;
482 // $FlowFixMe(greg): durationMillis is not always defined
483 this._finalDurationMillis = finalStatus.durationMillis;
484 _recorderExists = false;
485 if (NativeModules.ExponentAV.setUnloadedCallbackForAndroidRecording) {
486 NativeModules.ExponentAV.setUnloadedCallbackForAndroidRecording(null);
487 }
488 this._disablePolling();
489 return await this.getStatusAsync(); // Automatically calls onRecordingStatusUpdate for the final state.
490 };
491
492 _pollingLoop = async () => {
493 if (_enabled && this._canRecord && this._onRecordingStatusUpdate != null) {
494 this._progressUpdateTimeoutVariable = setTimeout(
495 this._pollingLoop,
496 this._progressUpdateIntervalMillis
497 );
498 try {
499 await this.getStatusAsync();
500 } catch (error) {
501 this._disablePolling();
502 }
503 }
504 };
505
506 _disablePolling() {
507 if (this._progressUpdateTimeoutVariable != null) {
508 clearTimeout(this._progressUpdateTimeoutVariable);
509 this._progressUpdateTimeoutVariable = null;
510 }
511 }
512
513 _enablePollingIfNecessaryAndPossible() {
514 if (_enabled && this._canRecord && this._onRecordingStatusUpdate != null) {
515 this._disablePolling();
516 this._pollingLoop();
517 }
518 }
519
520 _callOnRecordingStatusUpdateForNewStatus(status: RecordingStatus) {
521 if (this._onRecordingStatusUpdate != null) {
522 this._onRecordingStatusUpdate(status);
523 }
524 }
525
526 async _performOperationAndHandleStatusAsync(
527 operation: () => Promise<RecordingStatus>
528 ): Promise<RecordingStatus> {
529 if (!_enabled) {
530 throw _DISABLED_ERROR;
531 }
532 if (this._canRecord) {
533 const status = await operation();
534 this._callOnRecordingStatusUpdateForNewStatus(status);
535 return status;
536 } else {
537 throw new Error(
538 'Cannot complete operation because this recorder is not ready to record.'
539 );
540 }
541 }
542
543 // Note that all calls automatically call onRecordingStatusUpdate as a side effect.
544
545 // Get status API
546
547 getStatusAsync = async (): Promise<RecordingStatus> => {
548 // Automatically calls onRecordingStatusUpdate.
549 if (this._canRecord) {
550 return this._performOperationAndHandleStatusAsync(() =>
551 NativeModules.ExponentAV.getAudioRecordingStatus()
552 );
553 }
554 const status: RecordingStatus = this._isDoneRecording
555 ? {
556 canRecord: false,
557 isDoneRecording: true,
558 durationMillis: this._finalDurationMillis,
559 }
560 : {
561 canRecord: false,
562 isDoneRecording: false,
563 };
564 this._callOnRecordingStatusUpdateForNewStatus(status);
565 return status;
566 };
567
568 setOnRecordingStatusUpdate(
569 onRecordingStatusUpdate: ?(status: RecordingStatus) => void
570 ) {
571 this._onRecordingStatusUpdate = onRecordingStatusUpdate;
572 if (onRecordingStatusUpdate == null) {
573 this._disablePolling();
574 } else {
575 this._enablePollingIfNecessaryAndPossible();
576 }
577 this.getStatusAsync();
578 }
579
580 // DEPRECATED -- WILL BE REMOVED IN SDK21:
581 setCallback(callback: ?(status: RecordingStatus) => void) {
582 // DEPRECATED -- WILL BE REMOVED IN SDK21:
583 console.warn(
584 `'Recording.setCallback()' is deprecated and will be removed in SDK21. Use 'Recording.setOnRecordingStatusUpdate()' instead.`
585 );
586 this.setOnRecordingStatusUpdate(callback);
587 }
588
589 setProgressUpdateInterval(progressUpdateIntervalMillis: number) {
590 this._progressUpdateIntervalMillis = progressUpdateIntervalMillis;
591 this.getStatusAsync();
592 }
593
594 // Record API
595
596 async prepareToRecordAsync(
597 options: RecordingOptions = RECORDING_OPTIONS_PRESET_LOW_QUALITY
598 ): Promise<RecordingStatus> {
599 if (!_enabled) {
600 throw _DISABLED_ERROR;
601 }
602
603 if (_recorderExists) {
604 throw new Error(
605 'Only one Recording object can be prepared at a given time.'
606 );
607 }
608
609 if (this._isDoneRecording) {
610 throw new Error(
611 'This Recording object is done recording; you must make a new one.'
612 );
613 }
614
615 if (!options || !options.android || !options.ios) {
616 throw new Error(
617 'You must provide recording options for android and ios in order to prepare to record.'
618 );
619 }
620
621 const extensionRegex = /^\.\w+$/;
622 if (
623 !options.android.extension ||
624 !options.ios.extension ||
625 !extensionRegex.test(options.android.extension) ||
626 !extensionRegex.test(options.ios.extension)
627 ) {
628 throw new Error(
629 `Your file extensions must match ${extensionRegex.toString()}.`
630 );
631 }
632
633 if (!this._canRecord) {
634 if (NativeModules.ExponentAV.setUnloadedCallbackForAndroidRecording) {
635 NativeModules.ExponentAV.setUnloadedCallbackForAndroidRecording(
636 this._cleanupForUnloadedRecorder
637 );
638 }
639
640 const {
641 uri,
642 status,
643 }: {
644 uri: string,
645 status: Object, // status is of type RecordingStatus, but without the canRecord field populated.
646 } = await NativeModules.ExponentAV.prepareAudioRecorder(options);
647 _recorderExists = true;
648 this._uri = uri;
649 this._options = options;
650 this._canRecord = true;
651 this._callOnRecordingStatusUpdateForNewStatus(status);
652 this._enablePollingIfNecessaryAndPossible();
653 return status;
654 } else {
655 throw new Error('This Recording object is already prepared to record.');
656 }
657 }
658
659 async startAsync(): Promise<RecordingStatus> {
660 return this._performOperationAndHandleStatusAsync(() =>
661 NativeModules.ExponentAV.startAudioRecording()
662 );
663 }
664
665 async pauseAsync(): Promise<RecordingStatus> {
666 return this._performOperationAndHandleStatusAsync(() =>
667 NativeModules.ExponentAV.pauseAudioRecording()
668 );
669 }
670
671 async stopAndUnloadAsync(): Promise<RecordingStatus> {
672 if (!this._canRecord) {
673 if (this._isDoneRecording) {
674 throw new Error(
675 'Cannot unload a Recording that has already been unloaded.'
676 );
677 } else {
678 throw new Error(
679 'Cannot unload a Recording that has not been prepared.'
680 );
681 }
682 }
683 // We perform a separate native API call so that the state of the Recording can be updated with
684 // the final duration of the recording. (We cast stopStatus as Object to appease Flow)
685 const finalStatus: Object = await NativeModules.ExponentAV.stopAudioRecording();
686 await NativeModules.ExponentAV.unloadAudioRecorder();
687 return this._cleanupForUnloadedRecorder(finalStatus);
688 }
689
690 // Read API
691
692 getURI(): ?string {
693 return this._uri;
694 }
695
696 async createNewLoadedSound(
697 initialStatus: PlaybackStatusToSet = {},
698 onPlaybackStatusUpdate: ?(status: PlaybackStatus) => void = null
699 ): Promise<{ sound: Sound, status: PlaybackStatus }> {
700 if (this._uri == null || !this._isDoneRecording) {
701 throw new Error(
702 'Cannot create sound when the Recording has not finished!'
703 );
704 }
705 return Sound.create(
706 // $FlowFixMe: Flow can't distinguish between this literal and Asset
707 { uri: this._uri },
708 initialStatus,
709 onPlaybackStatusUpdate,
710 false
711 );
712 }
713}