ソースを参照

项目代码重构

杨充 4 年 前
コミット
96731fcde3
100 ファイル変更7945 行追加75 行削除
  1. 5 2
      .idea/modules.xml
  2. 1 0
      VideoBarrage/.gitignore
  3. 32 0
      VideoBarrage/build.gradle
  4. 0 0
      VideoBarrage/consumer-rules.pro
  5. 21 0
      VideoBarrage/proguard-rules.pro
  6. 5 0
      VideoBarrage/src/main/AndroidManifest.xml
  7. 1 0
      VideoCache/.gitignore
  8. 144 0
      VideoCache/build.gradle
  9. 0 0
      VideoCache/consumer-rules.pro
  10. 21 0
      VideoCache/proguard-rules.pro
  11. 5 0
      VideoCache/src/main/AndroidManifest.xml
  12. 63 0
      VideoCache/src/main/java/com/yc/videocache/ByteArrayCache.java
  13. 39 0
      VideoCache/src/main/java/com/yc/videocache/ByteArraySource.java
  14. 21 0
      VideoCache/src/main/java/com/yc/videocache/Cache.java
  15. 14 0
      VideoCache/src/main/java/com/yc/videocache/CacheListener.java
  16. 34 0
      VideoCache/src/main/java/com/yc/videocache/Config.java
  17. 71 0
      VideoCache/src/main/java/com/yc/videocache/GetRequest.java
  18. 112 0
      VideoCache/src/main/java/com/yc/videocache/HttpProxyCache.java
  19. 454 0
      VideoCache/src/main/java/com/yc/videocache/HttpProxyCacheServer.java
  20. 115 0
      VideoCache/src/main/java/com/yc/videocache/HttpProxyCacheServerClients.java
  21. 202 0
      VideoCache/src/main/java/com/yc/videocache/HttpUrlSource.java
  22. 50 0
      VideoCache/src/main/java/com/yc/videocache/IgnoreHostProxySelector.java
  23. 21 0
      VideoCache/src/main/java/com/yc/videocache/InterruptedProxyCacheException.java
  24. 34 0
      VideoCache/src/main/java/com/yc/videocache/Logger.java
  25. 123 0
      VideoCache/src/main/java/com/yc/videocache/Pinger.java
  26. 38 0
      VideoCache/src/main/java/com/yc/videocache/Preconditions.java
  27. 186 0
      VideoCache/src/main/java/com/yc/videocache/ProxyCache.java
  28. 23 0
      VideoCache/src/main/java/com/yc/videocache/ProxyCacheException.java
  29. 93 0
      VideoCache/src/main/java/com/yc/videocache/ProxyCacheUtils.java
  30. 41 0
      VideoCache/src/main/java/com/yc/videocache/Source.java
  31. 28 0
      VideoCache/src/main/java/com/yc/videocache/SourceInfo.java
  32. 100 0
      VideoCache/src/main/java/com/yc/videocache/StorageUtils.java
  33. 13 0
      VideoCache/src/main/java/com/yc/videocache/file/DiskUsage.java
  34. 125 0
      VideoCache/src/main/java/com/yc/videocache/file/FileCache.java
  35. 10 0
      VideoCache/src/main/java/com/yc/videocache/file/FileNameGenerator.java
  36. 92 0
      VideoCache/src/main/java/com/yc/videocache/file/Files.java
  37. 75 0
      VideoCache/src/main/java/com/yc/videocache/file/LruDiskUsage.java
  38. 29 0
      VideoCache/src/main/java/com/yc/videocache/file/Md5FileNameGenerator.java
  39. 25 0
      VideoCache/src/main/java/com/yc/videocache/file/TotalCountLruDiskUsage.java
  40. 25 0
      VideoCache/src/main/java/com/yc/videocache/file/TotalSizeLruDiskUsage.java
  41. 17 0
      VideoCache/src/main/java/com/yc/videocache/file/UnlimitedDiskUsage.java
  42. 18 0
      VideoCache/src/main/java/com/yc/videocache/headers/EmptyHeadersInjector.java
  43. 20 0
      VideoCache/src/main/java/com/yc/videocache/headers/HeaderInjector.java
  44. 98 0
      VideoCache/src/main/java/com/yc/videocache/sourcestorage/DatabaseSourceInfoStorage.java
  45. 24 0
      VideoCache/src/main/java/com/yc/videocache/sourcestorage/NoSourceInfoStorage.java
  46. 17 0
      VideoCache/src/main/java/com/yc/videocache/sourcestorage/SourceInfoStorage.java
  47. 19 0
      VideoCache/src/main/java/com/yc/videocache/sourcestorage/SourceInfoStorageFactory.java
  48. 0 0
      VideoPlayer/.gitignore
  49. 16 9
      VideoPlayer/build.gradle
  50. 0 0
      VideoPlayer/proguard-rules.pro
  51. 0 0
      VideoPlayer/src/main/AndroidManifest.xml
  52. 39 6
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/config/ConstantKeys.java
  53. 27 14
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/config/VideoInfoBean.java
  54. 26 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/dev/OnPlayerStatesListener.java
  55. 9 13
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/dev/OnPlayerTypeListener.java
  56. 2 2
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/dev/OnVideoControlListener.java
  57. 0 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/listener/OnClarityChangedListener.java
  58. 0 1
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/listener/OnSurfaceListener.java
  59. 0 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/listener/OnTextureListener.java
  60. 0 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/player/InterPropertyVideoPlayer.java
  61. 0 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/player/InterScreenVideoPlayer.java
  62. 0 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/player/InterStateVideoPlayer.java
  63. 3 2
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/player/InterVideoController.java
  64. 2 1
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/player/VideoControllerView.java
  65. 650 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/controller/BaseVideoController.java
  66. 275 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/controller/ControlWrapper.java
  67. 325 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/controller/GestureVideoController.java
  68. 24 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/controller/IControlComponent.java
  69. 33 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/controller/IGestureComponent.java
  70. 60 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/controller/IVideoController.java
  71. 54 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/controller/MediaPlayerControl.java
  72. 37 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/controller/OrientationHelper.java
  73. 116 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/helper/AudioFocusHelper.java
  74. 328 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/impl/exo/ExoMediaPlayer.java
  75. 18 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/impl/exo/ExoMediaPlayerFactory.java
  76. 180 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/impl/exo/ExoMediaSourceHelper.java
  77. 264 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/impl/ijk/IjkPlayer.java
  78. 18 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/impl/ijk/IjkPlayerFactory.java
  79. 90 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/impl/ijk/RawDataSourceProvider.java
  80. 273 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/impl/media/AndroidMediaPlayer.java
  81. 20 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/impl/media/AndroidMediaPlayerFactory.java
  82. 174 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/inter/AbstractPlayer.java
  83. 15 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/player/PlayerFactory.java
  84. 22 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/player/ProgressManager.java
  85. 149 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/player/VideoViewConfig.java
  86. 139 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/player/VideoViewManager.java
  87. 52 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/render/IRenderView.java
  88. 90 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/render/MeasureHelper.java
  89. 15 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/render/RenderViewFactory.java
  90. 113 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/render/TextureRenderView.java
  91. 15 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/render/TextureRenderViewFactory.java
  92. 1068 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/view/VideoView.java
  93. 1 1
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/manager/VideoPlayerManager.java
  94. 8 8
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/receiver/BatterReceiver.java
  95. 11 11
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/receiver/NetChangedReceiver.java
  96. 196 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/tool/timer/CountDownTimer.java
  97. 101 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/tool/timer/CountTimeTools.java
  98. 29 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/tool/timer/TimerListener.java
  99. 8 5
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/tool/toast/BaseToast.java
  100. 146 0
      VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/tool/utils/CutoutUtil.java

+ 5 - 2
.idea/modules.xml

@@ -3,10 +3,13 @@
   <component name="ProjectModuleManager">
   <component name="ProjectModuleManager">
     <modules>
     <modules>
       <module fileurl="file://$PROJECT_DIR$/GitHub-YCVideoPlayer.iml" filepath="$PROJECT_DIR$/GitHub-YCVideoPlayer.iml" group="YCVideoPlayer" />
       <module fileurl="file://$PROJECT_DIR$/GitHub-YCVideoPlayer.iml" filepath="$PROJECT_DIR$/GitHub-YCVideoPlayer.iml" group="YCVideoPlayer" />
+      <module fileurl="file://$PROJECT_DIR$/.idea/modules/VideoBarrage/VideoBarrage.iml" filepath="$PROJECT_DIR$/.idea/modules/VideoBarrage/VideoBarrage.iml" group="YCVideoPlayer/VideoBarrage" />
+      <module fileurl="file://$PROJECT_DIR$/.idea/modules/VideoCache/VideoCache.iml" filepath="$PROJECT_DIR$/.idea/modules/VideoCache/VideoCache.iml" group="YCVideoPlayer/VideoCache" />
+      <module fileurl="file://$PROJECT_DIR$/VideoPlayer/VideoPlayer.iml" filepath="$PROJECT_DIR$/VideoPlayer/VideoPlayer.iml" group="YCVideoPlayer/VideoPlayer" />
+      <module fileurl="file://$PROJECT_DIR$/.idea/modules/VideoUi/VideoUi.iml" filepath="$PROJECT_DIR$/.idea/modules/VideoUi/VideoUi.iml" group="YCVideoPlayer/VideoUi" />
       <module fileurl="file://$PROJECT_DIR$/.idea/YCVideoPlayer.iml" filepath="$PROJECT_DIR$/.idea/YCVideoPlayer.iml" />
       <module fileurl="file://$PROJECT_DIR$/.idea/YCVideoPlayer.iml" filepath="$PROJECT_DIR$/.idea/YCVideoPlayer.iml" />
-      <module fileurl="file://$PROJECT_DIR$/YCVideoPlayerLib/YCVideoPlayer-YCVideoPlayerLib.iml" filepath="$PROJECT_DIR$/YCVideoPlayerLib/YCVideoPlayer-YCVideoPlayerLib.iml" group="YCVideoPlayer/YCVideoPlayerLib" />
       <module fileurl="file://$PROJECT_DIR$/app/YCVideoPlayer-app.iml" filepath="$PROJECT_DIR$/app/YCVideoPlayer-app.iml" group="YCVideoPlayer/app" />
       <module fileurl="file://$PROJECT_DIR$/app/YCVideoPlayer-app.iml" filepath="$PROJECT_DIR$/app/YCVideoPlayer-app.iml" group="YCVideoPlayer/app" />
-      <module fileurl="file://$PROJECT_DIR$/YCVideoPlayerLib/YCVideoPlayerLib.iml" filepath="$PROJECT_DIR$/YCVideoPlayerLib/YCVideoPlayerLib.iml" />
+      <module fileurl="file://$PROJECT_DIR$/VideoPlayer/YCVideoPlayerLib.iml" filepath="$PROJECT_DIR$/VideoPlayer/YCVideoPlayerLib.iml" />
       <module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
       <module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
     </modules>
     </modules>
   </component>
   </component>

+ 1 - 0
VideoBarrage/.gitignore

@@ -0,0 +1 @@
+/build

+ 32 - 0
VideoBarrage/build.gradle

@@ -0,0 +1,32 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 29
+    buildToolsVersion "29.0.3"
+
+    defaultConfig {
+        minSdkVersion 17
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: "libs", include: ["*.jar"])
+    implementation 'androidx.appcompat:appcompat:1.2.0'
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+
+}

+ 0 - 0
VideoBarrage/consumer-rules.pro


+ 21 - 0
VideoBarrage/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 5 - 0
VideoBarrage/src/main/AndroidManifest.xml

@@ -0,0 +1,5 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.zwwl.videobarrage">
+
+    /
+</manifest>

+ 1 - 0
VideoCache/.gitignore

@@ -0,0 +1 @@
+/build

+ 144 - 0
VideoCache/build.gradle

@@ -0,0 +1,144 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 29
+    buildToolsVersion "29.0.3"
+
+    defaultConfig {
+        minSdkVersion 17
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: "libs", include: ["*.jar"])
+    implementation 'androidx.appcompat:appcompat:1.2.0'
+}
+
+
+/** 以下开始是将Android Library上传到jcenter的相关配置**/
+apply plugin: 'com.github.dcendents.android-maven'
+apply plugin: 'com.jfrog.bintray'
+
+//项目主页
+def siteUrl = 'https://github.com/yangchong211/YCVideoPlayer'    // project homepage
+//项目的版本控制地址
+def gitUrl = 'https://github.com/yangchong211/YCVideoPlayer.git' // project git
+
+//发布到组织名称名字,必须填写
+group = "cn.yc"
+//发布到JCenter上的项目名字,必须填写
+def libName = "YCVideoCacheLib"
+// 版本号,下次更新是只需要更改版本号即可
+version = "1.0.0"
+/**  上面配置后上传至jcenter后的编译路径是这样的: compile 'cn.yc:YCVideoCacheLib:1.0.0'  **/
+
+//生成源文件
+task sourcesJar(type: Jar) {
+    from android.sourceSets.main.java.srcDirs
+    classifier = 'sources'
+}
+//生成文档
+task javadoc(type: Javadoc) {
+    source = android.sourceSets.main.java.srcDirs
+    classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
+    options.encoding "UTF-8"
+    options.charSet 'UTF-8'
+    options.author true
+    options.version true
+    options.links "https://github.com/linglongxin24/FastDev/tree/master/mylibrary/docs/javadoc"
+    failOnError false
+}
+
+//文档打包成jar
+task javadocJar(type: Jar, dependsOn: javadoc) {
+    classifier = 'javadoc'
+    from javadoc.destinationDir
+}
+//拷贝javadoc文件
+task copyDoc(type: Copy) {
+    from "${buildDir}/docs/"
+    into "docs"
+}
+
+//上传到jcenter所需要的源码文件
+artifacts {
+    archives javadocJar
+    archives sourcesJar
+}
+
+// 配置maven库,生成POM.xml文件
+install {
+    repositories.mavenInstaller {
+        // This generates POM.xml with proper parameters
+        pom {
+            project {
+                packaging 'aar'
+                //项目描述,自由填写
+                name 'This is video cache lib'
+                url siteUrl
+                licenses {
+                    license {
+                        //开源协议
+                        name 'The Apache Software License, Version 2.0'
+                        url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+                    }
+                }
+                developers {
+                    developer {
+                        //开发者的个人信息,根据个人信息填写
+                        id 'yangchong'
+                        name 'yc'
+                        email 'yangchong211@163.com'
+                    }
+                }
+                scm {
+                    connection gitUrl
+                    developerConnection gitUrl
+                    url siteUrl
+                }
+            }
+        }
+    }
+}
+
+//上传到jcenter
+Properties properties = new Properties()
+properties.load(project.rootProject.file('local.properties').newDataInputStream())
+bintray {
+    user = properties.getProperty("bintray.user")    //读取 local.properties 文件里面的 bintray.user
+    key = properties.getProperty("bintray.apikey")  //读取 local.properties 文件里面的 bintray.apikey
+    configurations = ['archives']
+    pkg {
+        repo = "maven"
+        name = libName    //发布到JCenter上的项目名字,必须填写
+        desc = 'android video cache'    //项目描述
+        websiteUrl = siteUrl
+        vcsUrl = gitUrl
+        licenses = ["Apache-2.0"]
+        publish = true
+    }
+}
+
+javadoc {
+    options {
+        //如果你的项目里面有中文注释的话,必须将格式设置为UTF-8,不然会出现乱码
+        encoding "UTF-8"
+        charSet 'UTF-8'
+        author true
+        version true
+        links "http://docs.oracle.com/javase/7/docs/api"
+    }
+}

+ 0 - 0
VideoCache/consumer-rules.pro


+ 21 - 0
VideoCache/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 5 - 0
VideoCache/src/main/AndroidManifest.xml

@@ -0,0 +1,5 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.yc.videocache">
+
+    /
+</manifest>

+ 63 - 0
VideoCache/src/main/java/com/yc/videocache/ByteArrayCache.java

@@ -0,0 +1,63 @@
+package com.yc.videocache;
+
+import java.io.ByteArrayInputStream;
+import java.util.Arrays;
+
+/**
+ * Simple memory based {@link Cache} implementation.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public class ByteArrayCache implements Cache {
+
+    private volatile byte[] data;
+    private volatile boolean completed;
+
+    public ByteArrayCache() {
+        this(new byte[0]);
+    }
+
+    public ByteArrayCache(byte[] data) {
+        this.data = Preconditions.checkNotNull(data);
+    }
+
+    @Override
+    public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
+        if (offset >= data.length) {
+            return -1;
+        }
+        if (offset > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Too long offset for memory cache " + offset);
+        }
+        return new ByteArrayInputStream(data).read(buffer, (int) offset, length);
+    }
+
+    @Override
+    public long available() throws ProxyCacheException {
+        return data.length;
+    }
+
+    @Override
+    public void append(byte[] newData, int length) throws ProxyCacheException {
+        Preconditions.checkNotNull(data);
+        Preconditions.checkArgument(length >= 0 && length <= newData.length);
+
+        byte[] appendedData = Arrays.copyOf(data, data.length + length);
+        System.arraycopy(newData, 0, appendedData, data.length, length);
+        data = appendedData;
+    }
+
+    @Override
+    public void close() throws ProxyCacheException {
+    }
+
+    @Override
+    public void complete() {
+        completed = true;
+    }
+
+    @Override
+    public boolean isCompleted() {
+        return completed;
+    }
+}

+ 39 - 0
VideoCache/src/main/java/com/yc/videocache/ByteArraySource.java

@@ -0,0 +1,39 @@
+package com.yc.videocache;
+
+import java.io.ByteArrayInputStream;
+
+/**
+ * Simple memory based {@link Source} implementation.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public class ByteArraySource implements Source {
+
+    private final byte[] data;
+    private ByteArrayInputStream arrayInputStream;
+
+    public ByteArraySource(byte[] data) {
+        this.data = data;
+    }
+
+    @Override
+    public int read(byte[] buffer) throws ProxyCacheException {
+        return arrayInputStream.read(buffer, 0, buffer.length);
+    }
+
+    @Override
+    public long length() throws ProxyCacheException {
+        return data.length;
+    }
+
+    @Override
+    public void open(long offset) throws ProxyCacheException {
+        arrayInputStream = new ByteArrayInputStream(data);
+        arrayInputStream.skip(offset);
+    }
+
+    @Override
+    public void close() throws ProxyCacheException {
+    }
+}
+

+ 21 - 0
VideoCache/src/main/java/com/yc/videocache/Cache.java

@@ -0,0 +1,21 @@
+package com.yc.videocache;
+
+/**
+ * Cache for proxy.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public interface Cache {
+
+    long available() throws ProxyCacheException;
+
+    int read(byte[] buffer, long offset, int length) throws ProxyCacheException;
+
+    void append(byte[] data, int length) throws ProxyCacheException;
+
+    void close() throws ProxyCacheException;
+
+    void complete() throws ProxyCacheException;
+
+    boolean isCompleted();
+}

+ 14 - 0
VideoCache/src/main/java/com/yc/videocache/CacheListener.java

@@ -0,0 +1,14 @@
+package com.yc.videocache;
+
+import java.io.File;
+
+/**
+ * Listener for cache availability.
+ *
+ * @author Egor Makovsky (yahor.makouski@gmail.com)
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public interface CacheListener {
+
+    void onCacheAvailable(File cacheFile, String url, int percentsAvailable);
+}

+ 34 - 0
VideoCache/src/main/java/com/yc/videocache/Config.java

@@ -0,0 +1,34 @@
+package com.yc.videocache;
+
+import com.yc.videocache.file.DiskUsage;
+import com.yc.videocache.file.FileNameGenerator;
+import com.yc.videocache.headers.HeaderInjector;
+import com.yc.videocache.sourcestorage.SourceInfoStorage;
+
+import java.io.File;
+
+/**
+ * Configuration for proxy cache.
+ */
+class Config {
+
+    public final File cacheRoot;
+    public final FileNameGenerator fileNameGenerator;
+    public final DiskUsage diskUsage;
+    public final SourceInfoStorage sourceInfoStorage;
+    public final HeaderInjector headerInjector;
+
+    Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage, SourceInfoStorage sourceInfoStorage, HeaderInjector headerInjector) {
+        this.cacheRoot = cacheRoot;
+        this.fileNameGenerator = fileNameGenerator;
+        this.diskUsage = diskUsage;
+        this.sourceInfoStorage = sourceInfoStorage;
+        this.headerInjector = headerInjector;
+    }
+
+    File generateCacheFile(String url) {
+        String name = fileNameGenerator.generate(url);
+        return new File(cacheRoot, name);
+    }
+
+}

+ 71 - 0
VideoCache/src/main/java/com/yc/videocache/GetRequest.java

@@ -0,0 +1,71 @@
+package com.yc.videocache;
+
+import android.text.TextUtils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static com.yc.videocache.Preconditions.checkNotNull;
+
+/**
+ * Model for Http GET request.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+class GetRequest {
+
+    private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("[R,r]ange:[ ]?bytes=(\\d*)-");
+    private static final Pattern URL_PATTERN = Pattern.compile("GET /(.*) HTTP");
+
+    public final String uri;
+    public final long rangeOffset;
+    public final boolean partial;
+
+    public GetRequest(String request) {
+        checkNotNull(request);
+        long offset = findRangeOffset(request);
+        this.rangeOffset = Math.max(0, offset);
+        this.partial = offset >= 0;
+        this.uri = findUri(request);
+    }
+
+    public static GetRequest read(InputStream inputStream) throws IOException {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
+        StringBuilder stringRequest = new StringBuilder();
+        String line;
+        while (!TextUtils.isEmpty(line = reader.readLine())) { // until new line (headers ending)
+            stringRequest.append(line).append('\n');
+        }
+        return new GetRequest(stringRequest.toString());
+    }
+
+    private long findRangeOffset(String request) {
+        Matcher matcher = RANGE_HEADER_PATTERN.matcher(request);
+        if (matcher.find()) {
+            String rangeValue = matcher.group(1);
+            return Long.parseLong(rangeValue);
+        }
+        return -1;
+    }
+
+    private String findUri(String request) {
+        Matcher matcher = URL_PATTERN.matcher(request);
+        if (matcher.find()) {
+            return matcher.group(1);
+        }
+        throw new IllegalArgumentException("Invalid request `" + request + "`: url not found!");
+    }
+
+    @Override
+    public String toString() {
+        return "GetRequest{" +
+                "rangeOffset=" + rangeOffset +
+                ", partial=" + partial +
+                ", uri='" + uri + '\'' +
+                '}';
+    }
+}

+ 112 - 0
VideoCache/src/main/java/com/yc/videocache/HttpProxyCache.java

@@ -0,0 +1,112 @@
+package com.yc.videocache;
+
+import android.text.TextUtils;
+
+import com.yc.videocache.file.FileCache;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.util.Locale;
+
+import static com.yc.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
+
+/**
+ * {@link ProxyCache} that read http url and writes data to {@link Socket}
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+class HttpProxyCache extends ProxyCache {
+
+    private static final float NO_CACHE_BARRIER = .2f;
+
+    private final HttpUrlSource source;
+    private final FileCache cache;
+    private CacheListener listener;
+
+    public HttpProxyCache(HttpUrlSource source, FileCache cache) {
+        super(source, cache);
+        this.cache = cache;
+        this.source = source;
+    }
+
+    public void registerCacheListener(CacheListener cacheListener) {
+        this.listener = cacheListener;
+    }
+
+    public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException {
+        OutputStream out = new BufferedOutputStream(socket.getOutputStream());
+        String responseHeaders = newResponseHeaders(request);
+        out.write(responseHeaders.getBytes("UTF-8"));
+
+        long offset = request.rangeOffset;
+        if (isUseCache(request)) {
+            responseWithCache(out, offset);
+        } else {
+            responseWithoutCache(out, offset);
+        }
+    }
+
+    private boolean isUseCache(GetRequest request) throws ProxyCacheException {
+        long sourceLength = source.length();
+        boolean sourceLengthKnown = sourceLength > 0;
+        long cacheAvailable = cache.available();
+        // do not use cache for partial requests which too far from available cache. It seems user seek video.
+        return !sourceLengthKnown || !request.partial || request.rangeOffset <= cacheAvailable + sourceLength * NO_CACHE_BARRIER;
+    }
+
+    private String newResponseHeaders(GetRequest request) throws IOException, ProxyCacheException {
+        String mime = source.getMime();
+        boolean mimeKnown = !TextUtils.isEmpty(mime);
+        long length = cache.isCompleted() ? cache.available() : source.length();
+        boolean lengthKnown = length >= 0;
+        long contentLength = request.partial ? length - request.rangeOffset : length;
+        boolean addRange = lengthKnown && request.partial;
+        return new StringBuilder()
+                .append(request.partial ? "HTTP/1.1 206 PARTIAL CONTENT\n" : "HTTP/1.1 200 OK\n")
+                .append("Accept-Ranges: bytes\n")
+                .append(lengthKnown ? format("Content-Length: %d\n", contentLength) : "")
+                .append(addRange ? format("Content-Range: bytes %d-%d/%d\n", request.rangeOffset, length - 1, length) : "")
+                .append(mimeKnown ? format("Content-Type: %s\n", mime) : "")
+                .append("\n") // headers end
+                .toString();
+    }
+
+    private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
+        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+        int readBytes;
+        while ((readBytes = read(buffer, offset, buffer.length)) != -1) {
+            out.write(buffer, 0, readBytes);
+            offset += readBytes;
+        }
+        out.flush();
+    }
+
+    private void responseWithoutCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
+        HttpUrlSource newSourceNoCache = new HttpUrlSource(this.source);
+        try {
+            newSourceNoCache.open((int) offset);
+            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+            int readBytes;
+            while ((readBytes = newSourceNoCache.read(buffer)) != -1) {
+                out.write(buffer, 0, readBytes);
+                offset += readBytes;
+            }
+            out.flush();
+        } finally {
+            newSourceNoCache.close();
+        }
+    }
+
+    private String format(String pattern, Object... args) {
+        return String.format(Locale.US, pattern, args);
+    }
+
+    @Override
+    protected void onCachePercentsAvailableChanged(int percents) {
+        if (listener != null) {
+            listener.onCacheAvailable(cache.file, source.getUrl(), percents);
+        }
+    }
+}

+ 454 - 0
VideoCache/src/main/java/com/yc/videocache/HttpProxyCacheServer.java

@@ -0,0 +1,454 @@
+package com.yc.videocache;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.yc.videocache.file.DiskUsage;
+import com.yc.videocache.file.FileNameGenerator;
+import com.yc.videocache.file.Md5FileNameGenerator;
+import com.yc.videocache.file.TotalCountLruDiskUsage;
+import com.yc.videocache.file.TotalSizeLruDiskUsage;
+import com.yc.videocache.headers.EmptyHeadersInjector;
+import com.yc.videocache.headers.HeaderInjector;
+import com.yc.videocache.sourcestorage.SourceInfoStorage;
+import com.yc.videocache.sourcestorage.SourceInfoStorageFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static com.yc.videocache.Preconditions.checkAllNotNull;
+import static com.yc.videocache.Preconditions.checkNotNull;
+
+/**
+ * Simple lightweight proxy server with file caching support that handles HTTP requests.
+ * Typical usage:
+ * <pre><code>
+ * public onCreate(Bundle state) {
+ *      super.onCreate(state);
+ *
+ *      HttpProxyCacheServer proxy = getProxy();
+ *      String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
+ *      videoView.setVideoPath(proxyUrl);
+ * }
+ *
+ * private HttpProxyCacheServer getProxy() {
+ * // should return single instance of HttpProxyCacheServer shared for whole app.
+ * }
+ * </code></pre>
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public class HttpProxyCacheServer {
+
+    private static final String PROXY_HOST = "127.0.0.1";
+
+    private final Object clientsLock = new Object();
+    private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8);
+    private final Map<String, HttpProxyCacheServerClients> clientsMap = new ConcurrentHashMap<>();
+    private final ServerSocket serverSocket;
+    private final int port;
+    private final Thread waitConnectionThread;
+    private final Config config;
+
+    public HttpProxyCacheServer(Context context) {
+        this(new Builder(context).buildConfig());
+    }
+
+    private HttpProxyCacheServer(Config config) {
+        this.config = checkNotNull(config);
+        try {
+            InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
+            this.serverSocket = new ServerSocket(0, 8, inetAddress);
+            this.port = serverSocket.getLocalPort();
+            IgnoreHostProxySelector.install(PROXY_HOST, port);
+            CountDownLatch startSignal = new CountDownLatch(1);
+            this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
+            this.waitConnectionThread.start();
+            startSignal.await(); // freeze thread, wait for server starts
+        } catch (IOException | InterruptedException e) {
+            socketProcessor.shutdown();
+            throw new IllegalStateException("Error starting local proxy server", e);
+        }
+    }
+
+    /**
+     * Returns url that wrap original url and should be used for client (MediaPlayer, ExoPlayer, etc).
+     * <p>
+     * If file for this url is fully cached (it means method {@link #isCached(String)} returns {@code true})
+     * then file:// uri to cached file will be returned.
+     * <p>
+     * Calling this method has same effect as calling {@link #getProxyUrl(String, boolean)} with 2nd parameter set to {@code true}.
+     *
+     * @param url a url to file that should be cached.
+     * @return a wrapped by proxy url if file is not fully cached or url pointed to cache file otherwise.
+     */
+    public String getProxyUrl(String url) {
+        return getProxyUrl(url, true);
+    }
+
+    /**
+     * Returns url that wrap original url and should be used for client (MediaPlayer, ExoPlayer, etc).
+     * <p>
+     * If parameter {@code allowCachedFileUri} is {@code true} and file for this url is fully cached
+     * (it means method {@link #isCached(String)} returns {@code true}) then file:// uri to cached file will be returned.
+     *
+     * @param url                a url to file that should be cached.
+     * @param allowCachedFileUri {@code true} if allow to return file:// uri if url is fully cached
+     * @return a wrapped by proxy url if file is not fully cached or url pointed to cache file otherwise (if {@code allowCachedFileUri} is {@code true}).
+     */
+    public String getProxyUrl(String url, boolean allowCachedFileUri) {
+        if (allowCachedFileUri && getCacheFile(url).exists()) {
+            File cacheFile = getCacheFile(url);
+            touchFileSafely(cacheFile);
+            return Uri.fromFile(cacheFile).toString();
+        }
+        return appendToProxyUrl(url);
+    }
+
+    public void registerCacheListener(CacheListener cacheListener, String url) {
+        checkAllNotNull(cacheListener, url);
+        synchronized (clientsLock) {
+            try {
+                getClients(url).registerCacheListener(cacheListener);
+            } catch (ProxyCacheException e) {
+                Logger.warn("Error registering cache listener");
+            }
+        }
+    }
+
+    public void unregisterCacheListener(CacheListener cacheListener, String url) {
+        checkAllNotNull(cacheListener, url);
+        synchronized (clientsLock) {
+            try {
+                getClients(url).unregisterCacheListener(cacheListener);
+            } catch (ProxyCacheException e) {
+                Logger.warn("Error registering cache listener");
+            }
+        }
+    }
+
+    public void unregisterCacheListener(CacheListener cacheListener) {
+        checkNotNull(cacheListener);
+        synchronized (clientsLock) {
+            for (HttpProxyCacheServerClients clients : clientsMap.values()) {
+                clients.unregisterCacheListener(cacheListener);
+            }
+        }
+    }
+
+    /**
+     * Checks is cache contains fully cached file for particular url.
+     *
+     * @param url an url cache file will be checked for.
+     * @return {@code true} if cache contains fully cached file for passed in parameters url.
+     */
+    public boolean isCached(String url) {
+        checkNotNull(url, "Url can't be null!");
+        return getCacheFile(url).exists();
+    }
+
+    public void shutdown() {
+        Logger.info("Shutdown proxy server");
+
+        shutdownClients();
+
+        config.sourceInfoStorage.release();
+
+        waitConnectionThread.interrupt();
+        try {
+            if (!serverSocket.isClosed()) {
+                serverSocket.close();
+            }
+        } catch (IOException e) {
+            onError(new ProxyCacheException("Error shutting down proxy server", e));
+        }
+    }
+
+    private String appendToProxyUrl(String url) {
+        return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));
+    }
+
+    public File getCacheFile(String url) {
+        File cacheDir = config.cacheRoot;
+        String fileName = config.fileNameGenerator.generate(url);
+        return new File(cacheDir, fileName);
+    }
+
+    public File getTempCacheFile(String url) {
+        File cacheDir = config.cacheRoot;
+        String fileName = config.fileNameGenerator.generate(url) + ".download";
+        return new File(cacheDir, fileName);
+    }
+
+    public File getCacheRoot() {
+        return config.cacheRoot;
+    }
+
+    private void touchFileSafely(File cacheFile) {
+        try {
+            config.diskUsage.touch(cacheFile);
+        } catch (IOException e) {
+            Logger.error("Error touching file " + cacheFile);
+        }
+    }
+
+    private void shutdownClients() {
+        synchronized (clientsLock) {
+            for (HttpProxyCacheServerClients clients : clientsMap.values()) {
+                clients.shutdown();
+            }
+            clientsMap.clear();
+        }
+    }
+
+    private void waitForRequest() {
+        try {
+            while (!Thread.currentThread().isInterrupted()) {
+                Socket socket = serverSocket.accept();
+                Logger.debug("Accept new socket " + socket);
+                socketProcessor.submit(new SocketProcessorRunnable(socket));
+            }
+        } catch (IOException e) {
+            onError(new ProxyCacheException("Error during waiting connection", e));
+        }
+    }
+
+    private void processSocket(Socket socket) {
+        try {
+            GetRequest request = GetRequest.read(socket.getInputStream());
+            Logger.debug("Request to cache proxy:" + request);
+            String url = ProxyCacheUtils.decode(request.uri);
+            HttpProxyCacheServerClients clients = getClients(url);
+            clients.processRequest(request, socket);
+        } catch (SocketException e) {
+            // There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
+            // So just to prevent log flooding don't log stacktrace
+            Logger.debug("Closing socket… Socket is closed by client.");
+        } catch (ProxyCacheException | IOException e) {
+            onError(new ProxyCacheException("Error processing request", e));
+        } finally {
+            releaseSocket(socket);
+            Logger.debug("Opened connections: " + getClientsCount());
+        }
+    }
+
+    private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException {
+        synchronized (clientsLock) {
+            HttpProxyCacheServerClients clients = clientsMap.get(url);
+            if (clients == null) {
+                clients = new HttpProxyCacheServerClients(url, config);
+                clientsMap.put(url, clients);
+            }
+            return clients;
+        }
+    }
+
+    private int getClientsCount() {
+        synchronized (clientsLock) {
+            int count = 0;
+            for (HttpProxyCacheServerClients clients : clientsMap.values()) {
+                count += clients.getClientsCount();
+            }
+            return count;
+        }
+    }
+
+    private void releaseSocket(Socket socket) {
+        closeSocketInput(socket);
+        closeSocketOutput(socket);
+        closeSocket(socket);
+    }
+
+    private void closeSocketInput(Socket socket) {
+        try {
+            if (!socket.isInputShutdown()) {
+                socket.shutdownInput();
+            }
+        } catch (SocketException e) {
+            // There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
+            // So just to prevent log flooding don't log stacktrace
+            Logger.debug("Releasing input stream… Socket is closed by client.");
+        } catch (IOException e) {
+            onError(new ProxyCacheException("Error closing socket input stream", e));
+        }
+    }
+
+    private void closeSocketOutput(Socket socket) {
+        try {
+            if (!socket.isOutputShutdown()) {
+                socket.shutdownOutput();
+            }
+        } catch (IOException e) {
+            Logger.warn("Failed to close socket on proxy side: {}. It seems client have already closed connection.");
+        }
+    }
+
+    private void closeSocket(Socket socket) {
+        try {
+            if (!socket.isClosed()) {
+                socket.close();
+            }
+        } catch (IOException e) {
+            onError(new ProxyCacheException("Error closing socket", e));
+        }
+    }
+
+    private void onError(Throwable e) {
+        Logger.error("HttpProxyCacheServer error");
+    }
+
+    private final class WaitRequestsRunnable implements Runnable {
+
+        private final CountDownLatch startSignal;
+
+        public WaitRequestsRunnable(CountDownLatch startSignal) {
+            this.startSignal = startSignal;
+        }
+
+        @Override
+        public void run() {
+            startSignal.countDown();
+            waitForRequest();
+        }
+    }
+
+    private final class SocketProcessorRunnable implements Runnable {
+
+        private final Socket socket;
+
+        public SocketProcessorRunnable(Socket socket) {
+            this.socket = socket;
+        }
+
+        @Override
+        public void run() {
+            processSocket(socket);
+        }
+    }
+
+    /**
+     * Builder for {@link HttpProxyCacheServer}.
+     */
+    public static final class Builder {
+
+        private static final long DEFAULT_MAX_SIZE = 512 * 1024 * 1024;
+
+        private File cacheRoot;
+        private FileNameGenerator fileNameGenerator;
+        private DiskUsage diskUsage;
+        private SourceInfoStorage sourceInfoStorage;
+        private HeaderInjector headerInjector;
+
+        public Builder(Context context) {
+            this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context);
+            this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context);
+            this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE);
+            this.fileNameGenerator = new Md5FileNameGenerator();
+            this.headerInjector = new EmptyHeadersInjector();
+        }
+
+        /**
+         * Overrides default cache folder to be used for caching files.
+         * <p>
+         * By default AndroidVideoCache uses
+         * '/Android/data/[app_package_name]/cache/video-cache/' if card is mounted and app has appropriate permission
+         * or 'video-cache' subdirectory in default application's cache directory otherwise.
+         * </p>
+         * <b>Note</b> directory must be used <b>only</b> for AndroidVideoCache files.
+         *
+         * @param file a cache directory, can't be null.
+         * @return a builder.
+         */
+        public Builder cacheDirectory(File file) {
+            this.cacheRoot = checkNotNull(file);
+            return this;
+        }
+
+        /**
+         * Overrides default cache file name generator {@link Md5FileNameGenerator} .
+         *
+         * @param fileNameGenerator a new file name generator.
+         * @return a builder.
+         */
+        public Builder fileNameGenerator(FileNameGenerator fileNameGenerator) {
+            this.fileNameGenerator = checkNotNull(fileNameGenerator);
+            return this;
+        }
+
+        /**
+         * Sets max cache size in bytes.
+         * <p>
+         * All files that exceeds limit will be deleted using LRU strategy.
+         * Default value is 512 Mb.
+         * </p>
+         * Note this method overrides result of calling {@link #maxCacheFilesCount(int)}
+         *
+         * @param maxSize max cache size in bytes.
+         * @return a builder.
+         */
+        public Builder maxCacheSize(long maxSize) {
+            this.diskUsage = new TotalSizeLruDiskUsage(maxSize);
+            return this;
+        }
+
+        /**
+         * Sets max cache files count.
+         * All files that exceeds limit will be deleted using LRU strategy.
+         * Note this method overrides result of calling {@link #maxCacheSize(long)}
+         *
+         * @param count max cache files count.
+         * @return a builder.
+         */
+        public Builder maxCacheFilesCount(int count) {
+            this.diskUsage = new TotalCountLruDiskUsage(count);
+            return this;
+        }
+
+        /**
+         * Set custom DiskUsage logic for handling when to keep or clean cache.
+         *
+         * @param diskUsage a disk usage strategy, cant be {@code null}.
+         * @return a builder.
+         */
+        public Builder diskUsage(DiskUsage diskUsage) {
+            this.diskUsage = checkNotNull(diskUsage);
+            return this;
+        }
+
+        /**
+         * Add headers along the request to the server
+         *
+         * @param headerInjector to inject header base on url
+         * @return a builder
+         */
+        public Builder headerInjector(HeaderInjector headerInjector) {
+            this.headerInjector = checkNotNull(headerInjector);
+            return this;
+        }
+
+        /**
+         * Builds new instance of {@link HttpProxyCacheServer}.
+         *
+         * @return proxy cache. Only single instance should be used across whole app.
+         */
+        public HttpProxyCacheServer build() {
+            Config config = buildConfig();
+            return new HttpProxyCacheServer(config);
+        }
+
+        private Config buildConfig() {
+            return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage, headerInjector);
+        }
+
+    }
+}

