chore: Move everything into package/ (#1745)
				
					
				
			* Move everything into package * Remove .DS_Store * Move scripts and eslintrc to package * Create CODE_OF_CONDUCT.md * fix some links * Update all links (I think) * Update generated docs * Update notice-yarn-changes.yml * Update validate-android.yml * Update validate-cpp.yml * Delete notice-yarn-changes.yml * Update validate-cpp.yml * Update validate-cpp.yml * Update validate-js.yml * Update validate-cpp.yml * Update validate-cpp.yml * wrong c++ style * Revert "wrong c++ style" This reverts commit 55a3575589c6f13f8b05134d83384f55e0601ab2.
This commit is contained in:
		
							
								
								
									
										315
									
								
								package/example/src/CameraPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										315
									
								
								package/example/src/CameraPage.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,315 @@
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
import { useRef, useState, useMemo, useCallback } from 'react';
 | 
			
		||||
import { StyleSheet, Text, View } from 'react-native';
 | 
			
		||||
import { PinchGestureHandler, PinchGestureHandlerGestureEvent, TapGestureHandler } from 'react-native-gesture-handler';
 | 
			
		||||
import {
 | 
			
		||||
  CameraDeviceFormat,
 | 
			
		||||
  CameraRuntimeError,
 | 
			
		||||
  PhotoFile,
 | 
			
		||||
  sortFormats,
 | 
			
		||||
  useCameraDevices,
 | 
			
		||||
  useFrameProcessor,
 | 
			
		||||
  VideoFile,
 | 
			
		||||
} from 'react-native-vision-camera';
 | 
			
		||||
import { Camera } from 'react-native-vision-camera';
 | 
			
		||||
import { CONTENT_SPACING, MAX_ZOOM_FACTOR, SAFE_AREA_PADDING } from './Constants';
 | 
			
		||||
import Reanimated, { Extrapolate, interpolate, useAnimatedGestureHandler, useAnimatedProps, useSharedValue } from 'react-native-reanimated';
 | 
			
		||||
import { useEffect } from 'react';
 | 
			
		||||
import { useIsForeground } from './hooks/useIsForeground';
 | 
			
		||||
import { StatusBarBlurBackground } from './views/StatusBarBlurBackground';
 | 
			
		||||
import { CaptureButton } from './views/CaptureButton';
 | 
			
		||||
import { PressableOpacity } from 'react-native-pressable-opacity';
 | 
			
		||||
import MaterialIcon from 'react-native-vector-icons/MaterialCommunityIcons';
 | 
			
		||||
import IonIcon from 'react-native-vector-icons/Ionicons';
 | 
			
		||||
import type { Routes } from './Routes';
 | 
			
		||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
 | 
			
		||||
import { useIsFocused } from '@react-navigation/core';
 | 
			
		||||
import { examplePlugin } from './frame-processors/ExamplePlugin';
 | 
			
		||||
 | 
			
		||||
const ReanimatedCamera = Reanimated.createAnimatedComponent(Camera);
 | 
			
		||||
Reanimated.addWhitelistedNativeProps({
 | 
			
		||||
  zoom: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const SCALE_FULL_ZOOM = 3;
 | 
			
		||||
const BUTTON_SIZE = 40;
 | 
			
		||||
 | 
			
		||||
type Props = NativeStackScreenProps<Routes, 'CameraPage'>;
 | 
			
		||||
export function CameraPage({ navigation }: Props): React.ReactElement {
 | 
			
		||||
  const camera = useRef<Camera>(null);
 | 
			
		||||
  const [isCameraInitialized, setIsCameraInitialized] = useState(false);
 | 
			
		||||
  const [hasMicrophonePermission, setHasMicrophonePermission] = useState(false);
 | 
			
		||||
  const zoom = useSharedValue(0);
 | 
			
		||||
  const isPressingButton = useSharedValue(false);
 | 
			
		||||
 | 
			
		||||
  // check if camera page is active
 | 
			
		||||
  const isFocussed = useIsFocused();
 | 
			
		||||
  const isForeground = useIsForeground();
 | 
			
		||||
  const isActive = isFocussed && isForeground;
 | 
			
		||||
 | 
			
		||||
  const [cameraPosition, setCameraPosition] = useState<'front' | 'back'>('back');
 | 
			
		||||
  const [enableHdr, setEnableHdr] = useState(false);
 | 
			
		||||
  const [flash, setFlash] = useState<'off' | 'on'>('off');
 | 
			
		||||
  const [enableNightMode, setEnableNightMode] = useState(false);
 | 
			
		||||
 | 
			
		||||
  // camera format settings
 | 
			
		||||
  const devices = useCameraDevices();
 | 
			
		||||
  const device = devices[cameraPosition];
 | 
			
		||||
  const formats = useMemo<CameraDeviceFormat[]>(() => {
 | 
			
		||||
    if (device?.formats == null) return [];
 | 
			
		||||
    return device.formats.sort(sortFormats);
 | 
			
		||||
  }, [device?.formats]);
 | 
			
		||||
 | 
			
		||||
  //#region Memos
 | 
			
		||||
  const [is60Fps, setIs60Fps] = useState(true);
 | 
			
		||||
  const fps = useMemo(() => {
 | 
			
		||||
    if (!is60Fps) return 30;
 | 
			
		||||
 | 
			
		||||
    if (enableNightMode && !device?.supportsLowLightBoost) {
 | 
			
		||||
      // User has enabled Night Mode, but Night Mode is not natively supported, so we simulate it by lowering the frame rate.
 | 
			
		||||
      return 30;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const supportsHdrAt60Fps = formats.some((f) => f.supportsVideoHDR && f.maxFps >= 60);
 | 
			
		||||
    if (enableHdr && !supportsHdrAt60Fps) {
 | 
			
		||||
      // User has enabled HDR, but HDR is not supported at 60 FPS.
 | 
			
		||||
      return 30;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const supports60Fps = formats.some((f) => f.maxFps >= 60);
 | 
			
		||||
    if (!supports60Fps) {
 | 
			
		||||
      // 60 FPS is not supported by any format.
 | 
			
		||||
      return 30;
 | 
			
		||||
    }
 | 
			
		||||
    // If nothing blocks us from using it, we default to 60 FPS.
 | 
			
		||||
    return 60;
 | 
			
		||||
  }, [device?.supportsLowLightBoost, enableHdr, enableNightMode, formats, is60Fps]);
 | 
			
		||||
 | 
			
		||||
  const supportsCameraFlipping = useMemo(() => devices.back != null && devices.front != null, [devices.back, devices.front]);
 | 
			
		||||
  const supportsFlash = device?.hasFlash ?? false;
 | 
			
		||||
  const supportsHdr = useMemo(() => formats.some((f) => f.supportsVideoHDR || f.supportsPhotoHDR), [formats]);
 | 
			
		||||
  const supports60Fps = useMemo(() => formats.some((f) => f.maxFps >= 60), [formats]);
 | 
			
		||||
  const canToggleNightMode = enableNightMode
 | 
			
		||||
    ? true // it's enabled so you have to be able to turn it off again
 | 
			
		||||
    : (device?.supportsLowLightBoost ?? false) || fps > 30; // either we have native support, or we can lower the FPS
 | 
			
		||||
  //#endregion
 | 
			
		||||
 | 
			
		||||
  const format = useMemo(() => {
 | 
			
		||||
    let result = formats;
 | 
			
		||||
    if (enableHdr) {
 | 
			
		||||
      // We only filter by HDR capable formats if HDR is set to true.
 | 
			
		||||
      // Otherwise we ignore the `supportsVideoHDR` property and accept formats which support HDR `true` or `false`
 | 
			
		||||
      result = result.filter((f) => f.supportsVideoHDR || f.supportsPhotoHDR);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // find the first format that includes the given FPS
 | 
			
		||||
    return result.find((f) => f.maxFps >= fps);
 | 
			
		||||
  }, [formats, fps, enableHdr]);
 | 
			
		||||
 | 
			
		||||
  //#region Animated Zoom
 | 
			
		||||
  // This just maps the zoom factor to a percentage value.
 | 
			
		||||
  // so e.g. for [min, neutr., max] values [1, 2, 128] this would result in [0, 0.0081, 1]
 | 
			
		||||
  const minZoom = device?.minZoom ?? 1;
 | 
			
		||||
  const maxZoom = Math.min(device?.maxZoom ?? 1, MAX_ZOOM_FACTOR);
 | 
			
		||||
 | 
			
		||||
  const cameraAnimatedProps = useAnimatedProps(() => {
 | 
			
		||||
    const z = Math.max(Math.min(zoom.value, maxZoom), minZoom);
 | 
			
		||||
    return {
 | 
			
		||||
      zoom: z,
 | 
			
		||||
    };
 | 
			
		||||
  }, [maxZoom, minZoom, zoom]);
 | 
			
		||||
  //#endregion
 | 
			
		||||
 | 
			
		||||
  //#region Callbacks
 | 
			
		||||
  const setIsPressingButton = useCallback(
 | 
			
		||||
    (_isPressingButton: boolean) => {
 | 
			
		||||
      isPressingButton.value = _isPressingButton;
 | 
			
		||||
    },
 | 
			
		||||
    [isPressingButton],
 | 
			
		||||
  );
 | 
			
		||||
  // Camera callbacks
 | 
			
		||||
  const onError = useCallback((error: CameraRuntimeError) => {
 | 
			
		||||
    console.error(error);
 | 
			
		||||
  }, []);
 | 
			
		||||
  const onInitialized = useCallback(() => {
 | 
			
		||||
    console.log('Camera initialized!');
 | 
			
		||||
    setIsCameraInitialized(true);
 | 
			
		||||
  }, []);
 | 
			
		||||
  const onMediaCaptured = useCallback(
 | 
			
		||||
    (media: PhotoFile | VideoFile, type: 'photo' | 'video') => {
 | 
			
		||||
      console.log(`Media captured! ${JSON.stringify(media)}`);
 | 
			
		||||
      navigation.navigate('MediaPage', {
 | 
			
		||||
        path: media.path,
 | 
			
		||||
        type: type,
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    [navigation],
 | 
			
		||||
  );
 | 
			
		||||
  const onFlipCameraPressed = useCallback(() => {
 | 
			
		||||
    setCameraPosition((p) => (p === 'back' ? 'front' : 'back'));
 | 
			
		||||
  }, []);
 | 
			
		||||
  const onFlashPressed = useCallback(() => {
 | 
			
		||||
    setFlash((f) => (f === 'off' ? 'on' : 'off'));
 | 
			
		||||
  }, []);
 | 
			
		||||
  //#endregion
 | 
			
		||||
 | 
			
		||||
  //#region Tap Gesture
 | 
			
		||||
  const onDoubleTap = useCallback(() => {
 | 
			
		||||
    onFlipCameraPressed();
 | 
			
		||||
  }, [onFlipCameraPressed]);
 | 
			
		||||
  //#endregion
 | 
			
		||||
 | 
			
		||||
  //#region Effects
 | 
			
		||||
  const neutralZoom = device?.neutralZoom ?? 1;
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // Run everytime the neutralZoomScaled value changes. (reset zoom when device changes)
 | 
			
		||||
    zoom.value = neutralZoom;
 | 
			
		||||
  }, [neutralZoom, zoom]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    Camera.getMicrophonePermissionStatus().then((status) => setHasMicrophonePermission(status === 'granted'));
 | 
			
		||||
  }, []);
 | 
			
		||||
  //#endregion
 | 
			
		||||
 | 
			
		||||
  //#region Pinch to Zoom Gesture
 | 
			
		||||
  // The gesture handler maps the linear pinch gesture (0 - 1) to an exponential curve since a camera's zoom
 | 
			
		||||
  // function does not appear linear to the user. (aka zoom 0.1 -> 0.2 does not look equal in difference as 0.8 -> 0.9)
 | 
			
		||||
  const onPinchGesture = useAnimatedGestureHandler<PinchGestureHandlerGestureEvent, { startZoom?: number }>({
 | 
			
		||||
    onStart: (_, context) => {
 | 
			
		||||
      context.startZoom = zoom.value;
 | 
			
		||||
    },
 | 
			
		||||
    onActive: (event, context) => {
 | 
			
		||||
      // we're trying to map the scale gesture to a linear zoom here
 | 
			
		||||
      const startZoom = context.startZoom ?? 0;
 | 
			
		||||
      const scale = interpolate(event.scale, [1 - 1 / SCALE_FULL_ZOOM, 1, SCALE_FULL_ZOOM], [-1, 0, 1], Extrapolate.CLAMP);
 | 
			
		||||
      zoom.value = interpolate(scale, [-1, 0, 1], [minZoom, startZoom, maxZoom], Extrapolate.CLAMP);
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  //#endregion
 | 
			
		||||
 | 
			
		||||
  if (device != null && format != null) {
 | 
			
		||||
    console.log(
 | 
			
		||||
      `Re-rendering camera page with ${isActive ? 'active' : 'inactive'} camera. ` +
 | 
			
		||||
        `Device: "${device.name}" (${format.photoWidth}x${format.photoHeight} photo / ${format.videoWidth}x${format.videoHeight} video @ ${fps}fps)`,
 | 
			
		||||
    );
 | 
			
		||||
  } else {
 | 
			
		||||
    console.log('re-rendering camera page without active camera');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const frameProcessor = useFrameProcessor((frame) => {
 | 
			
		||||
    'worklet';
 | 
			
		||||
 | 
			
		||||
    console.log(`${frame.timestamp}: ${frame.width}x${frame.height} ${frame.pixelFormat} Frame (${frame.orientation})`);
 | 
			
		||||
    examplePlugin(frame);
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <View style={styles.container}>
 | 
			
		||||
      {device != null && (
 | 
			
		||||
        <PinchGestureHandler onGestureEvent={onPinchGesture} enabled={isActive}>
 | 
			
		||||
          <Reanimated.View style={StyleSheet.absoluteFill}>
 | 
			
		||||
            <TapGestureHandler onEnded={onDoubleTap} numberOfTaps={2}>
 | 
			
		||||
              <ReanimatedCamera
 | 
			
		||||
                ref={camera}
 | 
			
		||||
                style={StyleSheet.absoluteFill}
 | 
			
		||||
                device={device}
 | 
			
		||||
                format={format}
 | 
			
		||||
                fps={fps}
 | 
			
		||||
                hdr={enableHdr}
 | 
			
		||||
                lowLightBoost={device.supportsLowLightBoost && enableNightMode}
 | 
			
		||||
                isActive={isActive}
 | 
			
		||||
                onInitialized={onInitialized}
 | 
			
		||||
                onError={onError}
 | 
			
		||||
                enableZoomGesture={false}
 | 
			
		||||
                animatedProps={cameraAnimatedProps}
 | 
			
		||||
                enableFpsGraph={true}
 | 
			
		||||
                orientation="portrait"
 | 
			
		||||
                photo={true}
 | 
			
		||||
                video={true}
 | 
			
		||||
                audio={hasMicrophonePermission}
 | 
			
		||||
                frameProcessor={frameProcessor}
 | 
			
		||||
              />
 | 
			
		||||
            </TapGestureHandler>
 | 
			
		||||
          </Reanimated.View>
 | 
			
		||||
        </PinchGestureHandler>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <CaptureButton
 | 
			
		||||
        style={styles.captureButton}
 | 
			
		||||
        camera={camera}
 | 
			
		||||
        onMediaCaptured={onMediaCaptured}
 | 
			
		||||
        cameraZoom={zoom}
 | 
			
		||||
        minZoom={minZoom}
 | 
			
		||||
        maxZoom={maxZoom}
 | 
			
		||||
        flash={supportsFlash ? flash : 'off'}
 | 
			
		||||
        enabled={isCameraInitialized && isActive}
 | 
			
		||||
        setIsPressingButton={setIsPressingButton}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <StatusBarBlurBackground />
 | 
			
		||||
 | 
			
		||||
      <View style={styles.rightButtonRow}>
 | 
			
		||||
        {supportsCameraFlipping && (
 | 
			
		||||
          <PressableOpacity style={styles.button} onPress={onFlipCameraPressed} disabledOpacity={0.4}>
 | 
			
		||||
            <IonIcon name="camera-reverse" color="white" size={24} />
 | 
			
		||||
          </PressableOpacity>
 | 
			
		||||
        )}
 | 
			
		||||
        {supportsFlash && (
 | 
			
		||||
          <PressableOpacity style={styles.button} onPress={onFlashPressed} disabledOpacity={0.4}>
 | 
			
		||||
            <IonIcon name={flash === 'on' ? 'flash' : 'flash-off'} color="white" size={24} />
 | 
			
		||||
          </PressableOpacity>
 | 
			
		||||
        )}
 | 
			
		||||
        {supports60Fps && (
 | 
			
		||||
          <PressableOpacity style={styles.button} onPress={() => setIs60Fps(!is60Fps)}>
 | 
			
		||||
            <Text style={styles.text}>
 | 
			
		||||
              {is60Fps ? '60' : '30'}
 | 
			
		||||
              {'\n'}FPS
 | 
			
		||||
            </Text>
 | 
			
		||||
          </PressableOpacity>
 | 
			
		||||
        )}
 | 
			
		||||
        {supportsHdr && (
 | 
			
		||||
          <PressableOpacity style={styles.button} onPress={() => setEnableHdr((h) => !h)}>
 | 
			
		||||
            <MaterialIcon name={enableHdr ? 'hdr' : 'hdr-off'} color="white" size={24} />
 | 
			
		||||
          </PressableOpacity>
 | 
			
		||||
        )}
 | 
			
		||||
        {canToggleNightMode && (
 | 
			
		||||
          <PressableOpacity style={styles.button} onPress={() => setEnableNightMode(!enableNightMode)} disabledOpacity={0.4}>
 | 
			
		||||
            <IonIcon name={enableNightMode ? 'moon' : 'moon-outline'} color="white" size={24} />
 | 
			
		||||
          </PressableOpacity>
 | 
			
		||||
        )}
 | 
			
		||||
      </View>
 | 
			
		||||
    </View>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  container: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    backgroundColor: 'black',
 | 
			
		||||
  },
 | 
			
		||||
  captureButton: {
 | 
			
		||||
    position: 'absolute',
 | 
			
		||||
    alignSelf: 'center',
 | 
			
		||||
    bottom: SAFE_AREA_PADDING.paddingBottom,
 | 
			
		||||
  },
 | 
			
		||||
  button: {
 | 
			
		||||
    marginBottom: CONTENT_SPACING,
 | 
			
		||||
    width: BUTTON_SIZE,
 | 
			
		||||
    height: BUTTON_SIZE,
 | 
			
		||||
    borderRadius: BUTTON_SIZE / 2,
 | 
			
		||||
    backgroundColor: 'rgba(140, 140, 140, 0.3)',
 | 
			
		||||
    justifyContent: 'center',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
  },
 | 
			
		||||
  rightButtonRow: {
 | 
			
		||||
    position: 'absolute',
 | 
			
		||||
    right: SAFE_AREA_PADDING.paddingRight,
 | 
			
		||||
    top: SAFE_AREA_PADDING.paddingTop,
 | 
			
		||||
  },
 | 
			
		||||
  text: {
 | 
			
		||||
    color: 'white',
 | 
			
		||||
    fontSize: 11,
 | 
			
		||||
    fontWeight: 'bold',
 | 
			
		||||
    textAlign: 'center',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
		Reference in New Issue
	
	Block a user