You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
FFmpegAndroid/Live/src/main/java/com/frank/live/camera/Camera2Helper.java

581 lines
20 KiB

package com.frank.live.camera;
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.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
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: open, preview and close
* 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 Context context;
private String mCameraId;
private String specificCameraId;
private TextureView mTextureView;
private final int rotation;
private final Point previewViewSize;
private Camera2Listener camera2Listener;
/**
* 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 final CameraDevice.StateCallback mDeviceStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
Log.i(TAG, "onOpened: ");
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 final 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 final Semaphore mCameraOpenCloseLock = new Semaphore(1);
/**
* Orientation of the camera sensor
*/
private int mSensorOrientation;
private Size getBestSupportedSize(List<Size> 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);
for (Size size : sizes) {
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[] temp = null;
private byte[] yuvData = null;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireNextImage();
if (camera2Listener != null && image.getFormat() == ImageFormat.YUV_420_888) {
Image.Plane[] planes = image.getPlanes();
lock.lock();
int offset = 0;
int width = image.getWidth();
int height = image.getHeight();
int len = width * height;
if (yuvData == null) {
yuvData = new byte[len * 3 / 2];
}
planes[0].getBuffer().get(yuvData, offset, len);
offset += len;
for (int i = 1; i < planes.length; i++) {
int srcIndex = 0, dstIndex = 0;
int rowStride = planes[i].getRowStride();
int pixelsStride = planes[i].getPixelStride();
ByteBuffer buffer = planes[i].getBuffer();
if (temp == null || temp.length != buffer.capacity()) {
temp = new byte[buffer.capacity()];
}
buffer.get(temp);
for (int j = 0; j < height / 2; j++) {
for (int k = 0; k < width / 2; k++) {
yuvData[offset + dstIndex++] = temp[srcIndex];
srcIndex += pixelsStride;
}
if (pixelsStride == 2) {
srcIndex += rowStride - width;
} else if (pixelsStride == 1) {
srcIndex += rowStride - width / 2;
}
}
offset += len / 4;
}
if (camera2Listener != null) {
camera2Listener.onPreviewFrame(yuvData);
}
lock.unlock();
}
image.close();
}
}
}