diff --git a/core/src/androidTest/java/io/github/thibaultbee/streampack/core/streamer/state/CameraStreamerStateTest.kt b/core/src/androidTest/java/io/github/thibaultbee/streampack/core/streamer/state/CameraStreamerStateTest.kt index e6bfb6fba..f44bd74da 100644 --- a/core/src/androidTest/java/io/github/thibaultbee/streampack/core/streamer/state/CameraStreamerStateTest.kt +++ b/core/src/androidTest/java/io/github/thibaultbee/streampack/core/streamer/state/CameraStreamerStateTest.kt @@ -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() @@ -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() @@ -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() } @@ -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() } @@ -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() diff --git a/core/src/androidTest/java/io/github/thibaultbee/streampack/core/streamer/surface/SurfaceUtils.kt b/core/src/androidTest/java/io/github/thibaultbee/streampack/core/streamer/surface/SurfaceUtils.kt index aeb3e3e60..427c1c7a9 100644 --- a/core/src/androidTest/java/io/github/thibaultbee/streampack/core/streamer/surface/SurfaceUtils.kt +++ b/core/src/androidTest/java/io/github/thibaultbee/streampack/core/streamer/surface/SurfaceUtils.kt @@ -24,7 +24,7 @@ import kotlin.coroutines.suspendCoroutine object SurfaceUtils { private const val TAG = "SurfaceUtils" - private fun createSurfaceViewAsync( + private fun getSurfaceViewAsync( scenario: ActivityScenario, onSurfaceCreated: (SurfaceView) -> Unit ) { @@ -54,9 +54,56 @@ object SurfaceUtils { } } - suspend fun createSurfaceView(scenario: ActivityScenario): SurfaceView { + /** + * Gets the [SurfaceView] from the [SurfaceViewTestActivity] + */ + suspend fun getSurfaceView(scenario: ActivityScenario): SurfaceView { return suspendCoroutine { - createSurfaceViewAsync(scenario) { surfaceView -> + getSurfaceViewAsync(scenario) { surfaceView -> + it.resumeWith(Result.success(surfaceView)) + } + } + } + + private fun addsSurfaceViewAsync( + scenario: ActivityScenario, + 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): SurfaceView { + return suspendCoroutine { + addsSurfaceViewAsync(scenario) { surfaceView -> it.resumeWith(Result.success(surfaceView)) } } diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/camera/CameraController.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/camera/CameraController.kt index 3232560d2..c57c36233 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/camera/CameraController.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/camera/CameraController.kt @@ -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 @@ -180,6 +185,11 @@ class CameraController( targets: List, 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 { @@ -243,25 +253,29 @@ class CameraController( val isRequestSessionRunning: Boolean get() = captureRequest != null - fun startRequestSession(fps: Int, targets: List) { + suspend fun startRequestSession(fps: Int, targets: List) { 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 + } } /** @@ -271,30 +285,34 @@ class CameraController( return requestSessionSurface.contains(target) } - fun addTargets(targets: List) { + suspend fun addTargets(targets: List) { 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) { @@ -346,7 +364,9 @@ class CameraController( } fun release() { - stop() + runBlocking { + stop() + } cameraDispatcher.release() } diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/camera/CameraInfoProvider.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/camera/CameraInfoProvider.kt index 1571bd337..7ae11ba4e 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/camera/CameraInfoProvider.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/camera/CameraInfoProvider.kt @@ -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) diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/camera/CameraSource.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/camera/CameraSource.kt index 9cbe11891..605944480 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/camera/CameraSource.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/camera/CameraSource.kt @@ -25,93 +25,124 @@ import io.github.thibaultbee.streampack.core.internal.utils.av.video.DynamicRang import io.github.thibaultbee.streampack.core.logger.Logger import io.github.thibaultbee.streampack.core.utils.extensions.defaultCameraId import io.github.thibaultbee.streampack.core.utils.extensions.isFrameRateSupported +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext class CameraSource( private val context: Context, + private val dispatcher: CoroutineDispatcher = Dispatchers.Default ) : ICameraSourceInternal, ISurfaceSource { - var previewSurface: Surface? = null - set(value) { - if (field == value) { - Logger.w(TAG, "Preview surface is already set to $value") - return - } - if (cameraController.isCameraRunning) { - if (value == null) { - runBlocking { - stopPreview() - } - } else { - Logger.e(TAG, "Need to restart camera to change preview surface") - field = value - runBlocking { - restartCamera() - } - } - } else { - field = value - } + private val mutex = Mutex() + + private var _previewSurface: Surface? = null + val previewSurface: Surface? + get() = _previewSurface + + suspend fun setPreviewSurface(surface: Surface) { + if (surface == previewSurface) { + Logger.w(TAG, "Preview surface is already set to $surface") + return } + if (cameraController.isCameraRunning) { + Logger.e(TAG, "Need to restart camera to change preview surface") + _previewSurface = surface + restartCamera() + } else { + _previewSurface = surface + } + } - override var outputSurface: Surface? = null + suspend fun resetPreviewSurface() { + stopPreview() + _previewSurface = null + } + + private var _outputSurface: Surface? = null + override var outputSurface: Surface? + get() = _outputSurface set(value) { - if (field == value) { - Logger.w(TAG, "Output surface is already set to $value") - return - } - if (cameraController.isCameraRunning) { - if (value == null) { - runBlocking { - stopStream() - } + runBlocking { + if (value != null) { + setOutputSurface(value) } else { - Logger.e(TAG, "Need to restart camera to change output surface") - field = value - runBlocking { - restartCamera() - } + resetOutputSurface() } - } else { - field = value } } - override var cameraId: String = context.defaultCameraId - get() = cameraController.cameraId ?: field - @RequiresPermission(Manifest.permission.CAMERA) set(value) { - if (field == value) { - Logger.w(TAG, "Camera ID is already set to $value") - return - } - if (!context.isFrameRateSupported(value, fps)) { - throw UnsupportedOperationException("Camera $value does not support $fps fps") - } + private suspend fun setOutputSurface(surface: Surface) { + if (surface == _outputSurface) { + Logger.w(TAG, "Preview surface is already set to $surface") + return + } + if (cameraController.isCameraRunning) { + Logger.e(TAG, "Need to restart camera to change preview surface") + _outputSurface = surface + restartCamera() + } else { + _outputSurface = surface + } + } - field = value - infoProvider.cameraId = value - runBlocking { - restartCamera() + private suspend fun resetOutputSurface() { + stopStream() + _outputSurface = null + } + + private var _cameraId: String = context.defaultCameraId + override val cameraId: String + get() = cameraController.cameraId ?: _cameraId + + @RequiresPermission(Manifest.permission.CAMERA) + override suspend fun setCameraId(cameraId: String) { + if (this.cameraId == cameraId) { + Logger.w(TAG, "Camera ID is already set to $cameraId") + return + } + if (!context.isFrameRateSupported(cameraId, fps)) { + throw UnsupportedOperationException("Camera $cameraId does not support $fps fps") + } + + infoProvider.defaultCamera = cameraId + if (cameraController.isCameraRunning) { + val surfaces = executeSafely { + mutableListOf().apply { + if (isPreviewing) { + add(requireNotNull(_previewSurface)) + } + if (isStreaming) { + add(requireNotNull(outputSurface)) + } + } } + restartCamera(cameraId, surfaces) + } else { + _cameraId = cameraId } + } private val cameraController = CameraController(context) override val settings = CameraSettings(context, cameraController) override val timestampOffset = CameraHelper.getTimeOffsetToMonoClock(context, cameraId) - override val infoProvider = CameraInfoProvider(context, cameraId) + override val infoProvider = CameraInfoProvider(context, cameraController, cameraId) private var fps: Int = 30 private var dynamicRangeProfile: DynamicRangeProfile = DynamicRangeProfile.sdr private val isStreaming: Boolean get() { - val outputSurface = outputSurface ?: return false + val outputSurface = _outputSurface ?: return false return cameraController.hasTarget(outputSurface) && cameraController.isRequestSessionRunning && cameraController.isCameraRunning } override val isPreviewing: Boolean get() { - val previewSurface = previewSurface ?: return false + val previewSurface = _previewSurface ?: return false return cameraController.hasTarget(previewSurface) && cameraController.isRequestSessionRunning && cameraController.isCameraRunning } @@ -139,22 +170,33 @@ class CameraSource( } private suspend fun restartCamera() { - val surfacesToRestart = listOfNotNull(previewSurface, outputSurface).filter { - cameraController.hasTarget(it) + val surfaces = executeSafely { + listOfNotNull(_previewSurface, outputSurface).filter { + cameraController.hasTarget(it) + } } - cameraController.stop() - if (surfacesToRestart.isNotEmpty()) { - startCameraRequestSessionIfNeeded(surfacesToRestart) - } else { - Logger.w(TAG, "Trying to restart camera without surfaces") + restartCamera(cameraId, surfaces) + } + + private suspend fun restartCamera(cameraId: String, surfacesToRestart: List) { + executeSafely { + cameraController.stop() + if (surfacesToRestart.isNotEmpty()) { + startCameraRequestSessionIfNeeded(surfacesToRestart, cameraId) + } else { + Logger.w(TAG, "Trying to restart camera without surfaces") + } } } - private suspend fun startCameraRequestSessionIfNeeded(sessionTargets: List) { + private suspend fun startCameraRequestSessionIfNeeded( + sessionTargets: List, + cameraId: String + ) { if (!cameraController.isCameraRunning) { val targets = mutableListOf() - previewSurface?.let { targets.add(it) } - outputSurface?.let { targets.add(it) } + _previewSurface?.let { targets.add(it) } + _outputSurface?.let { targets.add(it) } cameraController.startCamera( cameraId, targets, dynamicRangeProfile.dynamicRange @@ -166,7 +208,6 @@ class CameraSource( cameraController.startRequestSession(fps, sessionTargets) } catch (t: Throwable) { cameraController.stop() - throw t } } else { cameraController.addTargets(sessionTargets) @@ -180,19 +221,21 @@ class CameraSource( return } - val previewSurface = requireNotNull(previewSurface) - startCameraRequestSessionIfNeeded(listOf(previewSurface)) + val previewSurface = requireNotNull(_previewSurface) { + "Preview surface is not set" + } + executeSafely { + startCameraRequestSessionIfNeeded(listOf(previewSurface), cameraId) + } } - fun stopPreview() { + suspend fun stopPreview() { if (!isPreviewing) { Logger.w(TAG, "Camera is not previewing") return } - runBlocking { - cameraController.removeTarget(requireNotNull(previewSurface)) - } + cameraController.removeTarget(requireNotNull(previewSurface)) } override suspend fun startStream() { @@ -201,8 +244,12 @@ class CameraSource( return } - val outputSurface = requireNotNull(outputSurface) - startCameraRequestSessionIfNeeded(listOf(outputSurface)) + val outputSurface = requireNotNull(outputSurface) { + "Output surface is not set" + } + executeSafely { + startCameraRequestSessionIfNeeded(listOf(outputSurface), cameraId) + } cameraController.muteVibrationAndSound() } @@ -221,6 +268,12 @@ class CameraSource( cameraController.release() } + private suspend fun executeSafely(block: suspend () -> T) = withContext(dispatcher) { + mutex.withLock { + block() + } + } + companion object { private const val TAG = "CameraSource" } diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/camera/ICameraSource.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/camera/ICameraSource.kt index da755fa1a..3025080d4 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/camera/ICameraSource.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/camera/ICameraSource.kt @@ -24,7 +24,7 @@ interface ICameraSource : IVideoSource { /** * Get/Set current camera id. */ - var cameraId: String + val cameraId: String /** * Whether the camera preview is running. @@ -35,4 +35,11 @@ interface ICameraSource : IVideoSource { * The camera settings (auto-exposure, auto-focus, etc.). */ val settings: CameraSettings + + /** + * Set camera id. + * + * @param cameraId The camera id to use + */ + suspend fun setCameraId(cameraId: String) } \ No newline at end of file diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/streamers/interfaces/ICameraStreamer.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/streamers/interfaces/ICameraStreamer.kt index 8050f1342..7d9464ea6 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/core/streamers/interfaces/ICameraStreamer.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/streamers/interfaces/ICameraStreamer.kt @@ -36,18 +36,37 @@ interface ICameraStreamer { * It is a shortcut for [ICameraSource.cameraId]. */ var cameraId: String +} + +interface ICameraCoroutineStreamer : ICameraStreamer { + /** + * Sets a camera id. + * + * @param cameraId The camera id to use + */ + suspend fun setCameraId(cameraId: String) /** * Sets a preview surface. * * @param surface The [Surface] used for camera preview */ - fun setPreview(surface: Surface) + suspend fun setPreview(surface: Surface) /** - * Stops camera preview. + * Starts video preview. */ - fun stopPreview() + suspend fun startPreview() + + /** + * Starts video preview on [previewSurface]. + */ + suspend fun startPreview(previewSurface: Surface) + + /** + * Stops video preview. + */ + suspend fun stopPreview() } /** @@ -55,48 +74,25 @@ interface ICameraStreamer { * * @param surfaceView The [SurfaceView] used for camera preview */ -fun ICameraStreamer.setPreview(surfaceView: SurfaceView) = setPreview(surfaceView.holder.surface) +suspend fun ICameraCoroutineStreamer.setPreview(surfaceView: SurfaceView) = + setPreview(surfaceView.holder.surface) /** * Sets a preview surface holder. * * @param surfaceHolder The [SurfaceHolder] used for camera preview */ -fun ICameraStreamer.setPreview(surfaceHolder: SurfaceHolder) = setPreview(surfaceHolder.surface) +suspend fun ICameraCoroutineStreamer.setPreview(surfaceHolder: SurfaceHolder) = + setPreview(surfaceHolder.surface) /** * Sets a preview surface. * * @param textureView The [TextureView] used for camera preview */ -fun ICameraStreamer.setPreview(textureView: TextureView) = +suspend fun ICameraCoroutineStreamer.setPreview(textureView: TextureView) = setPreview(Surface(textureView.surfaceTexture)) - -interface ICameraCoroutineStreamer : ICameraStreamer { - /** - * Starts audio and video capture. - */ - @RequiresPermission(Manifest.permission.CAMERA) - suspend fun startPreview() -} - - -/** - * Starts audio and video capture. - * If you can prefer to call [ISingleStreamer.setAudioConfig] before starting preview. - * It is a shortcut for [setPreview] and [startPreview]. - * - * @param previewSurface The [Surface] used for camera preview - * - * @see [ICameraStreamer.stopPreview] - */ -@RequiresPermission(Manifest.permission.CAMERA) -suspend fun ICameraCoroutineStreamer.startPreview(previewSurface: Surface) { - setPreview(previewSurface) - startPreview() -} - /** * Starts audio and video capture. * If you can prefer to call [SingleStreamer.setAudioConfig] before starting preview. @@ -104,7 +100,7 @@ suspend fun ICameraCoroutineStreamer.startPreview(previewSurface: Surface) { * * @param surfaceView The [SurfaceView] used for camera preview * - * @see [ICameraStreamer.stopPreview] + * @see [ICameraCoroutineStreamer.stopPreview] */ @RequiresPermission(Manifest.permission.CAMERA) suspend fun ICameraCoroutineStreamer.startPreview(surfaceView: SurfaceView) = @@ -117,7 +113,7 @@ suspend fun ICameraCoroutineStreamer.startPreview(surfaceView: SurfaceView) = * * @param surfaceHolder The [SurfaceHolder] used for camera preview * - * @see [ICameraStreamer.stopPreview] + * @see [ICameraCoroutineStreamer.stopPreview] */ @RequiresPermission(Manifest.permission.CAMERA) suspend fun ICameraCoroutineStreamer.startPreview(surfaceHolder: SurfaceHolder) = @@ -130,7 +126,7 @@ suspend fun ICameraCoroutineStreamer.startPreview(surfaceHolder: SurfaceHolder) * * @param textureView The [TextureView] used for camera preview * - * @see [ICameraStreamer.stopPreview] + * @see [ICameraCoroutineStreamer.stopPreview] */ @RequiresPermission(Manifest.permission.CAMERA) suspend fun ICameraCoroutineStreamer.startPreview(textureView: TextureView) = @@ -138,27 +134,28 @@ suspend fun ICameraCoroutineStreamer.startPreview(textureView: TextureView) = interface ICameraCallbackStreamer : ICameraStreamer { + /** + * Sets a preview surface. + * + * @param surface The [Surface] used for camera preview + */ + fun setPreview(surface: Surface) + /** * Starts audio and video capture. */ @RequiresPermission(Manifest.permission.CAMERA) fun startPreview() -} + /** + * Stops video preview. + */ + fun stopPreview() -/** - * Starts audio and video capture. - * If you can prefer to call [SingleStreamer.setAudioConfig] before starting preview. - * It is a shortcut for [setPreview] and [startPreview]. - * - * @param previewSurface The [Surface] used for camera preview - * - * @see [ICameraStreamer.stopPreview] - */ -@RequiresPermission(Manifest.permission.CAMERA) -fun ICameraCallbackStreamer.startPreview(previewSurface: Surface) { - setPreview(previewSurface) - startPreview() + /** + * Starts video preview on [previewSurface]. + */ + fun startPreview(previewSurface: Surface) } /** @@ -168,9 +165,8 @@ fun ICameraCallbackStreamer.startPreview(previewSurface: Surface) { * * @param surfaceView The [SurfaceView] used for camera preview * - * @see [ICameraStreamer.stopPreview] + * @see [ICameraCallbackStreamer.stopPreview] */ -@RequiresPermission(Manifest.permission.CAMERA) fun ICameraCallbackStreamer.startPreview(surfaceView: SurfaceView) = startPreview(surfaceView.holder.surface) @@ -181,9 +177,8 @@ fun ICameraCallbackStreamer.startPreview(surfaceView: SurfaceView) = * * @param surfaceHolder The [SurfaceHolder] used for camera preview * - * @see [ICameraStreamer.stopPreview] + * @see [ICameraCallbackStreamer.stopPreview] */ -@RequiresPermission(Manifest.permission.CAMERA) fun ICameraCallbackStreamer.startPreview(surfaceHolder: SurfaceHolder) = startPreview(surfaceHolder.surface) @@ -194,8 +189,7 @@ fun ICameraCallbackStreamer.startPreview(surfaceHolder: SurfaceHolder) = * * @param textureView The [TextureView] used for camera preview * - * @see [ICameraStreamer.stopPreview] + * @see [ICameraCallbackStreamer.stopPreview] */ -@RequiresPermission(Manifest.permission.CAMERA) fun ICameraCallbackStreamer.startPreview(textureView: TextureView) = startPreview(Surface(textureView.surfaceTexture)) diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/streamers/single/CameraSingleStreamer.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/streamers/single/CameraSingleStreamer.kt index a0b98c15c..1171796b7 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/core/streamers/single/CameraSingleStreamer.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/streamers/single/CameraSingleStreamer.kt @@ -19,7 +19,6 @@ import android.Manifest import android.content.Context import android.view.Surface import androidx.annotation.RequiresPermission -import androidx.annotation.RestrictTo.* import io.github.thibaultbee.streampack.core.configuration.mediadescriptor.MediaDescriptor import io.github.thibaultbee.streampack.core.internal.endpoints.DynamicEndpoint import io.github.thibaultbee.streampack.core.internal.endpoints.IEndpointInternal @@ -32,6 +31,11 @@ import io.github.thibaultbee.streampack.core.internal.utils.extensions.displayRo import io.github.thibaultbee.streampack.core.streamers.infos.CameraStreamerConfigurationInfo import io.github.thibaultbee.streampack.core.streamers.infos.IConfigurationInfo import io.github.thibaultbee.streampack.core.streamers.interfaces.ICameraCoroutineStreamer +import io.github.thibaultbee.streampack.core.streamers.interfaces.setPreview +import io.github.thibaultbee.streampack.core.streamers.interfaces.startPreview +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock /** * A [SingleStreamer] that sends microphone and camera frames. @@ -75,6 +79,11 @@ open class CameraSingleStreamer( ), ICameraCoroutineStreamer { private val cameraSource = videoSourceInternal as CameraSource + /** + * Mutex to avoid concurrent access to preview surface. + */ + private val previewMutex = Mutex() + /** * Gets the camera source. * It allows to configure camera settings and to set the camera id. @@ -96,17 +105,31 @@ open class CameraSingleStreamer( * Set current camera id. * Retrieves list of cameras from [Context.cameras] * + * It will block the current thread until the camera id is set. You can use [setCameraId] to + * set it in a coroutine. + * * @param value string that described the camera. */ @RequiresPermission(Manifest.permission.CAMERA) set(value) { - videoSource.cameraId = value - // If config has not been set yet, [configure] will update transformation later. - if (videoConfig != null) { - updateTransformation() + runBlocking { + setCameraId(value) } } + /** + * Sets a camera id with a suspend function. + * + * @param cameraId The camera id to use + */ + override suspend fun setCameraId(cameraId: String) { + cameraSource.setCameraId(cameraId) + // If config has not been set yet, [configure] will update transformation later. + if (videoConfig != null) { + updateTransformation() + } + } + /** * Gets configuration information. * @@ -137,8 +160,10 @@ open class CameraSingleStreamer( /** * Sets a preview surface. */ - override fun setPreview(surface: Surface) { - cameraSource.previewSurface = surface + override suspend fun setPreview(surface: Surface) { + previewMutex.withLock { + cameraSource.setPreviewSurface(surface) + } } /** @@ -151,8 +176,28 @@ open class CameraSingleStreamer( * @see [stopPreview] * @see [setPreview] */ + @RequiresPermission(Manifest.permission.CAMERA) override suspend fun startPreview() { - cameraSource.startPreview() + previewMutex.withLock { + cameraSource.startPreview() + } + } + + /** + * Starts audio and video capture. + * If you can prefer to call [ISingleStreamer.setAudioConfig] before starting preview. + * It is a shortcut for [setPreview] and [startPreview]. + * + * @param previewSurface The [Surface] used for camera preview + * + * @see [ICameraCoroutineStreamer.stopPreview] + */ + @RequiresPermission(Manifest.permission.CAMERA) + override suspend fun startPreview(previewSurface: Surface) { + previewMutex.withLock { + cameraSource.setPreviewSurface(previewSurface) + cameraSource.startPreview() + } } /** @@ -161,15 +206,19 @@ open class CameraSingleStreamer( * * @see [startPreview] */ - override fun stopPreview() { - cameraSource.stopPreview() + override suspend fun stopPreview() { + previewMutex.withLock { + cameraSource.stopPreview() + } } /** * Same as [SingleStreamer.release] but it also calls [stopPreview]. */ override fun release() { - stopPreview() + runBlocking { + stopPreview() + } super.release() } } \ No newline at end of file diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/streamers/single/callbacks/CameraCallbackSingleStreamer.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/streamers/single/callbacks/CameraCallbackSingleStreamer.kt index d7c34d07a..06b239c3d 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/core/streamers/single/callbacks/CameraCallbackSingleStreamer.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/streamers/single/callbacks/CameraCallbackSingleStreamer.kt @@ -12,10 +12,17 @@ import io.github.thibaultbee.streampack.core.internal.sources.video.camera.ICame import io.github.thibaultbee.streampack.core.streamers.infos.CameraStreamerConfigurationInfo import io.github.thibaultbee.streampack.core.streamers.infos.IConfigurationInfo import io.github.thibaultbee.streampack.core.streamers.interfaces.ICameraCallbackStreamer +import io.github.thibaultbee.streampack.core.streamers.interfaces.ICameraCoroutineStreamer +import io.github.thibaultbee.streampack.core.streamers.interfaces.setPreview +import io.github.thibaultbee.streampack.core.streamers.interfaces.startPreview import io.github.thibaultbee.streampack.core.streamers.single.CameraSingleStreamer import io.github.thibaultbee.streampack.core.streamers.single.ICallbackSingleStreamer import io.github.thibaultbee.streampack.core.streamers.single.ICoroutineSingleStreamer +import io.github.thibaultbee.streampack.core.streamers.single.SingleStreamer import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock /** * Default implementation of [ICallbackSingleStreamer] that uses [ICoroutineSingleStreamer] to handle streamer logic. @@ -33,6 +40,11 @@ class CameraCallbackSingleStreamer( ICameraCallbackStreamer { private val cameraSource = (streamer as CameraSingleStreamer).videoSource as CameraSource + /** + * Mutex to avoid concurrent access to preview surface. + */ + private val previewMutex = Mutex() + /** * Gets the camera source. * It allows to configure camera settings and to set the camera id. @@ -57,7 +69,9 @@ class CameraCallbackSingleStreamer( */ @RequiresPermission(Manifest.permission.CAMERA) set(value) { - videoSource.cameraId = value + runBlocking { + cameraSource.setCameraId(value) + } } /** @@ -88,7 +102,9 @@ class CameraCallbackSingleStreamer( * Sets a preview surface. */ override fun setPreview(surface: Surface) { - cameraSource.previewSurface = surface + runBlocking { + previewMutex.withLock { cameraSource.setPreviewSurface(surface) } + } } /** @@ -101,15 +117,44 @@ class CameraCallbackSingleStreamer( * @see [stopPreview] * @see [setPreview] */ + @RequiresPermission(Manifest.permission.CAMERA) override fun startPreview() { /** * Trying to set encoder surface to avoid a camera restart. */ coroutineScope.launch { - try { - cameraSource.startPreview() - } catch (t: Throwable) { - listeners.forEach { it.onError(t) } + previewMutex.withLock { + try { + cameraSource.startPreview() + } catch (t: Throwable) { + listeners.forEach { it.onError(t) } + } + } + } + } + + /** + * Starts audio and video capture. + * If you can prefer to call [SingleStreamer.setAudioConfig] before starting preview. + * It is a shortcut for [setPreview] and [startPreview]. + * + * @param previewSurface The [Surface] used for camera preview + * + * @see [ICameraCoroutineStreamer.stopPreview] + */ + @RequiresPermission(Manifest.permission.CAMERA) + override fun startPreview(previewSurface: Surface) { + /** + * Trying to set encoder surface to avoid a camera restart. + */ + coroutineScope.launch { + previewMutex.withLock { + try { + cameraSource.setPreviewSurface(previewSurface) + cameraSource.startPreview() + } catch (t: Throwable) { + listeners.forEach { it.onError(t) } + } } } } @@ -121,7 +166,11 @@ class CameraCallbackSingleStreamer( * @see [startPreview] */ override fun stopPreview() { - cameraSource.stopPreview() + runBlocking { + previewMutex.withLock { + cameraSource.stopPreview() + } + } } /** diff --git a/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/ui/main/PreviewViewModel.kt b/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/ui/main/PreviewViewModel.kt index 2494d7f79..52848cd82 100644 --- a/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/ui/main/PreviewViewModel.kt +++ b/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/ui/main/PreviewViewModel.kt @@ -248,8 +248,10 @@ class PreviewViewModel(private val application: Application) : ObservableViewMod */ val streamer = streamer if (streamer is ICameraStreamer) { - streamer.switchBackToFront(application) - notifyCameraChanged() + viewModelScope.launch { + streamer.switchBackToFront(application) + notifyCameraChanged() + } } return true } @@ -263,8 +265,10 @@ class PreviewViewModel(private val application: Application) : ObservableViewMod */ val streamer = streamer if (streamer is ICameraStreamer) { - streamer.toggleCamera(application) - notifyCameraChanged() + viewModelScope.launch { + streamer.toggleCamera(application) + notifyCameraChanged() + } } } diff --git a/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/utils/Extensions.kt b/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/utils/Extensions.kt index 6a1d184b0..c565b22e4 100644 --- a/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/utils/Extensions.kt +++ b/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/utils/Extensions.kt @@ -32,24 +32,24 @@ import io.github.thibaultbee.streampack.core.utils.extensions.frontCameras import io.github.thibaultbee.streampack.core.utils.extensions.isBackCamera @RequiresPermission(Manifest.permission.CAMERA) -fun ICameraStreamer.toggleCamera(context: Context) { +suspend fun ICameraStreamer.toggleCamera(context: Context) { val cameras = context.cameras val currentCameraIndex = cameras.indexOf(cameraId) val cameraIndex = (currentCameraIndex + 1) % cameras.size - cameraId = cameras[cameraIndex] + videoSource.setCameraId(cameras[cameraIndex]) } @RequiresPermission(Manifest.permission.CAMERA) -fun ICameraStreamer.switchBackToFront(context: Context) { +suspend fun ICameraStreamer.switchBackToFront(context: Context) { val cameras = if (context.isBackCamera(cameraId)) { context.frontCameras } else { context.backCameras } if (cameras.isNotEmpty()) { - cameraId = cameras[0] + videoSource.setCameraId(cameras[0]) } } diff --git a/ui/src/main/java/io/github/thibaultbee/streampack/ui/views/CameraPreviewView.kt b/ui/src/main/java/io/github/thibaultbee/streampack/ui/views/CameraPreviewView.kt index 106aecf57..534cbe5af 100644 --- a/ui/src/main/java/io/github/thibaultbee/streampack/ui/views/CameraPreviewView.kt +++ b/ui/src/main/java/io/github/thibaultbee/streampack/ui/views/CameraPreviewView.kt @@ -43,6 +43,7 @@ import io.github.thibaultbee.streampack.core.utils.extensions.getCameraCharacter import io.github.thibaultbee.streampack.ui.R import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import java.security.InvalidParameterException import java.util.concurrent.CancellationException @@ -95,7 +96,7 @@ class CameraPreviewView @JvmOverloads constructor( return } val isPreviewing = field?.videoSource?.isPreviewing - field?.stopPreview() + field?.let { runBlocking { stopPreview() } } field = value if (isPreviewing == true) { startPreviewAsyncInternal(true) @@ -163,7 +164,9 @@ class CameraPreviewView @JvmOverloads constructor( override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { if (w != oldw || h != oldh) { streamer?.let { - it.stopPreview() + runBlocking { + stopPreview(it) + } startPreviewAsyncInternal(true) } } @@ -229,7 +232,9 @@ class CameraPreviewView @JvmOverloads constructor( } private fun stopPreviewInternal() { - streamer?.stopPreview() + runBlocking { + streamer?.let { stopPreview(it) } + } viewfinderSurfaceRequest?.markSurfaceSafeToRelease() viewfinderSurfaceRequest = null } @@ -366,6 +371,16 @@ class CameraPreviewView @JvmOverloads constructor( } } + private suspend fun stopPreview(streamer: ICameraStreamer) { + when (streamer) { + is ICameraCoroutineStreamer -> streamer.stopPreview() + is ICameraCallbackStreamer -> streamer.stopPreview() + else -> { + throw InvalidParameterException("Streamer is not a recognized type: ${streamer::class.java.simpleName}") + } + } + } + private fun getPosition(scaleType: ScaleType): Position { return when (scaleType) { ScaleType.FILL_START -> Position.START