zhaoyadi 2 gadi atpakaļ
vecāks
revīzija
e735795e44
100 mainītis faili ar 6207 papildinājumiem un 0 dzēšanām
  1. 72 0
      kit/build.gradle
  2. 21 0
      kit/proguard-rules.pro
  3. 158 0
      kit/schemas/com.tencent.liteav.demo.superplayer.database.PlayerDatabase/1.json
  4. 12 0
      kit/src/main/AndroidManifest.xml
  5. 11 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/SuperPlayerCode.java
  6. 23 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/SuperPlayerDef.java
  7. 75 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/SuperPlayerGlobalConfig.java
  8. 32 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/SuperPlayerModel.java
  9. 617 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/SuperPlayerView.java
  10. 104 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/player/SuperPlayer.java
  11. 671 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/player/SuperPlayerImpl.java
  12. 57 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/player/SuperPlayerObserver.java
  13. 128 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/AbsPlayer.java
  14. 179 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/CastingPlayer.java
  15. 209 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/FullScreenPlayer.java
  16. 139 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/Player.java
  17. 87 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/TimeOutMPlayer.java
  18. 87 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/TimeOutPlayer.java
  19. 472 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/WindowPlayer.java
  20. 59 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/view/PlayerRelativeLayout.java
  21. 260 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/view/PointProgressBar.java
  22. 85 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/view/RoundImageView.java
  23. 138 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/view/UnlockProgressView.java
  24. 117 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/view/VideoProgressLayout.java
  25. 85 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/view/VolumeBrightnessProgressLayout.java
  26. 384 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/view/WheelView.java
  27. 91 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/util/CountDownUtil.java
  28. 117 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/util/TimerUtil.java
  29. 199 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/util/VideoGestureDetector.java
  30. 34 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/PlayerApplication.java
  31. 77 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/PlayerDatabase.kt
  32. 37 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/PlayerDatabaseProvider.kt
  33. 25 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/dao/CountDownDao.kt
  34. 43 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/dao/HistoryDao.kt
  35. 23 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/dao/RecordDao.kt
  36. 69 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/entity/CountDown.kt
  37. 54 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/entity/History.kt
  38. 23 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/entity/Record.kt
  39. 94 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/repo/PlayerRepository.kt
  40. 48 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/player/CastObject.kt
  41. 30 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/util/CastorUtil.kt
  42. 24 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/util/HistoryUtil.kt
  43. 18 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/util/HttpUtils.kt
  44. 64 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/util/PlayerUtil.kt
  45. 23 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/util/TimeoutUtil.kt
  46. 31 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/util/TimersUtil.kt
  47. BIN
      kit/src/main/res/drawable-xxhdpi/superplayer_ic_light_max.png
  48. BIN
      kit/src/main/res/drawable-xxhdpi/superplayer_ic_light_min.png
  49. BIN
      kit/src/main/res/drawable-xxhdpi/superplayer_ic_vod_play_normal.png
  50. BIN
      kit/src/main/res/drawable-xxhdpi/superplayer_ic_volume_max.png
  51. BIN
      kit/src/main/res/drawable-xxhdpi/superplayer_ic_volume_min.png
  52. 13 0
      kit/src/main/res/drawable/casting_cancel.xml
  53. 11 0
      kit/src/main/res/drawable/free_label.xml
  54. 11 0
      kit/src/main/res/drawable/item_vod.xml
  55. 11 0
      kit/src/main/res/drawable/item_vod_select.xml
  56. 44 0
      kit/src/main/res/drawable/list_progress_bar.xml
  57. 5 0
      kit/src/main/res/drawable/player_full_bg.xml
  58. 7 0
      kit/src/main/res/drawable/player_window_bg.xml
  59. 13 0
      kit/src/main/res/drawable/shadow_bottom.xml
  60. 11 0
      kit/src/main/res/drawable/timeout_back_chapter_deco.xml
  61. 10 0
      kit/src/main/res/drawable/timeout_resume_deco.xml
  62. 6 0
      kit/src/main/res/drawable/unlock_bg.xml
  63. 20 0
      kit/src/main/res/drawable/vod_video_progress.xml
  64. 79 0
      kit/src/main/res/layout/superplayer_item_vod.xml
  65. 35 0
      kit/src/main/res/layout/superplayer_video_progress_layout.xml
  66. 28 0
      kit/src/main/res/layout/superplayer_video_volume_brightness_progress_layout.xml
  67. 127 0
      kit/src/main/res/layout/superplayer_vod_player_casting.xml
  68. 46 0
      kit/src/main/res/layout/superplayer_vod_player_fullscreen.xml
  69. 78 0
      kit/src/main/res/layout/superplayer_vod_player_timeout.xml
  70. 78 0
      kit/src/main/res/layout/superplayer_vod_player_timeout_m.xml
  71. 141 0
      kit/src/main/res/layout/superplayer_vod_player_window.xml
  72. 27 0
      kit/src/main/res/layout/superplayer_vod_view.xml
  73. BIN
      kit/src/main/res/mipmap-hdpi/dog_track.png
  74. BIN
      kit/src/main/res/mipmap-hdpi/full_screen.png
  75. BIN
      kit/src/main/res/mipmap-hdpi/full_unlock.png
  76. BIN
      kit/src/main/res/mipmap-hdpi/icon_pause.png
  77. BIN
      kit/src/main/res/mipmap-hdpi/icon_resume.png
  78. BIN
      kit/src/main/res/mipmap-hdpi/lock_back.png
  79. BIN
      kit/src/main/res/mipmap-hdpi/pause_state.png
  80. BIN
      kit/src/main/res/mipmap-hdpi/play_next.png
  81. BIN
      kit/src/main/res/mipmap-hdpi/play_state.png
  82. BIN
      kit/src/main/res/mipmap-hdpi/timeout_bg.png
  83. BIN
      kit/src/main/res/mipmap-hdpi/timeout_logo.png
  84. BIN
      kit/src/main/res/mipmap-hdpi/timeout_resume.png
  85. BIN
      kit/src/main/res/mipmap-hdpi/video_lock.png
  86. BIN
      kit/src/main/res/mipmap-hdpi/window_back.png
  87. BIN
      kit/src/main/res/mipmap-mdpi/dog_track.png
  88. BIN
      kit/src/main/res/mipmap-mdpi/full_screen.png
  89. BIN
      kit/src/main/res/mipmap-mdpi/full_unlock.png
  90. BIN
      kit/src/main/res/mipmap-mdpi/icon_pause.png
  91. BIN
      kit/src/main/res/mipmap-mdpi/icon_resume.png
  92. BIN
      kit/src/main/res/mipmap-mdpi/lock_back.png
  93. BIN
      kit/src/main/res/mipmap-mdpi/pause_state.png
  94. BIN
      kit/src/main/res/mipmap-mdpi/play_next.png
  95. BIN
      kit/src/main/res/mipmap-mdpi/play_state.png
  96. BIN
      kit/src/main/res/mipmap-mdpi/timeout_bg.png
  97. BIN
      kit/src/main/res/mipmap-mdpi/timeout_logo.png
  98. BIN
      kit/src/main/res/mipmap-mdpi/timeout_resume.png
  99. BIN
      kit/src/main/res/mipmap-mdpi/video_lock.png
  100. BIN
      kit/src/main/res/mipmap-mdpi/window_back.png

+ 72 - 0
kit/build.gradle

@@ -0,0 +1,72 @@
+plugins {
+    id 'com.android.library'
+    id 'kotlin-android'
+    id 'kotlin-kapt'
+}
+
+android {
+    compileSdkVersion rootProject.ext.compileSdkVersion
+
+    defaultConfig {
+        minSdkVersion rootProject.ext.minSdkVersion
+        targetSdkVersion rootProject.ext.targetSdkVersion
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+
+        javaCompileOptions {
+            annotationProcessorOptions {
+                arguments += [
+                        "room.schemaLocation": "$projectDir/schemas".toString()
+                ]
+            }
+        }
+    }
+
+    buildFeatures {
+        viewBinding true
+    }
+
+    sourceSets {
+        main.java.srcDirs += 'src/main/kotlin'
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+    packagingOptions {
+        exclude 'META-INF/beans.xml'
+    }
+    compileOptions {
+        sourceCompatibility 1.8
+        targetCompatibility 1.8
+    }
+
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+    api 'com.tencent.liteav:LiteAVSDK_Player:10.5.0.11177'
+    implementation 'com.github.devin1014.DLNA-Cast:dlna-dmc:V1.0.0'
+
+    implementation 'androidx.appcompat:appcompat:1.4.2'
+    implementation 'androidx.recyclerview:recyclerview:1.2.1'
+    api 'com.github.bumptech.glide:glide:4.12.0'
+    implementation 'androidx.exifinterface:exifinterface:1.3.3'
+    api 'androidx.constraintlayout:constraintlayout:2.1.4'
+
+    def room_version = "2.4.0"
+    implementation "androidx.room:room-runtime:$room_version"
+    kapt "androidx.room:room-compiler:$room_version"
+    implementation("androidx.room:room-ktx:$room_version")
+
+    implementation "androidx.core:core-ktx:1.7.0"
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
+    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0'
+    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
+}

+ 21 - 0
kit/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

+ 158 - 0
kit/schemas/com.tencent.liteav.demo.superplayer.database.PlayerDatabase/1.json

@@ -0,0 +1,158 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 1,
+    "identityHash": "fe898d5cfe7a453225cbced50ee01829",
+    "entities": [
+      {
+        "tableName": "History",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `course_id` TEXT NOT NULL, `section_id` TEXT NOT NULL, `date` TEXT NOT NULL, `ct_id` TEXT)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "start",
+            "columnName": "start",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "end",
+            "columnName": "end",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "courseId",
+            "columnName": "course_id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "sectionId",
+            "columnName": "section_id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "date",
+            "columnName": "date",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "countDown",
+            "columnName": "ct_id",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "CountDown",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` INTEGER NOT NULL, `value` INTEGER NOT NULL, `rest` INTEGER NOT NULL, `datetime` TEXT, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "type",
+            "columnName": "type",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "value",
+            "columnName": "value",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "rest",
+            "columnName": "rest",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "datetime",
+            "columnName": "datetime",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "Record",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`aiCourseItemId` TEXT NOT NULL, `intervals` TEXT NOT NULL, `lastTime` INTEGER NOT NULL, `validDuration` INTEGER NOT NULL, `courseId` TEXT NOT NULL, PRIMARY KEY(`aiCourseItemId`))",
+        "fields": [
+          {
+            "fieldPath": "aiCourseItemId",
+            "columnName": "aiCourseItemId",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "intervals",
+            "columnName": "intervals",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "lastTime",
+            "columnName": "lastTime",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "validDuration",
+            "columnName": "validDuration",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "courseId",
+            "columnName": "courseId",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "aiCourseItemId"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fe898d5cfe7a453225cbced50ee01829')"
+    ]
+  }
+}

+ 12 - 0
kit/src/main/AndroidManifest.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.tencent.liteav.demo.superplayer">
+
+    <uses-permission android:name="android.permission.VIBRATE" />
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
+    <uses-permission android:name="android.permission.WAKE_LOCK"/>
+
+</manifest>

+ 11 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/SuperPlayerCode.java

@@ -0,0 +1,11 @@
+package com.tencent.liteav.demo.superplayer;
+
+public class SuperPlayerCode {
+    public static final int OK                       = 0;
+    public static final int NET_ERROR                = 10001;
+    public static final int PLAY_URL_EMPTY           = 20001;
+    public static final int LIVE_PLAY_END            = 30001;
+    public static final int LIVE_SHIFT_FAIL          = 30002;
+    public static final int VOD_PLAY_FAIL            = 40001;
+    public static final int VOD_REQUEST_FILE_ID_FAIL = 40002;
+}

+ 23 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/SuperPlayerDef.java

@@ -0,0 +1,23 @@
+package com.tencent.liteav.demo.superplayer;
+
+public class SuperPlayerDef {
+
+    public enum PlayerMode {
+        WINDOW,     // 窗口模式
+        FULLSCREEN, // 全屏模式
+        NO_LOCK, // 全屏模式但不锁定
+        CASTING, // 投屏模式
+    }
+
+    public enum PlayerState {
+        INIT,       //初始状态
+        PLAYING,    // 播放中
+        PAUSE,      // 暂停中
+        LOADING,    // 缓冲中
+        END         // 结束播放
+    }
+
+    public enum VerifyReason {
+        TIMEOUT, // 重置定时
+    }
+}

+ 75 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/SuperPlayerGlobalConfig.java

@@ -0,0 +1,75 @@
+package com.tencent.liteav.demo.superplayer;
+
+import com.tencent.rtmp.TXLiveConstants;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by yuejiaoli on 2018/7/4.
+ * <p>
+ * 超级播放器全局配置类
+ */
+
+public class SuperPlayerGlobalConfig {
+
+    private static class Singleton {
+        private static SuperPlayerGlobalConfig sInstance = new SuperPlayerGlobalConfig();
+    }
+
+    public static SuperPlayerGlobalConfig getInstance() {
+        return Singleton.sInstance;
+    }
+
+    /**
+     * 默认播放填充模式 ( 默认播放模式为 自适应模式 )
+     */
+    public int renderMode = TXLiveConstants.RENDER_MODE_ADJUST_RESOLUTION;
+
+    /**
+     * 播放器最大缓存个数 ( 默认缓存 5 )
+     */
+    public int maxCacheItem = 5;
+
+    /**
+     * 是否开启硬件加速 ( 默认开启硬件加速 )
+     */
+    public boolean enableHWAcceleration = true;
+
+    /**
+     * 是否静音
+     */
+    public boolean mute = false;
+
+    /**
+     * 是否开启镜面
+     */
+    public boolean mirror = false;
+
+    /**
+     * 请求header
+     */
+    public Map<String,String> headers = new HashMap<>();
+
+    /**
+     * 播放速率
+     */
+    public float playRate = 1.0F;
+
+    public final static class TXRect {
+        public int x;
+        public int y;
+        public int width;
+        public int height;
+
+        TXRect(int x, int y, int width, int height) {
+            this.x = x;
+            this.y = y;
+            this.width = width;
+            this.height = height;
+        }
+
+        public TXRect() {
+        }
+    }
+}

+ 32 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/SuperPlayerModel.java

@@ -0,0 +1,32 @@
+package com.tencent.liteav.demo.superplayer;
+
+/**
+ * 超级播放器支持三种方式播放视频:
+ * 1. 视频 URL
+ * 填写视频 URL, 如需使用直播时移功能,还需填写appId
+ * 2. 腾讯云点播 File ID 播放
+ * 填写 appId 及 videoId (如果使用旧版本V2, 请填写videoIdV2)
+ * 3. 多码率视频播放
+ * 是URL播放方式扩展,可同时传入多条URL,用于进行码率切换
+ */
+public class SuperPlayerModel {
+    public String sectionId = "";
+
+    public String url = "";      // 视频URL
+
+    public String title = "";    // 视频文件名 (用于显示在UI层);
+
+    public String cover;
+
+    public int duration;
+
+    public boolean isEnableCache = false; // 是否开启缓存能力,默认关闭
+
+    public boolean isLast = false;
+
+    public boolean isLock = false;
+
+    public boolean isFree = false;
+
+    public long lastPlay = 0;
+}

+ 617 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/SuperPlayerView.java

@@ -0,0 +1,617 @@
+package com.tencent.liteav.demo.superplayer;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.RelativeLayout;
+
+import com.tencent.liteav.demo.superplayer.player.SuperPlayer;
+import com.tencent.liteav.demo.superplayer.player.SuperPlayerImpl;
+import com.tencent.liteav.demo.superplayer.player.SuperPlayerObserver;
+import com.tencent.liteav.demo.superplayer.ui.player.CastingPlayer;
+import com.tencent.liteav.demo.superplayer.ui.player.FullScreenPlayer;
+import com.tencent.liteav.demo.superplayer.ui.player.Player;
+import com.tencent.liteav.demo.superplayer.ui.player.WindowPlayer;
+import com.tencent.liteav.demo.superplayer.util.CastorUtil;
+import com.tencent.liteav.demo.superplayer.util.TimeoutUtil;
+import com.tencent.rtmp.ui.TXCloudVideoView;
+
+import org.fourthline.cling.model.meta.Device;
+
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Listener {
+    private static final String TAG = "SuperPlayerView";
+
+    private ViewGroup mRootView;                                 // SuperPlayerView的根view
+
+    private TXCloudVideoView mTXCloudVideoView;                         // 腾讯云视频播放view
+    private FullScreenPlayer mFullScreenPlayer;                         // 全屏模式控制view
+    private WindowPlayer mWindowPlayer;                                 // 窗口模式控制view
+    private CastingPlayer mCastingPlayer;                               // 投屏模式控制view
+
+    private ViewGroup.LayoutParams mLayoutParamWindowMode;              // 窗口播放时SuperPlayerView的布局参数
+    private ViewGroup.LayoutParams mLayoutParamFullScreenMode;          // 全屏播放时SuperPlayerView的布局参数
+
+    private LayoutParams mVodControllerWindowParams;      // 窗口controller的布局参数
+    private LayoutParams mVodControllerFullScreenParams;  // 全屏controller的布局参数
+
+    private OnPlayerViewCallback mPlayerViewCallback;             // SuperPlayerView回调
+
+    private SuperPlayer mSuperPlayer;                    // 超级播放器
+
+    private int mPlayIndex = -1;                      // 正在播放model的索引
+    private SuperPlayerModel mCurrentSuperPlayerModel;        // 当前正在播放的SuperPlayerModel
+    private List<SuperPlayerModel> mSuperPlayerModelList;           // SuperPlayerModel列表
+
+    private boolean mIsPlayInit;
+    private boolean mIsTimeOut;
+
+    // 正常窗口模式下裁切的圆角值
+    private float topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius;
+
+    public SuperPlayerView(Context context) {
+        this(context, null);
+    }
+
+    public SuperPlayerView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public SuperPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        initView(context);
+        initPlayer(context);
+        initAttrs(context, attrs);
+        setWillNotDraw(false);
+    }
+
+    private void initAttrs(Context context, AttributeSet attrs) {
+        if (attrs != null) {
+            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView);
+            float radius = a.getDimension(R.styleable.RoundImageView_radius, 0f);
+            topLeftRadius = a.getDimension(R.styleable.RoundImageView_topLeftRadius, radius);
+            topRightRadius = a.getDimension(R.styleable.RoundImageView_topRightRadius, radius);
+            bottomLeftRadius = a.getDimension(R.styleable.RoundImageView_bottomLeftRadius, radius);
+            bottomRightRadius = a.getDimension(R.styleable.RoundImageView_bottomRightRadius, radius);
+            a.recycle();
+        }
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        TimeoutUtil.INSTANCE.addListener(this);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        TimeoutUtil.INSTANCE.removeListener(this);
+        super.onDetachedFromWindow();
+    }
+
+    /**
+     * 初始化view
+     */
+    private void initView(Context context) {
+        mRootView = (ViewGroup) LayoutInflater.from(context).inflate(R.layout.superplayer_vod_view, null);
+
+        mTXCloudVideoView = mRootView.findViewById(R.id.superplayer_cloud_video_view);
+        mFullScreenPlayer = mRootView.findViewById(R.id.superplayer_controller_large);
+        mWindowPlayer = mRootView.findViewById(R.id.superplayer_controller_small);
+        mCastingPlayer = mRootView.findViewById(R.id.superplayer_controller_cast);
+        //防止stop中空指针异常
+        mSuperPlayerModelList = new ArrayList<>();
+
+        mVodControllerWindowParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+        mVodControllerFullScreenParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+
+        mFullScreenPlayer.setCallback(mControllerCallback);
+        mWindowPlayer.setCallback(mControllerCallback);
+        mCastingPlayer.setCallback(mControllerCallback);
+
+        removeAllViews();
+        mRootView.removeView(mTXCloudVideoView);
+        mRootView.removeView(mCastingPlayer);
+        mRootView.removeView(mWindowPlayer);
+        mRootView.removeView(mFullScreenPlayer);
+
+        addView(mTXCloudVideoView);
+    }
+
+    private void initPlayer(Context context) {
+        mSuperPlayer = new SuperPlayerImpl(context, mTXCloudVideoView);
+        mSuperPlayer.setObserver(new PlayerObserver());
+
+        if (mSuperPlayer.getPlayerMode() == SuperPlayerDef.PlayerMode.FULLSCREEN) {
+            addView(mFullScreenPlayer);
+            mCastingPlayer.hide();
+            mFullScreenPlayer.hide();
+        } else if (mSuperPlayer.getPlayerMode() == SuperPlayerDef.PlayerMode.WINDOW) {
+            addView(mWindowPlayer);
+            mWindowPlayer.hide();
+            mCastingPlayer.hide();
+        } else if (mSuperPlayer.getPlayerMode() == SuperPlayerDef.PlayerMode.CASTING) {
+            addView(mCastingPlayer);
+            mWindowPlayer.hide();
+            mFullScreenPlayer.hide();
+        }
+
+        post(() -> {
+            if (mSuperPlayer.getPlayerMode() == SuperPlayerDef.PlayerMode.WINDOW) {
+                mLayoutParamWindowMode = getLayoutParams();
+            }
+            try {
+                // 依据上层Parent的LayoutParam类型来实例化一个新的fullscreen模式下的LayoutParam
+                Class parentLayoutParamClazz = getLayoutParams().getClass();
+                Constructor constructor = parentLayoutParamClazz.getDeclaredConstructor(int.class, int.class);
+                mLayoutParamFullScreenMode = (ViewGroup.LayoutParams) constructor.newInstance(
+                        ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        });
+    }
+
+    public void setupModelList(List<SuperPlayerModel> models) {
+        mSuperPlayerModelList = models;
+    }
+
+    public void maybePlayIndexModel(int index) {
+        int playIndex = (index) % mSuperPlayerModelList.size();
+        SuperPlayerModel playModel = mSuperPlayerModelList.get(playIndex);
+
+        if (playIndex == mPlayIndex){
+//            mSuperPlayer.resume();
+            return;
+        }
+
+        mPlayerViewCallback.onPlayPrepare(playIndex, playModel);
+    }
+
+    public void playIndexModel(int index) {
+        playModelInList(index);
+    }
+
+    /* 播放下一个 */
+    private void playNextVideo() {
+        maybePlayIndexModel(++mPlayIndex);
+    }
+
+    /* 播放指定的 */
+    private void playModelInList(int index) {
+        if (mIsTimeOut) return;
+        if ((index >= mSuperPlayerModelList.size())) return;
+
+        mPlayIndex = (index) % mSuperPlayerModelList.size();
+        mIsPlayInit = false;
+        mSuperPlayer.stop();
+        mPlayIndex = index;
+        updateNextButton();
+        mCurrentSuperPlayerModel = mSuperPlayerModelList.get(mPlayIndex);
+        playWithModelInner(mCurrentSuperPlayerModel);
+        mIsPlayInit = true;
+    }
+
+    private void updateNextButton() {
+        if (mPlayIndex < mSuperPlayerModelList.size() - 1) {
+            mWindowPlayer.setPlayNextButtonVisibility(true);
+            mFullScreenPlayer.setPlayNextButtonVisibility(true);
+        } else {
+            mWindowPlayer.setPlayNextButtonVisibility(false);
+            mFullScreenPlayer.setPlayNextButtonVisibility(false);
+        }
+    }
+
+    private void playWithModelInner(SuperPlayerModel model) {
+        if (mIsTimeOut) return;
+
+        if (mPlayerViewCallback != null) {
+            mPlayerViewCallback.onPlayIndex(mPlayIndex, model);
+        }
+
+        mSuperPlayer.play(model);
+        mFullScreenPlayer.preparePlayVideo(model);
+        mWindowPlayer.preparePlayVideo(model);
+    }
+
+    /**
+     * resume生命周期回调
+     */
+    public void onResume() {
+        if (mSuperPlayer.getPlayerState() == SuperPlayerDef.PlayerState.INIT) {
+            return;
+        }
+        mSuperPlayer.resume();
+    }
+
+    /**
+     * pause生命周期回调
+     */
+    public void onPause() {
+        if (mSuperPlayer.getPlayerState() == SuperPlayerDef.PlayerState.INIT) {
+            return;
+        }
+        mSuperPlayer.pause();
+    }
+
+    /**
+     * 重置播放器
+     */
+    public void resetPlayer() {
+        mSuperPlayerModelList.clear();
+        stopPlay();
+    }
+
+    /**
+     * 停止播放
+     */
+    public void stopPlay() {
+        mIsPlayInit = false;
+        mSuperPlayer.stop();
+    }
+
+    public void startCast(Device device) {
+        switchPlayMode(SuperPlayerDef.PlayerMode.CASTING);
+        mSuperPlayer.startCast(device);
+    }
+
+    public void stopCast() {
+        mIsPlayInit = false;
+        mSuperPlayer.stopCast();
+        switchPlayMode(SuperPlayerDef.PlayerMode.WINDOW);
+        mIsPlayInit = true;
+        CastorUtil.INSTANCE.selectDevice(null);
+    }
+
+    @Override
+    public void onTimeout(boolean timeout) {
+        this.mIsTimeOut = timeout;
+        setNeedToPause(timeout);
+
+        if (timeout) {
+            switchPlayMode(SuperPlayerDef.PlayerMode.FULLSCREEN);
+        }
+
+        mSuperPlayer.onTimeout(timeout);
+        mFullScreenPlayer.onTimeout(timeout);
+        mWindowPlayer.onTimeout(timeout);
+    }
+
+    /**
+     * 设置超级播放器的回调
+     *
+     * @param callback
+     */
+    public void setPlayerViewCallback(OnPlayerViewCallback callback) {
+        mPlayerViewCallback = callback;
+    }
+
+    public void switchPlayMode(SuperPlayerDef.PlayerMode playerMode) {
+        if (mControllerCallback != null) {
+            mControllerCallback.onSwitchPlayMode(playerMode);
+        }
+    }
+
+    private void handleSwitchPlayMode(SuperPlayerDef.PlayerMode playerMode) {
+        if (playerMode == getPlayerMode()) {
+            return;
+        }
+
+        mFullScreenPlayer.hide();
+        mWindowPlayer.hide();
+        mCastingPlayer.hide();
+
+        if (playerMode == SuperPlayerDef.PlayerMode.FULLSCREEN) {
+            onSwitchFullMode(playerMode,true);
+        } else if (playerMode == SuperPlayerDef.PlayerMode.NO_LOCK) {
+            onSwitchFullMode(playerMode,false);
+        } else if (playerMode == SuperPlayerDef.PlayerMode.WINDOW) {
+            onSwitchWindowMode(playerMode);
+        } else if (playerMode == SuperPlayerDef.PlayerMode.CASTING) {
+            onSwitchCastingMode(playerMode);
+        }
+    }
+
+    private void onSwitchFullMode(SuperPlayerDef.PlayerMode playerMode, boolean needLock) {
+        if (mLayoutParamFullScreenMode == null) {
+            return;
+        }
+
+        mFullScreenPlayer.setLock(needLock);
+        removeView(mWindowPlayer);
+        removeView(mCastingPlayer);
+        addView(mFullScreenPlayer, mVodControllerFullScreenParams);
+        setLayoutParams(mLayoutParamFullScreenMode);
+        if (mPlayerViewCallback != null) {
+            mPlayerViewCallback.onStartFullScreenPlay();
+        }
+
+        mSuperPlayer.switchPlayMode(playerMode);
+    }
+
+    private void onSwitchWindowMode(SuperPlayerDef.PlayerMode playerMode) {
+        if (mLayoutParamWindowMode == null) {
+            return;
+        }
+
+        removeView(mFullScreenPlayer);
+        removeView(mCastingPlayer);
+        addView(mWindowPlayer, mVodControllerWindowParams);
+        setLayoutParams(mLayoutParamWindowMode);
+        if (mPlayerViewCallback != null) {
+            mPlayerViewCallback.onStopFullScreenPlay();
+        }
+
+        mSuperPlayer.switchPlayMode(playerMode);
+    }
+
+    private void onSwitchCastingMode(SuperPlayerDef.PlayerMode playerMode) {
+        if (mLayoutParamWindowMode == null) {
+            return;
+        }
+
+        removeView(mWindowPlayer);
+        removeView(mFullScreenPlayer);
+        addView(mCastingPlayer, mVodControllerWindowParams);
+        setLayoutParams(mLayoutParamWindowMode);
+        if (mPlayerViewCallback != null) {
+            mPlayerViewCallback.onStopFullScreenPlay();
+        }
+
+        mSuperPlayer.switchPlayMode(playerMode);
+    }
+
+    /**
+     * 初始化controller回调
+     */
+    private Player.Callback mControllerCallback = new Player.Callback() {
+        @Override
+        public void onSwitchPlayMode(SuperPlayerDef.PlayerMode playerMode) {
+            handleSwitchPlayMode(playerMode);
+        }
+
+        @Override
+        public void onBackPressed(SuperPlayerDef.PlayerMode playMode) {
+            switch (playMode) {
+                case FULLSCREEN:// 当前是全屏模式,返回切换成窗口模式
+                    onSwitchPlayMode(SuperPlayerDef.PlayerMode.WINDOW);
+                    break;
+                case WINDOW:// 当前是窗口模式,返回退出播放器
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        @Override
+        public void onPause() {
+            mSuperPlayer.pause();
+        }
+
+        @Override
+        public void onResume() {
+            handleResume();
+        }
+
+        @Override
+        public void verify(SuperPlayerDef.VerifyReason reason) {
+            if (mPlayerViewCallback != null) {
+                mPlayerViewCallback.onVerify(reason);
+            }
+        }
+
+        @Override
+        public void onSeekTo(int position) {
+            mSuperPlayer.seek(position);
+        }
+
+        @Override
+        public void playNext() {
+            playNextVideo();
+        }
+
+        @Override
+        public void stopCast() {
+            SuperPlayerView.this.stopCast();
+        }
+
+        @Override
+        public List<SuperPlayerModel> getPlayList() {
+            if (null == mSuperPlayerModelList || mSuperPlayerModelList.isEmpty()) {
+                return new ArrayList<SuperPlayerModel>() {{
+                    add(mCurrentSuperPlayerModel);
+                }};
+            }
+            return mSuperPlayerModelList;
+        }
+
+        @Override
+        public SuperPlayerModel getPlayingVideoModel() {
+            return mCurrentSuperPlayerModel;
+        }
+    };
+
+    public SuperPlayerModel getPlayingVideoModel() {
+        return mCurrentSuperPlayerModel;
+    }
+
+    private void handleResume() {
+        if (mSuperPlayer.getPlayerState() == SuperPlayerDef.PlayerState.LOADING) {
+            mSuperPlayer.resume();
+        } else if (mSuperPlayer.getPlayerState() == SuperPlayerDef.PlayerState.INIT) {
+            mSuperPlayer.resume();
+        } else if (mSuperPlayer.getPlayerState() == SuperPlayerDef.PlayerState.END) { //重播
+            mSuperPlayer.restart();
+        } else if (mSuperPlayer.getPlayerState() == SuperPlayerDef.PlayerState.PAUSE) { //继续播放
+            mSuperPlayer.resume();
+        }
+    }
+
+    /**
+     * SuperPlayerView的回调接口
+     */
+    public interface OnPlayerViewCallback {
+
+        /**
+         * 开始全屏播放
+         */
+        void onStartFullScreenPlay();
+
+        /**
+         * 结束全屏播放
+         */
+        void onStopFullScreenPlay();
+
+        /**
+         * 播放下一个视频------
+         */
+        void onPlayIndex(int index, SuperPlayerModel model);
+
+        /**
+         * 检查是否可以播放
+         */
+        void onPlayPrepare(int index, SuperPlayerModel model);
+
+        /**
+         * 弹出验证框
+         */
+        void onVerify(SuperPlayerDef.VerifyReason reason);
+    }
+
+    public void release() {
+        if (mWindowPlayer != null) {
+            mWindowPlayer.release();
+        }
+        if (mFullScreenPlayer != null) {
+            mFullScreenPlayer.release();
+        }
+        if (mCastingPlayer != null) {
+            mCastingPlayer.release();
+        }
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        super.finalize();
+        try {
+            release();
+        } catch (Throwable e) {
+            Log.e(TAG, Log.getStackTraceString(e));
+        }
+    }
+
+
+    public SuperPlayerDef.PlayerMode getPlayerMode() {
+        return mSuperPlayer.getPlayerMode();
+    }
+
+    public SuperPlayerDef.PlayerState getPlayerState() {
+        return mSuperPlayer.getPlayerState();
+    }
+
+
+    class PlayerObserver extends SuperPlayerObserver {
+
+        @Override
+        public void onPlayPrepare() {
+            mWindowPlayer.updatePlayState(SuperPlayerDef.PlayerState.INIT);
+            mFullScreenPlayer.updatePlayState(SuperPlayerDef.PlayerState.INIT);
+            mCastingPlayer.updatePlayState(SuperPlayerDef.PlayerState.INIT);
+
+            mWindowPlayer.showBackground();
+            mFullScreenPlayer.showBackground();
+        }
+
+        @Override
+        public void onPlayBegin(String name) {
+            mWindowPlayer.updatePlayState(SuperPlayerDef.PlayerState.PLAYING);
+            mFullScreenPlayer.updatePlayState(SuperPlayerDef.PlayerState.PLAYING);
+            mCastingPlayer.updatePlayState(SuperPlayerDef.PlayerState.PLAYING);
+
+            mWindowPlayer.hideBackground();
+            mFullScreenPlayer.hideBackground();
+        }
+
+        @Override
+        public void onPlayPause() {
+            mWindowPlayer.updatePlayState(SuperPlayerDef.PlayerState.PAUSE);
+            mFullScreenPlayer.updatePlayState(SuperPlayerDef.PlayerState.PAUSE);
+            mCastingPlayer.updatePlayState(SuperPlayerDef.PlayerState.PAUSE);
+        }
+
+        @Override
+        public void onPlayStop() {
+            if (mIsPlayInit) {
+                playNextVideo();
+            }
+        }
+
+        @Override
+        public void onPlayLoading() {
+            mWindowPlayer.updatePlayState(SuperPlayerDef.PlayerState.LOADING);
+            mFullScreenPlayer.updatePlayState(SuperPlayerDef.PlayerState.LOADING);
+            mCastingPlayer.updatePlayState(SuperPlayerDef.PlayerState.LOADING);
+        }
+
+        @Override
+        public void onPlayProgress(long current, long duration) {
+            mWindowPlayer.updateVideoProgress(current, duration);
+            mFullScreenPlayer.updateVideoProgress(current, duration);
+            mCastingPlayer.updateVideoProgress(current, duration);
+        }
+
+        @Override
+        public void onSeek(int position) {
+
+        }
+
+        @Override
+        public void onError(int code, String message) {
+        }
+
+        @Override
+        public void onRcvFirstIframe() {
+            super.onRcvFirstIframe();
+            mWindowPlayer.toggleCoverView(false);
+            mFullScreenPlayer.toggleCoverView(false);
+        }
+    }
+
+    public void setNeedToPause(boolean value) {
+        mSuperPlayer.setNeedToPause(value);
+    }
+
+    public void setStartTime(double startTime) {
+        mSuperPlayer.setStartTime((float) startTime);
+    }
+
+    /*                                                  */
+    /*----------------正常窗口播放时裁切圆角----------------*/
+    @Override
+    public void draw(Canvas canvas) {
+        canvas.save();
+        canvas.clipPath(getClipPath(getWidth(), getHeight()));
+        super.draw(canvas);
+        canvas.restore();
+    }
+
+    private Path getClipPath(int width, int height) {
+        final Path path = new Path();
+        float[] radiusArray = new float[]{
+                topLeftRadius, topLeftRadius, topRightRadius, topRightRadius,
+                bottomRightRadius, bottomRightRadius, bottomLeftRadius, bottomLeftRadius
+        };
+        path.addRoundRect(new RectF(0, 0, width, height), radiusArray, Path.Direction.CW);
+
+        return path;
+    }
+}

+ 104 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/player/SuperPlayer.java

@@ -0,0 +1,104 @@
+package com.tencent.liteav.demo.superplayer.player;
+
+import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
+import com.tencent.liteav.demo.superplayer.SuperPlayerModel;
+import com.tencent.liteav.demo.superplayer.util.TimeoutUtil;
+
+import org.fourthline.cling.model.meta.Device;
+
+public interface SuperPlayer extends TimeoutUtil.Listener {
+    /**
+     * 开始播放
+     */
+    void play(SuperPlayerModel model);
+
+
+    /**
+     * 重播
+     */
+    void restart();
+
+    /**
+     * 暂停播放
+     */
+    void pause();
+
+    /**
+     * 恢复播放
+     */
+    void resume();
+
+    /**
+     * 停止播放
+     */
+    void stop();
+
+    /**
+     * 重置播放
+     */
+    void reset();
+
+    /**
+     * 跳转位置
+     */
+    void seek(int position);
+
+    /**
+     * 投屏到指定设备
+     */
+    void startCast(Device device);
+
+    /**
+     * 结束投屏
+     */
+    void stopCast();
+
+    /**
+     * 切换播放器模式
+     *
+     * @param playerMode {@link SuperPlayerDef.PlayerMode#WINDOW  }          窗口模式
+     *                   {@link SuperPlayerDef.PlayerMode#FULLSCREEN  }      全屏模式
+     */
+    void switchPlayMode(SuperPlayerDef.PlayerMode playerMode);
+
+    /**
+     * 获取当前播放器模式
+     *
+     * @return {@link SuperPlayerDef.PlayerMode#WINDOW  }          窗口模式
+     * {@link SuperPlayerDef.PlayerMode#FULLSCREEN  }              全屏模式
+     */
+    SuperPlayerDef.PlayerMode getPlayerMode();
+
+    /**
+     * 获取当前播放器状态
+     *
+     * @return {@link SuperPlayerDef.PlayerState#PLAYING  }     播放中
+     * {@link SuperPlayerDef.PlayerState#PAUSE  }               暂停中
+     * {@link SuperPlayerDef.PlayerState#LOADING  }             缓冲中
+     * {@link SuperPlayerDef.PlayerState#END  }                 结束播放
+     */
+    SuperPlayerDef.PlayerState getPlayerState();
+
+    /**
+     * 设置播放器状态回调
+     *
+     * @param observer {@link SuperPlayerObserver}
+     */
+    void setObserver(SuperPlayerObserver observer);
+
+    /**
+     * 设置是否循环
+     *
+     * @param isLoop true循环,false不循环
+     */
+    void setLoop(boolean isLoop);
+
+    /**
+     * 设置开始时间
+     *
+     * @param startPos 开始时间
+     */
+    void setStartTime(float startPos);
+
+    void setNeedToPause(boolean value);
+}

+ 671 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/player/SuperPlayerImpl.java

@@ -0,0 +1,671 @@
+package com.tencent.liteav.demo.superplayer.player;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.cast.dlna.dmc.DLNACastManager;
+import com.android.cast.dlna.dmc.control.ICastInterface;
+import com.tencent.liteav.base.util.TimeFormat;
+import com.tencent.liteav.demo.superplayer.SuperPlayerCode;
+import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
+import com.tencent.liteav.demo.superplayer.SuperPlayerGlobalConfig;
+import com.tencent.liteav.demo.superplayer.SuperPlayerModel;
+import com.tencent.liteav.demo.superplayer.util.CastorUtil;
+import com.tencent.liteav.demo.superplayer.util.HistoryUtil;
+import com.tencent.liteav.demo.superplayer.util.PlayerUtil;
+import com.tencent.rtmp.ITXVodPlayListener;
+import com.tencent.rtmp.TXLiveConstants;
+import com.tencent.rtmp.TXVodPlayConfig;
+import com.tencent.rtmp.TXVodPlayer;
+import com.tencent.rtmp.ui.TXCloudVideoView;
+
+import org.fourthline.cling.model.meta.Device;
+import org.fourthline.cling.support.model.PositionInfo;
+import org.fourthline.cling.support.model.TransportState;
+
+import java.io.File;
+import java.util.UUID;
+
+public class SuperPlayerImpl implements SuperPlayer, ITXVodPlayListener {
+
+    private static final String TAG = "SuperPlayerImpl";
+
+    private TXCloudVideoView mVideoView;        // 腾讯云视频播放view
+    private TXVodPlayer mVodPlayer;       // 点播播放器
+
+    private SuperPlayerModel mCurrentModel;  // 当前播放的model
+    private SuperPlayerObserver mObserver;
+    private SuperPlayerDef.PlayerMode mCurrentPlayMode = SuperPlayerDef.PlayerMode.WINDOW;    // 当前播放模式
+    private SuperPlayerDef.PlayerState mCurrentPlayState = SuperPlayerDef.PlayerState.INIT;
+
+    private float mStartPos;                // 视频开始播放时间
+
+    private boolean isPrepared = false;
+    private boolean isNeedResume = false;
+    private boolean mNeedToPause = false;
+    private boolean mTimeout = false;
+    private boolean isPauseByTimeout = false;
+    private boolean isPauseByCasting = false;
+
+    private long mCurrent = 0L;
+    private long mStartPoint = 0L;
+    private long mEndPoint = 0L;
+
+    private boolean isCasting = false;
+    private Device castDevice;
+
+    private CircleMessageHandler positionHandler = new CircleMessageHandler(1000L, new Runnable() {
+        @Override
+        public void run() {
+            if (castDevice == null) return;
+
+            DLNACastManager.getInstance().getPositionInfo(castDevice, new ICastInterface.GetInfoListener<PositionInfo>() {
+                @Override
+                public void onGetInfoResult(@Nullable PositionInfo positionInfo, @Nullable String errMsg) {
+                    if (positionInfo != null) {
+                        long current = parseTime(positionInfo.getRelTime());
+                        long duration = parseTime(positionInfo.getTrackDuration());
+
+                        updatePlayProgress(current, duration);
+                    }
+                }
+            });
+        }
+    });
+
+    private ICastInterface.CastEventListener castEventListener = new ICastInterface.CastEventListener() {
+        @Override
+        public void onSuccess(String result) {
+            positionHandler.start(0);
+            updatePlayerState(SuperPlayerDef.PlayerState.INIT);
+            updatePlayerState(SuperPlayerDef.PlayerState.PLAYING);
+
+            DLNACastManager.getInstance().seekTo(mCurrent * 1000);
+        }
+
+        @Override
+        public void onFailed(String errMsg) {
+
+        }
+    };
+
+    private ICastInterface.PlayEventListener playEventListener = new ICastInterface.PlayEventListener() {
+        @Override
+        public void onSuccess(Void result) {
+            updatePlayerState(SuperPlayerDef.PlayerState.PLAYING);
+        }
+
+        @Override
+        public void onFailed(String errMsg) {
+
+        }
+    };
+
+    private ICastInterface.PauseEventListener pauseEventListener = new ICastInterface.PauseEventListener() {
+        @Override
+        public void onSuccess(Void result) {
+            updatePlayerState(SuperPlayerDef.PlayerState.PAUSE);
+        }
+
+        @Override
+        public void onFailed(String errMsg) {
+
+        }
+    };
+
+    private ICastInterface.StopEventListener stopEventListener = new ICastInterface.StopEventListener() {
+        @Override
+        public void onSuccess(Void result) {
+            updatePlayerState(SuperPlayerDef.PlayerState.END);
+            positionHandler.stop();
+        }
+
+        @Override
+        public void onFailed(String errMsg) {
+
+        }
+    };
+
+    private ICastInterface.SeekToEventListener seekToEventListener = new ICastInterface.SeekToEventListener() {
+        @Override
+        public void onSuccess(Long result) {
+
+        }
+
+        @Override
+        public void onFailed(String errMsg) {
+
+        }
+    };
+
+    private ICastInterface.ISubscriptionListener subscriptionListener = new ICastInterface.ISubscriptionListener() {
+        @Override
+        public void onSubscriptionTransportStateChanged(TransportState event) {
+            if (event == TransportState.STOPPED) {
+                updatePlayerState(SuperPlayerDef.PlayerState.END);
+            }
+        }
+    };
+
+    public SuperPlayerImpl(Context context, TXCloudVideoView videoView) {
+        mVideoView = videoView;
+
+        initVodPlayer(context);
+    }
+
+    @Override
+    public void setNeedToPause(boolean value) {
+        mNeedToPause = value;
+    }
+
+    /**
+     * 点播播放器事件回调
+     *
+     * @param player
+     * @param event
+     * @param param
+     */
+    @Override
+    public void onPlayEvent(TXVodPlayer player, int event, Bundle param) {
+        if (isCasting) return;
+
+        if (event != TXLiveConstants.PLAY_EVT_PLAY_PROGRESS) {
+            String playEventLog = "TXVodPlayer onPlayEvent event: " + event + ", " + param.getString(TXLiveConstants.EVT_DESCRIPTION);
+            Log.d(TAG, playEventLog);
+        }
+        switch (event) {
+            case TXLiveConstants.PLAY_EVT_VOD_PLAY_PREPARED://视频播放开始
+                onVodPlayPrepared();
+                break;
+            case TXLiveConstants.PLAY_EVT_RCV_FIRST_I_FRAME:
+                Log.i(TAG, "PLAY_EVT_RCV_FIRST_I_FRAME");
+                if (mNeedToPause) {
+                    return;
+                }
+                updatePlayerState(SuperPlayerDef.PlayerState.PLAYING);
+                mObserver.onRcvFirstIframe();
+                break;
+            case TXLiveConstants.PLAY_EVT_PLAY_END:
+                Log.i(TAG, "PLAY_EVT_PLAY_END");
+                updatePlayerState(SuperPlayerDef.PlayerState.END);
+                break;
+            case TXLiveConstants.PLAY_EVT_PLAY_PROGRESS:
+                int progress = param.getInt(TXLiveConstants.EVT_PLAY_PROGRESS_MS);
+                int duration = param.getInt(TXLiveConstants.EVT_PLAY_DURATION_MS);
+
+                if (duration != 0) {
+                    updatePlayProgress(progress / 1000, duration / 1000);
+                }
+                break;
+            case TXLiveConstants.PLAY_EVT_PLAY_LOADING:
+                Log.i(TAG, "PLAY_EVT_PLAY_LOADING");
+                updatePlayerState(SuperPlayerDef.PlayerState.LOADING);
+                break;
+            case TXLiveConstants.PLAY_EVT_PLAY_BEGIN:
+                if (mNeedToPause) {
+                    pause();
+                    return;
+                }
+                updatePlayerState(SuperPlayerDef.PlayerState.PLAYING);
+                break;
+            default:
+                break;
+        }
+        if (event < 0) {// 播放点播文件失败
+            mVodPlayer.stopPlay(true);
+            updatePlayerState(SuperPlayerDef.PlayerState.PAUSE);
+            onError(SuperPlayerCode.VOD_PLAY_FAIL, param.getString(TXLiveConstants.EVT_DESCRIPTION));
+        }
+    }
+
+    private void onVodPlayPrepared() {
+        isPrepared = true;
+
+        if (mNeedToPause) {
+            pause();
+            return;
+        }
+        if (isNeedResume) {
+            mVodPlayer.resume();
+        }
+    }
+
+    /**
+     * 点播播放器网络状态回调
+     *
+     * @param player
+     * @param bundle
+     */
+    @Override
+    public void onNetStatus(TXVodPlayer player, Bundle bundle) {
+        // DO NOTHING
+    }
+
+    /**
+     * 初始化点播播放器
+     *
+     * @param context
+     */
+    private void initVodPlayer(Context context) {
+        mVodPlayer = new TXVodPlayer(context);
+        SuperPlayerGlobalConfig config = SuperPlayerGlobalConfig.getInstance();
+        // 点播播放器配置
+        TXVodPlayConfig mVodPlayConfig = new TXVodPlayConfig();
+
+        File sdcardDir = context.getExternalFilesDir(null);
+        if (sdcardDir != null) {
+            mVodPlayConfig.setCacheFolderPath(sdcardDir.getPath() + "/txcache");
+        }
+        mVodPlayConfig.setPreferredResolution(720 * 1280);
+        mVodPlayConfig.setMaxCacheItems(config.maxCacheItem);
+        mVodPlayConfig.setHeaders(config.headers);
+        mVodPlayer.setConfig(mVodPlayConfig);
+        mVodPlayer.setRenderMode(config.renderMode);
+        mVodPlayer.setVodListener(this);
+        mVodPlayer.enableHardwareDecode(config.enableHWAcceleration);
+        mVodPlayer.setRate(config.playRate);
+        mVodPlayer.setMute(config.mute);
+        mVodPlayer.setMirror(config.mirror);
+    }
+
+    private void initCastPlayer() {
+        DLNACastManager.getInstance().registerActionCallbacks(
+                castEventListener,
+                playEventListener,
+                pauseEventListener,
+                stopEventListener,
+                seekToEventListener
+        );
+
+        DLNACastManager.getInstance().registerSubscriptionListener(subscriptionListener);
+    }
+
+    /**
+     * 播放视频
+     *
+     * @param model
+     */
+    public void playWithModel(SuperPlayerModel model) {
+        reset();
+
+        String videoURL = model.url;
+
+        if (TextUtils.isEmpty(videoURL)) {
+            onError(SuperPlayerCode.PLAY_URL_EMPTY, "播放视频失败,播放链接为空");
+            return;
+        }
+
+        if (!isCasting) {
+            mVodPlayer.setPlayerView(mVideoView);
+
+            if (mVodPlayer != null) {
+                mVodPlayer.setStartTime(mStartPos);
+                mVodPlayer.setAutoPlay(true);
+                mVodPlayer.setVodListener(this);
+                mVodPlayer.setToken(null);
+                mVodPlayer.startPlay(videoURL);
+
+                mStartPos = 0;
+            }
+        } else {
+            if (castDevice != null) {
+                DLNACastManager.getInstance().cast(
+                        castDevice, CastObject.INSTANCE.newInstance(
+                                videoURL, UUID.randomUUID().toString(), model.title
+                        ));
+            }
+        }
+        updatePlayProgress(0, model.duration);
+    }
+
+    /**
+     * 更新播放进度
+     *
+     * @param current  当前播放进度(秒)
+     * @param duration 总时长(秒)
+     */
+    private void updatePlayProgress(long current, long duration) {
+        mCurrent = current;
+
+        if (mObserver != null) {
+            mObserver.onPlayProgress(current, duration);
+        }
+    }
+
+    /**
+     * 更新播放状态
+     *
+     * @param playState
+     */
+    private void updatePlayerState(SuperPlayerDef.PlayerState playState) {
+        mCurrentPlayState = playState;
+        if (mObserver == null) {
+            return;
+        }
+        switch (playState) {
+            case INIT:
+                mObserver.onPlayPrepare();
+                break;
+            case PLAYING:
+                PlayerUtil.INSTANCE.sendNewTimer(PlayerUtil.getSTATE_PLAY());
+                mObserver.onPlayBegin(getPlayName());
+                break;
+            case PAUSE:
+                PlayerUtil.INSTANCE.sendNewTimer(PlayerUtil.getSTATE_PAUSE());
+                mObserver.onPlayPause();
+                break;
+            case LOADING:
+                PlayerUtil.INSTANCE.sendNewTimer(PlayerUtil.getSTATE_LOADING());
+                mObserver.onPlayLoading();
+                break;
+            case END:
+                PlayerUtil.INSTANCE.sendNewTimer(PlayerUtil.getSTATE_STOP());
+                mObserver.onPlayStop();
+                break;
+        }
+
+        switch (playState) {
+            case PLAYING:
+                if (!isCasting) {
+                    mStartPoint = (int) mVodPlayer.getCurrentPlaybackTime();
+                } else {
+                    DLNACastManager.getInstance().getPositionInfo(castDevice, new ICastInterface.GetInfoListener<PositionInfo>() {
+                        @Override
+                        public void onGetInfoResult(@Nullable PositionInfo positionInfo, @Nullable String errMsg) {
+                            if (positionInfo != null) {
+                                mStartPoint = parseTime(positionInfo.getRelTime());
+                            }
+                        }
+                    });
+                }
+                break;
+            case PAUSE:
+            case END:
+                if (!isCasting) {
+                    mEndPoint = (int) mVodPlayer.getCurrentPlaybackTime();
+                } else {
+                    DLNACastManager.getInstance().getPositionInfo(castDevice, new ICastInterface.GetInfoListener<PositionInfo>() {
+                        @Override
+                        public void onGetInfoResult(@Nullable PositionInfo positionInfo, @Nullable String errMsg) {
+                            if (positionInfo != null) {
+                                mEndPoint = parseTime(positionInfo.getRelTime());
+                            }
+                        }
+                    });
+                }
+                saveHistory();
+                break;
+        }
+    }
+
+    private void onError(int code, String message) {
+        if (mObserver != null) {
+            mObserver.onError(code, message);
+        }
+    }
+
+    private String getPlayName() {
+        String title = "";
+        if (mCurrentModel != null && !TextUtils.isEmpty(mCurrentModel.title)) {
+            title = mCurrentModel.title;
+        }
+        return title;
+    }
+
+    @Override
+    public void play(SuperPlayerModel model) {
+        if (mTimeout) return;
+
+        mCurrentModel = model;
+        playWithModel(model);
+    }
+
+    @Override
+    public void restart() {
+        if (mTimeout) return;
+
+        playWithModel(mCurrentModel);
+    }
+
+    @Override
+    public void pause() {
+        if (!isCasting) {
+            mVodPlayer.pause();
+            updatePlayerState(SuperPlayerDef.PlayerState.PAUSE);
+        } else {
+            DLNACastManager.getInstance().pause();
+        }
+    }
+
+    @Override
+    public void resume() {
+        if (mTimeout) return;
+
+        if (isCasting) {
+            DLNACastManager.getInstance().play();
+            return;
+        }
+        isNeedResume = true;
+        if (isPrepared) {
+            mVodPlayer.resume();
+        }
+
+        updatePlayerState(SuperPlayerDef.PlayerState.PLAYING);
+    }
+
+    @Override
+    public void stop() {
+        if (!isCasting) {
+            resetPlayer();
+            updatePlayerState(SuperPlayerDef.PlayerState.END);
+        } else {
+            DLNACastManager.getInstance().stop();
+            DLNACastManager.getInstance().unregisterActionCallbacks();
+        }
+    }
+
+    @Override
+    public void reset() {
+        resetPlayer();
+        updatePlayerState(SuperPlayerDef.PlayerState.INIT);
+    }
+
+    @Override
+    public void onTimeout(boolean timeout) {
+        this.mTimeout = timeout;
+
+        if (timeout) {
+            if (!isCasting) {
+                if (mCurrentPlayState == SuperPlayerDef.PlayerState.PLAYING || mCurrentPlayState == SuperPlayerDef.PlayerState.LOADING) {
+                    isPauseByTimeout = true;
+                    pause();
+                }
+            } else {
+                stopCast();
+            }
+        } else {
+            mStartPoint = mEndPoint;
+
+            saveHistory();
+
+            if (isPauseByTimeout) {
+                resume();
+            }
+
+        }
+    }
+
+    private void resetPlayer() {
+        isPrepared = false;
+        isNeedResume = false;
+        if (mVodPlayer != null) {
+            mVodPlayer.setVodListener(null);
+            mVodPlayer.stopPlay(false);
+        }
+    }
+
+    @Override
+    public void switchPlayMode(SuperPlayerDef.PlayerMode playerMode) {
+        if (mCurrentPlayMode == playerMode) {
+            return;
+        }
+
+        mCurrentPlayMode = playerMode;
+
+        if (mCurrentPlayMode == SuperPlayerDef.PlayerMode.FULLSCREEN) {
+            if (getPlayerState() == SuperPlayerDef.PlayerState.PAUSE) {
+                resume();
+            }
+        }
+    }
+
+    @Override
+    public void seek(int position) {
+        if (!isCasting) {
+            mEndPoint = (int) mVodPlayer.getCurrentPlaybackTime();
+            saveHistory();
+        } else {
+            final long start = mStartPoint;
+
+            DLNACastManager.getInstance().getPositionInfo(castDevice, (positionInfo, errMsg) -> {
+                if (positionInfo != null) {
+                    mEndPoint = parseTime(positionInfo.getRelTime());
+
+                    if (mCurrentModel != null) {
+                        HistoryUtil.INSTANCE.sendHistory(start, mEndPoint, mCurrentModel.sectionId);
+                    }
+                }
+            });
+        }
+
+        mStartPoint = position;
+
+        if (!isCasting) {
+            mVodPlayer.seek(position);
+            mVodPlayer.resume();
+        } else {
+            DLNACastManager.getInstance().seekTo(position * 1000L);
+        }
+
+        if (mObserver != null) {
+            mObserver.onSeek(position);
+        }
+    }
+
+    @Override
+    public void startCast(Device device) {
+        if (isCasting) {
+            stopCast();
+        }
+
+        if (mVodPlayer.isPlaying()) {
+            isPauseByCasting = true;
+            mVodPlayer.pause();
+        }
+
+        if (castDevice != null) {
+            positionHandler.stop();
+        }
+
+        castDevice = device;
+        isCasting = true;
+        initCastPlayer();
+        DLNACastManager.getInstance().cast(device,
+                CastObject.INSTANCE.newInstance(mCurrentModel.url, UUID.randomUUID().toString(), mCurrentModel.title));
+    }
+
+    @Override
+    public void stopCast() {
+        if (!isCasting) return;
+
+        mStartPos = mCurrent;
+
+        DLNACastManager.getInstance().unregisterActionCallbacks();
+        DLNACastManager.getInstance().stop();
+        castDevice = null;
+        isCasting = false;
+
+        playWithModel(mCurrentModel);
+    }
+
+    @Override
+    public SuperPlayerDef.PlayerMode getPlayerMode() {
+        return mCurrentPlayMode;
+    }
+
+    @Override
+    public SuperPlayerDef.PlayerState getPlayerState() {
+        return mCurrentPlayState;
+    }
+
+    @Override
+    public void setObserver(SuperPlayerObserver observer) {
+        mObserver = observer;
+    }
+
+    @Override
+    public void setLoop(boolean isLoop) {
+        mVodPlayer.setLoop(isLoop);
+    }
+
+    @Override
+    public void setStartTime(float startPos) {
+        this.mStartPos = startPos;
+        mVodPlayer.setStartTime(startPos);
+    }
+
+    private void saveHistory() {
+        Log.d(TAG, "saveHistory: " + mStartPoint + " - " + mEndPoint);
+        if (mCurrentModel != null) {
+            HistoryUtil.INSTANCE.sendHistory(mStartPoint, mEndPoint, mCurrentModel.sectionId);
+        }
+    }
+
+    private static long parseTime(String time) {
+        String[] split = time.split(":");
+        long parsedTime = 0, radix = 1;
+        for (int i = split.length - 1; i >= 0; i--) {
+            parsedTime += Long.parseLong(split[i]) * radix;
+            radix *= 60;
+        }
+
+        return parsedTime;
+    }
+
+    private static class CircleMessageHandler extends Handler {
+        private static final int MSG = 101;
+
+        private final long duration;
+        private final Runnable runnable;
+
+        public CircleMessageHandler(long duration, Runnable runnable) {
+            super(Looper.getMainLooper());
+            this.duration = duration;
+            this.runnable = runnable;
+        }
+
+        @Override
+        public void handleMessage(@NonNull Message msg) {
+            runnable.run();
+            sendEmptyMessageDelayed(MSG, duration);
+        }
+
+        public void start(long delay) {
+            stop();
+            sendEmptyMessageDelayed(MSG, delay);
+        }
+
+        public void stop() {
+            removeMessages(MSG);
+        }
+    }
+}

+ 57 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/player/SuperPlayerObserver.java

@@ -0,0 +1,57 @@
+package com.tencent.liteav.demo.superplayer.player;
+
+public abstract class SuperPlayerObserver {
+
+    /**
+     * 准备播放
+     */
+    public void onPlayPrepare() {
+
+    }
+
+    /**
+     * 开始播放
+     *
+     * @param name 当前视频名称
+     */
+    public void onPlayBegin(String name) {
+    }
+
+    /**
+     * 播放暂停
+     */
+    public void onPlayPause() {
+    }
+
+    /**
+     * 播放器停止
+     */
+    public void onPlayStop() {
+    }
+
+    /**
+     * 播放器进入Loading状态
+     */
+    public void onPlayLoading() {
+    }
+
+    /**
+     * 播放进度回调
+     *
+     * @param current
+     * @param duration
+     */
+    public void onPlayProgress(long current, long duration) {
+    }
+
+    public void onSeek(int position) {
+    }
+
+
+    public void onError(int code, String message) {
+    }
+
+    public void onRcvFirstIframe() {
+
+    }
+}

+ 128 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/AbsPlayer.java

@@ -0,0 +1,128 @@
+package com.tencent.liteav.demo.superplayer.ui.player;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.RelativeLayout;
+
+import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
+import com.tencent.liteav.demo.superplayer.util.TimeoutUtil;
+
+/**
+ * 播放器公共逻辑
+ */
+public abstract class AbsPlayer extends RelativeLayout implements Player, TimeoutUtil.Listener {
+    protected static final int HIDDEN_DELAY = 3000;
+
+    protected Callback mControllerCallback; // 播放控制回调
+
+    protected Runnable mHideViewRunnable = new Runnable() {
+        @Override
+        public void run() {
+            hide();
+        }
+    };
+
+    public AbsPlayer(Context context) {
+        super(context);
+    }
+
+    public AbsPlayer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public AbsPlayer(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setCallback(Callback callback) {
+        mControllerCallback = callback;
+    }
+
+    @Override
+    public void onTimeout(boolean timeout) {
+
+    }
+    @Override
+    public void show() {
+
+    }
+
+    @Override
+    public void hide() {
+
+    }
+
+    @Override
+    public void release() {
+
+    }
+
+    @Override
+    public void updatePlayState(SuperPlayerDef.PlayerState playState) {
+
+    }
+
+    @Override
+    public void updateVideoProgress(long current, long duration) {
+
+    }
+
+    @Override
+    public void setBackground(Bitmap bitmap) {
+
+    }
+
+    @Override
+    public void showBackground() {
+
+    }
+
+    @Override
+    public void hideBackground() {
+
+    }
+
+
+    /**
+     * 设置控件的可见性
+     *
+     * @param view      目标控件
+     * @param isVisible 显示:true 隐藏:false
+     */
+    protected void toggleView(View view, boolean isVisible) {
+        view.setVisibility(isVisible ? View.VISIBLE : View.GONE);
+    }
+
+    /**
+     * 将秒数转换为hh:mm:ss的格式
+     *
+     * @param second
+     * @return
+     */
+    protected String formattedTime(long second) {
+        String formatTime;
+        long h, m, s;
+        h = second / 3600;
+        m = (second % 3600) / 60;
+        s = (second % 3600) % 60;
+        if (h == 0) {
+            formatTime = asTwoDigit(m) + ":" + asTwoDigit(s);
+        } else {
+            formatTime = asTwoDigit(h) + ":" + asTwoDigit(m) + ":" + asTwoDigit(s);
+        }
+        return formatTime;
+    }
+
+    protected String asTwoDigit(long digit) {
+        String value = "";
+        if (digit < 10) {
+            value = "0";
+        }
+        value += String.valueOf(digit);
+        return value;
+    }
+
+}

+ 179 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/CastingPlayer.java

@@ -0,0 +1,179 @@
+package com.tencent.liteav.demo.superplayer.ui.player;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.cast.dlna.dmc.control.ICastInterface;
+import com.tencent.liteav.demo.superplayer.R;
+import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
+import com.tencent.liteav.demo.superplayer.ui.view.PointProgressBar;
+
+public class CastingPlayer extends AbsPlayer implements View.OnClickListener,
+        PointProgressBar.OnProgressBarChangeListener {
+
+    private LinearLayout mLayoutBottom;                          // 底部进度条所在布局
+    private ImageView mIvPause;                               // 暂停播放按钮
+    private ImageView mIvPlayNext;                            // 播放下一个按钮
+    private ImageView mIvFullScreen;                          // 全屏按钮
+
+    private TextView mTvCurrent;                             // 当前进度文本
+    private TextView mTvDuration;                            // 总时长文本
+    private PointProgressBar mBarProgress;                       // 播放进度条
+
+    private long mLastClickTime;                         // 上次点击事件的时间
+    private TextView mStopCast;                            // 停止投屏
+
+    private boolean mIsChangingSeekBarProgress;             // 进度条是否正在拖动,避免SeekBar由于视频播放的update而跳动
+
+    private SuperPlayerDef.PlayerState mCurrentPlayState = SuperPlayerDef.PlayerState.END;                 // 当前播放状态
+    private long mDuration;                              // 视频总时长
+    private long mProgress;                              // 当前播放进度
+
+    public CastingPlayer(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public CastingPlayer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public CastingPlayer(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context);
+    }
+
+    private void init(Context context) {
+        LayoutInflater.from(context).inflate(R.layout.superplayer_vod_player_casting, this);
+        mLayoutBottom = (LinearLayout) findViewById(R.id.superplayer_ll_bottom);
+        mLayoutBottom.setOnClickListener(this);
+        mIvPause = (ImageView) findViewById(R.id.superplayer_iv_pause);
+        mTvCurrent = (TextView) findViewById(R.id.superplayer_tv_current);
+        mTvDuration = (TextView) findViewById(R.id.superplayer_tv_duration);
+
+        mStopCast = (TextView) findViewById(R.id.stop_cast);
+        mStopCast.setOnClickListener(this);
+
+        mBarProgress = (PointProgressBar) findViewById(R.id.superplayer_bar_progress);
+        mBarProgress.setProgress(0);
+        mBarProgress.setMax(100);
+        mBarProgress.setListener(this);
+
+        mIvFullScreen = (ImageView) findViewById(R.id.superplayer_iv_fullscreen);
+        mIvPlayNext = (ImageView) findViewById(R.id.superplayer_iv_play_next);
+
+//        mTimeoutView = (TimeOutPlayer) findViewById(R.id.superplayer_timeout_view);
+
+        mIvPause.setOnClickListener(this);
+        mIvPlayNext.setOnClickListener(this);
+        mIvFullScreen.setOnClickListener(this);
+    }
+
+    @Override
+    public void onClick(View view) {
+        if (System.currentTimeMillis() - mLastClickTime < 300) { //限制点击频率
+            return;
+        }
+        mLastClickTime = System.currentTimeMillis();
+        int id = view.getId();
+
+        if (id == R.id.superplayer_iv_pause) { //暂停\播放按钮
+            togglePlayState();
+        } else if (id == R.id.superplayer_iv_play_next) {
+            mControllerCallback.playNext();
+        }else if(id == R.id.stop_cast){
+            mControllerCallback.stopCast();
+        }
+    }
+
+    @Override
+    public void onProgressChanged(PointProgressBar progressBar, int progress, boolean fromUser) {
+
+    }
+
+    @Override
+    public void onStartTrackingTouch(PointProgressBar progressBar) {
+
+    }
+
+    @Override
+    public void onStopTrackingTouch(PointProgressBar progressBar) {
+        int curProgress = progressBar.getProgress();
+        int maxProgress = progressBar.getMax();
+
+        if (curProgress >= 0 && curProgress <= maxProgress) {
+            float percentage = ((float) curProgress) / maxProgress;
+            int position = (int) (mDuration * percentage);
+
+            if (mControllerCallback != null) {
+                mControllerCallback.onSeekTo(position);
+            }
+        }
+    }
+
+    @Override
+    public void updatePlayState(SuperPlayerDef.PlayerState playState) {
+        switch (playState) {
+            case INIT:
+            case PAUSE:
+            case END:
+                mIvPause.setImageResource(R.mipmap.play_state);
+                break;
+            case PLAYING:
+            case LOADING:
+                mBarProgress.setEnabled(true);
+                mIvPause.setImageResource(R.mipmap.pause_state);
+                break;
+        }
+        mCurrentPlayState = playState;
+    }
+
+    @Override
+    public void updateVideoProgress(long current, long duration) {
+        mProgress = current < 0 ? 0 : current;
+        mDuration = duration < 0 ? 0 : duration;
+        mTvCurrent.setText(formattedTime(mProgress));
+
+        float percentage = mDuration > 0 ? ((float) mProgress / (float) mDuration) : 1.0f;
+        if (mProgress == 0) {
+            percentage = 0;
+        }
+
+        if (percentage >= 0 && percentage <= 1) {
+            int progress = Math.round(percentage * mBarProgress.getMax());
+            if (!mIsChangingSeekBarProgress) {
+                mBarProgress.setProgress(progress);
+            }
+            mTvDuration.setText(formattedTime(mDuration));
+        }
+    }
+
+    private void togglePlayState() {
+        switch (mCurrentPlayState) {
+            case INIT:
+            case PAUSE:
+            case END:
+                if (mControllerCallback != null) {
+                    mControllerCallback.onResume();
+                }
+                break;
+            case PLAYING:
+            case LOADING:
+                if (mControllerCallback != null) {
+                    mControllerCallback.onPause();
+                }
+                break;
+        }
+//        show();
+    }
+
+}

+ 209 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/FullScreenPlayer.java

@@ -0,0 +1,209 @@
+package com.tencent.liteav.demo.superplayer.ui.player;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Vibrator;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.bumptech.glide.Glide;
+import com.tencent.liteav.demo.superplayer.R;
+import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
+import com.tencent.liteav.demo.superplayer.SuperPlayerModel;
+import com.tencent.liteav.demo.superplayer.ui.view.UnlockProgressView;
+
+/**
+ * 全屏模式播放控件
+ * <p>
+ * 除{@link WindowPlayer}基本功能外,还包括进度条关键帧打点信息显示与跳转、快进快退时缩略图的显示、切换画质
+ * 镜像播放、硬件加速、倍速播放、弹幕、截图等功能
+ * <p>
+ * 2、触摸事件监听{@link #onTouchEvent(MotionEvent)}
+ */
+public class FullScreenPlayer extends AbsPlayer implements UnlockProgressView.OnUnlockListener {
+    private UnlockProgressView mIvLock;                                // 锁屏按钮
+    private TextView mIvLockText;
+    private ImageView mImageCover;                            // 封面图
+    private TimeOutPlayer mTimeoutView;
+
+    private boolean isShowing;  // 自身是否可见
+    private SuperPlayerDef.PlayerState mCurrentPlayState = SuperPlayerDef.PlayerState.END; // 当前播放状态
+
+    private boolean isDestroy = false;  // Activity是否被销毁
+
+    private boolean isLock = false;
+
+    public FullScreenPlayer(Context context) {
+        super(context);
+        initialize(context);
+    }
+
+    public FullScreenPlayer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        initialize(context);
+    }
+
+    public FullScreenPlayer(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        initialize(context);
+    }
+
+    /**
+     * 初始化控件、手势检测监听器、亮度/音量/播放进度的回调
+     */
+    private void initialize(Context context) {
+        initView(context);
+    }
+
+    /**
+     * 初始化view
+     */
+    private void initView(Context context) {
+        LayoutInflater.from(context).inflate(R.layout.superplayer_vod_player_fullscreen, this);
+
+        mIvLock = (UnlockProgressView) findViewById(R.id.superplayer_iv_lock);
+        mIvLock.setUnlockListener(this);
+
+        mIvLockText = (TextView) findViewById(R.id.superplayer_iv_lock_text);
+        mImageCover = (ImageView) findViewById(R.id.superplayer_cover_view);
+
+        mTimeoutView = (TimeOutPlayer) findViewById(R.id.superplayer_timeout_view);
+    }
+
+    @Override
+    public void setCallback(Callback callback) {
+        super.setCallback(callback);
+        mTimeoutView.setCallback(this.mControllerCallback);
+    }
+
+    public void setPlayNextButtonVisibility(boolean isShowing) {
+
+    }
+
+    public void preparePlayVideo(SuperPlayerModel superPlayerModel) {
+        if (!isDestroy) {
+            Glide.with(getContext())
+                    .load(superPlayerModel.cover)
+                    .placeholder(R.mipmap.default_cover_dark)
+                    .into(mImageCover);
+        }
+        toggleView(mImageCover, true);
+        updateVideoProgress(0, superPlayerModel.duration);
+    }
+
+    /**
+     * 显示控件
+     */
+    @Override
+    public void show() {
+        isShowing = true;
+        mIvLock.setVisibility(VISIBLE);
+        mIvLockText.setVisibility(VISIBLE);
+    }
+
+    /**
+     * 隐藏控件
+     */
+    @Override
+    public void hide() {
+        isShowing = false;
+        mIvLock.setVisibility(GONE);
+        mIvLockText.setVisibility(GONE);
+    }
+
+    @Override
+    public void onTimeout(boolean timeout) {
+        mTimeoutView.onTimeout(timeout);
+        mTimeoutView.setVisibility(timeout ? VISIBLE : GONE);
+    }
+
+    /**
+     * 释放控件的内存
+     */
+    @Override
+    public void release() {
+        isDestroy = true;
+    }
+
+    public void toggleCoverView(boolean isVisible) {
+        toggleView(mImageCover, isVisible);
+    }
+
+    @Override
+    public void updatePlayState(SuperPlayerDef.PlayerState playState) {
+        mCurrentPlayState = playState;
+    }
+
+    @Override
+    public void setBackground(Bitmap bitmap) {
+        mImageCover.setImageBitmap(bitmap);
+    }
+
+    @Override
+    public void showBackground() {
+        mImageCover.setVisibility(VISIBLE);
+    }
+
+    /**
+     * 更新实时播放进度
+     *
+     * @param current  当前进度(秒)
+     * @param duration 视频总时长(秒)
+     */
+    @Override
+    public void updateVideoProgress(long current, long duration) {
+
+    }
+
+    public void setLock(boolean lock){
+        isLock = lock;
+        if(isLock){
+
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if(!isLock){
+            if (mControllerCallback != null) {
+                mControllerCallback.onSwitchPlayMode(SuperPlayerDef.PlayerMode.WINDOW);
+            }
+            return  true;
+        }
+
+        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+            if (isShowing) {
+                hide();
+            } else {
+                show();
+            }
+        } else if (event.getAction() == MotionEvent.ACTION_UP) {
+            if (isShowing) {
+                postDelayed(mHideViewRunnable, HIDDEN_DELAY);
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public void startUnlock() {
+        removeCallbacks(mHideViewRunnable);
+    }
+
+    @Override
+    public void unlockSuccess() {
+        if (mControllerCallback != null) {
+            mControllerCallback.onSwitchPlayMode(SuperPlayerDef.PlayerMode.WINDOW);
+        }
+        Vibrator service = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
+        service.vibrate(100);
+    }
+
+    @Override
+    public void unlockFailed() {
+        postDelayed(mHideViewRunnable, HIDDEN_DELAY);
+    }
+}

+ 139 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/Player.java

@@ -0,0 +1,139 @@
+package com.tencent.liteav.demo.superplayer.ui.player;
+
+
+import android.graphics.Bitmap;
+
+import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
+import com.tencent.liteav.demo.superplayer.SuperPlayerModel;
+
+import java.util.List;
+
+/**
+ * 播放控制接口
+ */
+public interface Player {
+
+    /**
+     * 设置回调
+     *
+     * @param callback 回调接口实现对象
+     */
+    void setCallback(Callback callback);
+
+    /**
+     * 显示控件
+     */
+    void show();
+
+    /**
+     * 隐藏控件
+     */
+    void hide();
+
+    /**
+     * 释放控件的内存
+     */
+    void release();
+
+    /**
+     * 更新播放状态
+     *
+     * @param playState 正在播放{@link SuperPlayerDef.PlayerState#PLAYING}
+     *                  正在加载{@link SuperPlayerDef.PlayerState#LOADING}
+     *                  暂停   {@link SuperPlayerDef.PlayerState#PAUSE}
+     *                  播放结束{@link SuperPlayerDef.PlayerState#END}
+     */
+    void updatePlayState(SuperPlayerDef.PlayerState playState);
+
+    /**
+     * 更新视频播放进度
+     *
+     * @param current  当前进度(秒)
+     * @param duration 视频总时长(秒)
+     */
+    void updateVideoProgress(long current, long duration);
+
+    /**
+     * 设置背景
+     *
+     * @param bitmap 背景图
+     */
+    void setBackground(final Bitmap bitmap);
+
+    /**
+     * 显示背景
+     */
+    void showBackground();
+
+    /**
+     * 隐藏背景
+     */
+    void hideBackground();
+
+    /**
+     * 播放控制回调接口
+     */
+    interface Callback {
+
+        /**
+         * 切换播放模式回调
+         *
+         * @param playMode 切换后的播放模式:
+         *                 窗口模式      {@link SuperPlayerDef.PlayerMode#WINDOW  }
+         *                 全屏模式      {@link SuperPlayerDef.PlayerMode#FULLSCREEN  }
+         */
+        void onSwitchPlayMode(SuperPlayerDef.PlayerMode playMode);
+
+        /**
+         * 返回点击事件回调
+         *
+         * @param playMode 当前播放模式:
+         *                 窗口模式      {@link SuperPlayerDef.PlayerMode#WINDOW  }
+         *                 全屏模式      {@link SuperPlayerDef.PlayerMode#FULLSCREEN  }
+         */
+        void onBackPressed(SuperPlayerDef.PlayerMode playMode);
+
+        /**
+         * 播放暂停回调
+         */
+        void onPause();
+
+        /**
+         * 播放继续回调
+         */
+        void onResume();
+
+        /**
+         * 播放跳转回调
+         *
+         * @param position 跳转的位置(秒)
+         */
+        void onSeekTo(int position);
+
+
+        /**
+         * 播放下一个
+         */
+        void playNext();
+
+        /**
+         * 结束投屏
+         */
+        void stopCast();
+
+        /**
+         * 弹出验证框
+         */
+        void verify(SuperPlayerDef.VerifyReason reason);
+
+        /**
+         * 获得当前剧集播放列表
+         */
+        List<SuperPlayerModel> getPlayList();
+
+        /**
+         * 获得当前正在播放的视频
+         */
+        SuperPlayerModel getPlayingVideoModel();
+    }
+}

+ 87 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/TimeOutMPlayer.java

@@ -0,0 +1,87 @@
+package com.tencent.liteav.demo.superplayer.ui.player;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.appcompat.widget.LinearLayoutCompat;
+
+import com.tencent.liteav.demo.superplayer.R;
+import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
+
+public class TimeOutMPlayer extends AbsPlayer implements View.OnClickListener {
+    private ImageView backHome;
+    private LinearLayoutCompat resumePlay;
+
+    public TimeOutMPlayer(Context context) {
+        this(context, null);
+    }
+
+    public TimeOutMPlayer(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public TimeOutMPlayer(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        initView(context, attrs);
+    }
+
+    private void initView(Context context, AttributeSet attrs) {
+        LayoutInflater.from(context).inflate(R.layout.superplayer_vod_player_timeout_m, this);
+
+        backHome = findViewById(R.id.timeout_back);
+        backHome.setOnClickListener(this);
+
+        resumePlay = findViewById(R.id.timeout_resume);
+        resumePlay.setOnClickListener(this);
+
+        if (attrs != null) {
+            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TimeOutPlayer);
+            boolean show = a.getBoolean(R.styleable.TimeOutPlayer_showControl, true);
+
+            if (!show) {
+                backHome.setVisibility(GONE);
+                resumePlay.setVisibility(GONE);
+            }
+
+            a.recycle();
+        }
+    }
+
+    @Override
+    public void onClick(View v) {
+        final int id = v.getId();
+
+        if (id == R.id.timeout_back) {
+            if (mControllerCallback != null) {
+                mControllerCallback.onBackPressed(SuperPlayerDef.PlayerMode.FULLSCREEN);
+            }
+        } else if (id == R.id.timeout_resume) {
+            if (mControllerCallback != null) {
+                mControllerCallback.verify(SuperPlayerDef.VerifyReason.TIMEOUT);
+            }
+        }
+    }
+
+    @Override
+    public void onTimeout(boolean timeout) {
+        super.onTimeout(timeout);
+        this.setOnClickListener(timeout ? this : null);
+    }
+
+    @Override
+    public void hide() {
+        super.hide();
+        this.setOnClickListener(null);
+    }
+
+    @Override
+    public void show() {
+        super.show();
+        this.setOnClickListener(this);
+    }
+}

+ 87 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/TimeOutPlayer.java

@@ -0,0 +1,87 @@
+package com.tencent.liteav.demo.superplayer.ui.player;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.appcompat.widget.LinearLayoutCompat;
+
+import com.tencent.liteav.demo.superplayer.R;
+import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
+
+public class TimeOutPlayer extends AbsPlayer implements View.OnClickListener {
+    private ImageView backHome;
+    private LinearLayoutCompat resumePlay;
+
+    public TimeOutPlayer(Context context) {
+        this(context, null);
+    }
+
+    public TimeOutPlayer(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public TimeOutPlayer(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        initView(context, attrs);
+    }
+
+    private void initView(Context context, AttributeSet attrs) {
+        LayoutInflater.from(context).inflate(R.layout.superplayer_vod_player_timeout, this);
+
+        backHome = findViewById(R.id.timeout_back);
+        backHome.setOnClickListener(this);
+
+        resumePlay = findViewById(R.id.timeout_resume);
+        resumePlay.setOnClickListener(this);
+
+        if (attrs != null) {
+            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TimeOutPlayer);
+            boolean show = a.getBoolean(R.styleable.TimeOutPlayer_showControl, true);
+
+            if (!show) {
+                backHome.setVisibility(GONE);
+                resumePlay.setVisibility(GONE);
+            }
+
+            a.recycle();
+        }
+    }
+
+    @Override
+    public void onClick(View v) {
+        final int id = v.getId();
+
+        if (id == R.id.timeout_back) {
+            if (mControllerCallback != null) {
+                mControllerCallback.onBackPressed(SuperPlayerDef.PlayerMode.FULLSCREEN);
+            }
+        } else if (id == R.id.timeout_resume) {
+            if (mControllerCallback != null) {
+                mControllerCallback.verify(SuperPlayerDef.VerifyReason.TIMEOUT);
+            }
+        }
+    }
+
+    @Override
+    public void onTimeout(boolean timeout) {
+        super.onTimeout(timeout);
+        this.setOnClickListener(timeout ? this : null);
+    }
+
+    @Override
+    public void hide() {
+        super.hide();
+        this.setOnClickListener(null);
+    }
+
+    @Override
+    public void show() {
+        super.show();
+        this.setOnClickListener(this);
+    }
+}

+ 472 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/WindowPlayer.java

@@ -0,0 +1,472 @@
+package com.tencent.liteav.demo.superplayer.ui.player;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.bumptech.glide.Glide;
+import com.tencent.liteav.demo.superplayer.R;
+import com.tencent.liteav.demo.superplayer.SuperPlayerDef;
+import com.tencent.liteav.demo.superplayer.SuperPlayerModel;
+import com.tencent.liteav.demo.superplayer.util.VideoGestureDetector;
+import com.tencent.liteav.demo.superplayer.ui.view.PointProgressBar;
+import com.tencent.liteav.demo.superplayer.ui.view.VideoProgressLayout;
+import com.tencent.liteav.demo.superplayer.ui.view.VolumeBrightnessProgressLayout;
+
+/**
+ * 窗口模式播放控件
+ * <p>
+ * 除基本播放控制外,还有手势控制快进快退、手势调节亮度音量等
+ * <p>
+ * 1、点击事件监听{@link #onClick(View)}
+ * <p>
+ * 2、触摸事件监听{@link #onTouchEvent(MotionEvent)}
+ */
+public class WindowPlayer extends AbsPlayer implements View.OnClickListener,
+        PointProgressBar.OnProgressBarChangeListener {
+
+    // UI控件
+    private LinearLayout mLayoutBottom;                          // 底部进度条所在布局
+    private ImageView mIvPause;                               // 暂停播放按钮
+    private ImageView mIvPlayNext;                            // 播放下一个按钮
+    private ImageView mIvFullScreen;                          // 全屏按钮
+
+    private TextView mTvCurrent;                             // 当前进度文本
+    private TextView mTvDuration;                            // 总时长文本
+    private PointProgressBar mBarProgress;                       // 播放进度条
+
+    private ImageView mImageCover;                            // 封面图
+    private VolumeBrightnessProgressLayout mGestureVolumeBrightnessProgressLayout; // 音量亮度调节布局
+    private VideoProgressLayout mGestureVideoProgressLayout;            // 手势快进提示布局
+
+    private TimeOutMPlayer mTimeoutView;
+
+    private GestureDetector mGestureDetector;                       // 手势检测监听器
+    private VideoGestureDetector mVideoGestureDetector;                      // 手势控制工具
+
+    private boolean isShowing;                              // 自身是否可见
+    private boolean mIsChangingSeekBarProgress;             // 进度条是否正在拖动,避免SeekBar由于视频播放的update而跳动
+
+    private SuperPlayerDef.PlayerState mCurrentPlayState = SuperPlayerDef.PlayerState.END;                 // 当前播放状态
+    private long mDuration;                              // 视频总时长
+    private long mProgress;                              // 当前播放进度
+
+    private long mLastClickTime;                         // 上次点击事件的时间
+    private boolean mIsOpenGesture = true;                  // 是否开启手势
+    private boolean isDestroy = false;              // Activity 是否被销毁
+
+    public WindowPlayer(Context context) {
+        super(context);
+        initialize(context);
+    }
+
+    public WindowPlayer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        initialize(context);
+    }
+
+    public WindowPlayer(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        initialize(context);
+    }
+
+    /**
+     * 初始化控件、手势检测监听器、亮度/音量/播放进度的回调
+     */
+    private void initialize(Context context) {
+        initView(context);
+        mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
+            @Override
+            public boolean onDoubleTap(MotionEvent e) {
+                togglePlayState();
+//                show();
+                if (mHideViewRunnable != null) {
+                    removeCallbacks(mHideViewRunnable);
+                    postDelayed(mHideViewRunnable, HIDDEN_DELAY);
+                }
+                return true;
+            }
+
+//            @Override
+//            public boolean onScroll(MotionEvent downEvent, MotionEvent moveEvent, float distanceX, float distanceY) {
+//                if (downEvent != null) toggle(false);
+//                if (downEvent == null || moveEvent == null) {
+//                    return false;
+//                }
+//                if (mVideoGestureDetector != null && mGestureVolumeBrightnessProgressLayout != null) {
+//                    mVideoGestureDetector.check(mGestureVolumeBrightnessProgressLayout.getHeight(), downEvent, moveEvent, distanceX, distanceY);
+//                }
+//                return true;
+//            }
+
+            @Override
+            public boolean onSingleTapConfirmed(MotionEvent e) {
+                if (mVideoGestureDetector != null) {
+                    mVideoGestureDetector.reset(getWidth(), mBarProgress.getProgress());
+                }
+
+                toggle(true);
+                return true;
+            }
+
+        });
+        mGestureDetector.setIsLongpressEnabled(false);
+
+        mVideoGestureDetector = new VideoGestureDetector(getContext());
+        mVideoGestureDetector.setVideoGestureListener(new VideoGestureDetector.VideoGestureListener() {
+            @Override
+            public void onBrightnessGesture(float newBrightness) {
+                if (mGestureVolumeBrightnessProgressLayout != null) {
+                    mGestureVolumeBrightnessProgressLayout.setProgress((int) (newBrightness * 100));
+                    mGestureVolumeBrightnessProgressLayout.setImageResource(R.drawable.superplayer_ic_light_max);
+                    mGestureVolumeBrightnessProgressLayout.show();
+                }
+            }
+
+            @Override
+            public void onVolumeGesture(float volumeProgress) {
+                if (mGestureVolumeBrightnessProgressLayout != null) {
+                    mGestureVolumeBrightnessProgressLayout.setImageResource(R.drawable.superplayer_ic_volume_max);
+                    mGestureVolumeBrightnessProgressLayout.setProgress((int) volumeProgress);
+                    mGestureVolumeBrightnessProgressLayout.show();
+                }
+            }
+
+            @Override
+            public void onSeekGesture(int progress) {
+                mIsChangingSeekBarProgress = true;
+                if (mGestureVideoProgressLayout != null) {
+                    if (progress > mBarProgress.getMax()) {
+                        progress = mBarProgress.getMax();
+                    }
+
+                    if (progress < 0) {
+                        progress = 0;
+                    }
+                    mGestureVideoProgressLayout.setProgress(progress);
+                    mGestureVideoProgressLayout.show();
+
+                    float percentage = ((float) progress) / mBarProgress.getMax();
+                    float currentTime = (mDuration * percentage);
+
+                    mGestureVideoProgressLayout.setTimeText(formattedTime((long) currentTime) + " / " + formattedTime((long) mDuration));
+                }
+                if (mBarProgress != null)
+                    mBarProgress.setProgress(progress);
+            }
+        });
+    }
+
+    /**
+     * 初始化view
+     */
+    private void initView(Context context) {
+        LayoutInflater.from(context).inflate(R.layout.superplayer_vod_player_window, this);
+        mLayoutBottom = (LinearLayout) findViewById(R.id.superplayer_ll_bottom);
+        mLayoutBottom.setOnClickListener(this);
+        mIvPause = (ImageView) findViewById(R.id.superplayer_iv_pause);
+        mTvCurrent = (TextView) findViewById(R.id.superplayer_tv_current);
+        mTvDuration = (TextView) findViewById(R.id.superplayer_tv_duration);
+
+        mBarProgress = (PointProgressBar) findViewById(R.id.superplayer_bar_progress);
+        mBarProgress.setProgress(0);
+        mBarProgress.setMax(100);
+        mBarProgress.setListener(this);
+
+        mIvFullScreen = (ImageView) findViewById(R.id.superplayer_iv_fullscreen);
+        mImageCover = (ImageView) findViewById(R.id.superplayer_cover_view);
+        mIvPlayNext = (ImageView) findViewById(R.id.superplayer_iv_play_next);
+
+        mTimeoutView = (TimeOutMPlayer) findViewById(R.id.superplayer_timeout_view);
+
+        mIvPause.setOnClickListener(this);
+        mIvPlayNext.setOnClickListener(this);
+        mIvFullScreen.setOnClickListener(this);
+
+
+        mGestureVolumeBrightnessProgressLayout =
+                (VolumeBrightnessProgressLayout) findViewById(R.id.superplayer_gesture_progress);
+
+        mGestureVideoProgressLayout = (VideoProgressLayout) findViewById(R.id.superplayer_video_progress_layout);
+    }
+
+    /**
+     * 切换播放状态
+     * <p>
+     * 双击和点击播放/暂停按钮会触发此方法
+     */
+    private void togglePlayState() {
+        switch (mCurrentPlayState) {
+            case INIT:
+            case PAUSE:
+            case END:
+                if (mControllerCallback != null) {
+                    mControllerCallback.onResume();
+                }
+                break;
+            case PLAYING:
+            case LOADING:
+                if (mControllerCallback != null) {
+                    mControllerCallback.onPause();
+                }
+                break;
+        }
+//        show();
+    }
+
+    /**
+     * 切换自身的可见性
+     */
+    private void toggle(boolean hide) {
+        if (hide && isShowing) {
+            hide();
+        } else {
+            show();
+            if (mHideViewRunnable != null) {
+                removeCallbacks(mHideViewRunnable);
+                postDelayed(mHideViewRunnable, HIDDEN_DELAY);
+            }
+        }
+    }
+
+    public void setPlayNextButtonVisibility(boolean isShowing) {
+        toggleView(mIvPlayNext, isShowing);
+    }
+
+    public void preparePlayVideo(SuperPlayerModel superPlayerModel) {
+        if (!isDestroy) {
+            if (superPlayerModel.cover != null) {
+                Glide.with(getContext())
+                        .load(superPlayerModel.cover)
+                        .placeholder(R.mipmap.default_cover_dark)
+                        .into(mImageCover);
+            }
+
+        }
+        toggleView(mImageCover, true);
+        mIvPause.setImageResource(R.mipmap.play_state);
+        updateVideoProgress(0, superPlayerModel.duration);
+        mBarProgress.setEnabled(true);
+    }
+
+    /**
+     * 显示控件
+     */
+    @Override
+    public void show() {
+        isShowing = true;
+        mLayoutBottom.setVisibility(View.VISIBLE);
+    }
+
+    /**
+     * 隐藏控件
+     */
+    @Override
+    public void hide() {
+        isShowing = false;
+        mLayoutBottom.setVisibility(View.GONE);
+    }
+
+    @Override
+    public void onTimeout(boolean timeout) {
+        mTimeoutView.onTimeout(timeout);
+        mTimeoutView.setVisibility(timeout ? VISIBLE : GONE);
+    }
+
+    public void toggleCoverView(boolean isVisible) {
+        toggleView(mImageCover, isVisible);
+    }
+
+    @Override
+    public void updatePlayState(SuperPlayerDef.PlayerState playState) {
+        switch (playState) {
+            case INIT:
+            case PAUSE:
+            case END:
+                mIvPause.setImageResource(R.mipmap.play_state);
+                break;
+            case PLAYING:
+            case LOADING:
+                mBarProgress.setEnabled(true);
+                mIvPause.setImageResource(R.mipmap.pause_state);
+                break;
+        }
+        mCurrentPlayState = playState;
+    }
+
+    /**
+     * 更新视频播放进度
+     *
+     * @param current  当前进度(秒)
+     * @param duration 视频总时长(秒)
+     */
+    @Override
+    public void updateVideoProgress(long current, long duration) {
+        mProgress = current < 0 ? 0 : current;
+        mDuration = duration < 0 ? 0 : duration;
+        mTvCurrent.setText(formattedTime(mProgress));
+
+        float percentage = mDuration > 0 ? ((float) mProgress / (float) mDuration) : 1.0f;
+        if (mProgress == 0) {
+            percentage = 0;
+        }
+
+        if (percentage >= 0 && percentage <= 1) {
+            int progress = Math.round(percentage * mBarProgress.getMax());
+            if (!mIsChangingSeekBarProgress) {
+                mBarProgress.setProgress(progress);
+            }
+            mTvDuration.setText(formattedTime(mDuration));
+        }
+    }
+
+    /**
+     * 设置背景
+     *
+     * @param bitmap 背景图
+     */
+    @Override
+    public void setBackground(final Bitmap bitmap) {
+        mImageCover.setImageBitmap(bitmap);
+    }
+
+    /**
+     * 显示背景
+     */
+    @Override
+    public void showBackground() {
+        mImageCover.setVisibility(VISIBLE);
+    }
+
+    @Override
+    public void release() {
+        isDestroy = true;
+    }
+
+    /**
+     * 隐藏背景
+     */
+    @Override
+    public void hideBackground() {
+
+    }
+
+    /**
+     * 重写触摸事件监听,实现手势调节亮度、音量以及播放进度
+     */
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+//        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+//            if (isShowing) {
+//                toggle(true);
+//            } else {
+//                toggle(false);
+//            }
+//        }
+
+        if (mIsOpenGesture && mGestureDetector != null) {
+            mGestureDetector.onTouchEvent(event);
+        }
+
+//        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+//            toggle();
+//        }
+
+//        boolean isCall = event.getAction() == MotionEvent.ACTION_CANCEL || event.getAction() == MotionEvent.ACTION_UP;
+//        if (isCall && mVideoGestureDetector != null && mVideoGestureDetector.isVideoProgressModel()) {
+//            int progress = mVideoGestureDetector.getVideoProgress();
+//
+//            if (progress > mBarProgress.getMax()) {
+//                progress = mBarProgress.getMax();
+//            }
+//            if (progress < 0) {
+//                progress = 0;
+//            }
+//            mBarProgress.setProgress(progress);
+//
+//            float percentage = progress * 1.0f / mBarProgress.getMax();
+//            int seekTime = (int) (percentage * mDuration);
+//
+//            if (mControllerCallback != null) {
+//                mControllerCallback.onSeekTo(seekTime);
+//            }
+//            mIsChangingSeekBarProgress = false;
+//        }
+
+        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+            removeCallbacks(mHideViewRunnable);
+        } else if (event.getAction() == MotionEvent.ACTION_UP) {
+            postDelayed(mHideViewRunnable, HIDDEN_DELAY);
+        }
+        return true;
+    }
+
+    /**
+     * 设置点击事件监听
+     */
+    @Override
+    public void onClick(View view) {
+        if (System.currentTimeMillis() - mLastClickTime < 300) { //限制点击频率
+            return;
+        }
+        mLastClickTime = System.currentTimeMillis();
+        int id = view.getId();
+
+        if (id == R.id.superplayer_iv_pause) { //暂停\播放按钮
+            togglePlayState();
+        } else if (id == R.id.superplayer_iv_fullscreen) { //全屏按钮
+            if (mControllerCallback != null) {
+                mControllerCallback.onSwitchPlayMode(SuperPlayerDef.PlayerMode.NO_LOCK);
+            }
+        } else if (id == R.id.superplayer_iv_play_next) {
+            if (mControllerCallback != null) {
+                mControllerCallback.playNext();
+            }
+        }
+    }
+
+    public void disableGesture(boolean flag) {
+        this.mIsOpenGesture = !flag;
+    }
+
+    @Override
+    public void onProgressChanged(PointProgressBar progressBar, int progress, boolean fromUser) {
+        if (mGestureVideoProgressLayout != null && fromUser) {
+            mGestureVideoProgressLayout.show();
+            float percentage = ((float) progress) / progressBar.getMax();
+            float currentTime = (mDuration * percentage);
+
+            mGestureVideoProgressLayout.setTimeText(formattedTime((long) currentTime) + " / " + formattedTime((long) mDuration));
+            mGestureVideoProgressLayout.setProgress(progress);
+        }
+    }
+
+    @Override
+    public void onStartTrackingTouch(PointProgressBar progressBar) {
+        removeCallbacks(mHideViewRunnable);
+    }
+
+    @Override
+    public void onStopTrackingTouch(PointProgressBar progressBar) {
+        int curProgress = progressBar.getProgress();
+        int maxProgress = progressBar.getMax();
+
+        if (curProgress >= 0 && curProgress <= maxProgress) {
+            float percentage = ((float) curProgress) / maxProgress;
+            int position = (int) (mDuration * percentage);
+
+            if (mControllerCallback != null) {
+                mControllerCallback.onSeekTo(position);
+            }
+        }
+        postDelayed(mHideViewRunnable, HIDDEN_DELAY);
+    }
+}

+ 59 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/view/PlayerRelativeLayout.java

@@ -0,0 +1,59 @@
+package com.tencent.liteav.demo.superplayer.ui.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.RelativeLayout;
+
+import com.tencent.liteav.demo.superplayer.R;
+
+public class PlayerRelativeLayout extends RelativeLayout {
+    private int windowMarginLeft;
+    private int windowMarginTop;
+    private int windowMarginRight;
+    private int windowMarginBottom;
+
+    public PlayerRelativeLayout(Context context) {
+        this(context, null);
+    }
+
+    public PlayerRelativeLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public PlayerRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public PlayerRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    private void init(Context context, AttributeSet set, int defStyleAttr, int defStyleRes) {
+        if (set != null) {
+            TypedArray a = context.obtainStyledAttributes(set, R.styleable.PlayerRelativeLayout);
+            windowMarginLeft = (int) a.getDimension( R.styleable.PlayerRelativeLayout_window_marginLeft, 0);
+            windowMarginTop = (int) a.getDimension(R.styleable.PlayerRelativeLayout_window_marginTop, 0);
+            windowMarginRight = (int) a.getDimension(R.styleable.PlayerRelativeLayout_window_marginRight, 0);
+            windowMarginBottom = (int) a.getDimension(R.styleable.PlayerRelativeLayout_window_marginBottom, 0);
+            a.recycle();
+        }
+    }
+
+    public void onWindowMode() {
+        setBackgroundResource(R.drawable.player_window_bg);
+
+        MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
+        layoutParams.setMargins(windowMarginLeft, windowMarginTop, windowMarginRight, windowMarginBottom);
+        setLayoutParams(layoutParams);
+    }
+
+    public void onFullScreenMode() {
+        setBackgroundResource(R.drawable.player_full_bg);
+
+        MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
+        layoutParams.setMargins(0, 0, 0, 0);
+        setLayoutParams(layoutParams);
+    }
+}

+ 260 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/view/PointProgressBar.java

@@ -0,0 +1,260 @@
+package com.tencent.liteav.demo.superplayer.ui.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.ColorInt;
+
+import com.tencent.liteav.demo.superplayer.R;
+
+/**
+ * 视频播放时的进度指示条
+ */
+public class PointProgressBar extends View {
+    @ColorInt
+    private static final int DEFAULT_BACKGROUND_COLOR = Color.parseColor("#FFFFFF");
+
+    @ColorInt
+    private static final int DEFAULT_PROGRESS_COLOR = Color.parseColor("#FF1234");
+
+    public interface OnProgressBarChangeListener {
+
+        void onProgressChanged(PointProgressBar progressBar, int progress, boolean fromUser);
+
+        void onStartTrackingTouch(PointProgressBar progressBar);
+
+        void onStopTrackingTouch(PointProgressBar progressBar);
+    }
+
+    @ColorInt
+    private int backgroundColor;
+
+    @ColorInt
+    private int progressColor;
+
+    private float progressHeight;
+
+    private Drawable trackDrawable;
+    private float trackWidth;
+    private float trackHeight;
+
+    private final int minValue = 0;
+    private int maxValue;
+    private int progress;
+
+    private Paint backgroundPaint;
+    private Paint progressPaint;
+
+    private RectF backgroundRect;
+    private RectF progressRect;
+
+    private OnProgressBarChangeListener mListener;
+    private boolean mIsOnDrag;
+    private float mLastX;
+
+    public PointProgressBar(Context context) {
+        this(context, null);
+    }
+
+    public PointProgressBar(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public PointProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public PointProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        setWillNotDraw(false);
+        if (attrs != null) {
+            TypedArray a = context.obtainStyledAttributes(
+                    attrs, R.styleable.PointProgressBar, defStyleAttr, defStyleRes);
+
+            backgroundColor = a.getColor(R.styleable.PointProgressBar_backgroundColor, DEFAULT_BACKGROUND_COLOR);
+            progressColor = a.getColor(R.styleable.PointProgressBar_progressColor, DEFAULT_PROGRESS_COLOR);
+            progressHeight = a.getDimension(R.styleable.PointProgressBar_progressHeight, 0f);
+
+            trackDrawable = a.getDrawable(R.styleable.PointProgressBar_trackDrawable);
+            trackWidth = a.getDimension(R.styleable.PointProgressBar_trackWidth, progressHeight);
+            trackHeight = a.getDimension(R.styleable.PointProgressBar_trackHeight, progressHeight);
+            trackDrawable.setBounds(0, 0, (int) trackWidth, (int) trackHeight);
+
+            maxValue = a.getInt(R.styleable.PointProgressBar_maxValue, 100);
+            progress = a.getInt(R.styleable.PointProgressBar_progress, 0);
+
+            a.recycle();
+
+            backgroundPaint = new Paint();
+            backgroundPaint.setColor(backgroundColor);
+
+            progressPaint = new Paint();
+            progressPaint.setColor(progressColor);
+
+            backgroundRect = new RectF();
+            progressRect = new RectF();
+        }
+    }
+
+    public int getMax() {
+        return maxValue;
+    }
+
+    /* 设置进度条最大值 */
+    public void setMax(int max) {
+        if (max > 0) {
+            maxValue = max;
+        }
+
+        internalResetValue();
+    }
+
+    public int getProgress() {
+        return progress;
+    }
+
+    public void setProgress(int progress) {
+        if (mIsOnDrag) return;
+        this.progress = progress;
+        internalResetValue();
+    }
+
+    public void setListener(OnProgressBarChangeListener listener) {
+        this.mListener = listener;
+    }
+
+    /* 调整进度条的当前值使其满足仍然处于[minValue,maxValue] */
+    private void internalResetValue() {
+        if (progress > maxValue) {
+            progress = maxValue;
+        }
+
+        if (progress < minValue) {
+            progress = minValue;
+        }
+
+        invalidate();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (!isEnabled()) return false;
+
+        boolean isHandle = false;
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                isHandle = handleDownEvent(event);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                isHandle = handleMoveEvent(event);
+                break;
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                isHandle = handleUpEvent(event);
+                break;
+
+        }
+        return isHandle;
+    }
+
+    private boolean handleDownEvent(MotionEvent event) {
+        float x = event.getX();
+
+        float min = (x - 100f) / getWidth();
+        float max = (x + 100f) / getWidth();
+        float current = (progress + 0f) / maxValue;
+
+        if (current >= min && current <= max) {
+            if (mListener != null)
+                mListener.onStartTrackingTouch(this);
+            mIsOnDrag = true;
+            mLastX = x;
+            return true;
+        }
+        return false;
+    }
+
+    private boolean handleMoveEvent(MotionEvent event) {
+        float x = event.getX();
+        if (x < 0) x = 0;
+        if (x > getWidth()) x = getWidth();
+
+        Log.d("PointProgressBar", "handleMoveEvent x:" + progress);
+
+        if (mIsOnDrag) {
+            progress = (int) ((x / getWidth()) * maxValue);
+            Log.d("PointProgressBar", "handleMoveEvent p:" + progress);
+            invalidate();
+            callbackProgress();
+            mLastX = x;
+            return true;
+        }
+        return false;
+    }
+
+    private boolean handleUpEvent(MotionEvent event) {
+        float x = event.getX();
+        float y = event.getY();
+        if (mIsOnDrag) {
+            mIsOnDrag = false;
+            if (mListener != null) {
+                mListener.onStopTrackingTouch(this);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    private void callbackProgress() {
+        if (mListener != null) {
+            mListener.onProgressChanged(this, progress, true);
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+
+        backgroundRect.set(0, 0, w, h);
+        progressRect.set(0, 0, 0, h);
+    }
+
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        float p = (progress + 0f) / (maxValue);
+        progressRect.right = getWidth() * p;
+        canvas.drawRoundRect(backgroundRect, progressHeight / 2, progressHeight / 2, backgroundPaint);
+        canvas.drawRoundRect(progressRect, progressHeight / 2, progressHeight / 2, progressPaint);
+
+        if (trackDrawable != null) {
+            canvas.save();
+            canvas.translate(-trackWidth / 2, -(trackHeight - progressHeight) / 2);
+            canvas.translate(progressRect.right, 0);
+            trackDrawable.draw(canvas);
+            canvas.restore();
+        }
+    }
+
+
+}

+ 85 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/view/RoundImageView.java

@@ -0,0 +1,85 @@
+package com.tencent.liteav.demo.superplayer.ui.view;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+import androidx.annotation.Nullable;
+
+import com.tencent.liteav.demo.superplayer.R;
+
+
+public class RoundImageView extends androidx.appcompat.widget.AppCompatImageView {
+    private float topLeftRadius;
+    private float topRightRadius;
+    private float bottomLeftRadius;
+    private float bottomRightRadius;
+
+    private boolean lock = false;
+
+    private Paint lockPaint = new Paint();
+    private Rect rect = new Rect();
+
+    public RoundImageView(Context context) {
+        this(context, null);
+    }
+
+    public RoundImageView(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public RoundImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context, attrs, defStyleAttr);
+    }
+
+    private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        if (attrs != null) {
+            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView, defStyleAttr, 0);
+            float radius = a.getDimension(R.styleable.RoundImageView_radius, 0f);
+            topLeftRadius = a.getDimension(R.styleable.RoundImageView_topLeftRadius, radius);
+            topRightRadius = a.getDimension(R.styleable.RoundImageView_topRightRadius, radius);
+            bottomLeftRadius = a.getDimension(R.styleable.RoundImageView_bottomLeftRadius, radius);
+            bottomRightRadius = a.getDimension(R.styleable.RoundImageView_bottomRightRadius, radius);
+            a.recycle();
+        }
+
+        lockPaint.setColor(Color.parseColor("#66000000"));
+        lockPaint.setStyle(Paint.Style.FILL);
+    }
+
+    public void setLock(boolean lock) {
+        this.lock = lock;
+        invalidate();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        canvas.save();
+        canvas.clipPath(getClipPath(getWidth(), getHeight()));
+        super.onDraw(canvas);
+        if (lock) {
+            rect.set(0, 0, getWidth(), getHeight());
+            canvas.drawRect(rect, lockPaint);
+        }
+        canvas.restore();
+    }
+
+    private Path getClipPath(int width, int height) {
+        final Path path = new Path();
+        float[] radiusArray = new float[]{
+                topLeftRadius, topLeftRadius, topRightRadius, topRightRadius,
+                bottomRightRadius, bottomRightRadius, bottomLeftRadius, bottomLeftRadius
+        };
+        path.addRoundRect(new RectF(0, 0, width, height), radiusArray, Path.Direction.CW);
+
+        return path;
+    }
+}

+ 138 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/view/UnlockProgressView.java

@@ -0,0 +1,138 @@
+package com.tencent.liteav.demo.superplayer.ui.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+
+import com.tencent.liteav.demo.superplayer.util.TimerUtil;
+
+public class UnlockProgressView extends androidx.appcompat.widget.AppCompatImageView {
+    public interface OnUnlockListener {
+        void startUnlock();
+
+        void unlockSuccess();
+
+        void unlockFailed();
+    }
+
+    private Paint paint;
+
+    private final Path segmentPath = new Path();
+
+    private final PathMeasure pathMeasure = new PathMeasure();
+
+    private TimerUtil timerUtil;
+
+    private OnUnlockListener unlockListener;
+
+    public void setUnlockListener(OnUnlockListener listener) {
+        this.unlockListener = listener;
+    }
+
+    public UnlockProgressView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public UnlockProgressView(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public UnlockProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context);
+    }
+
+    private void init(Context context) {
+        paint = new Paint();
+        paint.setColor(Color.RED);
+        paint.setStyle(Paint.Style.STROKE);
+        paint.setStrokeWidth(12f);
+        paint.setStrokeCap(Paint.Cap.ROUND);
+
+        timerUtil = new TimerUtil
+                .Builder()
+                .setPeriod(16)
+                .setOnTick(new TimerUtil.OnTickListener() {
+                    @Override
+                    public void onStart() {
+                        if (unlockListener != null) {
+                            unlockListener.startUnlock();
+                        }
+                    }
+
+                    @Override
+                    public void onTick(int count, long period) {
+                        if (count >= 60) {
+                            if (unlockListener != null) {
+                                getHandler().postDelayed(() -> {
+                                    unlockListener.unlockSuccess();
+                                }, 0);
+                            }
+                            timerUtil.reset();
+                        }
+
+                        invalidate();
+                    }
+                }).build();
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        Path entirePath = getPath(w ,h);
+        pathMeasure.setPath(entirePath, true);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+            timerUtil.start();
+        } else if (event.getAction() == MotionEvent.ACTION_UP) {
+            if (timerUtil.getCount() < 60 && unlockListener != null) {
+                unlockListener.unlockFailed();
+            }
+            timerUtil.reset();
+        }
+
+        return true;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+//        float pathLength = pathMeasure.getLength();
+//        segmentPath.reset();
+//        pathMeasure.getSegment(0, pathLength * timerUtil.getCount() / 120, segmentPath, true);
+//
+//        canvas.drawPath(segmentPath, paint);
+    }
+
+    private Path getPath(int width, int height) {
+        Path path = new Path();
+        final float radius = (width - getPaddingLeft() - getPaddingRight()) / 2;
+        path.addCircle(width / 2, height / 2, radius, Path.Direction.CW);
+        return path;
+    }
+}

+ 117 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/view/VideoProgressLayout.java

@@ -0,0 +1,117 @@
+package com.tencent.liteav.demo.superplayer.ui.view;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.tencent.liteav.demo.superplayer.R;
+
+/**
+ * 滑动手势控制播放进度时显示的进度提示view
+ */
+
+public class VideoProgressLayout extends RelativeLayout {
+    private ImageView    mIvThumbnail;       // 视频缩略图
+    private TextView     mTvTime;            // 视频进度文本
+    private ProgressBar  mProgressBar;       // 进度条
+    private HideRunnable mHideRunnable;      // 隐藏自身的线程
+    private int          duration = 1000;    // 自身消失的延迟事件ms
+
+    public VideoProgressLayout(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public VideoProgressLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    private void init(Context context) {
+        LayoutInflater.from(context).inflate(R.layout.superplayer_video_progress_layout, this);
+        mIvThumbnail = (ImageView) findViewById(R.id.superplayer_iv_progress_thumbnail);
+        mProgressBar = (ProgressBar) findViewById(R.id.superplayer_pb_progress_bar);
+        mTvTime = (TextView) findViewById(R.id.superplayer_tv_progress_time);
+        setVisibility(GONE);
+        mHideRunnable = new HideRunnable();
+    }
+
+    /**
+     * 显示view
+     */
+    public void show() {
+        setVisibility(VISIBLE);
+        removeCallbacks(mHideRunnable);
+        postDelayed(mHideRunnable, duration);
+    }
+
+    /**
+     * 设置视频进度事件文本
+     *
+     * @param text
+     */
+    public void setTimeText(String text) {
+        mTvTime.setText(text);
+    }
+
+    /**
+     * 设置progressbar的进度值
+     *
+     * @param progress
+     */
+    public void setProgress(int progress) {
+        mProgressBar.setProgress(progress);
+    }
+
+    /**
+     * 设置view消失延迟的时间
+     *
+     * @param duration
+     */
+    public void setDuration(int duration) {
+        this.duration = duration;
+    }
+
+    /**
+     * 设置缩略图图片
+     *
+     * @param bitmap
+     */
+    public void setThumbnail(Bitmap bitmap) {
+        mIvThumbnail.setVisibility(VISIBLE);
+        mIvThumbnail.setImageBitmap(bitmap);
+    }
+
+    /**
+     * 设置缩略图
+     */
+    public void hideThumbnail() {
+        mIvThumbnail.setVisibility(GONE);
+    }
+
+    /**
+     * 设置progressbar的可见性
+     *
+     * @param enable
+     */
+    public void setProgressVisibility(boolean enable) {
+        mProgressBar.setVisibility(enable ? VISIBLE : GONE);
+    }
+
+    /**
+     * 隐藏view的线程
+     */
+    private class HideRunnable implements Runnable {
+        @Override
+        public void run() {
+            mIvThumbnail.setImageBitmap(null);
+            mIvThumbnail.setVisibility(GONE);
+            VideoProgressLayout.this.setVisibility(GONE);
+        }
+    }
+}

+ 85 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/view/VolumeBrightnessProgressLayout.java

@@ -0,0 +1,85 @@
+package com.tencent.liteav.demo.superplayer.ui.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+
+import com.tencent.liteav.demo.superplayer.R;
+
+
+/**
+ * 滑动手势设置音量、亮度时显示的提示view
+ */
+public class VolumeBrightnessProgressLayout extends RelativeLayout {
+    private ImageView    mImageCenter;       // 中心图片:亮度提示、音量提示
+    private ProgressBar  mProgressBar;       // 进度条
+    private HideRunnable mHideRunnable;      // 隐藏view的runnable
+    private int          mDuration = 1000;   // view消失延迟时间(秒)
+
+    public VolumeBrightnessProgressLayout(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public VolumeBrightnessProgressLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    private void init(Context context) {
+        LayoutInflater.from(context).inflate(R.layout.superplayer_video_volume_brightness_progress_layout, this);
+        mImageCenter = (ImageView) findViewById(R.id.superplayer_iv_center);
+        mProgressBar = (ProgressBar) findViewById(R.id.superplayer_pb_progress_bar);
+        mHideRunnable = new HideRunnable();
+        setVisibility(GONE);
+    }
+
+    /**
+     * 显示
+     */
+    public void show() {
+        setVisibility(VISIBLE);
+        removeCallbacks(mHideRunnable);
+        postDelayed(mHideRunnable, mDuration);
+    }
+
+    /**
+     * 设置progressBar的进度值
+     *
+     * @param progress
+     */
+    public void setProgress(int progress) {
+        mProgressBar.setProgress(progress);
+    }
+
+    /**
+     * 设置view消失的延迟时间
+     *
+     * @param duration
+     */
+    public void setDuration(int duration) {
+        this.mDuration = duration;
+    }
+
+    /**
+     * 设置显示的图片,亮度提示图片或者音量提示图片
+     *
+     * @param resource
+     */
+    public void setImageResource(int resource) {
+        mImageCenter.setImageResource(resource);
+    }
+
+    /**
+     * 隐藏view的runnable
+     */
+    private class HideRunnable implements Runnable {
+        @Override
+        public void run() {
+            VolumeBrightnessProgressLayout.this.setVisibility(GONE);
+        }
+    }
+}

+ 384 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/view/WheelView.java

@@ -0,0 +1,384 @@
+package com.tencent.liteav.demo.superplayer.ui.view;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Author: wangjie
+ * Email: tiantian.china.2@gmail.com
+ * Date: 7/1/14.
+ */
+public class WheelView extends ScrollView {
+    public static final String TAG = WheelView.class.getSimpleName();
+
+    public static class OnWheelViewListener {
+        public void onSelected(int selectedIndex, String item) {
+        }
+    }
+
+
+    private Context context;
+//    private ScrollView scrollView;
+
+    private LinearLayout views;
+
+    public WheelView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public WheelView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public WheelView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    //    String[] items;
+    List<String> items;
+
+    private List<String> getItems() {
+        return items;
+    }
+
+    public void setItems(List<String> list) {
+        if (null == items) {
+            items = new ArrayList<String>();
+        }
+        items.clear();
+        items.addAll(list);
+
+        // 前面和后面补全
+        for (int i = 0; i < offset; i++) {
+            items.add(0, "");
+            items.add("");
+        }
+
+        initData();
+
+    }
+
+
+    public static final int OFF_SET_DEFAULT = 1;
+    int offset = OFF_SET_DEFAULT; // 偏移量(需要在最前面和最后面补全)
+
+    public int getOffset() {
+        return offset;
+    }
+
+    public void setOffset(int offset) {
+        this.offset = offset;
+    }
+
+    int displayItemCount; // 每页显示的数量
+
+    int selectedIndex = 1;
+
+
+    private void init(Context context) {
+        this.context = context;
+        Log.d(TAG, "parent: " + this.getParent());
+        this.setVerticalScrollBarEnabled(false);
+
+        views = new LinearLayout(context);
+        views.setOrientation(LinearLayout.VERTICAL);
+        this.addView(views);
+
+        scrollerTask = new Runnable() {
+
+            public void run() {
+
+                int newY = getScrollY();
+                if (initialY - newY == 0) { // stopped
+                    final int remainder = initialY % itemHeight;
+                    final int divided = initialY / itemHeight;
+                    if (remainder == 0) {
+                        selectedIndex = divided + offset;
+
+                        onSeletedCallBack();
+                    } else {
+                        if (remainder > itemHeight / 2) {
+                            WheelView.this.post(new Runnable() {
+                                @Override
+                                public void run() {
+                                    WheelView.this.smoothScrollTo(0, initialY - remainder + itemHeight);
+                                    selectedIndex = divided + offset + 1;
+                                    onSeletedCallBack();
+                                }
+                            });
+                        } else {
+                            WheelView.this.post(new Runnable() {
+                                @Override
+                                public void run() {
+                                    WheelView.this.smoothScrollTo(0, initialY - remainder);
+                                    selectedIndex = divided + offset;
+                                    onSeletedCallBack();
+                                }
+                            });
+                        }
+
+
+                    }
+
+
+                } else {
+                    initialY = getScrollY();
+                    WheelView.this.postDelayed(scrollerTask, newCheck);
+                }
+            }
+        };
+
+
+    }
+
+    int initialY;
+
+    Runnable scrollerTask;
+    int newCheck = 50;
+
+    public void startScrollerTask() {
+
+        initialY = getScrollY();
+        this.postDelayed(scrollerTask, newCheck);
+    }
+
+    private void initData() {
+        displayItemCount = offset * 2 + 1;
+
+        for (String item : items) {
+            views.addView(createView(item));
+        }
+
+        refreshItemView(0);
+    }
+
+    int itemHeight = 0;
+
+    private TextView createView(String item) {
+        TextView tv = new TextView(context);
+        tv.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+        tv.setSingleLine(true);
+        tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 15);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            tv.setLineHeight(21);
+        } else {
+            tv.setHeight(dip2px(21));
+        }
+        tv.setText(item);
+        tv.setGravity(Gravity.CENTER);
+        int padding = dip2px(5);
+        tv.setPadding(padding, padding, padding, padding);
+        if (0 == itemHeight) {
+            itemHeight = getViewMeasuredHeight(tv);
+            Log.d(TAG, "itemHeight: " + itemHeight);
+            views.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, itemHeight * displayItemCount));
+            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) this.getLayoutParams();
+            this.setLayoutParams(new LinearLayout.LayoutParams(lp.width, itemHeight * displayItemCount));
+        }
+        return tv;
+    }
+
+
+    @Override
+    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+        super.onScrollChanged(l, t, oldl, oldt);
+        refreshItemView(t);
+
+        if (t > oldt) {
+            scrollDirection = SCROLL_DIRECTION_DOWN;
+        } else {
+            scrollDirection = SCROLL_DIRECTION_UP;
+        }
+    }
+
+    private void refreshItemView(int y) {
+        int position = y / itemHeight + offset;
+        int remainder = y % itemHeight;
+        int divided = y / itemHeight;
+
+        if (remainder == 0) {
+            position = divided + offset;
+        } else {
+            if (remainder > itemHeight / 2) {
+                position = divided + offset + 1;
+            }
+        }
+
+        int childSize = views.getChildCount();
+        for (int i = 0; i < childSize; i++) {
+            TextView itemView = (TextView) views.getChildAt(i);
+            if (null == itemView) {
+                return;
+            }
+            if (position == i) {
+                itemView.setTextColor(Color.parseColor("#FF333333"));
+            } else {
+                itemView.setTextColor(Color.parseColor("#FF999999"));
+            }
+        }
+    }
+
+    /**
+     * 获取选中区域的边界
+     */
+    int[] selectedAreaBorder;
+
+    private int[] obtainSelectedAreaBorder() {
+        if (null == selectedAreaBorder) {
+            selectedAreaBorder = new int[2];
+            selectedAreaBorder[0] = itemHeight * offset;
+            selectedAreaBorder[1] = itemHeight * (offset + 1);
+        }
+        return selectedAreaBorder;
+    }
+
+
+    private int scrollDirection = -1;
+    private static final int SCROLL_DIRECTION_UP = 0;
+    private static final int SCROLL_DIRECTION_DOWN = 1;
+
+    Paint paint;
+    int viewWidth;
+
+    @Override
+    public void setBackground(Drawable background) {
+
+        if (viewWidth == 0) {
+            viewWidth = getWidth();
+            Log.d(TAG, "viewWidth: " + viewWidth);
+        }
+
+        if (null == paint) {
+            paint = new Paint();
+            paint.setColor(Color.parseColor("#FF0B57C7"));
+            paint.setStrokeWidth(dip2px(1f));
+        }
+
+        background = new Drawable() {
+            @Override
+            public void draw(Canvas canvas) {
+                canvas.drawLine(0, obtainSelectedAreaBorder()[0], viewWidth, obtainSelectedAreaBorder()[0], paint);
+                canvas.drawLine(0, obtainSelectedAreaBorder()[1], viewWidth, obtainSelectedAreaBorder()[1], paint);
+            }
+
+            @Override
+            public void setAlpha(int alpha) {
+
+            }
+
+            @Override
+            public void setColorFilter(ColorFilter cf) {
+
+            }
+
+            @Override
+            public int getOpacity() {
+                return PixelFormat.UNKNOWN;
+            }
+        };
+
+
+        super.setBackground(background);
+
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        Log.d(TAG, "w: " + w + ", h: " + h + ", oldw: " + oldw + ", oldh: " + oldh);
+        viewWidth = w;
+        setBackground(null);
+    }
+
+    /**
+     * 选中回调
+     */
+    private void onSeletedCallBack() {
+        if (null != onWheelViewListener) {
+            onWheelViewListener.onSelected(selectedIndex, items.get(selectedIndex));
+        }
+
+    }
+
+    public void setSeletion(int position) {
+        final int p = position;
+        selectedIndex = p + offset;
+        this.post(new Runnable() {
+            @Override
+            public void run() {
+                WheelView.this.smoothScrollTo(0, p * itemHeight);
+            }
+        });
+
+    }
+
+    public String getSeletedItem() {
+        return items.get(selectedIndex);
+    }
+
+    public int getSeletedIndex() {
+        return selectedIndex - offset;
+    }
+
+
+    @Override
+    public void fling(int velocityY) {
+        super.fling(velocityY / 3);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_UP) {
+
+            startScrollerTask();
+        }
+        return super.onTouchEvent(ev);
+    }
+
+    private OnWheelViewListener onWheelViewListener;
+
+    public OnWheelViewListener getOnWheelViewListener() {
+        return onWheelViewListener;
+    }
+
+    public void setOnWheelViewListener(OnWheelViewListener onWheelViewListener) {
+        this.onWheelViewListener = onWheelViewListener;
+    }
+
+    private int dip2px(float dpValue) {
+        final float scale = getContext().getResources().getDisplayMetrics().density;
+        return (int) (dpValue * scale + 0.5f);
+    }
+
+    private int getViewMeasuredHeight(View view) {
+        int width = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+        int expandSpec = View.MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, View.MeasureSpec.AT_MOST);
+        view.measure(width, expandSpec);
+        return view.getMeasuredHeight();
+    }
+
+}
+

+ 91 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/util/CountDownUtil.java

@@ -0,0 +1,91 @@
+package com.tencent.liteav.demo.superplayer.util;
+
+import java.util.ArrayList;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class CountDownUtil {
+    // 倒计时的监听者
+    public interface OnTickListener {
+        void onTimeout(CountDownUtil util);
+    }
+
+    private static final CountDownUtil INSTANCE = new CountDownUtil();
+
+    public static CountDownUtil getInstance() {
+        return INSTANCE;
+    }
+
+    public static void addListener(OnTickListener listener) {
+        INSTANCE.listeners.add(listener);
+    }
+
+    public static void removeListener(OnTickListener listener) {
+        INSTANCE.listeners.remove(listener);
+    }
+
+    // 周期时间
+    private static final long period = 1000;
+
+    // 定时数
+    private long maxCount = Integer.MAX_VALUE;
+
+    // 回调监听对象
+    private final ArrayList<CountDownUtil.OnTickListener> listeners = new ArrayList<>();
+
+    // 计时器
+    private Timer timer;
+
+    // 计数器
+    private final AtomicLong count = new AtomicLong(0L);
+
+    private CountDownUtil() {
+    }
+
+
+    public void startOrResume() {
+        pause();
+        setupTask();
+    }
+
+    private void setupTask() {
+        timer = new Timer();
+        // 计时任务
+        TimerTask task = new TimerTask() {
+            @Override
+            public void run() {
+                count.addAndGet(1);
+
+                if (count.get() >= maxCount) {
+                    pause();
+
+                    for (OnTickListener listener : listeners) {
+                        listener.onTimeout(CountDownUtil.this);
+                    }
+                }
+            }
+        };
+
+        timer.schedule(task, 0, period);
+    }
+
+    public void pause() {
+        if (timer != null) {
+            timer.cancel();
+            timer = null;
+        }
+    }
+
+    public void reset(long maxCount, Boolean autoStart) {
+        this.maxCount = maxCount;
+        pause();
+        count.set(0);
+        if (autoStart) startOrResume();
+    }
+
+    public Long getCount() {
+        return count.get();
+    }
+}

+ 117 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/util/TimerUtil.java

@@ -0,0 +1,117 @@
+package com.tencent.liteav.demo.superplayer.util;
+
+
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class TimerUtil {
+    // 周期时间
+    private final long period;
+
+    private OnTickListener listener;
+
+    private Timer timer = new Timer();
+
+    private TimerTask task;
+
+    private AtomicInteger count = new AtomicInteger(0);
+
+    public interface OnTickListener {
+        void onStart();
+
+        void onTick(int count, long period);
+    }
+
+    public static class Builder {
+        // 周期时间
+        private long period = 16;
+
+        private OnTickListener listener = null;
+
+        public Builder() {
+        }
+
+
+        public Builder setPeriod(long period) {
+            assert period > 0;
+            this.period = period;
+            return this;
+        }
+
+        public Builder setOnTick(OnTickListener listener) {
+            this.listener = listener;
+            return this;
+        }
+
+        public TimerUtil build() {
+            return new TimerUtil(period, listener);
+        }
+    }
+
+    private TimerUtil(long period, OnTickListener listener) {
+        this.period = period;
+        this.listener = listener;
+    }
+
+    public int getCount() {
+        return count.get();
+    }
+
+    public void start() {
+        if (task != null) {
+            task.cancel();
+            timer.purge();
+        }
+
+        if (listener != null) {
+            listener.onStart();
+        }
+
+        task = new TimerTask() {
+            @Override
+            public void run() {
+                count.addAndGet(1);
+                if (listener != null) {
+                    listener.onTick(count.get(), period);
+                }
+            }
+        };
+
+        timer.schedule(task, 0, period);
+    }
+
+    public void pause() {
+        if (task != null) {
+            task.cancel();
+            timer.purge();
+        }
+    }
+
+    public void resume() {
+        task = new TimerTask() {
+            @Override
+            public void run() {
+                count.addAndGet(1);
+                if (listener != null) {
+                    listener.onTick(count.get(), period);
+                }
+            }
+        };
+
+        timer.schedule(task, 0, period);
+    }
+
+    public void cancel() {
+        timer.cancel();
+    }
+
+    public void reset() {
+        if (task != null) {
+            task.cancel();
+        }
+        timer.purge();
+        count.set(0);
+        listener.onTick(0, period);
+    }
+}

+ 199 - 0
kit/src/main/java/com/tencent/liteav/demo/superplayer/util/VideoGestureDetector.java

@@ -0,0 +1,199 @@
+package com.tencent.liteav.demo.superplayer.util;
+
+import android.app.Activity;
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.media.AudioManager;
+import android.provider.Settings;
+import android.view.MotionEvent;
+import android.view.Window;
+import android.view.WindowManager;
+
+/**
+ * 手势控制视频播放进度、调节亮度音量的工具
+ */
+
+public class VideoGestureDetector {
+    // 手势类型
+    private static final int NONE           = 0;    // 无效果
+    private static final int VOLUME         = 1;    // 音量
+    private static final int BRIGHTNESS     = 2;    // 亮度
+    private static final int VIDEO_PROGRESS = 3;    // 播放进度
+
+    private int                        mScrollMode  = NONE;     // 手势类型
+    private VideoGestureListener       mVideoGestureListener;  // 回调
+    private int                        mVideoWidth;            // 视频宽度px
+    private float                      mBrightness  = 1;        // 当前亮度(0.0~1.0)
+    private Window                     mWindow;                // 当前window
+    private WindowManager.LayoutParams mLayoutParams;          // 用于获取和设置屏幕亮度
+    private ContentResolver            mResolver;              // 用于获取当前屏幕亮度
+    private AudioManager               mAudioManager;          // 音频管理器,用于设置音量
+    private int                        mMaxVolume   = 0;         // 最大音量值
+    private int                        mOldVolume   = 0;         // 记录调节音量之前的旧音量值
+    private int                        mVideoProgress;         // 记录滑动后的进度,在回调中抛出
+    private int                        mDownProgress;          // 滑动开始时的视频播放进度
+    private int                        offsetX      = 20; //手势临界值,当两滑动事件坐标的水平差值>20时判定为{@link #VIDEO_PROGRESS}, 否则判定为{@link #VOLUME}或者{@link #BRIGHTNESS}
+    private float                      mSensitivity = 0.3f;    // 调节音量、亮度的灵敏度   //手势灵敏度 0.0~1.0
+
+    public VideoGestureDetector(Context context) {
+        mAudioManager = (AudioManager) context.getSystemService(Service.AUDIO_SERVICE);
+        mMaxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+        if (context instanceof Activity) {
+            mWindow = ((Activity) context).getWindow();
+            mLayoutParams = mWindow.getAttributes();
+            mBrightness = mLayoutParams.screenBrightness;
+        }
+        mResolver = context.getContentResolver();
+    }
+
+    /**
+     * 设置回调
+     *
+     * @param videoGestureListener
+     */
+    public void setVideoGestureListener(VideoGestureListener videoGestureListener) {
+        mVideoGestureListener = videoGestureListener;
+    }
+
+    /**
+     * 重置数据以开始新的一次滑动
+     *
+     * @param videoWidth   视频宽度px
+     * @param downProgress 手势按下时视频的播放进度(秒)
+     */
+    public void reset(int videoWidth, int downProgress) {
+        mVideoProgress = 0;
+        mVideoWidth = videoWidth;
+        mScrollMode = NONE;
+        mOldVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+        mBrightness = mLayoutParams.screenBrightness;
+        if (mBrightness == -1) {
+            //一开始是默认亮度的时候,获取系统亮度,计算比例值
+            mBrightness = getBrightness() / 255.0f;
+        }
+        mDownProgress = downProgress;
+    }
+
+    /**
+     * 获取当前是否是视频进度滑动手势
+     *
+     * @return
+     */
+    public boolean isVideoProgressModel() {
+        return mScrollMode == VIDEO_PROGRESS;
+    }
+
+    /**
+     * 获取滑动后对应的视频进度
+     *
+     * @return
+     */
+    public int getVideoProgress() {
+        return mVideoProgress;
+    }
+
+    /**
+     * 滑动手势操控类别判定
+     *
+     * @param height    滑动事件的高度px
+     * @param downEvent 按下事件
+     * @param moveEvent 滑动事件
+     * @param distanceX 滑动水平距离
+     * @param distanceY 滑动竖直距离
+     */
+    public void check(int height, MotionEvent downEvent, MotionEvent moveEvent, float distanceX, float distanceY) {
+        switch (mScrollMode) {
+            case NONE:
+                //offset是让快进快退不要那么敏感的值
+                if (Math.abs(downEvent.getX() - moveEvent.getX()) > offsetX) {
+                    mScrollMode = VIDEO_PROGRESS;
+                } else {
+                    int halfVideoWidth = mVideoWidth / 2;
+                    if (downEvent.getX() < halfVideoWidth) {
+                        mScrollMode = BRIGHTNESS;
+                    } else {
+                        mScrollMode = VOLUME;
+                    }
+                }
+                break;
+            case VOLUME:
+                int value = height / mMaxVolume;
+                int newVolume = (int) ((downEvent.getY() - moveEvent.getY()) / value * mSensitivity + mOldVolume);
+                mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newVolume, AudioManager.FLAG_PLAY_SOUND);
+
+                float volumeProgress = newVolume / Float.valueOf(mMaxVolume) * 100;
+                if (mVideoGestureListener != null) {
+                    mVideoGestureListener.onVolumeGesture(volumeProgress);
+                }
+                break;
+            case BRIGHTNESS:
+                float newBrightness = height == 0 ? 0 : (downEvent.getY() - moveEvent.getY()) / height * mSensitivity;
+                newBrightness += mBrightness;
+
+                if (newBrightness < 0) {
+                    newBrightness = 0;
+                } else if (newBrightness > 1) {
+                    newBrightness = 1;
+                }
+                if (mLayoutParams != null) {
+                    mLayoutParams.screenBrightness = newBrightness;
+                }
+                if (mWindow != null) {
+                    mWindow.setAttributes(mLayoutParams);
+                }
+
+                if (mVideoGestureListener != null) {
+                    mVideoGestureListener.onBrightnessGesture(newBrightness);
+                }
+                break;
+            case VIDEO_PROGRESS:
+                float dis = moveEvent.getX() - downEvent.getX();
+                float percent = dis / mVideoWidth;
+                mVideoProgress = (int) (mDownProgress + percent * 100);
+                if (mVideoGestureListener != null) {
+                    mVideoGestureListener.onSeekGesture(mVideoProgress);
+                }
+                break;
+        }
+    }
+
+    /**
+     * 获取当前亮度
+     *
+     * @return
+     */
+    private int getBrightness() {
+        if (mResolver != null) {
+            return Settings.System.getInt(mResolver, Settings.System.SCREEN_BRIGHTNESS, 255);
+        } else {
+            return 255;
+        }
+    }
+
+    /**
+     * 回调
+     */
+    public interface VideoGestureListener {
+        /**
+         * 亮度调节回调
+         *
+         * @param newBrightness 滑动后的新亮度值
+         */
+        void onBrightnessGesture(float newBrightness);
+
+        /**
+         * 音量调节回调
+         *
+         * @param volumeProgress 滑动后的新音量值
+         */
+        void onVolumeGesture(float volumeProgress);
+
+        /**
+         * 播放进度调节回调
+         *
+         * @param seekProgress 滑动后的新视频进度
+         */
+        void onSeekGesture(int seekProgress);
+    }
+}

+ 34 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/PlayerApplication.java

@@ -0,0 +1,34 @@
+package com.tencent.liteav.demo.superplayer;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+
+import com.tencent.liteav.demo.superplayer.database.PlayerDatabase;
+import com.tencent.liteav.demo.superplayer.database.PlayerDatabaseProvider;
+import com.tencent.liteav.demo.superplayer.database.repo.PlayerRepository;
+import com.tencent.liteav.demo.superplayer.util.HttpUtils;
+
+public class PlayerApplication extends Application implements PlayerDatabaseProvider {
+    private PlayerDatabaseProvider.Instance provider;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        provider = new PlayerDatabaseProvider.Instance(this);
+        HttpUtils.INSTANCE.init(this);
+    }
+
+    @NonNull
+    @Override
+    public PlayerDatabase getPlayerDatabase() {
+        return provider.getPlayerDatabase();
+    }
+
+
+    @NonNull
+    @Override
+    public PlayerRepository getPlayerRepository() {
+        return provider.getPlayerRepository();
+    }
+}

+ 77 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/PlayerDatabase.kt

@@ -0,0 +1,77 @@
+package com.tencent.liteav.demo.superplayer.database
+
+import android.content.Context
+import android.util.Log
+import androidx.room.AutoMigration
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import com.tencent.liteav.demo.superplayer.database.dao.CountDownDao
+import com.tencent.liteav.demo.superplayer.database.dao.HistoryDao
+import com.tencent.liteav.demo.superplayer.database.dao.RecordDao
+import com.tencent.liteav.demo.superplayer.database.entity.CountDown
+import com.tencent.liteav.demo.superplayer.database.entity.History
+import com.tencent.liteav.demo.superplayer.database.entity.Record
+
+@Database(
+    version = 1,
+    entities = [History::class, CountDown::class, Record::class],
+    exportSchema = true,
+)
+abstract class PlayerDatabase : RoomDatabase() {
+    abstract fun historyDao(): HistoryDao
+    abstract fun countDownDao(): CountDownDao
+    abstract fun recordDao(): RecordDao
+
+    companion object {
+        @Volatile
+        private var INSTANCE: PlayerDatabase? = null
+
+        @JvmStatic
+        fun getDatabase(context: Context): PlayerDatabase {
+            return INSTANCE ?: synchronized(this) {
+                val instance = Room.databaseBuilder(
+                    context.applicationContext,
+                    PlayerDatabase::class.java,
+                    "player_database"
+                ).build()
+                INSTANCE = instance
+                // return instance
+                instance
+            }
+        }
+
+        private val migration1_2 = object : Migration(1, 2) {
+            override fun migrate(database: SupportSQLiteDatabase) {
+                database.execSQL("ALTER TABLE `History` ADD COLUMN `section_id` TEXT;")
+            }
+        }
+
+        private val migration2_3 = object : Migration(2, 3) {
+            override fun migrate(database: SupportSQLiteDatabase) {
+                database.execSQL("ALTER TABLE `History` ADD COLUMN `date` TEXT;")
+            }
+        }
+
+        private val migration3_4 = object : Migration(3, 4) {
+            override fun migrate(database: SupportSQLiteDatabase) {
+                database.execSQL("DELETE FROM `History`;")
+                database.execSQL("DELETE FROM `CountDown`;")
+            }
+        }
+
+        private val migration4_5 = object : Migration(4, 5) {
+            override fun migrate(database: SupportSQLiteDatabase) {
+                database.execSQL("ALTER TABLE `History` ADD COLUMN `ct_id` TEXT;")
+            }
+        }
+
+        private val migration5_6 = object : Migration(5, 6) {
+            override fun migrate(database: SupportSQLiteDatabase) {
+                database.execSQL("ALTER TABLE `CountDown` ADD COLUMN `datetime` TEXT;")
+            }
+        }
+    }
+}

+ 37 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/PlayerDatabaseProvider.kt

@@ -0,0 +1,37 @@
+package com.tencent.liteav.demo.superplayer.database
+
+import android.app.Application
+import com.tencent.liteav.demo.superplayer.database.PlayerDatabase.Companion.getDatabase
+import com.tencent.liteav.demo.superplayer.database.repo.PlayerRepository
+
+interface PlayerDatabaseProvider {
+    fun getPlayerDatabase(): PlayerDatabase
+    fun getPlayerRepository(): PlayerRepository
+
+    public class Instance(
+        val context: Application,
+    ) : PlayerDatabaseProvider {
+        private var database: PlayerDatabase? = null
+        private var repository: PlayerRepository? = null
+
+        override fun getPlayerDatabase(): PlayerDatabase {
+            if (database == null) {
+                database = getDatabase(context)
+            }
+
+            return database!!
+        }
+
+        override fun getPlayerRepository(): PlayerRepository {
+            if (repository == null) {
+                repository = PlayerRepository(
+                    getPlayerDatabase().historyDao(),
+                    getPlayerDatabase().countDownDao(),
+                    getPlayerDatabase().recordDao()
+                )
+            }
+            return repository!!
+        }
+
+    }
+}

+ 25 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/dao/CountDownDao.kt

@@ -0,0 +1,25 @@
+package com.tencent.liteav.demo.superplayer.database.dao
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy.REPLACE
+import androidx.room.Query
+import com.tencent.liteav.demo.superplayer.database.entity.CountDown
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * 倒计时相关的数据库操作
+ */
+@Dao
+interface CountDownDao {
+
+    @Query("SELECT * FROM countdown WHERE id = :courseId LIMIT 1")
+    fun queryCountDown(courseId: String): CountDown?
+
+    @Insert(onConflict = REPLACE)
+    fun insertCountDown(countDown: CountDown)
+
+    @Query("DELETE FROM countdown WHERE id = :courseId")
+    fun deleteCountDown(courseId: String): Int
+}

+ 43 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/dao/HistoryDao.kt

@@ -0,0 +1,43 @@
+package com.tencent.liteav.demo.superplayer.database.dao
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy.REPLACE
+import androidx.room.Query
+import com.tencent.liteav.demo.superplayer.database.entity.History
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+public interface HistoryDao {
+    @Query("SELECT * FROM history WHERE course_id = :courseId")
+    fun getById(courseId: String): Flow<List<History>>
+
+    @Insert(onConflict = REPLACE)
+    fun insertHistory(history: History)
+
+    @Insert
+    fun insertHistories(vararg histories: History)
+
+    @Query("SELECT COUNT(DISTINCT(section_id)) FROM History WHERE date = :date AND course_id = :courseId AND ct_id = :countDown")
+    fun queryTodayEpisodeHistory(
+        courseId: String,
+        countDown: String,
+        date: String,
+    ): Int
+
+    @Query("SELECT * FROM History WHERE date = :date AND course_id = :courseId AND ct_id = :countDown;")
+    fun queryTodayDurationHistory(
+        courseId: String,
+        countDown: String,
+        date: String,
+    ): List<History>
+
+    @Query("SELECT DISTINCT section_id FROM History WHERE course_id = :courseId")
+    fun queryAllHistory(courseId: String): List<String>
+
+    @Query("SELECT * FROM History WHERE course_id = :courseId AND section_id = :sectionId")
+    fun queryAllSectionHistory(courseId: String, sectionId: String): List<History>
+
+    @Query("DELETE FROM History WHERE course_id = :courseId")
+    fun deleteAllHistory(courseId:String)
+}

+ 23 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/dao/RecordDao.kt

@@ -0,0 +1,23 @@
+package com.tencent.liteav.demo.superplayer.database.dao
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import com.tencent.liteav.demo.superplayer.database.entity.Record
+
+@Dao
+interface RecordDao {
+
+    @Query("SELECT * FROM record WHERE courseId = :courseId")
+    fun queryRecord(courseId: String): List<Record>
+
+    @Query("SELECT * FROM record WHERE courseId = :courseId AND aiCourseItemId = :sectionId")
+    fun queryRecord(courseId: String, sectionId: String): Record?
+
+    @Insert(onConflict = OnConflictStrategy.REPLACE)
+    fun insertRecord(record: Record)
+
+    @Query("DELETE FROM record WHERE courseId = :courseId")
+    fun deleteRecord(courseId: String)
+}

+ 69 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/entity/CountDown.kt

@@ -0,0 +1,69 @@
+package com.tencent.liteav.demo.superplayer.database.entity
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+data class CountDown(
+    /**
+     * 使用course id作为主键
+     */
+    @PrimaryKey
+    val id: String,
+
+    /**
+     * 定时类型
+     *
+     * 1集数 2时长
+     */
+    @ColumnInfo(name = "type")
+    val type: Int,
+
+    /**
+     * 定时的值
+     *
+     * 类型为集数时则表示多少集
+     * 类型为时长是则表示多少秒
+     */
+    @ColumnInfo(name = "value")
+    val value: Int,
+
+    /**
+     * 定时剩下的值
+     *
+     * 类型为集数时则表示多少集
+     * 类型为时长是则表示多少秒
+     */
+    @ColumnInfo(name = "rest")
+    val rest: Int,
+
+    /**
+     * 作为count down的唯一的特征
+     */
+    @ColumnInfo(name = "datetime")
+    val datetime: String? = null,
+) {
+    fun copyWith(
+        type: Int? = null,
+        value: Int? = null,
+        rest: Int? = null,
+        datetime: String? = null,
+    ): CountDown {
+        return CountDown(
+            id = this.id,
+            type = type ?: this.type,
+            value = value ?: this.value,
+            rest = rest ?: this.rest,
+            datetime = datetime ?: this.datetime,
+        )
+    }
+
+    companion object {
+        @JvmStatic
+        public val TYPE_EPISODE = 1;
+
+        @JvmStatic
+        public val TYPE_DURATION = 2;
+    }
+}

+ 54 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/entity/History.kt

@@ -0,0 +1,54 @@
+package com.tencent.liteav.demo.superplayer.database.entity
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import kotlin.Int
+import kotlin.Long
+import kotlin.String
+import kotlin.TODO
+
+@Entity
+data class History(
+    @PrimaryKey(autoGenerate = true)
+    var id: Long? = null,
+
+    @ColumnInfo(name = "start")
+    val start: Long,
+
+    @ColumnInfo(name = "end")
+    val end: Long,
+
+    @ColumnInfo(name = "course_id")
+    val courseId: String,
+
+    @ColumnInfo(name = "section_id")
+    val sectionId: String,
+
+    @ColumnInfo(name = "date")
+    val date: String,
+
+    @ColumnInfo(name = "ct_id")
+    val countDown: String? = null,
+) : Comparator<History>, Comparable<History> {
+    override fun compare(o1: History, o2: History): Int {
+        if (o1.start < o2.start) return -1
+        if (o1.start > o2.start) return 1
+
+        if (o1.end < o2.end) return 1;
+        if (o1.end > o2.end) return -1
+
+        return 0;
+    }
+
+    override fun compareTo(other: History): Int {
+        if (this.start < other.start) return -1
+        if (this.start > other.start) return 1
+
+        if (this.end < other.end) return 1;
+        if (this.end > other.end) return -1
+
+        return 0;
+    }
+
+}

+ 23 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/entity/Record.kt

@@ -0,0 +1,23 @@
+package com.tencent.liteav.demo.superplayer.database.entity
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+data class Record(
+    @PrimaryKey
+    var aiCourseItemId: String,
+
+    @ColumnInfo(name = "intervals")
+    val intervals: String,
+
+    @ColumnInfo(name = "lastTime")
+    val lastTime: Long,
+
+    @ColumnInfo(name = "validDuration")
+    val validDuration: Long,
+
+    @ColumnInfo(name = "courseId")
+    val courseId: String,
+)

+ 94 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/database/repo/PlayerRepository.kt

@@ -0,0 +1,94 @@
+package com.tencent.liteav.demo.superplayer.database.repo
+
+import android.util.Log
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import com.tencent.liteav.demo.superplayer.database.dao.CountDownDao
+import com.tencent.liteav.demo.superplayer.database.dao.HistoryDao
+import com.tencent.liteav.demo.superplayer.database.dao.RecordDao
+import com.tencent.liteav.demo.superplayer.database.entity.CountDown
+import com.tencent.liteav.demo.superplayer.database.entity.History
+import com.tencent.liteav.demo.superplayer.database.entity.Record
+import kotlinx.coroutines.flow.Flow
+import java.text.SimpleDateFormat
+import java.util.*
+
+public class PlayerRepository(
+    private val historyDao: HistoryDao,
+    private val countDownDao: CountDownDao,
+    private val recordDao: RecordDao,
+) {
+    companion object {
+        val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.SIMPLIFIED_CHINESE)
+    }
+
+    /**
+     * 定时相关的数据库操作
+     */
+    fun insertCountDown(countDown: CountDown) {
+        countDownDao.insertCountDown(countDown)
+    }
+
+    fun deleteCountDown(courseId: String) {
+        countDownDao.deleteCountDown(courseId)
+    }
+
+    fun findHasCountDown(courseId: String): CountDown? {
+        return countDownDao.queryCountDown(courseId)
+    }
+
+
+    /**
+     * 历史记录有关的操作
+     */
+    fun insertHistory(history: History) {
+        historyDao.insertHistory(history)
+    }
+
+    fun queryTodayEpisodeHistory(
+        courseId: String,
+        countDown: String,
+    ): Int {
+        val date = dateFormat.format(Date())
+        return historyDao.queryTodayEpisodeHistory(courseId, countDown, date)
+    }
+
+    fun queryTodayDurationHistory(
+        courseId: String,
+        countDown: String,
+    ): List<History> {
+        val date = dateFormat.format(Date())
+        return historyDao.queryTodayDurationHistory(courseId, countDown, date)
+    }
+
+    // 先查询保存了哪几个小节的记录
+    fun queryAllHistory(courseId: String): List<String> {
+        return historyDao.queryAllHistory(courseId)
+    }
+
+    // 再依次查询每小节对应的记录
+    fun queryAllSectionHistory(courseId: String, sectionId: String): List<History> {
+        return historyDao.queryAllSectionHistory(courseId, sectionId)
+    }
+
+    fun deleteAllHistory(courseId: String) {
+        historyDao.deleteAllHistory(courseId)
+    }
+
+    fun queryRecord(courseId: String): List<Record> {
+        return recordDao.queryRecord(courseId)
+    }
+
+    fun queryRecord(courseId: String, sectionId: String): Record? {
+        return recordDao.queryRecord(courseId, sectionId)
+    }
+
+    fun insertRecord(record: Record) {
+        recordDao.insertRecord(record)
+    }
+
+    fun deleteRecord(courseId: String) {
+        recordDao.deleteRecord(courseId)
+    }
+}

+ 48 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/player/CastObject.kt

@@ -0,0 +1,48 @@
+package com.tencent.liteav.demo.superplayer.player
+
+import com.android.cast.dlna.core.ICast
+import com.android.cast.dlna.core.ICast.ICastImage
+import com.android.cast.dlna.core.ICast.ICastVideo
+
+object CastObject {
+    fun newInstance(url: String, id: String, name: String): ICast {
+        return if (url.endsWith(".mp4") || url.endsWith(".m3u8")) {
+            CastVideo(url, id, name)
+        } else if (url.endsWith(".jpg")) {
+            CastImage(url, id, name)
+        } else {
+            throw IllegalArgumentException("please check cast object type.")
+        }
+    }
+}
+
+data class CastImage(
+    private val url: String,
+    private val id: String,
+    private val name: String,
+    private val size: Long = 0L
+) : ICastImage {
+
+    override fun getId(): String = id
+    override fun getUri(): String = url
+    override fun getName(): String = name
+    override fun getSize(): Long = size
+}
+
+data class CastVideo(
+    private val url: String,
+    private val id: String,
+    private val name: String,
+    private val size: Long = 0L,
+    private val bitrate: Long = 0
+) : ICastVideo {
+
+    var duration: Long = 0
+
+    override fun getId(): String = id
+    override fun getUri(): String = url
+    override fun getName(): String = name
+    override fun getDurationMillSeconds(): Long = duration
+    override fun getSize(): Long = size
+    override fun getBitrate(): Long = bitrate
+}

+ 30 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/util/CastorUtil.kt

@@ -0,0 +1,30 @@
+package com.tencent.liteav.demo.superplayer.util
+
+import org.fourthline.cling.model.meta.Device
+
+object CastorUtil {
+    interface Listener {
+        fun onDevice(device: Device<*, *, *>)
+    }
+
+    var castDevice: Device<*, *, *>? = null
+
+    private val listeners: MutableList<Listener> = mutableListOf()
+
+    public fun addListener(listener: Listener) {
+        listeners.add(listener)
+    }
+
+    public fun removeListener(listener: Listener) {
+        listeners.remove(listener)
+    }
+
+    public fun selectDevice(device: Device<*, *, *>?) {
+        castDevice = device
+        if(device != null) {
+            for (listener in listeners) {
+                listener.onDevice(device)
+            }
+        }
+    }
+}

+ 24 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/util/HistoryUtil.kt

@@ -0,0 +1,24 @@
+package com.tencent.liteav.demo.superplayer.util
+
+object HistoryUtil {
+    interface Listener {
+        fun onHistory(start: Long, end: Long, sectionId: String)
+    }
+
+    private val listeners: MutableList<Listener> = mutableListOf()
+
+    public fun addListener(listener: Listener) {
+        listeners.add(listener)
+    }
+
+    public fun removeListener(listener: Listener) {
+        listeners.remove(listener)
+    }
+
+    public fun sendHistory(start: Long, end: Long, sectionId: String) {
+        if (end <= start) return
+        for (listener in listeners) {
+            listener.onHistory(start, end, sectionId)
+        }
+    }
+}

+ 18 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/util/HttpUtils.kt

@@ -0,0 +1,18 @@
+package com.tencent.liteav.demo.superplayer.util
+
+import android.content.Context
+import java.util.*
+
+object HttpUtils {
+    private var application: Context? = null
+
+    fun init(context: Context) {
+        application = context.applicationContext
+    }
+
+    fun getApplication(): Context {
+        Objects.requireNonNull(application, "使先用HttpUtils.init(context)初始化")
+
+        return application!!
+    }
+}

+ 64 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/util/PlayerUtil.kt

@@ -0,0 +1,64 @@
+package com.tencent.liteav.demo.superplayer.util
+
+object PlayerUtil {
+    @JvmStatic
+    val STATE_PLAY = 0
+
+    @JvmStatic
+    val STATE_RESUME = 1
+
+    @JvmStatic
+    val STATE_PAUSE = 2
+
+    @JvmStatic
+    val STATE_STOP = 3
+
+    @JvmStatic
+    val STATE_LOADING = 4
+
+    interface Listener {
+        fun onStatePlay()
+
+        fun onStateResume()
+
+        fun onStatePause()
+
+        fun onStateStop()
+
+        fun onStateLoading()
+    }
+
+    private val listeners: MutableList<Listener> = mutableListOf()
+
+    public fun addListener(listener: Listener) {
+        listeners.add(listener)
+    }
+
+    public fun removeListener(listener: Listener) {
+        listeners.remove(listener)
+    }
+
+    public fun sendNewTimer(state: Int) {
+        when (state) {
+            STATE_PLAY -> for (listener in listeners) {
+                listener.onStatePlay()
+            }
+            STATE_RESUME -> for (listener in listeners) {
+                listener.onStateResume()
+            }
+            STATE_PAUSE -> for (listener in listeners) {
+                listener.onStatePause()
+            }
+            STATE_STOP -> for (listener in listeners) {
+                listener.onStateStop()
+            }
+            STATE_LOADING -> for (listener in listeners) {
+                listener.onStateLoading()
+            }
+            else -> {
+
+            }
+        }
+
+    }
+}

+ 23 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/util/TimeoutUtil.kt

@@ -0,0 +1,23 @@
+package com.tencent.liteav.demo.superplayer.util
+
+object TimeoutUtil {
+    interface Listener {
+        fun onTimeout(timeout: Boolean)
+    }
+
+    private val listeners: MutableList<Listener> = mutableListOf()
+
+    fun addListener(listener: Listener) {
+        listeners.add(listener)
+    }
+
+    fun removeListener(listener: Listener) {
+        listeners.remove(listener)
+    }
+
+    fun sendTimeout(timeout: Boolean) {
+        for (listener in listeners) {
+            listener.onTimeout(timeout)
+        }
+    }
+}

+ 31 - 0
kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/util/TimersUtil.kt

@@ -0,0 +1,31 @@
+package com.tencent.liteav.demo.superplayer.util
+
+object TimersUtil {
+    interface Listener {
+        fun onNewTimer(type: Int, value: Int)
+
+        fun onResetTimer()
+    }
+
+    private val listeners: MutableList<Listener> = mutableListOf()
+
+    fun addListener(listener: Listener) {
+        listeners.add(listener)
+    }
+
+    fun removeListener(listener: Listener) {
+        listeners.remove(listener)
+    }
+
+    fun sendNewTimer(type: Int, value: Int) {
+        for (listener in listeners) {
+            listener.onNewTimer(type, value)
+        }
+    }
+
+    fun resetTimer(){
+        for (listener in listeners) {
+            listener.onResetTimer()
+        }
+    }
+}

BIN
kit/src/main/res/drawable-xxhdpi/superplayer_ic_light_max.png


BIN
kit/src/main/res/drawable-xxhdpi/superplayer_ic_light_min.png


BIN
kit/src/main/res/drawable-xxhdpi/superplayer_ic_vod_play_normal.png


BIN
kit/src/main/res/drawable-xxhdpi/superplayer_ic_volume_max.png


BIN
kit/src/main/res/drawable-xxhdpi/superplayer_ic_volume_min.png


+ 13 - 0
kit/src/main/res/drawable/casting_cancel.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:shape="rectangle">
+    <size
+        android:width="140dp"
+        android:height="44dp" />
+    <stroke
+        android:width="2dp"
+        android:color="#ffffffff" />
+    <solid android:color="#ff1d71ec" />
+    <corners android:radius="22dp" />
+</shape>

+ 11 - 0
kit/src/main/res/drawable/free_label.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <!-- 圆角弧度 10 -->
+    <corners
+        android:bottomLeftRadius="0dp"
+        android:bottomRightRadius="12dp"
+        android:topLeftRadius="14dp"
+        android:topRightRadius="0dp" />
+    <solid android:color="#FF0B59CC" />
+</shape>

+ 11 - 0
kit/src/main/res/drawable/item_vod.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <!-- 圆角弧度 10 -->
+    <corners
+        android:bottomLeftRadius="14dp"
+        android:bottomRightRadius="14dp"
+        android:topLeftRadius="16dp"
+        android:topRightRadius="16dp" />
+    <solid android:color="@android:color/white" />
+</shape>

+ 11 - 0
kit/src/main/res/drawable/item_vod_select.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <!-- 圆角弧度 10 -->
+    <corners
+        android:bottomLeftRadius="14dp"
+        android:bottomRightRadius="14dp"
+        android:topLeftRadius="16dp"
+        android:topRightRadius="16dp" />
+    <solid android:color="#FFFF9C4D" />
+</shape>

+ 44 - 0
kit/src/main/res/drawable/list_progress_bar.xml

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:id="@android:id/background">
+        <shape>
+            <corners android:radius="5dip" />
+            <gradient
+                android:angle="270"
+                android:centerColor="#fff"
+                android:centerY="0.75"
+                android:endColor="#fff"
+                android:startColor="#fff" />
+        </shape>
+    </item>
+
+    <item android:id="@android:id/secondaryProgress">
+        <clip>
+            <shape>
+                <corners android:radius="5dip" />
+                <gradient
+                    android:angle="270"
+                    android:centerColor="#0accac"
+                    android:centerY="0.75"
+                    android:endColor="#0accac"
+                    android:startColor="#0accac" />
+            </shape>
+        </clip>
+    </item>
+
+    <item android:id="@android:id/progress">
+        <clip>
+            <shape>
+                <corners android:radius="5dip" />
+                <gradient
+                    android:angle="270"
+                    android:centerColor="#0accac"
+                    android:centerY="0.75"
+                    android:endColor="#0accac"
+                    android:startColor="#0accac" />
+            </shape>
+        </clip>
+    </item>
+
+</layer-list>

+ 5 - 0
kit/src/main/res/drawable/player_full_bg.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@android:color/black" />
+</shape>

+ 7 - 0
kit/src/main/res/drawable/player_window_bg.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <!-- 圆角弧度 10 -->
+    <corners android:radius="@dimen/superplayer_vod_player_window_radius" />
+    <solid android:color="@android:color/black" />
+</shape>

+ 13 - 0
kit/src/main/res/drawable/shadow_bottom.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <corners
+        android:bottomLeftRadius="@dimen/superplayer_vod_player_window_radius"
+        android:bottomRightRadius="@dimen/superplayer_vod_player_window_radius" />
+
+    <gradient
+        android:angle="270"
+        android:endColor="#80000000"
+        android:startColor="#00050505"
+        android:type="linear" />
+</shape>

+ 11 - 0
kit/src/main/res/drawable/timeout_back_chapter_deco.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="#ff0b57c7" />
+    <corners android:radius="20dp" />
+    <padding
+        android:bottom="10dp"
+        android:left="18dp"
+        android:right="18dp"
+        android:top="10dp" />
+</shape>

+ 10 - 0
kit/src/main/res/drawable/timeout_resume_deco.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="#33000000" />
+    <corners
+        android:bottomLeftRadius="100dp"
+        android:bottomRightRadius="0dp"
+        android:topLeftRadius="100dp"
+        android:topRightRadius="0dp" />
+
+</shape>

+ 6 - 0
kit/src/main/res/drawable/unlock_bg.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="#7f000000" />
+    <corners android:radius="20dp" />
+</shape>

+ 20 - 0
kit/src/main/res/drawable/vod_video_progress.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@android:id/background">
+        <shape>
+            <gradient
+                android:endColor="@color/superplayer_biz_audio_progress_second"
+                android:startColor="@color/superplayer_biz_audio_progress_second" />
+        </shape>
+    </item>
+
+    <item android:id="@android:id/progress">
+        <clip>
+            <shape>
+                <gradient
+                    android:endColor="@android:color/white"
+                    android:startColor="@android:color/white" />
+            </shape>
+        </clip>
+    </item>
+</layer-list>

+ 79 - 0
kit/src/main/res/layout/superplayer_item_vod.xml

@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_marginBottom="6dp"
+    android:background="@drawable/item_vod"
+    android:orientation="vertical">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="143dp"
+        android:layout_height="80dp"
+        android:layout_marginLeft="9dp"
+        android:layout_marginTop="9dp"
+        android:layout_marginRight="9dp"
+        android:layout_marginBottom="3dp"
+        android:background="@drawable/item_vod">
+
+        <com.tencent.liteav.demo.superplayer.ui.view.RoundImageView
+            android:id="@+id/superplayer_iv"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:scaleType="centerCrop"
+            app:radius="14dp" />
+
+        <ImageView
+            android:id="@+id/superplayer_lock"
+            android:layout_width="20dp"
+            android:layout_height="20dp"
+            android:src="@mipmap/video_lock"
+            android:visibility="gone"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintLeft_toLeftOf="parent"
+            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <ImageView
+            android:id="@+id/superplayer_pause_or_resume"
+            android:layout_width="20dp"
+            android:layout_height="20dp"
+            android:src="@mipmap/icon_resume"
+            android:visibility="gone"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintLeft_toLeftOf="parent"
+            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <TextView
+            android:id="@+id/superplayer_free"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:background="@drawable/free_label"
+            android:paddingLeft="6dp"
+            android:paddingRight="6dp"
+            android:paddingBottom="1dp"
+            android:text="试看"
+            android:textColor="#ffffffff"
+            android:textSize="13sp"
+            android:textStyle="bold|italic"
+            android:visibility="gone"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintRight_toRightOf="parent" />
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+
+    <TextView
+        android:id="@+id/superplayer_tv"
+        android:layout_width="143dp"
+        android:layout_height="wrap_content"
+        android:layout_alignBottom="@+id/superplayer_iv"
+        android:layout_marginLeft="9dp"
+        android:layout_marginRight="9dp"
+        android:layout_marginBottom="6dp"
+        android:height="18dp"
+        android:gravity="left"
+        android:text="@string/superplayer_test_video"
+        android:textColor="@color/superplayer_title"
+        android:textSize="13sp" />
+</LinearLayout>

+ 35 - 0
kit/src/main/res/layout/superplayer_video_progress_layout.xml

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/superplayer_ll_progress_head"
+    android:layout_width="160dp"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center_horizontal"
+    android:gravity="center"
+    android:orientation="vertical">
+
+    <ImageView
+        android:id="@+id/superplayer_iv_progress_thumbnail"
+        android:layout_width="160dp"
+        android:layout_height="90dp"
+        android:visibility="gone" />
+
+    <TextView
+        android:id="@+id/superplayer_tv_progress_time"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text=""
+        android:textColor="@android:color/white"
+        android:textSize="16sp" />
+
+    <ProgressBar
+        android:id="@+id/superplayer_pb_progress_bar"
+        style="@style/SuperPlayerAppProgressBarStyle"
+        android:layout_width="100dp"
+        android:layout_height="2dp"
+        android:layout_below="@id/superplayer_ll_progress_head"
+        android:layout_marginTop="8dp"
+        android:max="100"
+        android:progress="0"
+        android:progressDrawable="@drawable/vod_video_progress" />
+
+</LinearLayout>

+ 28 - 0
kit/src/main/res/layout/superplayer_video_volume_brightness_progress_layout.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/superplayer_ll_progress_head"
+    android:layout_width="100dp"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center_horizontal"
+    android:gravity="center"
+    android:orientation="vertical">
+
+    <ImageView
+        android:id="@+id/superplayer_iv_center"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerHorizontal="true" />
+
+    <ProgressBar
+        android:id="@+id/superplayer_pb_progress_bar"
+        style="@style/SuperPlayerAppProgressBarStyle"
+        android:layout_width="100dp"
+        android:layout_height="2dp"
+        android:layout_below="@id/superplayer_ll_progress_head"
+        android:layout_marginTop="8dp"
+        android:max="100"
+        android:progress="0"
+        android:progressDrawable="@drawable/vod_video_progress" />
+</LinearLayout>
+
+

+ 127 - 0
kit/src/main/res/layout/superplayer_vod_player_casting.xml

@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#FF5A7090">
+
+    <TextView
+        android:id="@+id/stop_cast"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_above="@id/superplayer_ll_bottom"
+        android:layout_centerInParent="true"
+        android:layout_marginBottom="16dp"
+        android:background="@drawable/casting_cancel"
+        android:gravity="center"
+        android:text="退出连接"
+        android:textColor="#ffffffff"
+        android:textSize="15sp" />
+
+    <LinearLayout
+        android:id="@+id/superplayer_ll_bottom"
+        android:layout_width="match_parent"
+        android:layout_height="64dp"
+        android:layout_alignParentBottom="true"
+        android:background="@drawable/shadow_bottom"
+        android:clipChildren="false"
+        android:orientation="horizontal">
+
+        <!--播放/暂停-->
+        <ImageView
+            android:id="@+id/superplayer_iv_pause"
+            android:layout_width="38dp"
+            android:layout_height="39dp"
+            android:layout_gravity="center_vertical"
+            android:layout_marginLeft="23dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="15dp"
+            android:src="@mipmap/play_state" />
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_marginLeft="19dp"
+            android:layout_marginRight="19dp"
+            android:layout_weight="1"
+            android:clipChildren="false">
+
+            <com.tencent.liteav.demo.superplayer.ui.view.PointProgressBar
+                android:id="@+id/superplayer_bar_progress"
+                android:layout_width="match_parent"
+                android:layout_height="11dp"
+                android:layout_marginTop="23dp"
+                android:layout_marginBottom="30dp"
+                app:backgroundColor="#FFFFFFFF"
+                app:layout_constraintLeft_toLeftOf="parent"
+                app:layout_constraintRight_toRightOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:maxValue="100"
+                app:progress="0"
+                app:progressColor="#FFFF8642"
+                app:progressHeight="11dp"
+                app:trackDrawable="@mipmap/dog_track"
+                app:trackHeight="39dp"
+                app:trackWidth="38dp" />
+
+            <!--播放位置-->
+            <TextView
+                android:id="@+id/superplayer_tv_current"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="4dp"
+                android:text="00:00"
+                android:textColor="@android:color/white"
+                android:textSize="11.0sp"
+                app:layout_constraintRight_toLeftOf="@id/superplayer_tv_separator"
+                app:layout_constraintTop_toBottomOf="@id/superplayer_bar_progress" />
+
+            <TextView
+                android:id="@+id/superplayer_tv_separator"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="4dp"
+                android:text="/"
+                android:textColor="@android:color/white"
+                android:textSize="11.0sp"
+                app:layout_constraintRight_toLeftOf="@id/superplayer_tv_duration"
+                app:layout_constraintTop_toBottomOf="@id/superplayer_bar_progress" />
+
+
+            <!--总时长-->
+            <TextView
+                android:id="@+id/superplayer_tv_duration"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="4dp"
+                android:text="00:00"
+                android:textColor="@android:color/white"
+                android:textSize="11.0sp"
+                app:layout_constraintRight_toRightOf="@id/superplayer_bar_progress"
+                app:layout_constraintTop_toBottomOf="@id/superplayer_bar_progress" />
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+
+        <ImageView
+            android:id="@+id/superplayer_iv_play_next"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerInParent="true"
+            android:layout_gravity="center_vertical"
+            android:layout_marginTop="10dp"
+            android:layout_marginRight="12dp"
+            android:layout_marginBottom="15dp"
+            android:src="@mipmap/play_next"
+            android:visibility="visible" />
+
+        <ImageView
+            android:id="@+id/superplayer_iv_fullscreen"
+            android:layout_width="38dp"
+            android:layout_height="39dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginRight="23dp"
+            android:layout_marginBottom="15dp"
+            android:src="@mipmap/full_screen" />
+    </LinearLayout>
+
+</RelativeLayout>

+ 46 - 0
kit/src/main/res/layout/superplayer_vod_player_fullscreen.xml

@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.tencent.liteav.demo.superplayer.ui.view.UnlockProgressView
+        android:id="@+id/superplayer_iv_lock"
+        android:layout_width="50dp"
+        android:layout_height="50dp"
+        android:layout_alignParentLeft="true"
+        android:layout_centerVertical="true"
+        android:layout_marginLeft="30dp"
+        android:src="@mipmap/full_unlock" />
+
+    <TextView
+        android:id="@+id/superplayer_iv_lock_text"
+        android:layout_width="wrap_content"
+        android:layout_height="40dp"
+        android:layout_centerInParent="true"
+        android:layout_marginLeft="11dp"
+        android:layout_toRightOf="@id/superplayer_iv_lock"
+        android:background="@drawable/unlock_bg"
+        android:paddingLeft="20dp"
+        android:paddingTop="10dp"
+        android:paddingRight="20dp"
+        android:paddingBottom="9dp"
+        android:text="长按此处解锁"
+        android:textColor="#ffffffff"
+        android:textSize="15sp" />
+
+    <ImageView
+        android:id="@+id/superplayer_cover_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scaleType="centerCrop"
+        android:src="@mipmap/default_cover_dark"
+        android:visibility="gone" />
+
+    <com.tencent.liteav.demo.superplayer.ui.player.TimeOutPlayer
+        android:id="@+id/superplayer_timeout_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="gone" />
+
+</RelativeLayout>

+ 78 - 0
kit/src/main/res/layout/superplayer_vod_player_timeout.xml

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#adebd6">
+
+    <ImageView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scaleType="centerCrop"
+        android:src="@mipmap/timeout_bg" />
+
+    <ImageView
+        android:id="@+id/timeout_back"
+        android:layout_width="38dp"
+        android:layout_height="39dp"
+        android:layout_marginLeft="38dp"
+        android:layout_marginTop="20dp"
+        android:src="@mipmap/lock_back"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.appcompat.widget.LinearLayoutCompat
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <ImageView
+            android:layout_width="232dp"
+            android:layout_height="232dp"
+            android:layout_gravity="center_horizontal"
+            android:src="@mipmap/timeout_logo" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="25dp"
+            android:layout_gravity="center_horizontal"
+            android:layout_marginTop="22dp"
+            android:text="小朋友,定时观看时间到了,让眼睛休息一下吧"
+            android:textColor="#FFFFFF"
+            android:textStyle="bold"
+            android:textSize="20sp" />
+
+    </androidx.appcompat.widget.LinearLayoutCompat>
+
+    <androidx.appcompat.widget.LinearLayoutCompat
+        android:id="@+id/timeout_resume"
+        android:layout_width="wrap_content"
+        android:layout_height="46dp"
+        android:layout_marginTop="15dp"
+        android:background="@drawable/timeout_resume_deco"
+        android:paddingLeft="20dp"
+        android:paddingRight="24dp"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <ImageView
+            android:layout_width="16dp"
+            android:layout_height="16dp"
+            android:layout_gravity="center_vertical"
+            android:layout_marginRight="8dp"
+            android:src="@mipmap/timeout_resume" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="21dp"
+            android:layout_gravity="center_vertical"
+            android:text="继续播放"
+            android:textColor="#ffffffff"
+            android:textSize="15sp" />
+    </androidx.appcompat.widget.LinearLayoutCompat>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 78 - 0
kit/src/main/res/layout/superplayer_vod_player_timeout_m.xml

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#adebd6">
+
+    <ImageView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scaleType="centerCrop"
+        android:src="@mipmap/timeout_bg" />
+
+    <ImageView
+        android:id="@+id/timeout_back"
+        android:layout_width="38dp"
+        android:layout_height="39dp"
+        android:layout_marginLeft="38dp"
+        android:layout_marginTop="20dp"
+        android:src="@mipmap/lock_back"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.appcompat.widget.LinearLayoutCompat
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <ImageView
+            android:layout_width="168dp"
+            android:layout_height="168dp"
+            android:layout_gravity="center_horizontal"
+            android:src="@mipmap/timeout_logo" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="25dp"
+            android:layout_gravity="center_horizontal"
+            android:layout_marginTop="22dp"
+            android:text="小朋友,定时观看时间到了,让眼睛休息一下吧"
+            android:textColor="#FFFFFF"
+            android:textStyle="bold"
+            android:textSize="16sp" />
+
+    </androidx.appcompat.widget.LinearLayoutCompat>
+
+    <androidx.appcompat.widget.LinearLayoutCompat
+        android:id="@+id/timeout_resume"
+        android:layout_width="wrap_content"
+        android:layout_height="46dp"
+        android:layout_marginTop="15dp"
+        android:background="@drawable/timeout_resume_deco"
+        android:paddingLeft="20dp"
+        android:paddingRight="24dp"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <ImageView
+            android:layout_width="16dp"
+            android:layout_height="16dp"
+            android:layout_gravity="center_vertical"
+            android:layout_marginRight="8dp"
+            android:src="@mipmap/timeout_resume" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="21dp"
+            android:layout_gravity="center_vertical"
+            android:text="继续播放"
+            android:textColor="#ffffffff"
+            android:textSize="15sp" />
+    </androidx.appcompat.widget.LinearLayoutCompat>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 141 - 0
kit/src/main/res/layout/superplayer_vod_player_window.xml

@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.tencent.liteav.demo.superplayer.ui.view.VolumeBrightnessProgressLayout
+        android:id="@+id/superplayer_gesture_progress"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true" />
+
+    <com.tencent.liteav.demo.superplayer.ui.view.VideoProgressLayout
+        android:id="@+id/superplayer_video_progress_layout"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:gravity="center" />
+
+    <ImageView
+        android:id="@+id/superplayer_cover_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scaleType="centerCrop"
+        android:src="@mipmap/default_cover_dark"
+        android:visibility="visible" />
+
+    <LinearLayout
+        android:id="@+id/superplayer_ll_bottom"
+        android:layout_width="match_parent"
+        android:layout_height="64dp"
+        android:layout_alignParentBottom="true"
+        android:background="@drawable/shadow_bottom"
+        android:clipChildren="false"
+        android:orientation="horizontal">
+
+        <!--播放/暂停-->
+        <ImageView
+            android:id="@+id/superplayer_iv_pause"
+            android:layout_width="38dp"
+            android:layout_height="39dp"
+            android:layout_gravity="center_vertical"
+            android:layout_marginLeft="23dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginBottom="15dp"
+            android:src="@mipmap/play_state" />
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_marginLeft="19dp"
+            android:layout_marginRight="19dp"
+            android:layout_weight="1"
+            android:clipChildren="false">
+
+            <com.tencent.liteav.demo.superplayer.ui.view.PointProgressBar
+                android:id="@+id/superplayer_bar_progress"
+                android:layout_width="match_parent"
+                android:layout_height="11dp"
+                android:layout_marginTop="23dp"
+                android:layout_marginBottom="30dp"
+                app:backgroundColor="#FFFFFFFF"
+                app:layout_constraintLeft_toLeftOf="parent"
+                app:layout_constraintRight_toRightOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:maxValue="100"
+                app:progress="0"
+                app:progressColor="#FFFF8642"
+                app:progressHeight="11dp"
+                app:trackDrawable="@mipmap/dog_track"
+                app:trackHeight="39dp"
+                app:trackWidth="38dp" />
+
+            <!--播放位置-->
+            <TextView
+                android:id="@+id/superplayer_tv_current"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="4dp"
+                android:text="00:00"
+                android:textColor="@android:color/white"
+                android:textSize="11.0sp"
+                app:layout_constraintRight_toLeftOf="@id/superplayer_tv_separator"
+                app:layout_constraintTop_toBottomOf="@id/superplayer_bar_progress" />
+
+            <TextView
+                android:id="@+id/superplayer_tv_separator"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="4dp"
+                android:text="/"
+                android:textColor="@android:color/white"
+                android:textSize="11.0sp"
+                app:layout_constraintRight_toLeftOf="@id/superplayer_tv_duration"
+                app:layout_constraintTop_toBottomOf="@id/superplayer_bar_progress" />
+
+
+            <!--总时长-->
+            <TextView
+                android:id="@+id/superplayer_tv_duration"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="4dp"
+                android:text="00:00"
+                android:textColor="@android:color/white"
+                android:textSize="11.0sp"
+                app:layout_constraintRight_toRightOf="@id/superplayer_bar_progress"
+                app:layout_constraintTop_toBottomOf="@id/superplayer_bar_progress" />
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+
+        <ImageView
+            android:id="@+id/superplayer_iv_play_next"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerInParent="true"
+            android:layout_gravity="center_vertical"
+            android:layout_marginTop="10dp"
+            android:layout_marginRight="12dp"
+            android:layout_marginBottom="15dp"
+            android:src="@mipmap/play_next"
+            android:visibility="visible" />
+
+        <ImageView
+            android:id="@+id/superplayer_iv_fullscreen"
+            android:layout_width="38dp"
+            android:layout_height="39dp"
+            android:layout_marginTop="10dp"
+            android:layout_marginRight="23dp"
+            android:layout_marginBottom="15dp"
+            android:src="@mipmap/full_screen" />
+    </LinearLayout>
+
+    <com.tencent.liteav.demo.superplayer.ui.player.TimeOutMPlayer
+        android:id="@+id/superplayer_timeout_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="gone"
+        app:showControl="false" />
+
+</RelativeLayout>

+ 27 - 0
kit/src/main/res/layout/superplayer_vod_view.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.tencent.rtmp.ui.TXCloudVideoView
+        android:id="@+id/superplayer_cloud_video_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_centerInParent="true" />
+
+    <com.tencent.liteav.demo.superplayer.ui.player.CastingPlayer
+        android:id="@+id/superplayer_controller_cast"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <com.tencent.liteav.demo.superplayer.ui.player.WindowPlayer
+        android:id="@+id/superplayer_controller_small"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <com.tencent.liteav.demo.superplayer.ui.player.FullScreenPlayer
+        android:id="@+id/superplayer_controller_large"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+</RelativeLayout>

BIN
kit/src/main/res/mipmap-hdpi/dog_track.png


BIN
kit/src/main/res/mipmap-hdpi/full_screen.png


BIN
kit/src/main/res/mipmap-hdpi/full_unlock.png


BIN
kit/src/main/res/mipmap-hdpi/icon_pause.png


BIN
kit/src/main/res/mipmap-hdpi/icon_resume.png


BIN
kit/src/main/res/mipmap-hdpi/lock_back.png


BIN
kit/src/main/res/mipmap-hdpi/pause_state.png


BIN
kit/src/main/res/mipmap-hdpi/play_next.png


BIN
kit/src/main/res/mipmap-hdpi/play_state.png


BIN
kit/src/main/res/mipmap-hdpi/timeout_bg.png


BIN
kit/src/main/res/mipmap-hdpi/timeout_logo.png


BIN
kit/src/main/res/mipmap-hdpi/timeout_resume.png


BIN
kit/src/main/res/mipmap-hdpi/video_lock.png


BIN
kit/src/main/res/mipmap-hdpi/window_back.png


BIN
kit/src/main/res/mipmap-mdpi/dog_track.png


BIN
kit/src/main/res/mipmap-mdpi/full_screen.png


BIN
kit/src/main/res/mipmap-mdpi/full_unlock.png


BIN
kit/src/main/res/mipmap-mdpi/icon_pause.png


BIN
kit/src/main/res/mipmap-mdpi/icon_resume.png


BIN
kit/src/main/res/mipmap-mdpi/lock_back.png


BIN
kit/src/main/res/mipmap-mdpi/pause_state.png


BIN
kit/src/main/res/mipmap-mdpi/play_next.png


BIN
kit/src/main/res/mipmap-mdpi/play_state.png


BIN
kit/src/main/res/mipmap-mdpi/timeout_bg.png


BIN
kit/src/main/res/mipmap-mdpi/timeout_logo.png


BIN
kit/src/main/res/mipmap-mdpi/timeout_resume.png


BIN
kit/src/main/res/mipmap-mdpi/video_lock.png


BIN
kit/src/main/res/mipmap-mdpi/window_back.png


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels