From 2d96381b3ec5c1ea7495acf459d0c4f50d7acbc3 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Sat, 23 Sep 2023 11:24:15 +0200 Subject: [PATCH] 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 --- package/example/src/CameraPage.tsx | 12 +++---- package/src/devices/Filter.ts | 43 ------------------------ package/src/devices/getCameraFormat.ts | 46 ++++++++++++++++++++------ package/src/hooks/useCameraFormat.ts | 18 ++++++---- package/src/index.ts | 1 - 5 files changed, 53 insertions(+), 67 deletions(-) delete mode 100644 package/src/devices/Filter.ts diff --git a/package/example/src/CameraPage.tsx b/package/example/src/CameraPage.tsx index 93a501a..6680e03 100644 --- a/package/example/src/CameraPage.tsx +++ b/package/example/src/CameraPage.tsx @@ -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); diff --git a/package/src/devices/Filter.ts b/package/src/devices/Filter.ts deleted file mode 100644 index 4e312cb..0000000 --- a/package/src/devices/Filter.ts +++ /dev/null @@ -1,43 +0,0 @@ -export interface Filter { - /** - * 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; -} diff --git a/package/src/devices/getCameraFormat.ts b/package/src/devices/getCameraFormat.ts index 07779c8..fbface2 100644 --- a/package/src/devices/getCameraFormat.ts +++ b/package/src/devices/getCameraFormat.ts @@ -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; + 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; + 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; + 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; + 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; + 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; /** * 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; +} + +type FilterWithPriority = { + target: Exclude; + priority: number; +}; +type FilterMap = { + [K in keyof FormatFilter]: FilterWithPriority; +}; +function filtersToFilterMap(filters: FormatFilter[]): FilterMap { + return filters.reduce((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; diff --git a/package/src/hooks/useCameraFormat.ts b/package/src/hooks/useCameraFormat.ts index 2284125..803e31b 100644 --- a/package/src/hooks/useCameraFormat.ts +++ b/package/src/hooks/useCameraFormat.ts @@ -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; } diff --git a/package/src/index.ts b/package/src/index.ts index 9714818..6548119 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -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';