Added maxPhotoQualityPrioritization for iOS
Allows a massive capture speed-up of 60-80%
This commit is contained in:
parent
ba5f5d151b
commit
ace123eefa
@ -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** |
|
||||
|
||||
@ -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"
|
||||
@ -13,7 +13,7 @@ Pod::Spec.new do |s|
|
||||
s.platform = :ios, "11.0"
|
||||
|
||||
s.source = { :git => "https://github.com/teslamotors/react-native-camera-kit.git", :tag => "v#{s.version}" }
|
||||
s.source_files = "ios/**/*.{h,m,swift}"
|
||||
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
||||
|
||||
s.dependency 'React-Core'
|
||||
end
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -50,6 +49,8 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
|
||||
@ -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';
|
||||
@ -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);
|
||||
@ -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 }) {
|
||||
@ -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}
|
||||
@ -219,6 +236,7 @@ const CameraExample = ({ onBack }: { onBack: () => void }) => {
|
||||
}}
|
||||
torchMode={torchMode ? 'on' : 'off'}
|
||||
shutterPhotoSound
|
||||
maxPhotoQualityPrioritization="quality"
|
||||
onCaptureButtonPressIn={() => {
|
||||
console.log('capture button pressed in');
|
||||
}}
|
||||
@ -322,8 +340,8 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
},
|
||||
cameraPreview: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
bottomButtons: {
|
||||
margin: 10,
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
|
||||
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)
|
||||
|
||||
@ -26,6 +26,12 @@ RCT_ENUM_CONVERTER(CKFlashMode, (@{
|
||||
@"auto": @(CKFlashModeAuto)
|
||||
}), CKFlashModeAuto, integerValue)
|
||||
|
||||
RCT_ENUM_CONVERTER(CKMaxPhotoQualityPrioritization, (@{
|
||||
@"balanced": @(CKMaxPhotoQualityPrioritizationBalanced),
|
||||
@"quality": @(CKMaxPhotoQualityPrioritizationQuality),
|
||||
@"speed": @(CKMaxPhotoQualityPrioritizationSpeed)
|
||||
}), CKMaxPhotoQualityPrioritizationBalanced, integerValue)
|
||||
|
||||
RCT_ENUM_CONVERTER(CKTorchMode, (@{
|
||||
@"on": @(CKTorchModeOn),
|
||||
@"off": @(CKTorchModeOff)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -37,6 +37,7 @@ class CameraView: UIView {
|
||||
@objc var resizeMode: ResizeMode = .contain
|
||||
@objc var flashMode: FlashMode = .auto
|
||||
@objc var torchMode: TorchMode = .off
|
||||
@objc var maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization = .balanced
|
||||
// ratio overlay
|
||||
@objc var ratioOverlay: String?
|
||||
@objc var ratioOverlayColor: UIColor?
|
||||
@ -183,6 +184,9 @@ 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)
|
||||
|
||||
@ -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)?
|
||||
@ -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 {
|
||||
@ -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
|
||||
@ -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)
|
||||
|
||||
@ -510,7 +529,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
||||
|
||||
metadataOutput.metadataObjectTypes = filteredTypes
|
||||
}
|
||||
|
||||
|
||||
session.commitConfiguration()
|
||||
|
||||
return .success
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -19,7 +19,7 @@ export type OnReadCodeData = {
|
||||
|
||||
export type OnOrientationChangeData = {
|
||||
nativeEvent: {
|
||||
orientation: typeof Orientation;
|
||||
orientation: typeof Orientation[keyof typeof Orientation];
|
||||
};
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user