Android 实现视屏播放器、边播边缓存功能、外加铲屎(IJKPlayer)

Android 实现视屏播放器与边播边缓存功能外加蹲坑铲屎(IJKPlayer)android

 hello,你们好,我就是那个会掀桌子的话唠,刚刚结束两篇关于音频播放与录制的文章,旧坑未埋就挖新坑,还望多多关照。最近累趴了,周末果断休假。

快看,用力戳它:github.com/CarGuo/GSYV… 。项目是翻改至JieCaoVideoPlayer,本文特长,看官请耐心,妹子会有的。

效果
git

开源播放器选择

 Android上最为人熟知的MediaPlayer,对,就是这货,在上两篇音频文章中频频露脸的家伙,此次又有它的身影,然而仍是此次不讲他,就连他的封装类VideoView也不讲<( ̄︶ ̄)>,呸呸呸,又扯了一堆没用的。 github

  • ijkplayer,此次要推荐的是它,鼎鼎大名的BILIBILI开源的播放器。基于FFMPEG,支持Android与IOS,还封装了谷歌亲儿子MediaPlayer与干儿子EXOPlayer(为何要用EXO),支持直播流,Star-9000多与fork-3000的视频播放器你支持安利。(issues 600多算活跃吗┑( ̄Д  ̄)┍)

 集成工做仍是有定的工做量的,它的DEMO确定知足不了欲求不满的设计狮和产品汪的,这里咱们不跑分,不打广告,不讲原理,只求站在巨人的肩膀上学(cao)习(xi),快速集成。api

  • 定义一个单例的视频内核播放管理器。
  • 自定义一个知足你上下其手的TextureView
  • 定义一个UI层级逻辑播放器
  • 重力旋转的相关逻辑处理
  • 列表逻辑的相关处理
  • 列表到全屏相关的逻辑处理
  • 视频缓存逻辑

一、播放管理器:GSYVideoManager

 单例,没得商量,它须要负责真正的播放请求与显示逻辑,集成了IjkMediaPlayer,BILIBLI的开源小组仍是颇有心的,它的封装和接口使用基本和MediaPlayer没有什么区别,只须要用起来就行了。‘缓存

 这里咱们要实现IjkMediaPlayer的播放接口,监听IjkMediaPlayer的相关状态回调而后封发到各个逻辑播放器中。从下方代码能够看到,真的和MediaPlayer好像。服务器

mediaPlayer = new IjkMediaPlayer();
//音频类型
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
//数据源
mediaPlayer.setDataSource(((GSYModel) msg.obj).getUrl(), ((GSYModel) msg.obj).getMapHeadData());
//播放完成
mediaPlayer.setOnCompletionListener(GSYVideoManager.this);
//缓冲
mediaPlayer.setOnBufferingUpdateListener(GSYVideoManager.this);
//常亮
mediaPlayer.setScreenOnWhilePlaying(true);
//加载完毕
mediaPlayer.setOnPreparedListener(GSYVideoManager.this);
//拖动
mediaPlayer.setOnSeekCompleteListener(GSYVideoManager.this);
//失败
mediaPlayer.setOnErrorListener(GSYVideoManager.this);
//视频相关信息-重要
mediaPlayer.setOnInfoListener(GSYVideoManager.this);
//视频大小
mediaPlayer.setOnVideoSizeChangedListener(GSYVideoManager.this);】
//开始加载
mediaPlayer.prepareAsync();复制代码

 监听的回调接口里,大部分你们都耳目能详吧,没听过也不要紧,都写上就对了,可是最主要须要关注的两个,一个是经过setOnVideoSizeChangedListener拿到视频宽和高,这是咱们后续正常显示视频的依靠之一。ide

 另一个就是setOnInfoListener,这里咱们主要是获取到视频相关的元信息里视频旋转角度!还记得那时候对视频播放不熟悉,和产品还有QA力争“这个视频原本就是转了90度的,我就不改,你咬我吗···”这样的黑历史。Σ( ° △ °|||)工具

 特别是Android拍摄的竖屏视频,旋转不是视频自己的图像,而是增长了旋转信息,而这个时候你须要作的就是识别它,而后转了它丫的。另外,由于Android自己的MediaPlaer和VideoView自身就处理好因此不须要你旋转。((ノO益O)ノ彡┻━┻亲生的啊)布局

 这里的接口主要是把当前播放的视频状态和信息到返回到逻辑播放器中。post

