前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于Camera性能优化的一些建议

关于Camera性能优化的一些建议

作者头像
马上就说
发布2022-05-25 14:50:42
2.1K0
发布2022-05-25 14:50:42
举报
文章被收录于专栏:码上就说码上就说

上一篇文章主要介绍Camera的基本功能,我们在做相机应用的时候,除了相机的基本功能,还有一个非常重要的点,就是性能不能查,有几个方面:预览不能卡顿、拍照速度要快、录制视频不能卡。

我们做相机应用开发,不是做相机HAL层开发,优化的粒度没法像厂商name细致,上层可供优化的空间并不是很多,即使如此,大家在做相机调试的时候,还是有一些建议提供给大家。

相机处理放在子线程

如果你使用Camera1,开启预览要进行如下步骤:

  • 确定Camera前后摄像头位置
  • Camera.open打开特定位置摄像头
  • 设置Camera参数,通过调整Camera.Parameters调整相机焦距等参数
  • 设置Camera预览的SurfaceTexture
  • 开启预览

这些操作可以放在单一线程中,只要你控制好先后顺序就行,Camera1的相机操作是同步的,执行完一个步骤需要等它结束返回值才行进行下一步,手机中相机必须是一个独占资源,需要通过CameraServer和HAL层交互,然后调用底层的sensor,放在子线程操作,可以防止出现ANR。所有有关Camera实例的操作都要放在子线程中进行。

Camera1中你要设置帧回调要调用Camera.setPreviewCallback(...),如果将onPreviewFrame作为帧回调的监测接口,会发现部分手机上出帧比较慢,例如设置了30fps,但是出帧速度最多20fps,HAL层会将数据同步处理之后才返回。不过PreviewCallback已经被废弃了,用的人应该不多。

这种情况建议使用SurfaceTexture.setOnFrameAvailableListener(...)来监控帧回调。

Camera2支持你设置相机处理的Handler,你可以自己定义HandlerThread来设置Camera2的相机操作Handler。

Zero-Shot拍照

我们想要调用相机拍照,用户点击拍照,Camera1执行takePicture函数开始拍照,此函数是异步返回照片数据,Camera2通过CameraCaptureSession的capture方法开始拍照。换言之,它们都是在你点击拍照的瞬间去底层取下一帧,然后开始返回数据的,出帧的时间至少需要33ms(假设帧率是30fps),还不算其他的耗时。

拍照之前我一直在预览中,如果在用户点击拍照的瞬间,我将指令传递下去,之前预览的那一帧作为拍照的帧来处理,这样的耗时几乎为0,大大降低了拍照的耗时。实际过程中,可能会存在拍照时没有聚焦的问题,还需要手动聚焦一下,或者设置相机长期聚焦。Camera2原生也是支持Zero-Shot模式的,这样省去了你定制的精力了。

全局Surface设置

这是针对Camera2的优化,正常情况下,我们使用Camera2开启预览、拍照、录像,需要设置几个Surface?

操作Camera2调用预览的完整流程:

代码语言:javascript
复制
第一步:获取CameraManager实例
mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);

第二步:获取特定的摄像头ID
mCameraId = String.valueOf(CameraCharacteristics.LENS_FACING_BACK);

第三步:打开摄像头
mCameraManager.openCamera(mCameraId, mStateCallback, mCameraHandler);

第四步:开始预览
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
applyFocusInternal(mPreviewBuilder);
applyFlashInternal(mPreviewBuilder, mFlash);
mSurfaceTexture.setDefaultBufferSize(mPreviewWidth, mPreviewHeight);
Surface surface = new Surface(mSurfaceTexture);
mPreviewBuilder.addTarget(surface);
mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {
    @Override
    public void onConfigured(@NonNull CameraCaptureSession session) {
        if (mCameraDevice == null) {
            return;
        }
        Log.i(Constants.TAG, "onConfigured");
        mCameraCaptureSession = session;
        CaptureRequest previewRequest = mPreviewBuilder.build();
        mCameraCaptureSession.setRepeatingRequest(previewRequest, null, mCameraHandler);
    }

    @Override
    public void onConfigureFailed(@NonNull CameraCaptureSession session) {

    }
}, mCameraHandler);

如果你想加入拍照和录制视频,你需要创建额外两个CaptureRequest,而且需要分别设置拍照的surface——ImageReader.getSurface和录制视频的surface——MediaRecorder.getSurface,这样在预览、拍照、录制的过程中,你不仅需要创建多个CaptureRequest,还要设置多个Surface,这确实有点麻烦。

如果只设置一个Surface,后续所有的预览、拍照、录制视频都从这个Surface上取数据,也是可行的。上面我们拍照要借助ImageReader,利用其帧回调来取得具体的图片数据:

代码语言:javascript
复制
private ImageReader.OnImageAvailableListener mOnJpegImageAvailableListener = new ImageReader.OnImageAvailableListener() {
    @Override
    public void onImageAvailable(ImageReader reader) {
        Image image = reader.acquireNextImage();
        if (image != null) {
            Image.Plane[] planes = image.getPlanes();
            ByteBuffer jpegByteBuffer = planes[0].getBuffer();
            byte[] jpegByteArray = new byte[jpegByteBuffer.remaining()];
            jpegByteBuffer.get(jpegByteArray);
            generatePictureFile(jpegByteArray);

            image.close();
        }
    }
};

如果是录制视频,需要借助系统API——MediaRecorder,创建特定的CaptureRequest来实现抓取视频帧的目的:

代码语言:javascript
复制
private boolean prepareVideoRecorder() {
    mMediaRecorder = new MediaRecorder();
    mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
    mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);

    //3.设置视频的质量,但是这样设置不够精细化,如果想设置的更加细致一些,可以拆开来设置
    mMediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));
    mMediaRecorder.setVideoSize(mVideoWidth, mVideoHeight);

    mMediaRecorder.setOutputFile(MediaUtils.getOutputMediaFile(this.getApplicationContext(), MediaUtils.MEDIA_TYPE_VIDEO));

    //6.设置旋转的角度
    if (mCurrentCameraId.equals(mBackCameraId)) {
        mMediaRecorder.setOrientationHint(mOrientation);
    } else if (mCurrentCameraId.equals(mFrontCameraId)) {
        mMediaRecorder.setOrientationHint(360 - mOrientation);
    } else {
        throw new RuntimeException("No available camera");
    }
    try {
        mMediaRecorder.prepare();
    } catch (Exception e) {
        releaseMediaRecorder();
        LogUtils.w(TAG, "MediaRecorder prepare failed, exception = " + e.getMessage());
        return false;
    }
    return true;
}
      
private void recordVideoInternal() {
    CameraDevice cameraDevice = mCameraDevice;
    if (prepareVideoRecorder() && cameraDevice != null) {
        //说明可以创建MediaRecorder
        stopPreview();
        try {
            mCaptureVideoRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
            List<Surface> outputSurfaces = new ArrayList<>();
            Surface previewSurface = mPreviewSurface;
            mCaptureVideoRequestBuilder.addTarget(previewSurface);
            outputSurfaces.add(previewSurface);
            Surface recordSurface = mMediaRecorder.getSurface();
            if (recordSurface != null) {
                mCaptureVideoRequestBuilder.addTarget(recordSurface);
                outputSurfaces.add(recordSurface);
            }
            cameraDevice.createCaptureSession(outputSurfaces, mCaptureVideoStateCallback, mMainHandler);
        } catch (Exception e) {
            LogUtils.w(TAG, "Record video failed, exception = " + e.getMessage());
        }
    }
}

private CameraCaptureSession.StateCallback mCaptureVideoStateCallback = new CameraCaptureSession.StateCallback() {
    @Override
    public void onConfigured(@NonNull CameraCaptureSession session) {
        mCameraCaptureSession = session;
        //重新开始预览
        try {
            mCameraCaptureSession.setRepeatingRequest(mCaptureVideoRequestBuilder.build(), null, mMainHandler);
        } catch (Exception e) {
            LogUtils.w(TAG, "CaptureVideo setRepeatingRequest failed, exception = " + e.getMessage());
        }
        mMainHandler.post(new Runnable() {
            @Override
            public void run() {
                //开始录制视频
                mMediaRecorder.start();
                mIsRecording = true;
            }
        });
    }

    @Override
    public void onConfigureFailed(@NonNull CameraCaptureSession session) {
        mCameraCaptureSession = session;
    }
};

设置多个Surface实现预览、拍照、录制的功能,效率太低了,可以只设置一个Surface,这个Surface上渲染的画面同时用来预览、拍照、录制。

  • 正常情况下,Surface用来Camera预览
  • 如果点击拍照,将之前的预览帧保存为图片
  • 如果点击录制,将Surface的视频帧编码放入Video Packet Queue中,等着封装和时候和Audio Packet Queue中数据一起取出来按照时间戳封装成一个视频文件。

你还知道哪些Camera性能优化的方法,一起私信讨论下吧。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-05-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 音视频平凡之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档