aoi学院

Aisaka's Blog, School of Aoi, Aisaka University

Android-音视频-视频开发-(二)视频录制

相机

Android框架包含了各种相机和相机功能的支持,使你可以在你的应用中捕获图像和视频。本文讨论一个简单快速的获取图像和视频的方法,并概述一个创建自定义用户相机体验的方法。

录制视频有两种方法:

  1. 系统相机的录制视频功能

  2. 通过安卓自带的MediaRecorder来录制视频


基础知识

Android框架支持通过CameraAPI或Cemera intent来抓取图像和视频。下面就是相关的类:

Camera:此类是控制设备相机的主要API.此类用于在创建相机应用时获取图片和视频.

SurfaceView:此类为用户提供camera的实时图像预览.

MediaRecorder:此类用于从camera录制视频.

Intent:一个MediaStore.ACTION_IMAGE_CAPTURE或MediaStore.ACTION_VIDEO_CAPTURE型的intent,可以使用它来抓取图像或视频,而不用操作Camera对象。


Manifest中的声明

1
2
3
4
5
6
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.CAMERA" />

Camera(Android 5.0 API 21已弃用)

拍摄图片

利用Camera想要拍照,需要如下使用步骤:

  1. 利用open(int)获取Camera实例。

  2. 利用getParameters()获取默认设置,如果需要利用setParameters(Camera.Parameters)进行参数设置。

  3. 利用setDisplayOrientation(int)函数设置正确的预览方向。

  4. 想要预览,需要配合SurfaceView,利用setPreviewDisplay(SurfaceHolder)设置SurfaceView的SurfaceHolder用于预览。

  5. 调用startPreview()开始预览,拍照之前必须已经开始预览。

  6. takePicture 拍摄照片
    调用takePicture后预览会停止,需用重新调用startPreview才能再次开始预览。预览开始后,就可以通过Camera.takePicture()方法拍摄一张照片,返回的照片数据通过Callback接口获取。
    takePicture()接口可以获取三个类型的照片:
    第一个,ShutterCallback接口,在拍摄瞬间瞬间被回调,通常用于播放“咔嚓”这样的音效
    第二个,PictureCallback接口,返回未经压缩的RAW类型照片
    第三个,PictureCallback接口,返回经过压缩的JPEG类型照片

  7. 调用takePickture后预览会停止,想要继续预览需要调用startPreview()函数

  8. 调用stopPreview()停止预览

  9. 调用release()释放资源,为了节省资源在Activity.onPause是调用停止预览,在onResume是开始预览。

录制视频

跳转使用Intent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);

// 1. 设置视频录制的最长时间(单位:s),录制到达时间,系统会自动保存视频,停止录制
intent.putExtra (MediaStore.EXTRA_DURATION_LIMIT, 30);
// 2. 限制视频的大小,这里是100兆。当大小到达的时候,系统会自动停止录制
intent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, 1024 * 1024 * 100);
// 3. 设置视频录制的画质(0~1之间,0和1是所有相机都有的设置:0较差,一分钟约5m;默认1较好,一分钟约40m)
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);
// 4. 表示录制完后保存的录制,如果不写,则会保存到默认的路径,在onActivityResult()的回调,通过intent.getData中返回保存的路径
String filePath = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) + "/msc/" + "test.mp4";
Uri uri = Uri.fromFile(new File(filePath));
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);

startActivityForResult (intent, VIDEO_WITH_CAMERA);

录制完回调获取

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
try {
if (resultCode == Activity.RESULT_OK && requestCode == VIDEO_WITH_CAMERA) {
Uri uri = data.getData();
Log.e(TAG, "onActivityResult: " + uri.toString());
}
} catch (Exception e) {
e.printStackTrace();
}
}

获取视频时长

1
2
3
mediaPlayer.setDataSource(this, uri);
mediaPlayer.prepare();
int duration = mediaPlayer.getDuration() / 1000; // 获取到的是毫秒值

获取视频的第一帧的图片

1
2
3
4
MediaMetadataRetriever media = new MediaMetadataRetriever();
String videoPath = uri.getPath(); // 通过Uri获取绝对路径
media.setDataSource(videoPath);
Bitmap bitmap = media.getFrameAtTime(); // 视频的第一帧图片

选择本地视频

这里有个坑,不同品牌机子间获取本地相册有兼容性问题,需要查阅对应手册针对性调用,有一篇博客记录到:

“使用ACTION_PICK的方式打开相册在oppo、vivo、华为、小米使用均没问题,但是在魅族一加都不可以(跳转后的界面无法选择视频)。而魅族手机使用ACTION_OPEN_DOCUMENT方式可以选择视频文件,但是在华为手机(测试用的华为)上则会出现闪退(无法获取到被选中的视频文件的路径)。”

使用系统的选择本地视频的方法

1
2
3
4
5
6
7
8
9
10
Intent intent = new Intent();
if ("Meizu".equalsIgnoreCase(android.os.Build.MANUFACTURER)) { // 判断用户手机是否是“魅族”。忽略大小写的比较
intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("video/*");
} else {
intent.setAction(Intent.ACTION_PICK);
intent.setData(android.provider.MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
}
context.startActivityForResult(intent, REQUEST_CODE_CHOOSE_VIDEO);

选择视频后回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void handleVideoResult(int requestCode, int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK && null != data)
if (requestCode == REQUEST_CODE_CHOOSE_VIDEO) {
try {
Uri selectedVideo = data.getData(); // 获取视频的Uri
String[] filePathColumn = {MediaStore.Video.Media.DATA};
cursor = context.getContentResolver().query(selectedVideo,
filePathColumn, null, null, null);
cursor.moveToFirst();
int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
String path = cursor.getString(columnIndex); // 视频路径
} finally {
if (cursor != null) cursor.close(); // 关闭cursor
}
}
}

选中后,可以获取到视频的各种信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 视频ID:MediaStore.Audio.Media._ID
int videoId = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID));
// 视频名称:MediaStore.Audio.Media.TITLE
String title = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.TITLE));
// 视频路径:MediaStore.Audio.Media.DATA
String videoPath = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA));
// 视频时长:MediaStore.Audio.Media.DURATION
int duration = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION));
// 视频大小:MediaStore.Audio.Media.SIZE
long size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE));
// 视频缩略图路径:MediaStore.Images.Media.DATA
String imagePath = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));
// 缩略图ID:MediaStore.Audio.Media._ID
int imageId = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));

MediaRecorder基于Camera API实现自定义相机

视频录制也可以通过 MediaRecorder 类完成,其步骤与音频录制基本相同,只是添加了一些对视频进行处理的操作。

视频录制的基本步骤如下:

1. 调用Camera.open()方法打开摄像头。

2. 调用 Camera.setPreviewDisplay() 连接预览窗口。

以便将从摄像头获取的图像放置到预览窗口中显示出来。

3. 调用 Camera.startPreview()启动预览。

显示摄像头拍摄到的图像。

打开摄像机openCamera()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void openCamera() {
Log.d(TAG, "openCamera.");
try {
camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
// 旋转成竖屏
camera.setDisplayOrientation(90);
} catch (Exception e) {
Log.e(TAG, "open camera error!");
e.printStackTrace();
return;
}
try {
camera.setPreviewDisplay(mSurfaceHolder);
} catch (IOException e) {
Log.e(TAG, "preview failed.");
e.printStackTrace();
}
camera.startPreview();
}

4. 使用 MediaRecorder 进行视频录制。

1)使用 Camera.unlock() 方法解锁摄像头,以使 MediaRecorder 获得对摄像头的使用权。

unlock的作用是:Camera属于硬件设备,通常情况下Camera被一个使用Camera的进程锁定,是不允许其他进程使用的。unLock必须在你调用MediaRecorder.setCamera之前调用。

从api14 开始,录制视频时MediaRecorder调用start函数时Camera 会自动的调用Lock,所以再开始录制视频之前或者录制视频结束之后不需要手动的调用lock函数。

【注意:lock和unLock 都是只有在录制视频时才会使用,其他情况用不到这两个函数。如果你不是要录制视频,只是简单地预览不需要调用这个函数。】

unlock()解锁摄像机

1
2
3
4
5
6
7
Camera.Parameters params = camera.getParameters();
int height = params.getSupportedVideoSizes().get(3).height;
int width = params.getSupportedVideoSizes().get(3).width;
// 给摄像头解锁
camera.unlock();
// MediaRecorder获取到摄像头的访问权
mVideoRecorder.setCamera(camera);

2)配置 MediaRecorder。

音频源和视频源中包括

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
AudioSource类中包括:(一般使用默认或者主麦克风或者摄像头旁边的麦克风。)
MediaRecorder.AudioSource.DEFAULT: 默认音频源
MediaRecorder.AudioSource.MIC:主麦克风。
MediaRecorder.AudioSource.VOICE_CALL:来源为语音拨出的语音与对方说话的声音
MediaRecorder.AudioSource.VOICE_COMMUNICATION:摄像头旁边的麦克风
MediaRecorder.AudioSource.VOICE_DOWNLINK:下行声音
MediaRecorder.AudioSource.VOICE_RECOGNITION:语音识别
MediaRecorder.AudioSource.VOICE_UPLINK:上行声音

VideoSource类中包括:
VideoSource.DEFAULT:默认
VideoSource.CAMERA:摄像头
VideoSource.SURFACE:Surface作为来源

在录制视频的步骤中有一步是调用setCamera设置Camera,这一步相当于设置来源是摄像头,下面就需要使用VideoSource.CAMERA作为视频源,还可以使用MediaRecorder的getSurface播放视频,代替setCamera,这时的视频来源就是Surface。

建立 MediaRecorder 类的对象,并设置音频源和视频源

1
2
3
4
5
6
MediaRecorder mVideoRecorder = new MediaRecorder();
// 设置视频录制过程中所录制的音频来自手机的麦克风
mVideoRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
// mVideoRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);系统内部声音
// 设置视频源为摄像头
mVideoRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);

设置视频的输出和编码格式

1
2
3
4
5
6
7
8
9
10
11
// 设置视频录制的输出文件为mp4文件
mVideoRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
// 设置音频编码方式为AAC
mVideoRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
// 设置录制的视频编码为H.264
mVideoRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
// 设置视频录制的分辨率,必须放在设置编码和格式的后面,否则报错
mVideoRecorder.setVideoSize(width, height);
// 设置录制的视频的视频帧率,必须放在设置编码和格式的后面,否则报错
mVideoRecorder.setVideoFrameRate(30);
mVideoRecorder.setVideoEncodingBitRate(8*1024*1024);

也可以通过 CamcorderProfile 对象可用于对 MediaRecorder 进行相关设置。

CamcorderProfile 为预先定义好的一组视频录制相关配置信息。

Android SDK 共定义了10+种 CamcorderProfile 配置,如 CamcorderProfile. QUALITY_HIGH、CamcorderProfile. QUALITY_LOW、CamcorderProfile. QUALITY_TIME_LAPSE_1080P 等。其中,QUALITY_LOW 和 QUALITY_HIGH 两种配置是所有的摄像头都支持的,其他配置则根据硬件性能决定。

每一种配置都涉及文件输出格式、视频编码格式、视频比特率、视频帧率、视频的高和宽、音频编码格式、音频的比特率、音频采样率和音频录制的通道数几个方面。通过使用这些预定义配置能够降低代码复杂度,提高编码效率。

CamcorderProfile和参数设置

1
2
3
4
5
6
7
8
9
10
11
12
13
CamcorderProfile get(int cameraId, int quality)
CamcorderProfile get(int quality)
// 参数说明
// cameraId:摄像头id,分为前置摄像头或者后置摄像头。
// quality:质量情况
// 第一种例如QUALITY_HIGH:需要手动设置码率比特率采样率等
// QUALITY_TIME_LAPSE_HIGH:时间流逝质量(比特率)
// QUALITY_HIGH_SPEED_HIGH:高速(高帧率)质量

CamcorderProfile mCamcorderProfile = CamcorderProfile.get(Camera.CameraInfo.CAMERA_FACING_BACK, CamcorderProfile.QUALITY_HIGH);
mMediaRecorder.setProfile(mCamcorderProfile);
// 利用setProfile设置参数必须在设置了video和audio Source之后调用,在setOutputFile之前设置。如果时间流逝的CamcorderProfile被使用,audio相关的源或者参数设置将被忽略。
// 注意:如果调用了setProfile函数,setOutputFormat,setAudioEncoder,setVideoEncoder,不能再调用设置。

设置录制的视频文件的保存位置及文件名

1
mVideoRecorder.setOutputFile(PATH_NAME);

使用 MediaRecorder.setPreviewDisplay() 方法指定 MediaRecorder 的视频预览窗口

1
mVideoRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());

3)将录像器置于准备状态,然后启动录像器

1
2
mVideoRecorder.prepare();
mVideoRecorder.start();

5. 视频录制完成后,可使用以下方法停止视频录制。

停止录像器,重置录像器的相关配置,释放录像器对象

1
2
3
mMediaRecorder.stop();
mMediaRecorder.reset();
mMediaRecorder.release();

【调用 Camera.lock() 方法锁定摄像头。从 Android N(7.x) 开始,该调用也不再必需,除非 MediaRecorder.prepare() 方法失败】

6. 调用Camera.stopPreview()方法停止预览。

7. 调用Camera.release()方法释放摄像头。

1
2
3
4
5
6
private void releaseCamera() {
if (camera != null) {
camera.release();
camera = null;
}
}

#. 其它一些关于MediaRecorder的方法。

Android 4.0.3 引入可以使图像稳定化(通过修改参数)

1
2
3
4
5
6
Camera.Parameters parameters = camera.getParameters();
// 不是所有的照相机设备都支持图像稳定化,所以需要先检查下
if (parameters.isVideoStabilizationSupported()) {
parameters.setVideoStabilization(true);
}
camera.setParameters(parameters);

创建一个延时的视频

1
2
3
// Capture an image every 30 seconds.
// 参数是Double fps,即每秒的帧数
mediaRecorder.setCaptureRate(0.03);

Camera2

简介

Android 5.0(API 21)开始出现了新的相机Camera 2 API,弃用以前的Camera API,这是因为Camera1 那寥寥无几的 API 和极差的灵活性早已不能满足日益复杂的相机功能开发。Camera2 的出现给相机应用程序带来了巨大的变革,因为它的目的是为了给应用层提供更多的相机控制权限,从而构建出更高质量的相机应用程序。

Camera2 API不仅提高了android系统的拍照性能,还支持RAW照片输出,还可以设置相机的对焦模式,曝光模式,快门等等。

这里引用了管道的概念将安卓设备和摄像头之间联通起来,系统向摄像头发送 Capture 请求,而摄像头会返回 CameraMetadata。这一切建立在一个叫作 CameraCaptureSession 的会话中。

整个拍摄流程如下:

  1. 创建一个用于从 Pipeline 获取图片的 CaptureRequest。

  2. 修改 CaptureRequest 的闪光灯配置,让闪光灯在拍照过程中亮起来。

  3. 创建两个不同尺寸的 Surface 用于接收图片数据,并且将它们添加到 CaptureRequest 中。

  4. 发送配置好的 CaptureRequest 到 Pipeline 中等待它返回拍照结果。

一个新的 CaptureRequest 会被放入一个被称作 Pending Request Queue 的队列中等待被执行,当 In-Flight Capture Queue 队列空闲的时候就会从 Pending Request Queue 获取若干个待处理的 CaptureRequest,并且根据每一个 CaptureRequest 的配置进行 Capture 操作。最后我们从不同尺寸的 Surface 中获取图片数据并且还会得到一个包含了很多与本次拍照相关的信息的 CaptureResult,流程结束。

Camera2 中主要的API类

CameraManager类:摄像头管理类,用于检测、打开系统摄像头,通过getCameraCharacteristics(cameraId)可以获取摄像头特征。

CameraCharacteristics类:相机特性类,例如,是否支持自动调焦,是否支持zoom,是否支持闪光灯一系列特征。

CameraDevice类: 相机设备,类似早期的camera类。

CameraCaptureSession类:用于创建预览、拍照的Session类。通过它的setRepeatingRequest()方法控制预览界面,通过它的capture()方法控制拍照动作或者录像动作。

