前言

  本篇博客讲解一下如何在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开发游戏应用,就需要我们自己维护这个缓冲区了。

         // 为SurfaceHolder添加回调
sv.getHolder().addCallback(callback); // 4.0版本之下需要设置的属性
// 设置Surface不维护自己的缓冲区,而是等待屏幕的渲染引擎将内容推送到界面
sv.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

SurfaceView的Demo示例

  上面讲了那么多关于SurfaceView的内容,下面通过一个Demo简单演示一下SurfaceView如何播放视频,加了一个滚动条,用于显示进度,还可以拖动滚动条选择播放位置,Demo的注释比较完整,这里不再累述,视频是在网上随便找的,朋友们运行的时候保证/sdcard/ykzzldx.mp4,这个目录下有这个文件。

  布局文件:activity_main.xml

 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" > <EditText
android:id="@+id/et_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="/sdcard/ykzzldx.mp4" /> <SeekBar
android:id="@+id/seekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content" /> <LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal" > <Button
android:id="@+id/btn_play"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="播放" /> <Button
android:id="@+id/btn_pause"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="暂停" /> <Button
android:id="@+id/btn_replay"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="重播" /> <Button
android:id="@+id/btn_stop"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="停止" />
</LinearLayout> <SurfaceView
android:id="@+id/sv"
android:layout_width="fill_parent"
android:layout_height="fill_parent" /> </LinearLayout>

activity_main.xml

  实现代码: 

 package cn.bgxt.surfaceviewdemo;

 import java.io.File;

 import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.os.Bundle;
