react-native-audio-waveform icon indicating copy to clipboard operation
react-native-audio-waveform copied to clipboard

`stopAllPlayers` doesn't update the states of stoped

Open manssorr opened this issue 8 months ago • 2 comments
trafficstars

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,
  },
});

manssorr avatar Mar 17 '25 10:03 manssorr

Any Updates? 🤔

manssorr avatar May 13 '25 00:05 manssorr

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 avatar May 13 '25 03:05 manssorr

@manssorr is this working smoothly in both platforms? this seems great and i want to apply it

anxheloo avatar Jul 18 '25 14:07 anxheloo

@manssorr is this working smoothly in both platforms? this seems great and i want to apply it

For me it works super well!

manssorr avatar Jul 18 '25 16:07 manssorr

Thanks for your work man, you should make a pr for this.

anxheloo avatar Jul 18 '25 20:07 anxheloo

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!

manssorr avatar Jul 18 '25 22:07 manssorr