我们的生活已经越来越离不开相机,从自拍直播扫码再到VR等等。相机的优劣自然就成为了厂商竞相追逐的赛场。对于app开发者来说,如何快速驱动相机,提供优秀的拍摄体验,优化相机的使用功耗,是一直以来追求的目标。

本文可能是当下最新最全的CameraX解读,篇幅较长,慢慢享用。

作者

TechMerger

前言

Android 5.0 时期Camera接口便已弃用,所以一般的做法是使用其替代者Camera2接口。但随着CameraX的出现,这个选择变得不再唯一。

我们先来回顾下图像预览这一简单的需求,使用Camera2接口是如何实现的。

Camera2

抛开回调,异常等附加处理,仍然需要多个步骤才能实现,比较繁琐。※篇幅原因省略代码只概括步骤※

同样是图像预览采用CameraX的话,实现就非常简洁。

CameraX

图像预览

可以说十几行就可以完成。和Camera2一样需要展示预览的控件PreviewView到布局上,并确保获得了camera权限。差异的地方主要体现在相机的配置步骤上。

    private void setupCamera(PreviewView previewView) {
ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
ProcessCameraProvider.getInstance(this);
cameraProviderFuture.addListener(() -> {
try {
mCameraProvider = cameraProviderFuture.get();
bindPreview(mCameraProvider, previewView);
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}, ContextCompat.getMainExecutor(this));
} private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,
PreviewView previewView) {
mPreview = new Preview.Builder().build();
mCamera = cameraProvider.bindToLifecycle(this,
CameraSelector.DEFAULT_BACK_CAMERA, mPreview);
mPreview.setSurfaceProvider(previewView.getSurfaceProvider());
}

镜头切换

如果想要切换镜头,只要将目标镜头的CameraSelector示例绑定到CameraProvider即可。我们在画面上添加按钮以切换镜头。

    public void onChangeGo(View view) {
if (mCameraProvider != null) {
isBack = !isBack;
bindPreview(mCameraProvider, binding.previewView);
}
} private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,
PreviewView previewView) {
...
CameraSelector cameraSelector = isBack ? CameraSelector.DEFAULT_BACK_CAMERA
: CameraSelector.DEFAULT_FRONT_CAMERA;
// 绑定前确保解除了所有绑定,防止CameraProvider重复绑定到Lifecycle发生异常
cameraProvider.unbindAll();
mCamera = cameraProvider.bindToLifecycle(this, cameraSelector, mPreview);
...
}

镜头聚焦

无法聚焦的拍摄是不完整的,我们监听Preview的触摸事件将触摸坐标告知CameraX开始聚焦。

    protected void onCreate(@Nullable Bundle savedInstanceState) {
...
binding.previewView.setOnTouchListener((v, event) -> {
FocusMeteringAction action = new FocusMeteringAction.Builder(
binding.previewView.getMeteringPointFactory()
.createPoint(event.getX(), event.getY())).build();
try {
showTapView((int) event.getX(), (int) event.getY());
mCamera.getCameraControl().startFocusAndMetering(action);
}...
});
} private void showTapView(int x, int y) {
PopupWindow popupWindow = new PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
ImageView imageView = new ImageView(this);
imageView.setImageResource(R.drawable.ic_focus_view);
popupWindow.setContentView(imageView);
popupWindow.showAsDropDown(binding.previewView, x, y);
binding.previewView.postDelayed(popupWindow::dismiss, 600);
binding.previewView.playSoundEffect(SoundEffectConstants.CLICK);
}

除了图像预览以外还有很多其他使用场景,比如图像拍摄,图像分析和视频录制。CameraX将这些使用场景统一抽象为UseCase,它有四个子类,分别为PreviewImageCaptureImageAnalysisVideoCapture。接下来介绍下它们如何使用。

图像拍摄

借助ImageCapture提供的takePicture()可以将图像拍摄下来。支持保存到外部存储空间,当然需要获得external storage的读写权限。

    private void takenPictureInternal(boolean isExternal) {
final ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, CAPTURED_FILE_NAME
+ "_" + picCount++);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg"); ImageCapture.OutputFileOptions outputFileOptions =
new ImageCapture.OutputFileOptions.Builder(
getContentResolver(),
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
.build();
if (mImageCapture != null) {
mImageCapture.takePicture(outputFileOptions, CameraXExecutors.mainThreadExecutor(),
new ImageCapture.OnImageSavedCallback() {
@Override
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
Toast.makeText(DemoActivityLite.this, "Picture got"
+ (outputFileResults.getSavedUri() != null
? " @ " + outputFileResults.getSavedUri().getPath()
: "") + ".", Toast.LENGTH_SHORT)
.show();
}
...
});
}
} private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,
PreviewView previewView) {
...
mImageCapture = new ImageCapture.Builder()
.setTargetRotation(previewView.getDisplay().getRotation())
.build();
...
// 需要将ImageCapture场景一并绑定
mCamera = cameraProvider.bindToLifecycle(this, cameraSelector, mPreview, mImageCapture);
...
}

图像分析

图像分析指的是对预览的图像实时分析,将色彩,内容等信息识别出来,应用在机器学习二维码识别等业务场景。继续对demo做些改造,添加扫描二维码的按钮。点击按钮后进入扫码模式,并在二维码解析成功后弹出解析结果。

    public void onAnalyzeGo(View view) {
if (!isAnalyzing) {
mImageAnalysis.setAnalyzer(CameraXExecutors.mainThreadExecutor(), image -> {
analyzeQRCode(image);
});
}
...
} // 从ImageProxy取出图像数据,交由二维码框架zxing解析
private void analyzeQRCode(@NonNull ImageProxy imageProxy) {
ByteBuffer byteBuffer = imageProxy.getPlanes()[0].getBuffer();
byte[] data = new byte[byteBuffer.remaining()];
byteBuffer.get(data);
...
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
Result result;
try {
result = multiFormatReader.decode(bitmap);
}
...
showQRCodeResult(result);
imageProxy.close();
} private void showQRCodeResult(@Nullable Result result) {
if (binding != null && binding.qrCodeResult != null) {
binding.qrCodeResult.post(() ->
binding.qrCodeResult.setText(result != null ? "Link:\n" + result.getText() : ""));
binding.qrCodeResult.playSoundEffect(SoundEffectConstants.CLICK);
}
}

视频录制

依托VideoCapturestartRecording()可以进行视频录制。在demo上添加一个图像拍摄和视频录制模式的切换按钮,切换到视频录制模式的时候将视频拍摄的UseCase綁定到CameraProvider

    public void onVideoGo(View view) {
bindPreview(mCameraProvider, binding.previewView, isVideoMode);
} private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,
PreviewView previewView, boolean isVideo) {
...
mVideoCapture = new VideoCapture.Builder()
.setTargetRotation(previewView.getDisplay().getRotation())
.setVideoFrameRate(25)
.setBitRate(3 * 1024 * 1024)
.build();
cameraProvider.unbindAll();
if (isVideo) {
mCamera = cameraProvider.bindToLifecycle(this, cameraSelector,
mPreview, mVideoCapture);
} else {
mCamera = cameraProvider.bindToLifecycle(this, cameraSelector,
mPreview, mImageCapture, mImageAnalysis);
}
mPreview.setSurfaceProvider(previewView.getSurfaceProvider());
}

