155 lines
3.1 KiB
Vue
155 lines
3.1 KiB
Vue
<script setup lang="ts">
|
|
interface Props {
|
|
src: string
|
|
type?: 'mp4' | 'webm'
|
|
poster?: string
|
|
play?: boolean
|
|
loop?: boolean
|
|
bordered?: boolean
|
|
class?: string
|
|
/**
|
|
* 비디오 데이터 로딩 시점
|
|
* - none: 재생 전까지 로드 안 함 (가장 절약)
|
|
* - metadata: 길이·크기 등 메타만 로드
|
|
* - auto: 재생에 맞게 미리 로드 (autoplay 시 권장)
|
|
*/
|
|
preload?: 'none' | 'metadata' | 'auto'
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
type: 'mp4',
|
|
play: true,
|
|
loop: true,
|
|
bordered: true,
|
|
})
|
|
|
|
const effectivePreload = computed(
|
|
() => props.preload ?? (props.play ? 'auto' : 'none')
|
|
)
|
|
|
|
const videoRef = ref<HTMLVideoElement | null>(null)
|
|
const pauseTimer = ref<ReturnType<typeof setTimeout> | null>(null)
|
|
|
|
const clearPauseTimer = () => {
|
|
if (pauseTimer.value) {
|
|
clearTimeout(pauseTimer.value)
|
|
pauseTimer.value = null
|
|
}
|
|
}
|
|
|
|
const playVideo = async () => {
|
|
if (!videoRef.value) return
|
|
|
|
clearPauseTimer()
|
|
|
|
try {
|
|
await videoRef.value.play()
|
|
} catch (e) {
|
|
console.warn('Video play failed:', e)
|
|
}
|
|
}
|
|
|
|
const pauseVideo = () => {
|
|
if (!videoRef.value) return
|
|
|
|
clearPauseTimer()
|
|
|
|
pauseTimer.value = setTimeout(() => {
|
|
if (videoRef.value) {
|
|
videoRef.value.pause()
|
|
videoRef.value.currentTime = 0
|
|
}
|
|
pauseTimer.value = null
|
|
}, 500)
|
|
}
|
|
|
|
const waitForCanPlay = () =>
|
|
new Promise<void>(resolve => {
|
|
if (!videoRef.value) return resolve()
|
|
videoRef.value.addEventListener('canplay', () => resolve(), { once: true })
|
|
})
|
|
|
|
const reloadVideo = async () => {
|
|
if (!videoRef.value) return
|
|
|
|
clearPauseTimer()
|
|
videoRef.value.currentTime = 0
|
|
videoRef.value.load()
|
|
|
|
if (props.play) {
|
|
await waitForCanPlay()
|
|
await playVideo()
|
|
}
|
|
}
|
|
|
|
const syncPlayState = (shouldPlay: boolean) => {
|
|
if (shouldPlay) {
|
|
void playVideo()
|
|
} else {
|
|
pauseVideo()
|
|
}
|
|
}
|
|
|
|
// src 변경 시 비디오 다시 로드
|
|
watch(
|
|
() => props.src,
|
|
() => {
|
|
void reloadVideo()
|
|
}
|
|
)
|
|
|
|
// play 상태 변경 시 재생/일시정지
|
|
watch(
|
|
() => props.play,
|
|
shouldPlay => {
|
|
syncPlayState(!!shouldPlay)
|
|
}
|
|
)
|
|
|
|
onMounted(() => {
|
|
nextTick(async () => {
|
|
if (props.play) {
|
|
await waitForCanPlay()
|
|
await playVideo()
|
|
}
|
|
})
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
clearPauseTimer()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
v-if="props.src"
|
|
:class="['video-box', { 'has-bordered': props.bordered }, props.class]"
|
|
>
|
|
<video
|
|
ref="videoRef"
|
|
:poster="props.poster"
|
|
:loop="props.loop"
|
|
:autoplay="props.play"
|
|
:preload="effectivePreload"
|
|
muted
|
|
playsinline
|
|
webkit-playsinline
|
|
>
|
|
<source :src="props.src" :type="`video/${props.type}`" />
|
|
</video>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.video-box {
|
|
@apply overflow-hidden;
|
|
}
|
|
.video-box.has-bordered {
|
|
@apply relative rounded-[4px] md:rounded-[8px]
|
|
after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:border after:border-white after:opacity-10 after:rounded-[4px] after:md:rounded-[8px];
|
|
}
|
|
.video-box video {
|
|
@apply absolute top-0 left-0 w-full h-full object-cover;
|
|
}
|
|
</style>
|