本篇的任务目标是使用Camera API进行视频的采集,分别用SurfaceView和TextureView预览Camera数据,并获取到NV21数据回调。
因为camera api在5.0之后标记过时了,所以这儿使用的是camera2 api来实现这些功能。对于使用camera api预览数据在之前一篇博文Camera API笔记中有简单的介绍。也可以参考这篇文章。
NV21是Android Camera默认的图像格式。什么是NV21格式呢?NV21是一种YUV(又称YCbCr) 图像格式. Y表示亮度值(Luminance),V和U分别代表蓝色(Cb)和红色(Cr)色度(Chrominance)。 一个2×2的像素, 4个Y对应1个V和1个U。
一个用于检测、表征、连接 CameraDevices 的系统服务管理。官网是这样介绍的 A system service manager for detecting, characterizing, and connecting to CameraDevices.
代表具体连接到那个摄像头。
打开成功在CameraDevice.StateCallback.onOpened(CameraDevice) 回调中可获得 CameraDevice 实例,然后可以通过调用CameraDevice.createCaptureSession(SessionConfiguration)和CameraDevice.createCaptureRequest(int)来设置摄像机设备捕获图像的会话回调和请求。
一个捕获请求,可通过 CaptureRequest.Builder 的build() 方法获得。
可通过addTarget(Surface)方法添加一个目标显示,之后才能在SurfaceView、TextureView或ImageReader显示。其他属性设置通过set(Key key, T value) 方法。
相机捕获会话,可以通过其进行相机的预览与拍照等。
可以通过为creationCaptureSession() 方法提供一组输出显示来创建一个CamerCaptureSession,调用setRepeatingRequest() 方法开始预览,调用capture()方法进行拍照。
private void startCameraThread() {
mCameraThread = new HandlerThread("CameraThread");
mCameraThread.start();
mCameraHandler = new Handler(mCameraThread.getLooper());
}
/**
* 得到相机id和预览大小
*/
private void generalCameraIdAndSize(int width, int height) {
try {
for (String cameraId : mCameraManager.getCameraIdList()) {
//查询摄像头属性
CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);
Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
//默认打开后置摄像头
if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {
continue;
}
//获取StreamConfigurationMan,它管理着摄像头支持的输出格式和尺寸
StreamConfigurationMap configurationMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (configurationMap == null) {
continue;
}
//获取预览尺寸
mPreviewSize = chooseOptimalSize(configurationMap.getOutputSizes(SurfaceTexture.class), width, height);
mCameraId = cameraId;
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private Size chooseOptimalSize(Size[] outputSizes, int width, int height) {
List sizeList = new ArrayList<>();
for (Size size : outputSizes) {
if (width > height) {
if (size.getWidth() > width && size.getHeight() > height) {
sizeList.add(size);
}
} else {
if (size.getWidth() > height && size.getHeight() > width) {
sizeList.add(size);
}
}
}
if (sizeList.size() > 0) {
return Collections.min(sizeList, new Comparator() {
@Override
public int compare(Size o1, Size o2) {
return Long.signum(o1.getWidth() * o1.getHeight() - o2.getWidth() * o2.getHeight());
}
});
}
return outputSizes[0];
}
try {
mCameraManager.openCamera(mCameraId, mStateCallback, mCameraHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
/**
* 创建时的状态回调
*/
private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
mCameraDevice = camera;
//在开启预览前设置
setupImageReader();
if (isSurfaceView) {
startSurfaceViewPreview();
} else {
startTextureViewPreview();
}
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
if (mCameraDevice != null) {
mCameraDevice.close();
mCameraDevice = null;
}
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
if (mCameraDevice != null) {
mCameraDevice.close();
mCameraDevice = null;
}
finish();
}
};
/**
* 设置ImageReader
*/
private void setupImageReader() {
//前面三个参数分别是需要的尺寸和格式,最后一个是一次获取几帧数据
mImageReaderPreview = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(), ImageFormat.JPEG, 2);
//监听ImageReader事件,当有可用的图像流数据时回调,它的参数就是预览帧数据
mImageReaderPreview.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
//获取捕获的照片数据
Image image = reader.acquireLatestImage();
mCameraHandler.post(new ImageSaver(image, mFile));
}
}, null);
}
SurfaceView
/**
* SurfaceView开始预览
*/
private void startSurfaceViewPreview() {
//获取输出的surface
Surface surface = mSurfaceView.getHolder().getSurface();
try {
final CaptureRequest.Builder captureRequest = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
//添加输出的surface
captureRequest.addTarget(surface);
captureRequest.addTarget(mImageReaderPreview.getSurface());
//创建CameraCaptureSession时加上mImageReaderPreview.getSurface(),这样预览数据就同时输出到两个surface了
mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReaderPreview.getSurface()), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mCameraCaptureSession = session;
//自动对焦
captureRequest.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
CaptureRequest request = captureRequest.build();
try {
mCameraCaptureSession.setRepeatingRequest(request, null, mCameraHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
}
}, mCameraHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
TextureView
/**
* TextureView开始预览
*/
private void startTextureViewPreview() {
SurfaceTexture texture = mTextureView.getSurfaceTexture();
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
Surface surface = new Surface(texture);
try {
final CaptureRequest.Builder captureRequest = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
captureRequest.addTarget(surface);
captureRequest.addTarget(mImageReaderPreview.getSurface());
mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReaderPreview.getSurface()), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mCameraCaptureSession = session;
CaptureRequest request = captureRequest.build();
try {
mCameraCaptureSession.setRepeatingRequest(request, null, mCameraHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
}
}, mCameraHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
/**
* 拍照,点击之前请先预览
*/
public void takePhoto(View view) {
//创建请求拍照的CaptureRequest
try {
final CaptureRequest.Builder captureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
//获取屏幕方向
int rotation = getWindowManager().getDefaultDisplay().getRotation();
//设置CaptureRequest输出到ImageReader
captureRequestBuilder.addTarget(mImageReaderPreview.getSurface());
if (isSurfaceView) {
captureRequestBuilder.addTarget(mSurfaceView.getHolder().getSurface());
} else {
captureRequestBuilder.addTarget(new Surface(mTextureView.getSurfaceTexture()));
}
//设置拍照方向
captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATION.get(rotation));
//拍照完成后重启预览,因为拍照后会导致预览停止
final CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
//重启预览
try {
mCameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, mCameraHandler);
Toast.makeText(CameraPreviewActivity.this, "拍照成功", Toast.LENGTH_SHORT).show();
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
};
//停止预览
mCameraCaptureSession.stopRepeating();
//调用拍照方法
mCameraCaptureSession.capture(captureRequestBuilder.build(), mCaptureCallback, mCameraHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
/**
* 停止预览
*/
private void stopPreview() {
if (mCameraCaptureSession != null) {
mCameraCaptureSession.close();
mCameraCaptureSession = null;
}
if (mCameraDevice != null) {
mCameraDevice.close();
mCameraDevice = null;
}
if (mImageReaderPreview != null) {
mImageReaderPreview.close();
mImageReaderPreview = null;
}
}
/**
* 停止camera线程和其handler
*/
private void stopCameraThread() {
mCameraThread.quitSafely();
try {
mCameraThread.join();
mCameraThread = null;
mCameraHandler = null;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
这里是官方Demo中的一段,我理解的是camera2 API 没有PreviewCallback回调,通过image获取的字节数组就相当于camera1的PreviewCallback回调预览数据,不知道对不对,如有错误请告知。
/**
* Saves a JPEG {@link Image} into the specified {@link File}.
*/
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() {
//我们可以将这帧数据转成字节数组,类似于Camera1的PreviewCallback回调的预览帧数据
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 {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
至此,预览及采集数据完成。代码已上传Github
官方文档
Camera2 API 采集视频并SurfaceView、TextureView 预览