React Native+Expo-Audio实现WhatsApp式语音录制:暂停后无法续录求助
问题解决:暂停录制后无法继续录制的修复
你的代码核心问题是恢复录制时调用了错误的方法:在toggleRecording函数中,恢复暂停的录制应该用resumeAsync(),而不是startAsync()——startAsync()是用来启动全新录制的,并非恢复已暂停的录制。
同时补充几个细节优化,确保录制状态和功能完全符合预期:
关键修改点
1. 修复录制暂停/恢复逻辑
将toggleRecording里恢复录制的代码替换为resumeAsync(),并添加录制对象存在性校验:
async function toggleRecording() { if (!recording) return; // 避免录制未初始化时执行 try { if (isRecording) { await recording.pauseAsync(); } else { await recording.resumeAsync(); // 替换原有的startAsync() } setIsRecording(!isRecording); } catch (error) { console.error("Failed to toggle recording", error); } }
2. 完善删除录制功能
给删除按钮添加完整的资源清理逻辑,重置所有录制相关状态:
async function deleteRecording() { try { if (recording) { await recording.stopAndUnloadAsync(); setRecording(null); } if (currentPlaybackSound) { await currentPlaybackSound.unloadAsync(); setCurrentPlaybackSound(null); } setSound(null); setIsRecording(false); setIsPlaying(false); setPlaybackPosition(0); // 删除后自动重新开始录制,模拟WhatsApp逻辑 startRecording(); } catch (error) { console.error("Failed to delete recording", error); } }
然后给删除按钮绑定这个函数:
<TouchableOpacity onPress={deleteRecording}> <MaterialCommunityIcons name="delete" size={28} color="#cfd1d5" /> </TouchableOpacity>
3. 修复播放逻辑内存泄漏
当前播放逻辑每次调用都会创建新的Sound对象,容易导致内存泄漏。新增状态存储当前播放对象,并在播放前卸载旧对象:
// 新增状态存储当前播放的Sound对象 const [currentPlaybackSound, setCurrentPlaybackSound] = useState(null); async function playSound() { try { if (!sound) return; // 先卸载之前的播放对象 if (currentPlaybackSound) { await currentPlaybackSound.unloadAsync(); } const { sound: playbackObject } = await Audio.Sound.createAsync( { uri: sound }, { shouldPlay: true, position: playbackPosition } ); setCurrentPlaybackSound(playbackObject); setIsPlaying(true); playbackObject.setOnPlaybackStatusUpdate((status) => { if (status.didJustFinish) { setIsPlaying(false); setPlaybackPosition(0); playbackObject.unloadAsync(); setCurrentPlaybackSound(null); } else { setPlaybackPosition(status.positionMillis); } }); } catch (error) { console.error("Failed to play sound", error); } }
完整修改后的组件代码
import React, { useEffect, useState } from "react"; import { View, Text, TouchableOpacity } from "react-native"; import { MaterialCommunityIcons } from "@expo/vector-icons"; import { Audio } from "expo-av"; function AudioMessage({ ShowAudioModal }) { const [recording, setRecording] = useState(null); const [sound, setSound] = useState(null); const [currentPlaybackSound, setCurrentPlaybackSound] = useState(null); const [isRecording, setIsRecording] = useState(false); const [isPlaying, setIsPlaying] = useState(false); const [playbackPosition, setPlaybackPosition] = useState(0); const [recordingDuration, setRecordingDuration] = useState(0); // 实时更新录制时长 useEffect(() => { let interval; if (isRecording) { interval = setInterval(async () => { if (recording) { const status = await recording.getStatusAsync(); setRecordingDuration(Math.floor(status.durationMillis / 1000)); } }, 1000); } return () => clearInterval(interval); }, [isRecording, recording]); useEffect(() => { startRecording(); // 组件卸载时清理资源 return () => { if (recording) recording.stopAndUnloadAsync(); if (currentPlaybackSound) currentPlaybackSound.unloadAsync(); }; }, []); async function startRecording() { try { await Audio.requestPermissionsAsync(); await Audio.setAudioModeAsync({ allowsRecordingIOS: true, playsInSilentModeIOS: true, playsInSilentLockedModeIOS: true, shouldDuckAndroid: true, }); const { recording } = await Audio.Recording.createAsync( Audio.RecordingOptionsPresets.HIGH_QUALITY ); setRecording(recording); setIsRecording(true); setRecordingDuration(0); } catch (err) { console.error("Failed to start recording", err); } } async function toggleRecording() { if (!recording) return; try { if (isRecording) { await recording.pauseAsync(); } else { await recording.resumeAsync(); } setIsRecording(!isRecording); } catch (error) { console.error("Failed to toggle recording", error); } } async function stopRecording() { if (!recording) return; try { setIsRecording(false); await recording.stopAndUnloadAsync(); const uri = recording.getURI(); setSound(uri); } catch (error) { console.error("Failed to stop recording", error); } } async function deleteRecording() { try { if (recording) { await recording.stopAndUnloadAsync(); setRecording(null); } if (currentPlaybackSound) { await currentPlaybackSound.unloadAsync(); setCurrentPlaybackSound(null); } setSound(null); setIsRecording(false); setIsPlaying(false); setPlaybackPosition(0); setRecordingDuration(0); startRecording(); } catch (error) { console.error("Failed to delete recording", error); } } async function playSound() { try { if (!sound) return; if (currentPlaybackSound) { await currentPlaybackSound.unloadAsync(); } const { sound: playbackObject } = await Audio.Sound.createAsync( { uri: sound }, { shouldPlay: true, position: playbackPosition } ); setCurrentPlaybackSound(playbackObject); setIsPlaying(true); playbackObject.setOnPlaybackStatusUpdate((status) => { if (status.didJustFinish) { setIsPlaying(false); setPlaybackPosition(0); playbackObject.unloadAsync(); setCurrentPlaybackSound(null); } else { setPlaybackPosition(status.positionMillis); } }); } catch (error) { console.error("Failed to play sound", error); } } // 格式化时长为MM:SS const formatDuration = (seconds) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }; return ( <View style={{ position: "absolute", bottom: 0, width: "100%", height: 140, backgroundColor: "red", flexDirection: "column", }} > <View style={{ width: "100%", height: "50%", backgroundColor: "yellow", justifyContent: "center", alignItems: "center" }}> {isRecording ? ( <> <Text>{formatDuration(recordingDuration)}</Text> <Text>RECORDING AUDIO</Text> </> ) : sound ? ( <> <Text>{formatDuration(Math.floor(playbackPosition / 1000))}</Text> <Text>PLAYING AUDIO</Text> <TouchableOpacity onPress={playSound} disabled={isPlaying}> <MaterialCommunityIcons name={isPlaying ? "pause-circle" : "play-circle"} size={30} color={isPlaying ? "gray" : "#FF5421"} /> </TouchableOpacity> </> ) : ( <Text>No recording available</Text> )} </View> <View style={{ width: "100%", height: "50%", backgroundColor: "green", flexDirection: "row", justifyContent: "space-between", paddingHorizontal: 15, alignItems: "center", }} > <TouchableOpacity onPress={deleteRecording}> <MaterialCommunityIcons name="delete" size={28} color="#cfd1d5" /> </TouchableOpacity> <TouchableOpacity onPress={isRecording ? stopRecording : toggleRecording} > {isRecording ? ( <MaterialCommunityIcons name="pause-circle-outline" size={34} color="#FF2D21" /> ) : ( <MaterialCommunityIcons name="microphone" size={34} color="#FF2D21" /> )} </TouchableOpacity> <TouchableOpacity style={{ width: 40, height: 40, justifyContent: "center", alignItems: "center", borderRadius: 50, borderColor: "white", borderWidth: 2, padding: 5, backgroundColor: sound ? "#FFA121" : "gray", }} disabled={!sound} > <MaterialCommunityIcons size={25} name="send" color="white" /> </TouchableOpacity> </View> </View> ); } export default AudioMessage;
额外优化说明
- 新增录制时长实时更新和格式化显示,贴近WhatsApp交互体验
- 播放按钮在播放状态下切换为暂停图标,交互更直观
- 发送按钮在无录制内容时置灰禁用,符合逻辑
- 组件卸载时自动清理所有音频资源,避免内存泄漏
内容的提问来源于stack exchange,提问作者IMMERHAZE
相关产品推荐
相关产品推荐

