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_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,不會相互串擾
  • 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" />