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(() => {
  // 5 分钟后自动重拉新 URL
  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 :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                              // 直接返回 URL 字符串
  | { play_url: string; type?: string } // 兼容旧 c-player-preview 形状
  | { url: string; type?: string }      // 通用形状

fetchPlayUrl 的返回值组件内部会自动规整为 { url, type }。

注意事项

  • 零配置开箱即用:传 id 即可播,框架默认走 /movie/play
  • 支持非 HLS 格式:按 type / URL 后缀自动判断(mp4 / webm 等走原生 video.src)
  • id / url 自动重载:变更时自动 destroy 旧实例并重新加载
  • controls 永远关闭:preview 场景不需要控制条;需要控制条请用 SwVideoPlayer
  • 取不到源要接 @empty:否则消费方会一直停在自己的 loading 态(@loading 之后不会再有别的事件)
  • 重复 hover 会重新挂载、从头播:计时不跨挂载累计,所以上限的真实效果是「永远只能看前 N 秒」
  • autoplay 必须 muted:浏览器静默自动播放限制,强烈建议保持 muted=true

相关组件

waiting 事件的参数区分真正等待缓冲的 waiting 与下载暂缓的 stalled;后者可能仍有足够缓冲正常播放,不能单凭它判断播放超时。