zhaoyadi %!s(int64=2) %!d(string=hai) anos
pai
achega
3bb8c3188b
Modificáronse 58 ficheiros con 1208 adicións e 186 borrados
  1. 4 0
      build.gradle
  2. 4 1
      kit/build.gradle
  3. 5 0
      kit/src/main/AndroidManifest.xml
  4. 1 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/SuperPlayerDef.java
  5. 93 104
      kit/src/main/java/com/tencent/liteav/demo/superplayer/SuperPlayerView.java
  6. 12 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/player/SuperPlayer.java
  7. 234 23
      kit/src/main/java/com/tencent/liteav/demo/superplayer/player/SuperPlayerImpl.java
  8. 0 3
      kit/src/main/java/com/tencent/liteav/demo/superplayer/player/SuperPlayerObserver.java
  9. 179 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/CastingPlayer.java
  10. 0 4
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/FullScreenPlayer.java
  11. 5 0
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/Player.java
  12. 2 25
      kit/src/main/java/com/tencent/liteav/demo/superplayer/ui/player/WindowPlayer.java
  13. 48 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/player/CastObject.kt
  14. 25 0
      kit/src/main/kotlin/com/tencent/liteav/demo/superplayer/util/CastorUtil.kt
  15. 13 0
      kit/src/main/res/drawable/casting_cancel.xml
  16. 127 0
      kit/src/main/res/layout/superplayer_vod_player_casting.xml
  17. 0 7
      kit/src/main/res/layout/superplayer_vod_player_window.xml
  18. 5 0
      kit/src/main/res/layout/superplayer_vod_view.xml
  19. 7 1
      ui/build.gradle
  20. 9 1
      ui/src/main/AndroidManifest.xml
  21. 57 0
      ui/src/main/java/com/tencent/liteav/demo/player/ui/CastScanning.java
  22. 104 0
      ui/src/main/java/com/tencent/liteav/demo/player/ui/PlayerCastAdapter.kt
  23. 37 17
      ui/src/main/kotlin/com/tencent/liteav/demo/player/PlayerActivity.kt
  24. 73 0
      ui/src/main/kotlin/com/tencent/liteav/demo/player/PlayerCast.java
  25. 3 0
      ui/src/main/kotlin/com/tencent/liteav/demo/player/PlayerMenu.kt
  26. 23 0
      ui/src/main/kotlin/com/tencent/liteav/demo/player/viewmodel/UploadWorker.kt
  27. 6 0
      ui/src/main/res/anim/rotate_loading.xml
  28. 6 0
      ui/src/main/res/drawable/cast_cancel_background.xml
  29. 6 0
      ui/src/main/res/drawable/dialog_center_background.xml
  30. 4 0
      ui/src/main/res/drawable/icon_close.xml
  31. 67 0
      ui/src/main/res/layout/activity_cast.xml
  32. 20 0
      ui/src/main/res/layout/cast_scanning.xml
  33. 29 0
      ui/src/main/res/layout/item_cast.xml
  34. BIN=BIN
      ui/src/main/res/mipmap-hdpi/cast_help.png
  35. BIN=BIN
      ui/src/main/res/mipmap-hdpi/cast_loading.png
  36. BIN=BIN
      ui/src/main/res/mipmap-hdpi/icon_cast.png
  37. BIN=BIN
      ui/src/main/res/mipmap-hdpi/icon_check.png
  38. BIN=BIN
      ui/src/main/res/mipmap-hdpi/icon_uncheck.png
  39. BIN=BIN
      ui/src/main/res/mipmap-mdpi/cast_help.png
  40. BIN=BIN
      ui/src/main/res/mipmap-mdpi/cast_loading.png
  41. BIN=BIN
      ui/src/main/res/mipmap-mdpi/icon_cast.png
  42. BIN=BIN
      ui/src/main/res/mipmap-mdpi/icon_check.png
  43. BIN=BIN
      ui/src/main/res/mipmap-mdpi/icon_uncheck.png
  44. BIN=BIN
      ui/src/main/res/mipmap-xhdpi/cast_help.png
  45. BIN=BIN
      ui/src/main/res/mipmap-xhdpi/cast_loading.png
  46. BIN=BIN
      ui/src/main/res/mipmap-xhdpi/icon_cast.png
  47. BIN=BIN
      ui/src/main/res/mipmap-xhdpi/icon_check.png
  48. BIN=BIN
      ui/src/main/res/mipmap-xhdpi/icon_uncheck.png
  49. BIN=BIN
      ui/src/main/res/mipmap-xxhdpi/cast_help.png
  50. BIN=BIN
      ui/src/main/res/mipmap-xxhdpi/cast_loading.png
  51. BIN=BIN
      ui/src/main/res/mipmap-xxhdpi/icon_cast.png
  52. BIN=BIN
      ui/src/main/res/mipmap-xxhdpi/icon_check.png
  53. BIN=BIN
      ui/src/main/res/mipmap-xxhdpi/icon_uncheck.png
  54. BIN=BIN
      ui/src/main/res/mipmap-xxxhdpi/cast_help.png
  55. BIN=BIN
      ui/src/main/res/mipmap-xxxhdpi/cast_loading.png
  56. BIN=BIN
      ui/src/main/res/mipmap-xxxhdpi/icon_cast.png
  57. BIN=BIN
      ui/src/main/res/mipmap-xxxhdpi/icon_check.png
  58. BIN=BIN
      ui/src/main/res/mipmap-xxxhdpi/icon_uncheck.png

+ 4 - 0
build.gradle

@@ -7,6 +7,8 @@ buildscript {
         jcenter { "https://maven.aliyun.com/repository/public" }
         google()
         mavenCentral()
+        maven { url 'http://4thline.org/m2' }
+        maven { url 'https://jitpack.io' }
     }
     dependencies {
         classpath 'com.android.tools.build:gradle:4.2.2'
@@ -22,6 +24,8 @@ allprojects {
         jcenter { "https://maven.aliyun.com/repository/public" }
         google()
         mavenCentral()
+        maven { url 'http://4thline.org/m2' }
+        maven { url 'https://jitpack.io' }
     }
 }
 

+ 4 - 1
kit/build.gradle

@@ -38,7 +38,9 @@ android {
             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
         }
     }
