vp-outstream-player
Version:
Outstream video player with Google IMA integration
309 lines (262 loc) • 7.73 kB
text/typescript
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;
}
}
}