前言

  本篇博客讲解一下如何在Android下,使用SurfaceView播放一个视频流媒体。之前有讲到如何使用MediaPlayer播放音频流媒体,其实MediaPlayer还可以播放视频,只需需要SurfaceView的配合,SurfaceView主要用于显示MediaPlayer播放的视频流媒体的画面渲染。对MediaPlayer不了解的朋友,可以先看看那篇博客:Android--MediaPlayer播放MP3,本篇博客中关于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>
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 }
复制代码

  效果展示:

 

  源码下载

 

 

 

 

点赞(0)

评论列表 共有 0 条评论

暂无评论
立即
投稿
发表
评论
返回
顶部