Android Camera系列(五):Camera2
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的相关操作、预览方式、视频录制等。项目结构简单、代码耦合性低,旨在帮助大家能从中有所收获(方便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);// 相机IDString 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);// 设置预览输出的 SurfacemPreviewRequestBuilder.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 outputif (mPreviewSurface != null) {Log.d(TAG, "createCommonSession add target mPreviewSurface");outputs.add(mPreviewSurface);}// picture outputSize 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 outputif (!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() {@Overridepublic void onConfigured(@NonNull CameraCaptureSession session) {mCaptureSession = session;startPreview();}@Overridepublic 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 {// 创建一个用于拍照的RequestCaptureRequest.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() {@Overridepublic 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();@Overridepublic void onImageAvailable(ImageReader reader) {Image image = reader.acquireNextImage();// Y:U:V == 4:2:2if (image.getFormat() == ImageFormat.YUV_420_888) {Image.Plane[] planes = image.getPlanes();int width = image.getWidth();int height = image.getHeight();// 加锁确保y、u、v来源于同一个Imagelock.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();@Overridepublic void onImageAvailable(ImageReader reader) {Image image = reader.acquireNextImage();// Y:U:V == 4:2:2if (image.getFormat() == ImageFormat.YUV_420_888) {Image.Plane[] planes = image.getPlanes();int width = image.getWidth();int height = image.getHeight();// 加锁确保y、u、v来源于同一个Imagelock.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);// 数据前期处理// 处理yint 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还是spif (planes[1].getPixelStride() == 1) { // PyuvFormat = YUVFormat.I420;int offset = ySize;// 处理Uint 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;// 处理Vint 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) { // SPyuvFormat = YUVFormat.NV21;int offset = width * height;int uvSize = ySize / 2;// 处理UVint 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地址:https://github.com/xiaozhi003/AndroidCamera,https://gitee.com/xiaozhi003/android-camera
参考:
- https://github.com/afei-cn/CameraDemo
- https://github.com/saki4510t/UVCCamera
- https://github.com/google/grafika
- http://www.360doc.com/content/23/1205/11/474846_1106367495.shtml
相关文章:

