Przeglądaj źródła

feat: 视频房youtube增加额外两种加载方式

wutiaorong 6 miesięcy temu
rodzic
commit
e2fee8d55e
19 zmienionych plików z 535 dodań i 42 usunięć
  1. 1 0
      app/build.gradle
  2. 2 0
      app/dependencies/releaseRuntimeClasspath.txt
  3. 1 0
      app/src/main/java/com/adealink/weparty/config/Data.kt
  4. 2 0
      app/src/main/java/com/adealink/weparty/module/youtube/IYoutubeService.kt
  5. 8 0
      app/src/main/java/com/adealink/weparty/module/youtube/YoutubeModule.kt
  6. 2 0
      gradle/libs.versions.toml
  7. 2 0
      module/account/src/main/java/com/adealink/weparty/account/login/manager/LogoutCleaner.kt
  8. 158 0
      module/youtube/src/main/assets/youtube_embed.html
  9. 2 3
      module/youtube/src/main/java/com/adealink/weparty/youtube/AddYoutubeVideoFragment.kt
  10. 9 5
      module/youtube/src/main/java/com/adealink/weparty/youtube/YoutubeConfigManager.kt
  11. 8 2
      module/youtube/src/main/java/com/adealink/weparty/youtube/YoutubeFragment.kt
  12. 5 0
      module/youtube/src/main/java/com/adealink/weparty/youtube/YoutubeServiceImpl.kt
  13. 102 0
      module/youtube/src/main/java/com/adealink/weparty/youtube/config/YoutubePlayerConfigManager.kt
  14. 5 3
      module/youtube/src/main/java/com/adealink/weparty/youtube/data/Constants.kt
  15. 54 1
      module/youtube/src/main/java/com/adealink/weparty/youtube/data/Data.kt
  16. 57 0
      module/youtube/src/main/java/com/adealink/weparty/youtube/data/YoutubePlayerConfig.kt
  17. 9 0
      module/youtube/src/main/java/com/adealink/weparty/youtube/util/YoutubeUtil.kt
  18. 3 2
      module/youtube/src/main/java/com/adealink/weparty/youtube/view/YoutubePlayControlsView.kt
  19. 105 26
      module/youtube/src/main/java/com/adealink/weparty/youtube/view/YoutubePlayerWebView.kt

+ 1 - 0
app/build.gradle

