这一篇开始讲视频播放,这是整个项目最重要的部分,所以尽量说的详细点。我们的视频播放使用的是surfaceView+MediaPlayer,下面一步一步来看具体的实现,先看效果图:
一. 初始化
1. 进入PlayActivity后,肯定是需要先初始化此页面的所有控件,这个就不多说了。然后看其他初始化的信息:
@Overrideprotected void initView() {mHolder = mSv.getHolder();mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);mHolder.addCallback(this);Intent intent = getIntent();mVideoFrom = intent.getIntExtra(Contants.VIDEO_FROM, Contants.LOCAL);mCurrentPosition = intent.getIntExtra(Contants.VIDEO_POSITION, 0);mVideoList = (List<VideoInfo>) intent.getSerializableExtra(Contants.VIDEO_FILES);if (mVideoList == null || mVideoList.size() == 0) {Toast.makeText(this, "没有可播放的视频", Toast.LENGTH_SHORT).show();finish();}if (mCurrentPosition < mVideoList.size()) {mVideo = mVideoList.get(mCurrentPosition);}visibleSurfaceTopAndBottom();mHandler.sendEmptyMessage(SYSTEM_TIME_CHANED);}
首先从SurfaceView中获取SurfaceHolder对象mHolder,然后调用addCallback方法为mHolder设置回调接口,此接口中包括surfaceCreated,surfaceChanged,surfaceDestroyed三个方法,来控制SurfaceView内部的surface的生命周期。再是获取intent传递过来的数据,分别赋给mVideoFrom(视频来源),mCurrentPosition(视频在集合中的位置),mVideoList(视频集合),对集合做一些不合法判断的处理。最后,调用了visibleSurfaceTopAndBottom方法和使用mHandler发送了一个SYSTEM_TIME_CHANED的空消息。
visibleSurfaceTopAndBottom方法后边会详细讲,这里我们调用,只是为了一开始播放时隐藏播放界面的上下两个布局。看横屏的效果图,可以看到右上角有一个系统时间的显示,发送SYSTEM_TIME_CHANED消息就是为了获取系统时间并显示在右上角。
当然,在setListener中还需要对一些控件设置监听,这个就不贴代码了,回头再源码中自己看。
二. 视频的播放/暂停:
使用SurfaceView播放视频,必须等到其内部的surface初始化完成后才可以播放,所以在surfaceCreated方法中初始化MediaPlayer:
public void surfaceCreated(SurfaceHolder holder) {mVideoPlayer = new MediaPlayer();mVideoPlayer.setDisplay(mHolder);mVideoPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);mVideoPlayer.setOnCompletionListener(PlayActivity.this);// 错误监听回调函数mVideoPlayer.setOnErrorListener(PlayActivity.this);// 设置缓存变化监听mVideoPlayer.setOnBufferingUpdateListener(PlayActivity.this);play(mPlayPosition);}
首先创建MediaPlayer对象mVideoPlayer,设置显示画面为mHolder,再设置音频流为STREAM_MUSIC类型,然后为mVideoPlayer设置各种监听。最后调用play方法播放视频。
private void play(final int playPosition) {try {//获取音频焦点if (Utils.getAudioFocus(PlayActivity.this, null)) {mVideoPlayer.reset();mVideoPlayer.setDataSource(mVideo.getUrl());mVideoPlayer.prepare();mVideoPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {@Overridepublic void onPrepared(MediaPlayer mp) {try {mVideoPlayer.seekTo(playPosition);mVideoPlayer.setScreenOnWhilePlaying(true);updateUiInfo();mVideoPlayer.start();} catch (IllegalStateException e) {e.printStackTrace();Toast.makeText(PlayActivity.this, "非法状态", Toast.LENGTH_LONG).show();}}});}} catch (IOException e) {e.printStackTrace();Toast.makeText(this, "加载视频错误,可能格式不支持!", Toast.LENGTH_LONG).show();} catch (IllegalStateException e) {e.printStackTrace();Toast.makeText(this, "非法状态", Toast.LENGTH_LONG).show();}}
来看这段代码,首先调用reset方法是mVideoPlayer进入Idle(空闲)状态,然后调用setDataSource设置播放视频的路径,成功后进入Initialized状态,如果不是Idle状态调用setDataSource方法,则会抛IllegalStateException 异常。在Initialized状态下,调用prepare方法进入prepared状态,成功后调用onPrepared方法。如果不是Initialized状态,调用prepare方法也会抛IllegalStateException 异常。
在onPrepared方法中,现将视频恢复到之前播放的位置(一般会在onPause中保存播放位置),设置播放时屏幕常亮,更新界面上视频相关的一下信息,最后,调用start方法播放视频,此时mVideoPlayer就处于Started状态。
如果播放的视频格式不支持,则会抛IOException异常。
注意:一开始我们有获取音频焦点,为什么要获取这个呢?如果你不获取音频焦点,当你的手机上在播放音乐时,此时你打开视频播放器播放视频,音乐和视频是会同时播放的,而如果你获取了音频焦点,那么音乐播放去就会失去焦点,这个时候音乐会暂停,保证同一时刻只有一个声音播放,不至于混乱。
当点击播放按钮时,如果视频在播放,则暂停播放,如果视频暂停,则播放视频,在onClick方法中的代码如下:
case R.id.play_play:if (isPlaying()) {mVideoPlayer.pause();changeState(PAUSE);} else {if (mVideoState == PAUSE) {mVideoPlayer.start();changeState(PLAY);} else if (mVideoState == STOP) {play(0);}}break;
每次播放暂停时,都需要通过changeState方法来改变播放状态。
在finish页面或者Activity进入Stop状态时,surfaceDestroyed方法会被调用,此时应该释放的mVideoPlayer占用的资源,因为下次进来会重新初始化mVideoPlayer。
@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {if (mVideoPlayer != null) {changeState(STOP);mVideoPlayer.release();mVideoPlayer = null;}}
三. 上一首,下一首功能:
(1) 下一首:
private void playNext() {mCurrentPosition++;if (mCurrentPosition < mVideoList.size()) {mVideo = mVideoList.get(mCurrentPosition);play(0);} else {mCurrentPosition--;Toast.makeText(PlayActivity.this, "已经是最后一个了",Toast.LENGTH_SHORT).show();}}
将mCurrentPosition++,判断是否超出集合范围,如果没有,获取当前的视频,调用play播放此视频。如果已经是最后一个,在给出提示。
(2)上一首:
private void playPrevious() {mCurrentPosition--;if (mCurrentPosition >= 0) {mVideo = mVideoList.get(mCurrentPosition);play(0);} else {mCurrentPosition++;Toast.makeText(PlayActivity.this, "已经是第一个了",Toast.LENGTH_SHORT).show();}}
将mCurrentPosition–,判断是否小于0,如果没有,则获取当前视频,调用play播放,如果已经是第一个了,则给出提示。
四. 进度条的更新及快进快退:
进度条我们使用seekbar控件。
(1) 更新进度条:
对于视频来说,一般调用start方法开始播放后,进度条就需要开始更新,所以我们在changeState方法中,当状态改变为PLAY时,开始更新进度条。
/*** 改变Video的状态** @param state*/private void changeState(int state) {mVideoState = state;mHandler.sendEmptyMessage(STATE_CHANGED);if (state == PLAY) {mHandler.post(new Runnable() {@Overridepublic void run() {/**防止当onDestroy方法调用时,mVideoPlayer* 已经为null,但是这边还在发消息,导致空指针异常**/if (mVideoPlayer == null)return;int position = mVideoPlayer.getCurrentPosition();mTopSeekBar.setProgress(position);mBottomSeekBar.setProgress(position);mTvPlayedTime.setText(Utils.formatToString(position));if (isPlaying()) {mHandler.postDelayed(this, 1000);}}});}}
可以看到,当state==PLAY时,我们handler的post方法启动一个线程更新进度条,每隔1秒更新一次。这里边有一个mVideoPlayer的判空操作,这点很重要,防止页面退出时调用onDestroy方法释放mVideoPlayer后,handler这边还在继续发送消息,此时mVideoPlayer已经为null,调用mVideoPlayer.getCurrentPosition()方法回报空指针异常。
这里我们的进度条有两个,一个在竖屏时显示,一个在横屏时显示,竖屏时是不能快进快退的。
(2) 快进快退:
seekbar本身支持快进快退的功能,但是在手动操作完成后,我们需要让mVideoPlayer在相应的位置播放,所以seekbar应该注册OnSeekBarChangeListener监听:
mTopSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {mHandler.sendEmptyMessage(SEEKBAR_TOUCHED);}});}
可以看到,监听中实现三个方法, onProgressChanged是当进度条改变时调用,onStartTrackingTouch方法是在开始滑动进度条时调用,onStopTrackingTouch是在滑动结束后调用。这里我们只需要在滑动结束后发送SEEKBAR_TOUCHED消息,改变mVideoPlayer的播放位置就好。
在hanlderMessage方法中对此消息的处理:
case SEEKBAR_TOUCHED:mVideoPlayer.seekTo(mTopSeekBar.getProgress());mBottomSeekBar.setProgress(mTopSeekBar.getProgress());mTvPlayedTime.setText(Utils.formatToString(mTopSeekBar.getProgress()));break;
调用seekTo方法改变播放位置,并且更新已播放时间。
(3) 在线视频缓存的更新:
记得在初始化mVideoPlayer时设置的一堆监听吗?有一个mVideoPlayer.setOnBufferingUpdateListener(PlayActivity.this),这个就是设置缓存变化的监听。当缓存发生变化时,会调用onBufferingUpdate方法,如下:
/**** @param mp* @param percent 表示缓存加载进度,0为没开始,100表示加载完成,* 在加载完成以后也会一直调用该方法*/@Overridepublic void onBufferingUpdate(MediaPlayer mp, int percent) {// 如果是本地视频,则不更新缓存进度(按理说本地缓存不会调用此方法,// 但不知道为什么rmvb格式的视频会调用这个,所以需要做此判断)if (mVideoFrom == Contants.LOCAL)return;if (!isVideoCacheComplate){int second = (mTopSeekBar.getMax() * percent / 100);mTopSeekBar.setSecondaryProgress(second);mBottomSeekBar.setSecondaryProgress(second);if (percent == 100){isVideoCacheComplate = true;}}}
参数precent表示缓存加载进度,因为当precent==100时,此方法还是会不停地调用,所以设置了一个标志isVideoCacheComplate,当percent==100时让isVideoCacheComplate = true,就不需要再设置缓存进度了。当然,当isVideoCacheComplate == false时,需要根据percent的值计算当前的缓存进度,然后通过setSecondaryProgress方法设置给seekbar。
按照个人理解,onBufferingUpdate放只有播放在线视频时才会调用,但是测试时发现在播放本地的rmvb格式时候是也会调用,所以需要判断当前视频的来源mVideoFrom,如果时本地视频直接返回。
来张在线视频的图:
可以看到,缓存完的进度是黄色的,对比之前的横屏图,没有缓存的是灰色的。
这一篇将的东西有些多,还有两个功能播放界面顶部底部布局的隐藏和横竖屏的切换放在下一篇总结中讲,这一篇就到这里。