Camera開發系列之二 相機資料回撥處理

靚仔凌霄發表於2018-06-18

章節

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

相關文章