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