在线音频常用的在线传输方式之一是通过HTTP流。有多种流方法属于HTTP流方法的分支,包括服务器推送,这在历史上一直用于在浏览器中不断的刷新网络摄像头图像进行显示;以及一系列由Apple、Adobe和Microsoft等公司提出的新方法,用于他们各自的媒体播放应用程序。
通过HTTP实现在线音频流的主要方法是由一家称为Nullsoft的公司于1999年开发的,该公司后来被AOL(美国在线)收购。Nullsoft是WinAMP(一种流行的MP3播放器)的创作者,同时他们设立了一个使用HTTP的在线音频流服务器,称为SHOUTcast。SHOUTcast使用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,Othere Live Stream Name
http://www.nostreamhere.org/
但是,Android上的MediaPlayer不能自动分析M3U文件。因此,为了在Android上创建一个基于HTTP的流式音频播放器,就必须自己处理分析工作,同时将MediaPlayer用于实际的媒体播放。
下面是活动的一个示例,其分析并播放来自联机广播电台的M3U文件或在URL字段中输入的任何M3U文件。
1 package com.nthm.androidtestActivity; 2 3 import java.io.BufferedReader; 4 import java.io.IOException; 5 import java.io.InputStream; 6 import java.io.InputStreamReader; 7 import java.util.Vector; 8 import org.apache.http.HttpResponse; 9 import org.apache.http.HttpStatus; 10 import org.apache.http.client.ClientProtocolException; 11 import org.apache.http.client.HttpClient; 12 import org.apache.http.client.methods.HttpGet; 13 import org.apache.http.impl.client.DefaultHttpClient; 14 import android.app.Activity; 15 import android.media.MediaPlayer; 16 import android.media.MediaPlayer.OnCompletionListener; 17 import android.media.MediaPlayer.OnPreparedListener; 18 import android.os.Bundle; 19 import android.view.View; 20 import android.view.View.OnClickListener; 21 import android.widget.Button; 22 import android.widget.EditText; 23 import com.nthm.androidtest.R;
与之前的示例一样,这个活动将扩展OnCompletionListener和OnPreparedListener,以跟踪MediaPlayer的状态。
1 public class HTTPAudioPlaylistPlayer extends Activity implements 2 OnClickListener, OnCompletionListener, OnPreparedListener {
我们将使用一个向量来保存播放列表的条目列表。每个列表都将是一个PlaylistFile对象,在这个类末尾的一个内部类中定义该对象。
1 private Vector playlistItems;
在界面上,我们会有几个Button对象以及一个EditView对象,后者将包含指向M3U文件的URL。
1 private Button parseButton; 2 private Button playButton; 3 private Button stopButton; 4 private EditText editTextUrl; 5 private String baseURL=""; 6 private MediaPlayer mediaPlayer;
下面的整数用于跟踪目前正处于playlistItems向量中的哪个条目。
1 private int currentPlaylistItemNumber=0; 2 @Override 3 protected void onCreate(Bundle savedInstanceState) { 4 super.onCreate(savedInstanceState); 5 setContentView(R.layout.httpaudiopalylistplayeractivity); 6 parseButton=(Button) findViewById(R.id.ButtonParse); 7 playButton=(Button) findViewById(R.id.PlayButton); 8 stopButton=(Button) findViewById(R.id.StopButton);
将editTextUrl对象的文本设置为来自联机广播电台的M3U文件的URL。第一个(被注释的)URL是KBOO的URL,它是一个位于俄勒冈州波特兰市的社区广播电台(www.kboo.fm/)。第二个(没有被注释的)URL是KMFA的URL,它是一个位于德克萨斯州奥斯丁市的经典电台(www.kmfa.org/)。
用户能够对这个URL进行编辑,他可以是Intent上可用的任何M3U文件的URL。
1 editTextUrl=(EditText) findViewById(R.id.EditTextURL); 2 //editTextUrl.setText("http://live.kboo.fm:8000/high.m3u"); 3 editTextUrl.setText("http://pubint.ic.llnwd.net/stream/pubint_kmfa.m3u"); 4 5 parseButton.setOnClickListener(this); 6 playButton.setOnClickListener(this); 7 stopButton.setOnClickListener(this);
起初,不会启用playButton和stopButton,因此用户将不能够按下他们。另一方面,我们将启用parseButton。在获得M3U文件并分析它之后,将启用playButton,在播放音频之后,启用stopButton。这就是引导用户使用该应用程序的流程。
1 playButton.setEnabled(false); 2 stopButton.setEnabled(false); 3 4 mediaPlayer=new MediaPlayer(); 5 mediaPlayer.setOnCompletionListener(this); 6 mediaPlayer.setOnPreparedListener(this); 7 }
每个按钮都把他们的OnClickListener设置为这个活动。因此,当单击他们中的任何一个时,都将调用下面的onClick方法。这推动该了大多数应用程序的执行流程。
当按下parseButton时,将调用parsePlaylistFile方法。当按下playButton时,将调用playPlaylistItems方法。
1 @Override 2 public void onClick(View v) { 3 if(v==parseButton){ 4 parsePlaylistFile(); 5 }else if(v==playButton){ 6 playPlaylistItems(); 7 }else if(v==stopButton){ 8 stop(); 9 } 10 }
parsePlaylistFile是第一个被触发的方法。该方法下载由editTextUrl对象中的Url指定的M3U文件,并对他进行分析。分析的操作时选出任何表示待播放文件的行,创建一个PlaylistItem对象,然后把它添加到playlistItems向量。
1 private void parsePlaylistFile(){
该向量的初始时为空。如果分析了新的M3U文件,那么将丢弃其中原有的内容。
1 playlistItems=new Vector<>();
为了从Web获取M3U文件,可以使用Apache软件基金会的HttpClient库,它已被Android所包括。
首先创建一个HttpClient对象,其代表类似Web浏览器的事物:然后创建一个HttpGet对象,其表示指向一个文件的具体请求。HttpClient将执行HttpGet并返回一个HttpResponse。
1 HttpClient httpClient=new DefaultHttpClient(); 2 HttpGet getRequest=new HttpGet(editTextUrl.getText().toString()); 3 try { 4 HttpResponse httpReponse=httpClient.execute(getRequest); 5 if(httpReponse.getStatusLine().getStatusCode()!=HttpStatus.SC_OK){ 6 //错误 7 }else{
在发出请求后,可以从HttpResponse中获取一个InputStream,其包含了所请求文件的内容。
1 InputStream inputStream=httpReponse.getEntity().getContent(); 2 BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream));
借助一个bufferedReader,可以逐行地遍历该文件。
1 String line; 2 while((line=bufferedReader.readLine())!=null){
如果行以“#”符号开始,那么现在会忽略它。正如前面所描述的那样,这些行是元数据。
1 if(line.startsWith("#")){ 2 //元数据 忽略
否则,如果他不是一个空行,那么它的长度大于0,因此将假设它是一个播放列表条目。
1 }else if(line.length()>0){
如果行以“http://”开始,那么把它作为流的完整URL,否则把它作为一个相对URL,同时把针对该M3U文件的原始请求的URL附加上去。
1 String filePath=""; 2 if(line.startsWith("http://")){ 3 filePath=line; 4 }else{ 5 filePath=getRequest.getURI().resolve(line).toString(); 6 }
然后,将它添加到播放列表条目的向量中。
1 PlaylistFile palylistFile=new PlaylistFile(filePath); 2 playlistItems.add(palylistFile); 3 } 4 } 5 inputStream.close(); 6 } 7 } catch (ClientProtocolException e) { 8 e.printStackTrace(); 9 } catch (IOException e) { 10 e.printStackTrace(); 11 }
最后,既然已经完成了文件的分析,因此将启用playButton。
1 playButton.setEnabled(true); 2 }
当用户单击playButton时,调用playPlaylistItems方法。该方法接受PlaylistItems向量中的第一个条目,并把它交给MediaPlayer对象以进行准备工作。
1 private void playPlaylistItems(){ 2 playButton.setEnabled(false); 3 currentPlaylistItemNumber=0; 4 if(playlistItems.size()>0){ 5 String path=((PlaylistFile)playlistItems.get(currentPlaylistItemNumber)).getFilePath(); 6 try {
在提取出文件或者流的路径之后,就可以在MediaPlayer上的setDataSource方法调用中使用它。
1 mediaPlayer.setDataSource(path);
然后,调用prepareAsync,它允许MediaPlayer进行缓冲,准备在后台播放音频。当完成缓冲和其他的准备工作之后,将调用活动的onPrepared方法,因为该活动被注册为OnPreparedListener。
1 mediaPlayer.prepareAsync(); 2 } catch (IllegalArgumentException e) { 3 e.printStackTrace(); 4 } catch (SecurityException e) { 5 e.printStackTrace(); 6 } catch (IllegalStateException e) { 7 e.printStackTrace(); 8 } catch (IOException e) { 9 e.printStackTrace(); 10 } 11 12 } 13 }
一旦调用了onPrepared方法,则启用stopButton,触发MediaPlayer对象以开始播放音频。
1 @Override 2 public void onPrepared(MediaPlayer mp) { 3 stopButton.setEnabled(true); 4 mediaPlayer.start(); 5 }
当音频播放完成时,触发活动中的onCompletion方法,因为该活动扩展了OnCompletionListener,而且被注册为MediaPlayer的OnCompletionListener。
onCompletion方法会准备playlistItems向量中的下一个条目。
1 @Override 2 public void onCompletion(MediaPlayer mp) { 3 mediaPlayer.stop(); 4 mediaPlayer.reset(); 5 if(playlistItems.size()>currentPlaylistItemNumber+1){ 6 currentPlaylistItemNumber++; 7 String path=((PlaylistFile)playlistItems.get(currentPlaylistItemNumber)).getFilePath(); 8 try { 9 mediaPlayer.setDataSource(path); 10 mediaPlayer.prepareAsync(); 11 } catch (IllegalArgumentException e) { 12 e.printStackTrace(); 13 } catch (SecurityException e) { 14 e.printStackTrace(); 15 } catch (IllegalStateException e) { 16 e.printStackTrace(); 17 } catch (IOException e) { 18 e.printStackTrace(); 19 } 20 } 21 }
当用户按下stopButton时调用stop方法。该方法导致MediaPlayer暂停而不是停止。MediaPlayer有一个stop方法,但是他将使MediaPlayer处于为准备好的状态。pause方法仅仅是暂停播放。
1 private void stop(){ 2 mediaPlayer.pause(); 3 playButton.setEnabled(true); 4 stopButton.setEnabled(false); 5 }
最后,有一个称为PlaylistFile的内部类。为M3U文件中表示的每个文件创建一个PlaylistFile对象。
1 class PlaylistFile{ 2 String filePath; 3 public PlaylistFile(String _filePath){ 4 this.filePath=_filePath; 5 } 6 public String getFilePath() { 7 return filePath; 8 } 9 public void setFilePath(String filePath) { 10 this.filePath = filePath; 11 } 12 13 } 14 }
下面是用于上述活动的布局XML文件。
1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 android:layout_width="match_parent" 3 android:layout_height="match_parent" 4 android:orientation="vertical" 5 > 6 <TextView 7 android:id="@+id/EnterURLTextView" 8 android:text="Enter URL" 9 android:layout_width="fill_parent" 10 android:layout_height="wrap_content"></TextView> 11 <EditView 12 android:id="@+id/EditTextURL" 13 android:text="http://www.mobvcasting.com/android/audio/test.m3u" 14 android:layout_width="fill_parent" 15 android:layout_height="wrap_content"></EditView> 16 <Button 17 android:layout_width="wrap_content" 18 android:layout_height="wrap_content" 19 android:id="@+id/ButtonParse" 20 android:text="Parse"/> 21 <TextView 22 android:id="@+id/PlaylistTextView" 23 android:layout_width="fill_parent" 24 android:layout_height="wrap_content"></TextView> 25 <Button 26 android:layout_width="wrap_content" 27 android:layout_height="wrap_content" 28 android:id="@+id/PlayButton" 29 android:text="Play"/> 30 <Button 31 android:layout_width="wrap_content" 32 android:layout_height="wrap_content" 33 android:id="@+id/StopButton" 34 android:text="Stop"/> 35 </LinearLayout>
此示例需要把一下权限添加到Androidmanifest.xml文件中。
1 <uses-permission android:name="android.permission.INTERNET"/>
正如你在上述示例中所看到的那样,使用通过HTTP的在线音频流与使用通过HTTP传输的文件一样简单。