相机滤镜掉帧?用VisionCamera实时帧处理跑通60FPS
【免费下载链接】react-native-vision-camera📸 A powerful, high-performance React Native Camera library.项目地址: https://gitcode.com/GitHub_Trending/re/react-native-vision-camera
在 React Native 相机应用里做实时滤镜,最怕的就是画面一糊一卡:处理回调一旦挤进 JS 主线程,帧数据还要反复序列化,掉帧几乎是必然的。有没有办法让相机帧不经过 JS 桥、直接在你的代码里被处理,并且稳定跑满 60FPS?有办法,答案就是 react-native-vision-camera 的 Frame Output(帧输出)机制配合 Skia(GPU 加速的 2D 图形库)渲染。读完后你能直接做三件事:跑通一个真实时帧输出的 Demo、用 Skia 在相机帧上绘制实时滤镜贴纸、用分辨率与像素格式两个旋钮把 60FPS 调稳。
理解Frame Output的核心机制
传统做法里,相机帧先拷贝到 JS 线程再处理,每一帧都在排队等序列化;VisionCamera 的 Frame Output 相当于把处理回调直接搬进相机管线内部——useFrameOutput()的onFrame回调以 Worklet(在独立线程运行的 JS 函数)身份运行在相机原生线程上,Frame则是一个 GPU 帧缓冲的句柄,交给 Skia 时零拷贝导入,绘制全程不离开 GPU。整条链路里不存在"帧数据过 JS 桥"这一步,这正是它能跑满 60FPS 的原因。
VisionCamera 文档站首页的相机预览画面,示意实时帧处理的目标观感
| 对比维度 | 传统 JS 桥方案 | VisionCamera 帧处理 |
|---|---|---|
| 帧数据路径 | 拷贝到 JS 主线程再序列化 | 相机线程内直接处理,零拷贝 |
| 处理线程 | JS 主线程(易被 UI 抢占) | 独立相机线程 |
| 绘制能力 | 基本依赖 Canvas 2D | Skia 硬件加速 2D 绘制 |
| 开发者负担 | 手动管理回调时序 | 仍需手动dispose()帧(见后文) |
最后一行值得留意:库替你把管线搭好了,但帧缓冲池只有几个,用完不归还管线就会主动丢帧。这是后文所有调优问题的主线。
安装依赖并配置最小可运行环境
useFrameOutput()依赖 worklet 运行时,所以除核心库外还要装 worklets 桥接包与 Skia 集成包,这段命令解决"onFrame 回调跑在哪里、谁来绘制"两个问题:
npm install react-native-vision-camera # 原生模块与图像类型支持 npm install react-native-nitro-modules react-native-nitro-image # worklet 运行时 + VisionCamera 的 worklets 桥接 npm install react-native-worklets react-native-vision-camera-worklets # Skia 图形渲染(高级绘制场景需要) npm install @shopify/react-native-skia react-native-vision-camera-skiareact-native-worklets 的 Babel 转换配置请以 react-native-worklets 官方文档为准。
配置iOS相机权限
在ios/Info.plist的CFBundleLocalizations同级字典里补上相机用途描述,解决"冷启动直接崩溃、系统不弹授权框"的问题,改完执行cd ios && pod install重新构建原生层:
<dict> <!-- 缺少此 key 时,请求相机会直接失败而非弹窗 --> <key>NSCameraUsageDescription</key> <string>$(PRODUCT_NAME) 需要访问相机以实时处理画面</string> </dict>配置Android相机权限
在android/app/src/main/AndroidManifest.xml中声明相机权限,解决SecurityException崩溃问题:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <!-- 缺此行时 getCameraDevice 会直接抛 SecurityException --> <uses-permission android:name="android.permission.CAMERA" /> <!-- 可选:仅录视频才需要 RECORD_AUDIO --> </manifest>配好后,useCameraDevice('back')返回设备、useFrameOutput()订阅相机帧,就是全部的最小集。
官方 simple-camera 示例应用的相机帧预览效果,展示实时帧处理的目标形态
实战场景一:渲染实时相机帧输出
目标:让相机预览上屏,并确认你的 JS 回调真的在每帧被调用。
关键API:
useCameraDevice(position):按位置('back'/'front')拿到可用的CameraDevice。useFrameOutput(options):创建帧输出,onFrame以 Worklet 形式运行在相机线程。<Camera />:把frameOutput挂进outputs数组即开始推帧。
实现:
// tsx import { StyleSheet, View } from 'react-native' import { Camera, useCameraDevice, useFrameOutput } from 'react-native-vision-camera' function App() { const device = useCameraDevice('back') const frameOutput = useFrameOutput({ onFrame(frame) { 'worklet' // 必须声明:回调要运行在相机线程而非主线程 console.log(`收到 ${frame.width}x${frame.height} 帧`) frame.dispose() // 归还缓冲,缺失它管线很快会丢帧 } }) if (device == null) return <View /> return ( <View style={styles.container}> <Camera style={StyleSheet.absoluteFill} device={device} isActive={true} outputs={[frameOutput]} // 挂载帧输出,相机才开始推帧 /> </View> ) }效果与注意:控制台应稳定打印帧尺寸,预览正常上屏。最典型的坑:漏掉frame.dispose()后几秒内就会收到out-of-buffers丢帧告警——缓冲区被未释放的帧占满,管线只能丢新帧。
画框上屏只是起点。想让滤镜真正"画"在相机帧上,就得把 Skia 的 GPU 画布接进帧管线。
实战场景二:用Skia在GPU上绘制实时滤镜
目标:在相机帧上叠加一个 GPU 加速的滤镜贴纸,全程不离开 GPU。
关键API:
<SkiaCamera />:Skia 版相机视图,自带离屏 Skia 画布;忘记绘制时它是黑屏,画面必须手动画。render({ frameTexture, canvas }):把Frame包装成纹理frameTexture交给你,在canvas上自由绘制。Skia:@shopify/react-native-skia暴露的绘制 API,文本、矩形、图片、着色器都在此。
实现:
// tsx import { SkiaCamera } from 'react-native-vision-camera-skia' import { Skia } from '@shopify/react-native-skia' function App() { const paint = Skia.Paint() // 声明在外部,避免每帧新建原生对象 return ( <SkiaCamera style={StyleSheet.absoluteFill} isActive={true} device="back" onFrame={(frame, render) => { 'worklet' render(({ frameTexture, canvas }) => { // 先把原始帧画上去,否则画面是纯黑 canvas.drawImage(frameTexture, 0, 0) // 叠加滤镜贴纸:GPU 加速文本绘制 paint.setColor(Skia.Color('#FF3B30')) paint.setTextSize(48) canvas.drawText('REALTIME FILTER', 60, 200, paint) }) frame.dispose() }} /> ) }效果与注意:预览之上会实时叠加红色文字贴纸。一个容易踩的坑:<SkiaCamera />不自动上屏,render回调里不画frameTexture就是黑屏——它把"画什么"的权力完全交给了你,旋转与镜像补偿已在库内部完成(见 a-frames-nativebuffer 的 NativeBuffer 说明)。
贴纸目前还不会跟随交互。加上点按对焦,滤镜应用才算有了"操作感"。
实战场景三:实现点按对焦与坐标转换交互
目标:点屏幕哪里,镜头就对焦哪里,并且坐标自动落在帧坐标系内。
关键API:
SkiaCameraRef.convertViewPointToNormalizedPoint(viewPoint):把视图坐标转成0...1的归一化坐标。Frame.convertCameraPointToFramePoint(point):把归一化坐标映射回当前帧的像素坐标。focusTo(viewPoint):把焦点与测光移到指定帧坐标。
实现:
// tsx import { useRef } from 'react' import { StyleSheet } from 'react-native' import { SkiaCamera, type SkiaCameraRef } from 'react-native-vision-camera-skia' function App() { const ref = useRef<SkiaCameraRef>(null) return ( <SkiaCamera ref={ref} style={StyleSheet.absoluteFill} isActive={true} device="back" onLongPressGestureHandler={(evt) => { 'worklet' // 点按位置转成帧坐标系,避免横竖屏/镜像下的坐标错位 const cameraPoint = ref.current .convertViewPointToNormalizedPoint({ x: evt.localX, y: evt.localY }) ref.current?.focusTo(cameraPoint) }} onFrame={(frame, render) => { 'worklet' render(({ frameTexture, canvas }) => { canvas.drawImage(frameTexture, 0, 0) }) frame.dispose() }} /> ) }效果与注意:长按屏幕任意位置,焦点环会出现在你点的位置(参考 tap-to-focus)。注意focusTo的坐标是帧坐标系(已含旋转/镜像补偿),直接塞屏幕像素坐标在横屏时会点错地方——这也是仓库里 coordinate-systems 文档专门讲的事。
以上三段代码在 iOS 与 Android 上完全一致,平台差异由库内部消化:
| 差异项 | iOS | Android |
|---|---|---|
| 底层相机栈 | AVFoundation | CameraX |
| GPU 缓冲导入 | 系统像素缓冲 | 外部纹理(Skia 支持'private'格式) |
| 你的代码 | 同一份 TS 代码 | 同一份 TS 代码 |
排查高频问题并调优60FPS性能
问题1:帧输出持续告警out-of-buffers现象:控制台反复打印 Frame Dropped,预览明显卡顿。 根因:帧缓冲池只有几个,dispose()调用太晚,池被未归还的帧占满。 解法:把frame.dispose()放进finally,保证异常路径也归还缓冲:
onFrame(frame) { 'worklet' try { // 处理逻辑 } finally { frame.dispose() } }问题2:<SkiaCamera />黑屏现象:预览区域全黑,但相机会话日志显示已启动。 根因:Skia 视图不会替你画帧,render里没绘制frameTexture。 解法:回调里第一行就canvas.drawImage(frameTexture, 0, 0)。
问题3:前置摄像头画面左右镜像与贴纸错位现象:贴纸位置看起来"反了"。 根因:输出镜像模式与像素缓冲实际镜像不一致,你按屏幕直觉算了坐标。 解法:mirrorMode设为'auto'(前置自动镜像),坐标一律走transformPoint/归一化坐标 API,别手算。
问题4:人脸等原生模型识别精度低、耗时高现象:同样的模型,在 VisionCamera 里比原生 App 慢且糊。 根因:pixelFormat选了'rgb',每帧多一次转换,且占用约 2.6 倍于'yuv'的带宽。 解法:按模型选格式——MLKit/OpenCV 直接用'yuv';LiteRT 内部会转 RGB,直接给'rgb'更省(完整对照见 pixel-formats-map)。
性能调优策略:单帧处理耗时超过 16ms(60FPS 预算)时,优先降分辨率或换像素格式,最后才降帧率。像素数据量与分辨率面积成正比,把targetResolution从CommonResolutions.FHD_16_9(1080×1920)降到HD_16_9(720×1280),单帧数据量减少约 60%:
const frameOutput = useFrameOutput({ targetResolution: CommonResolutions.HD_16_9, // 720p:滤镜场景性价比最高的档位 pixelFormat: 'yuv', // 比 'rgb' 少一次转换,带宽低约 2.6 倍 onFrame(frame) { 'worklet' // ... frame.dispose() } })资源释放时机容易被忽视:Skia 离屏 Surface 缓存会在 15 秒无操作后自动释放(见 SurfacesCache),但频繁进出相机页时建议显式调用
clearSurfacesCache(),立即归还 GPU 显存。
onFrame必须是同步 worklet,'worklet'指令声明后回调运行在独立相机线程;在主线程里做的任何等待都会直接表现为丢帧。
从相机帧跑进相机线程、用 Skia 画在 GPU 上,到分辨率与像素格式两个旋钮调稳 60FPS——Frame Output、Skia 绘制与性能调优三件事现在都已落地。
继续深入可以看这几篇仓库文档:
- Frame Output 指南
- Skia Frame Processors
- 像素格式对照表
- NativeBuffer 零拷贝帧
- 帧率与分辨率约束
如果这篇对你有帮助,欢迎点赞收藏;关于用着色器写 GPU 滤镜的深入实践,我们下篇见。
【免费下载链接】react-native-vision-camera📸 A powerful, high-performance React Native Camera library.项目地址: https://gitcode.com/GitHub_Trending/re/react-native-vision-camera
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考