Bump CameraX versions to alpha2/alpha22 (#7)

* Bump CameraX versions to alpha2/alpha22
* Use `setDefaultResolution` to set format's photoSize
This commit is contained in:
Marc Rousavy 2021-02-26 17:34:28 +01:00 committed by GitHub
parent 03b9246afe
commit 6438b9a8bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 65 additions and 61 deletions

View File

@ -10,6 +10,10 @@ on:
- '*.js' - '*.js'
- '*.json' - '*.json'
- 'package.json' - 'package.json'
- 'tsconfig.json'
- '.prettierrc.js'
- '.eslintrc.js'
- 'babel.config.js'
pull_request: pull_request:
paths: paths:
- '.github/workflows/validate-js.yml' - '.github/workflows/validate-js.yml'
@ -17,6 +21,10 @@ on:
- '*.js' - '*.js'
- '*.json' - '*.json'
- 'package.json' - 'package.json'
- 'tsconfig.json'
- '.prettierrc.js'
- '.eslintrc.js'
- 'babel.config.js'
jobs: jobs:
vibe_check: vibe_check:

View File

@ -138,10 +138,10 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.4.2" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.4.2"
implementation "androidx.camera:camera-core:1.1.0-alpha01" implementation "androidx.camera:camera-core:1.1.0-alpha02"
implementation "androidx.camera:camera-camera2:1.1.0-alpha01" implementation "androidx.camera:camera-camera2:1.1.0-alpha02"
implementation "androidx.camera:camera-lifecycle:1.1.0-alpha01" implementation "androidx.camera:camera-lifecycle:1.1.0-alpha02"
implementation "androidx.camera:camera-extensions:1.0.0-alpha21" implementation "androidx.camera:camera-extensions:1.0.0-alpha22"
implementation "androidx.camera:camera-view:1.0.0-alpha21" implementation "androidx.camera:camera-view:1.0.0-alpha22"
implementation "androidx.exifinterface:exifinterface:1.3.2" implementation "androidx.exifinterface:exifinterface:1.3.2"
} }

View File

@ -17,6 +17,7 @@ import androidx.camera.extensions.HdrImageCaptureExtender
import androidx.camera.extensions.HdrPreviewExtender import androidx.camera.extensions.HdrPreviewExtender
import androidx.camera.extensions.NightImageCaptureExtender import androidx.camera.extensions.NightImageCaptureExtender
import androidx.camera.extensions.NightPreviewExtender import androidx.camera.extensions.NightPreviewExtender
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.* import androidx.lifecycle.*
@ -24,6 +25,7 @@ import com.facebook.react.bridge.*
import com.facebook.react.uimanager.events.RCTEventEmitter import com.facebook.react.uimanager.events.RCTEventEmitter
import com.mrousavy.camera.utils.* import com.mrousavy.camera.utils.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.guava.await
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.math.max import kotlin.math.max
@ -126,7 +128,7 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
scaleGestureListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() { scaleGestureListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean { override fun onScale(detector: ScaleGestureDetector): Boolean {
zoom = min(max(((zoom + 1) * detector.scaleFactor) - 1, 0.0), 1.0) zoom = min(max(((zoom + 1) * detector.scaleFactor) - 1, 0.0), 1.0)
update(arrayListOf("zoom")) update(arrayListOfZoom)
return true return true
} }
} }
@ -239,30 +241,34 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
Log.d(REACT_CLASS, "Configuring session with Camera ID $cameraId and default format options...") Log.d(REACT_CLASS, "Configuring session with Camera ID $cameraId and default format options...")
// Used to bind the lifecycle of cameras to the lifecycle owner // Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider = getCameraProvider(context) val cameraProvider = ProcessCameraProvider.getInstance(context).await()
val cameraSelector = CameraSelector.Builder().byID(cameraId!!).build() val cameraSelector = CameraSelector.Builder().byID(cameraId!!).build()
val rotation = previewView.display.rotation val rotation = previewView.display.rotation
val aspectRatio = aspectRatio(previewView.width, previewView.height)
val previewBuilder = Preview.Builder() val previewBuilder = Preview.Builder()
.setTargetAspectRatio(aspectRatio)
.setTargetRotation(rotation) .setTargetRotation(rotation)
val imageCaptureBuilder = ImageCapture.Builder() val imageCaptureBuilder = ImageCapture.Builder()
.setTargetAspectRatio(aspectRatio)
.setTargetRotation(rotation) .setTargetRotation(rotation)
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
val videoCaptureBuilder = VideoCapture.Builder() val videoCaptureBuilder = VideoCapture.Builder()
.setTargetAspectRatio(aspectRatio)
.setTargetRotation(rotation) .setTargetRotation(rotation)
if (format != null) { if (format == null) {
// let CameraX automatically find best resolution for the target aspect ratio
Log.d(REACT_CLASS, "No custom format has been set, CameraX will automatically determine best configuration...")
val aspectRatio = aspectRatio(previewView.width, previewView.height)
previewBuilder.setTargetAspectRatio(aspectRatio)
imageCaptureBuilder.setTargetAspectRatio(aspectRatio)
videoCaptureBuilder.setTargetAspectRatio(aspectRatio)
} else {
// User has selected a custom format={}. Use that // User has selected a custom format={}. Use that
val format = DeviceFormat(format!!) val format = DeviceFormat(format!!)
Log.d(REACT_CLASS, "Using custom format - photo: ${format.photoSize}, video: ${format.videoSize} @ $fps FPS")
// The format (exported in CameraViewModule) specifies the resolution in ROTATION_90 (horizontal) previewBuilder.setDefaultResolution(format.photoSize)
val rotationRelativeToFormat = rotation - 1 // subtract one, so that ROTATION_90 becomes ROTATION_0 and so on imageCaptureBuilder.setDefaultResolution(format.photoSize)
videoCaptureBuilder.setDefaultResolution(format.photoSize)
fps?.let { fps -> fps?.let { fps ->
if (format.frameRateRanges.any { it.contains(fps) }) { if (format.frameRateRanges.any { it.contains(fps) }) {
@ -273,9 +279,7 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
Camera2Interop.Extender(previewBuilder) Camera2Interop.Extender(previewBuilder)
.setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps)) .setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps))
.setCaptureRequestOption(CaptureRequest.SENSOR_FRAME_DURATION, frameDuration) .setCaptureRequestOption(CaptureRequest.SENSOR_FRAME_DURATION, frameDuration)
Camera2Interop.Extender(videoCaptureBuilder) videoCaptureBuilder.setVideoFrameRate(fps)
.setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(fps, fps))
.setCaptureRequestOption(CaptureRequest.SENSOR_FRAME_DURATION, frameDuration)
} else { } else {
throw FpsNotContainedInFormatError(fps) throw FpsNotContainedInFormatError(fps)
} }
@ -313,17 +317,6 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
} }
} }
} }
// TODO: qualityPrioritization for ImageCapture
imageCaptureBuilder.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
val photoResolution = format.photoSize.rotated(rotationRelativeToFormat)
// TODO: imageCaptureBuilder.setTargetResolution(photoResolution)
Log.d(REACT_CLASS, "Using Photo Capture resolution $photoResolution")
fps?.let { fps ->
Log.d(REACT_CLASS, "Setting video recording FPS to $fps")
videoCaptureBuilder.setVideoFrameRate(fps)
}
} }
val preview = previewBuilder.build() val preview = previewBuilder.build()
@ -396,5 +389,7 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
const val REACT_CLASS = "CameraView" const val REACT_CLASS = "CameraView"
private val propsThatRequireSessionReconfiguration = arrayListOf("cameraId", "format", "fps", "hdr", "lowLightBoost") private val propsThatRequireSessionReconfiguration = arrayListOf("cameraId", "format", "fps", "hdr", "lowLightBoost")
private val arrayListOfZoom = arrayListOf("zoom")
} }
} }

View File

