Android音视频-视频采集(Camera2预览基础)

Camera2是在API level 21后面取代Camera的一个API,我们以后开发的应用中实际和这个API打交到会比较多,毕竟现在很多Android使用都API21以上了。Camera2的使用我们也和上面说的Camera一样的功能来实现一遍,了解其中的一些细节。

创建Camera2应用

首先我们使用最简单的方式来使用Camera2来创建一个显示预览的应用。开始之前先总体了解一些Camera2用来拍照流程和相关的类。

  • CameraManager
    它是Android设备用来管理所有的相机设备类。例如检测打开摄像头,查看下面的相机特征。

  • CameraCharacteristics
    每一个相机设备拥有一些列属性和设置用来描述它。它可以通过CameraManager来获取。

  • CameraCaptureSession
    当我们使用Camera2从相机设备来拍照或者预览图像,应用程序必须先创建CameraCaptureSession

  • SurfaceTexture
    相机预览要一个View来显示预览的数据,在使用Camera的时候我们使用的是SurfaceView这个类。使用Camera2来操作相机也要一个Surface来显示View,Surface这个对象可以通过很多类来实例话,包括SurfaceView,SurfaceTexture,MediaCodec,MediaRecorder,Allocation,和ImageReader。我们demo使用的TextureView,它内部使用的是SurfaceTexture类。

  • CaptureRequest
    相机应用程序要构建一个CaptureRequest用来拍照,它定义了相机设备拍照的需要的一些参数。

  • 使用CaptureRequest
    一旦CaptureRequest请求被设置好,他就可以被发送到激活CaptureSession用来拍照或者连续重复使用。

看到网上有一个图很好的描述了Camera2相关的东西。




Camera2引入了管道的概念让Android设备和摄像头联系起来。例如系统向摄像头发送预览请求,摄像头会返回CameraMetaData。它们在CameraCaptureSession中操作



这里罗列了Camera2大致所有相关的类。以上参考自 这里

接下来我们的创建一个简单的预览的相机应用,先转起来心里舒畅一些。

  • 声明Camera权限和Camera2使用全部特征
<uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera2.full"/>
  • 创建一个View继承TextureView
    这个TextureVie用来显示相机传输过来的预览数据。
    并且在设置监听Surface可用的时候执行下面的步骤打开相机。

  • 请求打开相机

private void openCamera() {
        //获取CameraManager对象
        CameraManager manager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
        Log.e(TAG, "is camera open");
        try {
            //检测摄像头设备ID,有几个摄像头
            cameraId = manager.getCameraIdList()[0];
            //获取相机特征对象
            CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
            //获取相机输出流配置Map
            StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
            assert map != null;
            //获取预览输出尺寸
            imageDimension = map.getOutputSizes(SurfaceTexture.class)[0];
            // Add permission for camera and let user grant the permission
            if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                return;
            }
            //调用CameraManger对象打开相机函数
            manager.openCamera(cameraId, stateCallback, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
        Log.e(TAG, "openCamera X");
    }

这个方法现在很精简,用来打开相机设备,每个函数可以看注释,也可以自己跟踪到源码看大概是个什么东西,最后执行openCamera的时候设置了一个stateCallback,这个用来接受相机已经打开的回掉。如果相机可以使用了,那么就执行接下来的步骤把相机底层的数据显示到TextureView上面去。

  • 使用TextureView来显示预览数据
protected void createCameraPreview() {
        try {
            Log.e(TAG, "createCameraPreview: ");
            //获取当前TextureVie的SurfaceTexture
            SurfaceTexture texture = getSurfaceTexture();
            assert texture != null;
            //设置SurfaceTexture默认的缓冲区大小,为上面得到的预览的size大小
            texture.setDefaultBufferSize(imageDimension.getWidth(), imageDimension.getHeight());
            Surface surface = new Surface(texture);
            //创建CaptureRequest对象,并且声明类型为TEMPLATE_PREVIEW,可以看出是一个预览类型
            captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            //设置请求的结果返回到到Surface上
            captureRequestBuilder.addTarget(surface);
            //创建CaptureSession对象
            cameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                    //The camera is already closed
                    if (null == cameraDevice) {
                        return;
                    }
                    Log.e(TAG, "onConfigured: ");
                    // When the session is ready, we start displaying the preview.
                    cameraCaptureSessions = cameraCaptureSession;
                    //更新预览
                    updatePreview();
                }

                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
                    Toast.makeText(mContext, "Configuration change", Toast.LENGTH_SHORT).show();
                }
            }, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

