本文分享自己在視頻錄制播放過(guò)程中遇到的一些問(wèn)題,主要包括:
一、視頻錄制流程
    以微信為例,其錄制觸發(fā)為按下(住)錄制按鈕,結(jié)束錄制的觸發(fā)條件為松開(kāi)錄制按鈕或錄制時(shí)間結(jié)束,其流程大概可以用下圖來(lái)描述。

1.1、開(kāi)始錄制
   根據(jù)上述流程及項(xiàng)目的編程慣例,可在onCreate()定義如下函數(shù)來(lái)完成功能:
    
初始化過(guò)程主要包括View,Data以及Listener三部分。在初始化View時(shí),添加攝像頭預(yù)覽,添加倒計(jì)時(shí)文本組件,設(shè)置初始狀態(tài)UI組件的可見(jiàn);初始化Data時(shí),從Intent中獲取初始數(shù)據(jù);初始化Listener時(shí),分別對(duì)錄制觸發(fā)按鈕,保存/取消視頻錄制按鈕以及視頻預(yù)覽界面添加監(jiān)聽(tīng)。
    當(dāng)系統(tǒng)初始化成功后,等待用戶按下錄制按鈕,因此在錄制按鈕的監(jiān)聽(tīng)中,需要完成以下功能:錄制,計(jì)時(shí),更新界面組件。
if(isRecording) {  mMediaRecorder.stop();  releaseMediaRecorder();  mCamera.lock();  isRecording = false;}if(startRecordVideo()) {  startTimeVideoRecord();  isRecording = true;}首先判斷當(dāng)前錄制狀態(tài),如果正在錄制,則先停止錄制,釋放MediaRecorder資源,鎖定攝像頭,置位錄制狀態(tài);然后開(kāi)始視頻錄制startRecordVideo,其boolean型返回值表征是否啟動(dòng)成功,啟動(dòng)成功后,開(kāi)始視頻錄制計(jì)時(shí),并且置位錄制狀態(tài)。startRecordVideo涉及MediaRecorder的配置,準(zhǔn)備以及啟動(dòng)。

翻譯成代碼如下:
private boolean startRecordVideo() {  configureMediaRecorder();  if(!prepareConfiguredMediaRecorder()) {    return false;  }  mMediaRecorder.start();  return true;}1.2、結(jié)束錄制
根據(jù)上述流程圖可知,結(jié)束錄制的觸發(fā)條件為松開(kāi)錄制按鈕或計(jì)時(shí)時(shí)間到。在結(jié)束錄制方法中,需要釋放MediaRecorder,開(kāi)始循環(huán)播放已錄制視頻,設(shè)置界面更新等。

    翻譯成代碼如下:
private void stopRecordVideo() {    releaseMediaRecorder();    // 錄制視頻文件處理    if(currentRecordProgress < MIN_RECORD_TIME) {      Toast.makeText(VideoInputActivity.this, "錄制時(shí)間太短", Toast.LENGTH_SHORT).show();    } else {      startVideoPlay();      isPlaying = true;      setUiDisplayAfterVideoRecordFinish();    }    currentRecordProgress = 0;    updateProgressBar(currentRecordProgress);    releaseTimer();    // 狀態(tài)設(shè)置    isRecording = false;  } 二、視頻預(yù)覽及SurfaceHolder
      視頻預(yù)覽采用SurfaceView,相比于普通的View,SurfaceView在一個(gè)新起的單獨(dú)線程中繪制畫(huà)面,該實(shí)現(xiàn)的優(yōu)點(diǎn)是更新畫(huà)面不會(huì)阻塞UI主線程,缺點(diǎn)是會(huì)帶來(lái)事件同步的問(wèn)題。當(dāng)然,這涉及到UI事件的傳遞以及線程同步。
在實(shí)現(xiàn)中,通過(guò)繼承SurfaceView組件來(lái)實(shí)現(xiàn)自定義預(yù)覽控件。首先,SurfaceView的getHolder()方法會(huì)返回SurfaceHolder,需要為SurfaceHolder添加SurfaceHolder.Callback回調(diào);其次,重寫(xiě)surfaceCreated、surfaceChanged和surfaceDestroyed實(shí)現(xiàn)。
2.1、構(gòu)造器
      構(gòu)造器包含了初始化域以及添加上述回調(diào)的過(guò)程。
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);  }這里需要說(shuō)明mSupportedPreviewSizes,由于攝像頭支持的預(yù)覽尺寸由Camera本身的參數(shù)決定,因此需要首先獲取其所支持的預(yù)覽尺寸。
2.2、預(yù)覽尺寸的設(shè)置
      從Google官方的Camera示例程序中可以看出,選擇預(yù)覽尺寸的標(biāo)準(zhǔn)是(1)攝像頭支持的預(yù)覽尺寸的寬高比與SurfaceView的寬高比的絕對(duì)差值小于0.1;(2)在(1)獲得的尺寸中,選取與SurfaceView的高的差值最小的。通過(guò)代碼對(duì)這兩個(gè)標(biāo)準(zhǔn)進(jìn)行了實(shí)現(xiàn),這里貼一下官方的代碼:
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;  }在加載預(yù)覽畫(huà)面時(shí),需要考慮Camera支持的尺寸(getSupportedPreviewSizes)和加載預(yù)覽畫(huà)面的SurfaceView的尺寸(layout_width/layout_height),在預(yù)覽階段,兩者之間的關(guān)系直接影響清晰度及圖像拉伸。對(duì)于Camera的尺寸,由于設(shè)備的硬件差異,不同設(shè)備支持的尺寸存在差異,但在默認(rèn)情況(orientation=landscape)下,其width>height。以HTC609d為例,Camera支持的分辨率為1280*720(16:9)……640*480(4:3)……480*320(3:2)等十多種,而其屏幕的分辨率為960*540(16:9)。因此,很容易得到以下結(jié)論:(1)當(dāng)Camera預(yù)覽尺寸小于SurfaceView尺寸較多時(shí),預(yù)覽畫(huà)面就不清晰;(2)Camera預(yù)覽尺寸寬高比與SurfaceView寬高比相差較大時(shí),預(yù)覽畫(huà)面就會(huì)拉伸。
上述代碼在手機(jī)設(shè)置為橫屏?xí)r并沒(méi)有問(wèn)題,在設(shè)置為豎屏?xí)r,為獲得最優(yōu)的預(yù)覽尺寸,需要在調(diào)用此方法前比較SurfaceView的寬高。
if (mSupportedPreviewSizes != null) {  mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes,             Math.max(width, height), Math.min(width, height));}獲得與當(dāng)前SurfaceView匹配的預(yù)覽尺寸后,即可通過(guò)Camera.Parameters進(jìn)行設(shè)置。
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); 三、視頻清晰度及文件大小
      在第一節(jié)中講到startRecordVideo,包括配置MediaRecorder,準(zhǔn)備MediaRecorder以及啟動(dòng),其中配置MediaRecorder是視頻錄制的重點(diǎn),需要了解每項(xiàng)配置參數(shù)的作用,根據(jù)業(yè)務(wù)場(chǎng)景靈活配置。這里參考Google官方的示例給出一個(gè)可行的配置方案,然后再對(duì)其進(jìn)行解釋。
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);  }Step 1:
1.setCamera參數(shù)能夠使得在預(yù)覽和錄制中快速切換,避免Camera對(duì)象的重新加載。在某些Android手機(jī)自帶的照相機(jī)程序中,切換預(yù)覽與錄制中的短暫卡頓,讀者可自行體會(huì)。
2.mMediaRecorder.setOrientationHint(90)在錄制方向?yàn)樨Q直(portrait)時(shí)使用,它能使視頻文件的沿順時(shí)針?lè)较蛐D(zhuǎn)90度,如果不設(shè)置此項(xiàng),播放視頻時(shí),畫(huà)面會(huì)發(fā)生90度的旋轉(zhuǎn)。不過(guò)這里更重要的是,即使設(shè)置了此項(xiàng),在某些播放器上,畫(huà)面依然會(huì)有90度的旋轉(zhuǎn)(比如將在手機(jī)上正常播放的視頻導(dǎo)入到PC中進(jìn)行播放,或者嵌入H5的video標(biāo)簽中),這可是為什么呢?注意setOrientationHint的說(shuō)明:Note that some video players may choose to ignore the compostion matrix in a video during playback. 那么如何做到在所有播放器上都能以正常方向播放呢?稍等,后續(xù)專門(mén)對(duì)其進(jìn)行說(shuō)明。
Step 2:
1.setAudioSource(MediaRecorder.AudioSource.VOICE_RECOGNITION),VOICE_RECOGNITION相比于MIC會(huì)根據(jù)語(yǔ)音識(shí)別的需要做一些調(diào)諧,當(dāng)然,這需要在系統(tǒng)支持的情況下。
2.setVideoSource自然是VideoSource.CAMERA,只是在此兩項(xiàng)設(shè)置必須在設(shè)置編碼器之前設(shè)置,這無(wú)需說(shuō)明。
Step 3:
1.setOutputFormat需要在Step 2之后,并且在prepare()之前。這里采用OutputFormat.MPEG_4格式。
2.setVideoSize需要權(quán)衡的因素較多,主要包括三方面:MediaRecorder支持的錄制尺寸、視頻文件的大小以及兼容不同Android機(jī)型。這里采用640 * 480(微信小視頻的尺寸是320*240),文件大小在500-1000kb之間,并且市面上99%以上機(jī)型支持此錄制尺寸。
3.setVideoEncodingBitRate與視頻的清晰度有關(guān),設(shè)置此參數(shù)需要權(quán)衡清晰度與文件大小的關(guān)系。太高,文件大不易傳輸;太低,文件清晰度低,識(shí)別率低。需要根據(jù)實(shí)際業(yè)務(wù)場(chǎng)景靈活調(diào)整。
4.setVideoEncoder采用H264編碼,MPEG4、H263、H264等不同編碼的差別比較,實(shí)際使用中,H264的壓縮率較高,推薦使用。
5.setAudioEncoder采用AudioEncoder.AAC,該設(shè)置主要是考慮其通用性、兼容性。
Step 4:
setMaxFileSize指定錄制文件的大小限制,當(dāng)然還可以限制其最大錄制時(shí)間。
setOutputFile指定輸出視頻的路徑。
setOnErrorListener指定錯(cuò)誤監(jiān)聽(tīng)器。
   在完成上述配置之后,即可準(zhǔn)備MediaRecorder,并在返回成功后開(kāi)始視頻錄制。
private boolean prepareConfiguredMediaRecorder() {    // Step 5: Prepare configured MediaRecorder    try {      mMediaRecorder.prepare();    } catch (Exception e) {      releaseMediaRecorder();      return false;    }    return true;  } 四、視頻文件旋轉(zhuǎn)
      第三節(jié)中Step 1提到對(duì)視頻文件的旋轉(zhuǎn),因?yàn)槟承┎シ牌鲿?huì)忽略錄制視頻時(shí)的配置參數(shù),因此可嘗試通過(guò)第三方庫(kù)對(duì)視頻文件進(jìn)行旋轉(zhuǎn),例如:OpenCV,fastCV等,在Camera對(duì)象的Camera.PreviewCallback中截取每幀數(shù)據(jù)byte[] data,然后對(duì)其進(jìn)行處理,然后輸出。該方法需要考慮處理方法的高效性,在編程時(shí)一般采用NDK,在C++中完成關(guān)鍵的處理,這里貼出fastCV中該處理方法的邏輯。
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();   }  };其中,update為native方法,其實(shí)現(xiàn)由jni中對(duì)應(yīng)的文件完成,其中調(diào)用了libfastcv.a中相應(yīng)的API。這里涉及NDK編程的基本方法步驟:(1)開(kāi)發(fā)環(huán)境;(2)編寫(xiě)Java代碼、C/C++代碼;(3)編譯C/C++文件生成.so庫(kù);(4)重新編譯工程,生成apk。由于本章不重點(diǎn)講述NDK,這里不再展開(kāi)。
      除上述方法以外,筆者采用了另外一種思路進(jìn)行了探索,上述方法處理的數(shù)據(jù)為每幀圖像數(shù)據(jù),可以理解為在線處理,而如果在錄制完成之后再處理,可以理解為離線處理。這里采用了第三方庫(kù)mp4parser,mp4parser是一款支持在Android中進(jìn)行視頻分割的庫(kù),這里通過(guò)其進(jìn)行視頻旋轉(zhuǎn)。至于具體效果如何,讀者有興趣可自行嘗試,這里留個(gè)懸念。
private boolean rotateVideoFileWithClockwiseDegree(String sourceFilePath, int degree) {    if(!isFileAndDegreeValid(sourceFilePath, degree)) {      return false;    }    rotateVideoFile(sourceFilePath, degree);    return true;  }對(duì)輸入?yún)?shù)進(jìn)行合法性檢測(cè)之后,根據(jù)檢測(cè)結(jié)果判斷是否進(jìn)行旋轉(zhuǎn)。
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;  }private void rotateVideoFile(String sourceFilePath, int degree) {    List<TrackBox> trackBoxes = getTrackBoxesOfVideoFileByPath(sourceFilePath);    Movie rotatedMovie = getRotatedMovieOfTrackBox(trackBoxes);    writeMovieToModifiedFile(rotatedMovie);  }通過(guò)mp4parser旋轉(zhuǎn)視頻主要分為三步:
(1)獲取視頻文件對(duì)應(yīng)的TrackBoxes;
(2)根據(jù)TrackBoxes獲取旋轉(zhuǎn)后的Movie對(duì)象;
(3)將Movie對(duì)象寫(xiě)入文件。
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;  }private Movie getRotatedMovieOfTrackBox(List<TrackBox> trackBoxes) {    Movie rotatedMovie = new Movie();    // 旋轉(zhuǎn)    for (TrackBox trackBox : trackBoxes) {      trackBox.getTrackHeaderBox().setMatrix(Matrix.ROTATE_90);      rotatedMovie.addTrack(new Mp4TrackImpl(trackBox));    }    return rotatedMovie;  }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);      // 關(guān)閉文件流      fos.close();    } catch (Exception e) {      e.printStackTrace();    }   }本文對(duì)Android視頻錄制中常見(jiàn)的問(wèn)題進(jìn)行了說(shuō)明,希望對(duì)大家的學(xué)習(xí)有所幫助。
新聞熱點(diǎn)
疑難解答
圖片精選