CameraRequest类:一次捕获的请求,可以设置一些列的参数,用于控制预览和拍照参数,例如:对焦模式,曝光模式,zoom参数等等。

1
2
3
4
// CameraManager类

通过以下代码可以获取摄像头的特征对象,例如:前后摄像头,分辨率等。
CameraCharacteristics cameraCharacteristics = manager.getCameraCharacteristics(cameraId);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// CameraCharacteristics类

相机特性类
CameraCharacteristics是一个包含相机参数的对象,可以通过一些key获取对应的values。

以下几种常用的参数:
LENS_FACING:获取摄像头方向。LENS_FACING_FRONT是前摄像头,LENS_FACING_BACK是后摄像头。
SENSOR_ORIENTATION:获取摄像头拍照的方向。
FLASH_INFO_AVAILABLE:获取是否支持闪光灯。
SCALER_AVAILABLE_MAX_DIGITAL_ZOOM:获取最大的数字调焦值,也就是zoom最大值。
LENS_INFO_MINIMUM_FOCUS_DISTANCE:获取最小的调焦距离,某些手机上获取到的该values为null或者0.0。前摄像头大部分有固定焦距,无法调节。
INFO_SUPPORTED_HARDWARE_LEVEL:获取摄像头支持某些特性的程度。

以下手机中支持的若干种程度:
INFO_SUPPORTED_HARDWARE_LEVEL_FULL:全方位的硬件支持,允许手动控制全高清的摄像、支持连拍模式以及其他新特性。
INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED:有限支持,这个需要单独查询。
INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY:所有设备都会支持,也就是和过时的Camera API支持的特性是一致的。
1
2
3
4
5
6
7
8
9
10
11
// CameraDevice类

CameraDevice的reateCaptureRequest(int templateType)方法创建CaptureRequest.Builder。

templateType参数有以下几种:
TEMPLATE_PREVIEW:预览
TEMPLATE_RECORD:拍摄视频
TEMPLATE_STILL_CAPTURE:拍照
TEMPLATE_VIDEO_SNAPSHOT:创建视视频录制时截屏的请求
TEMPLATE_ZERO_SHUTTER_LAG:创建一个适用于零快门延迟的请求。在不影响预览帧率的情况下最大化图像质量。
TEMPLATE_MANUAL:创建一个基本捕获请求,这种请求中所有的自动控制都是禁用的(自动曝光,自动白平衡、自动焦点)。
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
// CameraDevice.StateCallback抽象类

该抽象类用于CemeraDevice相机设备状态的回调。

/**
* 当相机设备的状态发生改变的时候,将会回调。
*/
protected final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
/**
* 当相机打开的时候,调用
* @param cameraDevice
*/
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
mCameraDevice = cameraDevice;
startPreView();
}

@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
cameraDevice.close();
mCameraDevice = null;
}

/**
* 发生异常的时候调用
*
* 这里释放资源,然后关闭界面
* @param cameraDevice
* @param error
*/
@Override
public void onError(@NonNull CameraDevice cameraDevice, int error) {
cameraDevice.close();
mCameraDevice = null;
}

