react-native-vision-camera icon indicating copy to clipboard operation
react-native-vision-camera copied to clipboard

πŸ› Camera Rendering Issue in Portal/View on Android

Open abucher90 opened this issue 11 months ago β€’ 11 comments

What's happening?

Hello @all,

I am experiencing an issue when attempting to render the camera inside a Portal/ View component. The expected behavior and the actual behavior are inconsistent on Android devices. On iOS, it works stable and exactly as expected.

Expected Behavior: The camera should render consistently inside the container, below the Appbar.Header.

Expected View: Image

Actual Behavior: In most cases, the camera does not render as expected, resulting in inconsistent and incorrect positioning.

Actual View: Image

Additional Information: This issue occurs on real devices as well as in the simulator. If I replace the Camera component with a View using the same styling, it works consistently as expected. This leads me to believe the issue might be with the React-Native-Vision-Camera library. Please let me know if you need any additional details or steps to reproduce the issue.

Best regards, Andreas

Reproduceable Code

import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Linking, StyleSheet, View } from 'react-native';
import { Props } from './ScannerContainer';
import { ActivityIndicator, Appbar, Button, Portal, Text } from 'react-native-paper';
import BarcodeMask from 'react-native-barcode-mask';
import { CameraPermissionStatus, Code } from 'react-native-vision-camera';
import { useCameraDevice, useCodeScanner } from 'react-native-vision-camera';
import { Camera } from 'react-native-vision-camera';

export const Scanner: React.FC<Props> = ({
  isScannerOpen,
  navigateToUrl,
  setNavigationStack,
  closeBarCodeScanner,
  baseUrl,
  specialLinks,
}) => {
  const [isLoading, setIsLoading] = useState(false);

  const [cameraPermission, setCameraPermission] = useState<CameraPermissionStatus>('not-determined');
  const device = useCameraDevice('back', {
    physicalDevices: ['ultra-wide-angle-camera', 'wide-angle-camera', 'telephoto-camera'],
  });

  useEffect(() => {
    const requestPermissions = async () => {
      try {
        const permission = await Camera.requestCameraPermission();
        if (permission !== 'granted') {
          console.error('Not allowed to access camera');
        }
        setCameraPermission(permission);
      } catch (err) {
        console.error('Error requesting camera permission:', err);
      }
    };
    const promise = requestPermissions();
    promise.catch((err) => console.error('Unhandled error:', err));
  }, []);

  const onCodeScanned = useCallback((codes: Code[]) => {
    if (!codes?.length) {
      console.error('No codes scanned.');
      return;
    }

    const productNumber = codes[0]?.value;
    if (!productNumber) {
      console.error('Scanned code does not contain a product number.');
      return;
    }

    const fallbackUrl = specialLinks.searchProductsFallback(productNumber!);
    setIsLoading(true);

    fetch(specialLinks.searchProducts, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        Input: {
          q: productNumber,
        },
        SearchType: 'Products',
      }),
    })
      .then((response) => response.json())
      .then((response) => {
        const urlActionProductDetail = response?.products?.[0]?.urlActionProductDetail;
        if (urlActionProductDetail) {
          setNavigationStack([fallbackUrl, specialLinks.product(urlActionProductDetail)]);
        } else {
          navigateToUrl({ url: fallbackUrl, baseUrl });
        }
      })
      .catch((error) => {
        console.error('Failed to search product with scanned number', error);
        navigateToUrl({ url: fallbackUrl, baseUrl });
      })
      .finally(() => setIsLoading(false));
  }, []);

  const codeScanner = useCodeScanner({
    codeTypes: ['qr', 'ean-13', 'ean-8', "code-128", "code-93", "code-39"],
    onCodeScanned: onCodeScanned,
  });

  console.log(JSON.stringify(device, (k, v) => k === "formats" ? [] : v, 2))


  return isScannerOpen ? (
    <Portal>
      <View style={styles.container}>
        <Appbar.Header>
          <Appbar.Action icon="close" onPress={closeBarCodeScanner} />
          <Appbar.Content title={"Barcode scannen"} />
        </Appbar.Header>
        {cameraPermission === null || device == undefined ? (
          <View style={[styles.fill, styles.black]} />
        ) : !cameraPermission ? (
          <View style={[styles.fill, styles.noAccess]}>
            <Text>{"No access"}</Text>
            <Button onPress={() => Linking.openSettings().catch(() => console.warn('cannot open settings'))}>
              <Text>{"Open Settings"}</Text>
            </Button>
          </View>
        ) : isLoading ? (
          <View style={[styles.fill, styles.loadingContainer]}>
            <ActivityIndicator size="large" />
          </View>
        ) : (
          <View style={styles.fill}>
            <Camera
              style={StyleSheet.absoluteFill}
              device={device}
              isActive={true}
              codeScanner={codeScanner}
              focusable={true}
            >
            </Camera>
            <BarcodeMask height={130} showAnimatedLine={false} />
          </View>
        )}
      </View>
    </Portal>
  ) : null;
};

