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

图像预览

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

private void setupCamera(PreviewView previewView) {

ListenableFuture 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) {

学习分享,共勉

Android高级架构师进阶之路

题外话,我在阿里工作多年,深知技术改革和创新的方向,Android开发以其美观、快速、高效、开放等优势迅速俘获人心,但很多Android兴趣爱好者所需的进阶学习资料确实不太系统,完整。今天我把我搜集和整理的这份学习资料分享给有需要的人

  • Android进阶知识体系学习脑图

  • Android进阶高级工程师学习全套手册

  • 对标Android阿里P7,年薪50w+学习视频

  • 大厂内部Android高频面试题,以及面试经历


《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
xtender.enableExtension(cameraSelector);

}

}

private void setCaptureExtender(ImageCapture.Builder builder, CameraSelector cameraSelector) {

学习分享,共勉

Android高级架构师进阶之路

题外话,我在阿里工作多年,深知技术改革和创新的方向,Android开发以其美观、快速、高效、开放等优势迅速俘获人心,但很多Android兴趣爱好者所需的进阶学习资料确实不太系统,完整。今天我把我搜集和整理的这份学习资料分享给有需要的人

  • Android进阶知识体系学习脑图

[外链图片转存中…(img-ly5JG6t7-1715432340558)]

  • Android进阶高级工程师学习全套手册

[外链图片转存中…(img-PNn7d2D3-1715432340559)]

  • 对标Android阿里P7,年薪50w+学习视频

[外链图片转存中…(img-xYHhuELH-1715432340560)]

  • 大厂内部Android高频面试题,以及面试经历

[外链图片转存中…(img-8xOzZvn5-1715432340560)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值