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 t.expect(recordingObject.getURI()).toContain('file:///'); 256 await recordingObject.stopAndUnloadAsync(); 257 }); 258 }); 259 260 t.describe('Recording.createNewLoadedSound()', () => { 261 let originalConsoleWarn; 262 263 t.beforeAll(() => { 264 originalConsoleWarn = console.warn; 265 console.warn = (...args) => { 266 if (typeof args[0] === 'string' && args[0].indexOf('deprecated') > -1) { 267 return; 268 } 269 originalConsoleWarn(...args); 270 }; 271 }); 272 273 t.afterAll(() => { 274 console.warn = originalConsoleWarn; 275 originalConsoleWarn = null; 276 }); 277 278 t.it('fails if called before the recording is prepared', async () => { 279 let error = null; 280 try { 281 await recordingObject.createNewLoadedSound(); 282 } catch (err) { 283 error = err; 284 } 285 t.expect(error).toBeDefined(); 286 }); 287 288 if (Platform.OS !== 'android') { 289 t.it('fails if called before the recording is started', async () => { 290 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 291 let error = null; 292 try { 293 await recordingObject.createNewLoadedSound(); 294 } catch (err) { 295 error = err; 296 } 297 t.expect(error).toBeDefined(); 298 await recordingObject.stopAndUnloadAsync(); 299 }); 300 } 301 302 t.it('fails if called before the recording is recording', async () => { 303 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 304 await recordingObject.startAsync(); 305 await waitFor(defaultRecordingDurationMillis); 306 let error = null; 307 try { 308 await recordingObject.createNewLoadedSound(); 309 } catch (err) { 310 error = err; 311 } 312 t.expect(error).toBeDefined(); 313 await recordingObject.stopAndUnloadAsync(); 314 }); 315 316 t.it('returns a sound object once the recording is done', async () => { 317 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 318 await recordingObject.startAsync(); 319 320 const recordingDuration = defaultRecordingDurationMillis; 321 await new Promise(resolve => { 322 setTimeout(async () => { 323 await recordingObject.stopAndUnloadAsync(); 324 let error = null; 325 try { 326 const { sound } = await recordingObject.createNewLoadedSound(); 327 await retryForStatus(sound, { isBuffering: false }); 328 const status = await sound.getStatusAsync(); 329 // Android is slow and we have to take it into account when checking recording duration. 330 t.expect(status.durationMillis).toBeGreaterThan(recordingDuration * (7 / 10)); 331 t.expect(sound).toBeDefined(); 332 } catch (err) { 333 error = err; 334 } 335 t.expect(error).toBeNull(); 336 337 resolve(); 338 }, recordingDuration); 339 }); 340 }); 341 342 if (Platform.OS === 'android') { 343 t.it('raises an error when the recording is in an unreadable format', async () => { 344 await recordingObject.prepareToRecordAsync(amrSettings); 345 await recordingObject.startAsync(); 346 347 const recordingDuration = defaultRecordingDurationMillis; 348 await new Promise(resolve => { 349 setTimeout(async () => { 350 await recordingObject.stopAndUnloadAsync(); 351 let error = null; 352 try { 353 await recordingObject.createNewLoadedSound(); 354 } catch (err) { 355 error = err; 356 } 357 t.expect(error).toBeDefined(); 358 359 resolve(); 360 }, recordingDuration); 361 }); 362 }); 363 } 364 }); 365 366 t.describe('Recording.createNewLoadedSoundAsync()', () => { 367 t.it('fails if called before the recording is prepared', async () => { 368 let error = null; 369 try { 370 await recordingObject.createNewLoadedSoundAsync(); 371 } catch (err) { 372 error = err; 373 } 374 t.expect(error).toBeDefined(); 375 }); 376 377 if (Platform.OS !== 'android') { 378 t.it('fails if called before the recording is started', async () => { 379 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 380 let error = null; 381 try { 382 await recordingObject.createNewLoadedSoundAsync(); 383 } catch (err) { 384 error = err; 385 } 386 t.expect(error).toBeDefined(); 387 await recordingObject.stopAndUnloadAsync(); 388 }); 389 } 390 391 t.it('fails if called before the recording is recording', async () => { 392 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 393 await recordingObject.startAsync(); 394 await waitFor(defaultRecordingDurationMillis); 395 let error = null; 396 try { 397 await recordingObject.createNewLoadedSoundAsync(); 398 } catch (err) { 399 error = err; 400 } 401 t.expect(error).toBeDefined(); 402 await recordingObject.stopAndUnloadAsync(); 403 }); 404 405 t.it('returns a sound object once the recording is done', async () => { 406 await recordingObject.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY); 407 await recordingObject.startAsync(); 408 409 const recordingDuration = defaultRecordingDurationMillis; 410 await new Promise(resolve => { 411 setTimeout(async () => { 412 await recordingObject.stopAndUnloadAsync(); 413 let error = null; 414 try { 415 const { sound } = await recordingObject.createNewLoadedSoundAsync(); 416 await retryForStatus(sound, { isBuffering: false }); 417 const status = await sound.getStatusAsync(); 418 // Android is slow and we have to take it into account when checking recording duration. 419 t.expect(status.durationMillis).toBeGreaterThan(recordingDuration * (6 / 10)); 420 t.expect(sound).toBeDefined(); 421 } catch (err) { 422 error = err; 423 } 424 t.expect(error).toBeNull(); 425 426 resolve(); 427 }, recordingDuration); 428 }); 429 }); 430 431 if (Platform.OS === 'android') { 432 t.it('raises an error when the recording is in an unreadable format', async () => { 433 await recordingObject.prepareToRecordAsync(amrSettings); 434 await recordingObject.startAsync(); 435 436 const recordingDuration = defaultRecordingDurationMillis; 437 await new Promise(resolve => { 438 setTimeout(async () => { 439 await recordingObject.stopAndUnloadAsync(); 440 let error = null; 441 try { 442 await recordingObject.createNewLoadedSoundAsync(); 443 } catch (err) { 444 error = err; 445 } 446 t.expect(error).toBeDefined(); 447 448 resolve(); 449 }, recordingDuration); 450 }); 451 }); 452 } 453 }); 454 455 t.describe('Recording.createAsync()', () => { 456 t.afterEach(async () => { 457 await waitFor(defaultRecordingDurationMillis); 458 await recordingObject.stopAndUnloadAsync(); 459 }); 460 461 t.it('creates and starts recording', async () => { 462 recordingObject = await Audio.Recording.createAsync( 463 Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY 464 ); 465 await retryForStatus(recordingObject, { isRecording: true }); 466 }); 467 }); 468 }); 469} 470