1. 程式人生 > >Android--SurfaceView播放視訊

Android--SurfaceView播放視訊

前言

  本篇部落格講解一下如何在Android下,使用SurfaceView播放一個視訊流媒體。之前有講到如何使用MediaPlayer播放音訊流媒體,其實MediaPlayer還可以播放視訊,只需需要SurfaceView的配合,SurfaceView主要用於顯示MediaPlayer播放的視訊流媒體的畫面渲染。對MediaPlayer不瞭解的朋友,可以先看看那篇部落格:Android--MediaPlayer播放MP3,本篇部落格中關於MediaPlayer的內容將不再詳解,主要以SurfaceView為主,最後將會以一個簡單的Demo演示SurfaceView如何播放視訊流媒體。

  本篇部落格的主要內容:

SurfaceView  

  先來介紹一下大部分軟體如何解析一段視訊流。首先它需要先確定視訊的格式,這個和解碼相關,不同的格式視訊編碼不同,不是這裡的重點。知道了視訊的編碼格式後,再通過編碼格式進行解碼,最後得到一幀一幀的影象,並把這些影象快速的顯示在介面上,即為播放一段視訊。SurfaceView在Android中就是完成這個功能的。

  既然SurfaceView是配合MediaPlayer使用的,MediaPlayer也提供了相應的方法設定SurfaceView顯示圖片,只需要為MediaPlayer指定SurfaceView顯示影象即可。它的完整簽名如下:

    void setDisplay(SurfaceHolder sh)

  它需要傳遞一個SurfaceHolder物件,SurfaceHolder可以理解為SurfaceView裝載需要顯示的一幀幀影象的容器,它可以通過SurfaceHolder.getHolder()方法獲得。

  使用MediaPlayer配合SurfaceView播放視訊的步驟與播放使用MediaPlayer播放MP3大體一致,只需要額外設定顯示的SurfaceView即可。

SurfaceView雙緩衝

  上面有提到,SurfaceView和大部分視訊應用一樣,把視訊流解析成一幀幀的影象進行顯示,但是如果把這個解析的過程放到一個執行緒中完成,可能在上一幀影象已經顯示過後,下一幀影象還沒有來得及解析,這樣會導致畫面的不流暢或者聲音和視訊不同步的問題。所以SurfaceView和大部分視訊應用一樣,通過雙緩衝的機制來顯示幀影象。那麼什麼是雙緩衝呢?雙緩衝可以理解為有兩個執行緒輪番去解析視訊流的幀影象,當一個執行緒解析完幀影象後,把影象渲染到介面中,同時另一執行緒開始解析下一幀影象,使得兩個執行緒輪番配合去解析視訊流,以達到流暢播放的效果。

  下圖為演示了雙緩衝的過程,執行緒A和執行緒B配合解析渲染視訊流的幀影象:

SurfaceHolder

  SurfaceView內部實現了雙緩衝的機制,但是實現這個功能是非常消耗系統記憶體的。因為移動裝置的侷限性,Android在設計的時候規定,SurfaceView如果為使用者可見的時候,建立SurfaceView的SurfaceHolder用於顯示視訊流解析的幀圖片,如果發現SurfaceView變為使用者不可見的時候,則立即銷燬SurfaceView的SurfaceHolder,以達到節約系統資源的目的。

  如果開發人員不對SurfaceHolder進行維護,會出現最小化程式後,再開啟應用的時候,視訊的聲音在繼續播放,但是不顯示畫面了的情況,這就是因為當SurfaceView不被使用者可見的時候,之前的SurfaceHolder已經被銷燬了,再次進入的時候,介面上的SurfaceHolder已經是新的SurfaceHolder了。所以SurfaceHolder需要我們開發人員去編碼維護,維護SurfaceHolder需要用到它的一個回撥,SurfaceHolder.Callback(),它需要實現三個如下三個方法:

  • void surfaceDestroyed(SurfaceHolder holder):當SurfaceHolder被銷燬的時候回撥。
  • void surfaceCreated(SurfaceHolder holder):當SurfaceHolder被建立的時候回撥。
  • void surfaceChange(SurfaceHolder holder):當SurfaceHolder的尺寸發生變化的時候被回撥。

  以下是這三個方法的呼叫的過程,在應用中分別為SurfaceHolder實現了這三個方法,先進入應用,SurfaceHolder被建立,建立好之後會改變SurfaceHolder的大小,然後按Home鍵回退到桌面銷燬SurfaceHolder,最後再進入應用,重新SurfaceHolder並改變其大小。

SurfaceView的相容性

  對於Android4.0以下的裝置,在使用SurfaceView播放視訊的時候,需要為其設定一個額外的屬性。之前提到過,SurfaceView維護了一個雙緩衝的機制,它會自己維護緩衝區,無需我們手動維護,但是對於低版本(4.0以下)的裝置,需要為其制定它緩衝區的維護型別,讓其不自己維護緩衝區,而是等待介面渲染引擎將內容渲染到介面上。這裡僅僅是使用SurfaceView播放一個視訊,如果使用SurfaceView開發遊戲應用,就需要我們自己維護這個緩衝區了。

