Android开发探索

MediaRecorder结合SurfaceView录制视频

2018-08-07  本文已影响82人  安仔夏天勤奋

手机一般都有麦克风和摄像头,而Android系统就可以利用这些硬件来录制音视频了。为了增加对录制音视频的支持,Android系统提供了一个MediaRecorder的类。使用MediaRecorder结合SurfaceView录制视频。下面先简单了解一下MediaRecorder这个类。

MediaRecorder应用实例

MediaRecorder功能设置(方法/作用)

MediaRecorder内的嵌套类

MediaRecorder.AudioEncoder

大家都知道在录音的时候都要调用setAudioEncoder()方法,这个方法里面总有不同的参数,这个类就是参数的值,这里说一下各个不同值的区别:

default: 默认值。
AAC: 高级音频编码,简单说下优缺点:

AAC优点:相对于mp3,AAC格式的音质更佳,文件更小。
AAC不足:AAC属于有损压缩的格式,与时下流行的APE、FLAC等无损格式相比音质存在”本质上”的差距。加之,传输速度更快的USB3.0和16G以上大容量MP3正在加速普及,也使得AAC头上”小巧”的光环不复存在。

HE_AAC: HE-AAC混合了AAC与SBR技术。
AAC_ELD: 低延时的AAC音频编解码器。
AMR_NB: 编码的是无视频纯声音3gp文件就是amr,他的文件比AAC的小,音乐效果没ACC的好。
AMR_WB: VMR-WB 是新型可变速率多模式宽带语音编解码器,专为无线 CDMA 2000标准而设计,目的在于在 50 至 7000 HZ 的频带上进行语音编码,采样率为 16 KHZ。VMR-WB 基于 3GPP AMR-WB (G722.2) 编解码器,在每秒速率12.65 Kbit 上可实现互操作。
VORBIS: Vorbis是一种新的音频压缩格式,类似于MP3等现有的音乐格式。但有一点不同的是,它是完全免费、开放和没有专利限制的。OGG Vorbis有一个很出众的特点,就是支持多声道,随着它的流行,以后用随身听来听DTS编码的多声道作品将不会是梦想。

MediaRecorder.AudioSource

这个类对应setAudioSource(int) 方法,主要用来设置音频源; MediaRecorder.AudioSource音频参数说明如下:
MediaRecorder.AudioSource.CAMCORDER 设定录音来源于同方向的相机麦克风相同,若相机无内置相机或无法识别,则使用预设的麦克风
MediaRecorder.AudioSource.DEFAULT 默认音频源
MediaRecorder.AudioSource.MIC 设定录音来源为主麦克风。
MediaRecorder.AudioSource.VOICE_CALL设定录音来源为语音拨出的语音与对方说话的声音
MediaRecorder.AudioSource.VOICE_COMMUNICATION 摄像头旁边的麦克风
MediaRecorder.AudioSource.VOICE_DOWNLINK 下行声音
MediaRecorder.AudioSource.VOICE_RECOGNITION 语音识别
MediaRecorder.AudioSource.VOICE_UPLINK 上行声音
MediaRecorder.VideoEncoder

通过setVideoEncoder(int)来设置视频编码格式。
default: 默认编码
H263: H.263 多用于视频传输,其优点是压缩后体积小,占用带宽少;
MPEG_4_SP: 码率低代表它无需高码率即可有很好的视频效果,H264就更好了
H264 也是用于网络视频传输,优点也和H263差不多;再是H264会比前两者更优秀一点,不过一般用在标清或者高清压缩比较多。
VP8: 据说比H264优秀。
HEVC: 一种新的视频压缩标准。可以替代H.264/ AVC编码标准。它将在H.264标准2至4倍的复杂度基础上,将压缩效率提升一倍以上。

MediaRecorder.VideoSource

通过setVideoSource(int)方法,设置视频的来源。
CAMERA: 视频数据来源摄像头
DEFAULT: 系统默认
SURFACE: 视频数据来源于Surface

MediaRecorder.OutputFormat

通过setOutputFormat(int)方法来控制视频输出的格式:同理列举下各个参数的说明:
AAC_ADTS: ADTS的全称是Audio Data Transport Stream。是AAC音频的传输流格式。是AAC的一种非常常见的传输格式,
AMR_NB: 编码的是无视频纯声音3gp文件就是amr,他的文件比AAC的小,他的音乐效果没ACC的好
AMR_WB: VMR-WB 是新型可变速率多模式宽带语音编解码器,专为无线 CDMA 2000标准而设计,目的在于在 50 至 7000 HZ 的频带上进行语音编码,采样率为 16 KHZ。VMR-WB 基于 3GPP AMR-WB (G722.2) 编解码器,在每秒速率12.65 Kbit 上可实现互操作。
DEFAULT: 默认输出
MPEG_4: 这将指定录制的文件为mpeg-4格式,可以保护Audio和Video
RAW_AMR: 录制原始文件,这只支持音频录制,同时要求音频编码为AMR_NB
THREE_GPP: 录制后文件是一个3gp文件,支持音频和视频录制
WEBM: 编码为VP8/VORBIS的输出格式。
输出格式,大同小异,这里也没有做特别详细的讲解,将一下基本用法就可以了。一般情况下使用输出格式为MPEG_4的即可。

上述主要介绍了MediaRecorder的方法,作用以及部分参数的定义,下面进行MediaRecorder与SurfaceView结合使用并进行录制视频,视频格式为mp4。

MediaRecorder结合SurfaceView录制视频的步骤

SurfaceView与Camera进行绑定

