mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-28 02:01:20 -08:00
refactor(vast): [PKG-VAST-03] isolate lifecycle ownership checkpoint
This commit is contained in:
1 parent
d5e1feadf5
commit
4f324a8438
30 files changed
+1453
-383
No files matched your search
@@ -1,130 +0,0 @@
|
||||
import { loadImaSdk, Player, PlayerOptions } from '@glomex/vast-ima-player'
|
||||
|
||||
export default function artplayerPluginVast(callback) {
|
||||
return async (art) => {
|
||||
const { template, constructor } = art
|
||||
const { createElement, setStyles } = constructor.utils
|
||||
const { $video, $player } = template
|
||||
|
||||
await loadImaSdk()
|
||||
const google = window.google
|
||||
const ima = google.ima
|
||||
|
||||
const adsRenderingSettings = new ima.AdsRenderingSettings()
|
||||
adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true
|
||||
adsRenderingSettings.enablePreloading = true
|
||||
|
||||
const playerOptions = new PlayerOptions()
|
||||
let isAdPlaying = false
|
||||
let imaPlayer = null
|
||||
let $container = null
|
||||
|
||||
function createContainer() {
|
||||
const container = createElement('div')
|
||||
const id = `art-vast-${Date.now()}`
|
||||
container.id = id
|
||||
setStyles(container, {
|
||||
position: 'absolute',
|
||||
inset: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: '150',
|
||||
backgroundColor: 'black',
|
||||
display: 'none',
|
||||
pointerEvents: 'auto',
|
||||
})
|
||||
return container
|
||||
}
|
||||
|
||||
function initPlayer() {
|
||||
if (imaPlayer)
|
||||
return imaPlayer
|
||||
|
||||
$container = createContainer()
|
||||
$player.appendChild($container)
|
||||
|
||||
imaPlayer = new Player(ima, $video, $container, adsRenderingSettings, playerOptions)
|
||||
|
||||
imaPlayer.addEventListener('AdContentPauseRequested', () => {
|
||||
isAdPlaying = true
|
||||
$container.style.display = 'block'
|
||||
})
|
||||
|
||||
imaPlayer.addEventListener('AdContentResumeRequested', () => {
|
||||
isAdPlaying = false
|
||||
$container.style.display = 'none'
|
||||
})
|
||||
|
||||
imaPlayer.addEventListener('AdStarted', () => {
|
||||
isAdPlaying = true
|
||||
$container.style.display = 'block'
|
||||
})
|
||||
|
||||
imaPlayer.addEventListener('AdError', (event) => {
|
||||
console.error('VAST Ad Error:', event.detail)
|
||||
isAdPlaying = false
|
||||
$container.style.display = 'none'
|
||||
})
|
||||
|
||||
return imaPlayer
|
||||
}
|
||||
|
||||
function destroyPlayer() {
|
||||
if (imaPlayer?.destroy)
|
||||
imaPlayer.destroy()
|
||||
if ($container?.parentNode)
|
||||
$container.parentNode.removeChild($container)
|
||||
$container = null
|
||||
imaPlayer = null
|
||||
}
|
||||
|
||||
function playUrl(url, config = {}) {
|
||||
if (isAdPlaying)
|
||||
return
|
||||
if (!imaPlayer)
|
||||
initPlayer()
|
||||
const request = new ima.AdsRequest()
|
||||
request.adTagUrl = url
|
||||
for (const key in config) {
|
||||
request[key] = config[key]
|
||||
}
|
||||
imaPlayer.playAds(request)
|
||||
}
|
||||
|
||||
function playRes(res, config = {}) {
|
||||
if (isAdPlaying)
|
||||
return
|
||||
if (!imaPlayer)
|
||||
initPlayer()
|
||||
const request = new ima.AdsRequest()
|
||||
request.adsResponse = res
|
||||
for (const key in config) {
|
||||
request[key] = config[key]
|
||||
}
|
||||
imaPlayer.playAds(request)
|
||||
}
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
await callback({
|
||||
art,
|
||||
playUrl,
|
||||
playRes,
|
||||
init: initPlayer,
|
||||
ima,
|
||||
adsRenderingSettings,
|
||||
playerOptions,
|
||||
get imaPlayer() {
|
||||
return imaPlayer
|
||||
},
|
||||
get container() {
|
||||
return $container
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'artplayerPluginVast',
|
||||
destroy: destroyPlayer,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { Callback, Result } from './types'
|
||||
import { loadImaSdk } from './sdk'
|
||||
import { createSession } from './session'
|
||||
|
||||
export default function artplayerPluginVast(callback?: Callback) {
|
||||
return async (art: Artplayer): Promise<Result> => {
|
||||
let disposed = false
|
||||
let session: ReturnType<typeof createSession> | undefined
|
||||
const closed = () => disposed || art.isDestroy
|
||||
const result: Result = { name: 'artplayerPluginVast', destroy: () => session?.destroy() }
|
||||
function dispose(): void {
|
||||
if (disposed)
|
||||
return
|
||||
disposed = true
|
||||
try {
|
||||
art.off('destroy', dispose)
|
||||
}
|
||||
finally { session?.destroy() }
|
||||
}
|
||||
if (closed())
|
||||
return result
|
||||
art.on('destroy', dispose)
|
||||
try {
|
||||
if (closed())
|
||||
return result
|
||||
const ima = await loadImaSdk()
|
||||
if (closed())
|
||||
return result
|
||||
const utils = (art.constructor as typeof Artplayer).utils
|
||||
session = createSession(art, ima, utils, closed)
|
||||
if (closed()) {
|
||||
dispose()
|
||||
return result
|
||||
}
|
||||
if (typeof callback === 'function')
|
||||
await callback(session.context)
|
||||
return result
|
||||
}
|
||||
catch (error) {
|
||||
try {
|
||||
dispose()
|
||||
}
|
||||
catch (cleanupError) { console.error('VAST cleanup error:', cleanupError) }
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { ImaSdk } from '@alugha/ima'
|
||||
import type { RequestConfig } from './types'
|
||||
|
||||
export { loadImaSdk, Player, PlayerOptions } from '@glomex/vast-ima-player'
|
||||
|
||||
export function createRequest(ima: ImaSdk, field: 'adTagUrl' | 'adsResponse', value: string, config: RequestConfig) {
|
||||
const request = new ima.AdsRequest()
|
||||
request[field] = value
|
||||
// Preserve the historical SDK extension surface, including inherited config fields.
|
||||
const fields = request as unknown as Record<string, unknown>
|
||||
for (const key in config)
|
||||
fields[key] = config[key]
|
||||
return request
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import type { ImaSdk } from '@alugha/ima'
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { Context, RequestConfig, Utilities } from './types'
|
||||
import { createRequest, Player, PlayerOptions } from './sdk'
|
||||
import { createContainer } from './view'
|
||||
|
||||
interface Owner {
|
||||
container: HTMLDivElement
|
||||
player: Player | null
|
||||
listeners: [string, EventListener][]
|
||||
playing: boolean
|
||||
released: boolean
|
||||
}
|
||||
|
||||
export function createSession(art: Artplayer, ima: ImaSdk, utils: Utilities, closed: () => boolean) {
|
||||
const { $video, $player } = art.template
|
||||
const adsRenderingSettings = new ima.AdsRenderingSettings()
|
||||
adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true
|
||||
adsRenderingSettings.enablePreloading = true
|
||||
const playerOptions = new PlayerOptions()
|
||||
let current: Owner | null = null
|
||||
let initializing = false
|
||||
let releasing = 0
|
||||
|
||||
const live = (owner: Owner) => current === owner && !owner.released && !closed()
|
||||
|
||||
function release(owner: Owner): void {
|
||||
if (current === owner)
|
||||
current = null
|
||||
owner.released = true
|
||||
owner.playing = false
|
||||
const player = owner.player
|
||||
owner.player = null
|
||||
const listeners = owner.listeners.splice(0)
|
||||
let failed = false
|
||||
let failure: unknown
|
||||
const attempt = (cleanup: () => void) => {
|
||||
try {
|
||||
cleanup()
|
||||
}
|
||||
catch (error) {
|
||||
if (!failed) {
|
||||
failed = true
|
||||
failure = error
|
||||
}
|
||||
else {
|
||||
console.error('VAST cleanup error:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
releasing++
|
||||
try {
|
||||
if (player) {
|
||||
for (const [name, callback] of listeners)
|
||||
attempt(() => player.removeEventListener(name, callback))
|
||||
attempt(() => player.destroy())
|
||||
}
|
||||
attempt(() => {
|
||||
if (owner.container.parentNode)
|
||||
owner.container.parentNode.removeChild(owner.container)
|
||||
})
|
||||
}
|
||||
finally {
|
||||
releasing--
|
||||
}
|
||||
if (failed)
|
||||
throw failure
|
||||
}
|
||||
|
||||
function init(): Player | null {
|
||||
if (closed() || releasing)
|
||||
return null
|
||||
if (initializing)
|
||||
throw new Error('VAST initialization is already in progress')
|
||||
if (current?.player)
|
||||
return current.player
|
||||
initializing = true
|
||||
let owner: Owner | undefined
|
||||
try {
|
||||
owner = { container: createContainer(utils), player: null, listeners: [], playing: false, released: false }
|
||||
current = owner
|
||||
if (!live(owner)) {
|
||||
release(owner)
|
||||
return null
|
||||
}
|
||||
$player.appendChild(owner.container)
|
||||
if (!live(owner)) {
|
||||
release(owner)
|
||||
return null
|
||||
}
|
||||
owner.player = new Player(ima, $video, owner.container, adsRenderingSettings, playerOptions)
|
||||
if (!live(owner)) {
|
||||
release(owner)
|
||||
return null
|
||||
}
|
||||
const allocated = owner
|
||||
for (const [name, playing] of [
|
||||
['AdContentPauseRequested', true],
|
||||
['AdContentResumeRequested', false],
|
||||
['AdStarted', true],
|
||||
['AdError', false],
|
||||
] as const) {
|
||||
const callback: EventListener = (event) => {
|
||||
if (!live(allocated))
|
||||
return
|
||||
if (name === 'AdError')
|
||||
console.error('VAST Ad Error:', (event as CustomEvent<unknown>).detail)
|
||||
allocated.playing = playing
|
||||
allocated.container.style.display = playing ? 'block' : 'none'
|
||||
}
|
||||
owner.listeners.push([name, callback])
|
||||
owner.player.addEventListener(name, callback)
|
||||
if (!live(owner)) {
|
||||
release(owner)
|
||||
return null
|
||||
}
|
||||
}
|
||||
return owner.player
|
||||
}
|
||||
catch (error) {
|
||||
if (owner) {
|
||||
try {
|
||||
release(owner)
|
||||
}
|
||||
catch (cleanupError) { console.error('VAST cleanup error:', cleanupError) }
|
||||
}
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
initializing = false
|
||||
}
|
||||
}
|
||||
|
||||
function play(field: 'adTagUrl' | 'adsResponse', value: string, config: RequestConfig): void {
|
||||
if (closed() || current?.playing)
|
||||
return
|
||||
const player = init()
|
||||
const owner = current
|
||||
if (!player || !owner)
|
||||
return
|
||||
const request = createRequest(ima, field, value, config)
|
||||
// Request constructors and config accessors may synchronously destroy or replace the session.
|
||||
if (live(owner) && owner.player === player)
|
||||
player.playAds(request)
|
||||
}
|
||||
|
||||
function destroy(): void {
|
||||
if (current)
|
||||
release(current)
|
||||
}
|
||||
|
||||
const context: Context = {
|
||||
art,
|
||||
playUrl: (url, config = {}) => play('adTagUrl', url, config),
|
||||
playRes: (response, config = {}) => play('adsResponse', response, config),
|
||||
init,
|
||||
ima,
|
||||
adsRenderingSettings,
|
||||
playerOptions,
|
||||
get imaPlayer() { return current?.player || null },
|
||||
get container() { return current?.container || null },
|
||||
}
|
||||
return { context, destroy }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { google, ImaSdk } from '@alugha/ima'
|
||||
import type { Player, PlayerOptions } from '@glomex/vast-ima-player'
|
||||
import type Artplayer from 'artplayer'
|
||||
|
||||
export type Utilities = Pick<typeof Artplayer.utils, 'createElement' | 'setStyles'>
|
||||
export type RequestConfig = Record<string, unknown>
|
||||
|
||||
export interface Context {
|
||||
art: Artplayer
|
||||
playUrl: (url: string, config?: RequestConfig) => void
|
||||
playRes: (response: string, config?: RequestConfig) => void
|
||||
init: () => Player | null
|
||||
ima: ImaSdk
|
||||
adsRenderingSettings: google.ima.AdsRenderingSettings
|
||||
playerOptions: PlayerOptions
|
||||
readonly imaPlayer: Player | null
|
||||
readonly container: HTMLDivElement | null
|
||||
}
|
||||
|
||||
export type Callback = (context: Context) => unknown
|
||||
export interface Result {
|
||||
name: 'artplayerPluginVast'
|
||||
destroy: () => void
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Utilities } from './types'
|
||||
|
||||
let nextId = 0
|
||||
|
||||
export function createContainer(utils: Utilities): HTMLDivElement {
|
||||
const container = utils.createElement('div')
|
||||
container.id = `art-vast-${Date.now()}-${++nextId}`
|
||||
utils.setStyles(container, {
|
||||
position: 'absolute',
|
||||
inset: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: '150',
|
||||
backgroundColor: 'black',
|
||||
display: 'none',
|
||||
pointerEvents: 'auto',
|
||||
})
|
||||
return container
|
||||
}
|
||||
Reference in new issue
Block a user