feat: New JS API for useCameraDevice and useCameraFormat and much faster getAvailableCameraDevices() (#1784)

* Update podfile

* Update useCameraFormat.ts

* Update API

* Delete FormatFilter.md

* Format CameraViewManager.m ObjC style

* Make `getAvailableCameraDevices` synchronous/blocking

* Create some docs

* fix: Fix HardwareLevel types

* fix: Use new device/format API

* Use 60 FPS format as an example

* Replace `Camera.getAvailableCameraDevices` with new `CameraDevices` API/Module

* Fix Lint

* KTLint options

* Use continuation indent of 8

* Use 2 spaces for indent

* Update .editorconfig

* Format code

* Update .editorconfig

* Format more

* Update VideoStabilizationMode.kt

* fix: Expose `CameraDevicesManager` to ObjC

* Update CameraPage.tsx

* fix: `requiresMainQueueSetup() -> false`

* Always prefer higher resolution

* Update CameraDevicesManager.swift

* Update CameraPage.tsx

* Also filter pixelFormat

* fix: Add AVFoundation import
This commit is contained in:
Marc Rousavy
2023-09-21 11:20:33 +02:00
committed by GitHub
parent 9eed89aac6
commit 977b859e46
61 changed files with 1110 additions and 815 deletions

View File

@@ -0,0 +1,43 @@
export interface Filter<T> {
/**
* The target value for this specific requirement
*/
target: T;
/**
* The priority of this requirement.
* Filters with higher priority can take precedence over filters with lower priority.
*
* For example, if we have two formats:
* ```json
* [
* videoWidth: 3840,
* videoHeight: 2160,
* maxFps: 30,
* ...
* ],
* [
* videoWidth: 1920,
* videoHeight: 1080,
* maxFps: 60,
* ...
* ]
* ```
* And your filter looks like this:
* ```json
* {
* fps: { target: 60, priority: 1 }
* videoSize: { target: { width: 4000, height: 2000 }, priority: 3 }
* }
* ```
* The 4k format will be chosen since the `videoSize` filter has a higher priority (2) than the `fps` filter (1).
*
* To choose the 60 FPS format instead, use a higher priority for the `fps` filter:
* ```json
* {
* fps: { target: 60, priority: 2 }
* videoSize: { target: { width: 4000, height: 2000 }, priority: 1 }
* }
* ```
*/
priority: number;
}

View File

@@ -0,0 +1,62 @@
import { CameraDevice, CameraPosition, PhysicalCameraDeviceType } from '../CameraDevice';
import { CameraRuntimeError } from '../CameraError';
export interface DeviceFilter {
/**
* The desired physical devices your camera device should have.
*
* Many modern phones have multiple Camera devices on one side and can combine those physical camera devices to one logical camera device.
* For example, the iPhone 11 has two physical camera devices, the `ultra-wide-angle-camera` ("fish-eye") and the normal `wide-angle-camera`. You can either use one of those devices individually, or use a combined logical camera device which can smoothly switch over between the two physical cameras depending on the current `zoom` level.
* When the user is at 0.5x-1x zoom, the `ultra-wide-angle-camera` can be used to offer a fish-eye zoom-out effect, and anything above 1x will smoothly switch over to the `wide-angle-camera`.
*
* **Note:** Devices with less phyiscal devices (`['wide-angle-camera']`) are usually faster to start-up than more complex
* devices (`['ultra-wide-angle-camera', 'wide-angle-camera', 'telephoto-camera']`), but don't offer zoom switch-over capabilities.
*
* @example
* ```ts
* // This device is simpler, so it starts up faster.
* getCameraDevice({ physicalDevices: ['wide-angle-camera'] })
* // This device is more complex, so it starts up slower, but you can switch between devices on 0.5x, 1x and 2x zoom.
* getCameraDevice({ physicalDevices: ['ultra-wide-angle-camera', 'wide-angle-camera', 'telephoto-camera'] })
* ```
*/
physicalDevices?: PhysicalCameraDeviceType[];
}
/**
* Get the best matching Camera device that satisfies your requirements using a sorting filter.
* @param devices All available Camera Devices this function will use for filtering. To get devices, use `Camera.getAvailableCameraDevices()`.
* @param filter The filter you want to use. The device that matches your filter the closest will be returned.
* @returns The device that matches your filter the closest.
*/
export function getCameraDevice(devices: CameraDevice[], position: CameraPosition, filter: DeviceFilter = {}): CameraDevice {
const filtered = devices.filter((d) => d.position === position);
const sortedDevices = filtered.sort((left, right) => {
let leftPoints = 0;
let rightPoints = 0;
// prefer higher hardware-level
if (left.hardwareLevel === 'full') leftPoints += 4;
if (right.hardwareLevel === 'full') rightPoints += 4;
// compare devices. two possible scenarios:
// 1. user wants all cameras ([ultra-wide, wide, tele]) to zoom. prefer those devices that have all 3 cameras.
// 2. user wants only one ([wide]) for faster performance. prefer those devices that only have one camera, if they have more, we rank them lower.
if (filter.physicalDevices != null) {
for (const device of left.devices) {
if (filter.physicalDevices.includes(device)) leftPoints += 1;
else leftPoints -= 1;
}
for (const device of right.devices) {
if (filter.physicalDevices.includes(device)) rightPoints += 1;
else rightPoints -= 1;
}
}
return leftPoints - rightPoints;
});
const device = sortedDevices[0];
if (device == null) throw new CameraRuntimeError('device/invalid-device', 'No Camera Device could be found!');
return device;
}

View File

@@ -0,0 +1,153 @@
import type { CameraDevice, CameraDeviceFormat, VideoStabilizationMode } from '../CameraDevice';
import { CameraRuntimeError } from '../CameraError';
import { PixelFormat } from '../PixelFormat';
import { Filter } from './Filter';
interface Size {
width: number;
height: number;
}
export interface FormatFilter {
/**
* The target resolution of the video (and frame processor) output pipeline.
* If no format supports the given resolution, the format closest to this value will be used.
*/
videoResolution?: Filter<Size>;
/**
* The target resolution of the photo output pipeline.
* If no format supports the given resolution, the format closest to this value will be used.
*/
photoResolution?: Filter<Size>;
/**
* The target aspect ratio of the video (and preview) output, expressed as a factor: `width / height`.
*
* In most cases, you want this to be as close to the screen's aspect ratio as possible (usually ~9:16).
*
* @example
* ```ts
* const screen = Dimensions.get('screen')
* targetVideoAspectRatio: screen.width / screen.height
* ```
*/
videoAspectRatio?: Filter<number>;
/**
* The target aspect ratio of the photo output, expressed as a factor: `width / height`.
*
* In most cases, you want this to be the same as `targetVideoAspectRatio`, which you often want
* to be as close to the screen's aspect ratio as possible (usually ~9:16)
*
* @example
* ```ts
* const screen = Dimensions.get('screen')
* targetPhotoAspectRatio: screen.width / screen.height
* ```
*/
photoAspectRatio?: Filter<number>;
/**
* The target FPS you want to record video at.
* If the FPS requirements can not be met, the format closest to this value will be used.
*/
fps?: Filter<number>;
/**
* The target video stabilization mode you want to use.
* If no format supports the target video stabilization mode, the best other matching format will be used.
*/
videoStabilizationMode?: Filter<VideoStabilizationMode>;
/**
* The target pixel format you want to use.
* If no format supports the target pixel format, the best other matching format will be used.
*/
pixelFormat?: Filter<PixelFormat>;
}
/**
* Get the best matching Camera format for the given device that satisfies your requirements using a sorting filter. By default, formats are sorted by highest to lowest resolution.
* @param device The Camera Device you're currently using
* @param filter The filter you want to use. The format that matches your filter the closest will be returned
* @returns The format that matches your filter the closest.
*/
export function getCameraFormat(device: CameraDevice, filter: FormatFilter): CameraDeviceFormat {
const copy = [...device.formats];
const sortedFormats = copy.sort((left, right) => {
let leftPoints = 0;
let rightPoints = 0;
const leftVideoResolution = left.videoWidth * left.videoHeight;
const rightVideoResolution = right.videoWidth * right.videoHeight;
if (filter.videoResolution != null) {
// Find video resolution closest to the filter (ignoring orientation)
const targetResolution = filter.videoResolution.target.width * filter.videoResolution.target.height;
const leftDiff = Math.abs(leftVideoResolution - targetResolution);
const rightDiff = Math.abs(rightVideoResolution - targetResolution);
if (leftDiff < rightDiff) leftPoints += filter.videoResolution.priority;
else if (rightDiff < leftDiff) rightPoints += filter.videoResolution.priority;
} else {
// No filter is set, so just prefer higher resolutions
if (leftVideoResolution > rightVideoResolution) leftPoints++;
else if (rightVideoResolution > leftVideoResolution) rightPoints++;
}
const leftPhotoResolution = left.photoWidth * left.photoHeight;
const rightPhotoResolution = right.photoWidth * right.photoHeight;
if (filter.photoResolution != null) {
// Find closest photo resolution to the filter (ignoring orientation)
const targetResolution = filter.photoResolution.target.width * filter.photoResolution.target.height;
const leftDiff = Math.abs(leftPhotoResolution - targetResolution);
const rightDiff = Math.abs(rightPhotoResolution - targetResolution);
if (leftDiff < rightDiff) leftPoints += filter.photoResolution.priority;
else if (rightDiff < leftDiff) rightPoints += filter.photoResolution.priority;
} else {
// No filter is set, so just prefer higher resolutions
if (leftPhotoResolution > rightPhotoResolution) leftPoints++;
else if (rightPhotoResolution > leftPhotoResolution) rightPoints++;
}
// Find closest aspect ratio (video)
if (filter.videoAspectRatio != null) {
const leftAspect = left.videoWidth / right.videoHeight;
const rightAspect = right.videoWidth / right.videoHeight;
const leftDiff = Math.abs(leftAspect - filter.videoAspectRatio.target);
const rightDiff = Math.abs(rightAspect - filter.videoAspectRatio.target);
if (leftDiff < rightDiff) leftPoints += filter.videoAspectRatio.priority;
else if (rightDiff < leftDiff) rightPoints += filter.videoAspectRatio.priority;
}
// Find closest aspect ratio (photo)
if (filter.photoAspectRatio != null) {
const leftAspect = left.photoWidth / right.photoHeight;
const rightAspect = right.photoWidth / right.photoHeight;
const leftDiff = Math.abs(leftAspect - filter.photoAspectRatio.target);
const rightDiff = Math.abs(rightAspect - filter.photoAspectRatio.target);
if (leftDiff < rightDiff) leftPoints += filter.photoAspectRatio.priority;
else if (rightDiff < leftDiff) rightPoints += filter.photoAspectRatio.priority;
}
// Find closest max FPS
if (filter.fps != null) {
const leftDiff = Math.abs(left.maxFps - filter.fps.target);
const rightDiff = Math.abs(right.maxFps - filter.fps.target);
if (leftDiff < rightDiff) leftPoints += filter.fps.priority;
else if (rightDiff < leftDiff) rightPoints += filter.fps.priority;
}
// Find video stabilization mode
if (filter.videoStabilizationMode != null) {
if (left.videoStabilizationModes.includes(filter.videoStabilizationMode.target)) leftPoints++;
if (right.videoStabilizationModes.includes(filter.videoStabilizationMode.target)) rightPoints++;
}
// Find pixel format
if (filter.pixelFormat != null) {
if (left.pixelFormats.includes(filter.pixelFormat.target)) leftPoints++;
if (right.pixelFormats.includes(filter.pixelFormat.target)) rightPoints++;
}
return rightPoints - leftPoints;
});
const format = sortedFormats[0];
if (format == null)
throw new CameraRuntimeError('device/invalid-device', `The given Camera Device (${device.id}) does not have any formats!`);
return format;
}