#.Android中Camera1使用主要流程:
开启相机使用的关键步骤:0.获取相应权限1.检查相机可用性2.遍历相机列表,打开指定方向(前置/后置)相机,获取对应实例对象3.设置预览参数4.设置预览画面偏转方向5.设置预览画面输出方式6.开启预览结束相机使用的关键步骤:1.关闭对应相机预览2.释放对应相机资源
##.一些要点
1.摄像头相关回调执行的线程
Camera.open([index])会启动相应的摄像头,并返回其实例对象。该方法在哪个线程调用,意味着摄像头将被哪个线程占用和绑定,后继的回调都会在该线程中调用。
2.可以通过设置帧回调接口,获取相机传回的帧画面,来做自己的处理。
private final int PREVIEW_BUFFER_COUNT = 3;
private byte[][] mPreviewCallbackBuffer;
private void otherApiExplain(){
//方式1:设置回调接口,每当相机有新生成的帧画面,都会自动创建缓冲区,并将画面数据回调
// 但是会频繁的创建缓冲区和GC销毁,会有较高内存消耗,并不推荐该方式
mCamera.setPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
//这里面的Bytes的数据就是NV21格式的数据,或者YUV_420_888的数据
}
});
//方式2:设置该接口,只会回调一次,可用于拍照。会自动创建缓冲区,并将画面数据回调
mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
//这里面的Bytes的数据就是NV21格式的数据,或者YUV_420_888的数据
}
});
//方式3:设置带缓冲区的回调接口。推荐的方式
// 需要自己addCallbackBuffer()把缓冲区数组传入缓冲区队列,相机画面会存储在这些缓冲区中回调回来;
// 每次回调,都会从队列取出一个缓冲区数组;当缓冲区队列中没有数组时,就不再继续回调。
// 因此,可以在每次回调时,都把返回的数据缓冲区,重新放回缓冲区队列中。
// 这样可以有效复用这些缓冲区,降低内存消耗。
if (mPreviewCallbackBuffer == null) {
// nv21 一个像素点1.5byte
mPreviewCallbackBuffer = new byte[PREVIEW_BUFFER_COUNT][DESIRED_WIDTH * DESIRED_HEIGHT * 3 / 2];
}
// 加入预先生成的指定数量预览缓冲buffer,这里是3个
for (int i = 0; i < PREVIEW_BUFFER_COUNT; i++){
mCamera.addCallbackBuffer(mPreviewCallbackBuffer[i]);
}
mCamera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
//如果这里做费时的操作的话,最好优先把缓冲区放回去,再进行处理
// 以免队列中缓冲区暂时耗尽,不再回调
}
});
}
3.设置预览画面输出的两种方式
//设置预览画面输出方式
// 也可以不设置,而是通过setPreviewCallbackWithBuffer()等方式来获取相机预览画面帧。
//方式1:输出到一个SurfaceView的Surface上,
// 这种方式新的画面帧一旦生成,就会被绘制到SurfaceView的Surface上显示出来
mCamera.setPreviewDisplay(surfaceView.getHolder());
//方式2:输出到SurfaceTexture的缓冲区上,会被转化为一个OpenGl纹理
// SurfaceTexture通过调用updateTexImage()可以用画面流中最新的画面来更新纹理的内容
// 然后开发者可以将这个纹理做自己需要的业务处理。
// 例如用OpenGl进一步处理后显示到GlSurfaceView上,或者编码到直播视频流中。
// 可通过SurfaceTexture的API:getTransformMatrix(矩阵),来获取纹理采样坐标的变换矩阵;
mCamera.setPreviewTexture(SurfaceTexture);
4.关于预览中途手机画面方向旋转后,摄像头预览画面旋转角度变化的处理
4.1 手机预览中途,也可以通过调用 mCamera .setDisplayOrientation(result) 来重新设置预览画面的偏转角度。这一步很简单,只需要重新获取当前画面的旋转角度,然后计算预览画面的应偏转角度并设置就可以了。
4.2 其实有些麻烦的是,动态监听手机画面方向发生了变化。一般容易想到的是下面方法1和方法2两种方法,但都有一定问题。自己想的方法是,添加一个每个1s执行1次的循环,每次都检查方向是够改变,再结合方法2,来监听方向改变,即下面的方法3。
###方法1,该方法不可取。
//添加手机当前旋转方向的监听器,值返回范围0~359度,"自然方向"放置时回调角度为0.
// 但是仅仅是监听手机当前的旋转角度,手机中画面的旋转角度未必与之同步。
// 例如很缓慢地把手机转动180度时,这里的返回角度一直在变,但画面方向始终未变。
//所以,该方法不可取。
OrientationEventListener mOrientationListener = new OrientationEventListener(this) {
@Override
public void onOrientationChanged(int orientation) {
ILog.d(TAG, "onOrientationChanged(), orientation=" + orientation);
}
};
mOrientationListener.enable();
###方法2:
private int mOrientation = Configuration.ORIENTATION_PORTRAIT;//记录当前画面
//1.如果希望画面方向改变时,触发该回调,需要AndroidManifest.xml中注册该Activity时
// 配置项android:configChanges=""中要添加参数值orientation
//2.即使添加了方向改变触发回调,也只有在横向、竖向改变时,会触发该回调。
// 当手机画面方向直接旋转180度,例如从一侧横屏转到另一侧横屏时,不会触发该方法
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
ILog.d(TAG, "onConfigurationChanged()");
if(newConfig == null){
return;
}
ILog.d(TAG, "onConfigurationChanged(), newConfig.orientation=" + newConfig.orientation + ", mOrientation= "+mOrientation);
if(newConfig != null && newConfig.orientation != mOrientation){
mOrientation = newConfig.orientation;
Camera1Manager.getInstance().setDisplayOrientation(this);
}
}
###.方法3:自己用的方法
private int mOrientation = Configuration.ORIENTATION_PORTRAIT;//记录当前画面方向
//1.如果希望画面方向改变时,触发该回调,需要AndroidManifest.xml中注册该Activity时
// 配置项android:configChanges=""中要添加参数值orientation
//2.即使添加了方向改变触发回调,也只有在横向、竖向改变时,会触发该回调。
// 当手机画面方向直接旋转180度,例如从一侧横屏转到另一侧横屏时,不会触发该方法
//3.针对第2点中的情况,做补充处理,添加一个变量记录当前方向,进行循环,每1s检测一次当前方向
// 3.1若方向改变了,就更改摄像头预览画面方向。这样从用户操作,到处理改变,最多延迟1s;
// 3.2若onConfigurationChanged()被触发了,则可实时处理对应逻辑。
// 同时更新当前方向,所以3.1中检测方向时一定是相同的,不会重复触发对应处理逻辑。
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
ILog.d(TAG, "onConfigurationChanged()");
if(newConfig == null){
return;
}
ILog.d(TAG, "onConfigurationChanged(), newConfig.orientation=" + newConfig.orientation + ", mOrientation= "+mOrientation);
if(newConfig != null && newConfig.orientation != mOrientation){
mRotation = DeviceUtils.getRotation(this);//更新mRotation方向
mOrientation = newConfig.orientation;
Camera1Manager.getInstance().setDisplayOrientation(this);
}
}
private int mRotation = Surface.ROTATION_0;//记录当前画面旋转角度
//检查画面方向变化
private void checkRotation(){
int rotation = DeviceUtils.getRotation(this);
if(mRotation != rotation){
ILog.d(TAG, "checkRotation(), rotation changed!");
mRotation = rotation;
Camera1Manager.getInstance().setDisplayOrientation(this);
}
mHandler.sendEmptyMessageDelayed(MSG_ROTATION_CHECK, 1000);
}
private final int MSG_ROTATION_CHECK = 1;//检查画面方向变化
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if(msg == null){
return;
}
if(msg.what == MSG_ROTATION_CHECK){
checkRotation();
}
}
##.代码示例:使用SurfaceView+自己新建的HandlerThread
//管理类
public class Camera1Manager {
private final String TAG = getClass().getSimpleName();
private static class SingleTon {
public static Camera1Manager sInstance = new Camera1Manager();
}
private Camera1Manager(){}
public static Camera1Manager getInstance(){
return SingleTon.sInstance;
}
private final int DESIRED_WIDTH = 1280;
private final int DESIRED_HEIGHT = 720;
private boolean mState = false;
private Camera mCamera;
private Camera.CameraInfo mCameraInfo;
private int mFaceType = Camera.CameraInfo.CAMERA_FACING_FRONT;
public void changeDirection(Activity activity, SurfaceView surfaceView){
if(activity == null || surfaceView == null){
return;
}
stopPreview();
startPreview(activity, surfaceView);
if(mFaceType == Camera.CameraInfo.CAMERA_FACING_FRONT){
mFaceType = Camera.CameraInfo.CAMERA_FACING_BACK;
} else {
mFaceType = Camera.CameraInfo.CAMERA_FACING_FRONT;
}
}
public void setFaceType(boolean isFront){
if(isFront == (mFaceType == Camera.CameraInfo.CAMERA_FACING_FRONT)){
return;
}
mFaceType = isFront ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK;
}
/**
* 检查摄像头可用性
* @param context
* @return
*/
private boolean checkCameraEnable(Context context){
if(context == null){
return false;
}
DevicePolicyManager dpm = (DevicePolicyManager)context.getSystemService(Context.DEVICE_POLICY_SERVICE);
if(dpm.getCameraDisabled(null)){
return false;
}
return true;
}
private boolean openCamera(){
Camera.CameraInfo info = new Camera.CameraInfo();
// Try to find a front-facing camera (e.g. for videoconferencing).
int numCameras = Camera.getNumberOfCameras();
for(int i = 0; i < numCameras; i++) {
Camera.getCameraInfo(i, info);
if (info.facing == mFaceType) {
mCamera = Camera.open(i);
mCameraInfo = info;
break;
}
}
if(mCamera == null) {
ILog.d(TAG, "No front-facing camera found; opening default");
// mCamera = Camera.open(); // opens first back-facing camera
return false;
}
return true;
}
public void setDisplayOrientation(Activity activity) {
if(activity == null || mCamera == null || mCameraInfo == null){
return;
}
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 (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (mCameraInfo.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (mCameraInfo.orientation - degrees + 360) % 360;
}
ILog.d(TAG, "mCameraInfo.orientation= " + mCameraInfo.orientation
+ ",\ndegrees= " + degrees
+ ",\nresult= " + result);
try {
mCamera.setDisplayOrientation(result);
} catch (Exception e) {
ILog.e(TAG, "setDisplayOrientation", e);
}
}
private void setCameraParams(){
if(mCamera == null || mCameraInfo == null){
return;
}
Camera.Parameters parameters = mCamera.getParameters();
//自动对焦
List<String> focusModes = parameters.getSupportedFocusModes();
if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)){
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
}
parameters.setPreviewSize(DESIRED_WIDTH, DESIRED_HEIGHT);
// Give the camera a hint that we're recording video. This can have a big
// impact on frame rate.
parameters.setRecordingHint(true);
mCamera.setParameters(parameters); // 摄像头参数设置完成
}
public void startPreview(Activity activity, SurfaceView surfaceView){
if(mState){
return;
}
if(activity == null || surfaceView == null){
return;
}
//1.检查相机可用性
if(!checkCameraEnable(activity)){
return;
}
//2.遍历相机列表,打开指定相机,获取对应实例对象
if(!openCamera()){
return;
}
//3.设置预览参数
setCameraParams();
//4.设置预览画面偏转方向
setDisplayOrientation(activity);
if(mCamera == null){
return;
}
try {
//5.设置预览画面输出方式
mCamera.setPreviewDisplay(surfaceView.getHolder());
} catch (IOException e) {
e.printStackTrace();
}
//开启预览
mCamera.startPreview();
mState = true;
}
public void stopPreview(){
if(!mState){
return;
}
mState = false;
if(mCamera == null){
return;
}
//1.关闭预览
mCamera.stopPreview();
try {
mCamera.setPreviewDisplay(null);
} catch (IOException e) {
e.printStackTrace();
}
//2.释放资源
mCamera.release();
mCamera = null;
}
}
//使用
public class TestCamera1Activity extends BaseActivity {
@Override
protected BaseActivityPresenter getPresenter() {
return null;
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test_camera1_activity);
initView();
if(!AppOpsChecker.checkOps(this, AppOpsChecker.OP_CAMERA)){
PermissionUtils.getPermissions(this, new PermissionUtils.Callback() {
@Override
public void onPermissionGranted(List<String> data) {
T.show("相机权限获取成功");
}
@Override
public void onPermissionDenied(List<String> data) {
T.show("相机权限获取失败");
}
}, Permission.Group.CAMERA);
}
// //添加手机当前旋转方向的监听器,值返回范围0~359度,"自然方向"放置时回调角度为0.
// // 但是仅仅是监听手机当前的旋转角度,手机中画面的旋转角度未必与之同步。
// // 例如缓慢把手机转动180度时,这里的返回角度一直在变,但画面方向始终未变。
// //所以,该方法不可取。
// OrientationEventListener mOrientationListener = new OrientationEventListener(this) {
// @Override
// public void onOrientationChanged(int orientation) {
// ILog.d(TAG, "onOrientationChanged(), orientation=" + orientation);
// }
// };
// mOrientationListener.enable();
mCameraThread.start();
mCameraThread.setSurfaceView(videoSurfaceView);
checkRotation();
}
private VideoSurfaceView videoSurfaceView;
private Button btnStartPreview;
private Button btnStopPreview;
private Button btnChangeDirection;
private void initView(){
videoSurfaceView = (VideoSurfaceView) findViewById(R.id.video_surface_view);
btnStartPreview = (Button) findViewById(R.id.btn_start_preview);
btnStopPreview = (Button) findViewById(R.id.btn_stop_preview);
btnChangeDirection = (Button) findViewById(R.id.btn_change_direction);
btnStartPreview.setOnClickListener(mClickListener);
btnStopPreview.setOnClickListener(mClickListener);
btnChangeDirection.setOnClickListener(mClickListener);
videoSurfaceView = (VideoSurfaceView) findViewById(R.id.video_surface_view);
}
@Override
protected void handleNoDoubleClick(View v) {
super.handleNoDoubleClick(v);
if(v == btnStartPreview){
Camera1Manager.getInstance().startPreview(this, videoSurfaceView);
} else if(v == btnStopPreview){
Camera1Manager.getInstance().stopPreview();
} else if(v == btnChangeDirection){
Camera1Manager.getInstance().changeDirection(this, videoSurfaceView);
}
}
private int mOrientation = Configuration.ORIENTATION_PORTRAIT;//记录当前画面方向
//1.如果希望画面方向改变时,触发该回调,需要AndroidManifest.xml中注册该Activity时
// 配置项android:configChanges=""中要添加参数值orientation
//2.即使添加了方向改变触发回调,也只有在横向、竖向改变时,会触发该回调。
// 当手机画面方向直接旋转180度,例如从一侧横屏转到另一侧横屏时,不会触发该方法
//3.针对第2点中的情况,做补充处理,添加一个变量记录当前方向,进行循环,每1s检测一次当前方向
// 3.1若方向改变了,就更改摄像头预览画面方向。这样从用户操作,到处理改变,最多延迟1s;
// 3.2若onConfigurationChanged()被触发了,则可实时处理对应逻辑。
// 同时更新当前方向,所以3.1中检测方向时一定是相同的,不会重复触发对应处理逻辑。
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
ILog.d(TAG, "onConfigurationChanged()");
if(newConfig == null){
return;
}
ILog.d(TAG, "onConfigurationChanged(), newConfig.orientation=" + newConfig.orientation + ", mOrientation= "+mOrientation);
if(newConfig != null && newConfig.orientation != mOrientation){
mRotation = DeviceUtils.getRotation(this);//更新mRotation方向
mOrientation = newConfig.orientation;
mCameraThread.updateDisplayOrientation();
}
}
private int mRotation = Surface.ROTATION_0;//记录当前画面旋转角度
//检查画面方向变化
private void checkRotation(){
int rotation = DeviceUtils.getRotation(this);
if(mRotation != rotation){
ILog.d(TAG, "checkRotation(), rotation changed!");
mRotation = rotation;
mCameraThread.updateDisplayOrientation();
}
mHandler.sendEmptyMessageDelayed(MSG_ROTATION_CHECK, 1000);
}
private final int MSG_ROTATION_CHECK = 1;//检查画面方向变化
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if(msg == null){
return;
}
if(msg.what == MSG_ROTATION_CHECK){
checkRotation();
}
}
//新建一个线程,用于打开相机,该线程将占用和绑定相机,后继相机操作在该线程中进行
private CameraThread mCameraThread = new CameraThread(TAG);
private class CameraThread extends HandlerThread implements IHandler {
public CameraThread(String name) {
super(name);
}
public CameraThread(String name, int priority) {
super(name, priority);
}
@Override
public void run() {
super.run();
}
private VideoSurfaceView mSurfaceView;
public void setSurfaceView(VideoSurfaceView surfaceView){
if(surfaceView == null){
return;
}
mSurfaceView = surfaceView;
mSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
ILog.d(TAG, "surfaceCreated()");
startCameraPreview();
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
ILog.d(TAG, "surfaceChanged()");
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
ILog.d(TAG, "surfaceDestroyed()");
stopCameraPreview();
}
});
}
public void startCameraPreview(){
mHandler.sendEmptyMessage(MSG_START_PREVIEW);
}
public void stopCameraPreview(){
mHandler.sendEmptyMessage(MSG_STOP_PREVIEW);
}
public void updateDisplayOrientation(){
mHandler.sendEmptyMessage(MSG_UPDATE_ROTATION);
}
private final int MSG_START_PREVIEW = 1;
private final int MSG_STOP_PREVIEW = 2;
private final int MSG_UPDATE_ROTATION = 3;
private WeakHandler<CameraThread> mHandler = new WeakHandler<>(this);
@Override
public void handleMessage(Message msg) {
if(msg == null){
return;
}
int what = msg.what;
switch (what){
case MSG_START_PREVIEW:
Camera1Manager.getInstance().startPreview(TestCamera1Activity.this, videoSurfaceView);
break;
case MSG_STOP_PREVIEW:
Camera1Manager.getInstance().stopPreview();
break;
case MSG_UPDATE_ROTATION:
Camera1Manager.getInstance().setDisplayOrientation(TestCamera1Activity.this);
break;
default:
break;
}
}
}
}
##.其它补充
1.可通过takePicture()获取图片,拍照时用此方法比较方便。
调用takePicture后预览会停止,需用重新调用startPreview才能再次开始预览。预览开始后,就可以通过Camera.takePicture()方法拍摄一张照片,返回的照片数据通过Callback接口获取。takePicture()接口可以获取三个类型的照片:第一个,ShutterCallback接口,在拍摄瞬间瞬间被回调,通常用于播放“咔嚓”这样的音效;第二个,PictureCallback接口,返回未经压缩的RAW类型照片;第三个,PictureCallback接口,返回经过压缩的JPEG类型照片;
mCamera.takePicture(new Camera.ShutterCallback() {
@Override
public void onShutter() {
}
}, new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
}
}, new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
}
});