前言
在Android开发中自然少不了对视频录制的需求,然而视频录制虽然有系统提供给我们能够直接使用的API,但是我们往往在完成需求的过程中需要自定义实现短视频录制。网上虽然也有不少资料,但是总是零零碎碎的,因此自己收集了多方面的资料自己写了一个demo来理解和学习短视频录制,记录在此同时也希望能够给大家带来一些参考和启发。
一、调用系统相机的视频录制
首先我们来了解Andorid系统为我们提供的系统相机的API的实现方式,系统相机相对而言呢好处当然是使用简单,视频清晰啦。
首先记得添加必要的权限
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
因为这里我们主要的关注点在相机的使用上,关于Android6.0的权限管理这里就不做多的插入了,有需要的朋友可以到这儿学习学习啦
好的,回到主题,我们调用视频相机录制的核心代码如下
Uri fileUri = Uri.fromFile(getOutputMediaFile())
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE)
intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri)
intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 10)
startActivityForResult(intent, RECORD_SYSTEM_VIDEO)
简单讲解一下,这个MediaStore.ACTION_VIDEO_CAPTURE 顾名思义就是我们调用系统视频录制的action。开始我们应该设置一个Uri作为我们想要保存视频的路径,如果我们不传递自定义的路径,那么系统就会将录制后的视频保存在默认的路径(一般在DCIM/Camera)下。限制的视频录制时间单位是秒(s),这里我们就是设置成为10s。
好了,下面贴出这一段完整的demo代码
public static final int RECORD_SYSTEM_VIDEO = 1;
private VideoView mVideoView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mVideoView = (VideoView) findViewById(R.id.videoView);
}
/**
* 启用系统相机录制
*
* @param view
*/
public void reconverIntent(View view) {
Uri fileUri = Uri.fromFile(getOutputMediaFile());
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 10);
startActivityForResult(intent, RECORD_SYSTEM_VIDEO);
}
private File getOutputMediaFile() {
if(!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()){
Toast.makeText(this, "请检查SDCard!", Toast.LENGTH_SHORT).show();
return null;
}
File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory
(Environment.DIRECTORY_DCIM), "MyCameraApp");
if (!mediaStorageDir.exists()) {
mediaStorageDir.mkdirs();
}
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
File mediaFile = new File(mediaStorageDir.getPath() + File.separator + "VID_" + timeStamp + ".mp4");
return mediaFile;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != RESULT_OK) {
return;
}
switch (requestCode) {
case RECORD_SYSTEM_VIDEO:
mVideoView.setVideoURI(data.getData());
mVideoView.start();
break;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
这里是layout布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.liuzhongjun.cameratest.my.MainActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="reconverIntent"
android:text="启动系统视频录制视频"
android:textAllCaps="false" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="customVideo"
android:text="启动自定义相机录制视频" />
<VideoView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 多说一点,因为国内某些手机对相机做了系统上的修改,可能通过我们传递的fileUri无法保存到相对应的位置,因此我们可以在onActivityResult的回调数据中通过下面的方法自行保存视频
try {
AssetFileDescriptor videoAsset = getContentResolver().openAssetFileDescriptor(data.getData(), "r");
FileInputStream fis = videoAsset.createInputStream();
File tmpFile = new File(Environment.getExternalStorageDirectory(),"VideoFile.mp4");
FileOutputStream fos = new FileOutputStream(tmpFile);
byte[] buf = new byte[1024];
int len;
while ((len = fis.read(buf)) > 0) {
fos.write(buf, 0, len);
}
fis.close();
fos.close();
} catch (IOException io_e) {
}
OK,简单的使用就完成了,感觉时候还是很简单吧。然而优点虽然是简单好用,缺点就是无法自定义,相机默认录制的视频又过大(曾经用华为mate9亲测录制10s的视频大小就达到了19M多,Mi2s也差不多有10M,不同的手机因为手机分频率的不同就能够有很大的差别),按这样下去用户录制一点视频上传所耗费流量和时间都不少了啊,然而通过我们的自定义视频录制,10s的视频我们能够控制在2M以内,另外我们也可以加入视频录制的暂停和继续,同时可以更加需求做出各种各样的自定义功能。噼里啪啦说了这么多好处,那么就开始我们的自定义视频录制之旅吧!
二、自定义视频录制
因为我们在这里会使用到自定义相机的某些硬件特性,因此我们还需要在AndroidManifest.xml中加入如下特征(之前的系统相机所需要的权限这里同样需要),保证一些功能的正常使用。
<uses-feature
android:name="android.hardware.camera"
android:required="true" />
<uses-feature android:name="android.hardware.camera.autofocus" />
自定义视频录制相对系统相机录制而言较为复杂,在正式使用之前我们先来了解这个类 MediaRecorder
点击会链接到官网该类API的介绍,我们可以知道通过MediaRecorder我们就可以实现视频与音频的录制并将其合并为标准的视频格式。
另外如果英语还不错或者想要了解官方资料的,对于自定义音视频录制的实现流程完全可用通过这里:
接下来让我们好好分析下实现流程:
1.首先我们需要实现相机的预览(这里就需要通过Camera+SurfaceView实现自定义相机的显示)
2.监听录像按钮,在点击录像的同时完成对MediaRecorder初始化和参数配置,启动音视频录制。
OK,有了思路就就明白要怎么走了。首先我们需要实现相机的预览
1.实现相机预览
首先我们需要有显示界面相应的布局文件
<?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:gravity="center">
<SurfaceView
android:id="@+id/record_surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#20ffffff"
android:padding="10dp">
<ImageView
android:id="@+id/record_control"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_centerInParent="true"
android:contentDescription="@string/begin"
android:onClick="startRecord"
android:src="@drawable/recordvideo_start" />
<Chronometer
android:id="@+id/record_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:format="%s" />
</RelativeLayout>
</RelativeLayout>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
接下来是实现代码
public class CustomRecordActivity extends AppCompatActivity implements View.OnClickListener {
private ImageView mRecordControl;
private SurfaceView surfaceView;
private SurfaceHolder mSurfaceHolder;
private Chronometer mRecordTime;
private boolean isPause;
private boolean isRecording;
private long mRecordCurrentTime = 0;
private File mVecordFile;
private Camera mCamera;
private MediaRecorder mediaRecorder;
private MediaRecorder.OnErrorListener onErrorListener = new MediaRecorder.OnErrorListener() {
@Override
public void onError(MediaRecorder mediaRecorder, int what, int extra) {
try {
if (mediaRecorder != null) {
mediaRecorder.reset();
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_custom);
initView();
}
private void initView() {
surfaceView = (SurfaceView) findViewById(R.id.record_surfaceView);
mRecordControl = (ImageView) findViewById(R.id.record_control);
mRecordTime = (Chronometer) findViewById(R.id.record_time);
mRecordControl.setOnClickListener(this);
mSurfaceHolder = surfaceView.getHolder();
mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
mSurfaceHolder.setFixedSize(320, 280);
mSurfaceHolder.setKeepScreenOn(true);
mSurfaceHolder.addCallback(mCallBack);
}
private SurfaceHolder.Callback mCallBack = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
initCamera();
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
if (mSurfaceHolder.getSurface() == null) {
return;
}
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
stopCamera();
}
};
/**
* 初始化摄像头
*
* @author liuzhongjun
* @date 2016-3-16
*/
private void initCamera() {
if (mCamera != null) {
stopCamera();
}
mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
if (mCamera == null) {
Toast.makeText(this, "未能获取到相机!", Toast.LENGTH_SHORT).show();
return;
}
try {
mCamera.setPreviewDisplay(mSurfaceHolder);
setCameraParams();
mCamera.startPreview();
} catch (IOException e) {
Log.d(TAG, "Error starting camera preview: " + e.getMessage());
}
}
/**
* 设置摄像头为竖屏
*
* @author liuzhongjun
* @date 2016-3-16
*/
private void setCameraParams() {
if (mCamera != null) {
Camera.Parameters params = mCamera.getParameters();
if (this.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE) {
params.set("orientation", "portrait");
mCamera.setDisplayOrientation(90);
} else {
params.set("orientation", "landscape");
mCamera.setDisplayOrientation(0);
}
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
params.setRecordingHint(true);
if (params.isVideoStabilizationSupported())
params.setVideoStabilization(true);
mCamera.setParameters(params);
}
}
/**
* 释放摄像头资源
*
* @author liuzhongjun
* @date 2016-2-5
*/
private void stopCamera() {
if (mCamera != null) {
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.record_control:
if (!isRecording) {
} else {
}
break;
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
通过上面的代码即可实现了自定义相机的显示,能够有预览图像了。
另外,建议我们在AndroidManifest.xml声明的时候,设置screenOrientation为一直竖屏,这样能够防止录像时横竖切换造成的数据丢失(我看到的系统相机也都是固定为竖屏的,什么?你说看到横屏的录像?还不拿来我看看?),设置代码如下:
<activity
android:name=".my.CustomRecordActivity"
android:screenOrientation="portrait">
</activity>
2.实现视频录制功能
来吧,将MediaRecord的配置流程按照官网的样子来一波吧!(图片来自Android开发者官网)
原文是英文,这里我通过谷歌浏览器将其翻译了方便大家更好的观看
这一个系列标准的步骤中,我们需要着重理解的是第4步的开始录制视频:首先我们必须按照这样的顺序来配置MediaRecorder,what?你问我为什么?没有为什么想搞懂就去看源码去!(反正我知道你不按照套路来你就不行)。
ok,下面是实现的demo代码,相对于官方的demo做了一些优化和调整。
/**
* 开始录制视频
*/
public void startRecord() {
boolean creakOk = createRecordDir();
if (!creakOk) {
return;
}
initCamera();
mCamera.unlock();
setConfigRecord();
try {
mediaRecorder.prepare();
mediaRecorder.start();
} catch (IOException e) {
e.printStackTrace();
}
isRecording = true;
if (mRecordCurrentTime != 0) {
mRecordTime.setBase(SystemClock.elapsedRealtime() - (mRecordCurrentTime - mRecordTime.getBase()));
} else {
mRecordTime.setBase(SystemClock.elapsedRealtime());
}
mRecordTime.start();
}
/**
* 停止录制视频
*/
public void stopRecord() {
if (isRecording && mediaRecorder != null) {
mediaRecorder.setOnErrorListener(null);
mediaRecorder.setPreviewDisplay(null);
mediaRecorder.stop();
mediaRecorder.reset();
mediaRecorder.release();
mediaRecorder = null;
mRecordTime.stop();
mRecordControl.setEnabled(true);
mPauseRecord.setEnabled(false);
isRecording = false;
}
}
/**
* 创建视频文件保存路径
*/
private boolean createRecordDir() {
if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
Toast.makeText(this, "请查看您的SD卡是否存在!", Toast.LENGTH_SHORT).show();
return false;
}
File sampleDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "Record");
if (!sampleDir.exists()) {
sampleDir.mkdirs();
}
String recordName = "VID_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + ".mp4";
mVecordFile = new File(sampleDir, recordName);
return true;
}
/**
* 配置MediaRecorder()
*/
private void setConfigRecord() {
mediaRecorder = new MediaRecorder();
mediaRecorder.reset();
mediaRecorder.setCamera(mCamera);
mediaRecorder.setOnErrorListener(OnErrorListener);
mediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
CamcorderProfile mProfile = CamcorderProfile.get(CamcorderProfile.QUALITY_480P);
mediaRecorder.setAudioEncodingBitRate(44100);
if (mProfile.videoBitRate > 2 * 1024 * 1024)
mediaRecorder.setVideoEncodingBitRate(2 * 1024 * 1024);
else
mediaRecorder.setVideoEncodingBitRate(1024 * 1024);
mediaRecorder.setVideoFrameRate(mProfile.videoFrameRate);
mediaRecorder.setOrientationHint(90);
mediaRecorder.setVideoSize(352, 288);
mediaRecorder.setOutputFile(mVecordFile.getAbsolutePath());
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
核心代码就这么些啦,感觉重要部分注释已经标注的很清楚了!
有什么建议和问题欢迎大家提出。
源代码下载地址(含视频录制的暂停继续):https://github.com/Gentleman-jun/VideoRecordDemo