Skip to content

Commit

Permalink
fix: Take Orientation into account for PreviewView (#2565)
Browse files Browse the repository at this point in the history
* fix: Take Orientation into account for `PreviewView`

* Log

* Take aspect ratio into account

* Reorganize code a bit

* Set LANDSCAPE_LEFT as default

* chore: Format
  • Loading branch information
mrousavy authored Feb 15, 2024
1 parent 5df5ca9 commit 83c0184
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 83 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mrousavy.camera.core

import android.content.res.Resources
import android.graphics.ImageFormat
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraExtensionCharacteristics
Expand All @@ -9,11 +10,14 @@ import android.os.Build
import android.util.Log
import android.util.Range
import android.util.Size
import android.view.SurfaceHolder
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.mrousavy.camera.extensions.bigger
import com.mrousavy.camera.extensions.getPhotoSizes
import com.mrousavy.camera.extensions.getVideoSizes
import com.mrousavy.camera.extensions.smaller
import com.mrousavy.camera.extensions.toJSValue
import com.mrousavy.camera.types.AutoFocusSystem
import com.mrousavy.camera.types.DeviceType
Expand All @@ -29,6 +33,20 @@ import kotlin.math.sqrt
class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId: String) {
companion object {
private const val TAG = "CameraDeviceDetails"

fun getMaximumPreviewSize(): Size {
// See https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap
// 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.
val display1080p = Size(1920, 1080)
val displaySize = Size(
Resources.getSystem().displayMetrics.widthPixels,
Resources.getSystem().displayMetrics.heightPixels
)
val isHighResScreen = displaySize.bigger >= display1080p.bigger || displaySize.smaller >= display1080p.smaller

return if (isHighResScreen) display1080p else displaySize
}
}

val characteristics by lazy { cameraManager.getCameraCharacteristics(cameraId) }
Expand All @@ -50,7 +68,10 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
val sensorSize by lazy { characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE)!! }
val activeSize
get() = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!!
val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
val sensorOrientation by lazy {
val degrees = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
return@lazy Orientation.fromRotationDegrees(degrees)
}
val minFocusDistance by lazy { getMinFocusDistanceCm() }
val name by lazy {
val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) characteristics.get(CameraCharacteristics.INFO_VERSION) else null
Expand Down Expand Up @@ -121,6 +142,7 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId

// TODO: Also add 10-bit YUV here?
val videoFormat = ImageFormat.YUV_420_888
val photoFormat = ImageFormat.JPEG

// get extensions (HDR, Night Mode, ..)
private fun getSupportedExtensions(): List<Int> =
Expand Down Expand Up @@ -214,13 +236,18 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
return getFieldOfView(smallestFocalLength)
}

private fun getVideoSizes(): List<Size> = characteristics.getVideoSizes(cameraId, videoFormat)
private fun getPhotoSizes(): List<Size> = characteristics.getPhotoSizes(ImageFormat.JPEG)
fun getVideoSizes(format: Int): List<Size> = characteristics.getVideoSizes(cameraId, format)
fun getPhotoSizes(): List<Size> = characteristics.getPhotoSizes(photoFormat)
fun getPreviewSizes(): List<Size> {
val maximumPreviewSize = getMaximumPreviewSize()
return cameraConfig.getOutputSizes(SurfaceHolder::class.java)
.filter { it.bigger <= maximumPreviewSize.bigger && it.smaller <= maximumPreviewSize.smaller }
}

