Android 音视频开发【视频篇】【二】视频采集 | 8月更文挑战

1,465 阅读5分钟

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

上一章介绍了RGBYUV格式,本章将介绍如何采集YUV格式的数据

一、视频相关概念

1.1 分辨率

用以横向纵向的像素点个数来衡量一副图像的大小,比如1080*960,其中1080表示横向的像素点个数为1080个,960表示纵向的像素点个数为960

1.2 帧率

1s内有多少个副图像,比如30fps,表示1s内有30副图像,即30帧,而60fps,表示1s内有60帧,更高的还有90fps120fps,帧率越高,相对来说,画面就会更流畅

根据人眼的视觉暂留特性,24fps时,人眼就会认为是流畅的,而更高的帧率,会给人一种身临其境的感觉

当然,也不是帧率越高越好,需要考虑显示器的刷新率,帧率过高可能反而浪费不必要的资源

1.3 码率/比特率

码率,也即比特率,单位是bps,表示视频的比特数,计算公式是:

码率=文件大小(kb/时长(s码率 = 文件大小(kb) / 时长(s)

二、视频采集

采集视频,在手机中可以通过摄像头进行采集,那么,在Android也提供了很多对摄像头进行操作的类

  • Camera

    操作相对简单,提供对摄像头的很多的操作方法,比如打开、关闭摄像头、变焦、拍照等,不过谷歌已经不建议使用,而是建议使用CameraX,或者是Camera2

  • Camera2

    Camera2相对于Camera增加了很多特性,比如对Api的架构进行了很大的优化,比Camera更先进,还可以获取每帧的信息,等等,还有很多特性,不过,Camera2相对于Camera,使用上变得更加复杂

  • CameraX

    JetPack家族的一员,CameraX是在Camea2之后新出的一个相机框架,可能是谷歌觉得虽然Camera2功能强大,但是使用起来确实麻烦了很多,所有为了在保证功能的前提的下,操作变得更简洁,所有推出了CameraX

虽然谷歌建议我们使用CameraX或者是Camera2,但是视频系列的相关文章,还是以Camera,不为别的,就是为了简单,哈哈

2.1 Camera

关于相机的使用,可以放入子线程去使用

首先,第一步,当然是打开相机

打开相机

camera = Camera.open(id);

打开相机,使用的是Camera类的静态方法open(),该方法需要传入一个int类型的id,有两个值可选

  • Camera.CameraInfo.CAMERA_FACING_BACK

    后摄

  • Camera.CameraInfo.CAMERA_FACING_FRONT

    前摄

在获取camera实例后,此时摄像头并没有正在的打开

接下来需要对camera做一些参数配置,比如预览尺寸

获取相机参数

Camera.Parameters parameters = camera.getParameters();

camera进行参数设置,先得调用该方法,然后下面就会对parameters进行一些参数设置

获取全部预览尺寸

List<Camera.Size> previewSizeList = parameters.getSupportedPreviewSizes();

通过调用parameters.getSupportedPreviewSizes()获取当前相机所支持的预览尺寸,因为我们传入相机的预览尺寸必须得是相机所支持的

一般来说,我们通常希望相机的尺寸能够和屏幕一样大,或者是和屏幕同等比例,这样,显示出来的效果也就比较好,所有下面通过一个算法,找到最佳的尺寸

/**
 * 寻找最合适的尺寸
 */
public static Camera.Size findTheBestSize(List<Camera.Size> sizeList, int screenW, int screenH) {
    if (sizeList == null || sizeList.isEmpty()) {
        throw new IllegalArgumentException();
    }
    Camera.Size bestSize = sizeList.get(0);
    for (Camera.Size size : sizeList) {
        int width = size.height;
        int height = size.width;
        float ratioW = (float) width / screenW;
        float ratioH = (float) height / screenH;
        if (ratioW == ratioH) {
            bestSize = size;
            break;
        }
    }
    return bestSize;
}

一个参数,直接传入相机所支持的预览尺寸,后面两个参数是屏幕的尺寸

该算法的目的就是遍历每一个预览尺寸,但预览尺寸的比例和屏幕的尺寸比例一致时,就可以返回,而如果当遍历完,还是没找到合适的尺寸,就返回第一个相机所支持的尺寸

通过此方法,就获取到了一个最佳的尺寸

设置预览尺寸

parameters.setPreviewSize(width, height);

本章的目的是采样视频数据,所以我们得设置相机预览数据的回调格式

设置预览格式

parameters.setPreviewFormat(ImageFormat.NV21);

相机设置参数

camera.setParameters(parameters);

在对parameters做了一系列的设置后,我们就可以将其设置给camera

设置旋转角度

camera.setDisplayOrientation(CameraUtils.getDisplayOrientation(activity, id));

设置旋转角度,使用的是CameraUtils.getDisplayOrientation(activity, id)方法

/**
 * 获取摄像头旋转角度
 */
public static int getDisplayOrientation(Activity activity, int facing) {
    Camera.CameraInfo info = new Camera.CameraInfo();
    Camera.getCameraInfo(facing, 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;
    }
    return result;
}

设置数据缓存和回调

之前设置了预览数据的回调格式为NV21,它是一种YUV420的格式

camera提供了addCallbackBuffersetPreviewCallbackWithBuffer的方法,用于接收预览数据和设置回调,它能够减少对象的创建

camera.addCallbackBuffer(nv21Data);
camera.setPreviewCallbackWithBuffer(this);

设置回调后,我们需要重写onPreviewFrame方法

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
    Log.d(TAG, "onPreviewFrame: " + nv21Data.length);
    camera.addCallbackBuffer(nv21Data);
    if (fos == null) {
        return;
    }
    try {
        fos.write(nv21Data);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

在该方法中,每次回调预览数据,都会先调用addCallbackBuffer方法,该方法会复用对应的对象

接着后面将数据写入到文件中

设置SurfaceTexture,开启预览

当然,上面还没结束,还需要设置SurfaceTexture才能正常开启预览,也就才能正常获取预览回调数据

try {
    camera.setPreviewTexture(new SurfaceTexture(0));
} catch (IOException e) {
    e.printStackTrace();
}
camera.startPreview();

这里设置的是一个new出来的surfaceTexture,产生的效果是无预览采样视频

你也可以使用SurfaceView或者TextureView显示预览画面

停止预览,释放资源

当我们停止录制时,需要停止预览并释放资源

private void release() {
    if (camera != null) {
        camera.stopPreview();
        camera.release();
        camera = null;
    }
    if (fos != null) {
        try {
            fos.close();
            fos = null;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

三、GitHub

YuvRecord

YuvActivity