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:
43
package/src/devices/Filter.ts
Normal file
43
package/src/devices/Filter.ts
Normal 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;
|
||||
}
|
62
package/src/devices/getCameraDevice.ts
Normal file
62
package/src/devices/getCameraDevice.ts
Normal 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;
|
||||
}
|
153
package/src/devices/getCameraFormat.ts
Normal file
153
package/src/devices/getCameraFormat.ts
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user