1. 程式人生 > >android之surfaceView詳解--自定義surfaceView和用於視訊surfaceview

android之surfaceView詳解--自定義surfaceView和用於視訊surfaceview

一、SurfaceView和VIew的區別

       1、VIew主要適用於主動更新情況,並且只能在主執行緒繪製和更新畫面,以及在繪圖時沒有使用雙緩衝機制

      2、surfaceView主要適用於被動更新,如頻繁的重新整理,因為它可以通過子執行緒來進行頁面的重新整理,而且在底層已經實現雙緩衝機制,繪圖時不會出現閃爍問題

      總而言之,SurfaceView繼承自View,主要用於視訊、音訊或耗時的繪圖和經常更新檢視(地圖等等)顯示,如果自定義的view需要頻繁重新整理,或者重新整理時處理的資料量大,則可以考慮surfaceview取代view,因為View在主執行緒中會阻塞主執行緒,若view的重新整理操作超過16ms則使用者視覺會感到卡頓,由於surfaceView是子執行緒處理就不會

 SurfaceView類的成員變數mRequestedType描述的是SurfaceView的繪圖表面的型別,一般來說,它的值可能等於SURFACE_TYPE_NORMAL,也可能等於SURFACE_TYPE_PUSH_BUFFERS。

        當一個SurfaceView的繪圖表面的型別等於SURFACE_TYPE_NORMAL的時候,就表示該SurfaceView的繪圖表面所使用的記憶體是一塊普通的記憶體。一般來說,這塊記憶體是由SurfaceFlinger服務來分配的,我們可以在應用程式內部自由地訪問它,即可以在它上面填充任意的UI資料,然後交給SurfaceFlinger服務來合成,並且顯示在螢幕上。在這種情況下,SurfaceFlinger服務使用一個Layer物件來描述該SurfaceView的繪圖表面。

        當一個SurfaceView的繪圖表面的型別等於SURFACE_TYPE_PUSH_BUFFERS的時候,就表示該SurfaceView的繪圖表面所使用的記憶體不是由SurfaceFlinger服務分配的,因而我們不能夠在應用程式內部對它進行操作。例如,當一個SurfaceView是用來顯示攝像頭預覽或者視訊播放的時候,我們就會將它的繪圖表面的型別設定為SURFACE_TYPE_PUSH_BUFFERS,這樣攝像頭服務或者視訊播放服務就會為該SurfaceView繪圖表面建立一塊記憶體,並且將採集的預覽影象資料或者視訊幀資料來源源不斷地填充到該記憶體中去。注意,這塊記憶體有可能是來自專用的硬體的,例如,它可能是來自視訊卡的。在這種情況下,SurfaceFlinger服務使用一個LayerBuffer物件來描述該SurfaceView的繪圖表面。

        從上面的描述就得到一個重要的結論:繪圖表面型別為SURFACE_TYPE_PUSH_BUFFERS的SurfaceView的UI是不能由應用程式來控制的,而是由專門的服務來控制的,例如,攝像頭服務或者視訊播放服務,同時,SurfaceFlinger服務會使用一種特殊的LayerBuffer來描述這種繪圖表面。使用LayerBuffer來描述的繪圖表面在進行渲染的時候,可以使用硬體加速,例如,使用copybit或者overlay來加快渲染速度,從而可以獲得更流暢的攝像頭預覽或者視訊播放。

        注意,我們在建立了一個SurfaceView之後,可以呼叫它的成員函式getHolder獲得一個SurfaceHolder物件,然後再呼叫該SurfaceHolder物件的成員函式setType來修改該SurfaceView的繪圖表面的型別,即修改該SurfaceView的成員變數mRequestedType的值

         這段結論就說明決定surfaceView的記憶體是普通記憶體(由開發者自己決定用來繪製什麼)還是專用的記憶體(開發者無法使用這塊記憶體)由mRequestType決定,而我們可以通過holde.setType()設定,用於決定是顯示視訊還是自定義的surfaceView。

      以下直接用demo講解自定義的view和專用於視訊的surfaceView

1、自定義surfaceView,用於繪製sin曲線

package com.example.administrator.surfaceviewdemo;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

