unified-video-framework
Version:
Cross-platform video player framework supporting iOS, Android, Web, Smart TVs (Samsung/LG), Roku, and more
405 lines (398 loc) • 16.1 kB
JavaScript
export class GoogleAdsManager {
constructor(video, adContainer, config) {
this.adsManager = null;
this.adsLoader = null;
this.adDisplayContainer = null;
this.isAdPlaying = false;
this.isMuted = true;
this.unmuteButton = null;
this.video = video;
this.adContainer = adContainer;
this.config = config;
this.setupFocusHandler();
}
setupFocusHandler() {
window.addEventListener('focus', () => {
if (this.isAdPlaying) {
console.log('Window focused - resuming ad playback');
this.resumeAdPlayback();
setTimeout(() => this.resumeAdPlayback(), 200);
}
});
document.addEventListener('visibilitychange', () => {
if (!document.hidden && this.isAdPlaying) {
console.log('Tab became visible - resuming ad playback');
this.resumeAdPlayback();
}
});
}
async initialize() {
try {
await this.loadIMASDK();
this.setupAdsLoader();
}
catch (error) {
console.error('Failed to initialize Google Ads:', error);
this.config.onAdError?.(error);
}
}
loadIMASDK() {
return new Promise((resolve, reject) => {
if (window.google?.ima) {
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://imasdk.googleapis.com/js/sdkloader/ima3.js';
script.async = true;
script.onload = () => resolve();
script.onerror = () => reject(new Error('Failed to load Google IMA SDK'));
document.head.appendChild(script);
});
}
setupAdsLoader() {
const google = window.google;
this.adDisplayContainer = new google.ima.AdDisplayContainer(this.adContainer, this.video);
this.adsLoader = new google.ima.AdsLoader(this.adDisplayContainer);
this.adsLoader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, (event) => this.onAdsManagerLoaded(event), false);
this.adsLoader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (event) => this.onAdError(event), false);
this.video.addEventListener('ended', () => {
if (!this.isAdPlaying) {
this.adsLoader?.contentComplete();
}
});
}
requestAds() {
const google = window.google;
try {
const adsRequest = new google.ima.AdsRequest();
adsRequest.adTagUrl = this.config.adTagUrl;
adsRequest.linearAdSlotWidth = this.video.clientWidth;
adsRequest.linearAdSlotHeight = this.video.clientHeight;
adsRequest.nonLinearAdSlotWidth = this.video.clientWidth;
adsRequest.nonLinearAdSlotHeight = Math.floor(this.video.clientHeight / 3);
if (this.config.companionAdSlots && this.config.companionAdSlots.length > 0) {
const companionAdSlots = this.config.companionAdSlots.map(slot => {
return new google.ima.CompanionAdSelectionSettings();
});
}
adsRequest.setAdWillAutoPlay(true);
adsRequest.setAdWillPlayMuted(true);
this.adsLoader.requestAds(adsRequest);
}
catch (error) {
console.error('Error requesting ads:', error);
this.config.onAdError?.(error);
}
}
initAdDisplayContainer() {
try {
this.adDisplayContainer?.initialize();
}
catch (error) {
console.warn('Ad display container already initialized');
}
}
onAdsManagerLoaded(event) {
const google = window.google;
const adsRenderingSettings = new google.ima.AdsRenderingSettings();
adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true;
adsRenderingSettings.enablePreloading = true;
adsRenderingSettings.mute = this.video.muted;
this.adsManager = event.getAdsManager(this.video, adsRenderingSettings);
try {
const cuePoints = this.adsManager.getCuePoints();
if (cuePoints && cuePoints.length > 0) {
const allCuePoints = cuePoints.map((time) => {
if (time === 0)
return 0;
if (time === -1)
return -1;
return time;
});
console.log('📍 Ad cue points detected (pre/mid/post):', allCuePoints);
if (this.config.onAdCuePoints) {
this.config.onAdCuePoints(allCuePoints);
}
}
}
catch (error) {
console.warn('Could not extract ad cue points:', error);
}
this.setupAdsManagerListeners();
try {
this.adsManager.init(this.video.clientWidth, this.video.clientHeight, google.ima.ViewMode.NORMAL);
this.adsManager.start();
}
catch (error) {
console.error('Error starting ads:', error);
this.video.play().catch(() => { });
}
}
setupAdsManagerListeners() {
const google = window.google;
this.adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, () => {
console.log('Ad: Content paused');
this.isAdPlaying = true;
this.video.pause();
if (this.adContainer) {
this.adContainer.style.visibility = 'visible';
this.adContainer.style.opacity = '1';
this.adContainer.style.pointerEvents = 'auto';
this.adContainer.style.zIndex = '2147483647';
console.log('✅ Ad container visibility enforced');
}
const preventPlayDuringAd = (e) => {
if (this.isAdPlaying) {
e.preventDefault();
this.video.pause();
console.warn('Blocked video play attempt during ad');
}
};
this.video.addEventListener('play', preventPlayDuringAd);
this.video.__adPlayBlocker = preventPlayDuringAd;
this.config.onAdStart?.();
});
this.adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, () => {
console.log('Ad: Content resume');
this.isAdPlaying = false;
const preventPlayDuringAd = this.video.__adPlayBlocker;
if (preventPlayDuringAd) {
this.video.removeEventListener('play', preventPlayDuringAd);
delete this.video.__adPlayBlocker;
}
this.config.onAdEnd?.();
this.video.play().catch(() => { });
});
this.adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, (event) => {
const ad = event.getAd();
console.log('Ad started:', {
type: ad.isLinear() ? 'Linear (video)' : 'Non-linear (overlay)',
duration: ad.getDuration(),
skippable: ad.getSkipTimeOffset() !== -1,
title: ad.getTitle(),
});
this.isMuted = this.video.muted;
console.log(`Ad started - video.muted=${this.video.muted}, isMuted=${this.isMuted}`);
if (this.isMuted) {
this.showUnmuteButton();
}
});
this.adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, () => {
console.log('Ad completed');
});
this.adsManager.addEventListener(google.ima.AdEvent.Type.ALL_ADS_COMPLETED, () => {
console.log('All ads completed');
this.config.onAllAdsComplete?.();
});
this.adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (event) => this.onAdError(event));
this.adsManager.addEventListener(google.ima.AdEvent.Type.SKIPPED, () => {
console.log('Ad skipped by user');
});
this.adsManager.addEventListener(google.ima.AdEvent.Type.PAUSED, () => {
console.log('Ad paused (likely from click-through)');
});
this.adsManager.addEventListener(google.ima.AdEvent.Type.PLAYING, () => {
console.log('Ad resumed playing');
});
}
onAdError(event) {
const error = event.getError?.();
console.error('Ad error:', error?.getMessage?.() || error);
this.config.onAdError?.(error);
if (this.adsManager) {
this.adsManager.destroy();
}
this.isAdPlaying = false;
this.video.play().catch(() => { });
}
pause() {
if (this.adsManager && this.isAdPlaying) {
this.adsManager.pause();
}
}
resume() {
if (this.adsManager && this.isAdPlaying) {
this.adsManager.resume();
}
}
skip() {
if (this.adsManager) {
this.adsManager.skip();
}
}
resize(width, height, viewMode) {
const google = window.google;
if (this.adsManager && google && google.ima) {
const mode = viewMode || google.ima.ViewMode.NORMAL;
console.log(`📐 Resizing ads: ${width}x${height}, ViewMode: ${mode === google.ima.ViewMode.FULLSCREEN ? 'FULLSCREEN' : 'NORMAL'}`);
if (this.adContainer && mode === google.ima.ViewMode.FULLSCREEN) {
this.adContainer.style.position = 'fixed';
this.adContainer.style.top = '0';
this.adContainer.style.left = '0';
this.adContainer.style.width = `${width}px`;
this.adContainer.style.height = `${height}px`;
this.adContainer.style.zIndex = '2147483647';
console.log('✅ Ad container forced to fullscreen dimensions');
}
this.adsManager.resize(width, height, mode);
}
}
setVolume(volume) {
if (this.adsManager) {
this.adsManager.setVolume(volume);
}
}
isPlayingAd() {
return this.isAdPlaying;
}
showUnmuteButton() {
if (this.unmuteButton) {
this.unmuteButton.remove();
}
this.unmuteButton = document.createElement('button');
this.unmuteButton.id = 'ad-unmute-btn';
this.unmuteButton.className = 'uvf-unmute-btn';
this.unmuteButton.setAttribute('aria-label', 'Tap to unmute ad');
this.unmuteButton.innerHTML = `
<svg viewBox="0 0 24 24" class="uvf-unmute-icon">
<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>
</svg>
<span class="uvf-unmute-text">Tap to unmute</span>
`;
this.unmuteButton.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleAdMute();
});
if (!document.getElementById('uvf-unmute-styles')) {
const style = document.createElement('style');
style.id = 'uvf-unmute-styles';
style.textContent = `
.uvf-unmute-btn {
position: absolute !important;
bottom: 80px !important;
left: 20px !important;
z-index: 1000 !important;
display: flex !important;
align-items: center !important;
gap: 8px !important;
padding: 12px 16px !important;
background: rgba(0, 0, 0, 0.8) !important;
border: none !important;
border-radius: 4px !important;
color: white !important;
font-size: 14px !important;
font-weight: 500 !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
backdrop-filter: blur(10px) !important;
-webkit-backdrop-filter: blur(10px) !important;
animation: uvf-unmute-pulse 2s ease-in-out infinite !important;
}
.uvf-unmute-btn:hover {
background: rgba(0, 0, 0, 0.9) !important;
transform: scale(1.05) !important;
}
.uvf-unmute-btn:active {
transform: scale(0.95) !important;
}
.uvf-unmute-icon {
width: 20px !important;
height: 20px !important;
fill: white !important;
}
.uvf-unmute-text {
white-space: nowrap !important;
}
uvf-unmute-pulse {
0%, 100% {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
}
50% {
box-shadow: 0 2px 16px rgba(255, 255, 255, 0.2) !important;
}
}
(max-width: 767px) {
.uvf-unmute-btn {
bottom: 70px !important;
left: 50% !important;
transform: translateX(-50%) !important;
padding: 10px 14px !important;
font-size: 13px !important;
}
.uvf-unmute-btn:hover {
transform: translateX(-50%) scale(1.05) !important;
}
}
`;
document.head.appendChild(style);
}
this.adContainer.appendChild(this.unmuteButton);
console.log('Unmute button displayed (matching player style)');
}
toggleAdMute() {
this.isMuted = !this.isMuted;
if (this.adsManager) {
this.adsManager.setVolume(this.isMuted ? 0 : 1);
console.log(`Ad ${this.isMuted ? 'muted' : 'unmuted'}`);
}
this.video.muted = this.isMuted;
if (!this.isMuted && this.unmuteButton) {
this.unmuteButton.remove();
this.unmuteButton = null;
}
}
resumeAdPlayback() {
try {
if (!this.adsManager || !this.isAdPlaying) {
return;
}
console.log('Attempting to resume ad playback...');
try {
this.adsManager.resume();
console.log('✅ Ad resume() called');
}
catch (e) {
console.warn('resume() failed:', e);
}
if (this.video) {
try {
if (this.video.paused) {
const playPromise = this.video.play();
if (playPromise) {
playPromise.catch((err) => {
console.warn('Video play() failed:', err);
});
}
console.log('✅ Video play() called');
}
}
catch (e) {
console.warn('Video play failed:', e);
}
}
}
catch (error) {
console.error('Error resuming ad playback:', error);
}
}
hideUnmuteButton() {
if (this.unmuteButton) {
this.unmuteButton.remove();
this.unmuteButton = null;
}
}
destroy() {
this.hideUnmuteButton();
if (this.adsManager) {
this.adsManager.destroy();
this.adsManager = null;
}
if (this.adsLoader) {
this.adsLoader.destroy();
this.adsLoader = null;
}
this.isAdPlaying = false;
}
}
//# sourceMappingURL=GoogleAdsManager.js.map