-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
FrameProcessorPlugins.ts
195 lines (174 loc) · 6.64 KB
/
FrameProcessorPlugins.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import type { Frame, FrameInternal } from './Frame'
import type { FrameProcessor } from './CameraProps'
import { CameraRuntimeError } from './CameraError'
// only import typescript types
import type TWorklets from 'react-native-worklets-core'
import { CameraModule } from './NativeCameraModule'
import { assertJSIAvailable } from './JSIHelper'
type BasicParameterType = string | number | boolean | undefined
type ParameterType = BasicParameterType | BasicParameterType[] | Record<string, BasicParameterType | undefined>
interface FrameProcessorPlugin {
/**
* Call the native Frame Processor Plugin with the given Frame and options.
* @param frame The Frame from the Frame Processor.
* @param options (optional) Additional options. Options will be converted to a native dictionary
* @returns (optional) A value returned from the native Frame Processor Plugin (or undefined)
*/
call: (frame: Frame, options?: Record<string, ParameterType>) => ParameterType
}
interface TVisionCameraProxy {
setFrameProcessor: (viewTag: number, frameProcessor: FrameProcessor) => void
removeFrameProcessor: (viewTag: number) => void
/**
* Creates a new instance of a Frame Processor Plugin.
* The Plugin has to be registered on the native side, otherwise this returns `undefined`
*/
getFrameProcessorPlugin: (name: string) => FrameProcessorPlugin | undefined
}
let hasWorklets = false
let isAsyncContextBusy = { value: false }
let runOnAsyncContext = (_frame: Frame, _func: () => void): void => {
throw new CameraRuntimeError(
'system/frame-processors-unavailable',
'Frame Processors are not available, react-native-worklets-core is not installed!',
)
}
try {
assertJSIAvailable()
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { Worklets } = require('react-native-worklets-core') as typeof TWorklets
isAsyncContextBusy = Worklets.createSharedValue(false)
const asyncContext = Worklets.createContext('VisionCamera.async')
runOnAsyncContext = Worklets.createRunInContextFn((frame: Frame, func: () => void) => {
'worklet'
try {
// Call long-running function
func()
} finally {
// Potentially delete Frame if we were the last ref
const internal = frame as FrameInternal
internal.decrementRefCount()
isAsyncContextBusy.value = false
}
}, asyncContext)
hasWorklets = true
} catch (e) {
// Worklets are not installed, so Frame Processors are disabled.
}
let proxy: TVisionCameraProxy = {
getFrameProcessorPlugin: () => {
throw new CameraRuntimeError('system/frame-processors-unavailable', 'Frame Processors are not enabled!')
},
removeFrameProcessor: () => {
throw new CameraRuntimeError('system/frame-processors-unavailable', 'Frame Processors are not enabled!')
},
setFrameProcessor: () => {
throw new CameraRuntimeError('system/frame-processors-unavailable', 'Frame Processors are not enabled!')
},
}
if (hasWorklets) {
// Install native Frame Processor Runtime Manager
const result = CameraModule.installFrameProcessorBindings() as unknown
if (result !== true)
throw new CameraRuntimeError('system/frame-processors-unavailable', 'Failed to install Frame Processor JSI bindings!')
// @ts-expect-error global is untyped, it's a C++ host-object
proxy = global.VisionCameraProxy as TVisionCameraProxy
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (proxy == null) {
throw new CameraRuntimeError(
'system/frame-processors-unavailable',
'Failed to install VisionCameraProxy. Are Frame Processors properly enabled?',
)
}
}
export const VisionCameraProxy = proxy
declare global {
// eslint-disable-next-line no-var
var __frameProcessorRunAtTargetFpsMap: Record<string, number | undefined> | undefined
}
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.
*
* For example, if you want to run a heavy face detection algorithm
* only once per second, you can use `runAtTargetFps(1, ...)` to
* throttle it to 1 FPS.
*
* @param fps The target FPS rate at which the given function should be executed
* @param func The function to execute.
* @returns The result of the function if it was executed, or `undefined` otherwise.
* @example
*
* ```ts
* const frameProcessor = useFrameProcessor((frame) => {
* 'worklet'
* console.log('New Frame')
* runAtTargetFps(5, () => {
* 'worklet'
* const faces = detectFaces(frame)
* console.log(`Detected a new face: ${faces[0]}`)
* })
* })
* ```
*/
export function runAtTargetFps<T>(fps: number, func: () => T): T | undefined {
'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 now = performance.now()
const diffToLastCall = now - getLastFrameProcessorCall(funcId)
if (diffToLastCall >= targetIntervalMs) {
setLastFrameProcessorCall(funcId, now)
// Last Frame Processor call is already so long ago that we want to make a new call
return func()
}
return undefined
}
/**
* Runs the given function asynchronously, while keeping a strong reference to the Frame.
*
* For example, if you want to run a heavy face detection algorithm
* while still drawing to the screen at 60 FPS, you can use `runAsync(...)`
* to offload the face detection algorithm to a separate thread.
*
* @param frame The current Frame of the Frame Processor.
* @param func The function to execute.
* @example
*
* ```ts
* const frameProcessor = useFrameProcessor((frame) => {
* 'worklet'
* console.log('New Frame')
* runAsync(frame, () => {
* 'worklet'
* const faces = detectFaces(frame)
* const face = [faces0]
* console.log(`Detected a new face: ${face}`)
* })
* })
* ```
*/
export function runAsync(frame: Frame, func: () => void): void {
'worklet'
if (isAsyncContextBusy.value) {
// async context is currently busy, we cannot schedule new work in time.
// drop this frame/runAsync call.
return
}
// Increment ref count by one
const internal = frame as FrameInternal
internal.incrementRefCount()
isAsyncContextBusy.value = true
// Call in separate background context
runOnAsyncContext(frame, func)
}