react-native-audio-waveform
react-native-audio-waveform copied to clipboard
`stopAllPlayers` doesn't update the states of stoped
Hello, I found this issue when I have a list of Audio players.
And I need to maintain single Audio playing at once, so tried to use this function:
const { stopAllPlayers } = useAudioPlayer();
It works and stop the audio's playing, but with out change the state of playerState.
Thanks!
This is my player code if needed:
import { useState, useEffect, useRef } from 'react';
import { Box } from '@/components/ui/box';
import { Text } from '@/components/ui/text';
import { ActivityIndicator, Pressable, StyleSheet, View } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import {
Waveform,
type IWaveformRef,
PlayerState,
FinishMode,
useAudioPlayer,
PlaybackSpeedType,
} from '@simform_solutions/react-native-audio-waveform';
import { useDownloadAttachments } from '@/features/home/components/new-segment-card/hooks/useDownloadAttachments';
import { useColorModeValue } from '@/core/constants/app.colors';
import type { AttachmentDto } from '@/core/types/dtos/attachments.dto';
import * as FileSystem from 'expo-file-system';
import { Button, ButtonText } from '@/components/ui/button';
import { PressableScale } from 'pressto';
interface AudioPlayerProps {
audioUri: string;
attachment: AttachmentDto;
isVisible: boolean;
textForDebug?: string;
}
export const WavedAudioTrack = ({
audioUri,
attachment,
isVisible,
textForDebug,
}: AudioPlayerProps) => {
const waveFormRef = useRef<IWaveformRef>(null);
const [isLoading, setIsLoading] = useState(false);
const [localUri, setLocalUri] = useState<string | null>(null);
const [playerState, setPlayerState] = useState<PlayerState | undefined>(undefined);
const [isDownloading, setIsDownloading] = useState(false);
const [downloadProgress, setDownloadProgress] = useState(0);
const [error, setError] = useState<string | null>(null);
const [duration, setDuration] = useState(attachment?.duration ? attachment.duration * 1000 : 0);
const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState<PlaybackSpeedType>(1.0);
const [currentPosition, setCurrentPosition] = useState(0);
const { stopAllPlayers } = useAudioPlayer();
// Get theme colors
const backgroundColor = useColorModeValue('background-100', 'background-800');
const waveColor = useColorModeValue('info-500', 'info-400');
const scrubColor = useColorModeValue('info-600', 'info-300');
const textColor = useColorModeValue('typography-700', 'typography-300');
const iconColor = useColorModeValue('info-500', 'info-400');
const speedBgColor = useColorModeValue('info-500', 'info-400');
const handleDownload = async () => {
try {
setIsDownloading(true);
setError(null);
if (audioUri.startsWith('https://')) {
// Download the audio file from the remote URL
const fileUri = `${FileSystem.cacheDirectory}${audioUri.split('/').pop()}`;
const { exists } = await FileSystem.getInfoAsync(fileUri);
if (!exists) {
// If file doesn't exist locally, download it
const downloadResumable = FileSystem.createDownloadResumable(
audioUri,
fileUri,
{},
downloadProgress => {
const progress =
downloadProgress.totalBytesWritten / downloadProgress.totalBytesExpectedToWrite;
setDownloadProgress(progress);
},
);
const downloadResult = await downloadResumable.downloadAsync();
if (downloadResult) {
setLocalUri(downloadResult.uri);
}
// Update the duration with the attachment duration
setDuration(attachment?.duration ? attachment.duration * 1000 : 0);
} else {
setLocalUri(fileUri);
}
} else {
// If it's a local file, use it directly
setLocalUri(audioUri);
}
} catch (error) {
console.error('Error fetching or downloading audio:', error);
setError('Failed to download audio file');
} finally {
setIsDownloading(false);
}
};
const formatTime = (milliseconds: number) => {
if (isNaN(milliseconds) || milliseconds < 0) return '0:00';
const seconds = milliseconds / 1000;
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
};
const renderError = () => {
if (!error) return null;
return (
<Box style={styles.errorContainer}>
<Text style={styles.errorText}>{error}</Text>
<Pressable onPress={handleDownload} style={[styles.actionButton, styles.retryButton]}>
<Text style={{ color: 'white' }}>Retry</Text>
</Pressable>
</Box>
);
};
const changeSpeed = async () => {
if (!waveFormRef.current) return;
const speedSequence: PlaybackSpeedType[] = [1.0, 1.5, 2.0];
const nextSpeed =
speedSequence[(speedSequence.indexOf(currentPlaybackSpeed) + 1) % speedSequence.length] ||
1.0;
setCurrentPlaybackSpeed(nextSpeed);
// Note: The playback speed will be handled by the Waveform component through its props
};
const togglePlayPause = async () => {
if (playerState !== 'playing') {
waveFormRef.current?.startPlayer();
} else {
waveFormRef.current?.pausePlayer();
}
};
const handleOnProgressChange = (currentProgress: number, songDuration: number) => {
setCurrentPosition(currentProgress);
setDuration(songDuration);
};
const handleOnStateChange = (playerState: PlayerState) => {
console.log('playerState', playerState, textForDebug);
setPlayerState(playerState);
};
const renderWaveformOrPlaceholder = () => {
if (localUri) {
return (
<Waveform
ref={waveFormRef}
mode="static"
path={localUri}
candleSpace={2}
candleWidth={4}
scrubColor={scrubColor}
containerStyle={styles.waveform}
waveColor={waveColor}
onPlayerStateChange={handleOnStateChange}
playbackSpeed={currentPlaybackSpeed}
onCurrentProgressChange={handleOnProgressChange}
/>
);
} else {
return (
<View style={styles.placeholderWaveform}>
{isDownloading ? (
<>
<Text style={[styles.placeholderText, { color: textColor }]}>
Downloading... {Math.round(downloadProgress * 100)}%
</Text>
<View style={styles.progressBarContainer}>
<View
style={[
styles.progressBar,
{ width: `${Math.round(downloadProgress * 100)}%`, backgroundColor: waveColor },
]}
/>
</View>
</>
) : (
<Text style={[styles.downloadText, { color: textColor }]}>
Download it first to play
</Text>
)}
</View>
);
}
};
const renderLeftControls = () => {
return (
<Box style={styles.leftControls}>
{localUri && (
<PressableScale hitSlop={10} onPress={togglePlayPause} style={styles.actionButton}>
<Ionicons
name={playerState !== 'playing' ? 'play' : 'pause'}
size={28}
color={iconColor}
/>
</PressableScale>
)}
{!localUri && (
<View style={styles.actionButton}>
<PressableScale hitSlop={10} onPress={handleDownload} style={styles.downloadButton}>
<Ionicons name="arrow-down-circle" size={28} color={iconColor} />
</PressableScale>
</View>
)}
<View style={styles.timeContainer}>
<Text style={[styles.timeText, { color: textColor }]}>
{formatTime(
localUri ? currentPosition : attachment?.duration ? attachment.duration * 1000 : 0,
)}
</Text>
</View>
</Box>
);
};
const renderRightControls = () => {
return (
<Box style={styles.rightControls}>
{localUri && !isDownloading && !isLoading ? (
<>
<PressableScale
hitSlop={10}
onPress={changeSpeed}
style={[styles.speedButton, { backgroundColor: speedBgColor }]}
>
<Text style={styles.speedText}>{currentPlaybackSpeed}x</Text>
</PressableScale>
<View style={styles.timeContainer}>
<Text style={[styles.timeText, { color: textColor }]}>{formatTime(duration)}</Text>
</View>
</>
) : null}
</Box>
);
};
return (
<Box style={[styles.audioPlayerContainer, { backgroundColor }]}>
{/* {renderError()} */}
{/* Left Controls */}
{renderLeftControls()}
{/* Waveform or Placeholder */}
<Box style={styles.waveformContainer}>{renderWaveformOrPlaceholder()}</Box>
{/* Right side - Speed control */}
{renderRightControls()}
</Box>
);
};
const styles = StyleSheet.create({
audioPlayerContainer: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 20,
padding: 8,
width: '90%',
minHeight: 75,
marginVertical: 8,
},
leftControls: {
width: 40,
alignItems: 'center',
justifyContent: 'center',
},
actionButton: {
// padding: 6,
},
waveformContainer: {
flex: 1,
marginHorizontal: 8,
},
waveform: {
width: '100%',
height: 40,
},
timeContainer: {
alignItems: 'center',
},
timeText: {
fontSize: 12,
},
rightControls: {
width: 40,
alignItems: 'center',
justifyContent: 'center',
},
speedButton: {
borderRadius: 12,
paddingHorizontal: 8,
paddingVertical: 4,
},
speedText: {
color: 'white',
fontSize: 12,
fontWeight: 'bold',
},
errorContainer: {
backgroundColor: '#FFEBEE',
padding: 10,
borderRadius: 8,
marginBottom: 10,
width: '100%',
},
errorText: {
color: '#D32F2F',
marginBottom: 8,
},
retryButton: {
backgroundColor: '#D32F2F',
},
downloadProgressContainer: {
marginBottom: 10,
},
progressText: {
textAlign: 'center',
marginTop: 4,
},
placeholderWaveform: {
height: 40,
justifyContent: 'center',
alignItems: 'center',
width: '100%',
},
placeholderText: {
fontSize: 12,
marginBottom: 4,
},
progressBarContainer: {
width: '100%',
height: 10,
backgroundColor: '#E0E0E0',
borderRadius: 5,
overflow: 'hidden',
},
progressBar: {
height: '100%',
},
downloadButton: {
// flexDirection: 'row',
// alignItems: 'center',
// justifyContent: 'center',
// width: '100%',
// height: '100%',
},
downloadText: {
fontSize: 14,
marginLeft: 8,
},
});
Any Updates? 🤔
After analyzing the native implementation of the code, I discovered that the stopAllPlayers function is not notifying the React Native UI component about status updates. With careful review, I created a patch that resolves this issue - when using stopAllPlayers, it now properly stops all players while also updating the UI status.
at: patchs/@simform_solutions+react-native-audio-waveform+2.1.5.patch
diff --git a/node_modules/@simform_solutions/react-native-audio-waveform/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt b/node_modules/@simform_solutions/react-native-audio-waveform/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt
index 25b9376..3b22d19 100644
--- a/node_modules/@simform_solutions/react-native-audio-waveform/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt
+++ b/node_modules/@simform_solutions/react-native-audio-waveform/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt
@@ -237,8 +237,18 @@ class AudioWaveformModule(context: ReactApplicationContext): ReactContextBaseJav
@ReactMethod
fun stopAllPlayers(promise: Promise) {
try {
- audioPlayers.values.forEach{
- player -> player?.stop()
+ audioPlayers.forEach { (key, player) ->
+ player?.let {
+ // Emit event before stopping the player to notify UI
+ val args: WritableMap = Arguments.createMap()
+ args.putInt(Constants.finishType, 2) // Using 2 for Stop mode as in player.stop()
+ args.putString(Constants.playerKey, key)
+ reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
+ ?.emit("onDidFinishPlayingAudio", args)
+
+ // Stop the player
+ it.stop()
+ }
}
audioPlayers.clear()
promise.resolve(true)
diff --git a/node_modules/@simform_solutions/react-native-audio-waveform/ios/AudioWaveform.swift b/node_modules/@simform_solutions/react-native-audio-waveform/ios/AudioWaveform.swift
index 6ccb19e..a2b736b 100644
--- a/node_modules/@simform_solutions/react-native-audio-waveform/ios/AudioWaveform.swift
+++ b/node_modules/@simform_solutions/react-native-audio-waveform/ios/AudioWaveform.swift
@@ -230,8 +230,15 @@ class AudioWaveform: RCTEventEmitter {
}
@objc func stopAllPlayers(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) -> Void {
- for (playerKey,_) in audioPlayers{
- audioPlayers[playerKey]?.stopPlayer()
+ for (playerKey, player) in audioPlayers {
+ // Emit event before stopping the player to notify UI
+ EventEmitter.sharedInstance.dispatch(
+ name: Constants.onDidFinishPlayingAudio,
+ body: [Constants.finishType: FinishMode.stop.rawValue, Constants.playerKey: playerKey]
+ )
+
+ // Stop the player
+ player.stopPlayer()
}
audioPlayers.removeAll()
resolve(true)
@manssorr is this working smoothly in both platforms? this seems great and i want to apply it
@manssorr is this working smoothly in both platforms? this seems great and i want to apply it
For me it works super well!
Thanks for your work man, you should make a pr for this.
Thanks for your work man, you should make a pr for this.
let me know after you test! And maybe I can do it as PR!