Android自定义视频录制与播放 不错的文章,来不及看,先收藏了

来源:互联网 发布:输电线路三维设计软件 编辑:程序博客网 时间:2024/04/28 20:59

转自http://www.bg135.com/android-custom-video-recorder-play.html

Android自定义视频录制与播放

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 =newIntent(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=newSurfaceHolder.Callback() {
 
    @Override
    publicvoidsurfaceDestroyed(SurfaceHolder holder) {
        // TODO Auto-generated method stub
        Log.d(TAG,"surfaceDestroyed 将mVideoRecoderSurfaceHolder置空");
        mVideoRecoderSurfaceHolder =null;
    }
 
    @Override
    publicvoidsurfaceCreated(SurfaceHolder holder) {
        // TODO Auto-generated method stub
 
    }
 
    @Override
    publicvoidsurfaceChanged(SurfaceHolder holder,intformat, int width,
            intheight) {
        // 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
       privatevoidsetPreviewDisplay(SurfaceHolder holder) {
   try{
       camera.setPreviewDisplay(holder);
   }catch(Throwable ex) {
       closeCamera();
       thrownewRuntimeException("setPreviewDisplay failed", ex);
   }
}

在这个方法调用的时候,SurfaceHolder必须已经包含有surface,这个方法必须在startPreview()之前调用,如果预览的surface没有设或设置为null,则会报异常。

之后可以根据需要设定摄像机的一些参数,例如:设定预览大小,帧特率,旋转角度,白平衡,GPS坐标,录制模式,自动对焦等等。代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
privatevoidsetCameraParameters() {
        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();
            thrownewRuntimeException("startPreview failed", ex);
        }

此时你可以实时看到摄像机获取的图像,当然你可以左右、前后移动设备感受下录制的效果,在你需要的时候启动录制按钮,
在录制之前需要做些准备工作,首先需要初始化MediaRecorder对象,停止先前的预览,同时释放camera的锁定,使得MediaRecorder可以获得对camera的使用,

?
1
2
3
4
5
mMediaRecorder =newMediaRecorder();
 
        // Unlock the camera object before passing it to media recorder.
        camera.stopPreview();
        camera.unlock();

接下来是设定MediaRecorder录制视频的一些属性,视频的一些参数完全可以进行自定义,只要你非常精通这些参数之间的相互关系,不然很容易出现各种异常情况,
这里我们可以通过MediaRecorder的一些SET方法进行参数的自定义。

当然你也可以完全使用系统预设定的视频参数,可以通过以下代码获取:

?
1
2
3
4
@TargetApi(9)
    privatevoidreadVideoPreferences() {
        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();
            thrownewRuntimeException(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
privatevoidstopVideoRecording() {
        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,
                        newString[] { mCurrentVideoFilename},
                        newString[] {null},
                        newOnScanCompletedListener() {
                @Override
                publicvoidonScanCompleted(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,
                newString[] { mCurrentVideoFilename},
                newString[] {null},
                newOnScanCompletedListener() {
            @Override
            publicvoidonScanCompleted(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
privatevoidreleaseMediaRecorder() {
        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.
     */
    voidcreateMediaPlayerIfNeeded() {
        if(player ==null) {
            player =newMediaPlayer();
 
            // 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
privatevoidplayVideo(){
 
        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
    publicvoidonPrepared(MediaPlayer mp) {
        // TODO Auto-generated method stub
        Log.v(TAG,"onPrepared: ");
 
        setVideoLengthSize(getTimeString(Math.round(player.getDuration()/1000)));
 
        intmVideoWidth = mp.getVideoWidth();
        intmVideoHeight = 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.
     */
    voidconfigAndStartMediaPlayer() {
            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
privatevoidreleaseMediaplayer(){
            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”/>

原创粉丝点击