ESLint autofix
This commit is contained in:
parent
50509200aa
commit
dc2be934f6
@ -1,5 +1,5 @@
|
|||||||
import 'react-native-gesture-handler';
|
import 'react-native-gesture-handler';
|
||||||
import { Navigation } from "react-native-navigation";
|
import { Navigation } from 'react-native-navigation';
|
||||||
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
|
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
|
||||||
import { App } from './src/App';
|
import { App } from './src/App';
|
||||||
import { Settings } from './src/Settings';
|
import { Settings } from './src/Settings';
|
||||||
@ -16,14 +16,14 @@ Navigation.setDefaultOptions({
|
|||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
backgroundColor: 'black',
|
backgroundColor: 'black',
|
||||||
componentBackgroundColor: 'black'
|
componentBackgroundColor: 'black',
|
||||||
},
|
},
|
||||||
statusBar: {
|
statusBar: {
|
||||||
animated: true,
|
animated: true,
|
||||||
drawBehind: true,
|
drawBehind: true,
|
||||||
translucent: true,
|
translucent: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
style: 'dark'
|
style: 'dark',
|
||||||
},
|
},
|
||||||
animations: {
|
animations: {
|
||||||
setRoot: {
|
setRoot: {
|
||||||
@ -31,31 +31,40 @@ Navigation.setDefaultOptions({
|
|||||||
duration: 500,
|
duration: 500,
|
||||||
from: 0,
|
from: 0,
|
||||||
to: 1,
|
to: 1,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Navigation.registerComponent('Splash', () => gestureHandlerRootHOC(Splash), () => Splash);
|
Navigation.registerComponent(
|
||||||
Navigation.registerComponent('Home', () => gestureHandlerRootHOC(App), () => App);
|
'Splash',
|
||||||
Navigation.registerComponent('Media', () => gestureHandlerRootHOC(Media), () => Media);
|
() => gestureHandlerRootHOC(Splash),
|
||||||
Navigation.registerComponent('Settings', () => gestureHandlerRootHOC(Settings), () => Settings);
|
() => Splash,
|
||||||
|
);
|
||||||
|
Navigation.registerComponent(
|
||||||
|
'Home',
|
||||||
|
() => gestureHandlerRootHOC(App),
|
||||||
|
() => App,
|
||||||
|
);
|
||||||
|
Navigation.registerComponent(
|
||||||
|
'Media',
|
||||||
|
() => gestureHandlerRootHOC(Media),
|
||||||
|
() => Media,
|
||||||
|
);
|
||||||
|
Navigation.registerComponent(
|
||||||
|
'Settings',
|
||||||
|
() => gestureHandlerRootHOC(Settings),
|
||||||
|
() => Settings,
|
||||||
|
);
|
||||||
|
|
||||||
Navigation.events().registerNavigationButtonPressedListener((event) => {
|
Navigation.events().registerNavigationButtonPressedListener((event) => {
|
||||||
if (event.buttonId === "back") {
|
if (event.buttonId === 'back') Navigation.pop(event.componentId);
|
||||||
Navigation.pop(event.componentId);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Navigation.events().registerAppLaunchedListener(async () => {
|
Navigation.events().registerAppLaunchedListener(async () => {
|
||||||
const [cameraPermission, microphonePermission] = await Promise.all([
|
const [cameraPermission, microphonePermission] = await Promise.all([Camera.getCameraPermissionStatus(), Camera.getMicrophonePermissionStatus()]);
|
||||||
Camera.getCameraPermissionStatus(),
|
let rootName = 'Splash';
|
||||||
Camera.getMicrophonePermissionStatus(),
|
if (cameraPermission === 'authorized' && microphonePermission === 'authorized') rootName = 'Home';
|
||||||
]);
|
|
||||||
let rootName = "Splash";
|
|
||||||
if (cameraPermission === "authorized" && microphonePermission === "authorized") {
|
|
||||||
rootName = "Home";
|
|
||||||
}
|
|
||||||
|
|
||||||
Navigation.setRoot({
|
Navigation.setRoot({
|
||||||
root: {
|
root: {
|
||||||
@ -63,11 +72,11 @@ Navigation.events().registerAppLaunchedListener(async () => {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
component: {
|
component: {
|
||||||
name: rootName
|
name: rootName,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -16,12 +16,7 @@ module.exports = {
|
|||||||
// We need to make sure that only one version is loaded for peerDependencies
|
// We need to make sure that only one version is loaded for peerDependencies
|
||||||
// So we blacklist them at the root, and alias them to the versions in example's node_modules
|
// So we blacklist them at the root, and alias them to the versions in example's node_modules
|
||||||
resolver: {
|
resolver: {
|
||||||
blacklistRE: blacklist(
|
blacklistRE: blacklist(modules.map((m) => new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`))),
|
||||||
modules.map(
|
|
||||||
(m) =>
|
|
||||||
new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
extraNodeModules: modules.reduce((acc, name) => {
|
extraNodeModules: modules.reduce((acc, name) => {
|
||||||
acc[name] = path.join(__dirname, 'node_modules', name);
|
acc[name] = path.join(__dirname, 'node_modules', name);
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useRef, useState, useMemo, useCallback } from 'react';
|
import { useRef, useState, useMemo, useCallback } from 'react';
|
||||||
import { StyleSheet, View } from 'react-native';
|
import { StyleSheet, View } from 'react-native';
|
||||||
import { PinchGestureHandler, PinchGestureHandlerGestureEvent, State, TapGestureHandler, TapGestureHandlerStateChangeEvent } from 'react-native-gesture-handler';
|
import {
|
||||||
|
PinchGestureHandler,
|
||||||
|
PinchGestureHandlerGestureEvent,
|
||||||
|
State,
|
||||||
|
TapGestureHandler,
|
||||||
|
TapGestureHandlerStateChangeEvent,
|
||||||
|
} from 'react-native-gesture-handler';
|
||||||
import { Navigation, NavigationFunctionComponent } from 'react-native-navigation';
|
import { Navigation, NavigationFunctionComponent } from 'react-native-navigation';
|
||||||
import type { CameraDevice, CameraDeviceFormat, CameraProps, CameraRuntimeError, PhotoFile, VideoFile } from 'react-native-vision-camera';
|
import type { CameraDevice, CameraDeviceFormat, CameraProps, CameraRuntimeError, PhotoFile, VideoFile } from 'react-native-vision-camera';
|
||||||
import { Camera } from 'react-native-vision-camera';
|
import { Camera } from 'react-native-vision-camera';
|
||||||
@ -37,23 +43,15 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
const isForeground = useIsForeground();
|
const isForeground = useIsForeground();
|
||||||
const isActive = isFocussed && isForeground;
|
const isActive = isFocussed && isForeground;
|
||||||
|
|
||||||
const [cameraPosition, setCameraPosition] = useState<"front" | "back">(
|
const [cameraPosition, setCameraPosition] = useState<'front' | 'back'>('back');
|
||||||
"back"
|
|
||||||
);
|
|
||||||
const [enableHdr, setEnableHdr] = useState(false);
|
const [enableHdr, setEnableHdr] = useState(false);
|
||||||
const [flash, setFlash] = useState<"off" | "on">("off");
|
const [flash, setFlash] = useState<'off' | 'on'>('off');
|
||||||
const [enableNightMode, setEnableNightMode] = useState(false);
|
const [enableNightMode, setEnableNightMode] = useState(false);
|
||||||
|
|
||||||
// camera format settings
|
// camera format settings
|
||||||
const [devices, setDevices] = useState<CameraDevice[]>([]); // All available camera devices, sorted by "best device" (descending)
|
const [devices, setDevices] = useState<CameraDevice[]>([]); // All available camera devices, sorted by "best device" (descending)
|
||||||
const device = useMemo<CameraDevice | undefined>(
|
const device = useMemo<CameraDevice | undefined>(() => devices.find((d) => d.position === cameraPosition), [cameraPosition, devices]);
|
||||||
() => devices.find((d) => d.position === cameraPosition),
|
const formats = useMemo<CameraDeviceFormat[]>(() => device?.formats.sort(compareFormats) ?? [], [device?.formats]);
|
||||||
[cameraPosition, devices]
|
|
||||||
);
|
|
||||||
const formats = useMemo<CameraDeviceFormat[]>(
|
|
||||||
() => device?.formats.sort(compareFormats) ?? [],
|
|
||||||
[device?.formats]
|
|
||||||
);
|
|
||||||
|
|
||||||
//#region Memos
|
//#region Memos
|
||||||
const fps = useMemo(() => {
|
const fps = useMemo(() => {
|
||||||
@ -62,37 +60,24 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
return 30;
|
return 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
const supportsHdrAtHighFps = formats.some(
|
const supportsHdrAtHighFps = formats.some((f) => f.supportsVideoHDR && f.frameRateRanges.some((r) => frameRateIncluded(r, HIGH_FPS)));
|
||||||
(f) =>
|
|
||||||
f.supportsVideoHDR &&
|
|
||||||
f.frameRateRanges.some((r) => frameRateIncluded(r, HIGH_FPS))
|
|
||||||
);
|
|
||||||
if (enableHdr && !supportsHdrAtHighFps) {
|
if (enableHdr && !supportsHdrAtHighFps) {
|
||||||
// User has enabled HDR, but HDR is not supported at HIGH_FPS.
|
// User has enabled HDR, but HDR is not supported at HIGH_FPS.
|
||||||
return 30;
|
return 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
const supportsHighFps = formats.some((f) =>
|
const supportsHighFps = formats.some((f) => f.frameRateRanges.some((r) => frameRateIncluded(r, HIGH_FPS)));
|
||||||
f.frameRateRanges.some((r) => frameRateIncluded(r, HIGH_FPS))
|
|
||||||
);
|
|
||||||
if (!supportsHighFps) {
|
if (!supportsHighFps) {
|
||||||
// HIGH_FPS is not supported by any format.
|
// HIGH_FPS is not supported by any format.
|
||||||
return 30;
|
return 30;
|
||||||
}
|
}
|
||||||
// If nothing blocks us from using it, we default to HIGH_FPS.
|
// If nothing blocks us from using it, we default to HIGH_FPS.
|
||||||
return HIGH_FPS;
|
return HIGH_FPS;
|
||||||
}, [device, enableHdr, enableNightMode, formats,]);
|
}, [device, enableHdr, enableNightMode, formats]);
|
||||||
|
|
||||||
const supportsCameraFlipping = useMemo(
|
const supportsCameraFlipping = useMemo(() => devices.some((d) => d.position === 'back') && devices.some((d) => d.position === 'front'), [devices]);
|
||||||
() =>
|
|
||||||
devices.some((d) => d.position === "back") &&
|
|
||||||
devices.some((d) => d.position === "front"),
|
|
||||||
[devices]
|
|
||||||
);
|
|
||||||
const supportsFlash = device?.hasFlash ?? false;
|
const supportsFlash = device?.hasFlash ?? false;
|
||||||
const supportsHdr = useMemo(() => formats.some((f) => f.supportsVideoHDR), [
|
const supportsHdr = useMemo(() => formats.some((f) => f.supportsVideoHDR), [formats]);
|
||||||
formats,
|
|
||||||
]);
|
|
||||||
const canToggleNightMode = enableNightMode
|
const canToggleNightMode = enableNightMode
|
||||||
? true // it's enabled so you have to be able to turn it off again
|
? true // it's enabled so you have to be able to turn it off again
|
||||||
: (device?.supportsLowLightBoost ?? false) || fps > 30; // either we have native support, or we can lower the FPS
|
: (device?.supportsLowLightBoost ?? false) || fps > 30; // either we have native support, or we can lower the FPS
|
||||||
@ -118,14 +103,9 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
|
|
||||||
const cameraAnimatedProps = useAnimatedProps<Partial<CameraProps>>(
|
const cameraAnimatedProps = useAnimatedProps<Partial<CameraProps>>(
|
||||||
() => ({
|
() => ({
|
||||||
zoom: interpolate(
|
zoom: interpolate(zoom.value, [0, neutralZoomScaled, 1], [0, neutralZoom, maxZoomScaled], Extrapolate.CLAMP),
|
||||||
zoom.value,
|
|
||||||
[0, neutralZoomScaled, 1],
|
|
||||||
[0, neutralZoom, maxZoomScaled],
|
|
||||||
Extrapolate.CLAMP
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
[maxZoomScaled, neutralZoom, neutralZoomScaled, zoom]
|
[maxZoomScaled, neutralZoom, neutralZoomScaled, zoom],
|
||||||
);
|
);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
@ -134,45 +114,42 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
(_isPressingButton: boolean) => {
|
(_isPressingButton: boolean) => {
|
||||||
isPressingButton.value = _isPressingButton;
|
isPressingButton.value = _isPressingButton;
|
||||||
},
|
},
|
||||||
[isPressingButton]
|
[isPressingButton],
|
||||||
);
|
);
|
||||||
// Camera callbacks
|
// Camera callbacks
|
||||||
const onError = useCallback((error: CameraRuntimeError) => {
|
const onError = useCallback((error: CameraRuntimeError) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}, []);
|
}, []);
|
||||||
const onInitialized = useCallback(() => {
|
const onInitialized = useCallback(() => {
|
||||||
console.log(`Camera initialized!`);
|
console.log('Camera initialized!');
|
||||||
setIsCameraInitialized(true);
|
setIsCameraInitialized(true);
|
||||||
}, []);
|
}, []);
|
||||||
const onMediaCaptured = useCallback(
|
const onMediaCaptured = useCallback(async (media: PhotoFile | VideoFile, type: 'photo' | 'video') => {
|
||||||
async (media: PhotoFile | VideoFile, type: "photo" | "video") => {
|
console.log(`Media captured! ${JSON.stringify(media)}`);
|
||||||
console.log(`Media captured! ${JSON.stringify(media)}`);
|
await Navigation.showModal({
|
||||||
await Navigation.showModal({
|
component: {
|
||||||
component: {
|
name: 'Media',
|
||||||
name: 'Media',
|
passProps: {
|
||||||
passProps: {
|
type: type,
|
||||||
type: type,
|
path: media.path,
|
||||||
path: media.path,
|
},
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
})
|
}, []);
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
const onFlipCameraPressed = useCallback(() => {
|
const onFlipCameraPressed = useCallback(() => {
|
||||||
setCameraPosition((p) => (p === "back" ? "front" : "back"));
|
setCameraPosition((p) => (p === 'back' ? 'front' : 'back'));
|
||||||
}, []);
|
}, []);
|
||||||
const onHdrSwitchPressed = useCallback(() => {
|
const onHdrSwitchPressed = useCallback(() => {
|
||||||
setEnableHdr((h) => !h);
|
setEnableHdr((h) => !h);
|
||||||
}, []);
|
}, []);
|
||||||
const onFlashPressed = useCallback(() => {
|
const onFlashPressed = useCallback(() => {
|
||||||
setFlash((f) => (f === "off" ? "on" : "off"));
|
setFlash((f) => (f === 'off' ? 'on' : 'off'));
|
||||||
}, []);
|
}, []);
|
||||||
const onNightModePressed = useCallback(() => {
|
const onNightModePressed = useCallback(() => {
|
||||||
setEnableNightMode((n) => !n);
|
setEnableNightMode((n) => !n);
|
||||||
}, []);
|
}, []);
|
||||||
const onSettingsPressed = useCallback(() => {
|
const onSettingsPressed = useCallback(() => {
|
||||||
Navigation.push(componentId, { component: { name: 'Settings' } })
|
Navigation.push(componentId, { component: { name: 'Settings' } });
|
||||||
}, [componentId]);
|
}, [componentId]);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
@ -190,7 +167,7 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isPressingButton, onFlipCameraPressed]
|
[isPressingButton, onFlipCameraPressed],
|
||||||
);
|
);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
@ -199,12 +176,12 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
const loadDevices = async () => {
|
const loadDevices = async () => {
|
||||||
try {
|
try {
|
||||||
const availableCameraDevices = await Camera.getAvailableCameraDevices();
|
const availableCameraDevices = await Camera.getAvailableCameraDevices();
|
||||||
console.log(`Devices: ${availableCameraDevices.map((d) => d.name).join(", ")}`);
|
console.log(`Devices: ${availableCameraDevices.map((d) => d.name).join(', ')}`);
|
||||||
const sortedDevices = availableCameraDevices.sort(compareDevices);
|
const sortedDevices = availableCameraDevices.sort(compareDevices);
|
||||||
console.debug(`Devices (sorted): ${sortedDevices.map((d) => d.name).join(", ")}`);
|
console.debug(`Devices (sorted): ${sortedDevices.map((d) => d.name).join(', ')}`);
|
||||||
setDevices(sortedDevices);
|
setDevices(sortedDevices);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to get available devices!`, e);
|
console.error('Failed to get available devices!', e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadDevices();
|
loadDevices();
|
||||||
@ -216,9 +193,7 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Run everytime the camera gets set to isActive = false. (reset zoom when tab switching)
|
// Run everytime the camera gets set to isActive = false. (reset zoom when tab switching)
|
||||||
if (!isActive) {
|
if (!isActive) zoom.value = neutralZoomScaled;
|
||||||
zoom.value = neutralZoomScaled;
|
|
||||||
}
|
|
||||||
}, [neutralZoomScaled, isActive, zoom]);
|
}, [neutralZoomScaled, isActive, zoom]);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
@ -232,27 +207,19 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
onActive: (event, context) => {
|
onActive: (event, context) => {
|
||||||
// we're trying to map the scale gesture to a linear zoom here
|
// we're trying to map the scale gesture to a linear zoom here
|
||||||
const startZoom = context.startZoom ?? 0;
|
const startZoom = context.startZoom ?? 0;
|
||||||
const scale = interpolate(
|
const scale = interpolate(event.scale, [1 - 1 / SCALE_FULL_ZOOM, 1, SCALE_FULL_ZOOM], [-1, 0, 1], Extrapolate.CLAMP);
|
||||||
event.scale,
|
zoom.value = interpolate(scale, [-1, 0, 1], [0, startZoom, 1], Extrapolate.CLAMP);
|
||||||
[1 - 1 / SCALE_FULL_ZOOM, 1, SCALE_FULL_ZOOM],
|
|
||||||
[-1, 0, 1],
|
|
||||||
Extrapolate.CLAMP
|
|
||||||
);
|
|
||||||
zoom.value = interpolate(
|
|
||||||
scale,
|
|
||||||
[-1, 0, 1],
|
|
||||||
[0, startZoom, 1],
|
|
||||||
Extrapolate.CLAMP
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
if (device != null && format != null) {
|
if (device != null && format != null) {
|
||||||
console.log(`Re-rendering camera page with ${isActive ? "active" : "inactive"} camera. `
|
console.log(
|
||||||
+ `Device: "${device.name}" (${format.photoWidth}x${format.photoHeight} @ ${fps}fps)`);
|
`Re-rendering camera page with ${isActive ? 'active' : 'inactive'} camera. ` +
|
||||||
|
`Device: "${device.name}" (${format.photoWidth}x${format.photoHeight} @ ${fps}fps)`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log(`re-rendering camera page without active camera`);
|
console.log('re-rendering camera page without active camera');
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement camera flipping (back <-> front) while recording and stich the videos together
|
// TODO: Implement camera flipping (back <-> front) while recording and stich the videos together
|
||||||
@ -262,9 +229,7 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
{device != null && (
|
{device != null && (
|
||||||
<PinchGestureHandler onGestureEvent={onPinchGesture} enabled={isActive}>
|
<PinchGestureHandler onGestureEvent={onPinchGesture} enabled={isActive}>
|
||||||
<Reanimated.View style={StyleSheet.absoluteFill}>
|
<Reanimated.View style={StyleSheet.absoluteFill}>
|
||||||
<TapGestureHandler
|
<TapGestureHandler onHandlerStateChange={onDoubleTapGesture} numberOfTaps={2}>
|
||||||
onHandlerStateChange={onDoubleTapGesture}
|
|
||||||
numberOfTaps={2}>
|
|
||||||
<ReanimatedCamera
|
<ReanimatedCamera
|
||||||
ref={camera}
|
ref={camera}
|
||||||
style={StyleSheet.absoluteFill}
|
style={StyleSheet.absoluteFill}
|
||||||
@ -272,9 +237,7 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
format={format}
|
format={format}
|
||||||
fps={fps}
|
fps={fps}
|
||||||
hdr={enableHdr}
|
hdr={enableHdr}
|
||||||
lowLightBoost={
|
lowLightBoost={device.supportsLowLightBoost && enableNightMode}
|
||||||
device.supportsLowLightBoost && enableNightMode
|
|
||||||
}
|
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
onInitialized={onInitialized}
|
onInitialized={onInitialized}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
@ -293,7 +256,7 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
camera={camera}
|
camera={camera}
|
||||||
onMediaCaptured={onMediaCaptured}
|
onMediaCaptured={onMediaCaptured}
|
||||||
cameraZoom={zoom}
|
cameraZoom={zoom}
|
||||||
flash={supportsFlash ? flash : "off"}
|
flash={supportsFlash ? flash : 'off'}
|
||||||
enabled={isCameraInitialized && isActive}
|
enabled={isCameraInitialized && isActive}
|
||||||
setIsPressingButton={setIsPressingButton}
|
setIsPressingButton={setIsPressingButton}
|
||||||
/>
|
/>
|
||||||
@ -302,74 +265,45 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
|
|
||||||
<View style={styles.rightButtonRow}>
|
<View style={styles.rightButtonRow}>
|
||||||
{supportsCameraFlipping && (
|
{supportsCameraFlipping && (
|
||||||
<PressableOpacity
|
<PressableOpacity style={styles.button} onPress={onFlipCameraPressed} disabledOpacity={0.4}>
|
||||||
style={styles.button}
|
<IonIcon name="camera-reverse" color="white" size={24} />
|
||||||
onPress={onFlipCameraPressed}
|
|
||||||
disabledOpacity={0.4}>
|
|
||||||
<IonIcon
|
|
||||||
name="camera-reverse"
|
|
||||||
color="white"
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
</PressableOpacity>
|
</PressableOpacity>
|
||||||
)}
|
)}
|
||||||
{supportsFlash && (
|
{supportsFlash && (
|
||||||
<PressableOpacity
|
<PressableOpacity style={styles.button} onPress={onFlashPressed} disabledOpacity={0.4}>
|
||||||
style={styles.button}
|
<IonIcon name={flash === 'on' ? 'flash' : 'flash-off'} color="white" size={24} />
|
||||||
onPress={onFlashPressed}
|
|
||||||
disabledOpacity={0.4}>
|
|
||||||
<IonIcon
|
|
||||||
name={flash === "on" ? "flash" : "flash-off"}
|
|
||||||
color="white"
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
</PressableOpacity>
|
</PressableOpacity>
|
||||||
)}
|
)}
|
||||||
{canToggleNightMode && (
|
{canToggleNightMode && (
|
||||||
<PressableOpacity
|
<PressableOpacity style={styles.button} onPress={onNightModePressed} disabledOpacity={0.4}>
|
||||||
style={styles.button}
|
<IonIcon name={enableNightMode ? 'moon' : 'moon-outline'} color="white" size={24} />
|
||||||
onPress={onNightModePressed}
|
|
||||||
disabledOpacity={0.4}>
|
|
||||||
<IonIcon
|
|
||||||
name={enableNightMode ? "moon" : "moon-outline"}
|
|
||||||
color="white"
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
</PressableOpacity>
|
</PressableOpacity>
|
||||||
)}
|
)}
|
||||||
{supportsHdr && (
|
{supportsHdr && (
|
||||||
<PressableOpacity style={styles.button} onPress={onHdrSwitchPressed}>
|
<PressableOpacity style={styles.button} onPress={onHdrSwitchPressed}>
|
||||||
<MaterialIcon
|
<MaterialIcon name={enableHdr ? 'hdr' : 'hdr-off'} color="white" size={24} />
|
||||||
name={enableHdr ? "hdr" : "hdr-off"}
|
|
||||||
color="white"
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
</PressableOpacity>
|
</PressableOpacity>
|
||||||
)}
|
)}
|
||||||
<PressableOpacity style={styles.button} onPress={onSettingsPressed}>
|
<PressableOpacity style={styles.button} onPress={onSettingsPressed}>
|
||||||
<IonIcon
|
<IonIcon name="settings-outline" color="white" size={24} />
|
||||||
name="settings-outline"
|
|
||||||
color="white"
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
</PressableOpacity>
|
</PressableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: "black",
|
backgroundColor: 'black',
|
||||||
},
|
},
|
||||||
captureButton: {
|
captureButton: {
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
alignSelf: "center",
|
alignSelf: 'center',
|
||||||
bottom: SAFE_AREA_PADDING.paddingBottom
|
bottom: SAFE_AREA_PADDING.paddingBottom,
|
||||||
},
|
},
|
||||||
openLocalGalleryButton: {
|
openLocalGalleryButton: {
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
left: (SCREEN_WIDTH / 2 - CAPTURE_BUTTON_SIZE / 2) / 2,
|
left: (SCREEN_WIDTH / 2 - CAPTURE_BUTTON_SIZE / 2) / 2,
|
||||||
width: LOCAL_GALLERY_BUTTON_SIZE,
|
width: LOCAL_GALLERY_BUTTON_SIZE,
|
||||||
height: LOCAL_GALLERY_BUTTON_SIZE,
|
height: LOCAL_GALLERY_BUTTON_SIZE,
|
||||||
@ -380,13 +314,13 @@ const styles = StyleSheet.create({
|
|||||||
width: BUTTON_SIZE,
|
width: BUTTON_SIZE,
|
||||||
height: BUTTON_SIZE,
|
height: BUTTON_SIZE,
|
||||||
borderRadius: BUTTON_SIZE / 2,
|
borderRadius: BUTTON_SIZE / 2,
|
||||||
backgroundColor: "rgba(140, 140, 140, 0.3)",
|
backgroundColor: 'rgba(140, 140, 140, 0.3)',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
rightButtonRow: {
|
rightButtonRow: {
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
right: CONTENT_SPACING,
|
right: CONTENT_SPACING,
|
||||||
top: SAFE_AREA_PADDING.paddingTop
|
top: SAFE_AREA_PADDING.paddingTop,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import { Dimensions, Platform } from "react-native";
|
import { Dimensions, Platform } from 'react-native';
|
||||||
import StaticSafeAreaInsets from "react-native-static-safe-area-insets";
|
import StaticSafeAreaInsets from 'react-native-static-safe-area-insets';
|
||||||
|
|
||||||
export const CONTENT_SPACING = 15;
|
export const CONTENT_SPACING = 15;
|
||||||
|
|
||||||
const SAFE_BOTTOM = Platform.select({
|
const SAFE_BOTTOM =
|
||||||
ios: StaticSafeAreaInsets.safeAreaInsetsBottom
|
Platform.select({
|
||||||
}) ?? 0
|
ios: StaticSafeAreaInsets.safeAreaInsetsBottom,
|
||||||
|
}) ?? 0;
|
||||||
|
|
||||||
export const SAFE_AREA_PADDING = {
|
export const SAFE_AREA_PADDING = {
|
||||||
paddingLeft: StaticSafeAreaInsets.safeAreaInsetsLeft + CONTENT_SPACING,
|
paddingLeft: StaticSafeAreaInsets.safeAreaInsetsLeft + CONTENT_SPACING,
|
||||||
paddingTop: StaticSafeAreaInsets.safeAreaInsetsTop + CONTENT_SPACING,
|
paddingTop: StaticSafeAreaInsets.safeAreaInsetsTop + CONTENT_SPACING,
|
||||||
paddingRight: StaticSafeAreaInsets.safeAreaInsetsRight + CONTENT_SPACING,
|
paddingRight: StaticSafeAreaInsets.safeAreaInsetsRight + CONTENT_SPACING,
|
||||||
paddingBottom: SAFE_BOTTOM + CONTENT_SPACING
|
paddingBottom: SAFE_BOTTOM + CONTENT_SPACING,
|
||||||
}
|
};
|
||||||
|
|
||||||
// whether to use takeSnapshot() instead of takePhoto() on Android
|
// whether to use takeSnapshot() instead of takePhoto() on Android
|
||||||
export const USE_SNAPSHOT_ON_ANDROID = false;
|
export const USE_SNAPSHOT_ON_ANDROID = false;
|
||||||
@ -35,8 +36,8 @@ export const MAX_ZOOM_FACTOR = 16;
|
|||||||
|
|
||||||
export const SCREEN_WIDTH = Dimensions.get('window').width;
|
export const SCREEN_WIDTH = Dimensions.get('window').width;
|
||||||
export const SCREEN_HEIGHT = Platform.select<number>({
|
export const SCREEN_HEIGHT = Platform.select<number>({
|
||||||
android: Dimensions.get("screen").height - StaticSafeAreaInsets.safeAreaInsetsBottom,
|
android: Dimensions.get('screen').height - StaticSafeAreaInsets.safeAreaInsetsBottom,
|
||||||
ios: Dimensions.get("window").height,
|
ios: Dimensions.get('window').height,
|
||||||
}) as number;
|
}) as number;
|
||||||
|
|
||||||
// Capture Button
|
// Capture Button
|
||||||
|
@ -1,14 +1,5 @@
|
|||||||
import {
|
import { RESOLUTION_LIMIT, SCREEN_HEIGHT, SCREEN_WIDTH, USE_ULTRAWIDE_IF_AVAILABLE } from './Constants';
|
||||||
RESOLUTION_LIMIT,
|
import type { CameraDevice, CameraDeviceFormat, FrameRateRange } from 'react-native-vision-camera';
|
||||||
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:
|
* Compares two devices with the following criteria:
|
||||||
@ -23,14 +14,11 @@ import type {
|
|||||||
*
|
*
|
||||||
* Note that this makes the `sort()` function descending, so the first element (`[0]`) is the "best" device.
|
* Note that this makes the `sort()` function descending, so the first element (`[0]`) is the "best" device.
|
||||||
*/
|
*/
|
||||||
export const compareDevices = (
|
export const compareDevices = (left: CameraDevice, right: CameraDevice): -1 | 0 | 1 => {
|
||||||
left: CameraDevice,
|
|
||||||
right: CameraDevice
|
|
||||||
): -1 | 0 | 1 => {
|
|
||||||
let leftPoints = 0;
|
let leftPoints = 0;
|
||||||
|
|
||||||
const leftHasWideAngle = left.devices.includes("wide-angle-camera");
|
const leftHasWideAngle = left.devices.includes('wide-angle-camera');
|
||||||
const rightHasWideAngle = right.devices.includes("wide-angle-camera");
|
const rightHasWideAngle = right.devices.includes('wide-angle-camera');
|
||||||
if (leftHasWideAngle && !rightHasWideAngle) {
|
if (leftHasWideAngle && !rightHasWideAngle) {
|
||||||
// left does have a wide-angle-camera, but right doesn't.
|
// left does have a wide-angle-camera, but right doesn't.
|
||||||
leftPoints += 5;
|
leftPoints += 5;
|
||||||
@ -40,12 +28,8 @@ export const compareDevices = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!USE_ULTRAWIDE_IF_AVAILABLE) {
|
if (!USE_ULTRAWIDE_IF_AVAILABLE) {
|
||||||
const leftHasUltraWideAngle = left.devices.includes(
|
const leftHasUltraWideAngle = left.devices.includes('ultra-wide-angle-camera');
|
||||||
"ultra-wide-angle-camera"
|
const rightHasUltraWideAngle = right.devices.includes('ultra-wide-angle-camera');
|
||||||
);
|
|
||||||
const rightHasUltraWideAngle = right.devices.includes(
|
|
||||||
"ultra-wide-angle-camera"
|
|
||||||
);
|
|
||||||
if (leftHasUltraWideAngle && !rightHasUltraWideAngle) {
|
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.
|
// left does have an ultra-wide-angle-camera, but right doesn't. Ultra-Wide cameras are bad because of their poor quality.
|
||||||
leftPoints -= 5;
|
leftPoints -= 5;
|
||||||
@ -76,7 +60,7 @@ const CAMERA_VIEW_SIZE: Size = {
|
|||||||
|
|
||||||
const applyScaledMask = (
|
const applyScaledMask = (
|
||||||
clippedElementDimensions: Size, // 3024 x 4032 | 2160x3840
|
clippedElementDimensions: Size, // 3024 x 4032 | 2160x3840
|
||||||
maskDimensions: Size // 375 x 623
|
maskDimensions: Size, // 375 x 623
|
||||||
): Size => {
|
): Size => {
|
||||||
const wScale = maskDimensions.width / clippedElementDimensions.width;
|
const wScale = maskDimensions.width / clippedElementDimensions.width;
|
||||||
const hScale = maskDimensions.height / clippedElementDimensions.height;
|
const hScale = maskDimensions.height / clippedElementDimensions.height;
|
||||||
@ -111,10 +95,7 @@ const applyScaledMask = (
|
|||||||
*
|
*
|
||||||
* Note that this makes the `sort()` function descending, so the first element (`[0]`) is the "best" format.
|
* Note that this makes the `sort()` function descending, so the first element (`[0]`) is the "best" format.
|
||||||
*/
|
*/
|
||||||
export const compareFormats = (
|
export const compareFormats = (left: CameraDeviceFormat, right: CameraDeviceFormat): -1 | 0 | 1 => {
|
||||||
left: CameraDeviceFormat,
|
|
||||||
right: CameraDeviceFormat
|
|
||||||
): -1 | 0 | 1 => {
|
|
||||||
// Point score of the left format. Higher is better.
|
// Point score of the left format. Higher is better.
|
||||||
let leftPoints = 0;
|
let leftPoints = 0;
|
||||||
|
|
||||||
@ -122,8 +103,7 @@ export const compareFormats = (
|
|||||||
const rightPhotoPixels = right.photoHeight * right.photoWidth;
|
const rightPhotoPixels = right.photoHeight * right.photoWidth;
|
||||||
if (leftPhotoPixels > rightPhotoPixels) {
|
if (leftPhotoPixels > rightPhotoPixels) {
|
||||||
// left has greater photo dimensions
|
// left has greater photo dimensions
|
||||||
const isLeftAbovePixelLimit =
|
const isLeftAbovePixelLimit = RESOLUTION_LIMIT != null && leftPhotoPixels > RESOLUTION_LIMIT;
|
||||||
RESOLUTION_LIMIT != null && leftPhotoPixels > RESOLUTION_LIMIT;
|
|
||||||
if (isLeftAbovePixelLimit) {
|
if (isLeftAbovePixelLimit) {
|
||||||
// left exceeds our pixel limit
|
// left exceeds our pixel limit
|
||||||
leftPoints -= 3;
|
leftPoints -= 3;
|
||||||
@ -133,8 +113,7 @@ export const compareFormats = (
|
|||||||
}
|
}
|
||||||
} else if (leftPhotoPixels < rightPhotoPixels) {
|
} else if (leftPhotoPixels < rightPhotoPixels) {
|
||||||
// left has smaller photo dimensions
|
// left has smaller photo dimensions
|
||||||
const isRightAbovePixelLimit =
|
const isRightAbovePixelLimit = RESOLUTION_LIMIT != null && rightPhotoPixels > RESOLUTION_LIMIT;
|
||||||
RESOLUTION_LIMIT != null && rightPhotoPixels > RESOLUTION_LIMIT;
|
|
||||||
if (isRightAbovePixelLimit) {
|
if (isRightAbovePixelLimit) {
|
||||||
// right exceeds our pixel limit
|
// right exceeds our pixel limit
|
||||||
leftPoints += 3;
|
leftPoints += 3;
|
||||||
@ -146,17 +125,14 @@ export const compareFormats = (
|
|||||||
|
|
||||||
const leftCropped = applyScaledMask(
|
const leftCropped = applyScaledMask(
|
||||||
{ width: left.photoHeight, height: left.photoWidth }, // cameras are horizontal, we rotate to portrait
|
{ width: left.photoHeight, height: left.photoWidth }, // cameras are horizontal, we rotate to portrait
|
||||||
CAMERA_VIEW_SIZE
|
CAMERA_VIEW_SIZE,
|
||||||
);
|
);
|
||||||
const rightCropped = applyScaledMask(
|
const rightCropped = applyScaledMask(
|
||||||
{ width: right.photoHeight, height: right.photoWidth }, // cameras are horizontal, we rotate to portrait
|
{ width: right.photoHeight, height: right.photoWidth }, // cameras are horizontal, we rotate to portrait
|
||||||
CAMERA_VIEW_SIZE
|
CAMERA_VIEW_SIZE,
|
||||||
);
|
);
|
||||||
const leftOverflow =
|
const leftOverflow = left.photoWidth * left.photoHeight - leftCropped.width * leftCropped.height;
|
||||||
left.photoWidth * left.photoHeight - leftCropped.width * leftCropped.height;
|
const rightOverflow = right.photoWidth * right.photoHeight - rightCropped.width * rightCropped.height;
|
||||||
const rightOverflow =
|
|
||||||
right.photoWidth * right.photoHeight -
|
|
||||||
rightCropped.width * rightCropped.height;
|
|
||||||
if (leftOverflow > rightOverflow) {
|
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
|
// 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;
|
leftPoints -= 4;
|
||||||
@ -165,12 +141,7 @@ export const compareFormats = (
|
|||||||
leftPoints += 4;
|
leftPoints += 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (left.videoHeight != null && left.videoWidth != null && right.videoHeight != null && right.videoWidth != null) {
|
||||||
left.videoHeight != null &&
|
|
||||||
left.videoWidth != null &&
|
|
||||||
right.videoHeight != null &&
|
|
||||||
right.videoWidth != null
|
|
||||||
) {
|
|
||||||
const leftVideoPixels = left.videoWidth * left.videoHeight ?? 0;
|
const leftVideoPixels = left.videoWidth * left.videoHeight ?? 0;
|
||||||
const rightVideoPixels = right.videoWidth * right.videoHeight ?? 0;
|
const rightVideoPixels = right.videoWidth * right.videoHeight ?? 0;
|
||||||
if (leftVideoPixels > rightVideoPixels) {
|
if (leftVideoPixels > rightVideoPixels) {
|
||||||
@ -182,12 +153,8 @@ export const compareFormats = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const leftMaxFps = Math.max(
|
const leftMaxFps = Math.max(...left.frameRateRanges.map((r) => r.maxFrameRate));
|
||||||
...left.frameRateRanges.map((r) => r.maxFrameRate)
|
const rightMaxFps = Math.max(...right.frameRateRanges.map((r) => r.maxFrameRate));
|
||||||
);
|
|
||||||
const rightMaxFps = Math.max(
|
|
||||||
...right.frameRateRanges.map((r) => r.maxFrameRate)
|
|
||||||
);
|
|
||||||
if (leftMaxFps > rightMaxFps) {
|
if (leftMaxFps > rightMaxFps) {
|
||||||
// left has more fps
|
// left has more fps
|
||||||
leftPoints += 2;
|
leftPoints += 2;
|
||||||
@ -220,42 +187,27 @@ export const compareFormats = (
|
|||||||
/**
|
/**
|
||||||
* Selects the smallest difference between a FrameRateRange's `maxFrameRate` and the given `fps`
|
* Selects the smallest difference between a FrameRateRange's `maxFrameRate` and the given `fps`
|
||||||
*/
|
*/
|
||||||
const smallestFpsDiff = (
|
const smallestFpsDiff = (frameRateRanges: FrameRateRange[], fps: number): number => {
|
||||||
frameRateRanges: FrameRateRange[],
|
const bestFrameRateRange = frameRateRanges.reduce<FrameRateRange | undefined>((prev, curr) => {
|
||||||
fps: number
|
if (prev == null) return curr;
|
||||||
): number => {
|
|
||||||
const bestFrameRateRange = frameRateRanges.reduce<FrameRateRange | undefined>(
|
|
||||||
(prev, curr) => {
|
|
||||||
if (prev == null) return curr;
|
|
||||||
|
|
||||||
const prevDiff = Math.abs(prev.maxFrameRate - fps);
|
const prevDiff = Math.abs(prev.maxFrameRate - fps);
|
||||||
const currDiff = Math.abs(curr.maxFrameRate - fps);
|
const currDiff = Math.abs(curr.maxFrameRate - fps);
|
||||||
if (prevDiff < currDiff) return prev;
|
if (prevDiff < currDiff) return prev;
|
||||||
else return curr;
|
else return curr;
|
||||||
},
|
}, undefined);
|
||||||
undefined
|
|
||||||
);
|
|
||||||
const max = bestFrameRateRange?.maxFrameRate ?? 0;
|
const max = bestFrameRateRange?.maxFrameRate ?? 0;
|
||||||
return Math.abs(max - fps);
|
return Math.abs(max - fps);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const frameRateIncluded = (
|
export const frameRateIncluded = (range: FrameRateRange, fps: number): boolean => fps >= range.minFrameRate && fps <= range.maxFrameRate;
|
||||||
range: FrameRateRange,
|
|
||||||
fps: number
|
|
||||||
): boolean => fps >= range.minFrameRate && fps <= range.maxFrameRate;
|
|
||||||
|
|
||||||
const isFpsInFrameRateRange = (
|
const isFpsInFrameRateRange = (format: CameraDeviceFormat, fps: number): boolean => format.frameRateRanges.some((r) => frameRateIncluded(r, fps));
|
||||||
format: CameraDeviceFormat,
|
|
||||||
fps: number
|
|
||||||
): boolean => format.frameRateRanges.some((r) => frameRateIncluded(r, fps));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects the format with the closest frame rate ranges to the FPS
|
* Selects the format with the closest frame rate ranges to the FPS
|
||||||
*/
|
*/
|
||||||
export const formatWithClosestMatchingFps = (
|
export const formatWithClosestMatchingFps = (formats: CameraDeviceFormat[], fps: number): CameraDeviceFormat | undefined =>
|
||||||
formats: CameraDeviceFormat[],
|
|
||||||
fps: number
|
|
||||||
): CameraDeviceFormat | undefined =>
|
|
||||||
formats.reduce<CameraDeviceFormat | undefined>((prev, curr) => {
|
formats.reduce<CameraDeviceFormat | undefined>((prev, curr) => {
|
||||||
if (prev == null) return curr;
|
if (prev == null) return curr;
|
||||||
|
|
||||||
|
@ -12,42 +12,41 @@ import CameraRoll from '@react-native-community/cameraroll';
|
|||||||
import { StatusBarBlurBackground } from './views/StatusBarBlurBackground';
|
import { StatusBarBlurBackground } from './views/StatusBarBlurBackground';
|
||||||
|
|
||||||
interface MediaProps {
|
interface MediaProps {
|
||||||
path: string,
|
path: string;
|
||||||
type: 'video' | 'photo'
|
type: 'video' | 'photo';
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestSavePermission = async (): Promise<boolean> => {
|
const requestSavePermission = async (): Promise<boolean> => {
|
||||||
if (Platform.OS !== "android") {
|
if (Platform.OS !== 'android') return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE;
|
const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE;
|
||||||
let hasPermission = await PermissionsAndroid.check(permission);
|
let hasPermission = await PermissionsAndroid.check(permission);
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
const permissionRequestResult = await PermissionsAndroid.request(permission);
|
const permissionRequestResult = await PermissionsAndroid.request(permission);
|
||||||
hasPermission = permissionRequestResult === "granted";
|
hasPermission = permissionRequestResult === 'granted';
|
||||||
}
|
}
|
||||||
return hasPermission;
|
return hasPermission;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Media: NavigationFunctionComponent<MediaProps> = ({ componentId, type, path }) => {
|
export const Media: NavigationFunctionComponent<MediaProps> = ({ componentId, type, path }) => {
|
||||||
const [hasMediaLoaded, setHasMediaLoaded] = useState(false);
|
const [hasMediaLoaded, setHasMediaLoaded] = useState(false);
|
||||||
const isForeground = useIsForeground();
|
const isForeground = useIsForeground();
|
||||||
const isScreenFocused = useIsScreenFocused(componentId);
|
const isScreenFocused = useIsScreenFocused(componentId);
|
||||||
const isVideoPaused = !isForeground || !isScreenFocused;
|
const isVideoPaused = !isForeground || !isScreenFocused;
|
||||||
const [savingState, setSavingState] = useState<"none" | "saving" | "saved">("none");
|
const [savingState, setSavingState] = useState<'none' | 'saving' | 'saved'>('none');
|
||||||
|
|
||||||
const onClosePressed = useCallback(() => {
|
const onClosePressed = useCallback(() => {
|
||||||
Navigation.dismissModal(componentId);
|
Navigation.dismissModal(componentId);
|
||||||
}, [componentId]);
|
}, [componentId]);
|
||||||
|
|
||||||
const onMediaLoadEnd = useCallback(() => {
|
const onMediaLoadEnd = useCallback(() => {
|
||||||
console.log(`media has loaded.`);
|
console.log('media has loaded.');
|
||||||
setHasMediaLoaded(true);
|
setHasMediaLoaded(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSavePressed = useCallback(async () => {
|
const onSavePressed = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setSavingState("saving");
|
setSavingState('saving');
|
||||||
|
|
||||||
const hasPermission = await requestSavePermission();
|
const hasPermission = await requestSavePermission();
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
@ -57,33 +56,23 @@ export const Media: NavigationFunctionComponent<MediaProps> = ({ componentId, ty
|
|||||||
await CameraRoll.save(`file://${path}`, {
|
await CameraRoll.save(`file://${path}`, {
|
||||||
type: type,
|
type: type,
|
||||||
});
|
});
|
||||||
setSavingState("saved");
|
setSavingState('saved');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setSavingState("none");
|
setSavingState('none');
|
||||||
Alert.alert(
|
Alert.alert('Failed to save!', `An unexpected error occured while trying to save your ${type}. ${e?.message ?? JSON.stringify(e)}`);
|
||||||
"Failed to save!",
|
|
||||||
`An unexpected error occured while trying to save your ${type}. ${e?.message ?? JSON.stringify(e)}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, [path, type]);
|
}, [path, type]);
|
||||||
|
|
||||||
const source = useMemo(() => ({ uri: `file://${path}` }), [path]);
|
const source = useMemo(() => ({ uri: `file://${path}` }), [path]);
|
||||||
|
|
||||||
const screenStyle = useMemo(() => ({ opacity: hasMediaLoaded ? 1 : 0 }), [
|
const screenStyle = useMemo(() => ({ opacity: hasMediaLoaded ? 1 : 0 }), [hasMediaLoaded]);
|
||||||
hasMediaLoaded,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, screenStyle]}>
|
<View style={[styles.container, screenStyle]}>
|
||||||
{type === "photo" && (
|
{type === 'photo' && <Image source={source} style={StyleSheet.absoluteFill} resizeMode="cover" onLoadEnd={onMediaLoadEnd} />}
|
||||||
<Image
|
{type === 'video' && (
|
||||||
|
<Video
|
||||||
source={source}
|
source={source}
|
||||||
style={StyleSheet.absoluteFill}
|
|
||||||
resizeMode="cover"
|
|
||||||
onLoadEnd={onMediaLoadEnd} />
|
|
||||||
)}
|
|
||||||
{type === "video" && (
|
|
||||||
<Video source={source}
|
|
||||||
style={StyleSheet.absoluteFill}
|
style={StyleSheet.absoluteFill}
|
||||||
paused={isVideoPaused}
|
paused={isVideoPaused}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
@ -96,48 +85,24 @@ export const Media: NavigationFunctionComponent<MediaProps> = ({ componentId, ty
|
|||||||
controls={false}
|
controls={false}
|
||||||
playWhenInactive={true}
|
playWhenInactive={true}
|
||||||
ignoreSilentSwitch="ignore"
|
ignoreSilentSwitch="ignore"
|
||||||
onReadyForDisplay={onMediaLoadEnd} />
|
onReadyForDisplay={onMediaLoadEnd}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PressableOpacity
|
<PressableOpacity style={styles.closeButton} onPress={onClosePressed}>
|
||||||
style={styles.closeButton}
|
<IonIcon name="close" size={35} color="white" style={styles.icon} />
|
||||||
onPress={onClosePressed}>
|
|
||||||
<IonIcon
|
|
||||||
name="close"
|
|
||||||
size={35}
|
|
||||||
color="white"
|
|
||||||
style={styles.icon} />
|
|
||||||
</PressableOpacity>
|
</PressableOpacity>
|
||||||
|
|
||||||
<PressableOpacity
|
<PressableOpacity style={styles.saveButton} onPress={onSavePressed} disabled={savingState !== 'none'}>
|
||||||
style={styles.saveButton}
|
{savingState === 'none' && <IonIcon name="download" size={35} color="white" style={styles.icon} />}
|
||||||
onPress={onSavePressed}
|
{savingState === 'saved' && <IonIcon name="checkmark" size={35} color="white" style={styles.icon} />}
|
||||||
disabled={savingState !== "none"}>
|
{savingState === 'saving' && <ActivityIndicator color="white" />}
|
||||||
{savingState === "none" && (
|
|
||||||
<IonIcon
|
|
||||||
name="download"
|
|
||||||
size={35}
|
|
||||||
color="white"
|
|
||||||
style={styles.icon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{savingState === "saved" && (
|
|
||||||
<IonIcon
|
|
||||||
name="checkmark"
|
|
||||||
size={35}
|
|
||||||
color="white"
|
|
||||||
style={styles.icon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{savingState === "saving" && (
|
|
||||||
<ActivityIndicator color="white" />
|
|
||||||
)}
|
|
||||||
</PressableOpacity>
|
</PressableOpacity>
|
||||||
|
|
||||||
<StatusBarBlurBackground />
|
<StatusBarBlurBackground />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
Media.options = {
|
Media.options = {
|
||||||
modal: {
|
modal: {
|
||||||
@ -154,8 +119,8 @@ Media.options = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
backgroundColor: "transparent",
|
backgroundColor: 'transparent',
|
||||||
componentBackgroundColor: "transparent",
|
componentBackgroundColor: 'transparent',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -181,7 +146,7 @@ const styles = StyleSheet.create({
|
|||||||
height: 40,
|
height: 40,
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
textShadowColor: "black",
|
textShadowColor: 'black',
|
||||||
textShadowOffset: {
|
textShadowOffset: {
|
||||||
height: 0,
|
height: 0,
|
||||||
width: 0,
|
width: 0,
|
||||||
|
@ -3,33 +3,36 @@ import React, { useCallback } from 'react';
|
|||||||
import { StyleSheet, View, Text, Linking } from 'react-native';
|
import { StyleSheet, View, Text, Linking } from 'react-native';
|
||||||
import type { NavigationFunctionComponent } from 'react-native-navigation';
|
import type { NavigationFunctionComponent } from 'react-native-navigation';
|
||||||
|
|
||||||
|
|
||||||
export const Settings: NavigationFunctionComponent = () => {
|
export const Settings: NavigationFunctionComponent = () => {
|
||||||
const onCuventPressed = useCallback(() => {
|
const onCuventPressed = useCallback(() => {
|
||||||
Linking.openURL('https://cuvent.com')
|
Linking.openURL('https://cuvent.com');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.aboutText}>Vision Camera is powered by{" "}
|
<Text style={styles.aboutText}>
|
||||||
<Text style={styles.hyperlink} onPress={onCuventPressed}>Cuvent</Text>.
|
Vision Camera is powered by{' '}
|
||||||
|
<Text style={styles.hyperlink} onPress={onCuventPressed}>
|
||||||
|
Cuvent
|
||||||
|
</Text>
|
||||||
|
.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
Settings.options = {
|
Settings.options = {
|
||||||
topBar: {
|
topBar: {
|
||||||
visible: true,
|
visible: true,
|
||||||
title: {
|
title: {
|
||||||
text: 'Settings'
|
text: 'Settings',
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
id: 'back',
|
id: 'back',
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
@ -1,19 +1,13 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { StyleSheet, View, Text, Image } from 'react-native';
|
import { StyleSheet, View, Text, Image } from 'react-native';
|
||||||
import { Navigation, NavigationFunctionComponent } from 'react-native-navigation';
|
import { Navigation, NavigationFunctionComponent } from 'react-native-navigation';
|
||||||
import { Camera, CameraPermissionStatus } from 'react-native-vision-camera';
|
import { Camera, CameraPermissionStatus } from 'react-native-vision-camera';
|
||||||
import { CONTENT_SPACING, SAFE_AREA_PADDING } from './Constants';
|
import { CONTENT_SPACING, SAFE_AREA_PADDING } from './Constants';
|
||||||
|
|
||||||
export const Splash: NavigationFunctionComponent = ({ componentId }) => {
|
export const Splash: NavigationFunctionComponent = ({ componentId }) => {
|
||||||
const [
|
const [cameraPermissionStatus, setCameraPermissionStatus] = useState<CameraPermissionStatus>('not-determined');
|
||||||
cameraPermissionStatus,
|
const [microphonePermissionStatus, setMicrophonePermissionStatus] = useState<CameraPermissionStatus>('not-determined');
|
||||||
setCameraPermissionStatus,
|
|
||||||
] = useState<CameraPermissionStatus>("not-determined");
|
|
||||||
const [
|
|
||||||
microphonePermissionStatus,
|
|
||||||
setMicrophonePermissionStatus,
|
|
||||||
] = useState<CameraPermissionStatus>("not-determined");
|
|
||||||
|
|
||||||
const requestMicrophonePermission = useCallback(async () => {
|
const requestMicrophonePermission = useCallback(async () => {
|
||||||
console.log('Requesting microphone permission...');
|
console.log('Requesting microphone permission...');
|
||||||
@ -31,14 +25,12 @@ export const Splash: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkPermissions = async () => {
|
const checkPermissions = async () => {
|
||||||
console.log(`Checking Permission status...`);
|
console.log('Checking Permission status...');
|
||||||
let [cameraPermission, microphonePermission] = await Promise.all([
|
const [cameraPermission, microphonePermission] = await Promise.all([
|
||||||
Camera.getCameraPermissionStatus(),
|
Camera.getCameraPermissionStatus(),
|
||||||
Camera.getMicrophonePermissionStatus(),
|
Camera.getMicrophonePermissionStatus(),
|
||||||
]);
|
]);
|
||||||
console.log(
|
console.log(`Check: CameraPermission: ${cameraPermission} | MicrophonePermission: ${microphonePermission}`);
|
||||||
`Check: CameraPermission: ${cameraPermission} | MicrophonePermission: ${microphonePermission}`
|
|
||||||
);
|
|
||||||
setCameraPermissionStatus(cameraPermission);
|
setCameraPermissionStatus(cameraPermission);
|
||||||
setMicrophonePermissionStatus(microphonePermission);
|
setMicrophonePermissionStatus(microphonePermission);
|
||||||
};
|
};
|
||||||
@ -54,13 +46,13 @@ export const Splash: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
component: {
|
component: {
|
||||||
name: 'Home'
|
name: 'Home',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}, [cameraPermissionStatus, microphonePermissionStatus, componentId]);
|
}, [cameraPermissionStatus, microphonePermissionStatus, componentId]);
|
||||||
|
|
||||||
@ -71,17 +63,25 @@ export const Splash: NavigationFunctionComponent = ({ componentId }) => {
|
|||||||
|
|
||||||
<View style={styles.permissionsContainer}>
|
<View style={styles.permissionsContainer}>
|
||||||
{cameraPermissionStatus !== 'authorized' && (
|
{cameraPermissionStatus !== 'authorized' && (
|
||||||
<Text style={styles.permissionText}>Vision Camera needs <Text style={styles.bold}>Camera permission</Text>.
|
<Text style={styles.permissionText}>
|
||||||
<Text style={styles.hyperlink} onPress={requestCameraPermission}>Grant</Text></Text>
|
Vision Camera needs <Text style={styles.bold}>Camera permission</Text>.
|
||||||
|
<Text style={styles.hyperlink} onPress={requestCameraPermission}>
|
||||||
|
Grant
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
{microphonePermissionStatus !== 'authorized' && (
|
{microphonePermissionStatus !== 'authorized' && (
|
||||||
<Text style={styles.permissionText}>Vision Camera needs <Text style={styles.bold}>Microphone permission</Text>.
|
<Text style={styles.permissionText}>
|
||||||
<Text style={styles.hyperlink} onPress={requestMicrophonePermission}>Grant</Text></Text>
|
Vision Camera needs <Text style={styles.bold}>Microphone permission</Text>.
|
||||||
|
<Text style={styles.hyperlink} onPress={requestMicrophonePermission}>
|
||||||
|
Grant
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
welcome: {
|
welcome: {
|
||||||
@ -98,10 +98,10 @@ const styles = StyleSheet.create({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
...SAFE_AREA_PADDING
|
...SAFE_AREA_PADDING,
|
||||||
},
|
},
|
||||||
permissionsContainer: {
|
permissionsContainer: {
|
||||||
marginTop: CONTENT_SPACING *2
|
marginTop: CONTENT_SPACING * 2,
|
||||||
},
|
},
|
||||||
permissionText: {
|
permissionText: {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
|
@ -1,23 +1,17 @@
|
|||||||
import { useCallback, useRef, useState } from "react";
|
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)
|
* 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 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!
|
* @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>(
|
export const useCachedState = <T>(initialValue: T, comparator?: (oldState: T, newState: T) => boolean): [T, (newState: T) => void] => {
|
||||||
initialValue: T,
|
|
||||||
comparator?: (oldState: T, newState: T) => boolean
|
|
||||||
): [T, (newState: T) => void] => {
|
|
||||||
const [state, setState] = useState(initialValue);
|
const [state, setState] = useState(initialValue);
|
||||||
const cachedState = useRef(initialValue);
|
const cachedState = useRef(initialValue);
|
||||||
|
|
||||||
const dispatchState = useCallback(
|
const dispatchState = useCallback(
|
||||||
(newState: T) => {
|
(newState: T) => {
|
||||||
const areEqual =
|
const areEqual = comparator == null ? cachedState.current === newState : comparator(cachedState.current, newState);
|
||||||
comparator == null
|
|
||||||
? cachedState.current === newState
|
|
||||||
: comparator(cachedState.current, newState);
|
|
||||||
if (areEqual) {
|
if (areEqual) {
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@ -25,7 +19,7 @@ export const useCachedState = <T>(
|
|||||||
setState(newState);
|
setState(newState);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[comparator]
|
[comparator],
|
||||||
);
|
);
|
||||||
|
|
||||||
return [state, dispatchState];
|
return [state, dispatchState];
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import { useEffect } from "react"
|
import { useEffect } from 'react';
|
||||||
import { AppState, AppStateStatus } from "react-native";
|
import { AppState, AppStateStatus } from 'react-native';
|
||||||
import { useCachedState } from "./useCachedState";
|
import { useCachedState } from './useCachedState';
|
||||||
|
|
||||||
export const useIsForeground = (): boolean => {
|
export const useIsForeground = (): boolean => {
|
||||||
const [isForeground, setIsForeground] = useCachedState(true);
|
const [isForeground, setIsForeground] = useCachedState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onChange = (state: AppStateStatus) => {
|
const onChange = (state: AppStateStatus) => {
|
||||||
setIsForeground(state === "active");
|
setIsForeground(state === 'active');
|
||||||
};
|
};
|
||||||
AppState.addEventListener("change", onChange);
|
AppState.addEventListener('change', onChange);
|
||||||
return () => AppState.removeEventListener("change", onChange);
|
return () => AppState.removeEventListener('change', onChange);
|
||||||
}, [setIsForeground]);
|
}, [setIsForeground]);
|
||||||
|
|
||||||
return isForeground;
|
return isForeground;
|
||||||
}
|
};
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import { useEffect, useMemo, useReducer } from "react";
|
import { useEffect, useMemo, useReducer } from 'react';
|
||||||
import { Navigation } from "react-native-navigation";
|
import { Navigation } from 'react-native-navigation';
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| {
|
| {
|
||||||
action: "push";
|
action: 'push';
|
||||||
componentId: string;
|
componentId: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
action: "pop";
|
action: 'pop';
|
||||||
componentId: string;
|
componentId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const reducer = (stack: string[], action: Action): string[] => {
|
const reducer = (stack: string[], action: Action): string[] => {
|
||||||
switch (action.action) {
|
switch (action.action) {
|
||||||
case "push": {
|
case 'push': {
|
||||||
stack.push(action.componentId);
|
stack.push(action.componentId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "pop": {
|
case 'pop': {
|
||||||
const index = stack.indexOf(action.componentId);
|
const index = stack.indexOf(action.componentId);
|
||||||
if (index > -1) stack.splice(index, 1);
|
if (index > -1) stack.splice(index, 1);
|
||||||
break;
|
break;
|
||||||
@ -28,34 +28,27 @@ const reducer = (stack: string[], action: Action): string[] => {
|
|||||||
|
|
||||||
export const useIsScreenFocused = (componentId: string): boolean => {
|
export const useIsScreenFocused = (componentId: string): boolean => {
|
||||||
const [componentStack, dispatch] = useReducer(reducer, [componentId]);
|
const [componentStack, dispatch] = useReducer(reducer, [componentId]);
|
||||||
const isFocussed = useMemo(
|
const isFocussed = useMemo(() => componentStack[componentStack.length - 1] === componentId, [componentStack, componentId]);
|
||||||
() => componentStack[componentStack.length - 1] === componentId,
|
|
||||||
[componentStack, componentId]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = Navigation.events().registerComponentDidAppearListener(
|
const listener = Navigation.events().registerComponentDidAppearListener((event) => {
|
||||||
(event) => {
|
if (event.componentType !== 'Component') return;
|
||||||
if (event.componentType !== "Component") return;
|
dispatch({
|
||||||
dispatch({
|
action: 'push',
|
||||||
action: "push",
|
componentId: event.componentId,
|
||||||
componentId: event.componentId,
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => listener.remove();
|
return () => listener.remove();
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = Navigation.events().registerComponentDidDisappearListener(
|
const listener = Navigation.events().registerComponentDidDisappearListener((event) => {
|
||||||
(event) => {
|
if (event.componentType !== 'Component') return;
|
||||||
if (event.componentType !== "Component") return;
|
dispatch({
|
||||||
dispatch({
|
action: 'pop',
|
||||||
action: "pop",
|
componentId: event.componentId,
|
||||||
componentId: event.componentId,
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => listener.remove();
|
return () => listener.remove();
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import React, { useCallback, useMemo, useRef } from "react";
|
import React, { useCallback, useMemo, useRef } from 'react';
|
||||||
import { Platform, StyleSheet, View, ViewProps } from "react-native";
|
import { Platform, StyleSheet, View, ViewProps } from 'react-native';
|
||||||
import {
|
import {
|
||||||
PanGestureHandler,
|
PanGestureHandler,
|
||||||
PanGestureHandlerGestureEvent,
|
PanGestureHandlerGestureEvent,
|
||||||
State,
|
State,
|
||||||
TapGestureHandler,
|
TapGestureHandler,
|
||||||
TapGestureHandlerStateChangeEvent,
|
TapGestureHandlerStateChangeEvent,
|
||||||
} from "react-native-gesture-handler";
|
} from 'react-native-gesture-handler';
|
||||||
import Reanimated, {
|
import Reanimated, {
|
||||||
cancelAnimation,
|
cancelAnimation,
|
||||||
Easing,
|
Easing,
|
||||||
@ -18,28 +18,25 @@ import Reanimated, {
|
|||||||
useAnimatedGestureHandler,
|
useAnimatedGestureHandler,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
withRepeat,
|
withRepeat,
|
||||||
} from "react-native-reanimated";
|
} from 'react-native-reanimated';
|
||||||
import type { Camera, PhotoFile, TakePhotoOptions, TakeSnapshotOptions, VideoFile } from "react-native-vision-camera";
|
import type { Camera, PhotoFile, TakePhotoOptions, TakeSnapshotOptions, VideoFile } from 'react-native-vision-camera';
|
||||||
import { CAPTURE_BUTTON_SIZE, SCREEN_HEIGHT, SCREEN_WIDTH, USE_SNAPSHOT_ON_ANDROID } from "./../Constants";
|
import { CAPTURE_BUTTON_SIZE, SCREEN_HEIGHT, SCREEN_WIDTH, USE_SNAPSHOT_ON_ANDROID } from './../Constants';
|
||||||
|
|
||||||
const PAN_GESTURE_HANDLER_FAIL_X = [-SCREEN_WIDTH, SCREEN_WIDTH];
|
const PAN_GESTURE_HANDLER_FAIL_X = [-SCREEN_WIDTH, SCREEN_WIDTH];
|
||||||
const PAN_GESTURE_HANDLER_ACTIVE_Y = [-2, 2];
|
const PAN_GESTURE_HANDLER_ACTIVE_Y = [-2, 2];
|
||||||
|
|
||||||
const IS_ANDROID = Platform.OS === "android";
|
const IS_ANDROID = Platform.OS === 'android';
|
||||||
|
|
||||||
const START_RECORDING_DELAY = 200;
|
const START_RECORDING_DELAY = 200;
|
||||||
const BORDER_WIDTH = CAPTURE_BUTTON_SIZE * 0.1;
|
const BORDER_WIDTH = CAPTURE_BUTTON_SIZE * 0.1;
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
camera: React.RefObject<Camera>;
|
camera: React.RefObject<Camera>;
|
||||||
onMediaCaptured: (
|
onMediaCaptured: (media: PhotoFile | VideoFile, type: 'photo' | 'video') => void;
|
||||||
media: PhotoFile | VideoFile,
|
|
||||||
type: "photo" | "video"
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
cameraZoom: Reanimated.SharedValue<number>;
|
cameraZoom: Reanimated.SharedValue<number>;
|
||||||
|
|
||||||
flash: "off" | "on" | "auto";
|
flash: 'off' | 'on' | 'auto';
|
||||||
|
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|
||||||
@ -61,36 +58,29 @@ const _CaptureButton: React.FC<Props> = ({
|
|||||||
const recordingProgress = useSharedValue(0);
|
const recordingProgress = useSharedValue(0);
|
||||||
const takePhotoOptions = useMemo<TakePhotoOptions & TakeSnapshotOptions>(
|
const takePhotoOptions = useMemo<TakePhotoOptions & TakeSnapshotOptions>(
|
||||||
() => ({
|
() => ({
|
||||||
photoCodec: "jpeg",
|
photoCodec: 'jpeg',
|
||||||
qualityPrioritization: "speed",
|
qualityPrioritization: 'speed',
|
||||||
flash: flash,
|
flash: flash,
|
||||||
quality: 90,
|
quality: 90,
|
||||||
skipMetadata: true,
|
skipMetadata: true,
|
||||||
}),
|
}),
|
||||||
[flash]
|
[flash],
|
||||||
);
|
);
|
||||||
const isPressingButton = useSharedValue(false);
|
const isPressingButton = useSharedValue(false);
|
||||||
|
|
||||||
//#region Camera Capture
|
//#region Camera Capture
|
||||||
const takePhoto = useCallback(async () => {
|
const takePhoto = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
if (camera.current == null) throw new Error("Camera ref is null!");
|
if (camera.current == null) throw new Error('Camera ref is null!');
|
||||||
|
|
||||||
// If we're on Android and flash is disabled, we can use the "snapshot" method.
|
// If we're on Android and flash is disabled, we can use the "snapshot" method.
|
||||||
// this will take a snapshot of the current SurfaceView, which results in faster image
|
// this will take a snapshot of the current SurfaceView, which results in faster image
|
||||||
// capture rate at the cost of greatly reduced quality.
|
// capture rate at the cost of greatly reduced quality.
|
||||||
const photoMethod =
|
const photoMethod = USE_SNAPSHOT_ON_ANDROID && IS_ANDROID && takePhotoOptions.flash === 'off' ? 'snapshot' : 'photo';
|
||||||
USE_SNAPSHOT_ON_ANDROID &&
|
|
||||||
IS_ANDROID &&
|
|
||||||
takePhotoOptions.flash === "off"
|
|
||||||
? "snapshot"
|
|
||||||
: "photo";
|
|
||||||
console.log(`Taking ${photoMethod}...`);
|
console.log(`Taking ${photoMethod}...`);
|
||||||
const photo =
|
const photo =
|
||||||
photoMethod === "snapshot"
|
photoMethod === 'snapshot' ? await camera.current.takeSnapshot(takePhotoOptions) : await camera.current.takePhoto(takePhotoOptions);
|
||||||
? await camera.current.takeSnapshot(takePhotoOptions)
|
onMediaCaptured(photo, 'photo');
|
||||||
: await camera.current.takePhoto(takePhotoOptions);
|
|
||||||
onMediaCaptured(photo, "photo");
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to take photo!', e);
|
console.error('Failed to take photo!', e);
|
||||||
}
|
}
|
||||||
@ -99,24 +89,24 @@ const _CaptureButton: React.FC<Props> = ({
|
|||||||
const onStoppedRecording = useCallback(() => {
|
const onStoppedRecording = useCallback(() => {
|
||||||
isRecording.current = false;
|
isRecording.current = false;
|
||||||
cancelAnimation(recordingProgress);
|
cancelAnimation(recordingProgress);
|
||||||
console.log(`stopped recording video!`);
|
console.log('stopped recording video!');
|
||||||
}, [recordingProgress]);
|
}, [recordingProgress]);
|
||||||
const stopRecording = useCallback(async () => {
|
const stopRecording = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
if (camera.current == null) throw new Error("Camera ref is null!");
|
if (camera.current == null) throw new Error('Camera ref is null!');
|
||||||
|
|
||||||
console.log("calling stopRecording()...");
|
console.log('calling stopRecording()...');
|
||||||
await camera.current.stopRecording();
|
await camera.current.stopRecording();
|
||||||
console.log("called stopRecording()!");
|
console.log('called stopRecording()!');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`failed to stop recording!`, e);
|
console.error('failed to stop recording!', e);
|
||||||
}
|
}
|
||||||
}, [camera]);
|
}, [camera]);
|
||||||
const startRecording = useCallback(() => {
|
const startRecording = useCallback(() => {
|
||||||
try {
|
try {
|
||||||
if (camera.current == null) throw new Error("Camera ref is null!");
|
if (camera.current == null) throw new Error('Camera ref is null!');
|
||||||
|
|
||||||
console.log(`calling startRecording()...`);
|
console.log('calling startRecording()...');
|
||||||
camera.current.startRecording({
|
camera.current.startRecording({
|
||||||
flash: flash,
|
flash: flash,
|
||||||
onRecordingError: (error) => {
|
onRecordingError: (error) => {
|
||||||
@ -125,24 +115,17 @@ const _CaptureButton: React.FC<Props> = ({
|
|||||||
},
|
},
|
||||||
onRecordingFinished: (video) => {
|
onRecordingFinished: (video) => {
|
||||||
console.log(`Recording successfully finished! ${video.path}`);
|
console.log(`Recording successfully finished! ${video.path}`);
|
||||||
onMediaCaptured(video, "video");
|
onMediaCaptured(video, 'video');
|
||||||
onStoppedRecording();
|
onStoppedRecording();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// TODO: wait until startRecording returns to actually find out if the recording has successfully started
|
// TODO: wait until startRecording returns to actually find out if the recording has successfully started
|
||||||
console.log(`called startRecording()!`);
|
console.log('called startRecording()!');
|
||||||
isRecording.current = true;
|
isRecording.current = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`failed to start recording!`, e, "camera");
|
console.error('failed to start recording!', e, 'camera');
|
||||||
}
|
}
|
||||||
}, [
|
}, [camera, flash, onMediaCaptured, onStoppedRecording, recordingProgress, stopRecording]);
|
||||||
camera,
|
|
||||||
flash,
|
|
||||||
onMediaCaptured,
|
|
||||||
onStoppedRecording,
|
|
||||||
recordingProgress,
|
|
||||||
stopRecording,
|
|
||||||
]);
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Tap handler
|
//#region Tap handler
|
||||||
@ -180,8 +163,7 @@ const _CaptureButton: React.FC<Props> = ({
|
|||||||
case State.CANCELLED: {
|
case State.CANCELLED: {
|
||||||
// exit "recording mode"
|
// exit "recording mode"
|
||||||
try {
|
try {
|
||||||
if (pressDownDate.current == null)
|
if (pressDownDate.current == null) throw new Error('PressDownDate ref .current was null!');
|
||||||
throw new Error("PressDownDate ref .current was null!");
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diff = now.getTime() - pressDownDate.current.getTime();
|
const diff = now.getTime() - pressDownDate.current.getTime();
|
||||||
pressDownDate.current = undefined;
|
pressDownDate.current = undefined;
|
||||||
@ -204,46 +186,26 @@ const _CaptureButton: React.FC<Props> = ({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[isPressingButton, recordingProgress, setIsPressingButton, startRecording, stopRecording, takePhoto],
|
||||||
isPressingButton,
|
|
||||||
recordingProgress,
|
|
||||||
setIsPressingButton,
|
|
||||||
startRecording,
|
|
||||||
stopRecording,
|
|
||||||
takePhoto,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region Pan handler
|
//#region Pan handler
|
||||||
const panHandler = useRef<PanGestureHandler>();
|
const panHandler = useRef<PanGestureHandler>();
|
||||||
const onPanGestureEvent = useAnimatedGestureHandler<
|
const onPanGestureEvent = useAnimatedGestureHandler<PanGestureHandlerGestureEvent, { offsetY?: number; startY?: number }>({
|
||||||
PanGestureHandlerGestureEvent,
|
|
||||||
{ offsetY?: number; startY?: number }
|
|
||||||
>({
|
|
||||||
onStart: (event, context) => {
|
onStart: (event, context) => {
|
||||||
context.startY = event.absoluteY;
|
context.startY = event.absoluteY;
|
||||||
const yForFullZoom = context.startY * 0.7;
|
const yForFullZoom = context.startY * 0.7;
|
||||||
const offsetYForFullZoom = context.startY - yForFullZoom;
|
const offsetYForFullZoom = context.startY - yForFullZoom;
|
||||||
|
|
||||||
// extrapolate [0 ... 1] zoom -> [0 ... Y_FOR_FULL_ZOOM] finger position
|
// extrapolate [0 ... 1] zoom -> [0 ... Y_FOR_FULL_ZOOM] finger position
|
||||||
context.offsetY = interpolate(
|
context.offsetY = interpolate(Math.sqrt(cameraZoom.value), [0, 1], [0, offsetYForFullZoom], Extrapolate.CLAMP);
|
||||||
Math.sqrt(cameraZoom.value),
|
|
||||||
[0, 1],
|
|
||||||
[0, offsetYForFullZoom],
|
|
||||||
Extrapolate.CLAMP
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
onActive: (event, context) => {
|
onActive: (event, context) => {
|
||||||
const offset = context.offsetY ?? 0;
|
const offset = context.offsetY ?? 0;
|
||||||
const startY = context.startY ?? SCREEN_HEIGHT;
|
const startY = context.startY ?? SCREEN_HEIGHT;
|
||||||
const yForFullZoom = startY * 0.7;
|
const yForFullZoom = startY * 0.7;
|
||||||
|
|
||||||
const zoom = interpolate(
|
const zoom = interpolate(event.absoluteY - offset, [yForFullZoom, startY], [1, 0], Extrapolate.CLAMP);
|
||||||
event.absoluteY - offset,
|
|
||||||
[yForFullZoom, startY],
|
|
||||||
[1, 0],
|
|
||||||
Extrapolate.CLAMP
|
|
||||||
);
|
|
||||||
cameraZoom.value = zoom ** 2;
|
cameraZoom.value = zoom ** 2;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -261,44 +223,45 @@ const _CaptureButton: React.FC<Props> = ({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
[isPressingButton]
|
[isPressingButton],
|
||||||
);
|
);
|
||||||
const buttonStyle = useAnimatedStyle(
|
const buttonStyle = useAnimatedStyle(() => {
|
||||||
() => {
|
let scale: number;
|
||||||
let scale: number;
|
if (enabled) {
|
||||||
if (enabled) {
|
if (isPressingButton.value) {
|
||||||
if (isPressingButton.value) {
|
scale = withRepeat(
|
||||||
scale = withRepeat(withSpring(1, {
|
withSpring(1, {
|
||||||
stiffness: 100,
|
stiffness: 100,
|
||||||
damping: 1000,
|
damping: 1000,
|
||||||
}), -1, true);
|
}),
|
||||||
} else {
|
-1,
|
||||||
scale = withSpring(0.9, {
|
true,
|
||||||
stiffness: 500,
|
);
|
||||||
damping: 300,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
scale = withSpring(0.6, {
|
scale = withSpring(0.9, {
|
||||||
stiffness: 500,
|
stiffness: 500,
|
||||||
damping: 300,
|
damping: 300,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
scale = withSpring(0.6, {
|
||||||
|
stiffness: 500,
|
||||||
|
damping: 300,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
opacity: withTiming(enabled ? 1 : 0.3, {
|
opacity: withTiming(enabled ? 1 : 0.3, {
|
||||||
duration: 100,
|
duration: 100,
|
||||||
easing: Easing.linear,
|
easing: Easing.linear,
|
||||||
}),
|
}),
|
||||||
transform: [
|
transform: [
|
||||||
{
|
{
|
||||||
scale: scale
|
scale: scale,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
};
|
||||||
},
|
}, [enabled, isPressingButton]);
|
||||||
[enabled, isPressingButton]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TapGestureHandler
|
<TapGestureHandler
|
||||||
@ -333,17 +296,17 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
shadow: {
|
shadow: {
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
width: CAPTURE_BUTTON_SIZE,
|
width: CAPTURE_BUTTON_SIZE,
|
||||||
height: CAPTURE_BUTTON_SIZE,
|
height: CAPTURE_BUTTON_SIZE,
|
||||||
borderRadius: CAPTURE_BUTTON_SIZE / 2,
|
borderRadius: CAPTURE_BUTTON_SIZE / 2,
|
||||||
backgroundColor: "#e34077",
|
backgroundColor: '#e34077',
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
width: CAPTURE_BUTTON_SIZE,
|
width: CAPTURE_BUTTON_SIZE,
|
||||||
height: CAPTURE_BUTTON_SIZE,
|
height: CAPTURE_BUTTON_SIZE,
|
||||||
borderRadius: CAPTURE_BUTTON_SIZE / 2,
|
borderRadius: CAPTURE_BUTTON_SIZE / 2,
|
||||||
borderWidth: BORDER_WIDTH,
|
borderWidth: BORDER_WIDTH,
|
||||||
borderColor: "white",
|
borderColor: 'white',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,5 @@
|
|||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from 'react';
|
||||||
import {
|
import { PressableProps, Pressable, PressableStateCallbackType, StyleProp, ViewStyle } from 'react-native';
|
||||||
PressableProps,
|
|
||||||
Pressable,
|
|
||||||
PressableStateCallbackType,
|
|
||||||
StyleProp,
|
|
||||||
ViewStyle,
|
|
||||||
} from "react-native";
|
|
||||||
|
|
||||||
export interface PressableOpacityProps extends PressableProps {
|
export interface PressableOpacityProps extends PressableProps {
|
||||||
/**
|
/**
|
||||||
@ -22,9 +16,7 @@ export interface PressableOpacityProps extends PressableProps {
|
|||||||
activeOpacity?: number;
|
activeOpacity?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StyleType = (
|
export type StyleType = (state: PressableStateCallbackType) => StyleProp<ViewStyle>;
|
||||||
state: PressableStateCallbackType
|
|
||||||
) => StyleProp<ViewStyle>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Pressable component that lowers opacity when in pressed state. Uses the JS Pressability API.
|
* A Pressable component that lowers opacity when in pressed state. Uses the JS Pressability API.
|
||||||
@ -45,12 +37,9 @@ export const PressableOpacity = ({
|
|||||||
else return 1;
|
else return 1;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeOpacity, disabled, disabledOpacity]
|
[activeOpacity, disabled, disabledOpacity],
|
||||||
);
|
|
||||||
const _style = useCallback<StyleType>(
|
|
||||||
({ pressed }) => [style as ViewStyle, { opacity: getOpacity(pressed) }],
|
|
||||||
[getOpacity, style]
|
|
||||||
);
|
);
|
||||||
|
const _style = useCallback<StyleType>(({ pressed }) => [style as ViewStyle, { opacity: getOpacity(pressed) }], [getOpacity, style]);
|
||||||
|
|
||||||
return <Pressable style={_style} disabled={disabled} {...passThroughProps} />;
|
return <Pressable style={_style} disabled={disabled} {...passThroughProps} />;
|
||||||
};
|
};
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import { BlurView, BlurViewProperties } from "@react-native-community/blur";
|
import { BlurView, BlurViewProperties } from '@react-native-community/blur';
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { Platform, StyleSheet } from "react-native";
|
import { Platform, StyleSheet } from 'react-native';
|
||||||
import StaticSafeAreaInsets from "react-native-static-safe-area-insets";
|
import StaticSafeAreaInsets from 'react-native-static-safe-area-insets';
|
||||||
|
|
||||||
const FALLBACK_COLOR = "rgba(140, 140, 140, 0.3)";
|
const FALLBACK_COLOR = 'rgba(140, 140, 140, 0.3)';
|
||||||
|
|
||||||
const StatusBarBlurBackgroundImpl = ({
|
const StatusBarBlurBackgroundImpl = ({ style, ...props }: BlurViewProperties) => {
|
||||||
style,
|
|
||||||
...props
|
|
||||||
}: BlurViewProperties) => {
|
|
||||||
if (Platform.OS !== 'ios') return null;
|
if (Platform.OS !== 'ios') return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -26,7 +23,7 @@ export const StatusBarBlurBackground = React.memo(StatusBarBlurBackgroundImpl);
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
statusBarBackground: {
|
statusBarBackground: {
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
|
@ -12,15 +12,15 @@
|
|||||||
* * `"hevc-alpha"`: The HEVC (`muxa`) video codec that supports an alpha channel. This constant is used to select the appropriate encoder, but is NOT used on the encoded content, which is backwards compatible and hence uses `"hvc1"` as its codec type. _(iOS 13.0+)_
|
* * `"hevc-alpha"`: The HEVC (`muxa`) video codec that supports an alpha channel. This constant is used to select the appropriate encoder, but is NOT used on the encoded content, which is backwards compatible and hence uses `"hvc1"` as its codec type. _(iOS 13.0+)_
|
||||||
*/
|
*/
|
||||||
export type CameraVideoCodec =
|
export type CameraVideoCodec =
|
||||||
| "h264"
|
| 'h264'
|
||||||
| "hevc"
|
| 'hevc'
|
||||||
| "hevc-alpha"
|
| 'hevc-alpha'
|
||||||
| "jpeg"
|
| 'jpeg'
|
||||||
| "pro-res-4444"
|
| 'pro-res-4444'
|
||||||
| "pro-res-422"
|
| 'pro-res-422'
|
||||||
| "pro-res-422-hq"
|
| 'pro-res-422-hq'
|
||||||
| "pro-res-422-lt"
|
| 'pro-res-422-lt'
|
||||||
| "pro-res-422-proxy";
|
| 'pro-res-422-proxy';
|
||||||
|
|
||||||
// TODO: Support RAW photo codec
|
// TODO: Support RAW photo codec
|
||||||
/**
|
/**
|
||||||
@ -30,4 +30,4 @@ export type CameraVideoCodec =
|
|||||||
* * `"jpeg"`: The JPEG (`jpeg`) video codec. _(iOS 11.0+)_
|
* * `"jpeg"`: The JPEG (`jpeg`) video codec. _(iOS 11.0+)_
|
||||||
* * `"hevc-alpha"`: The HEVC (`muxa`) video codec that supports an alpha channel. This constant is used to select the appropriate encoder, but is NOT used on the encoded content, which is backwards compatible and hence uses `"hvc1"` as its codec type. _(iOS 13.0+)_
|
* * `"hevc-alpha"`: The HEVC (`muxa`) video codec that supports an alpha channel. This constant is used to select the appropriate encoder, but is NOT used on the encoded content, which is backwards compatible and hence uses `"hvc1"` as its codec type. _(iOS 13.0+)_
|
||||||
*/
|
*/
|
||||||
export type CameraPhotoCodec = "hevc" | "jpeg" | "hevc-alpha";
|
export type CameraPhotoCodec = 'hevc' | 'jpeg' | 'hevc-alpha';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { CameraPosition } from "./CameraPosition";
|
import type { CameraPosition } from './CameraPosition';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indentifiers for a physical camera (one that actually exists on the back/front of the device)
|
* Indentifiers for a physical camera (one that actually exists on the back/front of the device)
|
||||||
@ -7,10 +7,7 @@ import type { CameraPosition } from "./CameraPosition";
|
|||||||
* * `"wide-angle-camera"`: A built-in wide-angle camera. (focal length between 24mm and 35mm)
|
* * `"wide-angle-camera"`: A built-in wide-angle camera. (focal length between 24mm and 35mm)
|
||||||
* * `"telephoto-camera"`: A built-in camera device with a longer focal length than a wide-angle camera. (focal length between above 85mm)
|
* * `"telephoto-camera"`: A built-in camera device with a longer focal length than a wide-angle camera. (focal length between above 85mm)
|
||||||
*/
|
*/
|
||||||
export type PhysicalCameraDeviceType =
|
export type PhysicalCameraDeviceType = 'ultra-wide-angle-camera' | 'wide-angle-camera' | 'telephoto-camera';
|
||||||
| "ultra-wide-angle-camera"
|
|
||||||
| "wide-angle-camera"
|
|
||||||
| "telephoto-camera";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indentifiers for a logical camera (Combinations of multiple physical cameras to create a single logical camera).
|
* Indentifiers for a logical camera (Combinations of multiple physical cameras to create a single logical camera).
|
||||||
@ -20,38 +17,24 @@ export type PhysicalCameraDeviceType =
|
|||||||
* * `"triple-camera"`: A device that consists of three cameras of fixed focal length, one ultrawide angle, one wide angle, and one telephoto.
|
* * `"triple-camera"`: A device that consists of three cameras of fixed focal length, one ultrawide angle, one wide angle, and one telephoto.
|
||||||
* * `"true-depth-camera"`: A combination of cameras and other sensors that creates a capture device capable of photo, video, and depth capture.
|
* * `"true-depth-camera"`: A combination of cameras and other sensors that creates a capture device capable of photo, video, and depth capture.
|
||||||
*/
|
*/
|
||||||
export type LogicalCameraDeviceType =
|
export type LogicalCameraDeviceType = 'dual-camera' | 'dual-wide-camera' | 'triple-camera' | 'true-depth-camera';
|
||||||
| "dual-camera"
|
|
||||||
| "dual-wide-camera"
|
|
||||||
| "triple-camera"
|
|
||||||
| "true-depth-camera";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses an array of physical device types into a single `PhysicalCameraDeviceType` or `LogicalCameraDeviceType`, depending what matches.
|
* Parses an array of physical device types into a single `PhysicalCameraDeviceType` or `LogicalCameraDeviceType`, depending what matches.
|
||||||
*/
|
*/
|
||||||
export const parsePhysicalDeviceTypes = (
|
export const parsePhysicalDeviceTypes = (physicalDeviceTypes: PhysicalCameraDeviceType[]): PhysicalCameraDeviceType | LogicalCameraDeviceType => {
|
||||||
physicalDeviceTypes: PhysicalCameraDeviceType[]
|
if (physicalDeviceTypes.length === 1) return physicalDeviceTypes[0];
|
||||||
): PhysicalCameraDeviceType | LogicalCameraDeviceType => {
|
|
||||||
if (physicalDeviceTypes.length === 1) {
|
const hasWide = physicalDeviceTypes.includes('wide-angle-camera');
|
||||||
return physicalDeviceTypes[0];
|
const hasUltra = physicalDeviceTypes.includes('ultra-wide-angle-camera');
|
||||||
}
|
const hasTele = physicalDeviceTypes.includes('telephoto-camera');
|
||||||
const hasWide = physicalDeviceTypes.includes("wide-angle-camera");
|
if (hasTele && hasWide && hasUltra) return 'triple-camera';
|
||||||
const hasUltra = physicalDeviceTypes.includes("ultra-wide-angle-camera");
|
|
||||||
const hasTele = physicalDeviceTypes.includes("telephoto-camera");
|
if (hasWide && hasUltra) return 'dual-wide-camera';
|
||||||
if (hasTele && hasWide && hasUltra) {
|
|
||||||
return "triple-camera";
|
if (hasWide && hasTele) return 'dual-camera';
|
||||||
}
|
|
||||||
if (hasWide && hasUltra) {
|
throw new Error(`Invalid physical device type combination! ${physicalDeviceTypes.join(' + ')}`);
|
||||||
return "dual-wide-camera";
|
|
||||||
}
|
|
||||||
if (hasWide && hasTele) {
|
|
||||||
return "dual-camera";
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
`Invalid physical device type combination! ${physicalDeviceTypes.join(
|
|
||||||
" + "
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,7 +48,7 @@ export const parsePhysicalDeviceTypes = (
|
|||||||
* #### The following colorspaces are available on Android:
|
* #### The following colorspaces are available on Android:
|
||||||
* * `"yuv"`: The YCbCr color space.
|
* * `"yuv"`: The YCbCr color space.
|
||||||
*/
|
*/
|
||||||
export type ColorSpace = "hlg-bt2020" | "p3-d65" | "srgb" | "yuv";
|
export type ColorSpace = 'hlg-bt2020' | 'p3-d65' | 'srgb' | 'yuv';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates a format's autofocus system.
|
* Indicates a format's autofocus system.
|
||||||
@ -74,7 +57,7 @@ export type ColorSpace = "hlg-bt2020" | "p3-d65" | "srgb" | "yuv";
|
|||||||
* * `"contrast-detection"`: Indicates that autofocus is achieved by contrast detection. Contrast detection performs a focus scan to find the optimal position
|
* * `"contrast-detection"`: Indicates that autofocus is achieved by contrast detection. Contrast detection performs a focus scan to find the optimal position
|
||||||
* * `"phase-detection"`: Indicates that autofocus is achieved by phase detection. Phase detection has the ability to achieve focus in many cases without a focus scan. Phase detection autofocus is typically less visually intrusive than contrast detection autofocus
|
* * `"phase-detection"`: Indicates that autofocus is achieved by phase detection. Phase detection has the ability to achieve focus in many cases without a focus scan. Phase detection autofocus is typically less visually intrusive than contrast detection autofocus
|
||||||
*/
|
*/
|
||||||
export type AutoFocusSystem = "contrast-detection" | "phase-detection" | "none";
|
export type AutoFocusSystem = 'contrast-detection' | 'phase-detection' | 'none';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates a format's supported video stabilization mode
|
* Indicates a format's supported video stabilization mode
|
||||||
@ -85,12 +68,7 @@ export type AutoFocusSystem = "contrast-detection" | "phase-detection" | "none";
|
|||||||
* * `"cinematic-extended"`: Indicates that the video should be stabilized using the extended cinematic stabilization algorithm. Enabling extended cinematic stabilization introduces longer latency into the video capture pipeline compared to the AVCaptureVideoStabilizationModeCinematic and consumes more memory, but yields improved stability. It is recommended to use identical or similar min and max frame durations in conjunction with this mode (iOS 13.0+)
|
* * `"cinematic-extended"`: Indicates that the video should be stabilized using the extended cinematic stabilization algorithm. Enabling extended cinematic stabilization introduces longer latency into the video capture pipeline compared to the AVCaptureVideoStabilizationModeCinematic and consumes more memory, but yields improved stability. It is recommended to use identical or similar min and max frame durations in conjunction with this mode (iOS 13.0+)
|
||||||
* * `"auto"`: Indicates that the most appropriate video stabilization mode for the device and format should be chosen automatically
|
* * `"auto"`: Indicates that the most appropriate video stabilization mode for the device and format should be chosen automatically
|
||||||
*/
|
*/
|
||||||
export type VideoStabilizationMode =
|
export type VideoStabilizationMode = 'off' | 'standard' | 'cinematic' | 'cinematic-extended' | 'auto';
|
||||||
| "off"
|
|
||||||
| "standard"
|
|
||||||
| "cinematic"
|
|
||||||
| "cinematic-extended"
|
|
||||||
| "auto";
|
|
||||||
|
|
||||||
export type FrameRateRange = Readonly<{
|
export type FrameRateRange = Readonly<{
|
||||||
minFrameRate: number;
|
minFrameRate: number;
|
||||||
|
@ -1,42 +1,40 @@
|
|||||||
export type PermissionError =
|
export type PermissionError = 'permission/microphone-permission-denied' | 'permission/camera-permission-denied';
|
||||||
| "permission/microphone-permission-denied"
|
|
||||||
| "permission/camera-permission-denied";
|
|
||||||
export type ParameterError =
|
export type ParameterError =
|
||||||
| "parameter/invalid-parameter"
|
| 'parameter/invalid-parameter'
|
||||||
| "parameter/unsupported-os"
|
| 'parameter/unsupported-os'
|
||||||
| "parameter/unsupported-output"
|
| 'parameter/unsupported-output'
|
||||||
| "parameter/unsupported-input"
|
| 'parameter/unsupported-input'
|
||||||
| "parameter/invalid-combination";
|
| 'parameter/invalid-combination';
|
||||||
export type DeviceError =
|
export type DeviceError =
|
||||||
| "device/configuration-error"
|
| 'device/configuration-error'
|
||||||
| "device/no-device"
|
| 'device/no-device'
|
||||||
| "device/invalid-device"
|
| 'device/invalid-device'
|
||||||
| "device/torch-unavailable"
|
| 'device/torch-unavailable'
|
||||||
| "device/microphone-unavailable"
|
| 'device/microphone-unavailable'
|
||||||
| "device/low-light-boost-not-supported"
|
| 'device/low-light-boost-not-supported'
|
||||||
| "device/focus-not-supported"
|
| 'device/focus-not-supported'
|
||||||
| "device/camera-not-available-on-simulator";
|
| 'device/camera-not-available-on-simulator';
|
||||||
export type FormatError =
|
export type FormatError =
|
||||||
| "format/invalid-fps"
|
| 'format/invalid-fps'
|
||||||
| "format/invalid-hdr"
|
| 'format/invalid-hdr'
|
||||||
| "format/invalid-low-light-boost"
|
| 'format/invalid-low-light-boost'
|
||||||
| "format/invalid-format"
|
| 'format/invalid-format'
|
||||||
| "format/invalid-preset";
|
| 'format/invalid-preset';
|
||||||
export type SessionError = "session/camera-not-ready";
|
export type SessionError = 'session/camera-not-ready';
|
||||||
export type CaptureError =
|
export type CaptureError =
|
||||||
| "capture/invalid-photo-format"
|
| 'capture/invalid-photo-format'
|
||||||
| "capture/encoder-error"
|
| 'capture/encoder-error'
|
||||||
| "capture/muxer-error"
|
| 'capture/muxer-error'
|
||||||
| "capture/recording-in-progress"
|
| 'capture/recording-in-progress'
|
||||||
| "capture/no-recording-in-progress"
|
| 'capture/no-recording-in-progress'
|
||||||
| "capture/file-io-error"
|
| 'capture/file-io-error'
|
||||||
| "capture/create-temp-file-error"
|
| 'capture/create-temp-file-error'
|
||||||
| "capture/invalid-photo-codec"
|
| 'capture/invalid-photo-codec'
|
||||||
| "capture/not-bound-error"
|
| 'capture/not-bound-error'
|
||||||
| "capture/capture-type-not-supported"
|
| 'capture/capture-type-not-supported'
|
||||||
| "capture/unknown";
|
| 'capture/unknown';
|
||||||
export type SystemError = "system/no-camera-manager";
|
export type SystemError = 'system/no-camera-manager';
|
||||||
export type UnknownError = "unknown/unknown";
|
export type UnknownError = 'unknown/unknown';
|
||||||
|
|
||||||
export interface ErrorWithCause {
|
export interface ErrorWithCause {
|
||||||
/**
|
/**
|
||||||
@ -69,15 +67,7 @@ export interface ErrorWithCause {
|
|||||||
cause?: ErrorWithCause;
|
cause?: ErrorWithCause;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CameraErrorCode =
|
type CameraErrorCode = PermissionError | ParameterError | DeviceError | FormatError | SessionError | CaptureError | SystemError | UnknownError;
|
||||||
| PermissionError
|
|
||||||
| ParameterError
|
|
||||||
| DeviceError
|
|
||||||
| FormatError
|
|
||||||
| SessionError
|
|
||||||
| CaptureError
|
|
||||||
| SystemError
|
|
||||||
| UnknownError;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents any kind of error that occured in the Camera View Module.
|
* Represents any kind of error that occured in the Camera View Module.
|
||||||
@ -98,7 +88,7 @@ class CameraError<TCode extends CameraErrorCode> extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(code: TCode, message: string, cause?: ErrorWithCause) {
|
constructor(code: TCode, message: string, cause?: ErrorWithCause) {
|
||||||
super(`[${code}]: ${message}${cause ? ` (Cause: ${cause.message})` : ""}`);
|
super(`[${code}]: ${message}${cause ? ` (Cause: ${cause.message})` : ''}`);
|
||||||
this._code = code;
|
this._code = code;
|
||||||
this._message = message;
|
this._message = message;
|
||||||
this._cause = cause;
|
this._cause = cause;
|
||||||
@ -114,58 +104,44 @@ export class CameraCaptureError extends CameraError<CaptureError> {}
|
|||||||
* Represents any kind of error that occured in the Camera View Module.
|
* Represents any kind of error that occured in the Camera View Module.
|
||||||
*/
|
*/
|
||||||
export class CameraRuntimeError extends CameraError<
|
export class CameraRuntimeError extends CameraError<
|
||||||
| PermissionError
|
PermissionError | ParameterError | DeviceError | FormatError | SessionError | SystemError | UnknownError
|
||||||
| ParameterError
|
|
||||||
| DeviceError
|
|
||||||
| FormatError
|
|
||||||
| SessionError
|
|
||||||
| SystemError
|
|
||||||
| UnknownError
|
|
||||||
> {}
|
> {}
|
||||||
|
|
||||||
export const isErrorWithCause = (error: unknown): error is ErrorWithCause =>
|
export const isErrorWithCause = (error: unknown): error is ErrorWithCause =>
|
||||||
typeof error === "object" &&
|
typeof error === 'object' &&
|
||||||
error != null &&
|
error != null &&
|
||||||
// @ts-expect-error error is still unknown
|
// @ts-expect-error error is still unknown
|
||||||
typeof error.message === "string" &&
|
typeof error.message === 'string' &&
|
||||||
// @ts-expect-error error is still unknown
|
// @ts-expect-error error is still unknown
|
||||||
(typeof error.stacktrace === "string" || error.stacktrace == null) &&
|
(typeof error.stacktrace === 'string' || error.stacktrace == null) &&
|
||||||
// @ts-expect-error error is still unknown
|
// @ts-expect-error error is still unknown
|
||||||
(isErrorWithCause(error.cause) || error.cause == null);
|
(isErrorWithCause(error.cause) || error.cause == null);
|
||||||
|
|
||||||
const isCameraErrorJson = (
|
const isCameraErrorJson = (error: unknown): error is { code: string; message: string; cause?: ErrorWithCause } =>
|
||||||
error: unknown
|
typeof error === 'object' &&
|
||||||
): error is { code: string; message: string; cause?: ErrorWithCause } =>
|
|
||||||
typeof error === "object" &&
|
|
||||||
error != null &&
|
error != null &&
|
||||||
// @ts-expect-error error is still unknown
|
// @ts-expect-error error is still unknown
|
||||||
typeof error.code === "string" &&
|
typeof error.code === 'string' &&
|
||||||
// @ts-expect-error error is still unknown
|
// @ts-expect-error error is still unknown
|
||||||
typeof error.message === "string" &&
|
typeof error.message === 'string' &&
|
||||||
// @ts-expect-error error is still unknown
|
// @ts-expect-error error is still unknown
|
||||||
(typeof error.cause === "object" || error.cause == null);
|
(typeof error.cause === 'object' || error.cause == null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to parse an error coming from native to a typed JS camera error.
|
* Tries to parse an error coming from native to a typed JS camera error.
|
||||||
* @param nativeError The native error instance. This is a JSON in the legacy native module architecture.
|
* @param nativeError The native error instance. This is a JSON in the legacy native module architecture.
|
||||||
* @returns A `CameraRuntimeError` or `CameraCaptureError`, or the nativeError if it's not parsable
|
* @returns A `CameraRuntimeError` or `CameraCaptureError`, or the nativeError if it's not parsable
|
||||||
*/
|
*/
|
||||||
export const tryParseNativeCameraError = <T>(
|
export const tryParseNativeCameraError = <T>(nativeError: T): (CameraRuntimeError | CameraCaptureError) | T => {
|
||||||
nativeError: T
|
|
||||||
): (CameraRuntimeError | CameraCaptureError) | T => {
|
|
||||||
if (isCameraErrorJson(nativeError)) {
|
if (isCameraErrorJson(nativeError)) {
|
||||||
if (nativeError.code.startsWith("capture")) {
|
if (nativeError.code.startsWith('capture')) {
|
||||||
return new CameraCaptureError(
|
return new CameraCaptureError(nativeError.code as CaptureError, nativeError.message, nativeError.cause);
|
||||||
nativeError.code as CaptureError,
|
|
||||||
nativeError.message,
|
|
||||||
nativeError.cause
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return new CameraRuntimeError(
|
return new CameraRuntimeError(
|
||||||
// @ts-expect-error the code is string, we narrow it down to TS union.
|
// @ts-expect-error the code is string, we narrow it down to TS union.
|
||||||
nativeError.code,
|
nativeError.code,
|
||||||
nativeError.message,
|
nativeError.message,
|
||||||
nativeError.cause
|
nativeError.cause,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -10,4 +10,4 @@
|
|||||||
* #### Android only
|
* #### Android only
|
||||||
* * `"external"`: The camera device is an external camera, and has no fixed facing relative to the device's screen. (Android only)
|
* * `"external"`: The camera device is an external camera, and has no fixed facing relative to the device's screen. (Android only)
|
||||||
*/
|
*/
|
||||||
export type CameraPosition = "front" | "back" | "unspecified" | "external";
|
export type CameraPosition = 'front' | 'back' | 'unspecified' | 'external';
|
||||||
|
@ -15,15 +15,15 @@
|
|||||||
* * `"vga-640x480"`: Specifies capture settings suitable for VGA quality (640 x 480 pixel) video output.
|
* * `"vga-640x480"`: Specifies capture settings suitable for VGA quality (640 x 480 pixel) video output.
|
||||||
*/
|
*/
|
||||||
export type CameraPreset =
|
export type CameraPreset =
|
||||||
| "cif-352x288"
|
| 'cif-352x288'
|
||||||
| "hd-1280x720"
|
| 'hd-1280x720'
|
||||||
| "hd-1920x1080"
|
| 'hd-1920x1080'
|
||||||
| "hd-3840x2160"
|
| 'hd-3840x2160'
|
||||||
| "high"
|
| 'high'
|
||||||
| "iframe-1280x720"
|
| 'iframe-1280x720'
|
||||||
| "iframe-960x540"
|
| 'iframe-960x540'
|
||||||
| "input-priority"
|
| 'input-priority'
|
||||||
| "low"
|
| 'low'
|
||||||
| "medium"
|
| 'medium'
|
||||||
| "photo"
|
| 'photo'
|
||||||
| "vga-640x480";
|
| 'vga-640x480';
|
||||||
|
36
src/Code.ts
36
src/Code.ts
@ -2,24 +2,24 @@
|
|||||||
* Available code types
|
* Available code types
|
||||||
*/
|
*/
|
||||||
export type CodeType =
|
export type CodeType =
|
||||||
| "cat-body"
|
| 'cat-body'
|
||||||
| "dog-body"
|
| 'dog-body'
|
||||||
| "human-body"
|
| 'human-body'
|
||||||
| "salient-object"
|
| 'salient-object'
|
||||||
| "aztec"
|
| 'aztec'
|
||||||
| "code-128"
|
| 'code-128'
|
||||||
| "code-39"
|
| 'code-39'
|
||||||
| "code-39-mod-43"
|
| 'code-39-mod-43'
|
||||||
| "code-93"
|
| 'code-93'
|
||||||
| "data-matrix"
|
| 'data-matrix'
|
||||||
| "ean-13"
|
| 'ean-13'
|
||||||
| "ean-8"
|
| 'ean-8'
|
||||||
| "face"
|
| 'face'
|
||||||
| "interleaved-2-of-5"
|
| 'interleaved-2-of-5'
|
||||||
| "itf-14"
|
| 'itf-14'
|
||||||
| "pdf-417"
|
| 'pdf-417'
|
||||||
| "qr"
|
| 'qr'
|
||||||
| "upce";
|
| 'upce';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a File in the local filesystem.
|
* Represents a File in the local filesystem.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { CameraPhotoCodec } from "./CameraCodec";
|
import type { CameraPhotoCodec } from './CameraCodec';
|
||||||
import type { TemporaryFile } from "./TemporaryFile";
|
import type { TemporaryFile } from './TemporaryFile';
|
||||||
|
|
||||||
export interface TakePhotoOptions {
|
export interface TakePhotoOptions {
|
||||||
/**
|
/**
|
||||||
@ -18,13 +18,13 @@ export interface TakePhotoOptions {
|
|||||||
* @platform iOS 13.0+
|
* @platform iOS 13.0+
|
||||||
* @default "balanced"
|
* @default "balanced"
|
||||||
*/
|
*/
|
||||||
qualityPrioritization?: "quality" | "balanced" | "speed";
|
qualityPrioritization?: 'quality' | 'balanced' | 'speed';
|
||||||
/**
|
/**
|
||||||
* Whether the Flash should be enabled or disabled
|
* Whether the Flash should be enabled or disabled
|
||||||
*
|
*
|
||||||
* @default "auto"
|
* @default "auto"
|
||||||
*/
|
*/
|
||||||
flash?: "on" | "off" | "auto";
|
flash?: 'on' | 'off' | 'auto';
|
||||||
/**
|
/**
|
||||||
* Specifies whether red-eye reduction should be applied automatically on flash captures.
|
* Specifies whether red-eye reduction should be applied automatically on flash captures.
|
||||||
*
|
*
|
||||||
@ -86,8 +86,8 @@ export type PhotoFile = Readonly<
|
|||||||
*
|
*
|
||||||
* @platform iOS
|
* @platform iOS
|
||||||
*/
|
*/
|
||||||
"{MakerApple}"?: Record<string, unknown>;
|
'{MakerApple}'?: Record<string, unknown>;
|
||||||
"{TIFF}": {
|
'{TIFF}': {
|
||||||
ResolutionUnit: number;
|
ResolutionUnit: number;
|
||||||
Software: string;
|
Software: string;
|
||||||
Make: string;
|
Make: string;
|
||||||
@ -100,7 +100,7 @@ export type PhotoFile = Readonly<
|
|||||||
Model: string;
|
Model: string;
|
||||||
YResolution: number;
|
YResolution: number;
|
||||||
};
|
};
|
||||||
"{Exif}": {
|
'{Exif}': {
|
||||||
DateTimeOriginal: string;
|
DateTimeOriginal: string;
|
||||||
ExposureTime: number;
|
ExposureTime: number;
|
||||||
FNumber: number;
|
FNumber: number;
|
||||||
|
@ -26,14 +26,14 @@
|
|||||||
// maxKeyFrameIntervalDuration?: TCodec extends "h264" ? number : never;
|
// maxKeyFrameIntervalDuration?: TCodec extends "h264" ? number : never;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
import type { CameraCaptureError } from "./CameraError";
|
import type { CameraCaptureError } from './CameraError';
|
||||||
import type { TemporaryFile } from "./TemporaryFile";
|
import type { TemporaryFile } from './TemporaryFile';
|
||||||
|
|
||||||
export interface RecordVideoOptions {
|
export interface RecordVideoOptions {
|
||||||
/**
|
/**
|
||||||
* Set the video flash mode. Natively, this just enables the torch while recording.
|
* Set the video flash mode. Natively, this just enables the torch while recording.
|
||||||
*/
|
*/
|
||||||
flash?: "on" | "off" | "auto";
|
flash?: 'on' | 'off' | 'auto';
|
||||||
/**
|
/**
|
||||||
* Called when there was an unexpected runtime error while recording the video.
|
* Called when there was an unexpected runtime error while recording the video.
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user