Skip to content

Commit

Permalink
fix(core): fix concurrency exception due to camera source
Browse files Browse the repository at this point in the history
  • Loading branch information
ThibaultBee committed Jan 6, 2025
1 parent 2b67e83 commit 9f443f4
Show file tree
Hide file tree
Showing 12 changed files with 441 additions and 204 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class CameraStreamerStateTest(descriptor: MediaDescriptor) :
ConfigurationUtils.dummyValidAudioConfig(),
ConfigurationUtils.dummyValidVideoConfig()
)
streamer.startPreview(SurfaceUtils.createSurfaceView(activityScenarioRule.scenario))
streamer.startPreview(SurfaceUtils.getSurfaceView(activityScenarioRule.scenario))
streamer.startStream(descriptor)
streamer.stopStream()
streamer.stopPreview()
Expand All @@ -68,17 +68,17 @@ class CameraStreamerStateTest(descriptor: MediaDescriptor) :
// Single method calls
@Test
fun startPreviewTest() = runTest {
streamer.startPreview(SurfaceUtils.createSurfaceView(activityScenarioRule.scenario))
streamer.startPreview(SurfaceUtils.getSurfaceView(activityScenarioRule.scenario))
}

@Test
fun stopPreviewTest() {
fun stopPreviewTest() = runTest {
streamer.stopPreview()
}

// Multiple methods calls
@Test
fun configureStopPreviewTest() {
fun configureStopPreviewTest() = runTest {
streamer.setConfig(
ConfigurationUtils.dummyValidAudioConfig(),
ConfigurationUtils.dummyValidVideoConfig()
Expand All @@ -92,7 +92,7 @@ class CameraStreamerStateTest(descriptor: MediaDescriptor) :
ConfigurationUtils.dummyValidAudioConfig(),
ConfigurationUtils.dummyValidVideoConfig()
)
streamer.startPreview(SurfaceUtils.createSurfaceView(activityScenarioRule.scenario))
streamer.startPreview(SurfaceUtils.getSurfaceView(activityScenarioRule.scenario))
streamer.release()
}

Expand All @@ -102,7 +102,7 @@ class CameraStreamerStateTest(descriptor: MediaDescriptor) :
ConfigurationUtils.dummyValidAudioConfig(),
ConfigurationUtils.dummyValidVideoConfig()
)
streamer.startPreview(SurfaceUtils.createSurfaceView(activityScenarioRule.scenario))
streamer.startPreview(SurfaceUtils.getSurfaceView(activityScenarioRule.scenario))
streamer.stopPreview()
}

Expand All @@ -112,13 +112,13 @@ class CameraStreamerStateTest(descriptor: MediaDescriptor) :
ConfigurationUtils.dummyValidAudioConfig(),
ConfigurationUtils.dummyValidVideoConfig()
)
streamer.startPreview(SurfaceUtils.createSurfaceView(activityScenarioRule.scenario))
streamer.startPreview(SurfaceUtils.getSurfaceView(activityScenarioRule.scenario))
streamer.stopStream()
}

