Android录制视频

FFmpeg对于Android平台的视频输入设备API有一定的支持,但是比较局限,只支持Android N以上的版本,低版本无法使用。

其原因是因为FFmpeg采用的是Android N推出的native层camera API来实现的(NDK中的libcamera2ndk.so),而并非采用JNI的方式来调用Java层camera API。

Android平台视频录制API

对于视频录制API,Android平台上有两套API实现,一套是老版的Camera1,另一套是Android L之后推出的Camera2。

Camera1使用起来较为简单,但是功能相对较少,不支持多纹理输出。Camera2的API功能上虽然更加强大,但是API设计的非常底层化,不利于理解,并且YUV_420_888的数据格式,国内各大产商在实现上留下的坑太多。

Jetpack组件中,Google推出了全新的CameraX组件,对Camera1和Camera2进行了统一的封装,使得API更加简单易用。

Camera1

获取Camera设备信息

  • Camera.getNumberOfCameras() : 获取Camera设备数量
  • index : 索引就是后续会使用的CameraID
  • Camera.CameraInfo : Camera设备信息
  • Camera.getCameraInfo(index, info) : 获取指定索引的Camera设备信息,存储到info对象中
  • Camera.CameraInfo.facing : 相机面对的方向,前置还是后置 CAMERA_FACING_BACK or CAMERA_FACING_FRONT.
  • Camera.CameraInfo.orientation : 相机图像的方向,获取到的图像需要顺时针旋转该角度才能正常显示。值为0、90、180、270。(因为我们拿手机一般是竖着拿,但是摄像头可能是向左横着或向右横着被安装的)
  • Camera.open : 获取指定索引位置的Camera设备实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 遍历所有的camera设备
for (int index = 0; index < android.hardware.Camera.getNumberOfCameras(); ++index) {
// 获取camera设备信息
android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(index, info);
String facing =
(info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT) ? "front" : "back";
final android.hardware.Camera camera;
try {
// 开启camera设备,获取camera实例
camera = android.hardware.Camera.open(cameraId);
} catch (RuntimeException e) {
callback.onFailure(FailureType.ERROR, e.getMessage());
return;
}
}

设置采集参数

设置采集的尺寸、格式、对焦、闪光灯、白平衡等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 获取相机设备的参数集
final android.hardware.Camera.Parameters parameters = camera.getParameters();
// 获取手机支持的对焦模式(为了应对各手机厂商的差异,一般在设置某个功能时,先判断当前手机是否支持该功能)
final List<String> focusModes = parameters.getSupportedFocusModes();
// 设置FPS的区间
parameters.setPreviewFpsRange(framerate.min, framerate.max);
// 设置预览尺寸
parameters.setPreviewSize(width, height);

// 设置预览数据的像素格式,一般设置NV21
parameters.setPreviewFormat(ImageFormat.NV21);

// 视频稳像
if (parameters.isVideoStabilizationSupported()) {
parameters.setVideoStabilization(true);
}
// 如果当前手机支持,则设置对焦模式为 FOCUS_MODE_CONTINUOUS_VIDEO
if (focusModes.contains(android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
parameters.setFocusMode(android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
}
// 设置参数,重启相机才生效
camera.setParameters(parameters);
// 设置相机预览显示的图像方向
camera.setDisplayOrientation(0 /* degrees */);

下面的代码就是设置相机预览显示的图像角度根据activity显示的方向来调整

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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;
} else {
// 后置相机
result = (info.orientation - degrees + 360) % 360;
}
camera.setDisplayOrientation(result);
}

设置输出

获取输出数据有两种方式,一种是设置一个SurfaceTexture采用纹理进行接收;另一种是通过设置回调直接接收byte array数据。

Texture格式

SurfaceTexture有两种方式获取,一种是通过系统的SurfaceView或者TextureView来获取,另一种就是自建OES纹理,通过构造函数new。

  • 通过系统的View来获取的SurfaceTexture,相机设备将数据写入后,系统View会自动取出数据更新显示。
  • 自己创建的SurfaceTexture可以通过设置OnFrameAvailableListener来监听数据的变更,当相机设备写入数据时,会触发回调,在回调中使用updateTexImage从队列中取出一个数据绑定到OES纹理中,使用OES纹理来进行绘制显示或者编码输入等操作。
    1
    2
    3
    4
    5
    6
    7
    try {
    camera.setPreviewTexture(getSurfaceTexture());
    } catch (IOException | RuntimeException e) {
    camera.release();
    callback.onFailure(FailureType.ERROR, e.getMessage());
    return;
    }

