expo-av
Version:
Expo universal module for Audio and Video playback
287 lines • 13.4 kB
JavaScript
import { EventEmitter, Platform } from '@unimodules/core';
import { PermissionStatus } from 'unimodules-permissions-interface';
import { _DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS, } from '../AV';
import ExponentAV from '../ExponentAV';
import { isAudioEnabled, throwIfAudioIsDisabled } from './AudioAvailability';
import { Sound } from './Sound';
// TODO: consider changing these to enums
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_DEFAULT = 0;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_THREE_GPP = 1;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG_4 = 2;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AMR_NB = 3;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AMR_WB = 4;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AAC_ADIF = 5;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AAC_ADTS = 6;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_RTP_AVP = 7;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG2TS = 8;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_WEBM = 9;
export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_DEFAULT = 0;
export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AMR_NB = 1;
export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AMR_WB = 2;
export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC = 3;
export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_HE_AAC = 4;
export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC_ELD = 5;
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_LINEARPCM = 'lpcm';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AC3 = 'ac-3';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_60958AC3 = 'cac3';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_APPLEIMA4 = 'ima4';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC = 'aac ';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4CELP = 'celp';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4HVXC = 'hvxc';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4TWINVQ = 'twvq';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MACE3 = 'MAC3';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MACE6 = 'MAC6';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ULAW = 'ulaw';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ALAW = 'alaw';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_QDESIGN = 'QDMC';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_QDESIGN2 = 'QDM2';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_QUALCOMM = 'Qclp';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEGLAYER1 = '.mp1';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEGLAYER2 = '.mp2';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEGLAYER3 = '.mp3';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_APPLELOSSLESS = 'alac';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_HE = 'aach';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_LD = 'aacl';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_ELD = 'aace';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_ELD_SBR = 'aacf';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_ELD_V2 = 'aacg';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_HE_V2 = 'aacp';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_SPATIAL = 'aacs';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AMR = 'samr';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AMR_WB = 'sawb';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AUDIBLE = 'AUDB';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ILBC = 'ilbc';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_DVIINTELIMA = 0x6d730011;
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MICROSOFTGSM = 0x6d730031;
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AES3 = 'aes3';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ENHANCEDAC3 = 'ec-3';
export const RECORDING_OPTION_IOS_AUDIO_QUALITY_MIN = 0;
export const RECORDING_OPTION_IOS_AUDIO_QUALITY_LOW = 0x20;
export const RECORDING_OPTION_IOS_AUDIO_QUALITY_MEDIUM = 0x40;
export const RECORDING_OPTION_IOS_AUDIO_QUALITY_HIGH = 0x60;
export const RECORDING_OPTION_IOS_AUDIO_QUALITY_MAX = 0x7f;
export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_CONSTANT = 0;
export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_LONG_TERM_AVERAGE = 1;
export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_VARIABLE_CONSTRAINED = 2;
export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_VARIABLE = 3;
// TODO : maybe make presets for music and speech, or lossy / lossless.
export const RECORDING_OPTIONS_PRESET_HIGH_QUALITY = {
android: {
extension: '.m4a',
outputFormat: RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG_4,
audioEncoder: RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC,
sampleRate: 44100,
numberOfChannels: 2,
bitRate: 128000,
},
ios: {
extension: '.caf',
audioQuality: RECORDING_OPTION_IOS_AUDIO_QUALITY_MAX,
sampleRate: 44100,
numberOfChannels: 2,
bitRate: 128000,
linearPCMBitDepth: 16,
linearPCMIsBigEndian: false,
linearPCMIsFloat: false,
},
};
export const RECORDING_OPTIONS_PRESET_LOW_QUALITY = {
android: {
extension: '.3gp',
outputFormat: RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_THREE_GPP,
audioEncoder: RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AMR_NB,
sampleRate: 44100,
numberOfChannels: 2,
bitRate: 128000,
},
ios: {
extension: '.caf',
audioQuality: RECORDING_OPTION_IOS_AUDIO_QUALITY_MIN,
sampleRate: 44100,
numberOfChannels: 2,
bitRate: 128000,
linearPCMBitDepth: 16,
linearPCMIsBigEndian: false,
linearPCMIsFloat: false,
},
};
export { PermissionStatus };
let _recorderExists = false;
const eventEmitter = Platform.OS === 'android' ? new EventEmitter(ExponentAV) : null;
export async function getPermissionsAsync() {
return ExponentAV.getPermissionsAsync();
}
export async function requestPermissionsAsync() {
return ExponentAV.requestPermissionsAsync();
}
export class Recording {
constructor() {
this._subscription = null;
this._canRecord = false;
this._isDoneRecording = false;
this._finalDurationMillis = 0;
this._uri = null;
this._onRecordingStatusUpdate = null;
this._progressUpdateTimeoutVariable = null;
this._progressUpdateIntervalMillis = _DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS;
this._options = null;
// Internal methods
this._cleanupForUnloadedRecorder = async (finalStatus) => {
this._canRecord = false;
this._isDoneRecording = true;
// $FlowFixMe(greg): durationMillis is not always defined
this._finalDurationMillis = finalStatus.durationMillis;
_recorderExists = false;
if (this._subscription) {
this._subscription.remove();
this._subscription = null;
}
this._disablePolling();
return await this.getStatusAsync(); // Automatically calls onRecordingStatusUpdate for the final state.
};
this._pollingLoop = async () => {
if (isAudioEnabled() && this._canRecord && this._onRecordingStatusUpdate != null) {
this._progressUpdateTimeoutVariable = setTimeout(this._pollingLoop, this._progressUpdateIntervalMillis);
try {
await this.getStatusAsync();
}
catch (error) {
this._disablePolling();
}
}
};
// Note that all calls automatically call onRecordingStatusUpdate as a side effect.
// Get status API
this.getStatusAsync = async () => {
// Automatically calls onRecordingStatusUpdate.
if (this._canRecord) {
return this._performOperationAndHandleStatusAsync(() => ExponentAV.getAudioRecordingStatus());
}
const status = {
canRecord: false,
isRecording: false,
isDoneRecording: this._isDoneRecording,
durationMillis: this._finalDurationMillis,
};
this._callOnRecordingStatusUpdateForNewStatus(status);
return status;
};
}
_disablePolling() {
if (this._progressUpdateTimeoutVariable != null) {
clearTimeout(this._progressUpdateTimeoutVariable);
this._progressUpdateTimeoutVariable = null;
}
}
_enablePollingIfNecessaryAndPossible() {
if (isAudioEnabled() && this._canRecord && this._onRecordingStatusUpdate != null) {
this._disablePolling();
this._pollingLoop();
}
}
_callOnRecordingStatusUpdateForNewStatus(status) {
if (this._onRecordingStatusUpdate != null) {
this._onRecordingStatusUpdate(status);
}
}
async _performOperationAndHandleStatusAsync(operation) {
throwIfAudioIsDisabled();
if (this._canRecord) {
const status = await operation();
this._callOnRecordingStatusUpdateForNewStatus(status);
return status;
}
else {
throw new Error('Cannot complete operation because this recorder is not ready to record.');
}
}
setOnRecordingStatusUpdate(onRecordingStatusUpdate) {
this._onRecordingStatusUpdate = onRecordingStatusUpdate;
if (onRecordingStatusUpdate == null) {
this._disablePolling();
}
else {
this._enablePollingIfNecessaryAndPossible();
}
this.getStatusAsync();
}
setProgressUpdateInterval(progressUpdateIntervalMillis) {
this._progressUpdateIntervalMillis = progressUpdateIntervalMillis;
this.getStatusAsync();
}
// Record API
async prepareToRecordAsync(options = RECORDING_OPTIONS_PRESET_LOW_QUALITY) {
throwIfAudioIsDisabled();
if (_recorderExists) {
throw new Error('Only one Recording object can be prepared at a given time.');
}
if (this._isDoneRecording) {
throw new Error('This Recording object is done recording; you must make a new one.');
}
if (!options || !options.android || !options.ios) {
throw new Error('You must provide recording options for android and ios in order to prepare to record.');
}
const extensionRegex = /^\.\w+$/;
if (!options.android.extension ||
!options.ios.extension ||
!extensionRegex.test(options.android.extension) ||
!extensionRegex.test(options.ios.extension)) {
throw new Error(`Your file extensions must match ${extensionRegex.toString()}.`);
}
if (!this._canRecord) {
if (eventEmitter) {
this._subscription = eventEmitter.addListener('Expo.Recording.recorderUnloaded', this._cleanupForUnloadedRecorder);
}
const { uri, status, } = await ExponentAV.prepareAudioRecorder(options);
_recorderExists = true;
this._uri = uri;
this._options = options;
this._canRecord = true;
const currentStatus = { ...status, canRecord: true };
this._callOnRecordingStatusUpdateForNewStatus(currentStatus);
this._enablePollingIfNecessaryAndPossible();
return currentStatus;
}
else {
throw new Error('This Recording object is already prepared to record.');
}
}
async startAsync() {
return this._performOperationAndHandleStatusAsync(() => ExponentAV.startAudioRecording());
}
async pauseAsync() {
return this._performOperationAndHandleStatusAsync(() => ExponentAV.pauseAudioRecording());
}
async stopAndUnloadAsync() {
if (!this._canRecord) {
if (this._isDoneRecording) {
throw new Error('Cannot unload a Recording that has already been unloaded.');
}
else {
throw new Error('Cannot unload a Recording that has not been prepared.');
}
}
// We perform a separate native API call so that the state of the Recording can be updated with
// the final duration of the recording. (We cast stopStatus as Object to appease Flow)
const finalStatus = await ExponentAV.stopAudioRecording();
await ExponentAV.unloadAudioRecorder();
return this._cleanupForUnloadedRecorder(finalStatus);
}
// Read API
getURI() {
return this._uri;
}
async createNewLoadedSound(initialStatus = {}, onPlaybackStatusUpdate = null) {
console.warn(`createNewLoadedSound is deprecated in favor of createNewLoadedSoundAsync, which has the same API aside from the method name`);
return this.createNewLoadedSoundAsync(initialStatus, onPlaybackStatusUpdate);
}
async createNewLoadedSoundAsync(initialStatus = {}, onPlaybackStatusUpdate = null) {
if (this._uri == null || !this._isDoneRecording) {
throw new Error('Cannot create sound when the Recording has not finished!');
}
return Sound.createAsync(
// $FlowFixMe: Flow can't distinguish between this literal and Asset
{ uri: this._uri }, initialStatus, onPlaybackStatusUpdate, false);
}
}
//# sourceMappingURL=Recording.js.map