react-native-audio-api
Version:
react-native-audio-api provides system for controlling audio in React Native environment compatible with Web Audio API specification
203 lines (202 loc) • 6.31 kB
JavaScript
;
import { useCallback, useEffect, useRef, useState } from 'react';
import { Platform } from 'react-native';
import { probeDuration } from "../core/AudioFileUtils.js";
import { NotSupportedError } from "../errors/index.js";
import { NativeAudioAPIModule } from "../specs/index.js";
import { base64ToArrayBuffer, isFfmpegEnabled } from "../utils/index.js";
import { isRemoteHttpUrl, loadRemoteHttpSource } from "../utils/remoteHttpSource.js";
import { AudioFileSourceNode } from "./AudioFileSourceNode.js";
import { getSourceHeaders } from "./utils.js";
export function useAudioSourceLoader({
context,
path,
source,
preloadMode,
forceDownload,
loop,
playbackRate,
preservesPitch,
autoPlay,
effectiveVolumeRef,
onLoadStart,
onLoad,
onError,
onEnded,
onAutoPlay
}) {
const [ready, setReady] = useState(false);
const [duration, setDuration] = useState(0);
const fileSourceRef = useRef(null);
const sourceRef = useRef(null);
const isFetchingCancelled = useRef(false);
const fullDataFetched = useRef(false);
const onLoadStartRef = useRef(onLoadStart);
onLoadStartRef.current = onLoadStart;
const onLoadRef = useRef(onLoad);
onLoadRef.current = onLoad;
const onErrorRef = useRef(onError);
onErrorRef.current = onError;
const onEndedRef = useRef(onEnded);
onEndedRef.current = onEnded;
const onAutoPlayRef = useRef(onAutoPlay);
onAutoPlayRef.current = onAutoPlay;
const playbackRateRef = useRef(playbackRate);
playbackRateRef.current = playbackRate;
const preservesPitchRef = useRef(preservesPitch);
preservesPitchRef.current = preservesPitch;
const disposeSource = useCallback(() => {
fileSourceRef.current?.stopPositionTracking();
fileSourceRef.current?.dispose();
sourceRef.current = null;
fullDataFetched.current = false;
}, []);
const spawnFileSource = useCallback(() => {
const nextSource = sourceRef.current;
if (!context || !nextSource) {
return false;
}
fileSourceRef.current?.dispose();
const initialVolume = effectiveVolumeRef.current;
const initialPlaybackRate = playbackRateRef.current;
const initialPreservesPitch = preservesPitchRef.current;
const node = context.context.createFileSource({
source: nextSource,
headers: getSourceHeaders(source),
loop,
volume: initialVolume,
playbackRate: initialPlaybackRate,
preservesPitch: initialPreservesPitch
});
if (!node) {
onErrorRef.current(new NotSupportedError('This file format requires FFmpeg build'));
return false;
}
const fileSource = new AudioFileSourceNode(context, node);
const {
duration: nextDuration
} = fileSource.attach({
loop,
onEnded: () => {
onEndedRef.current(nextDuration);
spawnFileSource();
}
});
fileSource.setVolume(initialVolume);
fileSource.setPlaybackRate(initialPlaybackRate);
fileSource.setPreservesPitch(initialPreservesPitch);
fileSourceRef.current = fileSource;
setDuration(nextDuration);
onLoadRef.current();
if (autoPlay) {
onAutoPlayRef.current();
}
return true;
}, [autoPlay, context, effectiveVolumeRef, loop, source]);
const loadPlaybackSource = useCallback(async () => {
if (!path) {
return false;
}
isFetchingCancelled.current = false;
setReady(false);
onLoadStartRef.current();
const headers = getSourceHeaders(source);
try {
if (isRemoteHttpUrl(path)) {
sourceRef.current = await loadRemoteHttpSource(path, headers, forceDownload);
} else if (Platform.OS === 'android' && !__DEV__ && !path.startsWith('file://')) {
const base64Payload = await NativeAudioAPIModule.readAndroidReleaseAssetBytesAsBase64(path);
sourceRef.current = base64ToArrayBuffer(base64Payload);
} else if (path.startsWith('file://')) {
sourceRef.current = path.replace('file://', '');
} else {
sourceRef.current = path;
}
fullDataFetched.current = true;
if (!isFetchingCancelled.current && spawnFileSource()) {
setReady(true);
return true;
}
return false;
} catch (error) {
if (!isFetchingCancelled.current) {
onErrorRef.current(error);
}
setReady(false);
return false;
}
}, [forceDownload, path, source, spawnFileSource]);
const probeMetadataOnly = useCallback(async () => {
if (!isRemoteHttpUrl(path)) {
setReady(true);
return;
}
isFetchingCancelled.current = false;
setReady(false);
onLoadStartRef.current();
const headers = getSourceHeaders(source);
try {
const probedDuration = await probeDuration(path, context?.sampleRate, headers);
if (probedDuration && !isFetchingCancelled.current) {
setDuration(probedDuration);
}
setReady(true);
} catch (error) {
if (!isFetchingCancelled.current) {
onErrorRef.current(error);
}
setReady(false);
}
}, [context?.sampleRate, path, source]);
const loadForPlayback = useCallback(async () => {
if (fullDataFetched.current) {
return fileSourceRef.current != null;
}
return loadPlaybackSource();
}, [loadPlaybackSource]);
useEffect(() => {
isFetchingCancelled.current = false;
fullDataFetched.current = false;
if (!path) {
setReady(false);
setDuration(0);
disposeSource();
return () => {
isFetchingCancelled.current = true;
disposeSource();
};
}
if (preloadMode === 'none') {
setReady(true);
return () => {
isFetchingCancelled.current = true;
disposeSource();
};
}
if (preloadMode === 'metadata') {
if (!isFfmpegEnabled()) {
setReady(true);
} else {
probeMetadataOnly();
}
return () => {
isFetchingCancelled.current = true;
disposeSource();
};
}
loadPlaybackSource();
return () => {
isFetchingCancelled.current = true;
disposeSource();
};
}, [disposeSource, loadPlaybackSource, path, preloadMode, probeMetadataOnly]);
return {
fileSourceRef,
ready,
duration,
setDuration,
loadForPlayback,
disposeSource
};
}
//# sourceMappingURL=useAudioSourceLoader.js.map