SwVideoPreview

SwVideoPreview 是用於卡片懸停 / 小窗預覽的輕量播放器,靜音播放 + 無控制條 + 預設 60 秒播放上限。基於 HLS.js + 原生 <video>,SSR 安全。

取數雙形態 + fetchPlayUrl 三態

取數雙形態

  • id 模式(主):傳入 ID,元件透過 fetchPlayUrl(id) 非同步拉取臨時 URL
  • url 模式(輔):業務已知預覽 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

名稱型別必填預設值說明
idstring | number''媒體 ID,傳給 fetchPlayUrl
urlstring—直接傳入預覽 URL,跳過 fetch
typestring'm3u8'URL 媒體類型(m3u8 走 hls.js,其他走原生 video.src)
fetchPlayUrl((id) => Promise<PreviewSource | null>) | nullundefined三態:undefined 內建 /movie/play、null 停用、function 覆寫
autoplaybooleantrue自動播放
mutedbooleantrue靜音(autoplay 必需)
loopbooleanfalse循環(業界預覽片素材本就 55s~2min,循環是「素材太短」的補丁)
preload'none' | 'metadata' | 'auto''metadata'預載入策略
maxDurationnumber60000累計播放上限(毫秒),到點停播保留畫面 + 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 區分:「本來就沒有預覽來源」是業務合法狀態,不是失敗
settledreason: 'ended' | 'capped' | 'empty' | 'error'四個終態的彙總,緊隨對應細分事件之後觸發。只想「終態後卸載止流」的消費方接這一條即可,不必四個都記得接——漏接 empty 在 mock 環境下永遠發現不了
errorerr: unknownfetch 失敗 / 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:瀏覽器靜默自動播放限制

相關元件

waiting 事件參數區分真正等待緩衝的 waiting 與下載暫緩的 stalled;後者可能仍有足夠緩衝正常播放,不能單憑它判定播放逾時。