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