hello,大家吼,我是那個愛貓的老司機,愛好是掀桌子的話嘮程式猿。回想起剛開始碼文章的時候,沒想到內向的自己也可以擼出那麼多文字,真是挖坑不止,且行且珍惜啊。有猜到今天聊的主角是誰嗎?猜到是不是要送紅包呢?
請捂著你的良心說話,對於貧窮的作者(我)不是應該打賞麼 ̄へ ̄!,接下來工作又要忙起來了,更新應該是放緩了呢╮(╯_╰)╭,好傷心。
例牌飄過: github.com/CarGuo 請(bu yao)無視。
想一想,我們聊過AudioReord,AudioTrack,MediaPlayer,那多媒體四大金剛,就剩下了MediaRecorder了(SoundPool?我這裡訊號不好···)。其實MediaRecorder個人用的也不多,很久前用它在拍攝視訊上確實趟過無視次坑,那今天就聊它吧,把它聊到躺下(ノQ益Q)ノ彡┻━┻。
MediaRecorder
一般用在多媒體錄製上面,當然如果你只是簡單的想錄制音訊,用它最合適不過,不過如果你想更多樣化的錄製這裡推薦《Android MP3錄製,波形顯示,音訊許可權相容與播放》。今天的主題是錄製視訊,用的還是老式通用的Camera,不是新的camera2(這就尷尬了.....((/- -)/),反正個人秉承能用是王道的做法(懶)。之前也嘗試過FFMPEG的錄製合成音訊,大小和效果也不錯,只是有時候的相容性確實有些問題,最主要還是資料不多,不好改啊 ̄へ ̄(懶)。
既然是錄製視訊,那麼少不了Camera,這貨也是讓人又愛又恨(哪裡有愛了┑( ̄Д  ̄)┍?),也許是因為Android碎片化的原因,所以用起來也是坑坑窪窪的,接下來就讓我們結束廢話吧:
- 1、SurfaceView用於承載畫面。
- 2、初始化相機Camera。
- 3、初始化重力旋轉用於橫豎屏。
- 4、配置閃光燈和旋轉攝像頭功能。
- 5、配置MediaRecorder的錄製引數後開始錄製。
- 6、結束錄製預覽視訊。
1、SurfaceView顯示畫面
舊專案用的都是SurfaceView,這次就就它吧。這裡我們需要首先是implements SurfaceHolder.Callback,這樣我們才能在surface建立的時候初始化相機渲染畫面,在畫面銷燬的時候銷燬相機(畫面都沒有要初始化相機何用)。
SurfaceHolder holder = cameraShowView.getHolder();
holder.addCallback(this);
// setType必須設定,要不出錯.
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
···此處略過無數只草泥馬
@Override
public void surfaceCreated(SurfaceHolder holder) {
surfaceHolder = holder;
//更具當前的相機型別(前,後)初始化相機,閃光燈不啟動
initCamera(cameraType, false);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
surfaceHolder = holder;
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
//結束錄製
endRecord();
//是否相機
releaseCamera();
}複製程式碼
2、初始化Camera
除了有點坑外,流程上還是比較簡單的:
- 釋放已經初始化過的相機。
- 根據當前攝像頭型別開啟相機。
- 配置相機引數:預覽大小,對焦,閃光燈,豎屏顯示。
- 設定顯示畫面的surface
- 開始繪製
if (camera != null) {
//如果已經初始化過,就先釋放
releaseCamera();
}
try {
//根據前後攝像頭開啟攝像頭
camera = Camera.open(type);
if (camera == null) {
//拿不到可能是沒許可權
showCameraPermission();
return;
}
camera.lock();
//Point screen = new Point(getScreenWidth(this), getScreenHeight(this));
//現在不用獲取最高的顯示效果
//Point show = getBestCameraShow(camera.getParameters(), screen);
Camera.Parameters parameters = camera.getParameters();
if (type == 0) {
//基本是都支援這個比例
parameters.setPreviewSize(SIZE_1, SIZE_2);
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);//1連續對焦
camera.cancelAutoFocus();// 2如果要實現連續的自動對焦,這一句必須加上
}
camera.setParameters(parameters);
FlashLogic(camera.getParameters(), flashType, flashDo);
if (cameraType == 1) {
frontCameraRotate();
camera.setDisplayOrientation(frontRotate);
} else {
camera.setDisplayOrientation(90);
}
camera.setPreviewDisplay(surfaceHolder);
camera.startPreview();
camera.unlock();
} catch (Exception e) {
e.printStackTrace();
releaseCamera();
}複製程式碼
這裡需要注意坑(畫面變形)問題,那就是你配置的相機解析度畫面,在錄製的時候可能會因為和錄製的解析度畫面不一致,導致開始錄製的時候畫面奇怪的突變,所以Camera和MediaRecorder的解析度最好一致。
問題又來了Camera和MediaRecorder不是什麼解析度都支援的,他們分別都有對應的介面:getSupportedPreviewSizes和CamcorderProfile等來獲取對應支援的解析度的,路迢迢啊。
經過輪番的嘗試,還有上傳對大小要求,所以最終選擇寫死,對,寫死了640 * 480這樣的大小,這個解析度基本都支援(不支援那手機的尊嚴何在( ‵o′)凸),對於十來秒的視訊,這個解析度的尺寸還算可以(如果對畫質有需要可以另外配置,如果FFMPEG壓縮效能堪憂啊)。
那麼問題又來了(哪來那麼多問題),但是手機螢幕大部分情況下是16:9,而這個解析度明顯是4:3(萬惡的需求啊(ノQ益Q)ノ彡┻━┻)。這時候因為Surface的最外層是FrameLayout(搞不懂為什麼超出螢幕的時候RelativeLayout有時候會有問題),個人的做法是調整surface的比例。如果是不充滿螢幕高度的,就通過螢幕寬度比例算出surface的高度;如果充滿螢幕高度,就算出surface的寬度。
如此以來,不變形啦,在點選錄製的瞬間也不跳動啦,唯一有點小問題的就是充滿高度的時候,畫面是超過了螢幕寬度的一點的,所以可能錄到了什麼不想錄制的♂,但是剛好沒看到︿( ̄︶ ̄)︿。
int screenWidth = getScreenWidth(this);
int screenHeight = getScreenHeight(this);
//根據比例設定surface的寬度
setViewSize(cameraShowView, screenWidth * SIZE_1 / SIZE_2, screenHeight);複製程式碼
3、重力感應旋轉
當時看到IOS微博的視訊錄製是可以支援橫豎屏錄製,覺得挺有意思的,這裡用的是OrientationEventListener,具體的之前IJKPlayer視訊文章裡已經說過(懶),有興趣的可以去看看。我們是在畫面旋轉的時候把對應的logo用屬性動畫也旋轉了,然後得到當前的旋轉角度,告訴MediaRecorder,拍攝出來的視訊元資訊裡就帶有了角度資訊,播放的時候畫面會就旋轉為橫屏或者豎屏啦。
orientationEventListener = new OrientationEventListener(this) {
@Override
public void onOrientationChanged(int rotation) {
if (!flagRecord) {
if (((rotation >= 0) && (rotation <= 30)) || (rotation >= 330)) {
// 豎屏拍攝
if (rotationFlag != 0) {
//旋轉logo
rotationAnimation(rotationFlag, 0);
//這是豎屏視訊需要的角度
rotationRecord = 90;
//這是記錄當前角度的flag
rotationFlag = 0;
}
} else if (((rotation >= 230) && (rotation <= 310))) {
// 橫屏拍攝
if (rotationFlag != 90) {
//旋轉logo
rotationAnimation(rotationFlag, 90);
//這是正橫屏視訊需要的角度
rotationRecord = 0;
//這是記錄當前角度的flag
rotationFlag = 90;
}
} else if (rotation > 30 && rotation < 95) {
// 反橫屏拍攝
if (rotationFlag != 270) {
//旋轉logo
rotationAnimation(rotationFlag, 270);
//這是反橫屏視訊需要的角度
rotationRecord = 180;
//這是記錄當前角度的flag
rotationFlag = 270;
}
}
//倒過來就算了,你又不是小米MIX
}
}
};
orientationEventListener.enable();複製程式碼
前置攝像頭
此處有坑,還不止一個,如果你還需要支援前置攝像頭(能說不嗎?),直接使用上面的rotationRecord去配置MediaRecorder是會有問題的。
首先說Camera,如果測試說你的前置Camera在某些手機上畫面角度不對,這時候你可以偷偷把手機砸了,因為這是相容問題。如果你沒有勇氣砸手機,看下面。
傳說中,只要拿下面的frontRotate去配置Camera就正常顯示啦,偉人說的!而其中的frontOri,我們是用到配置後面MediaRecorder,具體看程式碼的,這是調出來的結果(。・・)ノ。
/**
* 旋轉前置攝像頭為正的
*/
private void frontCameraRotate() {
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(1, info);
int degrees = getDisplayRotation(this);
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360;
}
frontOri = info.orientation;
frontRotate = result;
}
/**
* 獲取旋轉角度
*/
private int getDisplayRotation(Activity activity) {
int rotation = activity.getWindowManager().getDefaultDisplay()
.getRotation();
switch (rotation) {
case Surface.ROTATION_0:
return 0;
case Surface.ROTATION_90:
return 90;
case Surface.ROTATION_180:
return 180;
case Surface.ROTATION_270:
return 270;
}
return 0;
}
···此處無數字草泥馬
//配置錄製角度
int frontRotation;
if (rotationRecord == 180) {
//反向橫屏的前置角度
frontRotation = 180;
} else {
//豎屏和正向橫屏的前置角度
//錄製下來的視屏選擇角度,此處為前置
frontRotation = (rotationRecord == 0) ? 270 - frontOri : frontOri;
}
//根據前後攝像頭給角度
recorder.setOrientationHint((cameraType == 1) ? frontRotation : rotationRecord);複製程式碼
4、閃光燈和旋轉攝像頭
閃光燈的開啟關閉遇到過一個問題,就是有的手機還沒有開啟錄製,一配置開啟它就亮了。(砸手機)最後解決的是在配置的時候標誌型別,設定好MediaRecorder之後拍攝才開始閃光燈。(其他的什麼一閃一閃的模式就算了吧= =)
至於旋轉切換相機,主要還是針對前置camera需要做如上面所說的畫面預覽旋轉。
/**
* 閃光燈邏輯
*
* @param p 相機引數
* @param type 開啟還是關閉
* @param isOn 是否立即啟動
*/
private void FlashLogic(Camera.Parameters p, int type, boolean isOn) {
flashType = type;
if (type == 0) {
if (isOn) {
p.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
camera.setParameters(p);
}
videoFlashLight.setImageResource(R.drawable.flash_off);
} else {
if (isOn) {
p.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
camera.setParameters(p);
}
videoFlashLight.setImageResource(R.drawable.flash);
}
if (cameraFlag == 0) {
videoFlashLight.setVisibility(View.GONE);
} else {
videoFlashLight.setVisibility(View.VISIBLE);
}
}
/**
* 切換攝像頭
*/
public void switchCamera() {
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
int cameraCount = Camera.getNumberOfCameras();//得到攝像頭的個數0或者1;
try {
for (int i = 0; i < cameraCount; i++) {
Camera.getCameraInfo(i, cameraInfo);//得到每一個攝像頭的資訊
if (cameraFlag == 1) {
//後置到前置
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {//代表攝像頭的方位,CAMERA_FACING_FRONT前置 CAMERA_FACING_BACK後置
frontCameraRotate();//前置旋轉攝像頭度數
switchCameraLogic(i, 0, frontRotate);
break;
}
} else {
//前置到後置
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {//代表攝像頭的方位,CAMERA_FACING_FRONT前置 CAMERA_FACING_BACK後置
switchCameraLogic(i, 1, 90);
break;
}
}
}
} catch (Exception exception) {
exception.printStackTrace();
}
}複製程式碼
5、配置MediaRecorder的錄製引數、生成視訊。
這裡最坑的就是MediaRecorder的配置引數是有前後關係的,先生小孩後再洞房這種綠色模式是不行的,具體順序參照下方程式碼,位元速率和幀數都是配置相對較小,適合拍攝上傳。此處還需要注意,如果應用沒有獲取到錄音許可權,在錄製的時候是會走catch裡面的。
停止錄製相對就簡單了,只要順序正常即可,之後就可以把視訊傳到VideoView快速實現預覽啦。作為谷歌親兒子,VideoView自帶對setOrientationHint的角度解析,只要根據視訊大小配置好介面顯示的效果即可。比起 之前本人擼的播放器 ,兒子還是自己的親┑( ̄Д  ̄)┍,如果需求不高用起來還是可以閉著眼睛的用的。(之前還有小夥伴自己用MediaPlayer播放呢)
//開始
private boolean startRecord() {
//懶人模式,根據閃光燈和攝像頭前後重新初始化一遍,開期閃光燈工作模式
initCamera(cameraType, true);
if (recorder == null) {
recorder = new MediaRecorder();
}
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
|| camera == null || recorder == null) {
camera = null;
recorder = null;
//還是沒許可權啊
showCameraPermission();
return false;
}
try {
recorder.setCamera(camera);
// 這兩項需要放在setOutputFormat之前
recorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
recorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
// Set output file format,輸出格式
recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
//必須在setEncoder之前
recorder.setVideoFrameRate(15); //幀數 一分鐘幀,15幀就夠了
recorder.setVideoSize(SIZE_1, SIZE_2);//這個大小就夠了
// 這兩項需要放在setOutputFormat之後
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
recorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
recorder.setVideoEncodingBitRate(3 * SIZE_1 * SIZE_2);//第一個數字越大,清晰度就越高,考慮檔案大小的緣故,就調整為1
int frontRotation;
if (rotationRecord == 180) {
//反向的前置
frontRotation = 180;
} else {
//正向的前置
frontRotation = (rotationRecord == 0) ? 270 - frontOri : frontOri; //錄製下來的視屏選擇角度,此處為前置
}
recorder.setOrientationHint((cameraType == 1) ? frontRotation : rotationRecord);
//把攝像頭的畫面給它
recorder.setPreviewDisplay(surfaceHolder.getSurface());
//建立好視訊檔案用來儲存
videoDir();
if (videoFile != null) {
//設定建立好的輸入路徑
recorder.setOutputFile(videoFile.getPath());
recorder.prepare();
recorder.start();
//不能旋轉啦
orientationEventListener.disable();
flagRecord = true;
}
} catch (Exception e) {
//一般沒有錄製許可權或者錄製引數出現問題都走這裡
e.printStackTrace();
//還是沒許可權啊
recorder.reset();
recorder.release();
recorder = null;
showCameraPermission();
FileUtils.deleteFile(videoFile.getPath());
return false;
}
return true;
}
//結束錄製
private void endRecord() {
//反正多次進入,比如surface的destroy和介面onPause
if (!flagRecord) {
return;
}
flagRecord = false;
try {
if (recorder != null) {
recorder.stop();
recorder.reset();
recorder.release();
orientationEventListener.enable();
recorder = null;
}
} catch (Exception e) {
e.printStackTrace();
}
videoTime.stop();
videoTime.setBase(SystemClock.elapsedRealtime());
Intent intent = new Intent(this, PlayActivity.class);
intent.putExtra(PlayActivity.DATA, videoFile.getAbsolutePath());
startActivityForResult(intent, 2222);
overridePendingTransition(R.anim.fab_in, R.anim.fab_out);
}複製程式碼
最後
總的來說,錄製視訊還是蠻簡單的,主要還是視訊的角度問題需要考慮:
- Camera的前置攝像頭角度注意。
- Android本身預設的是橫屏錄製效果,所以需要配置橫屏和豎屏的錄製角度。
- MediaRecorder引數的配置順序。
- Camera和MediaRecorder的解析度和拉伸問題。
- 閃光燈要在開始錄製的時候才開啟。
- 初始化攝像頭和釋放攝像頭需要在surface的surfaceCreated和surfaceDestroyed。
- 注意鎖屏、退到後臺、onPuase的是會走surface的surfaceDestroyed。
- 如果是要一次性上傳很長很長的拍攝視訊,推薦還是找FFMPEG的錄製方式吧,畢經錄製好了再壓縮的做法很費時。
- 告訴IOS,讓他支援視訊元資訊的角度旋轉播放。(不支援?網上那麼多視訊有角度資訊,難道歪著看?)
- 測試如果說前置畫面拍攝出來的視訊左右翻轉,用本機拍一個前置視訊或者照片給他看,不然你只能接FFMPEG了。
<( ̄︶ ̄)↗知道你想說什麼,DMEO在這裡 : github.com/CarGuo/Vide…