点击录制按钮后首先确保获得外部存储和audio权限,之后再开始视频的录制。

    public void onCaptureGo(View view) {
if (isVideoMode) {
if (!isRecording) {
// Check permission first.
ensureAudioStoragePermission(REQUEST_STORAGE_VIDEO);
}
}
...
} private void ensureAudioStoragePermission(int requestId) {
...
if (requestId == REQUEST_STORAGE_VIDEO) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED
|| ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(...);
return;
}
recordVideo();
}
} private void recordVideo() {
try {
mVideoCapture.startRecording(
new VideoCapture.OutputFileOptions.Builder(getContentResolver(),
MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues)
.build(),
CameraXExecutors.mainThreadExecutor(),
new VideoCapture.OnVideoSavedCallback() {
@Override
public void onVideoSaved(@NonNull VideoCapture.OutputFileResults outputFileResults) {
// Notify user...
}
}
);
}
...
toggleRecordingStatus();
} private void toggleRecordingStatus() {
// Stop recording when toggle to false.
if (!isRecording && mVideoCapture != null) {
mVideoCapture.stopRecording();
}
}

小插曲

实现视频录制功能的时候发现一个问题。

点击视频录制按钮的时候,如果此刻尚未获得audio权限,那么将申请该权限。即便此后获得了权限调用拍摄接口仍将发生异常。日志显示AudioRecorder实例为null引发了NPE

仔细查看相关逻辑发现,demo现在的处理是在切换为视频录制模式的时候,就将VideoCapture绑定到了CameraProvider。这个时间点如果还未获得audio权限的话,那么将无法初始化AudioRecorder。其实日志里也会给出相应提示:VideoCapture: AudioRecord object cannot initialized correctly

可是后面获得了权限再去调用VideoCapture的拍摄接口为何还是会发生NPE?

因为拍摄接口startRecording()的内部处理是AudioRecorder实例为null的话将直接终止请求。后面无论调用多少遍也无济于事。事实上该函数的后段存在再次获取AudioRecorder实例的逻辑,但因为前面发生了NPE而没有机会执行。

    // VideoCapture.java
public void startRecording(
@NonNull OutputFileOptions outputFileOptions, @NonNull Executor executor,
@NonNull OnVideoSavedCallback callback) {
...
try {
// mAudioRecorder为null将引发NPE终止录制的请求
mAudioRecorder.startRecording();
} catch (IllegalStateException e) {
postListener.onError(ERROR_ENCODER, "AudioRecorder start fail", e);
return;
} ...
mRecordingFuture.addListener(() -> {
...
if (getCamera() != null) {
// 前面发生了NPE,那么将失去此处再次获得AudioRecorder实例的机会
setupEncoder(getCameraId(), getAttachedSurfaceResolution());
notifyReset();
}
}, CameraXExecutors.mainThreadExecutor());
...
}

不知道这是VideoCapture实现上的漏洞还是开发者有意为之。但是在明明已经获得了audio权限的情况下调用录製接口却仍然发生NPE貌似并不合理。

当下只能采取一些回避方案,或者说开发者本该就这么做?

现在是在获得了audio权限前执行了VideoCapture的绑定,这存在发生上述反复NPE的可能。所以改成获得audio权限后再绑定VideoCapture即可回避。

话说回来,在VideoCaptue的文档里加上需要获得audio的权限的说明是不是更好一些呢?

相机效果扩展

光有上述几个场景的使用并不能满足日益丰富的拍摄需求,人像夜拍美颜等相机效果是必不可少的。幸好CameraX是支持效果扩展的。但不是所有设备都能兼容这种扩展,具体可在官网的设备兼容列表里查询到。

可供扩展的效果主要分为两大类,一个是用于图像预览时效果扩展的PreviewExtender,另一个是用于图像拍摄时效果扩展的ImageCaptureExtender

每个大类都包含几个典型的效果。

  • NightPreviewExtender 夜拍预览
  • BokehPreviewExtender 人像预览
  • BeautyPreviewExtender 美顔预览
  • HdrPreviewExtender HDR预览
  • AutoPreviewExtender 自动预览

