xref: /expo/apps/test-suite/tests/Recording.js (revision f10e66fc)
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