Android开发笔记——视频录制播放常见问题
Android開發筆記——視頻錄制播放常見問題
轉載http://www.cnblogs.com/younghao/p/5089118.html
本文分享自己在視頻錄制播放過程中遇到的一些問題,主要包括:
一、視頻錄制流程
??? 以微信為例,其錄制觸發為按下(住)錄制按鈕,結束錄制的觸發條件為松開錄制按鈕或錄制時間結束,其流程大概可以用下圖來描述。
1.1、開始錄制
?? 根據上述流程及項目的編程慣例,可在onCreate()定義如下函數來完成功能:
???? 初始化過程主要包括View,Data以及Listener三部分。在初始化View時,添加攝像頭預覽,添加倒計時文本組件,設置初始狀態UI組件的可見;初始化Data時,從Intent中獲取初始數據;初始化Listener時,分別對錄制觸發按鈕,保存/取消視頻錄制按鈕以及視頻預覽界面添加監聽。
??? 當系統初始化成功后,等待用戶按下錄制按鈕,因此在錄制按鈕的監聽中,需要完成以下功能:錄制,計時,更新界面組件。
??? 首先判斷當前錄制狀態,如果正在錄制,則先停止錄制,釋放MediaRecorder資源,鎖定攝像頭,置位錄制狀態;然后開始視頻錄制startRecordVideo,其boolean型返回值表征是否啟動成功,啟動成功后,開始視頻錄制計時,并且置位錄制狀態。startRecordVideo涉及MediaRecorder的配置,準備以及啟動。
??? 翻譯成代碼如下:
private boolean startRecordVideo() {configureMediaRecorder();if(!prepareConfiguredMediaRecorder()) {return false;}mMediaRecorder.start();return true; }1.2、結束錄制
??? 根據上述流程圖可知,結束錄制的觸發條件為松開錄制按鈕或計時時間到。在結束錄制方法中,需要釋放MediaRecorder,開始循環播放已錄制視頻,設置界面更新等。
??? 翻譯成代碼如下:
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;}?
二、視頻預覽及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、構造器
????? 構造器包含了初始化域以及添加上述回調的過程。
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);}????? 這里需要說明mSupportedPreviewSizes,由于攝像頭支持的預覽尺寸由Camera本身的參數決定,因此需要首先獲取其所支持的預覽尺寸。
2.2、預覽尺寸的設置
????? 從Google官方的Camera示例程序中可以看出,選擇預覽尺寸的標準是(1)攝像頭支持的預覽尺寸的寬高比與SurfaceView的寬高比的絕對差值小于0.1;(2)在(1)獲得的尺寸中,選取與SurfaceView的高的差值最小的。通過代碼對這兩個標準進行了實現,這里貼一下官方的代碼:
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;}????? 在加載預覽畫面時,需要考慮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進行設置。
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);?
三、視頻清晰度及文件大小
????? 在第一節中講到startRecordVideo,包括配置MediaRecorder,準備MediaRecorder以及啟動,其中配置MediaRecorder是視頻錄制的重點,需要了解每項配置參數的作用,根據業務場景靈活配置。這里參考Google官方的示例給出一個可行的配置方案,然后再對其進行解釋。
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 ErrorListenermMediaRecorder.setOnErrorListener(this);}Step 1:
Step 2:
Step 3:
Step 4:
?? 在完成上述配置之后,即可準備MediaRecorder,并在返回成功后開始視頻錄制。
private boolean prepareConfiguredMediaRecorder() {// Step 5: Prepare configured MediaRecordertry {mMediaRecorder.prepare();} catch (Exception e) {releaseMediaRecorder();return false;}return true;}?
四、視頻文件旋轉
????? 第三節中Step 1提到對視頻文件的旋轉,因為某些播放器會忽略錄制視頻時的配置參數,因此可嘗試通過第三方庫對視頻文件進行旋轉,例如:OpenCV,fastCV等,在Camera對象的Camera.PreviewCallback中截取每幀數據byte[] data,然后對其進行處理,然后輸出。該方法需要考慮處理方法的高效性,在編程時一般采用NDK,在C++中完成關鍵的處理,這里貼出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方法,其實現由jni中對應的文件完成,其中調用了libfastcv.a中相應的API。這里涉及NDK編程的基本方法步驟:(1)開發環境;(2)編寫Java代碼、C/C++代碼;(3)編譯C/C++文件生成.so庫;(4)重新編譯工程,生成apk。由于本章不重點講述NDK,這里不再展開。
????? 除上述方法以外,筆者采用了另外一種思路進行了探索,上述方法處理的數據為每幀圖像數據,可以理解為在線處理,而如果在錄制完成之后再處理,可以理解為離線處理。這里采用了第三方庫mp4parser,mp4parser是一款支持在Android中進行視頻分割的庫,這里通過其進行視頻旋轉。至于具體效果如何,讀者有興趣可自行嘗試,這里留個懸念。
private boolean rotateVideoFileWithClockwiseDegree(String sourceFilePath, int degree) {if(!isFileAndDegreeValid(sourceFilePath, degree)) {return false;}rotateVideoFile(sourceFilePath, degree);return true;}????? 對輸入參數進行合法性檢測之后,根據檢測結果判斷是否進行旋轉。
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);}????? 通過mp4parser旋轉視頻主要分為三步:(1)獲取視頻文件對應的TrackBoxes;(2)根據TrackBoxes獲取旋轉后的Movie對象;(3)將Movie對象寫入文件。
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();// 旋轉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);// 關閉文件流 fos.close();} catch (Exception e) {e.printStackTrace();} }總結
以上是生活随笔為你收集整理的Android开发笔记——视频录制播放常见问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 1999年冬发出第一个论坛帖,弹指二十年
- 下一篇: Golang——Go语言发展史(一)