SurfaceView+MediaPlayer封裝之路

SurfaceView+MediaPlayer封裝之路

我的播放器叫做JsPlayer阅嘶,喜歡的話睛琳,就給個star嘍_https://github.com/shuaijia/JsPlayer

這里我只介紹播放器封裝思路脖咐,會貼出部分代碼断箫,如果大家想查看完整代碼瓜浸,可以去github查看澳淑,有不清楚或錯誤或改進的地方,可以issues 我插佛!

寫在之前

先上效果圖:(1.5版本新增彈幕功能

這里寫圖片描述
這里寫圖片描述
這里寫圖片描述

為什么要用SurfaceView

它繼承自類View杠巡,因此它本質(zhì)上是一個View。但與普通View不同的是雇寇,它有自己的Surface氢拥。而SurfaceView自帶一個Surface,這個Surface在WMS中有自己對應(yīng)的WindowState锨侯,在SF中也會有自己的Layer嫩海。雖然在App端它仍在View hierachy中,但在Server端(WMS和SF)中囚痴,它與宿主窗口是分離的叁怪。這樣的好處是對這個Surface的渲染可以放到單獨線程去做,渲染時可以有自己的GL context深滚。這對于一些游戲奕谭、視頻等性能相關(guān)的應(yīng)用非常有益,因為它不會影響主線程對事件的響應(yīng)痴荐。

SurfaceView內(nèi)部自己持有surface血柳,surface 創(chuàng)建、銷毀生兆、大小改變時系統(tǒng)來處理的难捌,通過surfaceHolder 的callback回調(diào)通知。當畫布創(chuàng)建好時鸦难,可以將surface綁定到MediaPlayer中栖榨。SurfaceView如果為用戶可見的時候,創(chuàng)建SurfaceView的SurfaceHolder用于顯示視頻流解析的幀圖片明刷,如果發(fā)現(xiàn)SurfaceView變?yōu)橛脩舨豢梢姷臅r候婴栽,則立即銷毀SurfaceView的SurfaceHolder,以達到節(jié)約系統(tǒng)資源的目的辈末。

關(guān)于更多SurfaceView的介紹愚争,可參考我寫的另一片文章:http://blog.csdn.net/jiashuai94/article/details/77882644

MediaPlayer

MediaPlayer其實是一個封裝的很好的音頻映皆、視頻流媒體操作類,如果查看其源碼轰枝,會發(fā)現(xiàn)其內(nèi)部是調(diào)用的native方法捅彻,所以它其實是有C++實現(xiàn)的。既然是一個流媒體操作類鞍陨,那么必然涉及到步淹,播放、暫停诚撵、停止等操作缭裆,實際上MediaPlayer也為我們提供了相應(yīng)的方法來直接操作流媒體。

  • void statr():開始或恢復播放寿烟。
  • void stop():停止播放澈驼。
  • void pause():暫停播放。
  • void setDataSource(String path):通過一個媒體資源的地址指定MediaPlayer的數(shù)據(jù)源筛武,這里的path可以是一個本地路徑缝其,也可以是網(wǎng)絡(luò)路徑。

當然還有其他很多的方法徘六,例如獲取視頻時長内边、獲取當前位置、定位到某個位置等等方法待锈,就不再一一列舉漠其,閱讀JsPlayer的源碼便會有所了解。

播放器結(jié)構(gòu)

這里寫圖片描述

UML圖

這里寫圖片描述

已經(jīng)對SurfaceView+MediaPlayer封裝視屏播放器有了大致的了解炉擅,接下來就開始視屏播放器的封裝之旅吧辉懒!

1、工具類

工欲善其事谍失,必先利其器眶俩!

想封裝結(jié)構(gòu)清晰,使用方便的視頻播放器快鱼,工具類是少不了的颠印!JsPlayer主要用了以下幾個工具類:

  • DisplayUtils
  • NetworkUtils
  • StringUtils

DisplayUtils:負責界面展示相關(guān)工具,例如px抹竹、dp线罕、sp的相互轉(zhuǎn)換;獲取屏幕寬高度窃判;切換橫屏钞楼、豎屏等;

NetworkUtils:判斷手機是否聯(lián)網(wǎng)袄琳;是否為wifi询件;是否是流量燃乍;網(wǎng)絡(luò)狀態(tài)等;

StringUtils:主要將long型毫秒轉(zhuǎn)換為時間格式的字符串宛琅。
代碼就不貼了刻蟹,很簡單。大家想了解嘿辟,去github中查看吧舆瘪。


2、實體類

為了在使用視頻播放器時規(guī)范傳入的數(shù)據(jù)红伦,同時也方便使用者調(diào)用和封裝英古,故定義了視頻詳情的接口:其包含兩個抽象方法,分別返回視頻地址和視頻標題色建。

