基於網路音訊的Android播放程式簡單示例
隨著釋出MP3檔案、播客以及流式音訊變得越來越受歡迎,構建可以利用這些服務的音訊播放程式的需求也越來越強烈。幸運的是,Android擁有豐富的功能用於處理網路上存在的各種型別的音訊。
1.基於HTTP音訊播放
這是最簡單的的情況,僅僅播放線上的、可通過HTTP對其進行訪問的音訊檔案。比如http://www.mobvcasting.com/android/audio/goodmorningandroid.mp3
但是這裡和通常示例化MediaPlayer的方式不同,首先使用的是MediaPlayer的無參建構函式來例項化物件,接著,呼叫其setDataSource方法,傳入想要播放的音訊的HTTP位置,隨後我們呼叫prepare方法和start方法。
mediaPlayer = new MediaPlayer();
try {
mediaPlayer
.setDataSource("http://www.mobvcasting.com/android/audio/goodmorningandroid.mp3");
mediaPlayer.prepare();
mediaPlayer.start();
} catch (IOException e) {
Log.v("AUDIOHTTPPLAYER", e.getMessage());
}
但是,在應用程式載入到播放音訊之間有一個明顯的滯後時間。延遲的長度取決於用於構建電話Internet連線的資料網路的速度。如果詳細分析的話,可以找到是在呼叫prepare方法和start方法之間發生了這樣的延遲。在執行prepare期間,MediaPlayer將填充一個緩衝區,因為即使網路速度緩慢也能平穩的播放音訊。當這麼操作時,prepare方法實際上發生了阻塞。這意味著應用程式可能要等到prepare方法完成之後才會響應。幸運的是,有一種方法可以解決這個問題,即prepareAsync方法。該方法會立即返回,並在後臺執行緩衝和其他工作,從而允許應用程式繼續執行。
完整示例程式碼如下:
如上所示,MediaPlayer有良好的功能集,用來處理HTTP線上獲取的音訊檔案。public class AudioHTTPPlayer extends Activity implements OnClickListener, OnErrorListener, OnCompletionListener, OnBufferingUpdateListener, OnPreparedListener { /** Called when the activity is first created. */ MediaPlayer mediaPlayer; Button stopButton, startButton; TextView statusTextView, bufferValueTextView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); stopButton = (Button) findViewById(R.id.EndButton); startButton = (Button) findViewById(R.id.StartButton); startButton.setOnClickListener(this); stopButton.setOnClickListener(this); startButton.setEnabled(false); stopButton.setEnabled(false); bufferValueTextView = (TextView) findViewById(R.id.BufferValueTextView); statusTextView = (TextView) findViewById(R.id.StatusDisplayTextView); statusTextView.setText("onCreate"); mediaPlayer = new MediaPlayer(); mediaPlayer.setOnCompletionListener(this); mediaPlayer.setOnErrorListener(this); mediaPlayer.setOnBufferingUpdateListener(this); mediaPlayer.setOnPreparedListener(this); statusTextView.setText("MediaPlayer created"); try { mediaPlayer .setDataSource("http://www.mobvcasting.com/android/audio/goodmorningandroid.mp3"); // mediaPlayer.prepare(); // mediaPlayer.start(); statusTextView.setText("setDataSource done"); statusTextView.setText("calling prepareAsync"); mediaPlayer.prepareAsync();// 開始在後臺緩衝音訊檔案並返回 } catch (IOException e) { Log.v("AUDIOHTTPPLAYER", e.getMessage()); } } @Override public void onPrepared(MediaPlayer mp) { // TODO Auto-generated method stub // 當完成prepareAsync方法時,將呼叫活動的onPrepared方法 statusTextView.setText("onPrepared called"); startButton.setEnabled(true); } @Override public void onBufferingUpdate(MediaPlayer mp, int percent) { // TODO Auto-generated method stub // 當MediaPlayer正在緩衝時,將呼叫活動的onBufferingUpdate方法 bufferValueTextView.setText(""+percent+"%"); } @Override public void onCompletion(MediaPlayer mp) { // TODO Auto-generated method stub statusTextView.setText("onCompletion called"); stopButton.setEnabled(false); startButton.setEnabled(true); } @Override public boolean onError(MediaPlayer mp, int what, int extra) { // TODO Auto-generated method stub switch (what) { case MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK: statusTextView .setText("MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK" + extra); break; case MediaPlayer.MEDIA_ERROR_SERVER_DIED: statusTextView.setText("MEDIA_ERROR_SERVER_DIED" + extra); break; case MediaPlayer.MEDIA_ERROR_UNKNOWN: statusTextView.setText("MEDIA_ERROR_UNKNOWN" + extra); break; } return false; } @Override public void onClick(View v) { // TODO Auto-generated method stub if (v == stopButton) { mediaPlayer.pause(); statusTextView.setText("pause called"); startButton.setEnabled(true); } else if (v == startButton) { mediaPlayer.start(); statusTextView.setText("start called"); startButton.setEnabled(false); stopButton.setEnabled(true); } } }
2.基於HTTP的流式音訊
線上音訊常用的線上傳輸方法之一是通過HTTP流。有多種流方法屬於HTTP流方法的分支,包括伺服器推送,這在歷史上一直用於在瀏覽器中重新整理網路攝像頭影象顯示;以及一系列其他新方法。而聯機廣播事實上的標準則是ICY協議,其擴充套件了HTTP協議,目前大量的伺服器和播放軟體產品都支援這個協議。
幸運的是,android上的MediaPlayer支援播放ICY流,而無須開發人員費力地實現它。
然後,Internet廣播電臺並不直接公佈它們的音訊流的URL。這麼做是因為瀏覽器通常不支援ICY流,而是需要一個輔助應用程式或外掛來播放流。為了知道要開啟的是一個輔助應用程式,Internet廣播電臺會 傳遞一個特定的MIME型別的中間檔案,其中包含一個指向實際線上流的指標。在使用ICY流的情況下,這通常是一個PLS檔案或一個M3U檔案
PLS檔案:是一種多媒體播放列表檔案,其MIME型別是“audio/x-scpls”
M3U檔案:一個儲存多媒體播放列表的檔案,但是採用一種更基本的格式。它的MIME型別為“audio/x-mpegurl”。
例如M3U檔案的內容如下,其指向了一個虛假的線上流
#EXTM3U
#EXTINF:0,Live Stream Name
http://www.nostreamhere.org:8000/
第一行的#EXTM3U是必須的,其指定下面是一個擴充套件的M3U檔案,其中可以包含額外的資訊。可以在播放列表條目的上一行指定額外資訊,其以#EXTINF:開始,隨後是以秒為單位的持續時間和逗號,然後是媒體的名稱。
M3U檔案可以同時包含多個條目,這些條目依次指定一個檔案或流
#EXTM3U
#EXTINF:0,Live Stream Name
http://www.nostreamhere.org:8000/
#EXTINF:0,Other Live Stream Name
http://www.nostreamhere.org/
遺憾的是,android上的MediaPlayer不能自動分析M3U檔案。因此必須我們自己分析。下面就是一個示例,分析並播放來自聯機廣播電臺的M3U檔案或在URL欄位中輸入的任何M3U檔案。public class HTTPAudioPlaylistPlayer extends Activity implements
OnClickListener, OnCompletionListener, OnPreparedListener
{
Vector playlistItems;
Button parseBtn, playBtn, stopBtn;
EditText editTextUrl;
String baseURL = "";
MediaPlayer mediaPlayer;
int currentPlaylistItemNumber = 0;
@Override
protected void onCreate(Bundle savedInstanceState)
{
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.main2);
parseBtn = (Button) findViewById(R.id.ParseButton);
playBtn = (Button) findViewById(R.id.PlayButton);
stopBtn = (Button) findViewById(R.id.StopButton);
editTextUrl=(EditText) findViewById(R.id.EditTextURL);
playBtn.setOnClickListener(this);
parseBtn.setOnClickListener(this);
stopBtn.setOnClickListener(this);
playBtn.setEnabled(false);
stopBtn.setEnabled(false);
mediaPlayer = new MediaPlayer();
mediaPlayer.setOnCompletionListener(this);
mediaPlayer.setOnPreparedListener(this);
}
@Override
public void onPrepared(MediaPlayer mp)
{
// TODO Auto-generated method stub
stopBtn.setEnabled(true);
Log.v("HTTPAUDIOPLAYLIST", "Playing");
mediaPlayer.start();
}
@Override
public void onCompletion(MediaPlayer mp)
{
// TODO Auto-generated method stub
Log.v("ONCOMPLETION", "called");
mediaPlayer.stop();
mediaPlayer.reset();
if (playlistItems.size() > currentPlaylistItemNumber + 1)
{
currentPlaylistItemNumber++;
String path = ((PlaylistFile) playlistItems
.get(currentPlaylistItemNumber)).getFilePath();
try
{
mediaPlayer.setDataSource(path);
mediaPlayer.prepareAsync();
} catch (IllegalArgumentException e)
{
e.printStackTrace();
} catch (IllegalStateException e)
{
e.printStackTrace();
} catch (IOException e)
{
e.printStackTrace();
}
}
}
@Override
public void onClick(View v)
{
// TODO Auto-generated method stub
if (v == parseBtn)
{
// 下載由editTextUrl物件中的URL指定的M3U檔案,並對它進行分析。
// 分析的操作是選出任何表示待播放檔案的行,建立一個PlaylistItem物件,
// 然後把它新增到playlistItems容器裡
parsePlaylistFile();
} else if (v == playBtn)
{
playPlaylistItems();
} else if (v == stopBtn)
{
stop();
}
}
private void parsePlaylistFile()
{
// TODO Auto-generated method stub
playlistItems = new Vector();
// 為了從Web獲取M3U檔案,可以使用Apache軟體基金會的HttpClient庫,
// 它已被android所包括。
// 首先建立一個HttpClient物件,其代表類似Web瀏覽器的事物;
HttpClient httpClient = new DefaultHttpClient();
// 然後建立一個HttpGet物件,其表示指向一個檔案的具體請求。
HttpGet getRequest = new HttpGet(editTextUrl.getText().toString());
Log.v("URI", getRequest.getURI().toString());
// HttpClient將執行HttpGet,並返回一個HttpResponse
try
{
HttpResponse httpResponse = httpClient.execute(getRequest);
if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK)
{
Log.v("HTTP ERROR", httpResponse.getStatusLine()
.getReasonPhrase());
} else
{
// 在發出請求之後,可以從HttpRequest中獲取一個InputStream,
// 其包含了所請求檔案的內容
InputStream inputStream = httpResponse.getEntity().getContent();
// 藉助一個BufferedReader可以逐行得遍歷該檔案
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(inputStream));
String line;
while ((line = bufferedReader.readLine()) != null)
{
Log.v("PLAYLISTLINE", "ORIG:" + line);
if (line.startsWith("#"))
{
// 元資料,可以做更多的處理,但現在忽略它
} else if (line.length() > 0)
{
// 如果它的長度大於0,那麼就假設它是一個播放列表條目
String filePath = "";
if (line.startsWith("http://"))
{
// 如果行以“http://”開頭那麼就把它作為流的完整URL
filePath = line;
} else
{
// 否則把它作為一個相對的URL,
// 同時把針對該M3U檔案的原始請求的URL附加上去
filePath = getRequest.getURI().resolve(line)
.toString();
}
// 將其新增到播放列表條目的容器中去
PlaylistFile playlistFile = new PlaylistFile(filePath);
playlistItems.add(playlistFile);
}
}
}
} catch (ClientProtocolException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
playBtn.setEnabled(true);
}
private void playPlaylistItems()
{
playBtn.setEnabled(false);
currentPlaylistItemNumber = 0;
if (playlistItems.size() > 0)
{
String path = ((PlaylistFile) playlistItems
.get(currentPlaylistItemNumber)).getFilePath();
// 在提取出流的或者檔案的路徑之後,就可以在MediaPlayer上的setDataSource方法使用它了
try
{
mediaPlayer.setDataSource(path);
mediaPlayer.prepareAsync();
} catch (IllegalArgumentException e)
{
e.printStackTrace();
} catch (IllegalStateException e)
{
e.printStackTrace();
} catch (IOException e)
{
e.printStackTrace();
}
}
}
private void stop()
{
mediaPlayer.pause();
playBtn.setEnabled(true);
stopBtn.setEnabled(false);
}
class PlaylistFile
{
String filePath;
public PlaylistFile(String _filePath)
{
filePath = _filePath;
}
public void setFilePath(String _filePath)
{
filePath = _filePath;
}
public String getFilePath()
{
return filePath;
}
}
}