@@ -375,6 +375,7 @@ dependencies {
     implementation libs.androidx.camera.lifecycle
     implementation libs.androidx.camera.video
     implementation libs.androidx.camera.view
+    api libs.androidx.webkit
 
 
     //android

+ 2 - 0
app/dependencies/releaseRuntimeClasspath.txt

@@ -98,6 +98,7 @@ androidx.vectordrawable:vectordrawable:1.2.0
 androidx.versionedparcelable:versionedparcelable:1.1.1
 androidx.viewpager2:viewpager2:1.1.0
 androidx.viewpager:viewpager:1.0.0
+androidx.webkit:webkit:1.14.0
 androidx.work:work-runtime-ktx:2.9.0
 androidx.work:work-runtime:2.9.0
 cn.rongcloud.sdk:databuried_annotation:0.2.1
@@ -292,5 +293,6 @@ org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3
 org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3
 org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3
 org.jetbrains:annotations:23.0.0
+org.jspecify:jspecify:1.0.0
 org.lsposed.hiddenapibypass:hiddenapibypass:4.3
 org.reactivestreams:reactive-streams:1.0.3

+ 1 - 0
app/src/main/java/com/adealink/weparty/config/Data.kt

@@ -91,6 +91,7 @@ enum class GlobalConfigType(val value: Int) {
     GLOBAL_NORMAL_IMAGE_MAX_SIZE(109), // 普通图片最大尺寸,默认2048
     GLOBAL_WEEK_RECHARGE_ACTIVITY_ID(111), // 每周充值活动ID
     GLOBAL_WEEK_RECHARGE_LOTTERY_ID(112), // 每周充值抽奖活动ID
+    GLOBAL_VIDEO_ROOM_PLAYER_LOAD_TYPE(130)//视频房播放器初始化类型
     ;
 
     companion object {

+ 2 - 0
app/src/main/java/com/adealink/weparty/module/youtube/IYoutubeService.kt

@@ -20,6 +20,8 @@ interface IYoutubeService : IService<IYoutubeService> {
 
     fun getYoutubeConfigViewModel(viewModelStoreOwner: ViewModelStoreOwner): IYoutubeConfigViewModel?
 
+    fun logout()
+
     companion object {
         const val RES_FUNCTION_ENABLE = 0 //功能可用
         const val RES_ENTRANCE_DISABLE = 1 //入口关闭

+ 8 - 0
app/src/main/java/com/adealink/weparty/module/youtube/YoutubeModule.kt

@@ -35,6 +35,10 @@ object YoutubeModule : BaseDynamicModule<IYoutubeService>(IYoutubeService::class
                 return null
             }
 
+            override fun logout() {
+
+            }
+
             override fun getService(): IYoutubeService? {
                 return null
             }
@@ -61,5 +65,9 @@ object YoutubeModule : BaseDynamicModule<IYoutubeService>(IYoutubeService::class
         return getService().getYoutubeConfigViewModel(viewModelStoreOwner)
     }
 
+    override fun logout() {
+        getService().logout()
+    }
+
 
 }

+ 2 - 0
gradle/libs.versions.toml

@@ -36,6 +36,7 @@ androidxViewpager2 = "1.1.0"
 androidxRecyclerview = "1.3.2"
 androidxCardview = "1.0.0"
 androidxCamera = "1.4.2"
+androidxWebkit = "1.14.0"
 
 # android
 androidMaterial = "1.12.0"
@@ -219,6 +220,7 @@ androidx_camera_video = { group = "androidx.camera", name = "camera-video", vers
 androidx_camera_view = { group = "androidx.camera", name = "camera-view", version.ref = "androidxCamera" }
 androidx_camera_mlkit_vision = { group = "androidx.camera", name = "camera-mlkit-vision", version.ref = "androidxCamera" }
 androidx_camera_extensions = { group = "androidx.camera", name = "camera-extensions", version.ref = "androidxCamera" }
+androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "androidxWebkit" }
 
 # android
 android-material = { group = "com.google.android.material", name = "material", version.ref = "androidMaterial" }

+ 2 - 0
module/account/src/main/java/com/adealink/weparty/account/login/manager/LogoutCleaner.kt

@@ -18,6 +18,7 @@ import com.adealink.weparty.module.room.RoomModule
 import com.adealink.weparty.module.skin.SkinModule
 import com.adealink.weparty.module.task.UserTaskManager
 import com.adealink.weparty.module.wallet.WalletModule
+import com.adealink.weparty.module.youtube.YoutubeModule
 import com.adealink.weparty.setting.SettingPref
 import com.adealink.weparty.storage.AppPref
 
@@ -53,5 +54,6 @@ object LogoutCleaner {
         DialogShowManager.logout()
         UserTaskManager.logout()
         HeadlineModule.logout()
+        YoutubeModule.logout()
     }
 }

+ 158 - 0
module/youtube/src/main/assets/youtube_embed.html

@@ -0,0 +1,158 @@
+            <!DOCTYPE html>
+            <html lang="en">
+                <style type="text/css">
+              html, body {
+                    height:100%;
+                    width:100%;
+                    margin:0;
+                    padding:0;
+                    background-color: #202020;
+                    overflow: hidden;
+                    position: fixed;
+              }
+            </style>
+                <head>
+                    <meta charset="UTF-8">
+                    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+                    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+                    <title>play</title>
+                    <style>*,body,html,div,p,img{border:0;margin:0;padding:0;}</style>
+                </head>
+                <body>
+                    <div id="player"></div>
+                </body>
+                <script type="text/javascript">
+
+                var tag = document.createElement('script');
+                tag.src = "https://www.youtube.com/iframe_api";
+                var firstScriptTag = document.getElementsByTagName('script')[0];
+                firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
+
+                var player;
+                var timerId;
+
+                function onYouTubeIframeAPIReady() {
+                    player = new YT.Player('player', {
+                        height: "100%",
+                        width: "100%",
+
+                        events: {
+                            'onReady': onPlayerReady,
+                            'onStateChange': onPlayerStateChange,
+                            'onError': onPlayerError
+                        },
+                        playerVars: {"autoplay":0,"controls":0,"enablejsapi":1,"fs":0,"rel":0,"iv_load_policy":3,"modestbranding":1,"cc_load_policy":0,"origin":"https://appassets.androidplatform.net"}
+                    });
+                }
+
+                function getQueryString(name) {
+                    var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|${'$'})");
+                    var r = window.location.search.substr(1).match(reg);
+                    if(r != null) {
+                        return unescape(r[2]);
+                    }
+                    return null;
+                }
+
+                function onPlayerReady() {
+                    player.setVolume(100);
+                    onReady();
+                }
+
+                function onPlayerStateChange(event) {
+                    clearTimeout(timerId);
+
+                    switch (event.data) {
+                        case YT.PlayerState.UNSTARTED:
+                            onStateChange("UNSTARTED");
+                        break;
+                        case YT.PlayerState.CUED:
+                            onStateChange("CUED");
+                        break;
+                        case YT.PlayerState.PLAYING:
+                            onStateChange("PLAYING");
+                            window.YoutubeInterface.onVideoDuration( player.getDuration() );
+                            startSendCurrentTimeInterval();
+                        break;
+                        case YT.PlayerState.BUFFERING:
+                            onStateChange("BUFFERING");
+                        break;
+                        case YT.PlayerState.PAUSED:
+                            onStateChange("PAUSED");
+                        break;
+                        case YT.PlayerState.ENDED:
+                            onStateChange("ENDED");
+                        break;
+                    }
+
+                    function startSendCurrentTimeInterval() {
+                        timerId = setInterval(function() {
+                            window.YoutubeInterface.onCurrentTime( player.getCurrentTime() );
+                            window.YoutubeInterface.onLoadedFraction( player.getVideoLoadedFraction() );
+                        }, 500 );
+                    }
+                }
+
+                function onPlayerError(event) {
+                    onError(event.data);
+                }
+
+                function getVideoID(url) {
+                    url = url.split(/(vi\/|v=|\/v\/|youtu\.be\/|\/embed\/)/);
+                    return (url[2] !== undefined) ? url[2].split(/[^0-9a-z_\-]/i)[0] : url[0];
+                }
+
+                function loadVideoUrl(videoUrl, startSeconds) {
+                    player.loadVideoById(getVideoID(videoUrl), startSeconds);
+                }
+
+                function loadVideo(videoId, startSeconds) {
+                    player.loadVideoById(videoId, startSeconds);
+                }
+
+                function cueVideo(videoId, startSeconds) {
+                    player.cueVideoById(videoId, startSeconds);
+                }
+
+                function playVideo(){
+                    player.playVideo();
+                };
+
+                function pauseVideo(){
+                    player.pauseVideo();
+                };
+
+                function stopVideo(){
+                    player.stopVideo();
+                };
+
+                function seekToVideo(position){
+                    player.seekTo(position, true);
+                };
+
+                function mute() {
+                    player.mute();
+                }
+
+                function unMute() {
+                    player.unMute();
+                }
+
+                function setVolume(volume) {
+                    player.setVolume(volume);
+                }
+
+                function onReady(){
+                    window.YoutubeInterface.onReady();
+                }
+
+                function onStateChange(event){
+                    window.YoutubeInterface.onStateChange(event);
+                }
+
+                function onError(event){
+                    window.YoutubeInterface.onError(event);
+                }
+
+            </script>
+            </html>

+ 2 - 3
module/youtube/src/main/java/com/adealink/weparty/youtube/AddYoutubeVideoFragment.kt

@@ -28,6 +28,7 @@ import com.adealink.weparty.youtube.data.YOUTUBE_LOGIN_HOST1
 import com.adealink.weparty.youtube.data.YOUTUBE_LOGIN_HOST2
 import com.adealink.weparty.youtube.databinding.FragmentAddYoutubeVideoBinding
 import com.adealink.weparty.youtube.util.extractVideoIdFromYouTubeLink
+import com.adealink.weparty.youtube.util.isLoginYoutube
 import com.adealink.weparty.youtube.view.add.IAddVideoWebViewCallback
 import com.adealink.weparty.youtube.viewmodel.YoutubeViewModel
 
@@ -98,9 +99,7 @@ class AddYoutubeVideoFragment : BottomDialogFragment(R.layout.fragment_add_youtu
                 }
                 if (goLogin == true && url != null && !processedLogin) {
                     if (url.contains(YOUTUBE_LOGIN_HOST1) || url.contains(YOUTUBE_LOGIN_HOST2)) {
-                        val cookieManager = CookieManager.getInstance()
-                        val cookies = cookieManager.getCookie(YOUTUBE_LOGIN_COOKIE_URL)
-                        if (cookies != null && cookies.contains("SID")) {
+                        if (isLoginYoutube()) {
                             youtubeViewModel.notifyYoutubeLoginSuccess()
                             processedLogin = true
                             dismiss()

+ 9 - 5
module/youtube/src/main/java/com/adealink/weparty/youtube/YoutubeConfigManager.kt

@@ -8,6 +8,8 @@ import com.adealink.weparty.config.GlobalConfigType
 import com.adealink.weparty.config.IGlobalConfigListener
 import com.adealink.weparty.config.globalConfigManager
 import com.adealink.weparty.module.youtube.IYoutubeConfigManager
+import com.adealink.weparty.youtube.config.YoutubePlayerConfigManager
+import com.adealink.weparty.youtube.data.TAG_YOUTUBE_CONFIG
 
 val youtubeConfigManager: IYoutubeConfigManager by lazy { YoutubeConfigManager() }
 
@@ -21,9 +23,11 @@ class YoutubeConfigManager : IYoutubeConfigManager, IGlobalConfigListener {
     init {
         globalConfigManager.addListener(GlobalConfigType.GLOBAL_APPROVING_VERSION, this)
         globalConfigManager.addListener(GlobalConfigType.GLOBAL_VIDEO_ROOM_ENTRANCE_LEVEL_LIMIT, this)
+        globalConfigManager.addListener(GlobalConfigType.GLOBAL_VIDEO_ROOM_PLAYER_LOAD_TYPE, this)
 
         entranceEnable = getEntranceEnableBy(globalConfigManager.getConfig(GlobalConfigType.GLOBAL_APPROVING_VERSION)?.firstOrNull())
         userLevelLimit = getLevelLimitBy(globalConfigManager.getConfig(GlobalConfigType.GLOBAL_VIDEO_ROOM_ENTRANCE_LEVEL_LIMIT)?.firstOrNull())
+        YoutubePlayerConfigManager.setConfigFromJson(globalConfigManager.getConfig(GlobalConfigType.GLOBAL_VIDEO_ROOM_PLAYER_LOAD_TYPE)?.firstOrNull())
     }
 
     override fun isEntranceEnable(): Boolean {
@@ -54,6 +58,9 @@ class YoutubeConfigManager : IYoutubeConfigManager, IGlobalConfigListener {
             GlobalConfigType.GLOBAL_VIDEO_ROOM_ENTRANCE_LEVEL_LIMIT -> {
                 handleLevelLimitChanged(config.firstOrNull())
             }
+            GlobalConfigType.GLOBAL_VIDEO_ROOM_PLAYER_LOAD_TYPE -> {
+                YoutubePlayerConfigManager.setConfigFromJson(config.firstOrNull())
+            }
             else -> {
 
             }
@@ -87,20 +94,17 @@ class YoutubeConfigManager : IYoutubeConfigManager, IGlobalConfigListener {
     }
 
     private fun notifyEntranceEnableChanged() {
-        Log.d(TAG, "notifyEntranceEnableChanged: $entranceEnable")
+        Log.d(TAG_YOUTUBE_CONFIG, "notifyEntranceEnableChanged: $entranceEnable")
         listeners.dispatch {
             it.onEntranceChanged(entranceEnable)
         }
     }
 
     private fun notifyUserLevelLimitChanged() {
-        Log.d(TAG, "notifyUserLevelLimitChanged: $userLevelLimit")
+        Log.d(TAG_YOUTUBE_CONFIG, "notifyUserLevelLimitChanged: $userLevelLimit")
         listeners.dispatch {
             it.onUserLevelLimitChanged(userLevelLimit)
         }
     }
 
-    companion object {
-        const val TAG = "YoutubeConfigManager"
-    }
 }

+ 8 - 2
module/youtube/src/main/java/com/adealink/weparty/youtube/YoutubeFragment.kt

@@ -19,8 +19,9 @@ import com.adealink.weparty.module.youtube.data.YoutubeVideoData
 import com.adealink.weparty.module.youtube.data.YoutubeVideoInfo
 import com.adealink.weparty.module.youtube.fragment.IVideoPlayerFragment
 import com.adealink.weparty.youtube.data.TAG_YOUTUBE
-import com.adealink.weparty.youtube.data.YOUTUBE_NEED_LOGIN_ERROR
+import com.adealink.weparty.youtube.data.YoutubePlayerError
 import com.adealink.weparty.youtube.databinding.FragmentVideoPlayerBinding
+import com.adealink.weparty.youtube.util.isLoginYoutube
 import com.adealink.weparty.youtube.view.YouTubePlayerWebView
 import com.adealink.weparty.youtube.view.YoutubePlayControlsView
 import com.adealink.weparty.youtube.viewmodel.YoutubeViewModel
@@ -49,6 +50,9 @@ class YoutubeFragment : BaseFragment(R.layout.fragment_video_player), IVideoPlay
 
     override fun initViews() {
         super.initViews()
+        //触发初始化
+        youtubeConfigManager
+
         initPlayerView()
     }
 
@@ -187,6 +191,7 @@ class YoutubeFragment : BaseFragment(R.layout.fragment_video_player), IVideoPlay
             Log.e(TAG_YOUTUBE, "getVideoInfo fail, for roomId is 0")
             return
         }
+
         //获取当前房间的视频信息
         youtubeViewModel.syncVideoInfo(roomId)
     }
@@ -210,7 +215,8 @@ class YoutubeFragment : BaseFragment(R.layout.fragment_video_player), IVideoPlay
                 TAG,
                 "observeViewModel, youtubeLoginSuccess"
             )
-            if (errorMsg == YOUTUBE_NEED_LOGIN_ERROR) {
+            if (errorMsg == YoutubePlayerError.EMBEDDING_NOT_ALLOWED_ALT.toString() && !isLoginYoutube()) {
+                //150错误且未登录
                 binding.playerControls.reset()
                 binding.playerContainer.removeAllViews()
                 playerView = null

+ 5 - 0
module/youtube/src/main/java/com/adealink/weparty/youtube/YoutubeServiceImpl.kt

@@ -6,6 +6,7 @@ import com.adealink.frame.spi.RegisterService
 import com.adealink.weparty.module.youtube.IYoutubeService
 import com.adealink.weparty.module.youtube.fragment.IVideoPlayerFragment
 import com.adealink.weparty.module.youtube.viewmodel.IYoutubeConfigViewModel
+import com.adealink.weparty.youtube.config.YoutubePlayerConfigManager
 import com.adealink.weparty.youtube.viewmodel.YoutubeConfigConfigViewModel
 
 @RegisterService(IYoutubeService::class)
@@ -40,6 +41,10 @@ class YoutubeServiceImpl : IYoutubeService {
         return ViewModelProvider(viewModelStoreOwner)[YoutubeConfigConfigViewModel::class.java]
     }
 
+    override fun logout() {
+        YoutubePlayerConfigManager.clearConfig()
+    }
+
     override fun getService(): IYoutubeService {
         return this
     }

+ 102 - 0
module/youtube/src/main/java/com/adealink/weparty/youtube/config/YoutubePlayerConfigManager.kt

@@ -0,0 +1,102 @@
+package com.adealink.weparty.youtube.config
+
+import com.adealink.frame.data.json.froJsonErrorNull
+import com.adealink.frame.log.Log
+import com.adealink.weparty.youtube.data.PlayerLoadType
+import com.adealink.weparty.youtube.data.ResolvedPlayerConfig
+import com.adealink.weparty.youtube.data.TAG_YOUTUBE_PLAYER_CONFIG
+import com.adealink.weparty.youtube.data.YoutubePlayerConfig
+
+/**
+ * YouTube播放器配置管理器
+ * 支持用户和国家维度的配置
+ */
+object YoutubePlayerConfigManager {
+
+    @Volatile
+    private var currentConfig: YoutubePlayerConfig? = null
+    
+    /**
+     * 设置配置
+     */
+    private fun setConfig(config: YoutubePlayerConfig) {
+        currentConfig = config
+        Log.d(TAG_YOUTUBE_PLAYER_CONFIG, "Config updated: $config")
+    }
+    
+    /**
+     * 从JSON字符串设置配置
+     */
+    fun setConfigFromJson(jsonString: String?) {
+        val config = froJsonErrorNull<YoutubePlayerConfig>(jsonString) ?: return
+        setConfig(config)
+    }
+    
+    /**
+     * 解析配置,根据用户ID和国家代码获取最终配置
+     * 优先级:用户配置 > 国家配置 > 默认配置
+     */
+    fun resolveConfig(userId: Long? = null, countryCode: String? = null): ResolvedPlayerConfig {
+        val config = currentConfig ?: return getDefaultConfig()
+        
+        // 1. 优先检查用户配置
+        userId?.let { uid ->
+            config.user.forEach { userConfig ->
+                if (userConfig.uids.contains(uid)) {
+                    val loadType = parseLoadType(userConfig.loadType)
+                    if (loadType != null) {
+                        Log.d(TAG_YOUTUBE_PLAYER_CONFIG, "Using user config for uid: $uid, loadType: $loadType, url: ${userConfig.url}")
+                        return ResolvedPlayerConfig(loadType, userConfig.url)
+                    }
+                }
+            }
+        }
+        
+        // 2. 检查国家配置
+        countryCode?.let { country ->
+            config.country[country]?.let { countryConfig ->
+                val loadType = parseLoadType(countryConfig.loadType)
+                if (loadType != null) {
+                    Log.d(TAG_YOUTUBE_PLAYER_CONFIG, "Using country config for: $country, loadType: $loadType, url: ${countryConfig.url}")
+                    return ResolvedPlayerConfig(loadType, countryConfig.url)
+                }
+            }
+        }
+        
+        // 3. 使用默认配置
+        val defaultLoadType = parseLoadType(config.loadType)
+        if (defaultLoadType != null) {
+            Log.d(TAG_YOUTUBE_PLAYER_CONFIG, "Using default config, loadType: $defaultLoadType, url: ${config.url}")
+            return ResolvedPlayerConfig(defaultLoadType, config.url)
+        }
+        
+        // 4. 如果都没有,返回系统默认配置
+        Log.w(TAG_YOUTUBE_PLAYER_CONFIG, "No valid config found, using system default")
+        return getDefaultConfig()
+    }
+    
+    /**
+     * 解析加载类型字符串
+     */
+    private fun parseLoadType(loadTypeString: String): PlayerLoadType? {
+        return PlayerLoadType.map(loadTypeString)
+    }
+    
+    /**
+     * 获取系统默认配置
+     */
+    private fun getDefaultConfig(): ResolvedPlayerConfig {
+        return ResolvedPlayerConfig(
+            loadType = PlayerLoadType.ASSETS_HTML,
+            url = null
+        )
+    }
+    
+    /**
+     * 清除配置
+     */
+    fun clearConfig() {
+        currentConfig = null
+        Log.d(TAG_YOUTUBE_PLAYER_CONFIG, "Config cleared")
+    }
+}

+ 5 - 3
module/youtube/src/main/java/com/adealink/weparty/youtube/data/Constants.kt

@@ -1,11 +1,13 @@
 package com.adealink.weparty.youtube.data
 
 const val TAG_YOUTUBE = "tag_youtube"
-const val TAG_YOUTUBE_BRIDGE = "tag_youtube_bridge"
-const val TAG_YOUTUBE_CONTROLS = "tag_youtube_controls"
+const val TAG_YOUTUBE_BRIDGE = "${TAG_YOUTUBE}_bridge"
+const val TAG_YOUTUBE_CONTROLS = "${TAG_YOUTUBE}_controls"
+const val TAG_YOUTUBE_CONFIG = "${TAG_YOUTUBE}_config"
+const val TAG_YOUTUBE_PLAYER_CONFIG = "${TAG_YOUTUBE}_player_config"
+
 const val MANUAL_ADD_VIDEO_DEFAULT_DURATION = 21600L
 const val TAG_ADD_YOUTUBE_VIDEO = "tag_add_youtube_video"
-const val YOUTUBE_NEED_LOGIN_ERROR = "150"
 const val YOUTUBE_LOGIN_HOST1= "myaccount.google.com"
 const val YOUTUBE_LOGIN_HOST2= "accounts.google.com"
 const val YOUTUBE_LOGIN_COOKIE_URL= "https://accounts.google.com"

+ 54 - 1
module/youtube/src/main/java/com/adealink/weparty/youtube/data/Data.kt

@@ -8,4 +8,57 @@ import com.google.gson.annotations.SerializedName
 
 data class RoomYoutubeInfoReq(
     @SerializedName("roomId") val roomId: Long
-)
+)
+
+/**
+ * 播放器加载类型
+ */
+enum class PlayerLoadType(val type: String) {
+    /**
+     * 使用伪装官方origin方式加载
+     */
+    OFFICIAL_ORIGIN("OFFICIAL_ORIGIN"),
+
+    /**
+     * 加载assets里面的HTML文件方式
+     */
+    ASSETS_HTML("ASSETS_HTML"),
+
+    /**
+     * 自建网址方式加载
+     */
+    CUSTOM("CUSTOM");
+
+    companion object {
+        fun map(type: String): PlayerLoadType? {
+            return PlayerLoadType.entries.firstOrNull { it.type == type }
+        }
+    }
+}
+
+/**
+ * YouTube 播放器错误码枚举
+ * 对照官方 IFrame API 文档
+ */
+enum class YoutubePlayerError(val code: Int) {
+    /** 请求包含无效的参数值 */
+    INVALID_PARAMETER(2),
+
+    /** 请求的内容无法在 HTML5 播放器中播放,或者发生了与 HTML5 播放器有关的其他错误 */
+    HTML5_ERROR(5),
+
+    /** 找不到所请求的视频 */
+    VIDEO_NOT_FOUND(100),
+
+    /** 视频所有者不允许在嵌入式播放器中播放 */
+    EMBEDDING_NOT_ALLOWED(101),
+
+    /** 与 101 相同,只是另一种错误码 */
+    EMBEDDING_NOT_ALLOWED_ALT(150);
+
+    companion object {
+        fun map(code: Int): YoutubePlayerError? {
+            return entries.firstOrNull { it.code == code }
+        }
+    }
+}

+ 57 - 0
module/youtube/src/main/java/com/adealink/weparty/youtube/data/YoutubePlayerConfig.kt

@@ -0,0 +1,57 @@
+package com.adealink.weparty.youtube.data
+
+import com.google.gson.annotations.GsonNullable
+import com.google.gson.annotations.SerializedName
+
+/**
+ * YouTube播放器配置数据类
+ */
+data class YoutubePlayerConfig(
+    @SerializedName("loadType")
+    val loadType: String,
+
+    @GsonNullable
+    @SerializedName("url")
+    val url: String?,
+    
+    @SerializedName("country")
+    val country: Map<String, CountryConfig>,
+    
+    @SerializedName("user")
+    val user: List<UserConfig>
+)
+
+/**
+ * 国家配置
+ */
+data class CountryConfig(
+    @SerializedName("loadType")
+    val loadType: String,
+
+    @GsonNullable
+    @SerializedName("url")
+    val url: String?,
+)
+
+/**
+ * 用户配置
+ */
+data class UserConfig(
+    @SerializedName("uids")
+    val uids: List<Long>,
+    
+    @SerializedName("loadType")
+    val loadType: String,
+
+    @GsonNullable
+    @SerializedName("url")
+    val url: String?,
+)
+
+/**
+ * 解析后的配置结果
+ */
+data class ResolvedPlayerConfig(
+    val loadType: PlayerLoadType,
+    val url: String?
+)

+ 9 - 0
module/youtube/src/main/java/com/adealink/weparty/youtube/util/YoutubeUtil.kt

@@ -1,9 +1,18 @@
 package com.adealink.weparty.youtube.util
 
+import android.webkit.CookieManager
+import com.adealink.weparty.youtube.data.YOUTUBE_LOGIN_COOKIE_URL
+
 fun extractVideoIdFromYouTubeLink(link: String?): String? {
     link ?: return null
     val regex =
         Regex("(?:youtu\\.be\\/|youtube(?:-nocookie)?\\.com\\/(?:embed\\/|v\\/|shorts\\/|watch\\?v=|watch\\?.+&v=))([\\w-]{11})")
     val matchResult = regex.find(link)
     return matchResult?.groups?.get(1)?.value
+}
+
+fun isLoginYoutube(): Boolean {
+    val cookieManager = CookieManager.getInstance()
+    val cookies = cookieManager.getCookie(YOUTUBE_LOGIN_COOKIE_URL)
+    return cookies != null && cookies.contains("SID")
 }

+ 3 - 2
module/youtube/src/main/java/com/adealink/weparty/youtube/view/YoutubePlayControlsView.kt

@@ -21,9 +21,10 @@ import com.adealink.weparty.module.youtube.data.YoutubeVideoData
 import com.adealink.weparty.module.youtube.data.YoutubeVideoInfo
 import com.adealink.weparty.youtube.R
 import com.adealink.weparty.youtube.data.TAG_YOUTUBE_CONTROLS
-import com.adealink.weparty.youtube.data.YOUTUBE_NEED_LOGIN_ERROR
+import com.adealink.weparty.youtube.data.YoutubePlayerError
 import com.adealink.weparty.youtube.databinding.LayoutYoutubePlayerControlsBinding
 import com.adealink.weparty.youtube.datasource.YoutubeLocalService
+import com.adealink.weparty.youtube.util.isLoginYoutube
 import kotlin.math.abs
 
 /**
@@ -399,7 +400,7 @@ class YoutubePlayControlsView @JvmOverloads constructor(
         hideControlView()
         hideLoadingView()
         binding.selectVideoCl.show()
-        if (error == YOUTUBE_NEED_LOGIN_ERROR) {
+        if (error == YoutubePlayerError.EMBEDDING_NOT_ALLOWED_ALT.toString() && !isLoginYoutube()) {
             binding.guideLoginCl.root.show()
             binding.selectVideoTv.text = ""
         } else {

+ 105 - 26
module/youtube/src/main/java/com/adealink/weparty/youtube/view/YoutubePlayerWebView.kt

@@ -9,11 +9,18 @@ import android.os.Message
 import android.util.AttributeSet
 import android.view.MotionEvent
 import android.webkit.*
+import androidx.webkit.WebViewAssetLoader
+import com.adealink.frame.base.fastLazy
 import com.adealink.frame.log.Log
 import com.adealink.frame.util.ONE_SECOND
 import com.adealink.weparty.webview.BaseWebView
 import com.adealink.weparty.webview.CommonWebViewClient
 import com.adealink.weparty.webview.jsbridge.JSBridge
+import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.youtube.config.YoutubePlayerConfigManager
+import com.adealink.weparty.youtube.data.PlayerLoadType
+import com.adealink.weparty.youtube.data.TAG_YOUTUBE
+import com.adealink.weparty.youtube.util.isLoginYoutube
 
 /**
  * WebView implementation of YouTube Player based on IFrame Player API.
@@ -39,7 +46,6 @@ class YouTubePlayerWebView @JvmOverloads constructor(
     private var hasInitialized = false
     private var playerListener: IYoutubePlayerListener? = null
     private var defVideoThumb: Bitmap? = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565)
-    private var showErrorToast = true
 
     private val mainThreadHandler: Handler = object : Handler(Looper.getMainLooper()) {
         override fun handleMessage(msg: Message) {
@@ -59,23 +65,36 @@ class YouTubePlayerWebView @JvmOverloads constructor(
         }
     }
 
-    fun setShowErrorToast(showToast: Boolean) {
-        showErrorToast = showToast
+    /**
+     * 获取当前播放器加载类型
+     */
+    fun getCurrentLoadType(): PlayerLoadType {
+        return getCurrentPlayerConfig().loadType
     }
 
+    /**
+     * 重新初始化播放器(使用最新的配置)
+     */
+    fun reinitialize() {
+        hasInitialized = false
+        initializePlayer(getCurrentPlayerConfig().loadType)
+    }
+
+    /**
+     * 获取当前播放器配置
+     */
+    private fun getCurrentPlayerConfig() = YoutubePlayerConfigManager.resolveConfig(
+        userId = ProfileModule.getMyUserInfo()?.uid,
+        countryCode = ProfileModule.getMyUserInfo()?.country
+    )
+
     /**
      * If player not call back onReady for a time, then auto retry
      */
     private val tryReloadPlayerTask = Runnable {
         if (hasInitialized.not()) {
             Log.w(TAG, "player not initialize yet")
-            loadDataWithBaseURL(
-                playerOptions.getOrigin(),
-                videoHTMLBuilder(),
-                "text/html",
-                "utf-8",
-                null
-            )
+            initializePlayer(getCurrentPlayerConfig().loadType)
         }
     }
 
@@ -121,11 +140,35 @@ class YouTubePlayerWebView @JvmOverloads constructor(
         })
     }
 
+    private val assetLoader by fastLazy {
+        WebViewAssetLoader.Builder()
+            .addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(context))
+            .build()
+    }
+
     override fun initWebViewClient() {
         webViewClient = object : CommonWebViewClient(this) {
             override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
                 return false
             }
+
+            override fun shouldInterceptRequest(
+                view: WebView,
+                request: WebResourceRequest
+            ): WebResourceResponse? {
+                if (request.url.host == WebViewAssetLoader.DEFAULT_DOMAIN) {
+                    if (request.url.path == "/favicon.ico") {
+                        Log.d(TAG_YOUTUBE, "ignore load favicon.ico")
+                        return WebResourceResponse(
+                            "text/html",
+                            "UTF-8",
+                            ByteArray(0).inputStream()
+                        )
+                    }
+                    return assetLoader.shouldInterceptRequest(request.url)
+                }
+                return super.shouldInterceptRequest(view, request)
+            }
         }
     }
 
@@ -139,14 +182,21 @@ class YouTubePlayerWebView @JvmOverloads constructor(
         }
     }
 
-    fun initialize(options: YoutubePlayerOptions? = null, autoRetry: Boolean = true) {
+    fun initialize(
+        options: YoutubePlayerOptions? = null,
+        loadType: PlayerLoadType? = null,
+        autoRetry: Boolean = true
+    ) {
         if (options != null) {
             playerOptions = options
         }
-        loadDataWithBaseURL(
-            playerOptions.getOrigin(),
-            videoHTMLBuilder(), "text/html", "utf-8", null
-        )
+        
+        // 如果指定了loadType则使用指定的,否则通过配置管理器解析
+        val actualLoadType = loadType ?: getCurrentPlayerConfig().loadType
+        
+        Log.d(TAG, "Initialize with loadType: $actualLoadType, userId: ${ProfileModule.getMyUserInfo()?.uid}, country: ${ProfileModule.getMyUserInfo()?.country}")
+        
+        initializePlayer(actualLoadType)
         if (autoRetry) {
             mainThreadHandler.removeCallbacks(tryReloadPlayerTask)
             mainThreadHandler.postDelayed(tryReloadPlayerTask, 15 * ONE_SECOND)
@@ -207,23 +257,52 @@ class YouTubePlayerWebView @JvmOverloads constructor(
     private fun loadJSUrl(url: String) {
         Log.i(TAG, "loadJs: $url")
         if (hasInitialized.not()) {
-            loadDataWithBaseURL(
-                playerOptions.getOrigin(),
-                videoHTMLBuilder() ?: "",
-                "text/html",
-                "utf-8",
-                null
-            )
+            initializePlayer(getCurrentPlayerConfig().loadType)
             Log.w(TAG, "player not initialize yet")
-            if (showErrorToast) {
-//                Util.showToastFailed(context)
-            }
-//            reportVideoError("initialize_not")
             return
         }
         mainThreadHandler.post { loadUrl(url) }
     }
 
+    /**
+     * 根据指定的加载类型初始化播放器
+     */
+    private fun initializePlayer(loadType: PlayerLoadType) {
+        when (loadType) {
+            PlayerLoadType.OFFICIAL_ORIGIN -> loadWithOfficialOrigin()
+            PlayerLoadType.ASSETS_HTML -> loadWithAssetsHtml()
+            PlayerLoadType.CUSTOM -> loadWithCustom()
+        }
+    }
+
+    /**
+     * 使用伪装origin为官方域名方式加载播放器
+     */
+    private fun loadWithOfficialOrigin() {
+        loadDataWithBaseURL(
+            playerOptions.getOrigin(),
+            videoHTMLBuilder(), "text/html", "utf-8", null
+        )
+        Log.d(TAG_YOUTUBE, "isLogin:${isLoginYoutube()}")
+    }
+
+    /**
+     * 加载assets里面的HTML文件方式
+     */
+    private fun loadWithAssetsHtml() {
+        // 加载assets中的HTML文件
+        loadUrl("https://${WebViewAssetLoader.DEFAULT_DOMAIN}/assets/youtube_embed.html")
+    }
+
+    /**
+     * 自建网址方式加载
+     */
+    private fun loadWithCustom() {
+        val customUrl = getCurrentPlayerConfig().url ?: return
+        Log.d(TAG, "Loading custom URL: $url")
+        loadUrl(customUrl)
+    }
+
     @SuppressLint("ClickableViewAccessibility")
     override fun onTouchEvent(ev: MotionEvent): Boolean {
         return false