【Android Camera1】Camera1初始化销毁流程(四) —— CameraImpl类之OpenCamera伪代码

一、摘要

本篇文章阐述实现Camera1初始化的方法。会对照官方demo的代码模版进行针对性修改。

二、Camera1Impl初始化实现

public void initCamera(int cameraId, float aspectRatio){
    
    

}

2.1 Open函数:

Camera1源码分析中,最后的方法调用都会走到Native层面。并且根据Camera1综述文章中,在底层会去链接实际的相机设备,获取相机的信息。

Camera1 java层源码上层提供了open函数如下,具体open函数执行流程可参考Camera1源码分析 【3.1】。open函数会返回一个Camera.java的实例对象,并且有如下几个override函数:

    public static Camera open() {
    
    
       int numberOfCameras = getNumberOfCameras();
       CameraInfo cameraInfo = new CameraInfo();
       for (int i = 0; i < numberOfCameras; i++) {
    
    
           getCameraInfo(i, cameraInfo);
           if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {
    
    
               return new Camera(i);
           }
       }
       return null;
   }
    /* @param cameraId the hardware camera to access, between 0 and
     *     {@link #getNumberOfCameras()}-1.
     * @return a new Camera object, connected, locked and ready for use.
     * @throws RuntimeException if opening the camera fails (for example, if the
     *     camera is in use by another process or device policy manager has
     *     disabled the camera).
     * @see android.app.admin.DevicePolicyManager#getCameraDisabled(android.content.ComponentName)
     */
    public static Camera open(int cameraId) {
    
    
        return new Camera(cameraId);
    }
  1. open方法没有传入回掉函数,是同步调用执行的
  2. open方法会返回null 对象
  3. getCameraInfo会抛异常出错
  4. open会默认打开后置摄像头
  5. open有个重载函数支持传入CameraId,会throw exception and eturn a new Camera object, connected, locked and ready for use.

结论:考虑到cameraId需要UI层上层控制,一般就会选者支持cameraId参数传入的方法。

2.2 锁同步

综合如下几个原因需要使用锁同步

  • open函数对于上层是同步调用
  • 应用切换前后置,切换画幅,切换前后台都会触发initCamera方法调用
  • 底层链接Camera,获取Camera信息是在另一个进程中

initCamera里添加如下代码:

public void initCamera(int cameraId, float aspectRatio){
    
    
	synchronized(lock){
    
    
		if(acquireLock(timeout = 2000)){
    
    
			Log.i(TAG, "initCamera超时")
		}
	}
}

频繁暴力调用initCamera会通过锁申请来同步化。这里超时后不会直接return。直接return会导致功能不可用。具体的锁实现和锁超时机制这里就不赘述了,各有各的实现方法。

2.3 chooseCameraId

官方Demo中的逻辑如下:

 @Override
    boolean start() {
    
    
        chooseCamera();
        openCamera();
        if (mPreview.isReady()) {
    
    
            setUpPreview();
        }
        mShowingPreview = true;
        mCamera.startPreview();
        return true;
    }
  /**
     * This rewrites {@link #mCameraId} and {@link #mCameraInfo}.
     */
    private void chooseCamera() {
    
    
        for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) {
    
    
            Camera.getCameraInfo(i, mCameraInfo);
            if (mCameraInfo.facing == mFacing) {
    
    
                mCameraId = i;
                return;
            }
        }
        mCameraId = INVALID_CAMERA_ID;
    }

分析:

官方Demo通过mFacing来控制初始打开哪个Camera设备,我们可通过传入的cameraId参数来初始化对应Camera设备

Camera理论协议和规范文章中,所有的手机需要支持前置和后置摄像。但也存在一些特殊情况:如摄像头坏了,某些手机兼容确实不支持前置或后置。在遍历getNumberOfCameras无效ID时应给予用户提示做容错处理

Camera.getCameraInfo(i, mCameraInfo);API调用会抛异常该异常通常和camera设备状态、时序错乱、声音audio系统都可能有关系。但是该功能异常并不会是致命的Bug

