From a16c1d484612fa25d9e7fddb0781daa179afe69e Mon Sep 17 00:00:00 2001 From: thanhvudaynee Date: Thu, 28 May 2026 16:00:21 +0700 Subject: [PATCH] feat: implement movie playback page with HLS and embedded player support --- app/pages/xem/[slug].vue | 137 ++++++++++++++++++++++++++++++--------- 1 file changed, 106 insertions(+), 31 deletions(-) diff --git a/app/pages/xem/[slug].vue b/app/pages/xem/[slug].vue index 5e4bc9e..bd0ecad 100644 --- a/app/pages/xem/[slug].vue +++ b/app/pages/xem/[slug].vue @@ -29,6 +29,7 @@ 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) const isVideoPlaying = ref(false) @@ -61,6 +62,7 @@ let lastTapX = 0 let holdRestoreRate = 1 let ignoreNextTap = false let hlsFatalRetryCount = 0 +let autoOrientationFullscreen = false let hlsPlayer: Hls | undefined let progressTimer: ReturnType | undefined let lastSavedProgress = 0 @@ -187,6 +189,8 @@ async function startPlayer() { if (playerMode.value === 'hls') { nextTick(() => setupHlsPlayer(true)) } + + nextTick(() => handleOrientationFullscreen()) } function selectPlayerMode(mode: 'embed' | 'hls') { @@ -580,52 +584,117 @@ function changeQuality(event: Event) { showControlsTemporarily() } -async function toggleFullscreen() { - if (!import.meta.client || !videoRef.value) return - - const video = videoRef.value as HTMLVideoElement & { - webkitEnterFullscreen?: () => void - webkitRequestFullscreen?: () => Promise | void - } - const target = (playerShellRef.value || video.parentElement || video) as HTMLElement & { - webkitRequestFullscreen?: () => Promise | void - } +function getDocumentFullscreenElement() { + if (!import.meta.client) return null const documentWithWebkit = document as Document & { webkitFullscreenElement?: Element | null + } + + return document.fullscreenElement || documentWithWebkit.webkitFullscreenElement || null +} + +async function enterPlayerFullscreen() { + if (!import.meta.client) return false + + const video = videoRef.value as (HTMLVideoElement & { + webkitEnterFullscreen?: () => void + webkitRequestFullscreen?: () => Promise | void + }) | null + const target = (playerShellRef.value || playerViewportRef.value || video?.parentElement || video) as (HTMLElement & { + webkitRequestFullscreen?: () => Promise | void + }) | null + + if (!target && !video) return false + + try { + if (target?.requestFullscreen) { + await target.requestFullscreen() + return true + } + + if (target?.webkitRequestFullscreen) { + await target.webkitRequestFullscreen() + return true + } + + if (video?.webkitEnterFullscreen) { + video.webkitEnterFullscreen() + return true + } + } catch { + try { + video?.webkitEnterFullscreen?.() + return Boolean(video) + } catch { + return false + } + } + + return false +} + +async function exitPlayerFullscreen() { + const documentWithWebkit = document as Document & { webkitExitFullscreen?: () => Promise | void } + if (document.exitFullscreen) await document.exitFullscreen() + else await documentWithWebkit.webkitExitFullscreen?.() +} + +async function toggleFullscreen() { + if (!import.meta.client) return + try { - if (document.fullscreenElement || documentWithWebkit.webkitFullscreenElement) { - if (document.exitFullscreen) await document.exitFullscreen() - else await documentWithWebkit.webkitExitFullscreen?.() + if (getDocumentFullscreenElement()) { + await exitPlayerFullscreen() return } - if (target.requestFullscreen) { - await target.requestFullscreen() - return - } - - if (target.webkitRequestFullscreen) { - await target.webkitRequestFullscreen() - return - } - - video.webkitEnterFullscreen?.() + await enterPlayerFullscreen() } catch { - video.webkitEnterFullscreen?.() + await enterPlayerFullscreen() } } function updateFullscreenState() { if (!import.meta.client) return - const documentWithWebkit = document as Document & { - webkitFullscreenElement?: Element | null - } - const fullscreenElement = document.fullscreenElement || documentWithWebkit.webkitFullscreenElement - isHlsFullscreen.value = Boolean(fullscreenElement && playerShellRef.value?.contains(fullscreenElement)) + const fullscreenElement = getDocumentFullscreenElement() + const playerElement = playerShellRef.value || playerViewportRef.value + isHlsFullscreen.value = Boolean( + fullscreenElement + && playerElement + && (playerElement.contains(fullscreenElement) || fullscreenElement.contains(playerElement)), + ) if (!isHlsFullscreen.value) edgeProgressVisible.value = false + if (!fullscreenElement) autoOrientationFullscreen = false +} + +function isMobileLandscapeViewport() { + if (!import.meta.client) return false + const isCoarsePointer = window.matchMedia?.('(pointer: coarse)').matches ?? false + const isSmallScreen = window.matchMedia?.('(max-width: 940px)').matches ?? window.innerWidth <= 940 + const isLandscape = window.matchMedia?.('(orientation: landscape)').matches ?? window.innerWidth > window.innerHeight + + return isCoarsePointer && isSmallScreen && isLandscape +} + +async function handleOrientationFullscreen() { + if (!import.meta.client || !hasStarted.value) return + + const fullscreenElement = getDocumentFullscreenElement() + if (!isMobileLandscapeViewport()) { + if (fullscreenElement && autoOrientationFullscreen) await exitPlayerFullscreen() + return + } + + if (fullscreenElement) return + + autoOrientationFullscreen = await enterPlayerFullscreen() + if (autoOrientationFullscreen) { + controlsVisible.value = false + showFullscreenProgressOnly() + } } async function togglePictureInPicture() { @@ -858,6 +927,9 @@ onMounted(async () => { document.addEventListener('keydown', handleKeyboardShortcut) document.addEventListener('fullscreenchange', updateFullscreenState) document.addEventListener('webkitfullscreenchange', updateFullscreenState) + window.addEventListener('orientationchange', handleOrientationFullscreen) + window.addEventListener('resize', handleOrientationFullscreen) + screen.orientation?.addEventListener?.('change', handleOrientationFullscreen) }) onBeforeUnmount(() => { @@ -873,6 +945,9 @@ onBeforeUnmount(() => { if (import.meta.client) document.removeEventListener('keydown', handleKeyboardShortcut) if (import.meta.client) document.removeEventListener('fullscreenchange', updateFullscreenState) if (import.meta.client) document.removeEventListener('webkitfullscreenchange', updateFullscreenState) + if (import.meta.client) window.removeEventListener('orientationchange', handleOrientationFullscreen) + if (import.meta.client) window.removeEventListener('resize', handleOrientationFullscreen) + if (import.meta.client) screen.orientation?.removeEventListener?.('change', handleOrientationFullscreen) }) watch(() => route.query, () => { @@ -1043,7 +1118,7 @@ useHead(() => ({ --> -
+