SwVideoPlayer
SwVideoPlayer 基于 Artplayer + hls.js 实现的视频播放器,
支持 HLS 直播 / 点播、多线路切换、广告前贴片、播放记录上报等常见业务场景。
所有动态库通过 onMounted 异步导入,组件包裹 <client-only>,SSR 安全。
HLS 签名过期恢复
hls.js 路径下,分片或密钥首次出现 403 / 无响应时,播放器重拉同一播放地址,并从当前进度恢复;每次挂载最多重建一次;播放推进或超过 30 秒都不会恢复这个次数,后续 fatal 交给业务兜底。播放地址必须是可重新签发的稳定入口,直接传入已过期的 CDN 签名地址无法靠重拉续期。Feed 仅恢复原本正在播放且仍为 active 的条目,预载和暂停状态不会自动起播。分片/密钥超时(包括 keyLoadTimeOut)、404 和 5xx 不触发这条恢复,继续交给 hls.js 处理;fatal 仍通过 error 通知业务。
开发环境控制台的 HLS recovery 记录本次错误类型、HTTP 状态、重建前后进度及结果;进度超过恢复点才记录 progress,不把清单加载成功当作续播成功。诊断不包含签名 URL,也不上报生产后台。原生 HLS 不包含此恢复能力。
视频源(二选一)
- 直链模式:通过
url+type指定单个视频地址 - 多线路模式:通过
links提供线路列表,配合v-model:linkCode切换
H.265(HEVC)能不能播? 播放器软件栈(Artplayer + hls.js ≥1.6)不设限,取决于用户设备的浏览器硬解(Safari 原生支持;Chrome/Edge 105+ 仅硬解设备;Firefox 基本不支持)。转码侧应输出 HEVC + H.264 双 codec 变体让播放器自动选档兜底,规格与反例见部署手册 · 视频转码建议。
基础用法
最简用法:mp4 直链 + 封面 + 标题(demo 用本地小视频;type 必须与 url 配套,docs 站无播放记录后端故 :on-history="null")。
基础播放(mp4 直链)
<!-- 直链模式 -->
<SwVideoPlayer url="https://example.com/video.m3u8" />
<!-- 多线路模式(v-model:linkCode 父子双向同步) -->
<script setup lang="ts">
const currentCode = ref('line-1')
const links = [
{ code: 'line-1', id: 101, lid: 1, m3u8_url: 'https://a.com/1.m3u8' },
{ code: 'line-2', id: 101, lid: 2, m3u8_url: 'https://b.com/2.m3u8' },
]
</script>
<template>
<SwVideoPlayer v-model:link-code="currentCode" :links="links" />
</template>
常用配置组合
静音自动播放(autoplay 必配 muted,移动端硬性要求)+ 循环 + 画中画 + 主题色。
静音自动播放 + 循环 + 主题色
通过 ref 控制播放器
<script setup lang="ts">
const playerRef = ref()
const onPlay = () => playerRef.value?.play()
const onSeek = () => playerRef.value?.seek(60)
const useNativeApi = () => {
const art = playerRef.value?.getPlayer()
if (art) art.playbackRate = 1.5
}
</script>
<template>
<SwVideoPlayer ref="playerRef" url="..." @ready="(art) => console.log('artplayer ready', art)" />
</template>
断点续播
<!-- startTime 在初始化后跳转到指定秒数,结合 timeupdate 实现断点续播 -->
<script setup lang="ts">
const savedTime = ref(120)
</script>
<template>
<SwVideoPlayer
url="https://example.com/video.m3u8"
:start-time="savedTime"
@timeupdate="(t) => (savedTime = t)"
/>
</template>
续播位置接近结尾(≥ 95%,或落在 skip 的片尾区间里)视为已看完,从头播放——免得最后一集 / 电影一打开就停在结尾画面。整集时长要等元数据加载后才知道时(HLS 常见),先按原位置跳,拿到时长再判断。
前贴片广告(preroll-ad)
前贴片倒计时 + #preroll-ad 插槽 + 跳过
播放器只内置前贴片阶段状态机(拦正片 / 倒计时 / 放行),不内置广告内容——贴片视觉(轮播 / 静态图等)一律走 #preroll-ad 插槽由业务渲染,与组件族「引擎归 framework、视觉归 slot」公约一致。
prerollAd=true且prerollAdDuration > 0时先展示贴片层并倒计时- 倒计时归零:
prerollAdAutoClose=true自动关层进正片;false(默认)显示「进入」等用户点击 - 插槽作用域
{ countdown, skip }:countdown为剩余秒数;skip()提前结束贴片进正片(业务做"跳过广告"按钮用) - 不提供插槽时默认只渲染倒计时 +「进入」按钮
<script setup lang="ts">
// 对齐旧后端 play_ads 协议:item 形状 { id, name, content(图), link }
const { data: r } = await useFetch('/movie/detail/1')
</script>
<template>
<SwVideoPlayer
url="https://example.com/video.m3u8"
:preroll-ad="!!r.play_ads?.length"
:preroll-ad-duration="+r.play_ads_time"
:preroll-ad-auto-close="r.play_ads_auto_jump === 'y'"
>
<template #preroll-ad="{ countdown, skip }">
<SwCarousel :items="r.play_ads" :loop="r.play_ads.length > 1">
<template #default="{ item }">
<SwLink to="/link" :params="item.link"><SwImage :src="item.content" :alt="item.name" /></SwLink>
</template>
</SwCarousel>
</template>
</SwVideoPlayer>
</template>
prerollAdAutoClose不是 YouTube 式"可跳过按钮"语义——倒计时始终走完,它只决定归零后自动放行还是等用户点「进入」。业务要做"N 秒后可跳过"按钮,用插槽里的countdown+skip()自行组合。
播放下一集
show-play-next 让控制条出现「下一集」按钮,点击 emit play-next,由业务切换 url。
开着 show-play-next 时还会自动连播(framework#427):本集播完(或进入片尾,见下节)显示「5 秒后播放下一集 · 取消」,到点 emit 同一个 play-next——业务只接一个回调。取消后这一集不再弹;locked(付费墙)时不弹。用户可在设置面板关掉「自动播放下一集」(偏好见 useVideoPlayerPrefs)。
事件参数为 { source: 'manual' | 'auto' }:控制条点击是 manual,倒计时走完是 auto。忽略参数的旧回调继续兼容。单集有独立路径时,业务可按来源选择手动 router.push、自动 router.replace;播放器本身不负责导航。
最后一集请把
show-play-next关掉——播放器不知道集序,开着就会在最后一集也倒计时。
暂停、拖动或完播后主动重播会取消待执行的下一集,本次加载不再重复触发。自然完播产生的暂停不取消正常连播;后台、缓冲、贴片和付费墙期间倒计时不推进。
「下一集」按钮 + @play-next 切换剧集
<!-- showPlayNext 显示"下一集"按钮,点击触发 @play-next -->
<SwVideoPlayer url="..." title="第 1 集" :show-play-next="true" @play-next="loadNextEpisode" />
全屏与多集连播
播放器只有一个全屏按钮(framework#453):
- 进入:对页面根元素(
<html>)请求浏览器原生全屏,隐藏浏览器界面;播放器根节点挂.is-fullscreen,以position: fixed+z-index: 9999铺满视口。 - 降级:浏览器不支持或拒绝元素全屏时(iPhone Safari 没有元素全屏),停在同样的铺满层,浏览器地址栏仍可见。
- 入口:控件栏按钮(Tips「全屏」/「退出全屏」,可 Tab 聚焦后按 Enter / 空格)、
F键(播放器获得焦点时;本播放器处于全屏时不需要焦点也能按 F 退出,hotkey=false时关闭)、桌面双击画面;移动端双击保持播放 / 暂停。Esc退出;原生全屏下系统返回也会退出。 - 横屏视频:原生全屏成功后尝试锁定横屏(浏览器支持时),退出时解锁。
@fullscreen:进入 / 退出各触发一次;同 key 换集接管时,新播放器挂载即发一次true,业务不用另读状态。iPhone 系统播放器的进出也报告在这里。- 前贴片、付费墙、暂停广告与提示条都挂在播放器根元素内,全屏下都可见。
换集保持全屏(fullscreenKey)
换集通常会重建页面和播放器。原生全屏挂在 <html> 上,不随播放器销毁退出;新播放器只要传入同一个 fullscreen-key,就接管全屏会话:
<!-- 传作品 ID,不要传集 ID:同作品换集保持全屏,换作品退出 -->
<SwVideoPlayer :links="playLinks" show-play-next :fullscreen-key="String(work.id)" @play-next="navigateTo(nextPath)" />
- 同作品换集(路由跳转或同页
:key重建):保持全屏,业务不用记任何状态。 - 换作品 / 离开播放页:页面切换完成后没有同 key 的播放器接管,自动退出全屏并还原滚动位置。
- 不传 key:播放器卸载即退出全屏。
已知限制
- iPhone Safari:只能走铺满层,地址栏仍在;换集后新一集不会自动播放,需要用户点一下(iOS 按媒体元素授予播放权限)。
- 祖先样式:播放器祖先元素有
transform/filter/perspective/container-type(铺不满),或建立了层叠上下文(带z-index的定位元素、opacity < 1、isolation: isolate等,层级被限在祖先内)时,全屏可能铺不满或被顶栏盖住。开发环境进入全屏时控制台会给出警告,可用.is-fullscreen在全屏期间解除这些样式:
/* 全屏期间解除祖先的 container-type,并隐藏不该压在播放器上的悬浮入口 */
body:has(.sw-video-player.is-fullscreen) .player-wrap { container-type: normal; }
body:has(.sw-video-player.is-fullscreen) .floating-tools { display: none; }
- 需要压在全屏之上的业务浮层(如全屏内的选集面板)用大于
9999的z-index。
从 #447 迁移
- 删掉
restore-web-fullscreen及业务里记录「换集前是否全屏」的状态,改传fullscreen-key(restoreWebFullscreen已删除,framework#454)。 - 依赖
.art-fullscreen-web/.art-fullscreen的样式改用.sw-video-player.is-fullscreen。 - 站点自己实现的全屏控制器(如在根节点上调用原生全屏)可以删除,统一走组件。
- 不要再读写
getPlayer().fullscreen/fullscreenWeb:组件不再使用 Artplayer 的全屏,写入它们会与组件的全屏状态冲突。
跳过片头片尾
skip(秒)告诉播放器片头片尾在哪(framework#427):
<SwVideoPlayer
:links="links"
:show-play-next="hasNext"
:skip="{ introStart: null, introEnd: 90, outroLength: 105 }"
@play-next="playNext"
/>
- 片头 =
[introStart ?? 0, introEnd)。先放一段剧情再进片头的(冷开场)才填introStart。每次加载新源后播到这个区间就跳到introEnd,提示「已跳过片头 · 撤销」(约 4 秒消失,撤销回原位)。每次加载最多自动跳一次:断点续播起点已过片头不跳,用户自己拖回片头也不再跳。 - 片尾 = 每集最后
outroLength秒(适配每集时长不同)。开着show-play-next时,正常播放进入片尾即开始「下一集」倒计时,而不是硬跳——片尾后的彩蛋可以点「取消」看完。从片尾区间里直接开播、拖动或快进进片尾都不触发倒计时,播完照常倒计时。 - 不传 /
null= 不跳。「第一集不跳片头」这类集序规则归业务:第一集传introEnd: null即可,播放器不感知集序。 - 用户可在设置面板关掉「跳过片头片尾」(关掉后片头不跳、片尾不触发倒计时,播完照常倒计时)。
事件监听
播放器事件(play / pause / 1s 节流 timeupdate / ended)+ v-model:currentTime。
事件日志 + v-model:currentTime
播放记录上报(onHistory 三态)
onHistory 是 SwVideoPlayer 的核心业务回调,默认零配置:
undefined(默认):内置上报到POST /movie/doHistory(旧后端约定)。loadedmetadata时上报video_play,每 10 秒上报一次video_play,ended时上报video_completenull:完全关闭上报function:覆盖内置实现,业务自行处理
<!-- 内置默认(无需配置)-->
<SwVideoPlayer url="..." />
<!-- 关闭上报 -->
<SwVideoPlayer url="..." :on-history="null" />
<!-- 自定义上报 -->
<SwVideoPlayer url="..." :on-history="(p) => analytics.track('video_play', p)" />
options 逃生舱
<!-- 启用字幕和截图(Artplayer 原生配置,浅合并到内置 options 上) -->
<SwVideoPlayer
url="..."
:options="{
subtitle: { url: '/subtitle.vtt', type: 'vtt' },
screenshot: true,
}"
@ready="(art) => art.on('subtitleUpdate', handleSubtitle)"
/>
Props
| 名称 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
url | string | — | 视频直链(与 links 二选一,优先) | |
type | string | 'm3u8' | 与 url 配套 | |
links | PlayLink[] | [] | 多线路列表 | |
linkCode | string | — | 当前线路 code,支持 v-model:linkCode | |
startTime | number | 0 | 初始化后跳转的秒数(断点续播) | |
resume | { key: string | number; time: number | null } | null | null | Same-episode private history candidate; see resume behavior below. | |
poster | string | — | 封面图 | |
title | string | — | 标题 | |
prerollAd | boolean | false | 开启前贴片阶段(内容走 #preroll-ad 插槽) | |
prerollAdDuration | number | 5000 | 贴片倒计时时长(毫秒,须 > 0) | |
prerollAdAutoClose | boolean | false | 归零自动关层进正片;false 显示「进入」等用户点 | |
showPlayNext | boolean | false | 显示"下一集"按钮;同时启用播完 / 片尾的「下一集」倒计时与设置面板两个开关(见播放下一集) | |
fullscreenKey | string | — | 全屏会话归属,传作品 ID(不是集 ID):同 key 的新播放器接管全屏,换集不退出(见全屏与多集连播) | |
skip | SkipSegments | null | null | 跳过片头片尾(秒):{ introStart?, introEnd?, outroLength? }(见跳过片头片尾) | |
locked | boolean | false | 付费墙锁定态(路径 A,见付费墙):试看片段播完显 #paywall | |
previewReplayLimit | number | undefined | 重看试看次数上限(见付费墙):undefined=不限次 / 0=禁止 / N=最多 N 次 | |
pauseAd | boolean | false | 暂停广告(见暂停广告):暂停 gating 通过显 #pause-ad | |
pauseAdDelay | number | 700 | 暂停到弹广告的延迟去抖(ms,防快速 pause→play / 拖拽误弹) | |
pauseAdMinPlayed | number | 5000 | eligibility:已播未达此毫秒数不弹(开头暂停不打扰) | |
danmaku | boolean | false | 弹幕能力(见弹幕):开启条件懒加载 artplayer-plugin-danmuku 挂滚动层 | |
danmakuData | Danmu[] | [] | 弹幕数据(业务映射后端为 Danmu[]);变化自动重载 | |
danmakuOptions | DanmakuOptions | {} | 弹幕插件配置逃生舱(与默认浅合并;关键默认:emitter: false=外置输入、opacity: 0.8、fontSize: '4%' 随播放器高自适应·最小 12px) | |
theme | string | 'white' | Artplayer 主题色 | |
volume | number | 0.7 | 初始音量 0-1 | |
muted | boolean | false | 静音 | |
autoplay | boolean | false | 挂载后自动起播。未配 muted 时移动端会被拒,组件自动退回静音重试并触发 autoplay-muted | |
loop | boolean | false | 循环 | |
playbackRate | boolean | true | 显示倍速菜单 | |
pip | boolean | false | 画中画 | |
hotkey | boolean | true | 启用快捷键 | |
options | Partial<ArtplayerOption> | {} | Artplayer 原生配置逃生舱(浅合并) | |
plugins | ArtplayerPlugin[] | [] | 业务追加的 Artplayer 插件 | |
onHistory | ((p: VideoHistoryParams) => void) | null | undefined | 播放记录上报:undefined 走内置 /movie/doHistory;null 禁用;function 覆盖 | |
track | VideoPlayerTrackProp | undefined | WebSDK 播放行为埋点(opt-in),见下「播放行为埋点」 | |
blockHijackingBrowsers | boolean | true | 拦截劫持型浏览器(UC / 夸克 / QQ / 百度)。这些浏览器用自己的播放器接管 <video>,我们的元素从未被使用 ⇒ 播放策略全部失效;而它们这么做是为了给用户提供下载 / 转存网盘,对内容方是净损失 ⇒ 即使能播也不该播。命中时渲染换浏览器提示替代播放器(iOS 建议 Safari / 安卓建议 Chrome),可用 #hijacking-browser slot 整块自定义。微信有意不拦(标准 WKWebView + 最大流量来源) |
播放行为埋点(:track.video)
传入业务内容字段即自动上报播放行为,时长 / 进度 / 行为时机由组件自动补:
<SwVideoPlayer
:links="links"
:track="{ video: {
media_id: movie.id, video_id: episode.id, video_title: movie.name, video_content_type: 'video',
video_type_id: movie.type_id, video_type_name: movie.type_name,
recommend_trace_id: route.query.trace_id ?? '',
} }"
/>
自动上报的行为(videoEvent,词表对齐旧框架数据口径):
| video_behavior_key | 时机 |
|---|---|
video_play | 开始播放(每个视频一次,恢复播放不重报) |
video_pause | 暂停(开播后才计,过滤初始化噪音) |
play_progress_25/50/75/100 | 进度节点(各报一次,切换视频重置) |
video_complete | 播放结束(ended,单次) |
video_error | 播放错误(单次门,防 hls 错误风暴) |
video_heartbeat | 播放心跳:播放态每 N 毫秒一次(N = swiftConfig 的 tracker.heartbeatInterval,默认 30000、0 关闭),页面隐藏瞬间补发终帧(自建通道经 sendBeacon 带走,关页前最后一段不丢);暂停期不发 |
video_first_frame | 仅自建通道(tracker.endpoint):首帧(首次原生 playing),每次播放一次,带 startup_ms |
- 技术字段自动补:
video_duration(总时长)/play_duration(当前播放位置,对齐旧仓口径)/play_progress(百分比)/watch_time(累计观看秒——播放态墙钟累计、暂停不计、页面在后台不计(切后台停表、回前台仍在播放则接着计)、每段增量最多算 2 个心跳间隔(页面冻结 / 设备睡眠恢复后不会把挂起时长算进来)、拖拽不跳变,随所有播放事件上报)。 video_error同时覆盖原生 video error 与 hls 致命错误(framework#425 起)。- 播放质量字段(仅自建通道,framework#425):
play_id(单次播放标识,video_id变化时重新生成,换线路不变)·startup_ms(仅video_first_frame带:首帧之前最后一次play→ 首帧的毫秒;期间页面进过后台则不带)·rebuffer_count/rebuffer_ms(首帧之后的累计缓冲次数 / 毫秒,拖动进度引起的不计,随所有播放事件上报)。集团 WebSDK 通道的行为词表与字段保持不变。 track.video.media_id请显式传内容主键:不传时后端会退到video_id归属内容(dev 下 warn 一次)。track.video.video_id变化自动重置行为门闩(切片源场景)。- 与
onHistory(后端自有播放记录)互相独立,两者可同时启用。 - 逃生舱:自有打点 SDK 用原始事件(
@play/@pause/@timeupdate/@ended/@error)自接。
Events
| 事件名 | 载荷 | 说明 |
|---|---|---|
ready | player: ArtplayerInstance | Artplayer 实例就绪 |
loadedmetadata | — | 元数据加载完成 |
waiting | — | 缓冲中 / waiting(播放因缺数据暂停,可做 loading 动效) |
canplay | — | 可以播放(缓冲结束) |
play | — | 每轮开播一次;暂停或换源后再开播会再次触发,hls.js 自动起播时同样触发 |
pause | — | 暂停 |
seeked | currentTime: number | 跳转完成(整数秒) |
timeupdate | currentTime: number | 1s 节流的播放进度 |
update:currentTime | currentTime: number | 500ms 节流,供 v-model:currentTime |
ended | — | 播放结束 |
paywall | — | locked 条试看片段播完、付费墙显示时(挂墙曝光转化埋点) |
pause-ad | — | 用户暂停且 gating 通过、#pause-ad 弹出时(挂广告曝光埋点钩子) |
autoplay-muted | — | 带声播放被浏览器拦下、已自动退回静音重试。muted=false 不保证有声,业务据此显示「点击开启声音」提示 |
play-next | { source: 'manual' | 'auto' } | 点击按钮为 manual,倒计时走完为 auto |
resume-pending | boolean | Hold custom history writes while the resume offer is unresolved. |
volume-change | { volume: number, muted: boolean } | 音量或静音状态变化(桥接原生 volumechange;改音量、改静音都触发)。典型用途:回写声音偏好 |
fullscreen | state: boolean | 全屏状态变化,进入 / 退出各一次;换集接管的新播放器挂载即发 true(见全屏与多集连播) |
controls-show | — | 控制条显示 |
controls-hide | — | 控制条隐藏 |
error | err: unknown | 播放错误。只报致命错误 —— 非致命(分片 404 / 网络抖动)由 hls.js 自行重试,不再打扰业务 |
update:linkCode | code: string | 切换线路后同步 v-model |
Slots
| 插槽名 | 说明 |
|---|---|
preroll-ad | 前贴片层内容(广告轮播 / 静态图等业务自定),接收 { countdown, skip } 作用域 |
loading | 视频容器内的加载占位 |
paywall | 付费墙内容(locked 条试看完显示),接收 { locked } 作用域:业务渲染购买 UI(无强制 X)。覆盖层 Teleport 进 Artplayer $player(全屏安全) |
pause-ad | 暂停广告内容(pauseAd 开启 + gating 通过显示),接收 { close } 作用域:业务渲染 SwAdPause。覆盖层 Teleport 进 $player(全屏安全),wrapper pointer-events:none(点卡片外恢复播放即关) |
hijacking-browser | 劫持型浏览器命中时的整块提示内容(替代播放器,不是盖在上面)。browser 是命中的浏览器标识(uc / qq / baidu),suggested 是建议改用的浏览器名(iOS→Safari / 其他→Chrome),接收 { browser, suggested } 作用域 |
defineExpose
播放与切线的待完成回调会在调用 pause() / destroy() 或卸载后失效,避免迟到起播。error 只承载媒体加载失败;播放与全屏操作的非预期异常进入框架错误监控,不触发业务自动换线。iOS 退出全屏按进入前的播放状态恢复,并尊重组件 pause();原生控件内部的暂停意图不在此保证范围。
| 方法 | 说明 |
|---|---|
play() | 开始播放 |
pause() | 暂停 |
seek(time: number) | 跳转到指定秒数 |
replayPreview(): boolean | 付费墙显示后收墙 + 从头重播试看片段(重播到末尾自动复墙);返回是否成功(非 locked / 达 previewReplayLimit 上限时 false) |
destroy() | 销毁播放器实例 |
switchLink(code: string) | 切换到指定 code 的线路(保留进度) |
getPlayer() | 获取 Artplayer 原生实例 |
exitFullscreen() | 本播放器处于全屏时退出(返回 Promise);用于最终无源 / 全线路失败等播放器上没有退出入口的场景 |
emitDanmaku(danmu: Danmu) | 发送一条实时弹幕(弹幕外置输入栏用) |
showDanmaku() / hideDanmaku() | 显示 / 隐藏弹幕层 |
configDanmaku(opt) | 实时改弹幕配置(透明度 / 速度 / 区域等) |
loadDanmaku(data: Danmu[]) | 重载弹幕数据源 |
getDanmakuPlugin() | 取 artplayer-plugin-danmuku 实例(未开启 / 未就绪返回 null) |
付费墙(试看墙)
付费内容试看片段播完到点显示购买门(变现,不是广告)。机制走路径 A:后端只下发试看片段(完整正片不下发到客户端,从根上防扒源),片段播完自然 video:ended → 显 #paywall 插槽。传 locked: true(业务从后端付费字段算,如 is_vip && !has_access):
- 试看完显墙:
locked时试看片段播完 → 显#paywall+ 触发@paywall(挂「墙曝光」转化埋点)。 - 全屏不穿帮(关键):付费墙
Teleport进 Artplayer 的$player(播放器根节点内)→ 全屏时随之铺满覆盖。做成播放器容器的兄弟 DOM 会被全屏(z-index 9999)穿帮——framework 已处理好,业务只管在插槽里渲染 UI。 - 解锁续播:购买成功后业务清
locked+ 换正片源 + seek 到resume_offset——用getPlayer().switchUrl(正片URL)+seek(resume_offset)。framework 不提供unlock()。 - 重看试看(可选):付费墙底部「重看预览片段」按钮经 ref 调
replayPreview()——收墙 + 从头重播试看片段,重播到末尾自动复墙。次数上限用previewReplayLimit(undefined=不限次,默认;N=最多 N 次,适合「试看=正片前 N 分钟」需防白嫖的场景;0=禁止)。replayPreview()返回false表示非locked态或已达上限。解锁(locked→false)时计数重置。
#paywall 作用域 { locked }(player 无 skipToNext;退出 = 业务解锁 / 页面返回 / 重看试看):
<!-- preview-replay-limit:重看试看次数上限(防「反复重看白嫖」,默认不限次) -->
<SwVideoPlayer ref="playerRef" :url="trialUrl" type="m3u8" :locked="locked" :preview-replay-limit="2" @paywall="onPaywallExposure">
<template #paywall>
<div class="paywall">
<p>试看已结束</p>
<button @click="unlock">开通会员解锁</button>
</div>
</template>
</SwVideoPlayer>
<script setup>
const playerRef = ref()
const locked = ref(true)
async function unlock() {
await purchase() // 业务支付
const full = await fetchFullSource() // 拉正片 m3u8 + resume_offset
const art = playerRef.value.getPlayer()
await art.switchUrl(full.url) // 换正片源
playerRef.value.seek(full.resume_offset ?? 0) // seek 到续播点
locked.value = false // 清锁 → 墙隐
}
</script>
resume_offset由后端给(试看=正片前 N 秒→N/ 中段精彩片段→0/ 缺省→0从头);前端不靠试看时长推算(试看未必是正片开头)。付费 / 播放路径页面须ssr: false(命中 ESLintno-isr-on-paid-paths)。短视频流 SwVideoFeed 有同款付费墙。
暂停广告(Pause Ad)
用户暂停视频时弹出的可关闭广告层(广告,不是变现;与付费墙严格区分)。传 pause-ad 开启,播放器内置 gating 全通过才显 #pause-ad 插槽——广告创意 / 频控 / 埋点由插槽里的 SwAdPause 承载:
- gating(核心):
video:pause后延迟去抖(pauseAdDelay,默认 700ms)+ 过滤系统暂停(拖拽seeking/ 缓冲waiting/ 切后台 / 前贴片期 / 付费墙显示中 / 完播)+ eligibility(已播 ≥pauseAdMinPlayed)+ 与付费墙互斥(同时只显一个)。全通过 → 显#pause-ad+ 触发@pause-ad(挂广告曝光钩子)。 - 全屏不穿帮:暂停广告同付费墙
Teleport进 Artplayer 的$player,全屏时随之可见。 - 不挡恢复 + 恢复即关:覆盖层 wrapper
pointer-events:none,点卡片外画面穿透恢复播放(与付费墙的全屏死挡相反)→video:play自动收起;SwAdPause的 ✕ 经插槽close收起覆盖层。
#pause-ad 作用域 { close }:
<!-- pause-ad-delay:暂停到弹广告的去抖 ms(默认 700,拖拽误触多的场景可调高) -->
<SwVideoPlayer :url="videoUrl" type="m3u8" pause-ad :pause-ad-delay="1000" @pause-ad="onPauseExposure">
<template #pause-ad="{ close }">
<SwAdPause :items="pauseAds" slot-key="play_pause" @close="close" />
</template>
</SwVideoPlayer>
暂停广告(暂停触发 + 恢复即关)
劫持型浏览器拦截(#hijacking-browser slot)
暂停广告复用付费墙的「全屏安全覆盖层挂载」机制;两者互斥,付费墙显示时不弹暂停广告。完整组件文档见 SwAdPause。
弹幕(Danmaku)
设置面板支持鼠标悬停和点击聚焦;触摸打开后,滚动或重排不会因悬停状态消失而关闭,点击面板外部即可关闭。无需额外配置。
传 danmaku 开启。播放器挂载时条件动态 import artplayer-plugin-danmuku(≈14KB gzip 独立 chunk、启用才下载,关闭零开销)并挂滚动层。framework 不内置输入框(headless,emitter 默认 false)—— 输入栏由 applier 自渲染(外置在播放器外,样式 / 位置 / 颜色与类型选择器全由业务掌控),经 defineExpose 的 emitDanmaku / showDanmaku / hideDanmaku / configDanmaku / loadDanmaku 命令式驱动。
danmakuData:弹幕数据Danmu[]。framework 不绑后端形态——旧后端/danmaku/list协议用内置的danmakuListToDanmu一行归一成它(见下「后端数据归一」一节),其它数据源自行映射成Danmu[];变化时自动load重载。danmakuOptions:插件配置逃生舱(speed/opacity/margin/modes/fontSize/emitter等),与 framework 默认浅合并(关键默认:opacity: 0.8、fontSize: '4%'——播放器高 × 4%、最小 12px,随播放器缩放)。要用插件自带输入框传{ emitter: true };想回全不透明固定字号传{ opacity: 1, fontSize: 22 }。
<script setup lang="ts">
import type { Danmu } from '@swift-web/framework'
const playerRef = ref()
const list: Danmu[] = [
{ text: '第一条弹幕', time: 1, mode: 0, color: '#FFFFFF' },
{ text: '顶部弹幕', time: 2, mode: 1 },
]
</script>
<template>
<!-- danmaku-options:插件配置逃生舱(与默认浅合并;{ emitter: true } 用插件自带输入框) -->
<SwVideoPlayer ref="playerRef" :url="videoUrl" type="m3u8" danmaku :danmaku-data="list" :danmaku-options="{ speed: 7, opacity: 0.9 }" />
<!-- 外置弹幕输入栏(业务自渲染),点发送调 emitDanmaku -->
<MyDanmakuBar
@send="(text) => playerRef.emitDanmaku({ text })"
@toggle="(on) => (on ? playerRef.showDanmaku() : playerRef.hideDanmaku())"
/>
</template>
弹幕面板主色(--sw-video-player-danmaku-color)
artplayer-plugin-danmuku 控制面板(不透明度 / 显示区域 / 字号 / 速度滑块、勾选框、发送键、按类型屏蔽图标等)默认是插件自带的 B 站蓝。framework 把它统一收敛到 CSS 变量 --sw-video-player-danmaku-color,默认回退 var(--primary) —— 默认即跟随站点主色、零配置。要单独指定,在播放器或任意祖先上设该变量:
<template>
<!-- 让弹幕面板主色用品牌色 -->
<div style="--sw-video-player-danmaku-color: #e23744">
<SwVideoPlayer :url="videoUrl" danmaku :danmaku-data="list" />
</div>
</template>
面板坐落在深色播放器上;若站点
--primary在深底上对比不足,可用本变量单独换更高对比度的颜色。
Danmu 类型
interface Danmu {
text: string // 弹幕文本
time?: number // 出现时间点(视频秒数);emit 实时弹幕可省略
mode?: 0 | 1 | 2 // 轨道:0 滚动 / 1 顶部 / 2 底部,默认 0
color?: string // 颜色 #RRGGBB
border?: boolean // 是否描边,默认 false
}
后端数据归一(danmakuListToDanmu)
旧后端 /danmaku/list 协议(裸数组,字段 content / pos / color / mode)用内置的 danmakuListToDanmu 一行归一成 Danmu[](auto-import,对称广告侧的 playAdsToSwAdItems):
<script setup lang="ts">
import type { Danmu } from '@swift-web/framework'
// request() 剥 { status, data } 壳后是裸数组;danmakuListToDanmu 归一为 Danmu[]
const raw = await request('/danmaku/list', { body: { object_id, sub_id } })
const list: Danmu[] = danmakuListToDanmu(raw)
</script>
<template>
<SwVideoPlayer :url="url" type="m3u8" danmaku :danmaku-data="list" />
</template>
danmakuListToDanmu(list: DanmakuListRawItem[] | null | undefined): Danmu[]
归一规则(只取显示子集;纯函数,SSR / 客户端均可调用):
content→text;pos(秒,可能是字符串数字)→time(Number()容错,无值取0)mode(0滚动 /1顶部 /2底部,可能是字符串数字)→mode;非法 / 缺失则省略,由默认轨道接管color(十进制数字 / 字符串、#RRGGBB、#RGB短写)→#RRGGBB;'0'/0/ 缺失则省略,由danmakuOptions.color/ 插件默认色接管null/undefined/ 非数组 →[]- 后端的
id/like_count/user_id等业务字段不进Danmu——点赞 / 举报 / 发送的业务编排留在 applier;发送方向的颜色 hex→decimal 也不在本 util。
PlayLink 类型
interface PlayLink {
code: string // 线路唯一标识
id: number | string // 业务 id(用于上报)
lid: number | string // 线路 id(用于上报)
m3u8_url: string // 播放地址
}
样式定制 Token
以下 CSS 变量可在业务侧覆盖(fallback 即默认值):
| 变量 | 默认值 | 说明 |
|---|---|---|
--sw-video-player-overlay-z | 100 | 播放器内业务叠层的层级(fw#332 z 层旋钮) |
--sw-video-player-hijack-z | 200 | 劫持型浏览器拦截层的层级,刻意高于业务叠层 |
注意事项
- autoplay 限制:浏览器通常要求
muted: true才能自动播放,否则会被静默拦截 - 同页面多实例:组件使用 ref 指向自身容器 DOM,多个 SwVideoPlayer 共存时不会相互串扰
- switchLink 保留进度:切换线路前记录 currentTime,切换完成后恢复并 play
- 分片签名过期自动续播:hls.js 路径下分片 / 密钥首次出现 403 或无响应(防盗链签名过期的形态)时,播放器重建 hls 实例重拉同一地址(后端稳定代理路径 = 请求时现签)并从当前进度续播,每次挂载只兜一次;再命中不拦,交给 hls.js 重试直至致命错误经
@error上报。404 / 超时不触发。iPhone 原生 HLS 路径不覆盖。业务侧无需处理,也不必自己换址 - options 智能合并:
customType与plugins单独合并不被整体替换;其他字段浅合并
相关组件
- SwAudioPlayer — 音频播放器(HLS + 原生 audio)
- SwVideoPreview — 静音循环预览(卡片悬停)
同集续播提示
传入已经校验作品、集和账号归属的 resume: { key, time }。key 是当前播放会话标识;time 是私人历史秒数,未就绪时为 null。候选只在合法时长内、片头片尾之外提供提示;实际前台播放五秒后恢复,也可以立即恢复或取消。暂停、拖动、换源和离开当前条目使旧提示失效。恢复只调整位置,不解除暂停。播放器内部协调片头跳过,业务不要临时清空 skip。
明确点击继续观看时用 startTime(Feed 用 seek_time),不重复提示。Feed 的同会话回滑恢复也优先于历史候选,预加载条目不展示或执行恢复。不要把私人历史写进 SSR 或共享缓存。
自定义历史写入者监听 resume-pending,为 true 时不保存初始化进度,以免覆盖旧断点;播放器内置的 onHistory 已在内部暂停上报。Feed 事件参数为 (index, pending),仅影响对应条目的写入。跨集提示、身份校验和记录持久化仍由业务负责。
<SwVideoPlayer :resume="{ key: 'account:episode', time: 120 }" :skip="{ introEnd: 60, outroLength: 90 }" />
<SwVideoPlayer :start-time="120" />
<SwVideoPlayer :resume="validatedHistory" @resume-pending="historyPending = $event" :on-history="saveHistory" />