You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现Supabase存储中多Blob音频的选择与播放?

问题分析与修复方案

你的代码存在几个核心问题导致音频无法播放:

  • urls是普通变量而非React状态,组件重渲染时会丢失数据,且获取Blob URL后无法触发组件更新,导致audio标签拿不到有效src
  • 每个GeneratedClones组件都重复拉取所有音频文件,存在冗余请求
  • 点击事件未传递当前文件的正确标识,且依赖异步状态playAudio判断播放状态会有延迟
  • props传递的id是文件ID,不是文件名,urls[id.name]属于错误的取值方式

修复步骤

1. 父组件修改:传递文件名而非ID

在渲染GeneratedClones时,直接传递文件的完整名称(Supabase下载需要文件名):

{/* Displaying already generated audios */}
<div id="generated" className="pl-[10%] pt-[7.5%]">
    <Card id="generated-card" className="w-[450px] h-[73.5%]">
      <CardHeader>
        <CardTitle>Generated clones</CardTitle>
        <CardDescription>Here you can see your already generated clones.</CardDescription>
      </CardHeader>
      <CardContent className="space-y-3">
        {audioFiles.length > 0 ? (
          audioFiles.map((file) => {
          if (file.name !== '.emptyFolderPlaceholder') {
            const nameOnly = file.name.split("-")[0];
            return (
              <GeneratedClones 
                key={file.name}
                name={nameOnly}
                timestamp={file.updated_at}
                metadata={file.metadata}
                fileName={file.name} {/* 传递完整文件名 */}
              />
            )
          }
          })
        ) : <p>you currently have no generated clones :(</p>
      }
      </CardContent>
    </Card>
  </div>

2. GeneratedClones组件重构

const GeneratedClones = ({ name, timestamp, fileName }) => {
  const { user } = useUser();
  const audioRef = useRef();
  const [isPlaying, setIsPlaying] = useState(false);
  const [audioUrl, setAudioUrl] = useState(null); // 用状态存储当前音频的Blob URL

  // 仅获取当前组件对应的音频文件
  const fetchAudioUrl = async () => {
    if (!user || !fileName) return;
    const userEmail = user.email;
    try {
      const { data: file, error } = await supabase
        .storage
        .from("audios")
        .download(`${userEmail}/${fileName}`);
      if (error) throw error;
      const url = URL.createObjectURL(file);
      setAudioUrl(url);
    } catch (err) {
      console.error("下载音频失败:", err);
    }
  };

  useEffect(() => {
    fetchAudioUrl();
    // 组件卸载时释放Blob URL,避免内存泄漏
    return () => {
      if (audioUrl) {
        URL.revokeObjectURL(audioUrl);
      }
    };
  }, [user, fileName]);

  const handlePlayClick = () => {
    if (!audioRef.current) return;
    // 直接操作audio元素,避免状态异步更新的延迟问题
    if (isPlaying) {
      audioRef.current.pause();
    } else {
      audioRef.current.play();
    }
    setIsPlaying(prev => !prev);
  };

  return (
    <div>
      <Card className="pt-2">
        <CardContent>
          <div className="flex items-center">
            <div id="generated-svg" className="pr-4" onClick={handlePlayClick}>
              {isPlaying ? (
                <svg fill="#000000" height="65px" width="65px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" stroke="#000000"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M256,0C114.617,0,0,114.615,0,256s114.617,256,256,256s256-114.615,256-256S397.383,0,256,0z M224,320 c0,8.836-7.164,16-16,16h-32c-8.836,0-16-7.164-16-16V192c0-8.836,7.164-16,16-16h32c8.836,0,16,7.164,16,16V320z M352,320 c0,8.836-7.164,16-16,16h-32c-8.836,0-16-7.164-16-16V192c0-8.836,7.164-16,16-16h32c8.836,0,16,7.164,16,16V320z"></path> </g></svg> 
              ) : (
                <svg width={65} height={65} fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 408.221 408.221" stroke="#000000"><g id="SVGRepo_bgCarrier" strokeWidth="0"></g><g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g><g id="SVGRepo_iconCarrier"> <g> <g> <path d="M204.11,0C91.388,0,0,91.388,0,204.111c0,112.725,91.388,204.11,204.11,204.11c112.729,0,204.11-91.385,204.11-204.11 C408.221,91.388,316.839,0,204.11,0z M286.547,229.971l-126.368,72.471c-17.003,9.75-30.781,1.763-30.781-17.834V140.012 c0-19.602,13.777-27.575,30.781-17.827l126.368,72.466C303.551,204.403,303.551,220.217,286.547,229.971z"></path> </g> </g> </g></svg>
              )}
            </div>
            <div id="generated-content">
              <h2 id="name" className="font-semibold text-2xl">{name}</h2>
              <p className="text-muted-foreground">Created at {timestamp}</p>
            </div>
          </div>
        </CardContent>
      </Card>
      <audio 
        ref={audioRef}
        src={audioUrl}
        preload="metadata" {/* 优化加载体验 */}
      />
    </div>
  )
};

export default GeneratedClones;

关键优化点

  • 用audioUrl状态存储Blob URL,确保组件能感知到URL变化并更新audio标签
  • 每个组件仅请求自身对应的音频文件,减少冗余请求
  • 点击事件直接操作audio元素,避免异步状态更新的延迟问题
  • 组件卸载时释放Blob URL,防止内存泄漏
  • 添加preload="metadata"属性,提前加载音频元数据提升播放响应速度

内容的提问来源于stack exchange,提问作者anon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 10:39:50