feat: New array-based useCameraFormats API (#1841)

* feat: New array-based `useCameraFormats` API

* Use triple-camera in Example app

* fix: Remove invalid export

* fix: Use constant-time lookup Filter map and only run sort once
This commit is contained in:
Marc Rousavy 2023-09-23 11:24:15 +02:00 committed by GitHub
parent 3169444697
commit 2d96381b3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 53 additions and 67 deletions

View File

@ -45,14 +45,14 @@ export function CameraPage({ navigation }: Props): React.ReactElement {
const [enableNightMode, setEnableNightMode] = useState(false);
// camera format settings
const device = useCameraDevice(cameraPosition);
const format = useCameraFormat(device, {
fps: {
target: 60,
priority: 1,
},
const device = useCameraDevice(cameraPosition, {
physicalDevices: ['ultra-wide-angle-camera', 'wide-angle-camera', 'telephoto-camera'],
});
const format = useCameraFormat(device, [
{ fps: 60 }, //
]);
//#region Memos
const [targetFps, setTargetFps] = useState(30);
const fps = Math.min(format?.maxFps ?? 1, targetFps);

View File

@ -1,43 +0,0 @@
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

@ -1,7 +1,6 @@
import type { CameraDevice, CameraDeviceFormat, VideoStabilizationMode } from '../CameraDevice';
import { CameraRuntimeError } from '../CameraError';
import { PixelFormat } from '../PixelFormat';
import { Filter } from './Filter';
interface Size {
width: number;
@ -13,12 +12,12 @@ 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>;
videoResolution?: 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>;
photoResolution?: Size;
/**
* The target aspect ratio of the video (and preview) output, expressed as a factor: `width / height`.
*
@ -30,7 +29,7 @@ export interface FormatFilter {
* targetVideoAspectRatio: screen.width / screen.height
* ```
*/
videoAspectRatio?: Filter<number>;
videoAspectRatio?: number;
/**
* The target aspect ratio of the photo output, expressed as a factor: `width / height`.
*
@ -43,31 +42,58 @@ export interface FormatFilter {
* targetPhotoAspectRatio: screen.width / screen.height
* ```
*/
photoAspectRatio?: Filter<number>;
photoAspectRatio?: 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>;
fps?: 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>;
videoStabilizationMode?: 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>;
pixelFormat?: PixelFormat;
}
type FilterWithPriority<T> = {
target: Exclude<T, null | undefined>;
priority: number;
};
type FilterMap = {
[K in keyof FormatFilter]: FilterWithPriority<FormatFilter[K]>;
};
function filtersToFilterMap(filters: FormatFilter[]): FilterMap {
return filters.reduce<FilterMap>((map, curr, index) => {
for (const key in curr) {
// @ts-expect-error keys are untyped
map[key] = {
// @ts-expect-error keys are untyped
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
target: curr[key],
priority: filters.length - index,
};
}
return map;
}, {});
}
/**
* 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
* @param filters The filter you want to use. The format that matches your filter the closest will be returned. The filter is ranked by priority, descending.
* @returns The format that matches your filter the closest.
*/
export function getCameraFormat(device: CameraDevice, filter: FormatFilter): CameraDeviceFormat {
export function getCameraFormat(device: CameraDevice, filters: FormatFilter[]): CameraDeviceFormat {
// Combine filters into a single filter map for constant-time lookup
const filter = filtersToFilterMap(filters);
// Sort list because we will pick first element
// TODO: Use reduce instead of sort?
const copy = [...device.formats];
const sortedFormats = copy.sort((left, right) => {
let leftPoints = 0;

View File

@ -4,24 +4,28 @@ import { FormatFilter, getCameraFormat } from '../devices/getCameraFormat';
/**
* 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.
*
* The {@linkcode filters | filters} are ranked by priority, from highest to lowest.
* This means the first item you pass will have a higher priority than the second, and so on.
*
* @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.
* @example
* ```ts
* const device = useCameraDevice(...)
* const format = useCameraFormat(device, {
* videoResolution: { target: { width: 3048, height: 2160 }, priority: 2 },
* fps: { target: 60, priority: 1 }
* })
* const format = useCameraFormat(device, [
* { videoResolution: { width: 3048, height: 2160 } },
* { fps: 60 }
* ])
* ```
*/
export function useCameraFormat(device: CameraDevice | undefined, filter: FormatFilter): CameraDeviceFormat | undefined {
export function useCameraFormat(device: CameraDevice | undefined, filters: FormatFilter[]): CameraDeviceFormat | undefined {
const format = useMemo(() => {
if (device == null) return undefined;
return getCameraFormat(device, filter);
return getCameraFormat(device, filters);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [device, JSON.stringify(filter)]);
}, [device, JSON.stringify(filters)]);
return format;
}

View File

@ -10,7 +10,6 @@ export * from './PixelFormat';
export * from './Point';
export * from './VideoFile';
export * from './devices/Filter';
export * from './devices/getCameraFormat';
export * from './devices/getCameraDevice';