一、启动预览
1、打开摄像头
1、Android 6.0之后,谷歌要求在使用敏感权限时必现要App在流程中主动申请
而不是简单的写在AndroidManifest中声明,App中主动申请权限的代码示例如下:
if (mContext.checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
|| mContext.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED
|| mContext.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
String[] permissions = new String[] {
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE};
((FullscreenActivity) mContext).requestPermissions(permissions, PackageManager.PERMISSION_GRANTED);
return;
}
checkSelfPermission接口是判断权限是否已经被用户赋予,
requestPermissions是去申请权限,这里会在屏幕上弹出对话框,让用户选择是否赋予相应的权限。
权限被赋予后才能打开摄像头,不然会报错提示没有权限。
2、请求打开摄像头
示例代码如下:
CameraManager manager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
try {
manager.openCamera(
String.valueOf(mCurrentCameraID),//第一个参数是CameraID,String类型,指定要开启的摄像头的id
mCameraStateCallback,//第二个是一个CameraDevice.StateCallback,用来接收Camera状态
this);//第三个参数是一个Handler,用来指定CameraDevice.StateCallback回调发生在哪个线程,一般会用子线程做接收,可以为null
} catch (CameraAccessException e) {
e.printStackTrace();
}
执行以上代码成功之后,会在我们设定的mCameraStateCallback的回调中收到CameraDevice的实例,
private CameraDevice.StateCallback mCameraStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {//打开成功,获得CameraDevice实例
Log.d(TAG, "onOpened");
mCurrentCameraDevice = cameraDevice;
mCurrentCameraState = CAMERA_STATE_OPENED;
startPreview();
}
@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {//对应的CameraDevice已经关闭
Log.d(TAG, "onDisconnected");
}
@Override
public void onError(@NonNull CameraDevice cameraDevice, int i) {//对应的CameraDevice发生错误
Log.d(TAG, "onError " + i);
cameraDevice.close();
mCurrentCameraDevice = null;
mCurrentCameraState = CAMERA_STATE_CLOSED;
}
};
打开成功之后获取到CameraDevice实例,就可以用CameraDevice去启动预览了。
但是启动预览之前我们要准备预览使用的Surface。在Android Camera2架构中,底层的数据是ByteBuffer,这个buffer中包含的就是图像数据,但是是在底层的数据,上层无法直接显示,底层是通过应用提供的Surface创建对应的“流“,来实现数据的传输,具体细节这里不足描述。
2、准备Surface
Android系统已经给我们提供了足够的原生控件去实现相机预览,通常我们在App中做预览用的控件有SurfaceView、GLSurfaceView和TexutreView,他们都能直接提供Surface,并且在Surface接收到数据时将其解析为RBG图像数据并在其上绘制显示出来。
1、使用SurfaceView做预览
第一步,在布局文件中添加一个SurfaceView,例如
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
或者在布局中addView,例如
mSurfaceView = new SurfaceViewPreview(mContext);
mRootLayout.addView(mSurfaceView, 0);
第二步,给SurfaceView添加一个callback
mSurfaceView.getHolder().addCallback(SurfaceHolder.Callback);
callback中重写回调方法
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (mSurfaceListener != null) {
mSurfaceListener.onSurfaceSizeChange(width, height, holder.getSurface());
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (mSurfaceListener != null) {
mSurfaceListener.onSurfaceDestroyed(holder.getSurface());
}
}
在surfaceCreated回调发生之后,就表明我们需要的Surface已经创建出来了,已经可以用来绘制预览画面了,但是此时的Surface的尺寸不一定是我们需要的,我们需要通过
mSurfaceView.getHolder().setFixedSize(width, height);
接口来调整Surface的大小,这里的width和height是控制native的Surface的尺寸的,这个尺寸会被底层用来创建”流“时确定流的尺寸。再次强调,这里setFixedSize的尺寸是设置给底层的,与应用的显示尺寸无关,应用的预览界面显示大小是通过SurfaceView的尺寸确定的,例如
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mSurfaceView.getLayoutParams();
params.width = targetWidth;//targetWidth一般都是屏幕的宽度
params.height = targetHeight;//targetHeight通过预览宽高比算出
params.topMargin = targetMargin;
mSurfaceView.setLayoutParams(params);
这里要保证setFixedSize(width, height)中的宽高比,与params.width和params.height的宽高比相同,比如想要4:3的显示,那就要确保这两个宽高比都是4:3,比如
setFixedSize(4000, 3000)
params.width = 1080
params.height = 1440
这里要注意的是,setFixedSize(width, height)的宽高比是width/height = 4:3,而LayoutParams是params.height/params.width = 4:3,因为摄像头是横着安装的,屏幕是竖着的。
第三步,在这之后,我们就可以用mSurfaceView.getHolder().getSurface()来获取预览的Surface了。
GLSurfaceView与SurfaceView类似。
2、使用TextureView做预览
第一步,将TextureView加到布局中,与SurfaceView一样。
第二步,
3、创建拍照用的Surface
拍照用的Surface一般都用ImageReader来获取,创建和初始化ImageReader的示例
mImageReader = ImageReader.newInstance(mPhotoWidth, mPhotoHeight, ImageFormat.JPEG, 1);//创建一个ImageReader,设定照片的宽高
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {//获取照片用的回调
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireNextImage();//获取JPEG照片数据的流程
Image.Plane plane = image.getPlanes()[0];
ByteBuffer buffer = plane.getBuffer();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
buffer.rewind();
onPictureTaken(data);//保存照片
image.close();//记得用完之后一定要close这image
}
}, null);
mImageSurface = mImageReader.getSurface();//获取ImageReader的Surface
3、创建Session
等到CameraDevice和Surface都获取到之后,就可以创建Session了,示例代码
CameraDevice camera = controller.mCurrentCameraDevice;
if (camera == null) return;
ArrayList<Surface> surfaces = new ArrayList<>();
if (controller.mPreviewSurface != null) {
surfaces.add(controller.mPreviewSurface);//添加预览的Surface
}
if (controller.mImageSurface != null) {
surfaces.add(controller.mImageSurface);//添加拍照的Surface
}
if (controller.mVideoSurface != null) {
surfaces.add(controller.mVideoSurface);//添加录像的Surface
}
try {
camera.createCaptureSession(
surfaces,//所有要用到的Surface,都要在创建session的时候就注册进去,不然之后用不了这个Surface
controller.mSessionStateCallback,//Session状态的监听,类似于CameraDevice.StateCallback
this);//Hanlder,可以为null
} catch (CameraAccessException e) {
Log.d(TAG, "doCreateSession error", e);
e.printStackTrace();
}
这个方法主要是createCaptureSession这个接口,这个接口就是创建应用与底层的会话通道,接口的第一个参数是一组Surface,前面说过,底层与应用之间的数据传递是通过Surface,所以我们要将Surface注册到会话中。这里要主要的是要在创建时将所有要用到的Surface都加进去,比如这里需要拍照,就要将拍照的Surface加进去,需要启动预览,就要将预览的Surface加进去。
SessionCallback的作用与CameraStateCallback类似,用来监听Session的状态和获取Session的实例
private CameraCaptureSession.StateCallback mSessionStateCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
mCurrentSession = cameraCaptureSession;//获取到Session实例
mCurrentCameraState = CAMERA_STATE_SESSION_CONFIGURED;
}
之后与底层交互都是通过这个会话CameraCaptureSession。
4、启动预览画面
获取到CameraCaptureSession之后,我们就有了与底层沟通的途径。
可以把换个交互过程类比为网络请求,CameraCaptureSession就是我们创建起来的网络链接,CaptureRequest就是我们向服务器发送的请求体,CurrentSession.setRepeatingRequest和CurrentSession.capture等接口就是向服务器发送请求的动作。
try {
CaptureRequest.Builder previewBuilder = mCurrentCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);//创建请求体,指定请求类型为预览
if (mVideoSurface != null && mVideoSurface.isValid()) {
previewBuilder.addTarget(mVideoSurface);//添加返回数据的通道
}
previewBuilder.addTarget(mPreviewSurface);//添加返回数据的通道
Log.d(TAG, "setRepeatingRequest");
mCurrentSession.setRepeatingRequest(//连续重复的发生同一个请求,也就是不断的更新预览数据,形成动态的预览画面
previewBuilder.build(),
mFrameCallback, mCameraHandler);
mCurrentCameraState = CAMERA_STATE_PREVIEW_STARTED;
} catch (CameraAccessException e) {
e.printStackTrace();
}
至此,预览画面就动了起来。
总结一下流程:
首先要获取权限,已经获取之后就可以忽略了。
然后要打开相机和准备Surface,这两步在一般情况下不分先后,因为创建session时,我们需要这两个都已经准备完毕。
再然后是创建session,创建session的方法有好几种,我们这里一般使用createCaptureSession接口,还有其他接口以后再介绍。
最后使用CameraCaptureSession启动预览setRepeatingRequest。
二、拍照
拍照的动作很简单,但是拍照的前提是已经获取到了CameraCaptureSession,而且在创建session的时候注册了ImageReader的Surface,
try {
CaptureRequest.Builder builder = mCurrentCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);//创建请求实体,设定类型为拍照
builder.set(CaptureRequest.JPEG_ORIENTATION, 90);//设置一下照片的旋转,不然拍出的照片可能不是正的
builder.addTarget(mPreviewSurface);//添加预览数据的通道
builder.addTarget(mImageSurface);//添加照片数据的通道
mCurrentSession.capture(//通过CameraCaptureSession下发单次请求
builder.build(), //添加了通道和设定了参数的请求实体
mFrameCallback,
this);
} catch (CameraAccessException e) {
e.printStackTrace();
}
这就是一次拍照请求的下发,跟启动预览的请求下发类似,区别在于请求的类型换成了CameraDevice.TEMPLATE_STILL_CAPTURE,然后要将ImageReader的Surface添加到请求中,其次下发命令使用的是capture接口,这个接口是只下发一次请求,setRepeatingRequest是连续重复下发相同请求。拍照我们只需要点一次拍一张,所以使用capture下发单次请求。
拍照完成之后就会在我们之前就创建好的ImageReader中收到照片,就是这里的onImageAvailable回调,我们只需要将数据按照固定的格式流程解析出来,保存就行了。
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireNextImage();
Image.Plane plane = image.getPlanes()[0];
ByteBuffer buffer = plane.getBuffer();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
buffer.rewind();
onPictureTaken(data);
image.close();
}
}, null);
这里要注意的还是,在创建session时,必须要将ImageReader的Surface添加到那组Surfaces中,不然这里无法使用拍照。
三、录像
实现录像可以使用MediaRecorder或者MediaCodec。MediaRecorder是将音视频文件的编码、压缩、写入文件都交给底层来完成;使用MediaCodec录像是需要结合MediaMuxer在应用层自己控制文件编码、压缩、写入,流程较为复杂。
这里只介绍简单常用的MediaRecorder实现。
1、将Surface添加到Session中
首先,还是要保证录像用的Surface在创建Session是注册到Session中,
ArrayList<Surface> surfaces = new ArrayList<>();
if (controller.mPreviewSurface != null) {
surfaces.add(controller.mPreviewSurface);
}
if (controller.mImageSurface != null) {
surfaces.add(controller.mImageSurface);
}
if (controller.mVideoSurface != null) {
surfaces.add(controller.mVideoSurface);//这里,要录像就要确保video的surface添加进来
}
try {
camera.createCaptureSession(surfaces, controller.mSessionStateCallback, this);
} catch (CameraAccessException e) {
Log.d(TAG, "doCreateSession error", e);
e.printStackTrace();
}
获取录像的Surface的方式有两种。
1、创建可复用Surface
我们可以通过MediaCodec.createPersistentInputSurface()接口创建一个可复用的Surface,
private void initRecorder() {
mVideoSurface = MediaCodec.createPersistentInputSurface();//创建可复用Surface
mMediaRecorder = new MediaRecorder();//创建MediaRecorder
mVideoFileName = mStorageManager.getVideoFileName();
mMediaRecorder.setOutputFile(mStorageManager.getCameraDir() + mVideoFileName);//设置视频文件输出路径
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);//设置视频源为Surface
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);//设置音频来源
int cameraID = SettingManager.getInstance().getInt(SettingManager.KEY_CAMERA_ID);
int quality = SettingManager.getInstance().getInt(SettingManager.KEY_VIDEO_QUALITY);
CamcorderProfile profile = CamcorderProfile.get(cameraID, quality);//获取一个Recorder配置
mMediaRecorder.setProfile(profile);//将配置设置到MediaRecorder
mMediaRecorder.setOrientationHint(90);//设置MediaRecorder视频旋转
if (SettingManager.getInstance().getInt(SettingManager.KEY_CAMERA_ID) == 1) {
mMediaRecorder.setOrientationHint(270);
}
mMediaRecorder.setInputSurface(mVideoSurface);//将视频的Surface交给MediaRecorder,作为数据输入
try {
mMediaRecorder.prepare();//MediaRecorder初始化,这里会将录像的可复用Surface进行设定,在这之后Surface就确定了buffer的宽高和类型
} catch (IOException e) {
e.printStackTrace();
}
mCameraController.setVideoSurface(mVideoSurface);//交给Camera去createCaptureSession
}
2、使用MediaRecorder自带的Surface
跟上面的流程类似
private void initRecorder() {
//mVideoSurface = MediaCodec.createPersistentInputSurface();不创建可重用Surface
mMediaRecorder = new MediaRecorder();
mVideoFileName = mStorageManager.getVideoFileName();
mMediaRecorder.setOutputFile(mStorageManager.getCameraDir() + mVideoFileName);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
int cameraID = SettingManager.getInstance().getInt(SettingManager.KEY_CAMERA_ID);
int quality = SettingManager.getInstance().getInt(SettingManager.KEY_VIDEO_QUALITY);
CamcorderProfile profile = CamcorderProfile.get(cameraID, quality);
mMediaRecorder.setProfile(profile);
mMediaRecorder.setOrientationHint(90);
if (SettingManager.getInstance().getInt(SettingManager.KEY_CAMERA_ID) == 1) {
mMediaRecorder.setOrientationHint(270);
}
//mMediaRecorder.setInputSurface(mVideoSurface);//不用给MediaRecorder设置输入Surface
try {
mMediaRecorder.prepare();
} catch (IOException e) {
e.printStackTrace();
}
mVideoSurface = mMediaRecorder.getSurface();//不同在这里,不需要给setInputSurface,而是使用getSurface获取MediaRecorder中已经格式化好的Surface
mCameraController.setVideoSurface(mVideoSurface);
}
两种Surface的不同
一般推荐使用可重用Surface的方式,因为在创建和初始化MediaRecorder时,必须要设置输出文件路径,而这个参数在设置后,使用此MediaRecorder实例的过程中无法改变,所以一般会需要我们在录制完一个视频后重写将MediaRecorder进行reset处理并再进行一次init来重新设置输出路径,此时如果用MediaRecorder内部自带的Surface,reset时会将Surface资源释放无法继续使用,就会造成必须重新创建session,重新获取MediaRecorder的Surface,结果就是预览会卡顿一下,在性能较差的机器上这个卡顿会非常明显。而使用可复用Surface就可以避免这个问题,可重用Surface不归属于MediaRecorder,MediaRecorder只是将其进行了格式化,MediaRecorder的reset不会造成可复用Surface被释放。
所以一般情况下我们会用以下流程创建和初始化MediaRecorder和可重用Surface
private void initRecorder() {
if (mVideoSurface == null) {//如果为null才新创建
mVideoSurface = MediaCodec.createPersistentInputSurface();
}
if (mMediaRecorder != null) {//如果为null才新创建
mMediaRecorder.reset();//不为null就将其reset
} else {
mMediaRecorder = new MediaRecorder();
}
if (!mRecordingStarted) {//由于在创建session前我们就要将Surface初始化,但是此时并没有真正开始录像,所以我们只需要创建一个临时路径做初始化就可以,因为之后要删掉临时文件,所以一般会将这临时文件设置为固定的,方便我们在任何时刻做删除,都可以
mVideoFileName = mStorageManager.getTempVideoPath();
mMediaRecorder.setOutputFile(mVideoFileName);
} else {//真正开始录像时才创建正式的路径
mVideoFileName = mStorageManager.getVideoFileName();
mMediaRecorder.setOutputFile(mStorageManager.getCameraDir() + mVideoFileName);
}
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
int cameraID = SettingManager.getInstance().getInt(SettingManager.KEY_CAMERA_ID);
int quality = SettingManager.getInstance().getInt(SettingManager.KEY_VIDEO_QUALITY);
CamcorderProfile profile = CamcorderProfile.get(cameraID, quality);
mMediaRecorder.setProfile(profile);
mMediaRecorder.setOrientationHint(90);
if (SettingManager.getInstance().getInt(SettingManager.KEY_CAMERA_ID) == 1) {
mMediaRecorder.setOrientationHint(270);
}
mMediaRecorder.setInputSurface(mVideoSurface);
try {
mMediaRecorder.prepare();
} catch (IOException e) {
e.printStackTrace();
}
mCameraController.setVideoSurface(mVideoSurface);
}
所以,一般情况下我们会经历
mRecordingStarted = false (起始状态)->
initRecorder (创建可重用Surface,使用一个临时文件路径)->
createSession(带有可重用的Surface) ->
onShutterClock (用户点击快门,真正开始录像)->
mRecordingStarted = true ->
initRecorder(创建正式的文件路径)->
startRecorder(开始写入文件)
这里的第一次initRecorder就是为了创建session提供格式化好的Surface,其创建的临时文件要被删除,第二次initRecorder创建正式的录像文件路径,并启动录像。
2、启动录像
在MediaRecorder初始化完成,录像的Surface添加并创建Session之后,就可以用得到的Session做录像了,示例代码如下
private void doStartRecord() {
Log.d(TAG, "doStartRecord");
CameraController controller = mController.get();
try {
CaptureRequest.Builder builder = controller.mCurrentCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);//创建录像类型的请求体
builder.addTarget(controller.mPreviewSurface);//添加预览的Surface
builder.addTarget(controller.mVideoSurface);//添加录像的Surfaec
controller.mCurrentSession.setRepeatingRequest(//连续重复下发请求,使画面连续变化
builder.build(), controller.mFrameCallback, this);
} catch (CameraAccessException e) {
e.printStackTrace();
}
mMediaRecorder.start();//将MediaRecorder开启,底层会开始编码并将数据写入到之前设定的路径中
}
至此,录像就开始了,要停止录像就需要执行**mMediaRecorder.stop()**停止文件写入,并将文件信息写入到数据库中。