+ 115 - 0
VideoCache/src/main/java/com/yc/videocache/HttpProxyCacheServerClients.java

@@ -0,0 +1,115 @@
+package com.yc.videocache;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import com.yc.videocache.file.FileCache;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.Socket;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.yc.videocache.Preconditions.checkNotNull;
+
+/**
+ * Client for {@link HttpProxyCacheServer}
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+final class HttpProxyCacheServerClients {
+
+    private final AtomicInteger clientsCount = new AtomicInteger(0);
+    private final String url;
+    private volatile HttpProxyCache proxyCache;
+    private final List<CacheListener> listeners = new CopyOnWriteArrayList<>();
+    private final CacheListener uiCacheListener;
+    private final Config config;
+
+    public HttpProxyCacheServerClients(String url, Config config) {
+        this.url = checkNotNull(url);
+        this.config = checkNotNull(config);
+        this.uiCacheListener = new UiListenerHandler(url, listeners);
+    }
+
+    public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException {
+        startProcessRequest();
+        try {
+            clientsCount.incrementAndGet();
+            proxyCache.processRequest(request, socket);
+        } finally {
+            finishProcessRequest();
+        }
+    }
+
+    private synchronized void startProcessRequest() throws ProxyCacheException {
+        proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache;
+    }
+
+    private synchronized void finishProcessRequest() {
+        if (clientsCount.decrementAndGet() <= 0) {
+            proxyCache.shutdown();
+            proxyCache = null;
+        }
+    }
+
+    public void registerCacheListener(CacheListener cacheListener) {
+        listeners.add(cacheListener);
+    }
+
+    public void unregisterCacheListener(CacheListener cacheListener) {
+        listeners.remove(cacheListener);
+    }
+
+    public void shutdown() {
+        listeners.clear();
+        if (proxyCache != null) {
+            proxyCache.registerCacheListener(null);
+            proxyCache.shutdown();
+            proxyCache = null;
+        }
+        clientsCount.set(0);
+    }
+
+    public int getClientsCount() {
+        return clientsCount.get();
+    }
+
+    private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
+        HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage, config.headerInjector);
+        FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage);
+        HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
+        httpProxyCache.registerCacheListener(uiCacheListener);
+        return httpProxyCache;
+    }
+
+    private static final class UiListenerHandler extends Handler implements CacheListener {
+
+        private final String url;
+        private final List<CacheListener> listeners;
+
+        public UiListenerHandler(String url, List<CacheListener> listeners) {
+            super(Looper.getMainLooper());
+            this.url = url;
+            this.listeners = listeners;
+        }
+
+        @Override
+        public void onCacheAvailable(File file, String url, int percentsAvailable) {
+            Message message = obtainMessage();
+            message.arg1 = percentsAvailable;
+            message.obj = file;
+            sendMessage(message);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            for (CacheListener cacheListener : listeners) {
+                cacheListener.onCacheAvailable((File) msg.obj, url, msg.arg1);
+            }
+        }
+    }
+}

+ 202 - 0
VideoCache/src/main/java/com/yc/videocache/HttpUrlSource.java

@@ -0,0 +1,202 @@
+package com.yc.videocache;
+
+import android.text.TextUtils;
+
+import com.yc.videocache.headers.EmptyHeadersInjector;
+import com.yc.videocache.headers.HeaderInjector;
+import com.yc.videocache.sourcestorage.SourceInfoStorage;
+import com.yc.videocache.sourcestorage.SourceInfoStorageFactory;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Map;
+
+import static com.yc.videocache.Preconditions.checkNotNull;
+import static com.yc.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
+import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
+import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_PARTIAL;
+import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
+
+/**
+ * {@link Source} that uses http resource as source for {@link ProxyCache}.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public class HttpUrlSource implements Source {
+
+    private static final int MAX_REDIRECTS = 5;
+    private final SourceInfoStorage sourceInfoStorage;
+    private final HeaderInjector headerInjector;
+    private SourceInfo sourceInfo;
+    private HttpURLConnection connection;
+    private InputStream inputStream;
+
+    public HttpUrlSource(String url) {
+        this(url, SourceInfoStorageFactory.newEmptySourceInfoStorage());
+    }
+
+    public HttpUrlSource(String url, SourceInfoStorage sourceInfoStorage) {
+        this(url, sourceInfoStorage, new EmptyHeadersInjector());
+    }
+
+    public HttpUrlSource(String url, SourceInfoStorage sourceInfoStorage, HeaderInjector headerInjector) {
+        this.sourceInfoStorage = checkNotNull(sourceInfoStorage);
+        this.headerInjector = checkNotNull(headerInjector);
+        SourceInfo sourceInfo = sourceInfoStorage.get(url);
+        this.sourceInfo = sourceInfo != null ? sourceInfo :
+                new SourceInfo(url, Integer.MIN_VALUE, ProxyCacheUtils.getSupposablyMime(url));
+    }
+
+    public HttpUrlSource(HttpUrlSource source) {
+        this.sourceInfo = source.sourceInfo;
+        this.sourceInfoStorage = source.sourceInfoStorage;
+        this.headerInjector = source.headerInjector;
+    }
+
+    @Override
+    public synchronized long length() throws ProxyCacheException {
+        if (sourceInfo.length == Integer.MIN_VALUE) {
+            fetchContentInfo();
+        }
+        return sourceInfo.length;
+    }
+
+    @Override
+    public void open(long offset) throws ProxyCacheException {
+        try {
+            connection = openConnection(offset, -1);
+            String mime = connection.getContentType();
+            inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE);
+            long length = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
+            this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
+            this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
+        } catch (IOException e) {
+            throw new ProxyCacheException("Error opening connection for " + sourceInfo.url + " with offset " + offset, e);
+        }
+    }
+
+    private long readSourceAvailableBytes(HttpURLConnection connection, long offset, int responseCode) throws IOException {
+        long contentLength = getContentLength(connection);
+        return responseCode == HTTP_OK ? contentLength
+                : responseCode == HTTP_PARTIAL ? contentLength + offset : sourceInfo.length;
+    }
+
+    private long getContentLength(HttpURLConnection connection) {
+        String contentLengthValue = connection.getHeaderField("Content-Length");
+        return contentLengthValue == null ? -1 : Long.parseLong(contentLengthValue);
+    }
+
+    @Override
+    public void close() throws ProxyCacheException {
+        if (connection != null) {
+            try {
+                connection.disconnect();
+            } catch (NullPointerException | IllegalArgumentException e) {
+                String message = "Wait... but why? WTF!? " +
+                        "Really shouldn't happen any more after fixing https://github.com/danikula/AndroidVideoCache/issues/43. " +
+                        "If you read it on your device log, please, notify me danikula@gmail.com or create issue here " +
+                        "https://github.com/danikula/AndroidVideoCache/issues.";
+                throw new RuntimeException(message, e);
+            } catch (ArrayIndexOutOfBoundsException e) {
+                Logger.error("Error closing connection correctly. Should happen only on Android L. " +
+                        "If anybody know how to fix it, please visit https://github.com/danikula/AndroidVideoCache/issues/88. " +
+                        "Until good solution is not know, just ignore this issue.");
+            }
+        }
+    }
+
+    @Override
+    public int read(byte[] buffer) throws ProxyCacheException {
+        if (inputStream == null) {
+            throw new ProxyCacheException("Error reading data from " + sourceInfo.url + ": connection is absent!");
+        }
+        try {
+            return inputStream.read(buffer, 0, buffer.length);
+        } catch (InterruptedIOException e) {
+            throw new InterruptedProxyCacheException("Reading source " + sourceInfo.url + " is interrupted", e);
+        } catch (IOException e) {
+            throw new ProxyCacheException("Error reading data from " + sourceInfo.url, e);
+        }
+    }
+
+    private void fetchContentInfo() throws ProxyCacheException {
+        Logger.debug("Read content info from " + sourceInfo.url);
+        HttpURLConnection urlConnection = null;
+        InputStream inputStream = null;
+        try {
+            urlConnection = openConnection(0, 10000);
+            long length = getContentLength(urlConnection);
+            String mime = urlConnection.getContentType();
+            inputStream = urlConnection.getInputStream();
+            this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
+            this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
+            Logger.debug("Source info fetched: " + sourceInfo);
+        } catch (IOException e) {
+            Logger.error("Error fetching info from " + sourceInfo.url);
+        } finally {
+            ProxyCacheUtils.close(inputStream);
+            if (urlConnection != null) {
+                urlConnection.disconnect();
+            }
+        }
+    }
+
+    private HttpURLConnection openConnection(long offset, int timeout) throws IOException, ProxyCacheException {
+        HttpURLConnection connection;
+        boolean redirected;
+        int redirectCount = 0;
+        String url = this.sourceInfo.url;
+        do {
+            Logger.debug("Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
+            connection = (HttpURLConnection) new URL(url).openConnection();
+            injectCustomHeaders(connection, url);
+            if (offset > 0) {
+                connection.setRequestProperty("Range", "bytes=" + offset + "-");
+            }
+            if (timeout > 0) {
+                connection.setConnectTimeout(timeout);
+                connection.setReadTimeout(timeout);
+            }
+            int code = connection.getResponseCode();
+            redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP || code == HTTP_SEE_OTHER;
+            if (redirected) {
+                url = connection.getHeaderField("Location");
+                redirectCount++;
+                connection.disconnect();
+            }
+            if (redirectCount > MAX_REDIRECTS) {
+                throw new ProxyCacheException("Too many redirects: " + redirectCount);
+            }
+        } while (redirected);
+        return connection;
+    }
+
+    private void injectCustomHeaders(HttpURLConnection connection, String url) {
+        Map<String, String> extraHeaders = headerInjector.addHeaders(url);
+        for (Map.Entry<String, String> header : extraHeaders.entrySet()) {
+            connection.setRequestProperty(header.getKey(), header.getValue());
+        }
+    }
+
+    public synchronized String getMime() throws ProxyCacheException {
+        if (TextUtils.isEmpty(sourceInfo.mime)) {
+            fetchContentInfo();
+        }
+        return sourceInfo.mime;
+    }
+
+    public String getUrl() {
+        return sourceInfo.url;
+    }
+
+    @Override
+    public String toString() {
+        return "HttpUrlSource{sourceInfo='" + sourceInfo + "}";
+    }
+}

+ 50 - 0
VideoCache/src/main/java/com/yc/videocache/IgnoreHostProxySelector.java

@@ -0,0 +1,50 @@
+package com.yc.videocache;
+
+import java.io.IOException;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+
+import static com.yc.videocache.Preconditions.checkNotNull;
+
+/**
+ * {@link ProxySelector} that ignore system default proxies for concrete host.
+ * <p>
+ * It is important to <a href="https://github.com/danikula/AndroidVideoCache/issues/28">ignore system proxy</a> for localhost connection.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+class IgnoreHostProxySelector extends ProxySelector {
+
+    private static final List<Proxy> NO_PROXY_LIST = Arrays.asList(Proxy.NO_PROXY);
+
+    private final ProxySelector defaultProxySelector;
+    private final String hostToIgnore;
+    private final int portToIgnore;
+
+    IgnoreHostProxySelector(ProxySelector defaultProxySelector, String hostToIgnore, int portToIgnore) {
+        this.defaultProxySelector = checkNotNull(defaultProxySelector);
+        this.hostToIgnore = checkNotNull(hostToIgnore);
+        this.portToIgnore = portToIgnore;
+    }
+
+    static void install(String hostToIgnore, int portToIgnore) {
+        ProxySelector defaultProxySelector = ProxySelector.getDefault();
+        ProxySelector ignoreHostProxySelector = new IgnoreHostProxySelector(defaultProxySelector, hostToIgnore, portToIgnore);
+        ProxySelector.setDefault(ignoreHostProxySelector);
+    }
+
+    @Override
+    public List<Proxy> select(URI uri) {
+        boolean ignored = hostToIgnore.equals(uri.getHost()) && portToIgnore == uri.getPort();
+        return ignored ? NO_PROXY_LIST : defaultProxySelector.select(uri);
+    }
+
+    @Override
+    public void connectFailed(URI uri, SocketAddress address, IOException failure) {
+        defaultProxySelector.connectFailed(uri, address, failure);
+    }
+}

+ 21 - 0
VideoCache/src/main/java/com/yc/videocache/InterruptedProxyCacheException.java

@@ -0,0 +1,21 @@
+package com.yc.videocache;
+
+/**
+ * Indicates interruption error in work of {@link ProxyCache} fired by user.
+ *
+ * @author Alexey Danilov
+ */
+public class InterruptedProxyCacheException extends ProxyCacheException {
+
+    public InterruptedProxyCacheException(String message) {
+        super(message);
+    }
+
+    public InterruptedProxyCacheException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public InterruptedProxyCacheException(Throwable cause) {
+        super(cause);
+    }
+}

+ 34 - 0
VideoCache/src/main/java/com/yc/videocache/Logger.java

@@ -0,0 +1,34 @@
+package com.yc.videocache;
+
+import android.util.Log;
+
+public final class Logger {
+
+    private static final String TAG = "VideoCache";
+
+    private static boolean IS_DEBUG = false;
+
+    public static void debug(String msg) {
+        if (IS_DEBUG) {
+            Log.d(TAG, msg);
+        }
+    }
+
+    public static void info(String msg) {
+        if (IS_DEBUG) {
+            Log.i(TAG, msg);
+        }
+    }
+
+    public static void warn(String msg) {
+        if (IS_DEBUG) {
+            Log.w(TAG, msg);
+        }
+    }
+
+    public static void error(String msg) {
+        if (IS_DEBUG) {
+            Log.e(TAG, msg);
+        }
+    }
+}

+ 123 - 0
VideoCache/src/main/java/com/yc/videocache/Pinger.java

@@ -0,0 +1,123 @@
+package com.yc.videocache;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.Socket;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeoutException;
+
+import static com.yc.videocache.Preconditions.checkArgument;
+import static com.yc.videocache.Preconditions.checkNotNull;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+/**
+ * Pings {@link HttpProxyCacheServer} to make sure it works.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+
+class Pinger {
+
+    private static final String PING_REQUEST = "ping";
+    private static final String PING_RESPONSE = "ping ok";
+
+    private final ExecutorService pingExecutor = Executors.newSingleThreadExecutor();
+    private final String host;
+    private final int port;
+
+    Pinger(String host, int port) {
+        this.host = checkNotNull(host);
+        this.port = port;
+    }
+
+    boolean ping(int maxAttempts, int startTimeout) {
+        checkArgument(maxAttempts >= 1);
+        checkArgument(startTimeout > 0);
+
+        int timeout = startTimeout;
+        int attempts = 0;
+        while (attempts < maxAttempts) {
+            try {
+                Future<Boolean> pingFuture = pingExecutor.submit(new PingCallable());
+                boolean pinged = pingFuture.get(timeout, MILLISECONDS);
+                if (pinged) {
+                    return true;
+                }
+            } catch (TimeoutException e) {
+                Logger.warn("Error pinging server (attempt: " + attempts + ", timeout: " + timeout + "). ");
+            } catch (InterruptedException | ExecutionException e) {
+                Logger.error("Error pinging server due to unexpected error");
+            }
+            attempts++;
+            timeout *= 2;
+        }
+        String error = String.format(Locale.US, "Error pinging server (attempts: %d, max timeout: %d). " +
+                        "If you see this message, please, report at https://github.com/danikula/AndroidVideoCache/issues/134. " +
+                        "Default proxies are: %s"
+                , attempts, timeout / 2, getDefaultProxies());
+        Logger.error(error);
+        return false;
+    }
+
+    private List<Proxy> getDefaultProxies() {
+        try {
+            ProxySelector defaultProxySelector = ProxySelector.getDefault();
+            return defaultProxySelector.select(new URI(getPingUrl()));
+        } catch (URISyntaxException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    boolean isPingRequest(String request) {
+        return PING_REQUEST.equals(request);
+    }
+
+    void responseToPing(Socket socket) throws IOException {
+        OutputStream out = socket.getOutputStream();
+        out.write("HTTP/1.1 200 OK\n\n".getBytes());
+        out.write(PING_RESPONSE.getBytes());
+    }
+
+    private boolean pingServer() throws ProxyCacheException {
+        String pingUrl = getPingUrl();
+        HttpUrlSource source = new HttpUrlSource(pingUrl);
+        try {
+            byte[] expectedResponse = PING_RESPONSE.getBytes();
+            source.open(0);
+            byte[] response = new byte[expectedResponse.length];
+            source.read(response);
+            boolean pingOk = Arrays.equals(expectedResponse, response);
+            Logger.info("Ping response: `" + new String(response) + "`, pinged? " + pingOk);
+            return pingOk;
+        } catch (ProxyCacheException e) {
+            Logger.error("Error reading ping response");
+            return false;
+        } finally {
+            source.close();
+        }
+    }
+
+    private String getPingUrl() {
+        return String.format(Locale.US, "http://%s:%d/%s", host, port, PING_REQUEST);
+    }
+
+    private class PingCallable implements Callable<Boolean> {
+
+        @Override
+        public Boolean call() throws Exception {
+            return pingServer();
+        }
+    }
+
+}

+ 38 - 0
VideoCache/src/main/java/com/yc/videocache/Preconditions.java

@@ -0,0 +1,38 @@
+package com.yc.videocache;
+
+public final class Preconditions {
+
+    public static <T> T checkNotNull(T reference) {
+        if (reference == null) {
+            throw new NullPointerException();
+        }
+        return reference;
+    }
+
+    public static void checkAllNotNull(Object... references) {
+        for (Object reference : references) {
+            if (reference == null) {
+                throw new NullPointerException();
+            }
+        }
+    }
+
+    public static <T> T checkNotNull(T reference, String errorMessage) {
+        if (reference == null) {
+            throw new NullPointerException(errorMessage);
+        }
+        return reference;
+    }
+
+    static void checkArgument(boolean expression) {
+        if (!expression) {
+            throw new IllegalArgumentException();
+        }
+    }
+
+    static void checkArgument(boolean expression, String errorMessage) {
+        if (!expression) {
+            throw new IllegalArgumentException(errorMessage);
+        }
+    }
+}

+ 186 - 0
VideoCache/src/main/java/com/yc/videocache/ProxyCache.java

@@ -0,0 +1,186 @@
+package com.yc.videocache;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.yc.videocache.Preconditions.checkNotNull;
+
+/**
+ * Proxy for {@link Source} with caching support ({@link Cache}).
+ * <p/>
+ * Can be used only for sources with persistent data (that doesn't change with time).
+ * Method {@link #read(byte[], long, int)} will be blocked while fetching data from source.
+ * Useful for streaming something with caching e.g. streaming video/audio etc.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+class ProxyCache {
+
+    private static final int MAX_READ_SOURCE_ATTEMPTS = 1;
+
+    private final Source source;
+    private final Cache cache;
+    private final Object wc = new Object();
+    private final Object stopLock = new Object();
+    private final AtomicInteger readSourceErrorsCount;
+    private volatile Thread sourceReaderThread;
+    private volatile boolean stopped;
+    private volatile int percentsAvailable = -1;
+
+    public ProxyCache(Source source, Cache cache) {
+        this.source = checkNotNull(source);
+        this.cache = checkNotNull(cache);
+        this.readSourceErrorsCount = new AtomicInteger();
+    }
+
+    public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
+        ProxyCacheUtils.assertBuffer(buffer, offset, length);
+
+        while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) {
+            readSourceAsync();
+            waitForSourceData();
+            checkReadSourceErrorsCount();
+        }
+        int read = cache.read(buffer, offset, length);
+        if (cache.isCompleted() && percentsAvailable != 100) {
+            percentsAvailable = 100;
+            onCachePercentsAvailableChanged(100);
+        }
+        return read;
+    }
+
+    private void checkReadSourceErrorsCount() throws ProxyCacheException {
+        int errorsCount = readSourceErrorsCount.get();
+        if (errorsCount >= MAX_READ_SOURCE_ATTEMPTS) {
+            readSourceErrorsCount.set(0);
+            throw new ProxyCacheException("Error reading source " + errorsCount + " times");
+        }
+    }
+
+    public void shutdown() {
+        synchronized (stopLock) {
+            Logger.debug("Shutdown proxy for " + source);
+            try {
+                stopped = true;
+                if (sourceReaderThread != null) {
+                    sourceReaderThread.interrupt();
+                }
+                cache.close();
+            } catch (ProxyCacheException e) {
+                onError(e);
+            }
+        }
+    }
+
+    private synchronized void readSourceAsync() throws ProxyCacheException {
+        boolean readingInProgress = sourceReaderThread != null && sourceReaderThread.getState() != Thread.State.TERMINATED;
+        if (!stopped && !cache.isCompleted() && !readingInProgress) {
+            sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for " + source);
+            sourceReaderThread.start();
+        }
+    }
+
+    private void waitForSourceData() throws ProxyCacheException {
+        synchronized (wc) {
+            try {
+                wc.wait(1000);
+            } catch (InterruptedException e) {
+                throw new ProxyCacheException("Waiting source data is interrupted!", e);
+            }
+        }
+    }
+
+    private void notifyNewCacheDataAvailable(long cacheAvailable, long sourceAvailable) {
+        onCacheAvailable(cacheAvailable, sourceAvailable);
+
+        synchronized (wc) {
+            wc.notifyAll();
+        }
+    }
+
+    protected void onCacheAvailable(long cacheAvailable, long sourceLength) {
+        boolean zeroLengthSource = sourceLength == 0;
+        int percents = zeroLengthSource ? 100 : (int) ((float) cacheAvailable / sourceLength * 100);
+        boolean percentsChanged = percents != percentsAvailable;
+        boolean sourceLengthKnown = sourceLength >= 0;
+        if (sourceLengthKnown && percentsChanged) {
+            onCachePercentsAvailableChanged(percents);
+        }
+        percentsAvailable = percents;
+    }
+
+    protected void onCachePercentsAvailableChanged(int percentsAvailable) {
+    }
+
+    private void readSource() {
+        long sourceAvailable = -1;
+        long offset = 0;
+        try {
+            offset = cache.available();
+            source.open(offset);
+            sourceAvailable = source.length();
+            byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
+            int readBytes;
+            while ((readBytes = source.read(buffer)) != -1) {
+                synchronized (stopLock) {
+                    if (isStopped()) {
+                        return;
+                    }
+                    cache.append(buffer, readBytes);
+                }
+                offset += readBytes;
+                notifyNewCacheDataAvailable(offset, sourceAvailable);
+            }
+            tryComplete();
+            onSourceRead();
+        } catch (Throwable e) {
+            readSourceErrorsCount.incrementAndGet();
+            onError(e);
+        } finally {
+            closeSource();
+            notifyNewCacheDataAvailable(offset, sourceAvailable);
+        }
+    }
+
+    private void onSourceRead() {
+        // guaranteed notify listeners after source read and cache completed
+        percentsAvailable = 100;
+        onCachePercentsAvailableChanged(percentsAvailable);
+    }
+
+    private void tryComplete() throws ProxyCacheException {
+        synchronized (stopLock) {
+            if (!isStopped() && cache.available() == source.length()) {
+                cache.complete();
+            }
+        }
+    }
+
+    private boolean isStopped() {
+        return Thread.currentThread().isInterrupted() || stopped;
+    }
+
+    private void closeSource() {
+        try {
+            source.close();
+        } catch (ProxyCacheException e) {
+            onError(new ProxyCacheException("Error closing source " + source, e));
+        }
+    }
+
+    protected final void onError(final Throwable e) {
+        boolean interruption = e instanceof InterruptedProxyCacheException;
+        if (interruption) {
+            Logger.debug("ProxyCache is interrupted");
+        } else {
+            Logger.error("ProxyCache error");
+        }
+    }
+
+    private class SourceReaderRunnable implements Runnable {
+
+        @Override
+        public void run() {
+            readSource();
+        }
+    }
+}

+ 23 - 0
VideoCache/src/main/java/com/yc/videocache/ProxyCacheException.java

@@ -0,0 +1,23 @@
+package com.yc.videocache;
+
+/**
+ * Indicates any error in work of {@link ProxyCache}.
+ *
+ * @author Alexey Danilov
+ */
+public class ProxyCacheException extends Exception {
+
+    private static final String LIBRARY_VERSION = ". Version: " + BuildConfig.VERSION_NAME;
+
+    public ProxyCacheException(String message) {
+        super(message + LIBRARY_VERSION);
+    }
+
+    public ProxyCacheException(String message, Throwable cause) {
+        super(message + LIBRARY_VERSION, cause);
+    }
+
+    public ProxyCacheException(Throwable cause) {
+        super("No explanation error" + LIBRARY_VERSION, cause);
+    }
+}

+ 93 - 0
VideoCache/src/main/java/com/yc/videocache/ProxyCacheUtils.java

@@ -0,0 +1,93 @@
+package com.yc.videocache;
+
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import static com.yc.videocache.Preconditions.checkArgument;
+import static com.yc.videocache.Preconditions.checkNotNull;
+
+/**
+ * Just simple utils.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public class ProxyCacheUtils {
+
+    static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
+    static final int MAX_ARRAY_PREVIEW = 16;
+
+    static String getSupposablyMime(String url) {
+        MimeTypeMap mimes = MimeTypeMap.getSingleton();
+        String extension = MimeTypeMap.getFileExtensionFromUrl(url);
+        return TextUtils.isEmpty(extension) ? null : mimes.getMimeTypeFromExtension(extension);
+    }
+
+    static void assertBuffer(byte[] buffer, long offset, int length) {
+        checkNotNull(buffer, "Buffer must be not null!");
+        checkArgument(offset >= 0, "Data offset must be positive!");
+        checkArgument(length >= 0 && length <= buffer.length, "Length must be in range [0..buffer.length]");
+    }
+
+    static String preview(byte[] data, int length) {
+        int previewLength = Math.min(MAX_ARRAY_PREVIEW, Math.max(length, 0));
+        byte[] dataRange = Arrays.copyOfRange(data, 0, previewLength);
+        String preview = Arrays.toString(dataRange);
+        if (previewLength < length) {
+            preview = preview.substring(0, preview.length() - 1) + ", ...]";
+        }
+        return preview;
+    }
+
+    static String encode(String url) {
+        try {
+            return URLEncoder.encode(url, "utf-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Error encoding url", e);
+        }
+    }
+
+    static String decode(String url) {
+        try {
+            return URLDecoder.decode(url, "utf-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Error decoding url", e);
+        }
+    }
+
+    static void close(Closeable closeable) {
+        if (closeable != null) {
+            try {
+                closeable.close();
+            } catch (IOException e) {
+                Logger.error("Error closing resource");
+            }
+        }
+    }
+
+    public static String computeMD5(String string) {
+        try {
+            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
+            byte[] digestBytes = messageDigest.digest(string.getBytes());
+            return bytesToHexString(digestBytes);
+        } catch (NoSuchAlgorithmException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private static String bytesToHexString(byte[] bytes) {
+        StringBuffer sb = new StringBuffer();
+        for (byte b : bytes) {
+            sb.append(String.format("%02x", b));
+        }
+        return sb.toString();
+    }
+}

+ 41 - 0
VideoCache/src/main/java/com/yc/videocache/Source.java

@@ -0,0 +1,41 @@
+package com.yc.videocache;
+
+/**
+ * Source for proxy.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public interface Source {
+
+    /**
+     * Opens source. Source should be open before using {@link #read(byte[])}
+     *
+     * @param offset offset in bytes for source.
+     * @throws ProxyCacheException if error occur while opening source.
+     */
+    void open(long offset) throws ProxyCacheException;
+
+    /**
+     * Returns length bytes or <b>negative value</b> if length is unknown.
+     *
+     * @return bytes length
+     * @throws ProxyCacheException if error occur while fetching source data.
+     */
+    long length() throws ProxyCacheException;
+
+    /**
+     * Read data to byte buffer from source with current offset.
+     *
+     * @param buffer a buffer to be used for reading data.
+     * @return a count of read bytes
+     * @throws ProxyCacheException if error occur while reading source.
+     */
+    int read(byte[] buffer) throws ProxyCacheException;
+
+    /**
+     * Closes source and release resources. Every opened source should be closed.
+     *
+     * @throws ProxyCacheException if error occur while closing source.
+     */
+    void close() throws ProxyCacheException;
+}

+ 28 - 0
VideoCache/src/main/java/com/yc/videocache/SourceInfo.java

@@ -0,0 +1,28 @@
+package com.yc.videocache;
+
+/**
+ * Stores source's info.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public class SourceInfo {
+
+    public final String url;
+    public final long length;
+    public final String mime;
+
+    public SourceInfo(String url, long length, String mime) {
+        this.url = url;
+        this.length = length;
+        this.mime = mime;
+    }
+
+    @Override
+    public String toString() {
+        return "SourceInfo{" +
+                "url='" + url + '\'' +
+                ", length=" + length +
+                ", mime='" + mime + '\'' +
+                '}';
+    }
+}

+ 100 - 0
VideoCache/src/main/java/com/yc/videocache/StorageUtils.java

@@ -0,0 +1,100 @@
+package com.yc.videocache;
+
+import android.content.Context;
+import android.os.Environment;
+
+import java.io.File;
+
+import static android.os.Environment.MEDIA_MOUNTED;
+
+/**
+ * Provides application storage paths
+ * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
+ * @since 1.0.0
+ */
+public final class StorageUtils {
+
+    private static final String INDIVIDUAL_DIR_NAME = "video-cache";
+
+    /**
+     * Returns individual application cache directory (for only video caching from Proxy). Cache directory will be
+     * created on SD card <i>("/Android/data/[app_package_name]/cache/video-cache")</i> if card is mounted .
+     * Else - Android defines cache directory on device's file system.
+     *
+     * @param context Application context
+     * @return Cache {@link File directory}
+     */
+    static File getIndividualCacheDirectory(Context context) {
+        File cacheDir = getCacheDirectory(context);
+        return new File(cacheDir, INDIVIDUAL_DIR_NAME);
+    }
+
+    /**
+     * Returns application cache directory. Cache directory will be created on SD card
+     * <i>("/Android/data/[app_package_name]/cache")</i> (if card is mounted and app has appropriate permission) or
+     * on device's file system depending incoming parameters.
+     *
+     * @param context        Application context
+     * @return Cache {@link File directory}.<br />
+     * <b>NOTE:</b> Can be null in some unpredictable cases (if SD card is unmounted and
+     * {@link Context#getCacheDir() Context.getCacheDir()} returns null).
+     */
+    private static File getCacheDirectory(Context context) {
+        File appCacheDir = null;
+        if (MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
+            appCacheDir = context.getExternalCacheDir();
+        }
+        if (appCacheDir == null) {
+            appCacheDir = context.getCacheDir();
+        }
+        if (appCacheDir == null) {
+            String cacheDirPath = "/data/data/" + context.getPackageName() + "/cache/";
+            appCacheDir = new File(cacheDirPath);
+        }
+        return appCacheDir;
+    }
+
+    /**
+     * 删除文件
+     * @param root                              file
+     * @return                                  是否删除成功
+     */
+    public static boolean deleteFiles(File root) {
+        File[] files = root.listFiles();
+        if (files != null) {
+            for (File f : files) {
+                if (!f.isDirectory() && f.exists()) { // 判断是否存在
+                    if (!f.delete()) {
+                        return false;
+                    }
+                }
+            }
+        }
+        return true;
+    }
+
+
+    /**
+     * 删除文件
+     * @param filePath                          file路径
+     * @return                                  是否删除成功
+     */
+    public static boolean deleteFile(String filePath) {
+        File file = new File(filePath);
+        if (file.exists()) {
+            if (file.isFile()) {
+                return file.delete();
+            } else {
+                String[] filePaths = file.list();
+
+                if (filePaths != null) {
+                    for (String path : filePaths) {
+                        deleteFile(filePath + File.separator + path);
+                    }
+                }
+                return file.delete();
+            }
+        }
+        return true;
+    }
+}

+ 13 - 0
VideoCache/src/main/java/com/yc/videocache/file/DiskUsage.java

@@ -0,0 +1,13 @@
+package com.yc.videocache.file;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Declares how {@link FileCache} will use disc space.
+ */
+public interface DiskUsage {
+
+    void touch(File file) throws IOException;
+
+}

+ 125 - 0
VideoCache/src/main/java/com/yc/videocache/file/FileCache.java

@@ -0,0 +1,125 @@
+package com.yc.videocache.file;
+
+import com.yc.videocache.Cache;
+import com.yc.videocache.ProxyCacheException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+/**
+ * {@link Cache} that uses file for storing data.
+ */
+public class FileCache implements Cache {
+
+    private static final String TEMP_POSTFIX = ".download";
+
+    private final DiskUsage diskUsage;
+    public File file;
+    private RandomAccessFile dataFile;
+
+    public FileCache(File file) throws ProxyCacheException {
+        this(file, new UnlimitedDiskUsage());
+    }
+
+    public FileCache(File file, DiskUsage diskUsage) throws ProxyCacheException {
+        try {
+            if (diskUsage == null) {
+                throw new NullPointerException();
+            }
+            this.diskUsage = diskUsage;
+            File directory = file.getParentFile();
+            Files.makeDir(directory);
+            boolean completed = file.exists();
+            this.file = completed ? file : new File(file.getParentFile(), file.getName() + TEMP_POSTFIX);
+            this.dataFile = new RandomAccessFile(this.file, completed ? "r" : "rw");
+        } catch (IOException e) {
+            throw new ProxyCacheException("Error using file " + file + " as disc cache", e);
+        }
+    }
+
+    @Override
+    public synchronized long available() throws ProxyCacheException {
+        try {
+            return (int) dataFile.length();
+        } catch (IOException e) {
+            throw new ProxyCacheException("Error reading length of file " + file, e);
+        }
+    }
+
+    @Override
+    public synchronized int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
+        try {
+            dataFile.seek(offset);
+            return dataFile.read(buffer, 0, length);
+        } catch (IOException e) {
+            String format = "Error reading %d bytes with offset %d from file[%d bytes] to buffer[%d bytes]";
+            throw new ProxyCacheException(String.format(format, length, offset, available(), buffer.length), e);
+        }
+    }
+
+    @Override
+    public synchronized void append(byte[] data, int length) throws ProxyCacheException {
+        try {
+            if (isCompleted()) {
+                throw new ProxyCacheException("Error append cache: cache file " + file + " is completed!");
+            }
+            dataFile.seek(available());
+            dataFile.write(data, 0, length);
+        } catch (IOException e) {
+            String format = "Error writing %d bytes to %s from buffer with size %d";
+            throw new ProxyCacheException(String.format(format, length, dataFile, data.length), e);
+        }
+    }
+
+    @Override
+    public synchronized void close() throws ProxyCacheException {
+        try {
+            dataFile.close();
+            diskUsage.touch(file);
+        } catch (IOException e) {
+            throw new ProxyCacheException("Error closing file " + file, e);
+        }
+    }
+
+    @Override
+    public synchronized void complete() throws ProxyCacheException {
+        if (isCompleted()) {
+            return;
+        }
+
+        close();
+        String fileName = file.getName().substring(0, file.getName().length() - TEMP_POSTFIX.length());
+        File completedFile = new File(file.getParentFile(), fileName);
+        boolean renamed = file.renameTo(completedFile);
+        if (!renamed) {
+            throw new ProxyCacheException("Error renaming file " + file + " to " + completedFile + " for completion!");
+        }
+        file = completedFile;
+        try {
+            dataFile = new RandomAccessFile(file, "r");
+            diskUsage.touch(file);
+        } catch (IOException e) {
+            throw new ProxyCacheException("Error opening " + file + " as disc cache", e);
+        }
+    }
+
+    @Override
+    public synchronized boolean isCompleted() {
+        return !isTempFile(file);
+    }
+
+    /**
+     * Returns file to be used fo caching. It may as original file passed in constructor as some temp file for not completed cache.
+     *
+     * @return file for caching.
+     */
+    public File getFile() {
+        return file;
+    }
+
+    private boolean isTempFile(File file) {
+        return file.getName().endsWith(TEMP_POSTFIX);
+    }
+
+}

+ 10 - 0
VideoCache/src/main/java/com/yc/videocache/file/FileNameGenerator.java

@@ -0,0 +1,10 @@
+package com.yc.videocache.file;
+
+/**
+ * Generator for files to be used for caching.
+ */
+public interface FileNameGenerator {
+
+    String generate(String url);
+
+}

+ 92 - 0
VideoCache/src/main/java/com/yc/videocache/file/Files.java

@@ -0,0 +1,92 @@
+package com.yc.videocache.file;
+
+import com.yc.videocache.Logger;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Utils for work with files.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+class Files {
+
+    static void makeDir(File directory) throws IOException {
+        if (directory.exists()) {
+            if (!directory.isDirectory()) {
+                throw new IOException("File " + directory + " is not directory!");
+            }
+        } else {
+            boolean isCreated = directory.mkdirs();
+            if (!isCreated) {
+                throw new IOException(String.format("Directory %s can't be created", directory.getAbsolutePath()));
+            }
+        }
+    }
+
+    static List<File> getLruListFiles(File directory) {
+        List<File> result = new LinkedList<>();
+        File[] files = directory.listFiles();
+        if (files != null) {
+            result = Arrays.asList(files);
+            Collections.sort(result, new LastModifiedComparator());
+        }
+        return result;
+    }
+
+    static void setLastModifiedNow(File file) throws IOException {
+        if (file.exists()) {
+            long now = System.currentTimeMillis();
+            boolean modified = file.setLastModified(now); // on some devices (e.g. Nexus 5) doesn't work
+            if (!modified) {
+                modify(file);
+                if (file.lastModified() < now) {
+                    // NOTE: apparently this is a known issue (see: http://stackoverflow.com/questions/6633748/file-lastmodified-is-never-what-was-set-with-file-setlastmodified)
+                    Logger.warn(String.format("Last modified date %s is not set for file %s", new Date(file.lastModified()), file.getAbsolutePath()));
+                }
+            }
+        }
+    }
+
+    static void modify(File file) throws IOException {
+        long size = file.length();
+        if (size == 0) {
+            recreateZeroSizeFile(file);
+            return;
+        }
+
+        RandomAccessFile accessFile = new RandomAccessFile(file, "rwd");
+        accessFile.seek(size - 1);
+        byte lastByte = accessFile.readByte();
+        accessFile.seek(size - 1);
+        accessFile.write(lastByte);
+        accessFile.close();
+    }
+
+    private static void recreateZeroSizeFile(File file) throws IOException {
+        if (!file.delete() || !file.createNewFile()) {
+            throw new IOException("Error recreate zero-size file " + file);
+        }
+    }
+
+    private static final class LastModifiedComparator implements Comparator<File> {
+
+        @Override
+        public int compare(File lhs, File rhs) {
+            return compareLong(lhs.lastModified(), rhs.lastModified());
+        }
+
+        private int compareLong(long first, long second) {
+            return (first < second) ? -1 : ((first == second) ? 0 : 1);
+        }
+    }
+
+}

+ 75 - 0
VideoCache/src/main/java/com/yc/videocache/file/LruDiskUsage.java

@@ -0,0 +1,75 @@
+package com.yc.videocache.file;
+
+import com.yc.videocache.Logger;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * {@link DiskUsage} that uses LRU (Least Recently Used) strategy to trim cache.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public abstract class LruDiskUsage implements DiskUsage {
+
+    private final ExecutorService workerThread = Executors.newSingleThreadExecutor();
+
+    @Override
+    public void touch(File file) throws IOException {
+        workerThread.submit(new TouchCallable(file));
+    }
+
+    private void touchInBackground(File file) throws IOException {
+        Files.setLastModifiedNow(file);
+        List<File> files = Files.getLruListFiles(file.getParentFile());
+        trim(files);
+    }
+
+    protected abstract boolean accept(File file, long totalSize, int totalCount);
+
+    private void trim(List<File> files) {
+        long totalSize = countTotalSize(files);
+        int totalCount = files.size();
+        for (File file : files) {
+            boolean accepted = accept(file, totalSize, totalCount);
+            if (!accepted) {
+                long fileSize = file.length();
+                boolean deleted = file.delete();
+                if (deleted) {
+                    totalCount--;
+                    totalSize -= fileSize;
+                    Logger.info("Cache file " + file + " is deleted because it exceeds cache limit");
+                } else {
+                    Logger.error("Error deleting file " + file + " for trimming cache");
+                }
+            }
+        }
+    }
+
+    private long countTotalSize(List<File> files) {
+        long totalSize = 0;
+        for (File file : files) {
+            totalSize += file.length();
+        }
+        return totalSize;
+    }
+
+    private class TouchCallable implements Callable<Void> {
+
+        private final File file;
+
+        public TouchCallable(File file) {
+            this.file = file;
+        }
+
+        @Override
+        public Void call() throws Exception {
+            touchInBackground(file);
+            return null;
+        }
+    }
+}

+ 29 - 0
VideoCache/src/main/java/com/yc/videocache/file/Md5FileNameGenerator.java

@@ -0,0 +1,29 @@
+package com.yc.videocache.file;
+
+import android.text.TextUtils;
+
+import com.yc.videocache.ProxyCacheUtils;
+
+/**
+ * Implementation of {@link FileNameGenerator} that uses MD5 of url as file name
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public class Md5FileNameGenerator implements FileNameGenerator {
+
+    private static final int MAX_EXTENSION_LENGTH = 4;
+
+    @Override
+    public String generate(String url) {
+        String extension = getExtension(url);
+        String name = ProxyCacheUtils.computeMD5(url);
+        return TextUtils.isEmpty(extension) ? name : name + "." + extension;
+    }
+
+    private String getExtension(String url) {
+        int dotIndex = url.lastIndexOf('.');
+        int slashIndex = url.lastIndexOf('/');
+        return dotIndex != -1 && dotIndex > slashIndex && dotIndex + 2 + MAX_EXTENSION_LENGTH > url.length() ?
+                url.substring(dotIndex + 1, url.length()) : "";
+    }
+}

+ 25 - 0
VideoCache/src/main/java/com/yc/videocache/file/TotalCountLruDiskUsage.java

@@ -0,0 +1,25 @@
+package com.yc.videocache.file;
+
+import java.io.File;
+
+/**
+ * {@link DiskUsage} that uses LRU (Least Recently Used) strategy and trims cache size to max files count if needed.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public class TotalCountLruDiskUsage extends LruDiskUsage {
+
+    private final int maxCount;
+
+    public TotalCountLruDiskUsage(int maxCount) {
+        if (maxCount <= 0) {
+            throw new IllegalArgumentException("Max count must be positive number!");
+        }
+        this.maxCount = maxCount;
+    }
+
+    @Override
+    protected boolean accept(File file, long totalSize, int totalCount) {
+        return totalCount <= maxCount;
+    }
+}

+ 25 - 0
VideoCache/src/main/java/com/yc/videocache/file/TotalSizeLruDiskUsage.java

@@ -0,0 +1,25 @@
+package com.yc.videocache.file;
+
+import java.io.File;
+
+/**
+ * {@link DiskUsage} that uses LRU (Least Recently Used) strategy and trims cache size to max size if needed.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public class TotalSizeLruDiskUsage extends LruDiskUsage {
+
+    private final long maxSize;
+
+    public TotalSizeLruDiskUsage(long maxSize) {
+        if (maxSize <= 0) {
+            throw new IllegalArgumentException("Max size must be positive number!");
+        }
+        this.maxSize = maxSize;
+    }
+
+    @Override
+    protected boolean accept(File file, long totalSize, int totalCount) {
+        return totalSize <= maxSize;
+    }
+}

+ 17 - 0
VideoCache/src/main/java/com/yc/videocache/file/UnlimitedDiskUsage.java

@@ -0,0 +1,17 @@
+package com.yc.videocache.file;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Unlimited version of {@link DiskUsage}.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public class UnlimitedDiskUsage implements DiskUsage {
+
+    @Override
+    public void touch(File file) throws IOException {
+        // do nothing
+    }
+}

+ 18 - 0
VideoCache/src/main/java/com/yc/videocache/headers/EmptyHeadersInjector.java

@@ -0,0 +1,18 @@
+package com.yc.videocache.headers;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Empty {@link HeaderInjector} implementation.
+ *
+ * @author Lucas Nelaupe (https://github.com/lucas34).
+ */
+public class EmptyHeadersInjector implements HeaderInjector {
+
+    @Override
+    public Map<String, String> addHeaders(String url) {
+        return new HashMap<>();
+    }
+
+}

+ 20 - 0
VideoCache/src/main/java/com/yc/videocache/headers/HeaderInjector.java

@@ -0,0 +1,20 @@
+package com.yc.videocache.headers;
+
+import java.util.Map;
+
+/**
+ * Allows to add custom headers to server's requests.
+ *
+ * @author Lucas Nelaupe (https://github.com/lucas34).
+ */
+public interface HeaderInjector {
+
+    /**
+     * Adds headers to server's requests for corresponding url.
+     *
+     * @param url an url headers will be added for
+     * @return a map with headers, where keys are header's names, and values are header's values. {@code null} is not acceptable!
+     */
+    Map<String, String> addHeaders(String url);
+
+}

+ 98 - 0
VideoCache/src/main/java/com/yc/videocache/sourcestorage/DatabaseSourceInfoStorage.java

@@ -0,0 +1,98 @@
+package com.yc.videocache.sourcestorage;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import com.yc.videocache.SourceInfo;
+
+import static com.yc.videocache.Preconditions.checkAllNotNull;
+import static com.yc.videocache.Preconditions.checkNotNull;
+
+/**
+ * Database based {@link SourceInfoStorage}.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+class DatabaseSourceInfoStorage extends SQLiteOpenHelper implements SourceInfoStorage {
+
+    private static final String TABLE = "SourceInfo";
+    private static final String COLUMN_ID = "_id";
+    private static final String COLUMN_URL = "url";
+    private static final String COLUMN_LENGTH = "length";
+    private static final String COLUMN_MIME = "mime";
+    private static final String[] ALL_COLUMNS = new String[]{COLUMN_ID, COLUMN_URL, COLUMN_LENGTH, COLUMN_MIME};
+    private static final String CREATE_SQL =
+            "CREATE TABLE " + TABLE + " (" +
+                    COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
+                    COLUMN_URL + " TEXT NOT NULL," +
+                    COLUMN_MIME + " TEXT," +
+                    COLUMN_LENGTH + " INTEGER" +
+                    ");";
+
+    DatabaseSourceInfoStorage(Context context) {
+        super(context, "AndroidVideoCache.db", null, 1);
+        checkNotNull(context);
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        checkNotNull(db);
+        db.execSQL(CREATE_SQL);
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        throw new IllegalStateException("Should not be called. There is no any migration");
+    }
+
+    @Override
+    public SourceInfo get(String url) {
+        checkNotNull(url);
+        Cursor cursor = null;
+        try {
+            cursor = getReadableDatabase().query(TABLE, ALL_COLUMNS, COLUMN_URL + "=?", new String[]{url}, null, null, null);
+            return cursor == null || !cursor.moveToFirst() ? null : convert(cursor);
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+
+    @Override
+    public void put(String url, SourceInfo sourceInfo) {
+        checkAllNotNull(url, sourceInfo);
+        SourceInfo sourceInfoFromDb = get(url);
+        boolean exist = sourceInfoFromDb != null;
+        ContentValues contentValues = convert(sourceInfo);
+        if (exist) {
+            getWritableDatabase().update(TABLE, contentValues, COLUMN_URL + "=?", new String[]{url});
+        } else {
+            getWritableDatabase().insert(TABLE, null, contentValues);
+        }
+    }
+
+    @Override
+    public void release() {
+        close();
+    }
+
+    private SourceInfo convert(Cursor cursor) {
+        return new SourceInfo(
+                cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_URL)),
+                cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_LENGTH)),
+                cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MIME))
+        );
+    }
+
+    private ContentValues convert(SourceInfo sourceInfo) {
+        ContentValues values = new ContentValues();
+        values.put(COLUMN_URL, sourceInfo.url);
+        values.put(COLUMN_LENGTH, sourceInfo.length);
+        values.put(COLUMN_MIME, sourceInfo.mime);
+        return values;
+    }
+}

+ 24 - 0
VideoCache/src/main/java/com/yc/videocache/sourcestorage/NoSourceInfoStorage.java

@@ -0,0 +1,24 @@
+package com.yc.videocache.sourcestorage;
+
+import com.yc.videocache.SourceInfo;
+
+/**
+ * {@link SourceInfoStorage} that does nothing.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public class NoSourceInfoStorage implements SourceInfoStorage {
+
+    @Override
+    public SourceInfo get(String url) {
+        return null;
+    }
+
+    @Override
+    public void put(String url, SourceInfo sourceInfo) {
+    }
+
+    @Override
+    public void release() {
+    }
+}

+ 17 - 0
VideoCache/src/main/java/com/yc/videocache/sourcestorage/SourceInfoStorage.java

@@ -0,0 +1,17 @@
+package com.yc.videocache.sourcestorage;
+
+import com.yc.videocache.SourceInfo;
+
+/**
+ * Storage for {@link SourceInfo}.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public interface SourceInfoStorage {
+
+    SourceInfo get(String url);
+
+    void put(String url, SourceInfo sourceInfo);
+
+    void release();
+}

+ 19 - 0
VideoCache/src/main/java/com/yc/videocache/sourcestorage/SourceInfoStorageFactory.java

@@ -0,0 +1,19 @@
+package com.yc.videocache.sourcestorage;
+
+import android.content.Context;
+
+/**
+ * Simple factory for {@link SourceInfoStorage}.
+ *
+ * @author Alexey Danilov (danikula@gmail.com).
+ */
+public class SourceInfoStorageFactory {
+
+    public static SourceInfoStorage newSourceInfoStorage(Context context) {
+        return new DatabaseSourceInfoStorage(context);
+    }
+
+    public static SourceInfoStorage newEmptySourceInfoStorage() {
+        return new NoSourceInfoStorage();
+    }
+}

+ 0 - 0
YCVideoPlayerLib/.gitignore → VideoPlayer/.gitignore


+ 16 - 9
YCVideoPlayerLib/build.gradle → VideoPlayer/build.gradle

@@ -1,13 +1,11 @@
 apply plugin: 'com.android.library'
 apply plugin: 'com.android.library'
 
 
 android {
 android {
-    compileSdkVersion 28
-    buildToolsVersion '28.0.3'
-
-
+    compileSdkVersion 29
+    buildToolsVersion '29.0.0'
     defaultConfig {
     defaultConfig {
-        minSdkVersion 14
-        targetSdkVersion 28
+        minSdkVersion 17
+        targetSdkVersion 29
         versionCode 18
         versionCode 18
         versionName "2.6.4"
         versionName "2.6.4"
     }
     }
@@ -18,13 +16,13 @@ android {
             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
         }
         }
     }
     }
-
 }
 }
 
 
 dependencies {
 dependencies {
     implementation fileTree(dir: 'libs', include: ['*.jar'])
     implementation fileTree(dir: 'libs', include: ['*.jar'])
-    implementation 'com.android.support:appcompat-v7:28.0.0'
-    implementation 'com.android.support:cardview-v7:28.0.0'
+    implementation 'androidx.appcompat:appcompat:1.2.0'
+    implementation 'androidx.annotation:annotation:1.1.0'
+    implementation 'androidx.cardview:cardview:1.0.0'
     //这两个是必须要加的,其它的可供选择
     //这两个是必须要加的,其它的可供选择
     implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8'
     implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8'
     implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.4'
     implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.4'
@@ -33,6 +31,15 @@ dependencies {
     //implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8'
     //implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8'
     //implementation 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.8'
     //implementation 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.8'
     //implementation 'tv.danmaku.ijk.media:ijkplayer-x86_64:0.8.8'
     //implementation 'tv.danmaku.ijk.media:ijkplayer-x86_64:0.8.8'
+
+
+    //谷歌播放器
+    implementation  "com.google.android.exoplayer:exoplayer:2.11.3"
+    implementation "com.google.android.exoplayer:exoplayer-core:2.11.3"
+    implementation "com.google.android.exoplayer:exoplayer-dash:2.11.3"
+    implementation "com.google.android.exoplayer:exoplayer-hls:2.11.3"
+    implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:2.11.3"
+    implementation "com.google.android.exoplayer:extension-rtmp:2.11.3"
 }
 }
 
 
 /** 以下开始是将Android Library上传到jcenter的相关配置**/
 /** 以下开始是将Android Library上传到jcenter的相关配置**/

+ 0 - 0
YCVideoPlayerLib/proguard-rules.pro → VideoPlayer/proguard-rules.pro


+ 0 - 0
YCVideoPlayerLib/src/main/AndroidManifest.xml → VideoPlayer/src/main/AndroidManifest.xml


+ 39 - 6
YCVideoPlayerLib/src/main/java/org/yczbj/ycvideoplayerlib/constant/ConstantKeys.java → VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/config/ConstantKeys.java

@@ -13,9 +13,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 See the License for the specific language governing permissions and
 limitations under the License.
 limitations under the License.
 */
 */
-package org.yczbj.ycvideoplayerlib.constant;
+package org.yczbj.ycvideoplayerlib.config;
 
 
-import android.support.annotation.IntDef;
+
+import androidx.annotation.IntDef;
 
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.RetentionPolicy;
@@ -35,14 +36,18 @@ public final class ConstantKeys {
      * 通过注解限定类型
      * 通过注解限定类型
      * TYPE_IJK                 IjkPlayer,基于IjkPlayer封装播放器
      * TYPE_IJK                 IjkPlayer,基于IjkPlayer封装播放器
      * TYPE_NATIVE              MediaPlayer,基于原生自带的播放器控件
      * TYPE_NATIVE              MediaPlayer,基于原生自带的播放器控件
+     * TYPE_EXO                 基于谷歌视频播放器
+     * TYPE_RTC                 基于RTC视频播放器
      */
      */
     @Retention(RetentionPolicy.SOURCE)
     @Retention(RetentionPolicy.SOURCE)
-    public @interface IjkPlayerType {
-        int TYPE_IJK = 111;
-        int TYPE_NATIVE = 222;
+    public @interface VideoPlayerType {
+        int TYPE_IJK = 1;
+        int TYPE_NATIVE = 2;
+        int TYPE_EXO = 3;
+        int TYPE_RTC = 4;
     }
     }
 
 
-    @IntDef({IjkPlayerType.TYPE_IJK,IjkPlayerType.TYPE_NATIVE})
+    @IntDef({VideoPlayerType.TYPE_IJK,VideoPlayerType.TYPE_NATIVE,VideoPlayerType.TYPE_EXO,VideoPlayerType.TYPE_RTC})
     @Retention(RetentionPolicy.SOURCE)
     @Retention(RetentionPolicy.SOURCE)
     public @interface PlayerType{}
     public @interface PlayerType{}
 
 
@@ -138,4 +143,32 @@ public final class ConstantKeys {
         int BATTERY_100 = 86;
         int BATTERY_100 = 86;
     }
     }
 
 
+    /**
+     * 播放状态
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PlayerStatesType {
+        //播放完成
+        int COMPLETED = 101;
+        //正在播放
+        int PLAYING = 102;
+        //暂停状态
+        int PAUSE = 103;
+        //用户点击back。当视频退出全屏或者退出小窗口后,再次点击返回键,让用户自己处理返回键事件的逻辑
+        int BACK_CLICK = 104;
+    }
+
+    /**
+     * 播放模式状态
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PlayerPatternType {
+        //切换到全屏播放监听
+        int FULL_SCREEN = 101;
+        //切换到小窗口播放监听
+        int TINY_WINDOW = 102;
+        //切换到正常播放监听
+        int NORMAL = 103;
+    }
+
 }
 }

+ 27 - 14
YCVideoPlayerLib/src/main/java/org/yczbj/ycvideoplayerlib/bean/VideoInfo.java → VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/config/VideoInfoBean.java

@@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 See the License for the specific language governing permissions and
 limitations under the License.
 limitations under the License.
 */
 */
-package org.yczbj.ycvideoplayerlib.bean;
+package org.yczbj.ycvideoplayerlib.config;
 
 
 
 
 import java.io.Serializable;
 import java.io.Serializable;
@@ -28,7 +28,7 @@ import java.util.Map;
  *     revise:
  *     revise:
  * </pre>
  * </pre>
  */
  */
-public class VideoInfo implements Serializable {
+public class VideoInfoBean implements Serializable {
 
 
     /**
     /**
      * 视频的标题
      * 视频的标题
@@ -51,13 +51,26 @@ public class VideoInfo implements Serializable {
      */
      */
     private long length;
     private long length;
     /**
     /**
-     * 异常状态下的文案
+     * 清晰度等级
      */
      */
-    private String errorMsg;
+    private String grade;
     /**
     /**
-     * 播放完成的文案
+     * 270P、480P、720P、1080P、4K ...
      */
      */
-    private String completeMsg;
+    private String p;
+
+    public VideoInfoBean(String title, String cover, String url) {
+        this.title = title;
+        this.videoUrl = url;
+        this.cover = cover;
+    }
+
+    public VideoInfoBean(String title ,String grade, String p, String videoUrl) {
+        this.title = title;
+        this.grade = grade;
+        this.p = p;
+        this.videoUrl = videoUrl;
+    }
 
 
     public String getTitle() {
     public String getTitle() {
         return title;
         return title;
@@ -99,19 +112,19 @@ public class VideoInfo implements Serializable {
         this.length = length;
         this.length = length;
     }
     }
 
 
-    public String getErrorMsg() {
-        return errorMsg;
+    public String getGrade() {
+        return grade;
     }
     }
 
 
-    public void setErrorMsg(String errorMsg) {
-        this.errorMsg = errorMsg;
+    public void setGrade(String grade) {
+        this.grade = grade;
     }
     }
 
 
-    public String getCompleteMsg() {
-        return completeMsg;
+    public String getP() {
+        return p;
     }
     }
 
 
-    public void setCompleteMsg(String completeMsg) {
-        this.completeMsg = completeMsg;
+    public void setP(String p) {
+        this.p = p;
     }
     }
 }
 }

+ 26 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/dev/OnPlayerStatesListener.java

@@ -0,0 +1,26 @@
+package org.yczbj.ycvideoplayerlib.inter.dev;
+
+import org.yczbj.ycvideoplayerlib.config.ConstantKeys;
+
+/**
+ * <pre>
+ *     @author yangchong
+ *     blog  : https://github.com/yangchong211
+ *     time  : 2018/3/9
+ *     desc  : 视频播放状态抽象接口
+ *     revise:
+ * </pre>
+ */
+public interface OnPlayerStatesListener {
+
+    /**
+     * 视频播放状态监听,暴露给外部开发者调用
+     * int COMPLETED = 101; 播放完成
+     * int PLAYING = 102; 正在播放
+     * int PAUSE = 103; 暂停状态
+     * int BACK_CLICK = 104; 用户点击back。当视频退出全屏或者退出小窗口后,再次点击返回键,让用户自己处理返回键事件的逻辑
+     * @param states                            状态
+     */
+    void onPlayerStates(@ConstantKeys.PlayerStatesType int states);
+
+}

+ 9 - 13
YCVideoPlayerLib/src/main/java/org/yczbj/ycvideoplayerlib/inter/listener/OnPlayerTypeListener.java → VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/dev/OnPlayerTypeListener.java

@@ -13,9 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 See the License for the specific language governing permissions and
 limitations under the License.
 limitations under the License.
 */
 */
-package org.yczbj.ycvideoplayerlib.inter.listener;
+package org.yczbj.ycvideoplayerlib.inter.dev;
 
 
 
 
+import org.yczbj.ycvideoplayerlib.config.ConstantKeys;
+
 /**
 /**
  * <pre>
  * <pre>
  *     @author yangchong
  *     @author yangchong
@@ -28,18 +30,12 @@ package org.yczbj.ycvideoplayerlib.inter.listener;
 public interface OnPlayerTypeListener {
 public interface OnPlayerTypeListener {
 
 
     /**
     /**
-     * 切换到全屏播放监听
-     */
-    void onFullScreen();
-
-    /**
-     * 切换到小窗口播放监听
-     */
-    void onTinyWindow();
-
-    /**
-     * 切换到正常播放监听
+     * 视频播放模式监听
+     * int FULL_SCREEN = 101; 切换到全屏播放监听
+     * int TINY_WINDOW = 102; 切换到小窗口播放监听
+     * int NORMAL = 103; 切换到正常播放监听
+     * @param type                              类型
      */
      */
-    void onNormal();
+    void onPlayerPattern(@ConstantKeys.PlayerPatternType int type);
 
 
 }
 }

+ 2 - 2
YCVideoPlayerLib/src/main/java/org/yczbj/ycvideoplayerlib/inter/listener/OnVideoControlListener.java → VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/dev/OnVideoControlListener.java

@@ -13,10 +13,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 See the License for the specific language governing permissions and
 limitations under the License.
 limitations under the License.
 */
 */
-package org.yczbj.ycvideoplayerlib.inter.listener;
+package org.yczbj.ycvideoplayerlib.inter.dev;
 
 
 
 
-import org.yczbj.ycvideoplayerlib.constant.ConstantKeys;
+import org.yczbj.ycvideoplayerlib.config.ConstantKeys;
 
 
 /**
 /**
  * <pre>
  * <pre>

+ 0 - 0
YCVideoPlayerLib/src/main/java/org/yczbj/ycvideoplayerlib/inter/listener/OnClarityChangedListener.java → VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/listener/OnClarityChangedListener.java


+ 0 - 1
YCVideoPlayerLib/src/main/java/org/yczbj/ycvideoplayerlib/inter/listener/OnSurfaceListener.java → VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/listener/OnSurfaceListener.java

@@ -35,5 +35,4 @@ public interface OnSurfaceListener {
 
 
     void surfaceDestroyed(SurfaceHolder holder);
     void surfaceDestroyed(SurfaceHolder holder);
 
 
-
 }
 }

+ 0 - 0
YCVideoPlayerLib/src/main/java/org/yczbj/ycvideoplayerlib/inter/listener/OnTextureListener.java → VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/listener/OnTextureListener.java


+ 0 - 0
YCVideoPlayerLib/src/main/java/org/yczbj/ycvideoplayerlib/inter/player/InterPropertyVideoPlayer.java → VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/player/InterPropertyVideoPlayer.java


+ 0 - 0
YCVideoPlayerLib/src/main/java/org/yczbj/ycvideoplayerlib/inter/player/InterScreenVideoPlayer.java → VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/player/InterScreenVideoPlayer.java


+ 0 - 0
YCVideoPlayerLib/src/main/java/org/yczbj/ycvideoplayerlib/inter/player/InterStateVideoPlayer.java → VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/player/InterStateVideoPlayer.java


+ 3 - 2
YCVideoPlayerLib/src/main/java/org/yczbj/ycvideoplayerlib/inter/player/InterVideoController.java → VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/player/InterVideoController.java

@@ -15,10 +15,11 @@ limitations under the License.
 */
 */
 package org.yczbj.ycvideoplayerlib.inter.player;
 package org.yczbj.ycvideoplayerlib.inter.player;
 
 
-import android.support.annotation.DrawableRes;
+import androidx.annotation.DrawableRes;
+
 import android.widget.ImageView;
 import android.widget.ImageView;
 
 
-import org.yczbj.ycvideoplayerlib.constant.ConstantKeys;
+import org.yczbj.ycvideoplayerlib.config.ConstantKeys;
 
 
 /**
 /**
  * <pre>
  * <pre>

+ 2 - 1
YCVideoPlayerLib/src/main/java/org/yczbj/ycvideoplayerlib/inter/player/VideoControllerView.java → VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/inter/player/VideoControllerView.java

@@ -16,7 +16,8 @@ limitations under the License.
 package org.yczbj.ycvideoplayerlib.inter.player;
 package org.yczbj.ycvideoplayerlib.inter.player;
 
 
 import android.content.Context;
 import android.content.Context;
-import android.support.annotation.NonNull;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import android.widget.FrameLayout;
 import android.widget.FrameLayout;
 
 
 /**
 /**

+ 650 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/controller/BaseVideoController.java

@@ -0,0 +1,650 @@
+package org.yczbj.ycvideoplayerlib.kernel.controller;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.OrientationEventListener;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.widget.FrameLayout;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.yczbj.ycvideoplayerlib.kernel.player.VideoViewManager;
+import org.yczbj.ycvideoplayerlib.kernel.view.VideoView;
+import org.yczbj.ycvideoplayerlib.tool.utils.CutoutUtil;
+import org.yczbj.ycvideoplayerlib.tool.utils.NetworkUtils;
+import org.yczbj.ycvideoplayerlib.tool.utils.PlayerUtils;
+import org.yczbj.ycvideoplayerlib.tool.utils.VideoLogUtils;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 控制器基类
+ * 此类集成各种事件的处理逻辑,包括
+ * 1.播放器状态改变: {@link #handlePlayerStateChanged(int)}
+ * 2.播放状态改变: {@link #handlePlayStateChanged(int)}
+ * 3.控制视图的显示和隐藏: {@link #handleVisibilityChanged(boolean, Animation)}
+ * 4.播放进度改变: {@link #handleSetProgress(int, int)}
+ * 5.锁定状态改变: {@link #handleLockStateChanged(boolean)}
+ * 6.设备方向监听: {@link #onOrientationChanged(int)}
+ */
+public abstract class BaseVideoController extends FrameLayout implements IVideoController,
+        OrientationHelper.OnOrientationChangeListener {
+
+    //播放器包装类,集合了MediaPlayerControl的api和IVideoController的api
+    protected ControlWrapper mControlWrapper;
+
+    @Nullable
+    protected Activity mActivity;
+
+    //控制器是否处于显示状态
+    protected boolean mShowing;
+
+    //是否处于锁定状态
+    protected boolean mIsLocked;
+
+    //播放视图隐藏超时
+    protected int mDefaultTimeout = 4000;
+
+    //是否开启根据屏幕方向进入/退出全屏
+    private boolean mEnableOrientation;
+    //屏幕方向监听辅助类
+    protected OrientationHelper mOrientationHelper;
+
+    //用户设置是否适配刘海屏
+    private boolean mAdaptCutout;
+    //是否有刘海
+    private Boolean mHasCutout;
+    //刘海的高度
+    private int mCutoutHeight;
+
+    //是否开始刷新进度
+    private boolean mIsStartProgress;
+
+    //保存了所有的控制组件
+    protected LinkedHashMap<IControlComponent, Boolean> mControlComponents = new LinkedHashMap<>();
+
+    private Animation mShowAnim;
+    private Animation mHideAnim;
+
+    public BaseVideoController(@NonNull Context context) {
+        this(context, null);
+    }
+
+    public BaseVideoController(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+
+    }
+
+    public BaseVideoController(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        initView(context);
+    }
+
+    protected void initView(Context context) {
+        if (getLayoutId() != 0) {
+            LayoutInflater.from(getContext()).inflate(getLayoutId(), this, true);
+        }
+        mOrientationHelper = new OrientationHelper(context.getApplicationContext());
+        mEnableOrientation = VideoViewManager.getConfig().mEnableOrientation;
+        mAdaptCutout = VideoViewManager.getConfig().mAdaptCutout;
+
+        mShowAnim = new AlphaAnimation(0f, 1f);
+        mShowAnim.setDuration(300);
+        mHideAnim = new AlphaAnimation(1f, 0f);
+        mHideAnim.setDuration(300);
+
+        mActivity = PlayerUtils.scanForActivity(context);
+    }
+
+    /**
+     * 设置控制器布局文件,子类必须实现
+     */
+    protected abstract int getLayoutId();
+
+    /**
+     * 重要:此方法用于将{@link VideoView} 和控制器绑定
+     */
+    @CallSuper
+    public void setMediaPlayer(MediaPlayerControl mediaPlayer) {
+        mControlWrapper = new ControlWrapper(mediaPlayer, this);
+        //绑定ControlComponent和Controller
+        for (Map.Entry<IControlComponent, Boolean> next : mControlComponents.entrySet()) {
+            IControlComponent component = next.getKey();
+            component.attach(mControlWrapper);
+        }
+        //开始监听设备方向
+        mOrientationHelper.setOnOrientationChangeListener(this);
+    }
+
+    /**
+     * 添加控制组件,最后面添加的在最下面,合理组织添加顺序,可让ControlComponent位于不同的层级
+     */
+    public void addControlComponent(IControlComponent... component) {
+        for (IControlComponent item : component) {
+            addControlComponent(item, false);
+        }
+    }
+
+    /**
+     * 添加控制组件,最后面添加的在最下面,合理组织添加顺序,可让ControlComponent位于不同的层级
+     *
+     * @param isPrivate 是否为独有的组件,如果是就不添加到控制器中
+     */
+    public void addControlComponent(IControlComponent component, boolean isPrivate) {
+        mControlComponents.put(component, isPrivate);
+        if (mControlWrapper != null) {
+            component.attach(mControlWrapper);
+        }
+        View view = component.getView();
+        if (view != null && !isPrivate) {
+            addView(view, 0);
+        }
+    }
+
+    /**
+     * 移除控制组件
+     */
+    public void removeControlComponent(IControlComponent component) {
+        removeView(component.getView());
+        mControlComponents.remove(component);
+    }
+
+    public void removeAllControlComponent() {
+        for (Map.Entry<IControlComponent, Boolean> next : mControlComponents.entrySet()) {
+            removeView(next.getKey().getView());
+        }
+        mControlComponents.clear();
+    }
+
+    public void removeAllPrivateComponents() {
+        Iterator<Map.Entry<IControlComponent, Boolean>> it = mControlComponents.entrySet().iterator();
+        while (it.hasNext()) {
+            Map.Entry<IControlComponent, Boolean> next = it.next();
+            if (next.getValue()) {
+                it.remove();
+            }
+        }
+    }
+
+    /**
+     * {@link VideoView}调用此方法向控制器设置播放状态
+     */
+    @CallSuper
+    public void setPlayState(int playState) {
+        handlePlayStateChanged(playState);
+    }
+
+    /**
+     * {@link VideoView}调用此方法向控制器设置播放器状态
+     */
+    @CallSuper
+    public void setPlayerState(final int playerState) {
+        handlePlayerStateChanged(playerState);
+    }
+
+    /**
+     * 设置播放视图自动隐藏超时
+     */
+    public void setDismissTimeout(int timeout) {
+        if (timeout > 0) {
+            mDefaultTimeout = timeout;
+        }
+    }
+
+    /**
+     * 隐藏播放视图
+     */
+    @Override
+    public void hide() {
+        if (mShowing) {
+            stopFadeOut();
+            handleVisibilityChanged(false, mHideAnim);
+            mShowing = false;
+        }
+    }
+
+    /**
+     * 显示播放视图
+     */
+    @Override
+    public void show() {
+        if (!mShowing) {
+            handleVisibilityChanged(true, mShowAnim);
+            startFadeOut();
+            mShowing = true;
+        }
+    }
+
+    @Override
+    public boolean isShowing() {
+        return mShowing;
+    }
+
+    /**
+     * 开始计时
+     */
+    @Override
+    public void startFadeOut() {
+        //重新开始计时
+        stopFadeOut();
+        postDelayed(mFadeOut, mDefaultTimeout);
+    }
+
+    /**
+     * 取消计时
+     */
+    @Override
+    public void stopFadeOut() {
+        removeCallbacks(mFadeOut);
+    }
+
+    /**
+     * 隐藏播放视图Runnable
+     */
+    protected final Runnable mFadeOut = new Runnable() {
+        @Override
+        public void run() {
+            hide();
+        }
+    };
+
+    @Override
+    public void setLocked(boolean locked) {
+        mIsLocked = locked;
+        handleLockStateChanged(locked);
+    }
+
+    @Override
+    public boolean isLocked() {
+        return mIsLocked;
+    }
+
+    /**
+     * 开始刷新进度,注意:需在STATE_PLAYING时调用才会开始刷新进度
+     */
+    @Override
+    public void startProgress() {
+        if (mIsStartProgress) return;
+        post(mShowProgress);
+        mIsStartProgress = true;
+    }
+
+    /**
+     * 停止刷新进度
+     */
+    @Override
+    public void stopProgress() {
+        if (!mIsStartProgress) return;
+        removeCallbacks(mShowProgress);
+        mIsStartProgress = false;
+    }
+
+    /**
+     * 刷新进度Runnable
+     */
+    protected Runnable mShowProgress = new Runnable() {
+        @Override
+        public void run() {
+            int pos = setProgress();
+            if (mControlWrapper.isPlaying()) {
+                postDelayed(this, (long) ((1000  - pos % 1000) / mControlWrapper.getSpeed()));
+            } else {
+                mIsStartProgress = false;
+            }
+        }
+    };
+
+    private int setProgress() {
+        int position = (int) mControlWrapper.getCurrentPosition();
+        int duration = (int) mControlWrapper.getDuration();
+        handleSetProgress(duration, position);
+        return position;
+    }
+
+    /**
+     * 设置是否适配刘海屏
+     */
+    public void setAdaptCutout(boolean adaptCutout) {
+        mAdaptCutout = adaptCutout;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        checkCutout();
+    }
+
+    /**
+     * 检查是否需要适配刘海
+     */
+    private void checkCutout() {
+        if (!mAdaptCutout) return;
+        if (mActivity != null && mHasCutout == null) {
+            mHasCutout = CutoutUtil.allowDisplayToCutout(mActivity);
+            if (mHasCutout) {
+                //竖屏下的状态栏高度可认为是刘海的高度
+                mCutoutHeight = (int) PlayerUtils.getStatusBarHeightPortrait(mActivity);
+            }
+        }
+        VideoLogUtils.d("hasCutout: " + mHasCutout + " cutout height: " + mCutoutHeight);
+    }
+
+    /**
+     * 是否有刘海屏
+     */
+    @Override
+    public boolean hasCutout() {
+        return mHasCutout != null && mHasCutout;
+    }
+
+    /**
+     * 刘海的高度
+     */
+    @Override
+    public int getCutoutHeight() {
+        return mCutoutHeight;
+    }
+
+    /**
+     * 显示移动网络播放提示
+     *
+     * @return 返回显示移动网络播放提示的条件,false:不显示, true显示
+     * 此处默认根据手机网络类型来决定是否显示,开发者可以重写相关逻辑
+     */
+    public boolean showNetWarning() {
+        return NetworkUtils.getNetworkType(getContext()) == NetworkUtils.NETWORK_MOBILE
+                && !VideoViewManager.instance().playOnMobileNetwork();
+    }
+
+    /**
+     * 播放和暂停
+     */
+    protected void togglePlay() {
+        mControlWrapper.togglePlay();
+    }
+
+    /**
+     * 横竖屏切换
+     */
+    protected void toggleFullScreen() {
+        mControlWrapper.toggleFullScreen(mActivity);
+    }
+
+    /**
+     * 子类中请使用此方法来进入全屏
+     *
+     * @return 是否成功进入全屏
+     */
+    protected boolean startFullScreen() {
+        if (mActivity == null || mActivity.isFinishing()) return false;
+        mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+        mControlWrapper.startFullScreen();
+        return true;
+    }
+
+    /**
+     * 子类中请使用此方法来退出全屏
+     *
+     * @return 是否成功退出全屏
+     */
+    protected boolean stopFullScreen() {
+        if (mActivity == null || mActivity.isFinishing()) return false;
+        mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+        mControlWrapper.stopFullScreen();
+        return true;
+    }
+
+    /**
+     * 改变返回键逻辑,用于activity
+     */
+    public boolean onBackPressed() {
+        return false;
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        super.onWindowFocusChanged(hasWindowFocus);
+        if (mControlWrapper.isPlaying()
+                && (mEnableOrientation || mControlWrapper.isFullScreen())) {
+            if (hasWindowFocus) {
+                postDelayed(new Runnable() {
+                    @Override
+                    public void run() {
+                        mOrientationHelper.enable();
+                    }
+                }, 800);
+            } else {
+                mOrientationHelper.disable();
+            }
+        }
+    }
+
+    /**
+     * 是否自动旋转, 默认不自动旋转
+     */
+    public void setEnableOrientation(boolean enableOrientation) {
+        mEnableOrientation = enableOrientation;
+    }
+
+    private int mOrientation = 0;
+
+    @CallSuper
+    @Override
+    public void onOrientationChanged(int orientation) {
+        if (mActivity == null || mActivity.isFinishing()) return;
+
+        //记录用户手机上一次放置的位置
+        int lastOrientation = mOrientation;
+
+        if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
+            //手机平放时,检测不到有效的角度
+            //重置为原始位置 -1
+            mOrientation = -1;
+            return;
+        }
+
+        if (orientation > 350 || orientation < 10) {
+            int o = mActivity.getRequestedOrientation();
+            //手动切换横竖屏
+            if (o == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE && lastOrientation == 0) return;
+            if (mOrientation == 0) return;
+            //0度,用户竖直拿着手机
+            mOrientation = 0;
+            onOrientationPortrait(mActivity);
+        } else if (orientation > 80 && orientation < 100) {
+
+            int o = mActivity.getRequestedOrientation();
+            //手动切换横竖屏
+            if (o == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT && lastOrientation == 90) return;
+            if (mOrientation == 90) return;
+            //90度,用户右侧横屏拿着手机
+            mOrientation = 90;
+            onOrientationReverseLandscape(mActivity);
+        } else if (orientation > 260 && orientation < 280) {
+            int o = mActivity.getRequestedOrientation();
+            //手动切换横竖屏
+            if (o == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT && lastOrientation == 270) return;
+            if (mOrientation == 270) return;
+            //270度,用户左侧横屏拿着手机
+            mOrientation = 270;
+            onOrientationLandscape(mActivity);
+        }
+    }
+
+    /**
+     * 竖屏
+     */
+    protected void onOrientationPortrait(Activity activity) {
+        //屏幕锁定的情况
+        if (mIsLocked) return;
+        //没有开启设备方向监听的情况
+        if (!mEnableOrientation) return;
+
+        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+        mControlWrapper.stopFullScreen();
+    }
+
+    /**
+     * 横屏
+     */
+    protected void onOrientationLandscape(Activity activity) {
+        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+        if (mControlWrapper.isFullScreen()) {
+            handlePlayerStateChanged(VideoView.PLAYER_FULL_SCREEN);
+        } else {
+            mControlWrapper.startFullScreen();
+        }
+    }
+
+    /**
+     * 反向横屏
+     */
+    protected void onOrientationReverseLandscape(Activity activity) {
+        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
+        if (mControlWrapper.isFullScreen()) {
+            handlePlayerStateChanged(VideoView.PLAYER_FULL_SCREEN);
+        } else {
+            mControlWrapper.startFullScreen();
+        }
+    }
+
+    //------------------------ start handle event change ------------------------//
+
+    private void handleVisibilityChanged(boolean isVisible, Animation anim) {
+        if (!mIsLocked) { //没锁住时才向ControlComponent下发此事件
+            for (Map.Entry<IControlComponent, Boolean> next
+                    : mControlComponents.entrySet()) {
+                IControlComponent component = next.getKey();
+                component.onVisibilityChanged(isVisible, anim);
+            }
+        }
+        onVisibilityChanged(isVisible, anim);
+    }
+
+    /**
+     * 子类重写此方法监听控制的显示和隐藏
+     *
+     * @param isVisible 是否可见
+     * @param anim      显示/隐藏动画
+     */
+    protected void onVisibilityChanged(boolean isVisible, Animation anim) {
+
+    }
+
+    private void handlePlayStateChanged(int playState) {
+        for (Map.Entry<IControlComponent, Boolean> next
+                : mControlComponents.entrySet()) {
+            IControlComponent component = next.getKey();
+            component.onPlayStateChanged(playState);
+        }
+        onPlayStateChanged(playState);
+    }
+
+    /**
+     * 子类重写此方法并在其中更新控制器在不同播放状态下的ui
+     */
+    @CallSuper
+    protected void onPlayStateChanged(int playState) {
+        switch (playState) {
+            case VideoView.STATE_IDLE:
+                mOrientationHelper.disable();
+                mOrientation = 0;
+                mIsLocked = false;
+                mShowing = false;
+                removeAllPrivateComponents();
+                break;
+            case VideoView.STATE_PLAYBACK_COMPLETED:
+                mIsLocked = false;
+                mShowing = false;
+                break;
+            case VideoView.STATE_ERROR:
+                mShowing = false;
+                break;
+        }
+    }
+
+    private void handlePlayerStateChanged(int playerState) {
+        for (Map.Entry<IControlComponent, Boolean> next
+                : mControlComponents.entrySet()) {
+            IControlComponent component = next.getKey();
+            component.onPlayerStateChanged(playerState);
+        }
+        onPlayerStateChanged(playerState);
+    }
+
+    /**
+     * 子类重写此方法并在其中更新控制器在不同播放器状态下的ui
+     */
+    @CallSuper
+    protected void onPlayerStateChanged(int playerState) {
+        switch (playerState) {
+            case VideoView.PLAYER_NORMAL:
+                if (mEnableOrientation) {
+                    mOrientationHelper.enable();
+                } else {
+                    mOrientationHelper.disable();
+                }
+                if (hasCutout()) {
+                    CutoutUtil.adaptCutoutAboveAndroidP(getContext(), false);
+                }
+                break;
+            case VideoView.PLAYER_FULL_SCREEN:
+                //在全屏时强制监听设备方向
+                mOrientationHelper.enable();
+                if (hasCutout()) {
+                    CutoutUtil.adaptCutoutAboveAndroidP(getContext(), true);
+                }
+                break;
+            case VideoView.PLAYER_TINY_SCREEN:
+                mOrientationHelper.disable();
+                break;
+        }
+    }
+
+    private void handleSetProgress(int duration, int position) {
+        for (Map.Entry<IControlComponent, Boolean> next
+                : mControlComponents.entrySet()) {
+            IControlComponent component = next.getKey();
+            component.setProgress(duration, position);
+        }
+        setProgress(duration, position);
+    }
+
+    /**
+     * 刷新进度回调,子类可在此方法监听进度刷新,然后更新ui
+     *
+     * @param duration 视频总时长
+     * @param position 视频当前时长
+     */
+    protected void setProgress(int duration, int position) {
+
+    }
+
+    private void handleLockStateChanged(boolean isLocked) {
+        for (Map.Entry<IControlComponent, Boolean> next
+                : mControlComponents.entrySet()) {
+            IControlComponent component = next.getKey();
+            component.onLockStateChanged(isLocked);
+        }
+        onLockStateChanged(isLocked);
+    }
+
+    /**
+     * 子类可重写此方法监听锁定状态发生改变,然后更新ui
+     */
+    protected void onLockStateChanged(boolean isLocked) {
+
+    }
+
+    //------------------------ end handle event change ------------------------//
+}

+ 275 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/controller/ControlWrapper.java

@@ -0,0 +1,275 @@
+package org.yczbj.ycvideoplayerlib.kernel.controller;
+
+import android.app.Activity;
+import android.content.pm.ActivityInfo;
+import android.graphics.Bitmap;
+
+import androidx.annotation.NonNull;
+
+/**
+ * 此类的目的是为了在ControlComponent中既能调用VideoView的api又能调用BaseVideoController的api,
+ * 并对部分api做了封装,方便使用
+ */
+public class ControlWrapper implements MediaPlayerControl, IVideoController {
+    
+    private MediaPlayerControl mPlayerControl;
+    private IVideoController mController;
+    
+    public ControlWrapper(@NonNull MediaPlayerControl playerControl, @NonNull IVideoController controller) {
+        mPlayerControl = playerControl;
+        mController = controller;
+    }
+    
+    @Override
+    public void start() {
+        mPlayerControl.start();
+    }
+
+    @Override
+    public void pause() {
+        mPlayerControl.pause();
+    }
+
+    @Override
+    public long getDuration() {
+        return mPlayerControl.getDuration();
+    }
+
+    @Override
+    public long getCurrentPosition() {
+        return mPlayerControl.getCurrentPosition();
+    }
+
+    @Override
+    public void seekTo(long pos) {
+        mPlayerControl.seekTo(pos);
+    }
+
+    @Override
+    public boolean isPlaying() {
+        return mPlayerControl.isPlaying();
+    }
+
+    @Override
+    public int getBufferedPercentage() {
+        return mPlayerControl.getBufferedPercentage();
+    }
+
+    @Override
+    public void startFullScreen() {
+        mPlayerControl.startFullScreen();
+    }
+
+    @Override
+    public void stopFullScreen() {
+        mPlayerControl.stopFullScreen();
+    }
+
+    @Override
+    public boolean isFullScreen() {
+        return mPlayerControl.isFullScreen();
+    }
+
+    @Override
+    public void setMute(boolean isMute) {
+        mPlayerControl.setMute(isMute);
+    }
+
+    @Override
+    public boolean isMute() {
+        return mPlayerControl.isMute();
+    }
+
+    @Override
+    public void setScreenScaleType(int screenScaleType) {
+        mPlayerControl.setScreenScaleType(screenScaleType);
+    }
+
+    @Override
+    public void setSpeed(float speed) {
+        mPlayerControl.setSpeed(speed);
+    }
+
+    @Override
+    public float getSpeed() {
+        return mPlayerControl.getSpeed();
+    }
+
+    @Override
+    public long getTcpSpeed() {
+        return mPlayerControl.getTcpSpeed();
+    }
+
+    @Override
+    public void replay(boolean resetPosition) {
+        mPlayerControl.replay(resetPosition);
+    }
+
+    @Override
+    public void setMirrorRotation(boolean enable) {
+        mPlayerControl.setMirrorRotation(enable);
+    }
+
+    @Override
+    public Bitmap doScreenShot() {
+        return mPlayerControl.doScreenShot();
+    }
+
+    @Override
+    public int[] getVideoSize() {
+        return mPlayerControl.getVideoSize();
+    }
+
+    @Override
+    public void setRotation(float rotation) {
+        mPlayerControl.setRotation(rotation);
+    }
+
+    @Override
+    public void startTinyScreen() {
+        mPlayerControl.startTinyScreen();
+    }
+
+    @Override
+    public void stopTinyScreen() {
+        mPlayerControl.stopTinyScreen();
+    }
+
+    @Override
+    public boolean isTinyScreen() {
+        return mPlayerControl.isTinyScreen();
+    }
+
+    /**
+     * 播放和暂停
+     */
+    public void togglePlay() {
+        if (isPlaying()) {
+            pause();
+        } else {
+            start();
+        }
+    }
+
+    /**
+     * 横竖屏切换,会旋转屏幕
+     */
+    public void toggleFullScreen(Activity activity) {
+        if (activity == null || activity.isFinishing())
+            return;
+        if (isFullScreen()) {
+            activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+            stopFullScreen();
+        } else {
+            activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+            startFullScreen();
+        }
+    }
+
+    /**
+     * 横竖屏切换,不会旋转屏幕
+     */
+    public void toggleFullScreen() {
+        if (isFullScreen()) {
+            stopFullScreen();
+        } else {
+            startFullScreen();
+        }
+    }
+
+    /**
+     * 横竖屏切换,根据适配宽高决定是否旋转屏幕
+     */
+    public void toggleFullScreenByVideoSize(Activity activity) {
+        if (activity == null || activity.isFinishing())
+            return;
+        int[] size = getVideoSize();
+        int width = size[0];
+        int height = size[1];
+        if (isFullScreen()) {
+            stopFullScreen();
+            if (width > height) {
+               activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+            }
+        } else {
+            startFullScreen();
+            if (width > height) {
+                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+            }
+        }
+    }
+
+    @Override
+    public void startFadeOut() {
+        mController.startFadeOut();
+    }
+
+    @Override
+    public void stopFadeOut() {
+        mController.stopFadeOut();
+    }
+
+    @Override
+    public boolean isShowing() {
+        return mController.isShowing();
+    }
+
+    @Override
+    public void setLocked(boolean locked) {
+        mController.setLocked(locked);
+    }
+
+    @Override
+    public boolean isLocked() {
+        return mController.isLocked();
+    }
+
+    @Override
+    public void startProgress() {
+        mController.startProgress();
+    }
+
+    @Override
+    public void stopProgress() {
+        mController.stopProgress();
+    }
+
+    @Override
+    public void hide() {
+        mController.hide();
+    }
+
+    @Override
+    public void show() {
+        mController.show();
+    }
+
+    @Override
+    public boolean hasCutout() {
+        return mController.hasCutout();
+    }
+
+    @Override
+    public int getCutoutHeight() {
+        return mController.getCutoutHeight();
+    }
+
+    /**
+     * 切换锁定状态
+     */
+    public void toggleLockState() {
+        setLocked(!isLocked());
+    }
+
+
+    /**
+     * 切换显示/隐藏状态
+     */
+    public void toggleShowState() {
+        if (isShowing()) {
+            hide();
+        } else {
+            show();
+        }
+    }
+}

