justtreee / blog

31 stars 8 forks source link

【Camera】Camera2Basic 源码阅读 #20

Open justtreee opened 5 years ago

justtreee commented 5 years ago

Camera2Basic 源码阅读

本文将通过对 android-Camera2Basic 的源码分析,学习安卓相机的基本开发。

【TODO】getSupportFragmentManager() 这系列的语句是做什么用的?
【TODO】安卓基础:inflater
【TODO】Java 信号量 Semaphore
【TODO】camera:TextureView是什么?
【TODO】OpenGL相关:OpenGL ES texture是什么?

一、CameraActivity

public class CameraActivity extends AppCompatActivity {

    @Override
 protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_camera);
 if (null == savedInstanceState) {
            getSupportFragmentManager().beginTransaction()
                    .replace(R.id.container, Camera2BasicFragment.newInstance())
                    .commit();
 }
    }
}

首先肯定要看这个相机应用是怎么诞生的。

onCreate很简短,主要是实例化一个 Camera2BasicFragment,也是整个项目的重头戏。

【TODO】getSupportFragmentManager() 这系列的语句是做什么用的?

二、Camera2BasicFragment

public static Camera2BasicFragment newInstance() {
    return new Camera2BasicFragment();
}

通过这个方法,之前的活动 CameraActivity 实例化了Fragment:

【此处应有张截图】代码界面贴一张图片

由上图可以看到,整个 com.example.android.camera2basic.Camera2BasicFragment.java 总共有 25 个方法、一个图片保存类、一个比较类以及两个日志类。

根据本app的拍照流程,这25个方法可以分为以下几类:

创建界面 - 打开相机 - 显示预览 - 拍摄 - 保存图片 - 关闭服务

2.1 创建界面

(1) 导入视图的xml

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
 Bundle savedInstanceState) {
    return inflater.inflate(R.layout.fragment_camera2_basic, container, false);
}

【TODO】 安卓基础:inflater

(2)配置按钮

@Override
public void onViewCreated(final View view, Bundle savedInstanceState) {
    // 下方蓝底和拍摄按钮
 view.findViewById(R.id.picture).setOnClickListener(this);
 // 右侧感叹号 信息按钮
 view.findViewById(R.id.info).setOnClickListener(this);
 mTextureView = (AutoFitTextureView) view.findViewById(R.id.texture);
}

设置拍摄按钮以及info按钮的点击事件

(3)相关配置初始化

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
 // 为拍摄得到的图片定义输出路径文件
 mFile = new File(getActivity().getExternalFilesDir(null), "pic.jpg");
}

mFile是图片的输出目标文件

2.2 打开相机

2.2.1 通过API2调用

app层次的代码肯定是无法直接调用硬件层面的摄像头配置的,所以需要使用API2提供的接口来对相机进行打开和关闭,以及异常情况的处理:

/**
 * {@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);
 }
}

2.2.4 通过CameraManager管理相机

CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);

CameraManager是一个系统服务,将相机硬件相关的功能封装成接口供调用。

2.2.* 整体流程代码

整体流程封装如下,在最后的打开相机时,会使用Lock.tryAcquire() 检测锁是否被占用。

/**
 * 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();
}

打开摄像头之后,就要在应用界面显示预览画面。

预览画面时动态且实时的,所以需要摄像头持续不断的获取数据:

所以创建了一个拍摄会话,通过一个重复请求源源不断的获取图片,并以数组形式传递给surface用以显示预览

surface是什么?
是一个接受数据的原始缓冲
经常被SurfaceTexture、MediaRecorder等图片缓冲消费者(consumer of image buffers)创建
或者被生产者如opengl.EGL14、MediaPlayer等调用通过SurfaceTexture创建

【TODO】OpenGL相关:OpenGL ES texture是什么?({@link SurfaceTexture}: Captures frames from an image stream as an OpenGL ES texture.)

/**
 * Creates a new {@link CameraCaptureSession} for camera preview.
 */