/**
 * 視頻數(shù)據(jù)類
 * 請實現(xiàn)本接口
 */
public interface IVideoInfo extends Serializable {

    /**
     * 視頻標題
     */
    String getVideoTitle();

    /**
     * 視頻播放路徑(本地或網(wǎng)絡(luò))
     */
    String getVideoPath();

}

用戶可根據(jù)項目實際情況對其進行擴展(需實現(xiàn)此接口即可)哺呜,比如默認圖地址舌缤,點贊數(shù)箕戳,是否購買,彈幕信息等等国撵。但視頻標題和視頻地址必須返回陵吸!


3、回調(diào)相關(guān)

大家都知道介牙,VideoView或其他視頻播放器在使用時壮虫,有準備好監(jiān)聽、播放完成監(jiān)聽环础、錯誤監(jiān)聽等等囚似,可供開發(fā)者在對應(yīng)情況進行對應(yīng)處理;而且我們有時也需要在用戶點擊播放暫停线得、全屏饶唤、拖動進度條等情況下獲得操作回調(diào)。因此贯钩,我們封裝了兩個回調(diào)接口:

  • OnVideoControlListener:視頻控制回調(diào)
  • OnPlayerCallback:視頻狀態(tài)回調(diào)
/**
 * 視頻控制監(jiān)聽
 */
public interface OnVideoControlListener {

    /**
     * 開始播放按鈕
     */
    void onStartPlay();

    /**
     * 返回
     */
    void onBack();

    /**
     * 全屏
     */
    void onFullScreen();

    /**
     * 錯誤后的重試
     */
    void onRetry(int errorStatus);

}
/**
 * 視頻操作回調(diào)募狂,是將系統(tǒng)MediaPlayer的常見回調(diào)封裝
 */
public interface OnPlayerCallback {

    /**
     * 準備好
     */
    void onPrepared(MediaPlayer mp);

    /**
     * 視頻size變化
     */
    void onVideoSizeChanged(MediaPlayer mp, int width, int height);

    /**
     * 緩存更新變化
     *
     * @param percent 緩沖百分比
     */
    void onBufferingUpdate(MediaPlayer mp, int percent);

    /**
     * 播放完成
     */
    void onCompletion(MediaPlayer mp);

    /**
     * 視頻錯誤
     * @param what  錯誤類型
     * @param extra 特殊錯誤碼
     */
    void onError(MediaPlayer mp, int what, int extra);

    /**
     * 視頻加載狀態(tài)變化
     *
     * @param isShow 是否顯示loading
     */
    void onLoadingChanged(boolean isShow);

    /**
     * 視頻狀態(tài)變化
     */
    void onStateChanged(int curState);
}

當然了,各位使用上述兩個回調(diào)時角雷,必須先實現(xiàn)祸穷、再使用,當然也可以基于它拓展了勺三!


4雷滚、自定義view

關(guān)于播放器中涉及到的、需要自定義的view主要有手勢調(diào)節(jié)進度吗坚、音量祈远、亮度時的彈框胯舷、控制器界面、錯誤界面绊含。

當然我們的JsPlayer視頻播放器也是一自定義view桑嘶,其手勢控制也封裝了一個view,這些我們稍后會詳細介紹躬充。

  • JsVideoProgressOverlay: 調(diào)節(jié)進度 框
  • JsVideoSystemOverlay: 調(diào)節(jié)音量逃顶、亮度 框
  • JsVideoErrorView: 錯誤界面
  • JsVideoControllerView: 控制器

我的思路是這樣的:將錯誤界面JsVideoErrorView再封裝到控制器中JsVideoControllerView,這樣便于在出錯時的處理充甚;而調(diào)節(jié)進度等彈框以政、控制器,當然還有SurfaceView伴找,加載中等盈蛮,它們會一同封裝到視頻播放器JsPlayer的自定義View中。

JsVideoProgressOverlay

這里寫圖片描述
/**
 * 滑動快進快退進度框
 */
public class JsVideoProgressOverlay extends FrameLayout {

    private ImageView mSeekIcon;
    private TextView mSeekCurProgress;
    private TextView mSeekDuration;

    private int mDuration = -1;
    private int mDelSeekDialogProgress = -1;
    private int mSeekDialogStartProgress = -1;

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

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

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

    private void init() {
        LayoutInflater.from(getContext()).inflate(R.layout.video_overlay_progress, this);

        mSeekIcon = (ImageView) findViewById(R.id.iv_seek_direction);
        mSeekCurProgress = (TextView) findViewById(R.id.tv_seek_current_progress);
        mSeekDuration = (TextView) findViewById(R.id.tv_seek_duration);
    }

    /**
     * 顯示進度框
     *
     * @param delProgress 進度變化值
     * @param curPosition player當前進度
     * @param duration    player總長度
     */
    public void show(int delProgress, int curPosition, int duration) {
        if (duration <= 0) return;

        // 獲取第一次顯示時的開始進度
        if (mSeekDialogStartProgress == -1) {
            Log.i("DDD", "show: start seek = " + mSeekDialogStartProgress);
            mSeekDialogStartProgress = curPosition;
        }

        if (getVisibility() != View.VISIBLE) {
            setVisibility(View.VISIBLE);
        }

        mDuration = duration;
        mDelSeekDialogProgress -= delProgress;
        int targetProgress = getTargetProgress();

        if (delProgress > 0) {
            // 回退
            mSeekIcon.setImageResource(R.mipmap.ic_video_back);
        } else {
            // 前進
            mSeekIcon.setImageResource(R.mipmap.ic_video_speed);
        }
        mSeekCurProgress.setText(StringUtils.stringForTime(targetProgress));
        mSeekDuration.setText(StringUtils.stringForTime(mDuration));
    }

    /**
     * 獲取滑動結(jié)束后的目標進度
     */
    public int getTargetProgress() {
        if (mDuration == -1) {
            return -1;
        }

        int newSeekProgress = mSeekDialogStartProgress + mDelSeekDialogProgress;
        if (newSeekProgress <= 0) newSeekProgress = 0;
        if (newSeekProgress >= mDuration) newSeekProgress = mDuration;
        return newSeekProgress;
    }

    public void hide() {
        mDuration = -1;
        mSeekDialogStartProgress = -1;
        mDelSeekDialogProgress = -1;
        setVisibility(GONE);
    }

}

調(diào)節(jié)系統(tǒng)屬性彈框JsVideoSystemOverlay就不再貼出代碼了技矮,與上類似抖誉,這里我們只分享設(shè)計思路。

注意:

  • mDelSeekDialogProgress -= delProgress衰倦,因為向右滑動時傳進來的delProgress是負數(shù)袒炉、向左滑動是正數(shù),所以這里計算變化時是在自減樊零。
  • if (newSeekProgress <= 0) newSeekProgress = 0;
    if (newSeekProgress >= mDuration) newSeekProgress = mDuration;
    做了邊界控制我磁,防止計算出的數(shù)據(jù)超出范圍而導致出錯。

JsVideoErrorView

這里寫圖片描述

從界面來看很簡單了驻襟!

定義所有錯誤碼常量(可換為枚舉):

// 正常狀態(tài)
public static final int STATUS_NORMAL = 0;
// 普通一場
public static final int STATUS_VIDEO_DETAIL_ERROR = 1;
// 資源錯誤
public static final int STATUS_VIDEO_SRC_ERROR = 2;
// 無WIFI
public static final int STATUS_UN_WIFI_ERROR = 3;
// 無網(wǎng)絡(luò)
public static final int STATUS_NO_NETWORK_ERROR = 4;

另外就是顯示的控制:

switch (status) {
    case STATUS_VIDEO_DETAIL_ERROR:
        video_error_info.setText("視頻加載失敗");
        video_error_retry.setText("點此重試");
        break;
    case STATUS_VIDEO_SRC_ERROR:
        video_error_info.setText("視頻加載失敗");
        video_error_retry.setText("點此重試");
        break;
    case STATUS_NO_NETWORK_ERROR:
        video_error_info.setText("網(wǎng)絡(luò)連接異常夺艰,請檢查網(wǎng)絡(luò)設(shè)置后重試");
        video_error_retry.setText("重試");
        break;
    case STATUS_UN_WIFI_ERROR:
        video_error_info.setText("溫馨提示:您正在使用非WiFi網(wǎng)絡(luò),播放將產(chǎn)生流量費用");
        video_error_retry.setText("繼續(xù)播放");
        break;
}

注意:對重試按鈕的點擊事件:錯誤view內(nèi)置了視頻控制回調(diào)OnVideoControlListener沉衣,點擊重試時執(zhí)行回調(diào)的重試按鈕郁副。

JsVideoControllerView

