1import { Audio } from 'expo-av'; 2import { Platform } from 'react-native'; 3 4import * as TestUtils from '../TestUtils'; 5import { retryForStatus, waitFor } from './helpers'; 6 7export const name = 'Recording'; 8 9const defaultRecordingDurationMillis = 500; 10 11const amrSettings = { 12 android: { 13 extension: '.amr', 14 outputFormat: Audio.RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AMR_NB, 15 audioEncoder: Audio.RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AMR_NB, 16 sampleRate: 8000, 17 numberOfChannels: 1, 18 bitRate: 128000, 19 }, 20 ios: { 21 extension: '.amr', 22 outputFormat: Audio.RECORDING_OPTION_IOS_OUTPUT_FORMAT_AMR, 23 audioQuality: Audio.RECORDING_OPTION_IOS_AUDIO_QUALITY_HIGH, 24 sampleRate: 8000, 25 numberOfChannels: 1, 26 bitRate: 128000, 27 linearPCMBitDepth: 16, 28 linearPCMIsBigEndian: false, 29 linearPCMIsFloat: false, 30 }, 31}; 32 33// In some tests one can see: 34// 35// ``` 36// await recordingObject.startAsync(); 37// await waitFor(defaultRecordingDurationMillis); 38// await recordingObject.stopAndUnloadAsync(); 39// ``` 40// 41// iOS doesn't need starting to be able to `stopAndUnload`, however 42// Android throws an exception, as intended by the authors: 43// > Note that a RuntimeException is intentionally thrown to the application, 44// > if no valid audio/video data has been received when stop() is called. 45// > This happens if stop() is called immediately after start(). 46// > Source: https://developer.android.com/reference/android/media/MediaRecorder.html#stop() 47 48export async function test(t) { 49 const shouldSkipTestsRequiringPermissions = await TestUtils.shouldSkipTestsRequiringPermissionsAsync(); 50 const describeWithPermissions = shouldSkipTestsRequiringPermissions ? t.xdescribe : t.describe; 51 52 describeWithPermissions('Recording', () => { 53 t.beforeAll(async () => { 54 await Audio.setAudioModeAsync({ 55 shouldDuckAndroid: true, 56 allowsRecordingIOS: true, 57 playsInSilentModeIOS: true, 58 staysActiveInBackground: true, 59 playThroughEarpieceAndroid: false, 60 interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_MIX_WITH_OTHERS, 61 interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DUCK_OTHERS, 62 }); 63 64 await TestUtils.acceptPermissionsAndRunCommandAsync(() => { 65 return Audio.requestPermissionsAsync(); 66 }); 67 }); 68 69 // According to the documentation pausing should be supported on Android API >= 24, 70 // unfortunately such test fails on Android v24. 71 const pausingIsSupported = Platform.OS !== 'android' || Platform.Version >= 25; 72 let recordingObject = null; 73 74 t.beforeEach(async () => { 75 const { status } = await Audio.getPermissionsAsync(); 76 t.expect(status).toEqual('granted'); 77 recordingObject = new Audio.Recording(); 78 }); 79 80 t.afterEach(() => { 81 recordingObject = null; 82 }); 83 84 t.describe('Recording.prepareToRecordAsync(preset)', () => { 85 t.afterEach(async () => { 86 await recordingObject.startAsync(); 87 await waitFor(defaultRecordingDurationMillis); 88 await recordingObject.stopAndUnloadAsync(); 89 }); 90 91 t.it('sets high preset successfully', async () => { 92 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_HIGH_QUALITY); 93 }); 94 95 t.it('sets low preset successfully', async () => { 96 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 97 }); 98 99 t.it('sets custom preset successfully', async () => { 100 const commonOptions = { 101 bitRate: 8000, 102 sampleRate: 8000, 103 numberOfChannels: 1, 104 }; 105 await recordingObject.prepareToRecordAsync({ 106 android: { 107 extension: '.aac', 108 audioEncoder: Audio.RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC, 109 outputFormat: Audio.RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AAC_ADIF, 110 ...commonOptions, 111 }, 112 ios: { 113 extension: '.ulaw', 114 linearPCMBitDepth: 8, 115 linearPCMIsFloat: false, 116 linearPCMIsBigEndian: false, 117 outputFormat: Audio.RECORDING_OPTION_IOS_OUTPUT_FORMAT_ULAW, 118 audioQuality: Audio.RECORDING_OPTION_IOS_AUDIO_QUALITY_MEDIUM, 119 ...commonOptions, 120 }, 121 }); 122 }); 123 }); 124 125 // Such function exists in the documentation, but not in the implementation. 126 127 // t.describe('Recording.isPreparedToRecord()', () => { 128 // t.beforeEach( 129 // async () => 130 // await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY) 131 // ); 132 // t.afterEach(async () => await recordingObject.stopAndUnloadAsync()); 133 134 // t.it('returns a boolean', () => { 135 // const returnedValue = recordingObject.isPreparedToRecord(); 136 // const valueIsABoolean = returnedValue === false || returnedValue === true; 137 // t.expect(valueIsABoolean).toBe(true); 138 // }); 139 // }); 140 141 t.describe('Recording.setOnRecordingStatusUpdate(onRecordingStatusUpdate)', () => { 142 t.it('sets a function that gets called when status updates', async () => { 143 const onRecordingStatusUpdate = t.jasmine.createSpy('onRecordingStatusUpdate'); 144 recordingObject.setOnRecordingStatusUpdate(onRecordingStatusUpdate); 145 t.expect(onRecordingStatusUpdate).toHaveBeenCalledWith( 146 t.jasmine.objectContaining({ canRecord: false }) 147 ); 148 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 149 t.expect(onRecordingStatusUpdate).toHaveBeenCalledWith( 150 t.jasmine.objectContaining({ canRecord: true }) 151 ); 152 await recordingObject.startAsync(); 153 await waitFor(defaultRecordingDurationMillis); 154 await recordingObject.stopAndUnloadAsync(); 155 }); 156 157 t.it('sets a function that gets called when recording finishes', async () => { 158 const onRecordingStatusUpdate = t.jasmine.createSpy('onRecordingStatusUpdate'); 159 recordingObject.setOnRecordingStatusUpdate(onRecordingStatusUpdate); 160 t.expect(onRecordingStatusUpdate).toHaveBeenCalledWith( 161 t.jasmine.objectContaining({ canRecord: false }) 162 ); 163 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 164 t.expect(onRecordingStatusUpdate).toHaveBeenCalledWith( 165 t.jasmine.objectContaining({ canRecord: true }) 166 ); 167 await recordingObject.startAsync(); 168 await waitFor(defaultRecordingDurationMillis); 169 await recordingObject.stopAndUnloadAsync(); 170 t.expect(onRecordingStatusUpdate).toHaveBeenCalledWith( 171 t.jasmine.objectContaining({ isDoneRecording: true, canRecord: false }) 172 ); 173 }); 174 }); 175 176 /*t.describe('Recording.setProgressUpdateInterval(millis)', () => { 177 t.afterEach(async () => await recordingObject.stopAndUnloadAsync()); 178 179 t.it('sets frequence of the progress updates', async () => { 180 const onRecordingStatusUpdate = t.jasmine.createSpy('onRecordingStatusUpdate'); 181 recordingObject.setOnRecordingStatusUpdate(onRecordingStatusUpdate); 182 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 183 await recordingObject.startAsync(); 184 const updateInterval = 50; 185 recordingObject.setProgressUpdateInterval(updateInterval); 186 await new Promise(resolve => { 187 setTimeout(() => { 188 const expectedArgsCount = Platform.OS === 'android' ? 5 : 10; 189 t.expect(onRecordingStatusUpdate.calls.count()).toBeGreaterThan(expectedArgsCount); 190 191 const realMillis = map( 192 takeRight(filter(flatten(onRecordingStatusUpdate.calls.allArgs()), 'isRecording'), 4), 193 'durationMillis' 194 ); 195 196 for (let i = 3; i > 0; i--) { 197 const difference = Math.abs(realMillis[i] - realMillis[i - 1] - updateInterval); 198 t.expect(difference).toBeLessThan(updateInterval / 2 + 1); 199 } 200 201 resolve(); 202 }, 800); 203 }); 204 }); 205 });*/ 206 207 t.describe('Recording.startAsync()', () => { 208 t.afterEach(async () => { 209 await waitFor(defaultRecordingDurationMillis); 210 await recordingObject.stopAndUnloadAsync(); 211 }); 212 213 t.it('starts a clean recording', async () => { 214 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 215 await recordingObject.startAsync(); 216 await retryForStatus(recordingObject, { isRecording: true }); 217 }); 218 219 if (pausingIsSupported) { 220 t.it('starts a paused recording', async () => { 221 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 222 await recordingObject.startAsync(); 223 await retryForStatus(recordingObject, { isRecording: true }); 224 await recordingObject.pauseAsync(); 225 await retryForStatus(recordingObject, { isRecording: false }); 226 await recordingObject.startAsync(); 227 await retryForStatus(recordingObject, { isRecording: true }); 228 }); 229 } 230 }); 231 232 if (pausingIsSupported) { 233 t.describe('Recording.pauseAsync()', () => { 234 t.it('pauses the recording', async () => { 235 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 236 await recordingObject.startAsync(); 237 await retryForStatus(recordingObject, { isRecording: true }); 238 await waitFor(defaultRecordingDurationMillis); 239 await recordingObject.pauseAsync(); 240 await retryForStatus(recordingObject, { isRecording: false }); 241 await recordingObject.stopAndUnloadAsync(); 242 }); 243 }); 244 } 245 246 t.describe('Recording.getURI()', () => { 247 t.it('returns null before the recording is prepared', async () => { 248 t.expect(recordingObject.getURI()).toBeNull(); 249 }); 250 251 t.it('returns a string once the recording is prepared', async () => { 252 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 253 await recordingObject.startAsync(); 254 await waitFor(defaultRecordingDurationMillis); 255 if (Platform.OS === 'web') { 256 // On web, URI is not available until completion 257 t.expect(recordingObject.getURI()).toEqual(null); 258 } else { 259 t.expect(recordingObject.getURI()).toContain('file:///'); 260 } 261 await recordingObject.stopAndUnloadAsync(); 262 }); 263 }); 264 265 t.describe('Recording.createNewLoadedSound()', () => { 266 let originalConsoleWarn; 267 268 t.beforeAll(() => { 269 originalConsoleWarn = console.warn; 270 console.warn = (...args) => { 271 if (typeof args[0] === 'string' && args[0].indexOf('deprecated') > -1) { 272 return; 273 } 274 originalConsoleWarn(...args); 275 }; 276 }); 277 278 t.afterAll(() => { 279 console.warn = originalConsoleWarn; 280 originalConsoleWarn = null; 281 }); 282 283 t.it('fails if called before the recording is prepared', async () => { 284 let error = null; 285 try { 286 await recordingObject.createNewLoadedSound(); 287 } catch (err) { 288 error = err; 289 } 290 t.expect(error).toBeDefined(); 291 }); 292 293 if (Platform.OS !== 'android') { 294 t.it('fails if called before the recording is started', async () => { 295 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 296 let error = null; 297 try { 298 await recordingObject.createNewLoadedSound(); 299 } catch (err) { 300 error = err; 301 } 302 t.expect(error).toBeDefined(); 303 await recordingObject.stopAndUnloadAsync(); 304 }); 305 } 306 307 t.it('fails if called before the recording is recording', async () => { 308 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 309 await recordingObject.startAsync(); 310 await waitFor(defaultRecordingDurationMillis); 311 let error = null; 312 try { 313 await recordingObject.createNewLoadedSound(); 314 } catch (err) { 315 error = err; 316 } 317 t.expect(error).toBeDefined(); 318 await recordingObject.stopAndUnloadAsync(); 319 }); 320 321 t.it('returns a sound object once the recording is done', async () => { 322 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 323 await recordingObject.startAsync(); 324 325 const recordingDuration = defaultRecordingDurationMillis; 326 await new Promise(resolve => { 327 setTimeout(async () => { 328 await recordingObject.stopAndUnloadAsync(); 329 let error = null; 330 try { 331 const { sound } = await recordingObject.createNewLoadedSound(); 332 await retryForStatus(sound, { isBuffering: false }); 333 const status = await sound.getStatusAsync(); 334 // Web doesn't return durations in Chrome - https://bugs.chromium.org/p/chromium/issues/detail?id=642012 335 if (Platform.OS !== 'web') { 336 // Android is slow and we have to take it into account when checking recording duration. 337 t.expect(status.durationMillis).toBeGreaterThan(recordingDuration * (7 / 10)); 338 } 339 t.expect(sound).toBeDefined(); 340 } catch (err) { 341 error = err; 342 } 343 t.expect(error).toBeNull(); 344 345 resolve(); 346 }, recordingDuration); 347 }); 348 }); 349 350 if (Platform.OS === 'android') { 351 t.it('raises an error when the recording is in an unreadable format', async () => { 352 await recordingObject.prepareToRecordAsync(amrSettings); 353 await recordingObject.startAsync(); 354 355 const recordingDuration = defaultRecordingDurationMillis; 356 await new Promise(resolve => { 357 setTimeout(async () => { 358 await recordingObject.stopAndUnloadAsync(); 359 let error = null; 360 try { 361 await recordingObject.createNewLoadedSound(); 362 } catch (err) { 363 error = err; 364 } 365 t.expect(error).toBeDefined(); 366 367 resolve(); 368 }, recordingDuration); 369 }); 370 }); 371 } 372 }); 373 374 t.describe('Recording.createNewLoadedSoundAsync()', () => { 375 t.it('fails if called before the recording is prepared', async () => { 376 let error = null; 377 try { 378 await recordingObject.createNewLoadedSoundAsync(); 379 } catch (err) { 380 error = err; 381 } 382 t.expect(error).toBeDefined(); 383 }); 384 385 if (Platform.OS !== 'android') { 386 t.it('fails if called before the recording is started', async () => { 387 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 388 let error = null; 389 try { 390 await recordingObject.createNewLoadedSoundAsync(); 391 } catch (err) { 392 error = err; 393 } 394 t.expect(error).toBeDefined(); 395 await recordingObject.stopAndUnloadAsync(); 396 }); 397 } 398 399 t.it('fails if called before the recording is recording', async () => { 400 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 401 await recordingObject.startAsync(); 402 await waitFor(defaultRecordingDurationMillis); 403 let error = null; 404 try { 405 await recordingObject.createNewLoadedSoundAsync(); 406 } catch (err) { 407 error = err; 408 } 409 t.expect(error).toBeDefined(); 410 await recordingObject.stopAndUnloadAsync(); 411 }); 412 413 t.it('returns a sound object once the recording is done', async () => { 414 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 415 await recordingObject.startAsync(); 416 417 const recordingDuration = defaultRecordingDurationMillis; 418 await new Promise(resolve => { 419 setTimeout(async () => { 420 await recordingObject.stopAndUnloadAsync(); 421 let error = null; 422 try { 423 const { sound } = await recordingObject.createNewLoadedSoundAsync(); 424 await retryForStatus(sound, { isBuffering: false }); 425 const status = await sound.getStatusAsync(); 426 427 // Web doesn't return durations in Chrome - https://bugs.chromium.org/p/chromium/issues/detail?id=642012 428 if (Platform.OS !== 'web') { 429 // Android is slow and we have to take it into account when checking recording duration. 430 t.expect(status.durationMillis).toBeGreaterThan(recordingDuration * (6 / 10)); 431 } 432 t.expect(sound).toBeDefined(); 433 } catch (err) { 434 error = err; 435 } 436 t.expect(error).toBeNull(); 437 438 resolve(); 439 }, recordingDuration); 440 }); 441 }); 442 443 if (Platform.OS === 'android') { 444 t.it('raises an error when the recording is in an unreadable format', async () => { 445 await recordingObject.prepareToRecordAsync(amrSettings); 446 await recordingObject.startAsync(); 447 448 const recordingDuration = defaultRecordingDurationMillis; 449 await new Promise(resolve => { 450 setTimeout(async () => { 451 await recordingObject.stopAndUnloadAsync(); 452 let error = null; 453 try { 454 await recordingObject.createNewLoadedSoundAsync(); 455 } catch (err) { 456 error = err; 457 } 458 t.expect(error).toBeDefined(); 459 460 resolve(); 461 }, recordingDuration); 462 }); 463 }); 464 } 465 }); 466 467 t.describe('Recording.createAsync()', () => { 468 t.afterEach(async () => { 469 await waitFor(defaultRecordingDurationMillis); 470 await recordingObject.stopAndUnloadAsync(); 471 }); 472 473 t.it('creates and starts recording', async () => { 474 const { recording } = await Audio.Recording.createAsync( 475 Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY 476 ); 477 recordingObject = recording; 478 await retryForStatus(recordingObject, { isRecording: true }); 479 }); 480 }); 481 }); 482} 483