最近想要做一个客户端往服务器推送实时画面的功能,首先可以考虑到两种思路,一种是在客户端进行视频流的推送,主要利用RTSP等流媒体协议进行传输,而另外一种是通过摄像头获取当前画面,将每一帧作为对象单独传输。
项目想要实现的功能最终目的是对实时画面的每一帧进行处理,可以考虑客户端推流到服务器,再在服务器进行帧解析的操作,但由于目前很多的流媒体推送框架在推流端或者服务端都或多或少存在限制,很少有完全开源的项目,再加上传送画面的同时需要附带部分的数据,仍然需要另外建立连接进行传输,所以暂时搁置这一方案。选择第二种思路,获取每一帧的画面,单独传输。
要想获取实时画面,我们必须通过对安卓设备上的摄像头进行调用。
从API21开始,安卓引入了android.hardware.camera2这个包,来替代原有的camera类,原有的camera类已经不再建议使用了。camera2中最重要的变化是,摄像头的调用不再是简单地进行实例化,而是用一种类似服务申请的方式来进行调用。通过CameraManager来管理摄像服务,需要通过建立CameraCaptureSession来建立一个调用摄像设备CameraDevices的会话,来实现对摄像头的调用。而CaptureRequest.Builder类用于建立实际的调用请求,具体的参数设置也可以通过这个类来实现(而不是对camera设备进行直接设置),这样做的目的是把对摄像头的控制与摄像头本身分离开来,用户可以通过不同的session根据不同的配置来使用摄像头。
我们可以结合具体的代码来分析新api中摄像头调用的过程。
首先我们想要对摄像设备进行操作,需要获得CameraManager的实例
CameraManager cameraManager; cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
我们可以调用openCamera函数打开摄像头设备
cameraManager.openCamera(cameraId, cameraCallback, mainHandler);
这里需要传入三个参数,cameraId是设备编号,cameraCallback控制摄像服务的回调,最后一个参数指定HandlerThread对象
cameraId = Integer.toString(CameraCharacteristics.LENS_FACING_FRONT); private CameraDevice.StateCallback cameraCallback = new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice camera) { Log.d("CameraCallback", "Camera Opened"); cameraDevice = camera; takePreview(); } @Override public void onDisconnected(CameraDevice cameraDevice) { Log.d("CameraCallback", "Camera Disconnected"); closeCameraDevice(); } @Override public void onError(CameraDevice cameraDevice, int i) { Log.d("CameraCallback", "Camera Error"); Toast.makeText(PusherSurface.this, "摄像头开启失败", Toast.LENGTH_SHORT).show(); } };
回调函数用于指定连接摄像头设备时不同状态的操作。在这里,我们在摄像头成功连接的时候调用 takePreview()函数开启摄像头画面的预览。
private void takePreview() { try { final CaptureRequest.Builder previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); previewRequestBuilder.addTarget(surfaceHolder.getSurface()); previewRequestBuilder.addTarget(previewReader.getSurface()); cameraDevice.createCaptureSession(Arrays.asList(surfaceHolder.getSurface(), previewReader.getSurface(), imageReader.getSurface()), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession cameraCaptureSession) { if (cameraDevice == null) return; mCameraCaptureSession = cameraCaptureSession; try { previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF); CaptureRequest previewRequest = previewRequestBuilder.build(); mCameraCaptureSession.setRepeatingRequest(previewRequest, null, childHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) { Toast.makeText(PusherSurface.this, "配置失败", Toast.LENGTH_SHORT).show(); } }, childHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }
要从摄像设备中获取图像,我们首先需要建立一个camera capture session。函数
createCaptureSession(List, CameraCaptureSession.StateCallback, Handler)的第一个参数传入了我们想要绘制的视图列表,第二个参数传入的是建立摄像会话的状态回调函数,第三个参数传入相应的handler处理器。然后,我们需要利用capturerequest来定义摄像头捕获图像时候的具体参数,比如是否开启摄像头,是否自动对焦等。最后通过CamraCaptureSession.setRepeatingRequest来开启请求。这样我们就可以从capturesession传入的list中的surface列表获得连续的图像。留意到
previewRequestBuilder.addTarget(surfaceHolder.getSurface()); previewRequestBuilder.addTarget(previewReader.getSurface());
这里除了传入xml界面布局中的surfaceHolder的surface外,还传入了一个previewReader的surface。
previewReader是一个自定义的ImageReader对象。
previewReader = ImageReader.newInstance(1080, 1920, ImageFormat.YUV_420_888, 2); previewReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader imageReader) { Image image = null; try { image = imageReader.acquireLatestImage(); Log.d("PreviewListener", "GetPreviewImage"); if (image == null) { return; } byte[] bytes = ImageUtil.imageToByteArray(image); if (pushFlag == false) uploadImg(bytes); } finally { if (image != null) { image.close(); } } } }, mainHandler);
ImageReader是一个可以让我们对绘制到surface的图像进行直接操作的类。在这里我们从摄像设备中传入了连续的预览图片,也就是我们在屏幕上看到的画面,它们的格式都是未经压缩的YUV_420_888类型的(同样的如果要操作拍摄后的图片,就要设置成jpeg格式)。我们调用imageReader.acquireLatestImage或者acquireNextImage来获取图像队列中的图片。并进行操作。在这里我利用一个函数将图像压缩后转化成byte[]格式,并调用uploadImg函数上传至服务器。这样,整个摄像头的调用到预览图像的处理也就完成了。想要实现拍照功能也是大同小异,在这里我就不一一贴出了。
欢迎更多安卓开发者一同交流。