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

Expo-av音频仅首次播放,再次点击按钮无法播放求助

问题解决:Expo-av音频播放结束后无法再次播放

核心问题分析

  1. 音频播放完毕后,播放位置停留在末尾,未重置到起始点,导致再次调用playAsync()时无反应
  2. 使用useState存储Sound实例会导致引用不稳定,组件重渲染时易出现异常
  3. 状态更新未用函数式写法,存在闭包陷阱,导致状态不同步
  4. 未监听播放结束事件,无法及时同步播放状态

修复后的完整代码

import { useState, useEffect, useRef } from 'react';
import { View, Pressable, Text } from 'react-native';
import { Audio, InterruptionModeAndroid, InterruptionModeIOS } from 'expo-av';
import { Slider } from '@react-native-community/slider';
import { Icon } from '@rneui/base';

function SeriesAudioPlayer() {
  const { colors } = useTheme();
  const [audioStatus, setAudioStatus] = useState({
    isPlaying: false,
    isPaused: false,
    duration: 0,
  });
  const soundRef = useRef(null); // 用useRef保存Sound实例,保持引用稳定

  const setupAudio = async () => {
    await Audio.setAudioModeAsync({
      allowsRecordingIOS: false,
      interruptionModeAndroid: InterruptionModeAndroid.DuckOthers,
      interruptionModeIOS: InterruptionModeIOS.DoNotMix,
      playsInSilentModeIOS: true,
      shouldDuckAndroid: true,
      staysActiveInBackground: true,
      playThroughEarpieceAndroid: true,
    });
  };

  useEffect(() => {
    let isMounted = true;
    const initSound = async () => {
      await setupAudio();
      const sound = new Audio.Sound();
      if (!isMounted) return;
      
      // 监听播放结束事件,自动更新状态
      sound.setOnPlaybackStatusUpdate((status) => {
        if (status.didJustFinish) {
          setAudioStatus(prev => ({ ...prev, isPlaying: false, isPaused: false }));
        }
      });

      await sound.loadAsync(
        require('../../../../../assets/audio/cat-meow.wav'),
        { shouldPlay: false },
        false
      );
      soundRef.current = sound;
    };

    initSound();

    // 组件卸载时清理资源
    return () => {
      isMounted = false;
      if (soundRef.current) {
        soundRef.current.unloadAsync();
      }
    };
  }, []);

  const playSound = async () => {
    if (!soundRef.current) return;
    // 播放前重置位置到开头
    await soundRef.current.setPositionAsync(0);
    await soundRef.current.playAsync();
    // 函数式更新状态,避免闭包问题
    setAudioStatus(prev => ({ ...prev, isPlaying: true, isPaused: false }));
  };

  const pauseSound = async () => {
    if (!soundRef.current) return;
    await soundRef.current.pauseAsync();
    setAudioStatus(prev => ({ ...prev, isPlaying: false, isPaused: true }));
  };

  return (
    <View style={styles.container}>
      <Pressable
        style={{ paddingRight: 15 }}
        onPress={audioStatus.isPlaying ? pauseSound : playSound}
      >
        {/* 根据播放状态切换图标 */}
        <Icon name={audioStatus.isPlaying ? "pause" : "play"} size={17} />
      </Pressable>

      <Text style={styles.playTime}>0:00</Text>
      <Slider
        style={styles.slider}
        minimumValue={0} // 修正最小值为0,匹配音频起始位置
        maximumValue={audioStatus.duration || 5} // 后续可从播放状态获取真实时长
        step={1}
        minimumTrackTintColor={colors.highlight}
        maximumTrackTintColor={colors.card}
        thumbTintColor={Colors.fuelOrange}
        tapToSeek={true}
      />
      <Text style={[styles.playTime, { paddingRight: 15 }]}>2:32</Text>
      <Icon name="volume" size={17} />
    </View>
  );
}

// 补充样式定义
const styles = {
  container: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 8,
  },
  playTime: {
    fontSize: 12,
    marginHorizontal: 8,
  },
  slider: {
    flex: 1,
  },
};

关键修改说明

  • 用useRef存储Sound实例:避免组件重渲染时重复创建实例,保持引用稳定,防止操作已卸载的Sound对象
  • 播放前重置位置:调用setPositionAsync(0)确保每次播放都从音频开头启动
  • 监听播放结束事件:通过setOnPlaybackStatusUpdate捕获didJustFinish状态,自动同步播放状态
  • 等待异步操作完成:playAsync、pauseAsync等方法均为异步,用await确保操作完成后再更新状态
  • 函数式状态更新:使用setAudioStatus(prev => ...)避免闭包导致的状态不同步问题
  • 组件卸载清理:添加isMounted标志防止组件卸载后执行异步操作,确保Sound实例被正确卸载

内容的提问来源于stack exchange,提问作者Nguyễn Hiếu

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 16:20:59