import android.app.Activity;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.Toast; public class MainActivity extends Activity {
private final String TAG = "main";
private EditText et_path;
private SurfaceView sv;
private Button btn_play, btn_pause, btn_replay, btn_stop;
private MediaPlayer mediaPlayer;
private SeekBar seekBar;
private int currentPosition = 0;
private boolean isPlaying; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); seekBar = (SeekBar) findViewById(R.id.seekBar);
sv = (SurfaceView) findViewById(R.id.sv);
et_path = (EditText) findViewById(R.id.et_path); btn_play = (Button) findViewById(R.id.btn_play);
btn_pause = (Button) findViewById(R.id.btn_pause);
btn_replay = (Button) findViewById(R.id.btn_replay);
btn_stop = (Button) findViewById(R.id.btn_stop); btn_play.setOnClickListener(click);
btn_pause.setOnClickListener(click);
btn_replay.setOnClickListener(click);
btn_stop.setOnClickListener(click); // 为SurfaceHolder添加回调
sv.getHolder().addCallback(callback); // 4.0版本之下需要设置的属性
// 设置Surface不维护自己的缓冲区,而是等待屏幕的渲染引擎将内容推送到界面
// sv.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); // 为进度条添加进度更改事件
seekBar.setOnSeekBarChangeListener(change);
} private Callback callback = new Callback() {
// SurfaceHolder被修改的时候回调
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.i(TAG, "SurfaceHolder 被销毁");
// 销毁SurfaceHolder的时候记录当前的播放位置并停止播放
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
currentPosition = mediaPlayer.getCurrentPosition();
mediaPlayer.stop();
}
} @Override
public void surfaceCreated(SurfaceHolder holder) {
Log.i(TAG, "SurfaceHolder 被创建");
if (currentPosition > 0) {
// 创建SurfaceHolder的时候,如果存在上次播放的位置,则按照上次播放位置进行播放
play(currentPosition);
currentPosition = 0;
}
} @Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
Log.i(TAG, "SurfaceHolder 大小被改变");
} }; private OnSeekBarChangeListener change = new OnSeekBarChangeListener() { @Override
public void onStopTrackingTouch(SeekBar seekBar) {
// 当进度条停止修改的时候触发
// 取得当前进度条的刻度
int progress = seekBar.getProgress();
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
// 设置当前播放的位置
mediaPlayer.seekTo(progress);
}
} @Override
public void onStartTrackingTouch(SeekBar seekBar) { } @Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) { }
}; private View.OnClickListener click = new View.OnClickListener() { @Override
public void onClick(View v) { switch (v.getId()) {
case R.id.btn_play:
play(0);
break;
case R.id.btn_pause:
pause();
break;
case R.id.btn_replay:
replay();
break;
case R.id.btn_stop:
stop();
break;
default:
break;
}
}
}; /*
* 停止播放
*/
protected void stop() {
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.stop();
mediaPlayer.release();
mediaPlayer = null;
btn_play.setEnabled(true);
isPlaying = false;
}
} /**
* 开始播放
*
* @param msec 播放初始位置
*/
protected void play(final int msec) {
// 获取视频文件地址
String path = et_path.getText().toString().trim();
File file = new File(path);
if (!file.exists()) {
Toast.makeText(this, "视频文件路径错误", 0).show();
return;
}
try {
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
// 设置播放的视频源
mediaPlayer.setDataSource(file.getAbsolutePath());
// 设置显示视频的SurfaceHolder
mediaPlayer.setDisplay(sv.getHolder());
Log.i(TAG, "开始装载");
mediaPlayer.prepareAsync();
mediaPlayer.setOnPreparedListener(new OnPreparedListener() { @Override
public void onPrepared(MediaPlayer mp) {
Log.i(TAG, "装载完成");
mediaPlayer.start();
// 按照初始位置播放
mediaPlayer.seekTo(msec);
// 设置进度条的最大进度为视频流的最大播放时长
seekBar.setMax(mediaPlayer.getDuration());
// 开始线程,更新进度条的刻度
new Thread() { @Override
public void run() {
try {
isPlaying = true;
while (isPlaying) {
int current = mediaPlayer
.getCurrentPosition();
seekBar.setProgress(current); sleep(500);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}.start(); btn_play.setEnabled(false);
}
});
mediaPlayer.setOnCompletionListener(new OnCompletionListener() { @Override
public void onCompletion(MediaPlayer mp) {
// 在播放完毕被回调
btn_play.setEnabled(true);
}
}); mediaPlayer.setOnErrorListener(new OnErrorListener() { @Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// 发生错误重新播放
play(0);
isPlaying = false;
return false;
}
});
} catch (Exception e) {
e.printStackTrace();
} } /**
* 重新开始播放
*/
protected void replay() {
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.seekTo(0);
Toast.makeText(this, "重新播放", 0).show();
btn_pause.setText("暂停");
return;
}
isPlaying = false;
play(0); } /**
* 暂停或继续
*/
protected void pause() {
if (btn_pause.getText().toString().trim().equals("继续")) {
btn_pause.setText("暂停");
mediaPlayer.start();
Toast.makeText(this, "继续播放", 0).show();
return;
}
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.pause();
btn_pause.setText("继续");
Toast.makeText(this, "暂停播放", 0).show();
} } }

  效果展示:

  源码下载

