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); const [enableNightMode, setEnableNightMode] = useState(false);
// camera format settings // camera format settings
const device = useCameraDevice(cameraPosition); const device = useCameraDevice(cameraPosition, {
const format = useCameraFormat(device, { physicalDevices: ['ultra-wide-angle-camera', 'wide-angle-camera', 'telephoto-camera'],
fps: {
target: 60,
priority: 1,
},
}); });
const format = useCameraFormat(device, [
{ fps: 60 }, //
]);
//#region Memos //#region Memos
const [targetFps, setTargetFps] = useState(30); const [targetFps, setTargetFps] = useState(30);
const fps = Math.min(format?.maxFps ?? 1, targetFps); 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 type { CameraDevice, CameraDeviceFormat, VideoStabilizationMode } from '../CameraDevice';
import { CameraRuntimeError } from '../CameraError'; import { CameraRuntimeError } from '../CameraError';
import { PixelFormat } from '../PixelFormat'; import { PixelFormat } from '../PixelFormat';
import { Filter } from './Filter';
interface Size { interface Size {
width: number; width: number;
@ -13,12 +12,12 @@ export interface FormatFilter {
* The target resolution of the video (and frame processor) output pipeline. * 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. * 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. * The target resolution of the photo output pipeline.
* If no format supports the given resolution, the format closest to this value will be used. * 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`. * 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 * targetVideoAspectRatio: screen.width / screen.height
* ``` * ```
*/ */
videoAspectRatio?: Filter<number>; videoAspectRatio?: number;
/** /**
* The target aspect ratio of the photo output, expressed as a factor: `width / height`. * 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 * targetPhotoAspectRatio: screen.width / screen.height
* ``` * ```
*/ */
photoAspectRatio?: Filter<number>; photoAspectRatio?: number;
/** /**
* The target FPS you want to record video at. * 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. * 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. * 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. * 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. * The target pixel format you want to use.
* If no format supports the target pixel format, the best other matching format will be used. * 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. * 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 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. * @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 copy = [...device.formats];
const sortedFormats = copy.sort((left, right) => { const sortedFormats = copy.sort((left, right) => {
let leftPoints = 0; 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. * 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 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 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. * @returns The format that matches your filter the closest.
* @example * @example
* ```ts * ```ts
* const device = useCameraDevice(...) * const device = useCameraDevice(...)
* const format = useCameraFormat(device, { * const format = useCameraFormat(device, [
* videoResolution: { target: { width: 3048, height: 2160 }, priority: 2 }, * { videoResolution: { width: 3048, height: 2160 } },
* fps: { target: 60, priority: 1 } * { 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(() => { const format = useMemo(() => {
if (device == null) return undefined; if (device == null) return undefined;
return getCameraFormat(device, filter); return getCameraFormat(device, filters);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [device, JSON.stringify(filter)]); }, [device, JSON.stringify(filters)]);
return format; return format;
} }

View File

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