xref: /expo/packages/expo-av/build/Audio/Recording.js (revision 8a424beb)
1import { PermissionStatus, createPermissionHook, EventEmitter, Platform, } from 'expo-modules-core';
2import { isAudioEnabled, throwIfAudioIsDisabled } from './AudioAvailability';
3import { RecordingOptionsPresets } from './RecordingConstants';
4import { Sound } from './Sound';
5import { _DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS, } from '../AV';
6import ExponentAV from '../ExponentAV';
7let _recorderExists = false;
8const eventEmitter = Platform.OS === 'android' ? new EventEmitter(ExponentAV) : null;
9/**
10 * Checks user's permissions for audio recording.
11 * @return A promise that resolves to an object of type `PermissionResponse`.
12 */
13export async function getPermissionsAsync() {
14    return ExponentAV.getPermissionsAsync();
15}
16/**
17 * Asks the user to grant permissions for audio recording.
18 * @return A promise that resolves to an object of type `PermissionResponse`.
19 */
20export async function requestPermissionsAsync() {
21    return ExponentAV.requestPermissionsAsync();
22}
23/**
24 * Check or request permissions to record audio.
25 * This uses both `requestPermissionAsync` and `getPermissionsAsync` to interact with the permissions.
26 *
27 * @example
28 * ```ts
29 * const [permissionResponse, requestPermission] = Audio.usePermissions();
30 * ```
31 */
32export const usePermissions = createPermissionHook({
33    getMethod: getPermissionsAsync,
34    requestMethod: requestPermissionsAsync,
35});
36// @needsAudit
37/**
38 * This class represents an audio recording. After creating an instance of this class, `prepareToRecordAsync`
39 * must be called in order to record audio. Once recording is finished, call `stopAndUnloadAsync`. Note that
40 * only one recorder is allowed to exist in the state between `prepareToRecordAsync` and `stopAndUnloadAsync`
41 * at any given time.
42 *
43 * Note that your experience must request audio recording permissions in order for recording to function.
44 * See the [`Permissions` module](/guides/permissions) for more details.
45 *
46 * Additionally, audio recording is [not supported in the iOS Simulator](/workflow/ios-simulator/#limitations).
47 *
48 * @example
49 * ```ts
50 * const recording = new Audio.Recording();
51 * try {
52 *   await recording.prepareToRecordAsync(Audio.RecordingOptionsPresets.HIGH_QUALITY);
53 *   await recording.startAsync();
54 *   // You are now recording!
55 * } catch (error) {
56 *   // An error occurred!
57 * }
58 * ```
59 *
60 * @return A newly constructed instance of `Audio.Recording`.
61 */
62export class Recording {
63    _subscription = null;
64    _canRecord = false;
65    _isDoneRecording = false;
66    _finalDurationMillis = 0;
67    _uri = null;
68    _onRecordingStatusUpdate = null;
69    _progressUpdateTimeoutVariable = null;
70    _progressUpdateIntervalMillis = _DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS;
71    _options = null;
72    // Internal methods
73    _cleanupForUnloadedRecorder = async (finalStatus) => {
74        this._canRecord = false;
75        this._isDoneRecording = true;
76        this._finalDurationMillis = finalStatus?.durationMillis ?? 0;
77        _recorderExists = false;
78        if (this._subscription) {
79            this._subscription.remove();
80            this._subscription = null;
81        }
82        this._disablePolling();
83        return await this.getStatusAsync(); // Automatically calls onRecordingStatusUpdate for the final state.
84    };
85    _pollingLoop = async () => {
86        if (isAudioEnabled() && this._canRecord && this._onRecordingStatusUpdate != null) {
87            this._progressUpdateTimeoutVariable = setTimeout(this._pollingLoop, this._progressUpdateIntervalMillis);
88            try {
89                await this.getStatusAsync();
90            }
91            catch {
92                this._disablePolling();
93            }
94        }
95    };
96    _disablePolling() {
97        if (this._progressUpdateTimeoutVariable != null) {
98            clearTimeout(this._progressUpdateTimeoutVariable);
99            this._progressUpdateTimeoutVariable = null;
100        }
101    }
102    _enablePollingIfNecessaryAndPossible() {
103        if (isAudioEnabled() && this._canRecord && this._onRecordingStatusUpdate != null) {
104            this._disablePolling();
105            this._pollingLoop();
106        }
107    }
108    _callOnRecordingStatusUpdateForNewStatus(status) {
109        if (this._onRecordingStatusUpdate != null) {
110            this._onRecordingStatusUpdate(status);
111        }
112    }
113    async _performOperationAndHandleStatusAsync(operation) {
114        throwIfAudioIsDisabled();
115        if (this._canRecord) {
116            const status = await operation();
117            this._callOnRecordingStatusUpdateForNewStatus(status);
118            return status;
119        }
120        else {
121            throw new Error('Cannot complete operation because this recorder is not ready to record.');
122        }
123    }
124    /**
125     * Creates and starts a recording using the given options, with optional `onRecordingStatusUpdate` and `progressUpdateIntervalMillis`.
126     *
127     * ```ts
128     * const { recording, status } = await Audio.Recording.createAsync(
129     *   options,
130     *   onRecordingStatusUpdate,
131     *   progressUpdateIntervalMillis
132     * );
133     *
134     * // Which is equivalent to the following:
135     * const recording = new Audio.Recording();
136     * await recording.prepareToRecordAsync(options);
137     * recording.setOnRecordingStatusUpdate(onRecordingStatusUpdate);
138     * await recording.startAsync();
139     * ```
140     *
141     * @param options Options for the recording, including sample rate, bitrate, channels, format, encoder, and extension. If no options are passed to,
142     * the recorder will be created with options `Audio.RecordingOptionsPresets.LOW_QUALITY`. See below for details on `RecordingOptions`.
143     * @param onRecordingStatusUpdate A function taking a single parameter `status` (a dictionary, described in `getStatusAsync`).
144     * @param progressUpdateIntervalMillis The interval between calls of `onRecordingStatusUpdate`. This value defaults to 500 milliseconds.
145     *
146     * @example
147     * ```ts
148     * try {
149     *   const { recording: recordingObject, status } = await Audio.Recording.createAsync(
150     *     Audio.RecordingOptionsPresets.HIGH_QUALITY
151     *   );
152     *   // You are now recording!
153     * } catch (error) {
154     *   // An error occurred!
155     * }
156     * ```
157     *
158     * @return A `Promise` that is rejected if creation failed, or fulfilled with the following dictionary if creation succeeded.
159     */
160    static createAsync = async (options = RecordingOptionsPresets.LOW_QUALITY, onRecordingStatusUpdate = null, progressUpdateIntervalMillis = null) => {
161        const recording = new Recording();
162        if (progressUpdateIntervalMillis) {
163            recording._progressUpdateIntervalMillis = progressUpdateIntervalMillis;
164        }
165        recording.setOnRecordingStatusUpdate(onRecordingStatusUpdate);
166        await recording.prepareToRecordAsync({
167            ...options,
168            keepAudioActiveHint: true,
169        });
170        try {
171            const status = await recording.startAsync();
172            return { recording, status };
173        }
174        catch (err) {
175            recording.stopAndUnloadAsync();
176            throw err;
177        }
178    };
179    // Get status API
180    /**
181     * Gets the `status` of the `Recording`.
182     * @return A `Promise` that is resolved with the `RecordingStatus` object.
183     */
184    getStatusAsync = async () => {
185        // Automatically calls onRecordingStatusUpdate.
186        if (this._canRecord) {
187            return this._performOperationAndHandleStatusAsync(() => ExponentAV.getAudioRecordingStatus());
188        }
189        const status = {
190            canRecord: false,
191            isRecording: false,
192            isDoneRecording: this._isDoneRecording,
193            durationMillis: this._finalDurationMillis,
194        };
195        this._callOnRecordingStatusUpdateForNewStatus(status);
196        return status;
197    };
198    /**
199     * Sets a function to be called regularly with the `RecordingStatus` of the `Recording`.
200     *
201     * `onRecordingStatusUpdate` will be called when another call to the API for this recording completes (such as `prepareToRecordAsync()`,
202     * `startAsync()`, `getStatusAsync()`, or `stopAndUnloadAsync()`), and will also be called at regular intervals while the recording can record.
203     * Call `setProgressUpdateInterval()` to modify the interval with which `onRecordingStatusUpdate` is called while the recording can record.
204     *
205     * @param onRecordingStatusUpdate A function taking a single parameter `RecordingStatus`.
206     */
207    setOnRecordingStatusUpdate(onRecordingStatusUpdate) {
208        this._onRecordingStatusUpdate = onRecordingStatusUpdate;
209        if (onRecordingStatusUpdate == null) {
210            this._disablePolling();
211        }
212        else {
213            this._enablePollingIfNecessaryAndPossible();
214        }
215        this.getStatusAsync();
216    }
217    /**
218     * Sets the interval with which `onRecordingStatusUpdate` is called while the recording can record.
219     * See `setOnRecordingStatusUpdate` for details. This value defaults to 500 milliseconds.
220     * @param progressUpdateIntervalMillis The new interval between calls of `onRecordingStatusUpdate`.
221     */
222    setProgressUpdateInterval(progressUpdateIntervalMillis) {
223        this._progressUpdateIntervalMillis = progressUpdateIntervalMillis;
224        this.getStatusAsync();
225    }
226    // Record API
227    /**
228     * Loads the recorder into memory and prepares it for recording. This must be called before calling `startAsync()`.
229     * This method can only be called if the `Recording` instance has never yet been prepared.
230     *
231     * @param options `RecordingOptions` for the recording, including sample rate, bitrate, channels, format, encoder, and extension.
232     * If no options are passed to `prepareToRecordAsync()`, the recorder will be created with options `Audio.RecordingOptionsPresets.LOW_QUALITY`.
233     *
234     * @return A `Promise` that is fulfilled when the recorder is loaded and prepared, or rejects if this failed. If another `Recording` exists
235     * in your experience that is currently prepared to record, the `Promise` will reject. If the `RecordingOptions` provided are invalid,
236     * the `Promise` will also reject. The promise is resolved with the `RecordingStatus` of the recording.
237     */
238    async prepareToRecordAsync(options = RecordingOptionsPresets.LOW_QUALITY) {
239        throwIfAudioIsDisabled();
240        if (_recorderExists) {
241            throw new Error('Only one Recording object can be prepared at a given time.');
242        }
243        if (this._isDoneRecording) {
244            throw new Error('This Recording object is done recording; you must make a new one.');
245        }
246        if (!options || !options.android || !options.ios) {
247            throw new Error('You must provide recording options for android and ios in order to prepare to record.');
248        }
249        const extensionRegex = /^\.\w+$/;
250        if (!options.android.extension ||
251            !options.ios.extension ||
252            !extensionRegex.test(options.android.extension) ||
253            !extensionRegex.test(options.ios.extension)) {
254            throw new Error(`Your file extensions must match ${extensionRegex.toString()}.`);
255        }
256        if (!this._canRecord) {
257            if (eventEmitter) {
258                this._subscription = eventEmitter.addListener('Expo.Recording.recorderUnloaded', this._cleanupForUnloadedRecorder);
259            }
260            const { uri, status, } = await ExponentAV.prepareAudioRecorder(options);
261            _recorderExists = true;
262            this._uri = uri;
263            this._options = options;
264            this._canRecord = true;
265            const currentStatus = { ...status, canRecord: true };
266            this._callOnRecordingStatusUpdateForNewStatus(currentStatus);
267            this._enablePollingIfNecessaryAndPossible();
268            return currentStatus;
269        }
270        else {
271            throw new Error('This Recording object is already prepared to record.');
272        }
273    }
274    /**
275     * Returns a list of available recording inputs. This method can only be called if the `Recording` has been prepared.
276     * @return A `Promise` that is fulfilled with an array of `RecordingInput` objects.
277     */
278    async getAvailableInputs() {
279        return ExponentAV.getAvailableInputs();
280    }
281    /**
282     * Returns the currently-selected recording input. This method can only be called if the `Recording` has been prepared.
283     * @return A `Promise` that is fulfilled with a `RecordingInput` object.
284     */
285    async getCurrentInput() {
286        return ExponentAV.getCurrentInput();
287    }
288    /**
289     * Sets the current recording input.
290     * @param inputUid The uid of a `RecordingInput`.
291     * @return A `Promise` that is resolved if successful or rejected if not.
292     */
293    async setInput(inputUid) {
294        return ExponentAV.setInput(inputUid);
295    }
296    /**
297     * Begins recording. This method can only be called if the `Recording` has been prepared.
298     * @return A `Promise` that is fulfilled when recording has begun, or rejects if recording could not be started.
299     * The promise is resolved with the `RecordingStatus` of the recording.
300     */
301    async startAsync() {
302        return this._performOperationAndHandleStatusAsync(() => ExponentAV.startAudioRecording());
303    }
304    /**
305     * Pauses recording. This method can only be called if the `Recording` has been prepared.
306     *
307     * > This is only available on Android API version 24 and later.
308     *
309     * @return A `Promise` that is fulfilled when recording has paused, or rejects if recording could not be paused.
310     * If the Android API version is less than 24, the `Promise` will reject. The promise is resolved with the
311     * `RecordingStatus` of the recording.
312     */
313    async pauseAsync() {
314        return this._performOperationAndHandleStatusAsync(() => ExponentAV.pauseAudioRecording());
315    }
316    /**
317     * Stops the recording and deallocates the recorder from memory. This reverts the `Recording` instance
318     * to an unprepared state, and another `Recording` instance must be created in order to record again.
319     * This method can only be called if the `Recording` has been prepared.
320     *
321     * > On Android this method may fail with `E_AUDIO_NODATA` when called too soon after `startAsync` and
322     * > no audio data has been recorded yet. In that case the recorded file will be invalid and should be discarded.
323     *
324     * @return A `Promise` that is fulfilled when recording has stopped, or rejects if recording could not be stopped.
325     * The promise is resolved with the `RecordingStatus` of the recording.
326     */
327    async stopAndUnloadAsync() {
328        if (!this._canRecord) {
329            if (this._isDoneRecording) {
330                throw new Error('Cannot unload a Recording that has already been unloaded.');
331            }
332            else {
333                throw new Error('Cannot unload a Recording that has not been prepared.');
334            }
335        }
336        // We perform a separate native API call so that the state of the Recording can be updated with
337        // the final duration of the recording. (We cast stopStatus as Object to appease Flow)
338        let stopResult;
339        let stopError;
340        try {
341            stopResult = await ExponentAV.stopAudioRecording();
342        }
343        catch (err) {
344            stopError = err;
345        }
346        // Web has to return the URI at the end of recording, so needs a little destructuring
347        if (Platform.OS === 'web' && stopResult?.uri !== undefined) {
348            this._uri = stopResult.uri;
349        }
350        // Clean-up and return status
351        await ExponentAV.unloadAudioRecorder();
352        const status = await this._cleanupForUnloadedRecorder(stopResult);
353        return stopError ? Promise.reject(stopError) : status;
354    }
355    // Read API
356    /**
357     * Gets the local URI of the `Recording`. Note that this will only succeed once the `Recording` is prepared
358     * to record. On web, this will not return the URI until the recording is finished.
359     * @return A `string` with the local URI of the `Recording`, or `null` if the `Recording` is not prepared
360     * to record (or, on Web, if the recording has not finished).
361     */
362    getURI() {
363        return this._uri;
364    }
365    /**
366     * @deprecated Use `createNewLoadedSoundAsync()` instead.
367     */
368    async createNewLoadedSound(initialStatus = {}, onPlaybackStatusUpdate = null) {
369        console.warn(`createNewLoadedSound is deprecated in favor of createNewLoadedSoundAsync, which has the same API aside from the method name`);
370        return this.createNewLoadedSoundAsync(initialStatus, onPlaybackStatusUpdate);
371    }
372    /**
373     * Creates and loads a new `Sound` object to play back the `Recording`. Note that this will only succeed once the `Recording`
374     * is done recording and `stopAndUnloadAsync()` has been called.
375     *
376     * @param initialStatus The initial intended `PlaybackStatusToSet` of the sound, whose values will override the default initial playback status.
377     * This value defaults to `{}` if no parameter is passed. See the [AV documentation](/versions/latest/sdk/av) for details on `PlaybackStatusToSet`
378     * and the default initial playback status.
379     * @param onPlaybackStatusUpdate A function taking a single parameter `PlaybackStatus`. This value defaults to `null` if no parameter is passed.
380     * See the [AV documentation](/versions/latest/sdk/av) for details on the functionality provided by `onPlaybackStatusUpdate`
381     *
382     * @return A `Promise` that is rejected if creation failed, or fulfilled with the `SoundObject`.
383     */
384    async createNewLoadedSoundAsync(initialStatus = {}, onPlaybackStatusUpdate = null) {
385        if (this._uri == null || !this._isDoneRecording) {
386            throw new Error('Cannot create sound when the Recording has not finished!');
387        }
388        return Sound.createAsync(
389        // $FlowFixMe: Flow can't distinguish between this literal and Asset
390        { uri: this._uri }, initialStatus, onPlaybackStatusUpdate, false);
391    }
392}
393export { PermissionStatus };
394export * from './RecordingConstants';
395export * from './Recording.types';
396//# sourceMappingURL=Recording.js.map