react-native-vision-camera-zxing
Version: 
A powerful, high-performance React Native Camera library.
660 lines (638 loc) • 23.8 kB
JavaScript
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
import React from 'react';
import { findNodeHandle, StyleSheet } from 'react-native';
import { CameraRuntimeError, tryParseNativeCameraError, isErrorWithCause } from './CameraError';
import { CameraModule } from './NativeCameraModule';
import { VisionCameraProxy } from './frame-processors/VisionCameraProxy';
import { CameraDevices } from './CameraDevices';
import { SkiaCameraCanvas } from './skia/SkiaCameraCanvas';
import { FpsGraph, MAX_BARS } from './FpsGraph';
import { NativeCameraView } from './NativeCameraView';
import { RotationHelper } from './RotationHelper';
//#region Types
//#endregion
function isSkiaFrameProcessor(frameProcessor) {
  return frameProcessor?.type === 'drawable-skia';
}
//#region Camera Component
/**
 * ### A powerful `<Camera>` component.
 *
 * Read the [VisionCamera documentation](https://react-native-vision-camera.com/) for more information.
 *
 * The `<Camera>` component's most important properties are:
 *
 * * {@linkcode CameraProps.device | device}: Specifies the {@linkcode CameraDevice} to use. Get a {@linkcode CameraDevice} by using
 * the {@linkcode useCameraDevice | useCameraDevice(..)} hook, or manually by using
 * the {@linkcode CameraDevices.getAvailableCameraDevices | CameraDevices.getAvailableCameraDevices()} function.
 * * {@linkcode CameraProps.isActive | isActive}: A boolean value that specifies whether the Camera should
 * actively stream video frames or not. This can be compared to a Video component, where `isActive` specifies whether the video
 * is paused or not. If you fully unmount the `<Camera>` component instead of using `isActive={false}`, the Camera will take a bit longer to start again.
 *
 * @example
 * ```tsx
 * function App() {
 *   const device = useCameraDevice('back')
 *
 *   if (device == null) return <NoCameraErrorView />
 *   return (
 *     <Camera
 *       style={StyleSheet.absoluteFill}
 *       device={device}
 *       isActive={true}
 *     />
 *   )
 * }
 * ```
 *
 * @component
 */
