VideoPlayerDemo

项目地址:Zhaoss/VideoPlayerDemo 

简介:自定义高性能播放器, 实现边下边播缓存等功能

更多:作者   提 Bug

标签:

本项目使用播放器是ijkplay, 并且进行封装和修改

主要功能:
1.重新编辑 ijkplay 的 so 库, 使其更精简和支持 https 协议
2.自定义 MediaDataSource, 使用 okhttp 重写网络框架, 网络播放更流畅
3.实现视频缓存, 并且自定义 LRUCache 算法管理缓存文件
4.全局使用一个播放器, 实现视频在多个 Activity 之前无缝切换, 流畅播放
5.加入更多兼容性判断, 适配绝大数机型

①导入 ijkplay:

//需要的权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>首先将 lib 文件夹下的 so 库粘贴过来, (因为官方自带的 so 库是不支持 https 的, 我重新编译的这个 so 库支持 https 协议,
并且使用的是精简版的配置, 网上关于 ijkplay 编译的流程和配置挺多的, 可以根据自己的需求自定义)然后在 module 的 build 中加入 "implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8'"

②使用播放器的方法:

1.我封装了一个 MediaPlayerTool 工具类包含的初始化 so 库和一些回调等等

 //通过单例得到媒体播放工具mMediaPlayerTool = MediaPlayerTool.getInstance();//这里会自动初始化 so 库 有些手机会找不到 so, 会自动使用系统的播放器private MediaPlayerTool(){try {IjkMediaPlayer.loadLibrariesOnce(null);IjkMediaPlayer.native_profileBegin("libijkplayer.so");loadIjkSucc = true;}catch (UnsatisfiedLinkError e){e.printStackTrace();loadIjkSucc = false;}
}//一些生命周期回调
public static abstract class VideoListener {//视频开始播放public void onStart(){};//视频被停止播放public void onStop(){};//视频播放完成public void onCompletion(){};//视频旋转角度参数初始化完成public void onRotationInfo(int rotation){};//播放进度 0-1public void onPlayProgress(long currentPosition){};//缓存速度 1-100public void onBufferProgress(int progress){};}

2.因为我使用的是 RecyclerView,所以先找到当前屏幕中 处于可以播放范围的 item

    //首先循环 RecyclerView 中所有 itemView, 找到在屏幕可见范围内的 itemprivate void checkPlayVideo(){currentPlayIndex = 0;videoPositionList.clear();int childCount = rv_video.getChildCount();for (int x = 0; x < childCount; x++) {View childView = rv_video.getChildAt(x);//isPlayRange()这个方法很重要boolean playRange = isPlayRange(childView.findViewById(R.id.rl_video), rv_video);if(playRange){int position = rv_video.getChildAdapterPosition(childView);if(position>=0 && !videoPositionList.contains(position)){videoPositionList.add(position);}}}}//检查当前 item 是否在 RecyclerView 可见的范围内private boolean isPlayRange(View childView, View parentView){if(childView==null || parentView==null){return false;}int[] childLocal = new int[2];childView.getLocationOnScreen(childLocal);int[] parentLocal = new int[2];parentView.getLocationOnScreen(parentLocal);boolean playRange = childLocal[1]>=parentLocal[1] &&childLocal[1]<=parentLocal[1]+parentView.getHeight()-childView.getHeight();return playRange;}

3.我还封装了一个 TextureView, 里面包含一些初始化 SurfaceTexture 和视频裁剪播放的方法