-
+    packagingOptions {
+        exclude 'META-INF/beans.xml'
+    }
     compileOptions {
         sourceCompatibility 1.8
         targetCompatibility 1.8
@@ -50,6 +52,7 @@ dependencies {
     implementation fileTree(dir: 'libs', include: ['*.jar'])
 
     api 'com.tencent.liteav:LiteAVSDK_Player:latest.release'
+    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'

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

@@ -3,5 +3,10 @@
     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>

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

@@ -5,6 +5,7 @@ public class SuperPlayerDef {
     public enum PlayerMode {
         WINDOW,     // 窗口模式
         FULLSCREEN, // 全屏模式
+        CASTING, // 投屏模式
     }
 
     public enum PlayerState {

+ 93 - 104
kit/src/main/java/com/tencent/liteav/demo/superplayer/SuperPlayerView.java

@@ -14,12 +14,15 @@ 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.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;
@@ -27,36 +30,32 @@ import java.util.List;
 public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Listener {
     private static final String TAG = "SuperPlayerView";
 
-    private Context mContext;
     private ViewGroup mRootView;                                 // SuperPlayerView的根view
 
     private TXCloudVideoView mTXCloudVideoView;                         // 腾讯云视频播放view
     private FullScreenPlayer mFullScreenPlayer;                         // 全屏模式控制view
-    private WindowPlayer mWindowPlayer;                             // 窗口模式控制view
+    private WindowPlayer mWindowPlayer;                                 // 窗口模式控制view
+    private CastingPlayer mCastingPlayer;                               // 投屏模式控制view
 
-    private ViewGroup.LayoutParams mLayoutParamWindowMode;          // 窗口播放时SuperPlayerView的布局参数
-    private ViewGroup.LayoutParams mLayoutParamFullScreenMode;      // 全屏播放时SuperPlayerView的布局参数
+    private ViewGroup.LayoutParams mLayoutParamWindowMode;              // 窗口播放时SuperPlayerView的布局参数
+    private ViewGroup.LayoutParams mLayoutParamFullScreenMode;          // 全屏播放时SuperPlayerView的布局参数
 
     private LayoutParams mVodControllerWindowParams;      // 窗口controller的布局参数
     private LayoutParams mVodControllerFullScreenParams;  // 全屏controller的布局参数
 
-    private OnSuperPlayerViewCallback mPlayerViewCallback;             // SuperPlayerView回调
+    private OnPlayerViewCallback mPlayerViewCallback;             // SuperPlayerView回调
 
     private SuperPlayer mSuperPlayer;                    // 超级播放器
 
-    private SuperPlayerModel mCurrentSuperPlayerModel;        // 当前正在播放的SuperPlayerModel
-
     private int mPlayIndex;                      // 正在播放model的索引
+    private SuperPlayerModel mCurrentSuperPlayerModel;        // 当前正在播放的SuperPlayerModel
     private List<SuperPlayerModel> mSuperPlayerModelList;           // SuperPlayerModel列表
 
     private boolean mIsPlayInit;
     private boolean mIsTimeOut;
 
     // 正常窗口模式下裁切的圆角值
-    private float topLeftRadius;
-    private float topRightRadius;
-    private float bottomLeftRadius;
-    private float bottomRightRadius;
+    private float topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius;
 
     public SuperPlayerView(Context context) {
         this(context, null);
@@ -68,9 +67,8 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
 
     public SuperPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        mContext = context;
-        initView();
-        initPlayer();
+        initView(context);
+        initPlayer(context);
         initAttrs(context, attrs);
         setWillNotDraw(false);
     }
@@ -102,12 +100,13 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
     /**
      * 初始化view
      */
-    private void initView() {
-        mRootView = (ViewGroup) LayoutInflater.from(mContext).inflate(R.layout.superplayer_vod_view, null);
+    private void initView(Context context) {
+        mRootView = (ViewGroup) LayoutInflater.from(context).inflate(R.layout.superplayer_vod_view, null);
 
-        mTXCloudVideoView = (TXCloudVideoView) mRootView.findViewById(R.id.superplayer_cloud_video_view);
-        mFullScreenPlayer = (FullScreenPlayer) mRootView.findViewById(R.id.superplayer_controller_large);
-        mWindowPlayer = (WindowPlayer) mRootView.findViewById(R.id.superplayer_controller_small);
+        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<>();
 
@@ -116,41 +115,47 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
 
         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() {
-        mSuperPlayer = new SuperPlayerImpl(mContext, 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(new Runnable() {
-            @Override
-            public void run() {
-                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();
-                }
+        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();
             }
         });
     }
@@ -248,6 +253,17 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
         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);
+    }
+
     @Override
     public void onTimeout(boolean timeout) {
         this.mIsTimeOut = timeout;
@@ -267,7 +283,7 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
      *
      * @param callback
      */
-    public void setPlayerViewCallback(OnSuperPlayerViewCallback callback) {
+    public void setPlayerViewCallback(OnPlayerViewCallback callback) {
         mPlayerViewCallback = callback;
     }
 
@@ -278,13 +294,20 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
     }
 
     private void handleSwitchPlayMode(SuperPlayerDef.PlayerMode playerMode) {
+        if (playerMode == getPlayerMode()) {
+            return;
+        }
+
         mFullScreenPlayer.hide();
         mWindowPlayer.hide();
-        //请求全屏模式
+        mCastingPlayer.hide();
+
         if (playerMode == SuperPlayerDef.PlayerMode.FULLSCREEN) {
             onSwitchFullMode(playerMode);
-        } else if (playerMode == SuperPlayerDef.PlayerMode.WINDOW) { // 请求窗口模式
+        } else if (playerMode == SuperPlayerDef.PlayerMode.WINDOW) {
             onSwitchWindowMode(playerMode);
+        } else if (playerMode == SuperPlayerDef.PlayerMode.CASTING) {
+            onSwitchCastingMode(playerMode);
         }
     }
 
@@ -294,6 +317,7 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
         }
 
         removeView(mWindowPlayer);
+        removeView(mCastingPlayer);
         addView(mFullScreenPlayer, mVodControllerFullScreenParams);
         setLayoutParams(mLayoutParamFullScreenMode);
         if (mPlayerViewCallback != null) {
@@ -309,6 +333,7 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
         }
 
         removeView(mFullScreenPlayer);
+        removeView(mCastingPlayer);
         addView(mWindowPlayer, mVodControllerWindowParams);
         setLayoutParams(mLayoutParamWindowMode);
         if (mPlayerViewCallback != null) {
@@ -318,6 +343,22 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
         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回调
      */
@@ -367,6 +408,11 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
             playNextVideo();
         }
 
+        @Override
+        public void stopCast() {
+            SuperPlayerView.this.stopCast();
+        }
+
         @Override
         public List<SuperPlayerModel> getPlayList() {
             if (null == mSuperPlayerModelList || mSuperPlayerModelList.isEmpty()) {
@@ -402,7 +448,7 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
     /**
      * SuperPlayerView的回调接口
      */
-    public interface OnSuperPlayerViewCallback {
+    public interface OnPlayerViewCallback {
 
         /**
          * 开始全屏播放
@@ -414,11 +460,6 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
          */
         void onStopFullScreenPlay();
 
-        /**
-         * 开始播放回调
-         */
-        void onPlaying();
-
         /**
          * 播放下一个视频------
          */
@@ -429,26 +470,10 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
          */
         void onPlayPrepare(int index, SuperPlayerModel model);
 
-        /**
-         * 播放结束
-         */
-        void onPlayEnd();
-
-        /**
-         * 当播放失败的时候回调
-         *
-         * @param code
-         */
-        void onError(int code);
-
         /**
          * 弹出验证框
          */
         void onVerify(SuperPlayerDef.VerifyReason reason);
-
-        void onResumePlay();
-
-        void onPausePlay();
     }
 
     public void release() {
@@ -458,6 +483,9 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
         if (mFullScreenPlayer != null) {
             mFullScreenPlayer.release();
         }
+        if (mCastingPlayer != null) {
+            mCastingPlayer.release();
+        }
     }
 
     @Override
@@ -479,11 +507,6 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
         return mSuperPlayer.getPlayerState();
     }
 
-    private void actonOfPreloadOnPlayPrepare() {
-        mWindowPlayer.prepareLoading();
-        mFullScreenPlayer.prepareLoading();
-    }
-
 
     class PlayerObserver extends SuperPlayerObserver {
 
@@ -491,7 +514,7 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
         public void onPlayPrepare() {
             mWindowPlayer.updatePlayState(SuperPlayerDef.PlayerState.INIT);
             mFullScreenPlayer.updatePlayState(SuperPlayerDef.PlayerState.INIT);
-            actonOfPreloadOnPlayPrepare();
+            mCastingPlayer.updatePlayState(SuperPlayerDef.PlayerState.INIT);
 
             mWindowPlayer.showBackground();
             mFullScreenPlayer.showBackground();
@@ -501,19 +524,17 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
         public void onPlayBegin(String name) {
             mWindowPlayer.updatePlayState(SuperPlayerDef.PlayerState.PLAYING);
             mFullScreenPlayer.updatePlayState(SuperPlayerDef.PlayerState.PLAYING);
-
-            mPlayerViewCallback.onResumePlay();
+            mCastingPlayer.updatePlayState(SuperPlayerDef.PlayerState.PLAYING);
 
             mWindowPlayer.hideBackground();
             mFullScreenPlayer.hideBackground();
-            notifyCallbackPlaying();
         }
 
         @Override
         public void onPlayPause() {
-            mPlayerViewCallback.onPausePlay();
             mWindowPlayer.updatePlayState(SuperPlayerDef.PlayerState.PAUSE);
             mFullScreenPlayer.updatePlayState(SuperPlayerDef.PlayerState.PAUSE);
+            mCastingPlayer.updatePlayState(SuperPlayerDef.PlayerState.PAUSE);
         }
 
         @Override
@@ -521,20 +542,20 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
             if (mIsPlayInit) {
                 playNextVideo();
             }
-
-            notifyCallbackPlayEnd();
         }
 
         @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
@@ -544,7 +565,6 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
 
         @Override
         public void onError(int code, String message) {
-            notifyCallbackPlayError(code);
         }
 
         @Override
@@ -555,33 +575,6 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
         }
     }
 
-    /**
-     * 通知播放开始,降低圈复杂度,单独提取成一个方法
-     */
-    private void notifyCallbackPlaying() {
-        if (mPlayerViewCallback != null) {
-            mPlayerViewCallback.onPlaying();
-        }
-    }
-
-    /**
-     * 通知播放结束,降低圈复杂度,单独提取成一个方法
-     */
-    private void notifyCallbackPlayEnd() {
-        if (mPlayerViewCallback != null) {
-            mPlayerViewCallback.onPlayEnd();
-        }
-    }
-
-    /**
-     * 通知播放错误,降低圈复杂度,单独提取成一个方法
-     */
-    private void notifyCallbackPlayError(int code) {
-        if (mPlayerViewCallback != null) {
-            mPlayerViewCallback.onError(code);
-        }
-    }
-
     public void setNeedToPause(boolean value) {
         mSuperPlayer.setNeedToPause(value);
     }
@@ -590,10 +583,6 @@ public class SuperPlayerView extends RelativeLayout implements TimeoutUtil.Liste
         mSuperPlayer.setStartTime((float) startTime);
     }
 
-    public void setLoop(boolean b) {
-        mSuperPlayer.setLoop(b);
-    }
-
     /*                                                  */
     /*----------------正常窗口播放时裁切圆角----------------*/
     @Override

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

@@ -4,6 +4,8 @@ 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 {
     /**
      * 开始播放
@@ -41,6 +43,16 @@ public interface SuperPlayer extends TimeoutUtil.Listener {
      */
     void seek(int position);
 
+    /**
+     * 投屏到指定设备
+     */
+    void startCast(Device device);
+
+    /**
+     * 结束投屏
+     */
+    void stopCast();
+
     /**
      * 切换播放器模式
      *

+ 234 - 23
kit/src/main/java/com/tencent/liteav/demo/superplayer/player/SuperPlayerImpl.java

@@ -2,9 +2,19 @@ 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;
@@ -17,20 +27,18 @@ 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 java.io.File;
+import java.util.UUID;
 
 public class SuperPlayerImpl implements SuperPlayer, ITXVodPlayListener {
 
     private static final String TAG = "SuperPlayerImpl";
-    private static final int SUPERPLAYER_MODE = 1;
-    private static final int SUPPORT_MAJOR_VERSION = 8;
-    private static final int SUPPORT_MINOR_VERSION = 5;
 
-    private Context mContext;
     private TXCloudVideoView mVideoView;        // 腾讯云视频播放view
-
     private TXVodPlayer mVodPlayer;       // 点播播放器
-    private TXVodPlayConfig mVodPlayConfig;   // 点播播放器配置
 
     private SuperPlayerModel mCurrentModel;  // 当前播放的model
     private SuperPlayerObserver mObserver;
@@ -45,17 +53,101 @@ public class SuperPlayerImpl implements SuperPlayer, ITXVodPlayListener {
     private boolean mNeedToPause = false;
     private boolean mTimeout = false;
     private boolean isPauseByTimeout = false;
+    private boolean isPauseByCasting = false;
     private boolean isStartBySeek = 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);
+        }
+
+        @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) {
+            positionHandler.stop();
+            updatePlayerState(SuperPlayerDef.PlayerState.END);
+        }
+
+        @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) {
+
+        }
+    };
+
     public SuperPlayerImpl(Context context, TXCloudVideoView videoView) {
-        mContext = context;
         mVideoView = videoView;
 
-        initVodPlayer(mContext);
+        initVodPlayer(context);
     }
 
     @Override
@@ -73,6 +165,8 @@ public class SuperPlayerImpl implements SuperPlayer, ITXVodPlayListener {
      */
     @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);
@@ -96,7 +190,6 @@ public class SuperPlayerImpl implements SuperPlayer, ITXVodPlayListener {
             case TXLiveConstants.PLAY_EVT_PLAY_PROGRESS:
                 int progress = param.getInt(TXLiveConstants.EVT_PLAY_PROGRESS_MS);
                 int duration = param.getInt(TXLiveConstants.EVT_PLAY_DURATION_MS);
-                mCurrent = progress / 1000;
 
                 if (duration != 0) {
                     updatePlayProgress(progress / 1000, duration / 1000);
@@ -154,7 +247,8 @@ public class SuperPlayerImpl implements SuperPlayer, ITXVodPlayListener {
     private void initVodPlayer(Context context) {
         mVodPlayer = new TXVodPlayer(context);
         SuperPlayerGlobalConfig config = SuperPlayerGlobalConfig.getInstance();
-        mVodPlayConfig = new TXVodPlayConfig();
+        // 点播播放器配置
+        TXVodPlayConfig mVodPlayConfig = new TXVodPlayConfig();
 
         File sdcardDir = context.getExternalFilesDir(null);
         if (sdcardDir != null) {
@@ -172,6 +266,16 @@ public class SuperPlayerImpl implements SuperPlayer, ITXVodPlayListener {
         mVodPlayer.setMirror(config.mirror);
     }
 
+    private void initCastPlayer() {
+        DLNACastManager.getInstance().registerActionCallbacks(
+                castEventListener,
+                playEventListener,
+                pauseEventListener,
+                stopEventListener,
+                seekToEventListener
+        );
+    }
+
     /**
      * 播放视频
      *
@@ -187,9 +291,17 @@ public class SuperPlayerImpl implements SuperPlayer, ITXVodPlayListener {
             return;
         }
 
-        mVodPlayer.setPlayerView(mVideoView);
-        playVodURL(videoURL);
-
+        if (!isCasting) {
+            mVodPlayer.setPlayerView(mVideoView);
+            playVodURL(videoURL);
+        } else {
+            if (castDevice != null) {
+                DLNACastManager.getInstance().cast(
+                        castDevice, CastObject.INSTANCE.newInstance(
+                                videoURL, UUID.randomUUID().toString(), model.title
+                        ));
+            }
+        }
         updatePlayProgress(0, model.duration);
     }
 
@@ -218,6 +330,8 @@ public class SuperPlayerImpl implements SuperPlayer, ITXVodPlayListener {
      * @param duration 总时长(秒)
      */
     private void updatePlayProgress(long current, long duration) {
+        mCurrent = current;
+
         if (mObserver != null) {
             mObserver.onPlayProgress(current, duration);
         }
@@ -308,14 +422,22 @@ public class SuperPlayerImpl implements SuperPlayer, ITXVodPlayListener {
 
     @Override
     public void pause() {
-        mVodPlayer.pause();
-        updatePlayerState(SuperPlayerDef.PlayerState.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();
@@ -326,8 +448,13 @@ public class SuperPlayerImpl implements SuperPlayer, ITXVodPlayListener {
 
     @Override
     public void stop() {
-        resetPlayer();
-        updatePlayerState(SuperPlayerDef.PlayerState.END);
+        if (!isCasting) {
+            resetPlayer();
+            updatePlayerState(SuperPlayerDef.PlayerState.END);
+        } else {
+            DLNACastManager.getInstance().stop();
+            DLNACastManager.getInstance().unregisterActionCallbacks();
+        }
     }
 
     @Override
@@ -373,6 +500,7 @@ public class SuperPlayerImpl implements SuperPlayer, ITXVodPlayListener {
         if (mCurrentPlayMode == playerMode) {
             return;
         }
+
         mCurrentPlayMode = playerMode;
 
         if (mCurrentPlayMode == SuperPlayerDef.PlayerMode.FULLSCREEN) {
@@ -384,18 +512,25 @@ public class SuperPlayerImpl implements SuperPlayer, ITXVodPlayListener {
 
     @Override
     public void seek(int position) {
-        if(mCurrentModel != null) {
-            HistoryUtil.INSTANCE.sendHistory(mStartPoint, mEndPoint, mCurrentModel.sectionId);
+        if (mCurrentModel != null) {
+            final long start = mStartPoint;
+            final long end = mEndPoint;
+
+            HistoryUtil.INSTANCE.sendHistory(start, end, mCurrentModel.sectionId);
         }
 
         mStartPoint = position;
         isStartBySeek = true;
 
-        if (mVodPlayer != null) {
-            mVodPlayer.seek(position);
-            if (!mVodPlayer.isPlaying()) {
-                mVodPlayer.resume();
+        if (!isCasting) {
+            if (mVodPlayer != null) {
+                mVodPlayer.seek(position);
+                if (!mVodPlayer.isPlaying()) {
+                    mVodPlayer.resume();
+                }
             }
+        } else {
+            DLNACastManager.getInstance().seekTo(position);
         }
 
         if (mObserver != null) {
@@ -403,6 +538,42 @@ public class SuperPlayerImpl implements SuperPlayer, ITXVodPlayListener {
         }
     }
 
+    @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;
+
+        DLNACastManager.getInstance().unregisterActionCallbacks();
+        DLNACastManager.getInstance().stop();
+        castDevice = null;
+
+        if (isPauseByCasting) {
+            playWithModel(mCurrentModel);
+            mVodPlayer.seek(mCurrent);
+        }
+    }
+
     @Override
     public SuperPlayerDef.PlayerMode getPlayerMode() {
         return mCurrentPlayMode;
@@ -428,4 +599,44 @@ public class SuperPlayerImpl implements SuperPlayer, ITXVodPlayListener {
         this.mStartPos = startPos;
         mVodPlayer.setStartTime(startPos);
     }
+
+    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);
+        }
+    }
 }

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

@@ -1,8 +1,5 @@
 package com.tencent.liteav.demo.superplayer.player;
 
-
-import com.tencent.liteav.demo.superplayer.database.entity.History;
-
 public abstract class SuperPlayerObserver {
 
     /**

+ 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();
+    }
+
+}

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

@@ -130,10 +130,6 @@ public class FullScreenPlayer extends AbsPlayer implements UnlockProgressView.On
         toggleView(mImageCover, isVisible);
     }
 
-    public void prepareLoading() {
-
-    }
-
     @Override
     public void updatePlayState(SuperPlayerDef.PlayerState playState) {
         mCurrentPlayState = playState;

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

@@ -116,6 +116,11 @@ public interface Player {
          */
         void playNext();
 
+        /**
+         * 结束投屏
+         */
+        void stopCast();
+
         /**
          * 弹出验证框
          */

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

@@ -43,7 +43,6 @@ public class WindowPlayer extends AbsPlayer implements View.OnClickListener,
     private TextView mTvDuration;                            // 总时长文本
     private PointProgressBar mBarProgress;                       // 播放进度条
 
-    private ProgressBar mPbLiveLoading;                         // 加载圈
     private ImageView mImageCover;                            // 封面图
     private VolumeBrightnessProgressLayout mGestureVolumeBrightnessProgressLayout; // 音量亮度调节布局
     private VideoProgressLayout mGestureVideoProgressLayout;            // 手势快进提示布局
@@ -183,7 +182,6 @@ public class WindowPlayer extends AbsPlayer implements View.OnClickListener,
         mBarProgress.setListener(this);
 
         mIvFullScreen = (ImageView) findViewById(R.id.superplayer_iv_fullscreen);
-        mPbLiveLoading = (ProgressBar) findViewById(R.id.superplayer_pb_live);
         mImageCover = (ImageView) findViewById(R.id.superplayer_cover_view);
         mIvPlayNext = (ImageView) findViewById(R.id.superplayer_iv_play_next);
 
@@ -243,14 +241,6 @@ public class WindowPlayer extends AbsPlayer implements View.OnClickListener,
         toggleView(mIvPlayNext, isShowing);
     }
 
-    private void updateStartUI(boolean isAutoPlay) {
-        if (isAutoPlay) {
-            toggleView(mPbLiveLoading, true);
-        } else {
-            toggleView(mPbLiveLoading, false);
-        }
-    }
-
     public void preparePlayVideo(SuperPlayerModel superPlayerModel) {
         if (!isDestroy) {
             if (superPlayerModel.cover != null) {
@@ -265,7 +255,6 @@ public class WindowPlayer extends AbsPlayer implements View.OnClickListener,
         mIvPause.setImageResource(R.mipmap.play_state);
         updateVideoProgress(0, superPlayerModel.duration);
         mBarProgress.setEnabled(true);
-        updateStartUI(true);
     }
 
     /**
@@ -296,30 +285,18 @@ public class WindowPlayer extends AbsPlayer implements View.OnClickListener,
         toggleView(mImageCover, isVisible);
     }
 
-    public void prepareLoading() {
-        toggleView(mPbLiveLoading, true);
-    }
-
     @Override
     public void updatePlayState(SuperPlayerDef.PlayerState playState) {
         switch (playState) {
             case INIT:
+            case PAUSE:
+            case END:
                 mIvPause.setImageResource(R.mipmap.play_state);
                 break;
             case PLAYING:
-                mBarProgress.setEnabled(true);
-                mIvPause.setImageResource(R.mipmap.pause_state);
-                toggleView(mPbLiveLoading, false);
-                break;
             case LOADING:
                 mBarProgress.setEnabled(true);
                 mIvPause.setImageResource(R.mipmap.pause_state);
-                toggleView(mPbLiveLoading, true);
-                break;
-            case PAUSE:
-            case END:
-                mIvPause.setImageResource(R.mipmap.play_state);
-                toggleView(mPbLiveLoading, false);
                 break;
         }
         mCurrentPlayState = playState;

+ 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
+}

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

@@ -0,0 +1,25 @@
+package com.tencent.liteav.demo.superplayer.util
+
+import org.fourthline.cling.model.meta.Device
+
+object CastorUtil {
+    interface Listener {
+        fun onDevice(device: Device<*, *, *>)
+    }
+
+    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<*, *, *>) {
+        for (listener in listeners) {
+            listener.onDevice(device)
+        }
+    }
+}

+ 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>

+ 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>

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

@@ -131,13 +131,6 @@
             android:src="@mipmap/full_screen" />
     </LinearLayout>
 
-    <ProgressBar
-        android:id="@+id/superplayer_pb_live"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_centerInParent="true"
-        android:visibility="gone" />
-
     <com.tencent.liteav.demo.superplayer.ui.player.TimeOutPlayer
         android:id="@+id/superplayer_timeout_view"
         android:layout_width="match_parent"

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

@@ -9,6 +9,11 @@
         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"

+ 7 - 1
ui/build.gradle

@@ -1,4 +1,4 @@
-plugins{
+plugins {
     id 'com.android.application'
     id 'kotlin-android'
 }
@@ -14,6 +14,9 @@ android {
 
         testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
     }
+    packagingOptions {
+        exclude 'META-INF/beans.xml'
+    }
 
     buildFeatures {
         viewBinding true
@@ -42,11 +45,14 @@ dependencies {
 
     implementation project(':kit')
     api 'com.tencent.mm.opensdk:wechat-sdk-android:+'
+    api 'com.github.devin1014.DLNA-Cast:dlna-dmc:V1.0.0'
+
     implementation("com.alibaba.fastjson2:fastjson2-kotlin:2.0.12.android")
 
     implementation 'com.tencent.liteav:LiteAVSDK_Player:latest.release'
 
     implementation 'androidx.activity:activity-ktx:1.5.1'
+    implementation 'androidx.work:work-runtime-ktx:2.7.1'
 
     implementation("org.greenrobot:eventbus:3.3.1")
 

+ 9 - 1
ui/src/main/AndroidManifest.xml

@@ -31,7 +31,7 @@
             android:launchMode="singleTask"
             android:screenOrientation="landscape"
             android:theme="@style/PlayerTheme"
-            android:windowSoftInputMode="stateHidden"></activity>
+            android:windowSoftInputMode="stateHidden" />
 
         <activity
             android:name=".PlayerMenu"
@@ -62,5 +62,13 @@
             android:launchMode="singleTop"
             android:screenOrientation="landscape"
             android:theme="@style/AppTheme.Dialog" />
+
+        <activity
+            android:name=".PlayerCast"
+            android:launchMode="singleTop"
+            android:screenOrientation="landscape"
+            android:theme="@style/AppTheme.Dialog" />
+
+        <service android:name="com.android.cast.dlna.dmc.DLNACastService" />
     </application>
 </manifest>

+ 57 - 0
ui/src/main/java/com/tencent/liteav/demo/player/ui/CastScanning.java

@@ -0,0 +1,57 @@
+package com.tencent.liteav.demo.player.ui;
+
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.AnimationUtils;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.RotateAnimation;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import com.tencent.liteav.demo.player.R;
+
+public class CastScanning extends LinearLayout {
+    private ImageView imgLoading;
+
+
+    public CastScanning(Context context) {
+        this(context, null);
+    }
+
+    public CastScanning(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public CastScanning(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public CastScanning(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) {
+        LayoutInflater.from(context).inflate(R.layout.cast_scanning, this);
+
+        imgLoading = findViewById(R.id.img_cast_scanning);
+
+        Animation animation = AnimationUtils.loadAnimation(context, R.anim.rotate_loading);
+        animation.setInterpolator(new LinearInterpolator());
+        animation.setDuration(1200);
+        animation.setRepeatCount(-1);
+        animation.setRepeatMode(Animation.RESTART);
+        imgLoading.startAnimation(animation);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+    }
+}

+ 104 - 0
ui/src/main/java/com/tencent/liteav/demo/player/ui/PlayerCastAdapter.kt

@@ -0,0 +1,104 @@
+package com.tencent.liteav.demo.player.ui
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.os.Handler
+import android.os.Looper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.android.cast.dlna.dmc.OnDeviceRegistryListener
+import com.tencent.liteav.demo.player.R
+import org.fourthline.cling.model.meta.Device
+
+class PlayerCastAdapter(
+    val listener: OnItemSelectedListener? = null,
+) : RecyclerView.Adapter<PlayerCastAdapter.ViewHolder>(), OnDeviceRegistryListener {
+    interface OnItemSelectedListener {
+        fun onItemSelected(castDevice: Device<*, *, *>?, selected: Boolean)
+    }
+
+    private val handler = Handler(Looper.getMainLooper())
+    private val deviceList: MutableList<Device<*, *, *>> = ArrayList()
+
+    var castDevice: Device<*, *, *>? = null
+        @SuppressLint("NotifyDataSetChanged")
+        set(value) {
+            field = value
+            notifyDataSetChanged()
+        }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        val view = View.inflate(parent.context, R.layout.item_cast, null)
+        return ViewHolder(view, listener)
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        holder.setDevice(getItem(position), isSelected(position))
+    }
+
+    override fun getItemCount(): Int {
+        return deviceList.size
+    }
+
+    private fun getItem(position: Int): Device<*, *, *> {
+        return deviceList[position]
+    }
+
+    private fun isSelected(position: Int): Boolean {
+        val device = getItem(position)
+        return if (device != null && castDevice != null) {
+            device.identity.udn.identifierString == castDevice!!.identity.udn.identifierString
+        } else false
+    }
+
+    override fun onDeviceAdded(device: Device<*, *, *>) {
+        if (!deviceList.contains(device)) {
+            deviceList.add(device)
+            if (Thread.currentThread() !== Looper.getMainLooper().thread) {
+                handler.post { notifyDataSetChanged() }
+            } else {
+                notifyDataSetChanged()
+            }
+        }
+    }
+
+    override fun onDeviceUpdated(device: Device<*, *, *>?) {}
+
+    override fun onDeviceRemoved(device: Device<*, *, *>) {
+        if (deviceList.contains(device)) {
+            deviceList.remove(device)
+            if (Thread.currentThread() !== Looper.getMainLooper().thread) {
+                handler.post { notifyDataSetChanged() }
+            } else {
+                notifyDataSetChanged()
+            }
+        }
+    }
+
+    inner class ViewHolder(
+        val itemView: View,
+        private val itemSelectedListener: OnItemSelectedListener?,
+    ) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
+        private val textDevice: TextView
+        private var device: Device<*, *, *>? = null
+
+        fun setDevice(device: Device<*, *, *>, isSelected: Boolean) {
+            this.device = device
+            textDevice.text = device.details.friendlyName
+        }
+
+        init {
+            textDevice = itemView.findViewById(R.id.device_name)
+            itemView.setOnClickListener(this)
+        }
+
+        override fun onClick(v: View?) {
+            device?.let {
+                itemSelectedListener?.onItemSelected(it, true)
+            }
+        }
+    }
+}

+ 37 - 17
ui/src/main/kotlin/com/tencent/liteav/demo/player/PlayerActivity.kt

@@ -9,6 +9,11 @@ import android.widget.Toast
 import androidx.activity.viewModels
 import androidx.appcompat.app.AppCompatActivity
 import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.work.Constraints
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkManager
+import com.android.cast.dlna.dmc.DLNACastManager
 import com.tencent.liteav.demo.player.ViewInsets.ViewportMetrics
 import com.tencent.liteav.demo.player.databinding.ActivityPlayerBinding
 import com.tencent.liteav.demo.player.ui.PlayerListAdapter
@@ -16,17 +21,20 @@ import com.tencent.liteav.demo.player.ui.PlayerListDecoration
 import com.tencent.liteav.demo.player.util.ModelProvider
 import com.tencent.liteav.demo.player.viewmodel.PlayerViewModel
 import com.tencent.liteav.demo.player.viewmodel.PlayerViewModelFactory
+import com.tencent.liteav.demo.player.viewmodel.UploadWorker
 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.SuperPlayerView.OnSuperPlayerViewCallback
+import com.tencent.liteav.demo.superplayer.SuperPlayerView.OnPlayerViewCallback
 import com.tencent.liteav.demo.superplayer.database.PlayerDatabaseProvider
+import com.tencent.liteav.demo.superplayer.util.CastorUtil
 import com.tencent.liteav.demo.superplayer.util.TimeoutUtil
 import com.tencent.liteav.demo.superplayer.util.TimersUtil
 import com.tencent.rtmp.TXLiveConstants
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.MainScope
 import kotlinx.coroutines.launch
+import org.fourthline.cling.model.meta.Device
 
 /**
  * Created by liyuejiao on 2018/7/3.
@@ -34,8 +42,9 @@ import kotlinx.coroutines.launch
  */
 class PlayerActivity : AppCompatActivity(),
     View.OnClickListener,
-    OnSuperPlayerViewCallback,
+    OnPlayerViewCallback,
     PlayerListAdapter.OnItemClickListener,
+    CastorUtil.Listener,
     CoroutineScope by MainScope(), TimeoutUtil.Listener {
 
     private var _viewBinding: ActivityPlayerBinding? = null
@@ -135,6 +144,12 @@ class PlayerActivity : AppCompatActivity(),
         prefs.renderMode = TXLiveConstants.RENDER_MODE_ADJUST_RESOLUTION
     }
 
+    override fun onStart() {
+        super.onStart()
+        CastorUtil.addListener(this)
+        DLNACastManager.getInstance().bindCastService(this)
+    }
+
     override fun onResume() {
         super.onResume()
         TimeoutUtil.addListener(this)
@@ -159,18 +174,37 @@ class PlayerActivity : AppCompatActivity(),
     }
 
     override fun onStop() {
+        CastorUtil.removeListener(this)
+//        DLNACastManager.getInstance().unbindCastService(this);
         super.onStop()
         viewModel.onStop()
     }
 
     override fun onDestroy() {
         super.onDestroy()
-
+        startUpload()
         mSuperPlayerView.release()
         mSuperPlayerView.resetPlayer()
         _viewBinding = null
     }
 
+    override fun onDevice(device: Device<*, *, *>) {
+        mIsManualPause = false
+        mSuperPlayerView.startCast(device)
+    }
+
+    private fun startUpload() {
+        val constraints = Constraints.Builder()
+            .setRequiredNetworkType(NetworkType.CONNECTED)
+            .build()
+
+        val request = OneTimeWorkRequest
+            .Builder(UploadWorker::class.java)
+            .setConstraints(constraints)
+            .build()
+        WorkManager.getInstance(this).enqueue(request)
+    }
+
     override fun onItemClick(videoModel: SuperPlayerModel, index: Int) {
         playVideoModel(index)
     }
@@ -260,10 +294,6 @@ class PlayerActivity : AppCompatActivity(),
         }
     }
 
-    override fun onPlayEnd() {}
-
-    override fun onPlaying() {}
-
     override fun onVerify(reason: SuperPlayerDef.VerifyReason) {
         if (reason == SuperPlayerDef.VerifyReason.TIMEOUT) {
             PlayerVerify.start(this@PlayerActivity, 0)
@@ -279,16 +309,6 @@ class PlayerActivity : AppCompatActivity(),
         }
     }
 
-    override fun onResumePlay() {
-//        viewModel.resumeTimer()
-    }
-
-    override fun onPausePlay() {
-//        viewModel.pauseTimer()
-    }
-
-    override fun onError(code: Int) {}
-
     companion object {
         private const val TAG = "SuperPlayerActivity"
         private const val sPlayerViewDisplayRatio =

+ 73 - 0
ui/src/main/kotlin/com/tencent/liteav/demo/player/PlayerCast.java

@@ -0,0 +1,73 @@
+package com.tencent.liteav.demo.player;
+
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.android.cast.dlna.dmc.DLNACastManager;
+import com.android.cast.dlna.dmc.OnDeviceRegistryListener;
+import com.tencent.liteav.demo.player.databinding.ActivityCastBinding;
+import com.tencent.liteav.demo.player.ui.PlayerCastAdapter;
+import com.tencent.liteav.demo.superplayer.util.CastorUtil;
+
+import org.fourthline.cling.model.meta.Device;
+
+public class PlayerCast extends AppCompatActivity implements OnDeviceRegistryListener {
+    private ActivityCastBinding viewBinding;
+
+    private PlayerCastAdapter adapter;
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        viewBinding = ActivityCastBinding.inflate(getLayoutInflater());
+        setContentView(viewBinding.getRoot());
+
+        adapter = new PlayerCastAdapter(new PlayerCastAdapter.OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(@Nullable Device<?, ?, ?> castDevice, boolean selected) {
+                CastorUtil.INSTANCE.selectDevice(castDevice);
+                PlayerCast.this.finish();
+            }
+        });
+        viewBinding.castDevice.setAdapter(adapter);
+        viewBinding.castCancel.setOnClickListener((v) -> {
+            finish();
+        });
+
+        DLNACastManager.getInstance().registerDeviceListener(adapter);
+        DLNACastManager.getInstance().registerDeviceListener(this);
+
+        DLNACastManager.getInstance().search(null, 60);
+    }
+
+    @Override
+    protected void onDestroy() {
+        DLNACastManager.getInstance().unregisterListener(adapter);
+        DLNACastManager.getInstance().unregisterListener(this);
+        super.onDestroy();
+
+        viewBinding = null;
+    }
+
+    @Override
+    public void onDeviceAdded(Device<?, ?, ?> device) {
+        if (viewBinding.castDevice.getVisibility() != View.VISIBLE) {
+            viewBinding.castDevice.setVisibility(View.VISIBLE);
+            viewBinding.castLoading.setVisibility(View.GONE);
+        }
+    }
+
+    @Override
+    public void onDeviceUpdated(Device<?, ?, ?> device) {
+
+    }
+
+    @Override
+    public void onDeviceRemoved(Device<?, ?, ?> device) {
+
+    }
+}

+ 3 - 0
ui/src/main/kotlin/com/tencent/liteav/demo/player/PlayerMenu.kt

@@ -63,6 +63,9 @@ class PlayerMenu : AppCompatActivity(), View.OnClickListener, CoroutineScope by
                 finish()
             }
             R.id.menu_item_cast -> {
+                val intent = Intent(this, PlayerCast::class.java)
+                startActivity(intent)
+                finish()
             }
             R.id.menu_item_favorite -> {
                 launch(Dispatchers.IO) {

+ 23 - 0
ui/src/main/kotlin/com/tencent/liteav/demo/player/viewmodel/UploadWorker.kt

@@ -0,0 +1,23 @@
+package com.tencent.liteav.demo.player.viewmodel
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import com.tencent.liteav.demo.superplayer.database.PlayerDatabaseProvider
+
+class UploadWorker(
+    appContext: Context,
+    params: WorkerParameters,
+) : CoroutineWorker(appContext, params) {
+    override suspend fun doWork(): Result {
+        val database = applicationContext as PlayerDatabaseProvider
+        val repository = database.getPlayerRepository()
+
+
+        return Result.success()
+    }
+
+    private fun batchUpload(){
+
+    }
+}

+ 6 - 0
ui/src/main/res/anim/rotate_loading.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fromDegrees="0"
+    android:pivotX="50%"
+    android:pivotY="50%"
+    android:toDegrees="359" />

+ 6 - 0
ui/src/main/res/drawable/cast_cancel_background.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="#FFFF9744" />
+    <corners android:radius="20dp" />
+</shape>

+ 6 - 0
ui/src/main/res/drawable/dialog_center_background.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="@android:color/white" />
+    <corners android:radius="20dp" />
+</shape>

+ 4 - 0
ui/src/main/res/drawable/icon_close.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+</selector>

+ 67 - 0
ui/src/main/res/layout/activity_cast.xml

@@ -0,0 +1,67 @@
+<?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">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="273dp"
+        android:layout_height="219dp"
+        android:background="@drawable/dialog_center_background"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <TextView
+            android:id="@+id/text_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="24dp"
+            android:text="选择要投屏的设备"
+            android:textColor="#ff333333"
+            android:textSize="16sp"
+            android:textStyle="bold"
+            app:layout_constraintLeft_toLeftOf="parent"
+            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/cast_device"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_marginHorizontal="20dp"
+            android:layout_marginTop="6.5dp"
+            android:layout_marginBottom="5.5dp"
+            android:orientation="vertical"
+            android:visibility="gone"
+            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+            app:layout_constraintBottom_toTopOf="@id/cast_cancel"
+            app:layout_constraintTop_toBottomOf="@id/text_title" />
+
+        <com.tencent.liteav.demo.player.ui.CastScanning
+            android:id="@+id/cast_loading"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_marginHorizontal="20dp"
+            android:layout_marginTop="6.5dp"
+            android:layout_marginBottom="5.5dp"
+            app:layout_constraintBottom_toTopOf="@id/cast_cancel"
+            app:layout_constraintTop_toBottomOf="@id/text_title" />
+
+        <TextView
+            android:id="@+id/cast_cancel"
+            android:layout_width="match_parent"
+            android:layout_height="34dp"
+            android:layout_marginHorizontal="40dp"
+            android:layout_marginBottom="32dp"
+            android:background="@drawable/cast_cancel_background"
+            android:gravity="center"
+            android:text="取消"
+            android:textColor="#ffffffff"
+            android:textSize="14sp"
+            app:layout_constraintBottom_toBottomOf="parent" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 20 - 0
ui/src/main/res/layout/cast_scanning.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center">
+
+    <ImageView
+        android:id="@+id/img_cast_scanning"
+        android:layout_width="13dp"
+        android:layout_height="13dp"
+        android:src="@mipmap/cast_loading" />
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="4dp"
+        android:text="正在搜索设备…"
+        android:textColor="#ff666666"
+        android:textSize="13sp" />
+</LinearLayout>

+ 29 - 0
ui/src/main/res/layout/item_cast.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="31dp"
+    android:gravity="center_vertical">
+
+    <ImageView
+        android:layout_width="13dp"
+        android:layout_height="12dp"
+        android:layout_marginRight="5dp"
+        android:src="@mipmap/icon_cast" />
+
+    <TextView
+        android:id="@+id/device_name"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:ellipsize="end"
+        android:maxLines="1"
+        android:textColor="#FF333333" />
+
+    <ImageView
+        android:layout_width="14dp"
+        android:layout_height="14dp"
+        android:layout_marginLeft="5dp"
+        android:src="@mipmap/icon_check" />
+
+
+</androidx.appcompat.widget.LinearLayoutCompat>

BIN=BIN
ui/src/main/res/mipmap-hdpi/cast_help.png


BIN=BIN
ui/src/main/res/mipmap-hdpi/cast_loading.png


BIN=BIN
ui/src/main/res/mipmap-hdpi/icon_cast.png


BIN=BIN
ui/src/main/res/mipmap-hdpi/icon_check.png


BIN=BIN
ui/src/main/res/mipmap-hdpi/icon_uncheck.png


BIN=BIN
ui/src/main/res/mipmap-mdpi/cast_help.png


BIN=BIN
ui/src/main/res/mipmap-mdpi/cast_loading.png


BIN=BIN
ui/src/main/res/mipmap-mdpi/icon_cast.png


BIN=BIN
ui/src/main/res/mipmap-mdpi/icon_check.png


BIN=BIN
ui/src/main/res/mipmap-mdpi/icon_uncheck.png


BIN=BIN
ui/src/main/res/mipmap-xhdpi/cast_help.png


BIN=BIN
ui/src/main/res/mipmap-xhdpi/cast_loading.png


BIN=BIN
ui/src/main/res/mipmap-xhdpi/icon_cast.png


BIN=BIN
ui/src/main/res/mipmap-xhdpi/icon_check.png


BIN=BIN
ui/src/main/res/mipmap-xhdpi/icon_uncheck.png


BIN=BIN
ui/src/main/res/mipmap-xxhdpi/cast_help.png


BIN=BIN
ui/src/main/res/mipmap-xxhdpi/cast_loading.png


BIN=BIN
ui/src/main/res/mipmap-xxhdpi/icon_cast.png


BIN=BIN
ui/src/main/res/mipmap-xxhdpi/icon_check.png


BIN=BIN
ui/src/main/res/mipmap-xxhdpi/icon_uncheck.png


BIN=BIN
ui/src/main/res/mipmap-xxxhdpi/cast_help.png


BIN=BIN
ui/src/main/res/mipmap-xxxhdpi/cast_loading.png


BIN=BIN
ui/src/main/res/mipmap-xxxhdpi/icon_cast.png


BIN=BIN
ui/src/main/res/mipmap-xxxhdpi/icon_check.png


BIN=BIN
ui/src/main/res/mipmap-xxxhdpi/icon_uncheck.png