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:
parent
3169444697
commit
2d96381b3e
@ -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);
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user