音视频开发:为什么推荐使用Jetpack CameraX?
我们的生活已经越来越离不开相机,从自拍
到直播
,扫码
再到VR
等等。相机的优劣自然就成为了厂商竞相追逐的赛场。对于app开发者来说,如何快速驱动相机,提供优秀的拍摄体验,优化相机的使用功耗,是一直以来追求的目标。
本文可能是当下最新最全的
CameraX
解读,篇幅较长,慢慢享用。
作者
前言
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
,它有四个子类,分别为Preview
,ImageCapture
,ImageAnalysis
和VideoCapture
。接下来介绍下它们如何使用。
图像拍摄
借助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);
}
}
视频录制
依托VideoCapture
的startRecording()
可以进行视频录制。在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…
使用注意
调用
CameraProvider
的bindToLifecycle()
前记得先调用unbindAll()
,否则可能发生重复绑定的exception
ImageAnalyzer
的analyze()
在分析完图片之后应立即调用ImageProxy
的close()
释放图像,以便后续图像能继续传送过来。否则将阻塞回调。因而也要注意分析图像的耗时问题每个
ImageProxy
实例在关闭后不要存储它的引用,因为一旦调用close()
,这些图像将变得不合法图像分析结束后应当调用
ImageAnalysis
的clearAnalyzer()
以告知不用将图像流传输过来避免性能的浪费视频录制场景一定不要忘记获得
audio
权限
有趣的兼容性处理
实现图像拍摄功能的时候发现ImageCapture
的takePicture()
文档里写着这么一段有趣的注释。
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.
大意是拍摄保存的Uri
为MediaStore
的话,将插入一行以验证保存路径是否合法并可写。验证结束后会删除该测试行。
但是在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的优势
源于CameraX
在Camera2
的基础上进行了高度的封裝和对大量设备进行了兼容性的处理,使得CameraX
拥有了很多优势。
- 易用性 采用封装的API可以高效达到目标
- 设备一致性 不用在乎版本,忽略设备硬件差异带来的开发区别,达到一致的开发体验
- 新的相机体验 通过效果扩展可以实现和原生相机一样的美颜等拍摄功能
本文demo
demo的源码已经开源至Github
,大家可以查阅参考。
结语
CameraX
发布于2019年8月7日,从alpha版到现在的beta版,一直在更新。从上面有趣的Huawei设备兼容性处理可以看到CameraX
一统江湖的决心。
最新仍是beta版,需要继续改进,但并非不能投入生产环境。
这么好用的框架,大家要多多使用并给出建议,这样才能越来越完善,才能给开发者给用户带来福音。
参考资料
CameraX
使用指南:developer.android.google.cn/training/ca…CameraX
的历史版本:developer.android.google.cn/jetpack/and…CameraX
的兼容和效果扩展支持的设备:developer.android.google.cn/training/ca…CameraX
的官方示例:github.com/android/cam…
视频讲解
CameraX与手机屏幕采集、CameraX与摄像头数据采集
B站:https://www.bilibili.com/video/BV1kp4y187C7?p=20
百度云盘视频下载:
链接:https://pan.baidu.com/s/1RtvX1Zea6CuJNUJo2iOtHw
提取码:k3qp
音视频开发:为什么推荐使用Jetpack CameraX?的更多相关文章
- WebRTC 音视频开发
WebRTC 音视频开发 webrtc Android IOS WebRTC 音视频开发总结(七八)-- 为什么WebRTC端到端监控很关键? 摘要: 本文主要介绍WebRTC端到端监控(我们翻译 ...
- Android 音视频开发学习思路
Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的.只能通过一点点的学习和积累把这块的知识串联积累起来. 初级入门篇: Android 音视频开发(一) ...
- 转:Android IOS WebRTC 音视频开发总结 (系列文章集合)
随笔分类 - webrtc Android IOS WebRTC 音视频开发总结(七八)-- 为什么WebRTC端到端监控很关键? 摘要: 本文主要介绍WebRTC端到端监控(我们翻译和整理的,译 ...
- Android IOS WebRTC 音视频开发总结(八十五)-- 使用WebRTC广播网络摄像头视频(下)
本文主要介绍WebRTC (我们翻译和整理的,译者:weizhenwei,校验:blacker),最早发表在[编风网] 支持原创,转载必须注明出处,欢迎关注我的微信公众号blacker(微信ID:bl ...
- Android IOS WebRTC 音视频开发总结(八十三)-- 使用WebRTC广播网络摄像头视频(上)
本文主要介绍WebRTC (我们翻译和整理的,译者:weizhenwei,校验:blacker),最早发表在[编风网] 支持原创,转载必须注明出处,欢迎关注我的微信公众号blacker(微信ID:bl ...
- Android IOS WebRTC 音视频开发总结(四六)-- 从另一个角度看国内首届WebRTC大会
文章主要从开发者角度谈国内首届WebRTC大会,支持原创,文章来自博客园RTC.Blacker,支持原创,转载必须说明出处,更多详见www.rtc.help. -------------------- ...
- Android IOS WebRTC 音视频开发总结(六)-- iOS开发之含泪经验
前段时间在搞webrtc iOS开发,所以将标题改为了Android IOS WebRTC 音视频开发总结, 下面都是开发过程中的经验总结,转载请说明出处(博客园RTC.Blacker): 1. IO ...
- Android 音视频开发(一) : 通过三种方式绘制图片
版权声明:转载请说明出处:http://www.cnblogs.com/renhui/p/7456956.html 在 Android 音视频开发学习思路 里面,我们写到了,想要逐步入门音视频开发,就 ...
- Android 音视频开发(七): 音视频录制流程总结
在前面我们学习和使用了AudioRecord.AudioTrack.Camera.MediaExtractor.MediaMuxer API.MediaCodec. 学习和使用了上述的API之后,相信 ...
- Android 音视频开发入门指南
Android 音视频从入门到提高 —— 任务列表 http://blog.51cto.com/ticktick/1956269(以这个学习为基础往下面去学习) Android 音视频开发学习思路-- ...
随机推荐
- Solon 框架详解(九)- 渲染控制之定制统一的接口输出
Springboot min -Solon 详解系列文章: Springboot mini - Solon详解(一)- 快速入门 Springboot mini - Solon详解(二)- Solon ...
- python学习8 文件的操作
本文拷贝了on testing 的<python之文件操作:文件的读写>,只做学习之用 python的文件读写通过 一.用open函数 二.对文件读写操作 三.读取文件位置定位 1. op ...
- 通过 ASM 库生成和修改 class 文件
在 JVM中 Class 文件分析 主要详细讲解了Class文件的格式,并且在上一篇文章中做了总结. 众所周知,JVM 在运行时, 加载并执行class文件, 这个class文件基本上都是由我们所写的 ...
- 从源码剖析Go语言基于信号抢占式调度
转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com/archives/485 本文使用的go的源码15.7 这一次来讲讲基于信号式抢占式调度 ...
- python中数组切片[:,i] [i:j:k] [:-i] [i,j,:k]
逗号","分隔各个维度,":"表示各个维度内的切片,只有:表示取这个维度的全部值,举例说明如下 1 1.二维数组 2 3 X[:,0]取所有行的第0个数据,第二 ...
- Jenkins教程:使用Jenkins进行持续集成
[注]本文译自: https://www.edureka.co/blog/jenkins-tutorial/ 本文将重点介绍 Jenkins 架构和 Jenkins 构建管道,并向您展示如何在 J ...
- [Fundamental of Power Electronics]-PART I-6.变换器电路-6.4 变换器评估与设计/6.5 重点与小结
6.4 变换器评估与设计 没有完美适用于所有可能应用场合的统一变换器.对于给定的应用和规格,应该进行折中设计来选择变换器的拓扑.应该考虑几种符合规格的拓扑,对于每种拓扑方法,对比较重要的量进行计算,比 ...
- 定制开发——GitHub 热点速览 v.21.15
作者:HelloGitHub-小鱼干 自定义 或者说 定制 是本周 GitHub 热点的最佳写照.比如,lipgloss 这个项目,可以让你自己定义终端样式,五彩斑斓的黑终端来一个.接着,是 Appl ...
- 动图:删除链表的倒数第 N 个结点
本文主要介绍一道面试中常考链表删除相关的题目,即 leetcode 19. 删除链表的倒数第 N 个结点.采用 双指针 + 动图 的方式进行剖析,供大家参考,希望对大家有所帮组. 19. 删除链表的倒 ...
- 201873030133-杨子豪 实验三 结对项目—《D{0-1}KP 实例数据集算法实验平台》项目报告
项目 内容 课程班级博客链接 班级博客链接 这个作业要求链接 作业要求链接 我的课程学习目标 了解软件工程的作用与意义,将软件工程与过去所学相结合 这个作业在哪些方面帮助我实现学习目标 体验了结对式的 ...