Skip to content

Commit 7d386b4

Browse files
authored
[tfjs-react-native] make camera utils robust to unmounting
BUG * Close image tensor generator when component is unmounted * Make camera utils robust to unmounting * Enhancement. pass camera texture to callback
1 parent 62ba1f8 commit 7d386b4

File tree

3 files changed

+74
-29
lines changed

3 files changed

+74
-29
lines changed

tfjs-react-native/integration_rn59/components/webcam/realtime_demo.tsx

+21-5
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717

1818
import React from 'react';
19-
import {ActivityIndicator, StyleSheet, View, Platform } from 'react-native';
19+
import {ActivityIndicator, Button, StyleSheet, View, Platform } from 'react-native';
2020
import Svg, { Circle, Rect, G, Line} from 'react-native-svg';
2121

2222
import * as Permissions from 'expo-permissions';
@@ -54,6 +54,8 @@ const AUTORENDER = true;
5454
const TensorCamera = cameraWithTensors(Camera);
5555

5656
export class RealtimeDemo extends React.Component<ScreenProps,ScreenState> {
57+
rafID?: number;
58+
5759
constructor(props: ScreenProps) {
5860
super(props);
5961
this.state = {
@@ -95,8 +97,6 @@ export class RealtimeDemo extends React.Component<ScreenProps,ScreenState> {
9597
const flipHorizontal = Platform.OS === 'ios' ? false : true;
9698
const pose = await this.state.posenetModel.estimateSinglePose(
9799
imageTensor, { flipHorizontal });
98-
99-
// console.log('pose', pose);
100100
this.setState({pose});
101101
tf.dispose([imageTensor]);
102102
}
@@ -115,12 +115,18 @@ export class RealtimeDemo extends React.Component<ScreenProps,ScreenState> {
115115
if(!AUTORENDER) {
116116
gl.endFrameEXP();
117117
}
118-
requestAnimationFrame(loop);
118+
this.rafID = requestAnimationFrame(loop);
119119
};
120120

121121
loop();
122122
}
123123

124+
componentWillUnmount() {
125+
if(this.rafID) {
126+
cancelAnimationFrame(this.rafID);
127+
}
128+
}
129+
124130
async componentDidMount() {
125131
const { status } = await Permissions.askAsync(Permissions.CAMERA);
126132

@@ -254,11 +260,17 @@ export class RealtimeDemo extends React.Component<ScreenProps,ScreenState> {
254260
/>
255261
<View style={styles.modelResults}>
256262
{modelName === 'posenet' ? this.renderPose() : this.renderFaces()}
257-
258263
</View>
259264
</View>;
265+
260266
return (
261267
<View style={{width:'100%'}}>
268+
<View style={styles.sectionContainer}>
269+
<Button
270+
onPress={this.props.returnToMain}
271+
title='Back'
272+
/>
273+
</View>
262274
{isLoading ? <View style={[styles.loadingIndicator]}>
263275
<ActivityIndicator size='large' color='#FF0266' />
264276
</View> : camView}
@@ -275,6 +287,10 @@ const styles = StyleSheet.create({
275287
right: 20,
276288
zIndex: 200,
277289
},
290+
sectionContainer: {
291+
marginTop: 32,
292+
paddingHorizontal: 24,
293+
},
278294
cameraContainer: {
279295
display: 'flex',
280296
flexDirection: 'column',

tfjs-react-native/src/camera/camera_stream.tsx

+19-9
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ interface Props {
4343
onReady: (
4444
images: IterableIterator<tf.Tensor3D>,
4545
updateCameraPreview: () => void,
46-
gl: ExpoWebGLRenderingContext
46+
gl: ExpoWebGLRenderingContext,
47+
cameraTexture: WebGLTexture,
4748
) => void;
4849
}
4950

@@ -77,13 +78,14 @@ const DEFAULT_RESIZE_DEPTH = 3;
7778
* - __resizeHeight__: number — the height of the output tensor
7879
* - __resizeDepth__: number — the depth (num of channels) of the output tensor.
7980
* Should be 3 or 4.
80-
* - __autorender__: boolean — if true the view will be automatically updated with
81-
* the contents of the camera. Set this to false if you want more direct
81+
* - __autorender__: boolean — if true the view will be automatically updated
82+
* with the contents of the camera. Set this to false if you want more direct
8283
* control on when rendering happens.
8384
* - __onReady__: (
8485
* images: IterableIterator<tf.Tensor3D>,
8586
* updateCameraPreview: () => void,
86-
* gl: ExpoWebGLRenderingContext
87+
* gl: ExpoWebGLRenderingContext,
88+
* cameraTexture: WebGLTexture
8789
* ) => void — When the component is mounted and ready this callback will
8890
* be called and recieve the following 3 elements:
8991
* - __images__ is a (iterator)[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators]
@@ -95,8 +97,9 @@ const DEFAULT_RESIZE_DEPTH = 3;
9597
* `updateCameraPreview` and any other operations you want to synchronize
9698
* to the camera rendering you must call gl.endFrameExp() to display it
9799
* on the screen. This is also provided in case you want to do other
98-
* rendering using WebGL.
99-
* Not needed when `autorender` is true.
100+
* rendering using WebGL. Not needed when `autorender` is true.
101+
* - __cameraTexture__ The underlying cameraTexture. This can be used to
102+
* implement your own __updateCameraPreview__.
100103
*
101104
* ```js
102105
* import { Camera } from 'expo-camera';
@@ -171,6 +174,7 @@ export function cameraWithTensors<T extends WrappedComponentProps>(
171174
extends React.Component<T & Props, State> {
172175
camera: Camera;
173176
glView: GLView;
177+
glContext: ExpoWebGLRenderingContext;
174178
rafID: number;
175179

176180
constructor(props: T & Props) {
@@ -184,9 +188,13 @@ export function cameraWithTensors<T extends WrappedComponentProps>(
184188
}
185189

186190
componentWillUnmount() {
191+
cancelAnimationFrame(this.rafID);
192+
if(this.glContext) {
193+
GLView.destroyContextAsync(this.glContext);
194+
}
187195
this.camera = null;
188196
this.glView = null;
189-
cancelAnimationFrame(this.rafID);
197+
this.glContext = null;
190198
}
191199

192200
/*
@@ -219,6 +227,7 @@ export function cameraWithTensors<T extends WrappedComponentProps>(
219227
* @param gl
220228
*/
221229
async onGLContextCreate(gl: ExpoWebGLRenderingContext) {
230+
this.glContext = gl;
222231
const cameraTexture = await this.createCameraTexture();
223232
await detectGLCapabilities(gl);
224233

@@ -250,6 +259,7 @@ export function cameraWithTensors<T extends WrappedComponentProps>(
250259
// Set up a generator function that yields tensors representing the
251260
// camera on demand.
252261
//
262+
const cameraStreamView = this;
253263
function* nextFrameGenerator() {
254264
const RGBA_DEPTH = 4;
255265
const textureDims = {
@@ -264,7 +274,7 @@ export function cameraWithTensors<T extends WrappedComponentProps>(
264274
depth: resizeDepth || DEFAULT_RESIZE_DEPTH,
265275
};
266276

267-
while (true) {
277+
while (cameraStreamView.glContext != null) {
268278
const imageTensor = fromTexture(
269279
gl,
270280
cameraTexture,
@@ -277,7 +287,7 @@ export function cameraWithTensors<T extends WrappedComponentProps>(
277287
const nextFrameIterator = nextFrameGenerator();
278288

279289
// Pass the utility functions to the caller provided callback
280-
this.props.onReady(nextFrameIterator, updatePreview, gl);
290+
this.props.onReady(nextFrameIterator, updatePreview, gl, cameraTexture);
281291
}
282292

283293
/**

tfjs-react-native/src/camera/camera_webgl_util.ts

+34-15
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ interface Dimensions {
2929
}
3030

3131
// Shared cached frameBuffer object from external context
32-
let fbo: WebGLFramebuffer;
32+
const fboCache = new WeakMap<WebGL2RenderingContext, WebGLFramebuffer>();
3333

3434
// Internal target texture used for resizing camera texture input
35-
let resizeTexture: WebGLTexture;
36-
let resizeTextureDims: {width: number, height: number};
35+
const resizeTextureCache = new WeakMap<WebGL2RenderingContext, WebGLTexture>();
36+
const resizeTextureDimsCache =
37+
new WeakMap<WebGL2RenderingContext, {width: number, height: number}>();
3738

3839
interface ProgramObjects {
3940
program: WebGLProgram;
@@ -43,7 +44,9 @@ interface ProgramObjects {
4344
}
4445

4546
// Cache for shader programs and associated vertex array buffers.
46-
const programCache: Map<string, ProgramObjects> = new Map();
47+
const programCacheByContext:
48+
WeakMap<WebGL2RenderingContext, Map<string, ProgramObjects>> =
49+
new WeakMap();
4750

4851
/**
4952
* Download data from an texture.
@@ -58,9 +61,10 @@ export function downloadTextureData(
5861
const {width, height, depth} = dims;
5962
const pixels = new Uint8Array(width * height * depth);
6063

61-
if (fbo == null) {
62-
fbo = createFrameBuffer(gl);
64+
if (!fboCache.has(gl)) {
65+
fboCache.set(gl, createFrameBuffer(gl));
6366
}
67+
const fbo = fboCache.get(gl);
6468

6569
const debugMode = getDebugMode();
6670

@@ -197,9 +201,11 @@ export function runResizeProgram(
197201
//
198202
// Set up output texture.
199203
//
200-
if (resizeTexture == null) {
201-
resizeTexture = gl.createTexture();
204+
if (!resizeTextureCache.has(gl)) {
205+
resizeTextureCache.set(gl, gl.createTexture());
202206
}
207+
const resizeTexture = resizeTextureCache.get(gl);
208+
203209
const targetTexture = resizeTexture;
204210
const targetTextureWidth = outputDims.width;
205211
const targetTextureHeight = outputDims.height;
@@ -213,6 +219,11 @@ export function runResizeProgram(
213219
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
214220

215221
// Reallocate texture storage if target size has changed.
222+
if (!resizeTextureDimsCache.has(gl)) {
223+
resizeTextureDimsCache.set(gl, {width: -1, height: -1});
224+
}
225+
const resizeTextureDims = resizeTextureDimsCache.get(gl);
226+
216227
if (resizeTextureDims == null ||
217228
resizeTextureDims.width !== targetTextureWidth ||
218229
resizeTextureDims.height !== targetTextureHeight) {
@@ -228,18 +239,17 @@ export function runResizeProgram(
228239
targetTextureHeight, border, format, type, null);
229240
});
230241

231-
resizeTextureDims = {
232-
width: targetTextureWidth,
233-
height: targetTextureHeight
234-
};
242+
resizeTextureDimsCache.set(
243+
gl, {width: targetTextureWidth, height: targetTextureHeight});
235244
}
236245

237246
//
238247
// Render to output texture
239248
//
240-
if (fbo == null) {
241-
fbo = createFrameBuffer(gl);
249+
if (!fboCache.has(gl)) {
250+
fboCache.set(gl, createFrameBuffer(gl));
242251
}
252+
const fbo = fboCache.get(gl);
243253

244254
gl.viewport(0, 0, targetTextureWidth, targetTextureHeight);
245255
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
@@ -248,7 +258,6 @@ export function runResizeProgram(
248258

249259
const fboComplete = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
250260
if (fboComplete !== gl.FRAMEBUFFER_COMPLETE) {
251-
console.log('checkFramebufferStatus is not complete', fboComplete);
252261
switch (fboComplete) {
253262
case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
254263
throw new Error(
@@ -294,6 +303,11 @@ function createFrameBuffer(gl: WebGL2RenderingContext): WebGLFramebuffer {
294303

295304
function drawTextureProgram(
296305
gl: WebGL2RenderingContext, flipHorizontal: boolean): ProgramObjects {
306+
if (!programCacheByContext.has(gl)) {
307+
programCacheByContext.set(gl, new Map());
308+
}
309+
const programCache = programCacheByContext.get(gl);
310+
297311
const cacheKey = `drawTexture_${flipHorizontal}`;
298312
if (!programCache.has(cacheKey)) {
299313
const vertSource =
@@ -315,6 +329,11 @@ function resizeProgram(
315329
gl: WebGL2RenderingContext, sourceDims: Dimensions, targetDims: Dimensions,
316330
alignCorners: boolean,
317331
interpolation: 'nearest_neighbor'|'bilinear'): ProgramObjects {
332+
if (!programCacheByContext.has(gl)) {
333+
programCacheByContext.set(gl, new Map());
334+
}
335+
const programCache = programCacheByContext.get(gl);
336+
318337
const cacheKey = `resize_${sourceDims.width}_${sourceDims.height}_${
319338
sourceDims.depth}_${targetDims.width}_${targetDims.height}_${
320339
targetDims.depth}_${alignCorners}_${interpolation}`;

0 commit comments

Comments
 (0)