ByteArray格式

对于ByteArray格式的回调设置,又有两种方式,一种是setPreviewCallback方法,另一种是setPreviewCallbackWithBuffer方法。

  • 第一种onPreviewFrame回调方法会在每一帧数据准备好了就调用,回调时机是不可控的。
  • 第二种方式是在需要在前一帧的onPreviewFrame方法中调用addCallbackBuffer方法,下一帧的onPreviewFrame才会调用,同时addCallbackBuffer方法的参数的byte数据就是每一帧的原数据。
1
2
3
4
5
6
7
// 设置回调
camera.setPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {

}
});
1
2
3
4
5
6
7
8
9
10
11
12
final int frameSize = captureFormat.frameSize();
final ByteBuffer buffer = ByteBuffer.allocateDirect(frameSize);
// 设置数据回调的buffer
camera.addCallbackBuffer(buffer.array());
// 设置回调
camera.setPreviewCallbackWithBuffer(new android.hardware.Camera.PreviewCallback() {
@Override
public void onPreviewFrame(final byte[] data, android.hardware.Camera callbackCamera) {
// 获取下一个回调
camera.addCallbackBuffer(data);
}
}

采集

  • startPreview : 开启预览,数据开始回调,更改采集参数后需要重新开启预览才生效
  • stopPreview : 停止预览
    1
    2
    3
    4
    5
    6
    7
    8
    try {
    camera.startPreview();
    } catch (RuntimeException e) {

    }

    camera.stopPreview();
    camera.release();

Camera2

获取Camera设备信息

Camera设备的获取需要通过系统服务来操作。

  • getCameraIdList : 获取camera设备列表
  • getCameraCharacteristics : 获取指定camera设备参数集,通过对应的KEY从参数集中获取到各种参数值
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    this.cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);

    String[] cameraIdList = cameraManager.getCameraIdList();
    for (String cameraId : cameraIdList) {
    CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
    // 获取相机面对的方向,CameraMetadata.LENS_FACING_BACK or CameraMetadata.LENS_FACING_FRONT
    String facing = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_BACK;

    try {
    // 开启camera设备,设置开启回调,以及指定camera回调执行的线程
    cameraManager.openCamera(cameraId, new CameraStateCallback(), cameraThreadHandler);
    } catch (CameraAccessException e) {
    return;
    }
    }

    camera设备状态回调,在回调中可以开启一次会话,在会话中可以进行数据的请求
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    private class CameraStateCallback extends CameraDevice.StateCallback {
    private String getErrorDescription(int errorCode) {
    switch (errorCode) {
    case CameraDevice.StateCallback.ERROR_CAMERA_DEVICE:
    return "Camera device has encountered a fatal error.";
    case CameraDevice.StateCallback.ERROR_CAMERA_DISABLED:
    return "Camera device could not be opened due to a device policy.";
    case CameraDevice.StateCallback.ERROR_CAMERA_IN_USE:
    return "Camera device is in use already.";
    case CameraDevice.StateCallback.ERROR_CAMERA_SERVICE:
    return "Camera service has encountered a fatal error.";
    case CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE:
    return "Camera device could not be opened because"
    + " there are too many other open camera devices.";
    default:
    return "Unknown camera error: " + errorCode;
    }
    }

    @Override
    public void onDisconnected(CameraDevice camera) {
    // camera设备断开连接
    }

    @Override
    public void onError(CameraDevice camera, int errorCode) {
    // 发生错误
    }

    @Override
    public void onOpened(CameraDevice camera) {
    // camera设备成功开启
    cameraDevice = camera;

    // 创建采集会话,设置采集输出的Surface集合,可同时输出到多个Surface中
    try {
    camera.createCaptureSession(
    Arrays.asList(surface), new CaptureSessionCallback(), cameraThreadHandler);
    } catch (CameraAccessException e) {
    return;
    }
    }

    @Override
    public void onClosed(CameraDevice camera) {
    // camera设备关闭
    }
    }

设置采集参数

