Camera开发系列之二 相机数据回调处理

5,257 阅读8分钟

章节

Camera开发系列之一-显示摄像头实时画面

Camera开发系列之二-相机预览数据回调

Camera开发系列之三-相机数据硬编码为h264

Camera开发系列之四-使用MediaMuxer封装编码后的音视频到mp4容器

Camera开发系列之五-使用MediaExtractor制作一个简易播放器

Camera开发系列之六-使用mina框架实现视频推流

Camera开发系列之七-使用GLSurfaceviw绘制Camera预览画面

本篇文章主要实现的功能如下:

  1. 拍照功能实现
  2. 录像功能实现
  3. 实时视频流回调

先上效果图:

后置摄像头

拍照回调函数

相机拍照功能的实现主要依赖这两个方法:

void takePicture(Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback postview, Camera.PictureCallback jpeg)

void takePicture(Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg)

第二个方法等同于void takePicture( shutter, raw, null, jpeg),所以这里直接看第一个方法,先看第一个参数的官方文档解释:

Called as near as possible to the moment when a photo is captured from the sensor. This is a good opportunity to play a shutter sound or give other feedback of camera operation. This may be some time after the photo was triggered, but some time before the actual data is available.

大概就是可以在这个方法里进行拍照前的一些设置,比如播放快门声音之类的。

第二个参数的官方文档解释:

the callback for image capture moment, or null

在原始图像数据可用时触发,这里的原始数据是指未经处理的yuv数据,如果需要自己编码图片,可以使用该回调获取数据。

第三个参数的官方文档解释:

callback with postview image data, may be null

postview图像数据的回调,不是所有硬件都支持这个,可能为空。

第四个参数的官方文档解释:

The jpeg callback occurs when the compressed image is available

JPEG图像数据的回调,经过android底层处理好的数据,可能为空。

拍照并保存为jpeg

这里我就直接拿到jpeg数据做存储操作了,这里需要注意的是照片数据可能是旋转过的,需要将照片旋转回来。

拍照并保存为jpeg格式的文件:

mCamera.takePicture(null, null, new Camera.PictureCallback() {
            @Override
            public void onPictureTaken(byte[] data, Camera camera) {
                File file = null;
                try {
                    if (mPicListener != null) {
                        Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0,
                                data.length);
                        //因为照片有可能是旋转的,这里要做一下处理
                        Camera.CameraInfo info = new Camera.CameraInfo();
                        Camera.getCameraInfo(mCameraId, info);
                        Bitmap realBmp = FileUtil.rotaingBitmap(info.orientation, bitmap);

                        file = FileUtil.saveFile(realBmp, mFileName, mFileDir + "/");
                        mPicListener.onPictureTaken("", file);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    LogUtil.i("错误:  " + e.getMessage());
                    if (mPicListener != null) {
                        mPicListener.onPictureTaken("保存失败:" + e.getMessage(), file);
                    }
                }
                mCamera.startPreview(); //拍照之后需要重新设置预览画面
            }
        });

按角度旋转bitmap:

public static Bitmap rotaingBitmap(int angle, Bitmap bitmap) {
        Bitmap returnBm = null;
        // 根据旋转角度,生成旋转矩阵
        Matrix matrix = new Matrix();
        matrix.postRotate(angle);
        try {
            // 将原始图片按照旋转矩阵进行旋转,并得到新的图片
            returnBm = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
        } catch (OutOfMemoryError e) {
            e.printStackTrace();
        }
        if (returnBm == null) {
            returnBm = bitmap;
        }
        if (bitmap != returnBm && !bitmap.isRecycled()) {
            bitmap.recycle();
            bitmap = null;
        }
        return returnBm;
    }

嗯...好像是那么回事儿了,赶紧拍张照压压惊:

后置摄像头

恩,很完美,看看前置的摄像头拍的效果如何:

前置摄像头

还是很完美,有人就会说我在这儿胡扯了,你这拍出来的效果根本就不对好伐!没看见文字都不一样吗??

30米的大刀

