天天看點

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題

Android開發筆記——視訊錄制播放常見問題

轉載http://www.cnblogs.com/younghao/p/5089118.html

本文分享自己在視訊錄制播放過程中遇到的一些問題,主要包括:

  1. 視訊錄制流程
  2. 視訊預覽及SurfaceHolder
  3. 視訊清晰度及檔案大小
  4. 視訊檔案旋轉

一、視訊錄制流程

    以微信為例,其錄制觸發為按下(住)錄制按鈕,結束錄制的觸發條件為松開錄制按鈕或錄制時間結束,其流程大概可以用下圖來描述。

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題

1.1、開始錄制

   根據上述流程及項目的程式設計慣例,可在onCreate()定義如下函數來完成功能:

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題

     初始化過程主要包括View,Data以及Listener三部分。在初始化View時,添加攝像頭預覽,添加倒計時文本元件,設定初始狀态UI元件的可見;初始化Data時,從Intent中擷取初始資料;初始化Listener時,分别對錄制觸發按鈕,儲存/取消視訊錄制按鈕以及視訊預覽界面添加監聽。

    當系統初始化成功後,等待使用者按下錄制按鈕,是以在錄制按鈕的監聽中,需要完成以下功能:錄制,計時,更新界面元件。

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
if(isRecording) {
    mMediaRecorder.stop();
    releaseMediaRecorder();
    mCamera.lock();
    isRecording = false;
}
if(startRecordVideo()) {
    startTimeVideoRecord();
    isRecording = true;
}      
Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題

    首先判斷目前錄制狀态,如果正在錄制,則先停止錄制,釋放MediaRecorder資源,鎖定攝像頭,置位錄制狀态;然後開始視訊錄制startRecordVideo,其boolean型傳回值表征是否啟動成功,啟動成功後,開始視訊錄制計時,并且置位錄制狀态。startRecordVideo涉及MediaRecorder的配置,準備以及啟動。

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題

    翻譯成代碼如下:

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
private boolean startRecordVideo() {
    configureMediaRecorder();
    if(!prepareConfiguredMediaRecorder()) {
        return false;
    }
    mMediaRecorder.start();
    return true;
}      
Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題

1.2、結束錄制

    根據上述流程圖可知,結束錄制的觸發條件為松開錄制按鈕或計時時間到。在結束錄制方法中,需要釋放MediaRecorder,開始循環播放已錄制視訊,設定界面更新等。

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題

    翻譯成代碼如下:

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
private void stopRecordVideo() {

        releaseMediaRecorder();
        // 錄制視訊檔案處理
        if(currentRecordProgress < MIN_RECORD_TIME) {
            Toast.makeText(VideoInputActivity.this, "錄制時間太短", Toast.LENGTH_SHORT).show();
        } else {
            startVideoPlay();
            isPlaying = true;
            setUiDisplayAfterVideoRecordFinish();
        }
        currentRecordProgress = 0;
        updateProgressBar(currentRecordProgress);
        releaseTimer();
        // 狀态設定
        isRecording = false;
    }      
Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題

二、視訊預覽及SurfaceHolder

      視訊預覽采用SurfaceView,相比于普通的View,SurfaceView在一個新起的單獨線程中繪制畫面,該實作的優點是更新畫面不會阻塞UI主線程,缺點是會帶來事件同步的問題。當然,這涉及到UI事件的傳遞以及線程同步,這裡不做詳細說明,有興趣的可以參考連結:http://wugengxin.cn/download/pdf/android/PRE_andevcon_mastering-the-android-touch-system.pdf。

      在實作中,通過繼承SurfaceView元件來實作自定義預覽控件。首先,SurfaceView的getHolder()方法會傳回SurfaceHolder,需要為SurfaceHolder添加SurfaceHolder.Callback回調;其次,重寫surfaceCreated、surfaceChanged和surfaceDestroyed實作。

2.1、構造器

      構造器包含了初始化域以及添加上述回調的過程。

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
public CameraPreview(Context context, Camera camera) {
        super(context);

        mCamera = camera;
        mSupportedPreviewSizes = mCamera.getParameters().getSupportedPreviewSizes();
        mHolder = getHolder();
        mHolder.addCallback(this);
        mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }      
Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題

      這裡需要說明mSupportedPreviewSizes,由于攝像頭支援的預覽尺寸由Camera本身的參數決定,是以需要首先擷取其所支援的預覽尺寸。

2.2、預覽尺寸的設定

      從Google官方的Camera示例程式中可以看出,選擇預覽尺寸的标準是(1)攝像頭支援的預覽尺寸的寬高比與SurfaceView的寬高比的絕對內插補點小于0.1;(2)在(1)獲得的尺寸中,選取與SurfaceView的高的內插補點最小的。通過代碼對這兩個标準進行了實作,這裡貼一下官方的代碼:

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
public Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
        final double ASPECT_TOLERANCE = 0.1;
        double targetRatio = (double) w / h;
        if (sizes == null) {
            return null;
        }
        Camera.Size optimalSize = null;
        double minDiff = Double.MAX_VALUE;
        int targetHeight = h;
        for (Camera.Size size : sizes) {
            double ratio = (double) size.width / size.height;
            if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE)
                continue;
            if (Math.abs(size.height - targetHeight) < minDiff) {
                optimalSize = size;
                minDiff = Math.abs(size.height - targetHeight);
            }
        }
        if (optimalSize == null) {
            minDiff = Double.MAX_VALUE;
            for (Camera.Size size : sizes) {
                if (Math.abs(size.height - targetHeight) < minDiff) {
                    optimalSize = size;
                    minDiff = Math.abs(size.height - targetHeight);
                }
            }
        }
        return optimalSize;
    }      
Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題

      在加載預覽畫面時,需要考慮Camera支援的尺寸(getSupportedPreviewSizes)和加載預覽畫面的SurfaceView的尺寸(layout_width/layout_height),在預覽階段,兩者之間的關系直接影響清晰度及圖像拉伸。對于Camera的尺寸,由于裝置的硬體差異,不同裝置支援的尺寸存在差異,但在預設情況(orientation=landscape)下,其width>height。以HTC609d為例,Camera支援的分辨率為1280*720(16:9)……640*480(4:3)……480*320(3:2)等十多種,而其螢幕的分辨率為960*540(16:9)。是以,很容易得到以下結論:(1)當Camera預覽尺寸小于SurfaceView尺寸較多時,預覽畫面就不清晰;(2)Camera預覽尺寸寬高比與SurfaceView寬高比相差較大時,預覽畫面就會拉伸。

      上述代碼在手機設定為橫屏時并沒有問題,在設定為豎屏時,為獲得最優的預覽尺寸,需要在調用此方法前比較SurfaceView的寬高。

if (mSupportedPreviewSizes != null) {
    mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes, 
                        Math.max(width, height), Math.min(width, height));
}      

      獲得與目前SurfaceView比對的預覽尺寸後,即可通過Camera.Parameters進行設定。

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
Camera.Parameters mParams = mCamera.getParameters();
mParams.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
mCamera.setDisplayOrientation(90);

List<String> focusModes = mParams.getSupportedFocusModes();
if(focusModes.contains("continuous-video")){
    mParams.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
}
mCamera.setParameters(mParams);      
Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題