上面大致就是创建一个CaptureRequest,把这个请求发送到CaptureSession会话中去。最后在回话配置成功的回掉里面更新预览的视图

  • 更新预览视图
protected void updatePreview() {
        if (null == cameraDevice) {
            Log.e(TAG, "updatePreview error, return");
        }
        Log.e(TAG, "updatePreview: ");
        //设置相机的控制模式为自动,方法具体含义点进去(auto-exposure, auto-white-balance, auto-focus)
        captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
        try {
            //设置重复捕获图片信息
            cameraCaptureSessions.setRepeatingRequest(captureRequestBuilder.build(), null, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

OK,上面的步骤完成了一个相机的预览的功能。

我们接下来要仔细了解每一个步骤里面的具体的参数的设置以及扩展上面简单预览界面的参数设置。

初始化相机

这里我们主要了解的是CameraManager类和CameraCharacteristics类
出了上面的API,我们了解一些它们其他的API。

CameraManager

它是Camera2中的一个系统的管理服务,管理着检测,特征和连接CameraDevices相机设备。

  • getCamraIdList()

    这个方法返回当前连接的相机设备的ID,它包含了当前的设备所有的相机。包括其他可能正在使用的相机。

    它通过Binder远程连接相机服务获取相机,我们的到的string数组里面包含0和1。这个和Camera的ID相同,ID0代表位前置相机,ID1代表后置相机。

  • openCamera(String cameraId,CameraDevice.StateCallback callback,Handler hander)

    通过相机ID来打开相机,我们上面设置前面两个参数,第三个Hander设为null,如果我们知道onPreViewFrame的回掉机制,那么这个看一下就知道第三个参数的含义,它会隐形第二个参数的callback回掉的执行线程,如果设置为空就默认为主线程回掉。

  • registerAvailabilityCallback(CameraManager.AvailabilityCallback callback, Handler handler)


    这里写图片描述

    这个很正常,刚开始我们的前置相机是没有打开的,它收到两个相机都可以使用的回掉,后面我们的应用前置相机打开了,它收到前置相机不可用的回掉。

    我们然后退出相机预览界面:


    这里写图片描述

    它收到前置相机可用的回掉。可以知道这个方法在第一次注册的时候会收到设备所有相机的可以与否的回掉,然后当有某个相机的可以与否发送变化后,它会收到回掉。

    注册了监听回掉要记得退出时注销监听函数。

  • registerTorchCallback(CameraManager.TorchCallback callback, Handler handler)
    这是对手机的手电筒的一个监听。这里面的回掉函数的细节,以及使用的判断到下面的实现打开手电筒的功能去详细了解。

CameraCharacteristics

相机特征类,它是一个描述相机CameraDevice的属性的一个类。例如我们上面获取预览的图片尺寸的API,我们上面的代码直接就取的第一个尺寸,没有做一个最佳匹配的尺寸输出。

Camera图片大小

Camera预览图片大小

我们在上一篇学习Camera的API的时候,使用Camera.Parameter.setPreviewSize来设置PreviewSize。那个选择最佳的预览尺寸的算法是根据显示预览的View的宽高和Camera支持的预览大小做一个比较最相近的来选取。而Camera2API没有发现具体的API对Camera进行设置,但是我们在Camera2的时候使用的TextureView进行视图预览的,可以对这个TextureView设置预览大小。

  • 我们在Camera的使用使用API Camera.Parameters.getSupportedPreviewSizes()方法来获取当前的相机支持的预览的图像大小,再和我们给出来的预览View来进行一个比较,取它们直接一个接近的相机支持的大小。
  • Camera2API使用的时候通过CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(mCameraId);
    StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
    map.getOutputSizes(SurfaceTexture.class);

    Camera2API通过这种方法来获取当前相机支持的预览尺寸大小。拿到了这些大小再和我们给出的预览View来比较选取一个合适的预览大小。

在不考虑预览方向的前提下,这个预览尺寸设置的大小它影响我们看预览View的时候的清晰程度,假如现在竖屏一个预览的View,它的宽高为1080*1692,极端一点,我们现在设置的预览尺寸为177*144,那么我们会看到一个非常模糊的预览View,它们越接近我们看到的视图越清晰。

我们获取到的Camera支持的预览尺寸都是宽度大于高度的,这里我们考虑为这些尺寸为当前CameraSensor的方向的尺寸,当我们下面把它顺时针旋转90度,就是我们竖屏看到的视图和现实物理视图一样了。

下面给出Camera2中用来获取相对最佳的预览尺寸的代码。
下面参考自这里

private Size getPreferredPreviewSize(Size[] mapSizes, int width, int height) {
        Log.e(TAG, "getPreferredPreviewSize: surface width="+width+",surface height="+height);
        List<Size> collectorSizes = new ArrayList<>();
        for (Size option : mapSizes) {
            if (width > height) {
                if (option.getWidth() > width &&
                        option.getHeight() > height) {
                    collectorSizes.add(option);
                }
            } else {
                if (option.getWidth() > height &&
                        option.getHeight() > width) {
                    collectorSizes.add(option);
                }
            }
        }
        if (collectorSizes.size() > 0) {
            return Collections.min(collectorSizes, new Comparator<Size>() {
                @Override
                public int compare(Size lhs, Size rhs) {
                    return Long.signum(lhs.getWidth() * lhs.getHeight() - rhs.getWidth() * rhs.getHeight());
                }
            });
        }
        Log.e(TAG, "getPreferredPreviewSize: best width="+
                mapSizes[0].getWidth()+",height="+mapSizes[0].getHeight());
        return mapSizes[0];
    }

预览的尺寸我们详细了解了,他要的尺寸是和我们预览View相当接近的一个最佳的尺寸大小。这份代码同样可以应用到我们的上一篇博客中Camera来获取最佳尺寸里面

Camera方向基础

我们根据上一篇文章的理解,我们上次使用Camera的时候,如果不做预览的图片旋转,它默认是我们看到的图像逆时针转了90度的,这个预览图像我们看着很别扭,我们就使用Camera的API把预览图片图片旋转了90度,这下我们看到的预览图片和现实中的图片的方向一样了。还捎带看了一个Camera Sensor这个东西。我们还没有考虑屏幕旋转的情况,这几个概念要梳理一下。先有一个概念就是我们的预览方向的计算是根据屏幕方向,CameraSensor采集图片的方向和摄像头是前置还是后置来决定的,为啥这么说,应为Google Sample的示例代码是这么写的..

屏幕方向

下面这几张图片示例的出处为查看




这张图描述了我们要了解的两个概念,手机屏幕方向,CameraSensor的方向。我们先了解屏幕的方向:

手机设备屏幕方向
自然握持状态下为0度,逆时针旋状依次为:90度、180度、270度;这个手机设备的屏幕方向的角度信息是所有设备都相同的。我们来验证一下。

((Activity)mContext).getWindowManager().getDefaultDisplay().getRotation();

这个方法可以获取当前屏幕的方向信息,为了好验证使用模拟器来做这个比较方便,旁边有那个逆时针旋转屏幕的功能按钮。通过看数值,90度和270都完美的验证了,有个180度上面的方法无法验证,我们要的效果其实就是把竖屏的倒置过来,通过设置Activity的screenOrientation为reversePortrait这就把Activity倒过来了,完美的验证了角度为180度。证明我们上面的概念没有问题。

CameraSensor方向
这个方向概念来自官网的描述来自Camera的CameraInfo.orientation属性,它是相机图像方向,这也是我们前面上篇文章写Camera的时候说的图像采集Sensor的方向。这里在贴一张图。




这个和上面的图看出区别就是两个模拟器型号不同,然后重要的一点不同就是CameraSensor不同。这里我没有这两个设备,没有去验证,而模拟器获取的CameraSensor值也都是0。有设备了再验证。。。

CameraSensor对大多数设备都是屏幕方向90度的方向,但是例如Nexus5X它的CameraSensor的方向就是屏幕方向270度的方向。得出一个结论:CameraSensor的值和设备制造的硬件厂商有关,和Android API的版本无关的。

  • 如何获取CameraSensor方向
    • Camera:
      如果使用的Camera 的API可以使用Camera.CameraInfo.orientaion来获取CameraSensor的方向。
    • Camera2:
      Camera2的API可以通过
      StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
      assert map != null;
      // sensor orientation
      int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
      这个方法来获取CameraSensor的方向。

我们在前面的文章屏幕竖屏的时候使用Camera来操作,把后置相机的预览设置为90度,把前置相机摄像头设置为270度就可以完美的预览(屏幕方向和相机预览方向一致),为啥呢?

我们了解了上面的CameraSensor的概念可以完全解答这个问题。我使用的小米5手机测试的它的前置相机摄像头为90度。获取它的后置摄像头的CameraSensor为270度。这里我们进行把相机旋转90和270度,这里实际是把默认的后置相机摄像头采集的90度的方向图片帧数据,顺时针转90度到屏幕方向0度的方向,这个时候图片的方向和我们的竖屏View显示的方向一致了。同理后置的270也是这么来的(这个理解应该没啥问题吧)。

如何设置预览方向

我们上一篇的Camera就针对竖屏进行了处理,没有考虑到横屏预览啥效果,当然会有一些问题。这里针对Camera和Camera2这两个API看分别如何设置。

Camera设置预览方向

预览方向由当前的屏幕的方向和CameraSensor的方向来决定,通过算法计算它们两个来得到预览的方向。
我们在使用Camera的setDisplayOrientation的时候这个方法的源码注释上面有这么一些注释代码:

public static void setCameraDisplayOrientation(Activity activity,
              int cameraId, android.hardware.Camera camera) {
          android.hardware.Camera.CameraInfo info =
                  new android.hardware.Camera.CameraInfo();
          android.hardware.Camera.getCameraInfo(cameraId, info);
          int rotation = activity.getWindowManager().getDefaultDisplay()
                  .getRotation();
          int degrees = 0;
          switch (rotation) {
              case Surface.ROTATION_0: degrees = 0; break;
              case Surface.ROTATION_90: degrees = 90; break;
              case Surface.ROTATION_180: degrees = 180; break;
              case Surface.ROTATION_270: degrees = 270; break;
          }

          int result;
          if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
              result = (info.orientation + degrees) % 360;
              result = (360 - result) % 360;  // compensate the mirror
          } else {  // back-facing
              result = (info.orientation - degrees + 360) % 360;
          }
          camera.setDisplayOrientation(result);
      }

把这份代码应用到上一篇博客Camera中去,完美的解决竖直和横屏的预览效果。

Camera2设置预览方向

我们上面的Demo在横屏的时候预览的视图不是屏幕方向90度下来的。我们必须给它做一些变化。上面的Camera有专门的API来进行方向设置,但是Camera2我还没有发现,网上找到一种方法就是对TextureView进行变幻和上面的预览视图大小一样对TextureView进行设置即可。代码如下:

private void transformImage(int width, int height) {
        Matrix matrix = new Matrix();
        int rotation = ((Activity) mContext).getWindowManager().getDefaultDisplay().getRotation();
        RectF textureRectF = new RectF(0, 0, width, height);
        RectF previewRectF = new RectF(0, 0, mImageDimension.getHeight(), mImageDimension.getWidth());
        float centerX = textureRectF.centerX();
        float centerY = textureRectF.centerY();
        if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
            previewRectF.offset(centerX - previewRectF.centerX(),
                    centerY - previewRectF.centerY());
            matrix.setRectToRect(textureRectF, previewRectF, Matrix.ScaleToFit.FILL);
            float scale = Math.max((float) width / mImageDimension.getWidth(),
                    (float) height / mImageDimension.getHeight());
            matrix.postScale(scale, scale, centerX, centerY);
            matrix.postRotate(90 * (rotation - 2), centerX, centerY);
        }
        setTransform(matrix);
    }

预览方向总结

Camera 中通过API setDisplayOrientation来进行设置,难点是如何获取这个方向的值,因为在横竖屏的时候有一些变动,我们必须的弄明白屏幕方向,CameraSensor方向这两个概念,因为这个方向是由它们两可以动态的计算得来的。

Camera2的API中我现在没有发现具体方法对预览的大小和方向进行设置,但是Camera2通过TextureView来进行视图预览的。我们通过对TextureView设置大小和角度变幻。

这里有一个疑问。Camera2的图像显示数据,没有使用CameraSensor的那个原理来显示图像了么,我在不做任何设置的时候,竖屏的时候,视图仍然是正常的显示,根据对于上一节的CameraSensor的了解,它的方向是90度,没有对视图进行变幻应该是一个方向异常的。这里以后了解了。

这里我们对于Camera2的初始化的一部分API做了了解,重点理解了相机的预览大小和方向的概念和一些基础知识。还有一点对于拍照输出的图片的方向,Camera使用API Camera.Parameters.setRotation方法设置即可,对于Camera2的拍照图片设置我们在下一节去看了。这些内容有点乱了混在一起。下一篇重点了解Camera2来实现一些功能。

有错误的地方希望指正。

参考链接:
简单的Camera2应用如何去搭建
对于CameraSensor和屏幕方向的概念
对于细节知识点的讲解

猜你喜欢

转载自blog.csdn.net/lyman_ye/article/details/78840927
今日推荐