mrousavy / react-native-vision-camera

πŸ“Έ A powerful, high-performance React Native Camera library.
https://react-native-vision-camera.com
MIT License
7.51k stars 1.09k forks source link

πŸ› Unable to set minimum zoom on first render #3027

Open RenaudAubert opened 4 months ago

RenaudAubert commented 4 months ago

What's happening?

Hi! I'm trying to have the back camera to have the minimum zoom from the start. Setting zoom={device.minZoom} does not seem to work as the preview is still the neutral one. Disabling the preview also produces incorrect photos.

I've noticed that enabling the preview prop after the Camera has mounted display the preview with the correct zoom. Another weird behavior is if enableZoomGesture is enabled and the zoom is changed by pinching. When disabling then reenabling the preview then the bug is there as the view is once again in neutral mode.

❗ While the preview is displaying a neutral preview when it should not. The zoom level with pinch is correct. Which means I have a neutral zoom preview but can only pinch to zoom in.

I've managed to reproduce this behavior in the example app. I've added zoom={device.minZoom} to the camera component but when opening the app I have the neutral zoom.

I've also managed to reproduce this behavior with both v4.3.2 /react-native v0.74.2 and v4.3.1/react-native v0.73.8

Please let me know if the logs from logcat are incomplete. There are lots of logs and I tried to only paste what might be relevant which is the logs from the camera initialization.

Cheers

Reproduceable Code

function App(): React.JSX.Element {
  const { hasPermission, requestPermission } = useCameraPermission();
  const camera = useRef<Camera>(null);
  const device = useCameraDevice('back');
  const [isCameraInitialized, setIsCameraInitialized] = useState(false);
  const [preview, setPreview] = useState(false);

  const onError = useCallback<NonNullable<CameraProps['onError']>>(
    async error => {
      switch (error.code) {
        default:
          console.log(
            'An error occurred while mounting the camera or during runtime: ',
            error,
          );
      }
    },
    [],
  );

  useEffect(() => {
    const requestCameraPermissions = async () => {
      await requestPermission ();
    };

    if (!hasPermission) {
      requestCameraPermissions ();
    }
  }, [hasPermission, requestPermission ]);

  const onInitialized = useCallback(() => {
    console.log('initialized');
    setIsCameraInitialized(true);
  }, []);

  return (
    <SafeAreaView style={{ backgroundColor: 'black', flex: 1 }}>
      {device != null && (
        <Camera
          // While this prop is commented the preview is incorrect
          // preview={preview}
          device={device}
          isActive
          ref={camera}
          style={StyleSheet.absoluteFill}
          photo
          video={false}
          audio={false}
          onError={onError}
          onInitialized={onInitialized}
          zoom={device.minZoom}
         // If playing with pinch while preview prop is set, switching off/on will reproduce the preview bug
          enableZoomGesture
        />
      )}

      <TouchableOpacity
        activeOpacity={0.6}
        style={{
            alignSelf: 'center',
            borderColor: 'white',
            position: 'absolute',
            borderRadius: 100/ 2,
            borderWidth: 100* 0.1,
            bottom: 100* 0.2,
            height: 100,
            opacity: isCameraInitialized ? 1 : 0.25,
            width: 100,
          }}
        disabled={!isCameraInitialized}
        onPress={() => setPreview(!preview)}>
        <View />
      </TouchableOpacity>
    </SafeAreaView>
  );
}

Relevant log output

Camera Lifecycle changed to CREATED!
Updating CameraSession...
Updating CameraSession...
Updating CameraSession...
configure { ... }: Waiting for lock...
A new configure { ... } call arrived, aborting this one...
'test: ', false
configure { ... }: Waiting for lock...
A new configure { ... } call arrived, aborting this one...
Compat change id reported: 289878283; UID 10327; state: ENABLED
CameraView attached to window!
'test: ', false
configure { ... }: Waiting for lock...
configure { ... }: Updating CameraSession Configuration... Difference(deviceChanged=true, outputsChanged=true, sidePropsChanged=true, isActiveChanged=true, orientationChanged=true, locationChanged=true)
Creating new Outputs for Camera #0...
Using FPS Range: null
Creating Preview output...
Creating Photo output...
Successfully created new Outputs for Camera #0!
Binding Camera #0...
Binding 2 use-cases...
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
Resolved dynamic range for use case androidx.camera.core.Preview-7da098c9-978d-4e8e-8e6f-35b879d8a616 to no compatible HDR dynamic ranges.
DynamicRange@b314194{encoding=UNSPECIFIED, bitDepth=0}
->
DynamicRange@a0d61e7{encoding=SDR, bitDepth=8}
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
Surface created[total_surfaces=1, used_surfaces=0](androidx.camera.core.processing.SurfaceEdge$SettableSurface@cecd739}
Surface created[total_surfaces=2, used_surfaces=0](androidx.camera.core.SurfaceRequest$2@a3aadf5}
New surface in use[total_surfaces=2, used_surfaces=1](androidx.camera.core.SurfaceRequest$2@a3aadf5}
use count+1, useCount=1 androidx.camera.core.SurfaceRequest$2@a3aadf5
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
createPipeline(cameraId: 0, streamSpec: StreamSpec{resolution=4080x3072, dynamicRange=DynamicRange@a0d61e7{encoding=SDR, bitDepth=8}, expectedFrameRateRange=[0, 0], implementationOptions=androidx.camera.camera2.impl.Camera2ImplConfig@ca7f256})
Compat change id reported: 236825255; UID 10327; state: ENABLED
Surface created[total_surfaces=3, used_surfaces=1](androidx.camera.core.impl.ImmediateSurface@3dc92d7}
invokeOnInitialized()
{Camera@6807e0[id=0]} Use case androidx.camera.core.ImageCapture-2b0b48bd-eca3-48a4-9a5f-96db1a194f4b95756008 ACTIVE
Successfully bound Camera #0!
Active and attached use case: [] for camera: 0
Camera Lifecycle changed to STARTED!
PreviewView Stream State changed to IDLE
{Camera@6807e0[id=0]} Use case androidx.camera.core.Preview-7da098c9-978d-4e8e-8e6f-35b879d8a61699864587 ACTIVE
Active and attached use case: [] for camera: 0
Camera State: CLOSED (has error: false)
Camera Lifecycle changed to RESUMED!
Target Orientation changed DEVICE -> DEVICE!
Starting streaming device and screen orientation updates...
{Camera@6807e0[id=0]} Use case androidx.camera.core.ImageCapture-2b0b48bd-eca3-48a4-9a5f-96db1a194f4b95756008 ACTIVE
Active and attached use case: [] for camera: 0
Active and attached use case: [] for camera: 0
{Camera@6807e0[id=0]} Use cases [androidx.camera.core.Preview-7da098c9-978d-4e8e-8e6f-35b879d8a61699864587, androidx.camera.core.ImageCapture-2b0b48bd-eca3-48a4-9a5f-96db1a194f4b95756008] now ATTACHED
Failed to configure CameraSession! Error: The Location permission was denied! If you want to capture photos or videos without location tags, pass `enableLocation={false}`., Config-Diff: Difference(deviceChanged=true, outputsChanged=true, sidePropsChanged=true, isActiveChanged=true, orientationChanged=true, locationChanged=true)
com.mrousavy.camera.core.LocationPermissionError: The Location permission was denied! If you want to capture photos or videos without location tags, pass `enableLocation={false}`.
    at com.mrousavy.camera.core.MetadataProvider.enableLocationUpdates(MetadataProvider.kt:35)
    at com.mrousavy.camera.core.CameraSession.configure(CameraSession.kt:157)
    at com.mrousavy.camera.react.CameraView$update$1.invokeSuspend(CameraView.kt:153)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
    at android.os.Handler.handleCallback(Handler.java:959)
    at android.os.Handler.dispatchMessage(Handler.java:100)
    at android.os.Looper.loopOnce(Looper.java:232)
    at android.os.Looper.loop(Looper.java:317)
    at android.app.ActivityThread.main(ActivityThread.java:8501)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:878)
invokeOnError(...):
com.mrousavy.camera.core.LocationPermissionError: The Location permission was denied! If you want to capture photos or videos without location tags, pass `enableLocation={false}`.
    at com.mrousavy.camera.core.MetadataProvider.enableLocationUpdates(MetadataProvider.kt:35)
    at com.mrousavy.camera.core.CameraSession.configure(CameraSession.kt:157)
    at com.mrousavy.camera.react.CameraView$update$1.invokeSuspend(CameraView.kt:153)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
    at android.os.Handler.handleCallback(Handler.java:959)
    at android.os.Handler.dispatchMessage(Handler.java:100)
    at android.os.Looper.loopOnce(Looper.java:232)
All use case: [androidx.camera.core.ImageCapture-2b0b48bd-eca3-48a4-9a5f-96db1a194f4b95756008, androidx.camera.core.Preview-7da098c9-978d-4e8e-8e6f-35b879d8a61699864587] for camera: 0
    at android.os.Looper.loop(Looper.java:317)
    at android.app.ActivityThread.main(ActivityThread.java:8501)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:878)
