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, workspaceMode: boolean) { const { $video, $player } = art.template const adsRenderingSettings = new ima.AdsRenderingSettings() if (workspaceMode) { adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true adsRenderingSettings.enablePreloading = true } const playerOptions = new PlayerOptions() let current: Owner | null = null let initializing = false let releasing = 0 const context: Context = { art, playUrl: (url, config = {}) => play('adTagUrl', url, config), playRes: (response, config = {}) => play('adsResponse', response, config), init, ima, adsRenderingSettings, playerOptions, imaPlayer: null, id: null, $container: null, get container() { return current?.container || null }, } if (workspaceMode) { Object.defineProperties(context, { imaPlayer: { get: () => current?.player || null }, id: { get: () => current?.container.id || null }, $container: { get: () => current?.container || null }, }) } 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, workspaceMode), 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 const events = workspaceMode ? [ ['AdContentPauseRequested', true], ['AdContentResumeRequested', false], ['AdStarted', true], ['AdError', false], ] as const : [] for (const [name, playing] of events) { const callback: EventListener = (event) => { if (!live(allocated)) return if (name === 'AdError') console.error('VAST Ad Error:', (event as CustomEvent).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 } } if (!workspaceMode) { // Published fields are writable data properties, not the workspace getters. Object.defineProperties(context, { imaPlayer: { value: owner.player }, id: { value: owner.container.id }, $container: { value: owner.container }, }) } 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() || (workspaceMode && 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) } return { context, destroy } }