private fun getFormats(): ReadableArray {
val array = Arguments.createArray()

val videoSizes = getVideoSizes()
val videoSizes = getVideoSizes(videoFormat)
val photoSizes = getPhotoSizes()

videoSizes.forEach { videoSize ->
Expand Down Expand Up @@ -294,7 +321,7 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
map.putDouble("minExposure", exposureRange.lower.toDouble())
map.putDouble("maxExposure", exposureRange.upper.toDouble())
map.putString("hardwareLevel", hardwareLevel.unionValue)
map.putString("sensorOrientation", Orientation.fromRotationDegrees(sensorOrientation).unionValue)
map.putString("sensorOrientation", sensorOrientation.unionValue)
map.putArray("formats", getFormats())
return map
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ import com.mrousavy.camera.core.outputs.PhotoOutput
import com.mrousavy.camera.core.outputs.SurfaceOutput
import com.mrousavy.camera.core.outputs.VideoPipelineOutput
import com.mrousavy.camera.extensions.closestToOrMax
import com.mrousavy.camera.extensions.getPhotoSizes
import com.mrousavy.camera.extensions.getPreviewTargetSize
import com.mrousavy.camera.extensions.getVideoSizes
import com.mrousavy.camera.frameprocessor.Frame
import com.mrousavy.camera.types.Flash
import com.mrousavy.camera.types.LensFacing
Expand Down Expand Up @@ -245,20 +242,20 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
codeScannerOutput = null
isRunning = false

val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val deviceDetails = CameraDeviceDetails(cameraManager, cameraId)
val format = configuration.format

Log.i(TAG, "Creating outputs for Camera #$cameraId...")

val isSelfie = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
val isSelfie = deviceDetails.lensFacing == LensFacing.FRONT

val outputs = mutableListOf<SurfaceOutput>()

// Photo Output
val photo = configuration.photo as? CameraConfiguration.Output.Enabled<CameraConfiguration.Photo>
if (photo != null) {
val imageFormat = ImageFormat.JPEG
val sizes = characteristics.getPhotoSizes(imageFormat)
val imageFormat = deviceDetails.photoFormat
val sizes = deviceDetails.getPhotoSizes()
val size = sizes.closestToOrMax(format?.photoSize)
val maxImages = 10

Expand All @@ -278,7 +275,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
val video = configuration.video as? CameraConfiguration.Output.Enabled<CameraConfiguration.Video>
if (video != null) {
val imageFormat = video.config.pixelFormat.toImageFormat()
val sizes = characteristics.getVideoSizes(cameraId, imageFormat)
val sizes = deviceDetails.getVideoSizes(imageFormat)
val size = sizes.closestToOrMax(format?.videoSize)

Log.i(TAG, "Adding ${size.width}x${size.height} Video Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...")
Expand All @@ -301,7 +298,8 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
if (preview != null) {
// Compute Preview Size based on chosen video size
val videoSize = videoOutput?.size ?: format?.videoSize
val size = characteristics.getPreviewTargetSize(videoSize)
val sizes = deviceDetails.getPreviewSizes()
val size = sizes.closestToOrMax(videoSize)

val enableHdr = video?.config?.enableHdr ?: false

Expand All @@ -314,7 +312,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
)
outputs.add(output)
// Size is usually landscape, so we flip it here
previewView?.setSurfaceSize(size.width, size.height)
previewView?.setSurfaceSize(size.width, size.height, deviceDetails.sensorOrientation)
}

// CodeScanner Output
Expand All @@ -327,7 +325,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
}

val imageFormat = ImageFormat.YUV_420_888
val sizes = characteristics.getVideoSizes(cameraId, imageFormat)
val sizes = deviceDetails.getVideoSizes(imageFormat)
val size = sizes.closestToOrMax(Size(1280, 720))

Log.i(TAG, "Adding ${size.width}x${size.height} CodeScanner Output in ${ImageFormatUtils.imageFormatToString(imageFormat)}...")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import android.view.SurfaceHolder
import android.view.SurfaceView
import android.widget.FrameLayout
import com.facebook.react.bridge.UiThreadUtil
import com.mrousavy.camera.extensions.getMaximumPreviewSize
import com.mrousavy.camera.extensions.installHierarchyFitter
import com.mrousavy.camera.extensions.resize
import com.mrousavy.camera.extensions.rotatedBy
import com.mrousavy.camera.types.Orientation
import com.mrousavy.camera.types.ResizeMode
import kotlin.math.roundToInt
Expand All @@ -23,7 +23,7 @@ import kotlinx.coroutines.withContext
class PreviewView(context: Context, callback: SurfaceHolder.Callback) :
FrameLayout(context),
SurfaceHolder.Callback {
var size: Size = getMaximumPreviewSize()
var size: Size = CameraDeviceDetails.getMaximumPreviewSize()
private set
var resizeMode: ResizeMode = ResizeMode.COVER
set(value) {
Expand All @@ -34,6 +34,15 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) :
invalidate()
}
}
private var inputOrientation: Orientation = Orientation.LANDSCAPE_LEFT
set(value) {
field = value
UiThreadUtil.runOnUiThread {
Log.i(TAG, "Camera Input Orientation changed to $value!")
requestLayout()
invalidate()
}
}
private val viewSize: Size
get() {
val displayMetrics = context.resources.displayMetrics
Expand Down Expand Up @@ -66,25 +75,26 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) :
invalidate()
}