mMeteringRepeating is ATTACHED, SessionConfig Surfaces: 2, CaptureConfig Surfaces: 1
Active and attached use case: [androidx.camera.core.ImageCapture-2b0b48bd-eca3-48a4-9a5f-96db1a194f4b95756008, androidx.camera.core.Preview-7da098c9-978d-4e8e-8e6f-35b879d8a61699864587] for camera: 0
{Camera@6807e0[id=0]} Resetting Capture Session
{Camera@6807e0[id=0]} Releasing session in state INITIALIZED
{Camera@6807e0[id=0]} Attempting to force open the camera.
tryOpenCamera(Camera@6807e0[id=0]) [Available Cameras: 1, Already Open: false (Previous state: null)] --> SUCCESS
Recalculating open cameras:
Camera                                       State                 
-------------------------------------------------------------------
Camera@6807e0[id=0]                          OPENING               
Camera@ca32a5b[id=1]                         UNKNOWN               
-------------------------------------------------------------------
Open count: 1 (Max allowed: 1)
{Camera@6807e0[id=0]} Opening camera.
{Camera@6807e0[id=0]} Transitioning camera internal state: INITIALIZED --> OPENING
Initializing without READ_DEVICE_CONFIG permission. enabled=false, interval=1, missedFrameThreshold=3, frameTimeThreshold=64, package=com.awesomeproject
New public camera state CameraState{type=OPENING, error=null} from OPENING and null
Publishing new public camera state CameraState{type=OPENING, error=null}
All use case: [androidx.camera.core.ImageCapture-2b0b48bd-eca3-48a4-9a5f-96db1a194f4b95756008, androidx.camera.core.Preview-7da098c9-978d-4e8e-8e6f-35b879d8a61699864587] for camera: 0
Compat change id reported: 78294732; UID 10327; state: ENABLED
initialized
Surface requested by Preview.
'test: ', false
Camera State: OPENING (has error: false)
tagSocket(122) with statsTag=0xffffffff, statsUid=-1
Camera #0 is now unavailable.
{Camera@6807e0[id=0]} Use case androidx.camera.core.Preview-7da098c9-978d-4e8e-8e6f-35b879d8a61699864587 ACTIVE
Active and attached use case: [androidx.camera.core.ImageCapture-2b0b48bd-eca3-48a4-9a5f-96db1a194f4b95756008, androidx.camera.core.Preview-7da098c9-978d-4e8e-8e6f-35b879d8a61699864587] for camera: 0
Surface created.
{Camera@6807e0[id=0]} Use case androidx.camera.core.ImageCapture-2b0b48bd-eca3-48a4-9a5f-96db1a194f4b95756008 ACTIVE
Surface changed. Size: 1600x1200
Active and attached use case: [androidx.camera.core.ImageCapture-2b0b48bd-eca3-48a4-9a5f-96db1a194f4b95756008, androidx.camera.core.Preview-7da098c9-978d-4e8e-8e6f-35b879d8a61699864587] for camera: 0
'An error occurred while mounting the camera or during runtime: ', { [permission/location-permission-denied: The Location permission was denied! If you want to capture photos or videos without location tags, pass `enableLocation={false}`.]
  name: 'permission/location-permission-denied',
  _code: 'permission/location-permission-denied',
  _message: 'The Location permission was denied! If you want to capture photos or videos without location tags, pass `enableLocation={false}`.',
  _cause: undefined }
{Camera@6807e0[id=0]} CameraDevice.onOpened()
{Camera@6807e0[id=0]} Transitioning camera internal state: OPENING --> OPENED
'test: ', false
Recalculating open cameras:
Camera                                       State                 
-------------------------------------------------------------------
Camera@6807e0[id=0]                          OPEN                  
Camera@ca32a5b[id=1]                         UNKNOWN               
-------------------------------------------------------------------
Open count: 1 (Max allowed: 1)
New public camera state CameraState{type=OPEN, error=null} from OPEN and null
Publishing new public camera state CameraState{type=OPEN, error=null}
Preview orientation changed! PORTRAIT
invokeOnPreviewOrientationChanged(PORTRAIT)
All use case: [androidx.camera.core.ImageCapture-2b0b48bd-eca3-48a4-9a5f-96db1a194f4b95756008, androidx.camera.core.Preview-7da098c9-978d-4e8e-8e6f-35b879d8a61699864587] for camera: 0
Output orientation changed! PORTRAIT
invokeOnOutputOrientationChanged(PORTRAIT)
Preview transformation info updated. TransformationInfo{getCropRect=Rect(0, 0 - 1600, 1200), getRotationDegrees=90, getTargetRotation=0, hasCameraTransform=true, getSensorToBufferTransform=Matrix{[1.0, 0.0, 0.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0]}, isMirroring=false}
Transformation info set: TransformationInfo{getCropRect=Rect(0, 0 - 1600, 1200), getRotationDegrees=90, getTargetRotation=0, hasCameraTransform=true, getSensorToBufferTransform=Matrix{[1.0, 0.0, 0.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0]}, isMirroring=false} 1600x1200 false
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
Surface set on Preview.
Active and attached use case: [androidx.camera.core.ImageCapture-2b0b48bd-eca3-48a4-9a5f-96db1a194f4b95756008, androidx.camera.core.Preview-7da098c9-978d-4e8e-8e6f-35b879d8a61699864587] for camera: 0
[androidx.camera.camera2.internal.SynchronizedCaptureSessionImpl@897f3a8] getSurface done with results: [Surface(name=null)/@0x5d317bc, Surface(name=null)/@0x17dffcb]
Opening capture session.
[androidx.camera.camera2.internal.SynchronizedCaptureSessionImpl@897f3a8] start openCaptureSession
Camera State: OPEN (has error: false)
invokeOnStarted()
use count+1, useCount=2 androidx.camera.core.SurfaceRequest$2@a3aadf5
New surface in use[total_surfaces=3, used_surfaces=2](androidx.camera.core.impl.ImmediateSurface@3dc92d7}
use count+1, useCount=1 androidx.camera.core.impl.ImmediateSurface@3dc92d7
tagSocket(122) with statsTag=0xffffffff, statsUid=-1
[androidx.camera.camera2.internal.SynchronizedCaptureSessionImpl@897f3a8] Session onConfigured()
Attempting to send capture request onConfigured
Issuing request for session.
createCaptureRequest
CameraCaptureSession.onConfigured() mState=OPENED
CameraCaptureSession.onReady() OPENED
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
Update Preview stream state to STREAMING
PreviewView Stream State changed to STREAMING
invokeOnPreviewStarted()
invokeOnAverageFpsChanged(0.0)
tagSocket(184) with statsTag=0x90000, statsUid=-1
invokeOnAverageFpsChanged(0.0)
visibilityChanged oldVisibility=true newVisibility=false
Memory warning (pressure level: TRIM_MEMORY_UI_HIDDEN) received by JS VM, ignoring because it's non-severe
Surface destroyed.
Surface closed androidx.camera.core.SurfaceRequest@f5b2f2c
surface closed,  useCount=2 closed=true androidx.camera.core.SurfaceRequest$2@a3aadf5
[SurfaceView[com.awesomeproject/com.awesomeproject.MainActivity]#2(BLAST Consumer)2](id:4f3100000003,api:4,p:1477,c:20273) dequeueBuffer: BufferQueue has been abandoned
[SurfaceView[com.awesomeproject/com.awesomeproject.MainActivity]#2(BLAST Consumer)2](id:4f3100000003,api:4,p:1477,c:20273) queueBuffer: BufferQueue has been abandoned
applyTransactionOnDraw applyImmediately
Not drawing due to not visible
surface closed,  useCount=0 closed=true androidx.camera.core.processing.SurfaceEdge$SettableSurface@cecd739
Surface terminated[total_surfaces=2, used_surfaces=2](androidx.camera.core.processing.SurfaceEdge$SettableSurface@cecd739}
use count-1,  useCount=1 closed=true androidx.camera.core.SurfaceRequest$2@a3aadf5
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
CameraCaptureSession.onReady() OPENED
invokeOnAverageFpsChanged(0.0)
tagSocket(116) with statsTag=0x90000, statsUid=-1
Background concurrent mark compact GC freed 120651(21MB) AllocSpace objects, 8(160KB) LOS objects, 80% free, 5955KB/29MB, paused 575us,3.636ms total 150.107ms
ApkAssets: Deleting an ApkAssets object '<empty> and /data/app/~~ZozlL6S--jnUEtc-KckoCw==/com.google.android.webview-Uv_Jg3OzRquOgVfIt-qMmw==/base.apk' with 1 weak references
ApkAssets: Deleting an ApkAssets object '<empty> and /data/app/~~ZozlL6S--jnUEtc-KckoCw==/com.google.android.webview-Uv_Jg3OzRquOgVfIt-qMmw==/split_config.fr.apk' with 1 weak references
ApkAssets: Deleting an ApkAssets object '<empty> and /data/app/~~tXTDeKKATdZgqlVzQT7low==/com.google.android.trichromelibrary_647807133-CVQ_-Z4Lk2M7V2vZ0Gplfw==/base.apk' with 1 weak references
invokeOnAverageFpsChanged(0.0)

Camera Device

{
  "formats": [],
  "sensorOrientation": "landscape-left",
  "hardwareLevel": "full",
  "maxZoom": 20,
  "minZoom": 0.6704425811767578,
  "maxExposure": 24,
  "supportsLowLightBoost": true,
  "neutralZoom": 1,
  "physicalDevices": [
    "wide-angle-camera",
    "wide-angle-camera",
    "ultra-wide-angle-camera",
    "telephoto-camera"
  ],
  "supportsFocus": true,
  "supportsRawCapture": false,
  "isMultiCam": true,
  "minFocusDistance": 10.500000100135805,
  "minExposure": -24,
  "name": "0 (BACK) androidx.camera.camera2",
  "hasFlash": true,
  "hasTorch": true,
  "position": "back",
  "id": "0"
}

Device

Pixel 6 Pro

VisionCamera Version

4.3.2

Can you reproduce this issue in the VisionCamera Example app?

Yes, I can reproduce the same issue in the Example app here

Additional information

maintenance-hans[bot] commented 4 months ago

Guten Tag, Hans here.

[!NOTE] New features, bugfixes, updates and other improvements are all handled mostly by @mrousavy in his free time. To support @mrousavy, please consider πŸ’– sponsoring him on GitHub πŸ’–. Sponsored issues will be prioritized.

mrousavy commented 3 months ago

Hey - I created a PR to potentially fix this: https://github.com/mrousavy/react-native-vision-camera/pull/3066

Can you test if this fixes the issue for you?

mrousavy commented 3 months ago

Thanks for the logs btw. A lot of people simply don't get this, but I just read the logs and I can understand what went wrong.

RenaudAubert commented 3 months ago

Hi @mrousavy, Thanks a lot for taking the time to look into this issue. I apologize for not getting back to you sooner but I just tested with version 4.4.2 and the problem is still occurring.

Here's the log from the camera initialization (the "test" log is a console.log I added ensure I have the minimum zoom set) The code for the Camera is the same as in my original post

profiles = ImmutableEncoderProfilesProxy{defaultDurationSeconds=60, recommendedFileFormat=2, audioProfiles=[AudioProfileProxy{codec=3, mediaType=audio/mp4a-latm, bitrate=96000, sampleRate=48000, channels=1, profile=1}], videoProfiles=[VideoProfileProxy{codec=2, mediaType=video/avc, bitrate=72000000, frameRate=60, width=3840, height=2160, profile=-1, bitDepth=8, chromaSubsampling=0, hdrFormat=0}]}
profiles = ImmutableEncoderProfilesProxy{defaultDurationSeconds=60, recommendedFileFormat=2, audioProfiles=[AudioProfileProxy{codec=3, mediaType=audio/mp4a-latm, bitrate=96000, sampleRate=48000, channels=1, profile=1}], videoProfiles=[VideoProfileProxy{codec=2, mediaType=video/avc, bitrate=33000000, frameRate=60, width=1920, height=1080, profile=-1, bitDepth=8, chromaSubsampling=0, hdrFormat=0}]}
profiles = ImmutableEncoderProfilesProxy{defaultDurationSeconds=60, recommendedFileFormat=2, audioProfiles=[AudioProfileProxy{codec=3, mediaType=audio/mp4a-latm, bitrate=96000, sampleRate=48000, channels=1, profile=1}], videoProfiles=[VideoProfileProxy{codec=2, mediaType=video/avc, bitrate=12000000, frameRate=30, width=1280, height=720, profile=-1, bitDepth=8, chromaSubsampling=0, hdrFormat=0}]}
profiles = ImmutableEncoderProfilesProxy{defaultDurationSeconds=60, recommendedFileFormat=2, audioProfiles=[AudioProfileProxy{codec=3, mediaType=audio/mp4a-latm, bitrate=96000, sampleRate=48000, channels=1, profile=1}], videoProfiles=[VideoProfileProxy{codec=2, mediaType=video/avc, bitrate=6000000, frameRate=30, width=720, height=480, profile=-1, bitDepth=8, chromaSubsampling=0, hdrFormat=0}]}
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
Advanced Extensions Implemented 
Advanced Extensions Implemented 
profiles = ImmutableEncoderProfilesProxy{defaultDurationSeconds=60, recommendedFileFormat=2, audioProfiles=[AudioProfileProxy{codec=3, mediaType=audio/mp4a-latm, bitrate=96000, sampleRate=48000, channels=1, profile=1}], videoProfiles=[VideoProfileProxy{codec=2, mediaType=video/avc, bitrate=48000000, frameRate=30, width=3840, height=2160, profile=-1, bitDepth=8, chromaSubsampling=0, hdrFormat=0}]}
profiles = ImmutableEncoderProfilesProxy{defaultDurationSeconds=60, recommendedFileFormat=2, audioProfiles=[AudioProfileProxy{codec=3, mediaType=audio/mp4a-latm, bitrate=96000, sampleRate=48000, channels=1, profile=1}], videoProfiles=[VideoProfileProxy{codec=2, mediaType=video/avc, bitrate=33000000, frameRate=60, width=1920, height=1080, profile=-1, bitDepth=8, chromaSubsampling=0, hdrFormat=0}]}
profiles = ImmutableEncoderProfilesProxy{defaultDurationSeconds=60, recommendedFileFormat=2, audioProfiles=[AudioProfileProxy{codec=3, mediaType=audio/mp4a-latm, bitrate=96000, sampleRate=48000, channels=1, profile=1}], videoProfiles=[VideoProfileProxy{codec=2, mediaType=video/avc, bitrate=12000000, frameRate=30, width=1280, height=720, profile=-1, bitDepth=8, chromaSubsampling=0, hdrFormat=0}]}
profiles = ImmutableEncoderProfilesProxy{defaultDurationSeconds=60, recommendedFileFormat=2, audioProfiles=[AudioProfileProxy{codec=3, mediaType=audio/mp4a-latm, bitrate=96000, sampleRate=48000, channels=1, profile=1}], videoProfiles=[VideoProfileProxy{codec=2, mediaType=video/avc, bitrate=6000000, frameRate=30, width=720, height=480, profile=-1, bitDepth=8, chromaSubsampling=0, hdrFormat=0}]}
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=270, isOppositeFacing=false, result=270
Advanced Extensions Implemented 
Advanced Extensions Implemented 
'test: ', 0.5304937958717346
Cannot get virtualdevice_native service
Camera Lifecycle changed to CREATED!
Updating CameraSession...
Updating CameraSession...
Updating CameraSession...
configure { ... }: Waiting for lock...
A new configure { ... } call arrived, aborting this one...
CameraView attached to window!
Initializing without READ_DEVICE_CONFIG permission. enabled=false, interval=1, missedFrameThreshold=3, frameTimeThreshold=64, package=com.cropscan
configure { ... }: Waiting for lock...
A new configure { ... } call arrived, aborting this one...
configure { ... }: Waiting for lock...
configure { ... }: Updating CameraSession Configuration... Difference(deviceChanged=true, outputsChanged=true, sidePropsChanged=true, isActiveChanged=true, orientationChanged=true, locationChanged=true)
Creating new Outputs for Camera #0...
Using FPS Range: null
Creating Preview output...
Creating Photo output...
Photo size: 4624x3472
Successfully created new Outputs for Camera #0!
Binding Camera #0...
Binding 2 use-cases...
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
Resolved dynamic range for use case androidx.camera.core.Preview-19eb6392-4f43-4325-b6b6-01e99ca43955 to no compatible HDR dynamic ranges.
DynamicRange@93012fe{encoding=UNSPECIFIED, bitDepth=0}
->
DynamicRange@b6c4eb9{encoding=SDR, bitDepth=8}
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
Surface created[total_surfaces=1, used_surfaces=0](androidx.camera.core.processing.SurfaceEdge$SettableSurface@f348d7b}
Surface created[total_surfaces=2, used_surfaces=0](androidx.camera.core.SurfaceRequest$2@5f48657}
New surface in use[total_surfaces=2, used_surfaces=1](androidx.camera.core.SurfaceRequest$2@5f48657}
use count+1, useCount=1 androidx.camera.core.SurfaceRequest$2@5f48657
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
createPipeline(cameraId: 0, streamSpec: StreamSpec{resolution=4624x3472, dynamicRange=DynamicRange@b6c4eb9{encoding=SDR, bitDepth=8}, expectedFrameRateRange=[0, 0], implementationOptions=androidx.camera.camera2.impl.Camera2ImplConfig@86e11b0})
Compat change id reported: 236825255; UID 10318; state: ENABLED
Surface created[total_surfaces=3, used_surfaces=1](androidx.camera.core.impl.ImmediateSurface@e615829}
invokeOnInitialized()
Successfully bound Camera #0!
Camera Lifecycle changed to STARTED!
PreviewView Stream State changed to IDLE
{Camera@4d61596[id=0]} Use case androidx.camera.core.ImageCapture-1880673d-90b0-4500-a767-04cce8c13ff3104543165 ACTIVE
Active and attached use case: [] for camera: 0
Camera State: CLOSED (has error: false)
Camera Lifecycle changed to RESUMED!
Target Orientation changed DEVICE -> DEVICE!
Starting streaming device and screen orientation updates...
{Camera@4d61596[id=0]} Use case androidx.camera.core.Preview-19eb6392-4f43-4325-b6b6-01e99ca4395565441556 ACTIVE
Active and attached use case: [] for camera: 0
Start updating location...
{Camera@4d61596[id=0]} Use case androidx.camera.core.ImageCapture-1880673d-90b0-4500-a767-04cce8c13ff3104543165 ACTIVE
Active and attached use case: [] for camera: 0
Active and attached use case: [] for camera: 0
configure { ... }: Completed CameraSession Configuration! (State: RESUMED)
{Camera@4d61596[id=0]} Use cases [androidx.camera.core.Preview-19eb6392-4f43-4325-b6b6-01e99ca4395565441556, androidx.camera.core.ImageCapture-1880673d-90b0-4500-a767-04cce8c13ff3104543165] now ATTACHED
All use case: [androidx.camera.core.ImageCapture-1880673d-90b0-4500-a767-04cce8c13ff3104543165, androidx.camera.core.Preview-19eb6392-4f43-4325-b6b6-01e99ca4395565441556] for camera: 0
No need to remove a previous mMeteringRepeating, SessionConfig Surfaces: 2, CaptureConfig Surfaces: 1
Active and attached use case: [androidx.camera.core.ImageCapture-1880673d-90b0-4500-a767-04cce8c13ff3104543165, androidx.camera.core.Preview-19eb6392-4f43-4325-b6b6-01e99ca4395565441556] for camera: 0
Surface requested by Preview.
Location Provider gps has been disabled.
{Camera@4d61596[id=0]} Resetting Capture Session
{Camera@4d61596[id=0]} Skipping Capture Session state check due to current camera state: INITIALIZED and previous session status: false
{Camera@4d61596[id=0]} Releasing session in state INITIALIZED
{Camera@4d61596[id=0]} Attempting to force open the camera.
tryOpenCamera(Camera@4d61596[id=0]) [Available Cameras: 1, Already Open: false (Previous state: null)] --> SUCCESS
Surface created.
Surface changed. Size: 1920x1080
Recalculating open cameras:
Camera                                       State                 
-------------------------------------------------------------------
Camera@4d61596[id=0]                         OPENING               
Camera@9e970e9[id=1]                         UNKNOWN               
-------------------------------------------------------------------
Open count: 1 (Max allowed: 1)
{Camera@4d61596[id=0]} Opening camera.
{Camera@4d61596[id=0]} Transitioning camera internal state: INITIALIZED --> OPENING
New public camera state CameraState{type=OPENING, error=null} from OPENING and null
Publishing new public camera state CameraState{type=OPENING, error=null}
All use case: [androidx.camera.core.ImageCapture-1880673d-90b0-4500-a767-04cce8c13ff3104543165, androidx.camera.core.Preview-19eb6392-4f43-4325-b6b6-01e99ca4395565441556] for camera: 0
Preview transformation info updated. TransformationInfo{getCropRect=Rect(0, 0 - 1920, 1080), getRotationDegrees=90, getTargetRotation=0, hasCameraTransform=true, getSensorToBufferTransform=Matrix{[1.0, 0.0, 0.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0]}, isMirroring=false}
Transformation info set: TransformationInfo{getCropRect=Rect(0, 0 - 1920, 1080), getRotationDegrees=90, getTargetRotation=0, hasCameraTransform=true, getSensorToBufferTransform=Matrix{[1.0, 0.0, 0.0][0.0, 1.0, 0.0][0.0, 0.0, 1.0]}, isMirroring=false} 1920x1080 false
getRelativeImageRotation: destRotationDegrees=0, sourceRotationDegrees=90, isOppositeFacing=true, result=90
Surface set on Preview.
Camera State: OPENING (has error: false)
'test: ', 0.5304937958717346
Preview orientation changed! PORTRAIT
invokeOnPreviewOrientationChanged(PORTRAIT)
Output orientation changed! PORTRAIT
invokeOnOutputOrientationChanged(PORTRAIT)
Camera #0 is now unavailable.
{Camera@4d61596[id=0]} Use case androidx.camera.core.Preview-19eb6392-4f43-4325-b6b6-01e99ca4395565441556 ACTIVE
Active and attached use case: [androidx.camera.core.ImageCapture-1880673d-90b0-4500-a767-04cce8c13ff3104543165, androidx.camera.core.Preview-19eb6392-4f43-4325-b6b6-01e99ca4395565441556] for camera: 0
{Camera@4d61596[id=0]} Use case androidx.camera.core.ImageCapture-1880673d-90b0-4500-a767-04cce8c13ff3104543165 ACTIVE
Active and attached use case: [androidx.camera.core.ImageCapture-1880673d-90b0-4500-a767-04cce8c13ff3104543165, androidx.camera.core.Preview-19eb6392-4f43-4325-b6b6-01e99ca4395565441556] for camera: 0
{Camera@4d61596[id=0]} CameraDevice.onOpened()
{Camera@4d61596[id=0]} Transitioning camera internal state: OPENING --> OPENED
Recalculating open cameras:
Camera                                       State                 
-------------------------------------------------------------------
Camera@4d61596[id=0]                         OPEN                  
Camera@9e970e9[id=1]                         UNKNOWN               
-------------------------------------------------------------------
Open count: 1 (Max allowed: 1)
New public camera state CameraState{type=OPEN, error=null} from OPEN and null
Publishing new public camera state CameraState{type=OPEN, error=null}
Camera State: OPEN (has error: false)
invokeOnStarted()
All use case: [androidx.camera.core.ImageCapture-1880673d-90b0-4500-a767-04cce8c13ff3104543165, androidx.camera.core.Preview-19eb6392-4f43-4325-b6b6-01e99ca4395565441556] for camera: 0
Active and attached use case: [androidx.camera.core.ImageCapture-1880673d-90b0-4500-a767-04cce8c13ff3104543165, androidx.camera.core.Preview-19eb6392-4f43-4325-b6b6-01e99ca4395565441556] for camera: 0
[androidx.camera.camera2.internal.SynchronizedCaptureSessionImpl@d966127] getSurface done with results: [Surface(name=null)/@0xdce655b, Surface(name=null)/@0xb29c7e6]
Opening capture session.
[androidx.camera.camera2.internal.SynchronizedCaptureSessionImpl@d966127] start openCaptureSession
use count+1, useCount=2 androidx.camera.core.SurfaceRequest$2@5f48657
New surface in use[total_surfaces=3, used_surfaces=2](androidx.camera.core.impl.ImmediateSurface@e615829}
use count+1, useCount=1 androidx.camera.core.impl.ImmediateSurface@e615829
[androidx.camera.camera2.internal.SynchronizedCaptureSessionImpl@d966127] Session onConfigured()
Attempting to send capture request onConfigured
Issuing request for session.
createCaptureRequest
CameraCaptureSession.onConfigured() mState=OPENED
CameraCaptureSession.onReady() OPENED
Update Preview stream state to STREAMING
PreviewView Stream State changed to STREAMING
invokeOnPreviewStarted()
view not autofillable - not passing ime action check
view not autofillable - not passing ime action check
tagSocket(214) with statsTag=0xffffffff, statsUid=-1
invokeOnAverageFpsChanged(0.0)
tagSocket(218) with statsTag=0xffffffff, statsUid=-1
mrousavy commented 3 months ago

Hmm, okay then we need to reopen.

AdamWP commented 2 days ago

@RenaudAubert I have this same issue with v4.5.3 on a Pixel 7A. Here's a work-around that zooms out when recording starts. I used minZoom + .01 in a few places as minZoom didn't work (rounding down issue?)

import React, { useState, useRef, useEffect, useCallback } from 'react';
import { View, Text, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar, Alert, Button, useWindowDimensions } from 'react-native';
import { Camera, CameraPermissionStatus, useCameraDevice, getCameraFormat } from 'react-native-vision-camera';
import { Dropdown } from 'react-native-element-dropdown';
import RNFS from 'react-native-fs';
import Reanimated, { useAnimatedProps, useSharedValue } from 'react-native-reanimated';
import { GestureHandlerRootView } from 'react-native-gesture-handler';

Reanimated.addWhitelistedNativeProps({ zoom: true });
const ReanimatedCamera = Reanimated.createAnimatedComponent(Camera);

export default function CameraScreen() {
  const [minZoom, setMinZoom] = useState(1);
  const device = useCameraDevice('back', {
    physicalDevices: ['ultra-wide-angle-camera'],
  });
  const cameraRef = useRef<Camera>(null);

  const zoom = useSharedValue(device?.neutralZoom || 1);
  const zoomOffset = useSharedValue(0);

  const animatedProps = useAnimatedProps(() => ({
    zoom: zoom.value,
  }));

  const [cameraPermissionStatus, setCameraPermissionStatus] = useState<CameraPermissionStatus>('not-determined');
  const [microphonePermissionStatus, setMicrophonePermissionStatus] = useState<CameraPermissionStatus>('not-determined');
  const [isRecording, setIsRecording] = useState(false);
  const [resolution, setResolution] = useState<string>('1920x1080');
  const [fps, setFps] = useState<string>('30');

  const { width, height } = useWindowDimensions();
  const isLandscape = width > height;

  const resolutionOptions = [
    { label: '720p', value: '1280x720' },
    { label: '1080p', value: '1920x1080' },
    { label: '4K', value: '3840x2160' },
  ];

  const fpsOptions = [
    { label: '30 FPS', value: '30' },
    { label: '60 FPS', value: '60' },
  ];

  const requestCameraPermission = useCallback(async () => {
    const status = await Camera.requestCameraPermission();
    setCameraPermissionStatus(status);
  }, []);

  const requestMicrophonePermission = useCallback(async () => {
    const status = await Camera.requestMicrophonePermission();
    setMicrophonePermissionStatus(status);
  }, []);

  useEffect(() => {
    (async () => {
      await requestCameraPermission();
      await requestMicrophonePermission();
      const availableDevices = await Camera.getAvailableCameraDevices();
      const backDevice = availableDevices.find(d => d.position === 'back');
      if (backDevice) {
        setMinZoom(backDevice.minZoom);
        zoom.value = backDevice.minZoom + 0.01;
        console.log('backDevice.minZoom:', backDevice.minZoom);
      }
    })();
  }, []);

  useEffect(() => {
    if (isRecording) {
      zoom.value = (minZoom + 0.01); // Setting zoom to minZoom + small offset to avoid rounding issues
      console.log('Programmatically set zoom to:', zoom.value);
    }
  }, [isRecording, minZoom]);

  const startRecording = async () => {
    if (cameraRef.current) {
      setIsRecording(true);
      zoom.value = 1; // This is required. Setting zoom to minZoom + .01 doesn't work for some reason, but setting it to 1 does. minZoom + 0.1 also works so maybe another rounding issue?
      console.log('start recording zoom is set to:', zoom.value);
      cameraRef.current.startRecording({
        flash: 'off',
        onRecordingFinished: async (video) => {
          setIsRecording(false);
          await saveVideo(video.path);
        },
        onRecordingError: (error) => {
          setIsRecording(false);
          console.error('Recording error:', error);
        },
      });
    }
  };

  const stopRecording = () => {
    if (cameraRef.current && isRecording) {
      cameraRef.current.stopRecording();
      setIsRecording(false);
    }
  };

  const saveVideo = async (videoUri: string) => {
    try {
      const videoFileInfo = await RNFS.stat(videoUri);
      const recordingsDirectory = `${RNFS.DocumentDirectoryPath}/recordings/`;
      await ensureDirectoryExists(recordingsDirectory);
      const videoFileName = `video_${Date.now()}.mp4`;
      const fileUri = `${recordingsDirectory}${videoFileName}`;
      await RNFS.moveFile(videoUri, fileUri);
      Alert.alert('Video saved', `Video saved to: ${fileUri}`);
    } catch (error) {
      console.error('Error saving video:', error);
    }
  };

  const ensureDirectoryExists = async (directory: string) => {
    const dirInfo = await RNFS.stat(directory).catch(() => null);
    if (!dirInfo) {
      await RNFS.mkdir(directory);
    }
  };

  if (!device || cameraPermissionStatus !== 'granted' || microphonePermissionStatus !== 'granted') {
    return (
      <SafeAreaView style={styles.container}>
        <Text style={styles.text}>Requesting permissions...</Text>
        {cameraPermissionStatus !== 'granted' && (
          <Button title="Grant Camera Permission" onPress={requestCameraPermission} />
        )}
        {microphonePermissionStatus !== 'granted' && (
          <Button title="Grant Microphone Permission" onPress={requestMicrophonePermission} />
        )}
      </SafeAreaView>
    );
  }

  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <SafeAreaView style={styles.container}>
        <StatusBar hidden />

        <View style={isLandscape ? styles.landscapeDropdownContainer : styles.dropdownContainer}>
          <Dropdown
            style={[styles.dropdown, { backgroundColor: '#333' }]}
            data={resolutionOptions}
            labelField="label"
            valueField="value"
            placeholder="Select Resolution"
            placeholderStyle={{ color: '#fff' }}
            selectedTextStyle={{ color: '#fff' }}
            value={resolution}
            onChange={(item) => setResolution(item.value)}
          />
          <Dropdown
            style={[styles.dropdown, { backgroundColor: '#333' }]}
            data={fpsOptions}
            labelField="label"
            valueField="value"
            placeholder="Select FPS"
            placeholderStyle={{ color: '#fff' }}
            selectedTextStyle={{ color: '#fff' }}
            value={fps}
            onChange={(item) => setFps(item.value)}
          />
        </View>

          <ReanimatedCamera
            ref={cameraRef}
            style={styles.camera}
            format={getCameraFormat(device, [
              {
                videoResolution: resolution
                  ? { width: parseInt(resolution.split('x')[0]), height: parseInt(resolution.split('x')[1]) }
                  : { width: 1920, height: 1080 },
              },
              { fps: fps ? parseInt(fps) : 30 },
            ])}
            device={device}
            animatedProps={animatedProps}
            isActive={isRecording}
            video={true}
            audio={true}
            onInitialized={() => {
              console.log('Camera is ready.');
            }}
          />

        <View style={isLandscape ? styles.landscapeControls : styles.controls}>
          <TouchableOpacity onPress={isRecording ? stopRecording : startRecording} style={styles.button}>
            <Text style={styles.buttonText}>{isRecording ? 'Stop' : 'Record'}</Text>
          </TouchableOpacity>
        </View>
      </SafeAreaView>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'black',
  },
  cameraContainer: {
    flex: 1,
    position: 'relative',
  },
  camera: {
    flex: 1,
  },
  controls: {
    position: 'absolute',
    bottom: 20,
    left: 0,
    right: 0,
    alignItems: 'center',
  },
  landscapeControls: {
    position: 'absolute',
    bottom: 20,
    left: 50,
    right: 50,
    alignItems: 'center',
  },
  button: {
    backgroundColor: 'red',
    padding: 20,
    borderRadius: 50,
  },
  buttonText: {
    color: 'white',
    fontSize: 20,
    fontWeight: 'bold',
  },
  text: {
    color: 'white',
    fontSize: 16,
    textAlign: 'center',
    margin: 20,
  },
  dropdownContainer: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    paddingHorizontal: 10,
    paddingTop: 20,
  },
  landscapeDropdownContainer: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    paddingHorizontal: 20,
    paddingTop: 10,
    paddingBottom: 10,
  },
  dropdown: {
    flex: 1,
    marginHorizontal: 10,
    borderWidth: 1,
    borderColor: '#fff',
  },
  landscapeSideControls: {
    position: 'absolute',
    top: 50,
    right: 20,
    justifyContent: 'space-between',
    height: '60%',
  },
  sideButton: {
    backgroundColor: 'transparent',
    padding: 10,
  },
  sideButtonText: {
    color: 'white',
    fontSize: 16,
    textAlign: 'center',
  },
});