/**
 * author : zhongwr on 2016/7/12
 * SurfaceView類的成員函式draw和dispatchDraw的引數canvas所描述的都是建立在宿主視窗的繪圖表面上的畫布,
 * 因此,在這塊畫布上繪製的任何UI都是出現在宿主視窗的繪圖表面上的。
 * <p/>
 * 本來SurfaceView類的成員函式draw是用來將自己的UI繪製在宿主視窗的繪圖表面上的,
 * 但是這裡我們可以看到,如果當前正在處理的SurfaceView不是用作宿主視窗面板的時候,
 * 即其成員變數mWindowType的值不等於WindowManager.LayoutParams.TYPE_APPLICATION_PANEL的時候,
 * SurfaceView類的成員函式draw只是簡單地將它所佔據的區域繪製為黑色。
 */
public class SinSurfaceView extends SurfaceView implements Runnable {
    private SurfaceHolder mSurfaceHolder;
    /**
     * 用於儲存正弦路徑座標
     */
    private Path mPath;
    private Paint mPaint;

    public SinSurfaceView(Context context) {
        super(context);
        init();
    }

    public SinSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public SinSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);
        mPaint.setColor(Color.GREEN);
        //連線處更加平滑
        mPaint.setStrokeJoin(Paint.Join.ROUND);

        mPath = new Path();

        /**通過holder去申請繪圖表面的畫布,surfaceview其實draw()或dispathDraw()都只是一塊預設的黑色區域,並不是用作宿主
         * 真正要做的事情由開發者自行繪製,繪製之前就是通過holder獲取一塊記憶體區域的畫布,
         * 然後可在UI執行緒或工作執行緒在這個畫布上進行繪製所需要的檢視,最後還是通過holder提交這個畫布就可以顯示
         * */
        mSurfaceHolder = getHolder();
        //回撥
        mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
            /***
             * surfaceview的繪圖表面(就是activity宿主建立一個透明的表面用於surfaceView繪製)被建立時執行
             * 在updateWindow()建立宿主(activity的視窗)的繪圖表面時會回撥,雖然surfaceView是獨立於一個執行緒但還是離不開宿主視窗,
             * 最後還是要貼上到window中
             *
             * surfaceCreated方法,是當SurfaceView被顯示時會呼叫的方法,所以你需要再這邊開啟繪製的線 程
             *
             * @param holder
             */
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                new Thread(SinSurfaceView.this).start();
            }

            /**
             * 建立、更新會認為發生變化也會回撥這個方法
             * @param holder
             * @param format
             * @param width
             * @param height
             */
            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

            }

            /***
             *surfaceDestroyed方法是當SurfaceView被隱藏會銷燬時呼叫的方法,在這裡你可以關閉繪製的執行緒
             * @param holder
             */
            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                isDrawing = false;
            }
        });
    }

    /***
     * 是否在繪製:用於關閉子執行緒:true則表示一直迴圈
     */
    private boolean isDrawing = true;
    private int drawX;
    private int drawY;

    /***
     * 注意這個是在子執行緒中繪製的
     */
    @Override
    public void run() {
        while (isDrawing) {
            drawX++;
            drawY = (int) (100 * Math.sin(drawX * 2 * Math.PI / 180) + 400);
            mPath.lineTo(drawX, drawY);
            draw(mPath);
        }

    }

    /***
     * 注意這個是在子執行緒中繪製的,surface支援子執行緒更新ui,所以
     */
    private void draw(Path path) {
        Canvas canvas = null;
        //給畫布加鎖,防止執行緒安全,防止該記憶體區域被其他執行緒公用
        canvas = mSurfaceHolder.lockCanvas();
        if (null != canvas) {
            //清屏操作或者設定背景
            canvas.drawColor(Color.WHITE);
            canvas.drawPath(mPath, mPaint);
            //提交顯示檢視並解鎖,防止長期佔用此記憶體
            mSurfaceHolder.unlockCanvasAndPost(canvas);
        }
    }
}
    demo:http://download.csdn.net/detail/zhongwn/9574865

       2、surfaceView用於顯視訊的demo

           一、注意兩點:

               (1)mediaPlayer = new MediaPlayer()必須只初始化一次,若是放在play()方法中初始化,由於可能會多次執行直接或間接play()這個方法,導致多次初始化,以至於在onPrepared(MediaPlayer mp)的回撥中(假設監聽器用的是同一個,看demo),mediaPlayer和mp不是同一個,因為多次初始化原因,導致不是同一個物件,保證是一個物件就行

                (2)播放本地視訊音訊時,明明就有檔案,但是通過/sdcard/xxxx.mp4或者storage/sdcard0/xxx.mp4都說找不到這個路徑,然後檢視異常日誌會發現如下日誌

                       <1>、 QCMediaPlayer mediaplayer NOT present

                      發現這個不要驚慌,雖然我也搞了好長時間。最後才發現,其實就是沒加sd卡的許可權,只要在manifest中加入許可權              <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />基本就好了,跟網上說的 

                    使用高通的手機平臺都有這個問題,MTK 平臺的就沒有。主要是高通平臺不支援直接 new Mediaplayer(); 必須用        MediaPlayer.create(xxx)方法 好像並沒有什麼關係

           <2>、Should have subtitle controller already set
                     發現這個異常也不要驚慌,畢竟這個好像是sdk4.4之後原始碼才加入的,可以不用管,不會影響視訊的播放

        二、談談mediaPlayer的建立

               要是仔細的話你會發現,系統有好幾個MediaPlayer.create(xxx),但是我們幾乎都是自己new MediaPlayer()建立,這兩個有什麼區別呢,接下來說說

              要是看過 MediaPlayer.create(xxx)這個方法的原始碼,你會發現,最後它也是通過new MediaPlayer(),只不過MediaPlayer.create(xxx)是已經初始化了的,然後值得注意的是mediaPlayer.prepare()和mediaPlayer.setDatesource(context,Uri);

              mediaPlayer.prepare()主要是同步預載入,若是預載入資源則mediaPlayer.seekTo()是不會生效的,同樣mediaPlayer.getCurrentPosition()也不會生效,因為這都是mediaPlayer.prepareSync()這個非同步載入才會生效的,即他們都是非同步載入的方法            。

                  mediaPlayer.setDatesource(context,Uri); 這個方法載入資源時不需要許可權的,直接可以播放,所以網路上出現問題(2)的<1>時,網上都說用create()方法建立mediaPlayer的就沒問題

          以下摘抄子網路,各個方法詳解:

          1.如何獲得MediaPlayer例項:
   可以使用直接new的方式:
MediaPlayer mp = new MediaPlayer();
也可以使用create的方式,如:
MediaPlayer mp = MediaPlayer.create(this, R.raw.test);//這時就不用呼叫setDataSource了

2.如何設定要播放的檔案:
MediaPlayer要播放的檔案主要包括3個來源:
a. 使用者在應用中事先自帶的resource資源
例如:MediaPlayer.create(this, R.raw.test);
b. 儲存在SD卡或其他檔案路徑下的媒體檔案
例如:mp.setDataSource("/sdcard/test.mp3");
c. 網路上的媒體檔案
例如:mp.setDataSource("mp3或者mp4的地址");

3.MediaPlayer常用API
MediaPlayer的setDataSource一共四個方法:
setDataSource (String path) 
setDataSource (FileDescriptor fd) 
setDataSource (Context context, Uri uri) 
setDataSource (FileDescriptor fd, long offset, long length)

對播放器的主要控制方法:
Android通過控制播放器的狀態的方式來控制媒體檔案的播放,其中:

1.prepare()和prepareAsync() 提供了同步和非同步兩種方式設定播放器進入prepare狀態,需要注意的是,如果MediaPlayer例項是由create方法建立的,那麼第一次啟動播放前不需要再呼叫prepare()了,因為create方法裡已經呼叫過了。
2. start()是真正啟動檔案播放的方法
3.pause()和stop()比較簡單,起到暫停和停止播放的作用
4.seekTo()是定位方法,可以讓播放器從指定的位置開始播放,需要注意的是該方法是個非同步方法,也就是說該方法返回時並不意味著定位完成,尤其是播放的網路檔案,真正定位完成時會觸發OnSeekComplete.onSeekComplete(),如果需要是可以呼叫setOnSeekCompleteListener(OnSeekCompleteListener)設定監聽器來處理的。
5.release()可以釋放播放器佔用的資源,一旦確定不再使用播放器時應當儘早呼叫它釋放資源。
6.reset()可以使播放器從Error狀態中恢復過來,重新會到Idle狀態。

       廢話不說了:直接上程式碼:其它程式碼註釋很明瞭,直接看應該就能看懂,功能自己擴充套件