在SurfaceView与Camera进行绑定前一定要先取得holder即mSurfaceHolder = mSurfaceView.getHolder();并且保证屏幕常亮mSurfaceHolder.setKeepScreenOn(true)。
实现SurfaceHolder.Callback回调(mSurfaceHolder.addCallback(SurfaceHolder.Callback)),其回调有两种实现方式:

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        try {
            if (mCamera == null) {
                openCamera();
            }
            if (null != mCamera) {
                mCamera.setPreviewDisplay(mSurfaceHolder);//Camera屏幕通过SurfaceHolder与SurfaceView 进行绑定
                mCamera.startPreview();//开始预览
            }
        } catch (Exception e) {
            e.printStackTrace();
            Toast.makeText(MainActivity.this, "打开相机失败", Toast.LENGTH_SHORT).show();
        }
    }

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

        mScreenWidth = width;
        mScreenHeight = height;
        setCameraParameters();

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        releaseCameraResource();
    }
    /**
     * 打开相机
     */
    private void openCamera() {
        if (null != mCamera) {
            releaseCameraResource();
        }
        try {
            if (!checkCameraFacing(0) && !checkCameraFacing(1)) {
                Toast.makeText(MainActivity.this, "未发现有可用摄像头", Toast.LENGTH_SHORT).show();
                return;
            }
            if (!checkCameraFacing(mCameraPosition)) {
                Toast.makeText(MainActivity.this, mCameraPosition == 0 ? "后置摄像头不可用" : "前置摄像头不可用", Toast.LENGTH_SHORT).show();
                return;
            }
            mCamera = Camera.open(mCameraPosition);
//            mCamera = Camera.open(0);
        } catch (Exception e) {
            e.printStackTrace();
            releaseCameraResource();
        }
    }

    /**
     * 检查是否有摄像头
     *
     * @param facing 前置还是后置
     * @return
     */
    private boolean checkCameraFacing(int facing) {
        int cameraCount = Camera.getNumberOfCameras();
        Camera.CameraInfo info = new Camera.CameraInfo();
        for (int i = 0; i < cameraCount; i++) {
            Camera.getCameraInfo(i, info);
            if (facing == info.facing) {
                return true;
            }
        }
        return false;
    }

    //设置相机参数
    private void setCameraParameters(){
        try {
//            mCamera = Camera.open();// 打开摄像头
            if (mCamera == null)
                return;
//            mCamera.setDisplayOrientation(90);//将展示方向旋转90度
//            mCamera.setPreviewDisplay(mSurfaceHolder);//Surface 预览
            //可以通过获取相机的参数实例,设置里面各种效果,包括刚刚的预览图,前置摄像头,闪光灯等
            mParameters = mCamera.getParameters();// 获得相机参数

//            //设置图片格式
//            mParameters.setPictureFormat(ImageFormat.JPEG);
//            mParameters.setJpegQuality(100);
//            mParameters.setJpegThumbnailQuality(100);

//            mParameters.setPictureFormat(PixelFormat.JPEG);//设定图片格式为JPEG 默认为NV21
//            mParameters.setPreviewFormat(PixelFormat.YCbCr_420_SP);//设置预览版式为YCbCr_420_SP 默认为NV21

            //该方法返回了SurfaceView的宽与高,根据给出的尺寸与宽高比例,获取一个最适配的预览尺寸
            List<Camera.Size> mSupportedPreviewSizes = mParameters.getSupportedPreviewSizes();
            List<Camera.Size> mSupportedVideoSizes = mParameters.getSupportedVideoSizes();
            mOptimalSize = CameraHelper.getOptimalVideoSize(mSupportedVideoSizes,
                    mSupportedPreviewSizes, mScreenWidth, mScreenHeight);
            //该方法是获取最佳的预览与摄像尺寸。然后设置预览图像大小
            mParameters.setPreviewSize(mOptimalSize.width, mOptimalSize.height); // 设置预览图像大小

            mCamera.setDisplayOrientation(getDegree());
//            if(this.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE){
//                //如果是竖屏
////                mParameters.set("orientation", "portrait");
//                Log.e("lu","我是竖屏............");
//                //在2.2以上可以使用
//                mCamera.setDisplayOrientation(90);
//            }else{
////                mParameters.set("orientation", "landscape");
//                Log.e("lu","我是横屏............");
//                //在2.2以上可以使用
//                mCamera.setDisplayOrientation(0);
//            }

            List<String> focusModes = mParameters.getSupportedFocusModes();
            if (focusModes.contains("continuous-video")) {
                mParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
            }
            mFpsRange =  mParameters.getSupportedPreviewFpsRange();

            List<String> modes = mParameters.getSupportedFocusModes();
            if (modes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
                //支持自动聚焦模式
                mParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
            }

            mCamera.setParameters(mParameters);// 设置相机参数
//            mCamera.startPreview();// 开始预览


            //假设要支持自动对焦功能,则在需要的情况下,或者在上述surfaceChanged调用完startPreview函数后,可以调用Camera::autoFocus函数来设置自动对焦回调函数,该步是可选操作,有些设备可能不支持,可以通过Camera::getFocusMode函数查询。代码可以参考如下:
            // 自动对焦
//            mCamera.autoFocus(new Camera.AutoFocusCallback(){
//                @Override
//                public void onAutoFocus(boolean success, Camera camera){
//                    if (success){
//                        // success为true表示对焦成功,改变对焦状态图像
//                        ivFocus.setImageResource(R.drawable.focus2);
//                    }
//                }
//            });


        }catch (Exception io){
            io.printStackTrace();
        }
    }


    private int getDegree() {
        //获取当前屏幕旋转的角度
        int rotating = this.getWindowManager().getDefaultDisplay().getRotation();
        int degree = 0;//度数
        //根据手机旋转的角度,来设置surfaceView的显示的角度
        switch (rotating) {
            case Surface.ROTATION_0:
                degree = 90;
                break;
            case Surface.ROTATION_90:
                degree = 0;
                break;
            case Surface.ROTATION_180:
                degree = 270;
                break;
            case Surface.ROTATION_270:
                degree = 180;
                break;
        }
        return degree;
    }

    /**
     * 释放摄像头资源
     */
    private void releaseCameraResource() {
        if (mCamera != null) {
            mCamera.setPreviewCallback(null);
            mCamera.stopPreview();
            mCamera.lock();
            mCamera.release();
            mCamera = null;
        }
    }