兄dei别激动,先放下你手里的刀,听我慢慢跟你解释清楚,解释不清楚你再动手也不迟:

一般前置摄像头有270度的旋转,而且做了镜像翻转。镜像翻转指的是将屏幕进行水平的翻转,达到所有内容显示都会反向的效果,就像是在镜子中看到的界面一样。如果不想要这样的效果,可以拿到拍照的原始数据进行旋转270度和镜像翻转:

private byte[] rotateYUVDegree270AndMirror(byte[] data, int imageWidth, int imageHeight) {
        byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
        // Rotate and mirror the Y luma
        int i = 0;
        int maxY = 0;
        for (int x = imageWidth - 1; x >= 0; x--) {
            maxY = imageWidth * (imageHeight - 1) + x * 2;
            for (int y = 0; y < imageHeight; y++) {
                yuv[i] = data[maxY - (y * imageWidth + x)];
                i++;
            }
        }
        // Rotate and mirror the U and V color components
        int uvSize = imageWidth * imageHeight;
        i = uvSize;
        int maxUV = 0;
        for (int x = imageWidth - 1; x > 0; x = x - 2) {
            maxUV = imageWidth * (imageHeight / 2 - 1) + x * 2 + uvSize;
            for (int y = 0; y < imageHeight / 2; y++) {
                yuv[i] = data[maxUV - 2 - (y * imageWidth + x - 1)];
                i++;
                yuv[i] = data[maxUV - (y * imageWidth + x)];
                i++;
            }
        }
        return yuv;
    }

录像功能实现

首先不要忘记添加录音权限:

<uses-permission android:name="android.permission.RECORD_AUDIO"/>

这里录像功能的实现我依赖于android自带的MediaRecorder类,MediaRecorder是Android系统自带的一种非常强大的音频录制的控件,可以录制声音,也可以通过调用Camera达到录制视频的效果。MediaRecorder包含了Audio和video的记录功能,在Android的系统里,Music和Video两个应用程序都是调用MediaRecorder实现的。

本篇文章主要讲camer,对于的一些具体实现和方法就不在这儿具体赘述,有想了解的同学可以看这篇文章:Android系统的录音功能MediaRecorder,我这里就直接上代码了:

public boolean initRecorder(String filePath, SurfaceHolder holder) {

        if (!mInitCameraResult) {
            LogUtil.i("相机未初始化成功");
            return false;
        }
        try {
            // TODO init button
            //mCamera.stopPreview();
            mediaRecorder = new MediaRecorder();
            mCamera.unlock();
            mediaRecorder.setCamera(mCamera);
            if (mCameraId == 1) {
                mediaRecorder.setOrientationHint(270);
            } else {
                mediaRecorder.setOrientationHint(90);
            }

            // 这两项需要放在setOutputFormat之前,设置音频和视频的来源
            mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);//摄录像机
            mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);//相机

            // 设置录制完成后视频的封装格式THREE_GPP为3gp.MPEG_4为mp4
            mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
            //这两项需要放在setOutputFormat之后  设置编码器
            mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
            // 设置录制的视频编码h263 h264
            mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
            // 设置视频录制的分辨率。必须放在设置编码和格式的后面,否则报错
            mediaRecorder.setVideoSize(mWidth, mHeight);
            // 设置视频的比特率 (清晰度)
            mediaRecorder.setVideoEncodingBitRate(3 * 1024 * 1024);
            // 设置录制的视频帧率。必须放在设置编码和格式的后面,否则报错
            /*if (defaultVideoFrameRate != -1) {
                mediaRecorder.setVideoFrameRate(defaultVideoFrameRate);
            }*/
            // 设置视频文件输出的路径 .mp4
            mediaRecorder.setOutputFile(filePath);
            mediaRecorder.setMaxDuration(30000);
            mediaRecorder.setPreviewDisplay(holder.getSurface());
            mediaRecorder.prepare();
            mediaRecorder.start();  //开始
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

这里要注意的一个地方是上面备注提到的顺序!顺序!顺序!重要的事儿说三遍,顺序千万不能乱!如果顺序不对,可能会出现无法调用start()方法或者调用start()后闪退的情况。

