Create media captured page
This commit is contained in:
parent
1d71aada11
commit
c13c46f388
@ -4,6 +4,7 @@ import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
|
||||
import { App } from './src/App';
|
||||
import { Settings } from './src/Settings';
|
||||
import { Splash } from './src/Splash';
|
||||
import { Media } from './src/Media';
|
||||
|
||||
Navigation.setDefaultOptions({
|
||||
topBar: {
|
||||
@ -13,6 +14,7 @@ Navigation.setDefaultOptions({
|
||||
|
||||
Navigation.registerComponent('Splash', () => gestureHandlerRootHOC(Splash), () => Splash);
|
||||
Navigation.registerComponent('Home', () => gestureHandlerRootHOC(App), () => App);
|
||||
Navigation.registerComponent('Media', () => gestureHandlerRootHOC(Media), () => Media);
|
||||
Navigation.registerComponent('Settings', () => gestureHandlerRootHOC(Settings), () => Settings);
|
||||
|
||||
Navigation.events().registerAppLaunchedListener(() => {
|
||||
|
269
example/src/FormatFilter.ts
Normal file
269
example/src/FormatFilter.ts
Normal file
@ -0,0 +1,269 @@
|
||||
import {
|
||||
RESOLUTION_LIMIT,
|
||||
SCREEN_HEIGHT,
|
||||
SCREEN_WIDTH,
|
||||
USE_ULTRAWIDE_IF_AVAILABLE,
|
||||
} from "./Constants";
|
||||
import type {
|
||||
CameraDevice,
|
||||
CameraDeviceFormat,
|
||||
FrameRateRange,
|
||||
} from "react-native-vision-camera";
|
||||
|
||||
/**
|
||||
* Compares two devices with the following criteria:
|
||||
* * Cameras with wide-angle-cameras are 5x **better** than cameras without.
|
||||
* * Cameras with ultra-wide-angle-cameras are 5x **worse** than cameras without.
|
||||
* * Cameras with more physical devices are "better"
|
||||
*
|
||||
* @returns
|
||||
* * `-1` if left is BETTER than right
|
||||
* * `0` if left equals right
|
||||
* * `1` if left is WORSE than right
|
||||
*
|
||||
* Note that this makes the `sort()` function descending, so the first element (`[0]`) is the "best" device.
|
||||
*/
|
||||
export const compareDevices = (
|
||||
left: CameraDevice,
|
||||
right: CameraDevice
|
||||
): -1 | 0 | 1 => {
|
||||
let leftPoints = 0;
|
||||
|
||||
const leftHasWideAngle = left.devices.includes("wide-angle-camera");
|
||||
const rightHasWideAngle = right.devices.includes("wide-angle-camera");
|
||||
if (leftHasWideAngle && !rightHasWideAngle) {
|
||||
// left does have a wide-angle-camera, but right doesn't.
|
||||
leftPoints += 5;
|
||||
} else if (!leftHasWideAngle && rightHasWideAngle) {
|
||||
// left doesn't have a wide-angle-camera, but right does.
|
||||
leftPoints -= 5;
|
||||
}
|
||||
|
||||
if (!USE_ULTRAWIDE_IF_AVAILABLE) {
|
||||
const leftHasUltraWideAngle = left.devices.includes(
|
||||
"ultra-wide-angle-camera"
|
||||
);
|
||||
const rightHasUltraWideAngle = right.devices.includes(
|
||||
"ultra-wide-angle-camera"
|
||||
);
|
||||
if (leftHasUltraWideAngle && !rightHasUltraWideAngle) {
|
||||
// left does have an ultra-wide-angle-camera, but right doesn't. Ultra-Wide cameras are bad because of their poor quality.
|
||||
leftPoints -= 5;
|
||||
} else if (!leftHasUltraWideAngle && rightHasUltraWideAngle) {
|
||||
// left doesn't have an ultra-wide-angle-camera, but right does. Ultra-Wide cameras are bad because of their poor quality.
|
||||
leftPoints += 5;
|
||||
}
|
||||
}
|
||||
|
||||
if (left.devices.length > right.devices.length) {
|
||||
// left has more devices than right
|
||||
leftPoints += 1;
|
||||
} else if (left.devices.length < right.devices.length) {
|
||||
// left has less more devices than right
|
||||
leftPoints -= 1;
|
||||
}
|
||||
|
||||
if (leftPoints > 0) return -1;
|
||||
if (leftPoints < 0) return 1;
|
||||
return 0;
|
||||
};
|
||||
|
||||
type Size = { width: number; height: number };
|
||||
const CAMERA_VIEW_SIZE: Size = {
|
||||
width: SCREEN_WIDTH,
|
||||
height: SCREEN_HEIGHT,
|
||||
};
|
||||
|
||||
const applyScaledMask = (
|
||||
clippedElementDimensions: Size, // 3024 x 4032 | 2160x3840
|
||||
maskDimensions: Size // 375 x 623
|
||||
): Size => {
|
||||
const wScale = maskDimensions.width / clippedElementDimensions.width;
|
||||
const hScale = maskDimensions.height / clippedElementDimensions.height;
|
||||
|
||||
if (wScale < hScale) {
|
||||
return {
|
||||
width: maskDimensions.width / hScale,
|
||||
height: clippedElementDimensions.height,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
width: clippedElementDimensions.width,
|
||||
height: maskDimensions.height / wScale,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Compares two Formats with the following comparators:
|
||||
* * Photo Dimensions (higher is better) (weights x3)
|
||||
* * Video Dimensions (higher is better) (weights x2)
|
||||
* * Max FPS (higher is better) (weights x2)
|
||||
* * HDR Support (true is better) (weights x2)
|
||||
* * Max Zoom Factor (higher is better) (weights x1)
|
||||
* * MaxISO (higher is better) (weights x1)
|
||||
* * MinISO (lower is better) (weights x1)
|
||||
*
|
||||
* @returns
|
||||
* * `-1` if left is BETTER than right
|
||||
* * `0` if left equals right
|
||||
* * `1` if left is WORSE than right
|
||||
*
|
||||
* Note that this makes the `sort()` function descending, so the first element (`[0]`) is the "best" format.
|
||||
*/
|
||||
export const compareFormats = (
|
||||
left: CameraDeviceFormat,
|
||||
right: CameraDeviceFormat
|
||||
): -1 | 0 | 1 => {
|
||||
// Point score of the left format. Higher is better.
|
||||
let leftPoints = 0;
|
||||
|
||||
const leftPhotoPixels = left.photoHeight * left.photoWidth;
|
||||
const rightPhotoPixels = right.photoHeight * right.photoWidth;
|
||||
if (leftPhotoPixels > rightPhotoPixels) {
|
||||
// left has greater photo dimensions
|
||||
const isLeftAbovePixelLimit =
|
||||
RESOLUTION_LIMIT != null && leftPhotoPixels > RESOLUTION_LIMIT;
|
||||
if (isLeftAbovePixelLimit) {
|
||||
// left exceeds our pixel limit
|
||||
leftPoints -= 3;
|
||||
} else {
|
||||
// left does not exceed our pixel limit
|
||||
leftPoints += 3;
|
||||
}
|
||||
} else if (leftPhotoPixels < rightPhotoPixels) {
|
||||
// left has smaller photo dimensions
|
||||
const isRightAbovePixelLimit =
|
||||
RESOLUTION_LIMIT != null && rightPhotoPixels > RESOLUTION_LIMIT;
|
||||
if (isRightAbovePixelLimit) {
|
||||
// right exceeds our pixel limit
|
||||
leftPoints += 3;
|
||||
} else {
|
||||
// right does not exceed our pixel limit
|
||||
leftPoints -= 3;
|
||||
}
|
||||
}
|
||||
|
||||
const leftCropped = applyScaledMask(
|
||||
{ width: left.photoHeight, height: left.photoWidth }, // cameras are horizontal, we rotate to portrait
|
||||
CAMERA_VIEW_SIZE
|
||||
);
|
||||
const rightCropped = applyScaledMask(
|
||||
{ width: right.photoHeight, height: right.photoWidth }, // cameras are horizontal, we rotate to portrait
|
||||
CAMERA_VIEW_SIZE
|
||||
);
|
||||
const leftOverflow =
|
||||
left.photoWidth * left.photoHeight - leftCropped.width * leftCropped.height;
|
||||
const rightOverflow =
|
||||
right.photoWidth * right.photoHeight -
|
||||
rightCropped.width * rightCropped.height;
|
||||
if (leftOverflow > rightOverflow) {
|
||||
// left has a higher overflow, aka more pixels that aren't on-screen and therefore wasted. Maybe left is 4:3 and right is 16:9
|
||||
leftPoints -= 4;
|
||||
} else if (leftOverflow < rightOverflow) {
|
||||
// right has a higher overflow, aka more pixels that aren't on-screen and therefore wasted. Maybe right is 4:3 and left is 16:9
|
||||
leftPoints += 4;
|
||||
}
|
||||
|
||||
if (
|
||||
left.videoHeight != null &&
|
||||
left.videoWidth != null &&
|
||||
right.videoHeight != null &&
|
||||
right.videoWidth != null
|
||||
) {
|
||||
const leftVideoPixels = left.videoWidth * left.videoHeight ?? 0;
|
||||
const rightVideoPixels = right.videoWidth * right.videoHeight ?? 0;
|
||||
if (leftVideoPixels > rightVideoPixels) {
|
||||
// left has greater video dimensions
|
||||
leftPoints += 2;
|
||||
} else if (leftVideoPixels < rightVideoPixels) {
|
||||
// left has smaller video dimensions
|
||||
leftPoints -= 2;
|
||||
}
|
||||
}
|
||||
|
||||
const leftMaxFps = Math.max(
|
||||
...left.frameRateRanges.map((r) => r.maxFrameRate)
|
||||
);
|
||||
const rightMaxFps = Math.max(
|
||||
...right.frameRateRanges.map((r) => r.maxFrameRate)
|
||||
);
|
||||
if (leftMaxFps > rightMaxFps) {
|
||||
// left has more fps
|
||||
leftPoints += 2;
|
||||
} else if (leftMaxFps < rightMaxFps) {
|
||||
// left has less fps
|
||||
leftPoints -= 2;
|
||||
}
|
||||
|
||||
if (left.supportsVideoHDR && !right.supportsVideoHDR) {
|
||||
// left does support video HDR, right doesn't
|
||||
leftPoints += 1;
|
||||
} else if (!left.supportsVideoHDR && right.supportsVideoHDR) {
|
||||
// left doesn't support video HDR, right does
|
||||
leftPoints -= 1;
|
||||
}
|
||||
|
||||
if (left.supportsPhotoHDR && !right.supportsPhotoHDR) {
|
||||
// left does support photo HDR, right doesn't
|
||||
leftPoints += 1;
|
||||
} else if (!left.supportsPhotoHDR && right.supportsPhotoHDR) {
|
||||
// left doesn't support photo HDR, right does
|
||||
leftPoints -= 1;
|
||||
}
|
||||
|
||||
if (leftPoints > 0) return -1;
|
||||
if (leftPoints < 0) return 1;
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Selects the smallest difference between a FrameRateRange's `maxFrameRate` and the given `fps`
|
||||
*/
|
||||
const smallestFpsDiff = (
|
||||
frameRateRanges: FrameRateRange[],
|
||||
fps: number
|
||||
): number => {
|
||||
const bestFrameRateRange = frameRateRanges.reduce<FrameRateRange | undefined>(
|
||||
(prev, curr) => {
|
||||
if (prev == null) return curr;
|
||||
|
||||
const prevDiff = Math.abs(prev.maxFrameRate - fps);
|
||||
const currDiff = Math.abs(curr.maxFrameRate - fps);
|
||||
if (prevDiff < currDiff) return prev;
|
||||
else return curr;
|
||||
},
|
||||
undefined
|
||||
);
|
||||
const max = bestFrameRateRange?.maxFrameRate ?? 0;
|
||||
return Math.abs(max - fps);
|
||||
};
|
||||
|
||||
export const frameRateIncluded = (
|
||||
range: FrameRateRange,
|
||||
fps: number
|
||||
): boolean => fps >= range.minFrameRate && fps <= range.maxFrameRate;
|
||||
|
||||
const isFpsInFrameRateRange = (
|
||||
format: CameraDeviceFormat,
|
||||
fps: number
|
||||
): boolean => format.frameRateRanges.some((r) => frameRateIncluded(r, fps));
|
||||
|
||||
/**
|
||||
* Selects the format with the closest frame rate ranges to the FPS
|
||||
*/
|
||||
export const formatWithClosestMatchingFps = (
|
||||
formats: CameraDeviceFormat[],
|
||||
fps: number
|
||||
): CameraDeviceFormat | undefined =>
|
||||
formats.reduce<CameraDeviceFormat | undefined>((prev, curr) => {
|
||||
if (prev == null) return curr;
|
||||
|
||||
// if range is 3-30 and FPS is 31, it doesn't match.
|
||||
if (!isFpsInFrameRateRange(curr, fps)) return prev;
|
||||
|
||||
const prevFpsDiff = smallestFpsDiff(prev.frameRateRanges, fps);
|
||||
const currFpsDiff = smallestFpsDiff(curr.frameRateRanges, fps);
|
||||
if (currFpsDiff < prevFpsDiff) return curr;
|
||||
else return prev;
|
||||
}, undefined);
|
43
example/src/Media.tsx
Normal file
43
example/src/Media.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { StyleSheet, View, Text, Image, Pressable } from 'react-native';
|
||||
import { Navigation, NavigationFunctionComponent } from 'react-native-navigation';
|
||||
import Video from 'react-native-video';
|
||||
import { CONTENT_SPACING } from './Constants';
|
||||
|
||||
interface MediaProps {
|
||||
path: string,
|
||||
type: 'video' | 'photo'
|
||||
}
|
||||
|
||||
export const Media: NavigationFunctionComponent<MediaProps> = ({ componentId, type, path }) => {
|
||||
const onClosePressed = useCallback(() => {
|
||||
Navigation.dismissModal(componentId);
|
||||
}, [componentId]);
|
||||
|
||||
const source = useMemo(() => ({ uri: `file://${path}` }), [path])
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{type === "photo" && (<Image source={source} style={StyleSheet.absoluteFill} />)}
|
||||
{type === "video" && (<Video source={source} style={StyleSheet.absoluteFill} />)}
|
||||
|
||||
<Pressable style={styles.closeButton} onPress={onClosePressed}><Text>Close.</Text></Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
top: CONTENT_SPACING,
|
||||
left: CONTENT_SPACING,
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
});
|
32
example/src/hooks/useCachedState.ts
Normal file
32
example/src/hooks/useCachedState.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
/**
|
||||
* Same as `useState`, but swallow all calls to `setState` if the value didn't change (uses `===` comparator per default)
|
||||
* @param initialValue The initial state
|
||||
* @param comparator A custom comparator, useful if you want to only round numbers or use string locale for comparison. Make sure this function is memoized!
|
||||
*/
|
||||
export const useCachedState = <T>(
|
||||
initialValue: T,
|
||||
comparator?: (oldState: T, newState: T) => boolean
|
||||
): [T, (newState: T) => void] => {
|
||||
const [state, setState] = useState(initialValue);
|
||||
const cachedState = useRef(initialValue);
|
||||
|
||||
const dispatchState = useCallback(
|
||||
(newState: T) => {
|
||||
const areEqual =
|
||||
comparator == null
|
||||
? cachedState.current === newState
|
||||
: comparator(cachedState.current, newState);
|
||||
if (areEqual) {
|
||||
return;
|
||||
} else {
|
||||
cachedState.current = newState;
|
||||
setState(newState);
|
||||
}
|
||||
},
|
||||
[comparator]
|
||||
);
|
||||
|
||||
return [state, dispatchState];
|
||||
};
|
64
example/src/hooks/useIsScreenFocused.ts
Normal file
64
example/src/hooks/useIsScreenFocused.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { useEffect, useMemo, useReducer } from "react";
|
||||
import { Navigation } from "react-native-navigation";
|
||||
|
||||
type Action =
|
||||
| {
|
||||
action: "push";
|
||||
componentId: string;
|
||||
}
|
||||
| {
|
||||
action: "pop";
|
||||
componentId: string;
|
||||
};
|
||||
|
||||
const reducer = (stack: string[], action: Action): string[] => {
|
||||
switch (action.action) {
|
||||
case "push": {
|
||||
stack.push(action.componentId);
|
||||
break;
|
||||
}
|
||||
case "pop": {
|
||||
const index = stack.indexOf(action.componentId);
|
||||
if (index > -1) stack.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return [...stack];
|
||||
};
|
||||
|
||||
export const useIsScreenFocused = (componentId: string): boolean => {
|
||||
const [componentStack, dispatch] = useReducer(reducer, [componentId]);
|
||||
const isFocussed = useMemo(
|
||||
() => componentStack[componentStack.length - 1] === componentId,
|
||||
[componentStack, componentId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = Navigation.events().registerComponentDidAppearListener(
|
||||
(event) => {
|
||||
if (event.componentType !== "Component") return;
|
||||
dispatch({
|
||||
action: "push",
|
||||
componentId: event.componentId,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return () => listener.remove();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const listener = Navigation.events().registerComponentDidDisappearListener(
|
||||
(event) => {
|
||||
if (event.componentType !== "Component") return;
|
||||
dispatch({
|
||||
action: "pop",
|
||||
componentId: event.componentId,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return () => listener.remove();
|
||||
}, []);
|
||||
|
||||
return isFocussed;
|
||||
};
|
Loading…
Reference in New Issue
Block a user