先看布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include
        android:id="@+id/video_controller_bottom"
        layout="@layout/video_controller_bottom" />

    <ImageView
        android:id="@+id/player_lock_screen"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_marginLeft="10dp"
        android:src="@mipmap/video_unlock"
        android:visibility="gone" />

    <com.jia.jsplayer.view.JsVideoErrorView
        android:id="@+id/video_controller_error"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <include
        android:id="@+id/video_controller_title"
        layout="@layout/video_controller_title" />

    <ImageView
        android:id="@+id/video_back"
        android:layout_width="32dp"
        android:layout_height="44dp"
        android:layout_alignTop="@id/video_controller_title"
        android:padding="12dp"
        android:scaleType="fitCenter"
        android:src="@mipmap/ic_back_white" />

    <RelativeLayout
        android:id="@+id/rl_pre"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#000"
        android:visibility="gone">
        <ImageView
            android:id="@+id/iv_pre_play"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_video_play"
            android:layout_centerInParent="true"/>
    </RelativeLayout>
</RelativeLayout>

能夠看出:主要分為底部控制部分(播放按鈕、當前位置厢蒜、總時長霞势、進度條),頭部控制部分(返回鍵斑鸦、標題)愕贡,出錯界面、鎖屏按鈕和填充全屏的默認圖巷屿。

對控制器來說固以,我們應(yīng)該關(guān)心這些:


這里寫圖片描述

首先我們必須傳入MediaPlayer對象(關(guān)于它的封裝稍后會詳細介紹),因為我們各點擊事件和拖動事件都在控制MediaPlayer,如播放按鈕的點擊事件憨琳,在控制視頻的播放與暫停诫钓,進度條拖動完時,應(yīng)控制視頻定位 等篙螟。

注意:

  • 對UI的更新全部提出方法菌湃,方便其他地方調(diào)用
  • 是否鎖屏、控制器顯示時長等都應(yīng)有默認值
  • 顯示控制器時遍略,視頻當前位置和更新的精度
  • 對全屏按鈕惧所、重試按鈕的點擊交給OnVideoControlListener來做

我想著重講一下控制器的隱藏和顯示

1、控制器一顯示绪杏,就獲取MediaPlayer的當前位置下愈,更新UI(進度條,當前播放位置)蕾久,并將當前位置返回:

    /**
     * 設(shè)置進度势似,同時也返回進度
     *
     * @return
     */
    private int setProgress() {
        if (mPlayer == null || mDragging) {
            return 0;
        }
        int position = mPlayer.getCurrentPosition();
        int duration = mPlayer.getDuration();
        if (mPlayerSeekBar != null) {
            if (duration > 0) {
                // use long to avoid overflow
                long pos = 1000L * position / duration;
                mPlayerSeekBar.setProgress((int) pos);
            }
            // 設(shè)置緩沖進度
            int percent = mPlayer.getBufferPercentage();
            mPlayerSeekBar.setSecondaryProgress(percent * 10);
        }

        mVideoProgress.setText(StringUtils.stringForTime(position));
        mVideoDuration.setText(StringUtils.stringForTime(duration));

        return position;
    }

2、控制各UI布局顯示僧著,開始發(fā)送消息

    /**
     * 顯示控制器
     *
     * @param timeout 顯示時長
     */
    public void show(int timeout) {
        setProgress();

        if (!isScreenLock) {
            mControllerBack.setVisibility(VISIBLE);
            mControllerTitle.setVisibility(VISIBLE);
            mControllerBottom.setVisibility(VISIBLE);
        } else {
            if (!DisplayUtils.isPortrait(getContext())) {
                mControllerBack.setVisibility(GONE);
            }
            mControllerTitle.setVisibility(GONE);
            mControllerBottom.setVisibility(GONE);
        }

        if (!DisplayUtils.isPortrait(getContext())) {
            mScreenLock.setVisibility(VISIBLE);
        }

        mShowing = true;

        updatePausePlay();

        // 開始顯示
        post(mShowProgress);

        if (timeout > 0) {
            // 先移除之前的隱藏異步操作
            removeCallbacks(mFadeOut);
            //timeout后隱藏
            postDelayed(mFadeOut, timeout);
        }
    }
    /**
     * 異步操作隱藏
     */
    private final Runnable mFadeOut = new Runnable() {
        @Override
        public void run() {
            hide();
        }
    };

    /**
     * 異步操作顯示
     */
    private final Runnable mShowProgress = new Runnable() {
        @Override
        public void run() {
            int pos = setProgress();
            if (!mDragging && mShowing && mPlayer.isPlaying()) {
                // 解決1秒之內(nèi)的誤差履因,使得發(fā)送消息正好卡在整秒
                Log.e("TAG", "run: " + (1000 - (pos % 1000)));
                postDelayed(mShowProgress, 1000 - (pos % 1000));
            }
        }
    };
  • 首先注意,每當開始發(fā)送消息霹抛,都應(yīng)強制將之前的消息全部移除搓逾;
  • 發(fā)送兩個消息:一個是計時的消息卷谈,每隔大約一秒獲取當前位置并且更新UI杯拐,另一個是延遲顯示時長后隱藏控制器;
  • 為什么每隔大約1秒更新一次UI呢世蔗,postDelayed(mShowProgress, 1000 - (pos % 1000)); 我做了一個修正操作端逼,因為各消息可能會互相影響,其次就是發(fā)送消息時沒有卡在視頻的整秒位置上污淋,而我們確實整1秒發(fā)送一條消息顶滩,會導致誤差!

如果大家還想了解其他功能寸爆,可以去github閱讀我的源碼https://github.com/shuaijia/JsPlayer


5礁鲁、MediaPlayer封裝

主要封裝了

  • openVideo:播放視頻,處理各回調(diào)
  • start:開始播放
  • pause:暫停播放
  • seekTo:定位到
  • reset:視頻重置
  • stop:停止播放
  • isPlaying:是否正在播放
  • getDuration:獲取總時長
  • getCurrentPosition:獲取當前進度
  • getBufferPercentage:獲取緩沖進度 等

定義了視頻播放的所用狀態(tài)值常量

    //出錯狀態(tài)
    public static final int STATE_ERROR = -1;
    //通常狀態(tài)
    public static final int STATE_IDLE = 0;
    //視頻正在準備
    public static final int STATE_PREPARING = 1;
    //視頻已經(jīng)準備好
    public static final int STATE_PREPARED = 2;
    //視頻正在播放
    public static final int STATE_PLAYING = 3;
    //視頻暫停
    public static final int STATE_PAUSED = 4;
    //視頻播放完成
    public static final int STATE_PLAYBACK_COMPLETED = 5;
    // 播放核心使用MediaPlayer
    private MediaPlayer player;
    // 當前狀態(tài)
    private int curState = STATE_IDLE;
    // 當前緩沖進度
    private int currentBufferPercentage;
    // *視頻路徑
    private String path;

    // 播放監(jiān)聽
    private OnPlayerCallback onPlayerListener;
    // 播放視頻承載的view
    private SurfaceHolder surfaceHolder;

封裝了視頻播放狀態(tài)的判斷

    public boolean isInPlaybackState() {
        return (player != null &&
                curState != STATE_ERROR &&
                curState != STATE_IDLE &&
                curState != STATE_PREPARING);
    }

此方法會在其他的所有方法執(zhí)行之前判斷赁豆,如果返回false仅醇,則不進行開始播放、重新播放魔种、拖動定位等操作析二。

同時這些操作執(zhí)行完后都會更新當前播放狀態(tài),防止視頻不能播的情況下操作報錯。如

    /**
     * 開始播放
     */
    public void start() {
        if (isInPlaybackState()) {
            player.start();
            setCurrentState(STATE_PLAYING);
        }
    }

在openVideo中:

    public void openVideo() {
        if (path == null || surfaceHolder == null) {
            return;
        }

        reset();

        player = new MediaPlayer();

        // 準備好的監(jiān)聽
        player.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                //因為后面播放時要判斷當前視頻狀態(tài)叶摄,所以在此一定要先將狀態(tài)改變?yōu)镾TATE_PREPARED
                //即已經(jīng)準備好属韧,否則在第一次打開視頻時無法自動播放
                setCurrentState(STATE_PREPARED);
                if (onPlayerListener != null) {
                    onPlayerListener.onPrepared(mp);
                }

            }
        });
        // 緩沖監(jiān)聽
        player.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
            @Override
            public void onBufferingUpdate(MediaPlayer mp, int percent) {
                if (onPlayerListener != null) {
                    onPlayerListener.onBufferingUpdate(mp, percent);
                }
                currentBufferPercentage = percent;
            }
        });
        // 播放完成監(jiān)聽
        player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mp) {
                if (onPlayerListener != null) {
                    onPlayerListener.onCompletion(mp);
                }
                setCurrentState(STATE_PLAYBACK_COMPLETED);
            }
        });
        // 信息監(jiān)聽
        player.setOnInfoListener(new MediaPlayer.OnInfoListener() {
            @Override
            public boolean onInfo(MediaPlayer mp, int what, int extra) {
                if (onPlayerListener != null) {
                    // 701 加載中
                    if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) {
                        onPlayerListener.onLoadingChanged(true);
                        // 702 加載完成
                    } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) {
                        onPlayerListener.onLoadingChanged(false);
                    }
                }
                return false;
            }
        });
        // 出錯監(jiān)聽
        player.setOnErrorListener(onErrorListener);
        // 視頻大小切換監(jiān)聽
        player.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
            @Override
            public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
                if (onPlayerListener != null) {
                    onPlayerListener.onVideoSizeChanged(mp, width, height);
                }
            }
        });

        currentBufferPercentage = 0;
        try {
            /**
             * 在這里開始真正的播放
             */
            player.setDataSource(path);
            player.setDisplay(surfaceHolder);
            player.setAudioStreamType(AudioManager.STREAM_MUSIC);
            player.setScreenOnWhilePlaying(true);
            player.prepareAsync();
            Log.e(TAG, "openVideo: " );
            setCurrentState(STATE_PREPARING);
        } catch (Exception e) {
            Log.e(TAG, "openVideo: " + e.toString());
            setCurrentState(STATE_ERROR);
            onErrorListener.onError(player, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
        }
    }

openVideo就是播放視頻的核心方法:新建MediaPlayer對象徽鼎;將視頻播放的各回調(diào)交給OnPlayerCallback處理扼劈;將外部傳進來的SurfaceHolder設(shè)置給MediaPlayer,并且prepareAsync之后就可以播放了碎罚,當然会傲,不要忘了更新狀態(tài)樊破!

SurfaceHolder是surface的抽象接口,使你可以控制surface的大小和格式唆铐, 以及在surface上編輯像素哲戚,和監(jiān)視surace的改變。
SurfaceHolder用于顯示視頻流解析的幀圖片艾岂,如果發(fā)現(xiàn)SurfaceView變?yōu)橛脩舨豢梢姷臅r候顺少,則立即銷毀SurfaceView的SurfaceHolder,以達到節(jié)約系統(tǒng)資源的目的王浴。


6脆炎、手勢控制

說到手勢控制,主要是手勢控制視頻進度氓辣,手勢控制音量和屏幕亮度秒裕。

對于手勢控制,我自定義了BehaviorView:讓其實現(xiàn)GestureDetector的OnGestureListener

public class VideoBehaviorView extends FrameLayout implements GestureDetector.OnGestureListener{

在此view中定義以下方法钞啸,實現(xiàn)更新UI几蜻,交由子類去復寫

    // 更新進度UI,由子類重寫
    protected void updateSeekUI(int delProgress) {
        // sub
    }

    // 更新音量UI体斩,由子類重寫
    protected void updateVolumeUI(int max, int progress) {
        // sub
    }

    // 更新亮度UI梭稚,由子類重寫
    protected void updateLightUI(int max, int progress) {
        // sub
    }

我的思路是將view的觸摸事件全部交給GestureDetector處理

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_OUTSIDE:
            case MotionEvent.ACTION_CANCEL:
                endGesture(mFingerBehavior);
                break;
        }
        return true;
    }

當手指按下時,重置手指行為絮吵,獲取當前音量弧烤、亮度

    @Override
    public boolean onDown(MotionEvent e) {
        //重置 手指行為
        mFingerBehavior = -1;
        mCurrentVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC);
        try {
            mCurrentBrightness = (int) (activity.getWindow().getAttributes().screenBrightness * mMaxBrightness);
        } catch (Exception exception) {
            exception.printStackTrace();
        }
        return false;
    }

在onScroll方法中:

判斷決定當前為何種類型手勢:左右滑動為調(diào)節(jié)進度,左半屏上下滑動為調(diào)節(jié)亮度蹬敲,右半屏上下滑動為調(diào)節(jié)音量

        /**
         * 根據(jù)手勢起始2個點斷言 后續(xù)行為. 規(guī)則如下:
         *  屏幕切分為:
         *  1.左右扇形區(qū)域為視頻進度調(diào)節(jié)
         *  2.上下扇形區(qū)域 左半屏亮度調(diào)節(jié) 后半屏音量調(diào)節(jié).
         */
        if (mFingerBehavior < 0) {
            float moveX = e2.getX() - e1.getX();
            float moveY = e2.getY() - e1.getY();
            // 如果橫向滑動距離大于縱向滑動距離暇昂,則認為在調(diào)節(jié)進度
            if (Math.abs(moveX) >= Math.abs(moveY))
                mFingerBehavior = FINGER_BEHAVIOR_PROGRESS;
                // 否則為調(diào)節(jié)音量或亮度
                // 按下位置在屏幕左半邊,則是調(diào)節(jié)亮度
            else if (e1.getX() <= width / 2) mFingerBehavior = FINGER_BEHAVIOR_BRIGHTNESS;
                // 按下位置在屏幕右半邊伴嗡,則是在調(diào)節(jié)音量
            else mFingerBehavior = FINGER_BEHAVIOR_VOLUME;
        }

