Skip to content

playerKey is not defined error #187

@dishant0406

Description

@dishant0406
import {
  Forward02Icon,
  PauseIcon,
  PlayIcon,
  RepeatIcon,
  ShuffleSquareIcon,
  VolumeHighIcon,
  VolumeMute02Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react-native";
import {
  useAudioPlayer,
  Waveform,
  type IWaveformRef,
} from "@simform_solutions/react-native-audio-waveform";
import * as FileSystem from "expo-file-system";
import React, { useEffect, useRef, useState } from "react";
import {
  ActivityIndicator,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from "react-native";

type AudioPlayerProps = {
  url: string;
};

const formatTime = (sec: number) => {
  const min = Math.floor(sec / 60);
  const s = Math.floor(sec % 60);
  return `${min}:${s < 10 ? "0" : ""}${s}`;
};

const AudioPlayer: React.FC<AudioPlayerProps> = ({ url }) => {
  const [isMuted, setIsMuted] = useState(false);
  const [isShuffled, setIsShuffled] = useState(false);
  const [localPath, setLocalPath] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);
  const [waveformLoaded, setWaveformLoaded] = useState(false);
  const [waveformError, setWaveformError] = useState<string | null>(null);
  const [isPlaying, setIsPlaying] = useState(false);
  const [duration, setDuration] = useState(0);
  const [currentPosition, setCurrentPosition] = useState(0);
  const [playerReady, setPlayerReady] = useState(false);
  const [waveformRetryCount, setWaveformRetryCount] = useState(0);

  const waveformRef = useRef<IWaveformRef>(null);
  const { seekToPlayer, setVolume, stopAllWaveFormExtractors, stopAllPlayers } =
    useAudioPlayer();

  // Download file to local cache, overwrite if exists, and verify it's valid audio
  useEffect(() => {
    let isMounted = true;
    setLoading(true);
    setWaveformLoaded(false);
    setWaveformError(null);
    setPlayerReady(false);
    setWaveformRetryCount(0);

    const setupAudio = async () => {
      try {
        // Use a stable filename based on the URL to improve caching
        const filename = `audio-${url.split("/").pop() || Date.now()}.mp3`;
        const fileUri = `${FileSystem.cacheDirectory}${filename}`;

        // Check if file already exists
        const fileInfo = await FileSystem.getInfoAsync(fileUri);

        // Only download if file doesn't exist or has no size
        if (!fileInfo.exists || fileInfo.size === 0) {
          console.log("Downloading audio file...");
          const downloadResult = await FileSystem.downloadAsync(url, fileUri);
          console.log("Download complete:", downloadResult);

          // Verify download success
          const downloadedFileInfo = await FileSystem.getInfoAsync(fileUri);
          if (!downloadedFileInfo.exists || downloadedFileInfo.size === 0) {
            throw new Error("Audio file couldn't be downloaded or is empty.");
          }
        } else {
          console.log("Using cached audio file:", fileInfo);
        }

        // Log first bytes to ensure it's not HTML
        try {
          const bytes = await FileSystem.readAsStringAsync(fileUri, {
            encoding: FileSystem.EncodingType.Base64,
            length: 100, // Read just first 100 bytes
          });
          console.log("First file bytes (base64):", bytes.slice(0, 40));

          // Simple validation that it looks like an audio file (MP3 typically starts with ID3)
          if (!bytes.startsWith("SUQz") && !bytes.startsWith("AAAA")) {
            console.warn("File doesn't appear to be a valid audio file");
          }
        } catch (e) {
          console.warn("Could not log file bytes", e);
        }

        // Wait longer to ensure file is properly flushed to disk
        setTimeout(() => {
          if (isMounted) {
            setLocalPath(fileUri);
            setLoading(false);
          }
        }, 500);
      } catch (e: any) {
        console.error("Error setting up audio:", e);
        if (isMounted) {
          setLoading(false);
          setLocalPath(null);
          setWaveformError("Failed to set up audio. " + (e?.message || ""));
        }
      }
    };

    setupAudio();

    return () => {
      isMounted = false;
    };
  }, [url]);

  // Retry waveform creation if it fails
  useEffect(() => {
    if (waveformError && waveformRetryCount < 3 && localPath) {
      console.log(
        `Retrying waveform extraction (attempt ${waveformRetryCount + 1})...`
      );
      const timer = setTimeout(() => {
        setWaveformError(null);
        setWaveformRetryCount((prev) => prev + 1);
        // Force remount of waveform component by temporarily clearing localPath
        setLocalPath(null);
        setTimeout(() => setLocalPath(localPath), 100);
      }, 1000);

      return () => clearTimeout(timer);
    }
  }, [waveformError, waveformRetryCount, localPath]);

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      stopAllWaveFormExtractors().catch((e) =>
        console.warn("Error stopping extractors:", e)
      );
      stopAllPlayers().catch((e) => console.warn("Error stopping players:", e));
    };
  }, [stopAllPlayers, stopAllWaveFormExtractors]);

  // Watch for player readiness: either waveform is loaded, or we have a playerKey
  useEffect(() => {
    if (
      waveformLoaded ||
      (waveformRef.current && waveformRef.current.playerKey)
    ) {
      setPlayerReady(true);
    } else {
      setPlayerReady(false);
    }
  }, [waveformLoaded, waveformRef.current?.playerKey]);

  // If waveform extraction fails but we have a valid path, switch to simpler player
  const useFallbackPlayer =
    waveformRetryCount >= 3 && localPath && !waveformLoaded;

  // Play/Pause logic via ref
  const onPlayPause = async () => {
    try {
      if (!playerReady || !waveformRef.current) return;

      if (isPlaying) {
        await waveformRef.current.pausePlayer();
        setIsPlaying(false);
      } else {
        await waveformRef.current.startPlayer();
        setIsPlaying(true);
      }
    } catch (error) {
      console.error("Error during play/pause:", error);
    }
  };

  // Mute/Unmute (volume)
  const onMute = async () => {
    try {
      if (
        !playerReady ||
        !waveformRef.current ||
        !waveformRef.current.playerKey
      )
        return;
      const key = waveformRef.current.playerKey;
      const newMuteState = !isMuted;
      setIsMuted(newMuteState);
      await setVolume({
        playerKey: key,
        volume: newMuteState ? 0 : 1,
      });
    } catch (error) {
      console.error("Error toggling mute:", error);
    }
  };

  // Seek
  const onSeek = async (seconds: number) => {
    try {
      if (
        !playerReady ||
        !waveformRef.current ||
        !waveformRef.current.playerKey
      )
        return;
      let newPos = currentPosition + seconds;
      if (newPos < 0) newPos = 0;
      if (newPos > duration) newPos = duration;

      await seekToPlayer({
        playerKey: waveformRef.current.playerKey,
        progress: newPos * 1000, // ms
      });
    } catch (error) {
      console.error("Error seeking:", error);
    }
  };

  const handleWaveformLoad = (loaded: boolean) => {
    console.log("Waveform loaded:", loaded);
    setWaveformLoaded(loaded);
  };

  const handleWaveformError = (error: Error) => {
    console.error("Waveform error:", error);
    setWaveformError(error.message);
  };

  if (loading || !localPath) {
    return (
      <View
        style={[styles.container, { justifyContent: "center", minHeight: 150 }]}
      >
        <ActivityIndicator color="#fff" size="large" />
        <Text style={{ color: "#fff", marginTop: 12 }}>Loading audio...</Text>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      {/* Top controls */}
      <View style={styles.topRow}>
        <TouchableOpacity onPress={onMute} disabled={!playerReady}>
          <HugeiconsIcon
            icon={isMuted ? VolumeMute02Icon : VolumeHighIcon}
            color={playerReady ? "#fff" : "#666"}
            size={28}
          />
        </TouchableOpacity>
        <TouchableOpacity onPress={() => onSeek(-10)} disabled={!playerReady}>
          <HugeiconsIcon
            icon={RepeatIcon}
            color={playerReady ? "#fff" : "#666"}
            size={28}
          />
        </TouchableOpacity>
        <TouchableOpacity
          onPress={onPlayPause}
          style={[styles.playButton, !playerReady && styles.playButtonDisabled]}
          disabled={!playerReady}
        >
          <HugeiconsIcon
            icon={isPlaying ? PauseIcon : PlayIcon}
            color={playerReady ? "#fff" : "#666"}
            size={48}
          />
        </TouchableOpacity>
        <TouchableOpacity onPress={() => onSeek(10)} disabled={!playerReady}>
          <HugeiconsIcon
            icon={Forward02Icon}
            color={playerReady ? "#fff" : "#666"}
            size={28}
          />
        </TouchableOpacity>
        <TouchableOpacity
          onPress={() => setIsShuffled((s) => !s)}
          disabled={!playerReady}
        >
          <HugeiconsIcon
            icon={ShuffleSquareIcon}
            color={isShuffled ? "#4fd1c5" : playerReady ? "#fff" : "#666"}
            size={28}
          />
        </TouchableOpacity>
      </View>

      {/* Waveform & Time */}
      <View style={styles.waveformRow}>
        <Text style={styles.timeText}>{formatTime(currentPosition)}</Text>
        <View style={styles.waveformContainer}>
          {waveformError && !useFallbackPlayer ? (
            <Text style={styles.errorText}>
              {waveformError}
              {waveformRetryCount < 3
                ? `\nRetrying... (${waveformRetryCount + 1}/3)`
                : "\nSwitching to simplified player..."}
            </Text>
          ) : (
            <Waveform
              // Use key with retryCount to force re-mount component
              key={`waveform-${waveformRetryCount}`}
              mode={useFallbackPlayer ? "none" : "static"}
              path={localPath}
              ref={waveformRef}
              waveColor="#fff"
              scrubColor="#4fd1c5"
              candleSpace={3}
              candleWidth={3}
              candleHeightScale={6}
              onChangeWaveformLoadState={handleWaveformLoad}
              onError={handleWaveformError}
              containerStyle={styles.waveformInner}
              onCurrentProgressChange={(currentProgress, songDuration) => {
                setCurrentPosition(currentProgress / 1000);
                if (songDuration > 0) {
                  setDuration(songDuration / 1000);
                }
              }}
              onPlayerStateChange={(state) => {
                setIsPlaying(state === "playing");
                console.log(`Waveform player state: ${state}`);
              }}
              // Add fallback options
              playbackOptions={{
                // Try to avoid issues with challenging files
                iosPreferWaveformExtraction: !useFallbackPlayer,
                androidExtractWaveform: !useFallbackPlayer,
              }}
            />
          )}

          {/* Overlay progress bar for fallback mode */}
          {useFallbackPlayer && (
            <View style={styles.progressBarContainer}>
              <View
                style={[
                  styles.progressBar,
                  {
                    width: `${
                      (currentPosition / Math.max(duration, 1)) * 100
                    }%`,
                  },
                ]}
              />
            </View>
          )}
        </View>
        <Text style={styles.timeText}>{formatTime(duration)}</Text>
      </View>

      {!playerReady && !waveformError && (
        <Text style={styles.generatingText}>Preparing player...</Text>
      )}

      {useFallbackPlayer && playerReady && (
        <Text style={styles.fallbackText}>Using simplified player mode</Text>
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    backgroundColor: "#20160c",
    padding: 18,
    borderRadius: 20,
    alignItems: "center",
  },
  topRow: {
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
    width: "100%",
  },
  playButton: {
    width: 72,
    height: 72,
    borderRadius: 36,
    backgroundColor: "#181008",
    alignItems: "center",
    justifyContent: "center",
  },
  playButtonDisabled: {
    backgroundColor: "#1a1610",
  },
  waveformRow: {
    flexDirection: "row",
    alignItems: "center",
    marginTop: 24,
    width: "100%",
    justifyContent: "center",
  },
  timeText: {
    color: "#fff",
    width: 50,
    textAlign: "center",
    fontSize: 16,
  },
  waveformContainer: {
    flex: 1,
    height: 100,
    position: "relative",
    justifyContent: "center",
    alignItems: "center",
    marginHorizontal: 8,
  },
  waveformInner: {
    width: "100%",
    height: 80,
    backgroundColor: "transparent",
  },
  loadingOverlay: {
    ...StyleSheet.absoluteFillObject,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "rgba(32, 22, 12, 0.5)",
  },
  generatingText: {
    color: "#aaa",
    marginTop: 8,
    fontSize: 12,
  },
  errorText: {
    color: "#ff8080",
    fontSize: 12,
    textAlign: "center",
  },
  fallbackText: {
    color: "#aaa",
    marginTop: 8,
    fontSize: 12,
  },
  progressBarContainer: {
    position: "absolute",
    bottom: 15,
    left: 0,
    right: 0,
    height: 4,
    backgroundColor: "rgba(255, 255, 255, 0.2)",
    borderRadius: 2,
  },
  progressBar: {
    height: "100%",
    backgroundColor: "#4fd1c5",
    borderRadius: 2,
  },
});

export default AudioPlayer;

Error is
Error during play/pause: [Error: Can not pause player, Player key is null]
on play pasue

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions