UNPKG

vp-outstream-player

Version:

Outstream video player with Google IMA integration

309 lines (262 loc) 7.73 kB
import { AdBreakConfig, PlayerCallbacks } from "../config/types"; // Turn multiple ad URLs into a "waterfall" XML export function getVAST_XML(waterfallArray: string[]): string { if (!Array.isArray(waterfallArray) || waterfallArray.length === 0) { return ""; } const startLine = `<VAST version="3.0">`; const endLine = `</VAST>`; let xml = startLine; let valid = false; waterfallArray.forEach((adUrl, index) => { if (adUrl.trim() === "") return; // Skip empty URLs valid = true; xml += `<Ad id="${index + 1}"> <Wrapper> <AdSystem>AD_SYSTEM</AdSystem> <VASTAdTagURI><![CDATA[${adUrl}]]></VASTAdTagURI> <Extensions> <Extension type="waterfall" fallback_index="${index}"/> </Extensions> </Wrapper> </Ad>`; }); xml += endLine; return valid ? xml : ""; } /** * This service class is in charge of: * - Initializing the Google IMA SDK * - Requesting ads using either single VAST or waterfall * - Firing callbacks when relevant */ export class VastService { private playerContainer: HTMLElement; private container: HTMLElement; private callbacks: PlayerCallbacks; private adsLoader?: google.ima.AdsLoader; private adsManager?: google.ima.AdsManager; private adDisplayContainer?: google.ima.AdDisplayContainer; private playing: boolean = false; private currentAdData?: google.ima.AdData; constructor(playerContainer: HTMLElement, callbacks: PlayerCallbacks) { this.playerContainer = playerContainer; this.container = this.playerContainer.querySelector(".vp-ad-container")!; this.callbacks = callbacks; } public loadAd(adBreak: AdBreakConfig, muted: boolean) { if (!this.isImaSdkLoaded()) { this.callbacks.onAdError?.(new Error("Google IMA SDK not loaded")); return; } // Build final adTagUrl, handle waterfall let adTagUrl = ""; let type = "url"; if (Array.isArray(adBreak.adTagUrl)) { adTagUrl = getVAST_XML(adBreak.adTagUrl); type = "xml"; } else { adTagUrl = adBreak.adTagUrl; } // If there's no valid adTagUrl, skip if (!adTagUrl) { this.callbacks.onAdError?.(new Error("No valid adTagUrl")); return; } // If the IMA SDK script isn't loaded yet, do that (or assume it is in index.html). // For brevity, we'll assume google.ima is available globally. // 0. Clear previous ad data this.resetAdData(); // 1. Create AdDisplayContainer this.adDisplayContainer = new window.google.ima.AdDisplayContainer(this.container); // Check for proper method if (!this.adDisplayContainer || !this.adDisplayContainer.initialize) { this.callbacks.onAdError?.(new Error("AdDisplayContainer not created")); return; } this.adDisplayContainer.initialize?.(); // 2. Create AdsLoader this.adsLoader = new window.google.ima.AdsLoader(this.adDisplayContainer); // Check for proper method if (!this.adsLoader || !this.adsLoader.requestAds) { this.callbacks.onAdError?.(new Error("AdsLoader not created")); return; } // Listen for adsManagerLoaded or error events this.adsLoader.addEventListener( window.google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, (evt: any) => this.onAdsManagerLoaded(evt, muted) ); this.adsLoader.addEventListener(window.google.ima.AdErrorEvent.Type.AD_ERROR, (evt: any) => { this.callbacks.onAdError?.(evt.getError()); }); // 3. Build AdsRequest const adsRequest = new window.google.ima.AdsRequest(); if (type === "xml") { adsRequest.adsResponse = adTagUrl; } else { adsRequest.adTagUrl = adTagUrl; } adsRequest.linearAdSlotWidth = this.container.offsetWidth; adsRequest.linearAdSlotHeight = this.container.offsetHeight; adsRequest.setAdWillPlayMuted(muted); this.callbacks.onAdRequested?.(); this.adsLoader.requestAds(adsRequest); } private isImaSdkLoaded(): boolean { return !!window.google && !!window.google.ima; } private onAdsManagerLoaded(evt: google.ima.AdsManagerLoadedEvent, muted: boolean) { this.adsManager = evt.getAdsManager(); // Subscribe to adsManager events this.adsManager.addEventListener( window.google.ima.AdEvent.Type.LOADED, this.onAdLoaded.bind(this) ); this.adsManager.addEventListener( window.google.ima.AdEvent.Type.STARTED, this.onAdStarted.bind(this) ); this.adsManager.addEventListener( window.google.ima.AdEvent.Type.AD_PROGRESS, this.onTimeUpdate.bind(this) ); this.adsManager.addEventListener( window.google.ima.AdEvent.Type.SKIPPED, this.onAdSkipped?.bind(this) ); this.adsManager.addEventListener( window.google.ima.AdEvent.Type.COMPLETE, this.onadComplete.bind(this) ); this.adsManager.addEventListener( window.google.ima.AdErrorEvent.Type.AD_ERROR, this.onAdError.bind(this) ); this.adsManager.addEventListener( window.google.ima.AdEvent.Type.RESUMED, this.onAdResume.bind(this) ); this.adsManager.addEventListener( window.google.ima.AdEvent.Type.PAUSED, this.onAdPause.bind(this) ); try { // Initialize and start ad this.adsManager.init( this.container.offsetWidth, this.container.offsetHeight, window.google.ima.ViewMode.NORMAL ); this.adsManager.setVolume(muted ? 0 : 1); this.adsManager.start(); } catch (err) { this.callbacks.onAdError?.(new Error("AdsManager Error: " + err)); } } public setMuted(mute: boolean) { if (this.adsManager) { this.adsManager.setVolume(mute ? 0 : 1); } } private resetAdData() { this.currentAdData = undefined; if (this.adsManager) { this.adsManager.destroy(); this.adsManager = undefined; } if (this.adsLoader) { this.adsLoader.destroy(); this.adsLoader = undefined; } if (this.adDisplayContainer) { this.adDisplayContainer.destroy(); this.adDisplayContainer = undefined; } this.playing = false; } private onAdLoaded() { this.callbacks.onAdLoaded?.(); } private onAdStarted() { this.playing = true; this.callbacks.onAdStarted?.(); } private onTimeUpdate(event: google.ima.AdEvent) { const adData = event.getAdData(); if (!adData) return; this.currentAdData = adData; if (adData.currentTime === undefined) return; this.callbacks.onAdTimeUpdate?.(adData.currentTime); } private onAdSkipped() { this.resetAdData(); this.callbacks.onAdSkipped?.(); } private onadComplete() { this.resetAdData(); this.callbacks.onAdComplete?.(); } private onAdError(err: Error) { this.resetAdData(); this.callbacks.onAdError?.(err); } private onAdResume() { this.playing = true; this.callbacks.onAdResume?.(); } private onAdPause() { this.playing = false; this.callbacks.onAdPause?.(); } public pauseAd() { if (this.adsManager) { this.adsManager.pause(); this.playing = false; } } public resumeAd() { if (this.adsManager) { this.adsManager.resume(); this.playing = true; } } public setVolume(volume: number) { if (this.adsManager) { this.adsManager.setVolume(volume); } } public skipAd(): void { if (this.adsManager) { this.adsManager.skip(); this.playing = false; } } get isPlaying(): boolean { return this.playing; } get isMuted(): boolean { return this.adsManager ? this.adsManager.getVolume() === 0 : false; } get volume(): number { return this.adsManager ? this.adsManager.getVolume() : 0; } get currentTime(): number { return this.currentAdData ? this.currentAdData.currentTime : 0; } get duration(): number { return this.currentAdData ? this.currentAdData.duration : 0; } get adData(): google.ima.AdData | undefined { return this.currentAdData; } public destroy() { if (this.adsManager) { this.adsManager.destroy(); this.adsManager = undefined; } if (this.adsLoader) { this.adsLoader = undefined; } } }