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_complete
  • null:完全关闭上报
  • 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

名称类型必填默认值说明
urlstring—视频直链(与 links 二选一,优先)
typestring'm3u8'与 url 配套
linksPlayLink[][]多线路列表
linkCodestring—当前线路 code,支持 v-model:linkCode
startTimenumber0初始化后跳转的秒数(断点续播)
resume{ key: string | number; time: number | null } | nullnullSame-episode private history candidate; see resume behavior below.
posterstring—封面图
titlestring—标题
prerollAdbooleanfalse开启前贴片阶段(内容走 #preroll-ad 插槽)
prerollAdDurationnumber5000贴片倒计时时长(毫秒,须 > 0)
prerollAdAutoClosebooleanfalse归零自动关层进正片;false 显示「进入」等用户点
showPlayNextbooleanfalse显示"下一集"按钮;同时启用播完 / 片尾的「下一集」倒计时与设置面板两个开关(见播放下一集)
fullscreenKeystring—全屏会话归属,传作品 ID(不是集 ID):同 key 的新播放器接管全屏,换集不退出(见全屏与多集连播)
skipSkipSegments | nullnull跳过片头片尾(秒):{ introStart?, introEnd?, outroLength? }(见跳过片头片尾)
lockedbooleanfalse付费墙锁定态(路径 A,见付费墙):试看片段播完显 #paywall
previewReplayLimitnumberundefined重看试看次数上限(见付费墙):undefined=不限次 / 0=禁止 / N=最多 N 次
pauseAdbooleanfalse暂停广告(见暂停广告):暂停 gating 通过显 #pause-ad
pauseAdDelaynumber700暂停到弹广告的延迟去抖(ms,防快速 pause→play / 拖拽误弹)
pauseAdMinPlayednumber5000eligibility:已播未达此毫秒数不弹(开头暂停不打扰)
danmakubooleanfalse弹幕能力(见弹幕):开启条件懒加载 artplayer-plugin-danmuku 挂滚动层
danmakuDataDanmu[][]弹幕数据(业务映射后端为 Danmu[]);变化自动重载
danmakuOptionsDanmakuOptions{}弹幕插件配置逃生舱(与默认浅合并;关键默认:emitter: false=外置输入、opacity: 0.8、fontSize: '4%' 随播放器高自适应·最小 12px)
themestring'white'Artplayer 主题色
volumenumber0.7初始音量 0-1
mutedbooleanfalse静音
autoplaybooleanfalse挂载后自动起播。未配 muted 时移动端会被拒,组件自动退回静音重试并触发 autoplay-muted
loopbooleanfalse循环
playbackRatebooleantrue显示倍速菜单
pipbooleanfalse画中画
hotkeybooleantrue启用快捷键
optionsPartial<ArtplayerOption>{}Artplayer 原生配置逃生舱(浅合并)
pluginsArtplayerPlugin[][]业务追加的 Artplayer 插件
onHistory((p: VideoHistoryParams) => void) | nullundefined播放记录上报:undefined 走内置 /movie/doHistory;null 禁用;function 覆盖
trackVideoPlayerTrackPropundefinedWebSDK 播放行为埋点(opt-in),见下「播放行为埋点」
blockHijackingBrowsersbooleantrue拦截劫持型浏览器(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

事件名载荷说明
readyplayer: ArtplayerInstanceArtplayer 实例就绪
loadedmetadata—元数据加载完成
waiting—缓冲中 / waiting(播放因缺数据暂停,可做 loading 动效)
canplay—可以播放(缓冲结束)
play—每轮开播一次;暂停或换源后再开播会再次触发,hls.js 自动起播时同样触发
pause—暂停
seekedcurrentTime: number跳转完成(整数秒)
timeupdatecurrentTime: number1s 节流的播放进度
update:currentTimecurrentTime: number500ms 节流,供 v-model:currentTime
ended—播放结束
paywall—locked 条试看片段播完、付费墙显示时(挂墙曝光转化埋点)
pause-ad—用户暂停且 gating 通过、#pause-ad 弹出时(挂广告曝光埋点钩子)
autoplay-muted—带声播放被浏览器拦下、已自动退回静音重试。muted=false 不保证有声,业务据此显示「点击开启声音」提示
play-next{ source: 'manual' | 'auto' }点击按钮为 manual,倒计时走完为 auto
resume-pendingbooleanHold custom history writes while the resume offer is unresolved.
volume-change{ volume: number, muted: boolean }音量或静音状态变化(桥接原生 volumechange;改音量、改静音都触发)。典型用途:回写声音偏好
fullscreenstate: boolean全屏状态变化,进入 / 退出各一次;换集接管的新播放器挂载即发 true(见全屏与多集连播)
controls-show—控制条显示
controls-hide—控制条隐藏
errorerr: unknown播放错误。只报致命错误 —— 非致命(分片 404 / 网络抖动)由 hls.js 自行重试,不再打扰业务
update:linkCodecode: 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(命中 ESLint no-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。
interface PlayLink {
  code: string         // 线路唯一标识
  id: number | string  // 业务 id(用于上报)
  lid: number | string // 线路 id(用于上报)
  m3u8_url: string     // 播放地址
}

样式定制 Token

以下 CSS 变量可在业务侧覆盖(fallback 即默认值):

变量默认值说明
--sw-video-player-overlay-z100播放器内业务叠层的层级(fw#332 z 层旋钮)
--sw-video-player-hijack-z200劫持型浏览器拦截层的层级,刻意高于业务叠层

注意事项

  • autoplay 限制:浏览器通常要求 muted: true 才能自动播放,否则会被静默拦截
  • 同页面多实例:组件使用 ref 指向自身容器 DOM,多个 SwVideoPlayer 共存时不会相互串扰
  • switchLink 保留进度:切换线路前记录 currentTime,切换完成后恢复并 play
  • 分片签名过期自动续播:hls.js 路径下分片 / 密钥首次出现 403 或无响应(防盗链签名过期的形态)时,播放器重建 hls 实例重拉同一地址(后端稳定代理路径 = 请求时现签)并从当前进度续播,每次挂载只兜一次;再命中不拦,交给 hls.js 重试直至致命错误经 @error 上报。404 / 超时不触发。iPhone 原生 HLS 路径不覆盖。业务侧无需处理,也不必自己换址
  • options 智能合并:customType 与 plugins 单独合并不被整体替换;其他字段浅合并

相关组件

同集续播提示

传入已经校验作品、集和账号归属的 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" />