1. 程式人生 > >Android音視訊-視訊採集(Camera預覽)

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不是顯示和最佳預覽尺寸一樣的檢視大小呢?這裡還沒搞清楚。