MediaPlayer封装原生视频播放器

MediaPlayer可以用来控制视频和音频文件流,也就是说可以通过它播放音乐和视频。通常如果我们不用第三方的框架,有三种方式可以去播放视频。

1.VideoView
2.MediaPlayer+SurfaceView
3.MediaPlayer+TextureView

首先VideoView是继承自SurfaceView,内部维护着一个MediaPlayer,用过VideoView的人都知道,它的控制界面是比较丑陋的,当然我们一般在开发中是不会使用它的。而后面两种都可以自定义不妨界面,当然它们的区别是一个是SurfaceView,而另一个是TextureView。SurfaceView的原理就是在View的位置上重新创建一个Window,所有界面的绘制和渲染都是在新的Window中执行,不会影响主线程的执行,当然这也存在线程同步问题。另一个问题就是由于SurfaceView的显示是在新的Windows中,它不会受到View的属性控制,也不能放在RecycelerView或ListView中,也需要自己管理其生命周期。TextureView是在API14之后被加入的,和SurfaceView不同的是,TextureView不会在View的位置上创建一个新的窗口,所有的界面的绘制渲染都是在View上,这样就允许TextureView能够被移动,缩放或做些其它的动画。TextureView只能在硬件加速窗口中使用,当在软件中渲染时,TextureView将不会做任何绘制。

好了,说了这么多,决定用第三种封装自己的播放器,当然本文是参考这篇文章做的
用MediaPlayer+TextureView封装一个完美实现全屏、小窗口的视频播放器

TextureView使用

TexureView的使用是比较简单的,首先你需要做的就是获取它的SurfaceTexture,它能够被用来去渲染内容。它的使用大致如下

private TextureView mTextureView;

mTextureView = new TextureView(this);
mTextureView.setSurfaceTextureListener(this);

//监听回调
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
//SurfaceTexture准备就绪
}

public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
//SurfaceTexture缓冲大小变化
}

public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    //SurfaceTexture即将销毁
    return false;
}

public void onSurfaceTextureUpdated(SurfaceTexture surface) {
    //SurfaceTexture状态更新
}

TextureView不能直接被使用,只有当它attached到一个Window之后,SurfaceTexture准备就绪之后,TextureView才能起作用。当监听的回调onSurfaceTextureAvailable被调用后,可以通过传入的的surface关联Mediaplayer,SurfaceTexture作为数据通道,把从数据源Mediaplayer中获取到的图像帧数据转化为GL外部纹理,交给TextureView作为View heirachy中的一个硬件加速层来显示,从而实现视频播放的功能。

//关联Mediaplayer,之所以要做判断,是因为当mTextureView重新绘制之后,生命
//周期方法会被回调,监听器也会被回调,而mediaplayer不会被销毁任然持
//有SurfaceTexture的引用,所以当生命周期回调之后直接使用持有的
//SurfaceTexture,任然可以继续播放
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    if(mSurfaceTexture == null){
        mSurfaceTexture = surface;
        mediaPlayerStart();
    }else {
        mTextureView.setSurfaceTexture(mSurfaceTexture);
    }
}

代码设计

为了使业务逻辑分离解耦,将数据源和控制器部分分成两个类处理

//数据源播放
public class VideoPlayer extends FrameLayout implements TextureView.SurfaceTextureListener,
                                     VideoPlayerControl{}

//控制器部分
public class VideoPlayerController extends FrameLayout implements
        View.OnClickListener,
        SeekBar.OnSeekBarChangeListener,
        View.OnTouchListener{}

其中数据源部分专门负责播放视频,处理播放状态的初始化和状态变化,而控制器部分专门负责播放界面的 播放,暂停,全屏,小窗口 操作。而两个类之间通过一个接口VideoPlayerControl关联。

public interface VideoPlayerControl {

    void start();         //播放
    void pause();         //暂停
    void seekTo(int pos); //进度条拖动
    void restart();
    void release();

    boolean isIdle();  //是否是空闲
    boolean isError();
    boolean isPreparing();
    boolean isPrepared();
    boolean isBufferingPlaying();
    boolean isBufferingPaused();
    boolean isPlaying();
    boolean isPaused();
    boolean isCompleted();

    int getDuration();
    int getCurrentProgress();
    int getBufferPercent();
    FrameLayout getContainer();

    boolean isFullScreen();  //全屏
    boolean isNormalScreen();//普通窗口
    boolean isTinyScreen();  //小窗口

    void enterFullScreen();    //进入全屏
    boolean exitFullScreen();  //退出全屏
    void enterTinyScreen();    //进入小屏
    boolean exitTinyScreen();  //退出小屏
}

VideoPlayer

常量

定义几个常量来标注播放状态和界面窗口状态

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;       // 播放完成

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

mContainer

初始化界面时,mController和mTextureView是添加到mContainer中的,这样做的好处就是,方便移除和添加窗口,所有的操作只需要通过对mContainer操作来完成

关联视频播放器

public void setController(VideoPlayerController controller){
   mController = controller;
   mController.setVideoPlayer(this);
   updateVideoPlayerState();
   mContainer.removeView(mController);
   LayoutParams lp = new LayoutParams(
           ViewGroup.LayoutParams.MATCH_PARENT,
           ViewGroup.LayoutParams.MATCH_PARENT);
   mContainer.addView(mController, lp);
}

我们需要传入一个controller然后关联此VideoPlayer就可以了。然后添加到mContainer中就可以了。

设置视频播放uri

public void setPlayUri(String uri){
    mUri = uri;
}

MediaPlayer

要使用MediaPlayer需要设置几个监听器,

mMediaPlayer.setOnPreparedListener(mOnPreparedListener);   //mediaplayer准备就绪回调
mMediaPlayer.setOnCompletionListener(mOnCompletionListener); //mediaplayer播放完成监听
mMediaPlayer.setOnErrorListener(mOnErrorListener);   //播放错误监听回调
mMediaPlayer.setOnInfoListener(mOnInfoListener);     //播放器渲染状态变化监听
mMediaPlayer.setOnBufferingUpdateListener(mOnBufferingUpdateListener); //播放器缓冲进度0-100回调

初始化和播放

//初始化
private void mediaPlayerInit() {
    if(mMediaPlayer == null){
        mMediaPlayer = new MediaPlayer();

        mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        mMediaPlayer.setScreenOnWhilePlaying(true);  //在播放时屏幕一直开启着

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

//播放
private void mediaPlayerStart(){
    try {
        //设置数据源
        mMediaPlayer.setDataSource(mContext.getApplicationContext(), Uri.parse(mUri));
        //设置surface
        mMediaPlayer.setSurface(new Surface(mSurfaceTexture));
        //异步网络准备
        mMediaPlayer.prepareAsync();

        mCurrentState = STATE_PREPARING;
        updateVideoPlayerState();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

全屏和小窗口进入和退出

全屏和小窗口差不多,它们的实现原理大致是,先从自己的view中移除mContainer,然后设置LayoutParams,最后在android.R.id.content中添加mContainer就可以了。只要设置不同的lp参数就可以实现全屏和小窗口播放。退出的话就只需要移除然后添加到FrameLayout容器中就可以了。注意全屏要设置屏幕方向,通过setRequestedOrientation请求屏幕方向。ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE为横屏,SCREEN_ORIENTATION_PORTRAIT为竖屏。

注意: 变化的状态都可以通过updateVideoPlayerState()回调控制器更新窗口

 private void updateVideoPlayerState() {
    mController.setControllerState(mCurrentState, mWindowState);
 }

VideoPlayerController

这个类为自定义的一个控制器,所有播放器的操作都是通过这个控制器回调播放器VideoPlayer,这个控制器定义了一系列方法控制播放状态

setTitle 设置页面标题
setImage 设置封面背景
setTopBottomVisible 头部和底部是否隐藏
setControllerState 设置播放器工作状态
updateProgress 更新进度

同时做了一个小窗口界面拖拽

@Override
public boolean onTouch(View v, MotionEvent event) {

    if(!mVideoPlayerControl.isTinyScreen()) return super.onTouchEvent(event);

    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            startX = event.getRawX();
            startY = event.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:

            float endX = event.getRawX();
            float endY = event.getRawY();

            float dx = endX - startX;
            float dy = endY - startY;

            LayoutParams params = (LayoutParams) mVideoPlayerControl.getContainer().getLayoutParams();

            int left = (int) (params.leftMargin + dx);
            int top = (int) (params.topMargin + dy);
            int viewHeight = mVideoPlayerControl.getContainer().getHeight() + 50;
            int viewWidth = mVideoPlayerControl.getContainer().getWidth();

            if(left < -1){
                left = 0;
            }else if(left > screenWidth - viewWidth){
                left = screenWidth - viewWidth;
            }

            if(top < -1){
                top = 0;
            }else if(top > screenHeight - viewHeight){
                top = screenHeight - viewHeight;
            }

            params.leftMargin = left;
            params.topMargin = top;
            mVideoPlayerControl.getContainer().setLayoutParams(params);

            startX = endX;
            startY = endY;
            break;
    }

    return super.onTouchEvent(event);
}

比较简单,就不多解释了。

VideoPlayerManager

这个类的作用就是保证在recyclerview或者listview中播放时只有一个列表可以播放。

/**
 * 单例管理视频播放  在listview或者recyclerview中保证页面只有一个播放器在播放
 */
public class VideoPlayerManager {

    private VideoPlayer mVideoPlayer;

    private VideoPlayerManager(){}

    private static VideoPlayerManager sInstance;

    public static VideoPlayerManager getInstance(){

        if(sInstance == null){
            sInstance = new VideoPlayerManager();
        }

        return sInstance;
    }

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

    public void setCurrentVideoPlayer(VideoPlayer videoPlayer){
        mVideoPlayer = videoPlayer;
    }

    /**
     *  释放资源
     */
    public boolean onBackPress(){

        if(mVideoPlayer.isFullScreen()){
            mVideoPlayer.exitFullScreen();
            return true;
        }else if(mVideoPlayer.isTinyScreen()){
            mVideoPlayer.exitTinyScreen();  //退出小屏
            return true;
        }

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

        return false;
    }
}

用法

private VideoPlayer videoPlayer;
private VideoPlayerController mController;

videoPlayer = (VideoPlayer) findViewById(R.id.vp);
videoPlayer.setPlayUri("http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2017/05/2017-05-17_17-33-30.mp4");
mController = new VideoPlayerController(this);
mController.setTitle("办公室小野开番外了,居然在办公室开澡堂!老板还点赞?");
mController.setImage("http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2017/05/2017-05-17_17-30-43.jpg");
videoPlayer.setController(mController);

效果

这里写图片描述

源码:github

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容