/**
*当相机被关闭的时候
*/
@Override
public void onClosed(@NonNull CameraDevice camera) {
super.onClosed(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
// CameraCaptureSession.StateCallback抽象类

该抽象类用于Session过程中状态的回调。

public static abstract class StateCallback {

//摄像头完成配置,可以处理Capture请求了。
public abstract void onConfigured(@NonNull CameraCaptureSession session);

//摄像头配置失败
public abstract void onConfigureFailed(@NonNull CameraCaptureSession session);

//摄像头处于就绪状态,当前没有请求需要处理
public void onReady(@NonNull CameraCaptureSession session) {}

//摄像头正在处理请求
public void onActive(@NonNull CameraCaptureSession session) {}

//请求队列中为空,准备着接受下一个请求。
public void onCaptureQueueEmpty(@NonNull CameraCaptureSession session) {}

//会话被关闭
public void onClosed(@NonNull CameraCaptureSession session) {}

//Surface准备就绪
public void onSurfacePrepared(@NonNull CameraCaptureSession session,@NonNull Surface surface) {}
}

CameraManager 是那个站在高处统管所有摄像投设备(CameraDevice)的管理者,而每个 CameraDevice 自己会负责建立 CameraCaptureSession 以及建立 CaptureRequest。

CameraCharacteristics 是 CameraDevice 的属性描述类,非要做个对比的话,那么它与原来的 CameraInfo 有相似性。

类图中有着三个重要的 callback,其中 CameraCaptureSession.CaptureCallback 将处理预览和拍照图片的工作,需要重点对待。

这些类是如何相互配合的?下面是简单的流程图。

Camera2 拍照和录像流程例子

1. 打开指定的方向的相机。

最先获取CameraManager对象,通过该对象的getCameraIdList()获取到一些列的摄像头参数。

通过循环匹配,获取到指定方向的摄像头,例如后摄像头等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CameraManager manager = (CameraManager)getSystemService(Context.CAMERA_SERVICE);

// 获取到可用的相机
for (String cameraId : manager.getCameraIdList()) {

// 获取到每个相机的参数对象,包含前后摄像头,分辨率等
CameraCharacteristics cameraCharacteristics = manager.getCameraCharacteristics(cameraId);
// 摄像头的方向
Integer facing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING);

if (facing == null) {
continue;
}
// 匹配方向,指定打开后摄像头
if (facing != CameraCharacteristics.LENS_FACING_BACK) {
continue;
}

// 打开指定的摄像头
manager.openCamera(mCameraId, stateCallback, workThreadManager.getBackgroundHandler());

return;
}

当然,实际开发中,还需要获取相机支持的特性(闪光灯、zoom调焦、手动调焦等),和设置摄像头的参数(例如:预览的Size)。

2. 创建预览界面。

CameraDevice.StateCallback 对象传入CameraManager中openCamera(mCameraId, stateCallback, workThreadManager.getBackgroundHandler())的第二个参数,用于监听摄像头的状态。

创建 CameraDevice.StateCallback 对象,且开启一个相机。当相机开启后,将出现相机预览界面。

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
/**
* 相机设备
*/
protected CameraDevice mCameraDevice;

/**
* 当相机设备的状态发生改变的时候,将会回调。
*/
protected final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
/**
* 当相机打开的时候,调用
* @param cameraDevice
*/
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
mCameraDevice = cameraDevice;
createCameraPreviewSession();
}

// 省略该状态接口的部分方法
...............
};

/**
* 预览请求的Builder
*/
private CaptureRequest.Builder mPreviewRequestBuilder;

/**
* 相机开始预览,创建一个CameraCaptureSession对象
*/
private void createCameraPreviewSession() {
// 将CaptureRequest的构建器与Surface对象绑定在一起
mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

// 为相机预览,创建一个CameraCaptureSession对象
mCameraDevice.createCaptureSession(Arrays.asList(surface, imageReader.getSurface()), stateCallback, null);
}

3. 在预览界面过程中,需要间隔刷新界面。

相机预览使用TextureView来实现。

创建一个CameraCaptureSession ,通过一个用于预览界面的CaptureRequest,间隔复用给CameraCaptureSession。

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
private CameraCaptureSession mCaptureSession;

CameraCaptureSession.StateCallback stateCallback=new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
//当cameraCaptureSession已经准备完成,开始显示预览界面
mCaptureSession = cameraCaptureSession;
setCameraCaptureSession();
}

// 省略该接口的部分方法
.......
}

/**
* 设置CameraCaptureSession的特征:
* 自动对焦,闪光灯
*/
private void setCameraCaptureSession() {
// 设置预览界面的特征,通过mPreviewRequestBuilder.set()方法,例如,闪光灯,zoom调焦等
..........

// 为CameraCaptureSession设置间隔的CaptureRequest,用间隔刷新预览界面。
mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, workThreadManager.getBackgroundHandler());
}

只要未开始拍照动作或者录像动作,该复用的CaptureRequest会重复的刷新预览界面。

接下来,等待用户点击拍照按钮或者录像按钮,进行拍照动作,或者录像动作。

