用MediaPlayer+TextureView封裝一個完美實現(xiàn)全屏锈颗、小窗口的視頻播放器

項目已添加IjkPlayer支持,后續(xù)逐漸完善其他功能咪惠。
地址:https://github.com/xiaoyanger0825/NiceVieoPlayer

為什么使用TextureView

在Android總播放視頻可以直接使用VideoView击吱,VideoView是通過繼承自SurfaceView來實現(xiàn)的际度。SurfaceView的大概原理就是在現(xiàn)有View的位置上創(chuàng)建一個新的Window执赡,內(nèi)容的顯示和渲染都在新的Window中漂洋。這使得SurfaceView的繪制和刷新可以在單獨的線程中進(jìn)行摔吏,從而大大提高效率禽额。但是呢狂打,由于SurfaceView的內(nèi)容沒有顯示在View中而是顯示在新建的Window中泥技, 使得SurfaceView的顯示不受View的屬性控制贸宏,不能進(jìn)行平移鞋仍,縮放等變換憨奸,也不能放在其它RecyclerViewScrollView中,一些View中的特性也無法使用凿试。

TextureView是在4.0(API level 14)引入的排宰,與SurfaceView相比,它不會創(chuàng)建新的窗口來顯示內(nèi)容那婉。它是將內(nèi)容流直接投放到View中板甘,并且可以和其它普通View一樣進(jìn)行移動,旋轉(zhuǎn)详炬,縮放盐类,動畫等變化寞奸。TextureView必須在硬件加速的窗口中使用。

TextureView被創(chuàng)建后不能直接使用在跳,必須要在它被它添加到ViewGroup后枪萄,待SurfaceTexture準(zhǔn)備就緒才能起作用(看TextureView的源碼,TextureView是在繪制的時候創(chuàng)建的內(nèi)部SurfaceTexture)猫妙。通常需要給TextureView設(shè)置監(jiān)聽器SurfaceTextuListener

mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        // SurfaceTexture準(zhǔn)備就緒
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
        // SurfaceTexture緩沖大小變化
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        // SurfaceTexture即將被銷毀
        return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
        // SurfaceTexture通過updateImage更新
    }
});

SurfaceTexture的準(zhǔn)備就緒瓷翻、大小變化、銷毀割坠、更新等狀態(tài)變化時都會回調(diào)相對應(yīng)的方法齐帚。當(dāng)TextureView內(nèi)部創(chuàng)建好SurfaceTexture后,在監(jiān)聽器的onSurfaceTextureAvailable方法中彼哼,用SurfaceTexture來關(guān)聯(lián)MediaPlayer对妄,作為播放視頻的圖像數(shù)據(jù)來源。

SurfaceTexture作為數(shù)據(jù)通道敢朱,把從數(shù)據(jù)源(MediaPlayer)中獲取到的圖像幀數(shù)據(jù)轉(zhuǎn)為GL外部紋理剪菱,交給TextureVeiw作為View heirachy中的一個硬件加速層來顯示,從而實現(xiàn)視頻播放功能拴签。

MediaPlayer介紹

MediaPlayer是Android原生的多媒體播放器孝常,可以用它來實現(xiàn)本地或者在線音視頻的播放,同時它支持https和rtsp篓吁。

MediaPlayer定義了各種狀態(tài)茫因,可以理解為是它的生命周期蚪拦。

MediaPlayer狀態(tài)圖(生命周期)

這個狀態(tài)圖描述了MediaPlayer的各種狀態(tài)杖剪,以及主要方法調(diào)用后的狀態(tài)變化。

MediaPlayer的相關(guān)方法及監(jiān)聽接口:

方法 介紹 狀態(tài)
setDataSource 設(shè)置數(shù)據(jù)源 Initialized
prepare 準(zhǔn)備播放驰贷,同步 Preparing —> Prepared
prepareAsync 準(zhǔn)備播放盛嘿,異步 Preparing —> Prepared
start 開始或恢復(fù)播放 Started
pause 暫停 Paused
stop 停止 Stopped
seekTo 到指定時間點位置 PrePared/Started
reset 重置播放器 Idle
setAudioStreamType 設(shè)置音頻流類型 --
setDisplay 設(shè)置播放視頻的Surface --
setVolume 設(shè)置聲音 --
getBufferPercentage 獲取緩沖半分比 --
getCurrentPosition 獲取當(dāng)前播放位置 --
getDuration 獲取播放文件總時間 --
內(nèi)部回調(diào)接口 介紹 狀態(tài)
OnPreparedListener 準(zhǔn)備監(jiān)聽 Preparing ——>Prepared
OnVideoSizeChangedListener 視頻尺寸變化監(jiān)聽 --
OnInfoListener 指示信息和警告信息監(jiān)聽 --
OnCompletionListener 播放完成監(jiān)聽 PlaybackCompleted
OnErrorListener 播放錯誤監(jiān)聽 Error
OnBufferingUpdateListener 緩沖更新監(jiān)聽 --

MediaPlayer在直接new出來之后就進(jìn)入了Idle狀態(tài),此時可以調(diào)用多個重載的setDataSource()方法從idle狀態(tài)進(jìn)入Initialized狀態(tài)(如果調(diào)用setDataSource()方法的時候括袒,MediaPlayer對象不是出于Idle狀態(tài)次兆,會拋異常,可以調(diào)用reset()方法回到Idle狀態(tài))锹锰。

調(diào)用prepared()方法和preparedAsync()方法進(jìn)入Prepared狀態(tài)芥炭,prepared()方法直接進(jìn)入Parpared狀態(tài),preparedAsync()方法會先進(jìn)入PreParing狀態(tài)恃慧,播放引擎準(zhǔn)備完畢后會通過OnPreparedListener.onPrepared()回調(diào)方法通知Prepared狀態(tài)园蝠。

在Prepared狀態(tài)下就可以調(diào)用start()方法進(jìn)行播放了,此時進(jìn)入started()狀態(tài)痢士,如果播放的是網(wǎng)絡(luò)資源彪薛,Started狀態(tài)下也會自動調(diào)用客戶端注冊的OnBufferingUpdateListener.OnBufferingUpdate()回調(diào)方法,對流播放緩沖的狀態(tài)進(jìn)行追蹤。

pause()方法和start()方法是對應(yīng)的善延,調(diào)用pause()方法會進(jìn)入Paused狀態(tài)少态,調(diào)用start()方法重新進(jìn)入Started狀態(tài),繼續(xù)播放易遣。

stop()方法會使MdiaPlayer從Started彼妻、Paused、Prepared训挡、PlaybackCompleted等狀態(tài)進(jìn)入到Stoped狀態(tài)澳骤,播放停止。

當(dāng)資源播放完畢時澜薄,如果調(diào)用了setLooping(boolean)方法为肮,會自動進(jìn)入Started狀態(tài)重新播放,如果沒有調(diào)用則會自動調(diào)用客戶端播放器注冊的OnCompletionListener.OnCompletion()方法肤京,此時MediaPlayer進(jìn)入PlaybackCompleted狀態(tài)颊艳,在此狀態(tài)里可以調(diào)用start()方法重新進(jìn)入Started狀態(tài)。

封裝考慮

MediaPlayer的方法和接口比較多忘分,不同的狀態(tài)調(diào)用各個方法后狀態(tài)變化情況也比較復(fù)雜棋枕。播放相關(guān)的邏輯只與MediaPlayer的播放狀態(tài)和調(diào)用方法相關(guān),而界面展示和UI操作很多時候都需要根據(jù)自己項目來定制妒峦。參考原生的VideoView重斑,為了解耦和方便定制,把MediaPlayer的播放邏輯和UI界面展示及操作相關(guān)的邏輯分離肯骇。我是把MediaPlayer直接封裝到NiceVideoPlayer中窥浪,各種UI狀態(tài)和操作反饋都封裝到NiceVideoPlayerController里面。如果需要根據(jù)不同的項目需求來修改播放器的功能笛丙,就只重寫NiceVideoPlayerController就可以了漾脂。

NiceVideoPlayer

首先,需要一個FrameLayout容器mContainer胚鸯,里面有兩層內(nèi)容骨稿,第一層就是展示播放視頻內(nèi)容的TextureView,第二層就是播放器控制器mController姜钳。那么自定義一個NiceVideoPlayer繼承自FrameLayout,將mContainer添加到當(dāng)前控件:

public class NiceVideoPlayer extends FrameLayout{

    private Context mContext;
    private NiceVideoController mController;
    private FrameLayout mContainer;
    
    public NiceVideoPlayer(Context context) {
        this(context, null);
    }

    public NiceVideoPlayer(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        init();
    }

