diff --git a/android/src/main/kotlin/com/simform/audio_waveforms/AudioRecorder.kt b/android/src/main/kotlin/com/simform/audio_waveforms/AudioRecorder.kt index 24fd73bd..23df1df5 100644 --- a/android/src/main/kotlin/com/simform/audio_waveforms/AudioRecorder.kt +++ b/android/src/main/kotlin/com/simform/audio_waveforms/AudioRecorder.kt @@ -176,18 +176,18 @@ class AudioRecorder : PluginRegistry.RequestPermissionsResultListener { wavEncoder?.stop(result) recordingThread?.join() sendRecordingResult(result) + release() } else { commonEncoder.setOnEncodingCompleted { sendRecordingResult(result) + release() } commonEncoder.signalToStop() } - } catch (e: Exception) { result.error(LOG_TAG, e.message, "An error occurred while stopping the recorder") - return + release() } - release() } private fun sendRecordingResult(result: Result) { @@ -195,7 +195,9 @@ class AudioRecorder : PluginRegistry.RequestPermissionsResultListener { val hashMap = HashMap() hashMap[Constants.resultFilePath] = recorderSettings?.path hashMap[Constants.resultDuration] = duration - result.success(hashMap) + Handler(Looper.getMainLooper()).post { + result.success(hashMap) + } } private fun sendBytesToFlutter(chunk: ByteArray, rms: Double, milliSeconds: Long) { diff --git a/android/src/main/kotlin/com/simform/audio_waveforms/encoders/CommonEncoder.kt b/android/src/main/kotlin/com/simform/audio_waveforms/encoders/CommonEncoder.kt index 182a7fa8..37f36e56 100644 --- a/android/src/main/kotlin/com/simform/audio_waveforms/encoders/CommonEncoder.kt +++ b/android/src/main/kotlin/com/simform/audio_waveforms/encoders/CommonEncoder.kt @@ -153,17 +153,7 @@ class CommonEncoder { mediaCodec.setCallback(object : MediaCodec.Callback() { override fun onInputBufferAvailable(codec: MediaCodec, index: Int) { if (isEncodingComplete && inputQueue.isEmpty()) { - // Use the last calculated presentation time for EOF, not system time - val eofTimestamp = if (totalBytesEncoded > 0) { - val bytesPerSample = 2L - val channels = 1L - (totalBytesEncoded * 1_000_000L) / (recorderSettings.sampleRate * channels * bytesPerSample) - } else { - 0L - } - codec.queueInputBuffer( - index, 0, 0, eofTimestamp, MediaCodec.BUFFER_FLAG_END_OF_STREAM - ) + queueEosBuffer(codec, index) } else { currentInputBufferIndex = index feedEncoder() @@ -244,7 +234,7 @@ class CommonEncoder { isMuxerStarted = true } } - }) + }, handler) mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) mediaCodec.start() @@ -276,6 +266,21 @@ class CommonEncoder { */ fun signalToStop() { isEncodingComplete = true + + // Post the EOS check to the handler thread so it runs on the same + // thread as onInputBufferAvailable, avoiding race conditions where + // both threads try to queue EOS with the same buffer index. + handler.post { + if (isEncoderStopped) return@post + if (currentInputBufferIndex >= 0 && inputQueue.isEmpty()) { + try { + queueEosBuffer(mediaCodec, currentInputBufferIndex) + currentInputBufferIndex = -1 + } catch (e: Exception) { + Log.e(Constants.LOG_TAG, "Error queuing EOS in signalToStop: ${e.message}") + } + } + } } /** @@ -288,6 +293,22 @@ class CommonEncoder { } + /** + * Queues an end-of-stream buffer to signal that encoding is complete. + */ + private fun queueEosBuffer(codec: MediaCodec, bufferIndex: Int) { + val eofTimestamp = if (totalBytesEncoded > 0) { + val bytesPerSample = 2L + val channels = 1L + (totalBytesEncoded * 1_000_000L) / (recorderSettings.sampleRate * channels * bytesPerSample) + } else { + 0L + } + codec.queueInputBuffer( + bufferIndex, 0, 0, eofTimestamp, MediaCodec.BUFFER_FLAG_END_OF_STREAM + ) + } + /** * Feeds available audio data to the encoder * @@ -409,11 +430,14 @@ class CommonEncoder { mediaMuxer?.stop() mediaMuxer?.release() outputStream.close() - handlerThread.quitSafely() - handlerThread.join() - completionCallback?.invoke() } catch (e: Exception) { Log.e(Constants.LOG_TAG, "Error stopping encoder: ${e.message}") + } finally { + completionCallback?.invoke() + // Quit the handler thread after invoking the callback. + // Don't call join() -- stopEncoder() is called from callbacks + // running on this same thread, so joining would deadlock. + handlerThread.quitSafely() } // Reset state for next recording