Camera基本使用流程

一、启动预览

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()**停止文件写入,并将文件信息写入到数据库中。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是使用 Android 标准 Camera API 打开相机的基本流程: 1. 添加权限 在 AndroidManifest.xml 文件中添加相机权限: ```xml <uses-permission android:name="android.permission.CAMERA" /> ``` 2. 创建预览布局 在您的 Activity 中添加一个 SurfaceView 作为相机预览的布局容器,例如: ```xml <SurfaceView android:id="@+id/surfaceView" android:layout_width="match_parent" android:layout_height="match_parent"/> ``` 3. 创建相机实例 在您的 Activity 中创建相机实例并设置预览展示的 SurfaceView: ```java private Camera camera; private SurfaceView surfaceView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); surfaceView = (SurfaceView) findViewById(R.id.surfaceView); // 打开相机 camera = Camera.open(); // 绑定预览视图 try { camera.setPreviewDisplay(surfaceView.getHolder()); } catch (IOException e) { e.printStackTrace(); } } ``` 4. 启动预览 调用 Camera.startPreview() 方法启动相机预览: ```java @Override protected void onResume() { super.onResume(); // 启动预览 camera.startPreview(); } ``` 5. 释放相机资源 在 Activity 销毁时释放相机资源: ```java @Override protected void onDestroy() { super.onDestroy(); // 释放相机资源 camera.stopPreview(); camera.release(); camera = null; } ``` 以上就是使用 Android 标准 Camera API 打开相机的基本流程。需要注意的是,上述代码仅适用于 Android 5.0 及以下版本。在 Android 6.0 及以上版本中,您需要动态请求相机权限。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值