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