    //视频居中播放private void setVideoCenter(float viewWidth, float viewHeight, float videoWidth, float videoHeight){Matrix matrix = new Matrix();float sx = viewWidth/videoWidth;float sy = viewHeight/videoHeight;float maxScale = Math.max(sx, sy);matrix.preTranslate((viewWidth - videoWidth) / 2, (viewHeight - videoHeight) / 2);matrix.preScale(videoWidth/viewWidth, videoHeight/viewHeight);matrix.postScale(maxScale, maxScale, viewWidth/2, viewHeight/2);mTextureView.setTransform(matrix);mTextureView.postInvalidate();}//初始化 SurfaceTexturepublic SurfaceTexture newSurfaceTexture(){int[] textures = new int[1];GLES20.glGenTextures(1, textures, 0);int texName = textures[0];SurfaceTexture surfaceTexture = new SurfaceTexture(texName);surfaceTexture.detachFromGLContext();return surfaceTexture;}

4.接下来就是播放代码了

private void playVideoByPosition(int position){//根据传进来的 position 找到对应的 ViewHolderfinal MainAdapter.MyViewHolder vh = (MainAdapter.MyViewHolder)       rv_video.findViewHolderForAdapterPosition(position);if(vh == null){return ;}currentPlayView = vh.rl_video;//初始化一些播放状态, 如进度条,播放按钮,加载框等//显示正在加载的界面vh.iv_play_icon.setVisibility(View.GONE);vh.pb_video.setVisibility(View.VISIBLE);vh.iv_cover.setVisibility(View.VISIBLE);vh.tv_play_time.setText("");//初始化播放器mMediaPlayerTool.initMediaPLayer();mMediaPlayerTool.setVolume(0);//设置视频 urlString videoUrl = dataList.get(position).getVideoUrl();mMediaPlayerTool.setDataSource(videoUrl);myVideoListener = new MediaPlayerTool.VideoListener() {@Overridepublic void onStart() {//将播放图标和封面隐藏vh.iv_play_icon.setVisibility(View.GONE);vh.pb_video.setVisibility(View.GONE);//防止闪屏vh.iv_cover.postDelayed(new Runnable() {@Overridepublic void run() {vh.iv_cover.setVisibility(View.GONE);}}, 300);}@Overridepublic void onStop() {//播放停止vh.pb_video.setVisibility(View.GONE);vh.iv_cover.setVisibility(View.VISIBLE);vh.iv_play_icon.setVisibility(View.VISIBLE);vh.tv_play_time.setText("");currentPlayView = null;}@Overridepublic void onCompletion() {//播放下一个currentPlayIndex++;playVideoByPosition(-1);}@Overridepublic void onRotationInfo(int rotation) {//设置旋转播放vh.playTextureView.setRotation(rotation);}@Overridepublic void onPlayProgress(long currentPosition) {//显示播放时长String date = MyUtil.fromMMss(mMediaPlayerTool.getDuration() - currentPosition);vh.tv_play_time.setText(date);}};mMediaPlayerTool.setVideoListener(myVideoListener);//这里重置一下 TextureViewvh.playTextureView.resetTextureView();mMediaPlayerTool.setPlayTextureView(vh.playTextureView);mMediaPlayerTool.setSurfaceTexture(vh.playTextureView.getSurfaceTexture());//准备播放mMediaPlayerTool.prepare();}

③重写 MediaDataSource, 使用 okhttp 实现边下边播和视频缓存

1.一共需要重写 3 个方法 getSize(),close()和 readAt(); 先说 getSize()

    public long getSize() throws IOException {//开始播放时, 播放器会调用一下 getSize()来初始化视频大小, 这时我们就要初始化一条视频播放流if(networkInPutStream == null) {initInputStream();}return contentLength;}//初始化一个视频流出来, 可能是本地或网络private void initInputStream() throws IOException{File file = checkCache(mMd5);if(file != null){//更新一下缓存文件VideoLRUCacheUtil.updateVideoCacheBean(mMd5, file.getAbsolutePath(), file.length());//读取的本地缓存文件isCacheVideo = true;localVideoFile = file;//开启一个本地视频流localStream = new RandomAccessFile(localVideoFile, "rw");contentLength = file.length();}else {//没有缓存 开启一个网络流, 并且开启一个缓存流, 实现视频缓存isCacheVideo = false;//开启一个网络视频流networkInPutStream = openHttpClient(0);//要写入的本地缓存文件localVideoFile = VideoLRUCacheUtil.createCacheFile(MyApplication.mContext, mMd5, contentLength);//要写入的本地缓存视频流localStream = new RandomAccessFile(localVideoFile, "rw");}}

2.然后是 readAt()方法, 也是最重要的一个方法

    /*** @param position 视频流读取进度* @param buffer 要把读取到的数据存到这个数组* @param offset 数据开始写入的坐标* @param size 本次一共读取数据的大小* @throws IOException*///记录当前读取流的索引long mPosition = 0;@Overridepublic int readAt(long position, byte[] buffer, int offset, int size) throws IOException {if(position>=contentLength || localStream==null){return -1;}//是否将此字节缓存到本地boolean isWriteVideo = syncInputStream(position);//读取的流的长度不能大于 contentLengthif (position+size > contentLength) {size -= position+size-contentLength;}//读取指定大小的视频数据byte[] bytes;if(isCacheVideo){//从本地读取bytes = readByteBySize(localStream, size);}else{//从网络读取bytes = readByteBySize(networkInPutStream, size);}if(bytes != null) {//写入到播放器的数组中System.arraycopy(bytes, 0, buffer, offset, size);if (isWriteVideo && !isCacheVideo) {//将视频缓存到本地localStream.write(bytes);}//记录数据流读取到哪步了mPosition += size;}return size;}/*** 从 inputStream 里读取 size 大小的数据*/private byte[] readByteBySize(InputStream inputStream, int size) throws IOException{ByteArrayOutputStream out = new ByteArrayOutputStream();byte[] buf = new byte[size];int len;while ((len = inputStream.read(buf)) != -1) {out.write(buf, 0, len);if (out.size() == size) {return out.toByteArray();} else {buf = new byte[size - out.size()];}}return null;}/*** 删除 file 一部分字节, 从 position 到 file.size*/private void deleteFileByPosition(long position) throws IOException{FileInputStream in = new FileInputStream(localVideoFile);File tempFile = VideoLRUCacheUtil.createTempFile(MyApplication.mContext);FileOutputStream out = new FileOutputStream(tempFile);byte[] buf = new byte[8192];int len;while ((len = in.read(buf)) != -1) {if(position <= len){out.write(buf, 0, (int) position);out.close();in.close();localVideoFile.delete();tempFile.renameTo(localVideoFile);localStream = new RandomAccessFile(localVideoFile, "rw");return ;}else{position -= len;out.write(buf, 0, len);}}tempFile.delete();}

3.主要说一下 syncInputStream(), 因为有可能出现一种情况, 比如一个视频长度 100, 播放器首先读取视频的 1 到 10 之间的数据, 然后在读取 90 到 100 之间的数据, 然后在从 1 播放到 100; 所以这时我们需要同步视频流, 和播放进度保持一致这时就需要重新开启一个 IO 流(如果在读取本地缓存时可以直接使用 RandomAccessFile.seek()方法跳转)

 //同步数据流private boolean syncInputStream(long position) throws IOException{boolean isWriteVideo = true;//判断两次读取数据是否连续if(mPosition != position){if(isCacheVideo){//如果是本地缓存, 直接跳转到该索引localStream.seek(position);}else{if(mPosition > position){//同步本地缓存流localStream.close();deleteFileByPosition(position);localStream.seek(position);}else{isWriteVideo = false;}networkInPutStream.close();//重新开启一个网络流networkInPutStream = openHttpClient((int) position);}mPosition = position;}return isWriteVideo;}

4.最后一个是 close()方法, 主要播放停止后释放一些资源

public void close() throws IOException {if(networkInPutStream != null){networkInPutStream.close();networkInPutStream = null;}if(localStream != null){localStream.close();localStream = null;}if(localVideoFile.length()!=contentLength){localVideoFile.delete();}}

④视频缓存和 LRUCache 管理

1.首先创建缓存文件, 在刚才的 MediaDataSource.getSize()方法里有一句代码

localVideoFile = VideoLRUCacheUtil.createCacheFile(MyApplication.mContext, mMd5, contentLength);public static File createCacheFile(Context context, String md5, long fileSize){//创建一个视频缓存文件, 在 data/data 目录下File filesDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);File cacheFile = new File(filesDir, md5);if(!cacheFile.exists()) {cacheFile.createNewFile();}//将缓存信息存到数据库VideoLRUCacheUtil.updateVideoCacheBean(md5, cacheFile.getAbsolutePath(), fileSize);return cacheFile;}

2.然后是读取缓存文件, 在刚才的 MediaDataSource.getSize()方法里还有一句代码

 //检查本地是否有缓存, 2 步确认, 数据库中是否存在, 本地文件是否存在private File checkCache(String md5){//查询数据库VideoCacheBean bean = VideoCacheDBUtil.query(md5);if(bean != null){File file = new File(bean.getVideoPath());if(file.exists()){return file;}}return null;}

3.LRUCache 的实现

//清理超过大小和存储时间的视频缓存文件
VideoLRUCacheUtil.checkCacheSize(mContext);public static void checkCacheSize(Context context){ArrayList<VideoCacheBean> videoCacheList = VideoCacheDBUtil.query();//检查一下数据库里面的缓存文件是否存在for (VideoCacheBean bean : videoCacheList){if(bean.getFileSize() == 0){File videoFile = new File(bean.getVideoPath());//如果文件不存在或者文件大小不匹配, 那么删除if(!videoFile.exists() && videoFile.length()!=bean.getFileSize()){VideoCacheDBUtil.delete(bean);}}}long currentSize = 0;long currentTime = System.currentTimeMillis();for (VideoCacheBean bean : videoCacheList){//太久远的文件删除if(currentTime-bean.getPlayTime() > maxCacheTime){VideoCacheDBUtil.delete(bean);}else {//大于存储空间的删除if (currentSize + bean.getFileSize() > maxDirSize) {VideoCacheDBUtil.delete(bean);} else {currentSize += bean.getFileSize();}}}//删除不符合规则的缓存deleteDirRoom(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), VideoCacheDBUtil.query());}//更新缓存文件的播放次数和最后播放时间public static void updateVideoCacheBean(String md5, String videoPath, long fileSize){VideoCacheBean videoCacheBean = VideoCacheDBUtil.query(md5);if(videoCacheBean == null){videoCacheBean = new VideoCacheBean();videoCacheBean.setKey(md5);videoCacheBean.setVideoPath(videoPath);videoCacheBean.setFileSize(fileSize);}videoCacheBean.setPlayCount(videoCacheBean.getPlayCount()+1);videoCacheBean.setPlayTime(System.currentTimeMillis());VideoCacheDBUtil.save(videoCacheBean);}

⑤关于多个 Activity 同步播放状态, 无缝切换

1.首先在跳转时, 通知被覆盖的 activity 不关闭播放器

//首先跳转时通知一下 activitymainActivity.jumpNotCloseMediaPlay(position);//然后在 onPause 里
protected void onPause() {super.onPause();//如果要跳转播放, 那么不关闭播放器if (videoPositionList.size()>currentPlayIndex && jumpVideoPosition==videoPositionList.get(currentPlayIndex)) {...这里就不关闭播放器}else{//如果不要求跳转播放, 那么就重置播放器mMediaPlayerTool.reset();}
}

2.然后在新页面初始化播放器

private void playVideoByPosition(int position){......一切初始化代码照旧(注意不要重置播放器), 这里省略不提//把播放器当前绑定的 SurfaceTexture 取出起来, 设置给当前界面的 TextureViewvh.playTextureView.resetTextureView(mMediaPlayerTool.getAvailableSurfaceTexture());mMediaPlayerTool.setPlayTextureView(vh.playTextureView);//最后刷新一下 viewvh.playTextureView.postInvalidate();
}

至此代码讲解完毕, 亲测在 4g 网络下视频初始化速度毫秒级, 并且在低性能手机下, 页面来回切换无卡顿.

大家如果有不解, 可以查看源码了解更多, 有 bug 或优化思路 也可以提issues

自定义高性能播放器, 实现边下边播缓存等功能相关推荐

  1. iOS开发之仿微博视频边下边播之自定义AVPlayer播放器, 边下边播解剖。视频处理流程,建立连接-请求数据-统筹数据-解码数据-视频呈现

    Tips:这次的内容分为两篇文章讲述 01.[iOS]仿微博视频边下边播之封装播放器 讲述如何封装一个实现了边下边播并且缓存的视频播放器. 02.[iOS]仿微博视频边下边播之滑动TableView自 ...

  2. java 视频边下边播,VideoViewDemo android 播放器,支持边下边播 238万源代码下载- www.pudn.com...

    文件名称: VideoViewDemo下载 收藏√  [ 5  4  3  2  1 ] 开发工具: Java 文件大小: 194 KB 上传时间: 2014-09-19 下载次数: 5 详细说明:a ...

  3. 边下载边播放的播放器Android边下边播

    看到很多朋友有提问到Android边下载边播放的播放器,小编在这里给大家做个关于这方面的分享. 首先作为一款播放器,支持转码或者支持各种视频格式是必须的,比如常见的视频格式:MP4/FLV/M3U8/ ...

  4. Android开发笔记(一百二十六)自定义音乐播放器

    MediaRecorder/MediaPlayer 在Android手机上面,音频的处理比视频还要复杂,这真是出人意料.在前面的博文< Android开发笔记(五十七)录像录音与播放>中, ...

  5. android 自定义MP4播放器

    昨天,在网上找了好多资料,终于做了一个自定义的播发器. 视频播放方式 在Android中播放视频的方式有两种: 1.使用MediaPlayer结合SurfaceView进行播放.其中通过Surface ...

  6. Android自定义一个播放器控件

    介绍 最近要使用播放器做一个简单的视频播放功能,开始学习VideoView,在横竖屏切换的时候碰到了点麻烦,不过在查阅资料后总算是解决了.在写VideoView播放视频时候定义控制的代码全写在Actv ...

  7. html5 mp3播放器源码,HTML5自定义mp3播放器源码

    audio对象 src兼容.ogg .wav .mp3 width autoplay loop muted静音 播放play() var myAudio = new Audio(); myAudio. ...

  8. 实现简单的自定义音乐播放器

    这篇博客只是记录自己写的js插件,着重点在于js,而不是css或者html.所以在js方面会比较详细,而其他的就只是简单提提. 刚学前端js那会,只是应付式的把书看完了,demo也没写几个.碰巧这学期 ...

  9. vue自定义音频播放组件_易于创建Vue的自定义音频播放器组件

    vue自定义音频播放组件 音频更好 (vue-audio-better) Easy to create custom audio player components for Vue.js. 易于为Vu ...

最新文章

  1. mysql 命令行小结
  2. python 基础——变量
  3. STM32 GPIO与 EXTI的映射关系
  4. Android --- Session ‘app’: Error Launching activity解决办法
  5. Windows文件目录DOS窗口
  6. yii 使用 有赞sdk_有赞移动如何做到并行灰度的复杂场景?
  7. LeetCode 965. 单值二叉树
  8. 免费开源剪辑软件Shotcut推荐和使用教程
  9. select,poll,epoll区别
  10. 【英语学习】【Level 08】U02 Movie Time L4 From the page to the big screen
  11. JSONSerializer把类转换成JSON字符串
  12. SPSS26没有典型相关性分析怎么办
  13. HTTP协议(一些报头字段的作用,如cace-control、keep-alive)
  14. 算法笔记方法论4 枚举法 详细笔记
  15. 学好水彩,给自己做个手机壳吧
  16. mysql commit work_数据库commit work
  17. 躺平减脂减重法补充篇——无需控制碳水摄入的有效方法,另推一种健康的运动和防止老年慢性病的方式...
  18. Linux内核入门-- likely和unlikely
  19. WIN11+CUAD11.2+vs2019+tensorTR8.6+Yolov3/4/5模型加速
  20. 秒懂mysql中的group by用法

热门文章

  1. 虚拟主机换云服务器,云虚拟主机想换云服务器
  2. android模拟器如何正确安装HAXM加速器
  3. 他励直流电机 | 根据铭牌参数估算结构参数
  4. 174. 地下城游戏;剑指 Offer 40. 最小的k个数;378. 有序矩阵中第K小的元素;703. 数据流中的第K大元素
  5. c 多文件全局变量_必须知道的C语言知识细节:C程序编译后内存到底是如何布局...
  6. 对计算机学院建设性意见,计通学院云平台优质课程建设实施意见
  7. Perl qw()函数
  8. 排查Linux服务器是否被入侵步骤
  9. wince 蓝牙 --转载
  10. python里怎么使用snip_MacOS下好用的截图软件snip