expo
Version:
The Expo SDK
489 lines (432 loc) • 14.8 kB
JavaScript
// @flow
import { NativeModules } from 'react-native';
import Asset from './Asset';
import type { PlaybackSource, PlaybackStatus, PlaybackStatusToSet } from './AV';
import {
_DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS,
_DEFAULT_INITIAL_PLAYBACK_STATUS,
_COMMON_AV_PLAYBACK_METHODS,
_getURIAndFullInitialStatusForLoadAsync,
_throwErrorIfValuesOutOfBoundsInStatus,
_getUnloadedStatus,
} from './AV';
// TODO For consistency with PlaybackStatus, should we include progressUpdateIntervalMillis here as well?
export type RecordingStatus =
| {
canRecord: false,
isDoneRecording: false,
}
| {
canRecord: true,
isRecording: boolean,
durationMillis: number,
}
| {
canRecord: false,
isDoneRecording: true,
durationMillis: number,
};
export type AudioMode = {
allowsRecordingIOS: boolean,
interruptionModeIOS: number,
playsInSilentModeIOS: boolean,
interruptionModeAndroid: boolean,
shouldDuckAndroid: boolean,
};
export const INTERRUPTION_MODE_IOS_MIX_WITH_OTHERS = 0;
export const INTERRUPTION_MODE_IOS_DO_NOT_MIX = 1;
export const INTERRUPTION_MODE_IOS_DUCK_OTHERS = 2;
export const INTERRUPTION_MODE_ANDROID_DO_NOT_MIX = 1;
export const INTERRUPTION_MODE_ANDROID_DUCK_OTHERS = 2;
let _enabled: boolean = true; // @terribleben Note that @nikki93 and I think true should be the default here.
let _recorderExists: boolean = false;
const _DISABLED_ERROR: Error = new Error(
'Cannot complete operation because audio is not enabled.'
);
export async function setIsEnabledAsync(value: boolean): Promise<void> {
_enabled = value;
await NativeModules.ExponentAV.setAudioIsEnabled(value);
// TODO : We immediately pause all players when disabled, but we do not resume all shouldPlay
// players when enabled. Perhaps for completeness we should allow this; the design of the
// enabling API is for people to enable / disable this audio library, but I think that it should
// intuitively also double as a global pause/resume.
}
export async function setAudioModeAsync(mode: AudioMode): Promise<void> {
if (
!('allowsRecordingIOS' in mode) ||
!('interruptionModeIOS' in mode) ||
!('playsInSilentModeIOS' in mode) ||
!('interruptionModeAndroid' in mode) ||
!('shouldDuckAndroid' in mode)
) {
throw new Error(
'Audio mode must contain keys "allowsRecordingIOS", "interruptionModeIOS", "playsInSilentModeIOS", "interruptionModeAndroid", and "shouldDuckAndroid".'
);
}
if (
mode.interruptionModeIOS !== INTERRUPTION_MODE_IOS_MIX_WITH_OTHERS &&
mode.interruptionModeIOS !== INTERRUPTION_MODE_IOS_DO_NOT_MIX &&
mode.interruptionModeIOS !== INTERRUPTION_MODE_IOS_DUCK_OTHERS
) {
throw new Error(
`"interruptionModeIOS" must an integer between ${INTERRUPTION_MODE_IOS_MIX_WITH_OTHERS} and ${INTERRUPTION_MODE_IOS_DUCK_OTHERS}.`
);
}
if (
mode.interruptionModeAndroid !== INTERRUPTION_MODE_ANDROID_DO_NOT_MIX &&
mode.interruptionModeAndroid !== INTERRUPTION_MODE_ANDROID_DUCK_OTHERS
) {
throw new Error(
`"interruptionModeAndroid" must an integer between ${INTERRUPTION_MODE_ANDROID_DO_NOT_MIX} and ${INTERRUPTION_MODE_ANDROID_DUCK_OTHERS}.`
);
}
if (
typeof mode.allowsRecordingIOS !== 'boolean' ||
typeof mode.playsInSilentModeIOS !== 'boolean' ||
typeof mode.shouldDuckAndroid !== 'boolean'
) {
throw new Error(
'"allowsRecordingIOS", "playsInSilentModeIOS", and "shouldDuckAndroid" must be booleans.'
);
}
await NativeModules.ExponentAV.setAudioMode(mode);
}
export class Sound {
_loaded: boolean;
_loading: boolean;
_key: number;
_callback: ?(status: PlaybackStatus) => void;
constructor() {
this._loaded = false;
this._loading = false;
this._key = -1;
this._callback = null;
}
static create = async (
source: PlaybackSource,
initialStatus: PlaybackStatusToSet = {},
callback: ?(status: PlaybackStatus) => void = null,
downloadFirst: boolean = true
): Promise<{ sound: Sound, status: PlaybackStatus }> => {
const sound: Sound = new Sound();
sound.setCallback(callback);
const status: PlaybackStatus = await sound.loadAsync(
source,
initialStatus,
downloadFirst
);
return { sound, status };
};
// Internal methods
_callCallbackForNewStatus(status: PlaybackStatus) {
if (this._callback != null) {
this._callback(status);
}
}
async _performOperationAndHandleStatusAsync(
operation: () => Promise<PlaybackStatus>
): Promise<PlaybackStatus> {
if (!_enabled) {
throw _DISABLED_ERROR;
}
if (this._loaded) {
const status = await operation();
this._callCallbackForNewStatus(status);
return status;
} else {
throw new Error('Cannot complete operation because sound is not loaded.');
}
}
_statusUpdateCallback = (status: PlaybackStatus) => {
this._callCallbackForNewStatus(status);
this._setStatusUpdateCallback(); // Callbacks are only called once and then released.
};
// TODO: We can optimize by only using time observer on native if (this._callback).
_setStatusUpdateCallback() {
if (this._loaded) {
NativeModules.ExponentAV.setStatusUpdateCallbackForSound(
this._key,
this._statusUpdateCallback
);
}
}
_errorCallback = (error: string) => {
this._loaded = false;
this._key = -1;
this._callCallbackForNewStatus(_getUnloadedStatus(error));
};
// ### Unified playback API ### (consistent with Video.js)
// All calls automatically call the callback as a side effect.
// Get status API
getStatusAsync = async (): Promise<PlaybackStatus> => {
if (this._loaded) {
return this._performOperationAndHandleStatusAsync(() =>
NativeModules.ExponentAV.getStatusForSound(this._key)
);
}
const status: PlaybackStatus = _getUnloadedStatus();
this._callCallbackForNewStatus(status);
return status;
};
setCallback(callback: ?(status: PlaybackStatus) => void) {
this._callback = callback;
this.getStatusAsync();
}
// Loading / unloading API
async loadAsync(
source: PlaybackSource,
initialStatus: PlaybackStatusToSet = {},
downloadFirst: boolean = true
): Promise<PlaybackStatus> {
if (!_enabled) {
throw _DISABLED_ERROR;
}
if (this.loading) {
throw new Error('The Sound is already loading.');
}
if (!this._loaded) {
this._loading = true;
const {
uri,
fullInitialStatus,
} = await _getURIAndFullInitialStatusForLoadAsync(
source,
initialStatus,
downloadFirst
);
// This is a workaround, since using load with resolve / reject seems to not work.
return new Promise(
function(resolve, reject) {
const loadSuccess = (key: number, status: PlaybackStatus) => {
this._key = key;
this._loaded = true;
this._loading = false;
NativeModules.ExponentAV.setErrorCallbackForSound(
this._key,
this._errorCallback
);
this._setStatusUpdateCallback();
this._callCallbackForNewStatus(status);
resolve(status);
};
const loadError = (error: string) => {
this._loading = false;
reject(new Error(error));
};
NativeModules.ExponentAV.loadForSound(
uri,
fullInitialStatus,
loadSuccess,
loadError
);
}.bind(this)
);
} else {
throw new Error('The Sound is already loaded.');
}
}
async unloadAsync(): Promise<PlaybackStatus> {
if (this._loaded) {
this._loaded = false;
const key = this._key;
this._key = -1;
const status = await NativeModules.ExponentAV.unloadForSound(key);
this._callCallbackForNewStatus(status);
return status;
} else {
return this.getStatusAsync(); // Automatically calls the callback.
}
}
// Set status API (only available while isLoaded = true)
async setStatusAsync(status: PlaybackStatusToSet): Promise<PlaybackStatus> {
_throwErrorIfValuesOutOfBoundsInStatus(status);
return this._performOperationAndHandleStatusAsync(() =>
NativeModules.ExponentAV.setStatusForSound(this._key, status)
);
}
// Additional convenience methods on top of setStatusAsync are set via _COMMON_AV_PLAYBACK_METHODS.
playAsync: () => Promise<PlaybackStatus>;
playFromPositionAsync: (positionMillis: number) => Promise<PlaybackStatus>;
pauseAsync: () => Promise<PlaybackStatus>;
stopAsync: () => Promise<PlaybackStatus>;
setPositionAsync: (positionMillis: number) => Promise<PlaybackStatus>;
setRateAsync: (
rate: number,
shouldCorrectPitch: boolean
) => Promise<PlaybackStatus>;
setVolumeAsync: (volume: number) => Promise<PlaybackStatus>;
setIsMutedAsync: (isMuted: boolean) => Promise<PlaybackStatus>;
setIsLoopingAsync: (isLooping: boolean) => Promise<PlaybackStatus>;
setProgressUpdateIntervalAsync: (
progressUpdateIntervalMillis: number
) => Promise<PlaybackStatus>;
}
Object.assign(Sound.prototype, _COMMON_AV_PLAYBACK_METHODS);
export class Recording {
_canRecord: boolean;
_isDoneRecording: boolean;
_finalDurationMillis: number;
_uri: ?string;
_callback: ?(status: RecordingStatus) => void;
_progressUpdateTimeoutVariable: ?number;
_progressUpdateIntervalMillis: number;
constructor() {
this._canRecord = false;
this._isDoneRecording = false;
this._finalDurationMillis = 0;
this._uri = null;
this._progressUpdateTimeoutVariable = null;
this._progressUpdateIntervalMillis = _DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS;
}
// Internal methods
_pollingLoop = () => {
if (_enabled && this._canRecord && this._callback != null) {
this.getStatusAsync(); // Automatically calls this._callback.
this._progressUpdateTimeoutVariable = setTimeout(
this._pollingLoop,
this._progressUpdateIntervalMillis
);
}
};
_disablePolling() {
if (this._progressUpdateTimeoutVariable != null) {
clearTimeout(this._progressUpdateTimeoutVariable);
this._progressUpdateTimeoutVariable = null;
}
}
_enablePollingIfNecessaryAndPossible() {
if (_enabled && this._canRecord && this._callback != null) {
this._disablePolling();
this._pollingLoop();
}
}
_callCallbackForNewStatus(status: RecordingStatus) {
if (this._callback != null) {
this._callback(status);
}
}
async _performOperationAndHandleStatusAsync(
operation: () => Promise<RecordingStatus>
): Promise<RecordingStatus> {
if (!_enabled) {
throw _DISABLED_ERROR;
}
if (this._canRecord) {
const status = await operation();
this._callCallbackForNewStatus(status);
return status;
} else {
throw new Error(
'Cannot complete operation because this recorder is not ready to record.'
);
}
}
// Note that all calls automatically call the callback as a side effect.
// Get status API
getStatusAsync = async (): Promise<RecordingStatus> => {
// Automatically calls the callback.
if (this._canRecord) {
return this._performOperationAndHandleStatusAsync(() =>
NativeModules.ExponentAV.getAudioRecordingStatus()
);
}
const status: RecordingStatus = this._isDoneRecording
? {
canRecord: false,
isDoneRecording: true,
durationMillis: this._finalDurationMillis,
}
: {
canRecord: false,
isDoneRecording: false,
};
this._callCallbackForNewStatus(status);
return status;
};
setCallback(callback: ?(status: RecordingStatus) => void) {
this._callback = callback;
if (callback == null) {
this._disablePolling();
} else {
this._enablePollingIfNecessaryAndPossible();
}
this.getStatusAsync();
}
setProgressUpdateInterval(progressUpdateIntervalMillis: number) {
this._progressUpdateIntervalMillis = progressUpdateIntervalMillis;
this.getStatusAsync();
}
// Record API
async prepareToRecordAsync(): Promise<RecordingStatus> {
if (!_enabled) {
throw _DISABLED_ERROR;
}
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 (!this._canRecord) {
const {
uri,
status,
}: {
uri: string,
status: Object, // status is of type RecordingStatus, but without the canRecord field populated.
} = await NativeModules.ExponentAV.prepareAudioRecorder();
_recorderExists = true;
this._uri = uri;
this._canRecord = true;
this._callCallbackForNewStatus(status);
this._enablePollingIfNecessaryAndPossible();
return status;
} else {
throw new Error('This Recording object is already prepared to record.');
}
}
async startAsync(): Promise<RecordingStatus> {
return this._performOperationAndHandleStatusAsync(() =>
NativeModules.ExponentAV.startAudioRecording()
);
}
async pauseAsync(): Promise<RecordingStatus> {
return this._performOperationAndHandleStatusAsync(() =>
NativeModules.ExponentAV.pauseAudioRecording()
);
}
async stopAndUnloadAsync(): Promise<RecordingStatus> {
if (!this._canRecord) {
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 stopStatus: Object = await NativeModules.ExponentAV.stopAudioRecording();
this._finalDurationMillis = stopStatus.durationMillis;
this._disablePolling();
await NativeModules.ExponentAV.unloadAudioRecorder();
this._canRecord = false;
this._isDoneRecording = true;
_recorderExists = false;
return this.getStatusAsync(); // Automatically calls the callback for the final state.
}
// Read API
getURI(): ?string {
return this._uri;
}
async createNewLoadedSound(
initialStatus: PlaybackStatusToSet = {},
callback: ?(status: PlaybackStatus) => void = null
): Promise<{ sound: Sound, status: PlaybackStatus }> {
if (this._uri === null || !this._isDoneRecording) {
throw new Error(
'Cannot create sound when the Recording has not finished!'
);
}
return Sound.create({ uri: this._uri }, initialStatus, callback, false);
}
}