SwVideoPreview
SwVideoPreview 是用於卡片懸停 / 小窗預覽的輕量播放器,靜音播放 + 無控制條 +
預設 60 秒播放上限。基於 HLS.js + 原生 <video>,SSR 安全。
取數雙形態 + fetchPlayUrl 三態
取數雙形態
id模式(主):傳入 ID,元件透過fetchPlayUrl(id)非同步拉取臨時 URLurl模式(輔):業務已知預覽 URL 時直接傳入,跳過 fetch
fetchPlayUrl 三態
fetchPlayUrl 預設零組態——與 onHistory 同模式:
undefined(預設):內建走POST /movie/play(舊後端約定,回傳{ play_url })null:顯式停用 fetch(必須傳url)function:覆寫內建實作
基本用法
最簡用法:url 直傳(跳過取數),預設 靜音 + 自動播放 + 不循環 + 60 秒後停止。
基本預覽(url 直傳)
<!-- 預設行為:傳 id 即播(內部走 /movie/play)-->
<SwVideoPreview :id="movie.id" />
<!-- 業務覆寫拉取實作 -->
<SwVideoPreview :id="m.id" :fetch-play-url="(id) => api.movie.preview({ id })" />
<!-- 直接傳 URL(跳過 fetch) -->
<SwVideoPreview url="https://cdn.example.com/preview.mp4" type="mp4" />
<!-- 停用 fetch,必須傳 url -->
<SwVideoPreview :url="myUrl" :fetch-play-url="null" />
列表卡片懸停預覽
hover 進卡片 → 浮層放大 → 非同步 buffer → @playing 後影片淡入(yc001 首頁同款思路的簡化版)。
卡片 hover 放大 + 非同步載入播放
<script setup lang="ts">
const movies = [
{ id: 1, title: '影片 1', cover: '...' },
{ id: 2, title: '影片 2', cover: '...' },
]
const hoveringId = ref<number | null>(null)
</script>
<template>
<div class="grid">
<div
v-for="m in movies"
:key="m.id"
@mouseenter="hoveringId = m.id"
@mouseleave="hoveringId = null"
>
<SwVideoPreview v-if="hoveringId === m.id" :id="m.id" />
<img v-else :src="m.cover" />
</div>
</div>
</template>
fetchPlayUrl 取數 + 事件
傳 :id + 業務函式非同步換取播放地址,@loading / @ready / @playing 全程可觀測。
:id + fetchPlayUrl 取數(事件日誌)
播完重播(loop=false)
loop=false + @ended 重播按鈕
URL 過期重拉
預覽 URL 通常只有幾分鐘有效期,過期後呼叫 reload() 重新拉取:
<script setup lang="ts">
const previewRef = ref()
let expiryTimer: number | undefined
onMounted(() => {
expiryTimer = window.setInterval(() => previewRef.value?.reload(), 5 * 60 * 1000)
})
onBeforeUnmount(() => {
if (expiryTimer) clearInterval(expiryTimer)
})
</script>
<template>
<SwVideoPreview ref="previewRef" :id="movie.id" />
</template>
錯誤回退
取數失敗 → @error 回退封面
<script setup lang="ts">
const fetchFailed = ref(false)
</script>
<template>
<div class="card-cover">
<SwVideoPreview
v-if="!fetchFailed"
:id="movie.id"
@error="fetchFailed = true"
/>
<img v-else :src="movie.cover" alt="預覽失敗回退封面" />
</div>
</template>
播放時長上限(maxDuration)
預覽預設播滿 60 秒即停(hls.stopLoad() + pause(),保留最後一格畫面)並 emit capped。
為什麼預設有上限:預覽位址的常見形態是「營運沒單獨設定預覽資源時,回退正片播放位址」。 沒有上限代表使用者 hover 多久就能免費看多久 —— 付費正片被當預覽看完。上限讓這個缺口 在框架層就關上,不必每個業務方自己想起來堵。
為什麼是 60:實測業界策展預覽片素材本就 55s~2min(騰訊影片 112s、優酷 55s),
60 秒對它們幾乎不觸發(走 @ended 自然收尾);而 60 秒之於 90 分鐘正片是 1.1%,
靠 hover 累積看完不可能。也就是把預設值放在「多數場景對」的那一邊。
計時口徑是累計播放時長(timeupdate 累加,暫停 / 緩衝不計入),不是掛載後的牆鐘時間 ——
否則弱網緩衝久了會提前切斷。loop 場景下多圈累計到上限才停。
<!-- 依預覽來源形態給不同上限:策展預覽片不切、回退正片限時 -->
<SwVideoPreview :id="m.id" :max-duration="source === 'curated' ? 0 : 30000" @capped="showCover = true" />
<!-- 顯式不限(回歸無上限行為) -->
<SwVideoPreview :id="m.id" :max-duration="0" />
「停止網路請求」只對 m3u8 成立:hls.js 有 stopLoad(),而 mp4 / webm 走原生 video.src、
沒有對應手段 —— pause() 之後瀏覽器是否繼續填緩衝由它自己決定。要確定性停止串流只能清 src,
但那會丟掉最後一格畫面、與「定格」衝突,故不做。
自訂播放策略
舊元件硬編碼 autoplay/muted/loop=true、controls=false,新元件可設定(controls 仍硬編碼為 false):
<!-- 不循環、不靜音(使用者已互動後) -->
<SwVideoPreview :id="movie.id" :loop="false" :muted="false" />
事件契約:loading 之後至少有一個終態
loading 之後至少有一個終態必然到達:playing / empty / error 三者之一。
此前存在第四種情況——來源載入好了但永遠不播(readyState 已夠播卻停在 paused,
playing 永不觸發),沒有任何事件,消費方只能永遠停在自己的載入態。各 applier 因此
各寫各的逾時兜底、還各拍一個數(6 秒 / 2.5 秒不等)。現由框架統一兜住:autoplay 場景下
來源 attach 後 8 秒未開始播放即 emit error({ reason: 'start-timeout' })。
「至少一個」不是「恰好一個」 —— playing 在暫停後恢復播放時本就會再次觸發,
消費方靠它重隱封面的邏輯不受影響。
想更早收掉載入動效是消費方的事:框架的 8 秒只負責「保證有終點」,不替業務決定觀感節奏。 需要更快的視覺回饋就自己起一個 2~3 秒的短計時器只收視覺(掃描線 / 壓暗), 同時讓框架的 8 秒繼續給串流留機會——比把框架逾時值調小更好,因為誤報會把「只是慢」的串流判死。
Props
| 名稱 | 型別 | 必填 | 預設值 | 說明 |
|---|---|---|---|---|
id | string | number | '' | 媒體 ID,傳給 fetchPlayUrl | |
url | string | — | 直接傳入預覽 URL,跳過 fetch | |
type | string | 'm3u8' | URL 媒體類型(m3u8 走 hls.js,其他走原生 video.src) | |
fetchPlayUrl | ((id) => Promise<PreviewSource | null>) | null | undefined | 三態:undefined 內建 /movie/play、null 停用、function 覆寫 | |
autoplay | boolean | true | 自動播放 | |
muted | boolean | true | 靜音(autoplay 必需) | |
loop | boolean | false | 循環(業界預覽片素材本就 55s~2min,循環是「素材太短」的補丁) | |
preload | 'none' | 'metadata' | 'auto' | 'metadata' | 預載入策略 | |
maxDuration | number | 60000 | 累計播放上限(毫秒),到點停播保留畫面 + emit capped;0 = 不限 |
Events
| 事件名 | 載荷 | 說明 |
|---|---|---|
loading | — | 取源開始(id / url 兩條路徑都觸發) |
ready | — | URL 已 attach 到 video,可播 |
playing | — | 實際開始播放 |
waiting | 'waiting' | 'stalled' | 緩衝中 / waiting(含 url 直傳;播放中再緩衝也觸發,可做 loading 動效) |
canplay | — | 緩衝結束、可繼續播放 |
ended | — | 播放結束(預設 loop=false,素材播完即觸發) |
capped | — | 累計播放達到 maxDuration,已停播(保留最後一格畫面) |
empty | — | 取不到播放來源(fetchPlayUrl=null 卻沒傳 url / id 為空 / 拉取結果無可用位址)。與 error 區分:「本來就沒有預覽來源」是業務合法狀態,不是失敗 |
settled | reason: 'ended' | 'capped' | 'empty' | 'error' | 四個終態的彙總,緊隨對應細分事件之後觸發。只想「終態後卸載止流」的消費方接這一條即可,不必四個都記得接——漏接 empty 在 mock 環境下永遠發現不了 |
error | err: unknown | fetch 失敗 / HLS 致命錯誤 / 原生 error / 起播逾時(payload { reason: 'start-timeout', source: 'sw-video-preview' })。HLS 非致命錯誤由 hls.js 自行重試,不再轉發 |
defineExpose
| 方法 | 說明 |
|---|---|
play() | 開始播放。已達 maxDuration 後呼叫會重置計時、重新給一個上限視窗(不做靜默 no-op;要循環播就得反覆顯式呼叫,屬業務顯式意圖) |
pause() | 暫停(僅停畫面,hls 仍會繼續填緩衝) |
stop() | 停播 + 停止串流並保留目前畫面(m3u8 走 stopLoad();mp4 不保證停止串流,見上方說明) |
destroy() | 銷毀 hls 實例(畫面一併消失) |
reload() | 重新執行 fetch + initPlayer,應對 URL 過期 |
PreviewSource 型別
type PreviewSource =
| string
| { play_url: string; type?: string }
| { url: string; type?: string }
注意事項
- 零組態開箱即用:傳
id即可播,框架預設走/movie/play - 支援非 HLS 格式:按
type/ URL 副檔名自動判斷,mp4 / webm 等走原生video.src - id / url 自動重載:變更時自動 destroy 舊實例並重新載入
- controls 永遠關閉:preview 場景不需要控制條
- autoplay 必須 muted:瀏覽器靜默自動播放限制
相關元件
- SwVideoPlayer — 完整影片播放器
- SwAudioPlayer — 音訊播放器
waiting 事件參數區分真正等待緩衝的 waiting 與下載暫緩的 stalled;後者可能仍有足夠緩衝正常播放,不能單憑它判定播放逾時。