Android音视频-视频采集(Camera预览)

Camera的使用我们直接根据官网介绍的使用流程,然后细入每个环节的内容,完全掌握Camera的使用。
我们最终的Demo在最后贴上,最终的Demo显示效果如下:



创建Camera应用

我们快速的来显示一个相机预览的代码

  • 声明相机权限和相机特征权限
<uses-permission android:name="android.permission.CAMERA"/>
    <uses-feature android:name="android.hardware.camera" />
  • 初始化创建Camera实例对象
public Camera getCameraInstance(){
        Camera c = null;
        try {
            c = Camera.open(); // attempt to get a Camera instance
        } catch (Exception e){
            e.printStackTrace();
            // Camera is not available (in use or does not exist)
        }
        return c; // returns null if camera is unavailable
    }
  • 继承SurfaceView创建预览的View并且传入上面创建的Camera对象
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
    private static final String TAG = "CameraPreview";
    private SurfaceHolder mHolder;
    private Camera mCamera;

    public CameraPreview(Context context, Camera camera) {
        super(context);
        mCamera = camera;

        mHolder = getHolder();
        mHolder.addCallback(this);
        mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }

    public void surfaceCreated(SurfaceHolder holder) {
        try {
            mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
        } catch (IOException e) {
            Log.d(TAG, "Error setting camera preview: " + e.getMessage());
        }
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        // empty. Take care of releasing the Camera preview in your activity.
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        if (mHolder.getSurface() == null) {
            // preview surface does not exist
            return;
        }
        try {
            mCamera.stopPreview();
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            mCamera.setPreviewDisplay(mHolder);
            mCamera.startPreview();

        } catch (Exception e) {
            Log.d(TAG, "Error starting camera preview: " + e.getMessage());
        }
    }
}
  • 在Activity中结合预览View和Camera对象
private void initCamera() {
        // Create an instance of Camera
        mCamera = getCameraInstance();
        // Create our Preview view and set it as the content of our activity.
        mPreview = new CameraPreview(this, mCamera);
        FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
        preview.addView(mPreview);
    }

上面的代码非常少,就创建了一个最简单的相机预览功能。
我们下面要细化上面的步骤,了解Camera的更多内容并且实现拍照和录像功能。

初始化相机设置参数

我们上面实现了一个简单的相机,没有进行过多的设置相机的参数。下面了解几个重要的参数。

Camera预览数据尺寸

先看一下我们上面最原始的Demo的预览图片




我给SurfaceView设置了一个固定的大小,这里看预览的时候比率看上去没有有点怪。我们可以优化这个预览的尺寸大小。

首先通过API可以查看并且设置Camera支持的预览尺寸。

Camera.Parameters parameters = mCamera.getParameters();

            //查看支持的预览尺寸
            List<Camera.Size> sizeList = parameters.getSupportedPictureSizes();
            if(sizeList.size() > 1){
                Iterator<Camera.Size> iterator = sizeList.iterator();
                while (iterator.hasNext()){
                    Camera.Size size = iterator.next();
                    Log.d(TAG, "initCamera: support size:width=="+size.width+",height=="+size.height);
                }
            }
            //设置预览尺寸
            //parameters.setPreviewSize(640,480);

设置一个预览的View大小和Camera预览尺寸最优的尺寸大小

Camera.Parameters parameters = mCamera.getParameters();
            Log.d(TAG, "surfaceChanged: surface width=="+w+",height=="+h);
            Camera.Size bestSize = getBestCameraResolution(parameters,w,h);
            parameters.setPreviewSize(bestSize.width,bestSize.height);
            Log.d(TAG, "surfaceChanged: best size width=="
                    +bestSize.width+",best size height=="+bestSize.height);

private Camera.Size getBestCameraResolution(Camera.Parameters parameters, int width, int height) {
        float tmp = 0f;
        float mindiff = 100f;
        float x_d_y = (float) width / (float) height;
        Camera.Size best = null;
        //查询支持的预览尺寸大小集合
        List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
        for (Camera.Size s : supportedPreviewSizes) {
            tmp = Math.abs(((float) s.height / (float) s.width) - x_d_y);
            if (tmp < mindiff) {
                mindiff = tmp;
                best = s;
            }
        }
        return best;
    }

