From a35403a5b92451f34427bd4da7023e4b5ca402df Mon Sep 17 00:00:00 2001 From: ngthanhvu Date: Wed, 22 Jul 2026 23:11:12 -0400 Subject: [PATCH] feat: implement caching for .ts files in HLS proxy middleware --- app/pages/xem/[slug].vue | 180 +++++++------------------------- server/middleware/proxy-m3u8.ts | 55 +++++++++- 2 files changed, 93 insertions(+), 142 deletions(-) diff --git a/app/pages/xem/[slug].vue b/app/pages/xem/[slug].vue index 7022e92..5c55820 100644 --- a/app/pages/xem/[slug].vue +++ b/app/pages/xem/[slug].vue @@ -23,12 +23,10 @@ const route = useRoute() const selectedServer = ref(Math.max(Number(route.query.server || 0), 0)) const selectedEpisode = ref(Math.max(Number(route.query.ep || 1) - 1, 0)) const hasStarted = ref(false) -const hasManualPlayerMode = ref(false) const isFavoriteMovie = ref(false) const actionMessage = ref('') const actionBusy = ref(false) const progressSeconds = ref(0) -const playerMode = ref<'embed' | 'hls'>('embed') const playerViewportRef = ref(null) const playerShellRef = ref(null) const videoRef = ref(null) @@ -49,6 +47,7 @@ const edgeProgressVisible = ref(false) const gestureHint = ref('') const isHoldSpeedActive = ref(false) const pendingResumeSeconds = ref(0) +const descriptionExpanded = ref(false) let controlsHideTimer: ReturnType | undefined let edgeProgressTimer: ReturnType | undefined let singleTapTimer: ReturnType | undefined @@ -89,11 +88,7 @@ const { data: movie, pending, error } = useFetch(() => `/api/movies/${route.para const servers = computed(() => movie.value?.servers ?? []) const activeServer = computed(() => servers.value[selectedServer.value] ?? servers.value[0]) const activeEpisode = computed(() => activeServer.value?.episodes?.[selectedEpisode.value] ?? activeServer.value?.episodes?.[0]) -const embedPlayerUrl = computed(() => activeEpisode.value?.linkEmbed || '') const hlsPlayerUrl = computed(() => activeEpisode.value?.linkM3u8 || '') -const playerUrl = computed(() => playerMode.value === 'hls' ? hlsPlayerUrl.value : (embedPlayerUrl.value || hlsPlayerUrl.value)) -const canUseEmbed = computed(() => Boolean(embedPlayerUrl.value)) -const canUseHls = computed(() => Boolean(hlsPlayerUrl.value)) const actorSummary = computed(() => (movie.value?.actors ?? []).map((actor: any) => actor.name).filter(Boolean).slice(0, 6).join(', ')) const durationSeconds = computed(() => Math.floor(videoDuration.value || parseDurationSeconds(movie.value?.time || ''))) const hasNextEpisode = computed(() => Boolean(activeServer.value?.episodes?.[selectedEpisode.value + 1])) @@ -166,11 +161,8 @@ async function selectServer(index: number) { const nextEpisodeIndex = episodeIndexForServer(index) selectedServer.value = index selectedEpisode.value = nextEpisodeIndex - hasStarted.value = false - hasManualPlayerMode.value = false resetProgressTimer() destroyHlsPlayer() - choosePreferredPlayerMode(true) await navigateTo({ path: `/xem/${route.params.slug}`, query: { @@ -190,30 +182,10 @@ async function startPlayer() { await loadResumeProgress() startProgressTimer() saveWatchHistory() - - if (playerMode.value === 'hls') { - nextTick(() => setupHlsPlayer(true)) - } - + nextTick(() => setupHlsPlayer(true)) nextTick(() => handleOrientationFullscreen()) } -function selectPlayerMode(mode: 'embed' | 'hls') { - if (mode === playerMode.value) return - - hasManualPlayerMode.value = true - playerMode.value = mode - isSettingsOpen.value = false - hasStarted.value = false - resetProgressTimer() - destroyHlsPlayer() -} - -function choosePreferredPlayerMode(force = false) { - if (!force && hasManualPlayerMode.value) return - playerMode.value = canUseHls.value ? 'hls' : 'embed' -} - function parseDurationSeconds(value: string) { const normalizedValue = value.toLowerCase() const hourMatch = normalizedValue.match(/(\d+)\s*(?:h|giờ|gio)/) @@ -247,7 +219,7 @@ function stopProgressTimer() { function tickProgress() { if (!hasStarted.value || (import.meta.client && document.visibilityState === 'hidden')) return - if (playerMode.value === 'hls' && videoRef.value) { + if (videoRef.value) { progressSeconds.value = Math.floor(videoRef.value.currentTime || 0) videoCurrentTime.value = progressSeconds.value } else { @@ -275,7 +247,7 @@ function handleVisibilityChange() { } function handleKeyboardShortcut(event: KeyboardEvent) { - if (playerMode.value !== 'hls' || !hasStarted.value || !videoRef.value) return + if (!hasStarted.value || !videoRef.value) return if (['INPUT', 'TEXTAREA', 'SELECT'].includes((event.target as HTMLElement)?.tagName)) return const key = event.key.toLowerCase() @@ -326,29 +298,13 @@ function clearHlsFallbackTimer() { hlsFallbackTimer = undefined } -function scheduleHlsFallback(message = 'HLS phản hồi chậm, đang chuyển sang chế độ Nhúng...') { - if (!canUseEmbed.value) return +function scheduleHlsFallback(message = 'HLS phản hồi chậm, vui lòng thử lại...') { clearHlsFallbackTimer() hlsFallbackTimer = setTimeout(() => { - fallbackToEmbed(message) - }, 12000) -} - -function fallbackToEmbed(message = 'HLS đang lỗi, đã chuyển sang chế độ Nhúng.') { - clearHlsFallbackTimer() - if (!canUseEmbed.value) { - hlsErrorMessage.value = 'HLS đang lỗi và phim này chưa có link Nhúng ở server này.' + hlsErrorMessage.value = message isVideoBuffering.value = false - return - } - - hlsErrorMessage.value = '' - isVideoBuffering.value = false - isVideoPlaying.value = false - isSettingsOpen.value = false - playerMode.value = 'embed' - destroyHlsPlayer() - flashActionMessage(message) + isVideoPlaying.value = false + }, 12000) } function flashGestureHint(message: string) { @@ -373,7 +329,7 @@ function formatPlayerTime(seconds = 0) { async function setupHlsPlayer(autoplay = false, tryProxy = true) { console.log('[HLS] Setup called', { tryProxy, hasVideoRef: !!videoRef.value, hlsUrl: hlsPlayerUrl.value }) - + if (!import.meta.client || !videoRef.value || !hlsPlayerUrl.value) { console.log('[HLS] Early return - missing requirements') return @@ -416,7 +372,8 @@ async function setupHlsPlayer(autoplay = false, tryProxy = true) { if (!HlsPlayer.isSupported()) { console.log('[HLS] hls.js not supported') - fallbackToEmbed('Trình duyệt chưa hỗ trợ HLS, đã chuyển sang chế độ Nhúng.') + hlsErrorMessage.value = 'Trình duyệt chưa hỗ trợ HLS.' + isVideoBuffering.value = false return } @@ -427,7 +384,7 @@ async function setupHlsPlayer(autoplay = false, tryProxy = true) { xhr.withCredentials = false }, }) - + hlsPlayer.on(HlsPlayer.Events.MANIFEST_PARSED, () => { console.log('[HLS] Manifest parsed successfully') clearHlsFallbackTimer() @@ -440,7 +397,7 @@ async function setupHlsPlayer(autoplay = false, tryProxy = true) { selectedQuality.value = -1 applyResumeProgress() }) - + hlsPlayer.on(HlsPlayer.Events.ERROR, (_event, data) => { console.log('[HLS] Error:', data.type, data.details, data.fatal) if (!data.fatal) return @@ -460,8 +417,10 @@ async function setupHlsPlayer(autoplay = false, tryProxy = true) { hlsFatalRetryCount += 1 if (hlsFatalRetryCount >= 2) { - console.log('[HLS] Too many errors, falling back to embed') - fallbackToEmbed('HLS tải không ổn định, đã chuyển sang chế độ Nhúng.') + console.log('[HLS] Too many errors, showing error message') + hlsErrorMessage.value = 'HLS tải không ổn định.' + isVideoBuffering.value = false + isVideoPlaying.value = false return } @@ -482,7 +441,9 @@ async function setupHlsPlayer(autoplay = false, tryProxy = true) { scheduleHlsFallback() } catch (error) { console.error('[HLS] Setup error:', error) - fallbackToEmbed('Lỗi khởi tạo HLS player, đã chuyển sang chế độ Nhúng.') + hlsErrorMessage.value = 'Lỗi khởi tạo HLS player.' + isVideoBuffering.value = false + isVideoPlaying.value = false return } } @@ -492,7 +453,7 @@ async function setupHlsPlayer(autoplay = false, tryProxy = true) { await video.play() } catch { isVideoPlaying.value = false - fallbackToEmbed('HLS chưa phát được, đã chuyển sang chế độ Nhúng.') + hlsErrorMessage.value = 'HLS chưa phát được.' } } } @@ -547,7 +508,9 @@ function handleVideoPlaying() { } function handleVideoError() { - fallbackToEmbed('HLS phát lỗi, đã chuyển sang chế độ Nhúng.') + hlsErrorMessage.value = 'HLS phát lỗi.' + isVideoBuffering.value = false + isVideoPlaying.value = false } async function loadResumeProgress() { @@ -1006,22 +969,12 @@ watch(() => route.query, () => { selectedServer.value = Math.max(Number(route.query.server || 0), 0) selectedEpisode.value = Math.max(Number(route.query.ep || 1) - 1, 0) hasStarted.value = false - hasManualPlayerMode.value = false resetProgressTimer() destroyHlsPlayer() - choosePreferredPlayerMode(true) }) watch([movie, activeEpisode], () => { if (hasStarted.value) saveWatchHistory() - hasManualPlayerMode.value = false - choosePreferredPlayerMode(true) -}) - -watch([canUseEmbed, canUseHls], () => { - choosePreferredPlayerMode() -}, { - immediate: true, }) watch(libraryItem, () => { @@ -1063,12 +1016,6 @@ useHead(() => ({
-
-
-
-
-
-