해당 글은 VisionCamera frameProcessor를 통한 데이터 처리 과정 중에 발생한 문제에 대해서 해결을 위한 것들에 대해서 공유합니다.
1) 문제점
💡 문제점 - VisionCamera의 frameProcessor를 이용하여서 연산처리를 수행합니다. Frame 내에서 처리를 하는 과정이 아닌 Frame을 전달받아서 이를 연산처리를 수행하는 과정에서 아래와 같은 이슈가 발생하였습니다.
- Logcat 내에서만 발생하였지만, 실제로 앱을 터치하였을때 아무런 반응을 하지 않는 이슈가 있었습니다. 또한 아래의 profiler를 수행하였을 때, 앱 자체가 종료되는 문제가 발생하였습니다.
💡아래와 같이 일정 순간이 되면 종료가 됩니다.
1. waitForFreeSlotThenRelock: timeout
💡 waitForFreeSlotThenRelock: timeout
- vision-camera는 매 프레임을 네이티브에서 JS/Worklet 쪽으로 넘길 때 Frame Buffer Pool을 사용합니다. - 내부적으로 일정 개수의 프레임 슬롯(buffer slot)을 미리 확보해두며, 다음 프레임을 받기 전에 이전 프레임을 반드시 해제(release) 과정이 필요합니다. - 만약 Worklet 처리 시간이 길거나, frame.close() 호출을 빼먹으면 → 슬롯이 계속 점유됨 → 더 이상 쓸 수 있는 슬롯이 없음 → waitForFreeSlotThenRelock: timeout 발생
- 즉, 버퍼가 가득 차서 새로운 프레임을 넣을 수 없으면, waitForFreeSlotThenRelock()을 호출해서 빈 슬롯이 생길 때까지 기다립니다. 그런데 일정 시간 동안 슬롯이 비지 않으면 timeout 발생하게 됩니다.
2. 문제점 코드 확인
💡 문제점 코드 확인
- 아래의 코드를 확인해보면, 1초당 1 프레임의 Frame을 전달받고 있습니다. 그리고 해당 전달받은 프레임은 원하는 데이터 타입과, 사이즈를 조정하여서 전달을 받습니다. - 해당 내에서 처리를 수행하는 경우로는 정상적으로 처리가 되지만, 전달받은 JS 단에서 처리과정이 길어지다 보면 frame이 밀리는 증상이 발생합니다.
- 즉, 1초 내에 JS 연산을 마무리 하지 못하였는데, 계속적으로 frame이 전달이 되는 경우에 이러한 문제가 발생합니다.
const TARGET_FPS = 1;
const { resize } = useResizePlugin();
const onFrameData = useCallback(async (resizedArr) => {
// ... JS 단에서 추가 연산처리
}, []);
/**
* VisionCamera + react-native-worklets-core와 JS 함수간의 연결을 해주는 함수
*/
const runOnJSFrame = useRunOnJS(onFrameData, [onFrameData]);
const visionCameraHandler = (() => {
return {
/**
* VisionCamera + react-native-worklets-core를 이용하여 프레임당 사진 데이터를 전달받음
*/
frameProcessor: useFrameProcessor((frame) => {
'worklet';
/**
* 1초당 1프레임(TARGET_FPS)을 받는 구조로 지정
*/
runAtTargetFps(TARGET_FPS, () => {
'worklet';
/**
* 일반 이미지 처리를 위한 리사이징 수행
*/
const resized = resize(frame, {
scale: { width: RESIZE_WIDTH, height: RESIZE_HEIGHT },
pixelFormat: 'rgb'
dataType: 'uint8',
}) as Uint8Array;
const resizedArr = Array.from(resized);
runOnJSFrame(resizedArr);
});
}, []),
}
})();
3. 문제점 검증
💡 문제점 검증
- 실제 해당 문제가 맞는지 정확히 확인해 보는 방법은 1초당 Frame 전달수를 늘려서 수행을 하였을 때, 동일한 오류가 발생하는지 확인을 해보는 것입니다.
1. 1초당 60프레임을 전달받는 경우
💡 1초당 60프레임을 전달받는 경우
- 극단적인 테스트로 1초당 60프레임을 전달받아서 수행을 한 경우 아래와 같이 시작한 지 1분도 안되어서 아래와 같은 문제가 발생하였습니다
const TARGET_FPS = 60;
const visionCameraHandler = (() => {
return {
/**
* VisionCamera + react-native-worklets-core를 이용하여 프레임당 사진 데이터를 전달받음
*/
frameProcessor: useFrameProcessor((frame) => {
'worklet';
/**
* 1초당 1프레임(TARGET_FPS)을 받는 구조로 지정
*/
runAtTargetFps(TARGET_FPS, () => {
'worklet';
/**
* 일반 이미지 처리를 위한 리사이징 수행
*/
const resized = resize(frame, {
scale: { width: RESIZE_WIDTH, height: RESIZE_HEIGHT },
pixelFormat: Platform.OS === "ios" ? 'argb' : 'rgb',
dataType: 'uint8',
}) as Uint8Array;
const resizedArr = Array.from(resized); // ✅ 일반 배열로 변환!!! 해당 경우 밖에 전달이 안됨
runOnJSFrame(resizedArr);
});
}, []),
}
})();
💡 아래와 같이 1분도 안되어서 메모리가 급격하게 증가가 되는 문제가 발생하였습니다.
2) 해결방법
💡 해결방법
- 원천적인 해결방법이 되지는 않지만, 각각 상황에 따라 적용해 본 해결방법입니다.
1. TARGET_FPS 조정
💡 TARGET_FPS 조정
- 각기 태블릿마다, 1초마다 연산을 처리하는 디바이스도 있을 것이고, 10초가 되도 연산을 처리하지 못하는 태블릿도 존재할 것입니다. 그렇기에, 원천적인 해결방법은 아니지만, 특정 디바이스에 국한되어서 해결하는 방법은 TARGET_FPS를 지연시키는 방법입니다.
- 실제로 아래와 같은 테스트를 진행하였습니다.
💡 TARGET_FPS에 대해서 공식사이트 글을 확인해 보면 runAtTargetFps()는 초당 프레임에 대해서 지정하여서, FPS 속도보다 빠르게 들어오는 모든 프레임을 삭제한다고 아래서 정의하고 있습니다.
💡 아래와 같이 디바이스 별로 2초당 1 프레임을 전달받거나, 3초당, 5초당 1프레임을 전달받는 식으로 적용합니다.
- 이렇게 적용하여 정상적으로 waitForFreeSlotThenRelock: timeout 문제가 사라지고, frame이 밀리지 않는 현상을 확인하였습니다. - 하지만 이 문제는 모든 디바이스에 대해서 통용적으로 사용을 할 수가 없었던 것 같습니다. 특정 기기에서만 유효하게 된 것 같습니다.
const TARGET_FPS = 1 / 2; // 2초당 1프레임
const TARGET_FPS = 1 / 3; // 3초당 1프레임
const TARGET_FPS = 1 / 5; // 5초당 1프레임
const visionCameraHandler = (() => {
return {
/**
* VisionCamera + react-native-worklets-core를 이용하여 프레임당 사진 데이터를 전달받음
*/
frameProcessor: useFrameProcessor((frame) => {
'worklet';
/**
* 1초당 1프레임(TARGET_FPS)을 받는 구조로 지정
*/
runAtTargetFps(TARGET_FPS, () => {
'worklet';
/**
* 일반 이미지 처리를 위한 리사이징 수행
*/
const resized = resize(frame, {
scale: { width: RESIZE_WIDTH, height: RESIZE_HEIGHT },
pixelFormat: Platform.OS === "ios" ? 'argb' : 'rgb',
dataType: 'uint8',
}) as Uint8Array;
const resizedArr = Array.from(resized); // ✅ 일반 배열로 변환!!! 해당 경우 밖에 전달이 안됨
runOnJSFrame(resizedArr);
});
}, []),
}
})();
2. 안전장치 Frame Drop
💡 안전 장치 Frame Drop
- 말 그대로, 프레임을 전달받는 JS 함수 내에서 Frame Drop을 수행합니다. 즉, 연산처리가 완료되지 않았다면 다음 frame을 JS 내에서 전달받지 않는 방식입니다. - isProcessingRef로 상태 관리를 하고, 수행이 완료될 때까지, 상태를 받지 않고, 도중에 들어온 값에 대해서는 frame drop을 수행합니다.
const isProcessingRef = useRef(false);
const onFrameData = useCallback(async (resizedArr) => {
// 이미 프로세스가 진행중인지 여부를 체크함.
if (isProcessingRef.current) {
console.log('⏩ 프레임 drop!');
return;
}
isProcessingRef.current = true;
// 연산 처리 수행...
// 연산 처리 최종 완료 이후 상태를 바꿔줌
isProcessingRef.current = false;
}, []);
💡 아래와 같이 연산 처리가 안된 경우 프레임 drop이 수행이 되는 것을 확인할 수 있습니다.
3. 원천적인 해결은 Native 내에서 이를 해결
💡 원천적인 해결은 Native 내에서 이를 해결
- JS 내에서 부하를 주는 것이 아닌 Native 내에서 처리하고 JS의 부하를 줄여주는 방식입니다.
💡 JS 내에서는 전달 받은 frame 자체를 Android나 iOS Native로 전달하여서 연산처리를 수행하고, 최종적으로 JS 에게 전달해주는 방식으로 처리를 수행하는 방식입니다.
- 그렇게 되면 JS내에서는 부하가 발생하지 않고, Native에서 처리된 결과만 받기에 해당 문제를 해결할 수 있습니다.
/**
* VisionCamera + react-native-worklets-core와 JS 함수간의 연결을 해주는 함수
*/
const runOnJSFrame = useRunOnJS(onFrameData, [onFrameData]);
/**
* 프레임 프로세서
* width : 640,
* height: 480
*/
const frameProcessor = useFrameProcessor((frame: Frame) => {
'worklet';
const plugin = VisionCameraProxy.initFrameProcessorPlugin('my_yuv_plugin', { foo: 1234 });
if (plugin) {
const result = plugin.call(frame) as unknown as YUVPluginResult;
// ✅ Worklet → JS 안전 호출
runOnJSFrame(result);
}
}, []);