如果我们设置了相机的setPreviewCallback方法,这个方法结合下面的详细了解,我们可以打印出预览的尺寸大小,就是我们上面设置的大小,打印日志如下:


这里写图片描述

上面的那个getBestCameraResolution的方法的算法是在网上找的一个,它就是找到一个Camera支持的预览尺寸大小和实际View的物理尺寸直接最相近的一个尺寸设置上去。

Camera预览方向

API方法,使用Camera的setDisplayOrientation方法,不要搞错了用Camera.Parameters 的setRotation方法(友情提醒。。。)

首先我们通过
上面最开始的预览图片我们应该注意到了,它的方向是逆时针转了90度的,这里面的具体原理我们得了解一下。
这里从这里找到的答案:查看

我们总结一下。
Camera的图像数据来源于硬件的图像传感器(Image Sensor),这到底是个啥要了解的时候Google查一下。这个Sensor有一个默认的显示图片方向坐标来显示,为手机横屏放置的左上角。因为我们的应用是竖屏来显示的,这就导致了我们眼睛看到的实体对象和Camera渲染出来的实际图像不正确了,因为实际预览渲染的图片为固定的横屏左上角为原点来渲染。

当我们随意旋转手机屏幕时,系统底层根据屏幕方向和ImageSensor采集的数据进行了旋转。所以我们可以看到预览数据和我们实际看到的物理世界的数据一致的情况。

所以我们Activity竖屏的时候默认的预览角度为0,预览的图像来源相对于我们的Activity方向逆时针转了90度,我们调用设置预览角度顺时针旋转90度来达到预览数据和物理世界方向相同。当我们Activity为横屏的时候,预览生成的图片和我们的物理世界看到的图像方向一致,不要设置。

拍照生成的图片的方向和ImageSensor的采集的图片方向一致。所以我们设置预览方向不会影响到图片输出的方向的。

这里感觉有点绕,没有完全搞明白,在下一节重点了解这个方向和大小的问题

Camera摄像头采集数据格式

我们的Camera的数据是没一帧一帧的显示在我们的眼前的,通过onPreviewFrame回掉方法可以拿到每一帧的实际数据。我们知道音频图片都有编码格式,同样我们的摄像头采集的这一帧数据也有自己的编码格式。
代码获取并且设置支持数据格式

Camera.Parameters parameters = mCamera.getParameters();
            //查看支持的摄像头图片格式
            List<Integer> list = parameters.getSupportedPreviewFormats();
            for(Integer format:list){
                Log.d(TAG, "initCamera: support preview formats is "+format);
            }
            //设置摄像头采集数据的数据格式
            parameters.setPreviewFormat(ImageFormat.NV21);

查看日志打印数据格式


这里写图片描述

点击ImageFormat.NV21 查看它的值为16进制,我们打印的十进制结果转换为16进制为17->0x11,842094169->0x32315659对应支持NV21格式和YV12格式。深入了解这两种格式要一些图形学的知识,我们暂时不做深入, 参考链接要明白一点就是这两种数据格式可以和别的数据格式进行转换,便于我们对相机进行更深入的定制。

Camera摄像头选取

我们手机现在大多数都会有前置和后置摄像头。我们可以通过API来查看支持的摄像头的信息。

private void getDefaultCameraId() {
        Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
        for (int i = 0; i < Camera.getNumberOfCameras(); i++) {
            Camera.getCameraInfo(i, cameraInfo);
            Log.d(TAG, "getCameraInstance: camera facing=" + cameraInfo.facing
                    + ",camera orientation=" + cameraInfo.orientation);
            if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
                mCameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
                break;
            } else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
                mCameraId = Camera.CameraInfo.CAMERA_FACING_FRONT;
                break;
            }
        }
    }

这个方法可以获取默认为后置摄像头并且保存后置摄像头的ID。改变摄像头可以修改获取摄像头实例方法为ID来获取。

public Camera getCameraInstance() {
        Camera c = null;
        try {
            c = Camera.open(mCameraId); // attempt to get a Camera instance
        } catch (Exception e) {
            e.printStackTrace();
            // Camera is not available (in use or does not exist)
        }
        return c; // returns null if camera is unavailable
    }

