fix: Fix Preview stretching on Android (#2377)

* fix: Fix Preview stretching on Android

* fix: Simplify Preview size computation

* fix: Catch `stopRepeating` error

* fix: Fix preview size calculation

* Format code

* Update CameraSession.kt

* Enable CodeScanner in example app

* fix: Also update size on surface change

* Format

* fix: Flip sizes

* Revert that stuff again

* Update PreviewView.kt

* fix: Swap width and height in SurfaceHolder::setFixedSize
This commit is contained in:
Marc Rousavy 2024-01-11 16:33:40 +01:00 committed by GitHub
parent 2b10622559
commit 322b6fcbd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 20 additions and 48 deletions

View File

@ -28,7 +28,6 @@ import com.mrousavy.camera.core.outputs.BarcodeScannerOutput
import com.mrousavy.camera.core.outputs.PhotoOutput import com.mrousavy.camera.core.outputs.PhotoOutput
import com.mrousavy.camera.core.outputs.SurfaceOutput import com.mrousavy.camera.core.outputs.SurfaceOutput
import com.mrousavy.camera.core.outputs.VideoPipelineOutput import com.mrousavy.camera.core.outputs.VideoPipelineOutput
import com.mrousavy.camera.extensions.bigger
import com.mrousavy.camera.extensions.capture import com.mrousavy.camera.extensions.capture
import com.mrousavy.camera.extensions.closestToOrMax import com.mrousavy.camera.extensions.closestToOrMax
import com.mrousavy.camera.extensions.createCaptureSession import com.mrousavy.camera.extensions.createCaptureSession
@ -38,7 +37,6 @@ import com.mrousavy.camera.extensions.getPreviewTargetSize
import com.mrousavy.camera.extensions.getVideoSizes import com.mrousavy.camera.extensions.getVideoSizes
import com.mrousavy.camera.extensions.openCamera import com.mrousavy.camera.extensions.openCamera
import com.mrousavy.camera.extensions.setZoom import com.mrousavy.camera.extensions.setZoom
import com.mrousavy.camera.extensions.smaller
import com.mrousavy.camera.frameprocessor.FrameProcessor import com.mrousavy.camera.frameprocessor.FrameProcessor
import com.mrousavy.camera.types.Flash import com.mrousavy.camera.types.Flash
import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.Orientation
@ -48,6 +46,7 @@ import com.mrousavy.camera.types.Torch
import com.mrousavy.camera.types.VideoStabilizationMode import com.mrousavy.camera.types.VideoStabilizationMode
import com.mrousavy.camera.utils.ImageFormatUtils import com.mrousavy.camera.utils.ImageFormatUtils
import java.io.Closeable import java.io.Closeable
import java.lang.IllegalStateException
import java.util.concurrent.CancellationException import java.util.concurrent.CancellationException
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -246,6 +245,8 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
private fun destroyPreviewOutputSync() { private fun destroyPreviewOutputSync() {
Log.i(TAG, "Destroying Preview Output...") Log.i(TAG, "Destroying Preview Output...")
// This needs to run synchronously because after this method returns, the Preview Surface is no longer valid,
// and trying to use it will crash. This might result in a short UI Thread freeze though.
runBlocking { runBlocking {
configure { config -> configure { config ->
config.preview = CameraConfiguration.Output.Disabled.create() config.preview = CameraConfiguration.Output.Disabled.create()
@ -379,12 +380,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
if (preview != null) { if (preview != null) {
// Compute Preview Size based on chosen video size // Compute Preview Size based on chosen video size
val videoSize = videoOutput?.size ?: format?.videoSize val videoSize = videoOutput?.size ?: format?.videoSize
val size = if (videoSize != null) { val size = characteristics.getPreviewTargetSize(videoSize)
val formatAspectRatio = videoSize.bigger.toDouble() / videoSize.smaller
characteristics.getPreviewTargetSize(formatAspectRatio)
} else {
characteristics.getPreviewTargetSize(null)
}
val enableHdr = video?.config?.enableHdr ?: false val enableHdr = video?.config?.enableHdr ?: false
@ -396,7 +392,8 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
enableHdr enableHdr
) )
outputs.add(output) outputs.add(output)
previewView?.size = size // Size is usually landscape, so we flip it here
previewView?.size = Size(size.height, size.width)
} }
// CodeScanner Output // CodeScanner Output
@ -520,7 +517,11 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
if (!config.isActive) { if (!config.isActive) {
isRunning = false isRunning = false
captureSession?.stopRepeating() try {
captureSession?.stopRepeating()
} catch (e: IllegalStateException) {
// ignore - captureSession is already closed.
}
return return
} }
if (captureSession == null) { if (captureSession == null) {

View File

@ -19,8 +19,8 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceV
set(value) { set(value) {
field = value field = value
UiThreadUtil.runOnUiThread { UiThreadUtil.runOnUiThread {
Log.i(TAG, "Resizing PreviewView to ${value.width} x ${value.height}...") Log.i(TAG, "Setting PreviewView Surface Size to $width x $height...")
holder.setFixedSize(value.width, value.height) holder.setFixedSize(value.height, value.width)
requestLayout() requestLayout()
invalidate() invalidate()
} }
@ -44,20 +44,10 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceV
holder.addCallback(callback) holder.addCallback(callback)
} }
/*fun resizeToInputCamera(cameraId: String, cameraManager: CameraManager, format: CameraDeviceFormat?) {
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val targetPreviewSize = format?.videoSize
val formatAspectRatio = if (targetPreviewSize != null) targetPreviewSize.bigger.toDouble() / targetPreviewSize.smaller else null
size = characteristics.getPreviewTargetSize(formatAspectRatio)
}*/
private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size { private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size {
val contentAspectRatio = contentSize.height.toDouble() / contentSize.width val contentAspectRatio = contentSize.width.toDouble() / contentSize.height
val containerAspectRatio = containerSize.width.toDouble() / containerSize.height val containerAspectRatio = containerSize.width.toDouble() / containerSize.height
Log.i(TAG, "Content Size: $contentSize ($contentAspectRatio) | Container Size: $containerSize ($containerAspectRatio)")
val widthOverHeight = when (resizeMode) { val widthOverHeight = when (resizeMode) {
ResizeMode.COVER -> contentAspectRatio > containerAspectRatio ResizeMode.COVER -> contentAspectRatio > containerAspectRatio
ResizeMode.CONTAIN -> contentAspectRatio < containerAspectRatio ResizeMode.CONTAIN -> contentAspectRatio < containerAspectRatio

View File

@ -4,13 +4,12 @@ import android.content.res.Resources
import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraCharacteristics
import android.util.Size import android.util.Size
import android.view.SurfaceHolder import android.view.SurfaceHolder
import kotlin.math.abs
fun getMaximumPreviewSize(): Size { fun getMaximumPreviewSize(): Size {
// See https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap // See https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap
// According to the Android Developer documentation, PREVIEW streams can have a resolution // According to the Android Developer documentation, PREVIEW streams can have a resolution
// of up to the phone's display's resolution, with a maximum of 1920x1080. // of up to the phone's display's resolution, with a maximum of 1920x1080.
val display1080p = Size(1920, 1080) val display1080p = Size(1080, 1920)
val displaySize = Size( val displaySize = Size(
Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.widthPixels,
Resources.getSystem().displayMetrics.heightPixels Resources.getSystem().displayMetrics.heightPixels
@ -20,28 +19,11 @@ fun getMaximumPreviewSize(): Size {
return if (isHighResScreen) display1080p else displaySize return if (isHighResScreen) display1080p else displaySize
} }
fun CameraCharacteristics.getPreviewSizeFromAspectRatio(aspectRatio: Double): Size { fun CameraCharacteristics.getPreviewTargetSize(targetSize: Size?): Size {
val config = this.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! val config = this.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
val maximumPreviewSize = getMaximumPreviewSize() val maximumPreviewSize = getMaximumPreviewSize()
val outputSizes = config.getOutputSizes(SurfaceHolder::class.java) val outputSizes = config.getOutputSizes(SurfaceHolder::class.java)
.sortedByDescending { it.width * it.height } .filter { it.bigger <= maximumPreviewSize.bigger && it.smaller <= maximumPreviewSize.smaller }
.sortedBy { abs(aspectRatio - (it.bigger.toDouble() / it.smaller)) }
return outputSizes.first { it.bigger <= maximumPreviewSize.bigger && it.smaller <= maximumPreviewSize.smaller } return outputSizes.closestToOrMax(targetSize)
} }
fun CameraCharacteristics.getAutomaticPreviewSize(): Size {
val config = this.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
val maximumPreviewSize = getMaximumPreviewSize()
val outputSizes = config.getOutputSizes(SurfaceHolder::class.java)
.sortedByDescending { it.width * it.height }
return outputSizes.first { it.bigger <= maximumPreviewSize.bigger && it.smaller <= maximumPreviewSize.smaller }
}
fun CameraCharacteristics.getPreviewTargetSize(aspectRatio: Double?): Size =
if (aspectRatio != null) {
getPreviewSizeFromAspectRatio(aspectRatio)
} else {
getAutomaticPreviewSize()
}

View File

@ -9,7 +9,7 @@ import kotlin.math.min
fun List<Size>.closestToOrMax(size: Size?): Size = fun List<Size>.closestToOrMax(size: Size?): Size =
if (size != null) { if (size != null) {
this.minBy { abs(it.width - size.width) + abs(it.height - size.height) } this.minBy { abs((it.width * it.height) - (size.width * size.height)) }
} else { } else {
this.maxBy { it.width * it.height } this.maxBy { it.width * it.height }
} }

View File

@ -10,7 +10,6 @@ import com.facebook.proguard.annotations.DoNotStrip;
/** /**
* Represents a JS Frame Processor * Represents a JS Frame Processor
*/ */
@SuppressWarnings("JavaJniMissingFunction") // we're using fbjni.
public final class FrameProcessor { public final class FrameProcessor {
/** /**
* Call the JS Frame Processor function with the given Frame * Call the JS Frame Processor function with the given Frame

View File

@ -42,4 +42,4 @@ hermesEnabled=true
# Can be set to true to disable the build setup # Can be set to true to disable the build setup
#VisionCamera_disableFrameProcessors=true #VisionCamera_disableFrameProcessors=true
# Can be set to true to include the full 2.4 MB MLKit dependency # Can be set to true to include the full 2.4 MB MLKit dependency
#VisionCamera_enableCodeScanner=true VisionCamera_enableCodeScanner=true