Life was like a box of chocolates, you never know what you're gonna get.
生命就像一盒巧克力,你永远无法知道下一个是什么味道的。
- Android Camera系列(一):SurfaceView+Camera
- Android Camera系列(二):TextureView+Camera
- Android Camera系列(三):GLSurfaceView+Camera
- Android Camera系列(四):TextureView+OpenGL ES+Camera
- Android Camera系列(五):Camera2
- Android Camera系列(六):MediaCodec视频编码上-编码YUV
- Android Camera系列(七):MediaCodec视频编码中-OpenGL ES多线程渲染
- Android Camera系列(八):MediaCodec视频编码下-OpenGL ES离屏渲染
本系列主要讲述Android开发中Camera的相关操作、预览方式、视频录制等。项目结构简单、代码耦合性低,旨在帮助大家能从中有所收获(方便copy :) ),对于个人来说也是一个总结的好机会。
Android5.0开始Google推荐使用Camera2
替代android.hardware.Camera
,于是便有了这篇文章。这篇文章我们基于Android Camera系列(一):SurfaceView+Camera中定义好的ICameraManager接口来封装Camera2。
一.概述
Camera2 API是Android 5.0(Lollipop)之后引入的新版相机API。与早期的Camera API相比,Camera2提供了更多的功能和对摄像头硬件的更深入的控制。这使得开发者可以实现更复杂、更高级的摄像头功能,如实时预览、拍照、录像、对焦、闪光灯控制等。
camera2对于camera来说是一套全新的API,这就需要我们先了解下Camera2的整体架构,如下图:
Google重新设计Android Camera API 的目的在于大幅提高应用对于 Android 设备上的相机子系统的控制能力,同时重新组织 API,提高其效率和可维护性。
在CaptureRequest中设置不同的Surface用于接收不同的图片数据,最后从不同的Surface中获取到图片数据和包含拍照相关信息的CaptureResult。
Camera2的核心类和开发步骤请看下图:
二. 相关类
1. CameraManager
CameraManager 是一个负责查询和建立相机连接的系统服务,功能如下:
- 获取当前设备中可用的相机列表
getCameraIdList
- 根据摄像头id返回该摄像头的相关信息
getCameraCharacteristics(String cameraId)
- 根据指定的相机 ID 连接相机设备
- 提供将闪光灯设置成手电筒模式的快捷方式
2. CameraDevice
描述系统摄像头,类似于android.hardware.Camera
- 根据指定的参数创建 CameraCaptureSession。
- 根据指定的模板创建 CaptureRequest。
- 关闭相机设备。
- 监听相机设备的状态,例如断开连接、开启成功和开启失败等。
Camera1和CameraDevice很类似,但又不同。Camera 类几乎负责了所有相机的操作,而 CameraDevice 的功能则十分的单一,就是只负责建立相机连接的事务,而更加细化的相机操作则交给了稍后会介绍的 CameraCaptureSession。
3. CameraCaptureSession
当需要拍照、预览等功能时,需要先创建该类的实例,然后通过该实例里的方法进行控制。
一个 CameraDevice 一次只能开启一个 CameraCaptureSession,绝大部分的相机操作都是通过向 CameraCaptureSession 提交一个 Capture 请求实现的,例如拍照、连拍、设置闪光灯模式、触摸对焦、显示预览画面等等。
4. CameraCharacteristics
CameraCharacteristics 是一个只读的相机信息提供者,其内部携带大量的相机信息,包括代表相机朝向的 LENS_FACING
;判断闪光灯是否可用的 FLASH_INFO_AVAILABLE
;获取所有可用 AE 模式的 CONTROL_AE_AVAILABLE_MODES
等等。CameraCharacteristics 有点像 Camera1 的 Camera.CameraInfo
或者 Camera.Parameters
。
5. CaptureRequest
CaptureRequest 是向 CameraCaptureSession 提交 Capture 请求时的信息载体,其内部包括了本次 Capture 的参数配置和接收图像数据的 Surface。CaptureRequest 可以配置的信息非常多,包括图像格式、图像分辨率、传感器控制、闪光灯控制、3A 控制等等,可以说绝大部分的相机参数都是通过 CaptureRequest 配置的。值得注意的是每一个 CaptureRequest 表示一帧画面的操作,这意味着你可以精确控制每一帧的 Capture 操作。
6. CaptureResult
CaptureResult 是每一次 Capture 操作的结果,里面包括了很多状态信息,包括闪光灯状态、对焦状态、时间戳等等。例如你可以在拍照完成的时候,通过 CaptureResult 获取本次拍照时的对焦状态和时间戳。需要注意的是,CaptureResult 并不包含任何图像数据,前面我们在介绍 Surface 的时候说了,图像数据都是从 Surface 获取的。
7. TotalCaptureResult
TotalCaptureResult继承自CaptureResult,功能类似
8. StreamConfigurationMap
获取输出流配置,如:可支持的预览尺寸
9. Image
一个完整的图片缓存,可从该对象中获取YUV、JPEG、RGBA等数据
10. ImageReader
用于从相机打开的通道中读取需要的格式的原始图像数据,可以设置多个ImageReader。
三. 开发步骤
我们按照ICameraManager
定义的接口实现Camera2的API,我们这里只讲关键步骤
1. 打开摄像头
- 获取想要打开的CameraId
- 配置Camera参数,如:预览尺寸、拍照尺寸
- 打开Camera
public void openCamera() {
Log.v(TAG, "openCamera");
if (mCameraDevice != null) {
return;
}
mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
// 相机ID
String cameraId = setUpCameraOutputs(mCameraManager);
if (cameraId == null) return;
startBackgroundThread(); // 对应 releaseCamera() 方法中的 stopBackgroundThread()
mOrientationEventListener.enable();
try {
mCameraCharacteristics = mCameraManager.getCameraCharacteristics(cameraId);
// 每次切换摄像头计算一次就行,结果缓存到成员变量中
initDisplayRotation();
initZoomParameter();
mFacing = mCameraCharacteristics.get(CameraCharacteristics.LENS_FACING);
Logs.i(TAG, "facing:" + mFacing);
if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;
}
// 打开摄像头
mCameraManager.openCamera(cameraId, mStateCallback, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
选择想要打开的CameraId
private String setUpCameraOutputs(CameraManager cameraManager) {
String cameraId = null;
try {
// 获取相机ID列表
String[] cameraIdList = cameraManager.getCameraIdList();
for (String id : cameraIdList) {
// 获取相机特征
CameraCharacteristics cameraCharacteristics = cameraManager.getCameraCharacteristics(id);
int facing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING);
if (mCameraId == 0 && facing == CameraCharacteristics.LENS_FACING_BACK) {
cameraId = id;
break;
} else if (mCameraId == 1 && facing == CameraCharacteristics.LENS_FACING_FRONT) {
cameraId = id;
break;
}
}
if (cameraId == null) {
onOpenError(CAMERA_ERROR_NO_ID, "Camera id:" + mCameraId + " not found.");
return null;
}
if (!configCameraParams(cameraManager, cameraId)) {
onOpenError(CAMERA_ERROR_OPEN, "Config camera error.");
return null;
}
} catch (CameraAccessException e) {
onOpenError(CAMERA_ERROR_OPEN, e.getMessage());
return null;
} catch (NullPointerException e) {
onOpenError(CAMERA_ERROR_OPEN, e.getMessage());
return null;
}
return cameraId;
}
配置预览和拍照尺寸
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;
}
Size previewSize = getSuitableSize(new ArrayList<>(Arrays.asList(map.getOutputSizes(SurfaceTexture.class))));
Logs.i(TAG, "previewSize: " + previewSize);
mPreviewSize = previewSize;
mPreviewWidth = mPreviewSize.getWidth();
mPreviewHeight = mPreviewSize.getHeight();
Size[] supportPictureSizes = map.getOutputSizes(ImageFormat.JPEG);
Size pictureSize = Collections.max(Arrays.asList(supportPictureSizes), new CompareSizesByArea());
mPictureSize = pictureSize;
Logs.i(TAG, "pictureSize: " + pictureSize);
mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
return true;
}
2. 开始预览
- 创建预览请求
CaptureRequest
- 创建预览会话
public void startPreview(SurfaceTexture surfaceTexture) {
if (previewing || !isOpen()) {
return;
}
previewing = true;
surfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
mPreviewSurface = new Surface(surfaceTexture);
initPreviewRequest();
createCommonSession();
}
Camera2设置预览尺寸和Camera1不同,Camera2只需要设置SurfaceTexture的尺寸即可:
SurfaceTexture.setDefaultBufferSize(width, height)
,Camera2会选择匹配Surface的尺寸的数据渲染到上面。但是这也不是说我们可以随便设置SurfaceTexture的大小,我们最好是从Camera2中选择支持的预览尺寸进行设置。
初始化预览请求
private void initPreviewRequest() {
if (mPreviewSurface == null) {
Log.e(TAG, "initPreviewRequest failed, mPreviewSurface is null");
return;
}
if (!isOpen()) {
return;
}
try {
mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
// 设置预览输出的 Surface
mPreviewRequestBuilder.addTarget(mPreviewSurface);
// 设置连续自动对焦
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// 设置自动曝光
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
// 设置自动白平衡
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
创建Capture会话,我们需要将用到的所有Surface传进去。此后无论你需要预览还是拍照,摄像头都会将数据渲染到对应的Surface上。
private void createCommonSession() {
List<Surface> outputs = new ArrayList<>();
// preview output
if (mPreviewSurface != null) {
Log.d(TAG, "createCommonSession add target mPreviewSurface");
outputs.add(mPreviewSurface);
}
// picture output
Size pictureSize = mPictureSize;
if (pictureSize != null) {
Log.d(TAG, "createCommonSession add target mPictureImageReader");
mPictureImageReader = ImageReader.newInstance(pictureSize.getWidth(), pictureSize.getHeight(), ImageFormat.JPEG, 1);
outputs.add(mPictureImageReader.getSurface());
}
// preview output
if (!mPreviewBufferCallbacks.isEmpty()) {
mPreviewImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(), ImageFormat.YUV_420_888, 2);
mPreviewImageReader.setOnImageAvailableListener(new OnImageAvailableListenerImpl(), mBackgroundHandler);
outputs.add(mPreviewImageReader.getSurface());
mPreviewRequestBuilder.addTarget(mPreviewImageReader.getSurface());
}
try {
// 一个session中,所有CaptureRequest能够添加的target,必须是outputs的子集,所以在创建session的时候需要都添加进来
mCameraDevice.createCaptureSession(outputs, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mCaptureSession = session;
startPreview();
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
Log.e(TAG, "ConfigureFailed. session: " + session);
previewing = false;
}
}, mBackgroundHandler); // handle 传入 null 表示使用当前线程的 Looper
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private void startPreview() {
Log.v(TAG, "startPreview");
if (mCaptureSession == null || mPreviewRequestBuilder == null) {
Log.w(TAG, "startPreview: mCaptureSession or mPreviewRequestBuilder is null");
return;
}
try {
// 开始预览,即一直发送预览的请求
CaptureRequest captureRequest = mPreviewRequestBuilder.build();
mCaptureSession.setRepeatingRequest(captureRequest, null, mBackgroundHandler);
Logs.i(TAG, "name:" + Thread.currentThread().getName());
mUIHandler.post(() -> onPreview(mPreviewWidth, mPreviewHeight));
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
Camera2中很多的操作都不是线性的,都是在回调里获取结果才能进行下一步操作。例如创建capture会话
createCaptureSession
得在onConfigured
回调后才能开始真正的循环预览。
3. 关闭预览
将Capture会话关闭
public void stopPreview() {
Log.v(TAG, "stopPreview");
if (mCaptureSession == null) {
Log.w(TAG, "stopPreview: mCaptureSession is null");
return;
}
try {
mCaptureSession.stopRepeating();
previewing = false;
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
4. 拍照
- 使用同一个CaptureSession,拍照之前关闭预览,拍照成功后再次开启预览
public void takePicture(PictureBufferCallback pictureBufferCallback) {
mPictureBufferCallback = pictureBufferCallback;
captureStillPicture(reader -> {
Image image = reader.acquireNextImage();
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
image.close();
if (mPictureBufferCallback != null) {
mPictureBufferCallback.onPictureToken(bytes);
}
});
}
public void captureStillPicture(ImageReader.OnImageAvailableListener onImageAvailableListener) {
if (mPictureImageReader == null) {
Log.w(TAG, "captureStillPicture failed! mPictureImageReader is null");
return;
}
mPictureImageReader.setOnImageAvailableListener(onImageAvailableListener, mBackgroundHandler);
try {
// 创建一个用于拍照的Request
CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(mPictureImageReader.getSurface());
captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getJpegOrientation(mDeviceOrientation));
// 预览如果有放大,拍照的时候也应该保存相同的缩放
Rect zoomRect = mPreviewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION);
if (zoomRect != null) {
captureBuilder.set(CaptureRequest.SCALER_CROP_REGION, zoomRect);
}
stopPreview();
mCaptureSession.abortCaptures();
final long time = System.currentTimeMillis();
mCaptureSession.capture(captureBuilder.build(), new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
Log.w(TAG, "onCaptureCompleted, time: " + (System.currentTimeMillis() - time));
try {
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
mCaptureSession.capture(mPreviewRequestBuilder.build(), null, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
startPreview();
}
}, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
5. 释放摄像头
- 关闭Capture会话
- 关闭CameraDevice
- 关闭拍照ImageReader
- 关闭预览ImageReader
- 关闭Camera线程
public void releaseCamera() {
Log.v(TAG, "releaseCamera");
stopPreview();
if (null != mCaptureSession) {
mCaptureSession.close();
mCaptureSession = null;
}
if (mCameraDevice != null) {
mCameraDevice.close();
mCameraDevice = null;
}
if (mPictureImageReader != null) {
mPictureImageReader.close();
mPictureImageReader = null;
}
if (mPreviewImageReader != null) {
mPreviewImageReader.close();
mPreviewImageReader = null;
}
mOrientationEventListener.disable();
stopBackgroundThread(); // 对应 openCamera() 方法中的 startBackgroundThread()
mUIHandler.post(() -> onClose());
}
四. 预览YUV获取
Camera2想要正确获取预览帧数据接下来我们来详细介绍下Camera2获取YUV数据的各种坑。
上面的代码我们知道,在预览环节中我们设置了预览所需的ImageReader
,并且我们指定了数据采集格式为ImageFormat.YUV_420_888
mPreviewImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(), ImageFormat.YUV_420_888, 2);
mPreviewImageReader.setOnImageAvailableListener(new OnImageAvailableListenerImpl(), mBackgroundHandler);
outputs.add(mPreviewImageReader.getSurface());
mPreviewRequestBuilder.addTarget(mPreviewImageReader.getSurface());
通常获取YUV预览数据方式
private class OnImageAvailableListenerImpl implements ImageReader.OnImageAvailableListener {
private byte[] y;
private byte[] u;
private byte[] v;
private byte[] yuvData;
private ReentrantLock lock = new ReentrantLock();
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireNextImage();
// Y:U:V == 4:2:2
if (image.getFormat() == ImageFormat.YUV_420_888) {
Image.Plane[] planes = image.getPlanes();
int width = image.getWidth();
int height = image.getHeight();
// 加锁确保y、u、v来源于同一个Image
lock.lock();
/** Y */
ByteBuffer bufferY = planes[0].getBuffer();
/** U(Cb) */
ByteBuffer bufferU = planes[1].getBuffer();
/** V(Cr) */
ByteBuffer bufferV = planes[2].getBuffer();
// 重复使用同一批byte数组,减少gc频率
if (y == null) {
y = new byte[bufferY.limit() - bufferY.position()];
u = new byte[bufferU.limit() - bufferU.position()];
v = new byte[bufferV.limit() - bufferV.position()];
}
if (yuvData == null) {
yuvData = new byte[width * height * 3 / 2];
}
int ySize = width * height;
YUVFormat yuvFormat = YUVFormat.I420;
if (bufferY.remaining() == y.length) {
bufferY.get(y);
bufferU.get(u);
bufferV.get(v);
}
System.arraycopy(y, 0, yuvData, 0, y.length);
System.arraycopy(u, 0, yuvData, ySize, u.length);
System.arraycopy(v, 0, yuvData, ySize + u.length, v.length);
lock.unlock();
}
image.close();
}
}
正常情况下我们通过
image.getPlanes()
获取三个平面的数据分别是Y、U、V,然后组装起来即可。但是实际情况恰恰没有这么简单,接下来我们看看坑的地方。
1. YUV_420_888
YUV_420_888他是YCbCr的泛化格式,不会具体指明是YU12,YV12,NV12或是NV21。他能够表示任何4:2:0的平面和半平面格式,每个分量用8bits表示。
带有这种格式的图像使用3个独立的Buffer表示,每一个Buffer表示一个颜色平面(Plane)。除了Buffer外,他还提供了rowStride
、pixelStride
来描述对应的Plane。这两个参数是获取YUV数据坑的根源,下面来详细介绍。
使用Image.getPlanes()
获取plane数组:Image.Plane[] planes = image.getPlanes();
它保证planes[0]总是Y,planes[1]总是 U(Cb),planes[2]总是 V(Cr)。并保证Y-Plane永远不会和U/V交叉。U/V-Plane总是有相同的rowStride和pixelStride。
1.1 rowStride和pixelStride
pixelStride
通过getPixelStride()
获取,像素步长,取值1或2。他代表的是行内连续两个颜色之间的距离(步长)。
如果是1,那么每一行中的同一个颜色分量,比如Y分量是连续的。也就是行内索引0,1,2...的颜色分量都是他的。
如果是2,那么每一行中的同一个颜色分量,是不连续。中间会间隔一个元素,也就是行内索引为0,2,4,6...的颜色分量才是他的。
还有个重要的点:假如步长为2,意味索引间隔的颜色才是有效的元素,中间间隔的元素其实是没有意义的。而Android中确实也是这么做的,比如某个plane[1](U分量)步长是2,那么数组下标0,2,4,6...的数据是U分量的,而中间的间隔元素Android会补上V分量,也就是会是UVUVUV...这样去分布。但是当最后一个U分量出现后,最后一个没有意义的元素Android就不补了,也就是最后的V分量就不会补了,直接结束,即是这样分布:UVUVUV...UVUVU。
rowStride
通过getRowStride()
获取,每行数据的宽度,这个和分辨率不是一回事,他是每一行实际存储的空间宽度
1.2 其他参数
width和height
通过getWidth()
和getHeight()
获取,和预览数据分辨率一致
buffer size
这个主要就是plane数组的大小,一般就通过planes[i].length获取
1.3 YUV数据分布和排列
了解了rowStride和pixelStride两个参数后,我们可以来分析下实际场景中遇到的情况了。
(1)Planar格式(P)
我们先看下6*4的图片:
plane[0] 的pixelStride是1,说明没有间隔,Y是连续的;rowStride是6,也就是每行是6个;length=24,24 / 6 = 4,共4行;
plane[1] 的pixelStride也是1,说明没有间隔,U是连续的;rowStride是3,也就是每行是3个;length=6,6 / 3 = 2,共2行,符合YUV420的情况,横纵2:1采样;
plane[2] 和plane[1]相同,V是连续的
上述其实就是YUV420P的标准格式I420,我们期望的解析方式,直接可以取到Y、U、V三个分量。可惜的是,大多数手机不是这样的格式,而是下面要介绍的SP的情况。
(2)Semi-Planar格式(SP)
还是6*4的图片:
plane[0] 的pixelStride是1,说明没有间隔,Y是连续的;rowStride是6,也就是每行是6个;length=24,24 / 6 = 4,共4行;这个Y分量跟Planar格式是一样的。
plane[1] 的pixelStride是2,说明有间隔,U是间隔采样的;我们上面分析过,当pixelStride=2的时候,在U分量中就会间隔插入V分量,因此每一行由本来是Y的一半也就是3,变成了6(也就是rowStride的值),同时会放弃掉最后一个无意义的V分量,所以length=6*2-1=11,行数还是2,纵向不变;
plane[2] 和plane[1]相同
其实我们从上图可以看到,在plane[1]中已经包含了U和V分量了,只不过差了最后一个V分量;对于整张图片来说,少了一个V分量是不影响显示效果的;因此我们可以拿plane[1]的数据,就可以拿到U和V了;plane[2]同理其实也有V和U,这样我们就可以plane[0]+plane[1]组成NV12格式,plane[0]+plane[2]组成NV21格式。
(3)特殊情况
rowStride除了有P和SP格式导致不同以外,他其实还有一个重要的作用。就是在一些特殊的sensor采集的时候,因为芯片处理器要字节对齐取数据等原因导致补齐操作,从而使得每一行所占用的空间比实际数据要多。
继续上图:
plane[0] 的pixelStride是1,说明没有间隔,Y是连续的;rowStride本来应该是6,但是变成了8,最后补了两个空字节,也就是每行是8个;length=32,32 / 8 = 4,共4行;这里我们就需要判断getWidth()
和getRowStride()
是否匹配了,如果不匹配我们就得按行来获取Y分量了。同理plane[1] 和 plane[2] 也得按行获取对应的分量了。
2. 代码实现
根据以上的理论,我们要把所有的情况都考虑到,并且我们增加了YUVFormat
区分是P还是SP
private class OnImageAvailableListenerImpl implements ImageReader.OnImageAvailableListener {
private byte[] y;
private byte[] u;
private byte[] v;
private byte[] yuvData;
private ReentrantLock lock = new ReentrantLock();
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireNextImage();
// Y:U:V == 4:2:2
if (image.getFormat() == ImageFormat.YUV_420_888) {
Image.Plane[] planes = image.getPlanes();
int width = image.getWidth();
int height = image.getHeight();
// 加锁确保y、u、v来源于同一个Image
lock.lock();
/** Y */
ByteBuffer bufferY = planes[0].getBuffer();
/** U(Cb) */
ByteBuffer bufferU = planes[1].getBuffer();
/** V(Cr) */
ByteBuffer bufferV = planes[2].getBuffer();
// 重复使用同一批byte数组,减少gc频率
if (y == null) {
y = new byte[bufferY.limit() - bufferY.position()];
u = new byte[bufferU.limit() - bufferU.position()];
v = new byte[bufferV.limit() - bufferV.position()];
}
if (yuvData == null) {
yuvData = new byte[width * height * 3 / 2];
}
YUVFormat yuvFormat = YUVFormat.I420;
if (bufferY.remaining() == y.length) {
bufferY.get(y);
bufferU.get(u);
bufferV.get(v);
// 数据前期处理
// 处理y
int yRowStride = planes[0].getRowStride();
if (yRowStride == width) {
System.arraycopy(y, 0, yuvData, 0, y.length);
} else {
// 按行提取
for (int i = 0; i < height; i++) {
System.arraycopy(y, i * yRowStride, yuvData, i * width, width);
}
}
int ySize = width * height;
// 判断是p还是sp
if (planes[1].getPixelStride() == 1) { // P
yuvFormat = YUVFormat.I420;
int offset = ySize;
// 处理U
int uRowStride = planes[1].getRowStride();
if (uRowStride == width / 2) {
System.arraycopy(u, 0, yuvData, offset, u.length);
} else {
int rowStride = width / 2;
for (int i = 0; i < height / 2; i++) {
System.arraycopy(u, i * uRowStride, yuvData, offset + i * rowStride, rowStride);
}
}
offset = ySize + width * height / 4;
// 处理V
int vRowStride = planes[2].getRowStride();
if (vRowStride == width / 2) {
System.arraycopy(v, 0, yuvData, offset, v.length);
} else {
int rowStride = width / 2;
for (int i = 0; i < height / 2; i++) {
System.arraycopy(v, i * vRowStride, yuvData, offset + i * rowStride, rowStride);
}
}
} else if (planes[1].getPixelStride() == 2) { // SP
yuvFormat = YUVFormat.NV21;
int offset = width * height;
int uvSize = ySize / 2;
// 处理UV
int uvRowStride = planes[2].getRowStride();
if (uvRowStride == width) {
System.arraycopy(v, 0, yuvData, offset, v.length > uvSize ? uvSize : v.length);
} else {
// 按行提取
int rowSize = height / 2;
for (int i = 0; i < rowSize; i++) {
if (i == rowSize - 1) {
int lastLineSize = v.length - i * uvRowStride;
System.arraycopy(v, i * uvRowStride, yuvData, offset + i * width, lastLineSize < width ? lastLineSize : width);
} else {
System.arraycopy(v, i * uvRowStride, yuvData, offset + i * width, width);
}
}
}
}
}
if (!mPreviewBufferCallbacks.isEmpty()) {
for (PreviewBufferCallback previewBufferCallback : mPreviewBufferCallbacks) {
previewBufferCallback.onPreviewBufferFrame(yuvData, image.getWidth(), image.getHeight(), yuvFormat);
}
}
lock.unlock();
}
image.close();
}
}
最后
该篇文章我们讲述了Camera2的基本用法,当然Camera2还有很多高级的用法需要读者自行去挖掘了。前面的几篇文章我们都有对应的预览视图配合Camera使用,这里我们按照ICameraManager
实现Camera2后,Camera1的预览的视图SurfaceView
、TextureView
、GLSurfaceView
、SurfaceView+OpenGL ES
、TextureView+OpenGL ES
都可以无缝迁移使用Camera2Manager预览。
lib-camera库包结构如下:
包 | 说明 |
---|---|
camera | camera相关操作功能包,包括Camera和Camera2。以及各种预览视图 |
encoder | MediaCdoec录制视频相关,包括对ByteBuffer和Surface的录制 |
gles | opengles操作相关 |
permission | 权限相关 |
util | 工具类 |
每个包都可独立使用做到最低的耦合,方便白嫖
github地址:github.com/xiaozhi003/…,如果对你有帮助可以star下,万分感谢^_^
参考: