本次業務需求是錄不超過1分鐘的視頻,並且要求能暫停,繼續錄,最後錄製完成時需要有一個完整的視頻文件用於上傳。
方法:暫停時先將前一小段的視頻存下,最終結束錄製時將幾小段視頻合併成一整段保存。
首先,我們的工具類MediaUtils,此處參照https://github.com/Werb/MediaUtils稍作修改,主要需要其輸出的方法有
setTargetDir() 設置輸出視頻文件的目錄
setTargetName() 設置視頻文件名
getTargetFilePath() 獲取最終視頻文件的完整路徑
stopRecordSave() 結束錄製,錄製過程中沒有暫停,錄製的文件保留
combine() 合併多段視頻,合併成功後將原有視頻刪除,將視頻路徑列表清空,將合併後的視頻路徑加入,將合併後的視頻作爲結果視頻
pauseRecordSave() 暫停錄製,並將此段視頻加入待合併的視頻路徑列表
stopRecordUnSave() 停止錄製,刪除錄製過的所有視頻
import android.view.SurfaceHolder;
import android.app.Activity;
import android.graphics.Bitmap;
import android.hardware.Camera;
import android.media.CamcorderProfile;
import android.media.MediaMetadataRetriever;
import android.media.MediaRecorder;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.View;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static cn.reschool.parent.utils.DateUtil.getCurrentDetailTime;
/**
* Created by kLin 11509 on 8/15/2017.
* email [email protected]
*/
public class MediaUtils implements SurfaceHolder.Callback {
private static final String TAG = "MediaUtils";
public static final int MEDIA_AUDIO = 0;
public static final int MEDIA_VIDEO = 1;
private Activity activity;
private MediaRecorder mMediaRecorder;
private CamcorderProfile profile;
private Camera mCamera;
private SurfaceView mSurfaceView;
private SurfaceHolder mSurfaceHolder;
private File targetDir;
private String targetName;
List<String> paths = new ArrayList<>();
private File targetFile;
private int previewWidth, previewHeight;
private int recorderType;
private boolean isRecording;
private GestureDetector mDetector;
private boolean isZoomIn = false;
private int or = 90;
private int cameraPosition = 1;//0代表前置攝像頭,1代表後置攝像頭
public MediaUtils(Activity activity) {
this.activity = activity;
}
public void setRecorderType(int type) {
this.recorderType = type;
}
public void setTargetDir(File file) {
if (!file.exists()) {
file.mkdir();
}
this.targetDir = file;
}
public void setTargetName(String name) {
this.targetName = name;
}
public String getTargetFilePath() {
return targetFile.getPath();
}
public boolean deleteTargetFile() {
if (targetFile.exists()) {
return targetFile.delete();
} else {
return false;
}
}
public void setSurfaceView(SurfaceView view) {
this.mSurfaceView = view;
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.setFixedSize(previewWidth, previewHeight);
mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
mSurfaceHolder.addCallback(this);
mDetector = new GestureDetector(activity, new ZoomGestureListener());
mSurfaceView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
mDetector.onTouchEvent(event);
return true;
}
});
}
// public void setTextureView(AutoFitTextureView view) {
// this.textureView = view;
// initCamera();
// mDetector = new GestureDetector(activity, new ZoomGestureListener());
// this.textureView.setOnTouchListener(new View.OnTouchListener() {
// @Override
// public boolean onTouch(View v, MotionEvent event) {
// mDetector.onTouchEvent(event);
// return true;
// }
// });
// }
public int getPreviewWidth() {
return previewWidth;
}
public int getPreviewHeight() {
return previewHeight;
}
public boolean isRecording() {
return isRecording;
}
public void record() {
if (isRecording) {
try {
mMediaRecorder.stop(); // stop the recording
} catch (RuntimeException e) {
// RuntimeException is thrown when stop() is called immediately after start().
// In this case the output file is not properly constructed ans should be deleted.
Log.d(TAG, "RuntimeException: stop() is called immediately after start()");
//noinspection ResultOfMethodCallIgnored
targetFile.delete();
}
releaseMediaRecorder(); // release the MediaRecorder object
mCamera.lock(); // take camera access back from MediaRecorder
isRecording = false;
} else {
startRecordThread();
}
}
private boolean prepareRecord() {
try {
mMediaRecorder = new MediaRecorder();
if (recorderType == MEDIA_VIDEO) {
mCamera.unlock();
mMediaRecorder.setCamera(mCamera);
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mMediaRecorder.setProfile(profile);
// 實際視屏錄製後的方向
if(cameraPosition == 0){
mMediaRecorder.setOrientationHint(270);
}else {
mMediaRecorder.setOrientationHint(or);
}
} else if (recorderType == MEDIA_AUDIO) {
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
}
targetFile = new File(targetDir, targetName);
mMediaRecorder.setOutputFile(targetFile.getPath());
} catch (Exception e) {
e.printStackTrace();
Log.d("MediaRecorder", "Exception prepareRecord: ");
releaseMediaRecorder();
return false;
}
try {
mMediaRecorder.prepare();
} catch (IllegalStateException e) {
Log.d("MediaRecorder", "IllegalStateException preparing MediaRecorder: " + e.getMessage());
releaseMediaRecorder();
return false;
} catch (IOException e) {
Log.d("MediaRecorder", "IOException preparing MediaRecorder: " + e.getMessage());
releaseMediaRecorder();
return false;
}
return true;
}
public void stopRecordSave() {
Log.d("Recorder", "stopRecordSave");
if (isRecording) {
isRecording = false;
try {
mMediaRecorder.stop();
Log.d("Recorder", targetFile.getPath());
} catch (RuntimeException r) {
Log.d("Recorder", "RuntimeException: stop() is called immediately after start()");
} finally {
releaseMediaRecorder();
}
}
}
public boolean combine(){
if (paths == null || 1>= paths.size()) {
return false;
}else {
String combineMp3Path = targetDir +"/"+ getCurrentDetailTime() + "-combine.mp4";
String[] strings = new String[paths.size()];
for (int i = 0; i < paths.size(); i++) {
strings[i] = paths.get(i);
}
VideoSplicing videoSplicing = new VideoSplicing(activity,strings,combineMp3Path);
videoSplicing.videoSplice();
targetFile = new File(combineMp3Path);
if (targetFile.exists()) {
for (int i = 0; i < paths.size(); i++) {
File f = new File(paths.get(i));
f.delete();
}
paths.clear();
}
paths.add(combineMp3Path);
return true;
}
}
public void pauseRecordSave() {
Log.d("Recorder", "stopRecordSave");
if (isRecording) {
isRecording = false;
try {
mMediaRecorder.stop();
Log.d("Recorder", targetFile.getPath());
paths.add(targetDir + "/"+ targetName);
} catch (RuntimeException r) {
Log.d("Recorder", "RuntimeException: stop() is called immediately after start()");
} finally {
releaseMediaRecorder();
}
}
}
public void stopRecordUnSave() {
Log.d("Recorder", "stopRecordUnSave");
for (int i = 0; i < paths.size(); i++) {
File f = new File(paths.get(i));
f.delete();
}
paths.clear();
if (isRecording) {
isRecording = false;
try {
mMediaRecorder.stop();
} catch (RuntimeException r) {
Log.d("Recorder", "RuntimeException: stop() is called immediately after start()");
if (targetFile.exists()) {
//不保存直接刪掉
targetFile.delete();
}
} finally {
releaseMediaRecorder();
}
if (targetFile.exists()) {
//不保存直接刪掉
targetFile.delete();
}
}
}
private void startPreView(SurfaceHolder holder) {
if (mCamera == null) {
mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
}
if (mCamera != null) {
mCamera.setDisplayOrientation(or);
try {
mCamera.setPreviewDisplay(holder);
Camera.Parameters parameters = mCamera.getParameters();
List<Camera.Size> mSupportedPreviewSizes = parameters.getSupportedPreviewSizes();
List<Camera.Size> mSupportedVideoSizes = parameters.getSupportedVideoSizes();
Camera.Size optimalSize = CameraHelper.getOptimalVideoSize(mSupportedVideoSizes,
mSupportedPreviewSizes, mSurfaceView.getWidth(), mSurfaceView.getHeight());
// Use the same size for recording profile.
previewWidth = optimalSize.width;
previewHeight = optimalSize.height;
parameters.setPreviewSize(previewWidth, previewHeight);
profile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);
// 這裏是重點,分辨率和比特率
// 分辨率越大視頻大小越大,比特率越大視頻越清晰
// 清晰度由比特率決定,視頻尺寸和像素量由分辨率決定
// 比特率越高越清晰(前提是分辨率保持不變),分辨率越大視頻尺寸越大。
profile.videoFrameWidth = optimalSize.width;
profile.videoFrameHeight = optimalSize.height;
// 這樣設置 1080p的視頻 大小在5M , 可根據自己需求調節
profile.videoBitRate = 2 * optimalSize.width * optimalSize.height;
List<String> focusModes = parameters.getSupportedFocusModes();
if (focusModes != null) {
for (String mode : focusModes) {
mode.contains("continuous-video");
parameters.setFocusMode("continuous-video");
}
}
mCamera.setParameters(parameters);
mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void releaseMediaRecorder() {
if (mMediaRecorder != null) {
// clear recorder configuration
mMediaRecorder.reset();
// release the recorder object
mMediaRecorder.release();
mMediaRecorder = null;
// Lock camera for later use i.e taking it back from MediaRecorder.
// MediaRecorder doesn't need it anymore and we will release it if the activity pauses.
Log.d("Recorder", "release Recorder");
}
}
private void releaseCamera() {
if (mCamera != null) {
// release the camera for other applications
mCamera.release();
mCamera = null;
Log.d("Recorder", "release Camera");
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mSurfaceHolder = holder;
startPreView(holder);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (mCamera != null) {
Log.d(TAG, "surfaceDestroyed: ");
releaseCamera();
}
if (mMediaRecorder != null) {
releaseMediaRecorder();
}
}
private void startRecordThread() {
if (prepareRecord()) {
try {
mMediaRecorder.start();
isRecording = true;
Log.d("Recorder", "Start Record");
} catch (RuntimeException r) {
releaseMediaRecorder();
Log.d("Recorder", "RuntimeException: start() is called immediately after stop()");
}
}
}
private class ZoomGestureListener extends GestureDetector.SimpleOnGestureListener {
//雙擊手勢事件
@Override
public boolean onDoubleTap(MotionEvent e) {
super.onDoubleTap(e);
Log.d(TAG, "onDoubleTap: 雙擊事件");
if (!isZoomIn) {
setZoom(20);
isZoomIn = true;
} else {
setZoom(0);
isZoomIn = false;
}
return true;
}
}
private void setZoom(int zoomValue) {
if (mCamera != null) {
Camera.Parameters parameters = mCamera.getParameters();
if (parameters.isZoomSupported()) {
int maxZoom = parameters.getMaxZoom();
if (maxZoom == 0) {
return;
}
if (zoomValue > maxZoom) {
zoomValue = maxZoom;
}
parameters.setZoom(zoomValue);
mCamera.setParameters(parameters);
}
}
}
private String getVideoThumb(String path) {
MediaMetadataRetriever media = new MediaMetadataRetriever();
media.setDataSource(path);
return bitmap2File(media.getFrameAtTime());
}
private String bitmap2File(Bitmap bitmap) {
File thumbFile = new File(targetDir,
targetName);
if (thumbFile.exists()) thumbFile.delete();
FileOutputStream fOut;
try {
fOut = new FileOutputStream(thumbFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fOut);
fOut.flush();
fOut.close();
} catch (IOException e) {
return null;
}
return thumbFile.getAbsolutePath();
}
public void switchCamera() {
int cameraCount = 0;
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
cameraCount = Camera.getNumberOfCameras();//得到攝像頭的個數
for (int i = 0; i < cameraCount; i++) {
Camera.getCameraInfo(i, cameraInfo);//得到每一個攝像頭的信息
if (cameraPosition == 1) {
//現在是後置,變更爲前置
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {//代表攝像頭的方位,CAMERA_FACING_FRONT前置 CAMERA_FACING_BACK後置
mCamera.stopPreview();//停掉原來攝像頭的預覽
mCamera.release();//釋放資源
mCamera = null;//取消原來攝像頭
mCamera = Camera.open(i);//打開當前選中的攝像頭
startPreView(mSurfaceHolder);
cameraPosition = 0;
break;
}
} else {
//現在是前置, 變更爲後置
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {//代表攝像頭的方位,CAMERA_FACING_FRONT前置 CAMERA_FACING_BACK後置
mCamera.stopPreview();//停掉原來攝像頭的預覽
mCamera.release();//釋放資源
mCamera = null;//取消原來攝像頭
mCamera = Camera.open(i);//打開當前選中的攝像頭
startPreView(mSurfaceHolder);
cameraPosition = 1;
break;
}
}
}
}
}
上面工具類中的combine()方法使用了另一個工具類VideoSplicing,這個方法是使用mp4parser進行視頻合併
這裏需要引入進來的各種類需要我們向項目app/libs目錄下放入一個文件,在app/build.gradle中配置這個文件 compile files('libs/isoviewer-1.0-RC-27.jar')
import android.content.Context;
import android.widget.Toast;
import com.coremedia.iso.boxes.Container;
import com.googlecode.mp4parser.authoring.Movie;
import com.googlecode.mp4parser.authoring.Track;
import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder;
import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator;
import com.googlecode.mp4parser.authoring.tracks.AppendTrack;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* Created by kLin 11509 on 8/15/2017.
* email [email protected]
*/
public class VideoSplicing {
private Context context;
private String[] videoUris;
private String output;
public VideoSplicing(Context context,String[] videoUris,String output){
this.context=context;
this.videoUris=videoUris;
this.output=output;
}
public void videoSplice(){
//下面是github上mp4parser源碼,就可以拼接視頻也可以拼接聲音
try {
List<Movie> inMovies = new ArrayList<Movie>();
for (String videoUri : videoUris) {
inMovies.add(MovieCreator.build(videoUri));
}
List<Track> videoTracks = new LinkedList<Track>();
List<Track> audioTracks = new LinkedList<Track>();
for (Movie m : inMovies) {
for (Track t : m.getTracks()) {
if (t.getHandler().equals("soun")) {
audioTracks.add(t);
}
if (t.getHandler().equals("vide")) {
videoTracks.add(t);
}
}
}
Movie result = new Movie();
if (!audioTracks.isEmpty()) {
result.addTrack(new AppendTrack(audioTracks.toArray(new Track[audioTracks.size()])));
}
if (!videoTracks.isEmpty()) {
result.addTrack(new AppendTrack(videoTracks.toArray(new Track[videoTracks.size()])));
}
Container out = new DefaultMp4Builder().build(result);
FileChannel fc = new RandomAccessFile(output, "rw").getChannel();
out.writeContainer(fc);
fc.close();
Toast.makeText(context, "保存成功",
Toast.LENGTH_LONG).show();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(context, "拼接失敗",
Toast.LENGTH_LONG).show();
}
}
}
MediaUtils中還有一個工具類CameraHelper,業務不涉及其實現過程,故而代碼放入即可
/**
* Created by kLin 11509 on 8/15/2017.
* email [email protected]
*/
import android.annotation.TargetApi;
import android.hardware.Camera;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
public class CameraHelper {
public static final int MEDIA_TYPE_IMAGE = 1;
public static final int MEDIA_TYPE_VIDEO = 2;
/**
* Iterate over supported camera video sizes to see which one best fits the
* dimensions of the given view while maintaining the aspect ratio. If none can,
* be lenient with the aspect ratio.
*
* @param supportedVideoSizes Supported camera video sizes.
* @param previewSizes Supported camera preview sizes.
* @param w The width of the view.
* @param h The height of the view.
* @return Best match camera video size to fit in the view.
*/
public static Camera.Size getOptimalVideoSize(List<Camera.Size> supportedVideoSizes,
List<Camera.Size> previewSizes, int w, int h) {
// Use a very small tolerance because we want an exact match.
final double ASPECT_TOLERANCE = 0.1;
double targetRatio = (double) w / h;
// Supported video sizes list might be null, it means that we are allowed to use the preview
// sizes
List<Camera.Size> videoSizes;
if (supportedVideoSizes != null) {
videoSizes = supportedVideoSizes;
} else {
videoSizes = previewSizes;
}
Camera.Size optimalSize = null;
// Start with max value and refine as we iterate over available video sizes. This is the
// minimum difference between view and camera height.
double minDiff = Double.MAX_VALUE;
// Target view height
int targetHeight = h;
// Try to find a video size that matches aspect ratio and the target view size.
// Iterate over all available sizes and pick the largest size that can fit in the view and
// still maintain the aspect ratio.
for (Camera.Size size : videoSizes) {
double ratio = (double) size.width / size.height;
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE)
continue;
if (Math.abs(size.height - targetHeight) < minDiff && previewSizes.contains(size)) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
// Cannot find video size that matches the aspect ratio, ignore the requirement
if (optimalSize == null) {
minDiff = Double.MAX_VALUE;
for (Camera.Size size : videoSizes) {
if (Math.abs(size.height - targetHeight) < minDiff && previewSizes.contains(size)) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
}
return optimalSize;
}
/**
* @return the default camera on the device. Return null if there is no camera on the device.
*/
public static Camera getDefaultCameraInstance() {
return Camera.open();
}
/**
* @return the default rear/back facing camera on the device. Returns null if camera is not
* available.
*/
public static Camera getDefaultBackFacingCameraInstance() {
return getDefaultCamera(Camera.CameraInfo.CAMERA_FACING_BACK);
}
/**
* @return the default front facing camera on the device. Returns null if camera is not
* available.
*/
public static Camera getDefaultFrontFacingCameraInstance() {
return getDefaultCamera(Camera.CameraInfo.CAMERA_FACING_FRONT);
}
/**
*
* @param position Physical position of the camera i.e Camera.CameraInfo.CAMERA_FACING_FRONT
* or Camera.CameraInfo.CAMERA_FACING_BACK.
* @return the default camera on the device. Returns null if camera is not available.
*/
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
private static Camera getDefaultCamera(int position) {
// Find the total number of cameras available
int mNumberOfCameras = Camera.getNumberOfCameras();
// Find the ID of the back-facing ("default") camera
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
for (int i = 0; i < mNumberOfCameras; i++) {
Camera.getCameraInfo(i, cameraInfo);
if (cameraInfo.facing == position) {
return Camera.open(i);
}
}
return null;
}
/**
* Creates a media file in the {@code Environment.DIRECTORY_PICTURES} directory. The directory
* is persistent and available to other applications like gallery.
*
* @param type Media type. Can be video or image.
* @return A file object pointing to the newly created file.
*/
public static File getOutputMediaFile(int type){
// To be safe, you should check that the SDCard is mounted
// using Environment.getExternalStorageState() before doing this.
if (!Environment.getExternalStorageState().equalsIgnoreCase(Environment.MEDIA_MOUNTED)) {
return null;
}
File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), "CameraSample");
// This location works best if you want the created images to be shared
// between applications and persist after your app has been uninstalled.
// Create the storage directory if it does not exist
if (! mediaStorageDir.exists()){
if (! mediaStorageDir.mkdirs()) {
Log.d("CameraSample", "failed to create directory");
return null;
}
}
// Create a media file name
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
File mediaFile;
if (type == MEDIA_TYPE_IMAGE){
mediaFile = new File(mediaStorageDir.getPath() + File.separator +
"IMG_"+ timeStamp + ".jpg");
} else if(type == MEDIA_TYPE_VIDEO) {
mediaFile = new File(mediaStorageDir.getPath() + File.separator +
"VID_"+ timeStamp + ".mp4");
} else {
return null;
}
return mediaFile;
}
}
下面即將進入我們錄製視頻的VideoRecorderActivity,在進入之前一定要確保視頻錄製,音頻錄製,修改本地存儲的權限都已經獲得
佈局activity_video_recorder如下,主體是SurfaceView用於Preview和錄製,VideoView用於播放,然後一個開始/暫停按鈕,在屏幕的底端,然後暫停錄製時在開始暫停按鈕的上方出現一排按鈕分別是重錄,播放,和完成,屏幕頂端一個半透明的Toolbar,顯示一個一分鐘時長的倒計時,以及返回按鈕。
這個倒計時是引入https://github.com/iwgang/CountdownView
其中
app:isShowDay="false"
app:isShowHour="false"
app:isShowMillisecond="false"
app:isShowMinute="true"
app:isShowSecond="true"
控制顯示什麼位(天,時分秒,毫秒)
app:suffixGravity="center"
app:suffixMinute=":"
app:suffixTextColor="@color/white"
app:suffixTextSize="12sp"
後綴的對齊方式,某一位(分鐘位)的後綴符號或文字,後綴字體大小和顏色
app:timeTextColor="@color/white"
app:timeTextSize="12sp"
時間位的字體大小和顏色
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<RelativeLayout
android:id="@+id/bottom_view"
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingTop="@dimen/material_grid"
android:paddingBottom="@dimen/material_grid"
android:layout_alignParentBottom="true">
<Button
android:id="@+id/bt_start_pause"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/start_recording"
android:textColor="@color/white"
android:background="@color/orange"/>
</RelativeLayout>
<LinearLayout
android:id="@+id/after_pause_view"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_above="@id/bottom_view"
android:elevation="2dp"
android:visibility="gone">
<TextView
android:id="@+id/bt_redo"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="@dimen/small_text_padding"
android:paddingBottom="@dimen/small_text_padding"
android:drawableTop="@mipmap/icon_recorded_blue"
android:text="@string/redo_recording" />
<TextView
android:id="@+id/bt_play"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="@dimen/small_text_padding"
android:paddingBottom="@dimen/small_text_padding"
android:drawableTop="@mipmap/icon_hear_blue"
android:text="@string/check_recording" />
<TextView
android:id="@+id/bt_send"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="@dimen/small_text_padding"
android:paddingBottom="@dimen/small_text_padding"
android:drawableTop="@mipmap/icon_complete_blue"
android:text="@string/finish_recording" />
</LinearLayout>
<SurfaceView
android:id="@+id/main_surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/after_pause_view"/>
<VideoView
android:id="@+id/main_video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:layout_above="@id/after_pause_view"/>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="3dp"
android:layout_above="@id/bottom_view"
android:visibility="gone"
android:progressDrawable="@drawable/progressbar_carch" />
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
style="@style/ToolBarStyle"
android:background="#6f000000">
<cn.iwgang.countdownview.CountdownView
android:id="@+id/count_down"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@android:color/white"
android:textSize="20sp"
app:isShowDay="false"
app:isShowHour="false"
app:isShowMillisecond="false"
app:isShowMinute="true"
app:isShowSecond="true"
app:suffixGravity="center"
app:suffixMinute=":"
app:suffixTextColor="@color/white"
app:suffixTextSize="12sp"
app:timeTextColor="@color/white"
app:timeTextSize="12sp"/>
<TextView
android:id="@+id/toolbar_title"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@android:color/white"
android:textSize="20sp"
tools:text="@string/app_name"/>
</android.support.v7.widget.Toolbar>
</RelativeLayout>
進入Activity當中
各種定義,
isRecording 是否在錄像
isPausing 是否在暫停
handler控制視頻進度
@BindView(R.id.main_surface_view)
SurfaceView mSurfaceView;
@BindView(R.id.bt_start_pause)
Button btStartPause;
@BindView(R.id.progressBar)
ProgressBar progressBar;
@BindView(R.id.after_pause_view)
LinearLayout afterPauseLayout;
@BindView(R.id.count_down)
CountdownView countdownView;
@BindView(R.id.bt_redo)
TextView btRedo;
@BindView(R.id.bt_play)
TextView btPlay;
@BindView(R.id.bt_send)
TextView btSend;
@BindView(R.id.main_video_view)
VideoView mVideoView;
private MediaUtils mediaUtils;
boolean isRecording = false;
boolean isPausing = false;
private int mProgress;
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0:
progressBar.setProgress(mProgress);
if (mediaUtils.isRecording()) {
mProgress = mProgress + 1;
sendMessageDelayed(handler.obtainMessage(0), 100);
}
break;
}
}
};
private ProgressDialog progressDialog;
界面控件綁定完成後,完成一些錄製的初始化工作
初始化mediaUtils,設置錄製類型,文件存儲路徑,綁定SurfaceView,
初始化VideoView,播放完之後將SurfaceView顯示,VideoView隱藏
設置倒計時的結束監聽,結束時走完成錄製邏輯,錄製開關設爲false,開始/暫停按鈕文案,錄製文件的業務處理
mediaUtils = new MediaUtils(this);
mediaUtils.setRecorderType(MediaUtils.MEDIA_VIDEO);
mediaUtils.setTargetDir(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES));
mediaUtils.setTargetName(getCurrentDetailTime() + ".mp4");
mediaUtils.setSurfaceView(mSurfaceView);
mVideoView.setMediaController(new MediaController(this));
mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mVideoView.setVisibility(View.GONE);
mSurfaceView.setVisibility(View.VISIBLE);
}
});
countdownView.setOnCountdownEndListener(new CountdownView.OnCountdownEndListener() {
@Override
public void onEnd(CountdownView cv) {
isRecording = false;
btStartPause.setText(R.string.start_recording);
mediaUtils.stopRecordSave();
}
});
點擊開始/暫停按鈕的邏輯,錄製開關置反後,如果是錄製狀態,開始/暫停按鈕文案設爲暫停,mediaUtils調用錄製,調用startView()方法,暫停狀態開關置爲false,
如果不是錄製狀態,而且又是點擊開始/暫停按鈕導致的狀態變化,那麼一定是進入了暫停,所以暫停狀態開關置爲true,開始/暫停按鈕文案設爲繼續,mediaUtils調用暫停並保存一段視頻,待合併的文件列表增加一段,mediaUtils將文件名更新,下一次點擊開始/暫停按鈕繼續進行下一段的錄製時使用新文件名,調用stopView()方法。
@OnClick(R.id.bt_start_pause)
public void onStartPauseClicked() {
isRecording = !isRecording;
if (isRecording) {
btStartPause.setText(R.string.pause);
mediaUtils.record();
startView();
isPausing = false;
} else {
isPausing = true;
btStartPause.setText(R.string.continue_recording);
if (isPausing) {
mediaUtils.pauseRecordSave();
}
mediaUtils.setTargetName(getCurrentDetailTime() + ".mp4");
stopView();
}
}
startView()和stopView()主要控制了錄製進度,暫停按鈕欄的顯示與隱藏,以及倒計時的變化
private void startView() {
progressBar.setVisibility(View.VISIBLE);
afterPauseLayout.setVisibility(View.GONE);
mProgress = 0;
if (isPausing) {
countdownView.restart();
}else {
countdownView.start((long) 60 * 1000);
}
handler.removeMessages(0);
handler.sendMessage(handler.obtainMessage(0));
}
private void stopView() {
progressBar.setVisibility(View.GONE);
afterPauseLayout.setVisibility(View.VISIBLE);
mProgress = 0;
if (isPausing) {
countdownView.pause();
}
handler.removeMessages(0);
}
暫停時三個按鈕的業務邏輯
第一個 重錄
用戶點擊重錄時,mediaUtils清理臨時存下的視頻文件,開始/暫停文案調整,兩個開關全部置否,這三個按鈕消失,倒計時重置,等待用戶再次點擊開始按鈕錄製
@OnClick(R.id.bt_redo)
public void onBtRedoClicked() {
mediaUtils.stopRecordUnSave();
btStartPause.setText(R.string.start_recording);
isRecording = false;
isPausing = false;
afterPauseLayout.setVisibility(View.GONE);
countdownView.allShowZero();
}
第二個 播放
用戶點擊播放時,要先將之前錄製的視頻小段合併,VideoView顯示並播放combine的結果文件,SurfaceView隱藏
@OnClick(R.id.bt_play)
public void onBtPlayClicked() {
mediaUtils.combine();
mVideoView.setVisibility(View.VISIBLE);
mSurfaceView.setVisibility(View.GONE);
mVideoView.setVideoPath(mediaUtils.getTargetFilePath());
mVideoView.start();
}
第三個 完成錄製用戶完成錄製時,其實和播放的邏輯差不多,文件合併好後繼續下面的邏輯即可。