Skip to content

Native Crash (pthread_mutex_lock) and Surface Leak on Shutdown #1928

@ErAzOr2k

Description

@ErAzOr2k

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:

  1. 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.
  2. Instantiate the StreamManager within a Composable and call its destroy() method from a DisposableEffect's onDispose block to tie its lifecycle to the screen.
  3. Run the application and confirm the camera preview is showing.
  4. Close the application (e.g., by swiping it away from the recents screen).
  5. Observe the native crash in Logcat.
  6. Restart the application.
  7. 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 ---")
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions