汇总Android视频录制中常见问题

本文分享自己在视频录制播放过程中遇到的一些问题,主要包括:

  • 视频录制流程
  • 视频预览及SurfaceHolder
  • 视频清晰度及文件大小
  • 视频文件旋转

一、视频录制流程
    以微信为例,其录制触发为按下(住)录制按钮,结束录制的触发条件为松开录制按钮或录制时间结束,其流程大概可以用下图来描述。

1.1、开始录制
   根据上述流程及项目的编程惯例,可在onCreate()定义如下函数来完成功能:

初始化过程主要包括View,Data以及Listener三部分。在初始化View时,添加摄像头预览,添加倒计时文本组件,设置初始状态UI组件的可见;初始化Data时,从Intent中获取初始数据;初始化Listener时,分别对录制触发按钮,保存/取消视频录制按钮以及视频预览界面添加监听。
    当系统初始化成功后,等待用户按下录制按钮,因此在录制按钮的监听中,需要完成以下功能:录制,计时,更新界面组件。

if(isRecording) {
  mMediaRecorder.stop();
  releaseMediaRecorder();
  mCamera.lock();
  isRecording = false;
}
if(startRecordVideo()) {
  startTimeVideoRecord();
  isRecording = true;
}

首先判断当前录制状态,如果正在录制,则先停止录制,释放MediaRecorder资源,锁定摄像头,置位录制状态;然后开始视频录制startRecordVideo,其boolean型返回值表征是否启动成功,启动成功后,开始视频录制计时,并且置位录制状态。startRecordVideo涉及MediaRecorder的配置,准备以及启动。

翻译成代码如下:

private boolean startRecordVideo() {
  configureMediaRecorder();
  if(!prepareConfiguredMediaRecorder()) {
    return false;
  }
  mMediaRecorder.start();
  return true;
}

1.2、结束录制
根据上述流程图可知,结束录制的触发条件为松开录制按钮或计时时间到。在结束录制方法中,需要释放MediaRecorder,开始循环播放已录制视频,设置界面更新等。

翻译成代码如下:

private void stopRecordVideo() {

    releaseMediaRecorder();
    // 录制视频文件处理
    if(currentRecordProgress < MIN_RECORD_TIME) {
      Toast.makeText(VideoInputActivity.this, "录制时间太短", Toast.LENGTH_SHORT).show();
    } else {
      startVideoPlay();
      isPlaying = true;
      setUiDisplayAfterVideoRecordFinish();
    }
    currentRecordProgress = 0;
    updateProgressBar(currentRecordProgress);
    releaseTimer();
    // 状态设置
    isRecording = false;
  }

 二、视频预览及SurfaceHolder
      视频预览采用SurfaceView,相比于普通的View,SurfaceView在一个新起的单独线程中绘制画面,该实现的优点是更新画面不会阻塞UI主线程,缺点是会带来事件同步的问题。当然,这涉及到UI事件的传递以及线程同步。

在实现中,通过继承SurfaceView组件来实现自定义预览控件。首先,SurfaceView的getHolder()方法会返回SurfaceHolder,需要为SurfaceHolder添加SurfaceHolder.Callback回调;其次,重写surfaceCreated、surfaceChanged和surfaceDestroyed实现。

2.1、构造器
      构造器包含了初始化域以及添加上述回调的过程。

public CameraPreview(Context context, Camera camera) {
    super(context);

    mCamera = camera;
    mSupportedPreviewSizes = mCamera.getParameters().getSupportedPreviewSizes();
    mHolder = getHolder();
    mHolder.addCallback(this);
    mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
  }

这里需要说明mSupportedPreviewSizes,由于摄像头支持的预览尺寸由Camera本身的参数决定,因此需要首先获取其所支持的预览尺寸。

2.2、预览尺寸的设置
      从Google官方的Camera示例程序中可以看出,选择预览尺寸的标准是(1)摄像头支持的预览尺寸的宽高比与SurfaceView的宽高比的绝对差值小于0.1;(2)在(1)获得的尺寸中,选取与SurfaceView的高的差值最小的。通过代码对这两个标准进行了实现,这里贴一下官方的代码:

public Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
    final double ASPECT_TOLERANCE = 0.1;
    double targetRatio = (double) w / h;
    if (sizes == null) {
      return null;
    }
    Camera.Size optimalSize = null;
    double minDiff = Double.MAX_VALUE;
    int targetHeight = h;
    for (Camera.Size size : sizes) {
      double ratio = (double) size.width / size.height;
      if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE)
        continue;
      if (Math.abs(size.height - targetHeight) < minDiff) {
        optimalSize = size;
        minDiff = Math.abs(size.height - targetHeight);
      }
    }
    if (optimalSize == null) {
      minDiff = Double.MAX_VALUE;
      for (Camera.Size size : sizes) {
        if (Math.abs(size.height - targetHeight) < minDiff) {
          optimalSize = size;
          minDiff = Math.abs(size.height - targetHeight);
        }
      }
    }
    return optimalSize;
  }

在加载预览画面时,需要考虑Camera支持的尺寸(getSupportedPreviewSizes)和加载预览画面的SurfaceView的尺寸(layout_width/layout_height),在预览阶段,两者之间的关系直接影响清晰度及图像拉伸。对于Camera的尺寸,由于设备的硬件差异,不同设备支持的尺寸存在差异,但在默认情况(orientation=landscape)下,其width>height。以HTC609d为例,Camera支持的分辨率为1280*720(16:9)……640*480(4:3)……480*320(3:2)等十多种,而其屏幕的分辨率为960*540(16:9)。因此,很容易得到以下结论:(1)当Camera预览尺寸小于SurfaceView尺寸较多时,预览画面就不清晰;(2)Camera预览尺寸宽高比与SurfaceView宽高比相差较大时,预览画面就会拉伸。

上述代码在手机设置为横屏时并没有问题,在设置为竖屏时,为获得最优的预览尺寸,需要在调用此方法前比较SurfaceView的宽高。

if (mSupportedPreviewSizes != null) {
  mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes,
            Math.max(width, height), Math.min(width, height));
}

获得与当前SurfaceView匹配的预览尺寸后,即可通过Camera.Parameters进行设置。

Camera.Parameters mParams = mCamera.getParameters();
mParams.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
mCamera.setDisplayOrientation(90);

List<String> focusModes = mParams.getSupportedFocusModes();
if(focusModes.contains("continuous-video")){
  mParams.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
}
mCamera.setParameters(mParams);

 三、视频清晰度及文件大小
      在第一节中讲到startRecordVideo,包括配置MediaRecorder,准备MediaRecorder以及启动,其中配置MediaRecorder是视频录制的重点,需要了解每项配置参数的作用,根据业务场景灵活配置。这里参考Google官方的示例给出一个可行的配置方案,然后再对其进行解释。

private void configureMediaRecorder() {
    // BEGIN_INCLUDE (configure_media_recorder)
    mMediaRecorder = new MediaRecorder();

    // Step 1: Unlock and set camera to MediaRecorder
    mCamera.unlock();
    mMediaRecorder.setCamera(mCamera);
    mMediaRecorder.setOrientationHint(90);

    // Step 2: Set sources
    mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.VOICE_RECOGNITION);
    mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);

    // Step 3: Set a Camera Parameters
    mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
    /* Fixed video Size: 640 * 480*/
    mMediaRecorder.setVideoSize(640, 480);
    /* Encoding bit rate: 1 * 1024 * 1024*/
    mMediaRecorder.setVideoEncodingBitRate(1 * 1024 * 1024);
    mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
    mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);

    // Step 4: Set output file
    mMediaRecorder.setMaxFileSize(maxFileSizeInBytes);
    mMediaRecorder.setOutputFile(videoFilePath);
    // END_INCLUDE (configure_media_recorder)

    // Set MediaRecorder ErrorListener
    mMediaRecorder.setOnErrorListener(this);
  }

