From 5067cc0517ac226c95bd504402ffe4de14ef3720 Mon Sep 17 00:00:00 2001 From: nilesh-simform Date: Tue, 16 Jul 2024 15:45:16 +0530 Subject: [PATCH] feat(UNT-T27088): external url support --- README.md | 3 + example/src/App.tsx | 10 ++ example/src/constants/Audios.ts | 18 ++- package.json | 5 +- src/components/Waveform/Waveform.tsx | 142 +++++++++++++++-------- src/components/Waveform/WaveformTypes.ts | 3 + 6 files changed, 131 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 8f75eed..46559db 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ You can check out the full example at [Example](./example/src/App.tsx). | mode\* | - | ✅ | ✅ | 'live' or 'static' | Type of waveform. It can be either `static` for the resource file or `live` if you want to record audio | | ref\* | - | ✅ | ✅ | IWaveformRef | Type of ref provided to waveform component. If waveform mode is `static`, some methods from ref will throw error and same for `live`.
Check [IWaveformRef](#iwaveformref-methods) for more details about which methods these refs provides. | | path\* | - | ✅ | ❌ | string | Used for `static` type. It is the resource path of an audio source file. | +| isExternalUrl | false | ✅ | ❌ | boolean | Used for `static` type. If the resource path of an audio file is a URL, then pass true; otherwise, pass false. | | candleSpace | 2 | ✅ | ✅ | number | Space between two candlesticks of waveform | | candleWidth | 5 | ✅ | ✅ | number | Width of single candlestick of waveform | | candleHeightScale | 3 | ✅ | ✅ | number | Scaling height of candlestick of waveform | @@ -138,6 +139,8 @@ You can check out the full example at [Example](./example/src/App.tsx). | onRecorderStateChange | - | ❌ | ✅ | ( recorderState : RecorderState ) => void | callback function which returns the recorder state whenever the recorder state changes. Check RecorderState for more details | | onCurrentProgressChange | - | ✅ | ❌ | ( currentProgress : number, songDuration: number ) => void | callback function, which returns current progress of audio and total song duration. | | onChangeWaveformLoadState | - | ✅ | ❌ | ( state : boolean ) => void | callback function which returns the loading state of waveform candlestick. | +| onDownloadStateChange | - | ✅ | ❌ | ( state : boolean ) => void | A callback function that returns the loading state of a file download from an external URL. | +| onDownloadProgressChange | - | ✅ | ❌ | ( currentProgress : number ) => void | Used when isExternalUrl is true; a callback function that returns the current progress of a file download from an external URL | | onError | - | ✅ | ❌ | ( error : Error ) => void | callback function which returns the error for static audio waveform | ##### Know more about [ViewStyle](https://reactnative.dev/docs/view-style-props), [PlayerState](#playerstate), and [RecorderState](#recorderstate) diff --git a/example/src/App.tsx b/example/src/App.tsx index 29ec694..4b123d0 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -41,11 +41,13 @@ const ListItem = React.memo( currentPlaying, setCurrentPlaying, onPanStateChange, + isExternalUrl = false, }: { item: ListItem; currentPlaying: string; setCurrentPlaying: Dispatch>; onPanStateChange: (value: boolean) => void; + isExternalUrl?: boolean; }) => { const ref = useRef(null); const [playerState, setPlayerState] = useState(PlayerState.stopped); @@ -116,10 +118,17 @@ const ListItem = React.memo( setCurrentPlaying(''); } }} + isExternalUrl={isExternalUrl} onPanStateChange={onPanStateChange} onError={error => { console.log(error, 'we are in example'); }} + onDownloadStateChange={state => { + console.log('Download State', state); + }} + onDownloadProgressChange={progress => { + console.log('Download Progress', `${progress}%`); + }} onCurrentProgressChange={(currentProgress, songDuration) => { console.log( 'currentProgress ', @@ -241,6 +250,7 @@ const AppContainer = () => { currentPlaying={currentPlaying} setCurrentPlaying={setCurrentPlaying} item={item} + isExternalUrl={item.isExternalUrl} onPanStateChange={value => setShouldScroll(!value)} /> ))} diff --git a/example/src/constants/Audios.ts b/example/src/constants/Audios.ts index ef9b878..5f8915c 100644 --- a/example/src/constants/Audios.ts +++ b/example/src/constants/Audios.ts @@ -5,6 +5,7 @@ import { globalMetrics } from '../../src/theme'; export interface ListItem { fromCurrentUser: boolean; path: string; + isExternalUrl?: boolean; } /** @@ -54,15 +55,28 @@ const audioAssetArray = [ 'file_example_mp3_15s.mp3', ]; +const externalAudioAssetArray = [ + 'https://codeskulptor-demos.commondatastorage.googleapis.com/GalaxyInvaders/theme_01.mp3', + 'https://codeskulptor-demos.commondatastorage.googleapis.com/pang/paza-moduless.mp3', +]; + copyFilesToAndroidResources(); /** * List of file objects with information about the files. * @type {ListItem[]} */ -export const audioListArray: ListItem[] = audioAssetArray.map( +const audioList: ListItem[] = audioAssetArray.map((value, index) => ({ + fromCurrentUser: index % 2 !== 0, + path: `${filePath}/${value}`, +})); + +const externalAudioList: ListItem[] = externalAudioAssetArray.map( (value, index) => ({ fromCurrentUser: index % 2 !== 0, - path: `${filePath}/${value}`, + path: value, + isExternalUrl: true, }) ); + +export const audioListArray: ListItem[] = [...audioList, ...externalAudioList]; diff --git a/package.json b/package.json index 2736127..24d7ecc 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ ] }, "dependencies": { - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "rn-fetch-blob": "^0.12.0" } -} \ No newline at end of file +} diff --git a/src/components/Waveform/Waveform.tsx b/src/components/Waveform/Waveform.tsx index 6a90cd0..13dadcb 100644 --- a/src/components/Waveform/Waveform.tsx +++ b/src/components/Waveform/Waveform.tsx @@ -36,6 +36,7 @@ import { type LiveWaveform, type StaticWaveform, } from './WaveformTypes'; +import RNFetchBlob from 'rn-fetch-blob'; export const Waveform = forwardRef((props, ref) => { const { @@ -53,8 +54,14 @@ export const Waveform = forwardRef((props, ref) => { onCurrentProgressChange = () => {}, candleHeightScale = 3, onChangeWaveformLoadState, + isExternalUrl = false, + onDownloadStateChange, + onDownloadProgressChange, } = props as StaticWaveform & LiveWaveform; const viewRef = useRef(null); + const [audioPath, setAudioPath] = useState( + !isExternalUrl ? path : undefined + ); const scrollRef = useRef(null); const [waveform, setWaveform] = useState([]); const [viewLayout, setViewLayout] = useState(null); @@ -86,12 +93,42 @@ export const Waveform = forwardRef((props, ref) => { const { checkHasAudioRecorderPermission } = useAudioPermission(); + useEffect(() => { + if (isExternalUrl && path) { + (onDownloadStateChange as Function)?.(true); + + RNFetchBlob.config({ + fileCache: true, + }) + .fetch('GET', path) + .progress((received, total) => { + let progressPercentage: number = Number( + ((received / total) * 100).toFixed(2) + ); + (onDownloadProgressChange as Function)?.(progressPercentage); + }) + .then(res => { + // the temp file path + setAudioPath(res.path()); + (onDownloadStateChange as Function)?.(false); + (onDownloadProgressChange as Function)?.(100); + }) + .catch(e => { + console.log(e); + (onDownloadStateChange as Function)?.(false); + }); + } else { + (onDownloadStateChange as Function)?.(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isExternalUrl, path]); + const preparePlayerForPath = async () => { - if (!isNil(path) && !isEmpty(path)) { + if (!isNil(audioPath) && !isEmpty(audioPath)) { try { const prepare = await preparePlayer({ - path, - playerKey: `PlayerFor${path}`, + path: audioPath, + playerKey: `PlayerFor${audioPath}`, updateFrequency: UpdateFrequency.medium, volume: 10, }); @@ -101,7 +138,7 @@ export const Waveform = forwardRef((props, ref) => { } } else { return Promise.reject( - new Error(`Can not start player for path: ${path}`) + new Error(`Can not start player for path: ${audioPath}`) ); } }; @@ -109,14 +146,14 @@ export const Waveform = forwardRef((props, ref) => { const getAudioDuration = async () => { try { const duration = await getDuration({ - playerKey: `PlayerFor${path}`, + playerKey: `PlayerFor${audioPath}`, durationType: DurationType.max, }); if (!isNil(duration)) { setSongDuration(duration); } else { return Promise.reject( - new Error(`Could not get duration for path: ${path}`) + new Error(`Could not get duration for path: ${audioPath}`) ); } } catch (err) { @@ -137,12 +174,12 @@ export const Waveform = forwardRef((props, ref) => { }; const getAudioWaveFormForPath = async (noOfSample: number) => { - if (!isNil(path) && !isEmpty(path)) { + if (!isNil(audioPath) && !isEmpty(audioPath)) { try { (onChangeWaveformLoadState as Function)?.(true); const result = await extractWaveformData({ - path: path, - playerKey: `PlayerFor${path}`, + path: audioPath, + playerKey: `PlayerFor${audioPath}`, noOfSamples: noOfSample, }); (onChangeWaveformLoadState as Function)?.(false); @@ -161,9 +198,11 @@ export const Waveform = forwardRef((props, ref) => { } } else { (onError as Function)( - `Can not find waveform for mode ${mode} path: ${path}` + `Can not find waveform for mode ${mode} path: ${audioPath}` + ); + console.error( + `Can not find waveform for mode ${mode} path: ${audioPath}` ); - console.error(`Can not find waveform for mode ${mode} path: ${path}`); } }; @@ -171,7 +210,7 @@ export const Waveform = forwardRef((props, ref) => { if (mode === 'static') { try { const result = await stopPlayer({ - playerKey: `PlayerFor${path}`, + playerKey: `PlayerFor${audioPath}`, }); await preparePlayerForPath(); if (!isNil(result) && result) { @@ -180,7 +219,7 @@ export const Waveform = forwardRef((props, ref) => { return Promise.resolve(result); } else { return Promise.reject( - new Error(`error in stopping player for path: ${path}`) + new Error(`error in stopping player for path: ${audioPath}`) ); } } catch (err) { @@ -198,8 +237,8 @@ export const Waveform = forwardRef((props, ref) => { try { const play = await playPlayer({ finishMode: FinishMode.stop, - playerKey: `PlayerFor${path}`, - path: path, + playerKey: `PlayerFor${audioPath}`, + path: audioPath, ...args, }); @@ -208,7 +247,7 @@ export const Waveform = forwardRef((props, ref) => { return Promise.resolve(true); } else { return Promise.reject( - new Error(`error in starting player for path: ${path}`) + new Error(`error in starting player for path: ${audioPath}`) ); } } catch (error) { @@ -225,14 +264,14 @@ export const Waveform = forwardRef((props, ref) => { if (mode === 'static') { try { const pause = await pausePlayer({ - playerKey: `PlayerFor${path}`, + playerKey: `PlayerFor${audioPath}`, }); if (pause) { setPlayerState(PlayerState.paused); return Promise.resolve(true); } else { return Promise.reject( - new Error(`error in pause player for path: ${path}`) + new Error(`error in pause player for path: ${audioPath}`) ); } } catch (error) { @@ -359,7 +398,7 @@ export const Waveform = forwardRef((props, ref) => { }; useEffect(() => { - if (!isNil(viewLayout?.width)) { + if (!isNil(viewLayout?.width) && audioPath !== undefined) { const getNumberOfSamples = floor( (viewLayout?.width ?? 0) / (candleWidth + candleSpace) ); @@ -369,10 +408,10 @@ export const Waveform = forwardRef((props, ref) => { } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [viewLayout, mode, candleWidth, candleSpace]); + }, [viewLayout, mode, candleWidth, candleSpace, audioPath]); useEffect(() => { - if (!isNil(seekPosition)) { + if (!isNil(seekPosition) && audioPath !== undefined) { if (mode === 'static') { const seekAmount = (seekPosition?.pageX - (viewLayout?.x ?? 0)) / @@ -381,7 +420,7 @@ export const Waveform = forwardRef((props, ref) => { if (!panMoving) { seekToPlayer({ - playerKey: `PlayerFor${path}`, + playerKey: `PlayerFor${audioPath}`, progress: clampedSeekAmount * songDuration, }); if (playerState === PlayerState.playing) { @@ -393,25 +432,35 @@ export const Waveform = forwardRef((props, ref) => { } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [seekPosition, panMoving, mode, songDuration]); + }, [seekPosition, panMoving, mode, songDuration, audioPath]); useEffect(() => { - const tracePlayerState = onDidFinishPlayingAudio(async data => { - if (data.playerKey === `PlayerFor${path}`) { - if (data.finishType === FinishMode.stop) { - setPlayerState(PlayerState.stopped); - setCurrentProgress(0); - await preparePlayerForPath(); + if (audioPath !== undefined) { + const tracePlayerState = onDidFinishPlayingAudio(async data => { + if (data.playerKey === `PlayerFor${audioPath}`) { + if (data.finishType === FinishMode.stop) { + setPlayerState(PlayerState.stopped); + setCurrentProgress(0); + await preparePlayerForPath(); + } } - } - }); + }); - const tracePlaybackValue = onCurrentDuration(data => { - if (data.playerKey === `PlayerFor${path}`) { - setCurrentProgress(data.currentDuration); - } - }); + const tracePlaybackValue = onCurrentDuration(data => { + if (data.playerKey === `PlayerFor${audioPath}`) { + setCurrentProgress(data.currentDuration); + } + }); + return () => { + tracePlayerState.remove(); + tracePlaybackValue.remove(); + }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [audioPath]); + + useEffect(() => { const traceRecorderWaveformValue = onCurrentRecordingWaveformData( result => { if (mode === 'live') { @@ -424,9 +473,8 @@ export const Waveform = forwardRef((props, ref) => { } } ); + return () => { - tracePlayerState.remove(); - tracePlaybackValue.remove(); traceRecorderWaveformValue.remove(); }; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -447,17 +495,19 @@ export const Waveform = forwardRef((props, ref) => { }, [recorderState]); useEffect(() => { - if (panMoving) { - if (playerState === PlayerState.playing) { - pausePlayerAction(); - } - } else { - if (playerState === PlayerState.paused) { - startPlayerAction(); + if (audioPath !== undefined) { + if (panMoving) { + if (playerState === PlayerState.playing) { + pausePlayerAction(); + } + } else { + if (playerState === PlayerState.paused) { + startPlayerAction(); + } } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [panMoving]); + }, [panMoving, audioPath]); const panResponder = useRef( PanResponder.create({ diff --git a/src/components/Waveform/WaveformTypes.ts b/src/components/Waveform/WaveformTypes.ts index 1a26154..9010f89 100644 --- a/src/components/Waveform/WaveformTypes.ts +++ b/src/components/Waveform/WaveformTypes.ts @@ -25,6 +25,9 @@ export interface StaticWaveform extends BaseWaveform { songDuration: number ) => void; onChangeWaveformLoadState?: (state: boolean) => void; + isExternalUrl?: boolean; + onDownloadStateChange?: (state: boolean) => void; + onDownloadProgressChange?: (currentProgress: number) => void; } export interface LiveWaveform extends BaseWaveform {