用MediaPlayer+TextureView封装一个完美实现全屏、小窗口的视频播放器

项目已添加IjkPlayer支持,后续逐渐完善其余功能。
地址:github.com/xiaoyanger0…html

为何使用TextureView

在Android总播放视频能够直接使用VideoViewVideoView是经过继承自SurfaceView来实现的。SurfaceView的大概原理就是在现有View的位置上建立一个新的Window,内容的显示和渲染都在新的Window中。这使得SurfaceView的绘制和刷新能够在单独的线程中进行,从而大大提升效率。可是呢,因为SurfaceView的内容没有显示在View中而是显示在新建的Window中, 使得SurfaceView的显示不受View的属性控制,不能进行平移,缩放等变换,也不能放在其它RecyclerViewScrollView中,一些View中的特性也没法使用。android

TextureView是在4.0(API level 14)引入的,与SurfaceView相比,它不会建立新的窗口来显示内容。它是将内容流直接投放到View中,而且能够和其它普通View同样进行移动,旋转,缩放,动画等变化。TextureView必须在硬件加速的窗口中使用。git

TextureView被建立后不能直接使用,必需要在它被它添加到ViewGroup后,待SurfaceTexture准备就绪才能起做用(看TextureView的源码,TextureView是在绘制的时候建立的内部SurfaceTexture)。一般须要给TextureView设置监听器SurfaceTextuListenergithub

mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        // SurfaceTexture准备就绪
    }

    @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的准备就绪、大小变化、销毁、更新等状态变化时都会回调相对应的方法。当TextureView内部建立好SurfaceTexture后,在监听器的onSurfaceTextureAvailable方法中,用SurfaceTexture来关联MediaPlayer,做为播放视频的图像数据来源。网络

SurfaceTexture做为数据通道,把从数据源(MediaPlayer)中获取到的图像帧数据转为GL外部纹理,交给TextureVeiw做为View heirachy中的一个硬件加速层来显示,从而实现视频播放功能。异步

MediaPlayer介绍

MediaPlayer是Android原生的多媒体播放器,能够用它来实现本地或者在线音视频的播放,同时它支持https和rtspide

MediaPlayer定义了各类状态,能够理解为是它的生命周期。oop

MediaPlayer状态图(生命周期)
MediaPlayer状态图(生命周期)

这个状态图描述了MediaPlayer的各类状态,以及主要方法调用后的状态变化。动画

MediaPlayer的相关方法及监听接口:ui

方法 介绍 状态
setDataSource 设置数据源 Initialized
prepare 准备播放,同步 Preparing —> Prepared
prepareAsync 准备播放,异步 Preparing —> Prepared
start 开始或恢复播放 Started
pause 暂停 Paused
stop 中止 Stopped
seekTo 到指定时间点位置 PrePared/Started
reset 重置播放器 Idle
setAudioStreamType 设置音频流类型 --
setDisplay 设置播放视频的Surface --
setVolume 设置声音 --
getBufferPercentage 获取缓冲半分比 --
getCurrentPosition 获取当前播放位置 --
getDuration 获取播放文件总时间 --
内部回调接口 介绍 状态
OnPreparedListener 准备监听 Preparing ——>Prepared
OnVideoSizeChangedListener 视频尺寸变化监听 --
OnInfoListener 指示信息和警告信息监听 --
OnCompletionListener 播放完成监听 PlaybackCompleted
OnErrorListener 播放错误监听 Error
OnBufferingUpdateListener 缓冲更新监听 --

MediaPlayer在直接new出来以后就进入了Idle状态,此时能够调用多个重载的setDataSource()方法从idle状态进入Initialized状态(若是调用setDataSource()方法的时候,MediaPlayer对象不是出于Idle状态,会抛异常,能够调用reset()方法回到Idle状态)。

调用prepared()方法和preparedAsync()方法进入Prepared状态,prepared()方法直接进入Parpared状态,preparedAsync()方法会先进入PreParing状态,播放引擎准备完毕后会经过OnPreparedListener.onPrepared()回调方法通知Prepared状态。

在Prepared状态下就能够调用start()方法进行播放了,此时进入started()状态,若是播放的是网络资源,Started状态下也会自动调用客户端注册的OnBufferingUpdateListener.OnBufferingUpdate()回调方法,对流播放缓冲的状态进行追踪。

pause()方法和start()方法是对应的,调用pause()方法会进入Paused状态,调用start()方法从新进入Started状态,继续播放。

stop()方法会使MdiaPlayer从Started、Paused、Prepared、PlaybackCompleted等状态进入到Stoped状态,播放中止。