suspend fun setSurfaceSize(width: Int, height: Int) {
suspend fun setSurfaceSize(width: Int, height: Int, cameraSensorOrientation: Orientation) {
withContext(Dispatchers.Main) {
inputOrientation = cameraSensorOrientation
surfaceView.holder.resize(width, height)
}
}

fun convertLayerPointToCameraCoordinates(point: Point, cameraDeviceDetails: CameraDeviceDetails): Point {
val sensorOrientation = Orientation.fromRotationDegrees(cameraDeviceDetails.sensorOrientation)
val sensorOrientation = cameraDeviceDetails.sensorOrientation
val cameraSize = Size(cameraDeviceDetails.activeSize.width(), cameraDeviceDetails.activeSize.height())
val viewOrientation = Orientation.PORTRAIT

val rotated = Orientation.rotatePoint(point, viewSize, cameraSize, viewOrientation, sensorOrientation)
Log.i(TAG, "$point -> $sensorOrientation (in $cameraSize -> $viewSize) -> $rotated")
val rotated = point.rotatedBy(viewSize, cameraSize, viewOrientation, sensorOrientation)
Log.i(TAG, "Converted layer point $point to camera point $rotated! ($sensorOrientation, $cameraSize -> $viewSize)")
return rotated
}

private fun getSize(contentSize: Size, containerSize: Size, resizeMode: ResizeMode): Size {
// TODO: Take sensor orientation into account here
val contentAspectRatio = contentSize.height.toDouble() / contentSize.width
val contentAspectRatio = contentSize.width.toDouble() / contentSize.height
val containerAspectRatio = containerSize.width.toDouble() / containerSize.height

val widthOverHeight = when (resizeMode) {
Expand All @@ -108,9 +118,10 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) :
super.onMeasure(widthMeasureSpec, heightMeasureSpec)

val viewSize = Size(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec))
val fittedSize = getSize(size, viewSize, resizeMode)
val surfaceSize = size.rotatedBy(inputOrientation)
val fittedSize = getSize(surfaceSize, viewSize, resizeMode)

Log.i(TAG, "PreviewView is $viewSize, rendering $size content. Resizing to: $fittedSize ($resizeMode)")
Log.i(TAG, "PreviewView is $viewSize, rendering $surfaceSize content ($inputOrientation). Resizing to: $fittedSize ($resizeMode)")
setMeasuredDimension(fittedSize.width, fittedSize.height)
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.mrousavy.camera.extensions

import android.graphics.Point
import android.graphics.PointF
import android.util.Log
import android.util.Size
import com.mrousavy.camera.types.Orientation

fun Point.rotatedBy(fromSize: Size, toSize: Size, fromOrientation: Orientation, toOrientation: Orientation): Point {
val differenceDegrees = (fromOrientation.toDegrees() + toOrientation.toDegrees()) % 360
val difference = Orientation.fromRotationDegrees(differenceDegrees)
val normalizedPoint = PointF(this.x / fromSize.width.toFloat(), this.y / fromSize.height.toFloat())

val rotatedNormalizedPoint = when (difference) {
Orientation.PORTRAIT -> normalizedPoint
Orientation.PORTRAIT_UPSIDE_DOWN -> PointF(1 - normalizedPoint.x, 1 - normalizedPoint.y)
Orientation.LANDSCAPE_LEFT -> PointF(normalizedPoint.y, 1 - normalizedPoint.x)
Orientation.LANDSCAPE_RIGHT -> PointF(1 - normalizedPoint.y, normalizedPoint.x)
}

val rotatedX = rotatedNormalizedPoint.x * toSize.width
val rotatedY = rotatedNormalizedPoint.y * toSize.height
Log.i("ROTATE", "$this -> $normalizedPoint -> $difference -> $rotatedX, $rotatedY")
return Point(rotatedX.toInt(), rotatedY.toInt())
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package com.mrousavy.camera.types

import android.graphics.Point
import android.graphics.PointF
import android.util.Log
import android.util.Size
import com.mrousavy.camera.core.CameraDeviceDetails

enum class Orientation(override val unionValue: String) : JSUnionValue {
Expand All @@ -30,7 +26,7 @@ enum class Orientation(override val unionValue: String) : JSUnionValue {
}

// Rotate sensor rotation by target rotation
val newRotationDegrees = (deviceDetails.sensorOrientation + rotationDegrees + 360) % 360
val newRotationDegrees = (deviceDetails.sensorOrientation.toDegrees() + rotationDegrees + 360) % 360

return fromRotationDegrees(newRotationDegrees)
}
Expand All @@ -52,29 +48,5 @@ enum class Orientation(override val unionValue: String) : JSUnionValue {
in 225..315 -> LANDSCAPE_RIGHT
else -> PORTRAIT
}

fun rotatePoint(
point: Point,
fromSize: Size,
toSize: Size,
fromOrientation: Orientation,
toOrientation: Orientation
): Point {
val differenceDegrees = (fromOrientation.toDegrees() + toOrientation.toDegrees()) % 360
val difference = Orientation.fromRotationDegrees(differenceDegrees)
val normalizedPoint = PointF(point.x / fromSize.width.toFloat(), point.y / fromSize.height.toFloat())

val rotatedNormalizedPoint = when (difference) {
PORTRAIT -> normalizedPoint
PORTRAIT_UPSIDE_DOWN -> PointF(1 - normalizedPoint.x, 1 - normalizedPoint.y)
LANDSCAPE_LEFT -> PointF(normalizedPoint.y, 1 - normalizedPoint.x)
LANDSCAPE_RIGHT -> PointF(1 - normalizedPoint.y, normalizedPoint.x)
}

val rotatedX = rotatedNormalizedPoint.x * toSize.width
val rotatedY = rotatedNormalizedPoint.y * toSize.height
Log.i("ROTATE", "$point -> $normalizedPoint -> $difference -> $rotatedX, $rotatedY")
return Point(rotatedX.toInt(), rotatedY.toInt())
}
}
}

0 comments on commit 83c0184

Please sign in to comment.