+ 325 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/controller/GestureVideoController.java

@@ -0,0 +1,325 @@
+package org.yczbj.ycvideoplayerlib.kernel.controller;
+
+import android.app.Activity;
+import android.content.Context;
+import android.media.AudioManager;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.yczbj.ycvideoplayerlib.kernel.view.VideoView;
+import org.yczbj.ycvideoplayerlib.tool.utils.PlayerUtils;
+
+import java.util.Map;
+
+/**
+ * 包含手势操作的VideoController
+ */
+public abstract class GestureVideoController extends BaseVideoController implements
+        GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener, View.OnTouchListener {
+
+    private GestureDetector mGestureDetector;
+    private AudioManager mAudioManager;
+    private boolean mIsGestureEnabled = true;
+    private int mStreamVolume;
+    private float mBrightness;
+    private int mSeekPosition;
+    private boolean mFirstTouch;
+    private boolean mChangePosition;
+    private boolean mChangeBrightness;
+    private boolean mChangeVolume;
+
+    private boolean mCanChangePosition = true;
+
+    private boolean mEnableInNormal;
+
+    private boolean mCanSlide;
+
+    private int mCurPlayState;
+
+
+    public GestureVideoController(@NonNull Context context) {
+        super(context);
+    }
+
+    public GestureVideoController(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public GestureVideoController(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    protected void initView(Context context) {
+        super.initView(context);
+        mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
+        mGestureDetector = new GestureDetector(getContext(), this);
+        setOnTouchListener(this);
+    }
+
+    /**
+     * 设置是否可以滑动调节进度,默认可以
+     */
+    public void setCanChangePosition(boolean canChangePosition) {
+        mCanChangePosition = canChangePosition;
+    }
+
+    /**
+     * 是否在竖屏模式下开始手势控制,默认关闭
+     */
+    public void setEnableInNormal(boolean enableInNormal) {
+        mEnableInNormal = enableInNormal;
+    }
+
+    /**
+     * 是否开启手势空控制,默认开启,关闭之后,双击播放暂停以及手势调节进度,音量,亮度功能将关闭
+     */
+    public void setGestureEnabled(boolean gestureEnabled) {
+        mIsGestureEnabled = gestureEnabled;
+    }
+
+    @Override
+    public void setPlayerState(int playerState) {
+        super.setPlayerState(playerState);
+        if (playerState == VideoView.PLAYER_NORMAL) {
+            mCanSlide = mEnableInNormal;
+        } else if (playerState == VideoView.PLAYER_FULL_SCREEN) {
+            mCanSlide = true;
+        }
+    }
+
+    @Override
+    public void setPlayState(int playState) {
+        super.setPlayState(playState);
+        mCurPlayState = playState;
+    }
+
+    private boolean isInPlaybackState() {
+        return mControlWrapper != null
+                && mCurPlayState != VideoView.STATE_ERROR
+                && mCurPlayState != VideoView.STATE_IDLE
+                && mCurPlayState != VideoView.STATE_PREPARING
+                && mCurPlayState != VideoView.STATE_PREPARED
+                && mCurPlayState != VideoView.STATE_START_ABORT
+                && mCurPlayState != VideoView.STATE_PLAYBACK_COMPLETED;
+    }
+
+    @Override
+    public boolean onTouch(View v, MotionEvent event) {
+        return mGestureDetector.onTouchEvent(event);
+    }
+
+    /**
+     * 手指按下的瞬间
+     */
+    @Override
+    public boolean onDown(MotionEvent e) {
+        if (!isInPlaybackState() //不处于播放状态
+                || !mIsGestureEnabled //关闭了手势
+                || PlayerUtils.isEdge(getContext(), e)) //处于屏幕边沿
+            return true;
+        mStreamVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+        Activity activity = PlayerUtils.scanForActivity(getContext());
+        if (activity == null) {
+            mBrightness = 0;
+        } else {
+            mBrightness = activity.getWindow().getAttributes().screenBrightness;
+        }
+        mFirstTouch = true;
+        mChangePosition = false;
+        mChangeBrightness = false;
+        mChangeVolume = false;
+        return true;
+    }
+
+    /**
+     * 单击
+     */
+    @Override
+    public boolean onSingleTapConfirmed(MotionEvent e) {
+        if (isInPlaybackState()) {
+            mControlWrapper.toggleShowState();
+        }
+        return true;
+    }
+
+    /**
+     * 双击
+     */
+    @Override
+    public boolean onDoubleTap(MotionEvent e) {
+        if (!isLocked() && isInPlaybackState()) togglePlay();
+        return true;
+    }
+
+    /**
+     * 在屏幕上滑动
+     */
+    @Override
+    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+        if (!isInPlaybackState() //不处于播放状态
+                || !mIsGestureEnabled //关闭了手势
+                || !mCanSlide //关闭了滑动手势
+                || isLocked() //锁住了屏幕
+                || PlayerUtils.isEdge(getContext(), e1)) //处于屏幕边沿
+            return true;
+        float deltaX = e1.getX() - e2.getX();
+        float deltaY = e1.getY() - e2.getY();
+        if (mFirstTouch) {
+            mChangePosition = Math.abs(distanceX) >= Math.abs(distanceY);
+            if (!mChangePosition) {
+                //半屏宽度
+                int halfScreen = PlayerUtils.getScreenWidth(getContext(), true) / 2;
+                if (e2.getX() > halfScreen) {
+                    mChangeVolume = true;
+                } else {
+                    mChangeBrightness = true;
+                }
+            }
+
+            if (mChangePosition) {
+                //根据用户设置是否可以滑动调节进度来决定最终是否可以滑动调节进度
+                mChangePosition = mCanChangePosition;
+            }
+
+            if (mChangePosition || mChangeBrightness || mChangeVolume) {
+                for (Map.Entry<IControlComponent, Boolean> next : mControlComponents.entrySet()) {
+                    IControlComponent component = next.getKey();
+                    if (component instanceof IGestureComponent) {
+                        ((IGestureComponent) component).onStartSlide();
+                    }
+                }
+            }
+            mFirstTouch = false;
+        }
+        if (mChangePosition) {
+            slideToChangePosition(deltaX);
+        } else if (mChangeBrightness) {
+            slideToChangeBrightness(deltaY);
+        } else if (mChangeVolume) {
+            slideToChangeVolume(deltaY);
+        }
+        return true;
+    }
+
+    protected void slideToChangePosition(float deltaX) {
+        deltaX = -deltaX;
+        int width = getMeasuredWidth();
+        int duration = (int) mControlWrapper.getDuration();
+        int currentPosition = (int) mControlWrapper.getCurrentPosition();
+        int position = (int) (deltaX / width * 120000 + currentPosition);
+        if (position > duration) position = duration;
+        if (position < 0) position = 0;
+        for (Map.Entry<IControlComponent, Boolean> next : mControlComponents.entrySet()) {
+            IControlComponent component = next.getKey();
+            if (component instanceof IGestureComponent) {
+                ((IGestureComponent) component).onPositionChange(position, currentPosition, duration);
+            }
+        }
+        mSeekPosition = position;
+    }
+
+    protected void slideToChangeBrightness(float deltaY) {
+        Activity activity = PlayerUtils.scanForActivity(getContext());
+        if (activity == null) return;
+        Window window = activity.getWindow();
+        WindowManager.LayoutParams attributes = window.getAttributes();
+        int height = getMeasuredHeight();
+        if (mBrightness == -1.0f) mBrightness = 0.5f;
+        float brightness = deltaY * 2 / height * 1.0f + mBrightness;
+        if (brightness < 0) {
+            brightness = 0f;
+        }
+        if (brightness > 1.0f) brightness = 1.0f;
+        int percent = (int) (brightness * 100);
+        attributes.screenBrightness = brightness;
+        window.setAttributes(attributes);
+        for (Map.Entry<IControlComponent, Boolean> next : mControlComponents.entrySet()) {
+            IControlComponent component = next.getKey();
+            if (component instanceof IGestureComponent) {
+                ((IGestureComponent) component).onBrightnessChange(percent);
+            }
+        }
+    }
+
+    protected void slideToChangeVolume(float deltaY) {
+        int streamMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+        int height = getMeasuredHeight();
+        float deltaV = deltaY * 2 / height * streamMaxVolume;
+        float index = mStreamVolume + deltaV;
+        if (index > streamMaxVolume) index = streamMaxVolume;
+        if (index < 0) index = 0;
+        int percent = (int) (index / streamMaxVolume * 100);
+        mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, (int) index, 0);
+        for (Map.Entry<IControlComponent, Boolean> next : mControlComponents.entrySet()) {
+            IControlComponent component = next.getKey();
+            if (component instanceof IGestureComponent) {
+                ((IGestureComponent) component).onVolumeChange(percent);
+            }
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        //滑动结束时事件处理
+        if (!mGestureDetector.onTouchEvent(event)) {
+            int action = event.getAction();
+            switch (action) {
+                case MotionEvent.ACTION_UP:
+                    stopSlide();
+                    if (mSeekPosition > 0) {
+                        mControlWrapper.seekTo(mSeekPosition);
+                        mSeekPosition = 0;
+                    }
+                    break;
+                case MotionEvent.ACTION_CANCEL:
+                    stopSlide();
+                    mSeekPosition = 0;
+                    break;
+            }
+        }
+        return super.onTouchEvent(event);
+    }
+
+    private void stopSlide() {
+        for (Map.Entry<IControlComponent, Boolean> next : mControlComponents.entrySet()) {
+            IControlComponent component = next.getKey();
+            if (component instanceof IGestureComponent) {
+                ((IGestureComponent) component).onStopSlide();
+            }
+        }
+    }
+
+    @Override
+    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+        return false;
+    }
+
+    @Override
+    public void onLongPress(MotionEvent e) {
+
+    }
+
+    @Override
+    public void onShowPress(MotionEvent e) {
+
+    }
+
+    @Override
+    public boolean onDoubleTapEvent(MotionEvent e) {
+        return false;
+    }
+
+
+    @Override
+    public boolean onSingleTapUp(MotionEvent e) {
+        return false;
+    }
+}

+ 24 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/controller/IControlComponent.java

@@ -0,0 +1,24 @@
+package org.yczbj.ycvideoplayerlib.kernel.controller;
+
+import android.view.View;
+import android.view.animation.Animation;
+
+import androidx.annotation.NonNull;
+
+public interface IControlComponent {
+
+    void attach(@NonNull ControlWrapper controlWrapper);
+
+    View getView();
+
+    void onVisibilityChanged(boolean isVisible, Animation anim);
+
+    void onPlayStateChanged(int playState);
+
+    void onPlayerStateChanged(int playerState);
+
+    void setProgress(int duration, int position);
+
+    void onLockStateChanged(boolean isLocked);
+
+}

+ 33 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/controller/IGestureComponent.java

@@ -0,0 +1,33 @@
+package org.yczbj.ycvideoplayerlib.kernel.controller;
+
+public interface IGestureComponent extends IControlComponent {
+    /**
+     * 开始滑动
+     */
+    void onStartSlide();
+
+    /**
+     * 结束滑动
+     */
+    void onStopSlide();
+
+    /**
+     * 滑动调整进度
+     * @param slidePosition 滑动进度
+     * @param currentPosition 当前播放进度
+     * @param duration 视频总长度
+     */
+    void onPositionChange(int slidePosition, int currentPosition, int duration);
+
+    /**
+     * 滑动调整亮度
+     * @param percent 亮度百分比
+     */
+    void onBrightnessChange(int percent);
+
+    /**
+     * 滑动调整音量
+     * @param percent 音量百分比
+     */
+    void onVolumeChange(int percent);
+}

+ 60 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/controller/IVideoController.java

@@ -0,0 +1,60 @@
+package org.yczbj.ycvideoplayerlib.kernel.controller;
+
+public interface IVideoController {
+
+    /**
+     * 开始控制视图自动隐藏倒计时
+     */
+    void startFadeOut();
+
+    /**
+     * 取消控制视图自动隐藏倒计时
+     */
+    void stopFadeOut();
+
+    /**
+     * 控制视图是否处于显示状态
+     */
+    boolean isShowing();
+
+    /**
+     * 设置锁定状态
+     * @param locked 是否锁定
+     */
+    void setLocked(boolean locked);
+
+    /**
+     * 是否处于锁定状态
+     */
+    boolean isLocked();
+
+    /**
+     * 开始刷新进度
+     */
+    void startProgress();
+
+    /**
+     * 停止刷新进度
+     */
+    void stopProgress();
+
+    /**
+     * 显示控制视图
+     */
+    void hide();
+
+    /**
+     * 隐藏控制视图
+     */
+    void show();
+
+    /**
+     * 是否需要适配刘海
+     */
+    boolean hasCutout();
+
+    /**
+     * 获取刘海的高度
+     */
+    int getCutoutHeight();
+}

+ 54 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/controller/MediaPlayerControl.java

@@ -0,0 +1,54 @@
+package org.yczbj.ycvideoplayerlib.kernel.controller;
+
+import android.graphics.Bitmap;
+
+public interface MediaPlayerControl {
+
+    void start();
+
+    void pause();
+
+    long getDuration();
+
+    long getCurrentPosition();
+
+    void seekTo(long pos);
+
+    boolean isPlaying();
+
+    int getBufferedPercentage();
+
+    void startFullScreen();
+
+    void stopFullScreen();
+
+    boolean isFullScreen();
+
+    void setMute(boolean isMute);
+
+    boolean isMute();
+
+    void setScreenScaleType(int screenScaleType);
+
+    void setSpeed(float speed);
+
+    float getSpeed();
+
+    long getTcpSpeed();
+
+    void replay(boolean resetPosition);
+
+    void setMirrorRotation(boolean enable);
+
+    Bitmap doScreenShot();
+
+    int[] getVideoSize();
+
+    void setRotation(float rotation);
+
+    void startTinyScreen();
+
+    void stopTinyScreen();
+
+    boolean isTinyScreen();
+}

+ 37 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/controller/OrientationHelper.java

@@ -0,0 +1,37 @@
+package org.yczbj.ycvideoplayerlib.kernel.controller;
+
+import android.content.Context;
+import android.view.OrientationEventListener;
+
+/**
+ * 设备方向监听
+ */
+public class OrientationHelper extends OrientationEventListener {
+
+    private long mLastTime;
+
+    private OnOrientationChangeListener mOnOrientationChangeListener;
+
+    public OrientationHelper(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void onOrientationChanged(int orientation) {
+        long currentTime = System.currentTimeMillis();
+        if (currentTime - mLastTime < 300) return;//300毫秒检测一次
+        if (mOnOrientationChangeListener != null) {
+            mOnOrientationChangeListener.onOrientationChanged(orientation);
+        }
+        mLastTime = currentTime;
+    }
+
+
+    public interface OnOrientationChangeListener {
+        void onOrientationChanged(int orientation);
+    }
+
+    public void setOnOrientationChangeListener(OnOrientationChangeListener onOrientationChangeListener) {
+        mOnOrientationChangeListener = onOrientationChangeListener;
+    }
+}

+ 116 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/helper/AudioFocusHelper.java

@@ -0,0 +1,116 @@
+package org.yczbj.ycvideoplayerlib.kernel.helper;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.annotation.NonNull;
+
+import org.yczbj.ycvideoplayerlib.kernel.view.VideoView;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * 音频焦点改变监听
+ */
+public final class AudioFocusHelper implements AudioManager.OnAudioFocusChangeListener {
+
+    private Handler mHandler = new Handler(Looper.getMainLooper());
+
+    private WeakReference<VideoView> mWeakVideoView;
+
+    private AudioManager mAudioManager;
+
+    private boolean mStartRequested = false;
+    private boolean mPausedForLoss = false;
+    private int mCurrentFocus = 0;
+
+    public AudioFocusHelper(@NonNull VideoView videoView) {
+        mWeakVideoView = new WeakReference<>(videoView);
+        mAudioManager = (AudioManager) videoView.getContext().getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
+    }
+
+    @Override
+    public void onAudioFocusChange(final int focusChange) {
+        if (mCurrentFocus == focusChange) {
+            return;
+        }
+
+        //由于onAudioFocusChange有可能在子线程调用,
+        //故通过此方式切换到主线程去执行
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                handleAudioFocusChange(focusChange);
+            }
+        });
+
+        mCurrentFocus = focusChange;
+    }
+
+    private void handleAudioFocusChange(int focusChange) {
+        final VideoView videoView = mWeakVideoView.get();
+        if (videoView == null) {
+            return;
+        }
+        switch (focusChange) {
+            case AudioManager.AUDIOFOCUS_GAIN://获得焦点
+            case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT://暂时获得焦点
+                if (mStartRequested || mPausedForLoss) {
+                    videoView.start();
+                    mStartRequested = false;
+                    mPausedForLoss = false;
+                }
+                if (!videoView.isMute())//恢复音量
+                    videoView.setVolume(1.0f, 1.0f);
+                break;
+            case AudioManager.AUDIOFOCUS_LOSS://焦点丢失
+            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT://焦点暂时丢失
+                if (videoView.isPlaying()) {
+                    mPausedForLoss = true;
+                    videoView.pause();
+                }
+                break;
+            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK://此时需降低音量
+                if (videoView.isPlaying() && !videoView.isMute()) {
+                    videoView.setVolume(0.1f, 0.1f);
+                }
+                break;
+        }
+    }
+
+    /**
+     * Requests to obtain the audio focus
+     */
+    public void requestFocus() {
+        if (mCurrentFocus == AudioManager.AUDIOFOCUS_GAIN) {
+            return;
+        }
+
+        if (mAudioManager == null) {
+            return;
+        }
+
+        int status = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+        if (AudioManager.AUDIOFOCUS_REQUEST_GRANTED == status) {
+            mCurrentFocus = AudioManager.AUDIOFOCUS_GAIN;
+            return;
+        }
+
+        mStartRequested = true;
+    }
+
+    /**
+     * Requests the system to drop the audio focus
+     */
+    public void abandonFocus() {
+
+        if (mAudioManager == null) {
+            return;
+        }
+
+        mStartRequested = false;
+        mAudioManager.abandonAudioFocus(this);
+    }
+}

+ 328 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/impl/exo/ExoMediaPlayer.java

@@ -0,0 +1,328 @@
+package org.yczbj.ycvideoplayerlib.kernel.impl.exo;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.os.Handler;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+
+import com.google.android.exoplayer2.DefaultLoadControl;
+import com.google.android.exoplayer2.DefaultRenderersFactory;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.LoadControl;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.RenderersFactory;
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.analytics.AnalyticsCollector;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSourceEventListener;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
+import com.google.android.exoplayer2.util.Clock;
+import com.google.android.exoplayer2.util.EventLogger;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.VideoListener;
+import org.yczbj.ycvideoplayerlib.kernel.inter.AbstractPlayer;
+import org.yczbj.ycvideoplayerlib.kernel.player.VideoViewManager;
+
+import java.util.Map;
+
+
+public class ExoMediaPlayer extends AbstractPlayer implements VideoListener, Player.EventListener {
+
+    protected Context mAppContext;
+    protected SimpleExoPlayer mInternalPlayer;
+    protected MediaSource mMediaSource;
+    protected ExoMediaSourceHelper mMediaSourceHelper;
+
+    private PlaybackParameters mSpeedPlaybackParameters;
+
+    private int mLastReportedPlaybackState = Player.STATE_IDLE;
+    private boolean mLastReportedPlayWhenReady = false;
+    private boolean mIsPreparing;
+    private boolean mIsBuffering;
+
+    private LoadControl mLoadControl;
+    private RenderersFactory mRenderersFactory;
+    private TrackSelector mTrackSelector;
+
+    public ExoMediaPlayer(Context context) {
+        mAppContext = context.getApplicationContext();
+        mMediaSourceHelper = ExoMediaSourceHelper.getInstance(context);
+    }
+
+    @Override
+    public void initPlayer() {
+        mInternalPlayer = new SimpleExoPlayer.Builder(
+                mAppContext,
+                mRenderersFactory == null ? mRenderersFactory = new DefaultRenderersFactory(mAppContext) : mRenderersFactory,
+                mTrackSelector == null ? mTrackSelector = new DefaultTrackSelector(mAppContext) : mTrackSelector,
+                mLoadControl == null ? mLoadControl = new DefaultLoadControl() : mLoadControl,
+                DefaultBandwidthMeter.getSingletonInstance(mAppContext),
+                Util.getLooper(),
+                new AnalyticsCollector(Clock.DEFAULT),
+                /* useLazyPreparation= */ true,
+                Clock.DEFAULT)
+                .build();
+        setOptions();
+
+        //播放器日志
+        if (VideoViewManager.getConfig().mIsEnableLog && mTrackSelector instanceof MappingTrackSelector) {
+            mInternalPlayer.addAnalyticsListener(new EventLogger((MappingTrackSelector) mTrackSelector, "ExoPlayer"));
+        }
+
+        mInternalPlayer.addListener(this);
+        mInternalPlayer.addVideoListener(this);
+    }
+
+    public void setTrackSelector(TrackSelector trackSelector) {
+        mTrackSelector = trackSelector;
+    }
+
+    public void setRenderersFactory(RenderersFactory renderersFactory) {
+        mRenderersFactory = renderersFactory;
+    }
+
+    public void setLoadControl(LoadControl loadControl) {
+        mLoadControl = loadControl;
+    }
+
+    @Override
+    public void setDataSource(String path, Map<String, String> headers) {
+        mMediaSource = mMediaSourceHelper.getMediaSource(path, headers);
+    }
+
+    @Override
+    public void setDataSource(AssetFileDescriptor fd) {
+        //no support
+    }
+
+    @Override
+    public void start() {
+        if (mInternalPlayer == null)
+            return;
+        mInternalPlayer.setPlayWhenReady(true);
+    }
+
+    @Override
+    public void pause() {
+        if (mInternalPlayer == null)
+            return;
+        mInternalPlayer.setPlayWhenReady(false);
+    }
+
+    @Override
+    public void stop() {
+        if (mInternalPlayer == null)
+            return;
+        mInternalPlayer.stop();
+    }
+
+    @Override
+    public void prepareAsync() {
+        if (mInternalPlayer == null)
+            return;
+        if (mMediaSource == null) return;
+        if (mSpeedPlaybackParameters != null) {
+            mInternalPlayer.setPlaybackParameters(mSpeedPlaybackParameters);
+        }
+        mIsPreparing = true;
+        mMediaSource.addEventListener(new Handler(), mMediaSourceEventListener);
+        mInternalPlayer.prepare(mMediaSource);
+    }
+
+    private MediaSourceEventListener mMediaSourceEventListener = new MediaSourceEventListener() {
+        @Override
+        public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
+            if (mPlayerEventListener != null && mIsPreparing) {
+                mPlayerEventListener.onPrepared();
+            }
+        }
+    };
+
+    @Override
+    public void reset() {
+        if (mInternalPlayer != null) {
+            mInternalPlayer.stop(true);
+            mInternalPlayer.setVideoSurface(null);
+            mIsPreparing = false;
+            mIsBuffering = false;
+            mLastReportedPlaybackState = Player.STATE_IDLE;
+            mLastReportedPlayWhenReady = false;
+        }
+    }
+
+    @Override
+    public boolean isPlaying() {
+        if (mInternalPlayer == null)
+            return false;
+        int state = mInternalPlayer.getPlaybackState();
+        switch (state) {
+            case Player.STATE_BUFFERING:
+            case Player.STATE_READY:
+                return mInternalPlayer.getPlayWhenReady();
+            case Player.STATE_IDLE:
+            case Player.STATE_ENDED:
+            default:
+                return false;
+        }
+    }
+
+    @Override
+    public void seekTo(long time) {
+        if (mInternalPlayer == null)
+            return;
+        mInternalPlayer.seekTo(time);
+    }
+
+    @Override
+    public void release() {
+        if (mInternalPlayer != null) {
+            mInternalPlayer.removeListener(this);
+            mInternalPlayer.removeVideoListener(this);
+            final SimpleExoPlayer player = mInternalPlayer;
+            mInternalPlayer = null;
+            new Thread() {
+                @Override
+                public void run() {
+                    //异步释放,防止卡顿
+                    player.release();
+                }
+            }.start();
+        }
+
+        mIsPreparing = false;
+        mIsBuffering = false;
+        mLastReportedPlaybackState = Player.STATE_IDLE;
+        mLastReportedPlayWhenReady = false;
+        mSpeedPlaybackParameters = null;
+    }
+
+    @Override
+    public long getCurrentPosition() {
+        if (mInternalPlayer == null)
+            return 0;
+        return mInternalPlayer.getCurrentPosition();
+    }
+
+    @Override
+    public long getDuration() {
+        if (mInternalPlayer == null)
+            return 0;
+        return mInternalPlayer.getDuration();
+    }
+
+    @Override
+    public int getBufferedPercentage() {
+        return mInternalPlayer == null ? 0 : mInternalPlayer.getBufferedPercentage();
+    }
+
+    @Override
+    public void setSurface(Surface surface) {
+        if (mInternalPlayer != null) {
+            mInternalPlayer.setVideoSurface(surface);
+        }
+    }
+
+    @Override
+    public void setDisplay(SurfaceHolder holder) {
+        if (holder == null)
+            setSurface(null);
+        else
+            setSurface(holder.getSurface());
+    }
+
+    @Override
+    public void setVolume(float leftVolume, float rightVolume) {
+        if (mInternalPlayer != null)
+            mInternalPlayer.setVolume((leftVolume + rightVolume) / 2);
+    }
+
+    @Override
+    public void setLooping(boolean isLooping) {
+        if (mInternalPlayer != null)
+            mInternalPlayer.setRepeatMode(isLooping ? Player.REPEAT_MODE_ALL : Player.REPEAT_MODE_OFF);
+    }
+
+    @Override
+    public void setOptions() {
+        //准备好就开始播放
+        mInternalPlayer.setPlayWhenReady(true);
+    }
+
+    @Override
+    public void setSpeed(float speed) {
+        PlaybackParameters playbackParameters = new PlaybackParameters(speed);
+        mSpeedPlaybackParameters = playbackParameters;
+        if (mInternalPlayer != null) {
+            mInternalPlayer.setPlaybackParameters(playbackParameters);
+        }
+    }
+
+    @Override
+    public float getSpeed() {
+        if (mSpeedPlaybackParameters != null) {
+            return mSpeedPlaybackParameters.speed;
+        }
+        return 1f;
+    }
+
+    @Override
+    public long getTcpSpeed() {
+        // no support
+        return 0;
+    }
+
+    @Override
+    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+        if (mPlayerEventListener == null) return;
+        if (mIsPreparing) return;
+        if (mLastReportedPlayWhenReady != playWhenReady || mLastReportedPlaybackState != playbackState) {
+            switch (playbackState) {
+                case Player.STATE_BUFFERING:
+                    mPlayerEventListener.onInfo(MEDIA_INFO_BUFFERING_START, getBufferedPercentage());
+                    mIsBuffering = true;
+                    break;
+                case Player.STATE_READY:
+                    if (mIsBuffering) {
+                        mPlayerEventListener.onInfo(MEDIA_INFO_BUFFERING_END, getBufferedPercentage());
+                        mIsBuffering = false;
+                    }
+                    break;
+                case Player.STATE_ENDED:
+                    mPlayerEventListener.onCompletion();
+                    break;
+            }
+            mLastReportedPlaybackState = playbackState;
+            mLastReportedPlayWhenReady = playWhenReady;
+        }
+    }
+
+    @Override
+    public void onPlayerError(ExoPlaybackException error) {
+        if (mPlayerEventListener != null) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
+        if (mPlayerEventListener != null) {
+            mPlayerEventListener.onVideoSizeChanged(width, height);
+            if (unappliedRotationDegrees > 0) {
+                mPlayerEventListener.onInfo(MEDIA_INFO_VIDEO_ROTATION_CHANGED, unappliedRotationDegrees);
+            }
+        }
+    }
+
+    @Override
+    public void onRenderedFirstFrame() {
+        if (mPlayerEventListener != null && mIsPreparing) {
+            mPlayerEventListener.onInfo(MEDIA_INFO_VIDEO_RENDERING_START, 0);
+            mIsPreparing = false;
+        }
+    }
+}

+ 18 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/impl/exo/ExoMediaPlayerFactory.java

@@ -0,0 +1,18 @@
+package org.yczbj.ycvideoplayerlib.kernel.impl.exo;
+
+import android.content.Context;
+
+import org.yczbj.ycvideoplayerlib.kernel.player.PlayerFactory;
+
+
+public class ExoMediaPlayerFactory extends PlayerFactory<ExoMediaPlayer> {
+
+    public static ExoMediaPlayerFactory create() {
+        return new ExoMediaPlayerFactory();
+    }
+
+    @Override
+    public ExoMediaPlayer createPlayer(Context context) {
+        return new ExoMediaPlayer(context);
+    }
+}

+ 180 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/impl/exo/ExoMediaSourceHelper.java

