作者:恋猫月亮
原文链接:http://www.jianshu.com/p/9fe377dd9750
Android 实现视屏播放器与边播边缓存功能外加蹲坑铲屎(IJKPlayer)
hello,大家好,我就是那个会掀桌子的话唠,刚刚结束两篇关于音频播放与录制的文章,旧坑未埋就挖新坑,还望多多关照。最近累趴了,周末果断休假。
快看,用力戳它:https://github.com/CarGuo/GSYVideoPlayer 。项目是翻改至JieCaoVideoPlayer,本文特长,看官请耐心,妹子会有的。
效果
开源播放器选择
Android上最为人熟知的MediaPlayer,对,就是这货,在上两篇音频文章中频频露脸的家伙,这次又有它的身影,然而还是这次不讲他,就连他的封装类VideoView也不讲,呸呸呸,又扯了一堆没用的。
集成工作还是有定的工作量的,它的DEMO肯定满足不了欲求不满的设计狮和产品汪的,这里我们不跑分,不打广告,不讲原理,只求站在巨人的肩膀上学(cao)习(xi),快速集成。
定义一个单例的视频内核播放管理器。
自定义一个满足你上下其手的TextureView
定义一个UI层级逻辑播放器
重力旋转的相关逻辑处理
列表逻辑的相关处理
列表到全屏相关的逻辑处理
视频缓存逻辑
1、播放管理器:GSYVideoManager
单例,没得商量,它需要负责真正的播放请求与显示逻辑,集成了IjkMediaPlayer,BILIBLI的开源小组还是很有心的,它的封装和接口使用基本和MediaPlayer没有什么区别,只需要用起来就好了。‘
这里我们要实现IjkMediaPlayer的播放接口,加小编微信:AMEPRE,监听IjkMediaPlayer的相关状态回调然后封发到各个逻辑播放器中。从下方代码可以看到,真的和MediaPlayer好像。
监听的回调接口里,大部分大家都耳目能详吧,没听过也没关系,都写上就对了,但是最主要需要关注的两个,一个是通过setOnVideoSizeChangedListener拿到视频宽和高,这是我们后续正常显示视频的依靠之一。
另外一个就是setOnInfoListener,这里我们主要是获取到视频相关的元信息里视频旋转角度!还记得那时候对视频播放不熟悉,和产品还有QA力争“这个视频本来就是转了90度的,我就不改,你咬我吗···”这样的黑历史。Σ( ° △ °|||)
特别是Android拍摄的竖屏视频,旋转不是视频本身的图像,而是增加了旋转信息,而这个时候你需要做的就是识别它,然后转了它丫的。另外,因为Android本身的MediaPlaer和VideoView自身就处理好所以不需要你旋转。((ノO益O)ノ彡┻━┻亲生的啊)
这里的接口主要是把当前播放的视频状态和信息到返回到逻辑播放器中。
2、自定义TextureView:GSYTextureView
为什么不用SurfaceView?因为TextureView很可爱啊。这里我们主要针对视频的大小和旋转角度设置TextureView的大小,加小编微信:AMEPRE,详细就不多说了(不是懒),挑其中一类讲讲,因为主要也是这个。
3、UI层级逻辑播放器 GSYVideoPlayer
所有的UI逻辑基本都可以写到这里,目前继承了 FrameLayout,View.OnClickListener, View.OnTouchListener, SeekBar.OnSeekBarChangeListener, TextureView.SurfaceTextureListener和GSYMediaPlayerListener。
逻辑播放器实现的内容太多了,这里主要说几个地方,好吧,我承认我懒╮(╯_╰)╭ ,但是写太多了也没人看啊,所以这里主要是说一些关键的点,有需要留言再开个坑聊一聊,反正有DEMO。
在逻辑播放器中统一分发各种状态,把被播放的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;
}
···
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执行逻辑
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;
}
//这里其实就有播放管理器的监听分发保存的逻辑需要注意
GSYVideoManager.instance().setLastListener(this);
GSYVideoManager.instance().setListener(gsyVideoPlayer);
3、列表全屏逻辑 :Window层级的全屏、单例逻辑播放器的全屏ListVideoUtil。
效果GIF(比较大):
1)、Window层级的
传闻每一个Activity都有一个com.android.internal.R.id.content,它默默的包含了各种你塞进去的物体,而且是一个FrameLayout,谷歌有太多它的传说了,我们用它是就是。
既然是FrameLayout,那么我们往他里面塞东西就好了,这里我们可以在GSYVideoPlayer里面写一个方法,在点击全屏按钮的时候:
ConstructorGSYBaseVideoPlayer> constructor = (ConstructorGSYBaseVideoPlayer>) 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);
}
});
4、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) {
if (!(mIsLand == 1) && !mClickPort) {
return;
} else {
mClickLand = true;
mClick = false;
mIsLand = 1;
}
} else {
if (!(mIsLand == 1)) {
screenType = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
gsyVideoPlayer.getFullscreenButton().setImageResource(R.drawable.video_shrink);
mIsLand = 1;
mClick = false;
}
}
}
// 设置反向横屏
else if (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();
6、边播边缓存
好吧,老婆睡了,我偷偷起来了(。・・)ノ
这个需求曾经让我彻夜难眠,因为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。
7、一些坑和说明
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级:https://github.com/CarGuo/GSYVideoPlayer
能看到这里都是真爱啊,我最后问两句,你们会觉得文章太长阅读起来比较费劲吗?
友情链接:
GSYVideoPlayer(https://github.com/CarGuo/GSYVideoPlayer)
ijkplayer(https://github.com/Bilibili/ijkplayer)
AndroidVideoCache(https://github.com/danikula/AndroidVideoCache)
JieCaoVideoPlayer(https://github.com/lipangit/JieCaoVideoPlayer)
关于Java和Android大牛频道
Java和Android大牛频道是一个数万人关注的探讨Java和Android开发的公众号,分享和原创最有价值的干货文章,让你成为这方面的大牛!
我们探讨android和Java开发最前沿的技术:android性能优化 ,插件化,跨平台,动态化,加固和反破解等,也讨论设计模式/软件架构等。由一群来自BAT的工程师组成的团队。
关注即送红包,回复:“百度” 、“阿里”、“腾讯” 有惊喜!!!关注后可用入微信群。群里都是来自百度阿里腾讯的大牛。
欢迎关注我们,一起讨论技术,扫描和长按下方的二维码可快速关注我们。或搜索微信公众号:JANiubility。
公众号:JANiubility