Step 1:
1.setCamera参数能够使得在预览和录制中快速切换,避免Camera对象的重新加载。在某些Android手机自带的照相机程序中,切换预览与录制中的短暂卡顿,读者可自行体会。
2.mMediaRecorder.setOrientationHint(90)在录制方向为竖直(portrait)时使用,它能使视频文件的沿顺时针方向旋转90度,如果不设置此项,播放视频时,画面会发生90度的旋转。不过这里更重要的是,即使设置了此项,在某些播放器上,画面依然会有90度的旋转(比如将在手机上正常播放的视频导入到PC中进行播放,或者嵌入H5的video标签中),这可是为什么呢?注意setOrientationHint的说明:Note that some video players may choose to ignore the compostion matrix in a video during playback. 那么如何做到在所有播放器上都能以正常方向播放呢?稍等,后续专门对其进行说明。
Step 2:
1.setAudioSource(MediaRecorder.AudioSource.VOICE_RECOGNITION),VOICE_RECOGNITION相比于MIC会根据语音识别的需要做一些调谐,当然,这需要在系统支持的情况下。
2.setVideoSource自然是VideoSource.CAMERA,只是在此两项设置必须在设置编码器之前设置,这无需说明。
Step 3:
1.setOutputFormat需要在Step 2之后,并且在prepare()之前。这里采用OutputFormat.MPEG_4格式。
2.setVideoSize需要权衡的因素较多,主要包括三方面:MediaRecorder支持的录制尺寸、视频文件的大小以及兼容不同Android机型。这里采用640 * 480(微信小视频的尺寸是320*240),文件大小在500-1000kb之间,并且市面上99%以上机型支持此录制尺寸。
3.setVideoEncodingBitRate与视频的清晰度有关,设置此参数需要权衡清晰度与文件大小的关系。太高,文件大不易传输;太低,文件清晰度低,识别率低。需要根据实际业务场景灵活调整。
4.setVideoEncoder采用H264编码,MPEG4、H263、H264等不同编码的差别比较,实际使用中,H264的压缩率较高,推荐使用。
5.setAudioEncoder采用AudioEncoder.AAC,该设置主要是考虑其通用性、兼容性。
Step 4:
setMaxFileSize指定录制文件的大小限制,当然还可以限制其最大录制时间。
setOutputFile指定输出视频的路径。
setOnErrorListener指定错误监听器。
   在完成上述配置之后,即可准备MediaRecorder,并在返回成功后开始视频录制。

private boolean prepareConfiguredMediaRecorder() {
    // Step 5: Prepare configured MediaRecorder
    try {
      mMediaRecorder.prepare();
    } catch (Exception e) {
      releaseMediaRecorder();
      return false;
    }
    return true;
  }

四、视频文件旋转
      第三节中Step 1提到对视频文件的旋转,因为某些播放器会忽略录制视频时的配置参数,因此可尝试通过第三方库对视频文件进行旋转,例如:OpenCV,fastCV等,在Camera对象的Camera.PreviewCallback中截取每帧数据byte[] data,然后对其进行处理,然后输出。该方法需要考虑处理方法的高效性,在编程时一般采用NDK,在C++中完成关键的处理,这里贴出fastCV中该处理方法的逻辑。

public void onPreviewFrame( byte[] data, Camera c ) {
     // Increment FPS counter for camera.
     util.cameraFrameTick();

     // Perform processing on the camera preview data.
     update( data, mDesiredWidth, mDesiredHeight );

     // Simple IIR filter on time.
     mProcessTime = util.getFastCVProcessTime();

     if( c != null )
     {
      // with buffer requires addbuffer each callback frame.
      c.addCallbackBuffer( mPreviewBuffer );
      c.setPreviewCallbackWithBuffer( this );
     }

     // Mark dirty for render.
     requestRender();
   }
  };

其中,update为native方法,其实现由jni中对应的文件完成,其中调用了libfastcv.a中相应的API。这里涉及NDK编程的基本方法步骤:(1)开发环境;(2)编写Java代码、C/C++代码;(3)编译C/C++文件生成.so库;(4)重新编译工程,生成apk。由于本章不重点讲述NDK,这里不再展开。

除上述方法以外,笔者采用了另外一种思路进行了探索,上述方法处理的数据为每帧图像数据,可以理解为在线处理,而如果在录制完成之后再处理,可以理解为离线处理。这里采用了第三方库mp4parser,mp4parser是一款支持在Android中进行视频分割的库,这里通过其进行视频旋转。至于具体效果如何,读者有兴趣可自行尝试,这里留个悬念。

private boolean rotateVideoFileWithClockwiseDegree(String sourceFilePath, int degree) {
    if(!isFileAndDegreeValid(sourceFilePath, degree)) {
      return false;
    }
    rotateVideoFile(sourceFilePath, degree);
    return true;
  }

对输入参数进行合法性检测之后,根据检测结果判断是否进行旋转。

private boolean isFileAndDegreeValid(String sourceFilePath, int degree) {
    if(sourceFilePath == null || (!sourceFilePath.endsWith(".mp4"))
                 || (!new File(sourceFilePath).exists())) {
      return false;
    }
    if (degree == 0 || (degree % 90 != 0)) {
      return false;
    }
    return true;
  }
private void rotateVideoFile(String sourceFilePath, int degree) {
    List<TrackBox> trackBoxes = getTrackBoxesOfVideoFileByPath(sourceFilePath);
    Movie rotatedMovie = getRotatedMovieOfTrackBox(trackBoxes);
    writeMovieToModifiedFile(rotatedMovie);
  }

通过mp4parser旋转视频主要分为三步:

(1)获取视频文件对应的TrackBoxes;

(2)根据TrackBoxes获取旋转后的Movie对象;

(3)将Movie对象写入文件。