最优尺寸

    /**
     * 这两个队列分别是 该相机支持的 预览大小(一般就是拍照时照片的大小),另外一个就是支持适配的大小,
     * 因为都是队列,说明相机支持很多组尺寸,而且,照片的尺寸与视频的尺寸是不一样的。我debug看了几款手机,
     * 通常摄像支持的尺寸少一点,照片会多一些。这样,我们就要通过刚刚方法给出的宽高,
     * 获取一个最佳匹配的预览尺寸.
     *
     * @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;
    }

上述就是SurfaceView与Camera绑定,Camera初始化,Camera参数设置,Camera资源释放。

保存录制视频的路径

     /**
     * 创建目录与文件
     */
    private void createRecordDir() {
        mDirName = String.valueOf(System.currentTimeMillis()) +  String.valueOf( new Random().nextInt(1000));
        File FileDir = new File(BASE_PATH + mDirName);
        if (!FileDir.exists()) {
            FileDir.mkdirs();
        }
        // 创建文件
        try {
            mVecordFile = new File(FileDir.getAbsolutePath() + "/" + Utils.getDateNumber() +".mp4");
            Log.e("Path:", mVecordFile.getAbsolutePath());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

初始化MediaRecorder 并 设置MediaRecorder的参数

    /**
     * 录制前,初始化
     */
    private void initRecord() {
        try {
            //进入一个预览的拍摄页面了,该页面其实也可以用来做拍照。
            // 要想做拍摄,还要实例化MediaRecorder,然后传入camera并初始化相应的参数。
            if(mMediaRecorder == null){
                mMediaRecorder = new MediaRecorder();
            }
            if(mCamera != null){
                mCamera.unlock();
                mMediaRecorder.setCamera(mCamera);
            }

            mMediaRecorder.setOnErrorListener(this);

//            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);//音频源  麦克风
//            mMediaRecorder.setAudioChannels(1);//单声道
//            mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);//音频格式

            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);//音频源  麦克风
            // 设置录制视频源为Camera(相机)
            mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);//视频源
//            mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);//视频输出格式
//            mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);//视频录制格式
            mMediaRecorder.setOrientationHint(90);//视频旋转90度

            // Use the same size for recording profile.
            CamcorderProfile mProfile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);
            mProfile.videoFrameWidth = mOptimalSize.width;
            mProfile.videoFrameHeight = mOptimalSize.height;
//
            mMediaRecorder.setProfile(mProfile);
            //该设置是为了抽取视频的某些帧,真正录视频的时候,不要设置该参数
//            mMediaRecorder.setCaptureRate(mFpsRange.get(0)[0]);//获取最小的每一秒录制的帧数


//            // 设置视频录制的分辨率。必须放在设置编码和格式的后面,否则报错,而且这个值要适配
//            //手机,不然也会在后面stop方法报错!
//            mMediaRecorder.setVideoSize(1280,720);
//            // 设置录制的视频帧率。必须放在设置编码和格式的后面,否则报错,这样设置变清晰
//            mMediaRecorder.setVideoEncodingBitRate(10*1024*1024);

            mMediaRecorder.setOutputFile(mVecordFile.getAbsolutePath());

            mMediaRecorder.prepare();
            mMediaRecorder.start();
        } catch (Exception e) {
            e.printStackTrace();
            releaseRecord();
        }
    }

开始录制视频

开始录制视频时并计时,到达制定时间就停止录制。

    /**
     * 开始录制视频
     */
    public void startRecord(final OnRecordFinishListener onRecordFinishListener) {
        this.mOnRecordFinishListener = onRecordFinishListener;
        isStarting = true;
        lay_tool.setVisibility(View.INVISIBLE);
        tag_start.setVisibility(View.VISIBLE);
        anim.start();
        createRecordDir();
        try {
            initRecord();
            mTimeCount = 0;// 时间计数器重新赋值
            mTimer = new Timer();
            timerTask = new TimerTask() {
                @Override
                public void run() {
                    mTimeCount++;
                    mProgressBar.setProgress(mTimeCount);
                    if (mTimeCount == mRecordMaxTime) {// 达到指定时间,停止拍摄
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                stop();
                                if (mOnRecordFinishListener != null){
                                    mOnRecordFinishListener.onRecordFinish();
                                }
                            }
                        });
                    }
                }
            };
            mTimer.schedule(timerTask, 0, 100);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

停止录制视频

停止录制视频时一定要释放视频资源及相机资源

    /**
     * 停止拍摄
     */
    public void stop() {
        stopRecord();
        releaseRecord();
        releaseCameraResource();
    }

    /**
     * 停止录制
     */
    public void stopRecord() {
        mProgressBar.setProgress(0);
        isStarting = false;
        tag_start.setVisibility(View.GONE);
        anim.stop();
        lay_tool.setVisibility(View.VISIBLE);
        if(timerTask != null)
            timerTask.cancel();
        if (mTimer != null)
            mTimer.cancel();
        if (mMediaRecorder != null) {
            try {
                mMediaRecorder.stop();
                mMediaRecorder.reset();
            } catch (IllegalStateException e) {
                e.printStackTrace();
            } catch (RuntimeException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

释放MediaRecorder资源

    /**
     * 释放资源
     */
    private void releaseRecord() {
        if (mMediaRecorder != null) {
            mMediaRecorder.setPreviewDisplay(null);
            mMediaRecorder.setOnErrorListener(null);
            try {
                mMediaRecorder.release();
            } catch (IllegalStateException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        mMediaRecorder = null;
    }

闪光灯关闭与开启

    //闪光灯关闭与开启
    private void flashLightToggle(){
        try {
            if(isFlashLightOn){
                mParameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);//关闭闪光灯
                mCamera.setParameters(mParameters);
                isFlashLightOn = false;
            }else {
                mParameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);//开启闪光灯
                mCamera.setParameters(mParameters);
                isFlashLightOn = true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

前后摄像头切换

前后摄像头切换的代码还可以简化的,这里是没有简化的,大家看得懂就可以了

    //前后摄像头切换,就要重新初始化 camera实例
    private void switchCamera(){
        Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
        int cameraCount = Camera.getNumberOfCameras();//得到摄像头的个数

        for(int i = 0; i < cameraCount; i++ ) {
            Camera.getCameraInfo(i, cameraInfo);//得到每一个摄像头的信息
            if(mCameraPosition == 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);//打开当前选中的摄像头
                    try {
                        mCamera.setDisplayOrientation(90);// 打开摄像头并将展示方向旋转90度
                        mCamera.setPreviewDisplay(mSurfaceHolder);//通过surfaceview显示取景画面
                    } catch (IOException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    mCamera.setParameters(mParameters);// 设置相机参数
                    mCamera.startPreview();//开始预览
                    mCameraPosition = 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);//打开当前选中的摄像头
                    try {
                        mCamera.setDisplayOrientation(90);// 打开摄像头并将展示方向旋转90度
                        mCamera.setPreviewDisplay(mSurfaceHolder);//通过surfaceview显示取景画面
                    } catch (IOException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    mCamera.setParameters(mParameters);// 设置相机参数
                    mCamera.startPreview();//开始预览
                    mCameraPosition = 1;
                    break;
                }
            }

        }
    }

添加权限

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    <!-- 写入扩展存储,向扩展卡写入数据,用于写入离线定位数据 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.FLASHLIGHT" />

总结

上一篇下一篇

猜你喜欢

热点阅读