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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 02:57:02