視訊播放(三)——視訊播放
這一篇開始講視訊播放,這是整個專案最重要的部分,所以儘量說的詳細點。我們的視訊播放使用的是surfaceView+MediaPlayer,下面一步一步來看具體的實現,先看效果圖:
一. 初始化
1. 進入PlayActivity後,肯定是需要先初始化此頁面的所有控制元件,這個就不多說了。然後看其他初始化的資訊:
@Override
protected 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() {
@Override
public 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。
@Override
public 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() {
@Override
public 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() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public 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表示載入完成,
* 在載入完成以後也會一直呼叫該方法
*/
@Override
public 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,如果時本地視訊直接返回。
來張線上視訊的圖:
可以看到,快取完的進度是黃色的,對比之前的橫屏圖,沒有快取的是灰色的。
這一篇將的東西有些多,還有兩個功能播放介面頂部底部佈局的隱藏和橫豎屏的切換放在下一篇總結中講,這一篇就到這裡。