You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native前台服务中相机未就绪问题求助

问题:React Native前台服务中后台拍照失败,提示相机未就绪

应用前台运行时拍照功能正常,但切换到后台(前台服务仍处于活动状态)时相机停止工作,抛出以下错误:

WARN Possible Unhandled Promise Rejection (id: 3):
session/camera-not-ready: [session/camera-not-ready] The Camera is not ready yet! Wait for the onInitialized() callback!

使用notifee实现前台服务,react-native-vision-camera实现相机功能。


核心问题分析

当应用退到后台时,React Native的UI组件会被暂停或销毁,<Camera>组件的相机会话也会随之关闭。你当前的代码直接依赖UI层的cameraRef调用拍照,后台场景下该实例已无法正常工作,因此触发"相机未就绪"的错误。同时,前台服务回调中直接更新UI状态的逻辑在后台也会失效。


解决步骤

  1. 分离相机逻辑与UI层:使用react-native-vision-camera的低级API(CameraManager)在后台服务中独立管理相机会话,不依赖UI组件实例。
  2. 补充权限配置:申请后台相机相关权限,确保前台服务有权限访问相机。
  3. 管理相机生命周期:前台服务启动时初始化相机,停止时释放资源,避免内存泄漏。
  4. 避免后台UI更新:后台拍照后将路径保存到本地存储,仅在应用前台时更新UI显示。

修改后的代码示例

import React, {useRef, useState, useEffect} from 'react';
import {
  ActivityIndicator,
  Image,
  TouchableOpacity,
  Text,
  View,
  StyleSheet,
  PermissionsAndroid
} from 'react-native';
import {useCameraDevices, Camera, CameraManager} from 'react-native-vision-camera';
import {useIsForeground} from './hooks/useIsForeground';
import RNFS from 'react-native-fs';
import notifee from '@notifee/react-native';

const HelloWorldApp = () => {
  const isAppForeground = useIsForeground();
  const [finalPath, setPhotoPath] = useState('');
  const devices = useCameraDevices();
  const device = devices.front;
  let cameraSession = null;
  let intervalId = null;

  // 申请必要权限
  const requestPermissions = async () => {
    const cameraPerm = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA);
    const fgServicePerm = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.FOREGROUND_SERVICE);
    const fgCameraPerm = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.FOREGROUND_SERVICE_CAMERA);
    return cameraPerm === 'granted' && fgServicePerm === 'granted' && fgCameraPerm === 'granted';
  };

  // 后台拍照逻辑
  const startBackgroundCamera = async () => {
    try {
      // 初始化相机会话
      cameraSession = await CameraManager.openCamera(device.id, {
        format: device.formats[0],
        fps: 30,
      });
      intervalId = setInterval(async () => {
        const snapshot = await CameraManager.takeSnapshot(cameraSession, {
          quality: 20,
          skipMetadata: true,
        });
        // 保存到公共目录,确保后台可访问
        const saveDir = `${RNFS.ExternalStorageDirectoryPath}/camera_snapshots`;
        await RNFS.mkdir(saveDir, {NSURLIsDirectoryKey: true});
        const savePath = `${saveDir}/${Date.now()}.jpg`;
        await RNFS.moveFile(snapshot.path, savePath);
        // 仅前台时更新UI
        if (isAppForeground) setPhotoPath(`file://${savePath}`);
      }, 2000);
    } catch (err) {
      console.error('后台相机启动失败:', err);
    }
  };

  // 停止后台相机
  const stopBackgroundCamera = () => {
    if (intervalId) clearInterval(intervalId);
    if (cameraSession) {
      CameraManager.closeCamera(cameraSession);
      cameraSession = null;
    }
  };

  useEffect(() => {
    // 注册前台服务
    notifee.registerForegroundService(async () => {
      await startBackgroundCamera();
      // 保持服务运行,点击通知停止服务
      return new Promise((resolve) => {
        const unsubscribe = notifee.onNotificationPress(() => {
          stopBackgroundCamera();
          resolve();
          unsubscribe();
        });
      });
    });

    // 组件卸载时清理资源
    return () => stopBackgroundCamera();
  }, []);

  if (device == null) {
    return <ActivityIndicator style={styles.indicator} size="large" />;
  }

  async function onDisplayNotification() {
    const hasPerms = await requestPermissions();
    if (!hasPerms) {
      console.log('权限不足,无法启动服务');
      return;
    }

    const channelId = await notifee.createChannel({
      id: 'camera_service',
      name: '相机后台服务',
      importance: notifee.Importance.HIGH,
    });

    await notifee.displayNotification({
      title: '后台拍照服务运行中',
      body: '点击通知停止服务',
      android: {
        channelId,
        asForegroundService: true,
        color: '#FF0000',
        colorized: true,
        actions: [{title: '停止', pressAction: {id: 'stop'}}],
      },
    });
  }

  return (
    <View style={styles.container}>
      {isAppForeground && (
        <>
          <Camera style={styles.camera} device={device} isActive={true} />
          <Image
            source={{uri: finalPath + '?' + new Date()}}
            style={styles.image}
          />
        </>
      )}
      <TouchableOpacity style={styles.button} onPress={onDisplayNotification}>
        <Text>启动后台拍照服务</Text>
      </TouchableOpacity>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {flex: 1, justifyContent: 'center', alignItems: 'center'},
  camera: {width: '100%', height: '60%'},
  image: {width: 200, height: 200, margin: 20},
  button: {padding: 15, backgroundColor: '#2196F3', borderRadius: 8},
  indicator: {flex: 1, justifyContent: 'center', alignItems: 'center'},
});

export default HelloWorldApp;

额外配置说明

  • AndroidManifest.xml需添加以下权限:
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.front" />
    
  • iOS平台需开启Background Modes中的Camera权限,且需向Apple说明后台拍照的使用场景,否则App Store审核可能被拒。
  • 后台拍照会持续消耗电量,建议添加明确的启停控制逻辑,避免长时间运行。

内容的提问来源于stack exchange,提问作者Mfa Xyz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 22:00:56