Android錄視頻(包含文件操作,Mp4文件合併)

本次業務需求是錄不超過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();
    }
第三個 完成錄製
用戶完成錄製時,其實和播放的邏輯差不多,文件合併好後繼續下面的邏輯即可。

發佈了35 篇原創文章 · 獲贊 11 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章