Skip to content

Commit

Permalink
fix(*): multiple fixes on preview:
Browse files Browse the repository at this point in the history
- random stretch when switching from landscape to portrait
- remove `cameraFacingDirection` as it is possible to set it on the streamer directly.
- catch exception onTap when camera is somehow null
  • Loading branch information
ThibaultBee committed Apr 18, 2024
1 parent 649d89b commit adae8fd
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 135 deletions.
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,10 @@ To simplify integration, StreamPack provides an `PreviewView`.
<layout>
<io.github.thibaultbee.streampack.views.PreviewView android:id="@+id/preview"
android:layout_width="match_parent" android:layout_height="match_parent"
app:cameraFacingDirection="back" app:enableZoomOnPinch="true" />
app:enableZoomOnPinch="true" />
</layout>
```

`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)
Expand Down
189 changes: 65 additions & 124 deletions core/src/main/java/io/github/thibaultbee/streampack/views/PreviewView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand All @@ -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.
*/
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
}
}

}
Expand All @@ -235,7 +230,7 @@ class PreviewView @JvmOverloads constructor(
* Stops the preview.
*/
private fun stopPreview() {
post {
lifecycleOwner.lifecycleScope.launch {
stopPreviewInternal()
}
}
Expand All @@ -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,
Expand All @@ -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) {
Expand All @@ -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(
Expand All @@ -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<Surface> {
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) {
Expand Down Expand Up @@ -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.
*/
Expand Down
7 changes: 0 additions & 7 deletions core/src/main/res/values/attrs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@
<enum name="fit" value="1"/>
</attr>

<!-- Must be kept in sync with PreviewView -->
<attr name="cameraFacingDirection" format="enum">
<enum name="front" value="0"/>
<enum name="back" value="1"/>
</attr>

<!-- Must be kept in sync with PreviewView -->
<attr name="position" format="enum">
<enum name="start" value="0"/>
Expand All @@ -22,7 +16,6 @@
<declare-styleable name="PreviewView">
<attr name="enableZoomOnPinch" format="boolean" />
<attr name="enableTapToFocus" format="boolean" />
<attr name="cameraFacingDirection" />
<attr name="scaleMode" />
<attr name="position" />
</declare-styleable>
Expand Down
1 change: 0 additions & 1 deletion demos/camera/src/main/res/layout/main_fragment.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit adae8fd

Please sign in to comment.