相机实时视频流回调

和拍照函数类似的,主要看下面两个方法:

setPreviewCallback(Camera.PreviewCallback cb)
setPreviewCallbackWithBuffer(Camera.PreviewCallback cb)

看看官方文档的解释:

对第一个方法:

Installs a callback to be invoked for every preview frame in addition to displaying them on the screen. The callback will be repeatedly called for as long as preview is active. This method can be called at any time, even while preview is live. Any other preview callbacks are overridden.

除了在屏幕上显示预览之外,还增加一个回调函数,在每一帧出现时调用。只要预览处于活动状态,就会重复调用回调。这种方法可以随时调用,保证预览是实时的。

第二个方法:

Installs a callback to be invoked for every preview frame, using buffers supplied with addCallbackBuffer(byte[]), in addition to displaying them on the screen.

在摄像头开启时增加一个回调函数,在每一帧出现时调用.通过addCallbackBuffer(byte[])使用一个缓存容器来显示这些数据.

这里推荐使用第二个方法,第二个方法其实就是通过内存复用来提高预览的效率,但是如果没有调用这个方法addCallbackBuffer(byte[]),帧回调函数就不会被调用,也就是说在每一次回调函数调用后都必须调用addCallbackBuffer(byte[])。(所以可以直接在onPreviewFrame中调用addCallbackBuffer(byte[]),即camera.addCallbackBuffer(data);),复用这个原来的内存地址即可。是不是听懵了?没关系,直接看代码更容易理解:

//1.设置回调:系统相机某些核心部分不走JVM,进行特殊优化,所以效率很高
        mCamera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {
            @Override
            public void onPreviewFrame(byte[] datas, Camera camera) {
                //回收缓存处理
                camera.addCallbackBuffer(datas);
                
            }
        });
        //2.增加缓冲区buffer: 这里指定的是yuv420sp格式
        mCamera.addCallbackBuffer(new byte[((width * height) *
                ImageFormat.getBitsPerPixel(ImageFormat.NV21)) / 8]);

onPreviewFrame这个回调函数是在Camera.open(int)从中调用的事件线程上调用的,它的第一个参数byte[] datas就是我们需要的实时视频流数据。注意:如果Camera.Parameters.setPreviewFormat(int) 从未被调用,则datas数据默认为YCbCr_420_SP(NV21)格式。

视频流旋转角度

现在得到了相机的实时视频流,就可以进行编码封装格式保存了,不过需要注意的还是旋转角度问题,这里要根据之前的旋转角度将视频流数据进行相应的旋转镜像操作,如果是前置摄像头,前面拍照已经给出解决方案,如果是后置摄像头,则可能需要旋转90度:

private byte[] rotateYUVDegree90(byte[] data, int imageWidth, int imageHeight) {
        byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
        // Rotate the Y luma
        int i = 0;
        for (int x = 0; x < imageWidth; x++) {
            for (int y = imageHeight - 1; y >= 0; y--) {
                yuv[i] = data[y * imageWidth + x];
                i++;
            }
        }
        // Rotate the U and V color components
        i = imageWidth * imageHeight * 3 / 2 - 1;
        for (int x = imageWidth - 1; x > 0; x = x - 2) {
            for (int y = 0; y < imageHeight / 2; y++) {
                yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x];
                i--;
                yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + (x - 1)];
                i--;
            }
        }
        return yuv;
    }

关于相机开发,最坑的地方还是旋转角度的问题,不同手机可能有不同的旋转角度。鉴于本章篇幅有限,下篇再介绍如何利用Android自带的编码类Mediacodec硬编码yuv数据为h264。博主刚开始写博客,文字表达能力不是很好,如果有表述不清或者错误的地方,欢迎大家指出==

参考文章:

分享几个Android摄像头采集的YUV数据旋转与镜像翻转的方法

Android系统自带的MediaRecorder结合Camera实现视频录制及播放功能

项目地址:camera开发从入门到入土 欢迎start和fork