Native Crash (pthread_mutex_lock) and Surface Leak on Shutdown
Describe the bug When the application is closed while the camera preview is active, a native crash FORTIFY: pthread_mutex_lock called on a destroyed mutex occurs. This improper shutdown prevents graphics resources from being released correctly. As a result, upon restarting the application, a W/System: A resource failed to call Surface.release. warning is logged, indicating a resource leak from the previous session.
The issue seems to be a race condition during the cleanup process of GlStreamInterface and/or Camera2Source.
To Reproduce Steps to reproduce the behavior:
- Integrate the library into a modern Android application (e.g., using Jetpack Compose). 2.Use the provided StreamManager class below to manage the camera preview with GlStreamInterface and Camera2Source.
- Instantiate the StreamManager within a Composable and call its destroy() method from a DisposableEffect's onDispose block to tie its lifecycle to the screen.
- Run the application and confirm the camera preview is showing.
- Close the application (e.g., by swiping it away from the recents screen).
- Observe the native crash in Logcat.
- Restart the application.
- Observe the Surface.release warning in Logcat.
Expected behavior The application should shut down cleanly without any native crashes or resource leak warnings on the next start.
Additional context
package com.example.streamingtest
import android.content.Context
import android.util.Log
import android.view.Surface
import android.view.SurfaceHolder
import android.view.SurfaceView
import com.pedro.encoder.input.sources.video.Camera2Source
import com.pedro.library.view.GlStreamInterface
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
class StreamManager(context: Context) {
private val managerScope = CoroutineScope(Dispatchers.IO)
private val isShuttingDown = AtomicBoolean(false)
private val cameraSource = Camera2Source(context)
private val glInterface = GlStreamInterface(context)
fun setupSurfaceCallbacks(view: SurfaceView) {
view.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
startPreview(holder.surface, width, height)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
stopPreview()
}
})
}
private fun startPreview(surface: Surface, width: Int, height: Int) {
managerScope.launch(Dispatchers.Main) {
if (isShuttingDown.get()) return@launch
try {
Log.d("StreamManager", "Attempting to start preview.")
if (!glInterface.isRunning) {
glInterface.setEncoderSize(1920, 1080)
glInterface.start()
}
glInterface.deAttachPreview()
glInterface.attachPreview(surface)
glInterface.setPreviewResolution(width, height)
if (!cameraSource.isRunning()) {
cameraSource.start(glInterface.surfaceTexture)
}
Log.d("StreamManager", "Preview should be running.")
} catch (e: Exception) {
Log.e("StreamManager", "Failed to start preview.", e)
}
}
}
fun stopPreview() {
managerScope.launch(Dispatchers.Main) {
try {
Log.d("StreamManager", "Attempting to stop preview.")
if (cameraSource.isRunning()) {
cameraSource.stop()
}
if (glInterface.isRunning) {
glInterface.deAttachPreview()
}
Log.d("StreamManager", "Preview stopped.")
} catch (e: Exception) {
Log.e("StreamManager", "Failed to stop preview.", e)
}
}
}
fun destroy() {
if (isShuttingDown.compareAndSet(false, true)) {
Log.i("StreamManager", "--- EXECUTING FINAL DESTROY ---")
// The crash seems to be triggered by the sequence of stopping these components
stopPreview()
if (glInterface.isRunning) {
glInterface.stop()
}
managerScope.cancel()
Log.i("StreamManager", "--- DESTROY COMPLETE ---")
}
}
}
Hello,
For me, this look more a problem related with using CoroutineScope to stop the preview in the callback. I think that stopPreview is executed when the camera resources or main thread is already death.
Possible solutions:
- Remove CoroutineScope and use the viewmodel or activity scope
- Replace managerScope.launch(Dispatchers.Main) to managerScope.launch, this way you will use IO thread to avoid problems related with the main thread. You can sync the code in all methods using synchronized like:
private val lock = Any()
fun stopPreview() {
managerScope.launch {
synchronized(lock) {
try {
Log.d("StreamManager", "Attempting to stop preview.")
if (cameraSource.isRunning()) {
cameraSource.stop()
}
if (glInterface.isRunning) {
glInterface.deAttachPreview()
}
Log.d("StreamManager", "Preview stopped.")
} catch (e: Exception) {
Log.e("StreamManager", "Failed to stop preview.", e)
}
}
}
}