const styles = StyleSheet.create({
  fill: {
    flex: 1,
  },
  container: {
    flex: 1,
    flexDirection: 'column',
  },
  black: {
    backgroundColor: 'black',
  },
  loadingContainer: {
    backgroundColor: 'white',
    justifyContent: 'center',
    alignItems: 'center',
  },
  noAccess: {
    backgroundColor: 'white',
    padding: 20,
  },
});

Relevant log output

01-27 07:37:47.991   381   467 D CompositionEngine: Layer: ch.documed.xxxx/com.xxxx.MainActivity#993 had an out of bounds transparent region
01-27 07:37:47.991   381   467 D Region  :   Region transparentRegionHint (this=0xb400007b57878d90, count=2)
01-27 07:37:47.991   381   467 D Region  :     [-258, 210, -10, 240]
01-27 07:37:47.991   381   467 D Region  :     [-258, 240,   0, 1170]
01-27 07:37:48.092   381   467 D CompositionEngine: Layer: ch.documed.xxxx/com.xxxx.MainActivity#993 had an out of bounds transparent region
01-27 07:37:48.092   381   467 D Region  :   Region transparentRegionHint (this=0xb400007b57878d90, count=2)
01-27 07:37:48.092   381   467 D Region  :     [-258, 210, -10, 240]
01-27 07:37:48.092   381   467 D Region  :     [-258, 240,   0, 1170]
01-27 07:37:48.193   381   467 D CompositionEngine: Layer: ch.documed.xxxx/com.xxxx.MainActivity#993 had an out of bounds transparent region
01-27 07:37:48.193   381   467 D Region  :   Region transparentRegionHint (this=0xb400007b57878d90, count=2)
01-27 07:37:48.193   381   467 D Region  :     [-258, 210, -10, 240]
01-27 07:37:48.193   381   467 D Region  :     [-258, 240,   0, 1170]

Camera Device

{
  "id": "10",
  "position": "back",
  "hasTorch": false,
  "hasFlash": false,
  "name": "10 (BACK) androidx.camera.camera2",
  "minExposure": -6,
  "minFocusDistance": 999.999985098839,
  "isMultiCam": false,
  "supportsRawCapture": false,
  "supportsFocus": true,
  "physicalDevices": [
    "ultra-wide-angle-camera"
  ],
  "neutralZoom": 1,
  "supportsLowLightBoost": false,
  "maxExposure": 6,
  "minZoom": 1,
  "maxZoom": 1,
  "hardwareLevel": "limited",
  "sensorOrientation": "landscape-left",
  "formats": []
}

Device

e.g. Samsung Galaxy A20e (real device), Pixel 7 (real device), any simulated device

VisionCamera Version

4.6.3

Can you reproduce this issue in the VisionCamera Example app?

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

Additional information

abucher90 avatar Jan 27 '25 06:01 abucher90

Guten Tag, Hans here! 🍻

Thank you for your detailed report, Andreas. It appears you’re experiencing a rendering issue with ze camera in a Portal/View component on Android. You provided good information and logs which will be helpful for mrousavy to investigate this.

Could you please check if the issue persists in a standalone example app that uses the Vision Camera without Expo? It would help to narrow down ze problem. Additionally, sometimes issues like this can be related to specific device configurations. If you could share ze specific device model and OS version where you are facing this, it would also aid in troubleshooting.

Let’s keep zis issue open for now!

Note: If you think I made a mistake, please ping @mrousavy to take a look.

maintenance-hans[bot] avatar Jan 27 '25 06:01 maintenance-hans[bot]

I have them same problem

PetrVasilev avatar Jan 29 '25 10:01 PetrVasilev

I have them same problem

Thanks for your response! It does seem like a real bug in the lib. Have you worked with older versions of it before, or did you just start using it and encounter the same problem?

abucher90 avatar Jan 30 '25 06:01 abucher90

@mrousavy Would be great to get some feedback as we're somehow blocked with our next release. Thanks alot.

abucher90 avatar Jan 31 '25 16:01 abucher90

Is there any update on this issue? I'm experiencing the same problem.

JainilCitrusbug avatar Feb 03 '25 09:02 JainilCitrusbug

@mrousavy Would be great to get some feedback as we're somehow blocked with our next release. Thanks alot.

If you're blocked on releases feel free to contact my agency - we offer consultancy services to unblock you asap.


Other than that; no idea. I haven't worked with Portals yet. Try setting the preview surface mode - see Preview: Overlays and Masks

mrousavy avatar Feb 03 '25 09:02 mrousavy

Thanks for your response. Setting the androidPreviewViewType to "surface-view" didn't solve the problem.

It's not only limited to Portals, it also happens exactly the same if I use e.g. a Modal from react-native. I figured out, that it works much better if I use the onLayout() like this (but i'm still able to mess the layout up):

import React, { useCallback, useState } from 'react';
import { Linking, Modal, StyleSheet, View, ViewStyle } from 'react-native';
import { useTranslation } from 'react-i18next';
import { Props } from './ScannerContainer';
import { ActivityIndicator, Appbar, Button, Text } from 'react-native-paper';
import BarcodeMask from 'react-native-barcode-mask';
import { Camera, getCameraDevice, useCodeScanner, Code } from 'react-native-vision-camera';
import { useCameraPermissions } from '../../hooks/useCameraPermissions.ts';
import { theme } from '../theme.ts';