package com.example.administrator.surfaceviewdemo;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.TimedText;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;

import java.io.File;
import java.lang.ref.WeakReference;

/**
 * 視訊播放類
 * @author zhognwr
 */
public class PlayVedioActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = "PlayVedioActivity";
    private SeekBar videoSeekBar;
    private TextView tvPlayingTime;
    private TextView tvVideoTotalTime;
    private ImageView ivPlay;
    private ImageView ivPause;

    private MediaPlayer mediaPlayer;
    private SurfaceView surfaceView;
    private EditText etPath;
    /**
     * 當前播放視訊位置
     */
    private int currentPosition;

    public static void startInstance(Context context) {
        context.startActivity(new Intent(context, PlayVedioActivity.class));
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.video_player_layout);
        initView();
        initListener();
        initData();
    }

    @Override
    protected void onPause() {
        super.onPause();
        updateSeekBarHandler.removeMessages(0);
        mediaPlayer.reset();
        mediaPlayer.release();
    }

    private void initData() {

        mediaPlayer= new MediaPlayer();
        // 把輸送給surfaceView的視訊畫面,直接顯示到螢幕上,不要維持它自身的緩衝區
        surfaceView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
//		surfaceView.getHolder().setFixedSize(176, 144);
        surfaceView.getHolder().setKeepScreenOn(true);
        surfaceView.getHolder().addCallback(new SurfaceCallback());


    }

    private void initListener() {
        ivPause.setOnClickListener(this);
        ivPlay.setOnClickListener(this);
        videoSeekBar.setOnSeekBarChangeListener(new SeekBarListener());
        surfaceView.getHolder().addCallback(new SurfaceCallback());
    }

    private void initView() {
        surfaceView = (SurfaceView) findViewById(R.id.surfaceView);
        videoSeekBar = (SeekBar) findViewById(R.id.videoProgress);
        tvPlayingTime = (TextView) findViewById(R.id.textHasPalyTime);
        tvVideoTotalTime = (TextView) findViewById(R.id.video_total_time);
        ivPlay = (ImageView) findViewById(R.id.imagePlay);
        ivPause = (ImageView) findViewById(R.id.iv_pause);
        etPath = (EditText) findViewById(R.id.et_path);
    }

    @Override
    public void onClick(View v) {
        if (v == ivPlay) {//播放
            if (!mediaPlayer.isPlaying()) {//播放、繼續播放
//                pause = false;
                mediaPlayer.start();
            }
        } else if (v == ivPause) {//暫停
//            pause = true;
            mediaPlayer.pause();
        }
    }
    /**
     * 重新開始播放
     */
    protected void replay() {
        if (mediaPlayer != null && mediaPlayer.isPlaying()) {
            mediaPlayer.seekTo(0);
            Toast.makeText(this, "重新播放", Toast.LENGTH_SHORT).show();
            return;
        }
    }

    /**
     * 暫停或繼續
     */
    protected void pause() {
        if (mediaPlayer != null && mediaPlayer.isPlaying()) {
            mediaPlayer.pause();
            Toast.makeText(this, "暫停播放", Toast.LENGTH_LONG).show();
        }

    }
    /**
     * SeekBar監聽類,監聽使用者對seekbar滑動的位置,已達到控制視訊播放進度
     */
    private class SeekBarListener implements SeekBar.OnSeekBarChangeListener {

        /***
         * 一直滑動不斷變化的seekBar時,觸發
         *
         * @param seekBar
         * @param progress
         * @param fromUser
         */
        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            Log.d(TAG, "Changed progress " + progress);
            Log.d(TAG, "Changed getProgress()  " + seekBar.getProgress());
            tvPlayingTime.setText(getTime(seekBar.getProgress() / 1000));
        }

        /**
         * 使用者開始滑動seekBar時觸發
         *
         * @param seekBar
         */
        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
            Log.d(TAG, "start getProgress()  " + seekBar.getProgress());
        }

        /**
         * 當用戶結束對滑塊的滑動時,將mediaPlayer播放位置設為滑塊結束對應位置
         *
         * @param seekBar
         */
        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            Log.d(TAG, "Stop getProgress()  " + seekBar.getProgress());
            currentPosition = seekBar.getProgress();
            mediaPlayer.seekTo(currentPosition);
            tvPlayingTime.setText(getTime(seekBar.getProgress() / 1000));
        }
    }

    /*
     * 當SurfaceView所在的Activity離開了前臺,SurfaceView會被destroy,
     * 當Activity又回到了前臺時,SurfaceView會被重新建立,並且是在OnResume()方法之後被建立
     */
    private class SurfaceCallback implements SurfaceHolder.Callback {
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        }

        /**
         * 建立SurfaceView時開始從上次位置播放或重新播放
         *
         * @param holder
         */
        @SuppressLint("NewApi")
        public void surfaceCreated(SurfaceHolder holder) {
//            audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
//            maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);// 取得最大音量
//            curVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);// 獲取當前音量
            // 建立SurfaceHolder的時候,如果存在上次播放的位置,則按照上次播放位置進行播放
            play("/sdcard/basketball.mp4");
        }

        /**
         * 離開SurfaceView時停止播放,儲存播放位置
         */
        public void surfaceDestroyed(SurfaceHolder holder) {
            // 隱藏view的時候銷燬SurfaceHolder的時候記錄當前的播放位置並停止播放
            if (mediaPlayer != null && mediaPlayer.isPlaying()) {
                currentPosition = mediaPlayer.getCurrentPosition();
                mediaPlayer.stop();
                updateSeekBarHandler.removeMessages(0);
            }
        }
    }

    /**
     * 開始播放
     *
     * @param playUrl 播放視訊的地址
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    protected void play(String playUrl) {
        // 獲取視訊檔案地址:放在sd卡根目錄下的視訊地址,這個地址也支援網路地址:比如http://...
//        String path = "storage/sdcard0/test.mp4";
        // 獲取視訊檔案地址
        String path = etPath.getText().toString().trim();
//        String path = "/sdcard/basketball.mp4";
        File file = new File(path);
        if (!file.exists()) {
            Toast.makeText(this, "視訊檔案路徑錯誤或忘了加sd卡許可權了", Toast.LENGTH_LONG).show();
            return;
        }
        path = file.getAbsolutePath();
        Log.d(TAG, "path = " + path);
        try {
            //mediaPlayer= new MediaPlayer(); 這個初始化不能放在這裡,因為surfaceView可能會多次執行,導致mediaPlayer重新被初始化,由於監聽器用的是同一個導致回撥的時候會出現不適同一個mediaplayer的情況
            //此方法建立同步載入,直接start
//            mediaPlayer = MediaPlayer.create(this, Uri.parse(path), null);
            mediaPlayer.reset();
            // 設定播放的視訊源
            mediaPlayer.setDataSource(path);
            // 設定顯示視訊的SurfaceHolder
            mediaPlayer.setDisplay(surfaceView.getHolder());
            mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            mediaPlayer.setOnPreparedListener(mediaPlayerListener);
            mediaPlayer.setOnCompletionListener(mediaPlayerListener);
            mediaPlayer.setOnInfoListener(mediaPlayerListener);
            mediaPlayer.setOnErrorListener(mediaPlayerListener);
            mediaPlayer.setOnBufferingUpdateListener(mediaPlayerListener);
            mediaPlayer.setOnSeekCompleteListener(mediaPlayerListener);
            mediaPlayer.prepareAsync();// 緩衝,裝載
//            mediaPlayer.start();

        } catch (Exception e) {
            e.printStackTrace();
        }

    }



    /**
     * 內部訊息佇列類,主要獲取updateThread發來的CurrentPosition和MaxPosition設定給SeekBar
     */
    private Handler updateSeekBarHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            Log.d(TAG, "handleMessage curPosition = " + mediaPlayer.getCurrentPosition());
            videoSeekBar.setProgress(mediaPlayer.getCurrentPosition());
            tvPlayingTime.setText(getTime(mediaPlayer.getCurrentPosition() / 1000));
            sendEmptyMessageDelayed(0, 1000);
        }
    };

    private MediaPlayerListener mediaPlayerListener = new MediaPlayerListener();

    private class MediaPlayerListener implements MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnInfoListener
            , MediaPlayer.OnErrorListener, MediaPlayer.OnBufferingUpdateListener, MediaPlayer.OnSeekCompleteListener {

        /**
         * 視訊裝載完成,可以播放
         *
         * @param mp
         */
        public void onPrepared(MediaPlayer mp) {

            Log.d(TAG, "裝載完成");
            Log.d(TAG, "裝載完成 media same ? "+(mp==mediaPlayer));
            mediaPlayer.seekTo(currentPosition);
            mp.start();
            // 按照初始位置播放
            // 設定進度條的最大進度為視訊流的最大播放時長
            videoSeekBar.setMax(mediaPlayer.getDuration());
            tvVideoTotalTime.setText(getTime(mediaPlayer.getDuration() / 1000));
            //更新seekBar進度
            updateSeekBarHandler.sendEmptyMessageDelayed(0, 1000);
            // 開始執行緒,更新進度條的刻度
            int current = mediaPlayer.getCurrentPosition();
            videoSeekBar.setProgress(current);
        }

        /**
         * 在播放結束被回撥
         *
         * @param mp
         */
        @Override
        public void onCompletion(MediaPlayer mp) {
            Log.d(TAG, "onCompletion " + mp.getCurrentPosition());
            mediaPlayer.seekTo(currentPosition);
        }

        /**
         * 播放視訊,處理視訊過程的視訊資訊
         *
         * @param mp
         * @param what
         * @param extra
         * @return
         */
        @Override
        public boolean onInfo(MediaPlayer mp, int what, int extra) {
            Log.d(TAG, "---------onInfo------what---" + what + "--extra--" + extra);
            if (MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START == what) {
                //緩衝視訊開始
            } else if (MediaPlayer.MEDIA_INFO_BUFFERING_START == what) {
                //網路連線異常
            } else if (MediaPlayer.MEDIA_INFO_BUFFERING_END == what) {
                //緩衝完成
            } else if (MediaPlayer.MEDIA_INFO_METADATA_UPDATE == what) {
                Log.d(TAG, "---------MEDIA_INFO_METADATA_UPDATE---------");
            }
            return false;
        }


        /***
         * 播放出錯
         *
         * @param mp
         * @param what
         * @param extra
         * @return
         */
        @Override
        public boolean onError(MediaPlayer mp, int what, int extra) {
            // 發生錯誤重新播放
//            play("");
            Log.d(TAG, "onError");
            if (what == MediaPlayer.MEDIA_ERROR_SERVER_DIED ||
                    what == MediaPlayer.MEDIA_ERROR_TIMED_OUT) {// 網路連線異常
                return true;
            }
            return false;
        }

        @Override
        public void onBufferingUpdate(MediaPlayer mp, int percent) {
            Log.d(TAG, "percent = " + percent);
        }

        /***
         *
         * seekTo()是定位方法,可以讓播放器從指定的位置開始播放,需要注意的是該方法是個非同步方法,
         * 也就是說該方法返回時並不意味著定位完成,尤其是播放的網路檔案,
         * 真正定位完成時會觸發OnSeekComplete.onSeekComplete(),
         * 如果需要是可以呼叫setOnSeekCompleteListener(OnSeekCompleteListener)設定監聽器來處理的。
         * @param mp
         */
        @Override
        public void onSeekComplete(MediaPlayer mp) {
            Log.d(TAG, "---------onSeekComplete---------");
            mp.start();
        }
    }

    /**
     * 秒轉換成00:00
     *
     * @param ms
     * @return
     */
    private String getTime(int ms) {
        try {
            return appendStrs(ms / 60 + "") + ":" + appendStrs(ms % 60 + "");
        } catch (Exception e) {
        }
        return "00:00";
    }

    /**
     * 字串前面補加0
     *
     * @param str
     * @return
     */
    private String appendStrs(String str) {
        String appendStr = "00";
        if (null == str || str.trim().isEmpty()) {
            return appendStr;
        }
        if (str.length() == appendStr.length()) {
            return str;
        }
        return appendStr.substring(0, appendStr.length() - str.length()) + str;
    }
}