• Android--SurfaceView播放视频


    前言

      本篇博客讲解一下如何在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 }

      效果展示:

      源码下载

     

  • 相关阅读:
    AOD.net
    C# Eval()和Bind()
    .Net使用微軟自帶的用戶驗證和登錄授權
    .Net面試4套
    .Net面試題
    MVC开发模式
    .Net自帶Ajax和GridView
    HTML系列(HTMl+CSS+JavaScript+Jquery)--un
    .Net母版页
    .NetDOM操作--un
  • 原文地址:https://www.cnblogs.com/plokmju/p/android_SurfaceView.html
Copyright © 2020-2023  润新知