@Test
fun multipleStartPreviewStopPreviewTest() = runTest {
val surfaceView = SurfaceUtils.createSurfaceView(activityScenarioRule.scenario)
val surfaceView = SurfaceUtils.getSurfaceView(activityScenarioRule.scenario)
streamer.setConfig(
ConfigurationUtils.dummyValidAudioConfig(),
ConfigurationUtils.dummyValidVideoConfig()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import kotlin.coroutines.suspendCoroutine
object SurfaceUtils {
private const val TAG = "SurfaceUtils"

private fun createSurfaceViewAsync(
private fun getSurfaceViewAsync(
scenario: ActivityScenario<SurfaceViewTestActivity>,
onSurfaceCreated: (SurfaceView) -> Unit
) {
Expand Down Expand Up @@ -54,9 +54,56 @@ object SurfaceUtils {
}
}

suspend fun createSurfaceView(scenario: ActivityScenario<SurfaceViewTestActivity>): SurfaceView {
/**
* Gets the [SurfaceView] from the [SurfaceViewTestActivity]
*/
suspend fun getSurfaceView(scenario: ActivityScenario<SurfaceViewTestActivity>): SurfaceView {
return suspendCoroutine {
createSurfaceViewAsync(scenario) { surfaceView ->
getSurfaceViewAsync(scenario) { surfaceView ->
it.resumeWith(Result.success(surfaceView))
}
}
}

private fun addsSurfaceViewAsync(
scenario: ActivityScenario<SurfaceViewTestActivity>,
onSurfaceCreated: (SurfaceView) -> Unit
) {
scenario.onActivity {
val surfaceView = SurfaceView(it)
val callback =
object : SurfaceHolder.Callback {
override fun surfaceCreated(surfaceHolder: SurfaceHolder) {
Logger.i(TAG, "Surface created")
onSurfaceCreated(surfaceView)
}

override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) {
Logger.i(TAG, "Surface changed")
}

override fun surfaceDestroyed(holder: SurfaceHolder) {
Logger.i(TAG, "Surface destroyed")
}
}


surfaceView.holder.setFixedSize(10, 10)
it.addSurface(surfaceView, callback)
}
}

/**
* Adds a [SurfaceView] in the [SurfaceViewTestActivity]
*/
suspend fun addSurfaceView(scenario: ActivityScenario<SurfaceViewTestActivity>): SurfaceView {
return suspendCoroutine {
addsSurfaceViewAsync(scenario) { surfaceView ->
it.resumeWith(Result.success(surfaceView))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,21 @@ import io.github.thibaultbee.streampack.core.utils.extensions.getAutoFocusModes
import io.github.thibaultbee.streampack.core.utils.extensions.getCameraFps
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

class CameraController(
private val context: Context,
private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default
private val context: Context
) {
// Use single thread executor to avoid concurrent access to camera
private val coroutineDispatcher: CoroutineDispatcher =
Executors.newSingleThreadExecutor().asCoroutineDispatcher()

private var camera: CameraDevice? = null
val cameraId: String?
get() = camera?.id
Expand Down Expand Up @@ -180,6 +185,11 @@ class CameraController(
targets: List<Surface>,
dynamicRange: Long,
): CameraCaptureSession = suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
runBlocking {
stop()
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val outputConfigurations = targets.map {
OutputConfiguration(it).apply {
Expand Down Expand Up @@ -243,25 +253,29 @@ class CameraController(
val isRequestSessionRunning: Boolean
get() = captureRequest != null

fun startRequestSession(fps: Int, targets: List<Surface>) {
suspend fun startRequestSession(fps: Int, targets: List<Surface>) {
val camera = requireNotNull(camera) { "Camera must not be null" }
val captureSession = requireNotNull(captureSession) { "Capture session must not be null" }

captureRequest = createRequestSession(
camera, captureSession, getClosestFpsRange(camera.id, fps), targets
)
requestSessionSurface.addAll(targets)
withContext(coroutineDispatcher) {
captureRequest = createRequestSession(
camera, captureSession, getClosestFpsRange(camera.id, fps), targets
)
requestSessionSurface.addAll(targets)
}
}

fun stop() {
requestSessionSurface.clear()
captureRequest = null
suspend fun stop() {
withContext(coroutineDispatcher) {
requestSessionSurface.clear()
captureRequest = null

captureSession?.close()
captureSession = null
captureSession?.close()
captureSession = null

camera?.close()
camera = null
camera?.close()
camera = null
}
}

/**
Expand All @@ -271,30 +285,34 @@ class CameraController(
return requestSessionSurface.contains(target)
}

fun addTargets(targets: List<Surface>) {
suspend fun addTargets(targets: List<Surface>) {
val captureRequest = requireNotNull(captureRequest) { "capture request must not be null" }
require(targets.isNotEmpty()) { " At least one target is required" }

targets.forEach {
if (!hasTarget(it)) {
captureRequest.addTarget(it)
requestSessionSurface.add(it)
withContext(coroutineDispatcher) {
targets.forEach {
if (!hasTarget(it)) {
captureRequest.addTarget(it)
requestSessionSurface.add(it)
}
}
updateRepeatingSession()
}
updateRepeatingSession()
}

fun addTarget(target: Surface) {
suspend fun addTarget(target: Surface) {
val captureRequest = requireNotNull(captureRequest) { "capture request must not be null" }

if (hasTarget(target)) {
return
}

captureRequest.addTarget(target)
requestSessionSurface.add(target)
withContext(coroutineDispatcher) {
captureRequest.addTarget(target)
requestSessionSurface.add(target)

updateRepeatingSession()
updateRepeatingSession()
}
}

suspend fun removeTargets(targets: List<Surface>) {
Expand Down Expand Up @@ -346,7 +364,9 @@ class CameraController(
}

fun release() {
stop()
runBlocking {
stop()
}
cameraDispatcher.release()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,15 @@ import io.github.thibaultbee.streampack.core.internal.utils.extensions.rotationT
import io.github.thibaultbee.streampack.core.utils.extensions.getCameraCharacteristics
import io.github.thibaultbee.streampack.core.utils.extensions.getFacingDirection

class CameraInfoProvider(private val context: Context, initialCameraId: String) :
class CameraInfoProvider(
private val context: Context,
private val cameraController: CameraController,
var defaultCamera: String
) :
AbstractSourceInfoProvider() {

var cameraId: String = initialCameraId
set(value) {
if (field == value) {
return
}
field = value
}

val cameraId: String
get() = cameraController.cameraId ?: defaultCamera

override val rotationDegrees: Int
@IntRange(from = 0, to = 359)
Expand Down
Loading

0 comments on commit 9f443f4

Please sign in to comment.