fix: Fix runAtTargetFps for multiple invocations per FP

This commit is contained in:
Marc Rousavy 2023-03-21 16:10:09 +01:00
parent e1973b9b8d
commit af4e366312
3 changed files with 26 additions and 10 deletions

View File

@ -6,7 +6,7 @@ import type { ErrorWithCause } from './CameraError';
import { CameraCaptureError, CameraRuntimeError, tryParseNativeCameraError, isErrorWithCause } from './CameraError'; import { CameraCaptureError, CameraRuntimeError, tryParseNativeCameraError, isErrorWithCause } from './CameraError';
import type { CameraProps } from './CameraProps'; import type { CameraProps } from './CameraProps';
import type { Frame } from './Frame'; import type { Frame } from './Frame';
import { assertFrameProcessorsAvailable } from './JSIHelper'; import { assertFrameProcessorsAvailable, assertJSIAvailable } from './JSIHelper';
import { CameraModule } from './NativeCameraModule'; import { CameraModule } from './NativeCameraModule';
import type { PhotoFile, TakePhotoOptions } from './PhotoFile'; import type { PhotoFile, TakePhotoOptions } from './PhotoFile';
import type { Point } from './Point'; import type { Point } from './Point';
@ -310,6 +310,7 @@ export class Camera extends React.PureComponent<CameraProps> {
* Install JSI Bindings for Frame Processors * Install JSI Bindings for Frame Processors
*/ */
public static installFrameProcessorBindings(): void { public static installFrameProcessorBindings(): void {
assertJSIAvailable();
const result = CameraModule.installFrameProcessorBindings() as unknown; const result = CameraModule.installFrameProcessorBindings() as unknown;
if (result !== true) throw new Error('Failed to install Frame Processor JSI bindings!'); if (result !== true) throw new Error('Failed to install Frame Processor JSI bindings!');
} }

View File

@ -1,13 +1,15 @@
import type { Frame, FrameInternal } from './Frame'; import type { Frame, FrameInternal } from './Frame';
import { Camera } from './Camera'; import { Camera } from './Camera';
import { Worklets } from 'react-native-worklets/src'; import { Worklets } from 'react-native-worklets/src';
import { assertJSIAvailable } from './JSIHelper';
assertJSIAvailable();
// Install VisionCamera Frame Processor JSI Bindings and Plugins // Install VisionCamera Frame Processor JSI Bindings and Plugins
Camera.installFrameProcessorBindings(); Camera.installFrameProcessorBindings();
declare global {
// eslint-disable-next-line no-var
var __frameProcessorRunAtTargetFpsMap: Record<string, number | undefined> | undefined;
}
type BasicParameterType = string | number | boolean | undefined; type BasicParameterType = string | number | boolean | undefined;
type ParameterType = BasicParameterType | BasicParameterType[] | Record<string, BasicParameterType | undefined>; type ParameterType = BasicParameterType | BasicParameterType[] | Record<string, BasicParameterType | undefined>;
type FrameProcessor = (frame: Frame, parameters?: Record<string, ParameterType | undefined>) => unknown; type FrameProcessor = (frame: Frame, parameters?: Record<string, ParameterType | undefined>) => unknown;
@ -19,7 +21,15 @@ type TFrameProcessorPlugins = Record<string, FrameProcessor>;
// @ts-expect-error The global JSI Proxy object is not typed. // @ts-expect-error The global JSI Proxy object is not typed.
export const FrameProcessorPlugins = global.FrameProcessorPlugins as TFrameProcessorPlugins; export const FrameProcessorPlugins = global.FrameProcessorPlugins as TFrameProcessorPlugins;
const lastFrameProcessorCall = Worklets.createSharedValue(performance.now()); function getLastFrameProcessorCall(frameProcessorFuncId: string): number {
'worklet';
return global.__frameProcessorRunAtTargetFpsMap?.[frameProcessorFuncId] ?? 0;
}
function setLastFrameProcessorCall(frameProcessorFuncId: string, value: number): void {
'worklet';
if (global.__frameProcessorRunAtTargetFpsMap == null) global.__frameProcessorRunAtTargetFpsMap = {};
global.__frameProcessorRunAtTargetFpsMap[frameProcessorFuncId] = value;
}
/** /**
* Runs the given function at the given target FPS rate. * Runs the given function at the given target FPS rate.
@ -37,22 +47,25 @@ const lastFrameProcessorCall = Worklets.createSharedValue(performance.now());
* const frameProcessor = useFrameProcessor((frame) => { * const frameProcessor = useFrameProcessor((frame) => {
* 'worklet' * 'worklet'
* console.log('New Frame') * console.log('New Frame')
* const face = runAtTargetFps(5, () => { * runAtTargetFps(5, () => {
* 'worklet' * 'worklet'
* const faces = detectFaces(frame) * const faces = detectFaces(frame)
* return faces[0] * console.log(`Detected a new face: ${faces[0]}`)
* }) * })
* if (face != null) console.log(`Detected a new face: ${face}`)
* }) * })
* ``` * ```
*/ */
export function runAtTargetFps<T>(fps: number, func: () => T): T | undefined { export function runAtTargetFps<T>(fps: number, func: () => T): T | undefined {
'worklet'; 'worklet';
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const funcId = func.__workletHash ?? '1';
const targetIntervalMs = 1000 / fps; // <-- 60 FPS => 16,6667ms interval const targetIntervalMs = 1000 / fps; // <-- 60 FPS => 16,6667ms interval
const now = performance.now(); const now = performance.now();
const diffToLastCall = now - lastFrameProcessorCall.value; const diffToLastCall = now - getLastFrameProcessorCall(funcId);
if (diffToLastCall >= targetIntervalMs) { if (diffToLastCall >= targetIntervalMs) {
lastFrameProcessorCall.value = now; setLastFrameProcessorCall(funcId, now);
// Last Frame Processor call is already so long ago that we want to make a new call // Last Frame Processor call is already so long ago that we want to make a new call
return func(); return func();
} }

View File

@ -12,6 +12,8 @@ export function assertJSIAvailable(): void {
} }
export function assertFrameProcessorsAvailable(): void { export function assertFrameProcessorsAvailable(): void {
assertJSIAvailable();
// @ts-expect-error JSI functions aren't typed // @ts-expect-error JSI functions aren't typed
if (global.setFrameProcessor == null || global.unsetFrameProcessor == null) { if (global.setFrameProcessor == null || global.unsetFrameProcessor == null) {
throw new CameraRuntimeError( throw new CameraRuntimeError(