Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發實例 >> Android應用開發入門(六十二)使用SurfaceView播放視頻示例

Android應用開發入門(六十二)使用SurfaceView播放視頻示例

編輯:Android開發實例

前言

  本文講解一下如何在Android下,使用SurfaceView播放一個視頻流媒體。之前有講到如何使用MediaPlayer播放音頻流媒體,其實MediaPlayer還可以播放視頻,只需需要SurfaceView的配合,SurfaceView主要用於顯示MediaPlayer播放的視頻流媒體的畫面渲染。對MediaPlayer不了解的朋友,可以先看看http://www.fengfly.com/plus/view-214094-1.html,本文中關於MediaPlayer的內容將不再詳解,主要以SurfaceView為主,最後將會以一個簡單的Demo演示SurfaceView如何播放視頻流媒體。

  本文的主要內容:

  1. SurfaceView  
  2. SurfaceView雙緩沖
  3. SurfaceHolder
  4. SurfaceView的兼容性
  5. SurfaceView的Demo示例

 

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

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
  2.     xmlns:tools="http://schemas.android.com/tools" 
  3.     android:layout_width="match_parent" 
  4.     android:layout_height="match_parent" 
  5.     android:orientation="vertical" 
  6.     android:paddingBottom="@dimen/activity_vertical_margin" 
  7.     android:paddingLeft="@dimen/activity_horizontal_margin" 
  8.     android:paddingRight="@dimen/activity_horizontal_margin" 
  9.     android:paddingTop="@dimen/activity_vertical_margin" 
  10.     tools:context=".MainActivity" > 
  11.  
  12.     <EditText 
  13.         android:id="@+id/et_path" 
  14.         android:layout_width="match_parent" 
  15.         android:layout_height="wrap_content" 
  16.         android:text="/sdcard/ykzzldx.mp4" /> 
  17.  
  18.     <SeekBar 
  19.         android:id="@+id/seekBar" 
  20.         android:layout_width="match_parent" 
  21.         android:layout_height="wrap_content" /> 
  22.  
  23.     <LinearLayout 
  24.         android:layout_width="wrap_content" 
  25.         android:layout_height="wrap_content" 
  26.         android:orientation="horizontal" > 
  27.  
  28.         <Button 
  29.             android:id="@+id/btn_play" 
  30.             android:layout_width="0dip" 
  31.             android:layout_height="wrap_content" 
  32.             android:layout_weight="1" 
  33.             android:text="播放" /> 
  34.  
  35.         <Button 
  36.             android:id="@+id/btn_pause" 
  37.             android:layout_width="0dip" 
  38.             android:layout_height="wrap_content" 
  39.             android:layout_weight="1" 
  40.             android:text="暫停" /> 
  41.  
  42.         <Button 
  43.             android:id="@+id/btn_replay" 
  44.             android:layout_width="0dip" 
  45.             android:layout_height="wrap_content" 
  46.             android:layout_weight="1" 
  47.             android:text="重播" /> 
  48.  
  49.         <Button 
  50.             android:id="@+id/btn_stop" 
  51.             android:layout_width="0dip" 
  52.             android:layout_height="wrap_content" 
  53.             android:layout_weight="1" 
  54.             android:text="停止" /> 
  55.     </LinearLayout> 
  56.  
  57.     <SurfaceView 
  58.         android:id="@+id/sv" 
  59.         android:layout_width="fill_parent" 
  60.         android:layout_height="fill_parent" /> 
  61.  
  62. </LinearLayout> 
  63. 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.  

 效果展示:

 

  源碼下載

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved