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