当资源播放完毕时,若是调用了setLooping(boolean)方法,会自动进入Started状态从新播放,若是没有调用则会自动调用客户端播放器注册的OnCompletionListener.OnCompletion()方法,此时MediaPlayer进入PlaybackCompleted状态,在此状态里能够调用start()方法从新进入Started状态。

封装考虑

MediaPlayer的方法和接口比较多,不一样的状态调用各个方法后状态变化状况也比较复杂。播放相关的逻辑只与MediaPlayer的播放状态和调用方法相关,而界面展现和UI操做不少时候都须要根据本身项目来定制。参考原生的VideoView,为了解耦和方便定制,把MediaPlayer的播放逻辑和UI界面展现及操做相关的逻辑分离。我是把MediaPlayer直接封装到NiceVideoPlayer中,各类UI状态和操做反馈都封装到NiceVideoPlayerController里面。若是须要根据不一样的项目需求来修改播放器的功能,就只重写NiceVideoPlayerController就能够了。

NiceVideoPlayer

首先,须要一个FrameLayout容器mContainer,里面有两层内容,第一层就是展现播放视频内容的TextureView,第二层就是播放器控制器mController。那么自定义一个NiceVideoPlayer继承自FrameLayout,将mContainer添加到当前控件:

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方法来配置播放的视频资源路径(本地/网络资源):

public void setUp(String url, Map
  
  
  

 
  
  headers) { mUrl = url; mHeaders = headers; } 

 复制代码

用户要在mController中操做才能播放,所以须要在播放以前设置好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这个方法设置给播放器进行关联。

触发播放时,NiceVideoPlayer将展现视频图像内容的mTextureView添加到mContainer中(在mController的下层),同时初始化mMediaPlayer,待mTextureView的数据通道SurfaceTexture准备就绪后就能够打开播放器:

public void start() {
    initMediaPlayer();  // 初始化播放器
    initTextureView();  // 初始化展现视频内容的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数据通道准备就绪,打开播放器
    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) {

}复制代码

打开播放器调用prepareAsync()方法后,mMediaPlayer进入准备状态,准备就绪后就能够开始:

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

NiceVideoPlayer的这些逻辑已经实现视频播放了,操做相关以及UI展现的逻辑须要在控制器NiceVideoPlayerController中来实现。可是呢,UI的展现和反馈都须要依据播放器当前的播放状态,因此须要给播放器定义一些常量来表示它的播放状态:

public static final int STATE_ERROR = -1;          // 播放错误
public static final int STATE_IDLE = 0;            // 播放未开始
public static final int STATE_PREPARING = 1;       // 播放准备中
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_BUFFERING_PLAYING = 5;
// 正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区数据足够后恢复暂停)
public static final int STATE_BUFFERING_PAUSED = 6;
public static final int STATE_COMPLETED = 7;       // 播放完成复制代码

播放视频时,mMediaPlayer准备就绪(Prepared)后没有立刻进入播放状态,中间有一个时间延迟时间段,而后开始渲染图像。因此将Prepared——>“开始渲染”中间这个时间段定义为STATE_PREPARED

若是是播放网络视频,在播放过程当中,缓冲区数据不足时mMediaPlayer内部会停留在某一帧画面以进行缓冲。正在缓冲时,mMediaPlayer多是在正在播放也多是暂停状态,由于在缓冲时若是用户主动点击了暂停,就是处于STATE_BUFFERING_PAUSED,因此缓冲有STATE_BUFFERING_PLAYINGSTATE_BUFFERING_PAUSED两种状态,缓冲结束后,恢复播放或暂停。

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暂时不播放,以缓冲更多的数据
            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) {
            // 填充缓冲区后,MediaPlayer恢复播放/暂停
            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表示当前播放状态,mPlayerState表示播放器的全屏、小窗口,正常三种状态。

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

定义好播放状态后,开始暂停等操做逻辑也须要根据播放状态调整:

@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()方法是暂停时继续播放调用。

全屏、小窗口播放的实现

可能最能想到实现全屏的方式就是把当前播放器的宽高给放大到屏幕大小,同时隐藏除播放器之外的其余全部UI,并设置成横屏模式。可是这种方式有不少问题,好比在列表(ListView或RecyclerView)中,除了放大隐藏外,还须要去计算滑动多少距离才恰好让播放器与屏幕边缘重合,退出全屏的时候还须要滑动到以前的位置,这样实现逻辑不但繁琐,并且和外部UI偶合严重,后面改动维护起来很是困难(我曾经就用这种方式被坑了无数道)。

分析能不能有其余更好的实现方式呢?

整个播放器由mMediaPalyer+mTexutureView+mController组成,要实现全屏或小窗口播放,咱们只须要挪动播放器的展现界面mTexutureView和控制界面mController便可。而且呢咱们在上面定义播放器时,已经把mTexutureViewmController一块儿添加到mContainer中了,因此只须要将mContainer从当前视图中移除,并添加到全屏和小窗口的目标视图中便可。

那么怎么肯定全屏和小窗口的目标视图呢?

咱们知道每一个Activity里面都有一个android.R.content,它是一个FrameLayout,里面包含了咱们setContentView的全部控件。既然它是一个FrameLayout,咱们就能够将它做为全屏和小窗口的目标视图。

咱们把从当前视图移除的mContainer从新添加到android.R.content中,而且设置成横屏。这个时候还须要注意android.R.content是不包括ActionBar和状态栏的,因此要将Activity设置成全屏模式,同时隐藏ActionBar

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

    // 隐藏ActionBar、状态栏,并横屏
    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中移除,从新添加到当前视图,并恢复ActionBar、清除全屏模式就好了。

@Override
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;
}复制代码

切换横竖屏时为了不Activity从新走生命周期,别忘了须要在Manifest.xmlactivity标签下添加以下配置:

android:configChanges="orientation|keyboardHidden|screenSize"复制代码

进入小窗口播放和退出小窗口的实现原理就和全屏功能同样了,只须要修改它的宽高参数:

@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%,长宽比默认为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;
}复制代码

这里有个特别须要注意的一点:

mContainer移除从新添加后,mContainer及其内部的mTextureViewmController都会重绘,mTextureView重绘后,会从新new一个SurfaceTexture,并从新回调onSurfaceTextureAvailable方法,这样mTextureView的数据通道SurfaceTexture发生了变化,可是mMediaPlayer仍是持有原先的mSurfaceTexut,因此在切换全屏以前要保存以前的mSufaceTexture,当切换到全屏后从新调用onSurfaceTextureAvailable时,将以前的mSufaceTexture从新设置给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实现这个接口便可。

NiceVideoPlayerManager

同一界面上有多个视频,或者视频放在ReclerView或者ListView的容器中,要保证同一时刻只有一个视频在播放,其余的都是初始状态,因此须要一个NiceVideoPlayerManager来管理播放器,主要功能是保存当前已经开始了的播放器。

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中用户按返回键时调用。
NiceVideoPlayerstart方法以及onCompleted须要修改一下,保证开始播放一个视频时要先释放掉以前的播放器;同时本身播放完毕,要将NiceVideoPlayerManager中的mNiceVideoPlayer实例置空,避免内存泄露。

// 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监听。
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

播放控制界面上,播放、暂停、播放进度、缓冲动画、全屏/小屏等触发都是直接调用播放器对应的操做的。须要注意的就是调用以前要判断当前的播放状态,由于有些状态下调用播放器的操做可能引发错误(好比播放器还没准备就绪,就去获取当前的播放位置)。

播放器在触发相应功能的时候都会调用NiceVideoPlayerControllersetControllerState(int playerState, int playState)这个方法来让用户修改UI。

不一样项目均可能定制不一样的控制器(播放操做界面),这里我就不详细分析实现逻辑了,大体功能就相似腾讯视频的热点列表中的播放器。其中全屏模式下横向滑动改变播放进度、左侧上下滑动改变亮度,右侧上下滑动改变亮度等功能代码中并未实现,有须要的能够直接参考节操播放器,只须要在ControlleronInterceptTouchEvent中处理就好了(后续会添加上去)。

代码有点长,就不贴了,须要的直接下载源码

使用

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

RecyclerView或者ListView中使用时,须要监听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内部的播放器。

效果图

最后

整个功能有参考节操播放器,可是本身这样封装和节操播放器仍是有很大差别:一是分离了播放功能和控制界面,定制只需修改控制器便可。二是全屏/小窗口没有新建一个播放器,只是挪动了播放界面和控制器,不用每一个视频都须要新建两个播放器,也不用同步状态。


MediaPlayer有不少格式不支持,项目已添加IjkPlayer的扩展支持,能够切换IjkPlayer和原生MediaPlayer,后续还会考虑添加ExoPlayer,同时也会扩展更多功能。

若是有错误和更好的建议都请提出,源码已上传GitHub,欢迎Star,谢谢!。

源码:github.com/xiaoyanger0…


参考:
Android TextureView简易教程
视频画面帧的展现控件SurfaceView及TextureView对比
Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView
Android MediaPlayer生命周期详解
节操播放器 https://github.com/lipangit/JieCaoVideoPlayer

相关文章
相关标签/搜索