UNPKG

npaw-plugin-adapters

Version:
929 lines (833 loc) 29.2 kB
/* global tv */ export default class FreewheelAdsAdapter { getVersion() { return '7.0.2-freewheel-ads-js'; } getPlayhead() { // Try to get playhead from Freewheel ad instance first var playhead = this._getAdInstanceMethodCall('getPlayheadTime'); // If that returns null/0, try to get it from the video element if (!playhead || playhead === 0) { // Try to get from video element var videoElement = this._getVideoElement(); if (videoElement && videoElement.currentTime > 0) { return videoElement.currentTime; } // Last resort: check if we have a saved playhead position if (this._adPlayhead !== undefined) { return this._adPlayhead; } } return playhead; } getDuration() { return this._getAdInstanceMethodCall('getDuration'); } getTitle() { try { if (this.titles) { return this.titles[this._getAdNumber()]._adId; } } catch (err) { // Ignore errors and return default } return 'unknown'; } getResource() { var ret = null; var creativeRendition = this._getAdInstanceMethodCall('getActiveCreativeRendition'); if (creativeRendition) { if (typeof creativeRendition.getPrimaryCreativeRenditionAsset === 'function') { const asset = creativeRendition.getPrimaryCreativeRenditionAsset(); if (asset && typeof asset.getUrl === 'function') { ret = asset.getUrl(); } } else if (typeof creativeRendition.getWrapperUrl === 'function') { ret = creativeRendition.getWrapperUrl(); } } return ret; } getPlayerVersion() { // Try multiple ways to get the Freewheel SDK version try { // Method 1: Direct SDK version property if (typeof tv !== 'undefined' && tv.freewheel && tv.freewheel.SDK) { if (tv.freewheel.SDK.version) { return tv.freewheel.SDK.version; } // Method 2: Try VERSION constant if (tv.freewheel.SDK.VERSION) { return tv.freewheel.SDK.VERSION; } // Method 3: Try getVersion method if available if (typeof tv.freewheel.SDK.getVersion === 'function') { return tv.freewheel.SDK.getVersion(); } } // Method 4: Try to get from context if available if (this.player && this.player.currentAdContext) { var context = this.player.currentAdContext; if (context._version) return context._version; if (typeof context.getVersion === 'function') return context.getVersion(); } } catch (err) { // Ignore errors and return null } return null; } getPlayerName() { return 'Freewheel'; } getPosition() { return this.position; } getAdInstertionType() { return this.getNpawReference().Constants.AdInsertionType.ClientSide; } getIsVisible() { if (!this.contentPlayer) { for (var key in this.player) { var element = this.player[key]; if (element && element.videoHeight && element.clientHeight) { this.contentPlayer = element; break; } } } return this.getNpawUtils().calculateAdViewability(this.contentPlayer); } getAudioEnabled() { for (var key in this.player) { var element = this.player[key]; if (element && element.adManager && element.adManager._context && element.adManager._context.getAdVolume) { return !!element.adManager._context.getAdVolume(); } } } getGivenAds() { return this.slot.getAdCount(); } getBreaksTime() { var slots = this.getSlots(); if (!slots) return null; var times = []; try { for (var slotindex in slots) { var slot = slots[slotindex]; if (slot && typeof slot.getAdCount === 'function' && slot.getAdCount() > 0) { var ads = typeof slot.getAdInstances === 'function' ? slot.getAdInstances() : null; if (ads) { for (var adindex in ads) { // Check if ad has creative renditions and is a video ad var ad = ads[adindex]; if ( ad && ad._creativeRenditions && ad._creativeRenditions.length > 0 && ad._creativeRenditions[0]._baseUnit === 'video' ) { var position = typeof slot.getTimePosition === 'function' ? slot.getTimePosition() : 0; // Try to get content duration to cap postroll times var video = this.getVideo(); var contentDuration = video && typeof video.getDuration === 'function' ? video.getDuration() : null; if (contentDuration && position > contentDuration) { position = contentDuration; } times.push(position); break; } } } } } } catch (err) { // Ignore errors and return what we have } return times.length > 0 ? times : null; } getGivenBreaks() { var slots = this.getSlots(); if (!slots) return null; var breaks = 0; try { for (var slotindex in slots) { var slot = slots[slotindex]; if (slot && typeof slot.getAdCount === 'function' && slot.getAdCount() > 0) { var ads = typeof slot.getAdInstances === 'function' ? slot.getAdInstances() : null; if (ads) { for (var adindex in ads) { var ad = ads[adindex]; if (this._isVideoAd(ad)) { breaks++; break; } } } } } } catch (err) { // Ignore errors } return breaks > 0 ? breaks : null; } getExpectedBreaks() { // In Freewheel, expected breaks should match the pattern // We calculate it from the pattern to ensure consistency var pattern = this.getExpectedPattern(); if (!pattern) return null; var breaks = 0; if (pattern.pre && pattern.pre.length > 0) breaks += pattern.pre.length; if (pattern.mid && pattern.mid.length > 0) breaks += pattern.mid.length; if (pattern.post && pattern.post.length > 0) breaks += pattern.post.length; return breaks > 0 ? breaks : null; } getExpectedPattern() { // Returns the pattern of ad breaks: {pre: [ads_count], mid: [ads_count, ...], post: [ads_count]} var slots = this.getSlots(); if (!slots) return null; var pattern = { pre: [], mid: [], post: [] }; try { for (var slotindex in slots) { var slot = slots[slotindex]; if (!slot || typeof slot.getTimePositionClass !== 'function') continue; var timeClass = slot.getTimePositionClass(); if (!timeClass) continue; var cleanTimeClass = String(timeClass).trim().toUpperCase(); var adCount = 0; var ads = typeof slot.getAdInstances === 'function' ? slot.getAdInstances() : null; // Count video ads in this slot if (ads) { for (var adindex in ads) { if (this._isVideoAd(ads[adindex])) { adCount++; } } } // Only add if there are video ads in this slot if (adCount > 0) { // Add to the appropriate position array if (cleanTimeClass === 'PREROLL') { pattern.pre.push(adCount); } else if (cleanTimeClass === 'MIDROLL') { pattern.mid.push(adCount); } else if (cleanTimeClass === 'POSTROLL') { pattern.post.push(adCount); } } } } catch (err) { // Ignore errors } // Only return pattern if at least one position has breaks if (pattern.pre.length === 0 && pattern.mid.length === 0 && pattern.post.length === 0) { return null; } return pattern; } getExpectedAds() { // Returns the expected number of ads for the current break (slot) if (!this.slot) return null; var adCount = 0; try { var ads = typeof this.slot.getAdInstances === 'function' ? this.slot.getAdInstances() : null; if (ads) { for (var adindex in ads) { if (this._isVideoAd(ads[adindex])) { adCount++; } } } } catch (err) { // Ignore errors } return adCount > 0 ? adCount : null; } /** * Helper method to check if an ad instance is a video ad * @param {Object} ad - Freewheel ad instance * @returns {boolean} - True if the ad is a video ad */ _isVideoAd(ad) { if (!ad) return false; try { // Check for video base unit in creative renditions if ( ad._creativeRenditions && ad._creativeRenditions.length > 0 && ad._creativeRenditions[0]._baseUnit === 'video' ) { return true; } // Alternative check using getActiveCreativeRendition if (typeof ad.getActiveCreativeRendition === 'function') { var rendition = ad.getActiveCreativeRendition(); if (rendition && (rendition._baseUnit === 'video' || rendition.baseUnit === 'video')) { return true; } } } catch (err) { // Ignore errors } return false; } getCreativeId() { var ret = null; if (this.slot) { var instances = this.slot.getAdInstances(); var adnumber = this._getAdNumber(); if (instances && instances[adnumber]) { ret = instances[adnumber]._creativeId; } } return ret; } getSlots() { try { // Method 1: Try to get from currentAdContext if (this.player && this.player.currentAdContext) { if ( this.player.currentAdContext._adResponse && typeof this.player.currentAdContext._adResponse.getTemporalSlots === 'function' ) { return this.player.currentAdContext._adResponse.getTemporalSlots(); } // Try getTemporalSlots directly on context if (typeof this.player.currentAdContext.getTemporalSlots === 'function') { return this.player.currentAdContext.getTemporalSlots(); } } // Method 2: Try to find _adResponse in player object if (this.player) { for (var key in this.player) { var element = this.player[key]; if (element && element._adResponse && typeof element._adResponse.getTemporalSlots === 'function') { return element._adResponse.getTemporalSlots(); } } } // Method 3: Try to get from the current slot if available if (this.slot && this.slot._parentSlots) { return this.slot._parentSlots; } } catch (err) { // Ignore errors } return null; } _getAdNumber() { return (this.getVideo().requestBuilder.lastSent.adNumber || 1) - 1; } _getAdInstanceMethodCall(methdodName) { var ret = null; if (this.slot) { var instances = this.slot.getAdInstances(); var adNumber = this._getAdNumber(); if (instances && instances[adNumber] && typeof instances[adNumber][methdodName] === 'function') { ret = instances[adNumber][methdodName](); } } return ret; } registerListeners() { this.canRemoveListeners = true; this.events = tv.freewheel.SDK; this.manifestNoResponse = [this.events.ERROR_SECURITY, this.events.ERROR_TIMEOUT]; this.manifestEmpty = [this.events.ERROR_NO_AD_AVAILABLE, this.events.ERROR_VAST_NO_AD]; this.manifestWrong = [ this.events.ERROR_VAST_VERSION_NOT_SUPPORTED, this.events.ERROR_VAST_WRAPPER_LIMIT_REACH, this.events.ERROR_VAST_XML_PARSING, this.events.ERROR_PARSE ]; this.ignoredErrors = [this.events.ERROR_ADINSTANCE_UNAVAILABLE]; this.references = {}; this.references[this.events.EVENT_AD] = this.logListener.bind(this); this.references[this.events.EVENT_ERROR] = this.errorListener.bind(this); this.references[this.events.EVENT_SLOT_STARTED] = this.slotListener.bind(this); this.references[this.events.EVENT_SLOT_ENDED] = this.closeListener.bind(this); for (var key in this.manifestNoResponse) { this.references[key] = this.noResponseManifestListener.bind(this); } for (var key2 in this.manifestEmpty) { this.references[key2] = this.manifestEmptyListener.bind(this); } for (var key3 in this.manifestWrong) { this.references[key3] = this.manifestWrongListener.bind(this); } for (var key4 in this.references) { this.player.currentAdContext.addEventListener(key4, this.references[key4]); } } unregisterListeners() { if (this.player && this.references) { for (var key in this.references) { this.player.currentAdContext.removeEventListener(key, this.references[key]); } this.references = {}; } } noResponseManifestListener() { this.fireManifest(this.getNpawReference().Constants.ManifestError.NO_RESPONSE, 'No response'); } manifestEmptyListener() { this.fireManifest(this.getNpawReference().Constants.ManifestError.EMPTY, 'Empty manifest'); } manifestWrongListener() { this.fireManifest(this.getNpawReference().Constants.ManifestError.WRONG, 'Wrong manifest format'); } logListener(e) { this.log.debug(e.subType); if (e.errorCode || e.errorInfo || e.errorModule) { this.errorListener(e); return; } switch (e.subType) { case this.events.EVENT_AD_BUFFERING_START: this._onAdBufferStart(); break; case this.events.EVENT_AD_BUFFERING_END: this._onAdBufferEnd(); break; case this.events.EVENT_AD_PAUSE: this.pauseListener(e); break; case this.events.EVENT_AD_RESUME: this.resumeListener(e); break; case this.events.EVENT_AD_IMPRESSION_END: this.endedListener(e); break; case this.events.EVENT_AD_INITIATED: this.playListener(e); break; case this.events.EVENT_AD_IMPRESSION: this.playingListener(e); break; case this.events.EVENT_AD_SKIPPED: this.skipListener(e); break; case this.events.EVENT_SLOT_STARTED: this.slotListener(e); break; case this.events.EVENT_AD_CLOSE: case this.events.EVENT_SLOT_ENDED: this.closeListener(e); break; case this.events.EVENT_AD_CLICK: this.clickListener(e); break; case this.events.EVENT_AD_FIRST_QUARTILE: this.fireQuartile(1); break; case this.events.EVENT_AD_MIDPOINT: this.fireQuartile(2); break; case this.events.EVENT_AD_THIRD_QUARTILE: this.fireQuartile(3); break; case this.events.ERROR_SECURITY: case this.events.ERROR_TIMEOUT: this.noResponseManifestListener(e); break; case this.events.ERROR_NO_AD_AVAILABLE: case this.events.ERROR_VAST_NO_AD: this.manifestEmptyListener(e); break; case this.events.ERROR_VAST_VERSION_NOT_SUPPORTED: case this.events.ERROR_VAST_WRAPPER_LIMIT_REACH: case this.events.ERROR_VAST_XML_PARSING: case this.events.ERROR_PARSE: this.manifestWrongListener(e); break; } } slotListener(e) { this.slot = e.slot; var adapter = this.getVideo().getAdapter(); this.disable = false; const positionValue = this.slot.getTimePositionClass(); const cleanPositionValue = positionValue ? String(positionValue).trim().toUpperCase() : ''; switch (cleanPositionValue) { case 'POSTROLL': this.position = this.getNpawReference().Constants.AdPosition.Postroll; break; case 'MIDROLL': this.position = this.getNpawReference().Constants.AdPosition.Midroll; break; case 'PREROLL': if (adapter && adapter.flags.isJoined) { this.position = this.getNpawReference().Constants.AdPosition.Midroll; } else { this.position = this.getNpawReference().Constants.AdPosition.Preroll; } break; default: this.disable = true; } if (!this.disable) { this.titles = this.slot.getAdInstances(); this.unregister(); this.getVideo().fireInit(); this.fireInit(); } } playListener() { if (!this.disable) { this.fireStart(); } } playingListener() { if (!this.disable) { this.fireStart(); this.fireJoin(); // Only fire resume if we were paused (not on initial join) if (this.flags && this.flags.isPaused) { this.fireResume(); } // Start tracking ad playhead this._startAdPlayheadTracking(); // Start monitoring for buffer events this._startBufferMonitoring(); } } pauseListener() { if (!this.disable && this.flags && this.flags.isJoined) { this._stopAdPlayheadTracking(); // Don't stop buffer monitoring during pause this.firePause(); } } resumeListener() { if (!this.disable && this.flags && this.flags.isPaused) { this.fireResume(); this._startAdPlayheadTracking(); // Resume buffer monitoring after pause this._startBufferMonitoring(); } } endedListener() { this._stopAdPlayheadTracking(); this._stopBufferMonitoring(); this.fireStop(); } _startAdPlayheadTracking() { // Clear any existing tracking this._stopAdPlayheadTracking(); // Initialize playhead this._adPlayhead = 0; this._adPlayheadStartTime = Date.now(); // Update playhead every 100ms this._adPlayheadInterval = setInterval(() => { if (this._adPlayheadStartTime) { this._adPlayhead = (Date.now() - this._adPlayheadStartTime) / 1000; } }, 100); } _stopAdPlayheadTracking() { if (this._adPlayheadInterval) { clearInterval(this._adPlayheadInterval); this._adPlayheadInterval = null; } } _startBufferMonitoring() { // Stop any existing monitoring this._stopBufferMonitoring(); // Find the video element using the improved method this._adVideoElement = this._getVideoElement(); if (this._adVideoElement) { // Create bound handlers this._bufferWaitingHandler = this._onAdBufferStart.bind(this); this._bufferPlayingHandler = this._onAdBufferEnd.bind(this); this._bufferStalledHandler = this._onAdBufferStart.bind(this); // Listen for buffer events this._adVideoElement.addEventListener('waiting', this._bufferWaitingHandler); this._adVideoElement.addEventListener('stalled', this._bufferStalledHandler); this._adVideoElement.addEventListener('playing', this._bufferPlayingHandler); this._adVideoElement.addEventListener('canplay', this._bufferPlayingHandler); this._adVideoElement.addEventListener('canplaythrough', this._bufferPlayingHandler); } } _stopBufferMonitoring() { if (this._adVideoElement) { if (this._bufferWaitingHandler) { this._adVideoElement.removeEventListener('waiting', this._bufferWaitingHandler); } if (this._bufferStalledHandler) { this._adVideoElement.removeEventListener('stalled', this._bufferStalledHandler); } if (this._bufferPlayingHandler) { this._adVideoElement.removeEventListener('playing', this._bufferPlayingHandler); this._adVideoElement.removeEventListener('canplay', this._bufferPlayingHandler); this._adVideoElement.removeEventListener('canplaythrough', this._bufferPlayingHandler); } } this._bufferWaitingHandler = null; this._bufferStalledHandler = null; this._bufferPlayingHandler = null; this._adVideoElement = null; this._isBuffering = false; } _onAdBufferStart() { // Only fire buffer if we're joined (playing) and not already buffering if (!this._isBuffering && this.flags && this.flags.isJoined) { this._isBuffering = true; this._bufferStartTime = Date.now(); this.fireBufferBegin(); } } _onAdBufferEnd() { if (this._isBuffering) { this._isBuffering = false; if (this._bufferStartTime) { this._lastBufferDuration = Date.now() - this._bufferStartTime; } this.fireBufferEnd(); } } getBufferDuration() { // Return the last buffer duration in milliseconds return this._lastBufferDuration || null; } getBitrate() { // Try to get bitrate from Freewheel SDK var creativeRendition = this._getAdInstanceMethodCall('getActiveCreativeRendition'); if (creativeRendition) { try { // Try asset bitrate if (typeof creativeRendition.getPrimaryCreativeRenditionAsset === 'function') { var asset = creativeRendition.getPrimaryCreativeRenditionAsset(); if (asset) { if (typeof asset.getBitrate === 'function') { var bitrate = asset.getBitrate(); if (bitrate > 0) return bitrate; } if (asset.bitrate > 0) return asset.bitrate; if (asset._bitrate > 0) return asset._bitrate; } } // Try creative rendition bitrate if (typeof creativeRendition.getBitrate === 'function') { var br = creativeRendition.getBitrate(); if (br > 0) return br; } if (creativeRendition.bitrate > 0) return creativeRendition.bitrate; if (creativeRendition._bitrate > 0) return creativeRendition._bitrate; } catch (err) { // Continue to fallback } } // Fallback: Calculate bitrate from video element's decoded bytes var videoElement = this._getVideoElement(); if (videoElement && videoElement.webkitVideoDecodedByteCount) { var currentBytes = videoElement.webkitVideoDecodedByteCount; if (this._lastDecodedBytes && this._lastBitrateTime) { var bytesDelta = currentBytes - this._lastDecodedBytes; var timeDelta = (Date.now() - this._lastBitrateTime) / 1000; if (timeDelta > 0 && bytesDelta > 0) { var calculatedBitrate = Math.round((bytesDelta * 8) / timeDelta); this._lastDecodedBytes = currentBytes; this._lastBitrateTime = Date.now(); return calculatedBitrate > 0 ? calculatedBitrate : -1; } } this._lastDecodedBytes = currentBytes; this._lastBitrateTime = Date.now(); } return -1; } getRendition() { var width = null; var height = null; var bitrate = null; // Try to get from Freewheel creative rendition first var creativeRendition = this._getAdInstanceMethodCall('getActiveCreativeRendition'); if (creativeRendition) { try { // Try direct properties first width = creativeRendition._width || creativeRendition.width; height = creativeRendition._height || creativeRendition.height; bitrate = creativeRendition._bitrate || creativeRendition.bitrate; // Then try getter methods if (!width && typeof creativeRendition.getWidth === 'function') { width = creativeRendition.getWidth(); } if (!height && typeof creativeRendition.getHeight === 'function') { height = creativeRendition.getHeight(); } if (!bitrate && typeof creativeRendition.getBitrate === 'function') { bitrate = creativeRendition.getBitrate(); } // Try to get from primary asset if rendition doesn't have it if ( (!width || !height || !bitrate) && typeof creativeRendition.getPrimaryCreativeRenditionAsset === 'function' ) { var asset = creativeRendition.getPrimaryCreativeRenditionAsset(); if (asset) { if (!width) width = asset._width || asset.width || (typeof asset.getWidth === 'function' ? asset.getWidth() : null); if (!height) height = asset._height || asset.height || (typeof asset.getHeight === 'function' ? asset.getHeight() : null); if (!bitrate) bitrate = asset._bitrate || asset.bitrate || (typeof asset.getBitrate === 'function' ? asset.getBitrate() : null); } } } catch (err) { // Continue to fallback } } // Fallback to video element if SDK doesn't provide dimensions var videoElement = this._getVideoElement(); if (videoElement) { if (!width || width <= 0) width = videoElement.videoWidth; if (!height || height <= 0) height = videoElement.videoHeight; } // Convert bitrate to number if needed and validate if (bitrate) { bitrate = Number(bitrate); if (isNaN(bitrate) || bitrate <= 0) bitrate = null; } // Build rendition string: resolution@bitrate (e.g., "1920x1080@2500Kbps") if ((width > 0 && height > 0) || (bitrate && bitrate > 0)) { return this.getNpawUtils().buildRenditionString(width, height, bitrate); } return null; } getThroughput() { // Freewheel SDK doesn't provide throughput directly // Return -1 to indicate not available return -1; } _getVideoElement() { // Check if cached element is still valid if (this._cachedVideoElement && this._cachedVideoElement.parentNode) { return this._cachedVideoElement; } this._cachedVideoElement = null; // Method 1: Check this.player directly for video elements if (this.player) { for (var key in this.player) { var element = this.player[key]; if (element && element.tagName && element.tagName.toLowerCase() === 'video') { this._cachedVideoElement = element; return this._cachedVideoElement; } } } // Method 2: Try to get from adContext if available try { if (this.player && this.player.currentAdContext) { var context = this.player.currentAdContext; // Try getContentVideoElement if (typeof context.getContentVideoElement === 'function') { var contentVideo = context.getContentVideoElement(); if (contentVideo) { this._cachedVideoElement = contentVideo; return this._cachedVideoElement; } } // Try _videoElement if (context._videoElement) { this._cachedVideoElement = context._videoElement; return this._cachedVideoElement; } } } catch (err) { // Continue to fallback } // Method 3: Try to find video element in slot's video display try { if (this.slot && typeof this.slot.getVideoDisplayBase === 'function') { var videoDisplay = this.slot.getVideoDisplayBase(); if (videoDisplay && videoDisplay._videoElement) { this._cachedVideoElement = videoDisplay._videoElement; return this._cachedVideoElement; } } } catch (err) { // Continue to fallback } // Method 4: Fallback to finding any video element in the DOM that's playing ad content try { var videos = document.querySelectorAll('video'); for (var i = 0; i < videos.length; i++) { var video = videos[i]; // Look for a video that is currently playing and has duration if (video && video.duration > 0 && !video.paused) { this._cachedVideoElement = video; return this._cachedVideoElement; } } // If no playing video, try to find any video with a source for (var j = 0; j < videos.length; j++) { var v = videos[j]; if (v && (v.src || v.currentSrc)) { this._cachedVideoElement = v; return this._cachedVideoElement; } } } catch (err) { // Ignore DOM access errors } return this._cachedVideoElement; } skipListener() { this._stopAdPlayheadTracking(); this._stopBufferMonitoring(); this.fireSkip(); } clickListener(e) { var url = e.adInstance.getEventCallbackUrls(tv.freewheel.SDK.EVENT_AD_CLICK, tv.freewheel.SDK.EVENT_TYPE_CLICK)[0]; var now = new Date().getTime(); if (this.lastUrl === url && now < (this.lastTime || 0) + 2000) { return; } this.lastUrl = url; this.lastTime = now; this.fireClick(url); } closeListener() { this._stopAdPlayheadTracking(); this._stopBufferMonitoring(); this.register(); if (this.position === this.getNpawReference().Constants.AdPosition.Postroll && !this.disable) { this.getVideo().fireStop(); } } errorListener(e) { this._stopAdPlayheadTracking(); this._stopBufferMonitoring(); this.fireError(e.errorCode || e.subType, e.errorInfo); this.fireStop(); } register() { var adapter = this.getVideo().getAdapter(); if (adapter && !this.canRemoveListeners) { adapter.registerListeners(); adapter.fireResume(); this.canRemoveListeners = true; } } unregister() { var adapter = this.getVideo().getAdapter(); if (adapter && this.canRemoveListeners) { this.canRemoveListeners = false; adapter.unregisterListeners(); adapter.firePause(); } } changeVideo() { if (this.flags.isStarted || this.flags.isInited) { this.fireStop(); this.getVideo().fireStop(); this.register(); } } }