转自http://www.bg135.com/android-custom-video-recorder-play.html
Android系统提供了调用系统录像的功能,我们也可以通过指定一些参数来做一定的刻制化操作,
例如:我们可以指定参数MediaStore.EXTRA_VIDEO_QUALITY设定视频的质量,当前可以的值为0(低质量,176 x 144分辨率),1(高质量,如1080p (1920 x 1080)分辨率),
通过设置参数MediaStore.EXTRA_DURATION_LIMIT限定录制时间,
或者通过设置参数MediaStore.EXTRA_SIZE_LIMIT限定录制视频大小。实现代码如下:
1
2
3
4
5
|
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 2 ); // CamcorderProfile.get(0,CamcorderProfile.QUALITY_LOW);
//intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 5);
intent.putExtra(MediaStore.EXTRA_SIZE_LIMIT,VIDEO_MAX_SIZE);
startActivityForResult(intent, TAKE_VIDEO);
|
然而我们发现通过这种方式录制视频,不仅无法进行深度的刻制化,例如不能录制自定义的视频分辨率(320×240),也不能通过参数去设定白平衡,录像模式,自动对焦,开启闪光灯等等。
而且我们测试发现,就连传入的参数MediaStore.EXTRA_DURATION_LIMIT,MediaStore.EXTRA_SIZE_LIMIT也不是所有机型都能接收,也就是说,即使你限制了只录制5秒钟的视频,但超过时间视频仍然可以录制,
或者说你限制了只录制2M的视频,但当视频超过规定大小后,视频仍然在录制,直至用户停止或撑爆整个存储空间,
基于以上的事实,为了对录制视频深度的刻制化,解决手机平台间导致的差异,以下是我们本章的主题,自定义视频录制,以及解决视频花屏,视频倒转的问题。
在开始录制之前需要进行预览,视频的录制是通过SurfaceView来进行界面的渲染,要想使用SurfaceView,我们首先要获取到SurfaceHolder,在SurfaceView所在的窗口可见的时候,Surface将会被创建,
你可以通过以下代码获取SurfaceHolder的创建和销毁:
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
|
SurfaceHolder holder = mSurfaceview.getHolder();
holder.addCallback(videoRecorderSurfaceCallBck);
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
SurfaceHolder.Callback videoRecorderSurfaceCallBck= new SurfaceHolder.Callback() {
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// TODO Auto-generated method stub
Log.d(TAG, "surfaceDestroyed 将mVideoRecoderSurfaceHolder置空" );
mVideoRecoderSurfaceHolder = null ;
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
// TODO Auto-generated method stub
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
// TODO Auto-generated method stub
Log.v(TAG, "surfaceChanged width: " +width+ " , height: " +height);
if (holder.getSurface() == null ) {
Log.d(TAG, "holder.getSurface() == null" );
return ;
}
Log.d(TAG, "surfaceChanged 重新给mVideoRecoderSurfaceHolder赋值" );
mVideoRecoderSurfaceHolder = holder;
if (!playFlag){
if (mPausing) {
// We're pausing, the screen is off and we already stopped
// video recording. We don't want to start the camera again
// in this case in order to conserve power.
// The fact that surfaceChanged is called _after_ an onPause appears
// to be legitimate since in that case the lockscreen always returns
// to portrait orientation possibly triggering the notification.
return ;
}
// The mCameraDevice will be null if it is fail to connect to the
// camera hardware. In this case we will show a dialog and then
// finish the activity, so it's OK to ignore it.
if (camera == null ) return ;
// Set preview display if the surface is being created. Preview was
// already started.
if (holder.isCreating()) {
setPreviewDisplay(holder);
} else {
stopVideoRecording();
restartPreview();
}
} else {
playForPlayUsefulness();
}
}
};
|
在获取到SurfaceHolder之后,可以设定预览显示的SurfaceHolder:
1
2
3
4
5
6
7
8
|
private void setPreviewDisplay(SurfaceHolder holder) {
try {
camera.setPreviewDisplay(holder);
} catch (Throwable ex) {
closeCamera();
throw new RuntimeException( "setPreviewDisplay failed" , ex);
}
}
|
在这个方法调用的时候,SurfaceHolder必须已经包含有surface,这个方法必须在startPreview()之前调用,如果预览的surface没有设或设置为null,则会报异常。
之后可以根据需要设定摄像机的一些参数,例如:设定预览大小,帧特率,旋转角度,白平衡,GPS坐标,录制模式,自动对焦等等。代码如下:
1
2
3
4
5
6
7
8
9
10
11
|
private void setCameraParameters() {
mParameters = camera.getParameters();
mParameters.setPreviewSize(VIDEO_WIDTH, VIDEO_HEIGHT);
mParameters.setPreviewFrameRate(mProfile.videoFrameRate);
mParameters.setRotation( 90 );
camera.setParameters(mParameters);
// Keep preview size up to date.
mParameters = camera.getParameters();
}
|
这里先通过方法camera.getParameters()获取摄像机原先的一些配置信息,如果这里进行了修改,则必须再次调用setParameters方法才能使得参数生效。
经过以上准备工作之后可以启动视图预览:
1
2
3
4
5
6
7
|
try {
camera.startPreview();
mPreviewing = true ;
} catch (Throwable ex) {
closeCamera();
throw new RuntimeException( "startPreview failed" , ex);
}
|
此时你可以实时看到摄像机获取的图像,当然你可以左右、前后移动设备感受下录制的效果,在你需要的时候启动录制按钮,
在录制之前需要做些准备工作,首先需要初始化MediaRecorder对象,停止先前的预览,同时释放camera的锁定,使得MediaRecorder可以获得对camera的使用,
1
2
3
4
5
|
mMediaRecorder = new MediaRecorder();
// Unlock the camera object before passing it to media recorder.
camera.stopPreview();
camera.unlock();
|
接下来是设定MediaRecorder录制视频的一些属性,视频的一些参数完全可以进行自定义,只要你非常精通这些参数之间的相互关系,不然很容易出现各种异常情况,
这里我们可以通过MediaRecorder的一些SET方法进行参数的自定义。
当然你也可以完全使用系统预设定的视频参数,可以通过以下代码获取:
1
2
3
4
|
@TargetApi ( 9 )
private void readVideoPreferences() {
mProfile = CamcorderProfile.get( 0 ,CamcorderProfile.QUALITY_LOW); //240X320
}
|
get方法传入的第一个参数是摄像头的ID号,0为后置摄像头,1为前置摄像头。第二个参数是系统与设定的视频参数ID,即视频的质量等级,其中QUALITY_LOW,QUALITY_HIGH是所有系统一定都含有的,
然而其他的视频等级QUALITY_480P,QUALITY_QVGA等并不是所有系统都包含有的。这些视频的质量等级均包含有设定好的参数值,例如:音视频编码,音视频比特率,音视频采样率,视频帧的宽高,声道以及录制视频长度等等。
这里我们采取的策略是一部分采用系统自定义的参数,一部分参数进行刻制化,代码如下:
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
|
mMediaRecorder.setCamera(camera);
// mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
// mMediaRecorder.setProfile(mProfile);
// mMediaRecorder.setOrientationHint(90);
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
/*mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.MPEG_4_SP);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mMediaRecorder.setAudioEncodingBitRate(AUDIO_ENCODE_BIT_RATE);
mMediaRecorder.setVideoEncodingBitRate(VIDEO_ENCODE_BIT_RATE);
mMediaRecorder.setVideoSize(VIDEO_WIDTH, VIDEO_HEIGHT);
mMediaRecorder.setAudioChannels(2);*/
mProfile.fileFormat=MediaRecorder.OutputFormat.MPEG_4;
mProfile.videoCodec=MediaRecorder.VideoEncoder.H264;
mProfile.audioCodec=MediaRecorder.AudioEncoder.AAC;
mProfile.videoFrameHeight=VIDEO_HEIGHT;
mProfile.videoFrameWidth=VIDEO_WIDTH;
mMediaRecorder.setProfile(mProfile);
mMediaRecorder.setOrientationHint( 90 );
// mMediaRecorder.setVideoSize(VIDEO_WIDTH, VIDEO_HEIGHT);
mMediaRecorder.setMaxDuration(RECORDER_TIME);
if (mVideoDirectory.exists() == false )
mVideoDirectory.mkdirs();
createVideoPath();
// mMediaRecorder.setOutputFile("sdcard/1.mp4");
mMediaRecorder.setOutputFile(mVideoFilename);
// mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
mMediaRecorder.setPreviewDisplay(mVideoRecoderSurfaceHolder.getSurface()); // 这个方法无须调用 ????
|
参数设定好以后,就可以进入prepare生命周期,
1
2
3
4
5
6
7
|
try {
mMediaRecorder.prepare();
} catch (IOException e) {
Log.i(TAG, "initializeRecorder e=" +e);
releaseMediaRecorder();
throw new RuntimeException(e);
}
|
参数设定的时候需要注意的是,有些参数的设定会有一些顺序的要求,例如:setOutputFile的参数设定必须在setOutputFormat参数设定之前,
几乎所有参数的设定都必须在prepare方法之前,同时prepare方法必须在start方法之前。
在一切初始化工作完成之后,可以放心的启动start方法,代码如下:
1
2
3
4
5
6
7
|
try {
mMediaRecorder.start(); // Recording is now started
} catch (RuntimeException e) {
Log.e(TAG, "Could not start media recorder. " , e);
releaseMediaRecorder();
return ;
}
|
至此录制视频真正的开始,在录制的时候,我们可以进行一些信息显示或UI的更新,例如:我们这里实时显示当前录制的时间,以及根据需要进行UI的隐藏与显示。
在录制超过我们设定的一些限制,例如:时间或大小等,或在录制发生了异常情况,此时我们都应该及时停止视频录制:
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
|
private void stopVideoRecording() {
Log.v(TAG, "stopVideoRecording" );
first_time_record = false ;
if (mMediaRecorderRecording) {
mMediaRecorder.setOnErrorListener( null );
mMediaRecorder.setOnInfoListener( null );
try {
mMediaRecorder.stop();
mCurrentVideoFilename = mVideoFilename;
Log.v(TAG, "Setting current video filename: "
+ mCurrentVideoFilename);
// Add the Video to the media store
MediaScannerConnection.scanFile( this ,
new String[] { mCurrentVideoFilename},
new String[] { null },
new OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
// TODO Auto-generated method stub
Log.v(TAG, "扫描完成: "
+ path+ " ,uri" +uri);
}
});
} catch (RuntimeException e) {
Log.e(TAG, "stop fail: " + e.getMessage());
deleteVideoFile(mVideoFilename);
}
mMediaRecorderRecording = false ;
updateRecordingIndicator( false );
timer.setVisibility(View.GONE); // ???
keepScreenOnAwhile();
mVideoFilename = null ;
}
releaseMediaRecorder(); // always release media recorder
}
|
一旦录制停止,你必须重新配置它,就像刚创建的时候,值得注意的是,如果在调用该方法前无有效的音/视频文件,则会报RuntimeException的异常,这个很容易发生在当执行完start()方法后立即执行stop()方法,
当这个异常发生的时候,应用可以采取相应的措施去清楚输出文件(例如:删除输出文件),因为当这个异常发生的时候,输出文件没有正确生成。
这里我们还加入了扫描文件的功能方法,使得录制的视频能及时被系统读取入媒体库,
1
2
3
4
5
6
7
8
9
10
11
12
|
// Add the Video to the media store
MediaScannerConnection.scanFile( this ,
new String[] { mCurrentVideoFilename},
new String[] { null },
new OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
// TODO Auto-generated method stub
Log.v(TAG, "扫描完成: "
+ path+ " ,uri" +uri);
}
});
|
停止录制之后更重要的工作是及时释放MediaRecorder以及Camera所引用的资源:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
private void releaseMediaRecorder() {
Log.v(TAG, "Releasing media recorder." );
if (mMediaRecorder != null ) {
cleanupEmptyFile();
mMediaRecorder.reset();
mMediaRecorder.release();
mMediaRecorder = null ;
}
// Take back the camera object control from media recorder. Camera
// device may be null if the activity is paused.
if (camera != null ) {
camera.lock();
camera.release();
camera= null ;
}
}
|
到此,整个视频的录制过程就结束了,如果在视频存放的目录找到了对应的视频,那么恭喜你成功了。
这个Demo不仅可以录制视频,还能在当前界面实时播放录制的视频,其播放使用的是MediaPlayer API,使用的过程和播放音乐的大致相同,
同样我们首先要初始化MediaPlayer的对象,以及设定一些监听回调方法,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
/**
* Makes sure the media player exists and has been reset. This will create the media player
* if needed, or reset the existing media player if one already exists.
*/
void createMediaPlayerIfNeeded() {
if (player == null ) {
player = new MediaPlayer();
// Make sure the media player will acquire a wake-lock while playing. If we don't do
// that, the CPU might go to sleep while the song is playing, causing playback to stop.
//
// Remember that to use this, we have to declare the android.permission.WAKE_LOCK
// permission in AndroidManifest.xml.
player.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
// we want the media player to notify us when it's ready preparing, and when it's done
// playing:
player.setOnPreparedListener( this );
player.setOnCompletionListener( this );
player.setOnErrorListener( this );
}
else
player.reset();
}
|
初始化完成后,设定播放的视频源以及用于显示的SurfaceHolder等。同时启动异步prepare:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
private void playVideo(){
createMediaPlayerIfNeeded();
try {
// Log.v(TAG, "mSurfaceHolder==null: " + (mSurfaceHolder==null));
Log.v(TAG, "mVideoPlaySurfaceHolder==null: " + (mVideoPlaySurfaceHolder== null ));
player.setDataSource(HereTalkCamera. this ,Uri.parse(mCurrentVideoFilename));
// player.setDisplay(mSurfaceHolder);
player.setDisplay(mVideoPlaySurfaceHolder);
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
player.setScreenOnWhilePlaying( true );
// starts preparing the media player in the background. When it's done, it will call
// our OnPreparedListener (that is, the onPrepared() method on this class, since we set
// the listener to 'this').
//
// Until the media player is prepared, we *cannot* call start() on it!
player.prepareAsync();
} catch (IOException e){
// TODO Auto-generated catch block
Log.v(TAG, "playVideo IOException: " + e);
e.printStackTrace();
}
}
|
在异步prepare完成之后,我们在回调方法onPrepared中可以执行一些播放相关的操作:
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
|
@Override
public void onPrepared(MediaPlayer mp) {
// TODO Auto-generated method stub
Log.v(TAG, "onPrepared: " );
setVideoLengthSize(getTimeString(Math.round(player.getDuration()/ 1000 )));
int mVideoWidth = mp.getVideoWidth();
int mVideoHeight = mp.getVideoHeight();
Log.v(TAG, "onPrepared mVideoWidth: " +mVideoWidth+ " , mVideoWidth: " +mVideoHeight);
// mSurfaceview.getHolder().setFixedSize(mVideoWidth, mVideoHeight);
configAndStartMediaPlayer();
mRecordingStartTime = SystemClock.uptimeMillis();
/*play_time = new myCountDownTimer(player.getDuration(), 500, 0);
play_time.start();*/
updateRecordingTime();
}
/**
* Reconfigures MediaPlayer according to audio focus settings and starts/restarts it. This
* method starts/restarts the MediaPlayer respecting the current audio focus state. So if
* we have focus, it will play normally; if we don't have focus, it will either leave the
* MediaPlayer paused or set it to a low volume, depending on what is allowed by the
* current focus settings. This method assumes mPlayer != null, so if you are calling it,
* you have to do so from a context where you are sure this is the case.
*/
void configAndStartMediaPlayer() {
Log.v(TAG, "player.isPlaying(): " + player.isPlaying());
if (player.isPlaying()) {
player.pause();
mMediaPlaying= false ;
} else {
player.start();
mMediaPlaying= true ;
}
}
|
需要注意的是,这里我们setDisplay方法传入的并不是视频录制时候的mSurfaceHolder,而是视频播放专用的mVideoPlaySurfaceHolder,
其原因在于,如果共用同一个,则会出现花屏的异常,更多详细解读请阅读文章:《Android自定义视频录制问题总结》
在视频播放完成或任何异常发生后,一个好的习惯是及时释放MediaPlayer所引用的相关资源:
1
2
3
4
5
6
7
8
9
|
private void releaseMediaplayer(){
Log.v(TAG, "releaseMediaplayer(): " );
if (player!= null ){
player.reset();
player.release();
player = null ;
}
mMediaPlaying= false ;
}
|
最后别忘了在Manifest.xml文件中加入权限:
<uses-permission android:name=”android.permission.RECORD_AUDIO”/>
<uses-permission android:name=”android.permission.MODIFY_AUDIO_SETTINGS”/>
<uses-permission android:name=”android.permission.WAKE_LOCK”/>
<uses-permission android:name=”android.permission.WRITE_EXTERNAL_STORAGE” />
<uses-permission android:name=”android.permission.CAMERA”/>