Android--SurfaceView播放视频的更多相关文章

  1. Android SurfaceView播放视频时横竖屏的调整

    对于横屏录制的视频就横屏播放,对于竖屏录制的视频就竖屏播放. 在mainifest文件里对负责播放的Activity添加以下属性“ android:configChanges="orient ...

  2. Android实现播放视频

    转载:http://www.bdqn.cn/news/201311/12100.shtml 使用VideoView播放视频 VideoView,用于播放一段视频媒体,它继承了SurfaceView,位 ...

  3. [转]Android WebView播放视频(包括全屏播放),androidwebview

    Android WebView播放视频(包括全屏播放),androidwebview 最近项目开发中用到了WebView播放视频的功能,总结了开发中犯过的错误,这些错误在开发是及容易遇到的,所以我这里 ...

  4. android 98 MediaPlayer+SurfaceView播放视频

    package com.itheima.videoplayer; import java.io.IOException; import android.media.MediaPlayer; impor ...

  5. Android MediaPlayer和SurfaceView播放视频

    昨天介绍了VideoView播放视频,今天再介绍一种播放视频的方法MediaPlayer和SurfaceView,MediaPlayer播放音频,SurfaceView来显示图像,具体步骤如下: 1. ...

  6. 使用MediaPlayer和SurfaceView播放视频

    使用VideoView播放视频简单.方便,丹有些早期的开发者更喜欢使用MediaPlayer来播放视频,但由于MediaPlayer主要用于播放音频,因此它没有提供图像输出界面,此时 需要借助于Sur ...

  7. Android OpenGL 播放视频学习

    1, 初步接触Open GL: http://www.cnblogs.com/TerryBlog/archive/2010/07/09/1774475.html 使用GLSurfaceView和Ren ...

  8. Android VideoView播放视频

    今天介绍一下Android的视频播放控件VideoView,下面介绍一下VideoView的使用步骤: 1.在界面布局中定义VideoView组件,或者在程序中创建VideoView组件. 2.调用V ...

  9. 【转】Android WebView 播放视频总结

    今天发现 WebView里播放优酷的视频点击播放按钮后没反应,于是看官方文档和搜索解决,下面是我在别人基础上做的补充:   android webView 无法播放视频,无法暂停,继续播放视频问题,无 ...

  10. Android WebView播放视频flash(判断是否安装flash插件)

    Android WebView播放flash(判断是否安装flash插件)  最近帮一个同学做一个项目,断断续续的一些知识点记录一下.一个页面中有一个WebView,用来播放swf,如果系统中未安装f ...

随机推荐

  1. Nastya Is Buying Lunch

    At the big break Nastya came to the school dining room. There are nn pupils in the school, numbered ...

  2. 网防G01管理检测系统Linux版安装

    监测包内容: gov_defence_agent_x64_linux_v3.1.18.tar.gz LinuxVersion(datalog.sh  getlog.sh  setup.sh) 1.  ...

  3. 反沙箱——SetErrorMode

    目录 1.前言 2.原理讲解 3.代码实现 4.参考 1.前言 利用SetErrorMode进行反沙箱的技术,在2010年就有被提出,但是之前搜了很久都没有相关内容,这里简单的说一下这个反沙箱的实现. ...

  4. IntelliJ IDEA配置Tomcat和Lombok

    Tomcat的安装和配置 Tomcat 是在SUN公司的JSWDK(JavaServer Web DevelopmentKit)的基础上发展而来的一个优秀的Servlet容器,其本身完全是由Java编 ...

  5. BUAA-OO-第二单元总结

    OO第二单元总结 一.第五次作业 1.1 设计策略与架构 第五次作业要求的是完成设计支持一架傻瓜电梯的电梯系统.考虑到需要数据结构存放所有的请求,因此构建了FloorRequests类用来存放所有的请 ...

  6. html笔记第一天

    快速生成标签有序ol>li*3无序ul>(li>a{新闻标题})*3定义列表 dl>(dt+dd)*3制作表格table>(tr>td*5)*6pading:3个数 ...

  7. nodejs内存溢出

    npm-v 报错,错误信息如下: FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScri ...

  8. 分享使用tcb-router路由开发的云函数短信平台SDK

    上篇文章我们分享了如何使用纯的云函数开发的榛子短信短信(http://smsow.zhenzikj.com)SDK,由于微信对于未付费云函数个数的限制,这种方法存在缺陷,经过改进,使用tcb-rout ...

  9. 贪心算法----区间选点问题(POJ1201)

    题目: 题目的大致意思是,给定n个闭区间,并且这个闭区间上的点都是整数,现在要求你使用最少的点来覆盖这些区间并且每个区间的覆盖的点的数量满足输入的要求点覆盖区间的数量. 输入: 第一行输入n,代表n个 ...

  10. SUSE12Sp3-kafka安装

    1.安装java jdk sudo mkdir -p /usr/local/java #创建目录 将jdk-8u201-linux-x64.tar.gz上传到该目录 cd /user/local/ja ...