Skip to content

Commit

Permalink
Merge branch 'master' into @wolewicki/add-new-arch
Browse files Browse the repository at this point in the history
  • Loading branch information
scarlac authored Nov 14, 2024
2 parents 09578bd + ace123e commit 2376770
Show file tree
Hide file tree
Showing 14 changed files with 112 additions and 28 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ Additionally, the Camera can be used for barcode scanning
| `resetFocusWhenMotionDetected` | Boolean | Dismiss tap to focus when focus area content changes. Native iOS feature, see documentation: https://developer.apple.com/documentation/avfoundation/avcapturedevice/1624644-subjectareachangemonitoringenabl?language=objc). Default `true`. |
| `resizeMode` | `'cover' / 'contain'` | Determines the scaling and cropping behavior of content within the view. `cover` (resizeAspectFill on iOS) scales the content to fill the view completely, potentially cropping content if its aspect ratio differs from the view. `contain` (resizeAspect on iOS) scales the content to fit within the view's bounds without cropping, ensuring all content is visible but may introduce letterboxing. Default behavior depends on the specific use case. |
| `scanThrottleDelay` | `number` | Duration between scan detection in milliseconds. Default 2000 (2s) |
| `maxPhotoQualityPrioritization` | `'balanced'` / `'quality'` / `'speed'` | [iOS 13 and newer](https://developer.apple.com/documentation/avfoundation/avcapturephotooutput/3182995-maxphotoqualityprioritization). `'speed'` provides a 60-80% median capture time reduction vs 'quality' setting. Tested on iPhone 6S Max (66% faster) and iPhone 15 Pro Max (76% faster!). Default `balanced` |
| `onCaptureButtonPressIn` | Function | Callback when iPhone capture button is pressed in. Ex: `onCaptureButtonPressIn={() => console.log("volume button pressed in")}` |
| `onCaptureButtonPressOut` | Function | Callback when iPhone capture button is released. Ex: `onCaptureButtonPressOut={() => console.log("volume button released")}` |
| **Barcode only** |
Expand Down
2 changes: 1 addition & 1 deletion ReactNativeCameraKit.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
Pod::Spec.new do |s|
s.name = "ReactNativeCameraKit"
s.version = package["version"]
s.summary = "Advanced native camera and gallery controls and device photos API"
s.summary = "A high performance, easy to use camera API"
s.license = "MIT"

s.authors = "CameraKit"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
3 changes: 2 additions & 1 deletion example/ios/CameraKitExample/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<!-- Do not change NSAllowsArbitraryLoads to true, or you will risk app rejection! -->
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
Expand All @@ -50,6 +49,8 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
Expand Down
60 changes: 39 additions & 21 deletions example/src/CameraExample.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useRef } from 'react';
import { StyleSheet, Text, View, TouchableOpacity, Image, SafeAreaView, Animated, StatusBar } from 'react-native';
import { StyleSheet, Text, View, TouchableOpacity, Image, SafeAreaView, Animated, StatusBar, ScrollView } from 'react-native';
import Camera from '../../src/Camera';
import { CameraApi, CameraType, CaptureData } from '../../src/types';
import { Orientation } from '../../src';
Expand All @@ -25,6 +25,12 @@ const flashArray = [
},
] as const;

function median(values: number[]): number {
values = [...values].sort((a, b) => a - b);
const half = Math.floor(values.length / 2);
return values.length % 2 ? values[half] : (values[half - 1] + values[half]) / 2;
}

const CameraExample = ({ onBack }: { onBack: () => void }) => {
const cameraRef = useRef<CameraApi>(null);
const [currentFlashArrayPosition, setCurrentFlashArrayPosition] = useState(0);
Expand Down Expand Up @@ -79,25 +85,31 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {
};

const onCaptureImagePressed = async () => {
if (showImageUri) {
setShowImageUri('');
return;
}
if (!cameraRef.current || isCapturing.current) return;
let image: CaptureData | undefined;
try {
isCapturing.current = true;
image = await cameraRef.current.capture();
} catch (e) {
console.log('error', e);
} finally {
isCapturing.current = false;
}
if (!image) return;
const times: number[] = [];
for (let i = 1; i <= 5; i++) {
const start = Date.now();
if (showImageUri) {
setShowImageUri('');
return;
}
if (!cameraRef.current || isCapturing.current) return;
let image: CaptureData | undefined;
try {
isCapturing.current = true;
image = await cameraRef.current.capture();
} catch (e) {
console.log('error', e);
} finally {
isCapturing.current = false;
}
if (!image) return;

setCaptured(true);
setCaptureImages([...captureImages, image]);
console.log('image', image);
setCaptured(true);
setCaptureImages(prev => [...prev, image]);
console.log('image', image);
times.push(Date.now() - start);
}
console.log(`median capture time: ${median(times)}ms`);
};

function CaptureButton({ onPress, children }: { onPress: () => void; children?: React.ReactNode }) {
Expand Down Expand Up @@ -202,7 +214,12 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {

<View style={styles.cameraContainer}>
{showImageUri ? (
<Image source={{ uri: showImageUri }} style={styles.cameraPreview} resizeMode="contain" />
<ScrollView
maximumZoomScale={10}
contentContainerStyle={{ flexGrow: 1 }}
>
<Image source={{ uri: showImageUri }} style={styles.cameraPreview} />
</ScrollView>
) : (
<Camera
ref={cameraRef}
Expand All @@ -219,6 +236,7 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {
}}
torchMode={torchMode ? 'on' : 'off'}
shutterPhotoSound
maxPhotoQualityPrioritization="quality"
onCaptureButtonPressIn={() => {
console.log('capture button pressed in');
}}
Expand Down Expand Up @@ -322,8 +340,8 @@ const styles = StyleSheet.create({
flex: 1,
},
cameraPreview: {
flex: 1,
width: '100%',
height: '100%',
},
bottomButtons: {
margin: 10,
Expand Down
1 change: 1 addition & 0 deletions ios/ReactNativeCameraKit/CKCameraManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ @interface RCT_EXTERN_MODULE(CKCameraManager, RCTViewManager)

RCT_EXPORT_VIEW_PROPERTY(cameraType, CKCameraType)
RCT_EXPORT_VIEW_PROPERTY(flashMode, CKFlashMode)
RCT_EXPORT_VIEW_PROPERTY(maxPhotoQualityPrioritization, CKMaxPhotoQualityPrioritization)
RCT_EXPORT_VIEW_PROPERTY(torchMode, CKTorchMode)
RCT_EXPORT_VIEW_PROPERTY(ratioOverlay, NSString)
RCT_EXPORT_VIEW_PROPERTY(ratioOverlayColor, UIColor)
Expand Down
6 changes: 6 additions & 0 deletions ios/ReactNativeCameraKit/CKTypes+RCTConvert.m
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ @implementation RCTConvert (CKTypes)
@"auto": @(CKFlashModeAuto)
}), CKFlashModeAuto, integerValue)

RCT_ENUM_CONVERTER(CKMaxPhotoQualityPrioritization, (@{
@"balanced": @(CKMaxPhotoQualityPrioritizationBalanced),
@"quality": @(CKMaxPhotoQualityPrioritizationQuality),
@"speed": @(CKMaxPhotoQualityPrioritizationSpeed)
}), CKMaxPhotoQualityPrioritizationBalanced, integerValue)

RCT_ENUM_CONVERTER(CKTorchMode, (@{
@"on": @(CKTorchModeOn),
@"off": @(CKTorchModeOff)
Expand Down
1 change: 1 addition & 0 deletions ios/ReactNativeCameraKit/CameraProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ protocol CameraProtocol: AnyObject, FocusInterfaceViewDelegate {
func update(zoom: Double?)
func update(maxZoom: Double?)
func update(resizeMode: ResizeMode)
func update(maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization?)

func zoomPinchStart()
func zoomPinchChange(pinchScale: CGFloat)
Expand Down
4 changes: 4 additions & 0 deletions ios/ReactNativeCameraKit/CameraView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class CameraView: UIView {
@objc public var resizeMode: ResizeMode = .contain
@objc public var flashMode: FlashMode = .auto
@objc public var torchMode: TorchMode = .off
@objc public var maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization = .balanced
// ratio overlay
@objc public var ratioOverlay: String?
@objc public var ratioOverlayColor: UIColor?
Expand Down Expand Up @@ -184,6 +185,9 @@ public class CameraView: UIView {
if changedProps.contains("cameraType") || changedProps.contains("torchMode") {
camera.update(torchMode: torchMode)
}
if changedProps.contains("maxPhotoQualityPrioritization") {
camera.update(maxPhotoQualityPrioritization: maxPhotoQualityPrioritization)
}

if changedProps.contains("onOrientationChange") {
camera.update(onOrientationChange: onOrientationChange)
Expand Down
25 changes: 22 additions & 3 deletions ios/ReactNativeCameraKit/RealCamera.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
private var resizeMode: ResizeMode = .contain
private var flashMode: FlashMode = .auto
private var torchMode: TorchMode = .off
private var maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization?
private var resetFocus: (() -> Void)?
private var focusFinished: (() -> Void)?
private var onBarcodeRead: ((_ barcode: String,_ codeFormat : CodeFormat) -> Void)?
Expand Down Expand Up @@ -258,6 +259,16 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
func update(flashMode: FlashMode) {
self.flashMode = flashMode
}

func update(maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization?) {
guard maxPhotoQualityPrioritization != self.maxPhotoQualityPrioritization else { return }
if #available(iOS 13.0, *) {
self.session.beginConfiguration()
self.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization
self.photoOutput.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization?.avQualityPrioritization ?? .balanced
self.session.commitConfiguration()
}
}

func update(cameraType: CameraType) {
sessionQueue.async {
Expand Down Expand Up @@ -325,7 +336,9 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
}

let settings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
settings.isAutoStillImageStabilizationEnabled = true
if #available(iOS 13.0, *) {
settings.photoQualityPrioritization = self.photoOutput.maxPhotoQualityPrioritization
}

if self.videoDeviceInput?.device.isFlashAvailable == true {
settings.flashMode = self.flashMode.avFlashMode
Expand Down Expand Up @@ -477,7 +490,13 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
session.beginConfiguration()

session.sessionPreset = .photo


if #available(iOS 13.0, *) {
if let maxPhotoQualityPrioritization {
photoOutput.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization.avQualityPrioritization
}
}

if session.canAddInput(videoDeviceInput) {
session.addInput(videoDeviceInput)

Expand Down Expand Up @@ -510,7 +529,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega

metadataOutput.metadataObjectTypes = filteredTypes
}

session.commitConfiguration()

return .success
Expand Down
3 changes: 3 additions & 0 deletions ios/ReactNativeCameraKit/SimulatorCamera.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ class SimulatorCamera: CameraProtocol {
self.mockPreview.torchModeLabel.text = "Torch mode: \(torchMode)"
}
}

func update(maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization?) {
}

func update(flashMode: FlashMode) {
DispatchQueue.main.async {
Expand Down
23 changes: 23 additions & 0 deletions ios/ReactNativeCameraKit/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,29 @@ public enum FlashMode: Int, CustomStringConvertible {
}
}

@objc(CKMaxPhotoQualityPrioritization)
public enum MaxPhotoQualityPrioritization: Int, CustomStringConvertible {
case speed
case balanced
case quality

var avQualityPrioritization: AVCapturePhotoOutput.QualityPrioritization {
switch self {
case .speed: return .speed
case .balanced: return .balanced
case .quality: return .quality
}
}

public var description: String {
switch self {
case .speed: return "speed"
case .balanced: return "balanced"
case .quality: return "quality"
}
}
}

@objc(CKTorchMode)
public enum TorchMode: Int, CustomStringConvertible {
case on
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"version": "14.0.0-beta15",
"version": "14.0.0",
"description": "A high performance, fully featured, rock solid camera library for React Native applications",
"nativePackage": true,
"scripts": {
Expand Down
4 changes: 3 additions & 1 deletion src/CameraProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type OnReadCodeData = {

export type OnOrientationChangeData = {
nativeEvent: {
orientation: typeof Orientation;
orientation: typeof Orientation[keyof typeof Orientation];
};
};

Expand Down Expand Up @@ -106,6 +106,8 @@ export interface CameraProps extends ViewProps {
resizeMode?: ResizeMode;
/** **iOS Only**. Throttle how often the barcode scanner triggers a new scan */
scanThrottleDelay?: number;
/** **iOS Only**. 'speed' provides 60-80% faster image capturing */
maxPhotoQualityPrioritization?: 'balanced' | 'quality' | 'speed';
/** **Android only**. Play a shutter capture sound when capturing a photo */
shutterPhotoSound?: boolean;
onCaptureButtonPressIn?: ({ nativeEvent: {} }) => void;
Expand Down

0 comments on commit 2376770

Please sign in to comment.