@@ -0,0 +1,180 @@
+package org.yczbj.ycvideoplayerlib.kernel.impl.exo;
+
+import android.content.Context;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.database.ExoDatabaseProvider;
+import com.google.android.exoplayer2.ext.rtmp.RtmpDataSourceFactory;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
+import com.google.android.exoplayer2.source.dash.DashMediaSource;
+import com.google.android.exoplayer2.source.hls.HlsMediaSource;
+import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
+import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
+import com.google.android.exoplayer2.upstream.cache.Cache;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
+import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
+import com.google.android.exoplayer2.upstream.cache.SimpleCache;
+import com.google.android.exoplayer2.util.Util;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.Map;
+
+public final class ExoMediaSourceHelper {
+
+    private static ExoMediaSourceHelper sInstance;
+
+    private final String mUserAgent;
+    private Context mAppContext;
+    private HttpDataSource.Factory mHttpDataSourceFactory;
+    private Cache mCache;
+
+    private ExoMediaSourceHelper(Context context) {
+        mAppContext = context.getApplicationContext();
+        mUserAgent = Util.getUserAgent(mAppContext, mAppContext.getApplicationInfo().name);
+    }
+
+    public static ExoMediaSourceHelper getInstance(Context context) {
+        if (sInstance == null) {
+            synchronized (ExoMediaSourceHelper.class) {
+                if (sInstance == null) {
+                    sInstance = new ExoMediaSourceHelper(context);
+                }
+            }
+        }
+        return sInstance;
+    }
+
+    public MediaSource getMediaSource(String uri) {
+        return getMediaSource(uri, null, false);
+    }
+
+    public MediaSource getMediaSource(String uri, Map<String, String> headers) {
+        return getMediaSource(uri, headers, false);
+    }
+
+    public MediaSource getMediaSource(String uri, boolean isCache) {
+        return getMediaSource(uri, null, isCache);
+    }
+
+    public MediaSource getMediaSource(String uri, Map<String, String> headers, boolean isCache) {
+        Uri contentUri = Uri.parse(uri);
+        if ("rtmp".equals(contentUri.getScheme())) {
+            return new ProgressiveMediaSource.Factory(new RtmpDataSourceFactory(null))
+                    .createMediaSource(contentUri);
+        }
+        int contentType = inferContentType(uri);
+        DataSource.Factory factory;
+        if (isCache) {
+            factory = getCacheDataSourceFactory();
+        } else {
+            factory = getDataSourceFactory();
+        }
+        if (mHttpDataSourceFactory != null) {
+            setHeaders(headers);
+        }
+        switch (contentType) {
+            case C.TYPE_DASH:
+                return new DashMediaSource.Factory(factory).createMediaSource(contentUri);
+            case C.TYPE_SS:
+                return new SsMediaSource.Factory(factory).createMediaSource(contentUri);
+            case C.TYPE_HLS:
+                return new HlsMediaSource.Factory(factory).createMediaSource(contentUri);
+            default:
+            case C.TYPE_OTHER:
+                return new ProgressiveMediaSource.Factory(factory).createMediaSource(contentUri);
+        }
+    }
+
+    private int inferContentType(String fileName) {
+        fileName = Util.toLowerInvariant(fileName);
+        if (fileName.contains(".mpd")) {
+            return C.TYPE_DASH;
+        } else if (fileName.contains(".m3u8")) {
+            return C.TYPE_HLS;
+        } else if (fileName.matches(".*\\.ism(l)?(/manifest(\\(.+\\))?)?")) {
+            return C.TYPE_SS;
+        } else {
+            return C.TYPE_OTHER;
+        }
+    }
+
+    private DataSource.Factory getCacheDataSourceFactory() {
+        if (mCache == null) {
+            mCache = newCache();
+        }
+        return new CacheDataSourceFactory(
+                mCache,
+                getDataSourceFactory(),
+                CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);
+    }
+
+    private Cache newCache() {
+        return new SimpleCache(
+                new File(mAppContext.getExternalCacheDir(), "exo-video-cache"),//缓存目录
+                new LeastRecentlyUsedCacheEvictor(512 * 1024 * 1024),//缓存大小,默认512M,使用LRU算法实现
+                new ExoDatabaseProvider(mAppContext));
+    }
+
+    /**
+     * Returns a new DataSource factory.
+     *
+     * @return A new DataSource factory.
+     */
+    private DataSource.Factory getDataSourceFactory() {
+        return new DefaultDataSourceFactory(mAppContext, getHttpDataSourceFactory());
+    }
+
+    /**
+     * Returns a new HttpDataSource factory.
+     *
+     * @return A new HttpDataSource factory.
+     */
+    private DataSource.Factory getHttpDataSourceFactory() {
+        if (mHttpDataSourceFactory == null) {
+            mHttpDataSourceFactory = new DefaultHttpDataSourceFactory(
+                    mUserAgent,
+                    null,
+                    DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
+                    DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
+                    //http->https重定向支持
+                    true);
+        }
+        return mHttpDataSourceFactory;
+    }
+
+    private void setHeaders(Map<String, String> headers) {
+        if (headers != null && headers.size() > 0) {
+            for (Map.Entry<String, String> header : headers.entrySet()) {
+                String key = header.getKey();
+                String value = header.getValue();
+                //如果发现用户通过header传递了UA,则强行将HttpDataSourceFactory里面的userAgent字段替换成用户的
+                if (TextUtils.equals(key, "User-Agent")) {
+                    if (!TextUtils.isEmpty(value)) {
+                        try {
+                            Field userAgentField = mHttpDataSourceFactory.getClass().getDeclaredField("userAgent");
+                            userAgentField.setAccessible(true);
+                            userAgentField.set(mHttpDataSourceFactory, value);
+                        } catch (Exception e) {
+                            //ignore
+                        }
+                    }
+                } else {
+                    mHttpDataSourceFactory.getDefaultRequestProperties().set(key, value);
+                }
+            }
+        }
+    }
+
+    public void setCache(Cache cache) {
+        this.mCache = cache;
+    }
+}

+ 264 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/impl/ijk/IjkPlayer.java