    private void init() {
         mContainer = new FrameLayout(mContext);
         mContainer.setBackgroundColor(Color.BLACK);
         LayoutParams params = new LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT);
        this.addView(mContainer, params);
    }
}

添加setUp方法來配置播放的視頻資源路徑(本地/網(wǎng)絡(luò)資源):

public void setUp(String url, Map<String, String> headers) {
        mUrl = url;
        mHeaders = headers;
    }

用戶要在mController中操作才能播放坦冠,因此需要在播放之前設(shè)置好mController:

public void setController(NiceVideoPlayerController controller) {
    mController = controller;
    mController.setNiceVideoPlayer(this);
    LayoutParams params = new LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT);
    mContainer.addView(mController, params);
}

用戶在自定義好自己的控制器后通過setController這個方法設(shè)置給播放器進(jìn)行關(guān)聯(lián)。

觸發(fā)播放時哥桥,NiceVideoPlayer將展示視頻圖像內(nèi)容的mTextureView添加到mContainer中(在mController的下層)辙浑,同時初始化mMediaPlayer,待mTextureView的數(shù)據(jù)通道SurfaceTexture準(zhǔn)備就緒后就可以打開播放器:

public void start() {
    initMediaPlayer();  // 初始化播放器
    initTextureView();  // 初始化展示視頻內(nèi)容的TextureView
    addTextureView();   // 將TextureView添加到容器中
}

private void initTextureView() {
    if (mTextureView == null) {
        mTextureView = new TextureView(mContext);
        mTextureView.setSurfaceTextureListener(this);
    }
}

private void addTextureView() {
    mContainer.removeView(mTextureView);
    LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT);
    mContainer.addView(mTextureView, 0,  params);
}

private void initMediaPlayer() {
    if (mMediaPlayer == null) {
        mMediaPlayer = new MediaPlayer();

        mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        mMediaPlayer.setScreenOnWhilePlaying(true);

        mMediaPlayer.setOnPreparedListener(mOnPreparedListener);
        mMediaPlayer.setOnVideoSizeChangedListener(mOnVideoSizeChangedListener);
        mMediaPlayer.setOnCompletionListener(mOnCompletionListener);
        mMediaPlayer.setOnErrorListener(mOnErrorListener);
        mMediaPlayer.setOnInfoListener(mOnInfoListener);
        mMediaPlayer.setOnBufferingUpdateListener(mOnBufferingUpdateListener);
    }
}

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    // surfaceTexture數(shù)據(jù)通道準(zhǔn)備就緒泰讽,打開播放器
    openMediaPlayer(surface);
}

private void openMediaPlayer(SurfaceTexture surface) {
    try {
        mMediaPlayer.setDataSource(mContext.getApplicationContext(), Uri.parse(mUrl), mHeaders);
        mMediaPlayer.setSurface(new Surface(surface));
        mMediaPlayer.prepareAsync();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    return false;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {

}

打開播放器調(diào)用prepareAsync()方法后例衍,mMediaPlayer進(jìn)入準(zhǔn)備狀態(tài)昔期,準(zhǔn)備就緒后就可以開始:

private MediaPlayer.OnPreparedListener mOnPreparedListener
        = new MediaPlayer.OnPreparedListener() {
    @Override
    public void onPrepared(MediaPlayer mp) {
        mp.start();
    }
};

NiceVideoPlayer的這些邏輯已經(jīng)實現(xiàn)視頻播放了,操作相關(guān)以及UI展示的邏輯需要在控制器NiceVideoPlayerController中來實現(xiàn)佛玄。但是呢硼一,UI的展示和反饋都需要依據(jù)播放器當(dāng)前的播放狀態(tài),所以需要給播放器定義一些常量來表示它的播放狀態(tài):

public static final int STATE_ERROR = -1;          // 播放錯誤
public static final int STATE_IDLE = 0;            // 播放未開始
public static final int STATE_PREPARING = 1;       // 播放準(zhǔn)備中
public static final int STATE_PREPARED = 2;        // 播放準(zhǔn)備就緒
public static final int STATE_PLAYING = 3;         // 正在播放
public static final int STATE_PAUSED = 4;          // 暫停播放
// 正在緩沖(播放器正在播放時梦抢,緩沖區(qū)數(shù)據(jù)不足般贼,進(jìn)行緩沖,緩沖區(qū)數(shù)據(jù)足夠后恢復(fù)播放)
public static final int STATE_BUFFERING_PLAYING = 5;
// 正在緩沖(播放器正在播放時奥吩,緩沖區(qū)數(shù)據(jù)不足哼蛆,進(jìn)行緩沖,此時暫停播放器霞赫,繼續(xù)緩沖腮介,緩沖區(qū)數(shù)據(jù)足夠后恢復(fù)暫停)
public static final int STATE_BUFFERING_PAUSED = 6;
public static final int STATE_COMPLETED = 7;       // 播放完成

播放視頻時,mMediaPlayer準(zhǔn)備就緒(Prepared)后沒有馬上進(jìn)入播放狀態(tài)端衰,中間有一個時間延遲時間段叠洗,然后開始渲染圖像。所以將Prepared——>“開始渲染”中間這個時間段定義為STATE_PREPARED旅东。

如果是播放網(wǎng)絡(luò)視頻灭抑,在播放過程中,緩沖區(qū)數(shù)據(jù)不足時mMediaPlayer內(nèi)部會停留在某一幀畫面以進(jìn)行緩沖抵代。正在緩沖時腾节,mMediaPlayer可能是在正在播放也可能是暫停狀態(tài),因為在緩沖時如果用戶主動點擊了暫停荤牍,就是處于STATE_BUFFERING_PAUSED案腺,所以緩沖有STATE_BUFFERING_PLAYINGSTATE_BUFFERING_PAUSED兩種狀態(tài),緩沖結(jié)束后参淫,恢復(fù)播放或暫停救湖。

private MediaPlayer.OnPreparedListener mOnPreparedListener
        = new MediaPlayer.OnPreparedListener() {
    @Override
    public void onPrepared(MediaPlayer mp) {
        mp.start();
        mCurrentState = STATE_PREPARED;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("onPrepared ——> STATE_PREPARED");
    }
};

private MediaPlayer.OnVideoSizeChangedListener mOnVideoSizeChangedListener
        = new MediaPlayer.OnVideoSizeChangedListener() {
    @Override
    public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
        LogUtil.d("onVideoSizeChanged ——> width:" + width + "愧杯,height:" + height);
    }
};

private MediaPlayer.OnCompletionListener mOnCompletionListener
        = new MediaPlayer.OnCompletionListener() {
    @Override
    public void onCompletion(MediaPlayer mp) {
        mCurrentState = STATE_COMPLETED;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("onCompletion ——> STATE_COMPLETED");
    }
};

private MediaPlayer.OnErrorListener mOnErrorListener
        = new MediaPlayer.OnErrorListener() {
    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        mCurrentState = STATE_ERROR;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("onError ——> STATE_ERROR ———— what:" + what);
        return false;
    }
};

private MediaPlayer.OnInfoListener mOnInfoListener
        = new MediaPlayer.OnInfoListener() {
    @Override
    public boolean onInfo(MediaPlayer mp, int what, int extra) {
        if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
            // 播放器渲染第一幀
            mCurrentState = STATE_PLAYING;
            mController.setControllerState(mPlayerState, mCurrentState);
            LogUtil.d("onInfo ——> MEDIA_INFO_VIDEO_RENDERING_START:STATE_PLAYING");
        } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) {
            // MediaPlayer暫時不播放涎才,以緩沖更多的數(shù)據(jù)
            if (mCurrentState == STATE_PAUSED || mCurrentState == STATE_BUFFERING_PAUSED) {
                mCurrentState = STATE_BUFFERING_PAUSED;
                LogUtil.d("onInfo ——> MEDIA_INFO_BUFFERING_START:STATE_BUFFERING_PAUSED");
            } else {
                mCurrentState = STATE_BUFFERING_PLAYING;
                LogUtil.d("onInfo ——> MEDIA_INFO_BUFFERING_START:STATE_BUFFERING_PLAYING");
            }
            mController.setControllerState(mPlayerState, mCurrentState);
        } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) {
            // 填充緩沖區(qū)后,MediaPlayer恢復(fù)播放/暫停
            if (mCurrentState == STATE_BUFFERING_PLAYING) {
                mCurrentState = STATE_PLAYING;
                mController.setControllerState(mPlayerState, mCurrentState);
                LogUtil.d("onInfo ——> MEDIA_INFO_BUFFERING_END: STATE_PLAYING");
            }
            if (mCurrentState == STATE_BUFFERING_PAUSED) {
                mCurrentState = STATE_PAUSED;
                mController.setControllerState(mPlayerState, mCurrentState);
                LogUtil.d("onInfo ——> MEDIA_INFO_BUFFERING_END: STATE_PAUSED");
            }
        } else {
            LogUtil.d("onInfo ——> what:" + what);
        }
        return true;
    }
};

private MediaPlayer.OnBufferingUpdateListener mOnBufferingUpdateListener
        = new MediaPlayer.OnBufferingUpdateListener() {
    @Override
    public void onBufferingUpdate(MediaPlayer mp, int percent) {
        mBufferPercentage = percent;
    }
};

mController.setControllerState(mPlayerState, mCurrentState)力九,mCurrentState表示當(dāng)前播放狀態(tài)耍铜,mPlayerState表示播放器的全屏、小窗口跌前,正常三種狀態(tài)棕兼。

public static final int PLAYER_NORMAL = 10;        // 普通播放器
public static final int PLAYER_FULL_SCREEN = 11;   // 全屏播放器
public static final int PLAYER_TINY_WINDOW = 12;   // 小窗口播放器

定義好播放狀態(tài)后,開始暫停等操作邏輯也需要根據(jù)播放狀態(tài)調(diào)整:

@Override
public void start() {
    if (mCurrentState == STATE_IDLE
            || mCurrentState == STATE_ERROR
            || mCurrentState == STATE_COMPLETED) {
        initMediaPlayer();
        initTextureView();
        addTextureView();
    }
}

@Override
public void restart() {
    if (mCurrentState == STATE_PAUSED) {
        mMediaPlayer.start();
        mCurrentState = STATE_PLAYING;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("STATE_PLAYING");
    }
    if (mCurrentState == STATE_BUFFERING_PAUSED) {
        mMediaPlayer.start();
        mCurrentState = STATE_BUFFERING_PLAYING;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("STATE_BUFFERING_PLAYING");
    }
}

@Override
public void pause() {
    if (mCurrentState == STATE_PLAYING) {
        mMediaPlayer.pause();
        mCurrentState = STATE_PAUSED;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("STATE_PAUSED");
    }
    if (mCurrentState == STATE_BUFFERING_PLAYING) {
        mMediaPlayer.pause();
        mCurrentState = STATE_BUFFERING_PAUSED;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("STATE_BUFFERING_PAUSED");
    }
}

reStart()方法是暫停時繼續(xù)播放調(diào)用抵乓。

全屏伴挚、小窗口播放的實現(xiàn)

可能最能想到實現(xiàn)全屏的方式就是把當(dāng)前播放器的寬高給放大到屏幕大小靶衍,同時隱藏除播放器以外的其他所有UI,并設(shè)置成橫屏模式茎芋。但是這種方式有很多問題颅眶,比如在列表(ListView或RecyclerView)中,除了放大隱藏外田弥,還需要去計算滑動多少距離才剛好讓播放器與屏幕邊緣重合涛酗,退出全屏的時候還需要滑動到之前的位置,這樣實現(xiàn)邏輯不但繁瑣偷厦,而且和外部UI偶合嚴(yán)重商叹,后面改動維護(hù)起來非常困難(我曾經(jīng)就用這種方式被坑了無數(shù)道)。

分析能不能有其他更好的實現(xiàn)方式呢只泼?

整個播放器由mMediaPalyer+mTexutureView+mController組成剖笙,要實現(xiàn)全屏或小窗口播放,我們只需要挪動播放器的展示界面mTexutureView和控制界面mController即可请唱。并且呢我們在上面定義播放器時枯途,已經(jīng)把mTexutureViewmController一起添加到mContainer中了,所以只需要將mContainer從當(dāng)前視圖中移除籍滴,并添加到全屏和小窗口的目標(biāo)視圖中即可酪夷。

那么怎么確定全屏和小窗口的目標(biāo)視圖呢?

我們知道每個Activity里面都有一個android.R.content孽惰,它是一個FrameLayout晚岭,里面包含了我們setContentView的所有控件。既然它是一個FrameLayout勋功,我們就可以將它作為全屏和小窗口的目標(biāo)視圖坦报。

我們把從當(dāng)前視圖移除的mContainer重新添加到android.R.content中,并且設(shè)置成橫屏狂鞋。這個時候還需要注意android.R.content是不包括ActionBar和狀態(tài)欄的片择,所以要將Activity設(shè)置成全屏模式,同時隱藏ActionBar骚揍。