采集会话创建成功后,可以设置采集的参数,然后创建采集请求,进行数据的采集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
private class CaptureSessionCallback extends CameraCaptureSession.StateCallback {
@Override
public void onConfigureFailed(CameraCaptureSession session) {
// 配置失败
}

@Override
public void onConfigured(CameraCaptureSession session) {
// 设置采集参数
captureSession = session;
try {
// 创建视频采集请求构建器,设置采集的参数
final CaptureRequest.Builder captureRequestBuilder =
cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
// 设置FPS的区间
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
new Range<Integer>(framerate.min,framerate.max));
// 设置闪光灯模式
captureRequestBuilder.set(
CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false);
// 设置视频稳像
chooseStabilizationMode(captureRequestBuilder);
// 设置对焦模式
chooseFocusMode(captureRequestBuilder);

// 添加采集输出目标,可添加多个,添加的目标必须在createCaptureSession的集合中
captureRequestBuilder.addTarget(surface);

// 重复发送请求,开始采集
session.setRepeatingRequest(
captureRequestBuilder.build(), new CameraCaptureCallback(), cameraThreadHandler);
} catch (CameraAccessException e) {
reportError("Failed to start capture request. " + e);
return;
}
}

// 优先采用光学稳像,其次采用软件稳像
private void chooseStabilizationMode(CaptureRequest.Builder captureRequestBuilder) {
final int[] availableOpticalStabilization = cameraCharacteristics.get(
CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION);
if (availableOpticalStabilization != null) {
for (int mode : availableOpticalStabilization) {
if (mode == CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON) {
captureRequestBuilder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE,
CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON);
captureRequestBuilder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_OFF);
Logging.d(TAG, "Using optical stabilization.");
return;
}
}
}
// If no optical mode is available, try software.
final int[] availableVideoStabilization = cameraCharacteristics.get(
CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES);
for (int mode : availableVideoStabilization) {
if (mode == CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON) {
captureRequestBuilder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON);
captureRequestBuilder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE,
CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_OFF);
Logging.d(TAG, "Using video stabilization.");
return;
}
}
Logging.d(TAG, "Stabilization not available.");
}
// 设置对焦模式
private void chooseFocusMode(CaptureRequest.Builder captureRequestBuilder) {
final int[] availableFocusModes =
cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES);
for (int mode : availableFocusModes) {
if (mode == CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO) {
captureRequestBuilder.set(
CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO);
Logging.d(TAG, "Using continuous video auto-focus.");
return;
}
}
Logging.d(TAG, "Auto-focus is not available.");
}
}

设置输出

Camera2中数据的输出都是通过设置Surface来接收,可以同时设置多个Surface,Surface可以通过SurfaceTexture来创建。

  • 采用系统的SurfaceView、TextureView生成的Surface,设置给Camera,对相机采集的数据进行预览显示
  • 采用系统的ImageReader生成的Surface来接收指定格式的图像数据
  • 采用自己创建的SurfaceTexture来接收数据,通过设置OnFrameAvailableListener来监听数据的变更,当相机设备写入数据时,会触发回调,在回调中使用updateTexImage从队列中取出一个数据绑定到OES纹理中,使用OES纹理来进行绘制显示或者编码输入等操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建采集会话,设置采集输出的Surface集合,可同时输出到多个Surface中
try {
camera.createCaptureSession(
Arrays.asList(surface), new CaptureSessionCallback(), cameraThreadHandler);
} catch (CameraAccessException e) {
return;
}

// 会话创建成功后,在创建采集请求时,添加对应的输出目标就可以将采集请求到的数据传入到目标中

// 添加采集输出目标,可添加多个,添加的目标必须在createCaptureSession的集合中
captureRequestBuilder.addTarget(surface);
// 重复请求,开始采集
session.setRepeatingRequest(
captureRequestBuilder.build(), new CameraCaptureCallback(), cameraThreadHandler);

ImageReader

ImageReader是跟随Camera2出来的,用来接收相机采集的数据,在拍照时可以设置ImageFormat.JPEG格式,如果想要获取原始数据,则需要设置成ImageFormat.YUV_420_888。

ImageFormat.YUV_420_888格式,是指YUV_420格式,其内部具体格式可以是NV12、NV21、YV12、I420中的任何一种,也就是说如果设置的是YUV_420_888格式,则设备返回的数据格式需要根据其返回的具体数据去判断。(这点在国内手机上各不相同,有些个别低端手机给的数据不符合官方文档上的要求)

对于ImageReader来说,最最重要的就是理解好YUV_420_888格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建ImageReader对象,指定接收的数据尺寸和数据格式以及缓冲区大小
ImageReader imageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 2);
// 获取Surface,添加到会话请求的目标中作为输出
Surface surface = imageReader.getSurface();
// 设置数据回调
imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
// 获取采集到的数据
Image image = reader.acquireNextImage();

// 操作

// 释放
image.close();
}
}, mCameraHandler);