@ -34,7 +34,6 @@ class MicrophonePermissionError : CameraError("permission", "microphone-permissi
class CameraPermissionError : CameraError("permission", "camera-permission-denied", "The Camera permission was denied!") class CameraPermissionError : CameraError("permission", "camera-permission-denied", "The Camera permission was denied!")
class InvalidTypeScriptUnionError(unionName: String, unionValue: String) : CameraError("parameter", "invalid-parameter", "The given value for $unionName could not be parsed! (Received: $unionValue)") class InvalidTypeScriptUnionError(unionName: String, unionValue: String) : CameraError("parameter", "invalid-parameter", "The given value for $unionName could not be parsed! (Received: $unionValue)")
class UnsupportedOSError(unionName: String, unionValue: String, supportedOnOS: String) : CameraError("parameter", "unsupported-os", "The given value \"$unionValue\" could not be used for $unionName, as it is only available on Android $supportedOnOS and above!")
class NoCameraDeviceError : CameraError("device", "no-device", "No device was set! Use `getAvailableCameraDevices()` to select a suitable Camera device.") class NoCameraDeviceError : CameraError("device", "no-device", "No device was set! Use `getAvailableCameraDevices()` to select a suitable Camera device.")
class InvalidCameraDeviceError(cause: Throwable) : CameraError("device", "invalid-device", "The given Camera device could not be found for use-case binding!", cause) class InvalidCameraDeviceError(cause: Throwable) : CameraError("device", "invalid-device", "The given Camera device could not be found for use-case binding!", cause)

View File

@ -8,18 +8,16 @@ class DeviceFormat(map: ReadableMap) {
val frameRateRanges: List<Range<Int>> val frameRateRanges: List<Range<Int>>
val photoSize: Size val photoSize: Size
val videoSize: Size val videoSize: Size
val maxZoom: Double
init { init {
frameRateRanges = map.getArray("frameRateRanges")!!.toArrayList().map { range -> frameRateRanges = map.getArray("frameRateRanges")!!.toArrayList().map { range ->
if (range is HashMap<*, *>) if (range is HashMap<*, *>)
rangeFactory(range["minFrameRate"], range["maxFrameRate"]) rangeFactory(range["minFrameRate"], range["maxFrameRate"])
else else
throw IllegalArgumentException() throw IllegalArgumentException("DeviceFormat: frameRateRanges contained a Range that was not of type HashMap<*,*>! Actual Type: ${range?.javaClass?.name}")
} }
photoSize = Size(map.getInt("photoWidth"), map.getInt("photoHeight")) photoSize = Size(map.getInt("photoWidth"), map.getInt("photoHeight"))
videoSize = Size(map.getInt("videoWidth"), map.getInt("videoHeight")) videoSize = Size(map.getInt("videoWidth"), map.getInt("videoHeight"))
maxZoom = map.getDouble("maxZoom")
} }
} }
@ -27,6 +25,9 @@ fun rangeFactory(minFrameRate: Any?, maxFrameRate: Any?): Range<Int> {
return when (minFrameRate) { return when (minFrameRate) {
is Int -> Range(minFrameRate, maxFrameRate as Int) is Int -> Range(minFrameRate, maxFrameRate as Int)
is Double -> Range(minFrameRate.toInt(), (maxFrameRate as Double).toInt()) is Double -> Range(minFrameRate.toInt(), (maxFrameRate as Double).toInt())
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException(
"DeviceFormat: frameRateRanges contained a Range that didn't have minFrameRate/maxFrameRate of types Int/Double! " +
"Actual Type: ${minFrameRate?.javaClass?.name} & ${maxFrameRate?.javaClass?.name}"
)
} }
} }

View File

@ -1,18 +0,0 @@
package com.mrousavy.camera.utils
import android.content.Context
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
suspend fun getCameraProvider(context: Context) = suspendCoroutine<ProcessCameraProvider> { cont ->
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener(
{
cont.resume(cameraProviderFuture.get())
},
ContextCompat.getMainExecutor(context)
)
}

View File

@ -1,10 +1,10 @@
/** /*
* Copyright (c) Facebook, Inc. and its affiliates. Copyright (c) Facebook, Inc. and its affiliates.
*
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree. directory of this source tree.
*/ */
package com.mrousavy.camera; package com.mrousavy.camera.example;
import android.content.Context; import android.content.Context;
import com.facebook.flipper.android.AndroidFlipperClient; import com.facebook.flipper.android.AndroidFlipperClient;

View File

