Added maxPhotoQualityPrioritization for iOS

Allows a massive capture speed-up of 60-80%
This commit is contained in:
Seph Soliman 2024-11-14 13:32:23 -08:00
parent ba5f5d151b
commit ace123eefa
13 changed files with 112 additions and 28 deletions

View File

@ -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** |

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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;