1         // 為SurfaceHolder添加回調
2         sv.getHolder().addCallback(callback);
3         
4         // 4.0版本之下需要設定的屬性
5         // 設定Surface不維護自己的緩衝區,而是等待螢幕的渲染引擎將內容推送到介面
6         sv.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

SurfaceView的Demo示例

  上面講了那麼多關於SurfaceView的內容,下面通過一個Demo簡單演示一下SurfaceView如何播放視訊,加了一個滾動條,用於顯示進度,還可以拖動滾動條選擇播放位置,Demo的註釋比較完整,這裡不再累述,視訊是在網上隨便找的,朋友們執行的時候保證/sdcard/ykzzldx.mp4,這個目錄下有這個檔案。

  佈局檔案:activity_main.xml

 activity_main.xml

  實現程式碼: 

複製程式碼
  1 package cn.bgxt.surfaceviewdemo;
  2 
  3 import java.io.File;
  4 
  5 import android.media.AudioManager;
  6 import android.media.MediaPlayer;
  7 import android.media.MediaPlayer.OnCompletionListener;
  8 import android.media.MediaPlayer.OnErrorListener;
  9 import android.media.MediaPlayer.OnPreparedListener;
 10 import android.os.Bundle;
 11 import android.app.Activity;
 12 import android.util.Log;
 13 import android.view.SurfaceHolder;
 14 import android.view.SurfaceHolder.Callback;
 15 import android.view.SurfaceView;
 16 import android.view.View;
 17 import android.widget.Button;
 18 import android.widget.EditText;
 19 import android.widget.SeekBar;
 20 import android.widget.SeekBar.OnSeekBarChangeListener;
 21 import android.widget.Toast;
 22 
 23 public class MainActivity extends Activity {
 24     private final String TAG = "main";
 25     private EditText et_path;
 26     private SurfaceView sv;
 27     private Button btn_play, btn_pause, btn_replay, btn_stop;
 28     private MediaPlayer mediaPlayer;
 29     private SeekBar seekBar;
 30     private int currentPosition = 0;
 31     private boolean isPlaying;
 32 
 33     @Override
 34     protected void onCreate(Bundle savedInstanceState) {
 35         super.onCreate(savedInstanceState);
 36         setContentView(R.layout.activity_main);
 37 
 38         seekBar = (SeekBar) findViewById(R.id.seekBar);
 39         sv = (SurfaceView) findViewById(R.id.sv);
 40         et_path = (EditText) findViewById(R.id.et_path);
 41 
 42         btn_play = (Button) findViewById(R.id.btn_play);
 43         btn_pause = (Button) findViewById(R.id.btn_pause);
 44         btn_replay = (Button) findViewById(R.id.btn_replay);
 45         btn_stop = (Button) findViewById(R.id.btn_stop);
 46 
 47         btn_play.setOnClickListener(click);
 48         btn_pause.setOnClickListener(click);
 49         btn_replay.setOnClickListener(click);
 50         btn_stop.setOnClickListener(click);
 51 
 52         // 為SurfaceHolder添加回調
 53         sv.getHolder().addCallback(callback);
 54         
 55         // 4.0版本之下需要設定的屬性
 56         // 設定Surface不維護自己的緩衝區,而是等待螢幕的渲染引擎將內容推送到介面
 57         // sv.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
 58         
 59         // 為進度條新增進度更改事件
 60         seekBar.setOnSeekBarChangeListener(change);
 61     }
 62 
 63     private Callback callback = new Callback() {
 64         // SurfaceHolder被修改的時候回撥
 65         @Override
 66         public void surfaceDestroyed(SurfaceHolder holder) {
 67             Log.i(TAG, "SurfaceHolder 被銷燬");
 68             // 銷燬SurfaceHolder的時候記錄當前的播放位置並停止播放
 69             if (mediaPlayer != null && mediaPlayer.isPlaying()) {
 70                 currentPosition = mediaPlayer.getCurrentPosition();
 71                 mediaPlayer.stop();
 72             }
 73         }
 74 
 75         @Override
 76         public void surfaceCreated(SurfaceHolder holder) {
 77             Log.i(TAG, "SurfaceHolder 被建立");
 78             if (currentPosition > 0) {
 79                 // 建立SurfaceHolder的時候,如果存在上次播放的位置,則按照上次播放位置進行播放
 80                 play(currentPosition);
 81                 currentPosition = 0;
 82             }
 83         }
 84 
 85         @Override
 86         public void surfaceChanged(SurfaceHolder holder, int format, int width,
 87                 int height) {
 88             Log.i(TAG, "SurfaceHolder 大小被改變");
 89         }
 90 
 91     };
 92 
 93     private OnSeekBarChangeListener change = new OnSeekBarChangeListener() {
 94 
 95         @Override
 96         public void onStopTrackingTouch(SeekBar seekBar) {
 97             // 當進度條停止修改的時候觸發
 98             // 取得當前進度條的刻度
 99             int progress = seekBar.getProgress();
100             if (mediaPlayer != null && mediaPlayer.isPlaying()) {
101                 // 設定當前播放的位置
102                 mediaPlayer.seekTo(progress);
103             }
104         }
105 
106         @Override
107         public void onStartTrackingTouch(SeekBar seekBar) {
108 
109         }
110 
111         @Override
112         public void onProgressChanged(SeekBar seekBar, int progress,
113                 boolean fromUser) {
114 
115         }
116     };
117 
118     private View.OnClickListener click = new View.OnClickListener() {
119 
120         @Override
121         public void onClick(View v) {
122 
123             switch (v.getId()) {
124             case R.id.btn_play:
125                 play(0);
126                 break;
127             case R.id.btn_pause:
128                 pause();
129                 break;
130             case R.id.btn_replay:
131                 replay();
132                 break;
133             case R.id.btn_stop:
134                 stop();
135                 break;
136             default:
137                 break;
138             }
139         }
140     };
141 
142 
143     /*
144      * 停止播放
145      */
146     protected void stop() {
147         if (mediaPlayer != null && mediaPlayer.isPlaying()) {
148             mediaPlayer.stop();
149             mediaPlayer.release();
150             mediaPlayer = null;
151             btn_play.setEnabled(true);
152             isPlaying = false;
153         }
154     }
155 
156     /**
157      * 開始播放
158      * 
159      * @param msec 播放初始位置    
160      */
161     protected void play(final int msec) {
162         // 獲取視訊檔案地址
163         String path = et_path.getText().toString().trim();
164         File file = new File(path);
165         if (!file.exists()) {
166             Toast.makeText(this, "視訊檔案路徑錯誤", 0).show();
167             return;
168         }
169         try {
170             mediaPlayer = new MediaPlayer();
171             mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
172             // 設定播放的視訊源
173             mediaPlayer.setDataSource(file.getAbsolutePath());
174             // 設定顯示視訊的SurfaceHolder
175             mediaPlayer.setDisplay(sv.getHolder());
176             Log.i(TAG, "開始裝載");
177             mediaPlayer.prepareAsync();
178             mediaPlayer.setOnPreparedListener(new OnPreparedListener() {
179 
180                 @Override
181                 public void onPrepared(MediaPlayer mp) {
182                     Log.i(TAG, "裝載完成");
183                     mediaPlayer.start();
184                     // 按照初始位置播放
185                     mediaPlayer.seekTo(msec);
186                     // 設定進度條的最大進度為視訊流的最大播放時長
187                     seekBar.setMax(mediaPlayer.getDuration());
188                     // 開始執行緒,更新進度條的刻度
189                     new Thread() {
190 
191                         @Override
192                         public void run() {
193                             try {
194                                 isPlaying = true;
195                                 while (isPlaying) {
196                                     int current = mediaPlayer
197                                             .getCurrentPosition();
198                                     seekBar.setProgress(current);
199                                     
200                                     sleep(500);
201                                 }
202                             } catch (Exception e) {
203                                 e.printStackTrace();
204                             }
205                         }
206                     }.start();
207 
208                     btn_play.setEnabled(false);
209                 }
210             });
211             mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
212 
213                 @Override
214                 public void onCompletion(MediaPlayer mp) {
215                     // 在播放完畢被回撥
216                     btn_play.setEnabled(true);
217                 }
218             });
219 
220             mediaPlayer.setOnErrorListener(new OnErrorListener() {
221 
222                 @Override
223                 public boolean onError(MediaPlayer mp, int what, int extra) {
224                     // 發生錯誤重新播放
225                     play(0);
226                     isPlaying = false;
227                     return false;
228                 }
229             });
230         } catch (Exception e) {
231             e.printStackTrace();
232         }
233 
234     }
235 
236     /**
237      * 重新開始播放
238      */
239     protected void replay() {
240         if (mediaPlayer != null && mediaPlayer.isPlaying()) {
241             mediaPlayer.seekTo(0);
242             Toast.makeText(this, "重新播放", 0).show();
243             btn_pause.setText("暫停");
244             return;
245         }
246         isPlaying = false;
247         play(0);
248         
249 
250     }
251 
252     /**
253      * 暫停或繼續
254      */
255     protected void pause() {
256         if (btn_pause.getText().toString().trim().equals("繼續")) {
257             btn_pause.setText("暫停");
258             mediaPlayer.start();
259             Toast.makeText(this, "繼續播放", 0).show();
260             return;
261         }
262         if (mediaPlayer != null && mediaPlayer.isPlaying()) {
263             mediaPlayer.pause();
264             btn_pause.setText("繼續");
265             Toast.makeText(this, "暫停播放", 0).show();
266         }
267 
268     }
269 
270 }
複製程式碼

  效果展示:

  原始碼下載