Expo-av音频仅首次播放,再次点击按钮无法播放求助
问题解决:Expo-av音频播放结束后无法再次播放
核心问题分析
- 音频播放完毕后,播放位置停留在末尾,未重置到起始点,导致再次调用
playAsync()时无反应 - 使用
useState存储Sound实例会导致引用不稳定,组件重渲染时易出现异常 - 状态更新未用函数式写法,存在闭包陷阱,导致状态不同步
- 未监听播放结束事件,无法及时同步播放状态
修复后的完整代码
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
相关产品推荐
相关产品推荐

