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

Android Capacitor插件音频录制崩溃求助:releaseBuffer越界问题

问题描述

我不擅长Android开发,目前正在开发一个Capacitor插件,通过socket.io流式传输麦克风录制的音频。功能整体正常,但偶尔会收到崩溃报告,报错信息如下:

releaseBuffer: mUnreleased out of range, !(stepCount:480 <= mUnreleased:128 <= mFrameCount:1440), BufferSizeInFrames:1440

我理解报错含义,但不知道怎么解决,恳请帮忙。触发错误的函数代码如下:

@PluginMethod()
public void startRecording(PluginCall call) {
    try {
    if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(getActivity(), new String[]{Manifest.permission.RECORD_AUDIO}, AUDIO_RECORD_REQUEST_CODE);
        call.reject("Permission is required for recording audio.");
        return;
    }
    vibratePhone();
    sendUniqueId = generateRandomString(10);

    JSObject ret = new JSObject();
    ret.put("companyId", companyId);
    ret.put("name", name);
    ret.put("userId", userId);
    ret.put("uid", sendUniqueId);
    socket.emit("speaking", ret);

    int channelConfig = AudioFormat.CHANNEL_IN_MONO;
    int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
    int minBufferSize = AudioRecord.getMinBufferSize(pcmSampleRate, channelConfig, audioFormat);

    audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, pcmSampleRate, channelConfig, audioFormat, minBufferSize);
    if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
        call.reject("Failed to initialize audio recording.");
        return;
    }
    
    new Thread(() -> {
        audioRecord.startRecording();
        isRecording = true;
        byte[] buffer = new byte[minBufferSize];

        int chunksToSkip = 6; // Number of initial chunks to skip
        int chunksProcessed = 0; // Counter for processed chunks

        long startTime = System.currentTimeMillis();
        long totalDataSent = 0;

        while (isRecording && audioRecord != null && audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
            int bytesRead = audioRecord.read(buffer, 0, buffer.length);
            if (bytesRead > 0) {
                chunksProcessed++; // Increment the chunks processed counter
                if (chunksProcessed > chunksToSkip) {
                    byte[] actualData = Arrays.copyOf(buffer, bytesRead);
                    try {
                        if (actualData.length > 0) {
                            Constants.FrameSize frameSize2 = Constants.FrameSize.Companion.fromValue(buffer.length / 2);
                            byte[] encoded = codec.encode(actualData, frameSize2);
                            totalDataSent += encoded.length;
                            if (encoded != null) {
                                JSObject ret2 = new JSObject();
                                ret2.put("data", encoded);
                                ret2.put("frameSize", buffer.length / 2);
                                ret2.put("companyId", companyId);
                                ret2.put("name", name);
                                ret2.put("userId", userId);
                                socket.emit("send-chat-data", ret2);
                            } else {
                                JSObject errorLog = new JSObject();
                                errorLog.put("error", "CODEC.Encoded is null");
                                errorLog.put("type", "startRecording");
                                socket.emit("ptt-error-logging", errorLog);
                            }
                            long currentTime = System.currentTimeMillis();
                            if ((currentTime - startTime) >= 60000) { // 60,000 milliseconds = 1 minute
                                Log.d("DataSent", "Total data sent in last minute: " + (totalDataSent / 1024) + " KB");

                                // Reset counters
                                startTime = currentTime;
                                totalDataSent = 0;
                            }
                        }
                    } catch (Exception e) {
                        JSObject errorLog = new JSObject();
                        errorLog.put("error", e);
                        errorLog.put("type", "startRecording");
                        socket.emit("ptt-error-logging", errorLog);
                        stopRecordingFromSocket();
                    }
                }
            }
        }
    }).start();

    call.success();
    } catch (Exception e) {
        JSObject errorLog = new JSObject();
        errorLog.put("error", e);
        errorLog.put("type", "startRecording-whole");
        socket.emit("ptt-error-logging", errorLog);
        stopRecordingFromSocket();
    }
}

我尝试过调整缓冲区大小,但似乎毫无效果,感谢各位的帮助!


