package com.frank.live.camera2; import android.Manifest; import android.annotation.TargetApi; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.ImageFormat; import android.graphics.Matrix; import android.graphics.Point; import android.graphics.RectF; import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.StreamConfigurationMap; import android.media.Image; import android.media.ImageReader; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.util.Log; import android.util.Size; import android.view.Surface; import android.view.TextureView; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import androidx.annotation.NonNull; /** * Camera2操作 * Created by frank on 2019/12/18. */ @TargetApi(21) public class Camera2Helper { private static final String TAG = Camera2Helper.class.getSimpleName(); public static final String CAMERA_ID_FRONT = "1"; public static final String CAMERA_ID_BACK = "0"; private String mCameraId; private String specificCameraId; private Camera2Listener camera2Listener; private TextureView mTextureView; private int rotation; private Point previewViewSize; private Context context; /** * A {@link CameraCaptureSession } for camera preview. */ private CameraCaptureSession mCaptureSession; /** * A reference to the opened {@link CameraDevice}. */ private CameraDevice mCameraDevice; private Size mPreviewSize; private Camera2Helper(Builder builder) { mTextureView = builder.previewDisplayView; specificCameraId = builder.specificCameraId; camera2Listener = builder.camera2Listener; rotation = builder.rotation; previewViewSize = builder.previewViewSize; context = builder.context; } public void switchCamera() { if (CAMERA_ID_BACK.equals(mCameraId)) { specificCameraId = CAMERA_ID_FRONT; } else if (CAMERA_ID_FRONT.equals(mCameraId)) { specificCameraId = CAMERA_ID_BACK; } stop(); start(); } private int getCameraOri(int rotation, String cameraId) { int degrees = rotation * 90; switch (rotation) { case Surface.ROTATION_0: degrees = 0; break; case Surface.ROTATION_90: degrees = 90; break; case Surface.ROTATION_180: degrees = 180; break; case Surface.ROTATION_270: degrees = 270; break; default: break; } int result; if (CAMERA_ID_FRONT.equals(cameraId)) { result = (mSensorOrientation + degrees) % 360; result = (360 - result) % 360; } else { result = (mSensorOrientation - degrees + 360) % 360; } Log.i(TAG, "getCameraOri: " + rotation + " " + result + " " + mSensorOrientation); return result; } private final TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) { Log.i(TAG, "onSurfaceTextureAvailable: "); openCamera(); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) { Log.i(TAG, "onSurfaceTextureSizeChanged: "); configureTransform(width, height); } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) { Log.i(TAG, "onSurfaceTextureDestroyed: "); return true; } @Override public void onSurfaceTextureUpdated(SurfaceTexture texture) { } }; private CameraDevice.StateCallback mDeviceStateCallback = new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice cameraDevice) { Log.i(TAG, "onOpened: "); // This method is called when the camera is opened. We start camera preview here. mCameraOpenCloseLock.release(); mCameraDevice = cameraDevice; createCameraPreviewSession(); if (camera2Listener != null) { camera2Listener.onCameraOpened(mPreviewSize, getCameraOri(rotation, mCameraId)); } } @Override public void onDisconnected(@NonNull CameraDevice cameraDevice) { Log.i(TAG, "onDisconnected: "); mCameraOpenCloseLock.release(); cameraDevice.close(); mCameraDevice = null; if (camera2Listener != null) { camera2Listener.onCameraClosed(); } } @Override public void onError(@NonNull CameraDevice cameraDevice, int error) { Log.i(TAG, "onError: "); mCameraOpenCloseLock.release(); cameraDevice.close(); mCameraDevice = null; if (camera2Listener != null) { camera2Listener.onCameraError(new Exception("error occurred, code is " + error)); } } }; private CameraCaptureSession.StateCallback mCaptureStateCallback = new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) { Log.i(TAG, "onConfigured: "); // The camera is already closed if (null == mCameraDevice) { return; } // When the session is ready, we start displaying the preview. mCaptureSession = cameraCaptureSession; try { mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), new CameraCaptureSession.CaptureCallback() { }, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onConfigureFailed( @NonNull CameraCaptureSession cameraCaptureSession) { Log.i(TAG, "onConfigureFailed: "); if (camera2Listener != null) { camera2Listener.onCameraError(new Exception("configureFailed")); } } }; /** * An additional thread for running tasks that shouldn't block the UI. */ private HandlerThread mBackgroundThread; /** * A {@link Handler} for running tasks in the background. */ private Handler mBackgroundHandler; private ImageReader mImageReader; /** * {@link CaptureRequest.Builder} for the camera preview */ private CaptureRequest.Builder mPreviewRequestBuilder; /** * A {@link Semaphore} to prevent the app from exiting before closing the camera. */ private Semaphore mCameraOpenCloseLock = new Semaphore(1); /** * Orientation of the camera sensor */ private int mSensorOrientation; private Size getBestSupportedSize(List sizes) { Size defaultSize = sizes.get(0); Log.e(TAG, "default width=" + defaultSize.getWidth() + "--height=" + defaultSize.getHeight()); int defaultDelta = Math.abs(defaultSize.getWidth() * defaultSize.getHeight() - previewViewSize.x * previewViewSize.y); Iterator supportIterator = sizes.iterator(); while (supportIterator.hasNext()) { Size size = (Size) supportIterator.next(); Log.e(TAG, "current width=" + defaultSize.getWidth() + "--height=" + defaultSize.getHeight()); int currentDelta = Math.abs(size.getWidth() * size.getHeight() - previewViewSize.x * previewViewSize.y); if (currentDelta < defaultDelta) { defaultDelta = currentDelta; defaultSize = size; } } Log.e(TAG, "final width=" + defaultSize.getWidth() + "--height=" + defaultSize.getHeight()); return defaultSize; } public synchronized void start() { if (mCameraDevice != null) { return; } 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(); } else { mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); } } public synchronized void stop() { if (mCameraDevice == null) { return; } closeCamera(); stopBackgroundThread(); } public void release() { stop(); mTextureView = null; camera2Listener = null; context = null; } private void setUpCameraOutputs(CameraManager cameraManager) { try { if (configCameraParams(cameraManager, specificCameraId)) { return; } for (String cameraId : cameraManager.getCameraIdList()) { if (configCameraParams(cameraManager, cameraId)) { 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. if (camera2Listener != null) { camera2Listener.onCameraError(e); } } } private boolean configCameraParams(CameraManager manager, String cameraId) throws CameraAccessException { CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); StreamConfigurationMap map = characteristics.get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); if (map == null) { return false; } mPreviewSize = getBestSupportedSize(new ArrayList<>(Arrays.asList(map.getOutputSizes(SurfaceTexture.class)))); mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(), ImageFormat.YUV_420_888, 2); mImageReader.setOnImageAvailableListener( new OnImageAvailableListenerImpl(), mBackgroundHandler); mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); mCameraId = cameraId; return true; } private void openCamera() { CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); setUpCameraOutputs(cameraManager); configureTransform(mTextureView.getWidth(), mTextureView.getHeight()); try { if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) { throw new RuntimeException("Time out waiting to lock camera opening."); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { return; } cameraManager.openCamera(mCameraId, mDeviceStateCallback, mBackgroundHandler); } catch (CameraAccessException | InterruptedException e) { if (camera2Listener != null) { camera2Listener.onCameraError(e); } } } /** * Closes the current {@link CameraDevice}. */ private void closeCamera() { try { mCameraOpenCloseLock.acquire(); if (null != mCaptureSession) { mCaptureSession.close(); mCaptureSession = null; } if (null != mCameraDevice) { mCameraDevice.close(); mCameraDevice = null; } if (null != mImageReader) { mImageReader.close(); mImageReader = null; } if (camera2Listener != null) { camera2Listener.onCameraClosed(); } } catch (InterruptedException e) { if (camera2Listener != null) { camera2Listener.onCameraError(e); } } finally { mCameraOpenCloseLock.release(); } } /** * Starts a background thread and its {@link Handler}. */ private void startBackgroundThread() { mBackgroundThread = new HandlerThread("CameraBackground"); mBackgroundThread.start(); mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); } /** * Stops the background thread and its {@link Handler}. */ private void stopBackgroundThread() { mBackgroundThread.quitSafely(); try { mBackgroundThread.join(); mBackgroundThread = null; mBackgroundHandler = null; } catch (InterruptedException e) { e.printStackTrace(); } } /** * Creates a new {@link CameraCaptureSession} for camera preview. */ private void createCameraPreviewSession() { try { SurfaceTexture texture = mTextureView.getSurfaceTexture(); assert texture != null; // We configure the size of default buffer to be the size of camera preview we want. texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); // This is the output Surface we need to start preview. Surface surface = new Surface(texture); // We set up a CaptureRequest.Builder with the output Surface. mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); mPreviewRequestBuilder.addTarget(surface); mPreviewRequestBuilder.addTarget(mImageReader.getSurface()); // Here, we create a CameraCaptureSession for camera preview. mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()), mCaptureStateCallback, mBackgroundHandler ); } catch (CameraAccessException e) { e.printStackTrace(); } } /** * Configures the necessary {@link Matrix} transformation to `mTextureView`. * This method should be called after the camera preview size is determined in * setUpCameraOutputs and also the size of `mTextureView` is fixed. * * @param viewWidth The width of `mTextureView` * @param viewHeight The height of `mTextureView` */ private void configureTransform(int viewWidth, int viewHeight) { if (null == mTextureView || null == mPreviewSize) { return; } Matrix matrix = new Matrix(); RectF viewRect = new RectF(0, 0, viewWidth, viewHeight); RectF bufferRect = new RectF(0, 0, mPreviewSize.getHeight(), mPreviewSize.getWidth()); float centerX = viewRect.centerX(); float centerY = viewRect.centerY(); if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) { bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY()); matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL); float scale = Math.max( (float) viewHeight / mPreviewSize.getHeight(), (float) viewWidth / mPreviewSize.getWidth()); matrix.postScale(scale, scale, centerX, centerY); matrix.postRotate((90 * (rotation - 2)) % 360, centerX, centerY); } else if (Surface.ROTATION_180 == rotation) { matrix.postRotate(180, centerX, centerY); } Log.i(TAG, "configureTransform: " + getCameraOri(rotation, mCameraId) + " " + rotation * 90); mTextureView.setTransform(matrix); } public static final class Builder { private TextureView previewDisplayView; private String specificCameraId; private Camera2Listener camera2Listener; private Point previewViewSize; private int rotation; private Context context; public Builder() { } public Builder previewOn(TextureView val) { previewDisplayView = val; return this; } public Builder previewViewSize(Point val) { previewViewSize = val; return this; } public Builder rotation(int val) { rotation = val; return this; } public Builder specificCameraId(String val) { specificCameraId = val; return this; } public Builder cameraListener(Camera2Listener val) { camera2Listener = val; return this; } public Builder context(Context val) { context = val; return this; } public Camera2Helper build() { if (previewDisplayView == null) { throw new NullPointerException("must preview on a textureView or a surfaceView"); } return new Camera2Helper(this); } } private class OnImageAvailableListenerImpl implements ImageReader.OnImageAvailableListener { private byte[] yPlane; private byte[] uPlane; private byte[] vPlane; private ReentrantLock lock = new ReentrantLock(); @Override public void onImageAvailable(ImageReader reader) { Image image = reader.acquireNextImage(); // YUV_420_888 if (camera2Listener != null && image.getFormat() == ImageFormat.YUV_420_888) { Image.Plane[] planes = image.getPlanes(); lock.lock(); if (yPlane == null || uPlane == null || vPlane == null) { yPlane = new byte[planes[0].getBuffer().limit() - planes[0].getBuffer().position()]; uPlane = new byte[planes[1].getBuffer().limit() - planes[1].getBuffer().position()]; vPlane = new byte[planes[2].getBuffer().limit() - planes[2].getBuffer().position()]; } if (image.getPlanes()[0].getBuffer().remaining() == yPlane.length) { planes[0].getBuffer().get(yPlane); planes[1].getBuffer().get(uPlane); planes[2].getBuffer().get(vPlane); if (camera2Listener != null) { camera2Listener.onPreviewFrame(yPlane, uPlane, vPlane); } } lock.unlock(); } image.close(); } } }