@ -1,7 +1,7 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { StyleSheet, View, Image, ActivityIndicator, PermissionsAndroid, Platform } from 'react-native'; import { StyleSheet, View, Image, ActivityIndicator, PermissionsAndroid, Platform } from 'react-native';
import { Navigation, NavigationFunctionComponent, OptionsModalPresentationStyle } from 'react-native-navigation'; import { Navigation, NavigationFunctionComponent, OptionsModalPresentationStyle } from 'react-native-navigation';
import Video from 'react-native-video'; import Video, { OnLoadData } from 'react-native-video';
import { SAFE_AREA_PADDING } from './Constants'; import { SAFE_AREA_PADDING } from './Constants';
import { useIsForeground } from './hooks/useIsForeground'; import { useIsForeground } from './hooks/useIsForeground';
import { useIsScreenFocused } from './hooks/useIsScreenFocused'; import { useIsScreenFocused } from './hooks/useIsScreenFocused';
@ -10,6 +10,8 @@ import IonIcon from 'react-native-vector-icons/Ionicons';
import { Alert } from 'react-native'; import { Alert } from 'react-native';
import CameraRoll from '@react-native-community/cameraroll'; import CameraRoll from '@react-native-community/cameraroll';
import { StatusBarBlurBackground } from './views/StatusBarBlurBackground'; import { StatusBarBlurBackground } from './views/StatusBarBlurBackground';
import type { NativeSyntheticEvent } from 'react-native';
import type { ImageLoadEventData } from 'react-native';
interface MediaProps { interface MediaProps {
path: string; path: string;
@ -29,6 +31,9 @@ const requestSavePermission = async (): Promise<boolean> => {
return hasPermission; return hasPermission;
}; };
const isVideoOnLoadEvent = (event: OnLoadData | NativeSyntheticEvent<ImageLoadEventData>): event is OnLoadData =>
'duration' in event && 'naturalSize' in event;
export const Media: NavigationFunctionComponent<MediaProps> = ({ componentId, type, path }) => { export const Media: NavigationFunctionComponent<MediaProps> = ({ componentId, type, path }) => {
const [hasMediaLoaded, setHasMediaLoaded] = useState(false); const [hasMediaLoaded, setHasMediaLoaded] = useState(false);
const isForeground = useIsForeground(); const isForeground = useIsForeground();
@ -40,6 +45,15 @@ export const Media: NavigationFunctionComponent<MediaProps> = ({ componentId, ty
Navigation.dismissModal(componentId); Navigation.dismissModal(componentId);
}, [componentId]); }, [componentId]);
const onMediaLoad = useCallback((event: OnLoadData | NativeSyntheticEvent<ImageLoadEventData>) => {
if (isVideoOnLoadEvent(event)) {
console.log(
`Video loaded. Size: ${event.naturalSize.width}x${event.naturalSize.height} (${event.naturalSize.orientation}, ${event.duration} seconds)`,
);
} else {
console.log(`Image loaded. Size: ${event.nativeEvent.source.width}x${event.nativeEvent.source.height}`);
}
}, []);
const onMediaLoadEnd = useCallback(() => { const onMediaLoadEnd = useCallback(() => {
console.log('media has loaded.'); console.log('media has loaded.');
setHasMediaLoaded(true); setHasMediaLoaded(true);
@ -70,7 +84,9 @@ export const Media: NavigationFunctionComponent<MediaProps> = ({ componentId, ty
return ( return (
<View style={[styles.container, screenStyle]}> <View style={[styles.container, screenStyle]}>
{type === 'photo' && <Image source={source} style={StyleSheet.absoluteFill} resizeMode="cover" onLoadEnd={onMediaLoadEnd} />} {type === 'photo' && (
<Image source={source} style={StyleSheet.absoluteFill} resizeMode="cover" onLoadEnd={onMediaLoadEnd} onLoad={onMediaLoad} />
)}
{type === 'video' && ( {type === 'video' && (
<Video <Video
source={source} source={source}
@ -87,6 +103,7 @@ export const Media: NavigationFunctionComponent<MediaProps> = ({ componentId, ty
playWhenInactive={true} playWhenInactive={true}
ignoreSilentSwitch="ignore" ignoreSilentSwitch="ignore"
onReadyForDisplay={onMediaLoadEnd} onReadyForDisplay={onMediaLoadEnd}
onLoad={onMediaLoad}
/> />
)} )}

View File

@ -34,7 +34,9 @@
"release": "release-it", "release": "release-it",
"example": "yarn --cwd example", "example": "yarn --cwd example",
"pods": "cd example && pod-install --quiet", "pods": "cd example && pod-install --quiet",
"bootstrap": "yarn example && yarn && yarn pods" "bootstrap": "yarn example && yarn && yarn pods",
"ktlint-fix": "ktlint -F android/**/*.kt*",
"swiftlint-fix": "cd ios && swiftlint autocorrect"
}, },
"keywords": [ "keywords": [
"react-native", "react-native",