Android - SurfaceView + MediaPlayer實現(xiàn)分段視頻無縫播放
來源:程序員人生 發(fā)布時間:2015-04-24 07:49:00 閱讀次數(shù):5820次
Android當中實現(xiàn)視頻播放的方式有兩種,即:通過VideoView實現(xiàn)或通過SurfaceView + MediaPlayer實現(xiàn)。
由淺至深,首先來看下想要在Android上播放1段視頻,我們應當怎樣做。
前面我們已提到了兩種方式,這里我們來看1下具有更好的拓展性的第2種方式,也就是通過SurfaceView + MediaPlayer進行實現(xiàn)。
首先,我們來定義1個布局文件以下,為了方便起見,我們僅僅只在該布局中定義了1個SurfaceView:
<?xml version="1.0" encoding="utf⑻"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/videoLayout" >
<SurfaceView
android:id="@+id/surface"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="center">
</SurfaceView>
</FrameLayout>
接著就是Activity類文件的定義:
package com.example.videodemo;
import android.app.Activity;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class VideoPlayActivity extends Activity implements
SurfaceHolder.Callback {
/** Called when the activity is first created. */
MediaPlayer player;
SurfaceView surface;
SurfaceHolder surfaceHolder;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_play);
initView();
}
private void initView() {
surface = (SurfaceView) findViewById(R.id.surface);
surfaceHolder = surface.getHolder(); // SurfaceHolder是SurfaceView的控制接口
surfaceHolder.addCallback(this); // 由于這個類實現(xiàn)了SurfaceHolder.Callback接口,所以回調(diào)參數(shù)直接this
}
@Override
public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
}
@Override
public void surfaceCreated(SurfaceHolder arg0) {
// 必須在surface創(chuàng)建后才能初始化MediaPlayer,否則不會顯示圖象
player = new MediaPlayer();
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
player.setDisplay(surfaceHolder);
// 設置顯示視頻顯示在SurfaceView上
try {
player.setDataSource("你要播放的視頻的url");
player.prepare();
player.start();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void surfaceDestroyed(SurfaceHolder arg0) {
// TODO Auto-generated method stub
}
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
if (player.isPlaying()) {
player.stop();
}
player.release();
// Activity燒毀時停止播放,釋放資源。不做這個操作,即便退出還是能聽到視頻播放的聲音
}
}
由此你可以看到,這類實現(xiàn)方式有幾點值得注意的地方是:
1、你需要1個媒體播放器對象"MediaPlayer",該對象會負責播放你指定的視頻。
2、如果說MediaPlayer負責播放視頻,那末我們剛剛定義的SurfaceView則用于在屏幕中顯示播放視頻。
(所以又可以理解為,如果MediaPlayer是1副畫,而SurfaceView則是讓這幅畫顯現(xiàn)在人們眼前的畫紙)
3、MediaPlayer類的成員方法設置用于顯示媒體視頻的SurfaceHolder,正如上面所說,就猶如你擇不同的畫紙來顯現(xiàn)你的畫。
4、MediaPlayer類的成員方法setDataSource用于指定你要播放的視頻數(shù)據(jù)源。
5、僅僅是設置完數(shù)據(jù)源是不足夠的,設置完數(shù)據(jù)源和顯示的Surface后,你需要調(diào)用prepare()或prepareAsync()來讓你的視頻數(shù)據(jù)源stand by..
6、所以你也可能已發(fā)現(xiàn),對1段視頻的播放,MediaPlayer是關鍵,關于該類的更多使用,這篇博客里有更詳細的說明:Android - MediaPlayer類的使用說明
由此我們已基本掌握了,在android端簡單的播放視頻的方法。1切看上去10分美好。
但做開發(fā)就是有這么蛋疼,maybe有很多時候為了加快video與server端之間上傳于下載的速率,有時候會對視頻做分段處理。
正猶如做web開發(fā)時,上傳和下載文件時,如果文件過大,很多時候我們會選擇對文件做“切割處理1樣”。
那末這個時候,就出現(xiàn)了1種情況,就是可能你要播放的1段視頻,
事實上是由幾小段視頻組合而成的。所以就觸及到了連續(xù)播放。
可能當面對到這樣的需求時,我們首先最容易想到的就是:
對每段視頻進行監(jiān)聽,當監(jiān)聽到它播放結束時,立刻做Refresh切換到下1段視頻分段的播放。
而MediaPlayer的確也提供了這樣的監(jiān)聽事件,正是:MediaPlayer.OnCompletionListener()。
我在網(wǎng)上查閱相干實現(xiàn)的功能時,也只看到類似的說法,也就是說在該監(jiān)聽內(nèi)做實現(xiàn):
當1段數(shù)據(jù)源播放終了后,履行player.reset()釋放數(shù)據(jù)源,然后再設置新的資源進行播放。
但這樣做有很大的1個弊端就是,reset掉舊的數(shù)據(jù)源以后,新的數(shù)據(jù)源會有1段“加載時間”。
也就是說,在這段時間內(nèi),用戶看到的播放界面就處于1個停頓狀態(tài)。
那末,為了最大化的避免這個所謂的“停頓時間”,又應當怎樣去做呢?
首先斟酌到的便是,在1段視頻開始播放的同時,便開始做第2段視頻播放的“準備工作”。
但是通過前面的例子我們之前看到了,基于MediaPlayer本身的特性和限制。
如果我們想要實現(xiàn)這樣的方式,那末單1的MediaPlayer是滿足不了我們的需求的。
所以我們要做的工作便是:當我們進入視頻播放界面,第1段視頻準備終了,開始播放后,
便開始著手初始化另外一個新的MediaPlayer,這個新的MediaPlayer的數(shù)據(jù)源固然是接下來要播放的下1段視頻的url!
當這個MediaPlayer對象的準備工作都弄定后,剩下的工作就是:
我們需要“1顆釘子”,來將兩個分段的視頻段連接起來。
而這個釘子就是Android r16后添加的1個方法:setNextMediaPlayer()方法。
關于這個方法的使用,我找了又找,終究在1篇文章里,看到了1個這樣簡短的說明:
在第1個MediaPlayer類履行結束前的任什么時候間調(diào)用setNextMediaPlayer(MediaPlayernext)這個方法,
該方法的參數(shù)是第2個文件創(chuàng)建的MediaPlayer實例。然后Android系統(tǒng)將會在您第1個停止的時候緊接著播放第2個文件。
但我認為,在這個說明里,你應當注意到的關鍵點是:第1個MediaPlayer類履行結束前的任什么時候間調(diào)用這個方法。
也就是說,你必須在前1個MediaPlayer對象播放終了之前使用該方法。
例如我后來發(fā)現(xiàn),如果理想的在我們前面提到的OnCompletionListener監(jiān)聽中使用該方法,是無效的。
并且,似乎其實不如該說明而言的“Android系統(tǒng)將會在您第1個停止的時候緊接著播放第2個文件”。
也就是說,這個切換播放的動作不是自動的,還需要我們手動的做1個小的控制,馬上接下來就會說到。
到了這里,我們要實現(xiàn)的思路已很明確了:在1段視頻播放的同時,做下1段視頻的player的初始化準備工作。
而此時另外一個格外需要記住的就是:不要再在UI線程去開啟新的MediaPlayer的賦值工作.
原理很簡單,其實也是Android開發(fā)所必須記住的,即是永久不要在UI線程里去做耗時的操作。
這樣做的后果基本有幾種,1種是報告“在主線程做了太多操作”的異常,而另外也可能出現(xiàn),屏幕響應緩慢,
也就是說,例如你的視頻播放界面可能還存在1些按鈕和響應事件之類,這個響應會出現(xiàn)延遲。最后,固然也極可能出現(xiàn)ANR。
所以,我們還需要做的工作就是,將其它負責后續(xù)播放的MediaPlayer對象的初始化與賦值工作放在新的線程里去履行。
而最后我們需要做的,則是在OnCompletionListener里進行監(jiān)聽,當1段視頻播放終了后,
馬上履行mp.setDisplay(null),然后調(diào)用負責下1個視頻分段播放的MediaPlayer履行setDisplay(surfaceHolder)。
說了這么多,還是通過代碼說話吧:
@SuppressLint("NewApi")
public class MainActivity extends Activity implements SurfaceHolder.Callback {
//用于播放視頻的mediaPlayer對象
private MediaPlayer firstPlayer, //負責播放進入視頻播放界面后的第1段視頻
nextMediaPlayer, //負責1段視頻播放結束后,播放下1段視頻
cachePlayer, //負責setNextMediaPlayer的player緩存對象
currentPlayer; //負責當前播放視頻段落的player對象
//負責配合mediaPlayer顯示視頻圖象播放的surfaceView
private SurfaceView surface;
private SurfaceHolder surfaceHolder;
//底部聊天欄
private LinearLayout bottom_bar_layout;
private FrameLayout video_layout;
//================================================================
//寄存所有視頻真?zhèn)€url
private ArrayList<String> VideoListQueue = new ArrayList<String>();
//所有player對象的緩存
private HashMap<String, MediaPlayer> playersCache = new HashMap<String, MediaPlayer>();
//當前播放到的視頻段落數(shù)
private int currentVideoIndex;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//橫屏顯示
this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
//初始化界面控件
initView();
}
/*
* 負責界面燒毀時,release各個mediaplayer
* @see android.app.Activity#onDestroy()
*/
@Override
protected void onDestroy() {
super.onDestroy();
if (firstPlayer != null) {
if (firstPlayer.isPlaying()) {
firstPlayer.stop();
}
firstPlayer.release();
}
if (nextMediaPlayer != null) {
if (nextMediaPlayer.isPlaying()) {
nextMediaPlayer.stop();
}
nextMediaPlayer.release();
}
if (currentPlayer != null) {
if (currentPlayer.isPlaying()) {
currentPlayer.stop();
}
currentPlayer.release();
}
currentPlayer = null;
}
/*
* 界面控件的初始化
*/
private void initView() {
surface = (SurfaceView) findViewById(R.id.surface);
surfaceHolder = surface.getHolder();// SurfaceHolder是SurfaceView的控制接口
surfaceHolder.addCallback(this); // 由于這個類實現(xiàn)了SurfaceHolder.Callback接口,所以回調(diào)參數(shù)直接this
bottom_bar_layout = (LinearLayout) findViewById(R.id.live_buttom_bar);
//點擊屏幕任何地點,控制底部聊天欄的隱藏或顯示
video_layout = (FrameLayout) findViewById(R.id.videoLayout);
video_layout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View arg0) {
if (bottom_bar_layout.getVisibility() == View.VISIBLE) {
bottom_bar_layout.setVisibility(View.GONE);
} else {
bottom_bar_layout.setVisibility(View.VISIBLE);
}
}
});
}
@Override
public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
// TODO 自動生成的方法存根
}
@Override
public void surfaceCreated(SurfaceHolder arg0) {
//surfaceView創(chuàng)建終了后,首先獲得該直播間所有視頻分段的url
getVideoUrls();
//然后初始化播放手段視頻的player對象
initFirstPlayer();
}
@Override
public void surfaceDestroyed(SurfaceHolder arg0) {
// TODO 自動生成的方法存根
}
/*
* 初始化播放首段視頻的player
*/
private void initFirstPlayer() {
firstPlayer = new MediaPlayer();
firstPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
firstPlayer.setDisplay(surfaceHolder);
firstPlayer
.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
onVideoPlayCompleted(mp);
}
});
//設置cachePlayer為該player對象
cachePlayer = firstPlayer;
initNexttPlayer();
//player對象初始化完成后,開啟播放
startPlayFirstVideo();
}
private void startPlayFirstVideo() {
try {
firstPlayer.setDataSource(VideoListQueue.get(currentVideoIndex));
firstPlayer.prepare();
firstPlayer.start();
} catch (IOException e) {
// TODO 自動生成的 catch 塊
e.printStackTrace();
}
}
/*
* 新開線程負責初始化負責播放剩余視頻分段的player對象,避免UI線程做過量耗時操作
*/
private void initNexttPlayer() {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i < VideoListQueue.size(); i++) {
nextMediaPlayer = new MediaPlayer();
nextMediaPlayer
.setAudioStreamType(AudioManager.STREAM_MUSIC);
nextMediaPlayer
.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
onVideoPlayCompleted(mp);
}
});
try {
nextMediaPlayer.setDataSource(VideoListQueue.get(i));
nextMediaPlayer.prepare();
} catch (IOException e) {
// TODO 自動生成的 catch 塊
e.printStackTrace();
}
//set next mediaplayer
cachePlayer.setNextMediaPlayer(nextMediaPlayer);
//set new cachePlayer
cachePlayer = nextMediaPlayer;
//put nextMediaPlayer in cache
playersCache.put(String.valueOf(i), nextMediaPlayer);
}
}
}).start();
}
/*
* 負責處理1段視頻播放過后,切換player播放下1段視頻
*/
private void onVideoPlayCompleted(MediaPlayer mp) {
mp.setDisplay(null);
//get next player
currentPlayer = playersCache.get(String.valueOf(++currentVideoIndex));
if (currentPlayer != null) {
currentPlayer.setDisplay(surfaceHolder);
} else {
Toast.makeText(MainActivity.this, "視頻播放終了..", Toast.LENGTH_SHORT)
.show();
}
}
private void getVideoUrls() {
for (int i = 0; i < 5; i++) {
String url = getURI(i);
VideoListQueue.add(url);
}
}
private String getURI(int index) {
return "要播放的第"+index+"段視頻的URI";
}
}
而最后額外說明的就是,在上面的代碼中,我選擇新開線程直接根據(jù)總的視頻段數(shù),循環(huán)完成所有視頻段的MediaPlayer對象的初始化與賦值工作。
其實本來另外1種實現(xiàn)方式似乎也很不錯,即是在前1個MediaPlayer對象的OnInfoListener中進行下1個視頻段MediaPlayer的初始化工作。
也就是說,當前1段視頻開始或結束緩沖時,才開啟它以后的1段視頻段的初始化工作。但屢次測試后,發(fā)現(xiàn):
這類實現(xiàn)方式,如果你此次的播放中,視頻分段的數(shù)量較多時,總會出現(xiàn)1些稀里糊涂的異常,也沒能太弄清楚是甚么緣由釀成的。
所以總的來講,還是可以根據(jù)實際情況來選擇更適合的方式。
生活不易,碼農(nóng)辛苦
如果您覺得本網(wǎng)站對您的學習有所幫助,可以手機掃描二維碼進行捐贈