4. 拍照动作。

首先锁住焦点,通过在相机预览界面CaptureRequest,然后以类似方式运行一个预捕获序列。接下来,已经准备好捕捉图片。

创建一个新的CaptureRequest,且拍照。

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
/**
* 拍照一个静态的图片
* 当在CaptureCallback监听器响应的时候调用该方法。
* 当数字调焦缩放的时候,在写入图片数中也要设置。
*/
private void captureStillPicture() {
try {
// 创建一个拍照的CaptureRequest.Builder
final CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);

captureBuilder.addTarget(imageReader.getSurface());

// 设置一系列的拍照参数,这里省略
...........

// 先停止以前的预览状态
mCaptureSession.stopRepeating();
mCaptureSession.abortCaptures();

// 执行拍照动作
mCaptureSession.capture(captureBuilder.build(), captureCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

拍照界面产生的数据只是在手机内存中,图片是一个磁盘文件,还需要一个将拍照产生数据写入文件中的操作类ImageReader。

先是创建ImageReader对象,和设置监听器等一系列参数。

1
2
3
4
5
6
7
8
9
10
/**
* 处理静态图片的输出
*/
private ImageReader imageReader;

// 对于静态图片,使用可用的最大值来拍摄。
Size largest = Collections.max(Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)), new CompareSizeByArea());
// 设置ImageReader,将大小,图片格式
imageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(), ImageFormat.JPEG, /*maxImages*/2);
imageReader.setOnImageAvailableListener(onImageAvailableListener, workThreadManager.getBackgroundHandler());

将ImageReader的surface配置到captureBuilder对象中captureBuilder.addTarget(imageReader.getSurface());(captureStillPicture()中)

当拍照完成后,会在该监听状态中回调

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
// 这里采用RxJava+RxAndroid异步通讯,避免太多回调接口

/**
* ImageReader的回调监听器
* onImageAvailable被调用的时候,已经拍照完,准备保存的操作
* 通常写入磁盘文件中。
*/
protected final ImageReader.OnImageAvailableListener onImageAvailableListener = (ImageReader reader)
-> writePictureData(reader.acquireNextImage());


public void writePictureData(Image image) {
if (camera2ResultCallBack != null) {
camera2ResultCallBack.callBack(ObservableBuilder.createWriteCaptureImage(appContext, image));
}
}

/**
* 将JPEG图片的数据,写入磁盘中
*
* @param context
* @param mImage
* @return
*/
public static Observable<String> createWriteCaptureImage(final Context context, final Image mImage) {
Observable<String> observable = Observable.create(subscriber -> {
File file = FileUtils.createPictureDiskFile(context, FileUtils.createBitmapFileName());
ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
FileOutputStream output = null;
try {
output = new FileOutputStream(file);
output.write(bytes);
} catch (IOException e) {
e.printStackTrace();
} finally {
mImage.close();
if (null != output) {
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
subscriber.onNext(file.getAbsolutePath());
});
return observable;
}

5. 录像动作。

录像是长时间的动作,录像过程中需要重复性的刷新录制界面。其余的步骤和拍照动作基本类似。

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
/**
* 开始视频录制。
*/
private void startRecordingVideo() {
try {
// 创建录制的session会话中的请求
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);

// 设置录制参数,这里省略
.........

// Start a capture session
// Once the session starts, we can update the UI and start recording
mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
mPreviewSession = cameraCaptureSession;
Log.i(TAG, " startRecordingVideo 正式开始录制 ");
updatePreview();
}
// 该接口的方法,部分省略
.............
}, workThreadManager.getBackgroundHandler());
} catch (CameraAccessException | IOException e) {
e.printStackTrace();
}
}