@Override
public void enterFullScreen() {
    if (mPlayerState == PLAYER_FULL_SCREEN) return;

    // 隱藏ActionBar字管、狀態(tài)欄,并橫屏
    NiceUtil.hideActionBar(mContext);
    NiceUtil.scanForActivity(mContext)
            .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

    this.removeView(mContainer);
    ViewGroup contentView = (ViewGroup) NiceUtil.scanForActivity(mContext)
            .findViewById(android.R.id.content);
    LayoutParams params = new LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT);
    contentView.addView(mContainer, params);

    mPlayerState = PLAYER_FULL_SCREEN;
    mController.setControllerState(mPlayerState, mCurrentState);
    LogUtil.d("PLAYER_FULL_SCREEN");
}

退出全屏也就很簡單了信不,將mContainerandroid.R.content中移除嘲叔,重新添加到當(dāng)前視圖,并恢復(fù)ActionBar抽活、清除全屏模式就行了硫戈。

public boolean exitFullScreen() {
    if (mPlayerState == PLAYER_FULL_SCREEN) {
        NiceUtil.showActionBar(mContext);
        NiceUtil.scanForActivity(mContext)
                .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

        ViewGroup contentView = (ViewGroup) NiceUtil.scanForActivity(mContext)
                .findViewById(android.R.id.content);
        contentView.removeView(mContainer);
        LayoutParams params = new LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT);
        this.addView(mContainer, params);

        mPlayerState = PLAYER_NORMAL;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("PLAYER_NORMAL");
        return true;
    }
    return false;
}

切換橫豎屏?xí)r為了避免Activity重新走生命周期,別忘了需要在Manifest.xmlactivity標(biāo)簽下添加如下配置:

android:configChanges="orientation|keyboardHidden|screenSize"

進(jìn)入小窗口播放和退出小窗口的實現(xiàn)原理就和全屏功能一樣了下硕,只需要修改它的寬高參數(shù):

@Override
public void enterTinyWindow() {
    if (mPlayerState == PLAYER_TINY_WINDOW) return;

    this.removeView(mContainer);

    ViewGroup contentView = (ViewGroup) NiceUtil.scanForActivity(mContext)
            .findViewById(android.R.id.content);
    // 小窗口的寬度為屏幕寬度的60%丁逝,長寬比默認(rèn)為16:9汁胆,右邊距、下邊距為8dp霜幼。
    FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
            (int) (NiceUtil.getScreenWidth(mContext) * 0.6f),
            (int) (NiceUtil.getScreenWidth(mContext) * 0.6f * 9f / 16f));
    params.gravity = Gravity.BOTTOM | Gravity.END;
    params.rightMargin = NiceUtil.dp2px(mContext, 8f);
    params.bottomMargin = NiceUtil.dp2px(mContext, 8f);

    contentView.addView(mContainer, params);

    mPlayerState = PLAYER_TINY_WINDOW;
    mController.setControllerState(mPlayerState, mCurrentState);
    LogUtil.d("PLAYER_TINY_WINDOW");
}

@Override
public boolean exitTinyWindow() {
    if (mPlayerState == PLAYER_TINY_WINDOW) {
        ViewGroup contentView = (ViewGroup) NiceUtil.scanForActivity(mContext)
                .findViewById(android.R.id.content);
        contentView.removeView(mContainer);
        LayoutParams params = new LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT);
        this.addView(mContainer, params);

        mPlayerState = PLAYER_NORMAL;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("PLAYER_NORMAL");
        return true;
    }
    return false;
}

這里有個特別需要注意的一點:

當(dāng)mContainer移除重新添加后沦泌,mContainer及其內(nèi)部的mTextureViewmController都會重繪,mTextureView重繪后辛掠,會重新new一個SurfaceTexture谢谦,并重新回調(diào)onSurfaceTextureAvailable方法,這樣mTextureView的數(shù)據(jù)通道SurfaceTexture發(fā)生了變化萝衩,但是mMediaPlayer還是持有原先的mSurfaceTexut回挽,所以在切換全屏之前要保存之前的mSufaceTexture,當(dāng)切換到全屏后重新調(diào)用onSurfaceTextureAvailable時猩谊,將之前的mSufaceTexture重新設(shè)置給mTexutureView千劈。這樣就保證了切換時視頻播放的無縫銜接。

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
    if (mSurfaceTexture == null) {
        mSurfaceTexture = surfaceTexture;
        openMediaPlayer();
    } else {
        mTextureView.setSurfaceTexture(mSurfaceTexture);
    }
}