手勢處理

        switch (mFingerBehavior) {
            case FINGER_BEHAVIOR_PROGRESS: { // 進度變化
                // 默認滑動一個屏幕 視頻移動八分鐘.
                int delProgress = (int) (1.0f * distanceX / width * 480 * 1000);
                // 更新快進彈框
                updateSeekUI(delProgress);
                break;
            }
            case FINGER_BEHAVIOR_VOLUME: { // 音量變化
                float progress = mMaxVolume * (distanceY / height) + mCurrentVolume;

                // 控制調(diào)節(jié)臨界范圍
                if (progress <= 0) progress = 0;
                if (progress >= mMaxVolume) progress = mMaxVolume;

                am.setStreamVolume(AudioManager.STREAM_MUSIC, Math.round(progress), 0);
                updateVolumeUI(mMaxVolume, Math.round(progress));
                // 更新當前值
                mCurrentVolume = progress;
                break;
            }
            case FINGER_BEHAVIOR_BRIGHTNESS: { // 亮度變化
                try {
                    // 如果系統(tǒng)亮度為自動調(diào)節(jié)急波,則改為手動調(diào)節(jié)
                    if (Settings.System.getInt(getContext().getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE)
                            == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC) {
                        Settings.System.putInt(getContext().getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE,
                                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
                    }

                    int progress = (int) (mMaxBrightness * (distanceY / height) + mCurrentBrightness);

                    // 控制調(diào)節(jié)臨界范圍
                    if (progress <= 0) progress = 0;
                    if (progress >= mMaxBrightness) progress = mMaxBrightness;

                    Window window = activity.getWindow();
                    WindowManager.LayoutParams params = window.getAttributes();
                    params.screenBrightness = progress / (float) mMaxBrightness;
                    window.setAttributes(params);

                    updateLightUI(mMaxBrightness, progress);
                    // 更新當前值
                    mCurrentBrightness = progress;
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;
            }
        }

注意:

  • 所有的更新UI操作全部交由子類實現(xiàn)
  • 注意臨界范圍的控制
  • 控制進度時,百分比最后乘以8分鐘闹究,以達到較為適中的用戶體驗幔崖,防止視頻時長過大或太小情況下,拖動調(diào)節(jié)進度變化太過明顯或效果不明顯。

7赏寇、播放器JsPlayer封裝

先來看看布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

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

    <com.jia.jsplayer.view.JsVideoControllerView
        android:id="@+id/video_controller"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <include
        android:id="@+id/video_loading"
        layout="@layout/video_controller_loading" />

    <com.jia.jsplayer.view.JsVideoSystemOverlay
        android:id="@+id/video_system_overlay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:visibility="gone"/>

    <com.jia.jsplayer.view.JsVideoProgressOverlay
        android:id="@+id/video_progress_overlay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:visibility="gone"/>

</RelativeLayout>

JsPlayer視頻播放器集成自上一步中的VideoBehaviorView吉嫩,注意復寫VideoBehaviorView的更新UI方法。

    private SurfaceView surfaceView;
    private View loadingView;
    private JsVideoProgressOverlay progressView;
    private JsVideoSystemOverlay systemView;
    private JsVideoControllerView mediaController;

    private JsMediaPlayer mMediaPlayer;

內(nèi)置封裝過得JsMediaPlayer 對象嗅定,控制器自娩、和SurfaceView,還有網(wǎng)絡(luò)狀態(tài)廣播接收器渠退。

初始化player忙迁,創(chuàng)建JsMediaPlayer對象,設(shè)置視頻播放回調(diào)處理碎乃,然后將其設(shè)置給ControllerView姊扔。

注意:

  • 在準備好的監(jiān)聽中,mediaPlayer執(zhí)行開始播放梅誓,控制器展示恰梢,錯誤界面隱藏。
  • 在播放出錯時控制器檢查錯誤類型并展示
  • 在加載狀態(tài)發(fā)生改變時隱藏和展示加載中
    private void initPlayer() {
        mMediaPlayer = new JsMediaPlayer();

        // todo 這里可以優(yōu)化梗掰,將這些回調(diào)全部暴露出去
        mMediaPlayer.setOnPlayerListener(new OnPlayerCallback() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                Log.e(TAG, "onPrepared: " );
                mMediaPlayer.start();
                mediaController.show();
                mediaController.hideErrorView();
            }

            @Override
            public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {

            }

            @Override
            public void onBufferingUpdate(MediaPlayer mp, int percent) {

            }

            @Override
            public void onCompletion(MediaPlayer mp) {
                mediaController.updatePausePlay();
            }

            @Override
            public void onError(MediaPlayer mp, int what, int extra) {
                mediaController.checkShowError(false);
            }

            @Override
            public void onLoadingChanged(boolean isShow) {
                if (isShow) showLoading();
                else hideLoading();
            }

            @Override
            public void onStateChanged(int curState) {
                switch (curState) {
                    case JsMediaPlayer.STATE_IDLE:
                        am.abandonAudioFocus(null);
                        break;
                    case JsMediaPlayer.STATE_PREPARING:
                        am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
                        break;
                }
            }
        });

        mediaController.setMediaPlayer(mMediaPlayer);
    }