// 录制过程中,不断刷新录制界面
private void updatePreview() {
try {
mPreviewSession.setRepeatingRequest(mPreviewBuilder.build(), null, workThreadManager.getBackgroundHandler());
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

和拍照类似,将视频数据写入磁盘文件中,也是需要一个操作类MediaRecorder来实现的。

创建该操作类对象,设置一些参数

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
// MediaRecorder
private MediaRecorder mMediaRecorder;

/**
* 设置媒体录制器的配置参数
* 音频,视频格式,文件路径,频率,编码格式等等
*/
private void setUpMediaRecorder() throws IOException {
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mNextVideoAbsolutePath = FileUtils.createVideoDiskFile(appContext, FileUtils.createVideoFileName()).getAbsolutePath();
mMediaRecorder.setOutputFile(mNextVideoAbsolutePath);
mMediaRecorder.setVideoEncodingBitRate(10000000);
// 每秒30帧
mMediaRecorder.setVideoFrameRate(30);
mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
switch (mSensorOrientation) {
case SENSOR_ORIENTATION_DEFAULT_DEGREES:
mMediaRecorder.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation));
break;
case SENSOR_ORIENTATION_INVERSE_DEGREES:
mMediaRecorder.setOrientationHint(ORIENTATIONS.get(rotation));
break;
default:
break;
}
mMediaRecorder.prepare();
}

间隔性的随着视频录制而输出数据到文件中

1
2
3
4
// 为 MediaRecorder设置Surface
Surface recorderSurface = mMediaRecorder.getSurface();
surfaces.add(recorderSurface);
mPreviewBuilder.addTarget(recorderSurface);

最后,当录制视频结束后,停止输出

1
2
3
// 停止录制
mMediaRecorder.stop();
mMediaRecorder.reset();

6. 恢复到预览界面。

完成一些列拍照或录像动作后,重新恢复到预览界面

1
2
3
4
5
6
7
8
9
10
11
/**
* 完成一些列拍照或录像动作后,释放焦点。
*/
private void unlockFocus() {
try {
//向session重新发送,预览的间隔性请求,出现预览界面。
mCaptureSession.setRepeatingRequest(mPreviewRequest, mCaptureCallback, workThreadManager.getBackgroundHandler());
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

当然,还有关闭相机操作,和与Activity生命周期绑定的操作,这里不再做介绍了。


总结

  1. Camera Intent方便快捷,不需要太多的代码量;MediaRecorder代码量稍大

  2. Camera Intent视频清晰度只有两种,一个是最不清楚,一个是最清楚;MediaRecorder视频清晰度可根据数值自动往上调

  3. Camera Intent在录制过程中,操作方便,有自己的暂停、录制、播放按钮;MediaRecorder需要自己去写暂停、录制或播放按钮

  4. Camera1 严格区分了预览和拍照两个流程,而 Camera2 则把这两个流程都抽象成了 Capture 行为,只不过一个是一次性的 Capture,一个是不断重复的 Capture 而已

  5. 如同 Camera1 一样,Camera2 的一些 API 调用也会耗时,所以建议使用独立的线程执行所有的相机操作,尽量避免直接在主线程调用 Camera2 的 API,可使用 HandlerThread

  6. Camera2 所有的相机操作都可以注册相关的回调接口,然后在不同的回调方法里写业务逻辑,这可能会让代码因为不够线性而错综复杂,建议可以尝试使用子线程的阻塞方式来尽可能地保证代码的线性执行。例如在子线程阻塞等待 CaptureResult,然后继续执行后续的操作,而不是将代码拆分到到 CaptureCallback.onCaptureCompleted() 方法里

  7. 可以认为 Camera1 是 Camera2 的一个子集,也就是说 Camera1 能做的事情 Camera2 一定能做

一些其它地方别人的总结:

  1. 如果应用程序需要同时兼容 Camera1 和 Camera2,建议分开维护,因为 Camera1 蹩脚的 API 设计很可能让 Camera2 灵活的 API 无法得到充分的发挥,另外将两个设计上完全不兼容的东西搅和在一起带来的痛苦可能远大于其带来便利性,多写一些冗余的代码也许还更开心

  2. 官方说 Camera2 的性能会更好,但起码在较早期的一些机器上运行 Camera2 的性能并没有比 Camera1 好

  3. 当设备的 Supported Hardware Level 低于 FULL 的时候,建议还是使用 Camera1,因为 FULL 级别以下的 Camera2 能提供的功能几乎和 Camera1 一样,所以倒不如选择更加稳定的 Camera1


参考与鸣谢

https://www.jianshu.com/p/9a2e66916fcb