开启这些效果的实现也非常简单。

    private void bindPreview(@NonNull ProcessCameraProvider cameraProvider,
PreviewView previewView, boolean isVideo) {
Preview.Builder previewBuilder = new Preview.Builder();
ImageCapture.Builder captureBuilder = new ImageCapture.Builder()
.setTargetRotation(previewView.getDisplay().getRotation());
...
setPreviewExtender(previewBuilder, cameraSelector);
mPreview = previewBuilder.build(); setCaptureExtender(captureBuilder, cameraSelector);
mImageCapture = captureBuilder.build();
...
} private void setPreviewExtender(Preview.Builder builder, CameraSelector cameraSelector) {
BeautyPreviewExtender beautyPreviewExtender = BeautyPreviewExtender.create(builder);
if (beautyPreviewExtender.isExtensionAvailable(cameraSelector)) {
// Enable the extension if available.
beautyPreviewExtender.enableExtension(cameraSelector);
}
} private void setCaptureExtender(ImageCapture.Builder builder, CameraSelector cameraSelector) {
NightImageCaptureExtender nightImageCaptureExtender = NightImageCaptureExtender.create(builder);
if (nightImageCaptureExtender.isExtensionAvailable(cameraSelector)) {
// Enable the extension if available.
nightImageCaptureExtender.enableExtension(cameraSelector);
}
}

遗憾的是笔者手中的Redmi 6A不在支持OEM效果扩展的设备列表里,无法给大家展示成功扩展效果的样图。

高阶用法

除了上述常见相机使用场景外还有其他可选的配置方法。篇幅限制不再详细展开,感兴趣者可参考官网进行尝试。

  • 转换输出 CameraX支持将图像数据进行转换后输出,比如应用于人像识别后绘制人脸框图

developer.android.google.cn/training/ca…

  • 用例旋转 图像拍摄和分析的过程中屏幕可能发生旋转,学习如何配置使得CameraX能够实时获取到屏幕方向和旋转角度,以抓取到正确的图像

developer.android.google.cn/training/ca…

  • 配置选项 控制分辨率,自动对焦,取景框形状设置等配置的指导

developer.android.google.cn/training/ca…

使用注意

  1. 调用CameraProviderbindToLifecycle()前记得先调用unbindAll(),否则可能发生重复绑定的exception

  2. ImageAnalyzeranalyze()在分析完图片之后应立即调用ImageProxyclose()释放图像,以便后续图像能继续传送过来。否则将阻塞回调。因而也要注意分析图像的耗时问题

  3. 每个ImageProxy实例在关闭后不要存储它的引用,因为一旦调用close(),这些图像将变得不合法

  4. 图像分析结束后应当调用ImageAnalysisclearAnalyzer()以告知不用将图像流传输过来避免性能的浪费

  5. 视频录制场景一定不要忘记获得audio权限

有趣的兼容性处理

实现图像拍摄功能的时候发现ImageCapturetakePicture()文档里写着这么一段有趣的注释。

Before triggering the image capture pipeline, if the save location is a File or MediaStore, it is first verified to ensure it's valid and writable.

A File is verified by attempting to open a FileOutputStream to it, whereas a location in MediaStore is validated by ContentResolver#insert() creating a new row in the user defined table, retrieving a Uri pointing to it, then attempting to open an OutputStream to it. The newly created row is ContentResolver#delete() deleted at the end of the verification.

On Huawei devices, this deletion results in the system displaying a notification informing the user that a photo has been deleted. In order to avoid this, validating the image capture save location in MediaStore is skipped on Huawei devices.

大意是拍摄保存的UriMediaStore的话,将插入一行以验证保存路径是否合法并可写。验证结束后会删除该测试行。

但是在Huawei设备上删除行的操作将触发一条删除照片的通知。所以为避免困扰用户,CameraX将会在Huawei设备上跳过路径的验证。

