Android录制小视频(仿微信小视频)

Android录制小视频

一、概述

日常生活中,录制一些视频已经渐渐成为一种习惯,当然这对于我们技术来说并没有什么影响,因为无论大家用不用,你都需要开发,这只是需求制定者–PM应该关心的事情,我们需要关心的是视频开发的过程以及难点还有会碰上什么坑,这才是技术应该想的事情。不过,市场上面的视频以及直播的App确实也是与日俱增,蝌蚪音客、美拍、小影,小咖秀,快手等等。这类App的技术难点基本都是在音视频处理这一块,iOS对多媒体处理的支持比较丰富,但是Android就会差很多,不是总有着ios程序员的一句话:“我们系统支持呀……”,这时候你除了心里呵呵,好像也没有别的办法了哈,那么作为Android的屌丝,还是老老实实研究一下自己该怎么去爬坑吧。

二、类型

大家都知道Android原生录制比例是1:1,当然这并不是说我们万能的Android开发者就没有别的渠道,下面就是比较高大上的用法:

1.这时候大可以放弃原生的接口,使用FFmpeg和OpenCV进行录制,但是缺点也是明显的,多机型兼容复杂并且要求开发者一定程度的C语言功底,但是最难解决的问题是性能问题,FFmeg和OpenCV都是开源方案,如果要真正达实用级别往往还需要优化定制,这对于熟练于做Android展现的开发者来说完全就是一个新的领域。

2.使用原生API录制,做一些遮罩等处理,例如微信未更新钱的小视频,就是这样的展现形式,当然正常的录制就没什么大问题了,就是各种适配麻烦点,其他的都好说,而且对于开发者来说也能更好的接受。

三、视频录制
(一)、第一种方式可以直接调用系统录像

         // 激活系统的照相机进行录像
        Intent intent = new Intent();
        intent.setAction("android.media.action.VIDEO_CAPTURE");
        intent.addCategory("android.intent.category.DEFAULT");
        // 保存录像到指定的路径
        File file = new File("/sdcard/video.3pg");
        Uri uri = Uri.fromFile(file);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
        startActivityForResult(intent, 0);

(二)、当然大部分需求都需要自己定义并且修改某些属性,下面着重说一下自定义写法。
1.自定义录像布局

<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/background_dark"
        android:orientation="vertical">

        <SurfaceView
            android:id="@+id/surfaceview"
            android:layout_width="fill_parent"
            android:layout_height="0dp"
            android:layout_weight="1" />

        <ProgressBar
            android:id="@+id/progressBar"
            style="?android:attr/progressBarStyleHorizontal"
            android:layout_width="match_parent"
            android:layout_height="3dp" />
        </LinearLayout>   

surfaceView主要使用来显示录制视频的,progressbar显示进度条。

2.定义自定义RecordVideo

@SuppressLint("NewApi")
    public RecordVideo(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        // 初始化各项组件
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RecordVideo, defStyle, 0);
        mWidth = typedArray.getInteger(R.styleable.RecordVideo_witdh, 320);// 默认320
        mHeight = typedArray.getInteger(R.styleable.RecordVideo_height, 240);// 默认240
        isOpenCamera = typedArray.getBoolean(R.styleable.RecordVideo_open_camera, true);// 默认打开
        mRecordMaxTime=typedArray.getInteger(R.styleable.RecordVideo_timeLenght,60);//默认为10

        LayoutInflater.from(context).inflate(R.layout.movie_recorder_view, this);
        mSurfaceView = (SurfaceView) findViewById(R.id.surfaceview);
        mProgressBar = (ProgressBar) findViewById(R.id.progressBar);
        mProgressBar.setMax(mRecordMaxTime);// 设置进度条最大量

        mSurfaceHolder = mSurfaceView.getHolder();
        mSurfaceHolder.addCallback(new CustomCallBack());
        mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        //释放资源
        typedArray.recycle();
    }

初始化使用的组件,把第一步surfaceView设置进来

3.定义一个CallBack,并且初始化摄像头这个必备资源

 private class CustomCallBack implements SurfaceHolder.Callback {
        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            if (!isOpenCamera)
                return;
            initCamera();
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            if (!isOpenCamera)
                return;
            releaseCameraResource();
        }
    }
     //初始化摄像
    @SuppressLint("NewApi")
    public void initCamera()  {
        if (mCamera != null) {
            releaseCameraResource();
        }
        try {
            mCamera = Camera.open();
            mCamera.setDisplayOrientation(90);
            mCamera.setPreviewDisplay(mSurfaceHolder);
        } catch (Exception e) {
            //提示用户权限并且释放创建资源
            releaseCameraResource();
        }
        if (mCamera == null)
            return;
        mCamera.startPreview();
        mCamera.unlock();
    }
   //释放摄像头资源
    private void releaseCameraResource() {
        if (mCamera != null) {
            mCamera.setPreviewCallback(null);
            mCamera.stopPreview();
            mCamera.lock();
            mCamera.release();
            mCamera = null;
        }
    }

4.进入主题,初始化摄像一些基本参数

//初始化摄像头基本参数
    private void initRecord() {
        mMediaRecorder = new MediaRecorder();
        mMediaRecorder.reset();
        if (mCamera != null)
            mMediaRecorder.setCamera(mCamera);
        mMediaRecorder.setOnErrorListener(this);
        mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);//视频源
        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);//音频源
        mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);//视频输出格式
        mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);//音频格式
        mMediaRecorder.setVideoSize(mWidth, mHeight);//设置分辨率
        //设置帧频率,可以按照需求适当调整,这个直接相关录制视频的大小
        mMediaRecorder.setVideoEncodingBitRate(1 * 1024 * 1024);
        mMediaRecorder.setOrientationHint(90);// 输出旋转90度,保持竖屏录制
        mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);// 视频录制格式
        mMediaRecorder.setOutputFile(mRecordFile.getAbsolutePath());
        try {
            mMediaRecorder.prepare();
            mMediaRecorder.start();
        } catch (Exception e) {
            //提示录制权限存在问题,打开相关权限
        }
    }

上面比较注意的一个是设置帧频率,这个会影响视频录制后的大小;一个是MPEG_4格式,其实就是MP4格式,另一个就是H264格式,这个跟编解码有关,经过测试,设置H264格式会使Android跟ios通用,其他格式会出现视频在ios上面无法播放的情况,望大家多多注意。

5.下面就进入激动人心的录制视频的时刻,这里面产生的坑后面会说明一些碰到的

    /**
     * 开始录制视频
     * @param onRecordFinishListener 达到指定时间之后回调接口
     */
    public void record(OnRecordFinishListener onRecordFinishListener) {
        this.mOnRecordFinishListener = onRecordFinishListener;
        createRecordDir();
        if (!isOpenCamera)// 如果未打开摄像头,则打开
            initCamera();
        initRecord();
        mTimeCount = 0;// 时间计数器重新赋值
        mTimer = new Timer();
        mTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                mTimeCount++;
                mProgressBar.setProgress(mTimeCount);// 设置进度条
                if (mTimeCount == mRecordMaxTime) {// 达到指定时间,停止拍摄
                    stop();
                    if (mOnRecordFinishListener != null)
                        mOnRecordFinishListener.onRecordFinish();
                }
            }
        }, 0, 1000);
    }
//创建录制文件存放的文件夹以及录制文件名称
    private void createRecordDir() {
        File sampleDir = new File(Environment.getExternalStorageDirectory() + File.separator + "demo/video/");
        if (!sampleDir.exists()) {
            sampleDir.mkdirs();
        }
        File vecordDir = sampleDir;
        // 创建文件
        try {
            mRecordFile = File.createTempFile("example", ".mp4", vecordDir); //mp4格式
        } catch (IOException e) {
            //打开文件操作相关权限
        }
    }

录制时候切记要判断下用户摄像头的权限,假如用户在录制前关闭权限会导致crash的问题,所以判断下,防止误操作的产生

7.停止录制

//停止录制
public void stop() {
        stopRecord();
        releaseRecord();
        releaseCameraResource();
    }
//停止录制
    public void stopRecord() {
        mProgressBar.setProgress(0);
        if (mTimer != null)
            mTimer.cancel();
        if (mMediaRecorder != null) {
            // 设置后不会崩
            mMediaRecorder.setOnErrorListener(null);
            try {
                mMediaRecorder.stop();
            } catch (Exception e) {
                return;
            }
            mMediaRecorder.setPreviewDisplay(null);
        }
    }
//释放资源
    private void releaseRecord() {
        if (mMediaRecorder != null) {
            mMediaRecorder.setOnErrorListener(null);
            try {
                mMediaRecorder.release();
            } catch (Exception e) {
                return;
            }
        }
        mMediaRecorder = null;
    }

停止录制时候清空一些必要的资源,减轻App的负担,停止时候记得设置错误监听,做不做操作可以自己进行设置,但是一定要设置,否则系统不会回调,直接异常。

8.相关的其他方法以及接口

    public int getTimeCount() {
        return mTimeCount;
    }

    public File getmRecordFile() {
        return mRecordFile;
    }

    //录制完成回调接口
    public interface OnRecordFinishListener {
        public void onRecordFinish();
    }

    @Override
    public void onError(MediaRecorder mr, int what, int extra) {
        try {
            if (mr != null)
                mr.reset();
        } catch (Exception e) {
            //如果录制有错误,需要做相关操作来提醒用户
        }
    }

9.最后设置Activity就可以进行视频录制了

<?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"
    android:background="#64000000">

    <RelativeLayout
        android:id="@+id/record_video"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:background="@color/common_black"
        android:layout_alignParentBottom="true">

        <Button
            android:id="@+id/shoot_button"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:layout_centerHorizontal="true"
            android:layout_centerVertical="true"
            android:background="@drawable/bg_movie_add" />
    </RelativeLayout>

    <com.demo.RecordVideo
        android:id="@+id/movieRecorderView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@id/record_video" />
</RelativeLayout>

Activity里面按钮处理代码,这里主要需要主要需要onTouch进行监听,因为可能会出现短暂的按下立马松开,这样是需要做处理的

mRecorderView = (RecordVideo) findViewById(R.id.movieRecorderView);
        mShootBtn = (Button) findViewById(R.id.shoot_button);
        mShootBtn.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                        mRecorderView.record(new RecordVideo.OnRecordFinishListener() {
                            @Override
                            public void onRecordFinish() {
                                handler.sendEmptyMessage(1);
                            }
                        });
                    } else if (event.getAction() == MotionEvent.ACTION_UP) {
                        if (mRecorderView.getTimeCount() > 1)
                            handler.sendEmptyMessage(1);
                        else {
                            if (mRecorderView.getmRecordFile() != null) {
                                mRecorderView.getmRecordFile().delete();
                            }
                            mRecorderView.stop();
                            mRecorderView.initCamera();
                            Toast.makeText(RecordVideoActivity.this,"视频录制时间太短", Toast.LENGTH_SHORT).show();
                        }
                    }   
                }
            }
private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            finishActivity();
        }
    };

    private void finishActivity() {
        if (isFinish) {
            mRecorderView.stop();
            // 返回到播放页面
            Intent intent = new Intent();
            intent.putExtra("path", mRecorderView.getmRecordFile().getAbsolutePath());
            setResult(RESULT_OK, intent);
        }
        finish();
    }

这里面可能会碰到的问题,在过程中已经描述了几个需要注意的点,后面还有比较难搞的点就是权限,这个可以参考下权限检查

项目地址github
最后,欢迎讨论的小伙伴。
个人邮箱: jsmeli@163.com

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值