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官網
博客整體知識點參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章