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