diff --git a/integrationExamples/videoModule/jwplayerAdapter/index.html b/integrationExamples/videoModule/jwplayerAdapter/index.html
new file mode 100644
index 00000000000..636e48f03c7
--- /dev/null
+++ b/integrationExamples/videoModule/jwplayerAdapter/index.html
@@ -0,0 +1,145 @@
+-
+
+
+
+
+
+
+
+
+
+
+
+ Prebid.js Test
+ Div-1
+
+ Div-2
+
+
+
+
diff --git a/integrationExamples/videoModule/videojsAdapter/index.html b/integrationExamples/videoModule/videojsAdapter/index.html
new file mode 100644
index 00000000000..b631c720a18
--- /dev/null
+++ b/integrationExamples/videoModule/videojsAdapter/index.html
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ VideoJS Adapter Test
+
+ Div-1 existing Player
+
+
+
+ Div-2 existing player without automatic setup
+
+
+
+ Div-3 controlled Player
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libraries/video/constants/enums.js b/libraries/video/constants/enums.js
new file mode 100644
index 00000000000..b0755020580
--- /dev/null
+++ b/libraries/video/constants/enums.js
@@ -0,0 +1,5 @@
+export const PLAYBACK_MODE = {
+ VOD: 0,
+ LIVE: 1,
+ DVR: 2
+};
diff --git a/libraries/video/constants/events.js b/libraries/video/constants/events.js
new file mode 100644
index 00000000000..5be594b1b48
--- /dev/null
+++ b/libraries/video/constants/events.js
@@ -0,0 +1,97 @@
+// Life Cycle
+export const SETUP_COMPLETE = 'setupComplete';
+export const SETUP_FAILED = 'setupFailed';
+export const DESTROYED = 'destroyed';
+
+// Ads
+export const AD_REQUEST = 'adRequest';
+export const AD_BREAK_START = 'adBreakStart';
+export const AD_LOADED = 'adLoaded';
+export const AD_STARTED = 'adStarted';
+export const AD_IMPRESSION = 'adImpression';
+export const AD_PLAY = 'adPlay';
+export const AD_TIME = 'adTime';
+export const AD_PAUSE = 'adPause';
+export const AD_CLICK = 'adClick';
+export const AD_SKIPPED = 'adSkipped';
+export const AD_ERROR = 'adError';
+export const AD_COMPLETE = 'adComplete';
+export const AD_BREAK_END = 'adBreakEnd';
+
+// Media
+export const PLAYLIST = 'playlist';
+export const PLAYBACK_REQUEST = 'playbackRequest';
+export const AUTOSTART_BLOCKED = 'autostartBlocked';
+export const PLAY_ATTEMPT_FAILED = 'playAttemptFailed';
+export const CONTENT_LOADED = 'contentLoaded';
+export const PLAY = 'play';
+export const PAUSE = 'pause';
+export const BUFFER = 'buffer';
+export const TIME = 'time';
+export const SEEK_START = 'seekStart';
+export const SEEK_END = 'seekEnd';
+export const MUTE = 'mute';
+export const VOLUME = 'volume';
+export const RENDITION_UPDATE = 'renditionUpdate';
+export const ERROR = 'error';
+export const COMPLETE = 'complete';
+export const PLAYLIST_COMPLETE = 'playlistComplete';
+
+// Layout
+export const FULLSCREEN = 'fullscreen';
+export const PLAYER_RESIZE = 'playerResize';
+export const VIEWABLE = 'viewable';
+export const CAST = 'cast';
+
+export const allVideoEvents = [
+ SETUP_COMPLETE, SETUP_FAILED, DESTROYED, AD_REQUEST, AD_BREAK_START, AD_LOADED, AD_STARTED,
+ AD_IMPRESSION, AD_PLAY, AD_TIME, AD_PAUSE, AD_CLICK, AD_SKIPPED, AD_ERROR, AD_COMPLETE, AD_BREAK_END, PLAYLIST,
+ PLAYBACK_REQUEST, AUTOSTART_BLOCKED, PLAY_ATTEMPT_FAILED, CONTENT_LOADED, PLAY, PAUSE, BUFFER, TIME, SEEK_START,
+ SEEK_END, MUTE, VOLUME, RENDITION_UPDATE, ERROR, COMPLETE, PLAYLIST_COMPLETE, FULLSCREEN, PLAYER_RESIZE, VIEWABLE,
+ CAST
+];
+
+export const AUCTION_AD_LOAD_ATTEMPT = 'auctionAdLoadAttempt';
+export const AUCTION_AD_LOAD_ABORT = 'auctionAdLoadAbort';
+export const BID_IMPRESSION = 'bidImpression';
+export const BID_ERROR = 'bidError';
+
+export const videoEvents = {
+ SETUP_COMPLETE,
+ SETUP_FAILED,
+ DESTROYED,
+ AD_REQUEST,
+ AD_BREAK_START,
+ AD_LOADED,
+ AD_STARTED,
+ AD_IMPRESSION,
+ AD_PLAY,
+ AD_TIME,
+ AD_PAUSE,
+ AD_CLICK,
+ AD_SKIPPED,
+ AD_ERROR,
+ AD_COMPLETE,
+ AD_BREAK_END,
+ PLAYLIST,
+ PLAYBACK_REQUEST,
+ AUTOSTART_BLOCKED,
+ PLAY_ATTEMPT_FAILED,
+ CONTENT_LOADED,
+ PLAY,
+ PAUSE,
+ BUFFER,
+ TIME,
+ SEEK_START,
+ SEEK_END,
+ MUTE,
+ VOLUME,
+ RENDITION_UPDATE,
+ ERROR,
+ COMPLETE,
+ PLAYLIST_COMPLETE,
+ FULLSCREEN,
+ PLAYER_RESIZE,
+ VIEWABLE,
+ CAST,
+};
diff --git a/libraries/video/constants/ortb.js b/libraries/video/constants/ortb.js
new file mode 100644
index 00000000000..b5490380cdf
--- /dev/null
+++ b/libraries/video/constants/ortb.js
@@ -0,0 +1,145 @@
+/**
+ * @typedef {Object} OrtbParams
+ * @property {OrtbVideoParamst} video
+ * @property {OrtbContentParams} content
+ */
+
+/**
+ * @typedef OrtbVideoParams
+ * @property {[string]} mimes - Content MIME types supported (e.g., “video/x-ms-wmv”, “video/mp4”).
+ * @property {number|undefined} minduration - Minimum video ad duration in seconds.
+ * @property {number|undefined} maxduration - Maximum video ad duration in seconds.
+ * @property {[number]} protocols - Supported video protocols. At least one supported protocol must be specified.
+ * @property {number} w - Width of the video player in device independent pixels (DIPS).
+ * @property {number} h - Height of the video player in device independent pixels (DIPS).
+ * @property {number|undefined} startdelay - Indicates the offset of the ad placement.
+ * @property {number|undefined} placement - Placement type for the impression.
+ * @property {number|undefined} linearity - Indicates if the impression must be linear, nonlinear, etc. If omitted, assume all are allowed.
+ * @property {number} skip - Indicates if the player can allow the video to be skipped, where 0 is no, 1 is yes.
+ * @property {number|undefined} skipmin - Only ad creatives with a duration greater than this value can be skippable; only applicable if the ad is skippable.
+ * @property {number|undefined} skipafter - Number of seconds a video must play before skipping is enabled; only applicable if the ad is skippable.
+ * @property {number|undefined} sequence - If multiple ad impressions are offered in the same bid request, the sequence number will allow for the coordinated delivery of multiple creatives.
+ * @property {[number]|undefined} battr - Blocked creative attributes. Use this to indicate which creatives the player does not support.
+ * @property {number|undefined} maxextended - Maximum extended ad duration if extension is allowed.
+ * @property {number|undefined} minbitrate - Minimum bit rate in Kbps supported by the player.
+ * @property {number|undefined} maxbitrate - Maximum bit rate in Kbps supported by the player.
+ * @property {number|undefined} boxingallowed - Indicates if letter-boxing of 4:3 content into a 16:9 window is allowed. 0 is no, 1 is yes.
+ * @property {[number]|undefined} playbackmethod - Playback methods that may be in use.
+ * @property {number|undefined} playbackend - The scenario that causes playback to end.
+ * @property {[number]|undefined} delivery - Supported delivery methods (e.g., streaming, progressive).
+ * @property {number|undefined} pos - Ad position on screen.
+ * @property {[Object]|undefined} companionad - list of companion ads. Refer to Section 3.2.6 of the oRTB v2.5 spec for the interface of the companion ad object.
+ * @property {[number]|undefined} api - List of supported API frameworks for this impression.
+ * @property {[number]|undefined} companiontype - Supported VAST companion ad types. Refer to List 5.14 of the oRTB v2.5 spec for the interface.
+ * @property {Object|undefined} ext - Placeholder for exchange-specific extensions to OpenRTB.
+ */
+
+/**
+ * @typedef OrtbContentParams
+ * @property {string} id - ID uniquely identifying the content.
+ * @property {string} url - URL of the content, for buy-side contextualization or review.
+ * @property {number|undefined} episode - Episode number.
+ * @property {string|undefined} title - Content title.
+ * @property {string|undefined} series - Content series i.e. “The Office” (television), “Star Wars” (movie).
+ * @property {string|undefined} season - Content season (e.g., “Season 3”).
+ * @property {string|undefined} artist - Artist credited with the content.
+ * @property {string|undefined} genre - Genre that best describes the content (e.g., rock, pop, etc).
+ * @property {string|undefined} album - Album to which the content belongs. Typically for audio.
+ * @property {string|undefined} isrc - International Standard Recording Code conforming to ISO3901.
+ * @property {Object|undefined} producer - Details about the content Producer. For Producer interface visit Section 3.2.17 of the oRTB v2.5 spec.
+ * @property {[string]|undefined} cat - List of IAB content categories that describe the content. Refer to List 5.1. of the oRTB v2.5 spec for the complete list.
+ * @property {number|undefined} prodq - Production quality. Refer to List 5.13 of the oRTB v2.5 spec.
+ * @property {number|undefined} context - Type of content (game, video, text, etc.). Refer to List 5.18 of the oRTB v2.5 spec.
+ * @property {string|undefined} contentrating - Content rating (e.g., MPAA).
+ * @property {string|undefined} userrating - User rating of the content (e.g., number of stars, likes, etc.).
+ * @property {number|undefined} qagmediarating - Media rating per IQG guidelines. Refer to List 5.19 of the oRTB v2.5 spec.
+ * @property {string|undefined} keywords - Comma separated list of keywords describing the content.
+ * @property {number|undefined} livestream - Whether the stream is live or not. 0 means not live (VOD), 1 means content is live streaming.
+ * @property {number|undefined} sourcerelationship - 0 means indirect, 1 means direct.
+ * @property {number} len - Duration of content in seconds.
+ * @property {string|undefined} language - Content language using ISO-639-1-alpha-2.
+ * @property {number|undefined} embeddable - Indicator of whether or not the content is embeddable (e.g., an embeddable video player). 0 means no, 1 means yes.
+ * @property {[Object]|undefined} data - Additional content data. Each Data object represents a different data source. See Section 3.2.21 of the oRTB v2.5 spec.
+ * @property {Object|undefined} ext - Placeholder for exchange-specific extensions to OpenRTB.
+ */
+
+const VIDEO_PREFIX = 'video/';
+const APPLICATION_PREFIX = 'application/';
+
+/**
+ * ORTB 2.5 section 3.2.7 - Video.mimes
+ * @enum OrtbVideoParams.mimes
+ */
+export const VIDEO_MIME_TYPE = {
+ MP4: VIDEO_PREFIX + 'mp4',
+ MPEG: VIDEO_PREFIX + 'mpeg',
+ OGG: VIDEO_PREFIX + 'ogg',
+ WEBM: VIDEO_PREFIX + 'webm',
+ AAC: VIDEO_PREFIX + 'aac',
+ HLS: APPLICATION_PREFIX + 'vnd.apple.mpegurl'
+};
+
+export const JS_APP_MIME_TYPE = APPLICATION_PREFIX + 'javascript';
+export const VPAID_MIME_TYPE = JS_APP_MIME_TYPE;
+
+/**
+ * ORTB 2.5 section 5.9 - Video Placement Types
+ * @enum OrtbVideoParams.placement
+ */
+export const PLACEMENT = {
+ INSTREAM: 1,
+ BANNER: 2,
+ ARTICLE: 3,
+ FEED: 4,
+ INTERSTITIAL: 5,
+ SLIDER: 5,
+ FLOATING: 5,
+ INTERSTITIAL_SLIDER_FLOATING: 5
+};
+
+/**
+ * ORTB 2.5 section 5.10 - Playback Methods
+ * @enum OrtbVideoParams.playbackmethod
+ */
+export const PLAYBACK_METHODS = {
+ AUTOPLAY: 1,
+ AUTOPLAY_MUTED: 2,
+ CLICK_TO_PLAY: 3,
+ CLICK_TO_PLAY_MUTED: 4,
+ VIEWABLE: 5,
+ VIEWABLE_MUTED: 6
+};
+
+/**
+ * ORTB 2.5 section 5.8 - Protocols
+ * @enum OrtbVideoParams.protocols
+ */
+export const PROTOCOLS = {
+ // VAST_1_0: 1,
+ VAST_2_0: 2,
+ VAST_3_0: 3,
+ // VAST_1_O_WRAPPER: 4,
+ VAST_2_0_WRAPPER: 5,
+ VAST_3_0_WRAPPER: 6,
+ VAST_4_0: 7,
+ VAST_4_0_WRAPPER: 8
+};
+
+/**
+ * ORTB 2.5 section 5.6 - API Frameworks
+ * @enum OrtbVideoParams.api
+ */
+export const API_FRAMEWORKS = {
+ VPAID_1_0: 1,
+ VPAID_2_0: 2,
+ OMID_1_0: 7
+};
+
+/**
+ * ORTB 2.5 section 5.18 - Content Context
+ * @enum OrtbContentParams.context
+ */
+export const CONTEXT = {
+ VIDEO: 1,
+ AUDIO: 3
+};
diff --git a/libraries/video/constants/vendorCodes.js b/libraries/video/constants/vendorCodes.js
new file mode 100644
index 00000000000..370c151b997
--- /dev/null
+++ b/libraries/video/constants/vendorCodes.js
@@ -0,0 +1,6 @@
+// Video Vendors
+export const JWPLAYER_VENDOR = 1;
+export const VIDEO_JS_VENDOR = 2;
+
+// Ad Server Vendors
+export const GAM_VENDOR = 'gam';
diff --git a/libraries/video/coreVideo.js b/libraries/video/coreVideo.js
new file mode 100644
index 00000000000..5b9a8cdbb90
--- /dev/null
+++ b/libraries/video/coreVideo.js
@@ -0,0 +1,188 @@
+import { module } from '../../src/hook.js';
+import { ParentModule, SubmoduleBuilder } from './shared/parentModule.js';
+
+// define, ortb object, events
+
+/**
+ * Video Provider Submodule interface. All submodules of the Core Video module must adhere to this.
+ * @description attached to a video player instance.
+ * @typedef {Object} VideoProvider
+ * @function init - Instantiates the Video Provider and the video player, if not already instantiated.
+ * @function getId - retrieves the div id (unique identifier) of the attached player instance.
+ * @function getOrtbParams - retrieves the oRTB params for a player's current video session.
+ * @function setAdTagUrl - Requests that a player render the ad in the provided ad tag url.
+ * @function onEvents - attaches event listeners to the player instance.
+ * @function offEvents - removes event listeners to the player instance.
+ * @function destroy - deallocates the player instance
+ */
+
+/**
+ * @function VideoProvider#init
+ */
+
+/**
+ * @function VideoProvider#getId
+ * @returns {string}
+ */
+
+/**
+ * @function VideoProvider#getOrtParams
+ * @returns {Object}
+ */
+
+/**
+ * @function VideoProvider#setAdTagUrl
+ * @param {string} adTagUrl - URL to a VAST ad tag
+ * @param {Object} options - Optional params
+ */
+
+/**
+ * @function VideoProvider#onEvents
+ * @param {[string]} events - List of event names for which the listener should be added
+ * @param {function} callback - function that will get called when one of the events is triggered
+ */
+
+/**
+ * @function VideoProvider#offEvents
+ * @param {[string]} events - List of event names for which the attached listener should be removed
+ * @param {function} callback - function that was assigned as a callback when the listener was added
+ */
+
+/**
+ * @function VideoProvider#destroy
+ */
+
+/**
+ * @typedef {Object} videoProviderConfig
+ * @name videoProviderConfig
+ * @summary contains data indicating which submodule to create and which player instance to attach it to
+ * @property {string} divId - unique identifier of the player instance
+ * @property {number} vendorCode - numeric identifier of the Video Provider type i.e. video.js or jwplayer
+ * @property {playerConfig} playerConfig
+ */
+
+/**
+ * @typedef {Object} playerConfig
+ * @name playerConfig
+ * @summary contains data indicating the behavior the player instance should have
+ * @property {boolean} autoStart - determines if the player should start automatically when instantiated
+ * @property {boolean} mute - determines if the player should be muted when instantiated
+ * @property {string} licenseKey - authentication key required for commercial players. Optional for free players.
+ * @property {playerVendorParams} params
+ */
+
+/**
+ * @typedef playerVendorParams
+ * @name playerVendorParams
+ * @summary configuration options specific to a Video Vendor's Provider
+ * @property {Object} vendorConfig - the settings object which can be used as an argument when instantiating a player. Specific to the video player's API.
+ */
+
+/**
+ * Routes commands to the appropriate video submodule.
+ * @typedef {Object} VideoCore
+ * @class
+ * @function registerProvider
+ * @function getOrtbParams
+ * @function setAdTagUrl
+ * @function onEvents
+ * @function offEvents
+ */
+
+/**
+ * @summary Maps a Video Provider factory to the video player's vendor code.
+ * @type {vendorSubmoduleDirectory}
+ */
+const videoVendorDirectory = {};
+
+/**
+ * @constructor
+ * @param {ParentModule} parentModule_
+ * @returns {VideoCore}
+ */
+export function VideoCore(parentModule_) {
+ const parentModule = parentModule_;
+
+ /**
+ * requests that a submodule be instantiated for the specific player instance described by the @providerConfig
+ * @name VideoCore#registerProvider
+ * @param {videoProviderConfig} providerConfig
+ */
+ function registerProvider(providerConfig) {
+ try {
+ parentModule.registerSubmodule(providerConfig.divId, providerConfig.vendorCode, providerConfig);
+ } catch (e) {}
+ }
+
+ /**
+ * @name VideoCore#getOrtbParams
+ * @summary Obtains the oRTB params for a player's current video session.
+ * @param {string} divId - unique identifier of the player instance
+ * @returns {Object} oRTB params
+ */
+ function getOrtbParams(divId) {
+ const submodule = parentModule.getSubmodule(divId);
+ return submodule && submodule.getOrtbParams();
+ }
+
+ /**
+ * @name VideoCore#setAdTagUrl
+ * @summary Requests that a player render the ad in the provided ad tag
+ * @param {string} adTagUrl - URL to a VAST ad tag
+ * @param {string} divId - unique identifier of the player instance
+ * @param {Object} options - additional params
+ */
+ function setAdTagUrl(adTagUrl, divId, options) {
+ const submodule = parentModule.getSubmodule(divId);
+ submodule && submodule.setAdTagUrl(adTagUrl, options);
+ }
+
+ /**
+ * @name VideoCore#onEvents
+ * @summary attaches event listeners
+ * @param {[string]} events - List of event names for which the listener should be added
+ * @param {function} callback - function that will get called when one of the events is triggered
+ * @param {string} divId - unique identifier of the player instance
+ */
+ function onEvents(events, callback, divId) {
+ const submodule = parentModule.getSubmodule(divId);
+ submodule && submodule.onEvents(events, callback);
+ }
+
+ /**
+ * @name VideoCore#offEvents
+ * @summary removes event listeners
+ * @param {[string]} events - List of event names for which the listener should be removed
+ * @param {function} callback - function that was assigned as a callback when the listener was added
+ * @param {string} divId - unique identifier of the player instance
+ */
+ function offEvents(events, callback, divId) {
+ const submodule = parentModule.getSubmodule(divId);
+ submodule && submodule.offEvents(events, callback);
+ }
+
+ return {
+ registerProvider,
+ getOrtbParams,
+ setAdTagUrl,
+ onEvents,
+ offEvents
+ };
+}
+
+/**
+ * @function videoCoreFactory
+ * @summary Factory to create a Video Core instance
+ * @returns {VideoCore}
+ */
+export function videoCoreFactory() {
+ const videoSubmoduleBuilder = SubmoduleBuilder(videoVendorDirectory);
+ const parentModule = ParentModule(videoSubmoduleBuilder);
+ return VideoCore(parentModule);
+}
+
+function attachVideoProvider(submoduleFactory) {
+ videoVendorDirectory[submoduleFactory.vendorCode] = submoduleFactory;
+}
+
+module('video', attachVideoProvider);
diff --git a/libraries/video/gamAdServerSubmodule.js b/libraries/video/gamAdServerSubmodule.js
new file mode 100644
index 00000000000..2b0aaad3ca4
--- /dev/null
+++ b/libraries/video/gamAdServerSubmodule.js
@@ -0,0 +1,27 @@
+import { GAM_VENDOR } from './constants/vendorCodes.js';
+import { getGlobal } from '../../src/prebidGlobal.js';
+
+/**
+ * @constructor
+ * @param {Object} dfpModule_ - the DFP ad server module
+ * @returns {AdServerProvider}
+ */
+function GamAdServerProvider(dfpModule_) {
+ const dfp = dfpModule_;
+
+ function getAdTagUrl(adUnit, baseAdTag, params) {
+ return dfp.buildVideoUrl({ adUnit: adUnit, url: baseAdTag, params });
+ }
+
+ return {
+ getAdTagUrl
+ }
+}
+
+export function gamSubmoduleFactory() {
+ const dfp = getGlobal().adServers.dfp;
+ const gamProvider = GamAdServerProvider(dfp);
+ return gamProvider;
+}
+
+gamSubmoduleFactory.vendorCode = GAM_VENDOR;
diff --git a/libraries/video/index.js b/libraries/video/index.js
new file mode 100644
index 00000000000..430354269ef
--- /dev/null
+++ b/libraries/video/index.js
@@ -0,0 +1,50 @@
+import {getGlobal} from '../../src/prebidGlobal.js';
+import {config} from '../../src/config.js';
+import events from '../../src/events.js';
+import CONSTANTS from '../../src/constants.json'
+import { videoCoreFactory } from './coreVideo.js'
+
+export function PbVideo(videoCore_, getConfig_, requestBids_, onPbEvents_) {
+ const videoCore = videoCore_;
+ const getConfig = getConfig_;
+ const requestBids = requestBids_;
+ const onPbEvents = onPbEvents_;
+
+ function init() {
+ getConfig('video', ({ video }) => {
+ video.providers.forEach(provider => {
+ videoCore.registerProvider(provider);
+ });
+ // maybe video.providers to get changes on providers
+ // called whenever 'video' is updated
+ // instantiate anc check for new submodules
+ });
+
+ // before bids are requested , getOrtbParams and write to the ad units.
+ requestBids.before(enrichAdUnits, 40);
+
+ // bidsBackHandler -> setAdTagUrl
+ onPbEvents(CONSTANTS.EVENTS.AUCTION_END, function(auctionResult) {
+ // get winning bid + vast xml
+ });
+
+ // analytics registering and surfacing
+ }
+
+ function enrichAdUnits(nextFn, bidRequest) {
+ // get oRTB arams
+ // write to ad units
+ // let adUnits = bidRequest.adUnits
+ }
+
+ return { init };
+}
+
+function pbVideoFactory() {
+ const videoCore = videoCoreFactory();
+ const pbVideo = PbVideo(videoCore, config.getConfig, getGlobal().requestBids, events.on);
+ pbVideo.init();
+ return pbVideo;
+}
+
+pbVideoFactory();
diff --git a/libraries/video/shared/parentModule.js b/libraries/video/shared/parentModule.js
new file mode 100644
index 00000000000..e96113fd894
--- /dev/null
+++ b/libraries/video/shared/parentModule.js
@@ -0,0 +1,82 @@
+/**
+ * @typedef {Object} ParentModule
+ * @summary abstraction for any module to store and reference its submodules
+ * @param {SubmoduleBuilder} submoduleBuilder_
+ * @returns {ParentModule}
+ * @constructor
+ */
+export function ParentModule(submoduleBuilder_) {
+ const submoduleBuilder = submoduleBuilder_;
+ const submodules = {};
+
+ /**
+ * @function ParentModule#registerSubmodule
+ * @summary Stores a submodule
+ * @param {String} id - unique identifier of the submodule instance
+ * @param {String} vendorCode - identifier to the submodule type that must be built
+ * @param {Object} config - additional information necessary to instantiate the submodule
+ */
+ function registerSubmodule(id, vendorCode, config) {
+ if (submodules[id]) {
+ return;
+ }
+
+ let submodule;
+ try {
+ submodule = submoduleBuilder.build(vendorCode, config);
+ } catch (e) {
+ throw e;
+ }
+ submodules[id] = submodule;
+ }
+
+ /**
+ * @function ParentModule#getSubmodule
+ * @summary Stores a submodule
+ * @param {String} id - unique identifier of the submodule instance
+ * @returns {Object} - a submodule instance
+ */
+ function getSubmodule(id) {
+ return submodules[id];
+ }
+
+ return {
+ registerSubmodule,
+ getSubmodule
+ }
+}
+
+/**
+ * @typedef {Object} SubmoduleBuilder
+ * @summary Instantiates submodules
+ * @param {vendorSubmoduleDirectory} submoduleDirectory_
+ * @param {Object|null|undefined} sharedUtils_
+ * @returns {SubmoduleBuilder}
+ * @constructor
+ */
+export function SubmoduleBuilder(submoduleDirectory_, sharedUtils_) {
+ const submoduleDirectory = submoduleDirectory_;
+ const sharedUtils = sharedUtils_;
+
+ /**
+ * @function SubmoduleBuilder#build
+ * @param vendorCode - identifier to the submodule type that must be instantiated
+ * @param config - additional information necessary to instantiate the submodule
+ * @throws
+ * @returns {{init}|*} - a submodule instance
+ */
+ function build(vendorCode, config) {
+ const submoduleFactory = submoduleDirectory[vendorCode];
+ if (!submoduleFactory) {
+ throw new Error('Unrecognized submodule vendor code: ' + vendorCode);
+ }
+
+ const submodule = submoduleFactory(config, sharedUtils);
+ submodule && submodule.init && submodule.init();
+ return submodule;
+ }
+
+ return {
+ build
+ };
+}
diff --git a/libraries/video/shared/state.js b/libraries/video/shared/state.js
new file mode 100644
index 00000000000..b84cff8f997
--- /dev/null
+++ b/libraries/video/shared/state.js
@@ -0,0 +1,47 @@
+/**
+ * @typedef {Object} State
+ * @summary simple state object. Can be subclassed
+ * @function updateState
+ * @function getState
+ * @function clearState
+ */
+
+/**
+ * @summary factory to create a simple state object
+ * @returns {State}
+ */
+export default function stateFactory() {
+ let state = {};
+
+ /**
+ * @function State#updateState
+ * @summary updates the state
+ * @param {Object} stateUpdate
+ */
+ function updateState(stateUpdate) {
+ Object.assign(state, stateUpdate);
+ }
+
+ /**
+ * @function State#getState
+ * @summary provides the current state
+ * @returns {Object} the current state
+ */
+ function getState() {
+ return state;
+ }
+
+ /**
+ * @function State#clearState
+ * @summary erases the current state
+ */
+ function clearState() {
+ state = {};
+ }
+
+ return {
+ updateState,
+ getState,
+ clearState
+ };
+}
diff --git a/libraries/video/shared/vastXmlBuilder.js b/libraries/video/shared/vastXmlBuilder.js
new file mode 100644
index 00000000000..35547acc479
--- /dev/null
+++ b/libraries/video/shared/vastXmlBuilder.js
@@ -0,0 +1,77 @@
+import { getGlobal } from '../../../src/prebidGlobal.js';
+
+export function buildVastWrapper(adId, adTagUrl, impressionUrl, impressionId, errorUrl) {
+ let wrapperBody = getAdSystemNode('Prebid org', getGlobal().version);
+
+ if (adTagUrl) {
+ wrapperBody += getAdTagUriNode(adTagUrl);
+ }
+
+ if (impressionUrl) {
+ wrapperBody += getImpressionNode(impressionUrl, impressionId);
+ }
+
+ if (errorUrl) {
+ wrapperBody += getErrorNode(errorUrl);
+ }
+
+ return getVastNode(getAdNode(getWrapperNode(wrapperBody), adId), '4.2');
+}
+
+export function getVastNode(body, vastVersion) {
+ return getNode('VAST', body, { version: vastVersion });
+}
+
+export function getAdNode(body, adId) {
+ return getNode('Ad', body, { id: adId });
+}
+
+export function getWrapperNode(body) {
+ return getNode('Wrapper', body);
+}
+
+export function getAdSystemNode(system, version) {
+ return getNode('AdSystem', system, { version });
+}
+
+export function getAdTagUriNode(adTagUrl) {
+ return getUrlNode('VASTAdTagURI', adTagUrl);
+}
+
+export function getImpressionNode(pingUrl, id) {
+ return getUrlNode('Impression', pingUrl, { id });
+}
+
+export function getErrorNode(pingUrl) {
+ return getUrlNode('Error', pingUrl);
+}
+
+// Helpers
+
+function getUrlNode(labelName, url, attributes) {
+ const body = ``;
+ return getNode(labelName, body, attributes);
+}
+
+function getNode(labelName, body, attributes) {
+ const openingLabel = getOpeningLabel(labelName, attributes);
+ return `<${openingLabel}>${body}${labelName}>`;
+}
+
+/*
+attributes is a KVP Object.
+ */
+function getOpeningLabel(name, attributes) {
+ if (!attributes) {
+ return name;
+ }
+
+ return Object.keys(attributes).reduce((label, key) => {
+ const value = attributes[key];
+ if (!value) {
+ return label;
+ }
+
+ return label + ` ${key}="${value}"`;
+ }, name);
+}
diff --git a/libraries/video/shared/vastXmlEditor.js b/libraries/video/shared/vastXmlEditor.js
new file mode 100644
index 00000000000..b586e5b4c29
--- /dev/null
+++ b/libraries/video/shared/vastXmlEditor.js
@@ -0,0 +1,115 @@
+import { getErrorNode, getImpressionNode, buildVastWrapper } from './vastXmlBuilder.js';
+
+export const XML_MIME_TYPE = 'application/xml';
+
+export function VastXmlEditor(xmlUtil_) {
+ const xmlUtil = xmlUtil_;
+
+ function getVastXmlWithTracking(vastXml, adId, impressionUrl, impressionId, errorUrl) {
+ const impressionDoc = getImpressionDoc(impressionUrl, impressionId);
+ const errorDoc = getErrorDoc(errorUrl);
+ if (!adId && !impressionDoc && !errorDoc) {
+ return vastXml;
+ }
+
+ const vastXmlDoc = xmlUtil.parse(vastXml);
+ appendTrackingNodes(vastXmlDoc, impressionDoc, errorDoc);
+ replaceAdId(vastXmlDoc, adId);
+ return xmlUtil.serialize(vastXmlDoc);
+ }
+
+ function appendTrackingNodes(vastXmlDoc, impressionDoc, errorDoc) {
+ const nodes = vastXmlDoc.querySelectorAll('InLine,Wrapper');
+ const nodeCount = nodes.length;
+ for (let i = 0; i < nodeCount; i++) {
+ const node = nodes[i];
+ // A copy of the child is required until we reach the last node.
+ const requiresCopy = i < nodeCount - 1;
+ appendChild(node, impressionDoc, requiresCopy);
+ appendChild(node, errorDoc, requiresCopy);
+ }
+ }
+
+ function replaceAdId(vastXmlDoc, adId) {
+ if (!adId) {
+ return;
+ }
+
+ const adNode = vastXmlDoc.querySelector('Ad');
+ if (!adNode) {
+ return;
+ }
+
+ adNode.id = adId;
+ }
+
+ return {
+ getVastXmlWithTracking,
+ buildVastWrapper
+ }
+
+ function getImpressionDoc(impressionUrl, impressionId) {
+ if (!impressionUrl) {
+ return;
+ }
+
+ const impressionNode = getImpressionNode(impressionUrl, impressionId);
+ return xmlUtil.parse(impressionNode);
+ }
+
+ function getErrorDoc(errorUrl) {
+ if (!errorUrl) {
+ return;
+ }
+
+ const errorNode = getErrorNode(errorUrl);
+ return xmlUtil.parse(errorNode);
+ }
+
+ function appendChild(node, child, copy) {
+ if (!child) {
+ return;
+ }
+
+ const doc = copy ? child.cloneNode(true) : child;
+ node.appendChild(doc.documentElement);
+ }
+}
+
+function XMLUtil() {
+ let parser;
+ let serializer;
+
+ function getParser() {
+ if (!parser) {
+ // DOMParser instantiation is costly; instantiate only once throughout Prebid lifecycle.
+ parser = new DOMParser();
+ }
+ return parser;
+ }
+
+ function getSerializer() {
+ if (!serializer) {
+ // XMLSerializer instantiation is costly; instantiate only once throughout Prebid lifecycle.
+ serializer = new XMLSerializer();
+ }
+ return serializer;
+ }
+
+ function parse(xmlString) {
+ return getParser().parseFromString(xmlString, XML_MIME_TYPE);
+ }
+
+ function serialize(xmlDoc) {
+ return getSerializer().serializeToString(xmlDoc);
+ }
+
+ return {
+ parse,
+ serialize
+ };
+}
+
+export function vastXmlEditorFactory() {
+ return VastXmlEditor(XMLUtil());
+}
diff --git a/libraries/video/videoImpressionVerifier.js b/libraries/video/videoImpressionVerifier.js
new file mode 100644
index 00000000000..c5be3f6fe48
--- /dev/null
+++ b/libraries/video/videoImpressionVerifier.js
@@ -0,0 +1,200 @@
+import { find } from '../../src/polyfill.js';
+import { vastXmlEditorFactory } from './shared/vastXmlEditor.js';
+import { generateUUID } from '../../src/utils.js';
+
+export const PB_PREFIX = 'pb_';
+export const UUID_MARKER = PB_PREFIX + 'uuid';
+
+/**
+ * Video Impression Verifier interface. All implementations of a Video Impression Verifier must comply with this interface.
+ * @description adds tracking markers to an ad and extracts the bid identifiers from ad event information.
+ * @typedef {Object} VideoImpressionVerifier
+ * @function trackBid - requests that a bid's ad be tracked for impression verification.
+ * @function getBidIdentifiers - requests information from the ad event data that can be used to match the ad to a tracked bid.
+ */
+
+/**
+ * @function VideoImpressionVerifier#trackBid
+ * @param {Object} bid - Bid that should be tracked.
+ * @return {String} - Identifier for the bid being tracked.
+ */
+
+/**
+ * @function VideoImpressionVerifier#getBidIdentifiers
+ * @param {String} adId - In the VAST tag, this value is present in the Ad element's id property.
+ * @param {String} adTagUrl - The ad tag url that was loaded into the player.
+ * @param {[String]} adWrapperIds - List of ad id's that were obtained from the different wrappers. Each redirect points to an ad wrapper.
+ * @return {bidIdentifier} - Object allowing the bid matching the ad event to be identified.
+ */
+
+/**
+ * @typedef {Object} bidIdentifier
+ * @property {String} adId - Bid identifier.
+ * @property {String} adUnitCode - Identifier for the Ad Unit for which the bid was made.
+ * @property {String} auctionId - Id of the auction in which the bid was made.
+ * @property {String} requestId - Id of the bid request which resulted in the bid.
+ */
+
+/**
+ * Factory function for obtaining a Video Impression Verifier.
+ * @param {Boolean} isCacheUsed - wether Prebid is configured to use a cache.
+ * @return {VideoImpressionVerifier}
+ */
+export function videoImpressionVerifierFactory(isCacheUsed) {
+ const vastXmlEditor = vastXmlEditorFactory();
+ const bidTracker = tracker();
+ if (isCacheUsed) {
+ return cachedVideoImpressionVerifier(vastXmlEditor, bidTracker);
+ }
+
+ return videoImpressionVerifier(vastXmlEditor, bidTracker);
+}
+
+export function videoImpressionVerifier(vastXmlEditor_, bidTracker_) {
+ const verifier = baseImpressionVerifier(bidTracker_);
+ const superTrackBid = verifier.trackBid;
+ const vastXmlEditor = vastXmlEditor_;
+
+ verifier.trackBid = function(bid) {
+ let { vastXml, vastUrl } = bid;
+ if (!vastXml && !vastUrl) {
+ return;
+ }
+
+ const uuid = superTrackBid(bid);
+
+ if (vastUrl) {
+ const url = new URL(vastUrl);
+ url.searchParams.append(UUID_MARKER, uuid);
+ bid.vastUrl = url.toString();
+ } else if (vastXml) {
+ bid.vastXml = vastXmlEditor.getVastXmlWithTracking(vastXml, uuid);
+ }
+
+ return uuid;
+ }
+
+ return verifier;
+}
+
+export function cachedVideoImpressionVerifier(vastXmlEditor_, bidTracker_) {
+ const verifier = baseImpressionVerifier(bidTracker_);
+ const superTrackBid = verifier.trackBid;
+ const superGetBidIdentifiers = verifier.getBidIdentifiers;
+ const vastXmlEditor = vastXmlEditor_;
+
+ verifier.trackBid = function (bid, globalAdUnits) {
+ const adIdOverride = superTrackBid(bid);
+ let { vastXml, vastUrl, adId, adUnitCode } = bid;
+ const adUnit = find(globalAdUnits, adUnit => adUnitCode === adUnit.code);
+ const videoConfig = adUnit && adUnit.video;
+ const adServerConfig = videoConfig && videoConfig.adServer;
+ const trackingConfig = adServerConfig && adServerConfig.tracking;
+ let impressionUrl;
+ let impressionId;
+ let errorUrl;
+ const impressionTracking = trackingConfig.impression;
+ const errorTracking = trackingConfig.error;
+
+ if (impressionTracking) {
+ impressionUrl = getTrackingUrl(impressionTracking.getUrl, bid);
+ impressionId = impressionTracking.id || adId + '-impression';
+ }
+
+ if (errorTracking) {
+ errorUrl = getTrackingUrl(errorTracking.getUrl, bid);
+ }
+
+ if (vastXml) {
+ vastXml = vastXmlEditor.getVastXmlWithTracking(vastXml, adIdOverride, impressionUrl, impressionId, errorUrl);
+ } else if (vastUrl) {
+ vastXml = vastXmlEditor.buildVastWrapper(adIdOverride, vastUrl, impressionUrl, impressionId, errorUrl);
+ }
+
+ bid.vastXml = vastXml;
+ return adIdOverride;
+ }
+
+ verifier.getBidIdentifiers = function (adId, adTagUrl, adWrapperIds) {
+ // When the video is cached, the ad tag loaded into the player is a parent wrapper of the cache url.
+ // As a result, the ad tag Url cannot include identifiers.
+ return superGetBidIdentifiers(adId, null, adWrapperIds);
+ }
+
+ return verifier;
+
+ function getTrackingUrl(getUrl, bid) {
+ if (!getUrl || typeof getUrl !== 'function') {
+ return;
+ }
+
+ return getUrl(bid);
+ }
+}
+
+export function baseImpressionVerifier(bidTracker_) {
+ const bidTracker = bidTracker_;
+
+ function trackBid(bid) {
+ let { adId, adUnitCode, requestId, auctionId } = bid;
+ const trackingId = PB_PREFIX + generateUUID(10 ** 13);
+ bidTracker.store(trackingId, { adId, adUnitCode, requestId, auctionId });
+ return trackingId;
+ }
+
+ function getBidIdentifiers(adId, adTagUrl, adWrapperIds) {
+ return bidTracker.remove(adId) || getBidForAdTagUrl(adTagUrl) || getBidForAdWrappers(adWrapperIds);
+ }
+
+ return {
+ trackBid,
+ getBidIdentifiers
+ };
+
+ function getBidForAdTagUrl(adTagUrl) {
+ if (!adTagUrl) {
+ return;
+ }
+
+ const url = new URL(adTagUrl);
+ const queryParams = url.searchParams;
+ let uuid = queryParams.get(UUID_MARKER);
+ return uuid && bidTracker.remove(uuid);
+ }
+
+ function getBidForAdWrappers(adWrapperIds) {
+ if (!adWrapperIds || !adWrapperIds.length) {
+ return;
+ }
+
+ for (const wrapperId in adWrapperIds) {
+ const bidInfo = bidTracker.remove(wrapperId);
+ if (bidInfo) {
+ return bidInfo;
+ }
+ }
+ }
+}
+
+export function tracker() {
+ const model = {};
+
+ function store(key, value) {
+ model[key] = value;
+ }
+
+ function remove(key) {
+ const value = model[key];
+ if (!value) {
+ return;
+ }
+
+ delete model[key];
+ return value;
+ }
+
+ return {
+ store,
+ remove
+ }
+}
diff --git a/modules/.submodules.json b/modules/.submodules.json
index d808b10051b..7db557706c6 100644
--- a/modules/.submodules.json
+++ b/modules/.submodules.json
@@ -82,6 +82,13 @@
"resetdigitalBidAdapter",
"rtbhouseBidAdapter.js"
]
+ },
+ "video": {
+ "files": [ "./index.js" ],
+ "dependants": [
+ "jwplayerVideoProvider",
+ "videojsVideoProvider"
+ ]
}
}
}
diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js
index 9a2d3fa0a50..0ba2f533056 100644
--- a/modules/appnexusBidAdapter.js
+++ b/modules/appnexusBidAdapter.js
@@ -40,7 +40,7 @@ const URL = 'https://ib.adnxs.com/ut/v3/prebid';
const URL_SIMPLE = 'https://ib.adnxs-simple.com/ut/v3/prebid';
const VIDEO_TARGETING = ['id', 'minduration', 'maxduration',
'skippable', 'playback_method', 'frameworks', 'context', 'skipoffset'];
-const VIDEO_RTB_TARGETING = ['minduration', 'maxduration', 'skip', 'skipafter', 'playbackmethod', 'api'];
+const VIDEO_RTB_TARGETING = ['minduration', 'maxduration', 'skip', 'skipafter', 'playbackmethod', 'api', 'startdelay'];
const USER_PARAMS = ['age', 'externalUid', 'segments', 'gender', 'dnt', 'language'];
const APP_DEVICE_PARAMS = ['geo', 'device_id']; // appid is collected separately
const DEBUG_PARAMS = ['enabled', 'dongle', 'member_id', 'debug_timeout'];
@@ -949,6 +949,17 @@ function bidToTag(bid) {
tag['video_frameworks'] = apiTmp;
}
break;
+
+ case 'startdelay':
+ case 'placement':
+ const contextKey = 'context';
+ if (typeof tag.video[contextKey] !== 'number') {
+ const placement = videoMediaType['placement'];
+ const startdelay = videoMediaType['startdelay'];
+ const context = getContextFromPlacement(placement) || getContextFromStartDelay(startdelay);
+ tag.video[contextKey] = VIDEO_MAPPING[contextKey][context];
+ }
+ break;
}
});
}
@@ -996,6 +1007,32 @@ function transformSizes(requestSizes) {
return sizes;
}
+function getContextFromPlacement(ortbPlacement) {
+ if (!ortbPlacement) {
+ return;
+ }
+
+ if (ortbPlacement === 2) {
+ return 'in-banner';
+ } else if (ortbPlacement > 2) {
+ return 'outstream';
+ }
+}
+
+function getContextFromStartDelay(ortbStartDelay) {
+ if (!ortbStartDelay) {
+ return;
+ }
+
+ if (ortbStartDelay === 0) {
+ return 'pre_roll';
+ } else if (ortbStartDelay === -1) {
+ return 'mid_roll';
+ } else if (ortbStartDelay === -2) {
+ return 'post_roll';
+ }
+}
+
function hasUserInfo(bid) {
return !!bid.params.user;
}
diff --git a/modules/gridNMBidAdapter.js b/modules/gridNMBidAdapter.js
index 63c42f60933..22fad3df1f8 100644
--- a/modules/gridNMBidAdapter.js
+++ b/modules/gridNMBidAdapter.js
@@ -1,4 +1,4 @@
-import { isStr, deepAccess, isArray, isNumber, logError, logWarn, parseGPTSingleSizeArrayToRtbSize } from '../src/utils.js';
+import { isStr, deepAccess, isArray, isNumber, logError, logWarn, parseGPTSingleSizeArrayToRtbSize, mergeDeep } from '../src/utils.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { Renderer } from '../src/Renderer.js';
import { VIDEO } from '../src/mediaTypes.js';
@@ -25,8 +25,6 @@ const LOG_ERROR_MESS = {
hasNoArrayOfBids: 'Seatbid from response has no array of bid objects - '
};
-const VIDEO_KEYS = ['mimes', 'protocols', 'startdelay', 'placement', 'linearity', 'skip', 'skipmin', 'skipafter', 'sequence', 'battr', 'maxextended', 'minbitrate', 'maxbitrate', 'boxingallowed', 'playbackmethod', 'playbackend', 'delivery', 'pos', 'companionad', 'api', 'companiontype'];
-
export const spec = {
code: BIDDER_CODE,
supportedMediaTypes: [ VIDEO ],
@@ -96,7 +94,7 @@ export const spec = {
const impObj = {
id: bidId.toString(),
tagid: secid.toString(),
- video: createVideoForImp(video, sizes, mediaTypes && mediaTypes.video),
+ video: createVideoForImp(mergeDeep({}, video, mediaTypes && mediaTypes.video), sizes),
ext: {
divid: adUnitCode.toString()
}
@@ -361,13 +359,7 @@ function createRenderer (bid, rendererParams) {
return renderer;
}
-function createVideoForImp({ mind, maxd, size, ...paramsVideo }, bidSizes, bidVideo = {}) {
- VIDEO_KEYS.forEach((key) => {
- if (!(key in paramsVideo) && key in bidVideo) {
- paramsVideo[key] = bidVideo[key];
- }
- });
-
+function createVideoForImp({ mind, maxd, size, ...paramsVideo }, bidSizes) {
if (size && isStr(size)) {
const sizeArray = size.split('x');
if (sizeArray.length === 2 && parseInt(sizeArray[0]) && parseInt(sizeArray[1])) {
@@ -377,7 +369,7 @@ function createVideoForImp({ mind, maxd, size, ...paramsVideo }, bidSizes, bidVi
}
if (!paramsVideo.w || !paramsVideo.h) {
- const playerSizes = bidVideo.playerSize && bidVideo.playerSize.length === 2 ? bidVideo.playerSize : bidSizes;
+ const playerSizes = paramsVideo.playerSize && paramsVideo.playerSize.length === 2 ? paramsVideo.playerSize : bidSizes;
if (playerSizes) {
const playerSize = playerSizes[0];
if (playerSize) {
@@ -386,9 +378,13 @@ function createVideoForImp({ mind, maxd, size, ...paramsVideo }, bidSizes, bidVi
}
}
- const durationRangeSec = bidVideo.durationRangeSec || [];
- const minDur = mind || durationRangeSec[0] || bidVideo.minduration;
- const maxDur = maxd || durationRangeSec[1] || bidVideo.maxduration;
+ if (paramsVideo.playerSize) {
+ delete paramsVideo.playerSize;
+ }
+
+ const durationRangeSec = paramsVideo.durationRangeSec || [];
+ const minDur = mind || durationRangeSec[0] || paramsVideo.minduration;
+ const maxDur = maxd || durationRangeSec[1] || paramsVideo.maxduration;
if (minDur) {
paramsVideo.minduration = minDur;
diff --git a/modules/jwplayerVideoProvider.js b/modules/jwplayerVideoProvider.js
new file mode 100644
index 00000000000..b302884729e
--- /dev/null
+++ b/modules/jwplayerVideoProvider.js
@@ -0,0 +1,1059 @@
+import {
+ PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, PLACEMENT, VPAID_MIME_TYPE, AD_POSITION
+} from '../libraries/video/constants/ortb.js';
+import {
+ SETUP_COMPLETE, SETUP_FAILED, DESTROYED, AD_REQUEST, AD_BREAK_START, AD_LOADED, AD_STARTED, AD_IMPRESSION, AD_PLAY,
+ AD_TIME, AD_PAUSE, AD_CLICK, AD_SKIPPED, AD_ERROR, AD_COMPLETE, AD_BREAK_END, PLAYLIST, PLAYBACK_REQUEST,
+ AUTOSTART_BLOCKED, PLAY_ATTEMPT_FAILED, CONTENT_LOADED, PLAY, PAUSE, BUFFER, TIME, SEEK_START, SEEK_END, MUTE, VOLUME,
+ RENDITION_UPDATE, ERROR, COMPLETE, PLAYLIST_COMPLETE, FULLSCREEN, PLAYER_RESIZE, VIEWABLE, CAST
+} from '../libraries/video/constants/events.js';
+import { PLAYBACK_MODE } from '../libraries/video/constants/enums.js';
+import stateFactory from '../libraries/video/shared/state.js';
+import { JWPLAYER_VENDOR } from '../libraries/video/constants/vendorCodes.js';
+import { submodule } from '../src/hook.js';
+
+/**
+ * @constructor
+ * @param {videoProviderConfig} config
+ * @param {Object} jwplayer_ - JW Player global factory
+ * @param {State} adState_
+ * @param {State} timeState_
+ * @param {CallbackStorage} callbackStorage_
+ * @param {Object} utils
+ * @returns {VideoProvider}
+ */
+export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callbackStorage_, utils, sharedUtils) {
+ const jwplayer = jwplayer_;
+ let player = null;
+ let playerVersion = null;
+ const playerConfig = config.playerConfig;
+ const divId = config.divId;
+ let adState = adState_;
+ let timeState = timeState_;
+ let callbackStorage = callbackStorage_;
+ let pendingSeek = {};
+ let supportedMediaTypes = null;
+ let minimumSupportedPlayerVersion = '8.20.1';
+ let setupCompleteCallback = null;
+ let setupFailedCallback = null;
+ const MEDIA_TYPES = [
+ VIDEO_MIME_TYPE.MP4,
+ VIDEO_MIME_TYPE.OGG,
+ VIDEO_MIME_TYPE.WEBM,
+ VIDEO_MIME_TYPE.AAC,
+ VIDEO_MIME_TYPE.HLS
+ ];
+
+ function init() {
+ if (!jwplayer) {
+ triggerSetupFailure(-1); // TODO: come up with code for player absent
+ return;
+ }
+
+ playerVersion = jwplayer.version;
+
+ if (playerVersion < minimumSupportedPlayerVersion) {
+ triggerSetupFailure(-2); // TODO: come up with code for version not supported
+ return;
+ }
+
+ player = jwplayer(divId);
+ if (player.getState() === undefined) {
+ setupPlayer(playerConfig);
+ } else {
+ setupCompleteCallback && setupCompleteCallback(SETUP_COMPLETE, getSetupCompletePayload());
+ }
+ }
+
+ function getId() {
+ return divId;
+ }
+
+ function getOrtbParams() {
+ if (!player) {
+ return;
+ }
+ const config = player.getConfig() || {};
+ const adConfig = config.advertising || {};
+ supportedMediaTypes = supportedMediaTypes || utils.getSupportedMediaTypes(MEDIA_TYPES);
+
+ const video = {
+ mimes: supportedMediaTypes,
+ protocols: [
+ PROTOCOLS.VAST_2_0,
+ PROTOCOLS.VAST_3_0,
+ PROTOCOLS.VAST_4_0,
+ PROTOCOLS.VAST_2_0_WRAPPER,
+ PROTOCOLS.VAST_3_0_WRAPPER,
+ PROTOCOLS.VAST_4_0_WRAPPER
+ ],
+ h: player.getHeight(), // TODO does player call need optimization ?
+ w: player.getWidth(), // TODO does player call need optimization ?
+ startdelay: utils.getStartDelay(),
+ placement: utils.getPlacement(adConfig, player),
+ // linearity is omitted because both forms are supported.
+ // sequence - TODO not yet supported
+ battr: adConfig.battr,
+ maxextended: -1, // extension is allowed, and there is no time limit imposed.
+ boxingallowed: 1,
+ playbackmethod: [ utils.getPlaybackMethod(config) ],
+ playbackend: 1, // TODO: need to account for floating player - https://developer.jwplayer.com/jwplayer/docs/jw8-embed-an-outstream-player , https://developer.jwplayer.com/jwplayer/docs/jw8-player-configuration-reference#section-float-on-scroll
+ // companionad - TODO add in future version
+ // companiontype - TODO add in future version
+ // minbitrate - TODO add in future version
+ // maxbitrate - TODO add in future version
+ // delivery - omitted because all are supported.
+ // minduration - Is there value to specifying ?
+ // maxduration - Is there value to specifying ?
+ api: [
+ API_FRAMEWORKS.VPAID_2_0
+ ],
+ };
+
+ if (utils.isOmidSupported(adConfig.adClient)) {
+ video.api.push(API_FRAMEWORKS.OMID_1_0);
+ }
+
+ Object.assign(video, utils.getSkipParams(adConfig));
+
+ if (player.getFullscreen()) { // TODO does player call need optimization ?
+ // only specify ad position when in Fullscreen since computational cost is low
+ // ad position options are listed in oRTB 2.5 section 5.4
+ // https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf
+ video.pos = AD_POSITION.FULL_SCREEN; // TODO make constant in oRTB
+ }
+
+ const item = player.getPlaylistItem() || {}; // TODO does player call need optimization ?
+ let { duration, playbackMode } = timeState.getState();
+ if (duration === undefined) {
+ duration = player.getDuration();
+ }
+
+ const content = {
+ url: item.file,
+ title: item.title,
+ cat: item.iabCategories,
+ keywords: item.tags,
+ len: duration,
+ livestream: Math.min(playbackMode, 1),
+ embeddable: 1
+ };
+
+ const mediaId = item.mediaid;
+ if (mediaId) {
+ content.id = 'jw_' + mediaId;
+ }
+
+ const jwpseg = item.jwpseg;
+ const dataSegment = utils.getSegments(jwpseg);
+ const contentDatum = utils.getContentDatum(mediaId, dataSegment);
+ if (contentDatum) {
+ content.data = [contentDatum];
+ }
+
+ const isoLanguageCode = utils.getIsoLanguageCode(player);
+ if (isoLanguageCode) {
+ content.language = isoLanguageCode;
+ }
+
+ return {
+ video,
+ content
+ }
+ }
+
+ function setAdTagUrl(adTagUrl, options) {
+ if (!player || player.getPlugin('bidding') || player.getPlugin('biddingCore')) {
+ return;
+ }
+
+ player.playAd(adTagUrl || options.adXml, options);
+ }
+
+ function onEvents(events, callback) {
+ if (!callback) {
+ return;
+ }
+
+ for (let i = 0; i < events.length; i++) {
+ const type = events[i];
+ let payload = {
+ divId,
+ type
+ };
+
+ registerPreSetupListeners(type, callback, payload);
+ if (!player) {
+ return;
+ }
+
+ registerPostSetupListeners(type, callback, payload);
+ }
+ }
+
+ function offEvents(events, callback) {
+ events.forEach(event => {
+ const jwEvent = utils.getJwEvent(event);
+ if (!callback) {
+ player.off(jwEvent);
+ return;
+ }
+
+ const eventHandler = callbackStorage.getCallback(event, callback);
+ if (!eventHandler) {
+ // skip this iteration when event handler not found.
+ return;
+ }
+
+ player.off(jwEvent, eventHandler);
+ });
+ }
+
+ function destroy() {
+ if (!player) {
+ return;
+ }
+ player.remove();
+ player = null;
+ }
+
+ return {
+ init,
+ getId,
+ getOrtbParams,
+ setAdTagUrl,
+ onEvents,
+ offEvents,
+ destroy
+ };
+
+ function setupPlayer(config) {
+ if (!config) {
+ return;
+ }
+ player.setup(utils.getJwConfig(config));
+ }
+
+ function getSetupCompletePayload() {
+ return {
+ divId,
+ playerVersion,
+ type: SETUP_COMPLETE,
+ viewable: player.getViewable(),
+ viewabilityPercentage: player.getPercentViewable() * 100,
+ mute: player.getMute(),
+ volumePercentage: player.getVolume()
+ };
+ }
+
+ function triggerSetupFailure(errorCode) {
+ if (!setupFailedCallback) {
+ return;
+ }
+
+ const payload = {
+ divId,
+ playerVersion,
+ type: SETUP_FAILED,
+ errorCode,
+ errorMessage: '',
+ sourceError: null
+ };
+ setupFailedCallback(SETUP_FAILED, payload);
+ }
+
+ function registerPreSetupListeners(type, callback, payload) {
+ let eventHandler;
+
+ switch (type) {
+ case SETUP_COMPLETE:
+ setupCompleteCallback = callback;
+ eventHandler = () => {
+ payload = getSetupCompletePayload();
+ callback(type, payload);
+ setupCompleteCallback = null;
+ };
+ player && player.on('ready', eventHandler);
+ break;
+
+ case SETUP_FAILED:
+ setupFailedCallback = callback;
+ eventHandler = e => {
+ Object.assign(payload, {
+ playerVersion,
+ errorCode: e.code,
+ errorMessage: e.message,
+ sourceError: e.sourceError
+ });
+ callback(type, payload);
+ setupFailedCallback = null;
+ };
+ player && player.on('setupError', eventHandler);
+ break;
+
+ default:
+ return;
+ }
+ callbackStorage.storeCallback(type, eventHandler, callback);
+ }
+
+ function registerPostSetupListeners(type, callback, payload) {
+ let eventHandler;
+
+ switch (type) {
+ case DESTROYED:
+ eventHandler = () => {
+ callback(type, payload);
+ };
+ player.on('remove', eventHandler);
+ break;
+
+ case AD_REQUEST:
+ eventHandler = e => {
+ payload.adTagUrl = e.tag;
+ callback(type, payload);
+ };
+ player.on(AD_REQUEST, eventHandler);
+ break;
+
+ case AD_BREAK_START:
+ eventHandler = e => {
+ timeState.clearState();
+ payload.offset = e.adPosition;
+ callback(type, payload);
+ };
+ player.on(AD_BREAK_START, eventHandler);
+ break;
+
+ case AD_LOADED:
+ eventHandler = e => {
+ adState.updateForEvent(e);
+ const adConfig = player.getConfig().advertising;
+ adState.updateState(utils.getSkipParams(adConfig));
+ Object.assign(payload, adState.getState());
+ callback(type, payload);
+ };
+ player.on(AD_LOADED, eventHandler);
+ break;
+
+ case AD_STARTED:
+ eventHandler = () => {
+ Object.assign(payload, adState.getState());
+ callback(type, payload);
+ };
+ // JW Player adImpression fires when the ad starts, regardless of viewability.
+ player.on(AD_IMPRESSION, eventHandler);
+ break;
+
+ case AD_IMPRESSION:
+ eventHandler = () => {
+ Object.assign(payload, adState.getState(), timeState.getState());
+ callback(type, payload);
+ };
+ player.on('adViewableImpression', eventHandler);
+ break;
+
+ case AD_PLAY:
+ eventHandler = e => {
+ payload.adTagUrl = e.tag;
+ callback(type, payload);
+ };
+ player.on(AD_PLAY, eventHandler);
+ break;
+
+ case AD_TIME:
+ eventHandler = e => {
+ timeState.updateForEvent(e);
+ Object.assign(payload, {
+ adTagUrl: e.tag,
+ time: e.position,
+ duration: e.duration,
+ });
+ callback(type, payload);
+ };
+ player.on(AD_TIME, eventHandler);
+ break;
+
+ case AD_PAUSE:
+ eventHandler = e => {
+ payload.adTagUrl = e.tag;
+ callback(type, payload);
+ };
+ player.on(AD_PAUSE, eventHandler);
+ break;
+
+ case AD_CLICK:
+ eventHandler = () => {
+ Object.assign(payload, adState.getState(), timeState.getState());
+ callback(type, payload);
+ };
+ player.on(AD_CLICK, eventHandler);
+ break;
+
+ case AD_SKIPPED:
+ eventHandler = e => {
+ Object.assign(payload, {
+ time: e.position,
+ duration: e.duration,
+ });
+ callback(type, payload);
+ adState.clearState();
+ };
+ player.on(AD_SKIPPED, eventHandler);
+ break;
+
+ case AD_ERROR:
+ eventHandler = e => {
+ Object.assign(payload, {
+ playerErrorCode: e.adErrorCode,
+ vastErrorCode: e.code,
+ errorMessage: e.message,
+ sourceError: e.sourceError
+ // timeout
+ }, adState.getState(), timeState.getState());
+ adState.clearState();
+ callback(type, payload);
+ };
+ player.on(AD_ERROR, eventHandler);
+ break;
+
+ case AD_COMPLETE:
+ eventHandler = e => {
+ payload.adTagUrl = e.tag;
+ callback(type, payload);
+ adState.clearState();
+ };
+ player.on(AD_COMPLETE, eventHandler);
+ break;
+
+ case AD_BREAK_END:
+ eventHandler = e => {
+ payload.offset = e.adPosition;
+ callback(type, payload);
+ };
+ player.on(AD_BREAK_END, eventHandler);
+ break;
+
+ case PLAYLIST:
+ eventHandler = e => {
+ const playlistItemCount = e.playlist.length;
+ Object.assign(payload, {
+ playlistItemCount,
+ autostart: player.getConfig().autostart
+ });
+ callback(type, payload);
+ };
+ player.on(PLAYLIST, eventHandler);
+ break;
+
+ case PLAYBACK_REQUEST:
+ eventHandler = e => {
+ payload.playReason = e.playReason;
+ callback(type, payload);
+ };
+ player.on('playAttempt', eventHandler);
+ break;
+
+ case AUTOSTART_BLOCKED:
+ eventHandler = e => {
+ Object.assign(payload, {
+ sourceError: e.error,
+ errorCode: e.code,
+ errorMessage: e.message
+ });
+ callback(type, payload);
+ };
+ player.on('autostartNotAllowed', eventHandler);
+ break;
+
+ case PLAY_ATTEMPT_FAILED:
+ eventHandler = e => {
+ Object.assign(payload, {
+ playReason: e.playReason,
+ sourceError: e.sourceError,
+ errorCode: e.code,
+ errorMessage: e.message
+ });
+ callback(type, payload);
+ };
+ player.on(PLAY_ATTEMPT_FAILED, eventHandler);
+ break;
+
+ case CONTENT_LOADED:
+ eventHandler = e => {
+ const { item, index } = e;
+ Object.assign(payload, {
+ contentId: item.mediaid,
+ contentUrl: item.file, // cover other sources ? util ?
+ title: item.title,
+ description: item.description,
+ playlistIndex: index,
+ contentTags: item.tags
+ });
+ callback(type, payload);
+ };
+ player.on('playlistItem', eventHandler);
+ break;
+
+ case PLAY:
+ eventHandler = () => {
+ callback(type, payload);
+ };
+ player.on(PLAY, eventHandler);
+ break;
+
+ case PAUSE:
+ eventHandler = () => {
+ callback(type, payload);
+ };
+ player.on(PAUSE, eventHandler);
+ break;
+
+ case BUFFER:
+ eventHandler = () => {
+ Object.assign(payload, timeState.getState());
+ callback(type, payload);
+ };
+ player.on(BUFFER, eventHandler);
+ break;
+
+ case TIME:
+ eventHandler = e => {
+ timeState.updateForEvent(e);
+ Object.assign(payload, {
+ position: e.position,
+ duration: e.duration
+ });
+ callback(type, payload);
+ };
+ player.on(TIME, eventHandler);
+ break;
+
+ case SEEK_START:
+ eventHandler = e => {
+ const duration = e.duration;
+ const offset = e.offset;
+ pendingSeek = {
+ duration,
+ offset
+ };
+ Object.assign(payload, {
+ position: e.position,
+ destination: offset,
+ duration: duration
+ });
+ callback(type, payload);
+ }
+ player.on('seek', eventHandler);
+ break;
+
+ case SEEK_END:
+ eventHandler = () => {
+ Object.assign(payload, {
+ position: pendingSeek.offset,
+ duration: pendingSeek.duration
+ });
+ callback(type, payload);
+ pendingSeek = {};
+ };
+ player.on('seeked', eventHandler);
+ break;
+
+ case MUTE:
+ eventHandler = e => {
+ payload.mute = e.mute;
+ callback(type, payload);
+ };
+ player.on(MUTE, eventHandler);
+ break;
+
+ case VOLUME:
+ eventHandler = e => {
+ payload.volumePercentage = e.volume;
+ callback(type, payload);
+ };
+ player.on(VOLUME, eventHandler);
+ break;
+
+ case RENDITION_UPDATE:
+ eventHandler = e => {
+ const bitrate = e.bitrate;
+ const level = e.level;
+ Object.assign(payload, {
+ videoReportedBitrate: bitrate,
+ audioReportedBitrate: bitrate,
+ encodedVideoWidth: level.width,
+ encodedVideoHeight: level.height,
+ videoFramerate: e.frameRate
+ });
+ callback(type, payload);
+ };
+ player.on('visualQuality', eventHandler);
+ break;
+
+ case ERROR:
+ eventHandler = e => {
+ Object.assign(payload, {
+ sourceError: e.sourceError,
+ errorCode: e.code,
+ errorMessage: e.message,
+ });
+ callback(type, payload);
+ };
+ player.on(ERROR, eventHandler);
+ break;
+
+ case COMPLETE:
+ eventHandler = e => {
+ callback(type, payload);
+ timeState.clearState();
+ };
+ player.on(COMPLETE, eventHandler);
+ break;
+
+ case PLAYLIST_COMPLETE:
+ eventHandler = () => {
+ callback(type, payload);
+ };
+ player.on(PLAYLIST_COMPLETE, eventHandler);
+ break;
+
+ case FULLSCREEN:
+ eventHandler = e => {
+ payload.fullscreen = e.fullscreen;
+ callback(type, payload);
+ };
+ player.on(FULLSCREEN, eventHandler);
+ break;
+
+ case PLAYER_RESIZE:
+ eventHandler = e => {
+ Object.assign(payload, {
+ height: e.height,
+ width: e.width,
+ });
+ callback(type, payload);
+ };
+ player.on('resize', eventHandler);
+ break;
+
+ case VIEWABLE:
+ eventHandler = e => {
+ Object.assign(payload, {
+ viewable: e.viewable,
+ viewabilityPercentage: player.getPercentViewable() * 100,
+ });
+ callback(type, payload);
+ };
+ player.on(VIEWABLE, eventHandler);
+ break;
+
+ case CAST:
+ eventHandler = e => {
+ payload.casting = e.active;
+ callback(type, payload);
+ };
+ player.on(CAST, eventHandler);
+ break;
+
+ default:
+ return;
+ }
+ callbackStorage.storeCallback(type, eventHandler, callback);
+ }
+}
+
+/**
+ * @param {videoProviderConfig} config
+ * @param {sharedUtils} sharedUtils
+ * @returns {VideoProvider}
+ */
+const jwplayerSubmoduleFactory = function (config, sharedUtils) {
+ const adState = adStateFactory();
+ const timeState = timeStateFactory();
+ const callbackStorage = callbackStorageFactory();
+ return JWPlayerProvider(config, window.jwplayer, adState, timeState, callbackStorage, utils, sharedUtils);
+}
+
+jwplayerSubmoduleFactory.vendorCode = JWPLAYER_VENDOR;
+submodule('video', jwplayerSubmoduleFactory);
+export default jwplayerSubmoduleFactory;
+
+// HELPERS
+
+export const utils = {
+ getJwConfig: function(config) {
+ if (!config) {
+ return;
+ }
+
+ const params = config.params || {};
+ const jwConfig = params.vendorConfig || {};
+ if (jwConfig.autostart === undefined && config.autoStart !== undefined) {
+ jwConfig.autostart = config.autoStart;
+ }
+
+ if (jwConfig.mute === undefined && config.mute !== undefined) {
+ jwConfig.mute = config.mute;
+ }
+
+ if (!jwConfig.key && config.licenseKey !== undefined) {
+ jwConfig.key = config.licenseKey;
+ }
+
+ if (params.adOptimization === false) {
+ return jwConfig;
+ }
+
+ const advertising = jwConfig.advertising || { client: 'vast' };
+ if (!jwConfig.file && !jwConfig.playlist && !jwConfig.source) {
+ // TODO verify accuracy
+ advertising.outstream = true;
+ }
+
+ const bids = advertising.bids || {};
+ bids.prebid = true;
+ advertising.bids = bids;
+
+ jwConfig.advertising = advertising;
+ return jwConfig;
+ },
+
+ getJwEvent: function(eventName) {
+ switch (eventName) {
+ case SETUP_COMPLETE:
+ return 'ready';
+
+ case SETUP_FAILED:
+ return 'setupError';
+
+ case DESTROYED:
+ return 'remove';
+
+ case AD_STARTED:
+ return AD_IMPRESSION;
+
+ case AD_IMPRESSION:
+ return 'adViewableImpression';
+
+ case PLAYBACK_REQUEST:
+ return 'playAttempt';
+
+ case AUTOSTART_BLOCKED:
+ return 'autostartNotAllowed';
+
+ case CONTENT_LOADED:
+ return 'playlistItem';
+
+ case SEEK_START:
+ return 'seek';
+
+ case SEEK_END:
+ return 'seeked';
+
+ case RENDITION_UPDATE:
+ return 'visualQuality';
+
+ case PLAYER_RESIZE:
+ return 'resize';
+
+ default:
+ return eventName;
+ }
+ },
+
+ getSkipParams: function(adConfig) {
+ const skipParams = {};
+ const skipoffset = adConfig.skipoffset;
+ if (skipoffset !== undefined) {
+ const skippable = skipoffset >= 0;
+ skipParams.skip = skippable ? 1 : 0;
+ if (skippable) {
+ skipParams.skipmin = skipoffset + 2;
+ skipParams.skipafter = skipoffset;
+ }
+ }
+ return skipParams;
+ },
+
+ getSupportedMediaTypes: function(mediaTypes = []) {
+ const el = document.createElement('video');
+ return mediaTypes
+ .filter(mediaType => el.canPlayType(mediaType))
+ .concat(VPAID_MIME_TYPE); // Always allow VPAIDs.
+ },
+
+ getStartDelay: function() {
+ // todo calculate
+ // need to know which ad we are bidding on
+ // Might have to implement and set in Pb-video ; would required ad unit as param.
+ },
+
+ /**
+ * Determine the ad placement
+ * @param {Object} adConfig
+ * @param {Object} player
+ * @return {PLACEMENT|OrtbVideoParams.placement|undefined}
+ */
+ getPlacement: function(adConfig, player) {
+ if (!adConfig.outstream) {
+ // https://developer.jwplayer.com/jwplayer/docs/jw8-embed-an-outstream-player for more info on outstream
+ return PLACEMENT.INSTREAM;
+ }
+
+ if (player.getFloating()) {
+ return PLACEMENT.FLOATING;
+ }
+
+ const placement = adConfig.placement;
+ if (!placement) {
+ return;
+ }
+
+ return PLACEMENT[placement.toUpperCase()];
+ },
+
+ getPlaybackMethod: function({ autoplay, mute, autoplayAdsMuted }) {
+ if (autoplay) {
+ // Determine whether player is going to start muted.
+ const isMuted = mute || autoplayAdsMuted; // todo autoplayAdsMuted only applies to preRoll
+ return isMuted ? PLAYBACK_METHODS.AUTOPLAY_MUTED : PLAYBACK_METHODS.AUTOPLAY;
+ }
+ /*
+ TODO
+ could support the following with float player:
+ 5 Initiates on Entering Viewport with Sound On
+ 6 Initiates on Entering Viewport with Sound Off by Default
+ */
+ return PLAYBACK_METHODS.CLICK_TO_PLAY;
+ },
+
+ /**
+ * Indicates if Omid is supported
+ *
+ * @param {string} adClient - The identifier of the ad plugin requesting the bid
+ * @returns {boolean} - support of omid
+ */
+ isOmidSupported: function(adClient) {
+ const omidIsLoaded = window.OmidSessionClient !== undefined;
+ return omidIsLoaded && adClient === 'vast';
+ },
+
+ /**
+ * Gets ISO 639 language code of current audio track.
+ * @param {Object} player
+ * @returns {string|undefined} ISO 639 language code.
+ */
+ getIsoLanguageCode: function(player) {
+ const audioTracks = player.getAudioTracks();
+ if (!audioTracks || !audioTracks.length) {
+ return;
+ }
+
+ const currentTrackIndex = Math.max(player.getCurrentAudioTrack() || 0, 0); // returns -1 when there are no alternative tracks.
+ const audioTrack = audioTracks[currentTrackIndex];
+ return audioTrack && audioTrack.language;
+ },
+
+ /**
+ * Converts an array of jwpsegs into an array of data segments compliant with the oRTB content.data[index].segment
+ * @param {[String]} jwpsegs - jwplayer contextual targeting segments
+ * @return {[Object]|undefined} list of data segments compliant with the oRTB content.data[index].segment spec
+ */
+ getSegments: function (jwpsegs) {
+ if (!jwpsegs || !jwpsegs.length) {
+ return;
+ }
+
+ const formattedSegments = jwpsegs.reduce((convertedSegments, rawSegment) => {
+ convertedSegments.push({
+ id: rawSegment,
+ value: rawSegment
+ });
+ return convertedSegments;
+ }, []);
+
+ return formattedSegments;
+ },
+
+ /**
+ * Creates an object compliant with the oRTB content.data[index] spec.
+ * @param {String} mediaId - content identifier
+ * @param {[Object]} segments - list of data segments compliant with the oRTB content.data[index].segment spec
+ * @return {Object} - Object compliant with the oRTB content.data[index] spec.
+ */
+ getContentDatum: function (mediaId, segments) {
+ if (!mediaId && !segments) {
+ return;
+ }
+
+ const contentData = {
+ name: 'jwplayer.com',
+ ext: {}
+ };
+
+ if (mediaId) {
+ contentData.ext.cids = [mediaId];
+ }
+
+ if (segments) {
+ contentData.segment = segments;
+ contentData.ext.segtax = 502;
+ }
+
+ return contentData;
+ }
+}
+
+/**
+ * Tracks which functions are attached to events
+ * @typedef CallbackStorage
+ * @function storeCallback
+ * @function getCallback
+ * @function clearStorage
+ */
+
+/**
+ * @returns {CallbackStorage}
+ */
+export function callbackStorageFactory() {
+ let storage = {};
+
+ function storeCallback(eventType, eventHandler, callback) {
+ let eventHandlers = storage[eventType];
+ if (!eventHandlers) {
+ eventHandlers = storage[eventType] = {};
+ }
+
+ eventHandlers[callback] = eventHandler;
+ }
+
+ function getCallback(eventType, callback) {
+ let eventHandlers = storage[eventType];
+ if (!eventHandlers) {
+ return;
+ }
+
+ const eventHandler = eventHandlers[callback];
+ delete eventHandlers[callback];
+ return eventHandler;
+ }
+
+ function clearStorage() {
+ storage = {};
+ }
+
+ return {
+ storeCallback,
+ getCallback,
+ clearStorage
+ }
+}
+
+// STATE
+
+/**
+ * @returns {State}
+ */
+export function adStateFactory() {
+ const adState = Object.assign({}, stateFactory());
+
+ function updateForEvent(event) {
+ const updates = {
+ adTagUrl: event.tag,
+ offset: event.adPosition,
+ loadTime: event.timeLoading,
+ vastAdId: event.id,
+ adDescription: event.description,
+ adServer: event.adsystem,
+ adTitle: event.adtitle,
+ advertiserId: event.advertiserId,
+ advertiserName: event.advertiser,
+ dealId: event.dealId,
+ // adCategories
+ linear: event.linear,
+ vastVersion: event.vastversion,
+ // campaignId:
+ creativeUrl: event.mediaFile, // TODO: per AP, mediafile might be object w/ file property. verify
+ adId: event.adId,
+ universalAdId: event.universalAdId,
+ creativeId: event.creativeAdId,
+ creativeType: event.creativetype,
+ redirectUrl: event.clickThroughUrl,
+ adPlacementType: convertPlacementToOrtbCode(event.placement),
+ waterfallIndex: event.witem,
+ waterfallCount: event.wcount,
+ adPodCount: event.podcount,
+ adPodIndex: event.sequence,
+ wrapperAdIds: event.wrapperAdIds
+ };
+
+ if (event.client === 'googima' && !updates.wrapperAdIds) {
+ updates.wrapperAdIds = parseImaAdWrapperIds(event);
+ }
+
+ this.updateState(updates);
+ }
+
+ adState.updateForEvent = updateForEvent;
+
+ function convertPlacementToOrtbCode(placement) {
+ switch (placement) {
+ case 'instream':
+ return PLACEMENT.INSTREAM;
+
+ case 'banner':
+ return PLACEMENT.BANNER;
+
+ case 'article':
+ return PLACEMENT.ARTICLE;
+
+ case 'feed':
+ return PLACEMENT.FEED;
+
+ case 'interstitial':
+ case 'slider':
+ case 'floating':
+ return PLACEMENT.INTERSTITIAL_SLIDER_FLOATING;
+ }
+ }
+
+ function parseImaAdWrapperIds(adEvent) {
+ const ima = adEvent.ima;
+ const ad = ima && ima.ad;
+ const h = ad && ad.h;
+ return h && h.adWrapperIds;
+ }
+
+ return adState;
+}
+
+/**
+ * @returns {State}
+ */
+export function timeStateFactory() {
+ const timeState = Object.assign({}, stateFactory());
+
+ function updateForEvent(event) {
+ const { position, duration } = event;
+ this.updateState({
+ time: position,
+ duration,
+ playbackMode: getPlaybackMode(duration)
+ });
+ }
+
+ timeState.updateForEvent = updateForEvent;
+
+ function getPlaybackMode(duration) {
+ if (duration > 0) {
+ return PLAYBACK_MODE.VOD;
+ } else if (duration < 0) {
+ return PLAYBACK_MODE.DVR;
+ }
+
+ return PLAYBACK_MODE.LIVE;
+ }
+
+ return timeState;
+}
diff --git a/modules/spotxBidAdapter.js b/modules/spotxBidAdapter.js
index 86a37e97e2f..0f5daed7f74 100644
--- a/modules/spotxBidAdapter.js
+++ b/modules/spotxBidAdapter.js
@@ -148,7 +148,7 @@ export const spec = {
}
}
- const mimes = getBidIdParameter('mimes', bid.params) || ['application/javascript', 'video/mp4', 'video/webm'];
+ const mimes = deepAccess(bid, 'mediaTypes.video.mimes') || getBidIdParameter('mimes', bid.params) || ['application/javascript', 'video/mp4', 'video/webm'];
const spotxReq = {
id: bid.bidId,
@@ -175,28 +175,29 @@ export const spec = {
spotxReq.bidfloor = getBidIdParameter('price_floor', bid.params);
}
- if (getBidIdParameter('start_delay', bid.params) != '') {
- spotxReq.video.startdelay = 0 + Boolean(getBidIdParameter('start_delay', bid.params));
+ const startdelay = deepAccess(bid, 'mediaTypes.video.startdelay') || getBidIdParameter('start_delay', bid.params);
+ if (startdelay != '') {
+ spotxReq.video.startdelay = 0 + Boolean(startdelay);
}
- if (getBidIdParameter('min_duration', bid.params) != '') {
- spotxReq.video.minduration = getBidIdParameter('min_duration', bid.params);
+ const minduration = deepAccess(bid, 'mediaTypes.video.minduration') || getBidIdParameter('min_duration', bid.params);
+ if (minduration != '') {
+ spotxReq.video.minduration = minduration;
}
- if (getBidIdParameter('max_duration', bid.params) != '') {
- spotxReq.video.maxduration = getBidIdParameter('max_duration', bid.params);
+ const maxduration = deepAccess(bid, 'mediaTypes.video.maxduration') || getBidIdParameter('max_duration', bid.params);
+ if (maxduration != '') {
+ spotxReq.video.maxduration = maxduration;
}
- if (getBidIdParameter('placement_type', bid.params) != '') {
- spotxReq.video.ext.placement = getBidIdParameter('placement_type', bid.params);
+ const placement = deepAccess(bid, 'mediaTypes.video.placement') || getBidIdParameter('placement_type', bid.params);
+ if (placement != '') {
+ spotxReq.video.ext.placement = placement;
}
- if (getBidIdParameter('position', bid.params) != '') {
- spotxReq.video.ext.pos = getBidIdParameter('position', bid.params);
- } else {
- if (deepAccess(bid, 'mediaTypes.video.pos')) {
- spotxReq.video.ext.pos = deepAccess(bid, 'mediaTypes.video.pos');
- }
+ const position = deepAccess(bid, 'mediaTypes.video.pos') || getBidIdParameter('position', bid.params);
+ if (position != '') {
+ spotxReq.video.ext.pos = position;
}
if (bid.crumbs && bid.crumbs.pubcid) {
diff --git a/modules/synacormediaBidAdapter.js b/modules/synacormediaBidAdapter.js
index a48c1aaf55b..1b034790edf 100644
--- a/modules/synacormediaBidAdapter.js
+++ b/modules/synacormediaBidAdapter.js
@@ -71,7 +71,7 @@ export const spec = {
seatId = bid.params.seatId;
}
const tagIdOrPlacementId = bid.params.tagId || bid.params.placementId;
- let pos = parseInt(bid.params.pos, 10);
+ let pos = parseInt(deepAccess(bid.mediaTypes, 'video.pos') || bid.params.pos, 10);
if (isNaN(pos)) {
logWarn(`Synacormedia: there is an invalid POS: ${bid.params.pos}`);
pos = 0;
diff --git a/modules/videoModule/constants/ortb.js b/modules/videoModule/constants/ortb.js
new file mode 100644
index 00000000000..82bee1e9159
--- /dev/null
+++ b/modules/videoModule/constants/ortb.js
@@ -0,0 +1,88 @@
+
+const VIDEO_PREFIX = 'video/'
+
+/*
+ORTB 2.5 section 3.2.7 - Video.mimes
+ */
+export const VIDEO_MIME_TYPE = {
+ MP4: VIDEO_PREFIX + 'mp4',
+ MPEG: VIDEO_PREFIX + 'mpeg',
+ OGG: VIDEO_PREFIX + 'ogg',
+ WEBM: VIDEO_PREFIX + 'webm',
+ AAC: VIDEO_PREFIX + 'aac',
+ HLS: 'application/vnd.apple.mpegurl'
+};
+
+export const JS_APP_MIME_TYPE = 'application/javascript';
+export const VPAID_MIME_TYPE = JS_APP_MIME_TYPE;
+
+/*
+ORTB 2.5 section 5.9 - Video Placement Types
+ */
+export const PLACEMENT = {
+ IN_STREAM: 1,
+ BANNER: 2,
+ ARTICLE: 3,
+ FEED: 4,
+ INTERSTITIAL: 5,
+ SLIDER: 5,
+ FLOATING: 5,
+ INTERSTITIAL_SLIDER_FLOATING: 5
+};
+
+/*
+ORTB 2.5 section 5.4 - Ad Position
+ */
+export const AD_POSITION = {
+ UNKNOWN: 0,
+ ABOVE_THE_FOLD: 1,
+ BELOW_THE_FOLD: 3,
+ HEADER: 4,
+ FOOTER: 5,
+ SIDEBAR: 6,
+ FULL_SCREEN: 7
+}
+
+/*
+ORTB 2.5 section 5.11 - Playback Cessation Modes
+ */
+export const PLAYBACK_END = {
+ VIDEO_COMPLETION: 1,
+ VIEWPORT_LEAVE: 2,
+ FLOATING: 3
+}
+
+/*
+ORTB 2.5 section 5.10 - Playback Methods
+ */
+export const PLAYBACK_METHODS = {
+ AUTOPLAY: 1,
+ AUTOPLAY_MUTED: 2,
+ CLICK_TO_PLAY: 3,
+ CLICK_TO_PLAY_MUTED: 4,
+ VIEWABLE: 5,
+ VIEWABLE_MUTED: 6
+};
+
+/*
+ORTB 2.5 section 5.8 - Protocols
+ */
+export const PROTOCOLS = {
+ // VAST_1_0: 1,
+ VAST_2_0: 2,
+ VAST_3_0: 3,
+ // VAST_1_O_WRAPPER: 4,
+ VAST_2_0_WRAPPER: 5,
+ VAST_3_0_WRAPPER: 6,
+ VAST_4_0: 7,
+ VAST_4_0_WRAPPER: 8
+};
+
+/*
+ORTB 2.5 section 5.6 - API Frameworks
+ */
+export const API_FRAMEWORKS = {
+ VPAID_1_0: 1,
+ VPAID_2_0: 2,
+ OMID_1_0: 7
+};
diff --git a/modules/videoModule/coreVideo.js b/modules/videoModule/coreVideo.js
new file mode 100644
index 00000000000..36b0a90f4ff
--- /dev/null
+++ b/modules/videoModule/coreVideo.js
@@ -0,0 +1,73 @@
+import { vendorDirectory } from './vendorDirectory.js';
+
+export function VideoCore(submoduleBuilder_) {
+ const submodules = {};
+ const submoduleBuilder = submoduleBuilder_;
+
+ function registerProvider(providerConfig) {
+ const divId = providerConfig.divId;
+ if (submodules[divId]) {
+ return;
+ }
+
+ let submodule;
+ try {
+ submodule = submoduleBuilder.build(providerConfig);
+ } catch (e) {
+ throw e;
+ }
+ submodules[divId] = submodule;
+ }
+
+ function getOrtbParams(divId) {
+ const submodule = submodules[divId];
+ return submodule && submodule.getOrtbParams();
+ }
+
+ function setAdTagUrl(adTagUrl, divId) {
+ const submodule = submodules[divId];
+ return submodule && submodule.setAdTagUrl(adTagUrl);
+ }
+
+ function onEvents(events, callback, divId) {
+ const submodule = submodules[divId];
+ return submodule && submodule.onEvents(events, callback);
+ }
+
+ function offEvents(events, callback, divId) {
+ const submodule = submodules[divId];
+ return submodule && submodule.offEvents(events, callback);
+ }
+
+ return {
+ registerProvider,
+ getOrtbParams,
+ setAdTagUrl,
+ onEvents,
+ offEvents
+ };
+}
+
+export function videoCoreFactory() {
+ const submoduleBuilder = VideoSubmoduleBuilder(vendorDirectory);
+ return VideoCore(submoduleBuilder);
+}
+
+export function VideoSubmoduleBuilder(vendorDirectory_) {
+ const vendorDirectory = vendorDirectory_;
+
+ function build(providerConfig) {
+ const submoduleFactory = vendorDirectory[providerConfig.vendorCode];
+ if (!submoduleFactory) {
+ throw new Error('Unrecognized vendor code');
+ }
+
+ const submodule = submoduleFactory(providerConfig);
+ submodule && submodule.init && submodule.init();
+ return submodule;
+ }
+
+ return {
+ build
+ };
+}
diff --git a/modules/videoModule/index.js b/modules/videoModule/index.js
new file mode 100644
index 00000000000..73610eb7ed6
--- /dev/null
+++ b/modules/videoModule/index.js
@@ -0,0 +1,143 @@
+import { config } from '../../src/config.js';
+import events from '../../src/events.js';
+import { allVideoEvents } from './constants/events.js';
+import CONSTANTS from '../../src/constants.json';
+import { videoCoreFactory } from './coreVideo.js';
+import { coreAdServerFactory } from './adServer.js';
+import find from 'core-js-pure/features/array/find.js';
+import { vastXmlEditorFactory } from './shared/vastXmlEditor.js';
+
+events.addEvents(allVideoEvents);
+
+export function PbVideo(videoCore_, getConfig_, pbGlobal_, pbEvents_, videoEvents_, adServerCore_, vastXmlEditor_) {
+ const videoCore = videoCore_;
+ const getConfig = getConfig_;
+ const pbGlobal = pbGlobal_;
+ const requestBids = pbGlobal.requestBids;
+ const pbEvents = pbEvents_;
+ const videoEvents = videoEvents_;
+ const adServerCore = adServerCore_;
+ const vastXmlEditor = vastXmlEditor_;
+
+ function init() {
+ getConfig('video', ({ video }) => {
+ video.providers.forEach(provider => {
+ try {
+ videoCore.registerProvider(provider);
+ videoCore.onEvents(videoEvents, (type, payload) => {
+ pbEvents.emit(type, payload);
+ }, provider.divId);
+ } catch (e) {}
+
+ const adServerConfig = provider.adServer;
+ if (adServerConfig) {
+ adServerCore.registerAdServer(adServerConfig.vendorCode, adServerConfig.params);
+ }
+ });
+ });
+
+ requestBids.before(enrichAdUnits, 40);
+
+ pbEvents.on(CONSTANTS.EVENTS.AUCTION_END, function(auctionResult) {
+ auctionResult.adUnits.forEach(adUnit => {
+ if (adUnit.video) {
+ renderWinningBid(adUnit);
+ }
+ });
+ });
+
+ const cache = getConfig('cache');
+ if (!cache) {
+ return;
+ }
+
+ pbEvents.on(CONSTANTS.EVENTS.BID_ADJUSTMENT, function (bid) {
+ const adUnitCode = bid.adUnitCode;
+ const adUnit = find(pbGlobal.adUnits, adUnit => adUnitCode === adUnit.code);
+ const videoConfig = adUnit && adUnit.video;
+ const adServerConfig = videoConfig && videoConfig.adServer;
+ const trackingConfig = adServerConfig && adServerConfig.tracking;
+ addTrackingNodesToVastXml(bid, trackingConfig);
+ });
+ }
+
+ return { init };
+
+ function enrichAdUnits(nextFn, bidRequest) {
+ const adUnits = bidRequest.adUnits || pbGlobal.adUnits || [];
+ adUnits.forEach(adUnit => {
+ const oRtbParams = videoCore.getOrtbParams(adUnit.video.divId);
+ adUnit.mediaTypes.video = Object.assign({}, adUnit.mediaTypes.video, oRtbParams);
+ });
+ return nextFn.call(this, bidRequest);
+ }
+
+ function renderWinningBid(adUnit) {
+ const videoConfig = adUnit.video;
+ const divId = videoConfig.divId;
+ const adServerConfig = videoConfig.adServer;
+ let adTagUrl;
+ if (adServerConfig) {
+ adTagUrl = adServerCore.getAdTagUrl(adServerConfig.vendorCode, adUnit, adServerConfig.baseAdTagUrl);
+ }
+
+ const adUnitCode = adUnit.code;
+ const options = { adUnitCode };
+ if (adTagUrl) {
+ videoCore.setAdTagUrl(adTagUrl, divId, options);
+ return;
+ }
+
+ const highestCpmBids = pbGlobal.getHighestCpmBids(adUnit.code);
+ const highestBid = highestCpmBids && highestCpmBids.shift();
+ if (!highestBid) {
+ return;
+ }
+
+ adTagUrl = highestBid.vastUrl;
+ options.adXml = highestBid.vastXml;
+ videoCore.setAdTagUrl(adTagUrl, divId, options);
+ }
+
+ function addTrackingNodesToVastXml(bid, trackingConfig) {
+ if (!trackingConfig) {
+ return;
+ }
+
+ let { vastXml, vastUrl, adId } = bid;
+ let impressionUrl;
+ let impressionId;
+ let errorUrl;
+
+ const impressionTracking = trackingConfig.impression;
+ const errorTracking = trackingConfig.error;
+
+ if (impressionTracking) {
+ impressionUrl = impressionTracking.url;
+ impressionId = impressionTracking.id || adId + '-impression';
+ }
+
+ if (errorTracking) {
+ errorUrl = errorTracking.url;
+ }
+
+ if (vastXml) {
+ vastXml = vastXmlEditor.getVastXmlWithTrackingNodes(vastXml, impressionUrl, impressionId, errorUrl);
+ } else if (vastUrl) {
+ vastXml = vastXmlEditor.buildVastWrapper(adId, vastUrl, impressionUrl, impressionId, errorUrl);
+ }
+
+ bid.vastXml = vastXml;
+ }
+}
+
+export function pbVideoFactory() {
+ const videoCore = videoCoreFactory();
+ const adServerCore = coreAdServerFactory();
+ const vastXmlEditor = vastXmlEditorFactory();
+ const pbVideo = PbVideo(videoCore, config.getConfig, $$PREBID_GLOBAL$$, events, allVideoEvents, adServerCore, vastXmlEditor);
+ pbVideo.init();
+ return pbVideo;
+}
+
+pbVideoFactory();
diff --git a/modules/videojsVideoProvider.js b/modules/videojsVideoProvider.js
new file mode 100644
index 00000000000..e72fc54719c
--- /dev/null
+++ b/modules/videojsVideoProvider.js
@@ -0,0 +1,566 @@
+import {
+ SETUP_COMPLETE, SETUP_FAILED, DESTROYED, AD_REQUEST, AD_BREAK_START, AD_LOADED, AD_STARTED, AD_IMPRESSION, AD_PLAY,
+ AD_TIME, AD_PAUSE, AD_CLICK, AD_SKIPPED, AD_ERROR, AD_COMPLETE, AD_BREAK_END, PLAYLIST, PLAYBACK_REQUEST,
+ AUTOSTART_BLOCKED, PLAY_ATTEMPT_FAILED, CONTENT_LOADED, PLAY, PAUSE, BUFFER, TIME, SEEK_START, SEEK_END, MUTE, VOLUME,
+ RENDITION_UPDATE, ERROR, COMPLETE, PLAYLIST_COMPLETE, FULLSCREEN, PLAYER_RESIZE, VIEWABLE, CAST, PLAYBACK_MODE
+} from './videoModule/constants/events.js';
+import {
+ PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, PLACEMENT, VPAID_MIME_TYPE, AD_POSITION, PLAYBACK_END
+} from './videoModule/constants/ortb.js';
+import { VIDEO_JS_VENDOR } from './videoModule/constants/vendorCodes.js';
+import { videoVendorDirectory } from './videoModule/vendorDirectory.js';
+
+export function VideojsProvider(config, videojs_, adState_, timeState_, callbackStorage_, utils) {
+ let videojs = videojs_;
+ // Supplied callbacks are typically wrapped by handlers
+ // we use this dict to keep track of these pairings
+ const callback_to_handler = {};
+
+ let player = null;
+ let playerVersion = null;
+ let imaOptions = null;
+ const {playerConfig, divId} = config;
+
+ let setupCompleteCallback, setupFailedCallback;
+
+ // TODO: test with older videojs versions
+ let minimumSupportedPlayerVersion = '7.17.0';
+
+ function init() {
+ if (!videojs) {
+ triggerSetupFailure(-1, 'Videojs not present')
+ return;
+ }
+ playerVersion = videojs.VERSION;
+
+ if (playerVersion < minimumSupportedPlayerVersion) {
+ triggerSetupFailure(-2, 'Videojs version not supported')
+ return;
+ }
+
+ if (!document.getElementById(divId)) {
+ triggerSetupFailure(-3, `No div found with id ${divId}`)
+ return;
+ }
+
+ player = videojs(divId)
+
+ function adSetup(){
+ // Todo: the linter doesn't like optional chaining is there a better way to do this
+ const vendorConfig = config.playerConfig && config.playerConfig.params && config.playerConfig.params.vendorConfig;
+ const tags = vendorConfig && vendorConfig.advertising && vendorConfig.advertising.tag;
+ if (player.ima && tags) {
+ imaOptions = {
+ adTagUrl: tags[0]
+ };
+ player.ima(imaOptions);
+ }
+ }
+
+ // Instantiate player if it does not exist
+ if(!player){
+ // setupCompleteCallback should already be hooked to player.ready so no need to include it here
+ player = videojs(divId, playerConfig, adSetup)
+ setupFailedCallback && player.on('error', callback_to_handler[setupFailedCallback])
+ setupCompleteCallback && player.on('ready', callback_to_handler[setupCompleteCallback])
+ return
+ }
+
+ setupCompleteCallback && setupCompleteCallback(SETUP_COMPLETE, getSetupCompletePayload());
+ adSetup();
+
+ // TODO: make sure ortb gets called in integration example
+ // currently testing with a hacky solution by hooking it to window
+ // window.ortb = this.getOrtbParams
+ }
+
+ function triggerSetupFailure(errorCode, msg) {
+ const payload = {
+ divId,
+ playerVersion,
+ type: SETUP_FAILED,
+ errorCode,
+ errorMessage: msg,
+ sourceError: null
+ };
+ setupFailedCallback && setupFailedCallback(SETUP_FAILED, payload);
+ }
+
+ function getId() {
+ return divId;
+ }
+
+ function getOrtbParams() {
+ if (!player) {
+ return null;
+ }
+
+ const content = {
+ // id:, TODO: find a suitable id for videojs sources
+ url: player.currentSrc()
+ };
+ // Only include length if player is ready
+ // player.readyState() returns a level of readiness from 0 to 4
+ // https://docs.videojs.com/player#readyState
+ if (player.readyState() > 0) {
+ content.len = Math.round(player.duration());
+ }
+ const mediaItem = player.getMedia();
+ if (mediaItem) {
+ for (let param of ['album', 'artist', 'title']) {
+ if (mediaItem[param]) {
+ content[param] = mediaItem[param];
+ }
+ }
+ }
+
+ let playBackMethod = PLAYBACK_METHODS.CLICK_TO_PLAY;
+ // returns a boolean or a string with the autoplay strategy
+ const autoplay = player.autoplay();
+ const muted = player.muted() || autoplay === 'muted';
+ // check if autoplay is truthy since it may be a bool or string
+ if (autoplay) {
+ playBackMethod = muted ? PLAYBACK_METHODS.AUTOPLAY_MUTED : PLAYBACK_METHODS.AUTOPLAY;
+ }
+ const supportedMediaTypes = Object.values(VIDEO_MIME_TYPE).filter(
+ // Follows w3 spec https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype
+ type => player.canPlayType(type) !== ''
+ )
+ // IMA supports vpaid unless its expliclty turned off
+ if (imaOptions && imaOptions.vpaidMode !== 0) {
+ supportedMediaTypes.push(VPAID_MIME_TYPE);
+ }
+
+ const video = {
+ mimes: supportedMediaTypes,
+ // Based on the protocol support provided by the videojs-ima plugin
+ // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/compatibility
+ // Need to check for the plugins
+ protocols: imaOptions ? [
+ PROTOCOLS.VAST_2_0,
+ ] : [],
+ api: imaOptions ? [
+ API_FRAMEWORKS.VPAID_2_0
+ ] : [],
+ // TODO: Make sure this returns dimensions in DIPS
+ h: player.currentHeight(),
+ w: player.currentWidth(),
+ // TODO: implement startdelay since its reccomend param
+ // both linearity forms are supported so the param is excluded
+ // sequence - TODO not yet supported
+ maxextended: -1,
+ boxingallowed: 1,
+ playbackmethod: [ playBackMethod ],
+ playbackend: PLAYBACK_END.VIDEO_COMPLETION,
+ // Per ortb 7.4 skip is omitted since neither the player nor ima plugin imposes a skip button, or a skipmin/max
+ };
+
+ // TODO: Determine placement may not be in stream if videojs is only used to serve ad content
+ // ~ Sort of resolved check if the player has a source to tell if the placement is instream
+ // Still cannot reliably check what type of placement the player is if its outstream
+ // i.e. we can't tell if its interstitial, in article, etc.
+ if (content.url) {
+ video.placement = PLACEMENT.IN_STREAM;
+ }
+
+ // Placement according to IQG Guidelines 4.2.8
+ // https://cdn2.hubspot.net/hubfs/2848641/TrustworthyAccountabilityGroup_May2017/Docs/TAG-Inventory-Quality-Guidelines-v2_2-10-18-2016.pdf?t=1509469105938
+ const findPosition = videojs.dom.findPosition;
+ if (player.isFullscreen()) {
+ video.pos = AD_POSITION.FULL_SCREEN;
+ } else if (findPosition) {
+ video.pos = utils.getPositionCode(findPosition(player.el()))
+ }
+
+ return {video, content};
+ }
+
+ // Plugins to integrate: https://github.com/googleads/videojs-ima
+ function setAdTagUrl(adTagUrl, options) {
+ }
+
+ // Should this function return some sort of signal
+ // to specify whether or not the callback was succesfully hooked?
+ function onEvents(events, callback) {
+ if (!callback) {
+ return;
+ }
+
+ for (let i = 0; i < events.length; i++) {
+ const type = events[i];
+ const payload = {
+ divId,
+ type
+ };
+
+ registerPreSetupListeners(type, callback, payload);
+ if (!player) {
+ return;
+ }
+
+ registerPostSetupListeners(type, callback, payload);
+ }
+ }
+
+ function getSetupCompletePayload() {
+ return {
+ divId,
+ playerVersion,
+ type: SETUP_COMPLETE,
+ };
+ }
+
+ function registerPreSetupListeners(type, callback, payload) {
+ let eventHandler;
+ switch (type) {
+ case SETUP_COMPLETE:
+ setupCompleteCallback = callback
+ eventHandler = () => {
+ payload = getSetupCompletePayload();
+ callback(type, payload);
+ setupCompleteCallback = null;
+ };
+ break;
+ case SETUP_FAILED:
+ setupFailedCallback = callback
+ eventHandler = () => {
+ // Videojs has no specific setup error handler
+ // so we imitate it by hooking to the general error
+ // handler and checking to see if the player has been setup
+ if (player.readyState() == 0) {
+ const e = player.error()
+ Object.assign(payload, {
+ playerVersion,
+ errorCode: e.errorTypes,
+ errorMessage: e.message,
+ });
+ callback(type, payload);
+ setupFailedCallback = null;
+ }
+ }
+ break;
+ default:
+ return
+ }
+ callback_to_handler[callback] = eventHandler
+ player && player.on(utils.getVideojsEventName(type), eventHandler);
+ }
+
+ function registerPostSetupListeners(type, callback, payload){
+ let eventHandler;
+
+ switch (type) {
+ case DESTROYED:
+ eventHandler = () => {
+ callback(type, payload);
+ };
+ player.on(utils.getVideojsEventName(type), eventHandler);
+ break;
+
+ case PLAY:
+ eventHandler = () => {
+ callback(type, payload);
+ };
+ player.on(utils.getVideojsEventName(type), eventHandler);
+ break;
+
+ case PAUSE:
+ eventHandler = () => {
+ callback(type, payload);
+ };
+ player.on(utils.getVideojsEventName(type), eventHandler);
+ break;
+
+ case BUFFER:
+ eventHandler = () => {
+ Object.assign(payload, {
+ position: 0,
+ duration: 0,
+ playbackMode: -1
+ });
+ callback(type, payload);
+ };
+ player.el().addEventListener('waiting', eventHandler);
+ break;
+
+ // TODO: No time event fired by videojs
+ case TIME:
+ eventHandler = e => {
+ Object.assign(payload, {
+ position: e.position,
+ duration: e.duration
+ });
+ callback(type, payload);
+ };
+ player.el().addEventListener('timeupdate', eventHandler);
+ break;
+
+ case PLAYLIST:
+ eventHandler = e => {
+ const playlistItemCount = e.playlist.length;
+ Object.assign(payload, {
+ playlistItemCount,
+ autostart: player.getConfig().autostart
+ });
+ callback(type, payload);
+ };
+ break;
+ case PLAYBACK_REQUEST:
+ eventHandler = e => {
+ payload.playReason = e.playReason;
+ callback(type, payload);
+ };
+ break;
+ case AUTOSTART_BLOCKED:
+ eventHandler = e => {
+ Object.assign(payload, {
+ sourceError: e.error,
+ errorCode: e.code,
+ errorMessage: e.message
+ });
+ callback(type, payload);
+ };
+ break;
+ case PLAY_ATTEMPT_FAILED:
+ eventHandler = e => {
+ Object.assign(payload, {
+ playReason: e.playReason,
+ sourceError: e.sourceError,
+ errorCode: e.code,
+ errorMessage: e.message
+ });
+ callback(type, payload);
+ };
+ break;
+
+ case CONTENT_LOADED:
+ eventHandler = e => {
+ const {target} = e
+ Object.assign(payload, {
+ contentId: target.currentSrc,
+ contentUrl: target.currentSrc, // cover other sources ? util ?
+ title: null,
+ description: null,
+ playlistIndex: null,
+ contentTags: null
+ });
+ callback(type, payload);
+ };
+ player.el().addEventListener('loadeddata', eventHandler);
+ break;
+
+ case SEEK_START:
+ eventHandler = e => {
+ const duration = e.duration;
+ const offset = e.offset;
+ pendingSeek = {
+ duration,
+ offset
+ };
+ Object.assign(payload, {
+ position: e.position,
+ destination: offset,
+ duration: duration
+ });
+ callback(type, payload);
+ }
+ player.on('seek', eventHandler);
+ break;
+
+ case SEEK_END:
+ eventHandler = () => {
+ Object.assign(payload, {
+ position: pendingSeek.offset,
+ duration: pendingSeek.duration
+ });
+ callback(type, payload);
+ pendingSeek = {};
+ };
+ player.on('seeked', eventHandler);
+ break;
+
+ case MUTE:
+ eventHandler = e => {
+ payload.mute = e.mute;
+ callback(type, payload);
+ };
+ player.on(MUTE, eventHandler);
+ break;
+
+ case VOLUME:
+ eventHandler = e => {
+ payload.volumePercentage = e.volume;
+ callback(type, payload);
+ };
+ player.on(VOLUME, eventHandler);
+ break;
+
+ case RENDITION_UPDATE:
+ eventHandler = e => {
+ const bitrate = e.bitrate;
+ const level = e.level;
+ Object.assign(payload, {
+ videoReportedBitrate: bitrate,
+ audioReportedBitrate: bitrate,
+ encodedVideoWidth: level.width,
+ encodedVideoHeight: level.height,
+ videoFramerate: e.frameRate
+ });
+ callback(type, payload);
+ };
+ player.on('visualQuality', eventHandler);
+ break;
+
+ case ERROR:
+ eventHandler = e => {
+ Object.assign(payload, {
+ sourceError: e.sourceError,
+ errorCode: e.code,
+ errorMessage: e.message,
+ });
+ callback(type, payload);
+ };
+ player.on(ERROR, eventHandler);
+ break;
+
+ case COMPLETE:
+ eventHandler = e => {
+ callback(type, payload);
+ timeState.clearState();
+ };
+ player.on(COMPLETE, eventHandler);
+ break;
+
+ case PLAYLIST_COMPLETE:
+ eventHandler = () => {
+ callback(type, payload);
+ };
+ player.on(PLAYLIST_COMPLETE, eventHandler);
+ break;
+
+ case FULLSCREEN:
+ eventHandler = e => {
+ payload.fullscreen = e.fullscreen;
+ callback(type, payload);
+ };
+ player.on(FULLSCREEN, eventHandler);
+ break;
+
+ case PLAYER_RESIZE:
+ eventHandler = e => {
+ Object.assign(payload, {
+ height: e.height,
+ width: e.width,
+ });
+ callback(type, payload);
+ };
+ player.on('resize', eventHandler);
+ break;
+
+ case VIEWABLE:
+ eventHandler = e => {
+ Object.assign(payload, {
+ viewable: e.viewable,
+ viewabilityPercentage: player.getPercentViewable() * 100,
+ });
+ callback(type, payload);
+ };
+ player.on(VIEWABLE, eventHandler);
+ break;
+
+ case CAST:
+ eventHandler = e => {
+ payload.casting = e.active;
+ callback(type, payload);
+ };
+ player.on(CAST, eventHandler);
+ break;
+
+ default:
+ return;
+ }
+
+ }
+
+
+ function offEvents(events, callback) {
+ for (let event of events) {
+ const videojsEvent = utils.getVideojsEventName(event)
+ if (!callback) {
+ player.off(videojsEvent);
+ continue;
+ }
+
+ const eventHandler = callbackStorage.getCallback(event, callback);
+ if (eventHandler) {
+ player.off(videojsEvent, eventHandler);
+ }
+ }
+ }
+
+ function destroy() {
+ if (!player) {
+ return;
+ }
+ player.remove();
+ player = null;
+ }
+
+ return {
+ init,
+ getId,
+ getOrtbParams,
+ setAdTagUrl,
+ onEvents,
+ offEvents,
+ destroy
+ };
+}
+
+export const utils = {
+ getPositionCode: function({left, top, width, height}) {
+ const bottom = window.innerHeight - top - height;
+ const right = window.innerWidth - left - width;
+
+ if (left < 0 || right < 0 || top < 0) {
+ return AD_POSITION.UNKNOWN;
+ }
+
+ return bottom >= 0 ? AD_POSITION.ABOVE_THE_FOLD : AD_POSITION.BELOW_THE_FOLD;
+ },
+ getVideojsEventName: function(eventName) {
+ switch (eventName) {
+ case SETUP_COMPLETE:
+ return 'ready';
+ case SETUP_FAILED:
+ return 'error';
+ case DESTROYED:
+ return 'dispose';
+ case CONTENT_LOADED:
+ return 'loadeddata';
+ case SEEK_START:
+ return 'seeking';
+ case SEEK_END:
+ return 'timeupdate';
+ case VOLUME:
+ return 'volumechange';
+ case PLAYER_RESIZE:
+ return 'playerresize';
+ default:
+ return eventName;
+ }
+ }
+};
+
+const videojsSubmoduleFactory = function (config) {
+ const adState = null;
+ const timeState = null;
+ const callbackStorage = callbackStorageFactory();
+ // videojs factory is stored to window by default
+ const vjs = window.videojs;
+ return VideojsProvider(config, vjs, adState, timeState, callbackStorage, utils);
+}
+videojsSubmoduleFactory.vendorCode = VIDEO_JS_VENDOR;
+
+videoVendorDirectory[VIDEO_JS_VENDOR] = videojsSubmoduleFactory;
+export default videojsSubmoduleFactory;
diff --git a/package.json b/package.json
index c4a83e22774..0b0a797e62f 100644
--- a/package.json
+++ b/package.json
@@ -105,6 +105,9 @@
"through2": "^4.0.2",
"url": "^0.11.0",
"url-parse": "^1.0.5",
+ "video.js": "^7.17.0",
+ "videojs-contrib-ads": "^6.9.0",
+ "videojs-ima": "^1.11.0",
"webdriverio": "^7.6.1",
"webpack": "^5.70.0",
"webpack-bundle-analyzer": "^4.5.0",
diff --git a/src/events.js b/src/events.js
index e675ffef7a9..8e6117c3abf 100644
--- a/src/events.js
+++ b/src/events.js
@@ -133,6 +133,10 @@ const _public = (function () {
return _handlers;
};
+ _public.addEvents = function (events) {
+ allEvents = allEvents.concat(events);
+ }
+
/**
* This method can return a copy of all the events fired
* @return {Array} array of events fired
@@ -152,4 +156,4 @@ const _public = (function () {
utils._setEventEmitter(_public.emit.bind(_public));
-export const {on, off, get, getEvents, emit} = _public;
+export const {on, off, get, getEvents, emit, addEvents} = _public;
diff --git a/src/prebid.js b/src/prebid.js
index ab336af08b6..1c35cc5505c 100644
--- a/src/prebid.js
+++ b/src/prebid.js
@@ -450,86 +450,99 @@ $$PREBID_GLOBAL$$.renderAd = hook('async', function (doc, id, options) {
logInfo('Invoking $$PREBID_GLOBAL$$.renderAd', arguments);
logMessage('Calling renderAd with adId :' + id);
- if (doc && id) {
- try {
- // lookup ad by ad Id
- const bid = auctionManager.findBidByAdId(id);
-
- if (bid) {
- let shouldRender = true;
- if (bid && bid.status === CONSTANTS.BID_STATUS.RENDERED) {
- logWarn(`Ad id ${bid.adId} has been rendered before`);
- events.emit(STALE_RENDER, bid);
- if (deepAccess(config.getConfig('auctionOptions'), 'suppressStaleRender')) {
- shouldRender = false;
- }
- }
-
- if (shouldRender) {
- // replace macros according to openRTB with price paid = bid.cpm
- bid.ad = replaceAuctionPrice(bid.ad, bid.originalCpm || bid.cpm);
- bid.adUrl = replaceAuctionPrice(bid.adUrl, bid.originalCpm || bid.cpm);
- // replacing clickthrough if submitted
- if (options && options.clickThrough) {
- const {clickThrough} = options;
- bid.ad = replaceClickThrough(bid.ad, clickThrough);
- bid.adUrl = replaceClickThrough(bid.adUrl, clickThrough);
- }
-
- // save winning bids
- auctionManager.addWinningBid(bid);
-
- // emit 'bid won' event here
- events.emit(BID_WON, bid);
-
- const {height, width, ad, mediaType, adUrl, renderer} = bid;
-
- const creativeComment = document.createComment(`Creative ${bid.creativeId} served by ${bid.bidder} Prebid.js Header Bidding`);
- insertElement(creativeComment, doc, 'html');
-
- if (isRendererRequired(renderer)) {
- executeRenderer(renderer, bid, doc);
- reinjectNodeIfRemoved(creativeComment, doc, 'html');
- emitAdRenderSucceeded({ doc, bid, id });
- } else if ((doc === document && !inIframe()) || mediaType === 'video') {
- const message = `Error trying to write ad. Ad render call ad id ${id} was prevented from writing to the main document.`;
- emitAdRenderFail({reason: PREVENT_WRITING_ON_MAIN_DOCUMENT, message, bid, id});
- } else if (ad) {
- doc.write(ad);
- doc.close();
- setRenderSize(doc, width, height);
- reinjectNodeIfRemoved(creativeComment, doc, 'html');
- callBurl(bid);
- emitAdRenderSucceeded({ doc, bid, id });
- } else if (adUrl) {
- const iframe = createInvisibleIframe();
- iframe.height = height;
- iframe.width = width;
- iframe.style.display = 'inline';
- iframe.style.overflow = 'hidden';
- iframe.src = adUrl;
-
- insertElement(iframe, doc, 'body');
- setRenderSize(doc, width, height);
- reinjectNodeIfRemoved(creativeComment, doc, 'html');
- callBurl(bid);
- emitAdRenderSucceeded({ doc, bid, id });
- } else {
- const message = `Error trying to write ad. No ad for bid response id: ${id}`;
- emitAdRenderFail({reason: NO_AD, message, bid, id});
- }
- }
- } else {
- const message = `Error trying to write ad. Cannot find ad by given id : ${id}`;
- emitAdRenderFail({ reason: CANNOT_FIND_AD, message, id });
+ if (!id) {
+ const message = `Error trying to write ad Id :${id} to the page. Missing adId`;
+ emitAdRenderFail({ reason: MISSING_DOC_OR_ADID, message, id });
+ return;
+ }
+
+ try {
+ // lookup ad by ad Id
+ const bid = auctionManager.findBidByAdId(id);
+ if (!bid) {
+ const message = `Error trying to write ad. Cannot find ad by given id : ${id}`;
+ emitAdRenderFail({ reason: CANNOT_FIND_AD, message, id });
+ return;
+ }
+
+ if (bid.status === CONSTANTS.BID_STATUS.RENDERED) {
+ logWarn(`Ad id ${bid.adId} has been rendered before`);
+ events.emit(STALE_RENDER, bid);
+ if (deepAccess(config.getConfig('auctionOptions'), 'suppressStaleRender')) {
+ return;
}
- } catch (e) {
- const message = `Error trying to write ad Id :${id} to the page:${e.message}`;
- emitAdRenderFail({ reason: EXCEPTION, message, id });
}
- } else {
- const message = `Error trying to write ad Id :${id} to the page. Missing document or adId`;
- emitAdRenderFail({ reason: MISSING_DOC_OR_ADID, message, id });
+
+ // replace macros according to openRTB with price paid = bid.cpm
+ bid.ad = replaceAuctionPrice(bid.ad, bid.originalCpm || bid.cpm);
+ bid.adUrl = replaceAuctionPrice(bid.adUrl, bid.originalCpm || bid.cpm);
+ // replacing clickthrough if submitted
+ if (options && options.clickThrough) {
+ const {clickThrough} = options;
+ bid.ad = replaceClickThrough(bid.ad, clickThrough);
+ bid.adUrl = replaceClickThrough(bid.adUrl, clickThrough);
+ }
+
+ // save winning bids
+ auctionManager.addWinningBid(bid);
+
+ // emit 'bid won' event here
+ events.emit(BID_WON, bid);
+
+ const {height, width, ad, mediaType, adUrl, renderer} = bid;
+
+ // video module
+ const adUnitCode = bid.adUnitCode;
+ const adUnit = $$PREBID_GLOBAL$$.adUnits.filter(adUnit => adUnit.code === adUnitCode);
+ const videoModule = $$PREBID_GLOBAL$$.videoModule;
+ if (adUnit.video && videoModule) {
+ videoModule.renderBid(adUnit.video.divId, bid);
+ return;
+ }
+
+ if (!doc) {
+ const message = `Error trying to write ad Id :${id} to the page. Missing document`;
+ emitAdRenderFail({ reason: MISSING_DOC_OR_ADID, message, id });
+ return;
+ }
+
+ const creativeComment = document.createComment(`Creative ${bid.creativeId} served by ${bid.bidder} Prebid.js Header Bidding`);
+ insertElement(creativeComment, doc, 'html');
+
+ if (isRendererRequired(renderer)) {
+ executeRenderer(renderer, bid, doc);
+ reinjectNodeIfRemoved(creativeComment, doc, 'html');
+ emitAdRenderSucceeded({ doc, bid, id });
+ } else if ((doc === document && !inIframe()) || mediaType === 'video') {
+ const message = `Error trying to write ad. Ad render call ad id ${id} was prevented from writing to the main document.`;
+ emitAdRenderFail({reason: PREVENT_WRITING_ON_MAIN_DOCUMENT, message, bid, id});
+ } else if (ad) {
+ doc.write(ad);
+ doc.close();
+ setRenderSize(doc, width, height);
+ reinjectNodeIfRemoved(creativeComment, doc, 'html');
+ callBurl(bid);
+ emitAdRenderSucceeded({ doc, bid, id });
+ } else if (adUrl) {
+ const iframe = createInvisibleIframe();
+ iframe.height = height;
+ iframe.width = width;
+ iframe.style.display = 'inline';
+ iframe.style.overflow = 'hidden';
+ iframe.src = adUrl;
+
+ insertElement(iframe, doc, 'body');
+ setRenderSize(doc, width, height);
+ reinjectNodeIfRemoved(creativeComment, doc, 'html');
+ callBurl(bid);
+ emitAdRenderSucceeded({ doc, bid, id });
+ } else {
+ const message = `Error trying to write ad. No ad for bid response id: ${id}`;
+ emitAdRenderFail({reason: NO_AD, message, bid, id});
+ }
+ } catch (e) {
+ const message = `Error trying to write ad Id :${id} to the page:${e.message}`;
+ emitAdRenderFail({ reason: EXCEPTION, message, id });
}
});
diff --git a/test/spec/modules/videoModule/coreVideo_spec.js b/test/spec/modules/videoModule/coreVideo_spec.js
new file mode 100644
index 00000000000..4926c165af1
--- /dev/null
+++ b/test/spec/modules/videoModule/coreVideo_spec.js
@@ -0,0 +1,88 @@
+import { expect } from 'chai';
+import { VideoCore } from 'libraries/video/coreVideo.js';
+
+describe('Video Core', function () {
+ const mockSubmodule = {
+ getOrtbParams: sinon.spy(),
+ setAdTagUrl: sinon.spy(),
+ onEvents: sinon.spy(),
+ offEvents: sinon.spy(),
+ };
+
+ const otherSubmodule = {
+ getOrtbParams: () => {},
+ setAdTagUrl: () => {},
+ onEvents: () => {},
+ offEvents: () => {},
+ };
+
+ const testId = 'test_id';
+ const testVendorCode = 0;
+ const otherId = 'other_id';
+ const otherVendorCode = 1;
+
+ const parentModuleMock = {
+ registerSubmodule: sinon.spy(),
+ getSubmodule: sinon.spy(id => {
+ if (id === testId) {
+ return mockSubmodule;
+ } else if (id === otherId) {
+ return otherSubmodule;
+ }
+ })
+ };
+
+ const videoCore = VideoCore(parentModuleMock);
+
+ videoCore.registerProvider({
+ vendorCode: testVendorCode,
+ divId: testId
+ });
+
+ videoCore.registerProvider({
+ vendorCode: otherVendorCode,
+ divId: otherId
+ });
+
+ describe('registerProvider', function () {
+ it('should delegate the registration to the Parent Module', function () {
+ expect(parentModuleMock.registerSubmodule.calledTwice).to.be.true;
+ expect(parentModuleMock.registerSubmodule.args[0][0]).to.be.equal(testId);
+ expect(parentModuleMock.registerSubmodule.args[1][0]).to.be.equal(otherId);
+ expect(parentModuleMock.registerSubmodule.args[0][1]).to.be.equal(testVendorCode);
+ expect(parentModuleMock.registerSubmodule.args[1][1]).to.be.equal(otherVendorCode);
+ });
+ });
+
+ describe('getOrtbParams', function () {
+ it('delegates to the submodule of the right divId', function () {
+ videoCore.getOrtbParams(testId);
+ videoCore.getOrtbParams(otherId);
+ expect(mockSubmodule.getOrtbParams.calledOnce).to.be.true;
+ });
+ });
+
+ describe('setAdTagUrl', function () {
+ it('delegates to the submodule of the right divId', function () {
+ videoCore.setAdTagUrl('', testId);
+ videoCore.setAdTagUrl('', otherId);
+ expect(mockSubmodule.setAdTagUrl.calledOnce).to.be.true;
+ });
+ });
+
+ describe('onEvents', function () {
+ it('delegates to the submodule of the right divId', function () {
+ videoCore.onEvents([], () => {}, testId);
+ videoCore.onEvents([], () => {}, otherId);
+ expect(mockSubmodule.onEvents.calledOnce).to.be.true;
+ });
+ });
+
+ describe('offEvents', function () {
+ it('delegates to the submodule of the right divId', function () {
+ videoCore.offEvents([], () => {}, testId);
+ videoCore.offEvents([], () => {}, otherId);
+ expect(mockSubmodule.offEvents.calledOnce).to.be.true;
+ });
+ });
+});
diff --git a/test/spec/modules/videoModule/pbVideo_spec.js b/test/spec/modules/videoModule/pbVideo_spec.js
new file mode 100644
index 00000000000..da925c55c93
--- /dev/null
+++ b/test/spec/modules/videoModule/pbVideo_spec.js
@@ -0,0 +1,359 @@
+import { expect } from 'chai';
+import { PbVideo } from 'libraries/video/index.js';
+import CONSTANTS from 'src/constants.json';
+import { AD_IMPRESSION, AD_ERROR, BID_IMPRESSION, BID_ERROR } from 'libraries/video/constants/events.js';
+
+let ortbParamsMock;
+let videoCoreMock;
+let getConfigMock;
+let requestBidsMock;
+let pbGlobalMock;
+let pbEventsMock;
+let videoEventsMock;
+let gamSubmoduleMock;
+let gamSubmoduleFactoryMock;
+let videoImpressionVerifierFactoryMock;
+let videoImpressionVerifierMock;
+
+function resetTestVars() {
+ ortbParamsMock = {
+ 'video': {},
+ 'content': {}
+ };
+ videoCoreMock = {
+ registerProvider: sinon.spy(),
+ onEvents: sinon.spy(),
+ getOrtbParams: () => ortbParamsMock,
+ setAdTagUrl: sinon.spy()
+ };
+ getConfigMock = () => {};
+ requestBidsMock = {
+ before: sinon.spy()
+ };
+ pbGlobalMock = {
+ requestBids: requestBidsMock,
+ getHighestCpmBids: sinon.spy(),
+ getBidResponsesForAdUnitCode: sinon.spy(),
+ setConfig: sinon.spy()
+ };
+ pbEventsMock = {
+ emit: sinon.spy(),
+ on: sinon.spy()
+ };
+ videoEventsMock = [];
+ gamSubmoduleMock = {
+ getAdTagUrl: sinon.spy()
+ };
+
+ gamSubmoduleFactoryMock = sinon.spy(() => gamSubmoduleMock);
+
+ videoImpressionVerifierMock = {
+ trackBid: sinon.spy(),
+ getBidIdentifiers: sinon.spy()
+ };
+
+ videoImpressionVerifierFactoryMock = () => videoImpressionVerifierMock;
+}
+
+let pbVideoFactory = (videoCore, getConfig, pbGlobal, pbEvents, videoEvents, gamSubmoduleFactory, videoImpressionVerifierFactory) => {
+ const pbVideo = PbVideo(
+ videoCore || videoCoreMock,
+ getConfig || getConfigMock,
+ pbGlobal || pbGlobalMock,
+ pbEvents || pbEventsMock,
+ videoEvents || videoEventsMock,
+ gamSubmoduleFactory || gamSubmoduleFactoryMock,
+ videoImpressionVerifierFactory || videoImpressionVerifierFactoryMock
+ );
+ pbVideo.init();
+ return pbVideo;
+}
+
+describe('Prebid Video', function () {
+ beforeEach(() => resetTestVars());
+
+ describe('Setting video to config', function () {
+ let providers = [{ divId: 'div1' }, { divId: 'div2' }];
+ let getConfigCallback;
+ let getConfig = (propertyName, callback) => {
+ if (propertyName === 'video') {
+ getConfigCallback = callback;
+ }
+ };
+
+ beforeEach(() => {
+ pbVideoFactory(null, getConfig);
+ getConfigCallback({ video: { providers } });
+ });
+
+ it('Should register providers', function () {
+ expect(videoCoreMock.registerProvider.calledTwice).to.be.true;
+ });
+
+ it('Should register events', function () {
+ expect(videoCoreMock.onEvents.calledTwice).to.be.true;
+ const onEventsSpy = videoCoreMock.onEvents;
+ expect(onEventsSpy.getCall(0).args[2]).to.be.equal('div1');
+ expect(onEventsSpy.getCall(1).args[2]).to.be.equal('div2');
+ });
+
+ describe('Event triggering', function () {
+ it('Should emit events off of Prebid\'s Events', function () {
+ let eventHandler;
+ const videoCore = Object.assign({}, videoCoreMock, {
+ onEvents: (events, eventHandler_) => eventHandler = eventHandler_
+ });
+ pbVideoFactory(videoCore, getConfig);
+ getConfigCallback({ video: { providers } });
+ const expectedType = 'test_event';
+ const expectedPayload = {'test': 'data'};
+ eventHandler(expectedType, expectedPayload);
+ expect(pbEventsMock.emit.calledOnce).to.be.true;
+ expect(pbEventsMock.emit.getCall(0).args[0]).to.be.equal('video_' + expectedType);
+ expect(pbEventsMock.emit.getCall(0).args[1]).to.be.equal(expectedPayload);
+ });
+ });
+
+ describe('Ad Server configuration', function() {
+ const test_vendor_code = 5;
+ const test_params = { test: 'params' };
+ providers[0].adServer = { vendorCode: test_vendor_code, params: test_params };
+
+ it('should instantiate the GAM Submodule', function () {
+ expect(gamSubmoduleFactoryMock.calledOnce).to.be.true;
+ });
+ });
+ });
+
+ describe('Ad unit Enrichment', function () {
+ it('registers before:bidRequest hook', function () {
+ pbVideoFactory();
+ expect(requestBidsMock.before.calledOnce).to.be.true;
+ });
+
+ it('requests oRtb params and writes them to ad unit and config', function() {
+ const getOrtbParamsSpy = videoCoreMock.getOrtbParams = sinon.spy(() => ({
+ video: {
+ test: 'videoTestValue'
+ },
+ content: {
+ test: 'contentTestValue'
+ }
+ }));
+ const setConfigSpy = pbGlobalMock.setConfig;
+ setConfigSpy.resetHistory();
+ let beforeBidRequestCallback;
+ const requestBids = {
+ before: callback_ => beforeBidRequestCallback = callback_
+ };
+
+ pbVideoFactory(null, null, { requestBids, setConfig: setConfigSpy });
+ expect(beforeBidRequestCallback).to.not.be.undefined;
+ const nextFn = sinon.spy();
+ const adUnits = [{
+ code: 'ad1',
+ mediaTypes: {
+ video: {}
+ },
+ video: { divId: 'divId' }
+ }];
+ beforeBidRequestCallback(nextFn, { adUnits });
+ expect(getOrtbParamsSpy.calledOnce).to.be.true;
+ const adUnit = adUnits[0];
+ expect(adUnit.mediaTypes.video).to.have.property('test', 'videoTestValue');
+ expect(setConfigSpy.calledOnce).to.be.true;
+ expect(setConfigSpy.getCall(0).args[0]).to.be.deep.equal({ ortb2: { site: { test: 'contentTestValue' } } });
+ expect(nextFn.calledOnce).to.be.true;
+ });
+ });
+
+ describe('Ad tag injection', function () {
+ let auctionEndCallback;
+ let providers = [{ divId: 'div1', adServer: {} }, { divId: 'div2' }];
+ let getConfig = (propertyName, callbackFn) => {
+ if (propertyName === 'video') {
+ callbackFn({
+ video: { providers }
+ });
+ }
+ };
+
+ const pbEvents = {
+ emit: () => {},
+ on: (event, callback) => {
+ if (event === CONSTANTS.EVENTS.AUCTION_END) {
+ auctionEndCallback = callback
+ }
+ }
+ };
+
+ const expectedVendorCode = 5;
+ const expectedAdTag = 'test_tag';
+ const expectedAdUnitCode = 'expectedAdUnitcode';
+ const expectedDivId = 'expectedDivId';
+ const expectedAdUnit = {
+ code: expectedAdUnitCode,
+ video: {
+ divId: expectedDivId,
+ adServer: {
+ vendorCode: expectedVendorCode,
+ baseAdTagUrl: expectedAdTag
+ }
+ }
+ };
+ const auctionResults = { adUnits: [ expectedAdUnit, {} ] };
+
+ beforeEach(() => {
+ gamSubmoduleMock.getAdTagUrl.resetHistory();
+ videoCoreMock.setAdTagUrl.resetHistory();
+ });
+
+ let beforeBidRequestCallback;
+ const requestBids = {
+ before: callback_ => beforeBidRequestCallback = callback_
+ };
+
+ it('should request ad tag url from adServer when configured to use adServer', function () {
+ const expectedVastUrl = 'expectedVastUrl';
+ const expectedVastXml = 'expectedVastXml';
+ const pbGlobal = Object.assign({}, pbGlobalMock, {
+ requestBids,
+ getHighestCpmBids: () => [{
+ vastUrl: expectedVastUrl,
+ vastXml: expectedVastXml
+ }, {}, {}, {}]
+ });
+ pbVideoFactory(null, getConfig, pbGlobal, pbEvents);
+
+ beforeBidRequestCallback(() => {}, {});
+ auctionEndCallback(auctionResults);
+ expect(gamSubmoduleMock.getAdTagUrl.calledOnce).to.be.true;
+ expect(gamSubmoduleMock.getAdTagUrl.getCall(0).args[0]).is.equal(expectedAdUnit);
+ expect(gamSubmoduleMock.getAdTagUrl.getCall(0).args[1]).is.equal(expectedAdTag);
+ });
+
+ it('should load ad tag when ad server returns ad tag', function () {
+ const expectedAdTag = 'resulting ad tag';
+ const gamSubmoduleFactory = () => ({
+ getAdTagUrl: () => expectedAdTag
+ });
+ const expectedVastUrl = 'expectedVastUrl';
+ const expectedVastXml = 'expectedVastXml';
+ const pbGlobal = Object.assign({}, pbGlobalMock, {
+ requestBids,
+ getHighestCpmBids: () => [{
+ vastUrl: expectedVastUrl,
+ vastXml: expectedVastXml
+ }, {}, {}, {}]
+ });
+ pbVideoFactory(null, getConfig, pbGlobal, pbEvents, null, gamSubmoduleFactory);
+ beforeBidRequestCallback(() => {}, {});
+ auctionEndCallback(auctionResults);
+ expect(videoCoreMock.setAdTagUrl.calledOnce).to.be.true;
+ expect(videoCoreMock.setAdTagUrl.args[0][0]).to.be.equal(expectedAdTag);
+ expect(videoCoreMock.setAdTagUrl.args[0][1]).to.be.equal(expectedDivId);
+ expect(videoCoreMock.setAdTagUrl.args[0][2]).to.have.property('adUnitCode', expectedAdUnitCode);
+ });
+
+ it('should load ad tag from highest bid when ad server is not configured', function () {
+ const expectedVastUrl = 'expectedVastUrl';
+ const expectedVastXml = 'expectedVastXml';
+ const pbGlobal = Object.assign({}, pbGlobalMock, {
+ requestBids,
+ getHighestCpmBids: () => [{
+ vastUrl: expectedVastUrl,
+ vastXml: expectedVastXml
+ }, {}, {}, {}]
+ });
+ const expectedAdUnit = {
+ code: expectedAdUnitCode,
+ video: { divId: expectedDivId }
+ };
+ const auctionResults = { adUnits: [ expectedAdUnit, {} ] };
+
+ pbVideoFactory(null, null, pbGlobal, pbEvents);
+ beforeBidRequestCallback(() => {}, {});
+ auctionEndCallback(auctionResults);
+ expect(videoCoreMock.setAdTagUrl.calledOnce).to.be.true;
+ expect(videoCoreMock.setAdTagUrl.args[0][0]).to.be.equal(expectedVastUrl);
+ expect(videoCoreMock.setAdTagUrl.args[0][1]).to.be.equal(expectedDivId);
+ expect(videoCoreMock.setAdTagUrl.args[0][2]).to.have.property('adUnitCode', expectedAdUnitCode);
+ expect(videoCoreMock.setAdTagUrl.args[0][2]).to.have.property('adXml', expectedVastXml);
+ });
+ });
+
+ describe('Ad tracking', function () {
+ const expectedAdEventPayload = { adEventPayloadMarker: 'marker' };
+ const expectedBid = { bidMarker: 'marker' };
+ let bidAdjustmentCb;
+ let adImpressionCb;
+ let adErrorCb;
+
+ const pbEvents = {
+ on: (event, callback) => {
+ if (event === CONSTANTS.EVENTS.BID_ADJUSTMENT) {
+ bidAdjustmentCb = callback;
+ } else if (event === 'video_' + AD_IMPRESSION) {
+ adImpressionCb = callback;
+ } else if (event === 'video_' + AD_ERROR) {
+ adErrorCb = callback;
+ }
+ },
+ emit: sinon.spy()
+ };
+
+ it('should ask Impression Verifier to track bid on Bid Adjustment', function () {
+ pbVideoFactory(null, null, null, pbEvents);
+ bidAdjustmentCb();
+ expect(videoImpressionVerifierMock.trackBid.calledOnce).to.be.true;
+ });
+
+ it('should trigger video bid impression when the bid matched', function () {
+ pbEvents.emit.resetHistory();
+ const pbGlobal = Object.assign({}, pbGlobalMock, { getBidResponsesForAdUnitCode: () => ({ bids: [expectedBid] }) });
+ const videoImpressionVerifier = Object.assign({}, videoImpressionVerifierMock, { getBidIdentifiers: () => ({}) });
+ pbVideoFactory(null, null, pbGlobal, pbEvents, null, null, () => videoImpressionVerifier);
+ adImpressionCb(expectedAdEventPayload);
+
+ expect(pbEvents.emit.calledOnce).to.be.true;
+ expect(pbEvents.emit.getCall(0).args[0]).to.be.equal('video_' + BID_IMPRESSION);
+ const payload = pbEvents.emit.getCall(0).args[1];
+ expect(payload.bid).to.be.equal(expectedBid);
+ expect(payload.adEvent).to.be.equal(expectedAdEventPayload);
+ });
+
+ it('should trigger video bid error when the bid matched', function () {
+ pbEvents.emit.resetHistory();
+ const pbGlobal = Object.assign({}, pbGlobalMock, { getBidResponsesForAdUnitCode: () => ({ bids: [expectedBid] }) });
+ const videoImpressionVerifier = Object.assign({}, videoImpressionVerifierMock, { getBidIdentifiers: () => ({}) });
+ pbVideoFactory(null, null, pbGlobal, pbEvents, null, null, () => videoImpressionVerifier);
+ adErrorCb(expectedAdEventPayload);
+
+ expect(pbEvents.emit.calledOnce).to.be.true;
+ expect(pbEvents.emit.getCall(0).args[0]).to.be.equal('video_' + BID_ERROR);
+ const payload = pbEvents.emit.getCall(0).args[1];
+ expect(payload.bid).to.be.equal(expectedBid);
+ expect(payload.adEvent).to.be.equal(expectedAdEventPayload);
+ });
+
+ it('should not trigger a bid impression when the bid did not match', function () {
+ pbEvents.emit.resetHistory();
+ const pbGlobal = Object.assign({}, pbGlobalMock, { getBidResponsesForAdUnitCode: () => ({ bids: [expectedBid] }) });
+ const videoImpressionVerifier = Object.assign({}, videoImpressionVerifierMock, { getBidIdentifiers: () => ({ auctionId: 'id' }) });
+ pbVideoFactory(null, null, pbGlobal, pbEvents, null, null, () => videoImpressionVerifier);
+ adImpressionCb(expectedAdEventPayload);
+
+ expect(pbEvents.emit.called).to.be.false;
+ });
+
+ it('should not trigger a bid error when the bid did not match', function () {
+ pbEvents.emit.resetHistory();
+ const pbGlobal = Object.assign({}, pbGlobalMock, { getBidResponsesForAdUnitCode: () => ({ bids: [expectedBid] }) });
+ const videoImpressionVerifier = Object.assign({}, videoImpressionVerifierMock, { getBidIdentifiers: () => ({ auctionId: 'id' }) });
+ pbVideoFactory(null, null, pbGlobal, pbEvents, null, null, () => videoImpressionVerifier);
+ adErrorCb(expectedAdEventPayload);
+
+ expect(pbEvents.emit.called).to.be.false;
+ });
+ });
+});
diff --git a/test/spec/modules/videoModule/shared/parentModule_spec.js b/test/spec/modules/videoModule/shared/parentModule_spec.js
new file mode 100644
index 00000000000..1e8e7fda380
--- /dev/null
+++ b/test/spec/modules/videoModule/shared/parentModule_spec.js
@@ -0,0 +1,73 @@
+import { SubmoduleBuilder, ParentModule } from 'libraries/video/shared/parentModule.js';
+import { expect } from 'chai';
+
+describe('Parent Module', function() {
+ const idForMock = 0;
+ const vendorCodeForMock = 'a';
+ const unrecognizedId = 999;
+ const unrecognizedVendorCode = 'zzz';
+ const mockSubmodule = { test: 'test' };
+ const mockSubmoduleBuilder = {
+ build: vendorCode => {
+ if (vendorCode === vendorCodeForMock) {
+ return mockSubmodule;
+ } else {
+ throw new Error('flawed');
+ }
+ }
+ };
+ const parentModule = ParentModule(mockSubmoduleBuilder);
+
+ describe('Register Submodule', function () {
+ it('should throw when the builder fails to build', function () {
+ expect(() => parentModule.registerSubmodule(unrecognizedId, unrecognizedVendorCode)).to.throw('flawed');
+ });
+ });
+
+ describe('Get Submodule', function () {
+ it('should return registered submodules', function () {
+ parentModule.registerSubmodule(idForMock, vendorCodeForMock);
+ const submodule = parentModule.getSubmodule(idForMock);
+ expect(submodule).to.be.equal(mockSubmodule);
+ });
+
+ it('should return undefined when submodule is not registered', function () {
+ const submodule = parentModule.getSubmodule(unrecognizedId);
+ expect(submodule).to.be.undefined;
+ });
+ })
+});
+
+describe('Submodule Builder', function () {
+ const vendorCode1 = 1;
+ const vendorCode2 = 2;
+ const submodule1 = {};
+ const initSpy = sinon.spy();
+ const submodule2 = { init: initSpy };
+ const submoduleFactory1 = () => submodule1;
+ const submoduleFactory2 = () => submodule2;
+ const submoduleFactory1Spy = sinon.spy(submoduleFactory1);
+
+ const vendorDirectory = {};
+ vendorDirectory[vendorCode1] = submoduleFactory1Spy;
+ vendorDirectory[vendorCode2] = submoduleFactory2;
+
+ const submoduleBuilder = SubmoduleBuilder(vendorDirectory);
+
+ it('should call submodule factory when vendor code is supported', function () {
+ const submodule = submoduleBuilder.build(vendorCode1);
+ expect(submoduleFactory1Spy.calledOnce).to.be.true;
+ expect(submodule).to.be.equal(submodule1);
+ });
+
+ it('should instantiate the submodule, when supported', function () {
+ const submodule = submoduleBuilder.build(vendorCode2);
+ expect(initSpy.calledOnce).to.be.true;
+ expect(submodule).to.be.equal(submodule2);
+ });
+
+ it('should throw when vendor code is not recognized', function () {
+ const unrecognizedVendorCode = 999;
+ expect(() => submoduleBuilder.build(unrecognizedVendorCode)).to.throw('Unrecognized submodule vendor code: ' + unrecognizedVendorCode);
+ });
+});
diff --git a/test/spec/modules/videoModule/shared/state_spec.js b/test/spec/modules/videoModule/shared/state_spec.js
new file mode 100644
index 00000000000..bbd0a35b57e
--- /dev/null
+++ b/test/spec/modules/videoModule/shared/state_spec.js
@@ -0,0 +1,26 @@
+import stateFactory from 'libraries/video/shared/state.js';
+import { expect } from 'chai';
+
+describe('State', function () {
+ let state = stateFactory();
+ beforeEach(function () {
+ state.clearState();
+ });
+
+ it('should update state', function () {
+ state.updateState({ 'test': 'a' });
+ expect(state.getState()).to.have.property('test', 'a');
+ state.updateState({ 'test': 'b' });
+ expect(state.getState()).to.have.property('test', 'b');
+ state.updateState({ 'test_2': 'c' });
+ expect(state.getState()).to.have.property('test', 'b');
+ expect(state.getState()).to.have.property('test_2', 'c');
+ });
+
+ it('should clear state', function () {
+ state.updateState({ 'test': 'a' });
+ state.clearState();
+ expect(state.getState()).to.not.have.property('test', 'a');
+ expect(state.getState()).to.be.empty;
+ });
+});
diff --git a/test/spec/modules/videoModule/shared/vastXmlBuilder_spec.js b/test/spec/modules/videoModule/shared/vastXmlBuilder_spec.js
new file mode 100644
index 00000000000..2c67b898a53
--- /dev/null
+++ b/test/spec/modules/videoModule/shared/vastXmlBuilder_spec.js
@@ -0,0 +1,103 @@
+import { buildVastWrapper, getVastNode, getAdNode, getWrapperNode, getAdSystemNode,
+ getAdTagUriNode, getErrorNode, getImpressionNode } from 'libraries/video/shared/vastXmlBuilder.js';
+import { expect } from 'chai';
+
+describe('buildVastWrapper', function () {
+ it('should include impression and error nodes when requested', function () {
+ const vastXml = buildVastWrapper(
+ 'adId123',
+ 'http://wwww.testUrl.com/redirectUrl.xml',
+ 'http://wwww.testUrl.com/impression.jpg',
+ 'impressionId123',
+ 'http://wwww.testUrl.com/error.jpg'
+ );
+ expect(vastXml).to.be.equal(`Prebid org`);
+ });
+
+ it('should omit error nodes when excluded', function () {
+ const vastXml = buildVastWrapper(
+ 'adId123',
+ 'http://wwww.testUrl.com/redirectUrl.xml',
+ 'http://wwww.testUrl.com/impression.jpg',
+ 'impressionId123',
+ );
+ expect(vastXml).to.be.equal(`Prebid org`);
+ });
+
+ it('should omit impression nodes when excluded', function () {
+ const vastXml = buildVastWrapper(
+ 'adId123',
+ 'http://wwww.testUrl.com/redirectUrl.xml',
+ );
+ expect(vastXml).to.be.equal(`Prebid org`);
+ });
+});
+
+describe('getVastNode', function () {
+ it('should return well formed Vast node', function () {
+ const vastNode = getVastNode('body', '4.0');
+ expect(vastNode).to.be.equal('body');
+ });
+
+ it('should omit version when missing', function() {
+ const vastNode = getVastNode('body');
+ expect(vastNode).to.be.equal('body');
+ });
+});
+
+describe('getAdNode', function () {
+ it('should return well formed Ad node', function () {
+ const adNode = getAdNode('body', 'adId123');
+ expect(adNode).to.be.equal('body');
+ });
+
+ it('should omit id when missing', function() {
+ const adNode = getAdNode('body');
+ expect(adNode).to.be.equal('body');
+ });
+});
+
+describe('getWrapperNode', function () {
+ it('should return well formed Wrapper node', function () {
+ const wrapperNode = getWrapperNode('body');
+ expect(wrapperNode).to.be.equal('body');
+ });
+});
+
+describe('getAdSystemNode', function () {
+ it('should return well formed AdSystem node', function () {
+ const adSystemNode = getAdSystemNode('testSysName', '5.0');
+ expect(adSystemNode).to.be.equal('testSysName');
+ });
+
+ it('should omit version when missing', function() {
+ const adSystemNode = getAdSystemNode('testSysName');
+ expect(adSystemNode).to.be.equal('testSysName');
+ });
+});
+
+describe('getAdTagUriNode', function () {
+ it('should return well formed ad tag URI node', function () {
+ const adTagNode = getAdTagUriNode('http://wwww.testUrl.com/ad.xml');
+ expect(adTagNode).to.be.equal('');
+ });
+});
+
+describe('getImpressionNode', function () {
+ it('should return well formed Impression node', function () {
+ const impressionNode = getImpressionNode('http://wwww.testUrl.com/adImpression.jpg', 'impresionId123');
+ expect(impressionNode).to.be.equal('');
+ });
+
+ it('should omit id when missing', function() {
+ const impressionNode = getImpressionNode('http://wwww.testUrl.com/adImpression.jpg');
+ expect(impressionNode).to.be.equal('');
+ });
+});
+
+describe('getErrorNode', function () {
+ it('should return well formed Error node', function () {
+ const errorNode = getErrorNode('http://wwww.testUrl.com/adError.jpg');
+ expect(errorNode).to.be.equal('');
+ });
+});
diff --git a/test/spec/modules/videoModule/shared/vastXmlEditor_spec.js b/test/spec/modules/videoModule/shared/vastXmlEditor_spec.js
new file mode 100644
index 00000000000..2304b2f2833
--- /dev/null
+++ b/test/spec/modules/videoModule/shared/vastXmlEditor_spec.js
@@ -0,0 +1,209 @@
+import { vastXmlEditorFactory } from 'libraries/video/shared/vastXmlEditor.js';
+import { expect } from 'chai';
+
+describe('Vast XML Editor', function () {
+ const adWrapperXml = `
+
+
+
+ Prebid org
+
+
+
+
+`;
+
+ const inlineXml = `
+
+
+
+ Prebid org
+ Random Title
+
+
+
+`;
+
+ const inLineWithWrapper = `
+
+
+
+ Prebid org
+
+
+
+
+
+ Prebid org
+ Random Title
+
+
+
+`;
+
+ const vastXmlEditor = vastXmlEditorFactory();
+ const expectedImpressionUrl = 'https://test.impression.com/ping.gif';
+ const expectedImpressionId = 'test-impression-id';
+ const expectedErrorUrl = 'https://test.error.com/ping.gif';
+
+ it('should add Impression Nodes to the Ad Wrapper', function () {
+ const vastXml = vastXmlEditor.getVastXmlWithTracking(adWrapperXml, null, expectedImpressionUrl, expectedImpressionId);
+ const expectedXml = `
+
+
+ Prebid org
+
+
+
+`;
+ expect(vastXml).to.equal(expectedXml);
+ });
+
+ it('should add Impression Nodes to the InLine', function () {
+ const vastXml = vastXmlEditor.getVastXmlWithTracking(inlineXml, null, expectedImpressionUrl, expectedImpressionId);
+ const expectedXml = `
+
+
+ Prebid org
+ Random Title
+
+
+`;
+ expect(vastXml).to.equal(expectedXml);
+ });
+
+ it('should add Impression Nodes to the Ad Wrapper and Inline', function () {
+ const vastXml = vastXmlEditor.getVastXmlWithTracking(inLineWithWrapper, null, expectedImpressionUrl, expectedImpressionId);
+ const expectedXml = `
+
+
+ Prebid org
+
+
+
+
+
+ Prebid org
+ Random Title
+
+
+`;
+ expect(vastXml).to.equal(expectedXml);
+ });
+
+ it('should add Error Nodes to the Ad Wrapper', function () {
+ const vastXml = vastXmlEditor.getVastXmlWithTracking(adWrapperXml, null, null, null, expectedErrorUrl);
+ const expectedXml = `
+
+
+ Prebid org
+
+
+
+`;
+ expect(vastXml).to.equal(expectedXml);
+ });
+
+ it('should add Error Nodes to the InLine', function () {
+ const vastXml = vastXmlEditor.getVastXmlWithTracking(inlineXml, null, null, null, expectedErrorUrl);
+ const expectedXml = `
+
+
+ Prebid org
+ Random Title
+
+
+`;
+ expect(vastXml).to.equal(expectedXml);
+ });
+
+ it('should add Error Nodes to the Ad Wrapper and Inline', function () {
+ const vastXml = vastXmlEditor.getVastXmlWithTracking(inLineWithWrapper, null, null, null, expectedErrorUrl);
+ const expectedXml = `
+
+
+ Prebid org
+
+
+
+
+
+ Prebid org
+ Random Title
+
+
+`;
+ expect(vastXml).to.equal(expectedXml);
+ });
+
+ it('should add Impression Nodes and Error Nodes to the Ad Wrapper', function () {
+ const vastXml = vastXmlEditor.getVastXmlWithTracking(adWrapperXml, null, expectedImpressionUrl, expectedImpressionId, expectedErrorUrl);
+ const expectedXml = `
+
+
+ Prebid org
+
+
+
+`;
+ expect(vastXml).to.equal(expectedXml);
+ });
+
+ it('should add Impression Nodes and Error Nodes to the InLine', function () {
+ const vastXml = vastXmlEditor.getVastXmlWithTracking(inlineXml, null, expectedImpressionUrl, expectedImpressionId, expectedErrorUrl);
+ const expectedXml = `
+
+
+ Prebid org
+ Random Title
+
+
+`;
+ expect(vastXml).to.equal(expectedXml);
+ });
+
+ it('should add Impression Nodes and Error Nodes to the Ad Wrapper and Inline', function () {
+ const vastXml = vastXmlEditor.getVastXmlWithTracking(inLineWithWrapper, null, expectedImpressionUrl, expectedImpressionId, expectedErrorUrl);
+ const expectedXml = `
+
+
+ Prebid org
+
+
+
+
+
+ Prebid org
+ Random Title
+
+
+`;
+ expect(vastXml).to.equal(expectedXml);
+ });
+
+ it('should override the ad id in inline', function () {
+ const vastXml = vastXmlEditor.getVastXmlWithTracking(inlineXml, 'adIdOverride');
+ const expectedXml = `
+
+
+ Prebid org
+ Random Title
+
+
+`;
+ expect(vastXml).to.equal(expectedXml);
+ });
+
+ it('should override the ad id in the Ad Wrapper', function () {
+ const vastXml = vastXmlEditor.getVastXmlWithTracking(adWrapperXml, 'adIdOverride');
+ const expectedXml = `
+
+
+ Prebid org
+
+
+
+`;
+ expect(vastXml).to.equal(expectedXml);
+ });
+});
diff --git a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js
new file mode 100644
index 00000000000..0a1d34679da
--- /dev/null
+++ b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js
@@ -0,0 +1,836 @@
+import {
+ JWPlayerProvider,
+ adStateFactory,
+ timeStateFactory,
+ callbackStorageFactory,
+ utils
+} from 'modules/jwplayerVideoProvider';
+
+import {
+ PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, PLACEMENT, VPAID_MIME_TYPE
+} from 'libraries/video/constants/ortb.js';
+
+import {
+ SETUP_COMPLETE, SETUP_FAILED, PLAY, AD_IMPRESSION, videoEvents
+} from 'libraries/video/constants/events.js';
+
+import { PLAYBACK_MODE } from 'libraries/video/constants/enums.js';
+
+function getPlayerMock() {
+ return makePlayerFactoryMock({
+ getState: function () {},
+ setup: function () {},
+ getViewable: function () {},
+ getPercentViewable: function () {},
+ getMute: function () {},
+ getVolume: function () {},
+ getConfig: function () {},
+ getHeight: function () {},
+ getWidth: function () {},
+ getFullscreen: function () {},
+ getPlaylistItem: function () {},
+ playAd: function () {},
+ on: function () {},
+ off: function () {},
+ remove: function () {},
+ getAudioTracks: function () {},
+ getCurrentAudioTrack: function () {},
+ getPlugin: function () {},
+ getFloating: function () {}
+ })();
+}
+
+function makePlayerFactoryMock(playerMock_) {
+ const playerFactory = function () {
+ return playerMock_;
+ }
+ playerFactory.version = '8.21.0';
+ return playerFactory;
+}
+
+function getUtilsMock() {
+ return {
+ getJwConfig: function () {},
+ getSupportedMediaTypes: function () {},
+ getStartDelay: function () {},
+ getPlacement: function () {},
+ getPlaybackMethod: function () {},
+ isOmidSupported: function () {},
+ getSkipParams: function () {},
+ getJwEvent: function () {},
+ getIsoLanguageCode: function () {},
+ getSegments: function () {},
+ getContentDatum: function () {}
+ };
+}
+
+const sharedUtils = { videoEvents };
+
+describe('JWPlayerProvider', function () {
+ describe('init', function () {
+ let config;
+ let adState;
+ let timeState;
+ let callbackStorage;
+ let utilsMock;
+
+ beforeEach(() => {
+ config = {};
+ adState = adStateFactory();
+ timeState = timeStateFactory();
+ callbackStorage = callbackStorageFactory();
+ utilsMock = getUtilsMock();
+ });
+
+ it('should trigger failure when jwplayer is missing', function () {
+ const provider = JWPlayerProvider(config, null, adState, timeState, callbackStorage, utilsMock, sharedUtils);
+ const setupFailed = sinon.spy();
+ provider.onEvents([SETUP_FAILED], setupFailed);
+ provider.init();
+ expect(setupFailed.calledOnce).to.be.true;
+ const payload = setupFailed.args[0][1];
+ expect(payload.errorCode).to.be.equal(-1);
+ });
+
+ it('should trigger failure when jwplayer version is under min supported version', function () {
+ let jwplayerMock = () => {};
+ jwplayerMock.version = '8.20.0';
+ const provider = JWPlayerProvider(config, jwplayerMock, adState, timeState, callbackStorage, utilsMock, sharedUtils);
+ const setupFailed = sinon.spy();
+ provider.onEvents([SETUP_FAILED], setupFailed);
+ provider.init();
+ expect(setupFailed.calledOnce).to.be.true;
+ const payload = setupFailed.args[0][1];
+ expect(payload.errorCode).to.be.equal(-2);
+ });
+
+ it('should instantiate the player when uninstantied', function () {
+ const player = getPlayerMock();
+ config.playerConfig = {};
+ const setupSpy = player.setup = sinon.spy();
+ const provider = JWPlayerProvider(config, makePlayerFactoryMock(player), adState, timeState, callbackStorage, utilsMock, sharedUtils);
+ provider.init();
+ expect(setupSpy.calledOnce).to.be.true;
+ });
+
+ it('should trigger setup complete when player is already instantied', function () {
+ const player = getPlayerMock();
+ player.getState = () => 'idle';
+ const provider = JWPlayerProvider(config, makePlayerFactoryMock(player), adState, timeState, callbackStorage, utilsMock, sharedUtils);
+ const setupComplete = sinon.spy();
+ provider.onEvents([SETUP_COMPLETE], setupComplete);
+ provider.init();
+ expect(setupComplete.calledOnce).to.be.true;
+ });
+
+ it('should not reinstantiate player', function () {
+ const player = getPlayerMock();
+ player.getState = () => 'idle';
+ const setupSpy = player.setup = sinon.spy();
+ const provider = JWPlayerProvider(config, makePlayerFactoryMock(player), adState, timeState, callbackStorage, utilsMock, sharedUtils);
+ provider.init();
+ expect(setupSpy.called).to.be.false;
+ });
+ });
+
+ describe('getId', function () {
+ it('should return configured div id', function () {
+ const provider = JWPlayerProvider({ divId: 'test_id' }, undefined, undefined, undefined, undefined, undefined, sharedUtils);
+ expect(provider.getId()).to.be.equal('test_id');
+ });
+ });
+
+ describe('getOrtbParams', function () {
+ it('should populate oRTB params', function () {
+ const test_media_type = VIDEO_MIME_TYPE.MP4;
+ const test_height = 100;
+ const test_width = 200;
+ const test_start_delay = 5;
+ const test_placement = PLACEMENT.ARTICLE;
+ const test_battr = 'battr';
+ const test_playback_method = PLAYBACK_METHODS.CLICK_TO_PLAY;
+ const test_skip = 0;
+ const test_item = {
+ mediaid: 'id',
+ file: 'file',
+ title: 'title',
+ iabCategories: 'iabCategories',
+ tags: 'keywords',
+ };
+ const test_duration = 30;
+ let test_playback_mode = PLAYBACK_MODE.VOD;//
+
+ const config = {};
+ const player = getPlayerMock();
+ const utils = getUtilsMock();
+
+ player.getConfig = () => ({
+ advertising: {
+ battr: test_battr
+ }
+ });
+ player.getHeight = () => test_height;
+ player.getWidth = () => test_width;
+ player.getFullscreen = () => true; //
+ player.getPlaylistItem = () => test_item;
+
+ utils.getSupportedMediaTypes = () => [test_media_type];
+ utils.getStartDelay = () => test_start_delay;
+ utils.getPlacement = () => test_placement;
+ utils.getPlaybackMethod = () => test_playback_method;
+ utils.isOmidSupported = () => true; //
+ utils.getSkipParams = () => ({ skip: test_skip });
+
+ const timeState = {
+ getState: () => ({
+ duration: test_duration,
+ playbackMode: test_playback_mode
+ })
+ }
+
+ const provider = JWPlayerProvider(config, makePlayerFactoryMock(player), adStateFactory(), timeState, {}, utils, sharedUtils);
+ provider.init();
+ let oRTB = provider.getOrtbParams();
+
+ expect(oRTB).to.have.property('video');
+ expect(oRTB).to.have.property('content');
+ let { video, content } = oRTB;
+
+ expect(video.mimes).to.include(VIDEO_MIME_TYPE.MP4);
+ expect(video.protocols).to.include.members([
+ PROTOCOLS.VAST_2_0,
+ PROTOCOLS.VAST_3_0,
+ PROTOCOLS.VAST_4_0,
+ PROTOCOLS.VAST_2_0_WRAPPER,
+ PROTOCOLS.VAST_3_0_WRAPPER,
+ PROTOCOLS.VAST_4_0_WRAPPER
+ ]);
+ expect(video.h).to.equal(test_height);
+ expect(video.w).to.equal(test_width);
+ expect(video.startdelay).to.equal(test_start_delay);
+ expect(video.placement).to.equal(test_placement);
+ expect(video.battr).to.equal(test_battr);
+ expect(video.maxextended).to.equal(-1);
+ expect(video.boxingallowed).to.equal(1);
+ expect(video.playbackmethod).to.include(test_playback_method);
+ expect(video.playbackend).to.equal(1);
+ expect(video.api).to.have.length(2);
+ expect(video.api).to.include.members([API_FRAMEWORKS.VPAID_2_0, API_FRAMEWORKS.OMID_1_0]); //
+ expect(video.skip).to.equal(test_skip);
+ expect(video.pos).to.equal(7); //
+
+ expect(content.id).to.be.equal('jw_' + test_item.mediaid);
+ expect(content.url).to.be.equal(test_item.file);
+ expect(content.title).to.be.equal(test_item.title);
+ expect(content.cat).to.be.equal(test_item.iabCategories);
+ expect(content.keywords).to.be.equal(test_item.tags);
+ expect(content.len).to.be.equal(test_duration);
+ expect(content.livestream).to.be.equal(0);//
+
+ player.getFullscreen = () => false;
+ utils.isOmidSupported = () => false;
+ test_playback_mode = PLAYBACK_MODE.LIVE;
+
+ oRTB = provider.getOrtbParams();
+ video = oRTB.video;
+ content = oRTB.content;
+ expect(video).to.not.have.property('pos');
+ expect(video.api).to.have.length(1);
+ expect(video.api).to.include(API_FRAMEWORKS.VPAID_2_0);
+ expect(video.api).to.not.include(API_FRAMEWORKS.OMID_1_0);
+ expect(content.livestream).to.be.equal(1);
+
+ test_playback_mode = PLAYBACK_MODE.DVR;
+
+ oRTB = provider.getOrtbParams();
+ content = oRTB.content;
+ expect(content.livestream).to.be.equal(1);
+ });
+ });
+
+ describe('setAdTagUrl', function () {
+ it('should call playAd', function () {
+ const player = getPlayerMock();
+ const playAdSpy = player.playAd = sinon.spy();
+ const provider = JWPlayerProvider({}, makePlayerFactoryMock(player), {}, {}, {}, {}, sharedUtils);
+ provider.init();
+ provider.setAdTagUrl('tag');
+ expect(playAdSpy.called).to.be.true;
+ const argument = playAdSpy.args[0][0];
+ expect(argument).to.be.equal('tag');
+ });
+ });
+
+ describe('events', function () {
+ it('should register event listener on player', function () {
+ const player = getPlayerMock();
+ const onSpy = player.on = sinon.spy();
+ const provider = JWPlayerProvider({}, makePlayerFactoryMock(player), adStateFactory(), timeStateFactory(), callbackStorageFactory(), getUtilsMock(), sharedUtils);
+ provider.init();
+ const callback = () => {};
+ provider.onEvents([PLAY], callback);
+ expect(onSpy.calledOnce).to.be.true;
+ const eventName = onSpy.args[0][0];
+ expect(eventName).to.be.equal('play');
+ });
+
+ it('should remove event listener on player', function () {
+ const player = getPlayerMock();
+ const offSpy = player.off = sinon.spy();
+ const provider = JWPlayerProvider({}, makePlayerFactoryMock(player), adStateFactory(), timeStateFactory(), callbackStorageFactory(), utils, sharedUtils);
+ provider.init();
+ const callback = () => {};
+ provider.onEvents([AD_IMPRESSION], callback);
+ provider.offEvents([AD_IMPRESSION], callback);
+ expect(offSpy.calledOnce).to.be.true;
+ const eventName = offSpy.args[0][0];
+ expect(eventName).to.be.equal('adViewableImpression');
+ });
+ });
+
+ describe('destroy', function () {
+ it('should remove and null the player', function () {
+ const player = getPlayerMock();
+ const removeSpy = player.remove = sinon.spy();
+ player.remove = removeSpy;
+ const provider = JWPlayerProvider({}, makePlayerFactoryMock(player), adStateFactory(), timeStateFactory(), callbackStorageFactory(), getUtilsMock(), sharedUtils);
+ provider.init();
+ provider.destroy();
+ provider.destroy();
+ expect(removeSpy.calledOnce).to.be.true;
+ });
+ });
+});
+
+describe('adStateFactory', function () {
+ let adState = adStateFactory();
+
+ beforeEach(function() {
+ adState.clearState();
+ });
+
+ it('should update state for ad events', function () {
+ const tag = 'tag';
+ const adPosition = 'adPosition';
+ const timeLoading = 'timeLoading';
+ const id = 'id';
+ const description = 'description';
+ const adsystem = 'adsystem';
+ const adtitle = 'adtitle';
+ const advertiserId = 'advertiserId';
+ const advertiser = 'advertiser';
+ const dealId = 'dealId';
+ const linear = 'linear';
+ const vastversion = 'vastversion';
+ const mediaFile = 'mediaFile';
+ const adId = 'adId';
+ const universalAdId = 'universalAdId';
+ const creativeAdId = 'creativeAdId';
+ const creativetype = 'creativetype';
+ const clickThroughUrl = 'clickThroughUrl';
+ const witem = 'witem';
+ const wcount = 'wcount';
+ const podcount = 'podcount';
+ const sequence = 'sequence';
+
+ adState.updateForEvent({
+ tag,
+ adPosition,
+ timeLoading,
+ id,
+ description,
+ adsystem,
+ adtitle,
+ advertiserId,
+ advertiser,
+ dealId,
+ linear,
+ vastversion,
+ mediaFile,
+ adId,
+ universalAdId,
+ creativeAdId,
+ creativetype,
+ clickThroughUrl,
+ witem,
+ wcount,
+ podcount,
+ sequence
+ });
+
+ const state = adState.getState();
+ expect(state.adTagUrl).to.equal(tag);
+ expect(state.offset).to.equal(adPosition);
+ expect(state.loadTime).to.equal(timeLoading);
+ expect(state.vastAdId).to.equal(id);
+ expect(state.adDescription).to.equal(description);
+ expect(state.adServer).to.equal(adsystem);
+ expect(state.adTitle).to.equal(adtitle);
+ expect(state.advertiserId).to.equal(advertiserId);
+ expect(state.dealId).to.equal(dealId);
+ expect(state.linear).to.equal(linear);
+ expect(state.vastVersion).to.equal(vastversion);
+ expect(state.creativeUrl).to.equal(mediaFile);
+ expect(state.adId).to.equal(adId);
+ expect(state.universalAdId).to.equal(universalAdId);
+ expect(state.creativeId).to.equal(creativeAdId);
+ expect(state.creativeType).to.equal(creativetype);
+ expect(state.redirectUrl).to.equal(clickThroughUrl);
+ expect(state).to.have.property('adPlacementType');
+ expect(state.adPlacementType).to.be.undefined;
+ expect(state.waterfallIndex).to.equal(witem);
+ expect(state.waterfallCount).to.equal(wcount);
+ expect(state.adPodCount).to.equal(podcount);
+ expect(state.adPodIndex).to.equal(sequence);
+ });
+
+ it('should convert placement to oRTB value', function () {
+ adState.updateForEvent({
+ placement: 'instream'
+ });
+
+ let state = adState.getState();
+ expect(state.adPlacementType).to.be.equal(PLACEMENT.INSTREAM);
+
+ adState.updateForEvent({
+ placement: 'banner'
+ });
+
+ state = adState.getState();
+ expect(state.adPlacementType).to.be.equal(PLACEMENT.BANNER);
+
+ adState.updateForEvent({
+ placement: 'article'
+ });
+
+ state = adState.getState();
+ expect(state.adPlacementType).to.be.equal(PLACEMENT.ARTICLE);
+
+ adState.updateForEvent({
+ placement: 'feed'
+ });
+
+ state = adState.getState();
+ expect(state.adPlacementType).to.be.equal(PLACEMENT.FEED);
+
+ adState.updateForEvent({
+ placement: 'interstitial'
+ });
+
+ state = adState.getState();
+ expect(state.adPlacementType).to.be.equal(PLACEMENT.INTERSTITIAL);
+
+ adState.updateForEvent({
+ placement: 'slider'
+ });
+
+ state = adState.getState();
+ expect(state.adPlacementType).to.be.equal(PLACEMENT.SLIDER);
+
+ adState.updateForEvent({
+ placement: 'floating'
+ });
+
+ state = adState.getState();
+ expect(state.adPlacementType).to.be.equal(PLACEMENT.FLOATING);
+ });
+});
+
+describe('timeStateFactory', function () {
+ let timeState = timeStateFactory();
+
+ beforeEach(function() {
+ timeState.clearState();
+ });
+
+ it('should update state for VOD time event', function() {
+ const position = 5;
+ const test_duration = 30;
+
+ timeState.updateForEvent({
+ position,
+ duration: test_duration
+ });
+
+ const { time, duration, playbackMode } = timeState.getState();
+ expect(time).to.be.equal(position);
+ expect(duration).to.be.equal(test_duration);
+ expect(playbackMode).to.be.equal(PLAYBACK_MODE.VOD);
+ });
+
+ it('should update state for LIVE time events', function() {
+ const position = 0;
+ const test_duration = 0;
+
+ timeState.updateForEvent({
+ position,
+ duration: test_duration
+ });
+
+ const { time, duration, playbackMode } = timeState.getState();
+ expect(time).to.be.equal(position);
+ expect(duration).to.be.equal(test_duration);
+ expect(playbackMode).to.be.equal(PLAYBACK_MODE.LIVE);
+ });
+
+ it('should update state for DVR time events', function() {
+ const position = -5;
+ const test_duration = -30;
+
+ timeState.updateForEvent({
+ position,
+ duration: test_duration
+ });
+
+ const { time, duration, playbackMode } = timeState.getState();
+ expect(time).to.be.equal(position);
+ expect(duration).to.be.equal(test_duration);
+ expect(playbackMode).to.be.equal(PLAYBACK_MODE.DVR);
+ });
+});
+
+describe('callbackStorageFactory', function () {
+ let callbackStorage = callbackStorageFactory();
+
+ beforeEach(function () {
+ callbackStorage.clearStorage();
+ });
+
+ it('should store callbacks', function () {
+ const callback1 = () => 'callback1';
+ const eventHandler1 = () => 'eventHandler1';
+ callbackStorage.storeCallback('event', eventHandler1, callback1);
+
+ const callback2 = () => 'callback2';
+ const eventHandler2 = () => 'eventHandler2';
+ callbackStorage.storeCallback('event', eventHandler2, callback2);
+
+ const callback3 = () => 'callback3';
+
+ expect(callbackStorage.getCallback('event', callback1)).to.be.equal(eventHandler1);
+ expect(callbackStorage.getCallback('event', callback2)).to.be.equal(eventHandler2);
+ expect(callbackStorage.getCallback('event', callback3)).to.be.undefined;
+ });
+
+ it('should remove callbacks after retrieval', function () {
+ const callback1 = () => 'callback1';
+ const eventHandler1 = () => 'eventHandler1';
+ callbackStorage.storeCallback('event', eventHandler1, callback1);
+
+ expect(callbackStorage.getCallback('event', callback1)).to.be.equal(eventHandler1);
+ expect(callbackStorage.getCallback('event', callback1)).to.be.undefined;
+ });
+
+ it('should clear callbacks', function () {
+ const callback1 = () => 'callback1';
+ const eventHandler1 = () => 'eventHandler1';
+ callbackStorage.storeCallback('event', eventHandler1, callback1);
+
+ callbackStorage.clearStorage();
+ expect(callbackStorage.getCallback('event', callback1)).to.be.undefined;
+ });
+});
+
+describe('utils', function () {
+ describe('getJwConfig', function () {
+ const getJwConfig = utils.getJwConfig;
+ it('should return undefined when no config is provided', function () {
+ let jwConfig = getJwConfig();
+ expect(jwConfig).to.be.undefined;
+
+ jwConfig = getJwConfig(null);
+ expect(jwConfig).to.be.undefined;
+ });
+
+ it('should set vendor config params to top level', function () {
+ let jwConfig = getJwConfig({
+ params: {
+ vendorConfig: {
+ 'test': 'a',
+ 'test_2': 'b'
+ }
+ }
+ });
+ expect(jwConfig.test).to.be.equal('a');
+ expect(jwConfig.test_2).to.be.equal('b');
+ });
+
+ it('should convert video module params', function () {
+ let jwConfig = getJwConfig({
+ mute: true,
+ autoStart: true,
+ licenseKey: 'key'
+ });
+
+ expect(jwConfig.mute).to.be.true;
+ expect(jwConfig.autostart).to.be.true;
+ expect(jwConfig.key).to.be.equal('key');
+ });
+
+ it('should apply video module params only when absent from vendor config', function () {
+ let jwConfig = getJwConfig({
+ mute: true,
+ autoStart: true,
+ licenseKey: 'key',
+ params: {
+ vendorConfig: {
+ mute: false,
+ autostart: false,
+ key: 'other_key'
+ }
+ }
+ });
+
+ expect(jwConfig.mute).to.be.false;
+ expect(jwConfig.autostart).to.be.false;
+ expect(jwConfig.key).to.be.equal('other_key');
+ });
+
+ it('should not convert undefined properties', function () {
+ let jwConfig = getJwConfig({
+ params: {
+ vendorConfig: {
+ test: 'a'
+ }
+ }
+ });
+
+ expect(jwConfig).to.not.have.property('mute');
+ expect(jwConfig).to.not.have.property('autostart');
+ expect(jwConfig).to.not.have.property('key');
+ });
+
+ it('should exclude fallback ad block when adOptimization is explicitly disabled', function () {
+ let jwConfig = getJwConfig({
+ params: {
+ adOptimization: false,
+ vendorConfig: {}
+ }
+ });
+
+ expect(jwConfig).to.not.have.property('advertising');
+ });
+
+ it('should set advertising block when adOptimization is allowed', function () {
+ let jwConfig = getJwConfig({
+ params: {
+ vendorConfig: {
+ advertising: {
+ tag: 'test_tag'
+ }
+ }
+ }
+ });
+
+ expect(jwConfig).to.have.property('advertising');
+ expect(jwConfig.advertising).to.have.property('tag', 'test_tag');
+ });
+
+ it('should fallback to vast plugin', function () {
+ let jwConfig = getJwConfig({});
+
+ expect(jwConfig).to.have.property('advertising');
+ expect(jwConfig.advertising).to.have.property('client', 'vast');
+ });
+ });
+ describe('getSkipParams', function () {
+ const getSkipParams = utils.getSkipParams;
+
+ it('should return an empty object when skip is not configured', function () {
+ let skipParams = getSkipParams({});
+ expect(skipParams).to.be.empty;
+ });
+
+ it('should set skip to false when explicitly configured', function () {
+ let skipParams = getSkipParams({
+ skipoffset: -1
+ });
+ expect(skipParams.skip).to.be.equal(0);
+ expect(skipParams.skipmin).to.be.undefined;
+ expect(skipParams.skipafter).to.be.undefined;
+ });
+
+ it('should be skippable when skip offset is set', function () {
+ const skipOffset = 3;
+ let skipParams = getSkipParams({
+ skipoffset: skipOffset
+ });
+ expect(skipParams.skip).to.be.equal(1);
+ expect(skipParams.skipmin).to.be.equal(skipOffset + 2);
+ expect(skipParams.skipafter).to.be.equal(skipOffset);
+ });
+ });
+
+ describe('getSupportedMediaTypes', function () {
+ const getSupportedMediaTypes = utils.getSupportedMediaTypes;
+
+ it('should always support VPAID', function () {
+ let supportedMediaTypes = getSupportedMediaTypes([]);
+ expect(supportedMediaTypes).to.include(VPAID_MIME_TYPE);
+
+ supportedMediaTypes = getSupportedMediaTypes([VIDEO_MIME_TYPE.MP4]);
+ expect(supportedMediaTypes).to.include(VPAID_MIME_TYPE);
+ });
+ });
+
+ describe('getPlacement', function () {
+ const getPlacement = utils.getPlacement;
+
+ it('should be INSTREAM when not configured for outstream', function () {
+ let adConfig = {};
+ let placement = getPlacement(adConfig);
+ expect(placement).to.be.equal(PLACEMENT.INSTREAM);
+
+ adConfig = { outstream: false };
+ placement = getPlacement(adConfig);
+ expect(placement).to.be.equal(PLACEMENT.INSTREAM);
+ });
+
+ it('should be FLOATING when player is floating', function () {
+ const player = getPlayerMock();
+ player.getFloating = () => true;
+ const placement = getPlacement({outstream: true}, player);
+ expect(placement).to.be.equal(PLACEMENT.FLOATING);
+ });
+
+ it('should be the value defined in the ad config', function () {
+ const player = getPlayerMock();
+ player.getFloating = () => false;
+
+ let placement = getPlacement({placement: 'banner', outstream: true}, player);
+ expect(placement).to.be.equal(PLACEMENT.BANNER);
+
+ placement = getPlacement({placement: 'article', outstream: true}, player);
+ expect(placement).to.be.equal(PLACEMENT.ARTICLE);
+
+ placement = getPlacement({placement: 'feed', outstream: true}, player);
+ expect(placement).to.be.equal(PLACEMENT.FEED);
+
+ placement = getPlacement({placement: 'interstitial', outstream: true}, player);
+ expect(placement).to.be.equal(PLACEMENT.INTERSTITIAL);
+
+ placement = getPlacement({placement: 'slider', outstream: true}, player);
+ expect(placement).to.be.equal(PLACEMENT.SLIDER);
+ });
+
+ it('should be undefined when undetermined', function () {
+ const placement = getPlacement({ outstream: true }, getPlayerMock());
+ expect(placement).to.be.undefined;
+ });
+ });
+
+ describe('getPlaybackMethod', function() {
+ const getPlaybackMethod = utils.getPlaybackMethod;
+
+ it('should return autoplay with sound', function() {
+ const playbackMethod = getPlaybackMethod({
+ autoplay: true,
+ mute: false
+ });
+ expect(playbackMethod).to.equal(PLAYBACK_METHODS.AUTOPLAY);
+ });
+
+ it('should return autoplay muted', function() {
+ const playbackMethod = getPlaybackMethod({
+ autoplay: true,
+ mute: true
+ });
+ expect(playbackMethod).to.equal(PLAYBACK_METHODS.AUTOPLAY_MUTED);
+ });
+
+ it('should treat autoplayAdsMuted as mute', function () {
+ const playbackMethod = getPlaybackMethod({
+ autoplay: true,
+ autoplayAdsMuted: true
+ });
+ expect(playbackMethod).to.equal(PLAYBACK_METHODS.AUTOPLAY_MUTED);
+ });
+
+ it('should return click to play', function() {
+ let playbackMethod = getPlaybackMethod({ autoplay: false });
+ expect(playbackMethod).to.equal(PLAYBACK_METHODS.CLICK_TO_PLAY);
+
+ playbackMethod = getPlaybackMethod({
+ autoplay: false,
+ autoplayAdsMuted: true
+ });
+ expect(playbackMethod).to.equal(PLAYBACK_METHODS.CLICK_TO_PLAY);
+
+ playbackMethod = getPlaybackMethod({
+ autoplay: false,
+ mute: true
+ });
+ expect(playbackMethod).to.equal(PLAYBACK_METHODS.CLICK_TO_PLAY);
+ });
+ });
+
+ describe('isOmidSupported', function () {
+ const isOmidSupported = utils.isOmidSupported;
+ afterEach(() => {
+ window.OmidSessionClient = undefined;
+ });
+
+ it('should be true when Omid is loaded and client is VAST', function () {
+ window.OmidSessionClient = {};
+ expect(isOmidSupported('vast')).to.be.true;
+ });
+
+ it('should be false when Omid is not present', function () {
+ expect(isOmidSupported('vast')).to.be.false;
+ });
+
+ it('should be false when client is not Vast', function () {
+ window.OmidSessionClient = {};
+ expect(isOmidSupported('googima')).to.be.false;
+ expect(isOmidSupported('freewheel')).to.be.false;
+ expect(isOmidSupported('googimadai')).to.be.false;
+ expect(isOmidSupported('')).to.be.false;
+ expect(isOmidSupported(null)).to.be.false;
+ expect(isOmidSupported()).to.be.false;
+ });
+ });
+
+ describe('getIsoLanguageCode', function () {
+ const sampleAudioTracks = [{language: 'ht'}, {language: 'fr'}, {language: 'es'}, {language: 'pt'}];
+
+ it('should return undefined when audio tracks are unavailable', function () {
+ const player = getPlayerMock();
+ let languageCode = utils.getIsoLanguageCode(player);
+ expect(languageCode).to.be.undefined;
+ player.getAudioTracks = () => [];
+ languageCode = utils.getIsoLanguageCode(player);
+ expect(languageCode).to.be.undefined;
+ });
+
+ it('should return the first audio track language code if the getCurrentAudioTrack returns undefined', function () {
+ const player = getPlayerMock();
+ player.getAudioTracks = () => sampleAudioTracks;
+ let languageCode = utils.getIsoLanguageCode(player);
+ expect(languageCode).to.be.equal('ht');
+ });
+
+ it('should return the first audio track language code if the getCurrentAudioTrack returns null', function () {
+ const player = getPlayerMock();
+ player.getAudioTracks = () => sampleAudioTracks;
+ player.getCurrentAudioTrack = () => null;
+ let languageCode = utils.getIsoLanguageCode(player);
+ expect(languageCode).to.be.equal('ht');
+ });
+
+ it('should return the first audio track language code if the getCurrentAudioTrack returns -1', function () {
+ const player = getPlayerMock();
+ player.getAudioTracks = () => sampleAudioTracks;
+ player.getCurrentAudioTrack = () => -1;
+ const languageCode = utils.getIsoLanguageCode(player);
+ expect(languageCode).to.be.equal('ht');
+ });
+
+ it('should return the right audio track language code', function () {
+ const player = getPlayerMock();
+ player.getAudioTracks = () => sampleAudioTracks;
+ player.getCurrentAudioTrack = () => 2;
+ const languageCode = utils.getIsoLanguageCode(player);
+ expect(languageCode).to.be.equal('es');
+ });
+ });
+});
diff --git a/test/spec/modules/videoModule/submodules/videojsVideoProvider_spec.js b/test/spec/modules/videoModule/submodules/videojsVideoProvider_spec.js
new file mode 100644
index 00000000000..9f09e266d68
--- /dev/null
+++ b/test/spec/modules/videoModule/submodules/videojsVideoProvider_spec.js
@@ -0,0 +1,266 @@
+// Using require style imports for fine grained control of import time
+const {VideojsProvider, utils} = require('modules/videojsVideoProvider')
+
+const {
+ PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, PLACEMENT, VPAID_MIME_TYPE
+} = require('modules/videoModule/constants/ortb.js');
+const { AD_POSITION } = require('../../../../../modules/videoModule/constants/ortb');
+
+import {
+ PLAYBACK_MODE, SETUP_COMPLETE, SETUP_FAILED, PLAY, AD_IMPRESSION
+} from 'modules/videoModule/constants/events.js'
+
+const videojs = require('video.js').default
+
+describe('videojsProvider', function () {
+ let config;
+ let adState;
+ let timeState;
+ let callbackStorage;
+ let utilsMock;
+
+ describe('init', function () {
+ beforeEach(() => {
+ config = {};
+ document.body.innerHTML = '';
+ });
+
+ it('should trigger failure when videojs is missing', function () {
+ const provider = VideojsProvider(config, null, adState, timeState, callbackStorage, utils);
+ const setupFailed = sinon.spy();
+ provider.onEvents([SETUP_FAILED], setupFailed);
+ provider.init();
+ expect(setupFailed.calledOnce).to.be.true;
+ const payload = setupFailed.args[0][1];
+ expect(payload.errorCode).to.be.equal(-1);
+ });
+
+ it('should trigger failure when videojs version is under min supported version', function () {
+ const provider = VideojsProvider(config, {...videojs, VERSION:'0.0.0'}, adState, timeState, callbackStorage, utils);
+ const setupFailed = sinon.spy();
+ provider.onEvents([SETUP_FAILED], setupFailed);
+ provider.init();
+ expect(setupFailed.calledOnce).to.be.true;
+ const payload = setupFailed.args[0][1];
+ expect(payload.errorCode).to.be.equal(-2);
+ });
+
+ it('should trigger failure when the div is not found', function () {
+ config.divId = 'fake-div'
+ const provider = VideojsProvider(config, videojs, adState, timeState, callbackStorage, utils);
+ const setupFailed = sinon.spy();
+ provider.onEvents([SETUP_FAILED], setupFailed);
+ provider.init();
+ expect(setupFailed.calledOnce).to.be.true;
+ const payload = setupFailed.args[0][1];
+ expect(payload.errorCode).to.be.equal(-3);
+ });
+
+ it('should instantiate the player when uninstantied', function () {
+ config.playerConfig = {testAttr:true};
+ config.divId = 'test-div'
+ const div = document.createElement('div');
+ div.setAttribute('id', 'test-div');
+ document.body.appendChild(div);
+
+ const mockVideojs = sinon.spy();
+ const provider = VideojsProvider(config, mockVideojs, adState, timeState, callbackStorage, utils);
+ provider.init();
+ expect(mockVideojs.calledWith(config.divId, config.playerConfig)).to.be.true
+ });
+
+ it('should not reinstantiate the player', function () {
+ const div = document.createElement('div');
+ div.setAttribute('id', 'test-div');
+ document.body.appendChild(div);
+ const player = videojs(div, {})
+ config.playerConfig = {};
+ config.divId = 'test-div'
+ const provider = VideojsProvider(config, videojs, adState, timeState, callbackStorage, utils);
+ provider.init();
+ expect(videojs.getPlayer('test-div')).to.be.equal(player)
+ videojs.getPlayer('test-div').dispose()
+ });
+
+ it('should trigger setup complete when player is already insantiated', function () {
+ const div = document.createElement('div');
+ div.setAttribute('id', 'test-div');
+ document.body.appendChild(div);
+ videojs(div, {})
+ config.playerConfig = {};
+ config.divId = 'test-div'
+ const provider = VideojsProvider(config, videojs, adState, timeState, callbackStorage, utils);
+ const setupComplete = sinon.spy();
+ provider.onEvents([SETUP_COMPLETE], setupComplete);
+ provider.init();
+ expect(setupComplete.calledOnce).to.be.true;
+ videojs.getPlayer('test-div').dispose()
+ });
+
+ });
+
+ describe('getId', function () {
+ it('should return configured div id', function () {
+ const provider = VideojsProvider({ divId: 'test_id' });
+ expect(provider.getId()).to.be.equal('test_id');
+ });
+ });
+
+ describe('getOrtbParams', function () {
+ beforeEach(() => {
+ config = {divId: 'test'};
+ // initialize videojs element
+ document.body.innerHTML = `
+ `
+ });
+
+ it('should populate oRTB params without ima present', function () {
+ const provider = VideojsProvider(config, videojs, adState, timeState, callbackStorage, utils);
+ provider.init();
+
+ const oRTB = provider.getOrtbParams();
+ expect(oRTB).to.have.property('video');
+ expect(oRTB).to.have.property('content');
+ const { video, content } = oRTB;
+
+ expect(video.mimes).to.include(VIDEO_MIME_TYPE.MP4);
+ expect(video.protocols).to.deep.equal([]);
+ expect(video.h).to.equal(100);
+ expect(video.w).to.equal(200);
+
+ expect(video.maxextended).to.equal(-1);
+ expect(video.boxingallowed).to.equal(1);
+ expect(video.playbackmethod).to.include(PLAYBACK_METHODS.CLICK_TO_PLAY);
+ expect(video.playbackend).to.equal(1);
+ expect(video.api).to.deep.equal([]);
+ expect(video.placement).to.be.equal(PLACEMENT.IN_STREAM);
+
+ expect(content.url).to.be.equal('http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4');
+ expect(content).to.not.have.property('len');
+ });
+
+ it('should change populated oRTB params when ima present', function () {
+ require('videojs-contrib-ads');
+ require('videojs-ima');
+ document.body.innerHTML = `
+ `
+
+ let player = videojs('test')
+
+ config.playerConfig = {
+ params: {
+ vendorConfig: {
+ mediaid: 'vendor-id',
+ advertising: {
+ tag: ['test-tag']
+ }
+ }
+ }
+ }
+
+ let provider = VideojsProvider(config, videojs, null, null, null, utils);
+ provider.init();
+ let { video, content } = provider.getOrtbParams();
+
+ expect(video.protocols).to.include(PROTOCOLS.VAST_2_0);
+ expect(video.api).to.include(API_FRAMEWORKS.VPAID_2_0);
+ expect(video.mimes).to.include(VPAID_MIME_TYPE);
+ player.dispose();
+ });
+
+ // We can't determine what type of outstream play is occuring
+ // if the src is absent so we should not set placement
+ it('should not set placement when src is absent', function() {
+ document.body.innerHTML = ``
+ const provider = VideojsProvider(config, videojs, null, null, null, utils);
+ provider.init();
+ const { video, content } = provider.getOrtbParams();
+ expect(video).to.not.have.property('placement')
+ })
+
+ it('should populate position when fullscreen', function () {
+ const provider = VideojsProvider(config, videojs, null, null, null, utils);
+ provider.init();
+ const player = videojs.getPlayer('test')
+ player.isFullscreen = () => true;
+ const { video, content } = provider.getOrtbParams(); ;
+ expect(video.pos).to.equal(7);
+ });
+
+ it('should populate length when loaded', function () {
+ const provider = VideojsProvider(config, videojs, null, null, null, utils);
+ provider.init();
+ const player = videojs.getPlayer('test')
+ player.readyState = () => 1
+ player.duration = () => 100
+ const { video, content } = provider.getOrtbParams();
+ expect(content.len).to.equal(100);
+ });
+
+ it('should return the correct playback method for autoplay', function () {
+ const provider = VideojsProvider(config, videojs, null, null, null, utils);
+ provider.init();
+ const player = videojs.getPlayer('test')
+ player.autoplay(true)
+ const { video, content } = provider.getOrtbParams();
+ expect(video.playbackmethod).to.include(PLAYBACK_METHODS.AUTOPLAY);
+ });
+
+ it('should return the correct playback method for autoplay muted', function () {
+ const provider = VideojsProvider(config, videojs, null, null, null, utils);
+ provider.init();
+ const player = videojs.getPlayer('test')
+ player.muted = () => true
+ player.autoplay = () => true
+ const { video, content } = provider.getOrtbParams();
+ expect(video.playbackmethod).to.include(PLAYBACK_METHODS.AUTOPLAY_MUTED);
+ });
+
+ it('should return the correct playback method for the other autoplay muted', function () {
+ const provider = VideojsProvider(config, videojs, null, null, null, utils);
+ provider.init();
+ const player = videojs.getPlayer('test')
+ player.autoplay = () => 'muted'
+ const { video, content } = provider.getOrtbParams();
+ expect(video.playbackmethod).to.include(PLAYBACK_METHODS.AUTOPLAY_MUTED);
+ });
+ });
+});
+
+describe('utils', function() {
+ describe('getPositionCode', function() {
+ it('should return the correct position when video is above the fold', function () {
+ const code = utils.getPositionCode({
+ left: window.innerWidth / 10,
+ top: 0,
+ width: window.innerWidth - window.innerWidth / 10,
+ height: window.innerHeight,
+ })
+ expect(code).to.equal(AD_POSITION.ABOVE_THE_FOLD)
+ });
+
+ it('should return the correct position when video is below the fold', function () {
+ const code = utils.getPositionCode({
+ left: window.innerWidth / 10,
+ top: window.innerHeight,
+ width: window.innerWidth - window.innerWidth / 10,
+ height: window.innerHeight / 2,
+ })
+ expect(code).to.equal(AD_POSITION.BELOW_THE_FOLD)
+ });
+
+ it('should return the unkown position when the video is out of bounds', function () {
+ const code = utils.getPositionCode({
+ left: window.innerWidth / 10,
+ top: window.innerHeight,
+ width: window.innerWidth,
+ height: window.innerHeight,
+ })
+ expect(code).to.equal(AD_POSITION.UNKNOWN)
+ });
+ })
+})
diff --git a/test/spec/modules/videoModule/videoImpressionVerifier_spec.js b/test/spec/modules/videoModule/videoImpressionVerifier_spec.js
new file mode 100644
index 00000000000..cfce7933895
--- /dev/null
+++ b/test/spec/modules/videoModule/videoImpressionVerifier_spec.js
@@ -0,0 +1,112 @@
+import { baseImpressionVerifier, PB_PREFIX } from 'libraries/video/videoImpressionVerifier.js';
+
+let trackerMock;
+trackerMock = {
+ store: sinon.spy(),
+ remove: sinon.spy()
+}
+
+describe('Base Impression Verifier', function() {
+ describe('trackBid', function () {
+ it('should generate uuid', function () {
+ const baseVerifier = baseImpressionVerifier(trackerMock);
+ const uuid = baseVerifier.trackBid({});
+ expect(uuid.substring(0, 3)).to.equal(PB_PREFIX);
+ expect(uuid.length).to.be.lessThan(16);
+ });
+ });
+
+ describe('getBidIdentifiers', function () {
+ it('should match ad id to uuid', function () {
+
+ });
+ });
+});
+
+/*
+const adUnitCode = 'test_ad_unit_code';
+const sampleBid = {
+ adId: 'test_ad_id',
+ adUnitCode,
+ vastUrl: 'test_ad_url'
+};
+const sampleAdUnit = {
+ code: adUnitCode,
+};
+
+const expectedImpressionUrl = 'test_impression_url';
+const expectedImpressionId = 'test_impression_id';
+const expectedErrorUrl = 'test_error_url';
+const expectedVastXml = 'test_xml';
+
+it('should not modify the bid\'s adXml when the tracking config is omitted', function () {
+ const adUnit = Object.assign({}, sampleAdUnit, { video: { adServer: { tracking: null } } });
+ const pbGlobal = Object.assign({}, pbGlobalMock, { adUnits: [ adUnit ] });
+ pbVideoFactory(null, () => ({}), pbGlobal, pbEvents);
+
+ bidAdjustmentCb(sampleBid);
+ // expect(vastXmlEditorMock.getVastXmlWithTrackingNodes.called).to.be.false;
+ // expect(vastXmlEditorMock.buildVastWrapper.called).to.be.false;
+});
+
+it('should request a vast wrapper when only an ad url is provided', function () {
+ const adUnit = Object.assign({}, sampleAdUnit, { video: { adServer: { tracking: { } } } });
+ const pbGlobal = Object.assign({}, pbGlobalMock, { adUnits: [ adUnit ] });
+ pbVideoFactory(null, () => ({}), pbGlobal, pbEvents);
+
+ bidAdjustmentCb(sampleBid);
+ // expect(vastXmlEditorMock.getVastXmlWithTrackingNodes.called).to.be.false;
+ // expect(vastXmlEditorMock.buildVastWrapper.called).to.be.true;
+});
+
+it('should request the addition of tracking nodes when an ad xml is provided', function () {
+ const adUnit = Object.assign({}, sampleAdUnit, { video: { adServer: { tracking: { } } } });
+ const pbGlobal = Object.assign({}, pbGlobalMock, { adUnits: [ adUnit ] });
+ pbVideoFactory(null, () => ({}), pbGlobal, pbEvents);
+
+ const bid = Object.assign({}, sampleBid, { vastXml: 'test_xml' });
+ bidAdjustmentCb(bid);
+ // expect(vastXmlEditorMock.getVastXmlWithTrackingNodes.called).to.be.true;
+ // expect(vastXmlEditorMock.buildVastWrapper.called).to.be.false;
+});
+
+it('should pass the tracking information as args to the xml editing function', function () {
+ const adUnit = Object.assign({}, sampleAdUnit, { video: { adServer: { tracking: {
+ impression: {
+ url: expectedImpressionUrl,
+ id: expectedImpressionId
+ },
+ error: {
+ url: expectedErrorUrl
+ }
+ } } } });
+ const pbGlobal = Object.assign({}, pbGlobalMock, { adUnits: [ adUnit ] });
+ pbVideoFactory(null, () => ({}), pbGlobal, pbEvents);
+
+ const bid = Object.assign({}, sampleBid, { vastXml: expectedVastXml });
+ bidAdjustmentCb(bid);
+ // expect(vastXmlEditorMock.getVastXmlWithTrackingNodes.called).to.be.true;
+ // expect(vastXmlEditorMock.getVastXmlWithTrackingNodes.calledWith(expectedVastXml, expectedImpressionUrl, expectedImpressionId, expectedErrorUrl))
+ // expect(vastXmlEditorMock.buildVastWrapper.called).to.be.false;
+});
+
+it('should generate the impression id when not specified in config', function () {
+ const adUnit = Object.assign({}, sampleAdUnit, { video: { adServer: { tracking: {
+ impression: {
+ url: expectedImpressionUrl,
+ },
+ error: {
+ url: expectedErrorUrl
+ }
+ } } } });
+ const pbGlobal = Object.assign({}, pbGlobalMock, { adUnits: [ adUnit ] });
+ pbVideoFactory(null, () => ({}), pbGlobal, pbEvents);
+
+ const bid = Object.assign({}, sampleBid, { vastXml: expectedVastXml });
+ bidAdjustmentCb(bid);
+ const expectedGeneratedId = sampleBid.adId + '-impression';
+ // expect(vastXmlEditorMock.getVastXmlWithTrackingNodes.called).to.be.true;
+ // expect(vastXmlEditorMock.getVastXmlWithTrackingNodes.calledWith(expectedVastXml, expectedImpressionUrl, expectedGeneratedId, expectedErrorUrl))
+ // expect(vastXmlEditorMock.buildVastWrapper.called).to.be.false;
+});
+*/
diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js
index 1abb89f3d24..929de6d2014 100644
--- a/test/spec/unit/pbjs_api_spec.js
+++ b/test/spec/unit/pbjs_api_spec.js
@@ -1198,7 +1198,7 @@ describe('Unit: Prebid Module', function () {
it('should require doc and id params', function () {
$$PREBID_GLOBAL$$.renderAd();
- var error = 'Error trying to write ad Id :undefined to the page. Missing document or adId';
+ var error = 'Error trying to write ad Id :undefined to the page. Missing adId';
assert.ok(spyLogError.calledWith(error), 'expected param error was logged');
});