1,806
前面研究了一下如何在Android手机上
获取超广角镜头
:
一些获取您的Android设备超广角能力的思路 - 掘金 (juejin.cn)
。发现HUAWEI官方有推出过一个相机库
CameraKit
,就想着自己接入一下看看效果,顺便记录一些遇到的坑。
使用Gradle集成比较常规,看文档即可:
CameraKit - 相机能力接入准备
官方提供的集成流程如下图:
CameraKit
提供了一个
Mode
类作为一次拍照流程的相关抽象,可理解为一个Session。
CameraKit
的生命周期:
模式创建:
CameraKit
提供了
多种相机的模式
,譬如:普通拍照、人像、夜景等,当然还有录像相关的。
详情可参考文档:
Mode.Type
模式配置:主要是
配置预览分辨率、拍照分辨率
等,还有关于在该模式下的一些操作事件的回调、数据的回调。
基于模式的操作:比较好理解的是利用
Mode
类进行预览、拍照、缩放等。
操作回调:每当触发一个操作后,会通过在模式配置下注册的回调中回调相关事件或数据。
模式释放:不需要时释放资源。
在使用
CameraKit
时,一切的前提是需要实例化出
CameraKit
对象,它是一个
饿汉式的单例
,在实例化前会判断
一些约束条件
,符合条件后才会创建。
CameraKit cameraKit = CameraKit.getInstance(getApplicationContext());
在预览的视图准备好之后,就可以开始创建模式了,譬如在TextureView#onSurfaceTextureAvailable
后。在创建前还需要新建一个HandlerThread,作为整个相机运作的线程。
private final ModeStateCallback mModeStateCallback = new ModeStateCallback() {
@Override
public void onCreated(Mode mode) {
super.onCreated(mode);
mMode = mode;
configMode();
@Override
public void onCreateFailed(String cameraId, int modeType, int errorCode) {
super.onCreateFailed(cameraId, modeType, errorCode);
@Override
public void onConfigured(Mode mode) {
super.onConfigured(mode);
mMode.startPreview();
@Override
public void onConfigureFailed(Mode mode, int errorCode) {
super.onConfigureFailed(mode, errorCode);
@Override
public void onReleased(Mode mode) {
super.onReleased(mode);
@Override
public void onFatalError(Mode mode, int errorCode) {
super.onFatalError(mode, errorCode);
cameraKit.createMode(CameraInfo.FacingType.CAMERA_FACING_BACK,
Mode.Type.NORMAL_MODE, mModeStateCallback, mHandler);
CameraKit
中有对于手机物理摄像头进行抽象,在应用层只会提供前置/后置两个枚举。这里使用后置摄像头CameraInfo.FacingType.CAMERA_FACING_BACK
。
Mode.Type.NORMAL_MODE
为普通拍照模式,如果有拍摄人像、夜景等其他需求,可对应传入。
ModeStateCallback
用于监听Mode对象的事件。
最后还需要一个属于HandlerThread的Handler,用于消息分发。
从上述代码中ModeStateCallback#onCreated
的回调可以看到,在成功创建模式后就可以开始配置了。
比较重要的是预览分辨率和拍照分辨率,可通过以下代码获取设备支持的
List<Size> supportedPreviewSizes = mMode.getModeCharacteristics()
.getSupportedPreviewSizes(SurfaceTexture.class);
List<Size> supportedCaptureSizes = mMode.getModeCharacteristics()
.getSupportedCaptureSizes(ImageFormat.JPEG);
因为用的是TextureView
,所以传入SurfaceTexture.class
。预览分辨率还需要设置回TextureView中保证预览画面正常。ps:分辨率的筛选逻辑比较常规就不多赘述了,这里选一个最大的3:4比例。
textureView.getSurfaceTexture()
.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
ps:mMode.getModeCharacteristics()
可以获取到在该模式下一些支持的参数,除上述的分辨率外还有支持的对焦类型、缩放范围等。可参考:ModeCharacteristics
mMode.getModeConfigBuilder()
.addPreviewSurface(new Surface(textureView.getSurfaceTexture()))
.addCaptureImage(pictureSize, ImageFormat.JPEG);
mMode.getModeConfigBuilder().setDataCallback(mActionDataCallback, mHandler);
mMode.getModeConfigBuilder().setStateCallback(mActionStateCallback, mHandler);
mMode.configure();
配置时还需要传入ActionStateCallback
、ActionDataCallback
对象,用于在该模式下的一些操作事件的回调、数据的回调
private final ActionStateCallback mActionStateCallback = new ActionStateCallback() {
@Override
public void onPreview(Mode mode, int state, @Nullable PreviewResult result) {
super.onPreview(mode, state, result);
@Override
public void onTakePicture(Mode mode, int state, @Nullable TakePictureResult result) {
super.onTakePicture(mode, state, result);
@Override
public void onFocus(Mode mode, int state, @Nullable FocusResult result) {
super.onFocus(mode, state, result);
private final ActionDataCallback mActionDataCallback = new ActionDataCallback() {
@Override
public void onImageAvailable(Mode mode, int type, Image image) {
super.onImageAvailable(mode, type, image);
@Override
public void onThumbnailAvailable(Mode mode, int type, android.util.Size size, byte[] data) {
super.onThumbnailAvailable(mode, type, size, data);
ModeStateCallback#onConfigured
回调后即可调用Mode#startPreview
开启预览。
@Override
public void onConfigured(Mode mode) {
super.onConfigured(mode);
mMode.startPreview();
这时您的界面上应该就能看到预览画面了。
Mode#takePicture
触发拍照
mMode.takePicture();
ActionStateCallback#onTakePicture
会回调拍照相关的事件,包括错误事件
@Override
public void onTakePicture(Mode mode, int state, @Nullable TakePictureResult result) {
super.onTakePicture(mode, state, result);
if (state == TakePictureResult.State.CAPTURE_COMPLETED) {
} else if (state == TakePictureResult.State.ERROR_CAPTURE_NOT_READY
|| state == TakePictureResult.State.ERROR_FILE_IO
|| state == TakePictureResult.State.ERROR_UNKNOWN
|| state == TakePictureResult.State.ERROR_UNSUPPORTED_OPERATION) {
可参考:ActionStateCallback.TakePictureResult.State
拍照成功后在ActionDataCallback#onImageAvailable
回调原图的Image对象,数据格式为jpg
@Override
public void onImageAvailable(Mode mode, int type, Image image) {
super.onImageAvailable(mode, type, image);
if (type == Type.TAKE_PICTURE) {
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
image.close();
这样就会拿到jpg的byte数组了。
在结束时,需要释放相关资源,当然还包括创建的HandlerThread,避免内存泄漏。
if (mMode != null) {
mMode.release();
超广角能力
由于笔者最初是想研究超广角的,所以也来看一下
float[] zooms = mMode.getModeCharacteristics().getSupportedZoom();
mMode.setZoom(zooms[0]);
由于CameraKit
已经帮我们抽象了物理摄像头,对于后置摄像头当然也包括那颗超广角摄像头。使用以上代码可以获取到当前模式下所支持的缩放范围,一般是一个长度为2的数组。在华为P40上zooms[0] = 0.6f
。设置后即可获得超广角的预览。
一些疑难杂症
支持的分辨率较少
在华为P40上,通过CameraKit
获取支持的拍照分辨率极少
通过CameraKit
获取
通过原生Camera2
获取
即使是超广角镜头,通过原生Camera2
获取
CameraKit的实例约束条件
这个其实不太算是问题,只是限制罢了。CameraKit
实例化前会判断
app是否已经获取了拍照权限(ps:个人认为这个这个判断应该交给调用方判断的。。。)
设备是否支持,支持的范围如下图:
笔者有一台华为MatePad11,是高通芯片的,实测发现并不支持,所以该库支持的范围还是比较窄的。
拍照输出的时间很慢
一般使用Camera2拍照平均在500ms可以输出,使用CameraKit最快也要2s。如果使用一些更为专业的功能可能会更长,这个没有细测。
在HUAWEI的社区中也有人提问:CameraKit中拍照速度慢的情况下要5、6秒,太慢了,请问如何优化-华为开发者论坛。
笔者的猜测是,从CameraKit导入的一些类来看,其依赖的还是Camera2。推测是在输出到调用方之前,CameraKit会调用一些系统的服务对图像进行处理,就比如超广角的输出是处理过畸变的。还有就是上述说的分辨率支持极少,所能选用的3:4分辨率已经到4096 * 3072
,导致这些处理比较耗时。
无法获取预览帧
接入时发现该库并没有很好的提供获取预览帧的方法,只能通过在配置Mode时添加多一个Surface
。这里使用ImageReader
实现,具体可参考Camera2的做法,大同小异。
previewImageReader = ImageReader.newInstance(
previewSize.getWidth(),
previewSize.getHeight(),
ImageFormat.YUV_420_888,
previewImageReader.setOnImageAvailableListener(this, mHandler);
mMode.getModeConfigBuilder()
.addPreviewSurface(new Surface(textureView.getSurfaceTexture()))
.addPreviewSurface(previewImageReader.getSurface())
.addCaptureImage(pictureSize, ImageFormat.JPEG);
mMode.getModeConfigBuilder().setDataCallback(mActionDataCallback, mHandler);
mMode.getModeConfigBuilder().setStateCallback(mActionStateCallback, mHandler);
mMode.configure();
这里需要使用YUV_420_888
,提高输出效率。还需要注意的是输出的Image转成byte数组的问题。
还有一点,根据社区的一些反馈,并不是所有设备都支持这样同时注册两个预览流,可通过以下方式获取最大的支持数
mMode.getModeCharacteristics().getMaxPreviewSurfaceNumber()
该方法虽然可以稳定获取到预览帧,但是随之而来的是加大了拍照输出的时间。严重的可达到5、6s以上。
另一种思路
这个又有另外一个思路去解决:上述代码只注册一个ImageReader用于获取YUV_420_888
的预览帧,再通过GLSurfaceView绘制到视图上,同时将Image转成byte数组作为数据层的回调。具体的实现这里不细说了,推荐一个OPPO的Demo,里面有YUV_420_888
通过OpenGL绘制的逻辑,可以参考一下:oppo/CameraUnit。
这里需要注意的是:
ImageReader#OnImageAvailableListener
输出和GLSurfaceView.Renderer#onDrawFrame
的绘制在两个线程,所以需要保证同步。
由于Image转成byte数组的过程可能存在耗时,这一块主要来源于大内存的申请和gc,可采用全局变量避免频繁的内存申请。但由于采用了全局变量,但又不能因为这个转换导致绘制的掉帧情况,所以需要一些原子性的变量加以辅助,适当做一些丢帧操作。
以上只是笔者的设想,里面也有更好的优化空间。
以上就是笔者关于华为CameraKit
的研究记录。其实都2023年了,在一台鸿蒙手机上做一些Android开发确实有些不太靠谱。在官方文档中最近一次更新是在2021年,目前HUAWEI还是把焦点放在鸿蒙的更新上,华为社区关于该库的问题看上去也没有得到很好的解决。所以以上提到的那些问题可能不会得到很好的解决了。这篇文章也可以当是一篇冷知识看看吧。
Cy13er
粉丝