Android Camera系列(五):Camera2
Life was like a box of chocolates, you never know what you’re gonna get. 生命就像一盒巧克力,你永远无法知道下一个是什么味道的。 Android Camera系列(一):SurfaceViewCamera Android Camera系列(二࿰…...
从DexMV、VideoDex、MimicPlay到SeeDo:从人类视频中学习:机器人的主流训练方法之一
前言 在此文《UMI——斯坦福刷盘机器人:从手持夹持器到动作预测Diffusion Policy(含代码解读)》的1.1节开头有提到 机器人收集训练数据一般有多种方式,比如来自人类视频的视觉演示 有的工作致力于从视频数据——例如YouTube视频中进行策略学习 即最常见…...

如何在Docker中运行Squid
测试环境 VMware Rocky Linux 9.4 实现步骤 过程:写一个Dockerfile构建Squid镜像; 再写一个启动脚本start_squid.sh,在启动脚本中配置并运行Squid。 编写Dockerfile 以rockylinux9.3做基础镜像,通过yum安装Squid, 拷贝squid.conf FROM …...

Ubuntu22.04 加入AD域
Ubuntu22.04 加入AD域 要在Ubuntu 22.04上加入Active Directory (AD) 域,你可以使用realmd和sssd服务。以下是加入AD域的步骤和示例配置: 更新系统软件包列表: sudo apt update 下载安装必要的软件包: sudo apt install realm…...

Docker 构建 Miniconda3 Python 运行环境实战指南
Docker 构建 Miniconda3 Python 运行环境实战指南 文章目录 Docker 构建 Miniconda3 Python 运行环境实战指南一 准备 environment.yml二 获取项目 pip 信息三 Dockerfile 编写四 构建多平台镜像1 准备组件2 构建镜像3 导出镜像4 导入镜像 五 注意事项 本文详细介绍了如何通过 …...

029 elasticsearch文档管理(ElasticsearchRepository、ElasticsearchRestTemplate)
文章目录 BlogRepository.javaBlogRepositoryTest.javaBulkTest.java 文档的管理 ElasticSearchRepository接口 使用方法: 创建一个接口,继承于ElasticSearchRepository,指定使用的Entity类及对应主键数据类型 Springboot自动扫描接口并创建代…...

【Flutter】Dart:Isolate
在 Dart 和 Flutter 中,所有的代码默认都运行在单一的线程(即主线程)上,这个线程也叫做 UI 线程。当进行耗时操作(如复杂计算或网络请求)时,如果不使用多线程处理,主线程会被阻塞&am…...

微信小程序 页面间传递数据
在小程序中,给页面传递参数通常有以下几种方法: 通过URL传递参数: 在小程序中,可以在页面的路径后面添加参数,然后在页面的 onLoad 函数中获取这些参数。 // 在app.json中配置页面路径 "pages": [{"pat…...

前端_005_Nodejs
文章目录 npm包管理器cjs和mjsYarn包管理器 1.Node.js 是js的一个运行环境,从nodejs诞生后js代码不局限于只在浏览器中执行,此外还能再nodejs里写服务端,用js可以前后端全栈开发 2.Node.js不跟浏览器一样默认含有document,window对象…...

SpringCache缓存介绍
1.为什么需要缓存 前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果: 使用缓存是一个很…...

python实战(一)——iris鸢尾花数据集分类
一、任务背景 本文是python实战系列专栏的第一篇文章,我们将从分类开始由浅入深逐步学习如何使用python完成常规的机器学习/深度学习任务。iris数据集是经典的机器学习入门数据集,许多分类任务教程都会以这个数据集作为示例,它的数据量是150条…...

k8s-对命名空间资源配额
对k8s命名空间限制的方法有很多种,今天来演示一下很常用的一种 用的k8s对象就是ResourceQuota 一:创建命名空间 kubectl create ns test #namespace命名空间可以简写成ns 二: 对命名空间进行限制 创建resourcequota vim resourcequ…...

Failed to connect to github.com port 443
git push无法连接443端口 **问题1****方法一:取消代理设置**git命令 其他解决方案1. **设置 Git 使用 HTTP 而不是 HTTPS**2. **检查证书**3. **配置 Git 忽略 SSL 验证(不推荐)**4. **检查代理设置** 问题1 Failed to connect to github.com…...

【设计模式系列】简单工厂模式
一、什么是简单工厂模式 简单工厂模式(Simple Factory Pattern)是一种设计模式,其中包含一个工厂类,根据传入的参数不同,返回不同类的实例。这个工厂类封装了对象的创建逻辑,使得客户端代码可以从直接创建…...

给定一个正整数n随机生成n个字节即生成2n个十六进制数将其组成字符串返回secrets.token_hex(n)
【小白从小学Python、C、Java】 【考研初试复试毕业设计】 【Python基础AI数据分析】 给定一个正整数n 随机生成n个字节 即生成2n个十六进制数 将其组成字符串返回 secrets.token_hex(n) [太阳]选择题 根据题目代码,执行的结果错误的是? import secrets …...

[Gtk] 工程
MediaPlayer 可执行文件工程 结构 . ├── BUILD ├── ButtonHelper.cpp ├── ButtonHelper.h ├── CMakeLists.txt ├── DrawingAreaHelper.cpp ├── DrawingAreaHelper.h ├── layout.ui └── main.cpp CMakeLists.txt # 1) cmake basic cmake_minimum_r…...

基于Multisim的汽车尾灯控制电路设计与仿真
假设汽车尾部左右量测各有3个指示灯(用发光二极管模拟)1. 汽车正常运行时指示灯全灭;2.右转弯时,右侧3个指示灯按右循环顺序点亮;.3. 左转弯时,左侧3个指示灯按左循环顺序点亮;4.临时刹车时所有…...

Leetcode 3326. Minimum Division Operations to Make Array Non Decreasing
Leetcode 3326. Minimum Division Operations to Make Array Non Decreasing 1. 解题思路2. 代码实现 题目链接:3326. Minimum Division Operations to Make Array Non Decreasing 1. 解题思路 这一题的话就是要看出来题中给出的operation的本质事实上就是将任意…...

redo文件误删除后通过逻辑备份进行恢复
问题描述 开发同事让在一个服务器上查找下先前库的备份文件是否存在,如果存在进行下恢复。翻了服务器发现备份文件存在,多愁了一眼竟翻到了该备份文件于2024.6.17日恢复过的日志,赶紧和开发沟通说2024.6.17号已经恢复过了为啥还要恢复&#x…...

7805的输出电压如何调整?
7805稳压集成电路的输出电压通常是固定的,标称为5V。然而,在实际应用中,可以通过一些方法调整其输出电压,尽管这些调整方法可能会使电路变得更加复杂或需要额外的元件。以下是几种可能的调整方法: 1. 使用不同型号的稳…...

git命令使用一览【自用】
git常见操作: git initgit remote add master【分支名字】 gitgits.xxxxx【仓库中获取的ssh链接或者http协议的链接】检查远程仓库是否链接成功。 git remote -v出现以下画面就可以git pull,git push了...

MES系列-报表和分析
MES系列-报表和分析 MES系列文章目录 ISA-95制造业中企业和控制系统的集成的国际标准-(1) ISA-95制造业中企业和控制系统的集成的国际标准-(2) ISA-95制造业中企业和控制系统的集成的国际标准-(3) ISA-95制造业中企业和控制系统的集成的国际标准-(4) ISA-95制造业中企业和控制…...

如何在分布式环境中实现高可靠性分布式锁
目录 一、简单了解分布式锁 (一)分布式锁:应对分布式环境的同步挑战 (二)分布式锁的实现方式 (三)分布式锁的使用场景 (四)分布式锁需满足的特点 二、Redis 实现分…...

Vue基础(4)
自定义指令 除了默认设置的核心指令( v-model 和 v-show ), Vue 也允许注册自定义指令。在vue中使用directive来创建自定义指令 钩子函数 指令定义函数提供了几个钩子函数(可选): bind: 只调用一次,指令第一次绑定到元素时调用&…...

Redis高阶篇之Redis单线程与多线程
文章目录 0 前言1. 为什么Redis是单线程?1.1 Redis单线程1.2 为什么Redis3时代单线程快的原因1.3 使用单线程原因 2.为什么逐渐加入多线程呢?2.1 如何解决 3.redis6/7的多线程特性和IO多路复用入门3.1主线程和IO线程怎么协作完成请求处理的3.2 Unix网络编…...

【C++】STL——priority_queue优先级队列
目录 前言priority_queue的使用简单使用在OJ中的使用 priority_queue的模拟实现基本功能仿函数在这里插入图片描述 前言 上一节我们说了stack和queue这两种容器适配器,而priority_queue(优先级队列)同样也是属于容器适配器,它会优…...

大数据新视界 --大数据大厂之大数据在智慧城市建设中的应用:打造智能生活的基石
💖💖💖亲爱的朋友们,热烈欢迎你们来到 青云交的博客!能与你们在此邂逅,我满心欢喜,深感无比荣幸。在这个瞬息万变的时代,我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…...

使用枚举来实现策略模式
使用很多if else的场景 public void save(String type,Object data){if("db".equals(type)){saveInDb(data);}else if("file".equals(type)){saveInFile(data);}else if("oss".equals(type)){saveInOss(data);}}使用枚举来解决 public enum Save…...

区块链技术原理
1. 引言 区块链的定义 区块链是一种分布式账本技术(Distributed Ledger Technology,DLT),其核心特征是通过密码学的方式将数据打包成一个个区块,按时间顺序依次相连,形成一个不可篡改、公开透明的链式数据…...

Spring Boot 接口数据加解密
今天聊下接口安全问题,涉及到接口的加密和解密 经常和外部单位接口调用梳理了相关技术方案,主要的需求点如下: 1,尽量少改动,不影响之前的业务逻辑 2,考虑到时间紧迫性,可采用对称性加密方式&…...