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 1150954859@qq.com
 */

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 1150954859@qq.com
 */

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 1150954859@qq.com
 */
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万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览