解决方案

这个错误是Android AudioRecord内部缓冲区管理异常,通常由读取/释放不同步、缓冲区设置不合理或线程阻塞导致,以下是针对性修复方案:

1. 合理设置AudioRecord缓冲区大小

getMinBufferSize返回的是底层要求的最小缓冲区,实际使用时建议用其整数倍(比如2倍),避免底层缓冲区溢出:

int minBufferSize = AudioRecord.getMinBufferSize(pcmSampleRate, channelConfig, audioFormat);
// 使用最小缓冲区的2倍作为实际缓冲区,提升容错性
int bufferSize = minBufferSize * 2;
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, pcmSampleRate, channelConfig, audioFormat, bufferSize);
// 同时更新录音线程内的缓冲区
byte[] buffer = new byte[bufferSize];

2. 修复帧大小计算逻辑

当前用buffer.length / 2作为帧大小,但实际读取的字节数是bytesRead,应该用bytesRead / 2(16位PCM每个样本占2字节),避免传递错误参数给编解码器:

// 替换原frameSize相关代码
Constants.FrameSize frameSize2 = Constants.FrameSize.Companion.fromValue(bytesRead / 2);
ret2.put("frameSize", bytesRead / 2);

3. 保证线程安全的录制停止逻辑

  • 给isRecording添加volatile修饰,确保线程间可见性:
private volatile boolean isRecording = false;
  • 完善stopRecordingFromSocket方法,确保正确停止并释放AudioRecord:
public void stopRecordingFromSocket() {
    isRecording = false;
    if (audioRecord != null) {
        if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
            audioRecord.stop();
        }
        audioRecord.release();
        audioRecord = null;
    }
}

4. 避免录音线程被阻塞

当前录音线程中直接执行编码和socket发送操作,这些操作可能阻塞线程,导致AudioRecord读取不及时。建议将编码和发送逻辑放到单独的线程池:

// 提前初始化单线程池(在类中定义)
private ExecutorService codecExecutor = Executors.newSingleThreadExecutor();

// 替换录音线程内的编码发送代码
if (chunksProcessed > chunksToSkip) {
    byte[] actualData = Arrays.copyOf(buffer, bytesRead);
    // 提交到线程池处理
    codecExecutor.execute(() -> {
        try {
            if (actualData.length > 0) {
                Constants.FrameSize frameSize2 = Constants.FrameSize.Companion.fromValue(bytesRead / 2);
                byte[] encoded = codec.encode(actualData, frameSize2);
                totalDataSent += encoded.length;
                if (encoded != null) {
                    JSObject ret2 = new JSObject();
                    ret2.put("data", encoded);
                    ret2.put("frameSize", bytesRead / 2);
                    ret2.put("companyId", companyId);
                    ret2.put("name", name);
                    ret2.put("userId", userId);
                    socket.emit("send-chat-data", ret2);
                } else {
                    JSObject errorLog = new JSObject();
                    errorLog.put("error", "CODEC.Encoded is null");
                    errorLog.put("type", "startRecording");
                    socket.emit("ptt-error-logging", errorLog);
                }
                long currentTime = System.currentTimeMillis();
                if ((currentTime - startTime) >= 60000) {
                    Log.d("DataSent", "Total data sent in last minute: " + (totalDataSent / 1024) + " KB");
                    startTime = currentTime;
                    totalDataSent = 0;
                }
            }
        } catch (Exception e) {
            JSObject errorLog = new JSObject();
            errorLog.put("error", e);
            errorLog.put("type", "startRecording");
            socket.emit("ptt-error-logging", errorLog);
            stopRecordingFromSocket();
        }
    });
}

5. 处理AudioRecord.read的异常返回值

read方法可能返回错误码,需要及时处理避免异常扩散:

int bytesRead = audioRecord.read(buffer, 0, buffer.length);
if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION || bytesRead == AudioRecord.ERROR_BAD_VALUE) {
    stopRecordingFromSocket();
    break;
} else if (bytesRead > 0) {
    // 正常处理逻辑
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 14:02:02