Skip to content

Commit 49bf4dd

Browse files
nilesh-simformprince-d-simform
authored andcommitted
feat(UNT-T27088): external url support
1 parent 1e538d0 commit 49bf4dd

File tree

9 files changed

+308
-74
lines changed

9 files changed

+308
-74
lines changed

README.md

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ Here's how to get started with react-native-audio-waveform in your React Native
3333
##### 1. Install the package
3434

3535
```sh
36-
npm install @simform_solutions/react-native-audio-waveform react-native-gesture-handler
36+
npm install @simform_solutions/react-native-audio-waveform rn-fetch-blob react-native-gesture-handler
3737
```
3838

3939
###### --- or ---
4040

4141
```sh
42-
yarn add @simform_solutions/react-native-audio-waveform react-native-gesture-handler
42+
yarn add @simform_solutions/react-native-audio-waveform rn-fetch-blob react-native-gesture-handler
4343
```
4444

4545
##### 2. Install CocoaPods in the iOS project
@@ -48,7 +48,7 @@ yarn add @simform_solutions/react-native-audio-waveform react-native-gesture-han
4848
npx pod-install
4949
```
5050

51-
##### Know more about [react-native-gesture-handler](https://www.npmjs.com/package/react-native-gesture-handler)
51+
##### Know more about [rn-fetch-blob](https://www.npmjs.com/package/rn-fetch-blob) and [react-native-gesture-handler](https://www.npmjs.com/package/react-native-gesture-handler)
5252

5353
##### 3. Add audio recording permissions
5454

@@ -90,7 +90,34 @@ const ref = useRef<IWaveformRef>(null);
9090
<Waveform
9191
mode="static"
9292
ref={ref}
93-
path={item}
93+
path={path}
94+
candleSpace={2}
95+
candleWidth={4}
96+
scrubColor="white"
97+
onPlayerStateChange={playerState => console.log(playerState)}
98+
onPanStateChange={isMoving => console.log(isMoving)}
99+
/>;
100+
```
101+
102+
When you want to show a waveform for a external audio URL, you need to use `static` mode for the waveform and set isExternalUrl to true.
103+
104+
Check the example below for more information.
105+
106+
```tsx
107+
import {
108+
Waveform,
109+
type IWaveformRef,
110+
} from '@simform_solutions/react-native-audio-waveform';
111+
112+
const url = 'https://www2.cs.uic.edu/~i101/SoundFiles/taunt.wav'; // URL to the audio file for which you want to show waveform
113+
const ref = useRef<IWaveformRef>(null);
114+
<Waveform
115+
mode="static"
116+
ref={ref}
117+
path={url}
118+
isExternalUrl={true}
119+
onDownloadStateChange={state => console.log(state)}
120+
onDownloadProgressChange={progress => console.log(progress)}
94121
candleSpace={2}
95122
candleWidth={4}
96123
scrubColor="white"
@@ -133,6 +160,9 @@ You can check out the full example at [Example](./example/src/App.tsx).
133160
| 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`.<br> Check [IWaveformRef](#iwaveformref-methods) for more details about which methods these refs provides. |
134161
| path\* | - ||| string | Used for `static` type. It is the resource path of an audio source file. |
135162
| playbackSpeed | 1.0 ||| 1.0 / 1.5 / 2.0 | The playback speed of the audio player. Note: Currently playback speed only supports, Normal (1x) Faster(1.5x) and Fastest(2.0x), any value passed to playback speed greater than 2.0 will be automatically adjusted to normal playback speed |
163+
| volume | 3 ||| number | Used for `static` type. It is a volume level for the media player, ranging from 1 to 10. |
164+
| isExternalUrl | false ||| boolean | Used for `static` type. If the resource path of an audio file is a URL, then pass true; otherwise, pass false. |
165+
| downloadExternalAudio | true ||| boolean | Used for `static` type. Indicates whether the external media should be downloaded. |
136166
| candleSpace | 2 ||| number | Space between two candlesticks of waveform |
137167
| candleWidth | 5 ||| number | Width of single candlestick of waveform |
138168
| candleHeightScale | 3 ||| number | Scaling height of candlestick of waveform |
@@ -145,6 +175,8 @@ You can check out the full example at [Example](./example/src/App.tsx).
145175
| onRecorderStateChange | - ||| ( recorderState : RecorderState ) => void | callback function which returns the recorder state whenever the recorder state changes. Check RecorderState for more details |
146176
| onCurrentProgressChange | - ||| ( currentProgress : number, songDuration: number ) => void | callback function, which returns current progress of audio and total song duration. |
147177
| onChangeWaveformLoadState | - ||| ( state : boolean ) => void | callback function which returns the loading state of waveform candlestick. |
178+
| onDownloadStateChange | - ||| ( state : boolean ) => void | A callback function that returns the loading state of a file download from an external URL. |
179+
| 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 |
148180
| onError | - ||| ( error : Error ) => void | callback function which returns the error for static audio waveform |
149181

150182
##### Know more about [ViewStyle](https://reactnative.dev/docs/view-style-props), [PlayerState](#playerstate), and [RecorderState](#recorderstate)

example/src/App.tsx

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
ScrollView,
2626
StatusBar,
2727
Text,
28+
TouchableOpacity,
2829
View,
2930
} from 'react-native';
3031
import { GestureHandlerRootView } from 'react-native-gesture-handler';
@@ -52,27 +53,40 @@ const RenderListItem = React.memo(
5253
onPanStateChange,
5354
currentPlaybackSpeed,
5455
changeSpeed,
56+
isExternalUrl = false,
5557
}: {
5658
item: ListItem;
5759
currentPlaying: string;
5860
setCurrentPlaying: Dispatch<SetStateAction<string>>;
5961
onPanStateChange: (value: boolean) => void;
6062
currentPlaybackSpeed: PlaybackSpeedType;
6163
changeSpeed: () => void;
64+
isExternalUrl?: boolean;
6265
}) => {
6366
const ref = useRef<IWaveformRef>(null);
6467
const [playerState, setPlayerState] = useState(PlayerState.stopped);
6568
const styles = stylesheet({ currentUser: item.fromCurrentUser });
66-
const [isLoading, setIsLoading] = useState(true);
69+
const [isLoading, setIsLoading] = useState(isExternalUrl ? false : true);
70+
const [downloadExternalAudio, setDownloadExternalAudio] = useState(false);
71+
const [isAudioDownloaded, setIsAudioDownloaded] = useState(false);
6772

68-
const handleButtonAction = () => {
73+
const handleButtonAction = (): void => {
6974
if (playerState === PlayerState.stopped) {
7075
setCurrentPlaying(item.path);
7176
} else {
7277
setCurrentPlaying('');
7378
}
7479
};
7580

81+
const handleDownloadPress = (): void => {
82+
setDownloadExternalAudio(true);
83+
if (currentPlaying === item.path) {
84+
setCurrentPlaying('');
85+
}
86+
87+
setIsLoading(true);
88+
};
89+
7690
useEffect(() => {
7791
if (currentPlaying !== item.path) {
7892
ref.current?.stopPlayer();
@@ -82,15 +96,23 @@ const RenderListItem = React.memo(
8296
}, [currentPlaying]);
8397

8498
return (
85-
<View key={item.path} style={[styles.listItemContainer]}>
99+
<View
100+
key={item.path}
101+
style={[
102+
styles.listItemContainer,
103+
item.fromCurrentUser &&
104+
isExternalUrl &&
105+
!isAudioDownloaded &&
106+
styles.listItemReverseContainer,
107+
]}>
86108
<View style={styles.listItemWidth}>
87109
<View style={[styles.buttonContainer]}>
88110
<Pressable
89111
disabled={isLoading}
90112
onPress={handleButtonAction}
91113
style={styles.playBackControlPressable}>
92114
{isLoading ? (
93-
<ActivityIndicator color={'#FF0000'} />
115+
<ActivityIndicator color={'#FFFFFF'} />
94116
) : (
95117
<FastImage
96118
source={
@@ -115,6 +137,7 @@ const RenderListItem = React.memo(
115137
scrubColor={Colors.white}
116138
waveColor={Colors.lightWhite}
117139
candleHeightScale={4}
140+
downloadExternalAudio={downloadExternalAudio}
118141
onPlayerStateChange={state => {
119142
setPlayerState(state);
120143
if (
@@ -124,10 +147,20 @@ const RenderListItem = React.memo(
124147
setCurrentPlaying('');
125148
}
126149
}}
150+
isExternalUrl={isExternalUrl}
127151
onPanStateChange={onPanStateChange}
128152
onError={error => {
129153
console.log(error, 'we are in example');
130154
}}
155+
onDownloadStateChange={state => {
156+
console.log('Download State', state);
157+
}}
158+
onDownloadProgressChange={progress => {
159+
console.log('Download Progress', `${progress}%`);
160+
if (progress === 100) {
161+
setIsAudioDownloaded(true);
162+
}
163+
}}
131164
onCurrentProgressChange={(currentProgress, songDuration) => {
132165
console.log(
133166
'currentProgress ',
@@ -151,6 +184,15 @@ const RenderListItem = React.memo(
151184
)}
152185
</View>
153186
</View>
187+
{isExternalUrl && !downloadExternalAudio && !isAudioDownloaded ? (
188+
<TouchableOpacity onPress={handleDownloadPress}>
189+
<Image
190+
source={Icons.download}
191+
style={styles.downloadIcon}
192+
resizeMode="contain"
193+
/>
194+
</TouchableOpacity>
195+
) : null}
154196
</View>
155197
);
156198
}
@@ -328,6 +370,7 @@ const AppContainer = () => {
328370
currentPlaying={currentPlaying}
329371
setCurrentPlaying={setCurrentPlaying}
330372
item={item}
373+
isExternalUrl={item.isExternalUrl}
331374
onPanStateChange={value => setShouldScroll(!value)}
332375
{...{ currentPlaybackSpeed, changeSpeed }}
333376
/>
8.4 KB
Loading

example/src/assets/icons/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export const Icons = {
55
mic: require('./mic.png'),
66
logo: require('./logo.png'),
77
delete: require('./delete.png'),
8+
download: require('./download.png'),
89
};

example/src/constants/Audios.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Platform } from 'react-native';
66
export interface ListItem {
77
fromCurrentUser: boolean;
88
path: string;
9+
isExternalUrl?: boolean;
910
}
1011

1112
/**
@@ -70,30 +71,56 @@ const audioAssetArray = [
7071
'file_example_mp3_15s.mp3',
7172
];
7273

74+
const externalAudioAssetArray = [
75+
'https://codeskulptor-demos.commondatastorage.googleapis.com/GalaxyInvaders/theme_01.mp3',
76+
'https://codeskulptor-demos.commondatastorage.googleapis.com/pang/paza-moduless.mp3',
77+
];
78+
7379
/**
7480
* Retrieve previously recorded audio files from the cache/document directory.
75-
* @returns
81+
* @returns
7682
*/
7783
export const getRecordedAudios = async (): Promise<string[]> => {
78-
const recordingSavingPath = Platform.select({ ios: fs.DocumentDirectoryPath, default: fs.CachesDirectoryPath })
84+
const recordingSavingPath = Platform.select({
85+
ios: fs.DocumentDirectoryPath,
86+
default: fs.CachesDirectoryPath,
87+
});
7988

80-
const items = await fs.readDir(recordingSavingPath)
81-
return items.filter(item => item.path.endsWith('.m4a')).map(item => item.path)
82-
}
89+
const items = await fs.readDir(recordingSavingPath);
90+
return items
91+
.filter(item => item.path.endsWith('.m4a'))
92+
.map(item => item.path);
93+
};
8394

8495
/**
8596
* Generate a list of file objects with information about successfully copied files (Android)
8697
* or all files (iOS).
8798
* @returns {Promise<ListItem[]>} A Promise that resolves to the list of file objects.
8899
*/
89100
export const generateAudioList = async (): Promise<ListItem[]> => {
90-
const audioAssetPaths = (await copyFilesToNativeResources()).map(value => `${filePath}/${value}`);
91-
const recordedAudios = await getRecordedAudios()
101+
const audioAssetPaths = (await copyFilesToNativeResources()).map(
102+
value => `${filePath}/${value}`
103+
);
104+
const recordedAudios = await getRecordedAudios();
92105

93106
// Generate the final list based on the copied or available files
94-
return [...audioAssetPaths, ...recordedAudios].map?.((value, index) => ({
95-
fromCurrentUser: index % 2 !== 0,
107+
const localAssetList = [...audioAssetPaths, ...recordedAudios].map?.(
108+
value => ({
109+
path: value,
110+
})
111+
);
112+
113+
const externalAudioList = externalAudioAssetArray.map(value => ({
96114
path: value,
115+
isExternalUrl: true,
97116
}));
98117

118+
const finalAudios = [...localAssetList, ...externalAudioList].map(
119+
(value, index) => ({
120+
...value,
121+
fromCurrentUser: index % 2 !== 0,
122+
})
123+
);
124+
125+
return finalAudios;
99126
};

example/src/styles.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,16 @@ const styles = (params: StyleSheetParams = {}) =>
4343
},
4444
listItemContainer: {
4545
marginTop: scale(16),
46-
alignItems: params.currentUser ? 'flex-end' : 'flex-start',
46+
flexDirection: 'row',
47+
justifyContent: params.currentUser ? 'flex-end' : 'flex-start',
48+
alignItems: 'center',
49+
},
50+
listItemReverseContainer: {
51+
flexDirection: 'row-reverse',
52+
alignSelf: 'flex-end',
4753
},
4854
listItemWidth: {
49-
width: '90%',
55+
width: '88%',
5056
},
5157
buttonImage: {
5258
height: scale(22),
@@ -129,6 +135,13 @@ const styles = (params: StyleSheetParams = {}) =>
129135
textAlign: 'center',
130136
fontWeight: '600',
131137
},
138+
downloadIcon: {
139+
width: 20,
140+
height: 20,
141+
tintColor: Colors.pink,
142+
marginLeft: 10,
143+
marginRight: 10,
144+
},
132145
});
133146

134147
export default styles;

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
]
111111
},
112112
"dependencies": {
113-
"lodash": "^4.17.21"
113+
"lodash": "^4.17.21",
114+
"rn-fetch-blob": "^0.12.0"
114115
}
115-
}
116+
}

0 commit comments

Comments
 (0)