Skip to content

Commit

Permalink
refactor(ui): simplify PreviewView
Browse files Browse the repository at this point in the history
  • Loading branch information
ThibaultBee committed Oct 4, 2024
1 parent 916f26d commit a88f272
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,13 @@ class CameraSource(
private val isStreaming: Boolean
get() {
val outputSurface = outputSurface ?: return false
return cameraController.hasTarget(outputSurface)
return cameraController.hasTarget(outputSurface) && cameraController.isRequestSessionRunning && cameraController.isCameraRunning
}

override val isPreviewing: Boolean
get() {
val previewSurface = previewSurface ?: return false
return cameraController.hasTarget(previewSurface)
return cameraController.hasTarget(previewSurface) && cameraController.isRequestSessionRunning && cameraController.isCameraRunning
}

override fun configure(config: VideoConfig) {
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ guava = "33.3.1-android"
videoApiClient = "1.5.7"
androidxActivity = "1.9.2"
androidxAppcompat = "1.7.0"
androidxCamera = "1.4.0-alpha08"
androidxCamera = "1.4.0-alpha09"
androidxConstraintlayout = "2.1.4"
androidxCore = "1.13.1"
androidxDatabinding = "8.6.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,19 @@ package io.github.thibaultbee.streampack.ui.views

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.PointF
import android.graphics.Rect
import android.util.AttributeSet
import android.util.Size
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener
import android.view.Surface
import android.view.SurfaceHolder
import android.view.ViewConfiguration
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.camera.viewfinder.CameraViewfinder
import androidx.camera.viewfinder.CameraViewfinderExt.requestSurface
import androidx.camera.viewfinder.surface.ViewfinderSurfaceRequest
import androidx.camera.viewfinder.surface.populateFromCharacteristics
import androidx.core.app.ActivityCompat
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import io.github.thibaultbee.streampack.core.internal.utils.OrientationUtils
Expand All @@ -47,6 +42,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 java.security.InvalidParameterException
import java.util.concurrent.CancellationException

/**
Expand All @@ -64,9 +60,9 @@ class PreviewView @JvmOverloads constructor(
attrs: AttributeSet? = null,
defStyle: Int = 0
) : FrameLayout(context, attrs, defStyle) {
private val cameraViewFinder = CameraViewfinder(context, attrs, defStyle)
private val cameraViewfinder = CameraViewfinder(context, attrs, defStyle)

private var viewFinderSurfaceRequest: ViewfinderSurfaceRequest? = null
private var viewfinderSurfaceRequest: ViewfinderSurfaceRequest? = null

private val lifecycleScope: CoroutineScope?
get() = findViewTreeLifecycleOwner()?.lifecycleScope
Expand Down Expand Up @@ -94,29 +90,33 @@ class PreviewView @JvmOverloads constructor(
* @param value the [ICameraStreamer] to preview
*/
set(value) {
stopPreviewInternal()
if (field == value) {
Logger.w(TAG, "Streamer is already set to $value")
return
}
field?.stopPreview()
field = value
value?.let {
startPreviewIfReady(it, size, false)
startPreviewAsyncInternal(true)
}
}

/**
* The position of the [PreviewView] within its container.
*/
var position: Position
get() = getPosition(cameraViewFinder.scaleType)
get() = getPosition(cameraViewfinder.scaleType)
set(value) {
cameraViewFinder.scaleType = getScaleType(scaleMode, value)
cameraViewfinder.scaleType = getScaleType(scaleMode, value)
}

/**
* The scale mode of the [PreviewView] within its container.
*/
var scaleMode: ScaleMode
get() = getScaleMode(cameraViewFinder.scaleType)
get() = getScaleMode(cameraViewfinder.scaleType)
set(value) {
cameraViewFinder.scaleType = getScaleType(value, position)
cameraViewfinder.scaleType = getScaleType(value, position)
}

/**
Expand Down Expand Up @@ -158,7 +158,7 @@ class PreviewView @JvmOverloads constructor(
}

addView(
cameraViewFinder,
cameraViewfinder,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
Expand All @@ -168,8 +168,10 @@ class PreviewView @JvmOverloads constructor(

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
if (w != oldw || h != oldh) {
stopPreviewInternal()
streamer?.let { startPreviewIfReady(it, size, true) }
streamer?.let {
it.stopPreview()
startPreviewAsyncInternal(true)
}
}
}

Expand Down Expand Up @@ -234,42 +236,31 @@ class PreviewView @JvmOverloads constructor(

private fun stopPreviewInternal() {
streamer?.stopPreview()
viewFinderSurfaceRequest?.markSurfaceSafeToRelease()
viewFinderSurfaceRequest = null
viewfinderSurfaceRequest?.markSurfaceSafeToRelease()
viewfinderSurfaceRequest = null
}

/**
* Starts the preview.
*/
fun startPreview() {
streamer?.let {
startPreviewIfReady(it, size, false)
} ?: throw UnsupportedOperationException("Streamer has not been set")
suspend fun startPreview() {
startPreviewInternal(false)
}

/**
* 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
* Starts the preview asynchronously.
*/
private fun startPreviewIfReady(
streamer: ICameraStreamer,
targetViewSize: Size,
shouldFailSilently: Boolean
) {
try {
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
throw SecurityException("Camera permission is needed to run this application")
}
fun startPreviewAsync() {
startPreviewAsyncInternal(false)
}

/**
* Starts the preview.
*/
private fun startPreviewAsyncInternal(shouldFailSilently: Boolean) {
try {
lifecycleScope?.launch {
startPreviewInternal(streamer, streamer.camera, targetViewSize)
startPreviewInternal(true)
} ?: throw IllegalStateException("LifecycleScope is not available")
} catch (t: Throwable) {
if (shouldFailSilently) {
Expand All @@ -280,68 +271,68 @@ class PreviewView @JvmOverloads constructor(
}
}

private suspend fun startPreviewInternal(
/**
* Starts the preview.
*/
private suspend fun startPreviewInternal(shouldFailSilently: Boolean) {
try {
if (size.width == 0 || size.height == 0) {
return
}
streamer?.let {
setPreview(it, size)
startPreview(it)
listener?.onPreviewStarted()
} ?: throw UnsupportedOperationException("Streamer has not been set")
} catch (t: Throwable) {
if (shouldFailSilently) {
Logger.w(TAG, t.toString(), t)
} else {
throw t
}
}
}

/**
* Sets the preview if the view size is ready.
*
* @param streamer the camera streamer
* @param targetViewSize the view size
*/
private suspend fun setPreview(
streamer: ICameraStreamer,
targetViewSize: Size,
) = setPreviewInternal(streamer, streamer.camera, targetViewSize)

private suspend fun setPreviewInternal(
streamer: ICameraStreamer,
camera: String,
targetViewSize: Size
) {
Logger.d(TAG, "Target view size: $targetViewSize")
Logger.i(TAG, "Starting on camera: $camera")

val request = createRequest(targetViewSize, camera)
viewFinderSurfaceRequest?.markSurfaceSafeToRelease()
viewFinderSurfaceRequest = request
val previewSize = getPreviewSize(targetViewSize, camera)

try {
val surface = sendRequest(request)
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
viewFinderSurfaceRequest?.markSurfaceSafeToRelease()
viewFinderSurfaceRequest = null
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) {
when (streamer) {
is ICameraCoroutineStreamer -> {
streamer.startPreview(surface)
}

is ICameraCallbackStreamer -> {
streamer.startPreview(surface)
}

else -> {
Logger.e(TAG, "Streamer is not a ICameraCoroutineStreamer")
listener?.onPreviewFailed(IllegalStateException("Streamer is not a ICameraCoroutineStreamer"))
}
}
listener?.onPreviewStarted()
} else {
Logger.w(TAG, "Invalid surface")
listener?.onPreviewFailed(IllegalStateException("Invalid surface"))
}
}
// Request a new preview
viewfinderSurfaceRequest = setPreview(streamer, cameraViewfinder, previewSize)
} catch (e: CancellationException) {
Logger.w(TAG, "Preview request cancelled")
throw e
} catch (t: Throwable) {
viewFinderSurfaceRequest?.markSurfaceSafeToRelease()
viewFinderSurfaceRequest = null
viewfinderSurfaceRequest?.markSurfaceSafeToRelease()
viewfinderSurfaceRequest = null
Logger.w(TAG, "Failed to get a Surface: $t", t)
listener?.onPreviewFailed(t)
throw t
}
}

private fun createRequest(
private fun getPreviewSize(
targetViewSize: Size,
camera: String,
): ViewfinderSurfaceRequest {
): Size {
/**
* Get the closest available preview size to the view size.
*/
Expand All @@ -353,19 +344,35 @@ class PreviewView @JvmOverloads constructor(

Logger.d(TAG, "Selected preview size: $previewSize")

val builder = ViewfinderSurfaceRequest.Builder(previewSize)
builder.populateFromCharacteristics(context.getCameraCharacteristics(camera))

return builder.build()
}

private suspend fun sendRequest(request: ViewfinderSurfaceRequest): Surface {
return cameraViewFinder.requestSurface(request)
return previewSize
}

companion object {
private const val TAG = "PreviewView"

private suspend fun startPreview(streamer: ICameraStreamer) {
when (streamer) {
is ICameraCoroutineStreamer -> streamer.startPreview()
is ICameraCallbackStreamer -> streamer.startPreview()
else -> {
throw InvalidParameterException("Streamer is not a recognized type: ${streamer::class.java.simpleName}")
}
}
}

private suspend fun setPreview(
streamer: ICameraStreamer,
viewfinder: CameraViewfinder,
previewSize: Size
): ViewfinderSurfaceRequest {
return when (streamer) {
is ICameraCoroutineStreamer -> streamer.setPreview(viewfinder, previewSize)
is ICameraCallbackStreamer -> streamer.setPreview(viewfinder, previewSize)
else -> {
throw InvalidParameterException("Streamer is not a recognized type: ${streamer::class.java.simpleName}")
}
}
}

private fun getPosition(scaleType: CameraViewfinder.ScaleType): Position {
return when (scaleType) {
Expand Down

0 comments on commit a88f272

Please sign in to comment.