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