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(() => {
// 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
| 名称 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
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 // 直接返回 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
相关组件
- SwVideoPlayer — 完整视频播放器(带控制条、进度上报)
- SwAudioPlayer — 音频播放器
waiting 事件的参数区分真正等待缓冲的 waiting 与下载暂缓的 stalled;后者可能仍有足够缓冲正常播放,不能单凭它判断播放超时。