diff --git a/README.md b/README.md index fc97ff507..a811a41c2 100644 --- a/README.md +++ b/README.md @@ -121,12 +121,10 @@ To simplify integration, StreamPack provides an `PreviewView`. + app:enableZoomOnPinch="true" /> ``` -`app:cameraFacingDirection` can be `back` to start preview on the first back camera or `front` to -start preview on the first front camera. `app:enableZoomOnPinch` is a boolean to enable zoom on pinch gesture. 3. Instantiate the streamer (main live streaming class) diff --git a/core/build.gradle b/core/build.gradle index 9bae34945..063f5c574 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -15,8 +15,8 @@ dependencies { implementation "androidx.core:core-ktx:${androidxCoreVersion}" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' + implementation 'androidx.lifecycle:lifecycle-runtime:2.7.0' implementation 'androidx.camera:camera-viewfinder:1.4.0-alpha04' - implementation 'com.google.guava:guava:31.0.1-jre' testImplementation 'androidx.test:rules:1.5.0' testImplementation 'junit:junit:4.13.2' diff --git a/core/src/main/java/io/github/thibaultbee/streampack/views/PreviewView.kt b/core/src/main/java/io/github/thibaultbee/streampack/views/PreviewView.kt index 62db01b11..6d5b30d29 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/views/PreviewView.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/views/PreviewView.kt @@ -32,19 +32,19 @@ import android.view.ViewGroup import android.widget.FrameLayout import androidx.camera.viewfinder.CameraViewfinder import androidx.camera.viewfinder.CameraViewfinder.ScaleType +import androidx.camera.viewfinder.CameraViewfinderExt.requestSurface import androidx.camera.viewfinder.ViewfinderSurfaceRequest import androidx.camera.viewfinder.populateFromCharacteristics import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import com.google.common.util.concurrent.FutureCallback -import com.google.common.util.concurrent.Futures +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.lifecycleScope import io.github.thibaultbee.streampack.R import io.github.thibaultbee.streampack.logger.Logger import io.github.thibaultbee.streampack.streamers.interfaces.ICameraStreamer import io.github.thibaultbee.streampack.utils.OrientationUtils -import io.github.thibaultbee.streampack.utils.backCameraList -import io.github.thibaultbee.streampack.utils.frontCameraList import io.github.thibaultbee.streampack.utils.getCameraCharacteristics +import kotlinx.coroutines.launch +import java.util.concurrent.CancellationException /** * A [FrameLayout] containing a preview for the [ICameraStreamer]. @@ -64,11 +64,10 @@ class PreviewView @JvmOverloads constructor( private val cameraViewFinder = CameraViewfinder(context, attrs, defStyle) private var viewFinderSurfaceRequest: ViewfinderSurfaceRequest? = null - private val cameraFacingDirection: CameraFacingDirection - private val defaultCameraId: String? - private var previewState = PreviewState.STOPPED + private val lifecycleOwner by lazy { findViewTreeLifecycleOwner()!! } + /** * Enables zoom on pinch gesture. */ @@ -94,8 +93,12 @@ class PreviewView @JvmOverloads constructor( set(value) { post { stopPreviewInternal() + value?.let { + lifecycleOwner.lifecycleScope.launch { + startPreviewInternal(it, it.camera, size) + } + } field = value - startPreviewInternal(size) } } @@ -133,21 +136,6 @@ class PreviewView @JvmOverloads constructor( val a = context.obtainStyledAttributes(attrs, R.styleable.PreviewView) try { - cameraFacingDirection = - CameraFacingDirection.entryOf( - a.getInt(R.styleable.PreviewView_cameraFacingDirection, DEFAULT_CAMERA_FACING.value) - ) - - defaultCameraId = when (cameraFacingDirection) { - CameraFacingDirection.FRONT -> { - context.frontCameraList.firstOrNull() - } - - CameraFacingDirection.BACK -> { - context.backCameraList.firstOrNull() - } - } - enableZoomOnPinch = a.getBoolean(R.styleable.PreviewView_enableZoomOnPinch, true) enableTapToFocus = @@ -180,7 +168,10 @@ class PreviewView @JvmOverloads constructor( } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { - startPreviewIfReady(size, true) + if (w != oldw || h != oldh) { + stopPreviewInternal() + streamer?.let { startPreviewIfReady(it, size, true) } + } } override fun onDetachedFromWindow() { @@ -219,11 +210,15 @@ class PreviewView @JvmOverloads constructor( // mTouchUpEvent == null means it's an accessibility click. Focus at the center instead. val x = touchUpEvent?.x ?: (width / 2f) val y = touchUpEvent?.y ?: (height / 2f) - it.settings.camera.focusMetering.onTap( - PointF(x, y), - Rect(this.x.toInt(), this.y.toInt(), width, height), - OrientationUtils.getSurfaceOrientationDegrees(display.rotation) - ) + try { + it.settings.camera.focusMetering.onTap( + PointF(x, y), + Rect(this.x.toInt(), this.y.toInt(), width, height), + OrientationUtils.getSurfaceOrientationDegrees(display.rotation) + ) + } catch (e: Exception) { + Logger.e(TAG, "Failed to focus at $x, $y", e) + } } } @@ -235,7 +230,7 @@ class PreviewView @JvmOverloads constructor( * Stops the preview. */ private fun stopPreview() { - post { + lifecycleOwner.lifecycleScope.launch { stopPreviewInternal() } } @@ -250,10 +245,15 @@ class PreviewView @JvmOverloads constructor( /** * Starts the preview if the view size is ready. * + * @param streamer the camera streamer * @param targetViewSize the view size * @param shouldFailSilently true to fail silently */ - private fun startPreviewIfReady(targetViewSize: Size, shouldFailSilently: Boolean) { + private fun startPreviewIfReady( + streamer: ICameraStreamer, + targetViewSize: Size, + shouldFailSilently: Boolean + ) { try { if (ActivityCompat.checkSelfPermission( context, @@ -263,8 +263,8 @@ class PreviewView @JvmOverloads constructor( throw SecurityException("Camera permission is needed to run this application") } - post { - startPreviewInternal(targetViewSize) + lifecycleOwner.lifecycleScope.launch { + startPreviewInternal(streamer, streamer.camera, targetViewSize) } } catch (e: Exception) { if (shouldFailSilently) { @@ -275,61 +275,50 @@ class PreviewView @JvmOverloads constructor( } } - private fun startPreviewInternal( + private suspend fun startPreviewInternal( + streamer: ICameraStreamer, + camera: String, targetViewSize: Size ) { - val streamer = streamer ?: run { - Logger.w(TAG, "Streamer has not been set") - return - } - if (width == 0 || height == 0) { - Logger.w(TAG, "View size is not ready") - return - } - if (previewState != PreviewState.STOPPED) { - Logger.w(TAG, "Preview is already running or starting") - return - } previewState = PreviewState.STARTING Logger.d(TAG, "Target view size: $targetViewSize") - - val camera = defaultCameraId ?: streamer.camera Logger.i(TAG, "Starting on camera: $camera") val request = createRequest(targetViewSize, camera) viewFinderSurfaceRequest = request - sendRequest(request, { surface -> - post { - if (ActivityCompat.checkSelfPermission( - context, - Manifest.permission.CAMERA - ) != PackageManager.PERMISSION_GRANTED - ) { - viewFinderSurfaceRequest?.markSurfaceSafeToRelease() - viewFinderSurfaceRequest = null - previewState = PreviewState.STOPPED - Logger.e( - TAG, - "Camera permission is needed to run this application" - ) - listener?.onPreviewFailed(SecurityException("Camera permission is needed to run this application")) - } else { + try { + val surface = sendRequest(request) + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.CAMERA + ) != PackageManager.PERMISSION_GRANTED + ) { + viewFinderSurfaceRequest?.markSurfaceSafeToRelease() + viewFinderSurfaceRequest = null + previewState = PreviewState.STOPPED + Logger.e( + TAG, + "Camera permission is needed to run this application" + ) + listener?.onPreviewFailed(SecurityException("Camera permission is needed to run this application")) + } else { + if (surface.isValid) { streamer.startPreview(surface, camera) previewState = PreviewState.RUNNING listener?.onPreviewStarted() } } - }, { t -> - post { - viewFinderSurfaceRequest?.markSurfaceSafeToRelease() - viewFinderSurfaceRequest = null - previewState = PreviewState.STOPPED - Logger.w(TAG, "Failed to get a Surface: $t", t) - listener?.onPreviewFailed(t) - } - }) + } catch (e: CancellationException) { + Logger.w(TAG, "Preview request cancelled") + } catch (t: Throwable) { + viewFinderSurfaceRequest?.markSurfaceSafeToRelease() + viewFinderSurfaceRequest = null + previewState = PreviewState.STOPPED + Logger.w(TAG, "Failed to get a Surface: $t", t) + listener?.onPreviewFailed(t) + } } private fun createRequest( @@ -353,34 +342,13 @@ class PreviewView @JvmOverloads constructor( return builder.build() } - private fun sendRequest( - request: ViewfinderSurfaceRequest, - onSuccess: (Surface) -> Unit, - onFailure: (Throwable) -> Unit - ) { - val surfaceListenableFuture = - cameraViewFinder.requestSurfaceAsync(request) - - Futures.addCallback( - surfaceListenableFuture, - object : FutureCallback { - override fun onSuccess(surface: Surface) { - onSuccess(surface) - } - - override fun onFailure(t: Throwable) { - onFailure(t) - } - }, - ContextCompat.getMainExecutor(context) - ) + private suspend fun sendRequest(request: ViewfinderSurfaceRequest): Surface { + return cameraViewFinder.requestSurface(request) } companion object { private const val TAG = "PreviewView" - private val DEFAULT_CAMERA_FACING = CameraFacingDirection.BACK - private fun getPosition(scaleType: ScaleType): Position { return when (scaleType) { @@ -463,33 +431,6 @@ class PreviewView @JvmOverloads constructor( fun onZoomRationOnPinchChanged(zoomRatio: Float) {} } - /** - * Options for the camera facing direction. - */ - enum class CameraFacingDirection(val value: Int, val id: String) { - /** - * The facing of the camera is the same as that of the screen. - */ - FRONT(0, "front"), - - /** - * The facing of the camera is opposite to that of the screen. - */ - BACK(1, "back"); - - companion object { - /** - * Returns the [CameraFacingDirection] from the given id. - */ - internal fun entryOf(value: Int) = entries.first { it.value == value } - - /** - * Returns the [CameraFacingDirection] from the given id. - */ - internal fun entryOf(value: String) = entries.first { it.id == value } - } - } - /** * Options for the position of the [PreviewView] within its container. */ diff --git a/core/src/main/res/values/attrs.xml b/core/src/main/res/values/attrs.xml index 480053030..dafde487c 100644 --- a/core/src/main/res/values/attrs.xml +++ b/core/src/main/res/values/attrs.xml @@ -6,12 +6,6 @@ - - - - - - @@ -22,7 +16,6 @@ - diff --git a/demos/camera/src/main/res/layout/main_fragment.xml b/demos/camera/src/main/res/layout/main_fragment.xml index 9ecbf91eb..258ba7e69 100644 --- a/demos/camera/src/main/res/layout/main_fragment.xml +++ b/demos/camera/src/main/res/layout/main_fragment.xml @@ -19,7 +19,6 @@ android:id="@+id/preview" android:layout_width="match_parent" android:layout_height="match_parent" - app:cameraFacingDirection="back" app:enableZoomOnPinch="true" app:scaleMode="fill" app:position="center"