更新代码如下:

public void initCamera(int cameraId, float aspectRatio){
    
    
	synchronized(lock){
    
    
		if(acquireLock(timeout = 2000)){
    
    
			Log.i(TAG, "initCamera超时")
		}
		int count = 0;
		// @return total number of accessible camera devices, 
		//or 0 if there are no cameras or an error was encountered enumerating them.
		try{
    
    
			count = Camera.getNumberOfCameras();
		}catch(Exception e){
    
    
			count = -1;
		}
		if(count == 0){
    
    
			Toast("清检查设备前后置摄像头")
			return;
		}
	}
}

通过调用Camera.getNumberOfCameras();方法,如果返回0则表示当前无可用Camera设备。如果返回camera = -1则表示获取信息发生异常,更新代码如下:

		try{
    
    
			count = Camera.getNumberOfCameras();
		}catch(Exception e){
    
    
			count = -1;
		}
		if(count == 0){
    
    
			Toast("清检查设备前后置摄像头")
			//释放锁
			releaseLock();
			return;
		}
		if(count == -1){
    
    
			releaseLock();
			if(retryCount> DEFAULT_RETRY_COUNT){
    
    
				retryInitCamera();
				retryCount++;
			}
			return;
		}
		retryCount.= 0;

当count == -1时,做一个简单的重试逻辑。但是由于瞬间立马调用重试可能获取到的CameraDevice信息还是错误,因此稍微通过CameraHandler做一定时间的延迟,可更新代码如下:

		if(count == -1){
    
    
			releaseLock();
			if(retryCount> DEFAULT_RETRY_COUNT){
    
    
				getCameraHandle().postDelay(()->{
    
    
					retryInitCamera();
					retryCount++;
				},20);
			}
			return;
		}
		retryCount= 0;

2.4 openCamera();

CameraId是用0开始排列的,0为后置,1为前置。因此通过源码mCameraId = i看。是直接把序号赋值给mCameraId。我们直接调用open(cameraId)方法,可继续更新代码如下:

mcamera = Camera.open(cameraId);

通过源码我们可以看到如下注释:

You must call {@link #release()} when you are done using the camera,
* otherwise it will remain locked and be unavailable to other applications.

继续修改代码如下:

if(mCamera!=null){
    
    
	//releaseCamera释放camera,并set null to mCamera。
	releaseCamera();
}
mcamera = Camera.open(cameraId);

由于某些手机没有后置摄像头,或者前置、后置摄像头返回nullCamera.open(cameraId);可能会返回null对象。添加容错代码如下:

if(mCamera!=null){
    
    
	//releaseCamera释放camera,并set null to mCamera。
	releaseCamera();
}
mCamera = Camera.open(cameraId);
 if(mCamera == null){
    
    
	Toast("打开摄像头失败");
	releaseLock();
	return;
}

2.5 getCameraInfo

如果之前都顺利,那么我们已经打开了特定cameraId相机设备。那么接下来就是读取设备信息:getCameraInfo 源码如下,并且会抛异常。

    /**
     * Returns the information about a particular camera.
     * If {@link #getNumberOfCameras()} returns N, the valid id is 0 to N-1.
     *
     * @throws RuntimeException if an invalid ID is provided, or if there is an
     *    error retrieving the information (generally due to a hardware or other
     *    low-level failure).
     */
    public static void getCameraInfo(int cameraId, CameraInfo cameraInfo) {
    
    
        _getCameraInfo(cameraId, cameraInfo);
        IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
        IAudioService audioService = IAudioService.Stub.asInterface(b);
        try {
    
    
            if (audioService.isCameraSoundForced()) {
    
    
                // Only set this when sound is forced; otherwise let native code
                // decide.
                cameraInfo.canDisableShutterSound = false;
            }
        } catch (RemoteException e) {
    
    
            Log.e(TAG, "Audio service is unavailable for queries");
        }
    }

继续添加代码:

mCamera = Camera.open(cameraId);
if(mCamera == null){
    
    
	Toast("打开摄像头失败");
	releaseLock();
	return;
}
mCameraId = cameraId;
Camera.CameraInfo info = new Camera.CameraInfo();
try{
    
    
	Camera.getCameraInfo(mCameraId,info)
}catch(Exception e){
    
    
	Log.e(TAG,"exception info = "+ e.getInfo());
}

mCanDisableSound = info.canDisableShutterSound;
if(mCanDisableSound){
    
    
	try {
    
    
		// 关闭快门声
       mCamera.enableShutterSound(false);  
   	}catch (Exception e){
    
    
    }
}else{
    
    
	//TODO 需要做额外的关闭声音容错处理
}

同样enableShutterSound会抛异常,但不是致命问题,需要try-catch

/* @throws RuntimeException if the call fails; usually this would be because
     *    of a hardware or other low-level error, or because release() has been
     *    called on this Camera instance.
     */
    public final boolean enableShutterSound(boolean enabled)

如果mCanDisableSound = false 则需要在拍照时做额外的处理逻辑,将在Camera1拍照逻辑中阐述。到这里我们总算链接成功了Camera设备,结下来需要做的就是applyDefaultParameter();startPreview()两个部分。

三、伪代码整理

这里我们整理伪代码如下:

public void initCamera(int cameraId, float aspectRatio){
    
    
	synchronized(lock){
    
    
		try{
    
    
			if(acquireLock(timeout = 2000)){
    
    
				Log.i(TAG, "initCamera超时")
			}
			openCamera(cameraId);
			applyDefaultParameters();
			startPreview();
			releaseLock();
			Log.i(TAG, "initCamera succ")
			retryCount = 0;
		}catch(Exception e){
    
    
			releaseLock();
			Log.e(TAG, "initCamera exception info = "+e.getInfo();
			if(retryCount > DEFAULT_RETRY_COUNT){
    
    
				getCameraHandler().postDelay(()->{
    
    
					retryCount++;
					initCamera(cameraId, aspectRatio);
				},20);
			}
		}
	}
}
  1. 把initCamera()拆分成三个逻辑部分
  2. 整体try - catch容错并添加 exception 重试逻辑
  3. 锁同步时序
public void openCamera(int cameraId){
    
    
	int count = 0;
		// @return total number of accessible camera devices, 
		//or 0 if there are no cameras or an error was encountered enumerating them.
	try{
    
    
		count = Camera.getNumberOfCameras();
	}catch(Exception e){
    
    
		count = -1;
	}
	if(count == 0){
    
    
		Toast("清检查设备前后置摄像头")
		//释放锁
		releaseLock();
		return;
	}
	if(count == -1){
    
    
		releaseLock();
		if(retryCount> DEFAULT_RETRY_COUNT){
    
    
			getCameraHandle().postDelay(()->{
    
    
				openCamera(cameraId);
				retryCount++;
			},20);
		}
		return;
	}
	retryCount= 0;
	if(mCamera!=null){
    
    
		//releaseCamera释放camera,并set null to mCamera。
		releaseCamera();
	}
	mCamera = Camera.open(cameraId);
	if(mCamera == null){
    
    
		Toast("打开摄像头失败");
		releaseLock();
		return;
	}
	mCameraId = cameraId;
	Camera.CameraInfo info = new Camera.CameraInfo();
	try{
    
    
		Camera.getCameraInfo(mCameraId,info)
	}catch(Exception e){
    
    
		Log.e(TAG,"exception info = "+ e.getInfo());
	}
	
	mCanDisableSound = info.canDisableShutterSound;
	if(mCanDisableSound){
    
    
		try {
    
    
			// 关闭快门声
	       mCamera.enableShutterSound(false);  
	   	}catch (Exception e){
    
    
	    }
	}else{
    
    
		//TODO 需要做额外的关闭声音容错处理
	}
}

下一篇讲阐述applyDefaultFocus()逻辑

【Camera1】Camera1初始化销毁流程(五) —— CameraImpl类之applyDefaultFocus

猜你喜欢

转载自blog.csdn.net/Scott_S/article/details/122269942