Android SurfaceView 双缓冲视频播放器模型编写——随手笔记知识详细概括

Android SurfaceView 日常应用中应用开发用到的比较少,通常在一些视频播放器和游戏中用到,SurfaceView比起View来说

开发类似播放器的东西还是比较方便的,虽然说是View的子类,都是在onDraw( )里进行绘画和处理,View的流程是通过

Paint,绘画之后通过Canvas通过UI线程直接渲染到屏幕上,而SurfaceView则是双缓冲,这里简单说说所谓的双缓冲,意为两

个子线程分开处理,因为比如我们的动画在屏幕播放都是一帧一帧的,播放一帧,解析一帧,在播放,在解析,这样的做法明

显是不科学的,很容易早成UI主线程的阻塞,双缓冲就可以很好的解决这个问题,一个线程负责在后台把所有帧解析,之后再

由另一个帧执行整个解析之后的数据进行屏幕的渲染,这样就避免了线程的阻塞问题,当数据处理且执行完之后,在关闭线程

避免不必要的内存资源浪费

我写了如下2种情况处理的视频播放器对比:

因为我是在模拟器测试的,会涉及到SD卡得读写权限,所以我们需要添加如下权限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

XML布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/LinearLayout01"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/login_bg"
    android:orientation="vertical" >

    <RelativeLayout
        android:id="@+id/login_div"
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:layout_margin="15dip"
        android:layout_weight="0.90"
        android:background="@drawable/login_background"
        android:padding="15dip" >

        <SurfaceView
            android:id="@+id/surfaceView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            />

    </RelativeLayout>
    <Button
        android:id="@+id/btn_play"
        android:layout_width="fill_parent"
        android:layout_height="25dp"
        android:layout_marginTop="10dp"
        android:background="@drawable/button_style"
        android:gravity="center"
        android:text="Play" />

    <Button
        android:id="@+id/btn_pause"
        android:layout_width="fill_parent"
        android:layout_height="25dp"
        android:layout_marginTop="10dp"
        android:background="@drawable/button_style"
        android:gravity="center"
        android:text="Pause" />

    <Button
        android:id="@+id/btn_stop"
        android:layout_width="fill_parent"
        android:layout_height="25dp"
        android:layout_marginTop="10dp"
        android:background="@drawable/button_style"
        android:gravity="center"
        android:text="Stop" />

</LinearLayout>

活动类代码:

package com.example.engineerjspsurfaceview;
/**
 * SurfaceView 
 * @author Engineer-Jsp
 * @date 2014.11.11
 * */
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.app.Activity;
public class MainActivity extends Activity implements SurfaceHolder.Callback{
	private static final String TAG = "MainActivity";
    SurfaceHolder surfaceholder;
    MediaPlayer player;
    SurfaceView surfaceview;
    Button play,pause,stop;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		initView();//初始化
		surfaceholder = surfaceview.getHolder();//SurfaceHolder是SurfaceView的控制接口
		surfaceholder.addCallback(this);//因为这个类实现了SurfaceHolder.Callback接口,所以回调参数直接this
		//设置surfaceview不维护自己的缓冲区,而是等待屏幕的渲染引擎将内容推送到用户面前,由MediaPlayer对象来完成
		surfaceholder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);//高版本此方法已过时
		
		//播放
		play.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View arg0) {
				player.start();
			}
		});
		//暂停
		pause.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View arg0) {
				player.pause();
			}
		});
		//结束播放
		stop.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View arg0) {
				player.stop();
			}
		});
	}
    // surface对象改变方法
	@Override
	public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
		Log.d(TAG, "surfaceChanged");
	}
	// surface对象生成
	@Override
	public void surfaceCreated(SurfaceHolder arg0) {
		Log.d(TAG, "surfaceCreated");
		//必须在surface创建后才能初始化MediaPlayer,否则不会显示图像
		player = new MediaPlayer();
		player.setAudioStreamType(AudioManager.STREAM_MUSIC);//流媒体类型
		player.setDisplay(surfaceholder);//显示
		//设置显示视频显示在SurfaceView上
		try {
			player.setDataSource("/sdcard/video.avi");//媒体文件地址
			player.prepare();//缓冲
		} catch (Exception e) {
			e.printStackTrace();
		} 
	}
	// surface对象销毁
	@Override
	public void surfaceDestroyed(SurfaceHolder arg0) {
		Log.d(TAG, "surfaceDestroyed");
	}
	//初始化方法
	public void initView(){
		surfaceview = (SurfaceView)findViewById(R.id.surfaceView);
		pause = (Button)findViewById(R.id.btn_pause);
		play  = (Button)findViewById(R.id.btn_play);
		stop  = (Button)findViewById(R.id.btn_stop);
	}
	//Activity销毁
	@Override
	protected void onDestroy() {
		super.onDestroy();
		Log.d(TAG, "onDestroy");
		if(player.isPlaying()){
			player.stop();
		}
		//Activity销毁时停止播放,释放资源。不做这个操作,即使退出还是能听到视频播放的声音
		player.release();
		
	}
	@Override
	protected void onStart() {
		super.onStart();
		Log.d(TAG, "onStart");
	}
	@Override
	protected void onStop() {
		super.onStop();
		Log.d(TAG, "onStop");
	}
	@Override
	protected void onPause() {
		super.onPause();
		Log.d(TAG, "onPause");
	}
	@Override
	protected void onResume() {
		super.onResume();
		Log.d(TAG, "onResume");
	}
	@Override
	protected void onRestart() {
		super.onRestart();
		Log.d(TAG, "onRestart");
	}
}

在活动里,我分别把生命周期函数全部写了打印测试语句,看Surface对象生命周期与Activity生命周期的联系,方便对下面的操作更进一步了解

启动播放器之后的生命周期函数图:


因为我没有在onCreate()写打印语句,所以没有onCreate打印结果,可以看到Activity优先于Surface创建,还有就是第66行代码,为什么要在这里进行媒体播放器创建呢,因为在SurfaceView对象再未渲染之前,创建播放器的话,视频播放器将不会播放任何视频数据,所以应该放在Surface对象被创建渲染函数里才是正确的

执行效果:


布局不是很美观,因为是供测试用的,暂且不做美化,点击play视频开始播放,Pause暂停,Stop结束,可以看上图我专门用红框标记了那个房子,也就是Home键,当我们点击播放器的暂停时,在执行Home键,那会产生什么结果呢?

下面我给大家继续演示,当我们点击HOME键之后,生命周期如下:


当我们点击HOME键之后,活动被暂停,Surface对象被销毁,活动结束,当我们在次打开应用程序,发现屏幕是黑的,什么也没有,这不是BUG,这跟Surface的生命周期有关,后面将会详细介绍产生这种结果的原因和解决办法,下面我们再来看看重新进入应用程序的生命周期函数图


可以清楚的看到,当再次进入程序,活动被重启了,活动开始,活动继续,Surface对象重新被创建,但是应用程序没有任何动画,这是为什么呢?

原因:主要是由于Android的回收机制,为了防止程序的内存问题,Android会把比较耗内存的且在当前活动不可见得操作回收掉,因为SurfaceView是特别耗内存的,且我们按下HOME键之后,活动被暂停结束,SurfaceView也被销毁了,变为不可见,Android就将其回收掉了,因为之前的Surface对象播放的时候呗暂停了,之后又被销毁,所以在此进入程序,我们是不会看到动画的,要怎么解决这个BUG呢?这里我们需要用到Surface的生命周期函数来处理

SurfaceView更进版代码:

package com.example.engineerjspsurfaceview;
/**
 * SurfaceView更进版
 * @author Engineer-Jsp
 * @date 2014.11.11
 * */
import java.io.File;
import java.io.FileInputStream;

import android.app.Activity;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class EngineerJspSurfaceView extends Activity{
	private static final String TAG = "EngineerJspSurfaceView";//Logcat 过滤标签
	SurfaceHolder surfaceholder;//SurfaceHolder对象
	MediaPlayer player;//播放器对象
    EditText filepath;//路径输入框
    SurfaceView surfaceview;//SurfaceView对象
    Button play,pause,stop,replay;//按钮组件
    String filename;//绝对路径
    int lastnode;//Video最后保存帧
	@Override
	//Activity创建
	protected void onCreate(Bundle savedInstanceState) {
		//Activity的当前状态
		super.onCreate(savedInstanceState);
		//实例化xml
		setContentView(R.layout.main);
		//需要初始化的组件,方法调用
		initView();
	}
	/**
	 * 自定义按钮事件
	 * */
	class EngineerJspSurfaceListenner implements View.OnClickListener{
		@Override
		public void onClick(View view) {
			//Environment.MEDIA_MOUNTED  sd卡在手机上正常使用状态
			//Environment.MEDIA_UNMOUNTED  用户手工到手机设置中卸载sd卡之后的状态
			//Environment.MEDIA_REMOVED  用户手动卸载,然后将sd卡从手机取出之后的状态
			//Environment.MEDIA_BAD_REMOVAL  用户未到手机设置中手动卸载sd卡,直接拨出之后的状态
			//Environment.MEDIA_SHARED  手机直接连接到电脑作为u盘使用之后的状态
			//Environment.MEDIA_CHECKINGS  手机正在扫描sd卡过程中的状态
			//在做android开发对sd操作时,最好是sd卡处于Environment.MEDIA_MOUNTED状态时,对sd卡上的文件进行操作,其他状态不宜进行操作
			if(Environment.getExternalStorageState()==Environment.MEDIA_UNMOUNTED){
				Log.d(TAG, "SD卡异常...");
			}
			//视频文件路径,EditText中获取
			filename = filepath.getText().toString();
			Log.d(TAG, "Video路径:"+filename);
			switch (view.getId()) {
			//开始播放
			case R.id.play:
				play();
				break;
			//结束播放	
            case R.id.stop:
				if(player.isPlaying()){
					player.stop();
				}
				break;
			//暂停/继续播放	
            case R.id.pause:
            	if(player.isPlaying()){
            		player.pause();
            	}else{
            		player.start();
            	}
	            break;
	        //重头播放    
           case R.id.replay:
        	   if(player.isPlaying()){
        		   player.seekTo(0);
        	   }else{
        		   play();
        	   }
	            break;
			}
		}
		
	}
	/**
	 * 自定义接口
	 * 启动自定义接口类对象(被监听端)
	 * */
	class EngineerJspSurface implements SurfaceHolder.Callback{
        //Surface对象改变
		@Override
		public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2,
				int arg3) {
			Log.d(TAG, "surfaceChanged");
		}
		//Surface对象创建
		@Override
		public void surfaceCreated(SurfaceHolder arg0) {
			Log.d(TAG, "surfaceCreated");
			if(lastnode>0&&null!=filename){//判断二次Surface渲染对象最后帧的值,及文件路径,(帧值保存过,文件存在)
				play();//启动,渲染
				player.seekTo(lastnode);//跳至lastnode保存的帧为起始的帧
				lastnode=0;//重新初始化,方便下次Surface渲染时存getCurrentPosition的帧值
			}
		}
		//Surface对象销毁
		@Override
		public void surfaceDestroyed(SurfaceHolder arg0) {
			Log.d(TAG, "surfaceDestroyed");
			if(player.isPlaying()){//判断当前播放器状态,播放中
				lastnode = player.getCurrentPosition();//取当前帧
				player.stop();//kill掉
			}
		}
		
	}
	    //Activity销毁
		@Override
		protected void onDestroy() {
			super.onDestroy();
			Log.d(TAG, "onDestroy");
		}
		//Activity开始
		@Override
		protected void onStart() {
			super.onStart();
			Log.d(TAG, "onStart");
		}
		//Activity停止
		@Override
		protected void onStop() {
			super.onStop();
			Log.d(TAG, "onStop");
		}
		//Activity暂停
		@Override
		protected void onPause() {
			super.onPause();
			Log.d(TAG, "onPause");
		}
		//Activity继续
		@Override
		protected void onResume() {
			super.onResume();
			Log.d(TAG, "onResume");
		}
		//Activity重启
		@Override
		protected void onRestart() {
			super.onRestart();
			Log.d(TAG, "onRestart");
		}
		//播放方法
		public void play(){
			try {
				File file = new File(Environment.getExternalStorageDirectory(),filename);//取文件目录
				player.reset();//重置初始化状态
				player.setAudioStreamType(AudioManager.STREAM_MUSIC);//流媒体类型
				player.setDisplay(surfaceview.getHolder());//影片以SurfceView播放
				Log.d(TAG, "Video绝对路径:"+file.getAbsolutePath());
				FileInputStream fis = new FileInputStream(filename);
				player.setDataSource(fis.getFD());//媒体路径
				player.prepare();//缓冲
				player.start();//播放
			} catch (Exception e) {
				Log.d(TAG, e.toString());//打印捕获异常
				e.printStackTrace();
			}
		}
		//初始化/实例化组件
		public void initView(){
			filepath = (EditText)findViewById(R.id.filepath);
			surfaceview = (SurfaceView)findViewById(R.id.surfaceView);
			play = (Button)findViewById(R.id.play);
			pause= (Button)findViewById(R.id.pause);
			stop = (Button)findViewById(R.id.stop);
			replay=(Button)findViewById(R.id.replay);
			surfaceview.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);//设置surfaceview不维护自己的缓冲区,而是等待屏幕的渲染引擎将内容推送到用户面前,MediaPlayer来完成
			surfaceview.getHolder().addCallback(new EngineerJspSurface());//对Surface对象实时监听
			player = new MediaPlayer();//创建播放器
			EngineerJspSurfaceListenner listenner = new EngineerJspSurfaceListenner();//监听器对象
			play.setOnClickListener(listenner);
			pause.setOnClickListener(listenner);
			stop.setOnClickListener(listenner);
			replay.setOnClickListener(listenner);
		}
}