三、視訊清晰度及檔案大小

      在第一節中講到startRecordVideo,包括配置MediaRecorder,準備MediaRecorder以及啟動,其中配置MediaRecorder是視訊錄制的重點,需要了解每項配置參數的作用,根據業務場景靈活配置。這裡參考Google官方的示例給出一個可行的配置方案,然後再對其進行解釋。

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
private void configureMediaRecorder() {
        // BEGIN_INCLUDE (configure_media_recorder)
        mMediaRecorder = new MediaRecorder();

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

        // Step 2: Set sources
        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.VOICE_RECOGNITION);
        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);

        // Step 3: Set a Camera Parameters
        mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
        /* Fixed video Size: 640 * 480*/
        mMediaRecorder.setVideoSize(640, 480);
        /* Encoding bit rate: 1 * 1024 * 1024*/
        mMediaRecorder.setVideoEncodingBitRate(1 * 1024 * 1024);
        mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
        mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);

        // Step 4: Set output file
        mMediaRecorder.setMaxFileSize(maxFileSizeInBytes);
        mMediaRecorder.setOutputFile(videoFilePath);
        // END_INCLUDE (configure_media_recorder)

        // Set MediaRecorder ErrorListener
        mMediaRecorder.setOnErrorListener(this);
    }      
Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
Step 1:
  1. setCamera參數能夠使得在預覽和錄制中快速切換,避免Camera對象的重新加載。在某些Android手機自帶的照相機程式中,切換預覽與錄制中的短暫卡頓,讀者可自行體會。
  2. mMediaRecorder.setOrientationHint(90)在錄制方向為豎直(portrait)時使用,它能使視訊檔案的沿順時針方向旋轉90度,如果不設定此項,播放視訊時,畫面會發生90度的旋轉。不過這裡更重要的是,即使設定了此項,在某些播放器上,畫面依然會有90度的旋轉(比如将在手機上正常播放的視訊導入到PC中進行播放,或者嵌入H5的video标簽中),這可是為什麼呢?注意setOrientationHint的說明:Note that some video players may choose to ignore the compostion matrix in a video during playback. 那麼如何做到在所有播放器上都能以正常方向播放呢?稍等,後續專門對其進行說明。
Step 2:
  1. setAudioSource(MediaRecorder.AudioSource.VOICE_RECOGNITION),VOICE_RECOGNITION相比于MIC會根據語音識别的需要做一些調諧,當然,這需要在系統支援的情況下。
  2. setVideoSource自然是VideoSource.CAMERA,隻是在此兩項設定必須在設定編碼器之前設定,這無需說明。
Step 3:
  1. setOutputFormat需要在Step 2之後,并且在prepare()之前。這裡采用OutputFormat.MPEG_4格式。
  2. setVideoSize需要權衡的因素較多,主要包括三方面:MediaRecorder支援的錄制尺寸、視訊檔案的大小以及相容不同Android機型。這裡采用640 * 480(微信小視訊的尺寸是320*240),檔案大小在500-1000kb之間,并且市面上99%以上機型支援此錄制尺寸。
  3. setVideoEncodingBitRate與視訊的清晰度有關,設定此參數需要權衡清晰度與檔案大小的關系。太高,檔案大不易傳輸;太低,檔案清晰度低,識别率低。需要根據實際業務場景靈活調整。
  4. setVideoEncoder采用H264編碼,MPEG4、H263、H264等不同編碼的差别比較可參考http://blog.csdn.net/wcl0715/article/details/676137,實際使用中,H264的壓縮率較高,推薦使用。
  5. setAudioEncoder采用AudioEncoder.AAC,該設定主要是考慮其通用性、相容性。
Step 4:
  1. setMaxFileSize指定錄制檔案的大小限制,當然還可以限制其最大錄制時間。
  2. setOutputFile指定輸出視訊的路徑。
  3. setOnErrorListener指定錯誤監聽器。

   在完成上述配置之後,即可準備MediaRecorder,并在傳回成功後開始視訊錄制。

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
private boolean prepareConfiguredMediaRecorder() {
        // Step 5: Prepare configured MediaRecorder
        try {
            mMediaRecorder.prepare();
        } catch (Exception e) {
            releaseMediaRecorder();
            return false;
        }
        return true;
    }      
Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題

四、視訊檔案旋轉

      第三節中Step 1提到對視訊檔案的旋轉,因為某些播放器會忽略錄制視訊時的配置參數,是以可嘗試通過第三方庫對視訊檔案進行旋轉,例如:OpenCV,fastCV等,在Camera對象的Camera.PreviewCallback中截取每幀資料byte[] data,然後對其進行處理,然後輸出。該方法需要考慮處理方法的高效性,在程式設計時一般采用NDK,在C++中完成關鍵的處理,這裡貼出fastCV中該處理方法的邏輯。

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
public void onPreviewFrame( byte[] data, Camera c ) {
         // Increment FPS counter for camera.
         util.cameraFrameTick();

         // Perform processing on the camera preview data.
         update( data, mDesiredWidth, mDesiredHeight );

         // Simple IIR filter on time.
         mProcessTime = util.getFastCVProcessTime();

         if( c != null )
         {
            // with buffer requires addbuffer each callback frame.
            c.addCallbackBuffer( mPreviewBuffer );
            c.setPreviewCallbackWithBuffer( this );
         }

         // Mark dirty for render.
         requestRender();
      }
   };      
Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題

      其中,update為native方法,其實作由jni中對應的檔案完成,其中調用了libfastcv.a中相應的API。這裡涉及NDK程式設計的基本方法步驟:(1)開發環境;(2)編寫Java代碼、C/C++代碼;(3)編譯C/C++檔案生成.so庫;(4)重新編譯工程,生成apk。由于本章不重點講述NDK,這裡不再展開。

      除上述方法以外,筆者采用了另外一種思路進行了探索,上述方法處理的資料為每幀圖像資料,可以了解為線上處理,而如果在錄制完成之後再處理,可以了解為離線處理。這裡采用了第三方庫mp4parser,mp4parser是一款支援在Android中進行視訊分割的庫,這裡通過其進行視訊旋轉。至于具體效果如何,讀者有興趣可自行嘗試,這裡留個懸念。

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
private boolean rotateVideoFileWithClockwiseDegree(String sourceFilePath, int degree) {
        if(!isFileAndDegreeValid(sourceFilePath, degree)) {
            return false;
        }
        rotateVideoFile(sourceFilePath, degree);
        return true;
    }      
Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題

      對輸入參數進行合法性檢測之後,根據檢測結果判斷是否進行旋轉。

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
private boolean isFileAndDegreeValid(String sourceFilePath, int degree) {
        if(sourceFilePath == null || (!sourceFilePath.endsWith(".mp4")) 
                                  || (!new File(sourceFilePath).exists())) {
            return false;
        }
        if (degree == 0 || (degree % 90 != 0)) {
            return false;
        }
        return true;
    }      
Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
private void rotateVideoFile(String sourceFilePath, int degree) {
        List<TrackBox> trackBoxes = getTrackBoxesOfVideoFileByPath(sourceFilePath);
        Movie rotatedMovie = getRotatedMovieOfTrackBox(trackBoxes);
        writeMovieToModifiedFile(rotatedMovie);
    }      

      通過mp4parser旋轉視訊主要分為三步:(1)擷取視訊檔案對應的TrackBoxes;(2)根據TrackBoxes擷取旋轉後的Movie對象;(3)将Movie對象寫入檔案。

Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
private List<TrackBox> getTrackBoxesOfVideoFileByPath(String sourceFilePath) {
        IsoFile isoFile = null;
        List<TrackBox> trackBoxes = null;
        try {
            isoFile = new IsoFile(sourceFilePath);
            trackBoxes = isoFile.getMovieBox().getBoxes(TrackBox.class);
            isoFile.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return trackBoxes;
    }      
Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
private Movie getRotatedMovieOfTrackBox(List<TrackBox> trackBoxes) {
        Movie rotatedMovie = new Movie();
        // 旋轉
        for (TrackBox trackBox : trackBoxes) {
            trackBox.getTrackHeaderBox().setMatrix(Matrix.ROTATE_90);
            rotatedMovie.addTrack(new Mp4TrackImpl(trackBox));
        }
        return rotatedMovie;
    }      
Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題
private void writeMovieToModifiedFile(Movie movie) {
        Container container = new DefaultMp4Builder().build(movie);
        File modifiedVideoFile = new File(videoFilePath.replace(".mp4", "_MOD.mp4"));
        FileOutputStream fos;
        try {
            fos = new FileOutputStream(modifiedVideoFile);
            WritableByteChannel bb = Channels.newChannel(fos);
            container.writeContainer(bb);
            // 關閉檔案流
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }      
Android開發筆記——視訊錄制播放常見問題 Android開發筆記——視訊錄制播放常見問題