class ImageSaveLocationValidator {
// 将判断设备品牌是否为华为或荣耀,是则直接跳过验证
static boolean isValid(final @NonNull ImageCapture.OutputFileOptions outputFileOptions) {
...
if (isSaveToMediaStore(outputFileOptions)) {
// Skip verification on Huawei devices
final HuaweiMediaStoreLocationValidationQuirk huaweiQuirk =
DeviceQuirks.get(HuaweiMediaStoreLocationValidationQuirk.class);
if (huaweiQuirk != null) {
return huaweiQuirk.canSaveToMediaStore();
} return canSaveToMediaStore(outputFileOptions.getContentResolver(),
outputFileOptions.getSaveCollection(), outputFileOptions.getContentValues());
}
return true;
}
...
} public class HuaweiMediaStoreLocationValidationQuirk implements Quirk {
static boolean load() {
return "HUAWEI".equals(Build.BRAND.toUpperCase())
|| "HONOR".equals(Build.BRAND.toUpperCase());
} /**
* Always skip checking if the image capture save destination in
* {@link android.provider.MediaStore} is valid.
*/
public boolean canSaveToMediaStore() {
return true;
}
}

CameraX的优势

源于CameraXCamera2的基础上进行了高度的封裝和对大量设备进行了兼容性的处理,使得CameraX拥有了很多优势。

  • 易用性 采用封装的API可以高效达到目标
  • 设备一致性 不用在乎版本,忽略设备硬件差异带来的开发区别,达到一致的开发体验
  • 新的相机体验 通过效果扩展可以实现和原生相机一样的美颜等拍摄功能

本文demo

demo的源码已经开源至Github,大家可以查阅参考。

github.com/ellisonchan…

结语

CameraX发布于2019年8月7日,从alpha版到现在的beta版,一直在更新。从上面有趣的Huawei设备兼容性处理可以看到CameraX一统江湖的决心。

最新仍是beta版,需要继续改进,但并非不能投入生产环境。

这么好用的框架,大家要多多使用并给出建议,这样才能越来越完善,才能给开发者给用户带来福音。

参考资料

视频讲解

CameraX与手机屏幕采集、CameraX与摄像头数据采集

B站:https://www.bilibili.com/video/BV1kp4y187C7?p=20

百度云盘视频下载:

链接:https://pan.baidu.com/s/1RtvX1Zea6CuJNUJo2iOtHw

提取码:k3qp