給SurfaceView設(shè)置Callback嵌言,返回SurfaceHolder后設(shè)置給JsMediaPlayer

        surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                Log.e(TAG, "surfaceCreated: " );
                initWidth = getWidth();
                initHeight = getHeight();

                if (mMediaPlayer != null) {
                    mMediaPlayer.setSurfaceHolder(holder);
                }
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {

            }
        });

設(shè)置路徑,開始播放

    public void setPath(final IVideoInfo video) {
        if (video == null) {
            return;
        }

        mMediaPlayer.reset();

        String videoPath = video.getVideoPath();
        mediaController.setVideoInfo(video);
        mMediaPlayer.setPath(videoPath);

    }

    public void startPlay(){
        mMediaPlayer.openVideo();
    }

更新UI

    @Override
    protected void updateSeekUI(int delProgress) {
        progressView.show(delProgress, mMediaPlayer.getCurrentPosition(), mMediaPlayer.getDuration());
    }

    @Override
    protected void updateVolumeUI(int max, int progress) {
        systemView.show(JsVideoSystemOverlay.SystemType.VOLUME, max, progress);
    }

    @Override
    protected void updateLightUI(int max, int progress) {
        systemView.show(JsVideoSystemOverlay.SystemType.BRIGHTNESS, max, progress);
    }

當然不會忘記封裝播放及穗、暫停摧茴、停止、定位埂陆、獲取總時長等等的基本方法苛白,這里就不再累贅。


8猜惋、使用

涉及到播放網(wǎng)路視頻丸氛,權(quán)限少不了

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

播放本地視頻別忘了6.0權(quán)限適配

布局中添加

代碼中

        player = (JsPlayer) findViewById(R.id.player);

        player.setOnVideoControlListener(new OnVideoControlListener() {
            @Override
            public void onStartPlay() {
                player.startPlay();
            }

            @Override
            public void onBack() {

            }

            @Override
            public void onFullScreen() {
                DisplayUtils.toggleScreenOrientation(MainActivity.this);
            }

            @Override
            public void onRetry(int errorStatus) {

            }
        });

        player.setPath(new VideoInfo("藝術(shù)人生", path));

生命周期綁定

    @Override
    protected void onStop() {
        super.onStop();
        player.onStop();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        player.onDestroy();
    }

全屏操作


    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        } else if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        }
    }

    @Override
    public void onBackPressed() {
        if (!DisplayUtils.isPortrait(this)) {
            if (!player.isLock()) {
                DisplayUtils.toggleScreenOrientation(this);
            }
        } else {
            super.onBackPressed();
        }
    }

注意所在Activity在清單文件中應(yīng)設(shè)置android:configChanges="orientation|keyboardHidden|screenSize"

這樣就ok了,播放器封裝完美完成著摔!

希望對大家有所幫助!

Android機動車.jpg

掃描二維碼定续,加入我們谍咆,獲取更多資訊!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末私股,一起剝皮案震驚了整個濱河市摹察,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌倡鲸,老刑警劉巖供嚎,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡克滴,警方通過查閱死者的電腦和手機逼争,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來劝赔,“玉大人誓焦,你說我怎么就攤上這事∽琶保” “怎么了杂伟?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長仍翰。 經(jīng)常有香客問我赫粥,道長,這世上最難降的妖魔是什么予借? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任傅是,我火速辦了婚禮,結(jié)果婚禮上蕾羊,老公的妹妹穿的比我還像新娘喧笔。我一直安慰自己,他們只是感情好龟再,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布书闸。 她就那樣靜靜地躺著,像睡著了一般利凑。 火紅的嫁衣襯著肌膚如雪浆劲。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天哀澈,我揣著相機與錄音牌借,去河邊找鬼。 笑死割按,一個胖子當著我的面吹牛膨报,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播适荣,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼现柠,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了弛矛?” 一聲冷哼從身側(cè)響起够吩,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎丈氓,沒想到半個月后周循,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體强法,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年湾笛,在試婚紗的時候發(fā)現(xiàn)自己被綠了饮怯。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡迄本,死狀恐怖硕淑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情嘉赎,我是刑警寧澤置媳,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站公条,受9級特大地震影響拇囊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜靶橱,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一寥袭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧关霸,春花似錦传黄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至佳遣,卻和暖如春识埋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背零渐。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工窒舟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人诵盼。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓惠豺,卻偏偏與公主長得像,于是被迫代替她去往敵國和親拦耐。 傳聞我的和親對象是個殘疾皇子耕腾,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容