編輯:關於Android編程
本文分享自己在視頻錄制播放過程中遇到的一些問題,主要包括:
一、視頻錄制流程
以微信為例,其錄制觸發為按下(住)錄制按鈕,結束錄制的觸發條件為松開錄制按鈕或錄制時間結束,其流程大概可以用下圖來描述。

1.1、開始錄制
根據上述流程及項目的編程慣例,可在onCreate()定義如下函數來完成功能:
初始化過程主要包括View,Data以及Listener三部分。在初始化View時,添加攝像頭預覽,添加倒計時文本組件,設置初始狀態UI組件的可見;初始化Data時,從Intent中獲取初始數據;初始化Listener時,分別對錄制觸發按鈕,保存/取消視頻錄制按鈕以及視頻預覽界面添加監聽。
當系統初始化成功後,等待用戶按下錄制按鈕,因此在錄制按鈕的監聽中,需要完成以下功能:錄制,計時,更新界面組件。
if(isRecording) {
mMediaRecorder.stop();
releaseMediaRecorder();
mCamera.lock();
isRecording = false;
}
if(startRecordVideo()) {
startTimeVideoRecord();
isRecording = true;
}
首先判斷當前錄制狀態,如果正在錄制,則先停止錄制,釋放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事件的傳遞以及線程同步。
在實現中,通過繼承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 ErrorListener
mMediaRecorder.setOnErrorListener(this);
}
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等不同編碼的差別比較,實際使用中,H264的壓縮率較高,推薦使用。
5.setAudioEncoder采用AudioEncoder.AAC,該設置主要是考慮其通用性、兼容性。
Step 4:
setMaxFileSize指定錄制文件的大小限制,當然還可以限制其最大錄制時間。
setOutputFile指定輸出視頻的路徑。
setOnErrorListener指定錯誤監聽器。
在完成上述配置之後,即可准備MediaRecorder,並在返回成功後開始視頻錄制。
private boolean prepareConfiguredMediaRecorder() {
// Step 5: Prepare configured MediaRecorder
try {
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視頻錄制中常見的問題進行了說明,希望對大家的學習有所幫助。
用Fiddler在Android上抓包(Http+https)
Fiddler是一個HTTP協議調試代理工具,在開發網絡應用的時候經常會用到,其最基本的作用是能抓HTTP的數據包,當然它還有更高級的用法,如添加斷點、修改請求與相應的數
Android 2D繪圖解析之 Canvas,Paint
【Android 2D繪圖解析】系列文章將全面介紹Android繪圖相關,這篇簡單介紹下如何利用Android API進行一些簡單圖形的繪制,繪圖的前提是需要繼承自Vie
如何使用android命令
作為Windows用戶,擁有著出色的圖形化界面體驗,但命令行則的圖形化界面的根基,下面就來看看如何用命令行來執行android命令吧。首先你得進入你的Android SD
APP漏洞自動化掃描專業評測報告(中篇)
前言樣本測試後的掃描時間對比和漏洞項專業對比後,本篇將以各個廠商的掃描能力作為分析維度展開。測試方法使用自己編寫的測試APP測試各個掃描平台的掃描能力。這些掃描能力主要分