export class Camera extends React.PureComponent {
  /** @internal */
  static displayName = 'Camera';
  /** @internal */
  displayName = Camera.displayName;
  isNativeViewMounted = false;
  lastUIRotation = undefined;
  rotationHelper = new RotationHelper();
  /** @internal */
  constructor(props) {
    super(props);
    this.onViewReady = this.onViewReady.bind(this);
    this.onAverageFpsChanged = this.onAverageFpsChanged.bind(this);
    this.onInitialized = this.onInitialized.bind(this);
    this.onStarted = this.onStarted.bind(this);
    this.onStopped = this.onStopped.bind(this);
    this.onPreviewStarted = this.onPreviewStarted.bind(this);
    this.onPreviewStopped = this.onPreviewStopped.bind(this);
    this.onShutter = this.onShutter.bind(this);
    this.onOutputOrientationChanged = this.onOutputOrientationChanged.bind(this);
    this.onPreviewOrientationChanged = this.onPreviewOrientationChanged.bind(this);
    this.onError = this.onError.bind(this);
    this.onCodeScanned = this.onCodeScanned.bind(this);
    this.ref = /*#__PURE__*/React.createRef();
    this.lastFrameProcessor = undefined;
    this.state = {
      isRecordingWithFlash: false,
      averageFpsSamples: []
    };
  }
  get handle() {
    const nodeHandle = findNodeHandle(this.ref.current);
    if (nodeHandle == null || nodeHandle === -1) {
      throw new CameraRuntimeError('system/view-not-found', "Could not get the Camera's native view tag! Does the Camera View exist in the native view-tree?");
    }
    return nodeHandle;
  }
  //#region View-specific functions (UIViewManager)
  /**
   * Take a single photo and write it's content to a temporary file.
   *
   * @throws {@linkcode CameraCaptureError} When any kind of error occured while capturing the photo.
   * Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
   * @example
   * ```ts
   * const photo = await camera.current.takePhoto({
   *   flash: 'on',
   *   enableAutoRedEyeReduction: true
   * })
   * ```
   */
  async takePhoto(options) {
    try {
      return await CameraModule.takePhoto(this.handle, options ?? {});
    } catch (e) {
      throw tryParseNativeCameraError(e);
    }
  }
  /**
   * Captures a snapshot of the Camera view and write it's content to a temporary file.
   *
   * - On iOS, `takeSnapshot` waits for a Frame from the video pipeline and therefore requires `video` to be enabled.
   * - On Android, `takeSnapshot` performs a GPU view screenshot from the preview view.
   *
   * @throws {@linkcode CameraCaptureError} When any kind of error occured while capturing the photo.
   * Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
   * @example
   * ```ts
   * const snapshot = await camera.current.takeSnapshot({
   *   quality: 100
   * })
   * ```
   */
  async takeSnapshot(options) {
    try {
      return await CameraModule.takeSnapshot(this.handle, options ?? {});
    } catch (e) {
      throw tryParseNativeCameraError(e);
    }
  }
  getBitRateMultiplier(bitRate) {
    if (typeof bitRate === 'number' || bitRate == null) return 1;
    switch (bitRate) {
      case 'extra-low':
        return 0.6;
      case 'low':
        return 0.8;
      case 'normal':
        return 1;
      case 'high':
        return 1.2;
      case 'extra-high':
        return 1.4;
    }
  }
  /**
   * Start a new video recording.
   *
   * @throws {@linkcode CameraCaptureError} When any kind of error occured while starting the video recording.
   * Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
   *
   * @example
   * ```ts
   * camera.current.startRecording({
   *   onRecordingFinished: (video) => console.log(video),
   *   onRecordingError: (error) => console.error(error),
   * })
   * setTimeout(() => {
   *   camera.current.stopRecording()
   * }, 5000)
   * ```
   */
  startRecording(options) {
    const {
      onRecordingError,
      onRecordingFinished,
      ...passThruOptions
    } = options;
    if (typeof onRecordingError !== 'function' || typeof onRecordingFinished !== 'function') throw new CameraRuntimeError('parameter/invalid-parameter', 'The onRecordingError or onRecordingFinished functions were not set!');
    if (options.flash === 'on') {
      // Enable torch for video recording
      this.setState({
        isRecordingWithFlash: true
      });
    }
    const onRecordCallback = (video, error) => {
      if (this.state.isRecordingWithFlash) {
        // disable torch again if it was enabled
        this.setState({
          isRecordingWithFlash: false
        });
      }
      if (error != null) return onRecordingError(error);
      if (video != null) return onRecordingFinished(video);
    };
    const nativeRecordVideoOptions = passThruOptions;
    try {
      // TODO: Use TurboModules to make this awaitable.
      CameraModule.startRecording(this.handle, nativeRecordVideoOptions, onRecordCallback);
    } catch (e) {
      throw tryParseNativeCameraError(e);
    }
  }
  /**
   * Pauses the current video recording.
   *
   * @throws {@linkcode CameraCaptureError} When any kind of error occured while pausing the video recording.
   * Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
   *
   * @example
   * ```ts
   * // Start
   * await camera.current.startRecording({
   *   onRecordingFinished: (video) => console.log(video),
   *   onRecordingError: (error) => console.error(error),
   * })
   * await timeout(1000)
   * // Pause
   * await camera.current.pauseRecording()
   * await timeout(500)
   * // Resume
   * await camera.current.resumeRecording()
   * await timeout(2000)
   * // Stop
   * await camera.current.stopRecording()
   * ```
   */
  async pauseRecording() {
    try {
      return await CameraModule.pauseRecording(this.handle);
    } catch (e) {
      throw tryParseNativeCameraError(e);
    }
  }
  /**
   * Resumes a currently paused video recording.
   *
   * @throws {@linkcode CameraCaptureError} When any kind of error occured while resuming the video recording.
   * Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
   *
   * @example
   * ```ts
   * // Start
   * await camera.current.startRecording({
   *   onRecordingFinished: (video) => console.log(video),
   *   onRecordingError: (error) => console.error(error),
   * })
   * await timeout(1000)
   * // Pause
   * await camera.current.pauseRecording()
   * await timeout(500)
   * // Resume
   * await camera.current.resumeRecording()
   * await timeout(2000)
   * // Stop
   * await camera.current.stopRecording()
   * ```
   */
  async resumeRecording() {
    try {
      return await CameraModule.resumeRecording(this.handle);
    } catch (e) {
      throw tryParseNativeCameraError(e);
    }
  }
  /**
   * Stop the current video recording.
   *
   * @throws {@linkcode CameraCaptureError} When any kind of error occured while stopping the video recording.
   * Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
   *
   * @example
   * ```ts
   * await camera.current.startRecording({
   *   onRecordingFinished: (video) => console.log(video),
   *   onRecordingError: (error) => console.error(error),
   * })
   * setTimeout(async () => {
   *   await camera.current.stopRecording()
   * }, 5000)
   * ```
   */
  async stopRecording() {
    try {
      return await CameraModule.stopRecording(this.handle);
    } catch (e) {
      throw tryParseNativeCameraError(e);
    }
  }
  /**
   * Cancel the current video recording. The temporary video file will be deleted,
   * and the `startRecording`'s `onRecordingError` callback will be invoked with a `capture/recording-canceled` error.
   *
   * @throws {@linkcode CameraCaptureError} When any kind of error occured while canceling the video recording.
   * Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
   *
   * @example
   * ```ts
   * await camera.current.startRecording({
   *   onRecordingFinished: (video) => console.log(video),
   *   onRecordingError: (error) => {
   *     if (error.code === 'capture/recording-canceled') {
   *       // recording was canceled.
   *     } else {
   *       console.error(error)
   *     }
   *   },
   * })
   * setTimeout(async () => {
   *   await camera.current.cancelRecording()
   * }, 5000)
   * ```
   */
  async cancelRecording() {
    try {
      return await CameraModule.cancelRecording(this.handle);
    } catch (e) {
      throw tryParseNativeCameraError(e);
    }
  }
  /**
   * Focus the camera to a specific point in the coordinate system.
   * @param {Point} point The point to focus to. This should be relative
   * to the Camera view's coordinate system and is expressed in points.
   *  * `(0, 0)` means **top left**.
   *  * `(CameraView.width, CameraView.height)` means **bottom right**.
   *
   * Make sure the value doesn't exceed the CameraView's dimensions.
   *
   * @throws {@linkcode CameraRuntimeError} When any kind of error occured while focussing.
   * Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
   * @example
   * ```ts
   * await camera.current.focus({
   *   x: tapEvent.x,
   *   y: tapEvent.y
   * })
   * ```
   */
  async focus(point) {
    try {
      return await CameraModule.focus(this.handle, point);
    } catch (e) {
      throw tryParseNativeCameraError(e);
    }
  }
  //#endregion
  //#region Static Functions (NativeModule)
  /**
   * Get a list of all available camera devices on the current phone.
   *
   * If you use Hooks, use the `useCameraDevices(..)` hook instead.
   *
   * * For Camera Devices attached to the phone, it is safe to assume that this will never change.
   * * For external Camera Devices (USB cameras, Mac continuity cameras, etc.) the available Camera Devices
   * could change over time when the external Camera device gets plugged in or plugged out, so
   * use {@link addCameraDevicesChangedListener | addCameraDevicesChangedListener(...)} to listen for such changes.
   *
   * @example
   * ```ts
   * const devices = Camera.getAvailableCameraDevices()
   * const backCameras = devices.filter((d) => d.position === "back")
   * const frontCameras = devices.filter((d) => d.position === "front")
   * ```
   */
  static getAvailableCameraDevices() {
    return CameraDevices.getAvailableCameraDevices();
  }
  /**
   * Adds a listener that gets called everytime the Camera Devices change, for example
   * when an external Camera Device (USB or continuity Camera) gets plugged in or plugged out.
   *
   * If you use Hooks, use the `useCameraDevices()` hook instead.
   */
  static addCameraDevicesChangedListener(listener) {
    return CameraDevices.addCameraDevicesChangedListener(listener);
  }
  /**
   * Gets the current Camera Permission Status. Check this before mounting the Camera to ensure
   * the user has permitted the app to use the camera.
   *
   * To actually prompt the user for camera permission, use {@linkcode Camera.requestCameraPermission | requestCameraPermission()}.
   */
  static getCameraPermissionStatus() {
    return CameraModule.getCameraPermissionStatus();
  }
  /**
   * Gets the current Microphone-Recording Permission Status.
   * Check this before enabling the `audio={...}` property to make sure the
   * user has permitted the app to use the microphone.
   *
   * To actually prompt the user for microphone permission, use {@linkcode Camera.requestMicrophonePermission | requestMicrophonePermission()}.
   */
  static getMicrophonePermissionStatus() {
    return CameraModule.getMicrophonePermissionStatus();
  }
  /**
   * Gets the current Location Permission Status.
   * Check this before enabling the `location={...}` property to make sure the
   * the user has permitted the app to use the location.
   *
   * To actually prompt the user for location permission, use {@linkcode Camera.requestLocationPermission | requestLocationPermission()}.
   *
   * Note: This method will throw a `system/location-not-enabled` error if the Location APIs are not enabled at build-time.
   * See [the "GPS Location Tags" documentation](https://react-native-vision-camera.com/docs/guides/location) for more information.
   */
  static getLocationPermissionStatus() {
    return CameraModule.getLocationPermissionStatus();
  }
  /**
   * Shows a "request permission" alert to the user, and resolves with the new camera permission status.
   *
   * If the user has previously blocked the app from using the camera, the alert will not be shown
   * and `"denied"` will be returned.
   *
   * @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission.
   * Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
   */
  static async requestCameraPermission() {
    try {
      return await CameraModule.requestCameraPermission();
    } catch (e) {
      throw tryParseNativeCameraError(e);
    }
  }
  /**
   * Shows a "request permission" alert to the user, and resolves with the new microphone permission status.
   *
   * If the user has previously blocked the app from using the microphone, the alert will not be shown
   * and `"denied"` will be returned.
   *
   * @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission.
   * Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
   */
  static async requestMicrophonePermission() {
    try {
      return await CameraModule.requestMicrophonePermission();
    } catch (e) {
      throw tryParseNativeCameraError(e);
    }
  }
  /**
   * Shows a "request permission" alert to the user, and resolves with the new location permission status.
   *
   * If the user has previously blocked the app from using the location, the alert will not be shown
   * and `"denied"` will be returned.
   *
   * @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission.
   * Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
   */
  static async requestLocationPermission() {
    try {
      return await CameraModule.requestLocationPermission();
    } catch (e) {
      throw tryParseNativeCameraError(e);
    }
  }
  //#endregion
  //#region Events (Wrapped to maintain reference equality)
  onError(event) {
    const error = event.nativeEvent;
    const cause = isErrorWithCause(error.cause) ? error.cause : undefined;
    // @ts-expect-error We're casting from unknown bridge types to TS unions, I expect it to hopefully work
    const cameraError = new CameraRuntimeError(error.code, error.message, cause);
    if (this.props.onError != null) {
      this.props.onError(cameraError);
    } else {
      // User didn't pass an `onError` handler, so just log it to console
      console.error(cameraError);
    }
  }
  onInitialized() {
    this.props.onInitialized?.();
  }
  onStarted() {
    this.props.onStarted?.();
  }
  onStopped() {
    this.props.onStopped?.();
  }
  onPreviewStarted() {
    this.props.onPreviewStarted?.();
  }
  onPreviewStopped() {
    this.props.onPreviewStopped?.();
  }
  onShutter(event) {
    this.props.onShutter?.(event.nativeEvent);
  }
  onOutputOrientationChanged({
    nativeEvent: {
      outputOrientation
    }
  }) {
    this.rotationHelper.outputOrientation = outputOrientation;
    this.props.onOutputOrientationChanged?.(outputOrientation);
    this.maybeUpdateUIRotation();
  }
  onPreviewOrientationChanged({
    nativeEvent: {
      previewOrientation
    }
  }) {
    this.rotationHelper.previewOrientation = previewOrientation;
    this.props.onPreviewOrientationChanged?.(previewOrientation);
    this.maybeUpdateUIRotation();
    if (isSkiaFrameProcessor(this.props.frameProcessor)) {
      // If we have a Skia Frame Processor, we need to update it's orientation so it knows how to render.
      this.props.frameProcessor.previewOrientation.value = previewOrientation;
    }
  }
  maybeUpdateUIRotation() {
    const uiRotation = this.rotationHelper.uiRotation;
    if (uiRotation !== this.lastUIRotation) {
      this.props.onUIRotationChanged?.(uiRotation);
      this.lastUIRotation = uiRotation;
    }
  }
  //#endregion
  onCodeScanned(event) {
    const codeScanner = this.props.codeScanner;
    if (codeScanner == null) return;
    codeScanner.onCodeScanned(event.nativeEvent.codes, event.nativeEvent.frame);
  }
  //#region Lifecycle
  setFrameProcessor(frameProcessor) {
    VisionCameraProxy.setFrameProcessor(this.handle, frameProcessor);
  }
  unsetFrameProcessor() {
    VisionCameraProxy.removeFrameProcessor(this.handle);
  }
  onViewReady() {
    this.isNativeViewMounted = true;
    if (this.props.frameProcessor != null) {
      // user passed a `frameProcessor` but we didn't set it yet because the native view was not mounted yet. set it now.
      this.setFrameProcessor(this.props.frameProcessor.frameProcessor);
      this.lastFrameProcessor = this.props.frameProcessor.frameProcessor;
    }
  }
  onAverageFpsChanged({
    nativeEvent: {
      averageFps
    }
  }) {
    this.setState(state => {
      const averageFpsSamples = [...state.averageFpsSamples, averageFps];
      while (averageFpsSamples.length >= MAX_BARS + 1) {
        // we keep a maximum of 30 FPS samples in our history
        averageFpsSamples.shift();
      }
      return {
        ...state,
        averageFpsSamples: averageFpsSamples
      };
    });
  }
  /** @internal */
  componentDidUpdate() {
    if (!this.isNativeViewMounted) return;
    const frameProcessor = this.props.frameProcessor;
    if (frameProcessor?.frameProcessor !== this.lastFrameProcessor) {
      // frameProcessor argument identity changed. Update native to reflect the change.
      if (frameProcessor != null) this.setFrameProcessor(frameProcessor.frameProcessor);else this.unsetFrameProcessor();
      this.lastFrameProcessor = frameProcessor?.frameProcessor;
    }
  }
  //#endregion
  /** @internal */
  render() {
    // We remove the big `device` object from the props because we only need to pass `cameraId` to native.
    const {
      device,
      frameProcessor,
      codeScanner,
      enableFpsGraph,
      fps,
      videoBitRate,
      ...props
    } = this.props;
    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
    if (device == null) {
      throw new CameraRuntimeError('device/no-device', 'Camera: `device` is null! Select a valid Camera device. See: https://mrousavy.com/react-native-vision-camera/docs/guides/devices');
    }
    const shouldEnableBufferCompression = props.video === true && frameProcessor == null;
    const torch = this.state.isRecordingWithFlash ? 'on' : props.torch;
    const isRenderingWithSkia = isSkiaFrameProcessor(frameProcessor);
    const shouldBeMirrored = device.position === 'front';
    // minFps/maxFps is either the fixed `fps` value, or a value from the [min, max] tuple
    const minFps = fps == null ? undefined : typeof fps === 'number' ? fps : fps[0];
    const maxFps = fps == null ? undefined : typeof fps === 'number' ? fps : fps[1];
    // bitrate is number (override) or string (multiplier)
    let bitRateMultiplier;
    let bitRateOverride;
    if (typeof videoBitRate === 'number') {
      // If the user passed an absolute number as a bit-rate, we just use this as a full override.
      bitRateOverride = videoBitRate;
    } else if (typeof videoBitRate === 'string' && videoBitRate !== 'normal') {
      // If the user passed 'low'/'normal'/'high', we need to apply this as a multiplier to the native bitrate instead of absolutely setting it
      bitRateMultiplier = this.getBitRateMultiplier(videoBitRate);
    }
    return /*#__PURE__*/React.createElement(NativeCameraView, _extends({}, props, {
      cameraId: device.id,
      ref: this.ref,
      torch: torch,
      minFps: minFps,
      maxFps: maxFps,
      isMirrored: props.isMirrored ?? shouldBeMirrored,
      onViewReady: this.onViewReady,
      onAverageFpsChanged: enableFpsGraph ? this.onAverageFpsChanged : undefined,
      onInitialized: this.onInitialized,
      onCodeScanned: this.onCodeScanned,
      onStarted: this.onStarted,
      onStopped: this.onStopped,
      onPreviewStarted: this.onPreviewStarted,
      onPreviewStopped: this.onPreviewStopped,
      onShutter: this.onShutter,
      videoBitRateMultiplier: bitRateMultiplier,
      videoBitRateOverride: bitRateOverride,
      onOutputOrientationChanged: this.onOutputOrientationChanged,
      onPreviewOrientationChanged: this.onPreviewOrientationChanged,
      onError: this.onError,
      codeScannerOptions: codeScanner,
      enableFrameProcessor: frameProcessor != null,
      enableBufferCompression: props.enableBufferCompression ?? shouldEnableBufferCompression,
      preview: isRenderingWithSkia ? false : props.preview ?? true
    }), isRenderingWithSkia && /*#__PURE__*/React.createElement(SkiaCameraCanvas, {
      style: styles.customPreviewView,
      offscreenTextures: frameProcessor.offscreenTextures,
      resizeMode: props.resizeMode
    }), enableFpsGraph && /*#__PURE__*/React.createElement(FpsGraph, {
      style: styles.fpsGraph,
      averageFpsSamples: this.state.averageFpsSamples,
      targetMaxFps: props.format?.maxFps ?? 60
    }));
  }
}
//#endregion
const styles = StyleSheet.create({
  customPreviewView: {
    flex: 1
  },
  fpsGraph: {
    elevation: 1,
    position: 'absolute',
    left: 15,
    top: 30
  }
});
//# sourceMappingURL=Camera.js.map