切换摄像头实现
切换摄像头把之前的摄像头destroy掉,然后重新调用我们init方法,通过SurfaceHolder重新绑定预览的数据就可以了。最后的Demo里面有详细代码。里面的几个方法就不贴了。

public void switchCamera() {
        if (!checkHaveCameraHardWare(1 - mCameraId)) {
            String cameraId = ((1 - mCameraId) == Camera.CameraInfo.CAMERA_FACING_FRONT) ? "前置" : "后置";
            Toast.makeText(mContext, "没有" + cameraId + "摄像头", Toast.LENGTH_SHORT).show();
            return;
        }
        mCameraId = 1 - mCameraId;
        destroyCamera();
        initCamera(mSurfaceViewWidth, mSurfaceViewHeight);
    }

添加Camera新功能

我们上面把摄像头的预览终于整了一遍,并且对其中的API熟悉了一番,但是只有预览的效果,我们下面要让它可以拍照,录制视频,添加滤镜等预览效果

拍照

开始拍照

使用Camera API来实现拍照很简单,调用Camera的takePicture方法就好了,我们看API代码的参数以及解释

* @param shutter   the callback for image capture moment, or null
     * @param raw       the callback for raw (uncompressed) image data, or null
     * @param postview  callback with postview image data, may be null
     * @param jpeg      the callback for JPEG image data, or null
     */
    public final void takePicture(ShutterCallback shutter, PictureCallback raw,
            PictureCallback postview, PictureCallback jpeg) {

我们选择返回的数据为JPEG格式的回掉来接受,别的都可以为空。
看我们定义的Camera.PictureCallback类

private Camera.PictureCallback mPictureCallback = new Camera.PictureCallback() {

        @Override
        public void onPictureTaken(byte[] data, Camera camera) {

            File pictureFile = getOutputMediaFile();
            if (pictureFile == null) {
                Log.d(TAG, "Error creating media file, check storage permissions: ");
                return;
            }

            try {
                FileOutputStream fos = new FileOutputStream(pictureFile);
                fos.write(data);
                fos.close();
                Log.d(TAG, "onPictureTaken: save take picture image success");
            } catch (FileNotFoundException e) {
                Log.d(TAG, "File not found: " + e.getMessage());
            } catch (IOException e) {
                Log.d(TAG, "Error accessing file: " + e.getMessage());
            }
        }
    };

图片保存的路径为getOutputMediaFile: absolutePath==/storage/emulated/0/Android/data/com.lyman.video/files/Pictures/JPEG_20171215_185838_1814543993.jpg
看一张拍出来的图片


这里写图片描述

拍照输出图片处理

  • 图片方向

我们看这个图片第一反应就是它和预览的原理一样它是逆时针转了90度,因为这个是默认的ImageSensor往文件默认为横屏左上角为原点写入的图片。我们只要在初始化相机的时候调用Camera.Parameters的setRotation方法为90就OK了。
效果如下:


这里写图片描述

  • 图片尺寸
    • 未做设置图片方向物理输出图片尺寸:176*144
    • 设置了图片倒置的物理图片尺寸:144*176

这个数据怎么来的呢,有两个疑问,第一是宽高顺序我们在预览的时候设置了一个最佳的预览尺寸,从日志看到尾352*288。这和我们上面的数据一看就是生成的小了个二分之一,这个方向问题又得愁了。

也比较好分析,先看第一张未设置图片方向的,它和预览尺寸成比率缩小了二分之一。它的宽和高就是ImageSensor根据预览的比率来绘制到文件里面去的。

而我们设置了输出图片选择顺时针旋转90度,相信一下把横屏的输出图片顺时针转90度,宽高则交换了。

不管是预览的尺寸还是拍照输出的图片它们都是相对于ImageSensor输出的图片来进行尺寸改变的。

至于输出的尺寸为什么变成了预览尺寸的二分之一呢,这里我跟踪源码native_takePicture这个方法,它会回掉Camera的Handler里面的方法,这里涉及到Camera的native层源码实现,现在不去深究。留下一个todo任务。
设置输出图片尺寸
调用Camera.Parameters的setPictureSize方法来设置输出图片的尺寸。设置以后我们图片的宽高也变成了288*352.

总结:我们的日志打印的尺寸为width=352,height=288但是我们把预览尺寸和拍照图片都做了一个顺时针旋转90,我们实际看到的预览效果和输出的照片的尺寸都是288*352。

拍照自动对焦

我们上面的图片拍出来都比较模糊,一个是我们设置的输出的预览和拍照图片比较小,再是我们可以添加一个自动对焦的效果,然后再拍照,这样拍摄的照片会清晰一些。
我们使用连续对焦parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
可以简单的处理我们的demo的效果。
推荐一个关于对焦的详细分析的文章中查看

拍照人脸检测

人脸检测的接口为FaceDetectionListener,

private class MyFaceDetectionListener implements Camera.FaceDetectionListener {

        @Override
        public void onFaceDetection(Camera.Face[] faces, Camera camera) {
            if (faces.length > 0){
                Log.d("FaceDetection", "face detected: "+ faces.length +
                        " Face 1 Location X: " + faces[0].rect.centerX() +
                        "Y: " + faces[0].rect.centerY() );
            }
        }
    }

通过Camera的setFaceDetedtionListener方法来接受底层检测到脸的回掉。

mCamera.setFaceDetectionListener(new MyFaceDetectionListener());

在摄像机开始预览了之后调用开始检测方法

private void startFaceDetection(){
        // Try starting Face Detection
        Camera.Parameters params = mCamera.getParameters();

        // start face detection only *after* preview has started
        if (params.getMaxNumDetectedFaces() > 0){
            // camera supports face detection, so can start it:
            mCamera.startFaceDetection();
        }
    }

录制视频

录制视频使用我们前面了解的MediaRecorder类来做。
请求录制音频权限

配置MediaRecorder

配置步骤如下:

  • 使用Camera的unlock方法解锁Camera设置给MediaRecorder
  • 设置MediaRecorder的音视频资源
  • 设置CamcorderProfile(API 8或者以上)
  • 设置输出文件路径
  • 设置MediaRecorder的预览SurfaceView
  • 准备MediaRecorder

代码如下:

private boolean prepareVideoRecorder() {

        //mCamera = getCameraInstance();
        mMediaRecorder = new MediaRecorder();

        // Step 1: Unlock and set camera to MediaRecorder
        mCamera.unlock();
        mMediaRecorder.setCamera(mCamera);

        // Step 2: Set sources
        try{
            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
            mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
        }catch (Exception e){
            e.printStackTrace();
        }


        // Step 3: Set a CamcorderProfile (requires API Level 8 or higher)
        mMediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));

        // Step 4: Set output file
        mMediaRecorder.setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString());

        // Step 5: Set the preview output
        mMediaRecorder.setPreviewDisplay(mHolder.getSurface());

        // Step 6: Prepare configured MediaRecorder
        try {
            mMediaRecorder.prepare();
        } catch (IllegalStateException e) {
            Log.d(TAG, "IllegalStateException preparing MediaRecorder: " + e.getMessage());
            releaseMediaRecorder();
            return false;
        } catch (IOException e) {
            Log.d(TAG, "IOException preparing MediaRecorder: " + e.getMessage());
            releaseMediaRecorder();
            return false;
        }
        return true;
    }

开始停止MediaRecorder

开始停止录制视频遵循如下步骤:

  • 解锁Camera
  • 配置MediaRecorder如上
  • 开始MediaRecorder调用MediaRecorder.start()
  • 停止录制调用MediaRecorder.stop
  • 释放MediaRecorder调用MediaRecorder.release()
  • 上锁Camera调用Camera.lock()
    代码实现如下:
public int toggleVideo(){
        if (mIsRecording) {
            // stop recording and release camera
            mMediaRecorder.stop();  // stop the recording
            releaseMediaRecorder(); // release the MediaRecorder object
            mCamera.lock();         // take camera access back from MediaRecorder
            // inform the user that recording has stopped
            mIsRecording = false;
            Toast.makeText(mContext,"结束录制视频成功",Toast.LENGTH_SHORT).show();
            return 1;
        } else {
            // initialize video camera
            if (prepareVideoRecorder()) {
                // Camera is available and unlocked, MediaRecorder is prepared,
                // now you can start recording
                mMediaRecorder.start();
                // inform the user that recording has started
                mIsRecording = true;
                Toast.makeText(mContext,"开始录制视频成功",Toast.LENGTH_SHORT).show();
                return 2;
            } else {
                releaseMediaRecorder();
            }
        }
        Toast.makeText(mContext,"操作异常",Toast.LENGTH_SHORT).show();
        return 0;
    }

滤镜水印

这个的简单实现我想的就是拿到相机的每一帧的数据对当个Bitmap做处理然后绘制回去。这里就在onPreviewFrame这个每一帧的数据回掉里面拿到数据。有一个问题困扰了我,就是这个onPreviewFrame的执行的线程问题,上面的代码不做任何处理,它是在主线程里面执行,我们并不希望他在主线程里面处理我们的图片水印数据。看onPreviewFrame的方法介绍。

onPreviewFrame执行线程问题

/**
         * Called as preview frames are displayed.  This callback is invoked
         * on the event thread {@link #open(int)} was called from.
         *

这是这个方法的头部的注释,我们可以了解到这个回掉是在相机创建的事件线程里面执行的,我第一反应就是把这个获取相机的方法放到一个子线程里面就可以让onPreviewFrame里面执行就OK了洛。

没想到这里出现了一个大错误,这个子线程不能是一个简单的子线程。查看Camera的init源码的时候我们跟踪onPreviewFrame的回掉是怎么来的。看到Camera最终的初始化方法

private int cameraInitVersion(int cameraId, int halVersion) {
        mShutterCallback = null;
        mRawImageCallback = null;
        mJpegCallback = null;
        mPreviewCallback = null;
        mPostviewCallback = null;
        mUsingPreviewAllocation = false;
        mZoomListener = null;

        Looper looper;
        if ((looper = Looper.myLooper()) != null) {
            mEventHandler = new EventHandler(this, looper);
        } else if ((looper = Looper.getMainLooper()) != null) {
            mEventHandler = new EventHandler(this, looper);
        } else {
            mEventHandler = null;
        }

        return native_setup(new WeakReference<Camera>(this), cameraId, halVersion,
                ActivityThread.currentOpPackageName());
    }

这里有一个EventHandler,我们的回掉就是这个函数发过来的。仔细一看我们可以知道关键问题所在,创建一个子线程必须得有Looper的,它在构造的时候才会构造一个子线程的Handler,然后我们的onPreviewFrame才会在子线程里面处理。修改一下我们的相机实例获取方法。
为了只修改getCameraInstance我们得为它添加个异步变为同步的操作,代码如下:

public Camera getCameraInstance() {
        final Camera[] camera = new Camera[1];
        //for异步变同步
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        Log.d(TAG, "getCameraInstance: "+Thread.currentThread().getName());
        HandlerThread handlerThread = new HandlerThread("CameraThread");
        handlerThread.start();
        Handler handler = new Handler(handlerThread.getLooper());
        handler.post(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "run: "+Thread.currentThread().getName());
                camera[0] = Camera.open(mCameraId);
                countDownLatch.countDown();
            }
        });

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return camera[0];
    }

在运行代码,onPreViewFrame终于到子线程里面去执行了。

实现添加水印

我在onPreviewFrame里面执行如下代码:

@Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        if (mIsAddWaterMark) {
            try {
                Camera.Size size = camera.getParameters().getPictureSize();
                YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null);
                if (yuvImage == null) return;
                ByteArrayOutputStream stream = new ByteArrayOutputStream();
                yuvImage.compressToJpeg(new Rect(0, 0, size.width, size.height), 60, stream);
                Bitmap bitmap = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
                //图片旋转 后置旋转90度,前置旋转270度
                bitmap = BitmapUtils.rotateBitmap(bitmap, mCameraId == 0 ? 90 : 270);
                //文字水印
                bitmap = BitmapUtils.drawTextToCenter(mContext, bitmap,
                        System.currentTimeMillis() + "", 16, Color.RED);
                //Canvas canvas = mHolder.lockCanvas();
                // 获取到画布
                Log.d(TAG, "onPreviewFrame: start get canvas");
                Canvas canvas = mHolder.lockCanvas();
                Log.d(TAG, "onPreviewFrame: get canvas success");
                if (canvas == null) return;
                canvas.drawBitmap(bitmap, 0, 0, new Paint());
                Log.d(TAG, "onPreviewFrame: draw bitmap success");
                mHolder.unlockCanvasAndPost(canvas);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        Log.d(TAG, "onPreviewFrame: "+Thread.currentThread().getName());
    }

看代码就知道什么意思,拿到一帧图片做图片处理。
但是抛出如下错误:

E/SurfaceHolder: Exception locking surface
                                                              java.lang.IllegalArgumentException
                                                                  at android.view.Surface.nativeLockCanvas(Native Method)
                                                                  at android.view.Surface.lockCanvas(Surface.java:310)
                                                                  at android.view.SurfaceView$4.internalLockCanvas(SurfaceView.java:990)
                                                                  at android.view.SurfaceView$4.lockCanvas(SurfaceView.java:958)
                                                                  at com.lyman.video.camera.CameraPreview.onPreviewFrame(CameraPreview.java:173)
                                                                  at android.hardware.Camera$EventHandler.handleMessage(Camera.java:1110)
                                                                  at android.os.Handler.dispatchMessage(Handler.java:102)
                                                                  at android.os.Looper.loop(Looper.java:154)
                                                                  at android.os.HandlerThread.run(HandlerThread.java:61)

这里要注意一下这个问题,找了很久的原因,最后在这里看到了查看


这里写图片描述

总体意思就是我们的SurfaceHolder已经和Camera绑定了,它们维持一个生产消费者的相对关系,所以我们一执行获取Canvas的操作,那行代码就crash了。
所以这里我觉得有两种方式可以实现添加水印的功能。

通过FrameLayout盖在SurfaceView上面

这种方式我们都很容易想到,就是自己整一个View来添加我们要添加的东西到预览的SurfaceView上面,就不用Camera的回掉数据来纠结处理了,要保存水印或者图片遮罩的图片就截取那个View上面的内容好了。感觉这是一种很投机的方法。

另外创建一个SurfaceView来显示水印图片

修改onPreviewFrame代码:

public void onPreviewFrame(byte[] data, Camera camera) {
        //Log.d(TAG, "onPreviewFrame: is add watermark="+mIsAddWaterMark);
        if (mIsAddWaterMark) {
            Log.d(TAG, "onPreviewFrame: show water mark");
            try {
                Camera.Size size = camera.getParameters().getPreviewSize();
                YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null);
                if (yuvImage == null) return;
                ByteArrayOutputStream stream = new ByteArrayOutputStream();
                yuvImage.compressToJpeg(new Rect(0, 0, size.width, size.height), 100, stream);
                Bitmap bitmap = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
                //图片旋转 后置旋转90度,前置旋转270度
                bitmap = BitmapUtils.rotateBitmap(bitmap, mCameraId == 0 ? 90 : 270);
                //文字水印
                bitmap = BitmapUtils.drawTextToCenter(mContext, bitmap,
                        System.currentTimeMillis() + "", 16, Color.RED);
                //Canvas canvas = mHolder.lockCanvas();
                Log.d(TAG, "onPreviewFrame: bitmap width=" + bitmap.getWidth() + ",bitmap height=" + bitmap.getHeight());
                // 获取到画布
                Log.d(TAG, "onPreviewFrame: start get canvas");
                Canvas canvas = mWaterMarkPreview.getHolder().lockCanvas();
                Log.d(TAG, "onPreviewFrame: get canvas success");
                if (canvas == null) return;
                canvas.drawBitmap(bitmap, 0, 0, new Paint());
                Log.d(TAG, "onPreviewFrame: draw bitmap success");
                mWaterMarkPreview.getHolder().unlockCanvasAndPost(canvas);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

上面这个mWaterMarkPreview是一个另外的SurfaceView对象通过外部设置进来。
预览效果如下:


这里写图片描述

我们布局的时候两个SurfaceView的大小是整的一样的,但是我们看下面的图片要小一些,因为我们在第一个Surface View里面拿到的图片数据来计算的时候并不是第一个Surface View我们看到的大小,是通过计算一个最佳的效果来得到的预览大小和拍照图片大小,在第二个SurfaceView上面输出的也是计算的一个最佳的大小的图片。至于为什么第一个Surface View不是显示和最佳预览尺寸一样的视图大小呢?这里还没搞清楚。

Demo 查看

参考网站:
Camera官网
博客整体知识点参考

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值