写点什么

音视频开发:为什么推荐使用 Jetpack CameraX?

用户头像
Android架构
关注
发布于: 13 小时前

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);


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


}


}


视频录制

依托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()文档里写着这么一段有趣的注释。

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
音视频开发:为什么推荐使用Jetpack CameraX?