private void createCameraPreviewSession() {
    try {
        SurfaceTexture texture = mTextureView.getSurfaceTexture();
 assert texture != null;

 // 我们将默认缓冲区的大小配置为我们想要的相机预览的大小。
 texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());

 // 这是需要开始预览的输出surface
 Surface surface = new Surface(texture);

 // 通过surface配置CaptureRequest.Builder
 mPreviewRequestBuilder
 = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
 mPreviewRequestBuilder.addTarget(surface);

 // Here, we create a CameraCaptureSession for camera preview.
 // 创建CaptureSession会话。
 // 第一个参数 outputs 是一个 List 数组,相机会把捕捉到的图片数据传递给该参数中的 Surface 。
 // 第二个参数 StateCallback 是创建会话的状态回调。
 // 第三个参数描述了 StateCallback 被调用时所在的线程,这里为 null
 mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
 new CameraCaptureSession.StateCallback() {

                    @Override
 public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                        // The camera is already closed
 if (null == mCameraDevice) {
                            return;
 }

                        // When the session is ready, we start displaying the preview.
 mCaptureSession = cameraCaptureSession;
 try {
                            // Auto focus should be continuous for camera preview.
 mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
 CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
 // Flash is automatically enabled when necessary.
 setAutoFlash(mPreviewRequestBuilder);

 // Finally, we start displaying the camera preview.
 mPreviewRequest = mPreviewRequestBuilder.build();

 // 为预览会话持续不断的重复获取捕捉到的图片
 // 并且mPreviewRequest由之前的代码定义:设置为连续自动对焦模式,并在必要时自动开启闪光
 mCaptureSession.setRepeatingRequest(mPreviewRequest,
 mCaptureCallback, mBackgroundHandler);
 } catch (CameraAccessException e) {
                            e.printStackTrace();
 }
                    }

                    @Override
 public void onConfigureFailed(
                            @NonNull CameraCaptureSession cameraCaptureSession) {
                        showToast("Failed");
 }
                }, null
 );
 } catch (CameraAccessException e) {
        e.printStackTrace();
 }
}

2.4 拍摄

2.4.1 点击拍摄按钮拍照:

public void onClick(View view) {
    switch (view.getId()) {
        case R.id.picture: {
            // 点击拍摄键拍照
 takePicture();
 break;
 }
        case R.id.info: {
           。。。
 }
            break;
 }
    }
}

private void takePicture() {
    lockFocus();
}

2.4.2 对焦曝光要正确

拍照,肯定要对焦,并且对焦要稳定不能拉风箱,对于对焦的状态,可通过mState = STATE_WAITING_LOCK;进行控制。

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();
 }
}

通过RequestBuilder配置好自动对焦后,等待对焦稳定,通过session拍摄,此时会调用一个回调进行拍摄,也就是mCaptureCallback。

而拍摄的处理过程就是靠这个回调定义的。

这个回调mCaptureCallback,会分为四种不同的情况:

预览时需要调用摄像头的数据但并不做拍摄操作; mState = STATE_WAITING_LOCK时表示要准备拍照了,此时会根据【对焦状态】进行拍摄或尝试进行预拍摄(precapture);

根据【曝光状态】如“需要闪光灯补光”,将mstate由等待预拍摄(STATE_WAITING_PRECAPTURE)转换为等待非预拍摄(STATE_WAITING_NON_PRECAPTURE)

曝光稳定后拍摄

/**
 * 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();
 }
}

2.7 特殊情况的处理

在阅读代码的过程中,经常会出现一些变量,与“Background”有关,比如mBackgroundThread。最开始看到很疑惑这个东西是干什么的。

/**
 * 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();
}

HandlerThread既然是一个循环线程,那么怎么退出呢?有两种方式,分别是不安全的退出方法quit()和安全的退出方法quitSafely();

具体有关HandlerThread的内容可以看另一篇博文。

private void stopBackgroundThread() {
    // HandlerThread既然是一个循环线程,那么怎么退出呢?有两种方式,分别是不安全的退出方法quit()和安全的退出方法quitSafely():
 mBackgroundThread.quitSafely();
 try {
        mBackgroundThread.join();
 mBackgroundThread = null;
 mBackgroundHandler = null;
 } catch (InterruptedException e) {
        e.printStackTrace();
 }
}

三、总结

整体代码就是按照拍照应该有的流程进行,包括创建界面 - 打开相机 - 显示预览 - 拍摄 - 保存图片 - 关闭服务,以及特殊情况的处理。

通过功能来梳理源码,结构很清晰,同时了解了拍摄前后的一些细节,比如对焦曝光检测。

同时编码过程中也有很多trick,比如信号量在相机开关时的使用,以及handlerThread实现的后台线程,应该会对以后的编码有所启发。

DONE