/**
* {@link CameraDevice.StateCallback} is called when {@link CameraDevice} changes its state.
*/
private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
// 打开摄像头
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
// This method is called when the camera is opened. We start camera preview here.
mCameraOpenCloseLock.release();
mCameraDevice = cameraDevice;
// 创建预览绘画
createCameraPreviewSession();
}
@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
// 关闭相机连接时 解锁(此处省略代码)
}
@Override
public void onError(@NonNull CameraDevice cameraDevice, int error) {
//(此处省略代码)
};
对于摄像头这个独占资源的使用,采用了
private Semaphore mCameraOpenCloseLock = new Semaphore(1);
信号量进行控制。 【TODO】Java 信号量 Semaphore
并创建了之后为用户提供画面的预览会话。
这部分API的使用,会在之后的app调用中通过 CameraManager 来使用。
2.2.2 获得相机权限
回到app层面,要使用相机,肯定需要获得Android的相机权限:
private void requestCameraPermission() {
if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
new ConfirmationDialog().show(getChildFragmentManager(), FRAGMENT_DIALOG);
} else {
requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);
}
}
2.2.3 相机输出设置
获得所有摄像头的特性,并根据长度宽度这两个参数,设置预览与输出的分辨率大小。
private void setUpCameraOutputs(int width, int height) {
Activity activity = getActivity();
CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
try {
// 0. 可能有多个摄像头,比如前置后置,遍历每个摄像头进行设置
for (String cameraId : manager.getCameraIdList()) {
// 1. 获得描述摄像头的各种特性
// 通过CameraManager的getCameraCharacteristics(String cameraId)方法来获取
CameraCharacteristics characteristics
= manager.getCameraCharacteristics(cameraId);
// 2. 跳过前置摄像头
Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {
continue;
}
// 3. 获得该摄像头支持的分辨率信息
StreamConfigurationMap map = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (map == null) {
continue;
}
// 4. 使用可行的最大的尺寸来输出照片
Size largest = Collections.Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),new CompareSizesByArea());
。。。
// Find out if we need to swap dimension to get the preview size relative to sensor coordinate.
int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
//noinspection ConstantConditions
// 5. 获取摄像头方向,按照顺时针来衡量角度
mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
boolean swappedDimensions = false;
switch (displayRotation) {
。。。
}
// 6. 通过点的坐标获得预览显示区域的像素大小
Point displaySize = new Point();
activity.getWindowManager().getDefaultDisplay().getSize(displaySize);
。。。
// 7. 将TextureView的宽高与预览的大小匹配,并且会与手机横置与否匹配
int orientation = getResources().getConfiguration().orientation;
。。。
// 8. 检查闪光灯是否支持
Boolean available = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
。。。
return;
}
} catch (CameraAccessException e) {
e.printStackTrace();
} catch (NullPointerException e) {
// Currently an NPE is thrown when the Camera2API is used but not supported on the
// device this code runs.
ErrorDialog.newInstance(getString(R.string.camera_error))
.show(getChildFragmentManager(), FRAGMENT_DIALOG);
}
}
/**
* Opens the camera specified by {@link Camera2BasicFragment#mCameraId}.
*/
private void openCamera(int width, int height) {
// 1. 获取权限
if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
requestCameraPermission();
return;
}
// 设置相机输出格式:长宽,并配置好预览的长宽、闪光灯配置等
setUpCameraOutputs(width, height);
//
configureTransform(width, height);
Activity activity = getActivity();
// 2. 获取CameraManager:"摄像头管理器,用于打开和关闭系统摄像头"
CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
try {
// 请求摄像头时检测锁是否被占用
if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Time out waiting to lock camera opening.");
}
// 调用时内部还会有同步锁进行控制
manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
}
}
【TODO】TextureView是什么? 旋转?
2.3 显示预览
// 打开摄像头
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
// This method is called when the camera is opened. We start camera preview here.
mCameraOpenCloseLock.release();
mCameraDevice = cameraDevice;
// 创建预览会话
createCameraPreviewSession();
}
private void lockFocus() {
try {
// This is how to tell the camera to lock focus.
// 告诉摄像头 配置自动对焦
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
CameraMetadata.CONTROL_AF_TRIGGER_START);
// Tell #mCaptureCallback to wait for the lock.
mState = STATE_WAITING_LOCK;
mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
/**
* A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture.
*/
private CameraCaptureSession.CaptureCallback mCaptureCallback
= new CameraCaptureSession.CaptureCallback() {
private void process(CaptureResult result) {
// 按下拍照键后根据设置的不同,有不同的拍摄方式
switch (mState) {
case STATE_PREVIEW: {
// 预览
// We have nothing to do when the camera preview is working normally.
break;
}
case STATE_WAITING_LOCK: {
// 对焦稳定后拍摄或尝试拍摄
Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
if (afState == null) {
captureStillPicture();
} else if (CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED == afState ||
CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED == afState) {
// CONTROL_AE_STATE can be null on some devices
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
if (aeState == null ||
aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
// 对焦状态无特殊情况或与当前场景契合 正常拍照
mState = STATE_PICTURE_TAKEN;
captureStillPicture();
} else {
runPrecaptureSequence();
}
}
break;
}
case STATE_WAITING_PRECAPTURE: {
// CONTROL_AE_STATE can be null on some devices
// 曝光,等待曝光稳定,比如需要闪光灯
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
if (aeState == null ||
aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE ||
aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) {
mState = STATE_WAITING_NON_PRECAPTURE;
}
break;
}
case STATE_WAITING_NON_PRECAPTURE: {
// CONTROL_AE_STATE can be null on some devices
// 曝光稳定后拍摄
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) {
mState = STATE_PICTURE_TAKEN;
captureStillPicture();
}
break;
}
}
}
2.4.3 拍照
真正获得静态图像的是captureStillPicture()这个方法。
/**
* Capture a still picture. This method should be called when we get a response in
* {@link #mCaptureCallback} from both {@link #lockFocus()}.
* 拍照获得静态图像并保存
*/
private void captureStillPicture() {
try {
final Activity activity = getActivity();
if (null == activity || null == mCameraDevice) {
return;
}
// 和预览界面相似的一系列设置,包括自动曝光自动对焦等
// This is the CaptureRequest.Builder that we use to take a picture.
final CaptureRequest.Builder captureBuilder =
mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(mImageReader.getSurface());
// Use the same AE and AF modes as the preview.
captureBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
setAutoFlash(captureBuilder);
// Orientation
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));
// 通过保存了拍摄设置的session,调用拍摄回调,将图片保存到文件中,并toast路径
CameraCaptureSession.CaptureCallback CaptureCallback
= new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
showToast("Saved: " + mFile);
Log.d(TAG, mFile.toString());
// 拍摄结束时结束等待对焦状态,回到预览状态
unlockFocus();
}
};
// 中断传输给预览界面的循环
mCaptureSession.stopRepeating();
// 放弃当前所有任务,如果是循环任务如预览,会放弃之前缓存的数据
mCaptureSession.abortCaptures();
// 根据之前设置的参数,拍照获取当前帧
mCaptureSession.capture(captureBuilder.build(), CaptureCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
拍摄时,会中断为预览画面执行的循环任务,并放弃之前缓存的数据,然后拍照获取当前帧。
2.5 保存图片
图片的保存是通过一个实现了runnable的内部类,异步保存
private static class ImageSaver implements Runnable {
/**
* The JPEG image
*/
private final Image mImage;
/**
* The file we save the image into.
*/
private final File mFile;
ImageSaver(Image image, File file) {
mImage = image;
mFile = file;
}
// 使用另一个线程保存照片
@Override
public void run() {
ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
FileOutputStream output = null;
try {
output = new FileOutputStream(mFile);
output.write(bytes);
} catch (IOException e) {
e.printStackTrace();
} finally {
mImage.close();
if (null != output) {
try {
// 在finally里面调用close,如果在try中close,可能会因为出现异常,导致无法执行到close语句
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2.6 关闭服务
使用juc中的信号量Semaphore控制并发,依次关闭相机相关服务
private void closeCamera() {
// 使用juc中的信号量Semaphore控制并发,依次关闭相机相关服务
try {
mCameraOpenCloseLock.acquire();
if (null != mCaptureSession) {
mCaptureSession.close();
mCaptureSession = null;
}
if (null != mCameraDevice) {
mCameraDevice.close();
mCameraDevice = null;
}
if (null != mImageReader) {
mImageReader.close();
mImageReader = null;
}
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while trying to lock camera closing.", e);
} finally {
mCameraOpenCloseLock.release();
}
}
/**
* An additional thread for running tasks that shouldn't block the UI.
* 一个额外的线程用以运行维持UI的任务
*/
private HandlerThread mBackgroundThread;
搜寻了一番发现在 onResume() 中有段注释:
@Override
public void onResume() {
super.onResume();
// TODO:什么是HandlerThread,关于BackgroundThread是做什么的:见line224
startBackgroundThread();
// When the screen is turned off and turned back on, the SurfaceTexture is already
// available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open
// a camera and start preview from here (otherwise, we wait until the surface is ready in
// the SurfaceTextureListener).
if (mTextureView.isAvailable()) {
openCamera(mTextureView.getWidth(), mTextureView.getHeight());
} else {
mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
}
}
也就是说,但屏幕关闭之后又开启,相机应用可以通过这个“后台线程”快速可用。
同时可以注意到该线程是一个HandleThread类,似乎是个Android独有的线程实现方式。
private void startBackgroundThread() {
mBackgroundThread = new HandlerThread("CameraBackground");
mBackgroundThread.start();
// 在Android开发中,不熟悉多线程开发的人一想到要使用线程,可能就用new Thread(){…}.start()这样的方式。
//实质上在只有单个耗时任务时用这种方式是可以的,但若是有多个耗时任务要串行执行呢?
//那不得要多次创建多次销毁线程,这样导致的代价是很耗系统资源,容易存在性能问题。那么,怎么解决呢?
//我们可以只创建一个工作线程,然后在里面循环处理耗时任务,
mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
}
那么如何关闭这个线程?
在onPause()中调用:
@Override
public void onPause() {
closeCamera();
stopBackgroundThread();
super.onPause();
}
Camera2Basic 源码阅读
本文将通过对 android-Camera2Basic 的源码分析,学习安卓相机的基本开发。
【TODO】getSupportFragmentManager() 这系列的语句是做什么用的?
【TODO】安卓基础:inflater
【TODO】Java 信号量 Semaphore
【TODO】camera:TextureView是什么?
【TODO】OpenGL相关:OpenGL ES texture是什么?
一、CameraActivity
首先肯定要看这个相机应用是怎么诞生的。
onCreate很简短,主要是实例化一个 Camera2BasicFragment,也是整个项目的重头戏。
【TODO】getSupportFragmentManager() 这系列的语句是做什么用的?
二、Camera2BasicFragment
通过这个方法,之前的活动 CameraActivity 实例化了Fragment:
【此处应有张截图】代码界面贴一张图片
由上图可以看到,整个 com.example.android.camera2basic.Camera2BasicFragment.java 总共有 25 个方法、一个图片保存类、一个比较类以及两个日志类。
根据本app的拍照流程,这25个方法可以分为以下几类:
2.1 创建界面
(1) 导入视图的xml
【TODO】 安卓基础:inflater
(2)配置按钮
设置拍摄按钮以及info按钮的点击事件
(3)相关配置初始化
mFile是图片的输出目标文件
2.2 打开相机
2.2.1 通过API2调用
app层次的代码肯定是无法直接调用硬件层面的摄像头配置的,所以需要使用API2提供的接口来对相机进行打开和关闭,以及异常情况的处理:
对于摄像头这个独占资源的使用,采用了
private Semaphore mCameraOpenCloseLock = new Semaphore(1); 信号量进行控制。 【TODO】Java 信号量 Semaphore
并创建了之后为用户提供画面的预览会话。
这部分API的使用,会在之后的app调用中通过 CameraManager 来使用。
2.2.2 获得相机权限
回到app层面,要使用相机,肯定需要获得Android的相机权限:
2.2.3 相机输出设置
获得所有摄像头的特性,并根据长度宽度这两个参数,设置预览与输出的分辨率大小。
2.2.4 通过CameraManager管理相机
CameraManager是一个系统服务,将相机硬件相关的功能封装成接口供调用。
2.2.* 整体流程代码
整体流程封装如下,在最后的打开相机时,会使用Lock.tryAcquire() 检测锁是否被占用。
【TODO】TextureView是什么? 旋转?
2.3 显示预览
打开摄像头之后,就要在应用界面显示预览画面。
预览画面时动态且实时的,所以需要摄像头持续不断的获取数据:
所以创建了一个拍摄会话,通过一个重复请求源源不断的获取图片,并以数组形式传递给surface用以显示预览
【TODO】OpenGL相关:OpenGL ES texture是什么?({@link SurfaceTexture}: Captures frames from an image stream as an OpenGL ES texture.)
2.4 拍摄
2.4.1 点击拍摄按钮拍照:
2.4.2 对焦曝光要正确
拍照,肯定要对焦,并且对焦要稳定不能拉风箱,对于对焦的状态,可通过mState = STATE_WAITING_LOCK;进行控制。
通过RequestBuilder配置好自动对焦后,等待对焦稳定,通过session拍摄,此时会调用一个回调进行拍摄,也就是mCaptureCallback。
而拍摄的处理过程就是靠这个回调定义的。
这个回调mCaptureCallback,会分为四种不同的情况:
预览时需要调用摄像头的数据但并不做拍摄操作; mState = STATE_WAITING_LOCK时表示要准备拍照了,此时会根据【对焦状态】进行拍摄或尝试进行预拍摄(precapture);
根据【曝光状态】如“需要闪光灯补光”,将mstate由等待预拍摄(STATE_WAITING_PRECAPTURE)转换为等待非预拍摄(STATE_WAITING_NON_PRECAPTURE)
曝光稳定后拍摄
2.4.3 拍照
真正获得静态图像的是captureStillPicture()这个方法。
拍摄时,会中断为预览画面执行的循环任务,并放弃之前缓存的数据,然后拍照获取当前帧。
2.5 保存图片
图片的保存是通过一个实现了runnable的内部类,异步保存
2.6 关闭服务
使用juc中的信号量Semaphore控制并发,依次关闭相机相关服务
2.7 特殊情况的处理
在阅读代码的过程中,经常会出现一些变量,与“Background”有关,比如mBackgroundThread。最开始看到很疑惑这个东西是干什么的。
搜寻了一番发现在 onResume() 中有段注释:
也就是说,但屏幕关闭之后又开启,相机应用可以通过这个“后台线程”快速可用。
同时可以注意到该线程是一个HandleThread类,似乎是个Android独有的线程实现方式。
那么如何关闭这个线程?
在
onPause()
中调用:HandlerThread既然是一个循环线程,那么怎么退出呢?有两种方式,分别是不安全的退出方法quit()和安全的退出方法quitSafely();
具体有关HandlerThread的内容可以看另一篇博文。
三、总结
整体代码就是按照拍照应该有的流程进行,包括创建界面 - 打开相机 - 显示预览 - 拍摄 - 保存图片 - 关闭服务,以及特殊情况的处理。
通过功能来梳理源码,结构很清晰,同时了解了拍摄前后的一些细节,比如对焦曝光检测。
同时编码过程中也有很多trick,比如信号量在相机开关时的使用,以及handlerThread实现的后台线程,应该会对以后的编码有所启发。
DONE