├── .gitignore ├── .idea ├── caches │ ├── build_file_checksums.ser │ └── gradle_models.ser ├── codeStyles │ └── Project.xml ├── dictionaries │ └── zhaoshuang.xml ├── encodings.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── libs │ ├── armeabi-v7a │ │ ├── libijkffmpeg.so │ │ ├── libijkplayer.so │ │ └── libijksdl.so │ └── lite-orm-1.9.2.jar ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── zhaoss │ │ └── videoplayerdemo │ │ ├── MyApplication.java │ │ ├── activity │ │ ├── BaseActivity.java │ │ ├── MainActivity.java │ │ └── VideoDetailsActivity.java │ │ ├── adapter │ │ ├── MainAdapter.java │ │ └── VideoDetailsAdapter.java │ │ ├── bean │ │ ├── MainVideoBean.java │ │ └── VideoCacheBean.java │ │ ├── util │ │ ├── CacheMediaDataSource.java │ │ ├── DataUtil.java │ │ ├── IntentUtil.java │ │ ├── LogUtil.java │ │ ├── MediaPlayerTool.java │ │ ├── RxJavaUtil.java │ │ ├── StatusBarUtil.java │ │ ├── Util.java │ │ ├── VideoCacheDBUtil.java │ │ └── VideoLRUCacheUtil.java │ │ └── view │ │ ├── PlayTextureView.java │ │ └── VideoTouchView.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_launcher_background.xml │ └── progressbar_with_buffer.xml │ ├── layout │ ├── activity_main.xml │ ├── activity_video_details.xml │ ├── dialog_loading.xml │ ├── item_video.xml │ └── item_video_details.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_round.png │ └── video_pause.png │ ├── mipmap-xhdpi │ ├── avatar1.jpeg │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_round.png │ ├── video_fast_back.png │ ├── video_fast_forward.png │ └── video_landscape_cancel.png │ ├── raw │ └── demo.mp4 │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /.idea/caches/gradle_models.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/.idea/caches/gradle_models.ser -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /.idea/dictionaries/zhaoshuang.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Android 10 | 11 | 12 | C/C++ 13 | 14 | 15 | ComplianceLintAndroid 16 | 17 | 18 | Control flow issuesJava 19 | 20 | 21 | CorrectnessLintAndroid 22 | 23 | 24 | General 25 | 26 | 27 | GeneralC/C++ 28 | 29 | 30 | GradleMigrationKotlin 31 | 32 | 33 | IconsUsabilityLintAndroid 34 | 35 | 36 | Java 37 | 38 | 39 | Kotlin 40 | 41 | 42 | LintAndroid 43 | 44 | 45 | MessagesCorrectnessLintAndroid 46 | 47 | 48 | MigrationKotlin 49 | 50 | 51 | PerformanceJava 52 | 53 | 54 | PerformanceLintAndroid 55 | 56 | 57 | Plugin DevKit 58 | 59 | 60 | Probable bugsJava 61 | 62 | 63 | SecurityJava 64 | 65 | 66 | SecurityLintAndroid 67 | 68 | 69 | Threading issuesJava 70 | 71 | 72 | UsabilityLintAndroid 73 | 74 | 75 | Verbose or redundant code constructsJava 76 | 77 | 78 | 79 | 80 | Android 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 92 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### v1.1 增加本地视频播放功能, 视频会自动初始化宽高 不用手动传视频宽高了 2 | 3 | [源码下载](https://github.com/Zhaoss/VideoPlayerDemo) 4 | [演示Demo下载](https://fir.im/VideoPlayerDemo) 5 | 6 | ![](https://upload-images.jianshu.io/upload_images/2582948-3f8bedd90aef1c83.gif?imageMogr2/auto-orient/strip) 7 | 8 | ### 本项目使用播放器是[ijkplay](https://github.com/Bilibili/ijkplayer), 并且进行封装和修改 9 | ``` 10 | 主要功能: 11 | 1.重新编辑ijkplay的so库, 使其更精简和支持https协议 12 | 2.自定义MediaDataSource, 使用okhttp重写网络框架, 网络播放更流畅 13 | 3.实现视频缓存, 并且自定义LRUCache算法管理缓存文件 14 | 4.全局使用一个播放器, 实现视频在多个Activity之前无缝切换, 流畅播放 15 | 5.加入更多兼容性判断, 适配绝大数机型 16 | ``` 17 | --- 18 | 19 | ### ①导入ijkplay: 20 | ![](https://upload-images.jianshu.io/upload_images/2582948-6f7c3777a109f9b4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 21 | ``` 22 | //需要的权限 23 | 24 | 25 | 26 | 27 | 首先将lib文件夹下的so库粘贴过来, (因为官方自带的so库是不支持https的, 我重新编译的这个so库支持https协议, 28 | 并且使用的是精简版的配置, 网上关于ijkplay编译的流程和配置挺多的, 可以根据自己的需求自定义) 29 | 30 | 然后在module的build中加入 "implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8'" 31 | ``` 32 | 33 | 34 | ### ②使用播放器的方法: 35 | 36 | 1.我封装了一个MediaPlayerTool工具类包含的初始化so库和一些回调等等 37 | ``` 38 | //通过单例得到媒体播放工具 39 | mMediaPlayerTool = MediaPlayerTool.getInstance(); 40 | //这里会自动初始化so库 有些手机会找不到so, 会自动使用系统的播放器 41 | private MediaPlayerTool(){ 42 | try { 43 | IjkMediaPlayer.loadLibrariesOnce(null); 44 | IjkMediaPlayer.native_profileBegin("libijkplayer.so"); 45 | loadIjkSucc = true; 46 | }catch (UnsatisfiedLinkError e){ 47 | e.printStackTrace(); 48 | loadIjkSucc = false; 49 | } 50 | } 51 | 52 | //一些生命周期回调 53 | public static abstract class VideoListener { 54 | //视频开始播放 55 | public void onStart(){}; 56 | //视频被停止播放 57 | public void onStop(){}; 58 | //视频播放完成 59 | public void onCompletion(){}; 60 | //视频旋转角度参数初始化完成 61 | public void onRotationInfo(int rotation){}; 62 | //播放进度 0-1 63 | public void onPlayProgress(long currentPosition){}; 64 | //缓存速度 1-100 65 | public void onBufferProgress(int progress){}; 66 | } 67 | ``` 68 | 69 | 2.因为我使用的是RecyclerView,所以先找到当前屏幕中 处于可以播放范围的item 70 | ``` 71 | //首先循环RecyclerView中所有itemView, 找到在屏幕可见范围内的item 72 | private void checkPlayVideo(){ 73 | currentPlayIndex = 0; 74 | videoPositionList.clear(); 75 | 76 | int childCount = rv_video.getChildCount(); 77 | for (int x = 0; x < childCount; x++) { 78 | View childView = rv_video.getChildAt(x); 79 | //isPlayRange()这个方法很重要 80 | boolean playRange = isPlayRange(childView.findViewById(R.id.rl_video), rv_video); 81 | if(playRange){ 82 | int position = rv_video.getChildAdapterPosition(childView); 83 | if(position>=0 && !videoPositionList.contains(position)){ 84 | videoPositionList.add(position); 85 | } 86 | } 87 | } 88 | } 89 | 90 | //检查当前item是否在RecyclerView可见的范围内 91 | private boolean isPlayRange(View childView, View parentView){ 92 | 93 | if(childView==null || parentView==null){ 94 | return false; 95 | } 96 | 97 | int[] childLocal = new int[2]; 98 | childView.getLocationOnScreen(childLocal); 99 | 100 | int[] parentLocal = new int[2]; 101 | parentView.getLocationOnScreen(parentLocal); 102 | 103 | boolean playRange = childLocal[1]>=parentLocal[1] && 104 | childLocal[1]<=parentLocal[1]+parentView.getHeight()-childView.getHeight(); 105 | 106 | return playRange; 107 | } 108 | ``` 109 | 110 | 3.我还封装了一个TextureView, 里面包含一些初始化SurfaceTexture和视频裁剪播放的方法 111 | ``` 112 | //视频居中播放 113 | private void setVideoCenter(float viewWidth, float viewHeight, float videoWidth, float videoHeight){ 114 | 115 | Matrix matrix = new Matrix(); 116 | float sx = viewWidth/videoWidth; 117 | float sy = viewHeight/videoHeight; 118 | float maxScale = Math.max(sx, sy); 119 | 120 | matrix.preTranslate((viewWidth - videoWidth) / 2, (viewHeight - videoHeight) / 2); 121 | matrix.preScale(videoWidth/viewWidth, videoHeight/viewHeight); 122 | matrix.postScale(maxScale, maxScale, viewWidth/2, viewHeight/2); 123 | 124 | mTextureView.setTransform(matrix); 125 | mTextureView.postInvalidate(); 126 | } 127 | 128 | //初始化SurfaceTexture 129 | public SurfaceTexture newSurfaceTexture(){ 130 | 131 | int[] textures = new int[1]; 132 | GLES20.glGenTextures(1, textures, 0); 133 | int texName = textures[0]; 134 | SurfaceTexture surfaceTexture = new SurfaceTexture(texName); 135 | surfaceTexture.detachFromGLContext(); 136 | return surfaceTexture; 137 | } 138 | ``` 139 | 140 | 4.接下来就是播放代码了 141 | ``` 142 | private void playVideoByPosition(int position){ 143 | //根据传进来的position找到对应的ViewHolder 144 | final MainAdapter.MyViewHolder vh = (MainAdapter.MyViewHolder) 145 | rv_video.findViewHolderForAdapterPosition(position); 146 | if(vh == null){ 147 | return ; 148 | } 149 | 150 | currentPlayView = vh.rl_video; 151 | 152 | //初始化一些播放状态, 如进度条,播放按钮,加载框等 153 | //显示正在加载的界面 154 | vh.iv_play_icon.setVisibility(View.GONE); 155 | vh.pb_video.setVisibility(View.VISIBLE); 156 | vh.iv_cover.setVisibility(View.VISIBLE); 157 | vh.tv_play_time.setText(""); 158 | 159 | //初始化播放器 160 | mMediaPlayerTool.initMediaPLayer(); 161 | mMediaPlayerTool.setVolume(0); 162 | 163 | //设置视频url 164 | String videoUrl = dataList.get(position).getVideoUrl(); 165 | mMediaPlayerTool.setDataSource(videoUrl); 166 | 167 | myVideoListener = new MediaPlayerTool.VideoListener() { 168 | @Override 169 | public void onStart() { 170 | //将播放图标和封面隐藏 171 | vh.iv_play_icon.setVisibility(View.GONE); 172 | vh.pb_video.setVisibility(View.GONE); 173 | //防止闪屏 174 | vh.iv_cover.postDelayed(new Runnable() { 175 | @Override 176 | public void run() { 177 | vh.iv_cover.setVisibility(View.GONE); 178 | } 179 | }, 300); 180 | } 181 | @Override 182 | public void onStop() { 183 | //播放停止 184 | vh.pb_video.setVisibility(View.GONE); 185 | vh.iv_cover.setVisibility(View.VISIBLE); 186 | vh.iv_play_icon.setVisibility(View.VISIBLE); 187 | vh.tv_play_time.setText(""); 188 | currentPlayView = null; 189 | } 190 | @Override 191 | public void onCompletion() { 192 | //播放下一个 193 | currentPlayIndex++; 194 | playVideoByPosition(-1); 195 | } 196 | @Override 197 | public void onRotationInfo(int rotation) { 198 | //设置旋转播放 199 | vh.playTextureView.setRotation(rotation); 200 | } 201 | @Override 202 | public void onPlayProgress(long currentPosition) { 203 | //显示播放时长 204 | String date = MyUtil.fromMMss(mMediaPlayerTool.getDuration() - currentPosition); 205 | vh.tv_play_time.setText(date); 206 | } 207 | }; 208 | mMediaPlayerTool.setVideoListener(myVideoListener); 209 | 210 | //这里重置一下TextureView 211 | vh.playTextureView.resetTextureView(); 212 | mMediaPlayerTool.setPlayTextureView(vh.playTextureView); 213 | mMediaPlayerTool.setSurfaceTexture(vh.playTextureView.getSurfaceTexture()); 214 | //准备播放 215 | mMediaPlayerTool.prepare(); 216 | } 217 | ``` 218 | 219 | ### ③重写MediaDataSource, 使用okhttp实现边下边播和视频缓存 220 | 1.一共需要重写3个方法getSize(),close()和readAt(); 先说getSize() 221 | ``` 222 | public long getSize() throws IOException { 223 | //开始播放时, 播放器会调用一下getSize()来初始化视频大小, 这时我们就要初始化一条视频播放流 224 | if(networkInPutStream == null) { 225 | initInputStream(); 226 | } 227 | return contentLength; 228 | } 229 | 230 | //初始化一个视频流出来, 可能是本地或网络 231 | private void initInputStream() throws IOException{ 232 | 233 | File file = checkCache(mMd5); 234 | if(file != null){ 235 | //更新一下缓存文件 236 | VideoLRUCacheUtil.updateVideoCacheBean(mMd5, file.getAbsolutePath(), file.length()); 237 | //读取的本地缓存文件 238 | isCacheVideo = true; 239 | localVideoFile = file; 240 | //开启一个本地视频流 241 | localStream = new RandomAccessFile(localVideoFile, "rw"); 242 | contentLength = file.length(); 243 | }else { 244 | //没有缓存 开启一个网络流, 并且开启一个缓存流, 实现视频缓存 245 | isCacheVideo = false; 246 | //开启一个网络视频流 247 | networkInPutStream = openHttpClient(0); 248 | //要写入的本地缓存文件 249 | localVideoFile = VideoLRUCacheUtil.createCacheFile(MyApplication.mContext, mMd5, contentLength); 250 | //要写入的本地缓存视频流 251 | localStream = new RandomAccessFile(localVideoFile, "rw"); 252 | } 253 | } 254 | ``` 255 | 2.然后是readAt()方法, 也是最重要的一个方法 256 | ``` 257 | /** 258 | * @param position 视频流读取进度 259 | * @param buffer 要把读取到的数据存到这个数组 260 | * @param offset 数据开始写入的坐标 261 | * @param size 本次一共读取数据的大小 262 | * @throws IOException 263 | */ 264 | //记录当前读取流的索引 265 | long mPosition = 0; 266 | @Override 267 | public int readAt(long position, byte[] buffer, int offset, int size) throws IOException { 268 | 269 | if(position>=contentLength || localStream==null){ 270 | return -1; 271 | } 272 | 273 | //是否将此字节缓存到本地 274 | boolean isWriteVideo = syncInputStream(position); 275 | 276 | //读取的流的长度不能大于contentLength 277 | if (position+size > contentLength) { 278 | size -= position+size-contentLength; 279 | } 280 | 281 | //读取指定大小的视频数据 282 | byte[] bytes; 283 | if(isCacheVideo){ 284 | //从本地读取 285 | bytes = readByteBySize(localStream, size); 286 | }else{ 287 | //从网络读取 288 | bytes = readByteBySize(networkInPutStream, size); 289 | } 290 | if(bytes != null) { 291 | //写入到播放器的数组中 292 | System.arraycopy(bytes, 0, buffer, offset, size); 293 | if (isWriteVideo && !isCacheVideo) { 294 | //将视频缓存到本地 295 | localStream.write(bytes); 296 | } 297 | //记录数据流读取到哪步了 298 | mPosition += size; 299 | } 300 | 301 | return size; 302 | } 303 | 304 | /** 305 | * 从inputStream里读取size大小的数据 306 | */ 307 | private byte[] readByteBySize(InputStream inputStream, int size) throws IOException{ 308 | 309 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 310 | 311 | byte[] buf = new byte[size]; 312 | int len; 313 | while ((len = inputStream.read(buf)) != -1) { 314 | out.write(buf, 0, len); 315 | if (out.size() == size) { 316 | return out.toByteArray(); 317 | } else { 318 | buf = new byte[size - out.size()]; 319 | } 320 | } 321 | return null; 322 | } 323 | 324 | /** 325 | * 删除file一部分字节, 从position到file.size 326 | */ 327 | private void deleteFileByPosition(long position) throws IOException{ 328 | 329 | FileInputStream in = new FileInputStream(localVideoFile); 330 | 331 | File tempFile = VideoLRUCacheUtil.createTempFile(MyApplication.mContext); 332 | FileOutputStream out = new FileOutputStream(tempFile); 333 | 334 | byte[] buf = new byte[8192]; 335 | int len; 336 | while ((len = in.read(buf)) != -1) { 337 | if(position <= len){ 338 | out.write(buf, 0, (int) position); 339 | out.close(); 340 | 341 | in.close(); 342 | localVideoFile.delete(); 343 | tempFile.renameTo(localVideoFile); 344 | localStream = new RandomAccessFile(localVideoFile, "rw"); 345 | return ; 346 | }else{ 347 | position -= len; 348 | out.write(buf, 0, len); 349 | } 350 | } 351 | tempFile.delete(); 352 | } 353 | ``` 354 | 3.主要说一下syncInputStream(), 因为有可能出现一种情况, 355 | 比如一个视频长度100, 播放器首先读取视频的1到10之间的数据, 然后在读取90到100之间的数据, 然后在从1播放到100; 356 | 所以这时我们需要同步视频流, 和播放进度保持一致这时就需要重新开启一个IO流(如果在读取本地缓存时可以直接使用RandomAccessFile.seek()方法跳转) 357 | ``` 358 | //同步数据流 359 | private boolean syncInputStream(long position) throws IOException{ 360 | boolean isWriteVideo = true; 361 | //判断两次读取数据是否连续 362 | if(mPosition != position){ 363 | if(isCacheVideo){ 364 | //如果是本地缓存, 直接跳转到该索引 365 | localStream.seek(position); 366 | }else{ 367 | if(mPosition > position){ 368 | //同步本地缓存流 369 | localStream.close(); 370 | deleteFileByPosition(position); 371 | localStream.seek(position); 372 | }else{ 373 | isWriteVideo = false; 374 | } 375 | networkInPutStream.close(); 376 | //重新开启一个网络流 377 | networkInPutStream = openHttpClient((int) position); 378 | } 379 | mPosition = position; 380 | } 381 | return isWriteVideo; 382 | } 383 | ``` 384 | 4.最后一个是close()方法, 主要播放停止后释放一些资源 385 | ``` 386 | public void close() throws IOException { 387 | if(networkInPutStream != null){ 388 | networkInPutStream.close(); 389 | networkInPutStream = null; 390 | } 391 | if(localStream != null){ 392 | localStream.close(); 393 | localStream = null; 394 | } 395 | if(localVideoFile.length()!=contentLength){ 396 | localVideoFile.delete(); 397 | } 398 | } 399 | ``` 400 | 401 | ### ④视频缓存和LRUCache管理 402 | 1.首先创建缓存文件, 在刚才的MediaDataSource.getSize()方法里有一句代码 403 | ``` 404 | localVideoFile = VideoLRUCacheUtil.createCacheFile(MyApplication.mContext, mMd5, contentLength); 405 | 406 | public static File createCacheFile(Context context, String md5, long fileSize){ 407 | //创建一个视频缓存文件, 在data/data目录下 408 | File filesDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS); 409 | 410 | File cacheFile = new File(filesDir, md5); 411 | if(!cacheFile.exists()) { 412 | cacheFile.createNewFile(); 413 | } 414 | //将缓存信息存到数据库 415 | VideoLRUCacheUtil.updateVideoCacheBean(md5, cacheFile.getAbsolutePath(), fileSize); 416 | return cacheFile; 417 | } 418 | ``` 419 | 2.然后是读取缓存文件, 在刚才的MediaDataSource.getSize()方法里还有一句代码 420 | ``` 421 | //检查本地是否有缓存, 2步确认, 数据库中是否存在, 本地文件是否存在 422 | private File checkCache(String md5){ 423 | //查询数据库 424 | VideoCacheBean bean = VideoCacheDBUtil.query(md5); 425 | if(bean != null){ 426 | File file = new File(bean.getVideoPath()); 427 | if(file.exists()){ 428 | return file; 429 | } 430 | } 431 | return null; 432 | } 433 | ``` 434 | 435 | 3.LRUCache的实现 436 | ``` 437 | //清理超过大小和存储时间的视频缓存文件 438 | VideoLRUCacheUtil.checkCacheSize(mContext); 439 | 440 | public static void checkCacheSize(Context context){ 441 | 442 | ArrayList videoCacheList = VideoCacheDBUtil.query(); 443 | 444 | //检查一下数据库里面的缓存文件是否存在 445 | for (VideoCacheBean bean : videoCacheList){ 446 | if(bean.getFileSize() == 0){ 447 | File videoFile = new File(bean.getVideoPath()); 448 | //如果文件不存在或者文件大小不匹配, 那么删除 449 | if(!videoFile.exists() && videoFile.length()!=bean.getFileSize()){ 450 | VideoCacheDBUtil.delete(bean); 451 | } 452 | } 453 | } 454 | 455 | long currentSize = 0; 456 | long currentTime = System.currentTimeMillis(); 457 | for (VideoCacheBean bean : videoCacheList){ 458 | //太久远的文件删除 459 | if(currentTime-bean.getPlayTime() > maxCacheTime){ 460 | VideoCacheDBUtil.delete(bean); 461 | }else { 462 | //大于存储空间的删除 463 | if (currentSize + bean.getFileSize() > maxDirSize) { 464 | VideoCacheDBUtil.delete(bean); 465 | } else { 466 | currentSize += bean.getFileSize(); 467 | } 468 | } 469 | } 470 | 471 | //删除不符合规则的缓存 472 | deleteDirRoom(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), VideoCacheDBUtil.query()); 473 | } 474 | 475 | //更新缓存文件的播放次数和最后播放时间 476 | public static void updateVideoCacheBean(String md5, String videoPath, long fileSize){ 477 | 478 | VideoCacheBean videoCacheBean = VideoCacheDBUtil.query(md5); 479 | if(videoCacheBean == null){ 480 | videoCacheBean = new VideoCacheBean(); 481 | videoCacheBean.setKey(md5); 482 | videoCacheBean.setVideoPath(videoPath); 483 | videoCacheBean.setFileSize(fileSize); 484 | } 485 | videoCacheBean.setPlayCount(videoCacheBean.getPlayCount()+1); 486 | videoCacheBean.setPlayTime(System.currentTimeMillis()); 487 | 488 | VideoCacheDBUtil.save(videoCacheBean); 489 | } 490 | ``` 491 | 492 | ### ⑤关于多个Activity同步播放状态, 无缝切换 493 | 1.首先在跳转时, 通知被覆盖的activity不关闭播放器 494 | ``` 495 | //首先跳转时通知一下activity 496 | mainActivity.jumpNotCloseMediaPlay(position); 497 | 498 | //然后在onPause里 499 | protected void onPause() { 500 | super.onPause(); 501 | //如果要跳转播放, 那么不关闭播放器 502 | if (videoPositionList.size()>currentPlayIndex && jumpVideoPosition==videoPositionList.get(currentPlayIndex)) { 503 | ...这里就不关闭播放器 504 | }else{ 505 | //如果不要求跳转播放, 那么就重置播放器 506 | mMediaPlayerTool.reset(); 507 | } 508 | } 509 | ``` 510 | 2.然后在新页面初始化播放器 511 | ``` 512 | private void playVideoByPosition(int position){ 513 | ......一切初始化代码照旧(注意不要重置播放器), 这里省略不提 514 | 515 | //把播放器当前绑定的SurfaceTexture取出起来, 设置给当前界面的TextureView 516 | vh.playTextureView.resetTextureView(mMediaPlayerTool.getAvailableSurfaceTexture()); 517 | mMediaPlayerTool.setPlayTextureView(vh.playTextureView); 518 | //最后刷新一下view 519 | vh.playTextureView.postInvalidate(); 520 | } 521 | ``` 522 | ### 至此代码讲解完毕, 亲测在4g网络下视频初始化速度毫秒级, 并且在低性能手机下, 页面来回切换无卡顿. 523 | ### 大家如果有不解, 可以查看源码了解更多, 有bug或优化思路 也可以提[issues](https://github.com/Zhaoss/VideoPlayerDemo/issues) 524 | 525 | --- 526 | 527 | # MIT License 528 | Copyright (c) 2018 Zhaoss (838198688@qq.com) 529 | 530 | Permission is hereby granted, free of charge, to any person obtaining a copy 531 | of this software and associated documentation files (the "Software"), to deal 532 | in the Software without restriction, including without limitation the rights 533 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 534 | copies of the Software, and to permit persons to whom the Software is 535 | furnished to do so, subject to the following conditions: 536 | 537 | The above copyright notice and this permission notice shall be included in all 538 | copies or substantial portions of the Software. 539 | 540 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 541 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 542 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 543 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 544 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 545 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 546 | SOFTWARE. 547 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "com.zhaoss.videoplayerdemo" 7 | minSdkVersion 21 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | } 12 | buildTypes { 13 | release { 14 | minifyEnabled false 15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 16 | } 17 | } 18 | 19 | sourceSets { 20 | main { 21 | //设置so文件夹目录 22 | jniLibs.srcDirs = ['libs'] 23 | } 24 | 25 | release { java.srcDirs = ['src/release/java'] } 26 | } 27 | compileOptions { 28 | sourceCompatibility JavaVersion.VERSION_1_8 29 | targetCompatibility JavaVersion.VERSION_1_8 30 | } 31 | } 32 | 33 | dependencies { 34 | implementation fileTree(dir: 'libs', include: ['*.jar']) 35 | implementation 'com.android.support:appcompat-v7:28.0.0' 36 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 37 | 38 | implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8' 39 | implementation files('libs/lite-orm-1.9.2.jar') 40 | implementation 'io.reactivex.rxjava2:rxjava:2.2.8' 41 | implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' 42 | implementation "com.android.support:recyclerview-v7:28.0.0" 43 | implementation 'com.yanzhenjie:permission:1.1.2' 44 | 45 | implementation 'com.squareup.okio:okio:1.17.2' 46 | implementation 'com.squareup.okhttp3:okhttp:3.14.1' 47 | implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0' 48 | 49 | implementation "com.github.bumptech.glide:glide:4.9.0" 50 | } 51 | -------------------------------------------------------------------------------- /app/libs/armeabi-v7a/libijkffmpeg.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/libs/armeabi-v7a/libijkffmpeg.so -------------------------------------------------------------------------------- /app/libs/armeabi-v7a/libijkplayer.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/libs/armeabi-v7a/libijkplayer.so -------------------------------------------------------------------------------- /app/libs/armeabi-v7a/libijksdl.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/libs/armeabi-v7a/libijksdl.so -------------------------------------------------------------------------------- /app/libs/lite-orm-1.9.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/libs/lite-orm-1.9.2.jar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/MyApplication.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo; 2 | 3 | import android.app.Application; 4 | 5 | import com.zhaoss.videoplayerdemo.util.VideoLRUCacheUtil; 6 | 7 | /** 8 | * Created by zhaoshuang on 2018/11/1. 9 | */ 10 | 11 | public class MyApplication extends Application { 12 | 13 | public static MyApplication mContext; 14 | 15 | @Override 16 | public void onCreate() { 17 | super.onCreate(); 18 | 19 | this.mContext = this; 20 | 21 | //清理超过大小和存储时间的视频缓存文件 22 | VideoLRUCacheUtil.checkCacheSize(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/activity/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.activity; 2 | 3 | import android.app.Activity; 4 | import android.graphics.Color; 5 | import android.os.Bundle; 6 | import android.support.annotation.Nullable; 7 | import android.support.v4.content.ContextCompat; 8 | import android.support.v7.app.AlertDialog; 9 | import android.view.View; 10 | 11 | import com.zhaoss.videoplayerdemo.R; 12 | import com.zhaoss.videoplayerdemo.util.StatusBarUtil; 13 | 14 | /** 15 | * Created by zhaoshuang on 2018/6/1. 16 | */ 17 | 18 | public class BaseActivity extends Activity { 19 | 20 | protected BaseActivity mContext; 21 | private AlertDialog dialog; 22 | 23 | @Override 24 | protected void onCreate(@Nullable Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | this.mContext = this; 27 | 28 | StatusBarUtil.setColor(mContext, ContextCompat.getColor(mContext, R.color.my_black), false); 29 | } 30 | 31 | @Override 32 | protected void onStart() { 33 | super.onStart(); 34 | 35 | StatusBarUtil.setColor(this, Color.WHITE, true); 36 | } 37 | 38 | protected void showProDialog(){ 39 | 40 | dismissDialog(); 41 | 42 | AlertDialog.Builder builder = new AlertDialog.Builder(mContext); 43 | builder.setCancelable(false); 44 | View view = View.inflate(mContext, R.layout.dialog_loading, null); 45 | builder.setView(view); 46 | dialog = builder.create(); 47 | dialog.show(); 48 | } 49 | 50 | protected void dismissDialog(){ 51 | 52 | if(dialog != null){ 53 | dialog.dismiss(); 54 | dialog = null; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/activity/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.activity; 2 | 3 | import android.Manifest; 4 | import android.graphics.Rect; 5 | import android.os.Bundle; 6 | import android.support.annotation.NonNull; 7 | import android.support.v7.widget.LinearLayoutManager; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.view.View; 10 | 11 | import com.yanzhenjie.permission.AndPermission; 12 | import com.yanzhenjie.permission.PermissionListener; 13 | import com.zhaoss.videoplayerdemo.R; 14 | import com.zhaoss.videoplayerdemo.adapter.MainAdapter; 15 | import com.zhaoss.videoplayerdemo.bean.MainVideoBean; 16 | import com.zhaoss.videoplayerdemo.util.DataUtil; 17 | import com.zhaoss.videoplayerdemo.util.MediaPlayerTool; 18 | import com.zhaoss.videoplayerdemo.util.Util; 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | public class MainActivity extends BaseActivity { 24 | 25 | private RecyclerView rv_video; 26 | private MediaPlayerTool mMediaPlayerTool; 27 | private ArrayList dataList; 28 | 29 | @Override 30 | protected void onCreate(Bundle savedInstanceState) { 31 | super.onCreate(savedInstanceState); 32 | setContentView(R.layout.activity_main); 33 | 34 | rv_video = findViewById(R.id.rv_video); 35 | 36 | rv_video.setLayoutManager(new LinearLayoutManager(mContext)); 37 | dataList = DataUtil.createData(); 38 | rv_video.setAdapter(new MainAdapter(this, dataList)); 39 | 40 | rv_video.addItemDecoration(new RecyclerView.ItemDecoration(){ 41 | @Override 42 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 43 | 44 | int position = parent.getChildAdapterPosition(view); 45 | if (position != 0) { 46 | outRect.top = (int) getResources().getDimension(R.dimen.activity_margin2); 47 | } 48 | } 49 | }); 50 | 51 | rv_video.addOnScrollListener(new RecyclerView.OnScrollListener() { 52 | @Override 53 | public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 54 | if(currentPlayView!=null){ 55 | boolean playRange = isPlayRange(currentPlayView, recyclerView); 56 | if(!playRange){ 57 | mMediaPlayerTool.reset(); 58 | } 59 | } 60 | } 61 | @Override 62 | public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 63 | if (newState == RecyclerView.SCROLL_STATE_IDLE) { 64 | //检测播放视频 65 | checkPlayVideo(); 66 | if(currentPlayView == null){ 67 | playVideoByPosition(-1); 68 | } 69 | } 70 | } 71 | }); 72 | 73 | mMediaPlayerTool = MediaPlayerTool.getInstance(); 74 | 75 | AndPermission.with(mContext).permission(Manifest.permission.WRITE_EXTERNAL_STORAGE).callback(new PermissionListener() { 76 | @Override 77 | public void onSucceed(int requestCode, @NonNull List grantPermissions) { 78 | } 79 | @Override 80 | public void onFailed(int requestCode, @NonNull List deniedPermissions) { 81 | 82 | } 83 | }).start(); 84 | } 85 | 86 | boolean isFirst = true; 87 | @Override 88 | public void onWindowFocusChanged(boolean hasFocus) { 89 | super.onWindowFocusChanged(hasFocus); 90 | if(isFirst){ 91 | isFirst = false; 92 | refreshVideo(); 93 | } 94 | } 95 | 96 | //检测是否播放视频 97 | private void checkPlayVideo(){ 98 | 99 | currentPlayIndex = 0; 100 | videoPositionList.clear(); 101 | 102 | int childCount = rv_video.getChildCount(); 103 | for (int x = 0; x < childCount; x++) { 104 | View childView = rv_video.getChildAt(x); 105 | boolean playRange = isPlayRange(childView.findViewById(R.id.rl_video), rv_video); 106 | if(playRange){ 107 | int position = rv_video.getChildAdapterPosition(childView); 108 | if(position>=0 && !videoPositionList.contains(position)){ 109 | videoPositionList.add(position); 110 | } 111 | } 112 | } 113 | } 114 | 115 | //检查子view是否在父view显示布局里面 116 | private boolean isPlayRange(View childView, View parentView){ 117 | 118 | if(childView==null || parentView==null){ 119 | return false; 120 | } 121 | 122 | int[] childLocal = new int[2]; 123 | childView.getLocationOnScreen(childLocal); 124 | 125 | int[] parentLocal = new int[2]; 126 | parentView.getLocationOnScreen(parentLocal); 127 | 128 | boolean playRange = childLocal[1]>=parentLocal[1] && 129 | childLocal[1]<=parentLocal[1]+parentView.getHeight()-childView.getHeight(); 130 | 131 | return playRange; 132 | } 133 | 134 | MediaPlayerTool.VideoListener myVideoListener; 135 | //当前播放的视频角标 136 | int currentPlayIndex; 137 | //可以播放的视频集合 138 | ArrayList videoPositionList = new ArrayList<>(); 139 | View currentPlayView; 140 | /** 141 | * 播放视频 142 | * @param resumePosition 是否继续播放 否则可以传-1 143 | */ 144 | private void playVideoByPosition(int resumePosition){ 145 | 146 | boolean isResumePlay = resumePosition >= 0; 147 | 148 | if(!isResumePlay && (videoPositionList.size()==0 || mMediaPlayerTool ==null)){ 149 | return ; 150 | } 151 | 152 | if(!isResumePlay){ 153 | //一定要先重置播放器 154 | mMediaPlayerTool.reset(); 155 | } 156 | 157 | int playPosition = 0; 158 | if(isResumePlay){ 159 | playPosition = resumePosition; 160 | }else{ 161 | if(currentPlayIndex >= videoPositionList.size()){ 162 | currentPlayIndex = 0; 163 | } 164 | playPosition = videoPositionList.get(currentPlayIndex); 165 | } 166 | 167 | //根据传进来的position找到对应的ViewHolder 168 | final MainAdapter.MyViewHolder vh = (MainAdapter.MyViewHolder) rv_video.findViewHolderForAdapterPosition(playPosition); 169 | if(vh == null){ 170 | return ; 171 | } 172 | 173 | currentPlayView = vh.rl_video; 174 | 175 | //初始化一些播放状态, 如进度条,播放按钮,加载框等 176 | if(isResumePlay){ 177 | vh.pb_video.setVisibility(View.GONE); 178 | vh.iv_play_icon.setVisibility(View.GONE); 179 | vh.iv_cover.setVisibility(View.GONE); 180 | vh.playTextureView.setVideoSize(mMediaPlayerTool.getVideoWidth(), mMediaPlayerTool.getVideoHeight()); 181 | }else{ 182 | //显示正在加载的界面 183 | vh.iv_play_icon.setVisibility(View.GONE); 184 | vh.pb_video.setVisibility(View.VISIBLE); 185 | vh.iv_cover.setVisibility(View.VISIBLE); 186 | vh.tv_play_time.setText(""); 187 | 188 | mMediaPlayerTool.initMediaPLayer(); 189 | 190 | String videoUrl = dataList.get(playPosition).getVideoUrl(); 191 | mMediaPlayerTool.setDataSource(videoUrl); 192 | } 193 | 194 | mMediaPlayerTool.setVolume(0); 195 | myVideoListener = new MediaPlayerTool.VideoListener() { 196 | @Override 197 | public void onStart() { 198 | vh.pb_video.setVisibility(View.GONE); 199 | //防止闪屏 200 | vh.iv_cover.postDelayed(new Runnable() { 201 | @Override 202 | public void run() { 203 | vh.iv_cover.setVisibility(View.GONE); 204 | } 205 | }, 200); 206 | vh.playTextureView.setVideoSize(mMediaPlayerTool.getVideoWidth(), mMediaPlayerTool.getVideoHeight()); 207 | vh.iv_play_icon.setVisibility(View.GONE); 208 | } 209 | @Override 210 | public void onStop() { 211 | vh.pb_video.setVisibility(View.GONE); 212 | vh.iv_cover.setVisibility(View.VISIBLE); 213 | vh.iv_play_icon.setVisibility(View.VISIBLE); 214 | vh.tv_play_time.setText(""); 215 | currentPlayView = null; 216 | } 217 | @Override 218 | public void onCompletion() { 219 | currentPlayIndex++; 220 | playVideoByPosition(-1); 221 | } 222 | @Override 223 | public void onRotationInfo(int rotation) { 224 | vh.playTextureView.setRotation(rotation); 225 | } 226 | @Override 227 | public void onPlayProgress(long currentPosition) { 228 | String date = Util.fromMMss(mMediaPlayerTool.getDuration() - currentPosition); 229 | vh.tv_play_time.setText(date); 230 | } 231 | }; 232 | mMediaPlayerTool.setVideoListener(myVideoListener); 233 | 234 | if(isResumePlay){ 235 | //把播放器当前绑定的SurfaceTexture取出起来, 设置给当前界面的TextureView 236 | vh.playTextureView.resetTextureView(mMediaPlayerTool.getAvailableSurfaceTexture()); 237 | mMediaPlayerTool.setPlayTextureView(vh.playTextureView); 238 | vh.playTextureView.postInvalidate(); 239 | }else { 240 | vh.playTextureView.resetTextureView(); 241 | mMediaPlayerTool.setPlayTextureView(vh.playTextureView); 242 | mMediaPlayerTool.setSurfaceTexture(vh.playTextureView.getSurfaceTexture()); 243 | mMediaPlayerTool.prepare(); 244 | } 245 | } 246 | 247 | //跳转页面时是否关闭播放器 248 | private int jumpVideoPosition = -1; 249 | public void jumpNotCloseMediaPlay(int position){ 250 | jumpVideoPosition = position; 251 | } 252 | 253 | public void refreshVideo(){ 254 | 255 | if(mMediaPlayerTool !=null) { 256 | mMediaPlayerTool.reset(); 257 | checkPlayVideo(); 258 | playVideoByPosition(-1); 259 | } 260 | } 261 | 262 | @Override 263 | protected void onResume() { 264 | super.onResume(); 265 | 266 | //检测是否继续播放视频 267 | if(jumpVideoPosition!=-1 && 268 | (videoPositionList.size()>currentPlayIndex && jumpVideoPosition==videoPositionList.get(currentPlayIndex)) 269 | && mMediaPlayerTool!=null && mMediaPlayerTool.isPlaying()){ 270 | playVideoByPosition(jumpVideoPosition); 271 | }else{ 272 | refreshVideo(); 273 | } 274 | jumpVideoPosition = -1; 275 | 276 | } 277 | 278 | @Override 279 | protected void onPause() { 280 | super.onPause(); 281 | 282 | if(mMediaPlayerTool != null) { 283 | //如果要跳转播放, 那么不关闭播放器 284 | if (videoPositionList.size()>currentPlayIndex && jumpVideoPosition==videoPositionList.get(currentPlayIndex)) { 285 | rv_video.postDelayed(new Runnable() { 286 | @Override 287 | public void run() { 288 | if (myVideoListener != null) { 289 | myVideoListener.onStop(); 290 | } 291 | } 292 | }, 300); 293 | } else { 294 | mMediaPlayerTool.reset(); 295 | if (!videoPositionList.contains(jumpVideoPosition)) { 296 | videoPositionList.add(jumpVideoPosition); 297 | } 298 | currentPlayIndex = videoPositionList.indexOf(jumpVideoPosition); 299 | } 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/activity/VideoDetailsActivity.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.activity; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.PagerSnapHelper; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.view.WindowManager; 12 | import android.widget.ImageView; 13 | 14 | import com.zhaoss.videoplayerdemo.R; 15 | import com.zhaoss.videoplayerdemo.adapter.VideoDetailsAdapter; 16 | import com.zhaoss.videoplayerdemo.bean.MainVideoBean; 17 | import com.zhaoss.videoplayerdemo.util.IntentUtil; 18 | import com.zhaoss.videoplayerdemo.util.MediaPlayerTool; 19 | import com.zhaoss.videoplayerdemo.util.Util; 20 | import com.zhaoss.videoplayerdemo.util.StatusBarUtil; 21 | import com.zhaoss.videoplayerdemo.view.VideoTouchView; 22 | 23 | import java.util.ArrayList; 24 | 25 | /** 26 | * Created by zhaoshuang on 2018/11/12. 27 | */ 28 | 29 | public class VideoDetailsActivity extends BaseActivity { 30 | 31 | private RecyclerView rv_video_detail; 32 | private LinearLayoutManager linearLayoutManager; 33 | private PagerSnapHelper pagerSnapHelper; 34 | private MediaPlayerTool mMediaPlayerTool; 35 | private ArrayList dataList; 36 | private ImageView iv_close; 37 | private int playPosition; 38 | 39 | @Override 40 | protected void onCreate(@Nullable Bundle savedInstanceState) { 41 | super.onCreate(savedInstanceState); 42 | getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); 43 | setContentView(R.layout.activity_video_details); 44 | 45 | initIntent(); 46 | initUI(); 47 | 48 | mMediaPlayerTool = MediaPlayerTool.getInstance(); 49 | rv_video_detail.post(new Runnable() { 50 | @Override 51 | public void run() { 52 | rv_video_detail.scrollToPosition(playPosition); 53 | rv_video_detail.post(new Runnable() { 54 | @Override 55 | public void run() { 56 | playVisibleVideo(mMediaPlayerTool.isPlaying()); 57 | } 58 | }); 59 | } 60 | }); 61 | } 62 | 63 | private void initIntent() { 64 | 65 | Intent intent = getIntent(); 66 | playPosition = intent.getIntExtra(IntentUtil.INTENT_PLAY_POSITION, 0); 67 | dataList = (ArrayList) intent.getSerializableExtra(IntentUtil.INTENT_DATA_LIST); 68 | } 69 | 70 | private void initUI(){ 71 | 72 | rv_video_detail = findViewById(R.id.rv_video_detail); 73 | iv_close = findViewById(R.id.iv_close); 74 | 75 | linearLayoutManager = new LinearLayoutManager(mContext); 76 | rv_video_detail.setLayoutManager(linearLayoutManager); 77 | 78 | pagerSnapHelper = new PagerSnapHelper(); 79 | pagerSnapHelper.attachToRecyclerView(rv_video_detail); 80 | 81 | rv_video_detail.setAdapter(new VideoDetailsAdapter(mContext, dataList)); 82 | 83 | rv_video_detail.addOnScrollListener(new RecyclerView.OnScrollListener() { 84 | @Override 85 | public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 86 | if(newState == RecyclerView.SCROLL_STATE_IDLE){ 87 | if(pagerSnapHelper.findSnapView(linearLayoutManager) != playView){ 88 | playVisibleVideo(false); 89 | } 90 | } 91 | } 92 | }); 93 | 94 | iv_close.setOnClickListener(new View.OnClickListener() { 95 | @Override 96 | public void onClick(View v) { 97 | onBackPressed(); 98 | } 99 | }); 100 | } 101 | 102 | View playView; 103 | /** 104 | * @param isResumePlay 是否继续上个界面播放 105 | */ 106 | private void playVisibleVideo(boolean isResumePlay){ 107 | 108 | View snapView = pagerSnapHelper.findSnapView(linearLayoutManager); 109 | if(snapView == null){ 110 | return ; 111 | } 112 | final int position = linearLayoutManager.getPosition(snapView); 113 | if(position < 0){ 114 | return ; 115 | } 116 | 117 | if(!isResumePlay){ 118 | //重置播放器要在前面 119 | mMediaPlayerTool.reset(); 120 | } 121 | 122 | playView = snapView; 123 | final VideoDetailsAdapter.MyViewHolder vh = (VideoDetailsAdapter.MyViewHolder) rv_video_detail.getChildViewHolder(playView); 124 | 125 | if(isResumePlay){ 126 | vh.pb_video.setVisibility(View.GONE); 127 | vh.iv_cover.setVisibility(View.GONE); 128 | vh.playTextureView.setRotation(mMediaPlayerTool.getRotation()); 129 | vh.playTextureView.setVideoSize(mMediaPlayerTool.getVideoWidth(), mMediaPlayerTool.getVideoHeight()); 130 | setVideoSize(vh, mMediaPlayerTool.getVideoWidth(), mMediaPlayerTool.getVideoHeight()); 131 | }else{ 132 | //显示正在加载的界面 133 | vh.pb_video.setVisibility(View.VISIBLE); 134 | vh.iv_cover.setVisibility(View.VISIBLE); 135 | 136 | mMediaPlayerTool.initMediaPLayer(); 137 | mMediaPlayerTool.setDataSource(dataList.get(position).getVideoUrl()); 138 | } 139 | 140 | vh.videoTouchView.setOnTouchSlideListener(new VideoTouchView.OnTouchSlideListener() { 141 | @Override 142 | public void onSlide(float distant) { 143 | if(mMediaPlayerTool == null){ 144 | return ; 145 | } 146 | if(!vh.rl_change_progress.isShown()){ 147 | vh.rl_change_progress.setVisibility(View.VISIBLE); 148 | changeProgressTime = mMediaPlayerTool.getCurrentPosition(); 149 | } 150 | changeProgressText(vh, distant); 151 | } 152 | @Override 153 | public void onUp() { 154 | if(vh.rl_change_progress.isShown()){ 155 | vh.rl_change_progress.setVisibility(View.GONE); 156 | } 157 | mMediaPlayerTool.seekTo(changeProgressTime); 158 | } 159 | @Override 160 | public void onClick() { 161 | mContext.onBackPressed(); 162 | } 163 | }); 164 | 165 | mMediaPlayerTool.setVolume(1); 166 | mMediaPlayerTool.setVideoListener(new MediaPlayerTool.VideoListener() { 167 | @Override 168 | public void onStart() { 169 | vh.pb_video.setVisibility(View.GONE); 170 | vh.iv_cover.postDelayed(new Runnable() { 171 | @Override 172 | public void run() { 173 | vh.iv_cover.setVisibility(View.GONE); 174 | } 175 | }, 200); 176 | vh.playTextureView.setVideoSize(mMediaPlayerTool.getVideoWidth(), mMediaPlayerTool.getVideoHeight()); 177 | setVideoSize(vh, mMediaPlayerTool.getVideoWidth(), mMediaPlayerTool.getVideoHeight()); 178 | } 179 | @Override 180 | public void onRotationInfo(int rotation) { 181 | vh.playTextureView.setRotation(rotation); 182 | } 183 | @Override 184 | public void onStop() { 185 | vh.pb_video.setVisibility(View.GONE); 186 | vh.iv_cover.setVisibility(View.VISIBLE); 187 | vh.pb_play_progress.setSecondaryProgress(0); 188 | vh.pb_play_progress.setProgress(0); 189 | vh.tv_progress.setText(""); 190 | playView = null; 191 | } 192 | @Override 193 | public void onCompletion() { 194 | onStop(); 195 | if(position+1 >= dataList.size()) { 196 | rv_video_detail.smoothScrollToPosition(0); 197 | }else{ 198 | rv_video_detail.smoothScrollToPosition(position+1); 199 | } 200 | } 201 | @Override 202 | public void onPlayProgress(long currentPosition) { 203 | int pro = (int) (currentPosition*1f/ mMediaPlayerTool.getDuration()*100); 204 | vh.pb_play_progress.setProgress(pro); 205 | 206 | String currentPositionStr = Util.fromMMss(currentPosition); 207 | String videoDurationStr = Util.fromMMss(mMediaPlayerTool.getDuration()); 208 | vh.tv_progress.setText(currentPositionStr + "/" + videoDurationStr); 209 | } 210 | @Override 211 | public void onBufferProgress(int progress) { 212 | vh.pb_play_progress.setSecondaryProgress(progress); 213 | } 214 | }); 215 | 216 | if(isResumePlay){ 217 | vh.playTextureView.resetTextureView(mMediaPlayerTool.getAvailableSurfaceTexture()); 218 | mMediaPlayerTool.setPlayTextureView(vh.playTextureView); 219 | vh.playTextureView.postInvalidate(); 220 | }else{ 221 | vh.playTextureView.resetTextureView(); 222 | mMediaPlayerTool.setPlayTextureView(vh.playTextureView); 223 | mMediaPlayerTool.setSurfaceTexture(vh.playTextureView.getSurfaceTexture()); 224 | mMediaPlayerTool.prepare(); 225 | } 226 | } 227 | 228 | private void setVideoSize(VideoDetailsAdapter.MyViewHolder vh, int videoWidth, int videoHeight){ 229 | 230 | float videoRatio = videoWidth * 1f / videoHeight; 231 | int windowWidth = Util.getWindowWidth(); 232 | int windowHeight = Util.getWindowHeight() + StatusBarUtil.getStatusHeight(mContext); 233 | float windowRatio = Util.getWindowWidth()*1f/ Util.getWindowHeight(); 234 | ViewGroup.LayoutParams layoutParams = vh.videoTouchView.getLayoutParams(); 235 | if (videoRatio >= windowRatio) { 236 | layoutParams.width = windowWidth; 237 | layoutParams.height = (int) (layoutParams.width / videoRatio); 238 | } else { 239 | layoutParams.height = windowHeight; 240 | layoutParams.width = (int) (layoutParams.height * videoRatio); 241 | } 242 | vh.videoTouchView.setLayoutParams(layoutParams); 243 | } 244 | 245 | long changeProgressTime; 246 | private void changeProgressText(VideoDetailsAdapter.MyViewHolder vh, float distant){ 247 | 248 | float radio = distant/vh.pb_play_progress.getWidth(); 249 | changeProgressTime += mMediaPlayerTool.getDuration()*radio; 250 | 251 | if(changeProgressTime < 0){ 252 | changeProgressTime = 0; 253 | } 254 | if(changeProgressTime > mMediaPlayerTool.getDuration()){ 255 | changeProgressTime = mMediaPlayerTool.getDuration(); 256 | } 257 | 258 | String changeTimeStr = Util.fromMMss(changeProgressTime); 259 | String rawTime = Util.fromMMss(mMediaPlayerTool.getDuration()); 260 | vh.tv_change_progress.setText(changeTimeStr+" / "+rawTime); 261 | 262 | if(changeProgressTime > mMediaPlayerTool.getCurrentPosition()){ 263 | vh.iv_change_progress.setImageResource(R.mipmap.video_fast_forward); 264 | }else{ 265 | vh.iv_change_progress.setImageResource(R.mipmap.video_fast_back); 266 | } 267 | } 268 | 269 | boolean isFirst = true; 270 | @Override 271 | public void onWindowFocusChanged(boolean hasFocus) { 272 | super.onWindowFocusChanged(hasFocus); 273 | if(isFirst){ 274 | isFirst = false; 275 | } 276 | } 277 | 278 | @Override 279 | protected void onResume() { 280 | super.onResume(); 281 | 282 | if(!isFirst && mMediaPlayerTool !=null && !mMediaPlayerTool.isPlaying()){ 283 | playVisibleVideo(false); 284 | } 285 | } 286 | 287 | @Override 288 | protected void onPause() { 289 | super.onPause(); 290 | 291 | if(mMediaPlayerTool!=null && mMediaPlayerTool.isPlaying()) { 292 | if (dontPause) { 293 | View snapView = pagerSnapHelper.findSnapView(linearLayoutManager); 294 | if(snapView!=null && linearLayoutManager.getPosition(snapView)!=playPosition){ 295 | mMediaPlayerTool.reset(); 296 | } 297 | } else { 298 | mMediaPlayerTool.reset(); 299 | } 300 | } 301 | } 302 | 303 | boolean dontPause; 304 | @Override 305 | public void finish() { 306 | super.finish(); 307 | 308 | dontPause = true; 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/adapter/MainAdapter.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.adapter; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ImageView; 8 | import android.widget.ProgressBar; 9 | import android.widget.RelativeLayout; 10 | import android.widget.TextView; 11 | 12 | import com.bumptech.glide.Glide; 13 | import com.zhaoss.videoplayerdemo.R; 14 | import com.zhaoss.videoplayerdemo.activity.MainActivity; 15 | import com.zhaoss.videoplayerdemo.bean.MainVideoBean; 16 | import com.zhaoss.videoplayerdemo.util.IntentUtil; 17 | import com.zhaoss.videoplayerdemo.view.PlayTextureView; 18 | 19 | import java.util.ArrayList; 20 | 21 | /** 22 | * Created by zhaoshuang on 2018/11/1. 23 | */ 24 | 25 | public class MainAdapter extends RecyclerView.Adapter{ 26 | 27 | private MainActivity mContext; 28 | private ArrayList mainVideoBeanList; 29 | 30 | public MainAdapter(MainActivity context, ArrayList mainVideoBeanList){ 31 | this.mContext = context; 32 | this.mainVideoBeanList = mainVideoBeanList; 33 | } 34 | 35 | @NonNull 36 | @Override 37 | public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 38 | return new MyViewHolder(View.inflate(mContext, R.layout.item_video, null)); 39 | } 40 | 41 | @Override 42 | public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, final int position) { 43 | 44 | final MyViewHolder vh = (MyViewHolder) holder; 45 | MainVideoBean mainVideoBean = mainVideoBeanList.get(position); 46 | 47 | Glide.with(mContext).load(mainVideoBean.getAvatarRes()).into(vh.iv_avatar); 48 | vh.tv_content.setText(mainVideoBean.getContent()); 49 | vh.tv_name.setText(mainVideoBean.getUserName()); 50 | 51 | Glide.with(mContext).load(mainVideoBean.getCoverUrl()).into(vh.iv_cover); 52 | 53 | vh.playTextureView.setOnClickListener(new View.OnClickListener() { 54 | @Override 55 | public void onClick(View v) { 56 | mContext.jumpNotCloseMediaPlay(position); 57 | IntentUtil.gotoVideoDetailsActivity(mContext, mainVideoBeanList, position, vh.playTextureView); 58 | } 59 | }); 60 | } 61 | 62 | @Override 63 | public int getItemCount() { 64 | return mainVideoBeanList.size(); 65 | } 66 | 67 | public class MyViewHolder extends RecyclerView.ViewHolder{ 68 | 69 | public RelativeLayout rl_video; 70 | public PlayTextureView playTextureView; 71 | public ImageView iv_cover; 72 | public ProgressBar pb_video; 73 | public ImageView iv_play_icon; 74 | public TextView tv_play_time; 75 | 76 | private TextView tv_content; 77 | private ImageView iv_avatar; 78 | private TextView tv_name; 79 | 80 | public MyViewHolder(View itemView) { 81 | super(itemView); 82 | 83 | itemView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 84 | 85 | rl_video = itemView.findViewById(R.id.rl_video); 86 | playTextureView = itemView.findViewById(R.id.playTextureView); 87 | iv_cover = itemView.findViewById(R.id.iv_cover); 88 | pb_video = itemView.findViewById(R.id.pb_video); 89 | iv_play_icon = itemView.findViewById(R.id.iv_play_icon); 90 | tv_content = itemView.findViewById(R.id.tv_content); 91 | iv_avatar = itemView.findViewById(R.id.iv_avatar); 92 | tv_name = itemView.findViewById(R.id.tv_name); 93 | tv_play_time = itemView.findViewById(R.id.tv_play_time); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/adapter/VideoDetailsAdapter.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.adapter; 2 | 3 | import android.content.Context; 4 | import android.graphics.PorterDuff; 5 | import android.support.annotation.NonNull; 6 | import android.support.v4.content.ContextCompat; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.ImageView; 11 | import android.widget.ProgressBar; 12 | import android.widget.RelativeLayout; 13 | import android.widget.TextView; 14 | 15 | import com.bumptech.glide.Glide; 16 | import com.zhaoss.videoplayerdemo.R; 17 | import com.zhaoss.videoplayerdemo.bean.MainVideoBean; 18 | import com.zhaoss.videoplayerdemo.view.PlayTextureView; 19 | import com.zhaoss.videoplayerdemo.view.VideoTouchView; 20 | 21 | import java.util.ArrayList; 22 | 23 | /** 24 | * Created by zhaoshuang on 2018/11/12. 25 | */ 26 | 27 | public class VideoDetailsAdapter extends RecyclerView.Adapter { 28 | 29 | private Context mContext; 30 | private ArrayList mainVideoBeanList; 31 | 32 | public VideoDetailsAdapter(Context mContext, ArrayList mainVideoBeanList) { 33 | this.mContext = mContext; 34 | this.mainVideoBeanList = mainVideoBeanList; 35 | } 36 | 37 | @NonNull 38 | @Override 39 | public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 40 | return new MyViewHolder(View.inflate(mContext, R.layout.item_video_details, null)); 41 | } 42 | 43 | @Override 44 | public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { 45 | 46 | MyViewHolder vh = (MyViewHolder) holder; 47 | MainVideoBean mainVideoBean = mainVideoBeanList.get(position); 48 | 49 | vh.tv_progress.setText(""); 50 | vh.pb_play_progress.setSecondaryProgress(0); 51 | vh.pb_play_progress.setProgress(0); 52 | 53 | Glide.with(mContext).load(mainVideoBean.getAvatarRes()).into(vh.iv_avatar); 54 | Glide.with(mContext).load(mainVideoBean.getCoverUrl()).into(vh.iv_cover); 55 | 56 | vh.tv_content.setText(mainVideoBean.getContent()); 57 | vh.tv_name.setText(mainVideoBean.getUserName()); 58 | } 59 | 60 | @Override 61 | public int getItemCount() { 62 | return mainVideoBeanList.size(); 63 | } 64 | 65 | public class MyViewHolder extends RecyclerView.ViewHolder{ 66 | 67 | public VideoTouchView videoTouchView; 68 | public ImageView iv_cover; 69 | public PlayTextureView playTextureView; 70 | public ProgressBar pb_video; 71 | public ProgressBar pb_play_progress; 72 | public TextView tv_progress; 73 | public RelativeLayout rl_change_progress; 74 | public ImageView iv_change_progress; 75 | public TextView tv_change_progress; 76 | private ImageView iv_avatar; 77 | private TextView tv_name; 78 | private TextView tv_content; 79 | 80 | public MyViewHolder(View itemView) { 81 | super(itemView); 82 | 83 | videoTouchView = itemView.findViewById(R.id.videoTouchView); 84 | playTextureView = itemView.findViewById(R.id.playTextureView); 85 | iv_cover = itemView.findViewById(R.id.iv_cover); 86 | pb_video = itemView.findViewById(R.id.pb_video); 87 | pb_play_progress = itemView.findViewById(R.id.pb_play_progress); 88 | tv_progress = itemView.findViewById(R.id.tv_progress); 89 | rl_change_progress = itemView.findViewById(R.id.rl_change_progress); 90 | iv_change_progress = itemView.findViewById(R.id.iv_change_progress); 91 | tv_change_progress = itemView.findViewById(R.id.tv_change_progress); 92 | iv_avatar = itemView.findViewById(R.id.iv_avatar); 93 | tv_name = itemView.findViewById(R.id.tv_name); 94 | tv_content = itemView.findViewById(R.id.tv_content); 95 | 96 | itemView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 97 | pb_play_progress.getProgressDrawable().setColorFilter(ContextCompat.getColor(mContext, R.color.white), PorterDuff.Mode.SRC_IN); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/bean/MainVideoBean.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.bean; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Created by zhaoshuang on 2018/11/1. 7 | */ 8 | 9 | public class MainVideoBean implements Serializable{ 10 | 11 | private int avatarRes; 12 | private String videoUrl; 13 | private String userName; 14 | private String content; 15 | private String coverUrl; 16 | 17 | public MainVideoBean(int avatarRes, String videoUrl, String userName, String content, String coverUrl) { 18 | this.avatarRes = avatarRes; 19 | this.videoUrl = videoUrl; 20 | this.userName = userName; 21 | this.content = content; 22 | this.coverUrl = coverUrl; 23 | } 24 | 25 | public int getAvatarRes() { 26 | return avatarRes; 27 | } 28 | 29 | public void setAvatarRes(int avatarRes) { 30 | this.avatarRes = avatarRes; 31 | } 32 | 33 | public String getVideoUrl() { 34 | return videoUrl; 35 | } 36 | 37 | public void setVideoUrl(String videoUrl) { 38 | this.videoUrl = videoUrl; 39 | } 40 | 41 | public String getUserName() { 42 | return userName; 43 | } 44 | 45 | public void setUserName(String userName) { 46 | this.userName = userName; 47 | } 48 | 49 | public String getContent() { 50 | return content; 51 | } 52 | 53 | public void setContent(String content) { 54 | this.content = content; 55 | } 56 | 57 | public String getCoverUrl() { 58 | return coverUrl; 59 | } 60 | 61 | public void setCoverUrl(String coverUrl) { 62 | this.coverUrl = coverUrl; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/bean/VideoCacheBean.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.bean; 2 | 3 | import com.litesuits.orm.db.annotation.Column; 4 | import com.litesuits.orm.db.annotation.PrimaryKey; 5 | import com.litesuits.orm.db.enums.AssignType; 6 | 7 | /** 8 | * Created by zhaoshuang on 2018/10/25. 9 | */ 10 | 11 | public class VideoCacheBean { 12 | 13 | public static final String PLAY_TIME = "playTime"; 14 | public static final String KEY = "key"; 15 | 16 | @PrimaryKey(AssignType.BY_MYSELF) 17 | @Column(KEY) 18 | private String key; 19 | @Column(PLAY_TIME) 20 | private long playTime; 21 | @Column("playCount") 22 | private int playCount; 23 | @Column("videoPath") 24 | private String videoPath; 25 | @Column("indexPath") 26 | private String indexPath; 27 | @Column("fileSize") 28 | private long fileSize; 29 | 30 | public String getKey() { 31 | return key; 32 | } 33 | 34 | public void setKey(String key) { 35 | this.key = key; 36 | } 37 | 38 | public long getPlayTime() { 39 | return playTime; 40 | } 41 | 42 | public void setPlayTime(long playTime) { 43 | this.playTime = playTime; 44 | } 45 | 46 | public int getPlayCount() { 47 | return playCount; 48 | } 49 | 50 | public void setPlayCount(int playCount) { 51 | this.playCount = playCount; 52 | } 53 | 54 | public String getVideoPath() { 55 | return videoPath; 56 | } 57 | 58 | public void setVideoPath(String videoPath) { 59 | this.videoPath = videoPath; 60 | } 61 | 62 | public String getIndexPath() { 63 | return indexPath; 64 | } 65 | 66 | public void setIndexPath(String indexPath) { 67 | this.indexPath = indexPath; 68 | } 69 | 70 | public long getFileSize() { 71 | return fileSize; 72 | } 73 | 74 | public void setFileSize(long fileSize) { 75 | this.fileSize = fileSize; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/util/CacheMediaDataSource.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.util; 2 | 3 | import com.zhaoss.videoplayerdemo.bean.VideoCacheBean; 4 | 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.File; 7 | import java.io.FileInputStream; 8 | import java.io.FileOutputStream; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.RandomAccessFile; 12 | import java.nio.charset.StandardCharsets; 13 | import java.security.MessageDigest; 14 | 15 | import okhttp3.Call; 16 | import okhttp3.OkHttpClient; 17 | import okhttp3.Request; 18 | import okhttp3.ResponseBody; 19 | import tv.danmaku.ijk.media.player.misc.IMediaDataSource; 20 | 21 | /** 22 | * 自己实现播放器的网络下载 23 | * Created by zhaoshuang on 2018/11/9. 24 | */ 25 | 26 | public class CacheMediaDataSource implements IMediaDataSource { 27 | 28 | private String mVideoData; 29 | private String mMd5; 30 | 31 | //视频长度 32 | private long contentLength; 33 | //url对应的本地视频文件 34 | private File localVideoFile; 35 | //是否读取的缓存视频 36 | private boolean isCacheVideo; 37 | //网络流 38 | private InputStream networkInPutStream; 39 | //本地文件流 40 | private RandomAccessFile localStream; 41 | 42 | public CacheMediaDataSource(String videoData) { 43 | this.mVideoData = videoData; 44 | mMd5 = MD5(videoData); 45 | } 46 | 47 | public String MD5(String s) { 48 | try { 49 | MessageDigest md = MessageDigest.getInstance("MD5"); 50 | byte[] bytes = md.digest(s.getBytes(StandardCharsets.UTF_8)); 51 | 52 | final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray(); 53 | StringBuilder ret = new StringBuilder(bytes.length * 2); 54 | for (int i=0; i> 4) & 0x0f]); 56 | ret.append(HEX_DIGITS[bytes[i] & 0x0f]); 57 | } 58 | return ret.toString(); 59 | } catch (Exception e) { 60 | e.printStackTrace(); 61 | } 62 | return ""; 63 | } 64 | 65 | /** 66 | * 67 | * @param position 视频流读取进度 68 | * @param buffer 要把读取到的数据存到这个数组 69 | * @param offset 数据开始写入的坐标 70 | * @param size 本次一共读取数据的大小 71 | * @throws IOException 72 | */ 73 | //记录当前读取流的索引 74 | long mPosition = 0; 75 | @Override 76 | public int readAt(long position, byte[] buffer, int offset, int size) throws IOException { 77 | 78 | if(position>=contentLength || localStream==null){ 79 | return -1; 80 | } 81 | 82 | //是否将此字节缓存到本地 83 | boolean isWriteVideo = syncInputStream(position); 84 | 85 | //读取的流的长度不能大于contentLength 86 | if (position+size > contentLength) { 87 | size -= position+size-contentLength; 88 | } 89 | 90 | //读取指定大小的视频数据 91 | byte[] bytes; 92 | if(isCacheVideo){ 93 | //从本地读取 94 | bytes = readByteBySize(localStream, size); 95 | }else{ 96 | //从网络读取 97 | bytes = readByteBySize(networkInPutStream, size); 98 | } 99 | if(bytes != null) { 100 | //写入到播放器的数组中 101 | System.arraycopy(bytes, 0, buffer, offset, size); 102 | if (isWriteVideo && !isCacheVideo) { 103 | //缓存到本地 104 | localStream.write(bytes); 105 | } 106 | //记录数据流读取到哪步了 107 | mPosition += size; 108 | } 109 | 110 | return size; 111 | } 112 | 113 | //同步数据流 114 | private boolean syncInputStream(long position) throws IOException { 115 | 116 | boolean isWriteVideo = true; 117 | //判断两次读取数据是否连续 118 | if(mPosition != position){ 119 | if(isCacheVideo){ 120 | //如果是本地缓存, 直接跳转到该索引 121 | localStream.seek(position); 122 | }else{ 123 | if(mPosition > position){ 124 | //同步本地缓存流 125 | localStream.close(); 126 | deleteFileByPosition(position); 127 | localStream.seek(position); 128 | }else{ 129 | isWriteVideo = false; 130 | } 131 | networkInPutStream.close(); 132 | //重新开启一个网络流 133 | networkInPutStream = openHttpClient((int) position); 134 | } 135 | mPosition = position; 136 | } 137 | return isWriteVideo; 138 | } 139 | 140 | /** 141 | * 从inputStream里读取size大小的数据 142 | */ 143 | private byte[] readByteBySize(InputStream inputStream, int size) throws IOException { 144 | 145 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 146 | 147 | byte[] buf = new byte[size]; 148 | int len; 149 | while ((len = inputStream.read(buf)) != -1) { 150 | out.write(buf, 0, len); 151 | if (out.size() == size) { 152 | return out.toByteArray(); 153 | } else { 154 | buf = new byte[size - out.size()]; 155 | } 156 | } 157 | return null; 158 | } 159 | 160 | /** 161 | * 从inputStream里读取size大小的数据 162 | */ 163 | private byte[] readByteBySize(RandomAccessFile inputStream, int size) throws IOException { 164 | 165 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 166 | 167 | byte[] buf = new byte[size]; 168 | int len; 169 | while ((len = inputStream.read(buf)) != -1) { 170 | out.write(buf, 0, len); 171 | if (out.size() == size) { 172 | return out.toByteArray(); 173 | } else { 174 | buf = new byte[size - out.size()]; 175 | } 176 | } 177 | return null; 178 | } 179 | 180 | /** 181 | * 删除file一部分字节, 从position到file.size 182 | */ 183 | private void deleteFileByPosition(long position) throws IOException { 184 | 185 | FileInputStream in = new FileInputStream(localVideoFile); 186 | 187 | File tempFile = VideoLRUCacheUtil.createTempFile(); 188 | FileOutputStream out = new FileOutputStream(tempFile); 189 | 190 | byte[] buf = new byte[8192]; 191 | int len; 192 | while ((len = in.read(buf)) != -1) { 193 | if(position <= len){ 194 | out.write(buf, 0, (int) position); 195 | out.close(); 196 | 197 | in.close(); 198 | localVideoFile.delete(); 199 | tempFile.renameTo(localVideoFile); 200 | localStream = new RandomAccessFile(localVideoFile, "rw"); 201 | return ; 202 | }else{ 203 | position -= len; 204 | out.write(buf, 0, len); 205 | } 206 | } 207 | tempFile.delete(); 208 | } 209 | 210 | @Override 211 | public long getSize() throws IOException { 212 | 213 | if(networkInPutStream == null) { 214 | initInputStream(); 215 | } 216 | return contentLength; 217 | } 218 | 219 | //初始化一个视频流出来, 可能是本地或网络 220 | private void initInputStream() throws IOException { 221 | 222 | File file; 223 | if(!mVideoData.startsWith("http")){ 224 | file = new File(mVideoData); 225 | }else { 226 | file = checkCache(mMd5); 227 | } 228 | 229 | if(file!=null){ 230 | if(file.exists()) { 231 | //更新一下缓存文件 232 | VideoLRUCacheUtil.updateVideoCacheBean(mMd5, file.getAbsolutePath(), ""); 233 | //读取的本地缓存文件 234 | isCacheVideo = true; 235 | localVideoFile = file; 236 | //开启一个本地视频流 237 | localStream = new RandomAccessFile(localVideoFile, "rw"); 238 | contentLength = file.length(); 239 | }else{ 240 | throw new IOException("文件不存在"); 241 | } 242 | }else { 243 | //没有缓存 开启一个网络流, 并且开启一个缓存流, 实现视频缓存 244 | isCacheVideo = false; 245 | //开启一个网络视频流 246 | networkInPutStream = openHttpClient(0); 247 | //要写入的本地缓存文件 248 | localVideoFile = VideoLRUCacheUtil.createCacheFile(mMd5); 249 | //要写入的本地缓存视频流 250 | localStream = new RandomAccessFile(localVideoFile, "rw"); 251 | } 252 | } 253 | 254 | //检查本地是否有缓存, 2步确认, 数据库中是否存在, 本地文件是否存在 255 | private File checkCache(String md5){ 256 | 257 | //查询数据库 258 | VideoCacheBean bean = VideoCacheDBUtil.query(md5); 259 | if(bean != null){ 260 | File file = new File(bean.getVideoPath()); 261 | if(file.exists()){ 262 | return file; 263 | } 264 | } 265 | return null; 266 | } 267 | 268 | //打开一个网络视频流, 从startIndex开始下载 269 | private InputStream openHttpClient(int startIndex) throws IOException { 270 | 271 | OkHttpClient okHttpClient = new OkHttpClient(); 272 | Request request = new Request.Builder() 273 | .header("RANGE", "bytes=" + startIndex + "-") 274 | .url(mVideoData) 275 | .get() 276 | .build(); 277 | Call call = okHttpClient.newCall(request); 278 | ResponseBody responseBody = call.execute().body(); 279 | if(responseBody != null){ 280 | contentLength = responseBody.contentLength()+startIndex; 281 | return responseBody.byteStream(); 282 | }else{ 283 | return null; 284 | } 285 | } 286 | 287 | public void onError(){ 288 | VideoCacheBean bean = VideoCacheDBUtil.query(mMd5); 289 | if(bean != null){ 290 | VideoCacheDBUtil.delete(bean); 291 | } 292 | } 293 | 294 | @Override 295 | public void close() throws IOException { 296 | if(networkInPutStream != null){ 297 | networkInPutStream.close(); 298 | networkInPutStream = null; 299 | } 300 | if(localStream != null){ 301 | localStream.close(); 302 | localStream = null; 303 | } 304 | 305 | if(localVideoFile!=null && localVideoFile.length()!=contentLength){ 306 | localVideoFile.delete(); 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/util/DataUtil.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.util; 2 | 3 | import com.zhaoss.videoplayerdemo.R; 4 | import com.zhaoss.videoplayerdemo.bean.MainVideoBean; 5 | 6 | import java.util.ArrayList; 7 | 8 | /** 9 | * 假数据 10 | * Created by zhaoshuang on 2018/11/13. 11 | */ 12 | 13 | public class DataUtil { 14 | 15 | public static ArrayList createData(){ 16 | 17 | ArrayList mainVideoBeanList = new ArrayList<>(); 18 | 19 | mainVideoBeanList.add(new MainVideoBean(R.mipmap.avatar1, 20 | "https://oimryzjfe.qnssl.com/content/1F3D7F815F2C6870FB512B8CA2C3D2C1.mp4", 21 | "程序员小陈1", 22 | "在即将正式发布的 Android P 版本中,谷歌彻底取消了对Apache HTTPClient的支持, 谷歌就推荐在Android2.3版本以上使用HttpURLConnection", 23 | "https://oimryzjfe.qnssl.com/content/2EF3AE01AC1D36BDF42EFA87C275BB66.jpg")); 24 | mainVideoBeanList.add(new MainVideoBean(R.mipmap.avatar1, 25 | "https://oimryzjfe.qnssl.com/content/fe9cfd1402bb40490bc9a208db7c0921.mp4", 26 | "程序员小陈2", 27 | "在即将正式发布的 Android P 版本中,谷歌彻底取消了对Apache HTTPClient的支持, 谷歌就推荐在Android2.3版本以上使用HttpURLConnection", 28 | "https://oimryzjfe.qnssl.com/content/022294301d2d7da585eae4bbbe3653f6.png")); 29 | mainVideoBeanList.add(new MainVideoBean(R.mipmap.avatar1, 30 | "https://oimryzjfe.qnssl.com/content/47465d359406bb4b68c8c205e2974807.mp4", 31 | "程序员小陈3", 32 | "在即将正式发布的 Android P 版本中,谷歌彻底取消了对Apache HTTPClient的支持, 谷歌就推荐在Android2.3版本以上使用HttpURLConnection", 33 | "https://oimryzjfe.qnssl.com/content/07e99ee99ce291608687b2a9e87d546e.png")); 34 | mainVideoBeanList.add(new MainVideoBean(R.mipmap.avatar1, 35 | "https://oimryzjfe.qnssl.com/content/93fcbd491e40159e949bb4cb191e231e.mp4", 36 | "程序员小陈4", 37 | "在即将正式发布的 Android P 版本中,谷歌彻底取消了对Apache HTTPClient的支持, 谷歌就推荐在Android2.3版本以上使用HttpURLConnection", 38 | "https://oimryzjfe.qnssl.com/content/1f80d66e88c6215a9e70d59aab0c0dd3.png")); 39 | mainVideoBeanList.add(new MainVideoBean(R.mipmap.avatar1, 40 | "https://oimryzjfe.qnssl.com/content/beee1c9325330b845b13298842f711ff.mp4", 41 | "程序员小陈5", 42 | "在即将正式发布的 Android P 版本中,谷歌彻底取消了对Apache HTTPClient的支持, 谷歌就推荐在Android2.3版本以上使用HttpURLConnection", 43 | "https://oimryzjfe.qnssl.com/content/935d8258e862f8d2bfd50ac1fd6af639.png")); 44 | mainVideoBeanList.add(new MainVideoBean(R.mipmap.avatar1, 45 | "https://oimryzjfe.qnssl.com/content/807403BE56FD9503A609975B81BA4636.mp4", 46 | "程序员小陈6", 47 | "在即将正式发布的 Android P 版本中,谷歌彻底取消了对Apache HTTPClient的支持, 谷歌就推荐在Android2.3版本以上使用HttpURLConnection", 48 | "https://oimryzjfe.qnssl.com/content/D95667408AAC4209701ECF09B896C0B7.jpg")); 49 | mainVideoBeanList.add(new MainVideoBean(R.mipmap.avatar1, 50 | "https://oimryzjfe.qnssl.com/content/68239E7D6DC93D98E083137F0C537D97.mp4", 51 | "程序员小陈7", 52 | "在即将正式发布的 Android P 版本中,谷歌彻底取消了对Apache HTTPClient的支持, 谷歌就推荐在Android2.3版本以上使用HttpURLConnection", 53 | "https://oimryzjfe.qnssl.com/content/DCB821BA35E4DA20DF5CE450F621D10B.jpg")); 54 | mainVideoBeanList.add(new MainVideoBean(R.mipmap.avatar1, 55 | "https://oimryzjfe.qnssl.com/content/afc192cfae2df1366d7268bc7a181555.mp4", 56 | "程序员小陈8", 57 | "在即将正式发布的 Android P 版本中,谷歌彻底取消了对Apache HTTPClient的支持, 谷歌就推荐在Android2.3版本以上使用HttpURLConnection", 58 | "https://oimryzjfe.qnssl.com/content/e72446a57dbf64e234bad0582ecdb44e.png")); 59 | mainVideoBeanList.add(new MainVideoBean(R.mipmap.avatar1, 60 | "https://oimryzjfe.qnssl.com/content/2c61c7c5e95b3f4dec31aa42e4315bb1.mp4", 61 | "程序员小陈9", 62 | "在即将正式发布的 Android P 版本中,谷歌彻底取消了对Apache HTTPClient的支持, 谷歌就推荐在Android2.3版本以上使用HttpURLConnection", 63 | "https://oimryzjfe.qnssl.com/content/a92ba82c9c9c44152f3523d000cfbb9c.png")); 64 | mainVideoBeanList.add(new MainVideoBean(R.mipmap.avatar1, 65 | "https://oimryzjfe.qnssl.com/content/0fcbbe738abf1bf524dc2e7818200cc8.mp4", 66 | "程序员小陈10", 67 | "在即将正式发布的 Android P 版本中,谷歌彻底取消了对Apache HTTPClient的支持, 谷歌就推荐在Android2.3版本以上使用HttpURLConnection", 68 | "https://oimryzjfe.qnssl.com/content/f38ce694e89a462ea79eb6f16b94ead7.png")); 69 | 70 | return mainVideoBeanList; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/util/IntentUtil.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.util; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.support.v4.app.ActivityCompat; 6 | import android.support.v4.app.ActivityOptionsCompat; 7 | import android.view.View; 8 | 9 | import com.zhaoss.videoplayerdemo.activity.VideoDetailsActivity; 10 | import com.zhaoss.videoplayerdemo.bean.MainVideoBean; 11 | 12 | import java.util.ArrayList; 13 | 14 | /** 15 | * Created by zhaoshuang on 2018/11/12. 16 | */ 17 | 18 | public class IntentUtil { 19 | 20 | public static final String INTENT_DATA_LIST = "intent_data_list"; 21 | public static final String INTENT_PLAY_POSITION = "intent_play_position"; 22 | 23 | public static void gotoVideoDetailsActivity(Activity activity, ArrayList dataList, int playPosition, View animationView){ 24 | 25 | Intent intent = new Intent(activity, VideoDetailsActivity.class); 26 | intent.putExtra(INTENT_DATA_LIST, dataList); 27 | intent.putExtra(INTENT_PLAY_POSITION, playPosition); 28 | 29 | ActivityOptionsCompat compat = ActivityOptionsCompat.makeClipRevealAnimation(animationView, 0, 0, animationView.getWidth(), animationView.getHeight()); 30 | ActivityCompat.startActivity(activity, intent, compat.toBundle()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/util/LogUtil.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.util; 2 | 3 | import android.util.Log; 4 | 5 | /** 6 | * Created by zhaoshuang on 2018/6/1. 7 | */ 8 | 9 | public class LogUtil { 10 | 11 | public static void print(String text){ 12 | Log.i("Log.i", text); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/util/MediaPlayerTool.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.util; 2 | 3 | import android.graphics.SurfaceTexture; 4 | import android.text.TextUtils; 5 | import android.view.Surface; 6 | 7 | import com.zhaoss.videoplayerdemo.view.PlayTextureView; 8 | 9 | import java.io.File; 10 | import java.lang.ref.SoftReference; 11 | import java.util.concurrent.TimeUnit; 12 | import java.util.concurrent.atomic.AtomicInteger; 13 | 14 | import io.reactivex.Observable; 15 | import io.reactivex.ObservableSource; 16 | import io.reactivex.android.schedulers.AndroidSchedulers; 17 | import io.reactivex.disposables.Disposable; 18 | import io.reactivex.functions.Consumer; 19 | import io.reactivex.functions.Function; 20 | import tv.danmaku.ijk.media.player.AndroidMediaPlayer; 21 | import tv.danmaku.ijk.media.player.IMediaPlayer; 22 | import tv.danmaku.ijk.media.player.IjkMediaPlayer; 23 | 24 | /** 25 | * Created by zhaoshuang on 2018/9/13. 26 | */ 27 | 28 | public class MediaPlayerTool implements IMediaPlayer.OnCompletionListener, IMediaPlayer.OnErrorListener, 29 | IMediaPlayer.OnBufferingUpdateListener, IMediaPlayer.OnPreparedListener, IMediaPlayer.OnInfoListener { 30 | 31 | //ijkio协议 32 | public static final String IJK_CACHE_HEAD = "ijkio:cache:ffio:"; 33 | 34 | private IMediaPlayer mMediaPlayer; 35 | private VideoListener mVideoListener; 36 | private SurfaceTexture playSurfaceTexture; 37 | //记录上次播放器的hasCode 38 | private AtomicInteger playHasCode = new AtomicInteger(0); 39 | //视频旋转播放角度 40 | private int mRotation; 41 | //视频时长 42 | private long mDuration; 43 | //视频音量 44 | private float mVolume; 45 | 46 | //加载bilibili库成功 47 | private boolean loadIjkSucc; 48 | 49 | private SoftReference srPlayTextureView; 50 | private String mVideoUrl; 51 | private CacheMediaDataSource mMediaDataSource; 52 | 53 | private MediaPlayerTool(){ 54 | try { 55 | IjkMediaPlayer.loadLibrariesOnce(null); 56 | IjkMediaPlayer.native_profileBegin("libijkplayer.so"); 57 | loadIjkSucc = true; 58 | }catch (UnsatisfiedLinkError e){ 59 | e.printStackTrace(); 60 | loadIjkSucc = false; 61 | } 62 | } 63 | 64 | public static MediaPlayerTool mMediaPlayerTool; 65 | public synchronized static MediaPlayerTool getInstance(){ 66 | 67 | if(mMediaPlayerTool == null){ 68 | mMediaPlayerTool = new MediaPlayerTool(); 69 | } 70 | return mMediaPlayerTool; 71 | } 72 | 73 | public void start(){ 74 | if(mMediaPlayer != null){ 75 | mMediaPlayer.start(); 76 | } 77 | } 78 | 79 | public void pause() { 80 | if(mMediaPlayer != null){ 81 | mMediaPlayer.pause(); 82 | } 83 | } 84 | 85 | public int getVideoWidth(){ 86 | if(mMediaPlayer != null){ 87 | return mMediaPlayer.getVideoWidth(); 88 | } 89 | return 0; 90 | } 91 | 92 | public int getVideoHeight(){ 93 | if(mMediaPlayer != null){ 94 | return mMediaPlayer.getVideoHeight(); 95 | } 96 | return 0; 97 | } 98 | 99 | public void setLooping(boolean looping) { 100 | if(mMediaPlayer != null){ 101 | mMediaPlayer.setLooping(looping); 102 | } 103 | } 104 | 105 | public boolean isLooping() { 106 | if(mMediaPlayer != null){ 107 | return mMediaPlayer.isLooping(); 108 | } 109 | return false; 110 | } 111 | 112 | public void onDestroy() { 113 | reset(); 114 | IjkMediaPlayer.native_profileEnd(); 115 | } 116 | 117 | /** 118 | * @param volume 0-1 119 | */ 120 | public void setVolume(float volume){ 121 | if(mMediaPlayer != null) { 122 | this.mVolume = volume; 123 | mMediaPlayer.setVolume(volume, volume); 124 | } 125 | } 126 | 127 | public float getVolume(){ 128 | return mVolume; 129 | } 130 | 131 | public long getDuration(){ 132 | return mDuration; 133 | } 134 | 135 | public long getCurrentPosition(){ 136 | if(mMediaPlayer != null) { 137 | return mMediaPlayer.getCurrentPosition(); 138 | } 139 | return 0; 140 | } 141 | 142 | public void setPlayTextureView(PlayTextureView playTextureView){ 143 | if(srPlayTextureView != null){ 144 | srPlayTextureView.clear(); 145 | } 146 | srPlayTextureView = new SoftReference(playTextureView); 147 | } 148 | 149 | public void setSurfaceTexture(SurfaceTexture surfaceTexture){ 150 | this.playSurfaceTexture = surfaceTexture; 151 | if(mMediaPlayer!=null && surfaceTexture!=null) { 152 | mMediaPlayer.setSurface(new Surface(surfaceTexture)); 153 | } 154 | } 155 | 156 | public SurfaceTexture getAvailableSurfaceTexture(){ 157 | cleanTextureViewParent(); 158 | return playSurfaceTexture; 159 | } 160 | 161 | private void cleanTextureViewParent(){ 162 | if(srPlayTextureView != null) { 163 | PlayTextureView playTextureView = srPlayTextureView.get(); 164 | if (playTextureView != null) { 165 | playTextureView.resetTextureView(); 166 | } 167 | } 168 | } 169 | 170 | public void seekTo(long msec){ 171 | if(mMediaPlayer != null){ 172 | mMediaPlayer.seekTo(msec); 173 | } 174 | } 175 | 176 | private void checkPath(){ 177 | //初始化缓存路径 178 | File file = new File(VideoLRUCacheUtil.CACHE_DIR_PATH); 179 | if(!file.exists()){ 180 | file.mkdirs(); 181 | } 182 | } 183 | 184 | public void setDataSource(String url){ 185 | setDataSource(url, true); 186 | } 187 | 188 | public void setDataSource(String url, boolean isCache){ 189 | try { 190 | 191 | //自定义缓存架构 192 | //mMediaDataSource = new CacheMediaDataSource(url); 193 | //mMediaPlayer.setDataSource(mMediaDataSource); 194 | 195 | if(isCache){ 196 | mVideoUrl = IJK_CACHE_HEAD+url; 197 | mMediaPlayer.setDataSource(mVideoUrl); 198 | if(mMediaPlayer instanceof IjkMediaPlayer) { 199 | checkPath(); 200 | IjkMediaPlayer ijkMediaPlayer = (IjkMediaPlayer) mMediaPlayer; 201 | String name = Util.MD5(mVideoUrl); 202 | String videoPath = VideoLRUCacheUtil.CACHE_DIR_PATH+name+".v"; 203 | String indexPath = VideoLRUCacheUtil.CACHE_DIR_PATH+name+".i"; 204 | ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "cache_file_path", videoPath); 205 | ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "cache_map_path", indexPath); 206 | VideoLRUCacheUtil.updateVideoCacheBean(name, videoPath, indexPath); 207 | } 208 | }else{ 209 | mVideoUrl = url; 210 | mMediaPlayer.setDataSource(mVideoUrl); 211 | } 212 | } catch (Exception e) { 213 | e.printStackTrace(); 214 | onError(mMediaPlayer, 0, 0); 215 | } 216 | } 217 | 218 | public void prepare(){ 219 | try { 220 | if (mMediaPlayer != null) { 221 | mMediaPlayer.prepareAsync(); 222 | } 223 | }catch (Throwable e){ 224 | e.printStackTrace(); 225 | onError(mMediaPlayer, 0, 0); 226 | } 227 | } 228 | 229 | public int getRotation(){ 230 | return mRotation; 231 | } 232 | 233 | public boolean isPlaying(){ 234 | if(mMediaPlayer != null){ 235 | return mMediaPlayer.isPlaying(); 236 | } 237 | return false; 238 | } 239 | 240 | public void reset(){ 241 | 242 | if(subscribe != null){ 243 | subscribe.dispose(); 244 | subscribe = null; 245 | } 246 | 247 | if(srPlayTextureView != null){ 248 | PlayTextureView playTextureView = srPlayTextureView.get(); 249 | if(playTextureView != null){ 250 | playTextureView.resetTextureView(); 251 | } 252 | srPlayTextureView.clear(); 253 | srPlayTextureView = null; 254 | } 255 | 256 | if(playSurfaceTexture != null) { 257 | playSurfaceTexture.release(); 258 | playSurfaceTexture = null; 259 | } 260 | 261 | if(mVideoListener != null){ 262 | mVideoListener.onStop(); 263 | mVideoListener = null; 264 | } 265 | 266 | if(mMediaPlayer!=null && playHasCode.get()!=mMediaPlayer.hashCode()) { 267 | playHasCode.set(mMediaPlayer.hashCode()); 268 | final IMediaPlayer releaseMediaPlay = mMediaPlayer; 269 | mMediaPlayer = null; 270 | RxJavaUtil.run(new RxJavaUtil.OnRxAndroidListener() { 271 | @Override 272 | public Object doInBackground() throws Throwable { 273 | releaseMediaPlay.stop(); 274 | releaseMediaPlay.release(); 275 | return null; 276 | } 277 | @Override 278 | public void onFinish(Object result) { 279 | } 280 | @Override 281 | public void onError(Throwable e) { 282 | } 283 | }); 284 | } 285 | } 286 | 287 | public void setVideoListener(VideoListener videoListener){ 288 | this.mVideoListener = videoListener; 289 | } 290 | 291 | public static abstract class VideoListener { 292 | //视频开始播放 293 | public void onStart(){}; 294 | //视频被停止播放 295 | public void onStop(){}; 296 | //视频播放完成 297 | public void onCompletion(){}; 298 | //视频旋转角度参数初始化完成 299 | public void onRotationInfo(int rotation){}; 300 | //播放进度 0-1 301 | public void onPlayProgress(long currentPosition){}; 302 | //缓存速度 1-100 303 | public void onBufferProgress(int progress){}; 304 | } 305 | 306 | public void initMediaPLayer(){ 307 | 308 | if(loadIjkSucc){ 309 | IjkMediaPlayer ijkMediaPlayer = new IjkMediaPlayer(); 310 | ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 1); 311 | ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "parse_cache_map", 1); 312 | ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "auto_save_map", 1); 313 | ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 1); 314 | 315 | mMediaPlayer = ijkMediaPlayer; 316 | }else{ 317 | mMediaPlayer = new AndroidMediaPlayer(); 318 | } 319 | 320 | try { 321 | mMediaPlayer.setOnErrorListener(this); 322 | mMediaPlayer.setOnInfoListener(this); 323 | mMediaPlayer.setOnCompletionListener(this); 324 | mMediaPlayer.setOnBufferingUpdateListener(this); 325 | mMediaPlayer.setOnPreparedListener(this); 326 | } catch (Exception e) { 327 | e.printStackTrace(); 328 | } 329 | } 330 | 331 | @Override 332 | public boolean onInfo(IMediaPlayer iMediaPlayer, int what, int extra) { 333 | switch (what){ 334 | case IMediaPlayer.MEDIA_INFO_VIDEO_ROTATION_CHANGED://播放旋转 335 | if(mVideoListener != null){ 336 | mRotation = extra; 337 | mVideoListener.onRotationInfo(extra); 338 | } 339 | break; 340 | } 341 | return true; 342 | } 343 | 344 | @Override 345 | public void onCompletion(IMediaPlayer iMediaPlayer) { 346 | if(mVideoListener != null){ 347 | mVideoListener.onCompletion(); 348 | } 349 | } 350 | 351 | @Override 352 | public boolean onError(IMediaPlayer iMediaPlayer, int what, int extra) { 353 | if(mVideoUrl.startsWith(IJK_CACHE_HEAD)){ 354 | String rawUrl = mVideoUrl.substring(IJK_CACHE_HEAD.length()); 355 | setDataSource(rawUrl, false); 356 | }else{ 357 | if (mVideoListener != null) { 358 | mVideoListener.onStop(); 359 | mVideoListener = null; 360 | } 361 | if(!TextUtils.isEmpty(mVideoUrl)){ 362 | VideoLRUCacheUtil.deleteVideoBean(mVideoUrl); 363 | } 364 | if(mMediaDataSource != null) { 365 | mMediaDataSource.onError(); 366 | } 367 | } 368 | return true; 369 | } 370 | 371 | @Override 372 | public void onBufferingUpdate(IMediaPlayer iMediaPlayer, int percent) { 373 | if(mVideoListener!=null){ 374 | mVideoListener.onBufferProgress(percent); 375 | } 376 | } 377 | 378 | @Override 379 | public void onPrepared(IMediaPlayer iMediaPlayer) { 380 | if(mMediaPlayer != null) { 381 | mMediaPlayer.start(); 382 | mDuration = iMediaPlayer.getDuration(); 383 | loopPlayProgress(); 384 | if (mVideoListener != null) { 385 | mVideoListener.onStart(); 386 | } 387 | } 388 | } 389 | 390 | private Disposable subscribe; 391 | private void loopPlayProgress(){ 392 | subscribe = Observable.interval(0, 100, TimeUnit.MILLISECONDS) 393 | .flatMap(new Function>() { 394 | @Override 395 | public ObservableSource apply(Long aLong) throws Exception { 396 | return Observable.just(aLong + 1); 397 | } 398 | }) 399 | .observeOn(AndroidSchedulers.mainThread()) 400 | .subscribe(new Consumer() { 401 | @Override 402 | public void accept(Long aLong) throws Exception { 403 | if(mVideoListener!=null && mMediaPlayer!=null && mMediaPlayer.isPlaying()){ 404 | mVideoListener.onPlayProgress(mMediaPlayer.getCurrentPosition()); 405 | }else{ 406 | if(subscribe != null){ 407 | subscribe.dispose(); 408 | subscribe = null; 409 | } 410 | } 411 | } 412 | }); 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/util/RxJavaUtil.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.util; 2 | 3 | import io.reactivex.Observable; 4 | import io.reactivex.ObservableEmitter; 5 | import io.reactivex.ObservableOnSubscribe; 6 | import io.reactivex.Observer; 7 | import io.reactivex.android.schedulers.AndroidSchedulers; 8 | import io.reactivex.annotations.NonNull; 9 | import io.reactivex.disposables.Disposable; 10 | import io.reactivex.schedulers.Schedulers; 11 | 12 | /** 13 | * Created by zhaoshuang on 2018/7/13. 14 | * 封装一下RxJava, 更易用 15 | */ 16 | 17 | public class RxJavaUtil { 18 | 19 | public static void run(final OnRxAndroidListener onRxAndroidListener){ 20 | 21 | Observable.create(new ObservableOnSubscribe() { 22 | @Override 23 | public void subscribe(@NonNull ObservableEmitter e){ 24 | try { 25 | T t = onRxAndroidListener.doInBackground(); 26 | if(t != null){ 27 | e.onNext(t); 28 | }else{ 29 | e.onError(new NullPointerException("on doInBackground result not null")); 30 | } 31 | }catch (Throwable throwable){ 32 | e.onError(throwable); 33 | } 34 | } 35 | }) 36 | .subscribeOn(Schedulers.computation()) 37 | .observeOn(AndroidSchedulers.mainThread()) 38 | .safeSubscribe(new Observer() { 39 | @Override 40 | public void onSubscribe(@NonNull Disposable d) { 41 | } 42 | @Override 43 | public void onNext(@NonNull T result) { 44 | onRxAndroidListener.onFinish(result); 45 | } 46 | @Override 47 | public void onError(@NonNull Throwable e) { 48 | onRxAndroidListener.onError(e); 49 | } 50 | @Override 51 | public void onComplete() { 52 | } 53 | }); 54 | } 55 | 56 | public interface OnRxAndroidListener { 57 | //在子线程执行 58 | T doInBackground() throws Throwable; 59 | //事件执行成功, 在主线程回调 60 | void onFinish(T result); 61 | //事件执行失败, 在主线程回调 62 | void onError(Throwable e); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/util/StatusBarUtil.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.util; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.view.Window; 9 | import android.view.WindowManager; 10 | 11 | import com.zhaoss.videoplayerdemo.R; 12 | 13 | import java.lang.reflect.Field; 14 | import java.lang.reflect.Method; 15 | 16 | public class StatusBarUtil { 17 | 18 | //是否沉浸式 19 | private static boolean immersiveMode; 20 | //是否状态栏白底黑字 21 | private static boolean blackext; 22 | 23 | /** 24 | * 设置状态栏颜色 25 | * @param color 状态栏颜色值 26 | * @param isFontColorDark 深色字体模式 27 | */ 28 | public static void setColor(Activity activity, int color, boolean isFontColorDark) { 29 | 30 | if(isFullScreen(activity)) { 31 | return ; 32 | } 33 | 34 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {//5.0 35 | activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 36 | activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 37 | immersiveMode = true; 38 | 39 | setStatusBarColor(activity, color, isFontColorDark); 40 | } 41 | } 42 | 43 | /** 44 | * 覆盖状态栏模式 45 | * @param isFontColorDark 深色字体模式 46 | */ 47 | public static void setCoverStatus(Activity activity, boolean isFontColorDark) { 48 | 49 | if(isFullScreen(activity)) { 50 | return ; 51 | } 52 | 53 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {//5.0 54 | activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 55 | 56 | immersiveMode = true; 57 | setStatusBarColor(activity, 0, isFontColorDark); 58 | } 59 | } 60 | 61 | public static boolean isImmersiveMode(){ 62 | return immersiveMode; 63 | } 64 | 65 | public static boolean isBlackext(){ 66 | return blackext; 67 | } 68 | 69 | /** 70 | * 生成一个和状态栏大小相同的矩形条 71 | * @return 状态栏矩形条 72 | */ 73 | private static View createStatusBarView(Activity activity, int color) { 74 | // 绘制一个和状态栏一样高的矩形 75 | View statusBarView = new View(activity); 76 | ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusHeight(activity)); 77 | statusBarView.setLayoutParams(params); 78 | statusBarView.setBackgroundColor(color); 79 | return statusBarView; 80 | } 81 | 82 | /** 83 | * 设置根布局参数 84 | */ 85 | private static void setRootView(Activity activity) { 86 | ViewGroup rootView = (ViewGroup) ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0); 87 | rootView.setFitsSystemWindows(true); 88 | rootView.setClipToPadding(true); 89 | } 90 | 91 | /** 92 | * 黑色字体 93 | */ 94 | public static void setStatusBarColor(Activity activity, int BgColor, boolean isFontColorDark) { 95 | 96 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 97 | if (setMIUIStatusBarLightMode(activity, isFontColorDark)) {//MIUI 98 | //MIUI9以上api废除, 要调用系统的 99 | setAndroidStatusTextColor(activity, isFontColorDark); 100 | blackext = true; 101 | //miui设置成功 102 | } else if (setFLYMEStatusBarLightMode(activity, isFontColorDark)) {//Flyme 103 | blackext = true; 104 | //魅族设置成功 105 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {//6.0 106 | //系统6.0设置成功 107 | blackext = true; 108 | setAndroidStatusTextColor(activity, isFontColorDark); 109 | }else{ 110 | //黑色字体设置失败, 背景颜色默认 111 | BgColor = activity.getResources().getColor(R.color.my_black); 112 | } 113 | 114 | activity.getWindow().setStatusBarColor(BgColor); 115 | } 116 | } 117 | 118 | //android 6.0以上设置状态栏黑色 119 | private static void setAndroidStatusTextColor(Activity activity, boolean isFontColorDark){ 120 | 121 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {//6.0 122 | activity.getWindow().getDecorView().setSystemUiVisibility( 123 | isFontColorDark ? View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR : View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); 124 | } 125 | } 126 | 127 | /** 128 | * 设置状态栏字体图标为深色,需要MIUI6以上 129 | * @param isFontColorDark 是否把状态栏字体及图标颜色设置为深色 130 | * @return boolean 成功执行返回true 131 | */ 132 | private static boolean setMIUIStatusBarLightMode(Activity activity, boolean isFontColorDark) { 133 | Window window = activity.getWindow(); 134 | boolean result = false; 135 | if (window != null) { 136 | Class clazz = window.getClass(); 137 | try { 138 | int darkModeFlag = 0; 139 | Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams"); 140 | Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE"); 141 | darkModeFlag = field.getInt(layoutParams); 142 | Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class); 143 | if (isFontColorDark) { 144 | extraFlagField.invoke(window, darkModeFlag, darkModeFlag);//状态栏透明且黑色字体 145 | } else { 146 | extraFlagField.invoke(window, 0, darkModeFlag);//清除黑色字体 147 | } 148 | result = true; 149 | } catch (Exception e) { 150 | //not MIUI 151 | } 152 | } 153 | return result; 154 | } 155 | 156 | /** 157 | * 设置状态栏字体图标为深色,魅族4.4 158 | * @param isFontColorDark 是否把状态栏字体及图标颜色设置为深色 159 | * @return boolean 成功执行返回true 160 | */ 161 | private static boolean setFLYMEStatusBarLightMode(Activity activity, boolean isFontColorDark) { 162 | Window window = activity.getWindow(); 163 | boolean result = false; 164 | if (window != null) { 165 | try { 166 | WindowManager.LayoutParams lp = window.getAttributes(); 167 | Field darkFlag = WindowManager.LayoutParams.class.getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON"); 168 | Field meizuFlags = WindowManager.LayoutParams.class.getDeclaredField("meizuFlags"); 169 | darkFlag.setAccessible(true); 170 | meizuFlags.setAccessible(true); 171 | int bit = darkFlag.getInt(null); 172 | int value = meizuFlags.getInt(lp); 173 | if (isFontColorDark) { 174 | value |= bit; 175 | } else { 176 | value &= ~bit; 177 | } 178 | meizuFlags.setInt(lp, value); 179 | window.setAttributes(lp); 180 | result = true; 181 | } catch (Exception e) { 182 | //not meizu 183 | } 184 | } 185 | return result; 186 | } 187 | 188 | 189 | /** 190 | * 状态栏高度 191 | */ 192 | public static int getStatusHeight(Context context) { 193 | 194 | int statusBarHeight = -1; 195 | int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); 196 | if (resourceId > 0) { 197 | statusBarHeight = context.getResources().getDimensionPixelSize(resourceId); 198 | } 199 | return statusBarHeight; 200 | } 201 | 202 | /** 203 | * @param activity 204 | * @return 判断当前手机是否是全屏 205 | */ 206 | public static boolean isFullScreen(Activity activity) { 207 | int flag = activity.getWindow().getAttributes().flags; 208 | return (flag & WindowManager.LayoutParams.FLAG_FULLSCREEN) == WindowManager.LayoutParams.FLAG_FULLSCREEN; 209 | } 210 | 211 | /** 增加View的paddingTop,增加的值为状态栏高度 */ 212 | public static void setPadding(Context context, View view) { 213 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 214 | view.setPadding(view.getPaddingLeft(), view.getPaddingTop() + getStatusHeight(context), 215 | view.getPaddingRight(), view.getPaddingBottom()); 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/util/Util.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.util; 2 | 3 | import android.content.Context; 4 | import android.view.WindowManager; 5 | 6 | import com.zhaoss.videoplayerdemo.MyApplication; 7 | 8 | import java.security.MessageDigest; 9 | 10 | /** 11 | * Created by zhaoshuang on 2018/6/1. 12 | */ 13 | 14 | public class Util { 15 | 16 | public static String MD5(String s) { 17 | try { 18 | MessageDigest md = MessageDigest.getInstance("MD5"); 19 | byte[] bytes = md.digest(s.getBytes("utf-8")); 20 | 21 | final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray(); 22 | StringBuilder ret = new StringBuilder(bytes.length * 2); 23 | for (int i=0; i> 4) & 0x0f]); 25 | ret.append(HEX_DIGITS[bytes[i] & 0x0f]); 26 | } 27 | return ret.toString(); 28 | } catch (Exception e) { 29 | e.printStackTrace(); 30 | } 31 | return ""; 32 | } 33 | 34 | /** 35 | * 分秒 36 | * @param time ms 37 | */ 38 | public static String fromMMss(long time) { 39 | if (time < 0) { 40 | return "00:00"; 41 | } 42 | 43 | int ss = (int) (time / 1000); 44 | int mm = ss / 60; 45 | int s = ss % 60; 46 | int m = mm % 60; 47 | String strM = String.valueOf(m); 48 | String strS = String.valueOf(s); 49 | if (m < 10) { 50 | strM = "0" + strM; 51 | } 52 | if (s < 10) { 53 | strS = "0" + strS; 54 | } 55 | return strM + ":" + strS; 56 | } 57 | 58 | public static int getWindowWidth() { 59 | WindowManager wm = (WindowManager) MyApplication.mContext.getSystemService(Context.WINDOW_SERVICE); 60 | return wm.getDefaultDisplay().getWidth(); 61 | } 62 | 63 | public static int getWindowHeight() { 64 | WindowManager wm = (WindowManager) MyApplication.mContext.getSystemService(Context.WINDOW_SERVICE); 65 | return wm.getDefaultDisplay().getHeight(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/util/VideoCacheDBUtil.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.util; 2 | 3 | import com.litesuits.orm.LiteOrm; 4 | import com.litesuits.orm.db.DataBaseConfig; 5 | import com.litesuits.orm.db.assit.QueryBuilder; 6 | import com.zhaoss.videoplayerdemo.MyApplication; 7 | import com.zhaoss.videoplayerdemo.bean.VideoCacheBean; 8 | 9 | import java.util.ArrayList; 10 | 11 | /** 12 | * Created by zhaoshuang on 2018/10/25. 13 | */ 14 | 15 | public class VideoCacheDBUtil { 16 | 17 | private static LiteOrm liteOrmDB = LiteOrm.newSingleInstance(new DataBaseConfig(MyApplication.mContext, "VideoPlayerDemo")); 18 | 19 | public static void save(VideoCacheBean bean) { 20 | liteOrmDB.save(bean); 21 | } 22 | 23 | public static void delete(VideoCacheBean bean) { 24 | liteOrmDB.delete(bean); 25 | } 26 | 27 | //根据播放时间 降序 28 | public static ArrayList query() { 29 | ArrayList list = liteOrmDB 30 | .query(new QueryBuilder<>(VideoCacheBean.class).appendOrderDescBy(VideoCacheBean.PLAY_TIME)); 31 | return list; 32 | } 33 | 34 | public static VideoCacheBean query(String key) { 35 | ArrayList list = liteOrmDB 36 | .query(new QueryBuilder<>(VideoCacheBean.class).where(VideoCacheBean.KEY + "=?", key)); 37 | if(list.size() > 0){ 38 | return list.get(0); 39 | }else{ 40 | return null; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/util/VideoLRUCacheUtil.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.util; 2 | 3 | import android.os.Environment; 4 | 5 | import com.zhaoss.videoplayerdemo.bean.VideoCacheBean; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.util.ArrayList; 10 | 11 | /** 12 | * Created by zhaoshuang on 2018/10/25. 13 | * 视频缓存的LRUCache算法 14 | */ 15 | 16 | public class VideoLRUCacheUtil { 17 | 18 | public static final String CACHE_DIR_PATH = Environment.getExternalStorageDirectory() + "/VideoPlayerDemo/"; 19 | //缓存最大空间 200M 单位是字节 20 | public static final long maxDirSize = 1024*1024*200; 21 | //缓存最大时间 7天 22 | public static final long maxCacheTime = 1000*60*60*24*7; 23 | 24 | public static void updateVideoCacheBean(String md5, String videoPath, String indexPath){ 25 | 26 | VideoCacheBean videoCacheBean = new VideoCacheBean(); 27 | videoCacheBean.setKey(md5); 28 | videoCacheBean.setPlayTime(System.currentTimeMillis()); 29 | videoCacheBean.setVideoPath(videoPath); 30 | videoCacheBean.setIndexPath(indexPath); 31 | videoCacheBean.setPlayCount(videoCacheBean.getPlayCount()+1); 32 | VideoCacheDBUtil.save(videoCacheBean); 33 | } 34 | 35 | public static File createTempFile() throws IOException { 36 | File tempFile = new File(CACHE_DIR_PATH, System.currentTimeMillis()+""); 37 | if(!tempFile.exists()){ 38 | tempFile.createNewFile(); 39 | } 40 | return tempFile; 41 | } 42 | 43 | public static File createCacheFile(String md5) throws IOException{ 44 | 45 | //创建一个视频缓存文件, 在data/data目录下 46 | File filesDir = new File(CACHE_DIR_PATH); 47 | 48 | File cacheFile = new File(filesDir, md5); 49 | if(!cacheFile.exists()) { 50 | cacheFile.createNewFile(); 51 | } 52 | //将缓存信息存到数据库 53 | VideoLRUCacheUtil.updateVideoCacheBean(md5, cacheFile.getAbsolutePath(), ""); 54 | return cacheFile; 55 | } 56 | 57 | public static void checkCacheSize(){ 58 | 59 | ArrayList videoCacheList = VideoCacheDBUtil.query(); 60 | 61 | for (VideoCacheBean bean : videoCacheList){ 62 | if(bean.getFileSize() == 0){ 63 | File videoFile = new File(bean.getVideoPath()); 64 | if(videoFile.exists()){ 65 | bean.setFileSize(videoFile.length()); 66 | VideoCacheDBUtil.save(bean); 67 | }else{ 68 | VideoCacheDBUtil.delete(bean); 69 | } 70 | } 71 | } 72 | 73 | long currentSize = 0; 74 | long currentTime = System.currentTimeMillis(); 75 | for (VideoCacheBean bean : videoCacheList){ 76 | //太久远的文件删除 77 | if(currentTime-bean.getPlayTime() > maxCacheTime){ 78 | VideoCacheDBUtil.delete(bean); 79 | }else { 80 | //大于存储空间的删除 81 | if (currentSize + bean.getFileSize() > maxDirSize) { 82 | VideoCacheDBUtil.delete(bean); 83 | } else { 84 | currentSize += bean.getFileSize(); 85 | } 86 | } 87 | } 88 | 89 | deleteDirRoom(new File(CACHE_DIR_PATH), VideoCacheDBUtil.query()); 90 | } 91 | 92 | public static void deleteVideoBean(String url){ 93 | VideoCacheBean bean = VideoCacheDBUtil.query(Util.MD5(url)); 94 | if(bean != null){ 95 | VideoCacheDBUtil.delete(bean); 96 | new File(bean.getVideoPath()).delete(); 97 | } 98 | } 99 | 100 | private static void deleteDirRoom(File dir, ArrayList videoCacheList){ 101 | if(dir.exists()) { 102 | if(dir.isDirectory()) { 103 | File[] files = dir.listFiles(); 104 | if(files != null) { 105 | for (File f : files) { 106 | deleteDirRoom(f, videoCacheList); 107 | } 108 | } 109 | }else{ 110 | if(!isVideoExists(dir, videoCacheList)) { 111 | dir.delete(); 112 | } 113 | } 114 | } 115 | } 116 | 117 | private static boolean isVideoExists(File file, ArrayList videoCacheList){ 118 | for (VideoCacheBean bean : videoCacheList) { 119 | if(file.getAbsolutePath().equals(bean.getVideoPath()) || file.getAbsolutePath().equals(bean.getIndexPath())){ 120 | return true; 121 | } 122 | } 123 | return false; 124 | } 125 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/view/PlayTextureView.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Matrix; 5 | import android.graphics.SurfaceTexture; 6 | import android.opengl.GLES20; 7 | import android.support.annotation.NonNull; 8 | import android.support.annotation.Nullable; 9 | import android.util.AttributeSet; 10 | import android.view.TextureView; 11 | import android.view.ViewGroup; 12 | import android.widget.FrameLayout; 13 | 14 | /** 15 | * Created by zhaoshuang on 2018/9/25. 16 | */ 17 | 18 | public class PlayTextureView extends FrameLayout { 19 | 20 | private TextureView mTextureView; 21 | private SimpleSurfaceTextureListener mSurfaceTextureListener; 22 | private int mVideoWidth; 23 | private int mVideoHeight; 24 | private SurfaceTexture mSurfaceTexture; 25 | 26 | public PlayTextureView(@NonNull Context context) { 27 | super(context); 28 | init(); 29 | } 30 | 31 | public PlayTextureView(@NonNull Context context, @Nullable AttributeSet attrs) { 32 | super(context, attrs); 33 | init(); 34 | } 35 | 36 | public PlayTextureView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 37 | super(context, attrs, defStyleAttr); 38 | init(); 39 | } 40 | 41 | private void init(){ 42 | 43 | initTextureView(null); 44 | } 45 | 46 | private void initTextureView(SurfaceTexture surfaceTexture) { 47 | 48 | mTextureView = new TextureView(getContext()); 49 | mTextureView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 50 | 51 | if(surfaceTexture == null) { 52 | mSurfaceTexture = newSurfaceTexture(); 53 | }else{ 54 | mSurfaceTexture = surfaceTexture; 55 | } 56 | mTextureView.setSurfaceTexture(mSurfaceTexture); 57 | 58 | mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { 59 | @Override 60 | public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { 61 | if(mSurfaceTextureListener != null){ 62 | mSurfaceTextureListener.onSurfaceTextureAvailable(surface, width, height); 63 | } 64 | } 65 | @Override 66 | public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { 67 | if(mSurfaceTextureListener != null){ 68 | mSurfaceTextureListener.onSurfaceTextureSizeChanged(surface, width, height); 69 | } 70 | } 71 | @Override 72 | public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { 73 | if(mSurfaceTextureListener != null){ 74 | return mSurfaceTextureListener.onSurfaceTextureDestroyed(surface); 75 | } 76 | //当view被销毁时 不销毁SurfaceTexture 77 | return false; 78 | } 79 | @Override 80 | public void onSurfaceTextureUpdated(SurfaceTexture surface) { 81 | if(mSurfaceTextureListener != null){ 82 | mSurfaceTextureListener.onSurfaceTextureUpdated(surface); 83 | } 84 | } 85 | }); 86 | 87 | addView(mTextureView, 0); 88 | } 89 | 90 | //视频居中播放 91 | private void setVideoCenter(float viewWidth, float viewHeight, float videoWidth, float videoHeight){ 92 | 93 | Matrix matrix = new Matrix(); 94 | float sx = viewWidth/videoWidth; 95 | float sy = viewHeight/videoHeight; 96 | float maxScale = Math.max(sx, sy); 97 | 98 | matrix.preTranslate((viewWidth - videoWidth) / 2, (viewHeight - videoHeight) / 2); 99 | matrix.preScale(videoWidth/viewWidth, videoHeight/viewHeight); 100 | matrix.postScale(maxScale, maxScale, viewWidth/2, viewHeight/2); 101 | 102 | mTextureView.setTransform(matrix); 103 | mTextureView.postInvalidate(); 104 | } 105 | 106 | public void resetTextureView(){ 107 | resetTextureView(null); 108 | } 109 | 110 | public void resetTextureView(SurfaceTexture surfaceTexture){ 111 | 112 | removeView(mTextureView); 113 | initTextureView(surfaceTexture); 114 | } 115 | 116 | public void setVideoSize(int videoWidth, int videoHeight){ 117 | this.mVideoWidth = videoWidth; 118 | this.mVideoHeight = videoHeight; 119 | mTextureView.post(new Runnable() { 120 | @Override 121 | public void run() { 122 | if(mVideoWidth!=0 && mVideoHeight!=0){ 123 | setVideoCenter(mTextureView.getWidth(), mTextureView.getHeight(), mVideoWidth, mVideoHeight); 124 | } 125 | } 126 | }); 127 | } 128 | 129 | public void setSurfaceTextureListener(SimpleSurfaceTextureListener surfaceTextureListener){ 130 | this.mSurfaceTextureListener = surfaceTextureListener; 131 | } 132 | 133 | //初始化SurfaceTexture 134 | public SurfaceTexture newSurfaceTexture(){ 135 | 136 | int[] textures = new int[1]; 137 | GLES20.glGenTextures(1, textures, 0); 138 | int texName = textures[0]; 139 | SurfaceTexture surfaceTexture = new SurfaceTexture(texName); 140 | surfaceTexture.detachFromGLContext(); 141 | return surfaceTexture; 142 | } 143 | 144 | public SurfaceTexture getSurfaceTexture() { 145 | //mSurfaceTexture.isReleased() NoSuchMethodError No virtual method isReleased()Z in class Landroid/graphics/SurfaceTexture 146 | if(mSurfaceTexture != null){ 147 | return mSurfaceTexture; 148 | } 149 | return null; 150 | } 151 | 152 | public abstract static class SimpleSurfaceTextureListener implements TextureView.SurfaceTextureListener { 153 | @Override 154 | public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { 155 | 156 | } 157 | @Override 158 | public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { 159 | 160 | } 161 | @Override 162 | public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { 163 | return false; 164 | } 165 | @Override 166 | public void onSurfaceTextureUpdated(SurfaceTexture surface) { 167 | 168 | } 169 | } 170 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zhaoss/videoplayerdemo/view/VideoTouchView.java: -------------------------------------------------------------------------------- 1 | package com.zhaoss.videoplayerdemo.view; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | import android.util.AttributeSet; 7 | import android.view.MotionEvent; 8 | import android.view.ViewGroup; 9 | import android.view.ViewParent; 10 | import android.widget.FrameLayout; 11 | 12 | import com.zhaoss.videoplayerdemo.R; 13 | 14 | /** 15 | * Created by zhaoshuang on 2018/10/15. 16 | */ 17 | 18 | public class VideoTouchView extends FrameLayout { 19 | 20 | private OnTouchSlideListener onTouchSlideListener; 21 | private float slideMove; 22 | private float slideClick; 23 | 24 | public VideoTouchView(@NonNull Context context) { 25 | super(context); 26 | init(); 27 | } 28 | 29 | public VideoTouchView(@NonNull Context context, @Nullable AttributeSet attrs) { 30 | super(context, attrs); 31 | init(); 32 | } 33 | 34 | public VideoTouchView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 35 | super(context, attrs, defStyleAttr); 36 | init(); 37 | } 38 | 39 | private void init() { 40 | 41 | slideMove = getResources().getDimension(R.dimen.dp10); 42 | slideClick = getResources().getDimension(R.dimen.dp5); 43 | } 44 | 45 | float downX; 46 | float downY; 47 | boolean isSlideing; 48 | boolean dontSlide; 49 | @Override 50 | public boolean onTouchEvent(MotionEvent event) { 51 | switch (event.getAction()) { 52 | case MotionEvent.ACTION_DOWN: 53 | downX = event.getRawX(); 54 | downY = event.getRawY(); 55 | case MotionEvent.ACTION_MOVE: 56 | if(onTouchSlideListener!=null){ 57 | float moveX = event.getRawX(); 58 | float moveY = event.getRawY(); 59 | float slideX = moveX-downX; 60 | float slideY = moveY-downY; 61 | if(isSlideing){ 62 | onTouchSlideListener.onSlide(slideX); 63 | downX = moveX; 64 | }else{ 65 | if(Math.abs(slideX) > slideMove && !dontSlide){ 66 | requestDisallowInterceptTouchEvents(this, true); 67 | isSlideing = true; 68 | downX = moveX; 69 | }else if(Math.abs(slideY) > slideMove){ 70 | dontSlide = true; 71 | } 72 | } 73 | } 74 | break; 75 | case MotionEvent.ACTION_CANCEL: 76 | case MotionEvent.ACTION_UP: 77 | requestDisallowInterceptTouchEvents(this, false); 78 | if(isSlideing){ 79 | if(onTouchSlideListener != null){ 80 | onTouchSlideListener.onUp(); 81 | } 82 | }else{ 83 | float upX = event.getRawX(); 84 | float upY = event.getRawY(); 85 | 86 | if (Math.abs(upX - downX) < slideClick && Math.abs(upY - downY) < slideClick) { 87 | //单击事件 88 | if(onTouchSlideListener != null){ 89 | onTouchSlideListener.onClick(); 90 | } 91 | } 92 | } 93 | isSlideing = false; 94 | dontSlide = false; 95 | break; 96 | } 97 | return true; 98 | } 99 | 100 | //递归拦截所有父view的事件 101 | private void requestDisallowInterceptTouchEvents(ViewGroup viewGroup, boolean isIntercept){ 102 | 103 | ViewParent parent = viewGroup.getParent(); 104 | if(parent instanceof ViewGroup){ 105 | ViewGroup parenViewGroup = (ViewGroup) parent; 106 | requestDisallowInterceptTouchEvents(parenViewGroup, isIntercept); 107 | parenViewGroup.requestDisallowInterceptTouchEvent(isIntercept); 108 | } 109 | } 110 | 111 | public interface OnTouchSlideListener{ 112 | void onSlide(float distant); 113 | void onUp(); 114 | void onClick(); 115 | } 116 | 117 | public void setOnTouchSlideListener(OnTouchSlideListener onTouchSlideListener){ 118 | this.onTouchSlideListener = onTouchSlideListener; 119 | } 120 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/progressbar_with_buffer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_video_details.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 18 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_video.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 19 | 24 | 31 | 37 | 46 | 47 | 48 | 57 | 58 | 63 | 68 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_video_details.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 18 | 22 | 23 | 24 | 30 | 31 | 38 | 39 | 48 | 49 | 54 | 55 | 61 | 67 | 76 | 77 | 78 | 84 | 89 | 93 | 100 | 101 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/video_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/src/main/res/mipmap-mdpi/video_pause.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/avatar1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/src/main/res/mipmap-xhdpi/avatar1.jpeg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/video_fast_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/src/main/res/mipmap-xxxhdpi/video_fast_back.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/video_fast_forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/src/main/res/mipmap-xxxhdpi/video_fast_forward.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/video_landscape_cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/src/main/res/mipmap-xxxhdpi/video_landscape_cancel.png -------------------------------------------------------------------------------- /app/src/main/res/raw/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/app/src/main/res/raw/demo.mp4 -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | #313133 8 | #E7E7E7 9 | #ffffff 10 | #33ffffff 11 | #CCFFFFFF 12 | #66313133 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20dp 4 | 10dp 5 | 6 | 16sp 7 | 14sp 8 | 12sp 9 | 10dp 10 | 5dp 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | VideoPlayerDemo 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.4.1' 11 | 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zhaoss/VideoPlayerDemo/4d60fadd5b35b31cadbf97b4521bbf6fee9bb7fb/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 21 18:27:35 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------