音视频开发:为什么推荐使用Jetpack CameraX?的更多相关文章

  1. WebRTC 音视频开发

    WebRTC 音视频开发 webrtc   Android IOS WebRTC 音视频开发总结(七八)-- 为什么WebRTC端到端监控很关键? 摘要: 本文主要介绍WebRTC端到端监控(我们翻译 ...

  2. Android 音视频开发学习思路

    Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的.只能通过一点点的学习和积累把这块的知识串联积累起来. 初级入门篇: Android 音视频开发(一) ...

  3. 转:Android IOS WebRTC 音视频开发总结 (系列文章集合)

    随笔分类 - webrtc   Android IOS WebRTC 音视频开发总结(七八)-- 为什么WebRTC端到端监控很关键? 摘要: 本文主要介绍WebRTC端到端监控(我们翻译和整理的,译 ...

  4. Android IOS WebRTC 音视频开发总结(八十五)-- 使用WebRTC广播网络摄像头视频(下)

    本文主要介绍WebRTC (我们翻译和整理的,译者:weizhenwei,校验:blacker),最早发表在[编风网] 支持原创,转载必须注明出处,欢迎关注我的微信公众号blacker(微信ID:bl ...

  5. Android IOS WebRTC 音视频开发总结(八十三)-- 使用WebRTC广播网络摄像头视频(上)

    本文主要介绍WebRTC (我们翻译和整理的,译者:weizhenwei,校验:blacker),最早发表在[编风网] 支持原创,转载必须注明出处,欢迎关注我的微信公众号blacker(微信ID:bl ...

  6. Android IOS WebRTC 音视频开发总结(四六)-- 从另一个角度看国内首届WebRTC大会

    文章主要从开发者角度谈国内首届WebRTC大会,支持原创,文章来自博客园RTC.Blacker,支持原创,转载必须说明出处,更多详见www.rtc.help. -------------------- ...

  7. Android IOS WebRTC 音视频开发总结(六)-- iOS开发之含泪经验

    前段时间在搞webrtc iOS开发,所以将标题改为了Android IOS WebRTC 音视频开发总结, 下面都是开发过程中的经验总结,转载请说明出处(博客园RTC.Blacker): 1. IO ...

  8. Android 音视频开发(一) : 通过三种方式绘制图片

    版权声明:转载请说明出处:http://www.cnblogs.com/renhui/p/7456956.html 在 Android 音视频开发学习思路 里面,我们写到了,想要逐步入门音视频开发,就 ...

  9. Android 音视频开发(七): 音视频录制流程总结

    在前面我们学习和使用了AudioRecord.AudioTrack.Camera.MediaExtractor.MediaMuxer API.MediaCodec. 学习和使用了上述的API之后,相信 ...

  10. Android 音视频开发入门指南

    Android 音视频从入门到提高 —— 任务列表 http://blog.51cto.com/ticktick/1956269(以这个学习为基础往下面去学习) Android 音视频开发学习思路-- ...

随机推荐

  1. 如何在 C# 中使用 ArrayPool 和 MemoryPool

    对资源的可复用是提升应用程序性能的一个非常重要的手段,比如本篇要分享的 ArrayPool 和 MemoryPool,它们就有效的减少了内存使用和对GC的压力,从而提升应用程序性能. 什么是 Arra ...

  2. BuaacodingT651 我知道你不知道圣诞节做什么 题解(逻辑)

    题目链接 我知道你不知道圣诞节做什么 解题思路 第一句话:x,y不都为质数. 第二句话:对于xy=t,存在唯一一种x+y使得x,y不都为质数. 第三句话:对于x+y=s,存在唯一一种t=xy使得对于任 ...

  3. dk.exe自动填报程序的反编译

    dk.exe自动填报程序的反编译 dk.exe用于学校每日健康报的自动填写.

  4. 绿色物流-智慧仓储监控管理 3D 可视化系统

    前言 随着电子商务产业的迅速发展,快递爆仓已成为了困扰仓储物流的一大难题.大量的碎片化订单,传统仓储管理和运作方式已无法满足,加速仓储物流管理的智能化.自动化升级创新,延伸而出的智慧物流概念成为物流行 ...

  5. HTML5中window.postMessage,在两个页面之间的数据传递

    HTML5中window.postMessage,在两个页面之间的数据传递 2015年11月3日 8536次浏览 关于postMessage window.postMessage虽然说是html5的功 ...

  6. Django之缓存、信号和图片验证码、ORM性能

    一. 缓存 1. 介绍 缓存通俗来说:就是把数据先保存在某个地方,下次再读取的时候不用再去原位置读取,让访问速度更快. 缓存机制图解 2.Django中提供了6种缓存方式 1. 开发调试 2. 内存 ...

  7. A Color Game

    题目大意:  给定一个只包含七种字母的字符串,如果满足一段连续相同的字符长度大于等于K那么即可消除,问最后能不能变为空字符. 题解:很明显是用区间dp来解决,我们设dp[l][r][k]代表的是在[l ...

  8. 数据库SQL查询作业

    --设有三个关系 --S(S#,SNAME,AGE,SEX) --SC(S#,C#,GRADE) --C(C#,CNAME,TEACHER) --(1)检索LIU老师所授课程的课程号.课程名 sele ...

  9. Prometheus时序数据库-报警的计算

    Prometheus时序数据库-报警的计算 在前面的文章中,笔者详细的阐述了Prometheus的数据插入存储查询等过程.但作为一个监控神器,报警计算功能是必不可少的.自然的Prometheus也提供 ...

  10. 量体裁衣方得最优解:聊聊页面静态化架构和二级CDN建设

    量体裁衣方得最优解:聊聊页面静态化架构和二级CDN建设 上期文章中我们介绍了CDN和云存储的实践,以及云生态的崛起之路,今天,我们继续聊一聊CDN. 我们通常意义上讲的CDN,更多的是针对静态资源类的 ...