UNPKG

expo-av

Version:

Expo universal module for Audio and Video playback

287 lines 13.4 kB
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