Squash format-filter
This commit is contained in:
@@ -1,21 +1,64 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CameraRuntimeError } from 'src/CameraError';
|
||||
import { sortDevices } from 'src/utils/FormatFilter';
|
||||
import { Camera } from '../Camera';
|
||||
import { CameraDevice, LogicalCameraDeviceType, parsePhysicalDeviceTypes, PhysicalCameraDeviceType } from '../CameraDevice';
|
||||
|
||||
export const useCameraDevice = (deviceType: PhysicalCameraDeviceType | LogicalCameraDeviceType): CameraDevice | undefined => {
|
||||
/**
|
||||
* Gets the best available `CameraDevice`. Devices with more cameras are preferred.
|
||||
*
|
||||
* @returns A `CameraDevice` for the requested device type.
|
||||
* @throws `CameraRuntimeError` if no device was found.
|
||||
* @example
|
||||
* const device = useCameraDevice('wide-angle-camera')
|
||||
* // ...
|
||||
* return <Camera device={device} />
|
||||
*/
|
||||
export function useCameraDevice(): CameraDevice;
|
||||
|
||||
/**
|
||||
* Gets a `CameraDevice` for the requested device type.
|
||||
*
|
||||
* @returns A `CameraDevice` for the requested device type, or `undefined` if no matching device was found
|
||||
*
|
||||
* @example
|
||||
* const device = useCameraDevice('wide-angle-camera')
|
||||
* // ...
|
||||
* return <Camera device={device} />
|
||||
*/
|
||||
export function useCameraDevice(deviceType: PhysicalCameraDeviceType | LogicalCameraDeviceType): CameraDevice | undefined;
|
||||
|
||||
export function useCameraDevice(deviceType?: PhysicalCameraDeviceType | LogicalCameraDeviceType): CameraDevice | undefined {
|
||||
const [device, setDevice] = useState<CameraDevice>();
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const loadDevice = async (): Promise<void> => {
|
||||
const devices = await Camera.getAvailableCameraDevices();
|
||||
const bestMatch = devices.find((d) => {
|
||||
const parsedType = parsePhysicalDeviceTypes(d.devices);
|
||||
return parsedType === deviceType;
|
||||
});
|
||||
setDevice(bestMatch);
|
||||
if (!isMounted) return;
|
||||
|
||||
if (deviceType == null) {
|
||||
// use any device
|
||||
const sorted = devices.sort(sortDevices);
|
||||
const bestMatch = sorted[0];
|
||||
if (bestMatch == null) throw new CameraRuntimeError('device/no-device', 'No Camera device was found!');
|
||||
setDevice(bestMatch);
|
||||
} else {
|
||||
// use specified device (type)
|
||||
const bestMatch = devices.find((d) => {
|
||||
const parsedType = parsePhysicalDeviceTypes(d.devices);
|
||||
return parsedType === deviceType;
|
||||
});
|
||||
setDevice(bestMatch);
|
||||
}
|
||||
};
|
||||
loadDevice();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [deviceType]);
|
||||
|
||||
return device;
|
||||
};
|
||||
}
|
||||
|
24
src/hooks/useCameraFormat.ts
Normal file
24
src/hooks/useCameraFormat.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { CameraDevice, CameraDeviceFormat } from 'src/CameraDevice';
|
||||
import { filterFormatsByAspectRatio, sortFormatsByResolution } from 'src/utils/FormatFilter';
|
||||
import type { Size } from 'src/utils/FormatFilter';
|
||||
|
||||
/**
|
||||
* Returns the best format for the given camera device.
|
||||
*
|
||||
* This function tries to choose a format with the highest possible photo-capture resolution and best matching aspect ratio.
|
||||
*
|
||||
* @param device The Camera Device
|
||||
* @param cameraViewSize The Camera View's size. This can be an approximation and **must be memoized**! Default: `SCREEN_SIZE`
|
||||
*
|
||||
* @returns The best matching format for the given camera device, or `undefined` if the camera device is `undefined`.
|
||||
*/
|
||||
export function useCameraFormat(device?: CameraDevice, cameraViewSize?: Size): CameraDeviceFormat | undefined {
|
||||
const formats = useMemo(() => {
|
||||
if (device?.formats == null) return [];
|
||||
const filtered = filterFormatsByAspectRatio(device.formats, cameraViewSize);
|
||||
return filtered.sort(sortFormatsByResolution);
|
||||
}, [device?.formats, cameraViewSize]);
|
||||
|
||||
return formats[0];
|
||||
}
|
@@ -10,3 +10,6 @@ export * from './Point';
|
||||
export * from './Snapshot';
|
||||
export * from './TemporaryFile';
|
||||
export * from './VideoFile';
|
||||
export * from './hooks/useCameraDevice';
|
||||
export * from './hooks/useCameraFormat';
|
||||
export * from './utils/FormatFilter';
|
||||
|
109
src/utils/FormatFilter.ts
Normal file
109
src/utils/FormatFilter.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Dimensions } from 'react-native';
|
||||
import type { CameraDevice, CameraDeviceFormat, FrameRateRange } from 'react-native-vision-camera';
|
||||
|
||||
/**
|
||||
* Compares two devices by the following criteria:
|
||||
* * `wide-angle-camera`s are ranked higher than others
|
||||
* * Devices with more physical cameras are ranked higher than ones with less. (e.g. "Triple Camera" > "Wide-Angle Camera")
|
||||
*
|
||||
* > Note that this makes the `sort()` function descending, so the first element (`[0]`) is the "best" device.
|
||||
*
|
||||
* @example
|
||||
* const devices = camera.devices.sort(sortDevices)
|
||||
* const bestDevice = devices[0]
|
||||
*/
|
||||
export const sortDevices = (left: CameraDevice, right: CameraDevice): number => {
|
||||
let leftPoints = 0;
|
||||
let rightPoints = 0;
|
||||
|
||||
const leftHasWideAngle = left.devices.includes('wide-angle-camera');
|
||||
const rightHasWideAngle = right.devices.includes('wide-angle-camera');
|
||||
if (leftHasWideAngle) leftPoints += 5;
|
||||
if (rightHasWideAngle) rightPoints += 5;
|
||||
|
||||
if (left.devices.length > right.devices.length) leftPoints += 3;
|
||||
if (right.devices.length > left.devices.length) rightPoints += 3;
|
||||
|
||||
return rightPoints - leftPoints;
|
||||
};
|
||||
|
||||
export type Size = { width: number; height: number };
|
||||
const SCREEN_SIZE: Size = {
|
||||
width: Dimensions.get('window').width,
|
||||
height: Dimensions.get('window').height,
|
||||
};
|
||||
const applyScaledMask = (
|
||||
clippedElementDimensions: Size, // 12 x 12
|
||||
maskDimensions: Size, // 6 x 12
|
||||
): Size => {
|
||||
const wScale = maskDimensions.width / clippedElementDimensions.width; // 0.5
|
||||
const hScale = maskDimensions.height / clippedElementDimensions.height; // 1.0
|
||||
|
||||
if (wScale > hScale) {
|
||||
return {
|
||||
width: maskDimensions.width / hScale,
|
||||
height: maskDimensions.height / hScale,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
width: maskDimensions.width / wScale,
|
||||
height: maskDimensions.height / wScale,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getFormatAspectRatioOverflow = (format: CameraDeviceFormat, size: Size): number => {
|
||||
const downscaled = applyScaledMask(
|
||||
size,
|
||||
// cameras are landscape, so we intentionally rotate
|
||||
{ width: format.photoHeight, height: format.photoWidth },
|
||||
);
|
||||
return downscaled.width * downscaled.height - size.width * size.height;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters Camera Device Formats by the best matching aspect ratio for the given `viewSize`.
|
||||
*
|
||||
* @returns A list of Camera Device Formats that match the given `viewSize`' aspect ratio _as close as possible_.
|
||||
*
|
||||
* @example
|
||||
* const formats = useMemo(() => filterFormatsByAspectRatio(device.formats, CAMERA_VIEW_SIZE), [device.formats])
|
||||
*/
|
||||
export const filterFormatsByAspectRatio = (formats: CameraDeviceFormat[], viewSize = SCREEN_SIZE): CameraDeviceFormat[] => {
|
||||
const minOverflow = formats.reduce((prev, curr) => {
|
||||
const overflow = getFormatAspectRatioOverflow(curr, viewSize);
|
||||
if (overflow < prev) return overflow;
|
||||
else return prev;
|
||||
}, Number.MAX_SAFE_INTEGER);
|
||||
|
||||
return formats.filter((f) => getFormatAspectRatioOverflow(f, viewSize) === minOverflow);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sorts Camera Device Formats by highest photo-capture resolution, descending.
|
||||
*
|
||||
* @example
|
||||
* const formats = useMemo(() => device.formats.sort(sortFormatsByResolution), [device.formats])
|
||||
* const bestFormat = formats[0]
|
||||
*/
|
||||
export const sortFormatsByResolution = (left: CameraDeviceFormat, right: CameraDeviceFormat): number => {
|
||||
let leftPoints = left.photoHeight * left.photoWidth;
|
||||
let rightPoints = right.photoHeight * right.photoWidth;
|
||||
|
||||
if (left.videoHeight != null && left.videoWidth != null && right.videoHeight != null && right.videoWidth != null) {
|
||||
leftPoints += left.videoWidth * left.videoHeight ?? 0;
|
||||
rightPoints += right.videoWidth * right.videoHeight ?? 0;
|
||||
}
|
||||
|
||||
// "returns a negative value if left is better than one"
|
||||
return rightPoints - leftPoints;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns `true` if the given Frame Rate Range (`range`) contains the given frame rate (`fps`)
|
||||
*
|
||||
* @example
|
||||
* // get all formats that support 60 FPS
|
||||
* const formatsWithHighFps = useMemo(() => device.formats.filter((f) => f.frameRateRanges.some((r) => frameRateIncluded(r, 60))), [device.formats])
|
||||
*/
|
||||
export const frameRateIncluded = (range: FrameRateRange, fps: number): boolean => fps >= range.minFrameRate && fps <= range.maxFrameRate;
|
Reference in New Issue
Block a user