解决办法是我们需要在Surface生命周期函数SurfaceCreate()里对上一次最后播放的帧进行判断和处理,也就是Surface生命周期函数SurfaceDestory()执行之后,当我们点击HOME键,我们在 SurfaceDestory()函数调用getCurrentPosition()方法取到Surface在被销毁之前的最后一帧,将其保存,直到我们下一次执行SurfaceCreate()函数的时候,对帧进行一次判断就OK了,然后调用seekTo()方法进行跳帧播放,这样我们就可以解决当活动或者Surface被销毁之后再次进入程序无法看到动画的原因了

XML布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/LinearLayout01"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/login_bg"
    android:orientation="vertical" >
    <EditText 
        android:id="@+id/filepath"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:hint="Input your video Filepath"
        />
    <RelativeLayout
        android:id="@+id/login_div"
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:layout_margin="15dip"
        android:layout_weight="0.90"
        android:background="@drawable/login_background"
        android:padding="15dip" >

        <SurfaceView
            android:id="@+id/surfaceView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            />

    </RelativeLayout>
    <Button
        android:id="@+id/play"
        android:layout_width="fill_parent"
        android:layout_height="25dp"
        android:layout_marginTop="10dp"
        android:background="@drawable/button_style"
        android:gravity="center"
        android:text="Play" />

    <Button
        android:id="@+id/pause"
        android:layout_width="fill_parent"
        android:layout_height="25dp"
        android:layout_marginTop="10dp"
        android:background="@drawable/button_style"
        android:gravity="center"
        android:text="Pause" />

    <Button
        android:id="@+id/stop"
        android:layout_width="fill_parent"
        android:layout_height="25dp"
        android:layout_marginTop="10dp"
        android:background="@drawable/button_style"
        android:gravity="center"
        android:text="Stop" />
        <Button
        android:id="@+id/replay"
        android:layout_width="fill_parent"
        android:layout_height="25dp"
        android:layout_marginTop="10dp"
        android:background="@drawable/button_style"
        android:gravity="center"
        android:text="Replay" />

</LinearLayout>

效果图这里就不上了,跟之前的差不多,就是多了一个重头播放按钮,本篇博客需要掌握的重点:

Surface、SurfaceView、SurfaceHolder、SurfaceHolder.Callback接口、Surface生命周期函数、播放器逻辑

好了,内容就这么多,好好消化,试试自己动手写写,有助于加深印象


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Engineer-Jsp

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值