• Android 拍摄(横竖屏)视频的懒人之路


    想一想,我们聊过AudioReord,AudioTrack,MediaPlayer,那多媒体四大金刚,就剩下了MediaRecorder了(SoundPool?我这里信号不好···)。其实MediaRecorder个人用的也不多,很久前用它在拍摄视频上确实趟过无视次坑,那今天就聊它吧,把它聊到躺下(ノQ益Q)ノ彡┻━┻。

    MediaRecorder

     一般用在多媒体录制上面,当然如果你只是简单的想录制音频,用它最合适不过,不过如果你想更多样化的录制这里推荐《Android MP3录制,波形显示,音频权限兼容与播放》。今天的主题是录制视频,用的还是老式通用的Camera,不是新的camera2(这就尴尬了.....((/- -)/),反正个人秉承能用是王道的做法(懒)。之前也尝试过FFMPEG的录制合成音频,大小和效果也不错,只是有时候的兼容性确实有些问题,最主要还是资料不多,不好改啊 ̄へ ̄(懒)。

     既然是录制视频,那么少不了Camera,这货也是让人又爱又恨(哪里有爱了┑( ̄Д  ̄)┍?),也许是因为Android碎片化的原因,所以用起来也是坑坑洼洼的,接下来就让我们结束废话吧:

    • 1、SurfaceView用于承载画面。
    • 2、初始化相机Camera。
    • 3、初始化重力旋转用于横竖屏。
    • 4、配置闪光灯和旋转摄像头功能。
    • 5、配置MediaRecorder的录制参数后开始录制。
    • 6、结束录制预览视频。
    1、SurfaceView显示画面

     
     旧项目用的都是SurfaceView,这次就就它吧。这里我们需要首先是implements SurfaceHolder.Callback,这样我们才能在surface创建的时候初始化相机渲染画面,在画面销毁的时候销毁相机(画面都没有要初始化相机何用)。

    SurfaceHolder holder = cameraShowView.getHolder();
    holder.addCallback(this);
    // setType必须设置,要不出错.
    holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    
    ···此处略过无数只草泥马
    
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        surfaceHolder = holder;
        //更具当前的相机类型(前,后)初始化相机,闪光灯不启动
        initCamera(cameraType, false);
    }
    
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        surfaceHolder = holder;
    }
    
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        //结束录制
        endRecord();
        //是否相机
        releaseCamera();
    }
    2、初始化Camera

     
     除了有点坑外,流程上还是比较简单的:

    • 释放已经初始化过的相机。
    • 根据当前摄像头类型打开相机。
    • 配置相机参数:预览大小,对焦,闪光灯,竖屏显示。
    • 设置显示画面的surface
    • 开始绘制
    if (camera != null) {
        //如果已经初始化过,就先释放
        releaseCamera();
    }
    
    try {
        //根据前后摄像头打开摄像头
        camera = Camera.open(type);
        if (camera == null) {
            //拿不到可能是没权限
            showCameraPermission();
            return;
        }
        camera.lock();
    
        //Point screen = new Point(getScreenWidth(this), getScreenHeight(this));
        //现在不用获取最高的显示效果
        //Point show = getBestCameraShow(camera.getParameters(), screen);
    
        Camera.Parameters parameters = camera.getParameters();
        if (type == 0) {
            //基本是都支持这个比例
            parameters.setPreviewSize(SIZE_1, SIZE_2);
            parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);//1连续对焦
            camera.cancelAutoFocus();// 2如果要实现连续的自动对焦,这一句必须加上
        }
        camera.setParameters(parameters);
        FlashLogic(camera.getParameters(), flashType, flashDo);
        if (cameraType == 1) {
            frontCameraRotate();
            camera.setDisplayOrientation(frontRotate);
        } else {
            camera.setDisplayOrientation(90);
        }
        camera.setPreviewDisplay(surfaceHolder);
        camera.startPreview();
        camera.unlock();
    } catch (Exception e) {
        e.printStackTrace();
        releaseCamera();
    }

     这里需要注意坑(画面变形)问题,那就是你配置的相机分辨率画面,在录制的时候可能会因为和录制的分辨率画面不一致,导致开始录制的时候画面奇怪的突变,所以Camera和MediaRecorder的分辨率最好一致。

     问题又来了Camera和MediaRecorder不是什么分辨率都支持的,他们分别都有对应的接口:getSupportedPreviewSizes和CamcorderProfile等来获取对应支持的分辨率的,路迢迢啊。
     
     经过轮番的尝试,还有上传对大小要求,所以最终选择写死,对,写死了640 * 480这样的大小,这个分辨率基本都支持(不支持那手机的尊严何在( ‵o′)凸),对于十来秒的视频,这个分辨率的尺寸还算可以(如果对画质有需要可以另外配置,如果FFMPEG压缩性能堪忧啊)。

     那么问题又来了(哪来那么多问题),但是手机屏幕大部分情况下是16:9,而这个分辨率明显是4:3(万恶的需求啊(ノQ益Q)ノ彡┻━┻)。这时候因为Surface的最外层是FrameLayout(搞不懂为什么超出屏幕的时候RelativeLayout有时候会有问题),个人的做法是调整surface的比例。如果是不充满屏幕高度的,就通过屏幕宽度比例算出surface的高度;如果充满屏幕高度,就算出surface的宽度。

     如此以来,不变形啦,在点击录制的瞬间也不跳动啦,唯一有点小问题的就是充满高度的时候,画面是超过了屏幕宽度的一点的,所以可能录到了什么不想录制的♂,但是刚好没看到︿( ̄︶ ̄)︿。

    int screenWidth = getScreenWidth(this);
    int screenHeight = getScreenHeight(this);
    //根据比例设置surface的宽度
    setViewSize(cameraShowView, screenWidth * SIZE_1 / SIZE_2, screenHeight);

    3、重力感应旋转
     
     当时看到IOS微博的视频录制是可以支持横竖屏录制,觉得挺有意思的,这里用的是OrientationEventListener,具体的之前IJKPlayer视频文章里已经说过(懒),有兴趣的可以去看看。我们是在画面旋转的时候把对应的logo用属性动画也旋转了,然后得到当前的旋转角度,告诉MediaRecorder,拍摄出来的视频元信息里就带有了角度信息,播放的时候画面会就旋转为横屏或者竖屏啦。

    orientationEventListener = new OrientationEventListener(this) {
        @Override
        public void onOrientationChanged(int rotation) {
            if (!flagRecord) {
                if (((rotation >= 0) && (rotation <= 30)) || (rotation >= 330)) {
                    // 竖屏拍摄
                    if (rotationFlag != 0) {
                        //旋转logo
                        rotationAnimation(rotationFlag, 0);
                        //这是竖屏视频需要的角度
                        rotationRecord = 90;
                        //这是记录当前角度的flag
                        rotationFlag = 0;
                    }
                } else if (((rotation >= 230) && (rotation <= 310))) {
                    // 横屏拍摄
                    if (rotationFlag != 90) {
                        //旋转logo
                        rotationAnimation(rotationFlag, 90); 
                        //这是正横屏视频需要的角度
                        rotationRecord = 0;
                        //这是记录当前角度的flag
                        rotationFlag = 90;
                    }
                } else if (rotation > 30 && rotation < 95) {
                    // 反横屏拍摄
                    if (rotationFlag != 270) {
                        //旋转logo
                        rotationAnimation(rotationFlag, 270);
                        //这是反横屏视频需要的角度
                        rotationRecord = 180;
                        //这是记录当前角度的flag
                        rotationFlag = 270;
                    }
                }
                //倒过来就算了,你又不是小米MIX
            }
        }
    };
    orientationEventListener.enable();

    前置摄像头

     此处有坑,还不止一个,如果你还需要支持前置摄像头(能说不吗?),直接使用上面的rotationRecord去配置MediaRecorder是会有问题的。

     首先说Camera,如果测试说你的前置Camera在某些手机上画面角度不对,这时候你可以偷偷把手机砸了,因为这是兼容问题。如果你没有勇气砸手机,看下面。

     传说中,只要拿下面的frontRotate去配置Camera就正常显示啦,伟人说的!而其中的frontOri,我们是用到配置后面MediaRecorder,具体看代码的,这是调出来的结果(。・・)ノ。

     /**
     * 旋转前置摄像头为正的
     */
    private void frontCameraRotate() {
        Camera.CameraInfo info = new Camera.CameraInfo();
        Camera.getCameraInfo(1, info);
        int degrees = getDisplayRotation(this);
        int result;
        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            result = (info.orientation + degrees) % 360;
            result = (360 - result) % 360; // compensate the mirror
        } else { // back-facing
            result = (info.orientation - degrees + 360) % 360;
        }
        frontOri = info.orientation;
        frontRotate = result;
    }
    
    /**
     * 获取旋转角度
     */
    private int getDisplayRotation(Activity activity) {
        int rotation = activity.getWindowManager().getDefaultDisplay()
                .getRotation();
        switch (rotation) {
            case Surface.ROTATION_0:
                return 0;
            case Surface.ROTATION_90:
                return 90;
            case Surface.ROTATION_180:
                return 180;
            case Surface.ROTATION_270:
                return 270;
        }
        return 0;
    }
    
    ···此处无数字草泥马
    //配置录制角度
    int frontRotation;
    if (rotationRecord == 180) {
        //反向横屏的前置角度
        frontRotation = 180;
    } else {
        //竖屏和正向横屏的前置角度
        //录制下来的视屏选择角度,此处为前置
        frontRotation = (rotationRecord == 0) ? 270 - frontOri : frontOri; 
    }
    //根据前后摄像头给角度
    recorder.setOrientationHint((cameraType == 1) ? frontRotation : rotationRecord);

    4、闪光灯和旋转摄像头

     闪光灯的打开关闭遇到过一个问题,就是有的手机还没有开启录制,一配置打开它就亮了。(砸手机)最后解决的是在配置的时候标志类型,设置好MediaRecorder之后拍摄才开始闪光灯。(其他的什么一闪一闪的模式就算了吧= =)

     至于旋转切换相机,主要还是针对前置camera需要做如上面所说的画面预览旋转。

    /**
    * 闪光灯逻辑
    *
    * @param p    相机参数
    * @param type 打开还是关闭
    * @param isOn 是否立即启动
    */
    private void FlashLogic(Camera.Parameters p, int type, boolean isOn) {
        flashType = type;
        if (type == 0) {
            if (isOn) {
                p.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
                camera.setParameters(p);
            }
            videoFlashLight.setImageResource(R.drawable.flash_off);
        } else {
            if (isOn) {
                p.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
                camera.setParameters(p);
            }
            videoFlashLight.setImageResource(R.drawable.flash);
        }
        if (cameraFlag == 0) {
            videoFlashLight.setVisibility(View.GONE);
        } else {
            videoFlashLight.setVisibility(View.VISIBLE);
        }
    }
    
    /**
     * 切换摄像头
     */
    public void switchCamera() {
        Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
        int cameraCount = Camera.getNumberOfCameras();//得到摄像头的个数0或者1;
    
        try {
            for (int i = 0; i < cameraCount; i++) {
                Camera.getCameraInfo(i, cameraInfo);//得到每一个摄像头的信息
                if (cameraFlag == 1) {
                    //后置到前置
                    if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {//代表摄像头的方位,CAMERA_FACING_FRONT前置      CAMERA_FACING_BACK后置
                        frontCameraRotate();//前置旋转摄像头度数
                        switchCameraLogic(i, 0, frontRotate);
                        break;
                    }
                } else {
                    //前置到后置
                    if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {//代表摄像头的方位,CAMERA_FACING_FRONT前置      CAMERA_FACING_BACK后置
                        switchCameraLogic(i, 1, 90);
                        break;
                    }
                }
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

     
    5、配置MediaRecorder的录制参数、生成视频。

     这里最坑的就是MediaRecorder的配置参数是有前后关系的,先生小孩后再洞房这种绿色模式是不行的,具体顺序参照下方代码,码率和帧数都是配置相对较小,适合拍摄上传。此处还需要注意,如果应用没有获取到录音权限,在录制的时候是会走catch里面的。

     停止录制相对就简单了,只要顺序正常即可,之后就可以把视频传到VideoView快速实现预览啦。作为谷歌亲儿子,VideoView自带对setOrientationHint的角度解析,只要根据视频大小配置好界面显示的效果即可。比起 之前本人撸的播放器 ,儿子还是自己的亲┑( ̄Д  ̄)┍,如果需求不高用起来还是可以闭着眼睛的用的。(之前还有小伙伴自己用MediaPlayer播放呢)

    //开始
    private boolean startRecord() {
    
        //懒人模式,根据闪光灯和摄像头前后重新初始化一遍,开期闪光灯工作模式
        initCamera(cameraType, true);
    
        if (recorder == null) {
            recorder = new MediaRecorder();
        }
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
                || camera == null || recorder == null) {
            camera = null;
            recorder = null;
            //还是没权限啊
            showCameraPermission();
            return false;
        }
    
        try {
    
            recorder.setCamera(camera);
            // 这两项需要放在setOutputFormat之前
            recorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
            recorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
            // Set output file format,输出格式
            recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
    
            //必须在setEncoder之前
            recorder.setVideoFrameRate(15);  //帧数  一分钟帧,15帧就够了
            recorder.setVideoSize(SIZE_1, SIZE_2);//这个大小就够了
    
            // 这两项需要放在setOutputFormat之后
            recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
            recorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
    
            recorder.setVideoEncodingBitRate(3 * SIZE_1 * SIZE_2);//第一个数字越大,清晰度就越高,考虑文件大小的缘故,就调整为1
            int frontRotation;
            if (rotationRecord == 180) {
                //反向的前置
                frontRotation = 180;
            } else {
                //正向的前置
                frontRotation = (rotationRecord == 0) ? 270 - frontOri : frontOri; //录制下来的视屏选择角度,此处为前置
            }
            recorder.setOrientationHint((cameraType == 1) ? frontRotation : rotationRecord);
            //把摄像头的画面给它
            recorder.setPreviewDisplay(surfaceHolder.getSurface());
            //创建好视频文件用来保存
            videoDir();
            if (videoFile != null) {
                //设置创建好的输入路径
                recorder.setOutputFile(videoFile.getPath());
                recorder.prepare();
                recorder.start();
                //不能旋转啦
                orientationEventListener.disable();
                flagRecord = true;
            }
        } catch (Exception e) {
            //一般没有录制权限或者录制参数出现问题都走这里
            e.printStackTrace();
            //还是没权限啊
            recorder.reset();
            recorder.release();
            recorder = null;
            showCameraPermission();
            FileUtils.deleteFile(videoFile.getPath());
            return false;
        }
        return true;
    
    }
    
    //结束录制
    private void endRecord() {
        //反正多次进入,比如surface的destroy和界面onPause
        if (!flagRecord) {
            return;
        }
        flagRecord = false;
        try {
            if (recorder != null) {
                recorder.stop();
                recorder.reset();
                recorder.release();
                orientationEventListener.enable();
                recorder = null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        videoTime.stop();
        videoTime.setBase(SystemClock.elapsedRealtime());
        Intent intent = new Intent(this, PlayActivity.class);
        intent.putExtra(PlayActivity.DATA, videoFile.getAbsolutePath());
        startActivityForResult(intent, 2222);
        overridePendingTransition(R.anim.fab_in, R.anim.fab_out);
    }
    最后

     
     总的来说,录制视频还是蛮简单的,主要还是视频的角度问题需要考虑:

    • Camera的前置摄像头角度注意。
    • Android本身默认的是横屏录制效果,所以需要配置横屏和竖屏的录制角度。
    • MediaRecorder参数的配置顺序。
    • Camera和MediaRecorder的分辨率和拉伸问题。
    • 闪光灯要在开始录制的时候才开启。
    • 初始化摄像头和释放摄像头需要在surface的surfaceCreated和surfaceDestroyed。
    • 注意锁屏、退到后台、onPuase的是会走surface的surfaceDestroyed。
    • 如果是要一次性上传很长很长的拍摄视频,推荐还是找FFMPEG的录制方式吧,毕经录制好了再压缩的做法很费时。 
    • 告诉IOS,让他支持视频元信息的角度旋转播放。(不支持?网上那么多视频有角度信息,难道歪着看?)
    • 测试如果说前置画面拍摄出来的视频左右翻转,用本机拍一个前置视频或者照片给他看,不然你只能接FFMPEG了。  

    <( ̄︶ ̄)↗知道你想说什么,DMEO在这里 : https://github.com/CarGuo/VideoRecord

  • 相关阅读:
    使用 Spring + CXF 发布 REST 服务
    在web项目中使用CXF暴露REST服务
    学习SpringMVC——从HelloWorld开始
    java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet
    安装maven时JAVA_HOME的配置小问题
    java 接口的实现和继承关系
    servlet的两种配置方式
    C# MainWindowHandle为0的解决方法
    C# WindowsMediaPlayer 的一些用法
    C# 获取MP3信息
  • 原文地址:https://www.cnblogs.com/dongweiq/p/6347130.html
Copyright © 2020-2023  润新知