NiceVideoPlayerControl

為了解除NiceVideoPlayerNiceVideoPlayerController的耦合牌捷,把NiceVideoPlayer的一些功能性和判斷性方法抽象到NiceVideoPlayerControl接口中墙牌。

public interface NiceVideoPlayerControl {

    void start();
    void restart();
    void pause();
    void seekTo(int pos);

    boolean isIdle();
    boolean isPreparing();
    boolean isPrepared();
    boolean isBufferingPlaying();
    boolean isBufferingPaused();
    boolean isPlaying();
    boolean isPaused();
    boolean isError();
    boolean isCompleted();

    boolean isFullScreen();
    boolean isTinyWindow();
    boolean isNormal();

    int getDuration();
    int getCurrentPosition();
    int getBufferPercentage();

    void enterFullScreen();
    boolean exitFullScreen();
    void enterTinyWindow();
    boolean exitTinyWindow();

    void release();
}

NiceVideoPlayer實現(xiàn)這個接口即可。

NiceVideoPlayerManager

同一界面上有多個視頻暗甥,或者視頻放在ReclerView或者ListView的容器中喜滨,要保證同一時刻只有一個視頻在播放,其他的都是初始狀態(tài)撤防,所以需要一個NiceVideoPlayerManager來管理播放器虽风,主要功能是保存當(dāng)前已經(jīng)開始了的播放器。

public class NiceVideoPlayerManager {

    private NiceVideoPlayer mVideoPlayer;

    private NiceVideoPlayerManager() {
    }

    private static NiceVideoPlayerManager sInstance;

    public static synchronized NiceVideoPlayerManager instance() {
        if (sInstance == null) {
            sInstance = new NiceVideoPlayerManager();
        }
        return sInstance;
    }

    public void setCurrentNiceVideoPlayer(NiceVideoPlayer videoPlayer) {
        mVideoPlayer = videoPlayer;
    }

    public void releaseNiceVideoPlayer() {
        if (mVideoPlayer != null) {
            mVideoPlayer.release();
            mVideoPlayer = null;
        }
    }

    public boolean onBackPressd() {
        if (mVideoPlayer != null) {
            if (mVideoPlayer.isFullScreen()) {
                return mVideoPlayer.exitFullScreen();
            } else if (mVideoPlayer.isTinyWindow()) {
                return mVideoPlayer.exitTinyWindow();
            } else {
                mVideoPlayer.release();
                return false;
            }
        }
        return false;
    }
}

采用單例寄月,同時辜膝,onBackPressedActivity中用戶按返回鍵時調(diào)用。
NiceVideoPlayerstart方法以及onCompleted需要修改一下漾肮,保證開始播放一個視頻時要先釋放掉之前的播放器厂抖;同時自己播放完畢,要將NiceVideoPlayerManager中的mNiceVideoPlayer實例置空克懊,避免內(nèi)存泄露忱辅。

// NiceVideoPlayer的start()方法。
@Override
public void start() {
    NiceVideoPlayerManager.instance().releaseNiceVideoPlayer();
    NiceVideoPlayerManager.instance().setCurrentNiceVideoPlayer(this);
    if (mCurrentState == STATE_IDLE
            || mCurrentState == STATE_ERROR
            || mCurrentState == STATE_COMPLETED) {
        initMediaPlayer();
        initTextureView();
        addTextureView();
    }
}

// NiceVideoPlayer中的onCompleted監(jiān)聽保檐。
private MediaPlayer.OnCompletionListener mOnCompletionListener
        = new MediaPlayer.OnCompletionListener() {
    @Override
    public void onCompletion(MediaPlayer mp) {
        mCurrentState = STATE_COMPLETED;
        mController.setControllerState(mPlayerState, mCurrentState);
        LogUtil.d("onCompletion ——> STATE_COMPLETED");
        NiceVideoPlayerManager.instance().setCurrentNiceVideoPlayer(null);
    }
};

NiceVideoPlayerController

播放控制界面上耕蝉,播放崔梗、暫停夜只、播放進(jìn)度、緩沖動畫蒜魄、全屏/小屏等觸發(fā)都是直接調(diào)用播放器對應(yīng)的操作的扔亥。需要注意的就是調(diào)用之前要判斷當(dāng)前的播放狀態(tài)场躯,因為有些狀態(tài)下調(diào)用播放器的操作可能引起錯誤(比如播放器還沒準(zhǔn)備就緒,就去獲取當(dāng)前的播放位置)旅挤。

播放器在觸發(fā)相應(yīng)功能的時候都會調(diào)用NiceVideoPlayerControllersetControllerState(int playerState, int playState)這個方法來讓用戶修改UI踢关。

不同項目都可能定制不同的控制器(播放操作界面),這里我就不詳細(xì)分析實現(xiàn)邏輯了粘茄,大致功能就類似騰訊視頻的熱點列表中的播放器签舞。其中橫向滑動改變播放進(jìn)度、左側(cè)上下滑動改變亮度柒瓣,右側(cè)上下滑動改變亮度等功能在代碼中都有實現(xiàn)儒搭。代碼有點長,就不貼了芙贫,需要的直接下載源碼搂鲫。

使用

mNiceVideoPlayer.setUp(url, null);
NiceVideoPlayerController controller = new NiceVideoPlayerController(this);
controller.setTitle(title);
controller.setImage(imageUrl);
mNiceVideoPlayer.setController(controller);

RecyclerView或者ListView中使用時,需要監(jiān)聽itemViewdetached

mRecyclerView.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() {
    @Override
    public void onChildViewAttachedToWindow(View view) {

    }

    @Override
    public void onChildViewDetachedFromWindow(View view) {
        NiceVideoPlayer niceVideoPlayer = (NiceVideoPlayer) view.findViewById(R.id.nice_video_player);
        if (niceVideoPlayer != null) {
            niceVideoPlayer.release();
        }
    }
});

ItemViewdetach窗口時磺平,需要釋放掉itemView內(nèi)部的播放器魂仍。

效果圖

最后

整個功能有參考節(jié)操播放器,但是自己這樣封裝和節(jié)操播放器還是有很大差異:一是分離了播放功能和控制界面拣挪,定制只需修改控制器即可擦酌。二是全屏/小窗口沒有新建一個播放器,只是挪動了播放界面和控制器菠劝,不用每個視頻都需要新建兩個播放器仑氛,也不用同步狀態(tài)。


MediaPlayer有很多格式不支持闸英,項目已添加IjkPlayer的擴(kuò)展支持锯岖,可以切換IjkPlayer和原生MediaPlayer,后續(xù)還會考慮添加ExoPlayer甫何,同時也會擴(kuò)展更多功能出吹。

如果有錯誤和更好的建議都請?zhí)岢觯创a已上傳GitHub辙喂,歡迎Star捶牢,謝謝!巍耗。

源碼:https://github.com/xiaoyanger0825/NiceVieoPlayer


參考:
Android TextureView簡易教程
視頻畫面幀的展示控件SurfaceView及TextureView對比
Android 5.0(Lollipop)中的SurfaceTexture秋麸,TextureView, SurfaceView和GLSurfaceView
Android MediaPlayer生命周期詳解
節(jié)操播放器 https://github.com/lipangit/JieCaoVideoPlayer

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市炬太,隨后出現(xiàn)的幾起案子灸蟆,更是在濱河造成了極大的恐慌,老刑警劉巖亲族,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件炒考,死亡現(xiàn)場離奇詭異可缚,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)斋枢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門帘靡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瓤帚,你說我怎么就攤上這事描姚。” “怎么了戈次?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵轰胁,是天一觀的道長。 經(jīng)常有香客問我朝扼,道長赃阀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任擎颖,我火速辦了婚禮榛斯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘搂捧。我一直安慰自己驮俗,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布允跑。 她就那樣靜靜地躺著王凑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪聋丝。 梳的紋絲不亂的頭發(fā)上索烹,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機(jī)與錄音弱睦,去河邊找鬼百姓。 笑死,一個胖子當(dāng)著我的面吹牛况木,可吹牛的內(nèi)容都是我干的垒拢。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼火惊,長吁一口氣:“原來是場噩夢啊……” “哼求类!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起屹耐,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤尸疆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體仓技,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡鸵贬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年俗他,在試婚紗的時候發(fā)現(xiàn)自己被綠了脖捻。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡兆衅,死狀恐怖地沮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情羡亩,我是刑警寧澤摩疑,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站畏铆,受9級特大地震影響雷袋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜辞居,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一楷怒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瓦灶,春花似錦鸠删、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至碉怔,卻和暖如春烘贴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背撮胧。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工庙楚, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人趴樱。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓馒闷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親叁征。 傳聞我的和親對象是個殘疾皇子纳账,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

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