diff --git a/src/views/Activities/DailyRecharge/composables/useDailyRechargeRewardVap.js b/src/views/Activities/DailyRecharge/composables/useDailyRechargeRewardVap.js new file mode 100644 index 0000000..0d79ad1 --- /dev/null +++ b/src/views/Activities/DailyRecharge/composables/useDailyRechargeRewardVap.js @@ -0,0 +1,312 @@ +import { ref } from 'vue' +import { clearVapConfigCache, preloadVapConfig } from '@/utils/vapMp4Config.js' +import { resolveProtectedAssetUrl } from '@/utils/protectedAssets.js' + +const REWARD_VAP_MAX_RETRY_COUNT = 3 + +const isEmptySourceValue = (value) => { + if (typeof value !== 'string' && typeof value !== 'number') { + return true + } + + return ['', 'null', 'undefined'].includes(String(value || '').trim().toLowerCase()) +} + +const getRewardRawSourceUrl = (reward = {}) => { + const sourceCandidates = [ + reward.sourceUrl, + reward.mp4Url, + reward.vapUrl, + reward.animationUrl, + reward.resourceUrl, + reward.dynamicUrl, + reward.url, + reward?.source?.url, + reward?.resource?.url, + reward?.extValues?.sourceUrl, + reward?.extValues?.mp4Url, + reward?.extValues?.vapUrl, + ] + + return sourceCandidates.find((value) => !isEmptySourceValue(value)) || '' +} + +export function useDailyRechargeRewardVap(options = {}) { + const { isAndroidDevice, getCurrentMainRewardVapSourceUrl = () => '' } = options + + const rewardVapPreloadElements = new Map() + const criticalVapPreloadElements = new Map() + const rewardVapRetryTimers = new Map() + const failedRewardVapUrls = ref(new Set()) + const rewardVapRetryCounts = ref({}) + const rewardVapRetryVersions = ref({}) + let rewardVapPreloadTimer = null + let rewardVapPreloadIdleCallbackId = null + let rewardVapPreloadStarted = false + + const normalizeVapSourceUrl = (sourceUrl = '') => { + const resolvedUrl = String(resolveProtectedAssetUrl(sourceUrl || '') || '').trim() + if (!resolvedUrl) { + return '' + } + + if (resolvedUrl.startsWith('//')) { + const protocol = + typeof window !== 'undefined' && window.location?.protocol ? window.location.protocol : 'https:' + return `${protocol}${resolvedUrl}` + } + + if (/^http:\/\//i.test(resolvedUrl)) { + return resolvedUrl.replace(/^http:\/\//i, 'https://') + } + + if (/^https?:\/\//i.test(resolvedUrl)) { + return resolvedUrl + } + + if (resolvedUrl.startsWith('/oss/')) { + return resolvedUrl + } + + if (resolvedUrl.startsWith('/')) { + return process.env.NODE_ENV === 'development' + ? `/oss${resolvedUrl}` + : `https://tkm-likei.oss-ap-southeast-1.aliyuncs.com${resolvedUrl}` + } + + return `https://${resolvedUrl}` + } + + const isValidVapSourceUrl = (sourceUrl = '') => { + if (!sourceUrl) { + return false + } + + return /^https?:\/\/.+\.mp4(?:[?#].*)?$/i.test(sourceUrl) || /^\/oss\/.+\.mp4(?:[?#].*)?$/i.test(sourceUrl) + } + + const getRewardVapSourceUrl = (reward = {}) => { + const sourceUrl = normalizeVapSourceUrl(getRewardRawSourceUrl(reward)) + return isValidVapSourceUrl(sourceUrl) ? sourceUrl : '' + } + + const getRewardVapUrls = (rewardTopList) => { + return (rewardTopList || []) + .flatMap((rewardTop) => rewardTop?.propsGroup?.activityRewardProps || []) + .map((reward) => getRewardVapSourceUrl(reward)) + .filter(Boolean) + } + + const getRewardVapRetryVersion = (sourceUrl) => { + return sourceUrl ? rewardVapRetryVersions.value[sourceUrl] || 0 : 0 + } + + const preloadRewardVap = (sourceUrl) => { + if (!isAndroidDevice?.value || !sourceUrl || rewardVapPreloadElements.has(sourceUrl)) { + return + } + + preloadVapConfig(sourceUrl) + + if (typeof document === 'undefined') { + return + } + + const preloadVideo = document.createElement('video') + preloadVideo.preload = 'auto' + preloadVideo.muted = true + preloadVideo.defaultMuted = true + preloadVideo.playsInline = true + preloadVideo.setAttribute('playsinline', '') + preloadVideo.setAttribute('webkit-playsinline', '') + preloadVideo.setAttribute('x5-playsinline', '') + preloadVideo.src = sourceUrl + preloadVideo.style.position = 'fixed' + preloadVideo.style.left = '-9999px' + preloadVideo.style.top = '0' + preloadVideo.style.width = '1px' + preloadVideo.style.height = '1px' + preloadVideo.style.opacity = '0' + preloadVideo.style.pointerEvents = 'none' + document.body.appendChild(preloadVideo) + preloadVideo.load() + + rewardVapPreloadElements.set(sourceUrl, preloadVideo) + } + + const preloadRewardVaps = (rewardTopList) => { + getRewardVapUrls(rewardTopList).forEach(preloadRewardVap) + } + + const preloadCriticalVap = (sourceUrl) => { + if (!isAndroidDevice?.value || !sourceUrl) { + return Promise.resolve() + } + + const resolvedSourceUrl = resolveProtectedAssetUrl(sourceUrl) + const configPromise = preloadVapConfig(resolvedSourceUrl) + + if (criticalVapPreloadElements.has(resolvedSourceUrl) || typeof document === 'undefined') { + return configPromise + } + + const preloadVideo = document.createElement('video') + preloadVideo.preload = 'auto' + preloadVideo.muted = true + preloadVideo.playsInline = true + preloadVideo.src = resolvedSourceUrl + preloadVideo.style.position = 'fixed' + preloadVideo.style.left = '-9999px' + preloadVideo.style.top = '-9999px' + preloadVideo.style.width = '1px' + preloadVideo.style.height = '1px' + preloadVideo.style.opacity = '0' + preloadVideo.style.pointerEvents = 'none' + document.body.appendChild(preloadVideo) + preloadVideo.load() + criticalVapPreloadElements.set(resolvedSourceUrl, preloadVideo) + + return configPromise + } + + const startRewardVapPreload = (rewardTopList) => { + if (!isAndroidDevice?.value || rewardVapPreloadStarted || !rewardTopList?.length) { + return + } + + rewardVapPreloadStarted = true + preloadRewardVaps(rewardTopList) + } + + const queueRewardVapPreload = (rewardTopList) => { + if (!isAndroidDevice?.value || rewardVapPreloadStarted || typeof window === 'undefined') { + return + } + + if (typeof window.requestIdleCallback === 'function') { + rewardVapPreloadIdleCallbackId = window.requestIdleCallback(() => { + rewardVapPreloadIdleCallbackId = null + startRewardVapPreload(rewardTopList) + }) + return + } + + rewardVapPreloadTimer = window.setTimeout(() => { + rewardVapPreloadTimer = null + startRewardVapPreload(rewardTopList) + }, 0) + } + + const clearRewardVapRetryTimer = (sourceUrl) => { + const retryTimer = rewardVapRetryTimers.get(sourceUrl) + if (retryTimer && typeof window !== 'undefined') { + window.clearTimeout(retryTimer) + } + rewardVapRetryTimers.delete(sourceUrl) + } + + const clearRewardVapRetryState = () => { + rewardVapRetryTimers.forEach((retryTimer) => { + if (typeof window !== 'undefined') { + window.clearTimeout(retryTimer) + } + }) + rewardVapRetryTimers.clear() + rewardVapRetryCounts.value = {} + rewardVapRetryVersions.value = {} + } + + const scheduleRewardVapRetry = (sourceUrl, retryCount) => { + if (typeof window === 'undefined') { + return + } + + clearRewardVapRetryTimer(sourceUrl) + clearVapConfigCache(sourceUrl) + + const retryDelay = Math.min(500 + retryCount * 500, 2000) + const retryTimer = window.setTimeout(() => { + rewardVapRetryTimers.delete(sourceUrl) + if (getCurrentMainRewardVapSourceUrl() !== sourceUrl || failedRewardVapUrls.value.has(sourceUrl)) { + return + } + + preloadVapConfig(sourceUrl) + rewardVapRetryVersions.value = { + ...rewardVapRetryVersions.value, + [sourceUrl]: (rewardVapRetryVersions.value[sourceUrl] || 0) + 1, + } + }, retryDelay) + + rewardVapRetryTimers.set(sourceUrl, retryTimer) + } + + const handleRewardVapError = (error, sourceUrl = '') => { + console.error('Reward VAP playback failed:', error) + const normalizedSourceUrl = normalizeVapSourceUrl(sourceUrl) + if (!normalizedSourceUrl) { + return + } + + const retryCount = (rewardVapRetryCounts.value[normalizedSourceUrl] || 0) + 1 + rewardVapRetryCounts.value = { + ...rewardVapRetryCounts.value, + [normalizedSourceUrl]: retryCount, + } + + if (retryCount <= REWARD_VAP_MAX_RETRY_COUNT) { + scheduleRewardVapRetry(normalizedSourceUrl, retryCount) + return + } + + clearRewardVapRetryTimer(normalizedSourceUrl) + failedRewardVapUrls.value = new Set([...failedRewardVapUrls.value, normalizedSourceUrl]) + } + + const resetRewardVapState = () => { + failedRewardVapUrls.value = new Set() + clearRewardVapRetryState() + rewardVapPreloadStarted = false + } + + const cleanupRewardVap = () => { + if (rewardVapPreloadTimer !== null && typeof window !== 'undefined') { + window.clearTimeout(rewardVapPreloadTimer) + rewardVapPreloadTimer = null + } + if ( + rewardVapPreloadIdleCallbackId !== null && + typeof window !== 'undefined' && + typeof window.cancelIdleCallback === 'function' + ) { + window.cancelIdleCallback(rewardVapPreloadIdleCallbackId) + rewardVapPreloadIdleCallbackId = null + } + clearRewardVapRetryState() + rewardVapPreloadElements.forEach((video) => { + video.pause() + video.removeAttribute('src') + video.load() + video.parentNode && video.parentNode.removeChild(video) + }) + rewardVapPreloadElements.clear() + criticalVapPreloadElements.forEach((video) => { + video.pause() + video.removeAttribute('src') + video.load() + video.parentNode && video.parentNode.removeChild(video) + }) + criticalVapPreloadElements.clear() + } + + return { + failedRewardVapUrls, + getRewardVapRetryVersion, + getRewardVapSourceUrl, + handleRewardVapError, + preloadCriticalVap, + queueRewardVapPreload, + resetRewardVapState, + cleanupRewardVap, + } +} diff --git a/src/views/Activities/DailyRecharge/index.vue b/src/views/Activities/DailyRecharge/index.vue index a71c812..d0f8bbc 100644 --- a/src/views/Activities/DailyRecharge/index.vue +++ b/src/views/Activities/DailyRecharge/index.vue @@ -340,6 +340,7 @@ justify-content: flex-start; > { const mainReward = computed(() => { return selectedRewardList.value[0] || null }) +const { + failedRewardVapUrls, + getRewardVapRetryVersion, + getRewardVapSourceUrl, + handleRewardVapError, + preloadCriticalVap, + queueRewardVapPreload, + resetRewardVapState, + cleanupRewardVap, +} = useDailyRechargeRewardVap({ + isAndroidDevice, + getCurrentMainRewardVapSourceUrl: () => mainRewardVapSourceUrl.value, +}) const mainRewardVapSourceUrl = computed(() => { return getRewardVapSourceUrl(mainReward.value) }) @@ -797,12 +810,12 @@ const rewardGridList = computed(() => { const mainRewardRenderKey = computed(() => { return mainReward.value ? getRewardRenderKey(mainReward.value, 0) : 'empty-main-reward' }) -const rewardVapPreloadElements = new Map() -const criticalVapPreloadElements = new Map() -const failedRewardVapUrls = ref(new Set()) -let rewardVapPreloadTimer = null -let rewardVapPreloadIdleCallbackId = null -let rewardVapPreloadStarted = false +const mainRewardVapRetryVersion = computed(() => { + return getRewardVapRetryVersion(mainRewardVapSourceUrl.value) +}) +const mainRewardVapRenderKey = computed(() => { + return `${mainRewardRenderKey.value}-vap-${mainRewardVapRetryVersion.value}` +}) let pageInitializationStarted = false const isLocalRewardAsset = (reward) => { @@ -813,79 +826,6 @@ const getRewardDisplayText = (reward) => { return formatRewardDisplay(reward.type, reward) } -const isEmptySourceValue = (value) => { - if (typeof value !== 'string' && typeof value !== 'number') { - return true - } - - return ['', 'null', 'undefined'].includes(String(value || '').trim().toLowerCase()) -} - -const getRewardRawSourceUrl = (reward = {}) => { - const sourceCandidates = [ - reward.sourceUrl, - reward.mp4Url, - reward.vapUrl, - reward.animationUrl, - reward.resourceUrl, - reward.dynamicUrl, - reward.url, - reward?.source?.url, - reward?.resource?.url, - reward?.extValues?.sourceUrl, - reward?.extValues?.mp4Url, - reward?.extValues?.vapUrl, - ] - - return sourceCandidates.find((value) => !isEmptySourceValue(value)) || '' -} - -const normalizeVapSourceUrl = (sourceUrl = '') => { - const resolvedUrl = String(resolveProtectedAssetUrl(sourceUrl || '') || '').trim() - if (!resolvedUrl) { - return '' - } - - if (resolvedUrl.startsWith('//')) { - const protocol = - typeof window !== 'undefined' && window.location?.protocol ? window.location.protocol : 'https:' - return `${protocol}${resolvedUrl}` - } - - if (/^http:\/\//i.test(resolvedUrl)) { - return resolvedUrl.replace(/^http:\/\//i, 'https://') - } - - if (/^https?:\/\//i.test(resolvedUrl)) { - return resolvedUrl - } - - if (resolvedUrl.startsWith('/oss/')) { - return resolvedUrl - } - - if (resolvedUrl.startsWith('/')) { - return process.env.NODE_ENV === 'development' - ? `/oss${resolvedUrl}` - : `https://tkm-likei.oss-ap-southeast-1.aliyuncs.com${resolvedUrl}` - } - - return `https://${resolvedUrl}` -} - -const isValidVapSourceUrl = (sourceUrl = '') => { - if (!sourceUrl) { - return false - } - - return /^https?:\/\/.+\.mp4(?:[?#].*)?$/i.test(sourceUrl) || /^\/oss\/.+\.mp4(?:[?#].*)?$/i.test(sourceUrl) -} - -const getRewardVapSourceUrl = (reward = {}) => { - const sourceUrl = normalizeVapSourceUrl(getRewardRawSourceUrl(reward)) - return isValidVapSourceUrl(sourceUrl) ? sourceUrl : '' -} - const getRewardRenderKey = (reward, index) => { return [ selectedRewardLevel.value, @@ -897,109 +837,6 @@ const getRewardRenderKey = (reward, index) => { ].join('-') } -const getRewardVapUrls = (rewardTopList) => { - return (rewardTopList || []) - .flatMap((rewardTop) => rewardTop?.propsGroup?.activityRewardProps || []) - .map((reward) => getRewardVapSourceUrl(reward)) - .filter(Boolean) -} - -const preloadRewardVap = (sourceUrl) => { - if (!isAndroidDevice.value || !sourceUrl || rewardVapPreloadElements.has(sourceUrl)) { - return - } - - preloadVapConfig(sourceUrl) - - if (typeof document === 'undefined') { - return - } - - const preloadVideo = document.createElement('video') - preloadVideo.preload = 'auto' - preloadVideo.muted = true - preloadVideo.defaultMuted = true - preloadVideo.playsInline = true - preloadVideo.setAttribute('playsinline', '') - preloadVideo.setAttribute('webkit-playsinline', '') - preloadVideo.setAttribute('x5-playsinline', '') - preloadVideo.src = sourceUrl - preloadVideo.style.position = 'fixed' - preloadVideo.style.left = '-9999px' - preloadVideo.style.top = '0' - preloadVideo.style.width = '1px' - preloadVideo.style.height = '1px' - preloadVideo.style.opacity = '0' - preloadVideo.style.pointerEvents = 'none' - document.body.appendChild(preloadVideo) - preloadVideo.load() - - rewardVapPreloadElements.set(sourceUrl, preloadVideo) -} - -const preloadRewardVaps = (rewardTopList) => { - getRewardVapUrls(rewardTopList).forEach(preloadRewardVap) -} - -const preloadCriticalVap = (sourceUrl) => { - if (!isAndroidDevice.value || !sourceUrl) { - return Promise.resolve() - } - - const resolvedSourceUrl = resolveProtectedAssetUrl(sourceUrl) - const configPromise = preloadVapConfig(resolvedSourceUrl) - - if (criticalVapPreloadElements.has(resolvedSourceUrl) || typeof document === 'undefined') { - return configPromise - } - - const preloadVideo = document.createElement('video') - preloadVideo.preload = 'auto' - preloadVideo.muted = true - preloadVideo.playsInline = true - preloadVideo.src = resolvedSourceUrl - preloadVideo.style.position = 'fixed' - preloadVideo.style.left = '-9999px' - preloadVideo.style.top = '-9999px' - preloadVideo.style.width = '1px' - preloadVideo.style.height = '1px' - preloadVideo.style.opacity = '0' - preloadVideo.style.pointerEvents = 'none' - document.body.appendChild(preloadVideo) - preloadVideo.load() - criticalVapPreloadElements.set(resolvedSourceUrl, preloadVideo) - - return configPromise -} - -const startRewardVapPreload = () => { - if (!isAndroidDevice.value || rewardVapPreloadStarted || !activityRewards.value.length) { - return - } - - rewardVapPreloadStarted = true - preloadRewardVaps(activityRewards.value) -} - -const queueRewardVapPreload = () => { - if (!isAndroidDevice.value || rewardVapPreloadStarted || typeof window === 'undefined') { - return - } - - if (typeof window.requestIdleCallback === 'function') { - rewardVapPreloadIdleCallbackId = window.requestIdleCallback(() => { - rewardVapPreloadIdleCallbackId = null - startRewardVapPreload() - }) - return - } - - rewardVapPreloadTimer = window.setTimeout(() => { - rewardVapPreloadTimer = null - startRewardVapPreload() - }, 0) -} - const getExtraRewardsByLevel = (level) => { switch (Number(level)) { case 8: @@ -1097,14 +934,6 @@ const openPopup = (type) => { } } -const handleRewardVapError = (error, sourceUrl = '') => { - console.error('Reward VAP playback failed:', error) - const normalizedSourceUrl = normalizeVapSourceUrl(sourceUrl) - if (normalizedSourceUrl) { - failedRewardVapUrls.value = new Set([...failedRewardVapUrls.value, normalizedSourceUrl]) - } -} - const handleMainRewardPreviewClick = () => { if (!mainRewardVapSourceUrl.value) { return @@ -1174,8 +1003,7 @@ const getActivityRewards = async () => { const res = await apiGetActivityResource('DAILY_RECHARGE_TASK2') if (res.status && res.body) { activityRewards.value = Array.isArray(res.body) ? appendExtraRewards(res.body) : [] - failedRewardVapUrls.value = new Set() - rewardVapPreloadStarted = false + resetRewardVapState() return true } @@ -1261,7 +1089,7 @@ const completePreloading = async () => { console.error('棰勫姞杞借繃绋嬩腑鍙戠敓閿欒:', error) } finally { isLoading.value = false - queueRewardVapPreload() + queueRewardVapPreload(activityRewards.value) } } @@ -1297,32 +1125,7 @@ onBeforeUnmount(() => { delete window.__setDailyRechargeDevice delete window.__dailyRechargeDeviceMode } - if (rewardVapPreloadTimer !== null && typeof window !== 'undefined') { - window.clearTimeout(rewardVapPreloadTimer) - rewardVapPreloadTimer = null - } - if ( - rewardVapPreloadIdleCallbackId !== null && - typeof window !== 'undefined' && - typeof window.cancelIdleCallback === 'function' - ) { - window.cancelIdleCallback(rewardVapPreloadIdleCallbackId) - rewardVapPreloadIdleCallbackId = null - } - rewardVapPreloadElements.forEach((video) => { - video.pause() - video.removeAttribute('src') - video.load() - video.parentNode && video.parentNode.removeChild(video) - }) - rewardVapPreloadElements.clear() - criticalVapPreloadElements.forEach((video) => { - video.pause() - video.removeAttribute('src') - video.load() - video.parentNode && video.parentNode.removeChild(video) - }) - criticalVapPreloadElements.clear() + cleanupRewardVap() })