@@ -0,0 +1,264 @@
+package org.yczbj.ycvideoplayerlib.kernel.impl.ijk;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+
+
+import org.yczbj.ycvideoplayerlib.kernel.inter.AbstractPlayer;
+import org.yczbj.ycvideoplayerlib.kernel.player.VideoViewManager;
+
+import java.util.Map;
+
+import tv.danmaku.ijk.media.player.IMediaPlayer;
+import tv.danmaku.ijk.media.player.IjkMediaPlayer;
+
+public class IjkPlayer extends AbstractPlayer {
+
+    protected IjkMediaPlayer mMediaPlayer;
+    private int mBufferedPercent;
+    private Context mAppContext;
+
+    public IjkPlayer(Context context) {
+        mAppContext = context;
+    }
+
+    @Override
+    public void initPlayer() {
+        mMediaPlayer = new IjkMediaPlayer();
+        //native日志
+        IjkMediaPlayer.native_setLogLevel(VideoViewManager.getConfig().mIsEnableLog ? IjkMediaPlayer.IJK_LOG_INFO : IjkMediaPlayer.IJK_LOG_SILENT);
+        setOptions();
+        mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+        mMediaPlayer.setOnErrorListener(onErrorListener);
+        mMediaPlayer.setOnCompletionListener(onCompletionListener);
+        mMediaPlayer.setOnInfoListener(onInfoListener);
+        mMediaPlayer.setOnBufferingUpdateListener(onBufferingUpdateListener);
+        mMediaPlayer.setOnPreparedListener(onPreparedListener);
+        mMediaPlayer.setOnVideoSizeChangedListener(onVideoSizeChangedListener);
+        mMediaPlayer.setOnNativeInvokeListener(new IjkMediaPlayer.OnNativeInvokeListener() {
+            @Override
+            public boolean onNativeInvoke(int i, Bundle bundle) {
+                return true;
+            }
+        });
+    }
+
+
+    @Override
+    public void setOptions() {
+    }
+
+    @Override
+    public void setDataSource(String path, Map<String, String> headers) {
+        try {
+            Uri uri = Uri.parse(path);
+            if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())) {
+                RawDataSourceProvider rawDataSourceProvider = RawDataSourceProvider.create(mAppContext, uri);
+                mMediaPlayer.setDataSource(rawDataSourceProvider);
+            } else {
+                //处理UA问题
+                if (headers != null) {
+                    String userAgent = headers.get("User-Agent");
+                    if (!TextUtils.isEmpty(userAgent)) {
+                        mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "user_agent", userAgent);
+                    }
+                }
+                mMediaPlayer.setDataSource(mAppContext, uri, headers);
+            }
+        } catch (Exception e) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void setDataSource(AssetFileDescriptor fd) {
+        try {
+            mMediaPlayer.setDataSource(new RawDataSourceProvider(fd));
+        } catch (Exception e) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void pause() {
+        try {
+            mMediaPlayer.pause();
+        } catch (IllegalStateException e) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void start() {
+        try {
+            mMediaPlayer.start();
+        } catch (IllegalStateException e) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void stop() {
+        try {
+            mMediaPlayer.stop();
+        } catch (IllegalStateException e) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void prepareAsync() {
+        try {
+            mMediaPlayer.prepareAsync();
+        } catch (IllegalStateException e) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void reset() {
+        mMediaPlayer.reset();
+        mMediaPlayer.setOnVideoSizeChangedListener(onVideoSizeChangedListener);
+        setOptions();
+    }
+
+    @Override
+    public boolean isPlaying() {
+        return mMediaPlayer.isPlaying();
+    }
+
+    @Override
+    public void seekTo(long time) {
+        try {
+            mMediaPlayer.seekTo((int) time);
+        } catch (IllegalStateException e) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void release() {
+        mMediaPlayer.setOnErrorListener(null);
+        mMediaPlayer.setOnCompletionListener(null);
+        mMediaPlayer.setOnInfoListener(null);
+        mMediaPlayer.setOnBufferingUpdateListener(null);
+        mMediaPlayer.setOnPreparedListener(null);
+        mMediaPlayer.setOnVideoSizeChangedListener(null);
+        new Thread() {
+            @Override
+            public void run() {
+                try {
+                    mMediaPlayer.release();
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        }.start();
+    }
+
+    @Override
+    public long getCurrentPosition() {
+        return mMediaPlayer.getCurrentPosition();
+    }
+
+    @Override
+    public long getDuration() {
+        return mMediaPlayer.getDuration();
+    }
+
+    @Override
+    public int getBufferedPercentage() {
+        return mBufferedPercent;
+    }
+
+    @Override
+    public void setSurface(Surface surface) {
+        mMediaPlayer.setSurface(surface);
+    }
+
+    @Override
+    public void setDisplay(SurfaceHolder holder) {
+        mMediaPlayer.setDisplay(holder);
+    }
+
+    @Override
+    public void setVolume(float v1, float v2) {
+        mMediaPlayer.setVolume(v1, v2);
+    }
+
+    @Override
+    public void setLooping(boolean isLooping) {
+        mMediaPlayer.setLooping(isLooping);
+    }
+
+    @Override
+    public void setSpeed(float speed) {
+        mMediaPlayer.setSpeed(speed);
+    }
+
+    @Override
+    public float getSpeed() {
+        return mMediaPlayer.getSpeed(0);
+    }
+
+    @Override
+    public long getTcpSpeed() {
+        return mMediaPlayer.getTcpSpeed();
+    }
+
+    private IMediaPlayer.OnErrorListener onErrorListener = new IMediaPlayer.OnErrorListener() {
+        @Override
+        public boolean onError(IMediaPlayer iMediaPlayer, int framework_err, int impl_err) {
+            mPlayerEventListener.onError();
+            return true;
+        }
+    };
+
+    private IMediaPlayer.OnCompletionListener onCompletionListener = new IMediaPlayer.OnCompletionListener() {
+        @Override
+        public void onCompletion(IMediaPlayer iMediaPlayer) {
+            mPlayerEventListener.onCompletion();
+        }
+    };
+
+    private IMediaPlayer.OnInfoListener onInfoListener = new IMediaPlayer.OnInfoListener() {
+        @Override
+        public boolean onInfo(IMediaPlayer iMediaPlayer, int what, int extra) {
+            mPlayerEventListener.onInfo(what, extra);
+            return true;
+        }
+    };
+
+    private IMediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener = new IMediaPlayer.OnBufferingUpdateListener() {
+        @Override
+        public void onBufferingUpdate(IMediaPlayer iMediaPlayer, int percent) {
+            mBufferedPercent = percent;
+        }
+    };
+
+
+    private IMediaPlayer.OnPreparedListener onPreparedListener = new IMediaPlayer.OnPreparedListener() {
+        @Override
+        public void onPrepared(IMediaPlayer iMediaPlayer) {
+            mPlayerEventListener.onPrepared();
+        }
+    };
+
+    private IMediaPlayer.OnVideoSizeChangedListener onVideoSizeChangedListener = new IMediaPlayer.OnVideoSizeChangedListener() {
+        @Override
+        public void onVideoSizeChanged(IMediaPlayer iMediaPlayer, int i, int i1, int i2, int i3) {
+            int videoWidth = iMediaPlayer.getVideoWidth();
+            int videoHeight = iMediaPlayer.getVideoHeight();
+            if (videoWidth != 0 && videoHeight != 0) {
+                mPlayerEventListener.onVideoSizeChanged(videoWidth, videoHeight);
+            }
+        }
+    };
+}

+ 18 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/impl/ijk/IjkPlayerFactory.java

@@ -0,0 +1,18 @@
+package org.yczbj.ycvideoplayerlib.kernel.impl.ijk;
+
+import android.content.Context;
+
+import org.yczbj.ycvideoplayerlib.kernel.player.PlayerFactory;
+
+
+public class IjkPlayerFactory extends PlayerFactory<IjkPlayer> {
+
+    public static IjkPlayerFactory create() {
+        return new IjkPlayerFactory();
+    }
+
+    @Override
+    public IjkPlayer createPlayer(Context context) {
+        return new IjkPlayer(context);
+    }
+}

+ 90 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/impl/ijk/RawDataSourceProvider.java

@@ -0,0 +1,90 @@
+package org.yczbj.ycvideoplayerlib.kernel.impl.ijk;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import tv.danmaku.ijk.media.player.misc.IMediaDataSource;
+
+public class RawDataSourceProvider implements IMediaDataSource {
+
+    private AssetFileDescriptor mDescriptor;
+
+    private byte[] mMediaBytes;
+
+    public RawDataSourceProvider(AssetFileDescriptor descriptor) {
+        this.mDescriptor = descriptor;
+    }
+
+    @Override
+    public int readAt(long position, byte[] buffer, int offset, int size) {
+        if (position + 1 >= mMediaBytes.length) {
+            return -1;
+        }
+
+        int length;
+        if (position + size < mMediaBytes.length) {
+            length = size;
+        } else {
+            length = (int) (mMediaBytes.length - position);
+            if (length > buffer.length)
+                length = buffer.length;
+
+            length--;
+        }
+        System.arraycopy(mMediaBytes, (int) position, buffer, offset, length);
+
+        return length;
+    }
+
+    @Override
+    public long getSize() throws IOException {
+        long length = mDescriptor.getLength();
+        if (mMediaBytes == null) {
+            InputStream inputStream = mDescriptor.createInputStream();
+            mMediaBytes = readBytes(inputStream);
+        }
+
+
+        return length;
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (mDescriptor != null)
+            mDescriptor.close();
+
+        mDescriptor = null;
+        mMediaBytes = null;
+    }
+
+    private byte[] readBytes(InputStream inputStream) throws IOException {
+        ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
+
+        int bufferSize = 1024;
+        byte[] buffer = new byte[bufferSize];
+
+        int len;
+        while ((len = inputStream.read(buffer)) != -1) {
+            byteBuffer.write(buffer, 0, len);
+        }
+
+        return byteBuffer.toByteArray();
+    }
+
+    public static RawDataSourceProvider create(Context context, Uri uri) {
+        try {
+            AssetFileDescriptor fileDescriptor = context.getContentResolver().openAssetFileDescriptor(uri, "r");
+            return new RawDataSourceProvider(fileDescriptor);
+
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+}

+ 273 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/impl/media/AndroidMediaPlayer.java

@@ -0,0 +1,273 @@
+package org.yczbj.ycvideoplayerlib.kernel.impl.media;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Build;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import org.yczbj.ycvideoplayerlib.kernel.inter.AbstractPlayer;
+import java.util.Map;
+
+/**
+ * 封装系统的MediaPlayer,不推荐,系统的MediaPlayer兼容性较差,建议使用IjkPlayer或者ExoPlayer
+ */
+public class AndroidMediaPlayer extends AbstractPlayer {
+
+    protected MediaPlayer mMediaPlayer;
+    private int mBufferedPercent;
+    private Context mAppContext;
+    private boolean mIsPreparing;
+
+    public AndroidMediaPlayer(Context context) {
+        mAppContext = context.getApplicationContext();
+    }
+
+    @Override
+    public void initPlayer() {
+        mMediaPlayer = new MediaPlayer();
+        setOptions();
+        mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+        mMediaPlayer.setOnErrorListener(onErrorListener);
+        mMediaPlayer.setOnCompletionListener(onCompletionListener);
+        mMediaPlayer.setOnInfoListener(onInfoListener);
+        mMediaPlayer.setOnBufferingUpdateListener(onBufferingUpdateListener);
+        mMediaPlayer.setOnPreparedListener(onPreparedListener);
+        mMediaPlayer.setOnVideoSizeChangedListener(onVideoSizeChangedListener);
+    }
+
+    @Override
+    public void setDataSource(String path, Map<String, String> headers) {
+        try {
+            mMediaPlayer.setDataSource(mAppContext, Uri.parse(path), headers);
+        } catch (Exception e) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void setDataSource(AssetFileDescriptor fd) {
+        try {
+            mMediaPlayer.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
+        } catch (Exception e) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void start() {
+        try {
+            mMediaPlayer.start();
+        } catch (IllegalStateException e) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void pause() {
+        try {
+            mMediaPlayer.pause();
+        } catch (IllegalStateException e) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void stop() {
+        try {
+            mMediaPlayer.stop();
+        } catch (IllegalStateException e) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void prepareAsync() {
+        try {
+            mIsPreparing = true;
+            mMediaPlayer.prepareAsync();
+        } catch (IllegalStateException e) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void reset() {
+        mMediaPlayer.reset();
+        mMediaPlayer.setSurface(null);
+        mMediaPlayer.setDisplay(null);
+        mMediaPlayer.setVolume(1, 1);
+    }
+
+    @Override
+    public boolean isPlaying() {
+        return mMediaPlayer.isPlaying();
+    }
+
+    @Override
+    public void seekTo(long time) {
+        try {
+            mMediaPlayer.seekTo((int) time);
+        } catch (IllegalStateException e) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void release() {
+        mMediaPlayer.setOnErrorListener(null);
+        mMediaPlayer.setOnCompletionListener(null);
+        mMediaPlayer.setOnInfoListener(null);
+        mMediaPlayer.setOnBufferingUpdateListener(null);
+        mMediaPlayer.setOnPreparedListener(null);
+        mMediaPlayer.setOnVideoSizeChangedListener(null);
+        new Thread() {
+            @Override
+            public void run() {
+                try {
+                    mMediaPlayer.release();
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        }.start();
+    }
+
+    @Override
+    public long getCurrentPosition() {
+        return mMediaPlayer.getCurrentPosition();
+    }
+
+    @Override
+    public long getDuration() {
+        return mMediaPlayer.getDuration();
+    }
+
+    @Override
+    public int getBufferedPercentage() {
+        return mBufferedPercent;
+    }
+
+    @Override
+    public void setSurface(Surface surface) {
+        try {
+            mMediaPlayer.setSurface(surface);
+        } catch (Exception e) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void setDisplay(SurfaceHolder holder) {
+        try {
+            mMediaPlayer.setDisplay(holder);
+        } catch (Exception e) {
+            mPlayerEventListener.onError();
+        }
+    }
+
+    @Override
+    public void setVolume(float v1, float v2) {
+        mMediaPlayer.setVolume(v1, v2);
+    }
+
+    @Override
+    public void setLooping(boolean isLooping) {
+        mMediaPlayer.setLooping(isLooping);
+    }
+
+    @Override
+    public void setOptions() {
+    }
+
+    @Override
+    public void setSpeed(float speed) {
+        // only support above Android M
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            try {
+                mMediaPlayer.setPlaybackParams(mMediaPlayer.getPlaybackParams().setSpeed(speed));
+            } catch (Exception e) {
+                mPlayerEventListener.onError();
+            }
+        }
+    }
+
+    @Override
+    public float getSpeed() {
+        // only support above Android M
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            try {
+                return mMediaPlayer.getPlaybackParams().getSpeed();
+            } catch (Exception e) {
+                mPlayerEventListener.onError();
+            }
+        }
+        return 1f;
+    }
+
+    @Override
+    public long getTcpSpeed() {
+        // no support
+        return 0;
+    }
+
+    private MediaPlayer.OnErrorListener onErrorListener = new MediaPlayer.OnErrorListener() {
+        @Override
+        public boolean onError(MediaPlayer mp, int what, int extra) {
+            mPlayerEventListener.onError();
+            return true;
+        }
+    };
+
+    private MediaPlayer.OnCompletionListener onCompletionListener = new MediaPlayer.OnCompletionListener() {
+        @Override
+        public void onCompletion(MediaPlayer mp) {
+            mPlayerEventListener.onCompletion();
+        }
+    };
+
+    private MediaPlayer.OnInfoListener onInfoListener = new MediaPlayer.OnInfoListener() {
+        @Override
+        public boolean onInfo(MediaPlayer mp, int what, int extra) {
+            //解决MEDIA_INFO_VIDEO_RENDERING_START多次回调问题
+            if (what == AbstractPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
+                if (mIsPreparing) {
+                    mPlayerEventListener.onInfo(what, extra);
+                    mIsPreparing = false;
+                }
+            } else {
+                mPlayerEventListener.onInfo(what, extra);
+            }
+            return true;
+        }
+    };
+
+    private MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener = new MediaPlayer.OnBufferingUpdateListener() {
+        @Override
+        public void onBufferingUpdate(MediaPlayer mp, int percent) {
+            mBufferedPercent = percent;
+        }
+    };
+
+
+    private MediaPlayer.OnPreparedListener onPreparedListener = new MediaPlayer.OnPreparedListener() {
+        @Override
+        public void onPrepared(MediaPlayer mp) {
+            mPlayerEventListener.onPrepared();
+            start();
+        }
+    };
+
+    private MediaPlayer.OnVideoSizeChangedListener onVideoSizeChangedListener = new MediaPlayer.OnVideoSizeChangedListener() {
+        @Override
+        public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
+            int videoWidth = mp.getVideoWidth();
+            int videoHeight = mp.getVideoHeight();
+            if (videoWidth != 0 && videoHeight != 0) {
+                mPlayerEventListener.onVideoSizeChanged(videoWidth, videoHeight);
+            }
+        }
+    };
+}

+ 20 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/impl/media/AndroidMediaPlayerFactory.java

@@ -0,0 +1,20 @@
+package org.yczbj.ycvideoplayerlib.kernel.impl.media;
+
+import android.content.Context;
+
+import org.yczbj.ycvideoplayerlib.kernel.player.PlayerFactory;
+
+/**
+ * 创建{@link AndroidMediaPlayer}的工厂类,不推荐,系统的MediaPlayer兼容性较差,建议使用IjkPlayer或者ExoPlayer
+ */
+public class AndroidMediaPlayerFactory extends PlayerFactory<AndroidMediaPlayer> {
+
+    public static AndroidMediaPlayerFactory create() {
+        return new AndroidMediaPlayerFactory();
+    }
+
+    @Override
+    public AndroidMediaPlayer createPlayer(Context context) {
+        return new AndroidMediaPlayer(context);
+    }
+}

+ 174 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/inter/AbstractPlayer.java

@@ -0,0 +1,174 @@
+package org.yczbj.ycvideoplayerlib.kernel.inter;
+
+import android.content.res.AssetFileDescriptor;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+
+import java.util.Map;
+
+/**
+ * 抽象的播放器,继承此接口扩展自己的播放器
+ */
+public abstract class AbstractPlayer {
+
+    /**
+     * 开始渲染视频画面
+     */
+    public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3;
+
+    /**
+     * 缓冲开始
+     */
+    public static final int MEDIA_INFO_BUFFERING_START = 701;
+
+    /**
+     * 缓冲结束
+     */
+    public static final int MEDIA_INFO_BUFFERING_END = 702;
+
+    /**
+     * 视频旋转信息
+     */
+    public static final int MEDIA_INFO_VIDEO_ROTATION_CHANGED = 10001;
+
+    /**
+     * 播放器事件回调
+     */
+    protected PlayerEventListener mPlayerEventListener;
+
+    /**
+     * 初始化播放器实例
+     */
+    public abstract void initPlayer();
+
+    /**
+     * 设置播放地址
+     *
+     * @param path    播放地址
+     * @param headers 播放地址请求头
+     */
+    public abstract void setDataSource(String path, Map<String, String> headers);
+
+    /**
+     * 用于播放raw和asset里面的视频文件
+     */
+    public abstract void setDataSource(AssetFileDescriptor fd);
+
+    /**
+     * 播放
+     */
+    public abstract void start();
+
+    /**
+     * 暂停
+     */
+    public abstract void pause();
+
+    /**
+     * 停止
+     */
+    public abstract void stop();
+
+    /**
+     * 准备开始播放(异步)
+     */
+    public abstract void prepareAsync();
+
+    /**
+     * 重置播放器
+     */
+    public abstract void reset();
+
+    /**
+     * 是否正在播放
+     */
+    public abstract boolean isPlaying();
+
+    /**
+     * 调整进度
+     */
+    public abstract void seekTo(long time);
+
+    /**
+     * 释放播放器
+     */
+    public abstract void release();
+
+    /**
+     * 获取当前播放的位置
+     */
+    public abstract long getCurrentPosition();
+
+    /**
+     * 获取视频总时长
+     */
+    public abstract long getDuration();
+
+    /**
+     * 获取缓冲百分比
+     */
+    public abstract int getBufferedPercentage();
+
+    /**
+     * 设置渲染视频的View,主要用于TextureView
+     */
+    public abstract void setSurface(Surface surface);
+
+    /**
+     * 设置渲染视频的View,主要用于SurfaceView
+     */
+    public abstract void setDisplay(SurfaceHolder holder);
+
+    /**
+     * 设置音量
+     */
+    public abstract void setVolume(float v1, float v2);
+
+    /**
+     * 设置是否循环播放
+     */
+    public abstract void setLooping(boolean isLooping);
+
+    /**
+     * 设置其他播放配置
+     */
+    public abstract void setOptions();
+
+    /**
+     * 设置播放速度
+     */
+    public abstract void setSpeed(float speed);
+
+    /**
+     * 获取播放速度
+     */
+    public abstract float getSpeed();
+
+    /**
+     * 获取当前缓冲的网速
+     */
+    public abstract long getTcpSpeed();
+
+    /**
+     * 绑定VideoView
+     */
+    public void setPlayerEventListener(PlayerEventListener playerEventListener) {
+        this.mPlayerEventListener = playerEventListener;
+    }
+
+    public interface PlayerEventListener {
+
+        void onError();
+
+        void onCompletion();
+
+        void onInfo(int what, int extra);
+
+        void onPrepared();
+
+        void onVideoSizeChanged(int width, int height);
+
+    }
+
+}
+

+ 15 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/player/PlayerFactory.java

@@ -0,0 +1,15 @@
+package org.yczbj.ycvideoplayerlib.kernel.player;
+
+import android.content.Context;
+
+import org.yczbj.ycvideoplayerlib.kernel.inter.AbstractPlayer;
+
+/**
+ * 此接口使用方法:
+ * 1.继承{@link AbstractPlayer}扩展自己的播放器。
+ * 2.继承此接口并实现{@link #createPlayer(Context)},返回步骤1中的播放器。
+ */
+public abstract class PlayerFactory<T extends AbstractPlayer> {
+
+    public abstract T createPlayer(Context context);
+}

+ 22 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/player/ProgressManager.java

@@ -0,0 +1,22 @@
+package org.yczbj.ycvideoplayerlib.kernel.player;
+
+/**
+ * 播放进度管理器,继承此接口实现自己的进度管理器。
+ */
+public abstract class ProgressManager {
+
+    /**
+     * 此方法用于实现保存进度的逻辑
+     * @param url 播放地址
+     * @param progress 播放进度
+     */
+    public abstract void saveProgress(String url, long progress);
+
+    /**
+     * 此方法用于实现获取保存的进度的逻辑
+     * @param url 播放地址
+     * @return 保存的播放进度
+     */
+    public abstract long getSavedProgress(String url);
+
+}

+ 149 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/player/VideoViewConfig.java

@@ -0,0 +1,149 @@
+package org.yczbj.ycvideoplayerlib.kernel.player;
+
+
+import androidx.annotation.Nullable;
+
+import org.yczbj.ycvideoplayerlib.kernel.impl.media.AndroidMediaPlayerFactory;
+import org.yczbj.ycvideoplayerlib.kernel.render.RenderViewFactory;
+import org.yczbj.ycvideoplayerlib.kernel.render.TextureRenderViewFactory;
+
+
+/**
+ * 播放器全局配置
+ */
+public class VideoViewConfig {
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public final boolean mPlayOnMobileNetwork;
+
+    public final boolean mEnableOrientation;
+
+    public final boolean mEnableAudioFocus;
+
+    public final boolean mIsEnableLog;
+
+    public final ProgressManager mProgressManager;
+
+    public final PlayerFactory mPlayerFactory;
+
+    public final int mScreenScaleType;
+
+    public final RenderViewFactory mRenderViewFactory;
+
+    public final boolean mAdaptCutout;
+
+    private VideoViewConfig(Builder builder) {
+        mIsEnableLog = builder.mIsEnableLog;
+        mEnableOrientation = builder.mEnableOrientation;
+        mPlayOnMobileNetwork = builder.mPlayOnMobileNetwork;
+        mEnableAudioFocus = builder.mEnableAudioFocus;
+        mProgressManager = builder.mProgressManager;
+        mScreenScaleType = builder.mScreenScaleType;
+        if (builder.mPlayerFactory == null) {
+            //默认为AndroidMediaPlayer
+            mPlayerFactory = AndroidMediaPlayerFactory.create();
+        } else {
+            mPlayerFactory = builder.mPlayerFactory;
+        }
+        if (builder.mRenderViewFactory == null) {
+            //默认使用TextureView渲染视频
+            mRenderViewFactory = TextureRenderViewFactory.create();
+        } else {
+            mRenderViewFactory = builder.mRenderViewFactory;
+        }
+        mAdaptCutout = builder.mAdaptCutout;
+    }
+
+
+    public final static class Builder {
+
+        private boolean mIsEnableLog;
+        private boolean mPlayOnMobileNetwork;
+        private boolean mEnableOrientation;
+        private boolean mEnableAudioFocus = true;
+        private ProgressManager mProgressManager;
+        private PlayerFactory mPlayerFactory;
+        private int mScreenScaleType;
+        private RenderViewFactory mRenderViewFactory;
+        private boolean mAdaptCutout = true;
+
+        /**
+         * 是否监听设备方向来切换全屏/半屏, 默认不开启
+         */
+        public Builder setEnableOrientation(boolean enableOrientation) {
+            mEnableOrientation = enableOrientation;
+            return this;
+        }
+
+        /**
+         * 在移动环境下调用start()后是否继续播放,默认不继续播放
+         */
+        public Builder setPlayOnMobileNetwork(boolean playOnMobileNetwork) {
+            mPlayOnMobileNetwork = playOnMobileNetwork;
+            return this;
+        }
+
+        /**
+         * 是否开启AudioFocus监听, 默认开启
+         */
+        public Builder setEnableAudioFocus(boolean enableAudioFocus) {
+            mEnableAudioFocus = enableAudioFocus;
+            return this;
+        }
+
+        /**
+         * 设置进度管理器,用于保存播放进度
+         */
+        public Builder setProgressManager(@Nullable ProgressManager progressManager) {
+            mProgressManager = progressManager;
+            return this;
+        }
+
+        /**
+         * 是否打印日志
+         */
+        public Builder setLogEnabled(boolean enableLog) {
+            mIsEnableLog = enableLog;
+            return this;
+        }
+
+        /**
+         * 自定义播放核心
+         */
+        public Builder setPlayerFactory(PlayerFactory playerFactory) {
+            mPlayerFactory = playerFactory;
+            return this;
+        }
+
+        /**
+         * 设置视频比例
+         */
+        public Builder setScreenScaleType(int screenScaleType) {
+            mScreenScaleType = screenScaleType;
+            return this;
+        }
+
+        /**
+         * 自定义RenderView
+         */
+        public Builder setRenderViewFactory(RenderViewFactory renderViewFactory) {
+            mRenderViewFactory = renderViewFactory;
+            return this;
+        }
+
+        /**
+         * 是否适配刘海屏,默认适配
+         */
+        public Builder setAdaptCutout(boolean adaptCutout) {
+            mAdaptCutout = adaptCutout;
+            return this;
+        }
+
+        public VideoViewConfig build() {
+            return new VideoViewConfig(this);
+        }
+    }
+}

+ 139 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/player/VideoViewManager.java

@@ -0,0 +1,139 @@
+package org.yczbj.ycvideoplayerlib.kernel.player;
+
+import android.app.Application;
+
+import org.yczbj.ycvideoplayerlib.kernel.view.VideoView;
+import org.yczbj.ycvideoplayerlib.tool.utils.VideoLogUtils;
+
+import java.util.LinkedHashMap;
+
+/**
+ * 视频播放器管理器,管理当前正在播放的VideoView,以及播放器配置
+ * 你也可以用来保存常驻内存的VideoView,但是要注意通过Application Context创建,
+ * 以免内存泄漏
+ */
+public class VideoViewManager {
+
+    /**
+     * 保存VideoView的容器
+     */
+    private LinkedHashMap<String, VideoView> mVideoViews = new LinkedHashMap<>();
+
+    /**
+     * 是否在移动网络下直接播放视频
+     */
+    private boolean mPlayOnMobileNetwork;
+
+    /**
+     * VideoViewManager实例
+     */
+    private static VideoViewManager sInstance;
+
+    /**
+     * VideoViewConfig实例
+     */
+    private static VideoViewConfig sConfig;
+
+    private VideoViewManager() {
+        mPlayOnMobileNetwork = getConfig().mPlayOnMobileNetwork;
+    }
+
+    /**
+     * 设置VideoViewConfig
+     */
+    public static void setConfig(VideoViewConfig config) {
+        if (sConfig == null) {
+            synchronized (VideoViewConfig.class) {
+                if (sConfig == null) {
+                    sConfig = config == null ? VideoViewConfig.newBuilder().build() : config;
+                }
+            }
+        }
+    }
+
+    /**
+     * 获取VideoViewConfig
+     */
+    public static VideoViewConfig getConfig() {
+        setConfig(null);
+        return sConfig;
+    }
+
+    /**
+     * 获取是否在移动网络下直接播放视频配置
+     */
+    public boolean playOnMobileNetwork() {
+        return mPlayOnMobileNetwork;
+    }
+
+    /**
+     * 设置是否在移动网络下直接播放视频
+     */
+    public void setPlayOnMobileNetwork(boolean playOnMobileNetwork) {
+        mPlayOnMobileNetwork = playOnMobileNetwork;
+    }
+
+    public static VideoViewManager instance() {
+        if (sInstance == null) {
+            synchronized (VideoViewManager.class) {
+                if (sInstance == null) {
+                    sInstance = new VideoViewManager();
+                }
+            }
+        }
+        return sInstance;
+    }
+
+    /**
+     * 添加VideoView
+     * @param tag 相同tag的VideoView只会保存一个,如果tag相同则会release并移除前一个
+     */
+    public void add(VideoView videoView, String tag) {
+        if (!(videoView.getContext() instanceof Application)) {
+            VideoLogUtils.i("The Context of this VideoView is not an Application Context," +
+                    "you must remove it after release,or it will lead to memory leek.");
+        }
+        VideoView old = get(tag);
+        if (old != null) {
+            old.release();
+            remove(tag);
+        }
+        mVideoViews.put(tag, videoView);
+    }
+
+    public VideoView get(String tag) {
+        return mVideoViews.get(tag);
+    }
+
+    public void remove(String tag) {
+        mVideoViews.remove(tag);
+    }
+
+    public void removeAll() {
+        mVideoViews.clear();
+    }
+
+    /**
+     * 释放掉和tag关联的VideoView,并将其从VideoViewManager中移除
+     */
+    public void releaseByTag(String tag) {
+        releaseByTag(tag, true);
+    }
+
+    public void releaseByTag(String tag, boolean isRemove) {
+        VideoView videoView = get(tag);
+        if (videoView != null) {
+            videoView.release();
+            if (isRemove) {
+                remove(tag);
+            }
+        }
+    }
+
+    public boolean onBackPress(String tag) {
+        VideoView videoView = get(tag);
+        if (videoView == null) return false;
+        return videoView.onBackPressed();
+    }
+
+}

+ 52 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/render/IRenderView.java

@@ -0,0 +1,52 @@
+package org.yczbj.ycvideoplayerlib.kernel.render;
+
+import android.graphics.Bitmap;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+import org.yczbj.ycvideoplayerlib.kernel.inter.AbstractPlayer;
+
+
+public interface IRenderView {
+
+    /**
+     * 关联AbstractPlayer
+     */
+    void attachToPlayer(@NonNull AbstractPlayer player);
+
+    /**
+     * 设置视频宽高
+     * @param videoWidth 宽
+     * @param videoHeight 高
+     */
+    void setVideoSize(int videoWidth, int videoHeight);
+
+    /**
+     * 设置视频旋转角度
+     * @param degree 角度值
+     */
+    void setVideoRotation(int degree);
+
+    /**
+     * 设置screen scale type
+     * @param scaleType 类型
+     */
+    void setScaleType(int scaleType);
+
+    /**
+     * 获取真实的RenderView
+     */
+    View getView();
+
+    /**
+     * 截图
+     */
+    Bitmap doScreenShot();
+
+    /**
+     * 释放资源
+     */
+    void release();
+
+}

+ 90 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/render/MeasureHelper.java

@@ -0,0 +1,90 @@
+package org.yczbj.ycvideoplayerlib.kernel.render;
+
+import android.view.View;
+
+import org.yczbj.ycvideoplayerlib.kernel.view.VideoView;
+
+
+public class MeasureHelper {
+
+    private int mVideoWidth;
+
+    private int mVideoHeight;
+
+    private int mCurrentScreenScale;
+
+    private int mVideoRotationDegree;
+
+    public void setVideoRotation(int videoRotationDegree) {
+        mVideoRotationDegree = videoRotationDegree;
+    }
+
+    public void setVideoSize(int width, int height) {
+        mVideoWidth = width;
+        mVideoHeight = height;
+    }
+
+    public void setScreenScale(int screenScale) {
+        mCurrentScreenScale = screenScale;
+    }
+
+    /**
+     * 注意:VideoView的宽高一定要定死,否者以下算法不成立
+     */
+    public int[] doMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (mVideoRotationDegree == 90 || mVideoRotationDegree == 270) { // 软解码时处理旋转信息,交换宽高
+            widthMeasureSpec = widthMeasureSpec + heightMeasureSpec;
+            heightMeasureSpec = widthMeasureSpec - heightMeasureSpec;
+            widthMeasureSpec = widthMeasureSpec - heightMeasureSpec;
+        }
+
+        int width = View.MeasureSpec.getSize(widthMeasureSpec);
+        int height = View.MeasureSpec.getSize(heightMeasureSpec);
+
+        if (mVideoHeight == 0 || mVideoWidth == 0) {
+            return new int[]{width, height};
+        }
+
+        //如果设置了比例
+        switch (mCurrentScreenScale) {
+            case VideoView.SCREEN_SCALE_DEFAULT:
+            default:
+                if (mVideoWidth * height < width * mVideoHeight) {
+                    width = height * mVideoWidth / mVideoHeight;
+                } else if (mVideoWidth * height > width * mVideoHeight) {
+                    height = width * mVideoHeight / mVideoWidth;
+                }
+                break;
+            case VideoView.SCREEN_SCALE_ORIGINAL:
+                width = mVideoWidth;
+                height = mVideoHeight;
+                break;
+            case VideoView.SCREEN_SCALE_16_9:
+                if (height > width / 16 * 9) {
+                    height = width / 16 * 9;
+                } else {
+                    width = height / 9 * 16;
+                }
+                break;
+            case VideoView.SCREEN_SCALE_4_3:
+                if (height > width / 4 * 3) {
+                    height = width / 4 * 3;
+                } else {
+                    width = height / 3 * 4;
+                }
+                break;
+            case VideoView.SCREEN_SCALE_MATCH_PARENT:
+                width = widthMeasureSpec;
+                height = heightMeasureSpec;
+                break;
+            case VideoView.SCREEN_SCALE_CENTER_CROP:
+                if (mVideoWidth * height > width * mVideoHeight) {
+                    width = height * mVideoWidth / mVideoHeight;
+                } else {
+                    height = width * mVideoHeight / mVideoWidth;
+                }
+                break;
+        }
+        return new int[]{width, height};
+    }
+}

+ 15 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/render/RenderViewFactory.java

@@ -0,0 +1,15 @@
+package org.yczbj.ycvideoplayerlib.kernel.render;
+
+import android.content.Context;
+
+/**
+ * 此接口用于扩展自己的渲染View。使用方法如下:
+ * 1.继承IRenderView实现自己的渲染View。
+ * 2.重写createRenderView返回步骤1的渲染View。
+ * 可参考{@link TextureRenderView}和{@link TextureRenderViewFactory}的实现。
+ */
+public abstract class RenderViewFactory {
+
+    public abstract IRenderView createRenderView(Context context);
+
+}

+ 113 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/render/TextureRenderView.java

@@ -0,0 +1,113 @@
+package org.yczbj.ycvideoplayerlib.kernel.render;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.SurfaceTexture;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.yczbj.ycvideoplayerlib.kernel.inter.AbstractPlayer;
+
+
+@SuppressLint("ViewConstructor")
+public class TextureRenderView extends TextureView implements IRenderView, TextureView.SurfaceTextureListener {
+
+    private MeasureHelper mMeasureHelper;
+    private SurfaceTexture mSurfaceTexture;
+
+    @Nullable
+    private AbstractPlayer mMediaPlayer;
+    private Surface mSurface;
+
+    public TextureRenderView(Context context) {
+        super(context);
+    }
+
+    {
+        mMeasureHelper = new MeasureHelper();
+        setSurfaceTextureListener(this);
+    }
+
+    @Override
+    public void attachToPlayer(@NonNull AbstractPlayer player) {
+        this.mMediaPlayer = player;
+    }
+
+    @Override
+    public void setVideoSize(int videoWidth, int videoHeight) {
+        if (videoWidth > 0 && videoHeight > 0) {
+            mMeasureHelper.setVideoSize(videoWidth, videoHeight);
+            requestLayout();
+        }
+    }
+
+    @Override
+    public void setVideoRotation(int degree) {
+        mMeasureHelper.setVideoRotation(degree);
+        setRotation(degree);
+    }
+
+    @Override
+    public void setScaleType(int scaleType) {
+        mMeasureHelper.setScreenScale(scaleType);
+        requestLayout();
+    }
+
+    @Override
+    public View getView() {
+        return this;
+    }
+
+    @Override
+    public Bitmap doScreenShot() {
+        return getBitmap();
+    }
+
+    @Override
+    public void release() {
+        if (mSurface != null)
+            mSurface.release();
+
+        if (mSurfaceTexture != null)
+            mSurfaceTexture.release();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int[] measuredSize = mMeasureHelper.doMeasure(widthMeasureSpec, heightMeasureSpec);
+        setMeasuredDimension(measuredSize[0], measuredSize[1]);
+    }
+
+    @Override
+    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
+        if (mSurfaceTexture != null) {
+            setSurfaceTexture(mSurfaceTexture);
+        } else {
+            mSurfaceTexture = surfaceTexture;
+            mSurface = new Surface(surfaceTexture);
+            if (mMediaPlayer != null) {
+                mMediaPlayer.setSurface(mSurface);
+            }
+        }
+    }
+
+    @Override
+    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+
+    }
+
+    @Override
+    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+        return false;
+    }
+
+    @Override
+    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+
+    }
+}

+ 15 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/render/TextureRenderViewFactory.java

@@ -0,0 +1,15 @@
+package org.yczbj.ycvideoplayerlib.kernel.render;
+
+import android.content.Context;
+
+public class TextureRenderViewFactory extends RenderViewFactory {
+
+    public static TextureRenderViewFactory create() {
+        return new TextureRenderViewFactory();
+    }
+
+    @Override
+    public IRenderView createRenderView(Context context) {
+        return new TextureRenderView(context);
+    }
+}

+ 1068 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/kernel/view/VideoView.java

@@ -0,0 +1,1068 @@
+package org.yczbj.ycvideoplayerlib.kernel.view;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Parcelable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import org.yczbj.ycvideoplayerlib.R;
+import org.yczbj.ycvideoplayerlib.kernel.controller.BaseVideoController;
+import org.yczbj.ycvideoplayerlib.kernel.controller.MediaPlayerControl;
+import org.yczbj.ycvideoplayerlib.kernel.helper.AudioFocusHelper;
+import org.yczbj.ycvideoplayerlib.kernel.inter.AbstractPlayer;
+import org.yczbj.ycvideoplayerlib.kernel.player.PlayerFactory;
+import org.yczbj.ycvideoplayerlib.kernel.player.ProgressManager;
+import org.yczbj.ycvideoplayerlib.kernel.player.VideoViewConfig;
+import org.yczbj.ycvideoplayerlib.kernel.player.VideoViewManager;
+import org.yczbj.ycvideoplayerlib.kernel.render.IRenderView;
+import org.yczbj.ycvideoplayerlib.kernel.render.RenderViewFactory;
+import org.yczbj.ycvideoplayerlib.tool.utils.PlayerUtils;
+import org.yczbj.ycvideoplayerlib.tool.utils.VideoLogUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 播放器
+ */
+
+public class VideoView<P extends AbstractPlayer> extends FrameLayout
+        implements MediaPlayerControl, AbstractPlayer.PlayerEventListener {
+
+    protected P mMediaPlayer;//播放器
+    protected PlayerFactory<P> mPlayerFactory;//工厂类,用于实例化播放核心
+    @Nullable
+    protected BaseVideoController mVideoController;//控制器
+
+    /**
+     * 真正承载播放器视图的容器
+     */
+    protected FrameLayout mPlayerContainer;
+
+    protected IRenderView mRenderView;
+    protected RenderViewFactory mRenderViewFactory;
+
+    public static final int SCREEN_SCALE_DEFAULT = 0;
+    public static final int SCREEN_SCALE_16_9 = 1;
+    public static final int SCREEN_SCALE_4_3 = 2;
+    public static final int SCREEN_SCALE_MATCH_PARENT = 3;
+    public static final int SCREEN_SCALE_ORIGINAL = 4;
+    public static final int SCREEN_SCALE_CENTER_CROP = 5;
+    protected int mCurrentScreenScaleType;
+
+    protected int[] mVideoSize = {0, 0};
+
+    protected boolean mIsMute;//是否静音
+
+    //--------- data sources ---------//
+    protected String mUrl;//当前播放视频的地址
+    protected Map<String, String> mHeaders;//当前视频地址的请求头
+    protected AssetFileDescriptor mAssetFileDescriptor;//assets文件
+
+    protected long mCurrentPosition;//当前正在播放视频的位置
+
+    //播放器的各种状态
+    public static final int STATE_ERROR = -1;
+    public static final int STATE_IDLE = 0;
+    public static final int STATE_PREPARING = 1;
+    public static final int STATE_PREPARED = 2;
+    public static final int STATE_PLAYING = 3;
+    public static final int STATE_PAUSED = 4;
+    public static final int STATE_PLAYBACK_COMPLETED = 5;
+    public static final int STATE_BUFFERING = 6;
+    public static final int STATE_BUFFERED = 7;
+    public static final int STATE_START_ABORT = 8;//开始播放中止
+    protected int mCurrentPlayState = STATE_IDLE;//当前播放器的状态
+
+    public static final int PLAYER_NORMAL = 10;        // 普通播放器
+    public static final int PLAYER_FULL_SCREEN = 11;   // 全屏播放器
+    public static final int PLAYER_TINY_SCREEN = 12;   // 小屏播放器
+    protected int mCurrentPlayerState = PLAYER_NORMAL;
+
+    protected boolean mIsFullScreen;//是否处于全屏状态
+
+    protected boolean mIsTinyScreen;//是否处于小屏状态
+    protected int[] mTinyScreenSize = {0, 0};
+
+    /**
+     * 监听系统中音频焦点改变,见{@link #setEnableAudioFocus(boolean)}
+     */
+    protected boolean mEnableAudioFocus;
+    @Nullable
+    protected AudioFocusHelper mAudioFocusHelper;
+
+    /**
+     * OnStateChangeListener集合,保存了所有开发者设置的监听器
+     */
+    protected List<OnStateChangeListener> mOnStateChangeListeners;
+
+    /**
+     * 进度管理器,设置之后播放器会记录播放进度,以便下次播放恢复进度
+     */
+    @Nullable
+    protected ProgressManager mProgressManager;
+
+    /**
+     * 循环播放
+     */
+    protected boolean mIsLooping;
+
+    /**
+     * {@link #mPlayerContainer}背景色,默认黑色
+     */
+    private int mPlayerBackgroundColor;
+
+    public VideoView(@NonNull Context context) {
+        this(context, null);
+    }
+
+    public VideoView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public VideoView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+
+        //读取全局配置
+        VideoViewConfig config = VideoViewManager.getConfig();
+        mEnableAudioFocus = config.mEnableAudioFocus;
+        mProgressManager = config.mProgressManager;
+        mPlayerFactory = config.mPlayerFactory;
+        mCurrentScreenScaleType = config.mScreenScaleType;
+        mRenderViewFactory = config.mRenderViewFactory;
+
+        //读取xml中的配置,并综合全局配置
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.VideoView);
+        mEnableAudioFocus = a.getBoolean(R.styleable.VideoView_enableAudioFocus, mEnableAudioFocus);
+        mIsLooping = a.getBoolean(R.styleable.VideoView_looping, false);
+        mCurrentScreenScaleType = a.getInt(R.styleable.VideoView_screenScaleType, mCurrentScreenScaleType);
+        mPlayerBackgroundColor = a.getColor(R.styleable.VideoView_playerBackgroundColor, Color.BLACK);
+        a.recycle();
+
+        initView();
+    }
+
+    /**
+     * 初始化播放器视图
+     */
+    protected void initView() {
+        mPlayerContainer = new FrameLayout(getContext());
+        mPlayerContainer.setBackgroundColor(mPlayerBackgroundColor);
+        LayoutParams params = new LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT);
+        this.addView(mPlayerContainer, params);
+    }
+
+    /**
+     * 设置{@link #mPlayerContainer}的背景色
+     */
+    public void setPlayerBackgroundColor(int color) {
+        mPlayerContainer.setBackgroundColor(color);
+    }
+
+    /**
+     * 开始播放,注意:调用此方法后必须调用{@link #release()}释放播放器,否则会导致内存泄漏
+     */
+    @Override
+    public void start() {
+        boolean isStarted = false;
+        if (isInIdleState() || isInStartAbortState()) {
+            isStarted = startPlay();
+        } else if (isInPlaybackState()) {
+            startInPlaybackState();
+            isStarted = true;
+        }
+        if (isStarted) {
+            mPlayerContainer.setKeepScreenOn(true);
+            if (mAudioFocusHelper != null)
+                mAudioFocusHelper.requestFocus();
+        }
+    }
+
+    /**
+     * 第一次播放
+     * @return 是否成功开始播放
+     */
+    protected boolean startPlay() {
+        //如果要显示移动网络提示则不继续播放
+        if (showNetWarning()) {
+            //中止播放
+            setPlayState(STATE_START_ABORT);
+            return false;
+        }
+        //监听音频焦点改变
+        if (mEnableAudioFocus) {
+            mAudioFocusHelper = new AudioFocusHelper(this);
+        }
+        //读取播放进度
+        if (mProgressManager != null) {
+            mCurrentPosition = mProgressManager.getSavedProgress(mUrl);
+        }
+        initPlayer();
+        addDisplay();
+        startPrepare(false);
+        return true;
+    }
+
+    /**
+     * 是否显示移动网络提示,可在Controller中配置
+     */
+    protected boolean showNetWarning() {
+        //播放本地数据源时不检测网络
+        if (isLocalDataSource()) return false;
+        return mVideoController != null && mVideoController.showNetWarning();
+    }
+
+    /**
+     * 判断是否为本地数据源,包括 本地文件、Asset、raw
+     */
+    protected boolean isLocalDataSource() {
+        if (mAssetFileDescriptor != null) {
+            return true;
+        } else if (!TextUtils.isEmpty(mUrl)) {
+            Uri uri = Uri.parse(mUrl);
+            return ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())
+                    || ContentResolver.SCHEME_FILE.equals(uri.getScheme())
+                    || "rawresource".equals(uri.getScheme());
+        }
+        return false;
+    }
+
+    /**
+     * 初始化播放器
+     */
+    protected void initPlayer() {
+        mMediaPlayer = mPlayerFactory.createPlayer(getContext());
+        mMediaPlayer.setPlayerEventListener(this);
+        setInitOptions();
+        mMediaPlayer.initPlayer();
+        setOptions();
+    }
+
+    /**
+     * 初始化之前的配置项
+     */
+    protected void setInitOptions() {
+    }
+
+    /**
+     * 初始化之后的配置项
+     */
+    protected void setOptions() {
+        mMediaPlayer.setLooping(mIsLooping);
+    }
+
+    /**
+     * 初始化视频渲染View
+     */
+    protected void addDisplay() {
+        if (mRenderView != null) {
+            mPlayerContainer.removeView(mRenderView.getView());
+            mRenderView.release();
+        }
+        mRenderView = mRenderViewFactory.createRenderView(getContext());
+        mRenderView.attachToPlayer(mMediaPlayer);
+        LayoutParams params = new LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                Gravity.CENTER);
+        mPlayerContainer.addView(mRenderView.getView(), 0, params);
+    }
+
+    /**
+     * 开始准备播放(直接播放)
+     */
+    protected void startPrepare(boolean reset) {
+        if (reset) {
+            mMediaPlayer.reset();
+            //重新设置option,media player reset之后,option会失效
+            setOptions();
+        }
+        if (prepareDataSource()) {
+            mMediaPlayer.prepareAsync();
+            setPlayState(STATE_PREPARING);
+            setPlayerState(isFullScreen() ? PLAYER_FULL_SCREEN : isTinyScreen() ? PLAYER_TINY_SCREEN : PLAYER_NORMAL);
+        }
+    }
+
+    /**
+     * 设置播放数据
+     * @return 播放数据是否设置成功
+     */
+    protected boolean prepareDataSource() {
+        if (mAssetFileDescriptor != null) {
+            mMediaPlayer.setDataSource(mAssetFileDescriptor);
+            return true;
+        } else if (!TextUtils.isEmpty(mUrl)) {
+            mMediaPlayer.setDataSource(mUrl, mHeaders);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 播放状态下开始播放
+     */
+    protected void startInPlaybackState() {
+        mMediaPlayer.start();
+        setPlayState(STATE_PLAYING);
+    }
+
+    /**
+     * 暂停播放
+     */
+    @Override
+    public void pause() {
+        if (isInPlaybackState()
+                && mMediaPlayer.isPlaying()) {
+            mMediaPlayer.pause();
+            setPlayState(STATE_PAUSED);
+            if (mAudioFocusHelper != null) {
+                mAudioFocusHelper.abandonFocus();
+            }
+            mPlayerContainer.setKeepScreenOn(false);
+        }
+    }
+
+    /**
+     * 继续播放
+     */
+    public void resume() {
+        if (isInPlaybackState()
+                && !mMediaPlayer.isPlaying()) {
+            mMediaPlayer.start();
+            setPlayState(STATE_PLAYING);
+            if (mAudioFocusHelper != null) {
+                mAudioFocusHelper.requestFocus();
+            }
+            mPlayerContainer.setKeepScreenOn(true);
+        }
+    }
+
+    /**
+     * 释放播放器
+     */
+    public void release() {
+        if (!isInIdleState()) {
+            //释放播放器
+            if (mMediaPlayer != null) {
+                mMediaPlayer.release();
+                mMediaPlayer = null;
+            }
+            //释放renderView
+            if (mRenderView != null) {
+                mPlayerContainer.removeView(mRenderView.getView());
+                mRenderView.release();
+                mRenderView = null;
+            }
+            //释放Assets资源
+            if (mAssetFileDescriptor != null) {
+                try {
+                    mAssetFileDescriptor.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+            //关闭AudioFocus监听
+            if (mAudioFocusHelper != null) {
+                mAudioFocusHelper.abandonFocus();
+                mAudioFocusHelper = null;
+            }
+            //关闭屏幕常亮
+            mPlayerContainer.setKeepScreenOn(false);
+            //保存播放进度
+            saveProgress();
+            //重置播放进度
+            mCurrentPosition = 0;
+            //切换转态
+            setPlayState(STATE_IDLE);
+        }
+    }
+
+    /**
+     * 保存播放进度
+     */
+    protected void saveProgress() {
+        if (mProgressManager != null && mCurrentPosition > 0) {
+            VideoLogUtils.d("saveProgress: " + mCurrentPosition);
+            mProgressManager.saveProgress(mUrl, mCurrentPosition);
+        }
+    }
+
+    /**
+     * 是否处于播放状态
+     */
+    protected boolean isInPlaybackState() {
+        return mMediaPlayer != null
+                && mCurrentPlayState != STATE_ERROR
+                && mCurrentPlayState != STATE_IDLE
+                && mCurrentPlayState != STATE_PREPARING
+                && mCurrentPlayState != STATE_START_ABORT
+                && mCurrentPlayState != STATE_PLAYBACK_COMPLETED;
+    }
+
+    /**
+     * 是否处于未播放状态
+     */
+    protected boolean isInIdleState() {
+        return mCurrentPlayState == STATE_IDLE;
+    }
+
+    /**
+     * 播放中止状态
+     */
+    private boolean isInStartAbortState() {
+        return mCurrentPlayState == STATE_START_ABORT;
+    }
+
+    /**
+     * 重新播放
+     *
+     * @param resetPosition 是否从头开始播放
+     */
+    @Override
+    public void replay(boolean resetPosition) {
+        if (resetPosition) {
+            mCurrentPosition = 0;
+        }
+        addDisplay();
+        startPrepare(true);
+        mPlayerContainer.setKeepScreenOn(true);
+    }
+
+    /**
+     * 获取视频总时长
+     */
+    @Override
+    public long getDuration() {
+        if (isInPlaybackState()) {
+            return mMediaPlayer.getDuration();
+        }
+        return 0;
+    }
+
+    /**
+     * 获取当前播放的位置
+     */
+    @Override
+    public long getCurrentPosition() {
+        if (isInPlaybackState()) {
+            mCurrentPosition = mMediaPlayer.getCurrentPosition();
+            return mCurrentPosition;
+        }
+        return 0;
+    }
+
+    /**
+     * 调整播放进度
+     */
+    @Override
+    public void seekTo(long pos) {
+        if (isInPlaybackState()) {
+            mMediaPlayer.seekTo(pos);
+        }
+    }
+
+    /**
+     * 是否处于播放状态
+     */
+    @Override
+    public boolean isPlaying() {
+        return isInPlaybackState() && mMediaPlayer.isPlaying();
+    }
+
+    /**
+     * 获取当前缓冲百分比
+     */
+    @Override
+    public int getBufferedPercentage() {
+        return mMediaPlayer != null ? mMediaPlayer.getBufferedPercentage() : 0;
+    }
+
+    /**
+     * 设置静音
+     */
+    @Override
+    public void setMute(boolean isMute) {
+        if (mMediaPlayer != null) {
+            this.mIsMute = isMute;
+            float volume = isMute ? 0.0f : 1.0f;
+            mMediaPlayer.setVolume(volume, volume);
+        }
+    }
+
+    /**
+     * 是否处于静音状态
+     */
+    @Override
+    public boolean isMute() {
+        return mIsMute;
+    }
+
+    /**
+     * 视频播放出错回调
+     */
+    @Override
+    public void onError() {
+        mPlayerContainer.setKeepScreenOn(false);
+        setPlayState(STATE_ERROR);
+    }
+
+    /**
+     * 视频播放完成回调
+     */
+    @Override
+    public void onCompletion() {
+        mPlayerContainer.setKeepScreenOn(false);
+        mCurrentPosition = 0;
+        if (mProgressManager != null) {
+            //播放完成,清除进度
+            mProgressManager.saveProgress(mUrl, 0);
+        }
+        setPlayState(STATE_PLAYBACK_COMPLETED);
+    }
+
+    @Override
+    public void onInfo(int what, int extra) {
+        switch (what) {
+            case AbstractPlayer.MEDIA_INFO_BUFFERING_START:
+                setPlayState(STATE_BUFFERING);
+                break;
+            case AbstractPlayer.MEDIA_INFO_BUFFERING_END:
+                setPlayState(STATE_BUFFERED);
+                break;
+            case AbstractPlayer.MEDIA_INFO_VIDEO_RENDERING_START: // 视频开始渲染
+                setPlayState(STATE_PLAYING);
+                if (mPlayerContainer.getWindowVisibility() != VISIBLE) {
+                    pause();
+                }
+                break;
+            case AbstractPlayer.MEDIA_INFO_VIDEO_ROTATION_CHANGED:
+                if (mRenderView != null)
+                    mRenderView.setVideoRotation(extra);
+                break;
+        }
+    }
+
+    /**
+     * 视频缓冲完毕,准备开始播放时回调
+     */
+    @Override
+    public void onPrepared() {
+        setPlayState(STATE_PREPARED);
+        if (mCurrentPosition > 0) {
+            seekTo(mCurrentPosition);
+        }
+    }
+
+    /**
+     * 获取当前播放器的状态
+     */
+    public int getCurrentPlayerState() {
+        return mCurrentPlayerState;
+    }
+
+    /**
+     * 获取当前的播放状态
+     */
+    public int getCurrentPlayState() {
+        return mCurrentPlayState;
+    }
+
+    /**
+     * 获取缓冲速度
+     */
+    @Override
+    public long getTcpSpeed() {
+        return mMediaPlayer != null ? mMediaPlayer.getTcpSpeed() : 0;
+    }
+
+    /**
+     * 设置播放速度
+     */
+    @Override
+    public void setSpeed(float speed) {
+        if (isInPlaybackState()) {
+            mMediaPlayer.setSpeed(speed);
+        }
+    }
+
+    @Override
+    public float getSpeed() {
+        if (isInPlaybackState()) {
+            return mMediaPlayer.getSpeed();
+        }
+        return 1f;
+    }
+
+    /**
+     * 设置视频地址
+     */
+    public void setUrl(String url) {
+        setUrl(url, null);
+    }
+
+    /**
+     * 设置包含请求头信息的视频地址
+     *
+     * @param url     视频地址
+     * @param headers 请求头
+     */
+    public void setUrl(String url, Map<String, String> headers) {
+        mAssetFileDescriptor = null;
+        mUrl = url;
+        mHeaders = headers;
+    }
+
+    /**
+     * 用于播放assets里面的视频文件
+     */
+    public void setAssetFileDescriptor(AssetFileDescriptor fd) {
+        mUrl = null;
+        this.mAssetFileDescriptor = fd;
+    }
+
+    /**
+     * 一开始播放就seek到预先设置好的位置
+     */
+    public void skipPositionWhenPlay(int position) {
+        this.mCurrentPosition = position;
+    }
+
+    /**
+     * 设置音量 0.0f-1.0f 之间
+     *
+     * @param v1 左声道音量
+     * @param v2 右声道音量
+     */
+    public void setVolume(float v1, float v2) {
+        if (mMediaPlayer != null) {
+            mMediaPlayer.setVolume(v1, v2);
+        }
+    }
+
+    /**
+     * 设置进度管理器,用于保存播放进度
+     */
+    public void setProgressManager(@Nullable ProgressManager progressManager) {
+        this.mProgressManager = progressManager;
+    }
+
+    /**
+     * 循环播放, 默认不循环播放
+     */
+    public void setLooping(boolean looping) {
+        mIsLooping = looping;
+        if (mMediaPlayer != null) {
+            mMediaPlayer.setLooping(looping);
+        }
+    }
+
+    /**
+     * 是否开启AudioFocus监听, 默认开启,用于监听其它地方是否获取音频焦点,如果有其它地方获取了
+     * 音频焦点,此播放器将做出相应反应,具体实现见{@link AudioFocusHelper}
+     */
+    public void setEnableAudioFocus(boolean enableAudioFocus) {
+        mEnableAudioFocus = enableAudioFocus;
+    }
+
+    /**
+     * 自定义播放核心,继承{@link PlayerFactory}实现自己的播放核心
+     */
+    public void setPlayerFactory(PlayerFactory<P> playerFactory) {
+        if (playerFactory == null) {
+            throw new IllegalArgumentException("PlayerFactory can not be null!");
+        }
+        mPlayerFactory = playerFactory;
+    }
+
+    /**
+     * 自定义RenderView,继承{@link RenderViewFactory}实现自己的RenderView
+     */
+    public void setRenderViewFactory(RenderViewFactory renderViewFactory) {
+        if (renderViewFactory == null) {
+            throw new IllegalArgumentException("RenderViewFactory can not be null!");
+        }
+        mRenderViewFactory = renderViewFactory;
+    }
+
+    /**
+     * 进入全屏
+     */
+    @Override
+    public void startFullScreen() {
+        if (mIsFullScreen){
+            return;
+        }
+        ViewGroup decorView = getDecorView();
+        if (decorView == null){
+            return;
+        }
+        mIsFullScreen = true;
+        //隐藏NavigationBar和StatusBar
+        hideSysBar(decorView);
+        //从当前FrameLayout中移除播放器视图
+        this.removeView(mPlayerContainer);
+        //将播放器视图添加到DecorView中即实现了全屏
+        decorView.addView(mPlayerContainer);
+        setPlayerState(PLAYER_FULL_SCREEN);
+    }
+
+    private void hideSysBar(ViewGroup decorView) {
+        int uiOptions = decorView.getSystemUiVisibility();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+            uiOptions |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            uiOptions |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+        }
+        decorView.setSystemUiVisibility(uiOptions);
+        getActivity().getWindow().setFlags(
+                WindowManager.LayoutParams.FLAG_FULLSCREEN,
+                WindowManager.LayoutParams.FLAG_FULLSCREEN);
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        super.onWindowFocusChanged(hasWindowFocus);
+        if (hasWindowFocus && mIsFullScreen) {
+            //重新获得焦点时保持全屏状态
+            hideSysBar(getDecorView());
+        }
+    }
+
+    /**
+     * 退出全屏
+     */
+    @Override
+    public void stopFullScreen() {
+        if (!mIsFullScreen)
+            return;
+
+        ViewGroup decorView = getDecorView();
+        if (decorView == null)
+            return;
+
+        mIsFullScreen = false;
+
+        //显示NavigationBar和StatusBar
+        showSysBar(decorView);
+
+        //把播放器视图从DecorView中移除并添加到当前FrameLayout中即退出了全屏
+        decorView.removeView(mPlayerContainer);
+        this.addView(mPlayerContainer);
+
+        setPlayerState(PLAYER_NORMAL);
+    }
+
+    private void showSysBar(ViewGroup decorView) {
+        int uiOptions = decorView.getSystemUiVisibility();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+            uiOptions &= ~View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            uiOptions &= ~View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+        }
+        decorView.setSystemUiVisibility(uiOptions);
+        getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+    }
+
+    /**
+     * 获取DecorView
+     */
+    protected ViewGroup getDecorView() {
+        Activity activity = getActivity();
+        if (activity == null) return null;
+        return (ViewGroup) activity.getWindow().getDecorView();
+    }
+
+    /**
+     * 获取activity中的content view,其id为android.R.id.content
+     */
+    protected ViewGroup getContentView() {
+        Activity activity = getActivity();
+        if (activity == null) return null;
+        return activity.findViewById(android.R.id.content);
+    }
+
+    /**
+     * 获取Activity,优先通过Controller去获取Activity
+     */
+    protected Activity getActivity() {
+        Activity activity;
+        if (mVideoController != null) {
+            activity = PlayerUtils.scanForActivity(mVideoController.getContext());
+            if (activity == null) {
+                activity = PlayerUtils.scanForActivity(getContext());
+            }
+        } else {
+            activity = PlayerUtils.scanForActivity(getContext());
+        }
+        return activity;
+    }
+
+    /**
+     * 判断是否处于全屏状态
+     */
+    @Override
+    public boolean isFullScreen() {
+        return mIsFullScreen;
+    }
+
+    /**
+     * 开启小屏
+     */
+    public void startTinyScreen() {
+        if (mIsTinyScreen) return;
+        ViewGroup contentView = getContentView();
+        if (contentView == null) return;
+        this.removeView(mPlayerContainer);
+        int width = mTinyScreenSize[0];
+        if (width <= 0) {
+            width = PlayerUtils.getScreenWidth(getContext(), false) / 2;
+        }
+        int height = mTinyScreenSize[1];
+        if (height <= 0) {
+            height = width * 9 / 16;
+        }
+        LayoutParams params = new LayoutParams(width, height);
+        params.gravity = Gravity.BOTTOM | Gravity.END;
+        contentView.addView(mPlayerContainer, params);
+        mIsTinyScreen = true;
+        setPlayerState(PLAYER_TINY_SCREEN);
+    }
+
+    /**
+     * 退出小屏
+     */
+    public void stopTinyScreen() {
+        if (!mIsTinyScreen) return;
+
+        ViewGroup contentView = getContentView();
+        if (contentView == null) return;
+        contentView.removeView(mPlayerContainer);
+        LayoutParams params = new LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT);
+        this.addView(mPlayerContainer, params);
+
+        mIsTinyScreen = false;
+        setPlayerState(PLAYER_NORMAL);
+    }
+
+    public boolean isTinyScreen() {
+        return mIsTinyScreen;
+    }
+
+    @Override
+    public void onVideoSizeChanged(int videoWidth, int videoHeight) {
+        mVideoSize[0] = videoWidth;
+        mVideoSize[1] = videoHeight;
+
+        if (mRenderView != null) {
+            mRenderView.setScaleType(mCurrentScreenScaleType);
+            mRenderView.setVideoSize(videoWidth, videoHeight);
+        }
+    }
+
+    /**
+     * 设置控制器,传null表示移除控制器
+     */
+    public void setVideoController(@Nullable BaseVideoController mediaController) {
+        mPlayerContainer.removeView(mVideoController);
+        mVideoController = mediaController;
+        if (mediaController != null) {
+            mediaController.setMediaPlayer(this);
+            LayoutParams params = new LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.MATCH_PARENT);
+            mPlayerContainer.addView(mVideoController, params);
+        }
+    }
+
+    /**
+     * 设置视频比例
+     */
+    @Override
+    public void setScreenScaleType(int screenScaleType) {
+        mCurrentScreenScaleType = screenScaleType;
+        if (mRenderView != null) {
+            mRenderView.setScaleType(screenScaleType);
+        }
+    }
+
+    /**
+     * 设置镜像旋转,暂不支持SurfaceView
+     */
+    @Override
+    public void setMirrorRotation(boolean enable) {
+        if (mRenderView != null) {
+            mRenderView.getView().setScaleX(enable ? -1 : 1);
+        }
+    }
+
+    /**
+     * 截图,暂不支持SurfaceView
+     */
+    @Override
+    public Bitmap doScreenShot() {
+        if (mRenderView != null) {
+            return mRenderView.doScreenShot();
+        }
+        return null;
+    }
+
+    /**
+     * 获取视频宽高,其中width: mVideoSize[0], height: mVideoSize[1]
+     */
+    @Override
+    public int[] getVideoSize() {
+        return mVideoSize;
+    }
+
+    /**
+     * 旋转视频画面
+     *
+     * @param rotation 角度
+     */
+    @Override
+    public void setRotation(float rotation) {
+        if (mRenderView != null) {
+            mRenderView.setVideoRotation((int) rotation);
+        }
+    }
+
+    /**
+     * 设置小屏的宽高
+     *
+     * @param tinyScreenSize 其中tinyScreenSize[0]是宽,tinyScreenSize[1]是高
+     */
+    public void setTinyScreenSize(int[] tinyScreenSize) {
+        this.mTinyScreenSize = tinyScreenSize;
+    }
+
+    /**
+     * 向Controller设置播放状态,用于控制Controller的ui展示
+     */
+    protected void setPlayState(int playState) {
+        mCurrentPlayState = playState;
+        if (mVideoController != null) {
+            mVideoController.setPlayState(playState);
+        }
+        if (mOnStateChangeListeners != null) {
+            for (OnStateChangeListener l : PlayerUtils.getSnapshot(mOnStateChangeListeners)) {
+                if (l != null) {
+                    l.onPlayStateChanged(playState);
+                }
+            }
+        }
+    }
+
+    /**
+     * 向Controller设置播放器状态,包含全屏状态和非全屏状态
+     */
+    protected void setPlayerState(int playerState) {
+        mCurrentPlayerState = playerState;
+        if (mVideoController != null) {
+            mVideoController.setPlayerState(playerState);
+        }
+        if (mOnStateChangeListeners != null) {
+            for (OnStateChangeListener l : PlayerUtils.getSnapshot(mOnStateChangeListeners)) {
+                if (l != null) {
+                    l.onPlayerStateChanged(playerState);
+                }
+            }
+        }
+    }
+
+    /**
+     * 播放状态改变监听器
+     */
+    public interface OnStateChangeListener {
+        void onPlayerStateChanged(int playerState);
+        void onPlayStateChanged(int playState);
+    }
+
+    /**
+     * OnStateChangeListener的空实现。用的时候只需要重写需要的方法
+     */
+    public static class SimpleOnStateChangeListener implements OnStateChangeListener {
+        @Override
+        public void onPlayerStateChanged(int playerState) {}
+        @Override
+        public void onPlayStateChanged(int playState) {}
+    }
+
+    /**
+     * 添加一个播放状态监听器,播放状态发生变化时将会调用。
+     */
+    public void addOnStateChangeListener(@NonNull OnStateChangeListener listener) {
+        if (mOnStateChangeListeners == null) {
+            mOnStateChangeListeners = new ArrayList<>();
+        }
+        mOnStateChangeListeners.add(listener);
+    }
+
+    /**
+     * 移除某个播放状态监听
+     */
+    public void removeOnStateChangeListener(@NonNull OnStateChangeListener listener) {
+        if (mOnStateChangeListeners != null) {
+            mOnStateChangeListeners.remove(listener);
+        }
+    }
+
+    /**
+     * 设置一个播放状态监听器,播放状态发生变化时将会调用,
+     * 如果你想同时设置多个监听器,推荐 {@link #addOnStateChangeListener(OnStateChangeListener)}。
+     */
+    public void setOnStateChangeListener(@NonNull OnStateChangeListener listener) {
+        if (mOnStateChangeListeners == null) {
+            mOnStateChangeListeners = new ArrayList<>();
+        } else {
+            mOnStateChangeListeners.clear();
+        }
+        mOnStateChangeListeners.add(listener);
+    }
+
+    /**
+     * 移除所有播放状态监听
+     */
+    public void clearOnStateChangeListeners() {
+        if (mOnStateChangeListeners != null) {
+            mOnStateChangeListeners.clear();
+        }
+    }
+
+    /**
+     * 改变返回键逻辑,用于activity
+     */
+    public boolean onBackPressed() {
+        return mVideoController != null && mVideoController.onBackPressed();
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        VideoLogUtils.d("onSaveInstanceState: " + mCurrentPosition);
+        //activity切到后台后可能被系统回收,故在此处进行进度保存
+        saveProgress();
+        return super.onSaveInstanceState();
+    }
+}

+ 1 - 1
YCVideoPlayerLib/src/main/java/org/yczbj/ycvideoplayerlib/manager/VideoPlayerManager.java → VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/manager/VideoPlayerManager.java

@@ -16,7 +16,7 @@ limitations under the License.
 package org.yczbj.ycvideoplayerlib.manager;
 package org.yczbj.ycvideoplayerlib.manager;
 
 
 
 
-import org.yczbj.ycvideoplayerlib.player.VideoPlayer;
+import org.yczbj.ycvideoplayerlib.view.player.VideoPlayer;
 
 
 
 
 /**
 /**

+ 8 - 8
YCVideoPlayerLib/src/main/java/org/yczbj/ycvideoplayerlib/receiver/BatterReceiver.java → VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/receiver/BatterReceiver.java

@@ -21,11 +21,11 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.Intent;
 import android.os.BatteryManager;
 import android.os.BatteryManager;
 
 
-import org.yczbj.ycvideoplayerlib.constant.ConstantKeys;
-import org.yczbj.ycvideoplayerlib.controller.AbsVideoPlayerController;
+import org.yczbj.ycvideoplayerlib.config.ConstantKeys;
+import org.yczbj.ycvideoplayerlib.view.controller.AbsVideoPlayerController;
 import org.yczbj.ycvideoplayerlib.manager.VideoPlayerManager;
 import org.yczbj.ycvideoplayerlib.manager.VideoPlayerManager;
-import org.yczbj.ycvideoplayerlib.player.VideoPlayer;
-import org.yczbj.ycvideoplayerlib.utils.VideoLogUtil;
+import org.yczbj.ycvideoplayerlib.view.player.VideoPlayer;
+import org.yczbj.ycvideoplayerlib.tool.utils.VideoLogUtils;
 
 
 /**
 /**
  * <pre>
  * <pre>
@@ -40,7 +40,7 @@ public class BatterReceiver extends BroadcastReceiver {
 
 
     @Override
     @Override
     public void onReceive(Context context, Intent intent) {
     public void onReceive(Context context, Intent intent) {
-        VideoLogUtil.i("电量状态监听广播接收到数据了");
+        VideoLogUtils.i("电量状态监听广播接收到数据了");
         int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS,
         int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS,
                 BatteryManager.BATTERY_STATUS_UNKNOWN);
                 BatteryManager.BATTERY_STATUS_UNKNOWN);
         VideoPlayer mVideoPlayer = VideoPlayerManager.instance().getCurrentVideoPlayer();
         VideoPlayer mVideoPlayer = VideoPlayerManager.instance().getCurrentVideoPlayer();
@@ -59,9 +59,9 @@ public class BatterReceiver extends BroadcastReceiver {
                     // 总电量
                     // 总电量
                     int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
                     int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
                     int percentage = (int) (((float) level / scale) * 100);
                     int percentage = (int) (((float) level / scale) * 100);
-                    VideoLogUtil.i("广播NetworkReceiver------当前电量"+level);
-                    VideoLogUtil.i("广播NetworkReceiver------总电量"+scale);
-                    VideoLogUtil.i("广播NetworkReceiver------百分比"+percentage);
+                    VideoLogUtils.i("广播NetworkReceiver------当前电量"+level);
+                    VideoLogUtils.i("广播NetworkReceiver------总电量"+scale);
+                    VideoLogUtils.i("广播NetworkReceiver------百分比"+percentage);
                     if (percentage <= 10) {
                     if (percentage <= 10) {
                         controller.onBatterStateChanged(ConstantKeys.BatterMode.BATTERY_10);
                         controller.onBatterStateChanged(ConstantKeys.BatterMode.BATTERY_10);
                     } else if (percentage <= 20) {
                     } else if (percentage <= 20) {

+ 11 - 11
YCVideoPlayerLib/src/main/java/org/yczbj/ycvideoplayerlib/receiver/NetChangedReceiver.java → VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/receiver/NetChangedReceiver.java

@@ -20,12 +20,12 @@ import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Context;
 import android.content.Intent;
 import android.content.Intent;
 
 
-import org.yczbj.ycvideoplayerlib.constant.ConstantKeys;
-import org.yczbj.ycvideoplayerlib.controller.AbsVideoPlayerController;
+import org.yczbj.ycvideoplayerlib.config.ConstantKeys;
+import org.yczbj.ycvideoplayerlib.view.controller.AbsVideoPlayerController;
 import org.yczbj.ycvideoplayerlib.manager.VideoPlayerManager;
 import org.yczbj.ycvideoplayerlib.manager.VideoPlayerManager;
-import org.yczbj.ycvideoplayerlib.player.VideoPlayer;
-import org.yczbj.ycvideoplayerlib.utils.NetworkUtils;
-import org.yczbj.ycvideoplayerlib.utils.VideoLogUtil;
+import org.yczbj.ycvideoplayerlib.view.player.VideoPlayer;
+import org.yczbj.ycvideoplayerlib.tool.utils.NetworkUtils;
+import org.yczbj.ycvideoplayerlib.tool.utils.VideoLogUtils;
 
 
 /**
 /**
  * <pre>
  * <pre>
@@ -41,21 +41,21 @@ public class NetChangedReceiver extends BroadcastReceiver {
     @SuppressLint("UnsafeProtectedBroadcastReceiver")
     @SuppressLint("UnsafeProtectedBroadcastReceiver")
     @Override
     @Override
     public void onReceive(Context context, Intent intent) {
     public void onReceive(Context context, Intent intent) {
-        VideoLogUtil.i("网络状态监听广播接收到数据了");
+        VideoLogUtils.i("网络状态监听广播接收到数据了");
         VideoPlayer mVideoPlayer = VideoPlayerManager.instance().getCurrentVideoPlayer();
         VideoPlayer mVideoPlayer = VideoPlayerManager.instance().getCurrentVideoPlayer();
         if (mVideoPlayer!=null){
         if (mVideoPlayer!=null){
             AbsVideoPlayerController controller = mVideoPlayer.getController();
             AbsVideoPlayerController controller = mVideoPlayer.getController();
             switch (NetworkUtils.getConnectState(context)) {
             switch (NetworkUtils.getConnectState(context)) {
                 case MOBILE:
                 case MOBILE:
-                    VideoLogUtil.i("当网络状态监听前连接了移动数据");
+                    VideoLogUtils.i("当网络状态监听前连接了移动数据");
                     break;
                     break;
                 case WIFI:
                 case WIFI:
-                    VideoLogUtil.i("网络状态监听当前连接了Wifi");
+                    VideoLogUtils.i("网络状态监听当前连接了Wifi");
                     break;
                     break;
                 case UN_CONNECTED:
                 case UN_CONNECTED:
-                    VideoLogUtil.i("网络状态监听当前没有网络连接");
+                    VideoLogUtils.i("网络状态监听当前没有网络连接");
                     if (mVideoPlayer.isPlaying() || mVideoPlayer.isBufferingPlaying()) {
                     if (mVideoPlayer.isPlaying() || mVideoPlayer.isBufferingPlaying()) {
-                        VideoLogUtil.i("网络状态监听当前没有网络连接---设置暂停播放");
+                        VideoLogUtils.i("网络状态监听当前没有网络连接---设置暂停播放");
                         mVideoPlayer.pause();
                         mVideoPlayer.pause();
                     }
                     }
                     if (controller!=null){
                     if (controller!=null){
@@ -63,7 +63,7 @@ public class NetChangedReceiver extends BroadcastReceiver {
                     }
                     }
                     break;
                     break;
                 default:
                 default:
-                    VideoLogUtil.i("网络状态监听其他情况");
+                    VideoLogUtils.i("网络状态监听其他情况");
                     break;
                     break;
             }
             }
         }
         }

+ 196 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/tool/timer/CountDownTimer.java

@@ -0,0 +1,196 @@
+package org.yczbj.ycvideoplayerlib.tool.timer;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+
+import androidx.annotation.NonNull;
+
+
+/**
+ * <pre>
+ *     @author  yangchong
+ *     email  : yangchong211@163.com
+ *     time  :  2020/5/26
+ *     desc  :  自定义倒计时器
+ *     revise:  支持开始,暂停,恢复暂停,取消等业务逻辑
+ *              也可以用于多线程中
+ * </pre>
+ */
+public class CountDownTimer {
+
+    /**
+     * 时间,即开始的时间,通俗来说就是倒计时总时间
+     */
+    private long mMillisInFuture;
+    /**
+     * 布尔值,表示计时器是否被取消
+     * 只有调用cancel时才被设置为true
+     */
+    private boolean mCancelled = false;
+    /**
+     * 用户接收回调的时间间隔,一般是1秒
+     */
+    private long mCountdownInterval;
+    /**
+     * 记录暂停时候的时间
+     */
+    private long mStopTimeInFuture;
+    /**
+     * mas.what值
+     */
+    private static final int MSG = 520;
+    /**
+     * 暂停时,当时剩余时间
+     */
+    private long mCurrentMillisLeft;
+    /**
+     * 是否暂停
+     * 只有当调用pause时,才设置为true
+     */
+    private boolean mPause = false;
+    /**
+     * 监听listener
+     */
+    private TimerListener mCountDownListener;
+
+    public CountDownTimer(){
+
+    }
+
+    public CountDownTimer(long millisInFuture, long countdownInterval) {
+        this.mMillisInFuture = millisInFuture;
+        this.mCountdownInterval = countdownInterval;
+    }
+
+    /**
+     * 开始倒计时,每次点击,都会重新开始
+     */
+    public synchronized final void start() {
+        if (mMillisInFuture <= 0 && mCountdownInterval <= 0) {
+            throw new RuntimeException("you must set the millisInFuture > 0 or countdownInterval >0");
+        }
+        mCancelled = false;
+        mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture;
+        mPause = false;
+        mHandler.sendMessage(mHandler.obtainMessage(MSG));
+        if (mCountDownListener!=null){
+            mCountDownListener.onStart();
+        }
+    }
+
+    /**
+     * 取消计时器
+     */
+    public synchronized final void cancel() {
+        if (mHandler != null) {
+            //暂停
+            mPause = false;
+            mHandler.removeMessages(MSG);
+            //取消
+            mCancelled = true;
+        }
+    }
+
+    /**
+     * 按一下暂停,再按一下继续倒计时
+     */
+    public synchronized final void pause() {
+        if (mHandler != null) {
+            if (mCancelled) {
+                return;
+            }
+            if (mCurrentMillisLeft < mCountdownInterval) {
+                return;
+            }
+            if (!mPause) {
+                mHandler.removeMessages(MSG);
+                mPause = true;
+            }
+        }
+    }
+
+    /**
+     * 恢复暂停,开始
+     */
+    public synchronized final  void resume() {
+        if (mMillisInFuture <= 0 && mCountdownInterval <= 0) {
+            throw new RuntimeException("you must set the millisInFuture > 0 or countdownInterval >0");
+        }
+        if (mCancelled) {
+            return;
+        }
+        //剩余时长少于
+        if (mCurrentMillisLeft < mCountdownInterval || !mPause) {
+            return;
+        }
+        mStopTimeInFuture = SystemClock.elapsedRealtime() + mCurrentMillisLeft;
+        mHandler.sendMessage(mHandler.obtainMessage(MSG));
+        mPause = false;
+    }
+
+
+    @SuppressLint("HandlerLeak")
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(@NonNull Message msg) {
+            synchronized (CountDownTimer.this) {
+                if (mCancelled) {
+                    return;
+                }
+                //剩余毫秒数
+                final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();
+                if (millisLeft <= 0) {
+                    mCurrentMillisLeft = 0;
+                    if (mCountDownListener != null) {
+                        mCountDownListener.onFinish();
+                    }
+                } else if (millisLeft < mCountdownInterval) {
+                    mCurrentMillisLeft = 0;
+                    // 剩余时间小于一次时间间隔的时候,不再通知,只是延迟一下
+                    sendMessageDelayed(obtainMessage(MSG), millisLeft);
+                } else {
+                    //有多余的时间
+                    long lastTickStart = SystemClock.elapsedRealtime();
+                    if (mCountDownListener != null) {
+                        mCountDownListener.onTick(millisLeft);
+                    }
+                    mCurrentMillisLeft = millisLeft;
+                    // 考虑用户的onTick需要花费时间,处理用户onTick执行的时间
+                    long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
+                    // 特殊情况:用户的onTick方法花费的时间比interval长,那么直接跳转到下一次interval
+                    while (delay < 0){
+                        delay += mCountdownInterval;
+                    }
+                    sendMessageDelayed(obtainMessage(MSG), delay);
+                }
+            }
+        }
+    };
+
+    /**
+     * 设置倒计时总时间
+     * @param millisInFuture                    毫秒值
+     */
+    public void setMillisInFuture(long millisInFuture) {
+        this.mMillisInFuture = millisInFuture;
+    }
+
+    /**
+     * 设置倒计时间隔值
+     * @param countdownInterval                 间隔,一般设置为1000毫秒
+     */
+    public void setCountdownInterval(long countdownInterval) {
+        this.mCountdownInterval = countdownInterval;
+    }
+
+    /**
+     * 设置倒计时监听
+     * @param countDownListener                 listener
+     */
+    public void setCountDownListener(TimerListener countDownListener) {
+        this.mCountDownListener = countDownListener;
+    }
+
+}

+ 101 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/tool/timer/CountTimeTools.java

@@ -0,0 +1,101 @@
+package org.yczbj.ycvideoplayerlib.tool.timer;
+
+/**
+ * <pre>
+ *     @author  yangchong
+ *     email  : yangchong211@163.com
+ *     time  :  2020/5/26
+ *     desc  :  工具类
+ *     revise:
+ * </pre>
+ */
+public final class CountTimeTools {
+
+    /**
+     * 将毫秒换成00:00:00
+     * @param time                          毫秒
+     * @return                              时间字符串
+     */
+    public static String getCountTimeByLong(long time) {
+        //秒
+        long totalTime = time / 1000;
+        //时,分,秒
+        long hour = 0, minute = 0, second = 0;
+
+        if (3600 <= totalTime) {
+            hour = totalTime / 3600;
+            totalTime = totalTime - 3600 * hour;
+        }
+        if (60 <= totalTime) {
+            minute = totalTime / 60;
+            totalTime = totalTime - 60 * minute;
+        }
+        if (0 <= totalTime) {
+            second = totalTime;
+        }
+        StringBuilder sb = new StringBuilder();
+        if (hour < 10) {
+            sb.append("0").append(hour).append(":");
+        } else {
+            sb.append(hour).append(":");
+        }
+        if (minute < 10) {
+            sb.append("0").append(minute).append(":");
+        } else {
+            sb.append(minute).append(":");
+        }
+        if (second < 10) {
+            sb.append("0").append(second);
+        } else {
+            sb.append(second);
+        }
+        return sb.toString();
+    }
+
+
+    /**
+     * 将毫秒换成 00:00 或者 00,这个根据具体时间来计算
+     * @param time                          毫秒
+     * @return                              时间字符串
+     */
+    public static String getCountTime(long time) {
+        //秒
+        long totalTime = time / 1000;
+        //时,分,秒
+        long hour = 0, minute = 0, second = 0;
+
+        if (3600 <= totalTime) {
+            hour = totalTime / 3600;
+            totalTime = totalTime - 3600 * hour;
+        }
+        if (60 <= totalTime) {
+            minute = totalTime / 60;
+            totalTime = totalTime - 60 * minute;
+        }
+        if (0 <= totalTime) {
+            second = totalTime;
+        }
+        StringBuilder sb = new StringBuilder();
+        if (hour>0){
+            if (hour < 10) {
+                sb.append("0").append(hour).append(":");
+            } else {
+                sb.append(hour).append(":");
+            }
+        }
+        if (minute>0){
+            if (minute < 10) {
+                sb.append("0").append(minute).append(":");
+            } else {
+                sb.append(minute).append(":");
+            }
+        }
+        if (second < 10) {
+            sb.append("0").append(second);
+        } else {
+            sb.append(second);
+        }
+        return sb.toString();
+    }
+
+}

+ 29 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/tool/timer/TimerListener.java

@@ -0,0 +1,29 @@
+package org.yczbj.ycvideoplayerlib.tool.timer;
+
+/**
+ * <pre>
+ *     @author  yangchong
+ *     email  : yangchong211@163.com
+ *     time  :  2020/5/26
+ *     desc  :  倒计时监听器
+ *     revise:
+ * </pre>
+ */
+public interface TimerListener {
+
+    /**
+     * 当倒计时开始
+     */
+    void onStart();
+
+    /**
+     * 当倒计时结束
+     */
+    void onFinish();
+
+    /**
+     * @param millisUntilFinished 剩余时间
+     */
+    void onTick(long millisUntilFinished);
+
+}

+ 8 - 5
YCVideoPlayerLib/src/main/java/org/yczbj/ycvideoplayerlib/view/BaseToast.java → VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/tool/toast/BaseToast.java

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 limitations under the License.
 */
 */
 
 
-package org.yczbj.ycvideoplayerlib.view;
+package org.yczbj.ycvideoplayerlib.tool.toast;
 
 
 import android.annotation.SuppressLint;
 import android.annotation.SuppressLint;
 import android.app.Application;
 import android.app.Application;
@@ -22,10 +22,13 @@ import android.content.Context;
 import android.graphics.Color;
 import android.graphics.Color;
 import android.os.Build;
 import android.os.Build;
 import android.os.Looper;
 import android.os.Looper;
-import android.support.annotation.ColorInt;
-import android.support.annotation.LayoutRes;
-import android.support.annotation.NonNull;
-import android.support.v7.widget.CardView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.cardview.widget.CardView;
+
 import android.text.TextUtils;
 import android.text.TextUtils;
 import android.view.Gravity;
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.LayoutInflater;

+ 146 - 0
VideoPlayer/src/main/java/org/yczbj/ycvideoplayerlib/tool/utils/CutoutUtil.java

@@ -0,0 +1,146 @@
+package org.yczbj.ycvideoplayerlib.tool.utils;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Build;
+import android.view.DisplayCutout;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+
+import java.lang.reflect.Method;
+import java.util.List;
+
+/**
+ * 刘海屏工具
+ */
+public final class CutoutUtil {
+
+    private CutoutUtil() {
+    }
+
+    /**
+     * 是否为允许全屏界面显示内容到刘海区域的刘海屏机型(与AndroidManifest中配置对应)
+     */
+    public static boolean allowDisplayToCutout(Activity activity) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            // 9.0系统全屏界面默认会保留黑边,不允许显示内容到刘海区域
+            Window window = activity.getWindow();
+            WindowInsets windowInsets = window.getDecorView().getRootWindowInsets();
+            if (windowInsets == null) {
+                return false;
+            }
+            DisplayCutout displayCutout = windowInsets.getDisplayCutout();
+            if (displayCutout == null) {
+                return false;
+            }
+            List<Rect> boundingRects = displayCutout.getBoundingRects();
+            return boundingRects.size() > 0;
+        } else {
+            return hasCutoutHuawei(activity)
+                    || hasCutoutOPPO(activity)
+                    || hasCutoutVIVO(activity)
+                    || hasCutoutXIAOMI(activity);
+        }
+    }
+
+    /**
+     * 是否是华为刘海屏机型
+     */
+    @SuppressWarnings("unchecked")
+    private static boolean hasCutoutHuawei(Activity activity) {
+        if (!Build.MANUFACTURER.equalsIgnoreCase("HUAWEI")) {
+            return false;
+        }
+        try {
+            ClassLoader cl = activity.getClassLoader();
+            Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
+            if (HwNotchSizeUtil != null) {
+                Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen");
+                return (boolean) get.invoke(HwNotchSizeUtil);
+            }
+            return false;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /**
+     * 是否是oppo刘海屏机型
+     */
+    private static boolean hasCutoutOPPO(Activity activity) {
+        if (!Build.MANUFACTURER.equalsIgnoreCase("oppo")) {
+            return false;
+        }
+        return activity.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
+    }
+
+    /**
+     * 是否是vivo刘海屏机型
+     */
+    @SuppressWarnings("unchecked")
+    @SuppressLint("PrivateApi")
+    private static boolean hasCutoutVIVO(Activity activity) {
+        if (!Build.MANUFACTURER.equalsIgnoreCase("vivo")) {
+            return false;
+        }
+        try {
+            ClassLoader cl = activity.getClassLoader();
+            Class ftFeatureUtil = cl.loadClass("android.util.FtFeature");
+            if (ftFeatureUtil != null) {
+                Method get = ftFeatureUtil.getMethod("isFeatureSupport", int.class);
+                return (boolean) get.invoke(ftFeatureUtil, 0x00000020);
+            }
+            return false;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /**
+     * 是否是小米刘海屏机型
+     */
+    @SuppressWarnings("unchecked")
+    @SuppressLint("PrivateApi")
+    private static boolean hasCutoutXIAOMI(Activity activity) {
+        if (!Build.MANUFACTURER.equalsIgnoreCase("xiaomi")) {
+            return false;
+        }
+        try {
+            ClassLoader cl = activity.getClassLoader();
+            Class SystemProperties = cl.loadClass("android.os.SystemProperties");
+            Class[] paramTypes = new Class[2];
+            paramTypes[0] = String.class;
+            paramTypes[1] = int.class;
+            Method getInt = SystemProperties.getMethod("getInt", paramTypes);
+            //参数
+            Object[] params = new Object[2];
+            params[0] = "ro.miui.notch";
+            params[1] = 0;
+            int hasCutout = (int) getInt.invoke(SystemProperties, params);
+            return hasCutout == 1;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /**
+     * 适配刘海屏,针对Android P以上系统
+     */
+    public static void adaptCutoutAboveAndroidP(Context context, boolean isAdapt) {
+        Activity activity = PlayerUtils.scanForActivity(context);
+        if (activity == null) return;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
+            if (isAdapt) {
+                lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+            } else {
+                lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
+            }
+            activity.getWindow().setAttributes(lp);
+        }
+    }
+
+}

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません