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