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)
</script>
<template>
<SwVideoPlayer ref="playerRef" url="..." @ready="(art) => console.log('artplayer ready', art)" />
</template>
斷點續播
<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,不會相互串擾
- switchLink 保留進度:切換線路前記錄 currentTime,切換完成後恢復並 play
- 分片簽名過期自動續播:hls.js 路徑下分片 / 金鑰首次出現 403 或無回應(防盜鏈簽名過期的形態)時,播放器重建 hls 實例重拉同一地址(後端穩定代理路徑 = 請求時現簽)並從當前進度續播,每次掛載只兜一次;再命中不攔,交給 hls.js 重試直至致命錯誤經
@error上報。404 / 逾時不觸發。iPhone 原生 HLS 路徑不覆蓋。業務側無需處理,也不必自己換址 - options 智慧合併:
customType與plugins單獨合併不被整體取代
相關元件
- SwAudioPlayer — 音訊播放器
- 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" />