private List<TrackBox> getTrackBoxesOfVideoFileByPath(String sourceFilePath) {
    IsoFile isoFile = null;
    List<TrackBox> trackBoxes = null;
    try {
      isoFile = new IsoFile(sourceFilePath);
      trackBoxes = isoFile.getMovieBox().getBoxes(TrackBox.class);
      isoFile.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
    return trackBoxes;
  }
private Movie getRotatedMovieOfTrackBox(List<TrackBox> trackBoxes) {
    Movie rotatedMovie = new Movie();
    // 旋转
    for (TrackBox trackBox : trackBoxes) {
      trackBox.getTrackHeaderBox().setMatrix(Matrix.ROTATE_90);
      rotatedMovie.addTrack(new Mp4TrackImpl(trackBox));
    }
    return rotatedMovie;
  }
private void writeMovieToModifiedFile(Movie movie) {
    Container container = new DefaultMp4Builder().build(movie);
    File modifiedVideoFile = new File(videoFilePath.replace(".mp4", "_MOD.mp4"));
    FileOutputStream fos;
    try {
      fos = new FileOutputStream(modifiedVideoFile);
      WritableByteChannel bb = Channels.newChannel(fos);
      container.writeContainer(bb);
      // 关闭文件流
      fos.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

本文对Android视频录制中常见的问题进行了说明,希望对大家的学习有所帮助。

(0)

相关推荐

  • Android自定义录制视频功能

    Android录制视频MediaRecorder+SurfaceView的使用方法,供大家参考,具体内容如下 先看效果图: <1>将视频动画显示到SurfaceView控件上 <2>使用MediaRecorder类进行视频的录制 常用的方法: mediaRecorder.reset(); mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); //从照相机采集视频 mediaRecorder.setAudioS

  • Android录制声音文件(音频)并播放

    本文实例为大家分享了Android录制音频文件的具体代码,供大家参考,具体内容如下 1.这个demo中没有对多次点击同一个声音文件做详细处理,偶尔会有崩溃,用的时候需要注意. 2.按住录音按钮录音过程中,只对竖直方向处理了一下,水平方向没写: 3.没有做删除某个声音文件的操作,但是测试的时候实现了功能,需要用到的话,在MainActivity->onItemClick中的TODO中有详细说明: 4.这只是个demo,如果要在项目中使用,先写出demo,没问题了,再引入项目,在写成demo后,在真

  • Android音频录制MediaRecorder之简易的录音软件实现代码

    使用MediaRecorder的步骤:1.创建MediaRecorder对象2.调用MediRecorder对象的setAudioSource()方法设置声音的来源,一般传入MediaRecorder.MIC3.调用MediaRecorder对象的setOutputFormat()设置所录制的音频文件的格式4.调用MediaRecorder对象的setAudioRncoder().setAudioEncodingBitRate(int bitRate).setAudioSamlingRate(i

  • Android 微信小视频录制功能实现详细介绍

    Android 微信小视频录制功能 开发之前 这几天接触了一下和视频相关的控件, 所以, 继之前的微信摇一摇, 我想到了来实现一下微信小视频录制的功能, 它的功能点比较多, 我每天都抽出点时间来写写, 说实话, 有些东西还是比较费劲, 希望大家认真看看, 说得不对的地方还请大家在评论中指正. 废话不多说, 进入正题. 开发环境 最近刚更新的, 没更新的小伙伴们抓紧了 Android Studio 2.2.2 JDK1.7 API 24 Gradle 2.2.2 相关知识点 视频录制界面 Surf

  • Android使用MediaRecorder类进行录制视频

    我们提醒大家使用MediaRecorder录音录像的设置代码步骤一定要按照API指定的顺序来设置,否则报错 步骤为: 1.设置视频源,音频源,即输入源 2.设置输出格式 3.设置音视频的编码格式 一.首先看布局文件,这里有一个SurfaceView,这是一个绘制容器,可以直接从内存或者DMA等硬件接口取得图像数据, <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tool

  • 解决Android MediaRecorder录制视频过短问题

    具体表现: 调用MediaRecorder的start()与stop()间隔不能小于1秒(有时候大于1秒也崩),否则必崩.  错误信息: java.lang.RuntimeException: stop failed. at android.media.MediaRecorder.stop(Native Method)  解决办法: 在stop以前调用setOnErrorListener(null);就行了! 相关代码: /** 开始录制 */ @Override public MediaPar

  • Android仿微信、录制音频并发送功能

    MyRecorder(仿微信,录制音频并发送功能) ①布局实现(activity_main.xml) 布局采用线性布局,上面使用的一个ListView,下面使用的是一个自定义的Button(会在下面进行介绍) <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

  • Android下录制App操作生成Gif动态图的全过程

    Android App开发完了,自然希望录个gif做个展示.视频也可以做展示,但是需要上传到优酷.土豆等等,而且本来就十几秒的App演示操作过程,视频网站的广告就要一分钟,没有gif轻量简单省流量. 下图是我录制的一个短信消灭器应用的效果图: 本文教大家如何录制gif,分享给大家供大家参考,具体内容如下 思路 生成gif的思路是两步 1.把App操作过程录制成视频 2.根据视频转换成Gif 目前网上录制GIf的思路也基本都是分为这2步,不知道有没有更好的方法,一步就生成gif动态的? 利用adb

  • Android音频处理之通过AudioRecord去保存PCM文件进行录制,播放,停止,删除功能

    音频这方面很博大精深,我这里肯定讲不了什么高级的东西,最多也只是一些基础类知识,首先,我们要介绍一下Android他提供的录音类,实际上他有两个,一个是MediaRecorder,还有一个就是我们今天要用到的AudioRecord,那他们有什么区别呢? 一.区别 MediaRecorder和AudioRecord都可以录制音频,区别是MediaRecorder录制的音频文件是经过压缩后的,需要设置编码器.并且录制的音频文件可以用系统自带的Music播放器播放. 而AudioRecord录制的是P

  • android仿微信聊天界面 语音录制功能

    本例为模仿微信聊天界面UI设计,文字发送以及语言录制UI. 1先看效果图: 第一:chat.xml设计 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" andro

随机推荐