@Override
public void onInfo(int what, int extra) {
    if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) {
        BACKUP_PLAYING_BUFFERING_STATE = mCurrentState;
        setStateAndUi(CURRENT_STATE_PLAYING_BUFFERING_START);
    } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) {
        if (BACKUP_PLAYING_BUFFERING_STATE != -1) {
            setStateAndUi(BACKUP_PLAYING_BUFFERING_STATE);
            BACKUP_PLAYING_BUFFERING_STATE = -1;
        }
    } else if (what == IMediaPlayer.MEDIA_INFO_VIDEO_ROTATION_CHANGED) {
        //这里返回了视频旋转的角度,根据角度旋转视频到正确的画面
        mRotate = extra;
        if (mTextureView != null)
            mTextureView.setRotation(mRotate);
    }
}复制代码

二、自定义TextureView:GSYTextureView

 为何不用SurfaceView?由于TextureView很可爱啊。这里咱们主要针对视频的大小和旋转角度设置TextureView的大小,详细就很少说了(不是懒),挑其中一类讲讲,由于主要也是这个。

  • 例如根据视频的长宽比和屏幕的长宽比判断,若是视频宽与屏幕宽之比小于高之比,那么就须要按理比压缩宽度,而后高度适应屏幕。  
  • 例如根据旋转信息,判断TextureView界面的比例是横的仍是竖的,若是View是竖的,而视频也是竖的,那么由于旋转了90度,那么让视频的高显示为屏幕的宽度,重新计算旋转后的宽度。

以为看起来有点绕口?不要紧,用着用着就习惯了····

width = widthSpecSize;
height = heightSpecSize;
···
if (videoWidth * height < width * videoHeight) {
    width = height * videoWidth / videoHeight;
} else if (videoWidth * height > width * videoHeight) {
    height = width * videoHeight / videoWidth;
}
···
if (getRotation() != 0 && getRotation() % 90 == 0) {
    if (widthS < heightS) {
        if (width > height) {
            width = (int) (width * (float) widthS / height);
            height = widthS;
        } else {
            height = (int) (height * (float) width / widthS);
            width = widthS;
        }
    } else {
        if (width > height) {
            height = (int) (height * (float) width / widthS);
            width = widthS;
        } else {
            width = (int) (width * (float) widthS / height);
            height = widthS;
        }
    }
}复制代码

三、UI层级逻辑播放器 GSYVideoPlayer

 全部的UI逻辑基本均可以写到这里,目前继承了 FrameLayout,View.OnClickListener, View.OnTouchListener, SeekBar.OnSeekBarChangeListener, TextureView.SurfaceTextureListener和GSYMediaPlayerListener。
 
 逻辑播放器实现的内容太多了,这里主要说几个地方,好吧,我认可我懒╮(╯_╰)╭ ,可是写太多了也没人看啊,因此这里主要是说一些关键的点,有须要留言再开个坑聊一聊,反正有DEMO。

  • 记录界面的播放状态,把播放管理器GSYVideoManager的状态记录下来,若是有别的逻辑播放器点击播放了,就把本来的逻辑播放器状态清空,全部逻辑播放器的整个界面的UI都是根据这个State来决定的。

 在逻辑播放器中统一分发各类状态,把被播放的manager状态同步到这里,以后你想要在哪一个逻辑播放器里播放只须要对应的设置状态后把manager的监听同步过来。

switch (mCurrentState) {
    //正常初始化状态
    case CURRENT_STATE_NORMAL:
        if (isCurrentMediaListener()) {
            cancelProgressTimer();
            GSYVideoManager.instance().releaseMediaPlayer();
        }
        break;

    //loading中
    case CURRENT_STATE_PREPAREING:
        resetProgressAndTime();
        break;
    //播放中
    case CURRENT_STATE_PLAYING:
        startProgressTimer();
        break;
    //暂停
    case CURRENT_STATE_PAUSE:
        startProgressTimer();
        break;
    //错误-须要判断是否切换了逻辑播放器
    case CURRENT_STATE_ERROR:
        if (isCurrentMediaListener()) {
            GSYVideoManager.instance().releaseMediaPlayer();
        }
        break;
    //结束
    case CURRENT_STATE_AUTO_COMPLETE:
        cancelProgressTimer();
        mProgressBar.setProgress(100);
        mCurrentTimeTextView.setText(mTotalTimeTextView.getText());
        break;
}复制代码
  • 增长界面的onTouch事件,根据ViewgetId判断触摸的是进度条仍是界面,若是是界面判断是左右滑动就显示DialogseekTo,若是是上下就根据屏幕的左边仍是右边来选择是调节音量仍是亮度
···
case MotionEvent.ACTION_MOVE:
    float deltaX = x - mDownX;
    float deltaY = y - mDownY;
    float absDeltaX = Math.abs(deltaX);
    float absDeltaY = Math.abs(deltaY);
    //是全屏仍是设置了能够触摸
    if (mIfCurrentIsFullscreen || mIsTouchWiget) {
        //以前是否已经符合了触摸逻辑条件
        if (!mChangePosition && !mChangeVolume && !mBrightness) {
            //若是手指动了超过必定距离就能够判断是滑动,防止点击的误判的
            if (absDeltaX > mThreshold || absDeltaY > mThreshold) {
                cancelProgressTimer();
                //若是是左右的就是进度
                if (absDeltaX >= mThreshold) {
                    mChangePosition = true;
                    mDownPosition = getCurrentPositionWhenPlaying();
                    if (mVideoAllCallBack != null && isCurrentMediaListener()) {
                        mVideoAllCallBack.onTouchScreenSeekPosition(mUrl, mObjects);
                    }
                } else {

                //若是是上下的判断是左边仍是右边
                    if (mFirstTouch) {
                        mBrightness = mDownX < mScreenWidth * 0.5f;
                        mFirstTouch = false;
                    }
                    if (!mBrightness) {
                        mChangeVolume = true;
                        mGestureDownVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
                        if (mVideoAllCallBack != null && isCurrentMediaListener()) {
                            mVideoAllCallBack.onTouchScreenSeekVolume(mUrl, mObjects);
                        }
                    }
                }
            }
        }
    }
    ···
    //根据flag执行逻辑复制代码
  • 要监听TextureView.setSurfaceTextureListener来通知画面的建立和销毁,好比回到后台,onPause等。

这里有一个是TextureView的动态添加,动态添加的好处是你能够在不中止视频的状况下载不一样的逻辑播放器中切换视频播放,好比列表全屏。

protected void addTextureView() {
    if (mTextureViewContainer.getChildCount() > 0) {
        mTextureViewContainer.removeAllViews();
    }
    mTextureView = null;
    mTextureView = new GSYTextureView(getContext());
    mTextureView.setSurfaceTextureListener(this);
    mTextureView.setRotation(mRotate);

    RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
    mTextureViewContainer.addView(mTextureView, layoutParams);
}

···

//把Surface丢给视频播放管理
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    mSurface = new Surface(surface);
    GSYVideoManager.instance().setDisplay(mSurface);
}

//告诉视频播放渲染画面销毁了
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    GSYVideoManager.instance().setDisplay(null);
    surface.release();
    return true;
}复制代码
  • 每次播放都要把Manager的player的监听移到当前播放的逻辑播放器,这样才可以正确的监听视频的播放状态。
//这里其实就有播放管理器的监听分发保存的逻辑须要注意
GSYVideoManager.instance().setLastListener(this);
GSYVideoManager.instance().setListener(gsyVideoPlayer);复制代码

三、列表全屏逻辑 :Window层级的全屏、单例逻辑播放器的全屏ListVideoUtil。


效果GIF(比较大):

1)、Window层级的

 
 传闻每个Activity都有一个com.android.internal.R.id.content,它默默的包含了各类你塞进去的物体,并且是一个FrameLayout,谷歌有太多它的传说了,咱们用它是就是。

 既然是FrameLayout,那么咱们往他里面塞东西就行了,这里咱们能够在GSYVideoPlayer里面写一个方法,在点击全屏按钮的时候:

  • 隐藏状态栏,清除当前TextureView。
  • 而后新建立一个GSYVideoPlayer2,只有把这个G2添加到window下FrameLayout
  • 设置它的播放状态和当前列表这个逻辑播放器一致。
  • 最后把G2告知Manager承接画面,这样是就实现了无缝的列表到全屏啦,返回只须要倒着作就行了。

 
 在切换的时候能够作一些位移动画,让播放器的全屏更加友好,下面长代码来袭((/- -)/。深夜码字不易,不知道为何每次这个时候老婆的意见很大啊。

Constructor
  
  
  

 
  
  constructor = (Constructor 
 
  
    ) GSYBaseVideoPlayer.this.getClass().getConstructor(Context.class); final GSYBaseVideoPlayer gsyVideoPlayer = constructor.newInstance(getContext()); //记录新建立的这个video的id,在返回的时候经过它销毁 gsyVideoPlayer.setId(FULLSCREEN_ID); WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); final int w = wm.getDefaultDisplay().getWidth(); final int h = wm.getDefaultDisplay().getHeight(); //设置黑色背景,自动充满全屏 FrameLayout.LayoutParams lpParent = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); FrameLayout frameLayout = new FrameLayout(context); frameLayout.setBackgroundColor(Color.BLACK); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //若是5.0的话,先让播放器出现的位置和列表中一直,再样式一会执行到屏幕中间的过分动画效果 FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(getWidth(), getHeight()); lp.setMargins(mListItemRect[0], mListItemRect[1], 0, 0); frameLayout.addView(gsyVideoPlayer, lp); vp.addView(frameLayout, lpParent); mHandler.postDelayed(new Runnable() { @Override public void run() { TransitionManager.beginDelayedTransition(vp); resolveFullVideoShow(context, gsyVideoPlayer, h, w); } }, 300); } else { //5.0一下直接显示 FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(getWidth(), getHeight()); frameLayout.addView(gsyVideoPlayer, lp); vp.addView(frameLayout, lpParent); resolveFullVideoShow(context, gsyVideoPlayer, h, w); } //设置全屏逻辑播放器的状态,动态及添加播放view gsyVideoPlayer.setUp(mUrl, mCache, mObjects); gsyVideoPlayer.setStateAndUi(mCurrentState); gsyVideoPlayer.addTextureView(); //添加监听 GSYVideoManager.instance().setLastListener(this); GSYVideoManager.instance().setListener(gsyVideoPlayer); 
   

 复制代码
2)、ListVideoUtil的单例模式

 这里利用另一种实现思路,列表的逻辑播放器只用一个,由于普通的list在滑动的时候会有复用和销毁,这会致使视频被释放而中止了,若是你是和今日黄(tou)条同样的视频列表播放效果,滑出屏幕就中止那无所谓。

 若是你须要不管怎么滑动,视频都在原来的位置播放的话,那么ListVideoUtil适合你,,内部它已经带了全屏,防错位,旋转的各类逻辑,直接上代码,有兴趣的看DEMO。

listVideoUtil = new ListVideoUtil(this);
//设置列表最外层的布局用于全屏,空FrameLayout
listVideoUtil.setFullViewContainer(videoFullContainer);
//全屏隐藏状态栏,若是有的话
listVideoUtil.setHideStatusBar(true);

···
//在列表中吧列表位置,封面,哪一个列表的TAG,列表视频的承载ViewGroup,播放按键传入到Utils中
listVideoUtil.addVideoPlayer(position, imageView, TAG, holder.videoContainer, holder.playerBtn);
holder.playerBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //每次播放都要更新列表让其余的item恢复状态
        notifyDataSetChanged();
        //设置播放的tag和位置,防止错位
        listVideoUtil.setPlayPositionAndTag(position, TAG);
        //开始播放
        final String url = "http://baobab.wdjcdn.com/14564977406580.mp4";
        listVideoUtil.startPlay(url);
    }
});复制代码

四、OrientationUtils 重力旋转的工具类

OrientationUtils使用的是OrientationEventListener,经过手机的角度判断须要旋转到哪一个位置。为何用它?由于谷歌到的时候恰好看到,缘分啊懂吗。

这里须要个关注的是手动点击和自动旋转之间的冲突,主要看代码吧,老婆开始催我了 (ノಠ益ಠ)ノ彡┻━┻。

//判断系统是否开了旋转,是的,这货不须要系统旋转是否开启
boolean autoRotateOn = (android.provider.Settings.System.getInt(activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 1);
        if (!autoRotateOn) {
            if (mIsLand == 0) {
                return;
            }
        }
        // 设置竖屏
        if (((rotation >= 0) && (rotation <= 30))="" ||="" (rotation="">= 330)) {
            //是否点击致使的
            if (mClick) {
                if (mIsLand > 0 && !mClickLand) {
                    return;
                } else {
                    //清除状态
                    mClickPort = true;
                    mClick = false;
                    mIsLand = 0;
                }
            } else {
                //自动旋转
                if (mIsLand > 0) {
                    screenType = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
                    activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
                    gsyVideoPlayer.getFullscreenButton().setImageResource(R.drawable.video_enlarge);
                    mIsLand = 0;
                    mClick = false;
                }
            }
        }
        // 设置横屏
        else if (((rotation >= 230) && (rotation <= 310)))="" {="" if="" (mclick)="" (!(misland="=" 1)="" &&="" !mclickport)="" return;="" }="" else="" mclickland="true;" mclick="false;" misland="1;" 1))="" screentype="ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;" activity.setrequestedorientation(activityinfo.screen_orientation_landscape);="" gsyvideoplayer.getfullscreenbutton().setimageresource(r.drawable.video_shrink);="" 设置反向横屏="" (rotation=""> 30 && rotation < 95) {
            if (mClick) {
                if (!(mIsLand == 2) && !mClickPort) {
                    return;
                } else {
                    mClickLand = true;
                    mClick = false;
                    mIsLand = 2;
                }
            } else if (!(mIsLand == 2)) {
                screenType = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
                gsyVideoPlayer.getFullscreenButton().setImageResource(R.drawable.video_shrink);
                mIsLand = 2;
                mClick = false;
            }
        }
    }
};
orientationEventListener.enable();
  
  
  

 
  
  
  

 复制代码

六、边播边缓存

 好吧,老婆睡了,我偷偷起来了(。・・)ノ
 这个需求曾经让我彻夜难眠,由于IJKPlayer不支持,好吧,没见过哪一个播放器支持的,和产品争(tuo)论(yan)需(shi)求(jian)以后,最终仍是github大法好:AndroidVideoCache

 接入简单,使用简单,你能够趾高气扬的和产品说,这个so easy了。

HttpProxyCacheServer proxy = getProxy();
//注意不能传入本地路径,本地的你还传进来干吗。
String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
videoView.setVideoPath(proxyUrl);复制代码

 该项目的原理其实就是将url连接转化为本地连接 h t t p://127.0.0.1:LocalPort/url,而后它开一个服务器一边下载缓存视频,一边把缓存的数据正常返回给你的播放器,若是已经缓存过的这里会返回一个本地文件路径。Σ( ° △ °|||)︴曾经的我真的是too young too smiple。

五、一些坑和说明

  • 一、IJKPLAY的后台播放和回到前台恢复画面的速度之快是其余播放器(我坐井观天)没法比拟的,真的好快,并且适合你,由于你什么都不用作。

  • 二、IJKPLAY有一个问题,我也提过ISSUSE了 #2104,不过目前还未解决,就是某些短小的视频会没法seekTo,说是FFMEPG的问题,而后就太监了。

  • 三、IJKPLAY库里还封装了exoplayer谷歌干儿子,用法也基本一致,这个播放器本身内部判断旋转,不会有上面的seekto问题,但是后台或者onPause以后的画面恢复速度堪忧啊,各位遇到过吗?

  • 四、千万别开硬解码,否则会这样。 ( ‵o′)凸

  • 五、拖动进度条,须要在中止拖动的时候,判断视频是否是已经播放完了被释放了。

  • 六、若是横屏全屏的话,恢复到正常画面是最好有一个延时,这样画面才不会出现背景抖动的问题,还有最关键的,Maifest文件。

//不要忘记配置activity,全部背景的activity

android:configChanges="orientation|keyboardHidden|screenSize"复制代码
  • 七、普通列表中播放视频在快速移动可能出现的错位问题
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    int lastVisibleItem = firstVisibleItem + visibleItemCount;
    //大于0说明有播放
    if (GSYVideoManager.instance().getPlayPosition() >= 0) {
        //当前播放的位置
        int position = GSYVideoManager.instance().getPlayPosition();
        //对应的播放列表TAG
        if (GSYVideoManager.instance().getPlayTag().equals(ListNormalAdapter.TAG)
                && (position < firstVisibleItem || position > lastVisibleItem)) {
            //若是滑出去了上面和下面就是否,和今日头条同样
            GSYVideoPlayer.releaseAllVideos();
            listNormalAdapter.notifyDataSetChanged();
        }
    }
}复制代码

到底了呢(^o^)/。

下面的的看到了吗 ?<( ̄︶ ̄)>

点我点我上60级:github.com/CarGuo/GSYV…


能看到这里都是真爱啊,我最后问两句,大家会以为文章太长阅读起来比较费劲吗?

友情连接:

相关文章
相关标签/搜索