export const Scanner: React.FC<Props> = ({
  isScannerOpen,
  navigateToUrl,
  setNavigationStack,
  closeBarCodeScanner,
  baseUrl,
  specialLinks,
}) => {
  const { t } = useTranslation();

  const cameraPermission = useCameraPermissions(isScannerOpen);
  const allDevices = Camera.getAvailableCameraDevices();
  const devices = getCameraDevice(allDevices, 'back', {
    physicalDevices: ['ultra-wide-angle-camera', 'wide-angle-camera', 'telephoto-camera'],
  });

  const [isLoading, setIsLoading] = useState(false);
  const [camStyles, setCamStyles] = useState<ViewStyle>({ width: 0, height: 0 });   // SET DEFAULT camStyle

  const handleCodeScanned = useCallback(
    async (codes: Code[]) => {
    // handle the scanned Code callback
    },
    [baseUrl, navigateToUrl, setNavigationStack, specialLinks],
  );

  const codeScanner = useCodeScanner({
    codeTypes: ['qr', 'ean-13', 'ean-8', 'code-128', 'code-93', 'code-39'],
    onCodeScanned: handleCodeScanned,
  });

  const cameraContent = () => {
    if (cameraPermission === 'not-determined' || !devices) {
      return <View style={[styles.fill, styles.black]} />;
    }

    if (cameraPermission !== 'granted') {
      return (
        <View style={[styles.fill, styles.noAccess]}>
          <Text>{t('scanner.noAccess')}</Text>
          <Button
            onPress={() => Linking.openSettings().catch(() => console.warn('Unable to open settings'))}
          >
            {t('scanner.openSettings')}
          </Button>
        </View>
      );
    }

    if (isLoading) {
      return (
        <View style={[styles.fill, styles.loadingContainer]}>
          <ActivityIndicator size="large" color={theme.colors.actionColor} />
        </View>
      );
    }

    return (
      <View style={styles.fill}>
        <Camera
          style={camStyles}
          device={devices}
          isActive={true}
          codeScanner={codeScanner}
          focusable={true}
          onLayout={() => {
            // This is the workaround to correctly render the camera inside of its container.
            // Otherwise, it sometimes appears outside of the expected container
            // Still not perfect, but ok
            setCamStyles({
              flex: 1,
            });
          }}
        />
        <BarcodeMask height={130} showAnimatedLine={false} />
      </View>
    );
  };

  return (
    <Modal        // Using a Modal instead of a Portal
      statusBarTranslucent={true}
      transparent={true}
      visible={isScannerOpen}
      animationType={'slide'}
      onRequestClose={() => {
        closeBarCodeScanner();
      }}
    >
      <View style={styles.container}>
        <Appbar.Header>
          <Appbar.Action icon="close" onPress={closeBarCodeScanner} />
          <Appbar.Content title={t('scanner.title')} />
        </Appbar.Header>
        {cameraContent()}
      </View>
    </Modal>
  );
};

const styles = StyleSheet.create({
  fill: {
    flex: 1,
  },
  container: {
    flex: 1,
    flexDirection: 'column',
  },
  black: {
    backgroundColor: 'black',
  },
  loadingContainer: {
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'white',
  },
  noAccess: {
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
    backgroundColor: 'white',
  },
});

With this solution, I can currently live with - but it's still a bug to me that it doesn't work consistently.

Andreas

abucher90 avatar Feb 03 '25 12:02 abucher90

Setting the androidPreviewViewType to "surface-view" didn't solve the problem.

You should set the surface mode to texture-view, not surface-view. surface-view is the default.

mrousavy avatar Feb 03 '25 12:02 mrousavy

Sorry - that's what I ment. androidPreviewViewType={"texture-view"}

The combination of onLayout{() => ...} and setting this flag to "texture-view" seems to be the best working solution currently, though it's still not entirely consistent. I can still occasionally disrupt the layout, but it happens rarely. I can reproduce the issue by launching the application and quickly opening the camera modal view right after the app starts.

If I completely remove the onLayout{() => ...} and only set the "texture-view", it happens quite often. But so far, i'm happy with that solution for now.

      <View style={styles.fill}>
        <Camera
          androidPreviewViewType={"texture-view"}
          style={camStyles}
          device={devices}
          isActive={true}
          codeScanner={codeScanner}
          focusable={true}
          onLayout={() => {
            // This is a workaround to correctly render the camera inside of its container.
            // Otherwise, it sometimes appears outside of the expected container
            setCamStyles({
              flex: 1,
            });
          }}
        />
        <BarcodeMask height={130} showAnimatedLine={false} />
      </View>

abucher90 avatar Feb 03 '25 13:02 abucher90

I can confirm this bug. When I use the camera in a portal on Android, the layout of the view gets messed up weirdly. It doesn't always happen, but quite often. Thanks @abucher90 for your workaround.

c-goettert avatar May 05 '25 09:05 c-goettert