diff --git a/modules/videoModule/constants/events.js b/modules/videoModule/constants/events.js new file mode 100644 index 00000000000..fbf8444f4a9 --- /dev/null +++ b/modules/videoModule/constants/events.js @@ -0,0 +1,51 @@ +// 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'; + +// Param options +export const PLAYBACK_MODE = { + VOD: 0, + LIVE: 1, + DVR: 2 +}; diff --git a/modules/videoModule/constants/ortb.js b/modules/videoModule/constants/ortb.js new file mode 100644 index 00000000000..e2de8912884 --- /dev/null +++ b/modules/videoModule/constants/ortb.js @@ -0,0 +1,66 @@ + +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.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/constants/vendorCodes.js b/modules/videoModule/constants/vendorCodes.js new file mode 100644 index 00000000000..090b383fbf8 --- /dev/null +++ b/modules/videoModule/constants/vendorCodes.js @@ -0,0 +1,2 @@ +export const JWPLAYER_VENDOR = 1; +export const VIDEO_JS_VENDOR = 2; diff --git a/modules/videoModule/coreVideo.js b/modules/videoModule/coreVideo.js index 215c01d7f6c..971ffb203ca 100644 --- a/modules/videoModule/coreVideo.js +++ b/modules/videoModule/coreVideo.js @@ -59,7 +59,9 @@ export function VideoSubmoduleBuilder(vendorDirectory_) { throw new Error('Unrecognized vendor code'); } - return submoduleFactory(providerConfig); + const submodule = submoduleFactory(providerConfig); + submodule.init && submodule.init(); + return submodule; } return { diff --git a/modules/videoModule/shared/state.js b/modules/videoModule/shared/state.js new file mode 100644 index 00000000000..e96f8321c60 --- /dev/null +++ b/modules/videoModule/shared/state.js @@ -0,0 +1,21 @@ +export default function stateFactory() { + let state = {}; + + function updateState(stateUpdate) { + Object.assign(state, stateUpdate); + } + + function getState() { + return state; + } + + function clearState() { + state = {}; + } + + return { + updateState, + getState, + clearState + }; +} diff --git a/modules/videoModule/submodules/jwplayerVideoProvider.js b/modules/videoModule/submodules/jwplayerVideoProvider.js new file mode 100644 index 00000000000..7d74f1c65c3 --- /dev/null +++ b/modules/videoModule/submodules/jwplayerVideoProvider.js @@ -0,0 +1,894 @@ +import { + PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, PLACEMENT, VPAID_MIME_TYPE +} from '../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, PLAYBACK_MODE +} from '../constants/events.js'; +import stateFactory from '../shared/state.js'; +import { JWPLAYER_VENDOR } from '../constants/vendorCodes.js'; +import { submodule } from '../../../src/hook.js'; + +export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callbackStorage_, utils) { + 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), + // linearity is omitted because both forms are supported. + // sequence - TODO not yet supported + battr: adConfig.battr, + maxextended: -1, + boxingallowed: 1, + playbackmethod: [ utils.getPlaybackMethod(config) ], + playbackend: 1, + // companionad - TODO add in future version + 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 needs 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 = 7; // TODO make constant in oRTB + } + + const item = player.getPlaylistItem() || {}; // TODO does player call need optimization ? + const { duration, playbackMode } = timeState.getState(); + const content = { + id: item.mediaid, + url: item.file, + title: item.title, + cat: item.iabCategories, + keywords: item.tags, + len: duration, + livestream: Math.min(playbackMode, 1) + }; + + return { + video, + content + } + } + + function setAdTagUrl(adTagUrl) { + if (!player) { + return; + } + player.playAd(adTagUrl); + } + + 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); + } +} + +const jwplayerSubmoduleFactory = function (config) { + const adState = adStateFactory(); + const timeState = timeStateFactory(); + const callbackStorage = callbackStorageFactory(); + return JWPlayerProvider(config, window.jwplayer, adState, timeState, callbackStorage, utils); +} + +jwplayerSubmoduleFactory.vendorCode = JWPLAYER_VENDOR; +export default jwplayerSubmoduleFactory; +submodule('video', 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; + } + + 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. + }, + + getPlacement: function(adConfig) { + // TODO might be able to use getPlacement from ad utils! + if (!adConfig.outstream) { + // https://developer.jwplayer.com/jwplayer/docs/jw8-embed-an-outstream-player for more info on outstream + return PLACEMENT.IN_STREAM; + } + }, + + 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; + } + 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'; + } +} + +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 + +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, + }; + this.updateState(updates); + } + + adState.updateForEvent = updateForEvent; + + function convertPlacementToOrtbCode(placement) { + switch (placement) { + case 'instream': + return PLACEMENT.IN_STREAM; + + 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; + } + } + + return adState; +} + +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/test/spec/modules/videoModule/coreVideo_spec.js b/test/spec/modules/videoModule/coreVideo_spec.js index 274791d49be..b5f621fbab1 100644 --- a/test/spec/modules/videoModule/coreVideo_spec.js +++ b/test/spec/modules/videoModule/coreVideo_spec.js @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { VideoSubmoduleBuilder, VideoCore -} from 'modules/videoModule/coreVideo'; +} from 'modules/videoModule/coreVideo.js'; describe('Video Submodule Builder', function () { const playerSpecificSubmoduleFactory = sinon.spy(); 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..6cce97ab0a0 --- /dev/null +++ b/test/spec/modules/videoModule/shared/state_spec.js @@ -0,0 +1,26 @@ +import stateFactory from 'modules/videoModule/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/submodules/jwplayerVideoProvider_spec.js b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js new file mode 100644 index 00000000000..5d6f4eeb18d --- /dev/null +++ b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js @@ -0,0 +1,755 @@ +import { + JWPlayerProvider, + adStateFactory, + timeStateFactory, + callbackStorageFactory, + utils +} from 'modules/videoModule/submodules/jwplayerVideoProvider'; + +import { + PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, PLACEMENT, VPAID_MIME_TYPE +} from 'modules/videoModule/constants/ortb.js'; + +import { + PLAYBACK_MODE, SETUP_COMPLETE, SETUP_FAILED, PLAY, AD_IMPRESSION +} from 'modules/videoModule/constants/events.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 () {}, + })(); +} + +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 () {}, + }; +} + +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); + 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); + 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); + 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); + 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); + provider.init(); + expect(setupSpy.called).to.be.false; + }); + }); + + describe('getId', function () { + it('should return configured div id', function () { + const provider = JWPlayerProvider({ divId: 'test_id' }); + 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); + 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(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), {}, {}, {}, {}); + 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()); + 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); + 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()); + 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.IN_STREAM); + + 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 in_stream when not configured for outstream', function () { + let adConfig = {}; + let placement = getPlacement(adConfig); + expect(placement).to.be.equal(PLACEMENT.IN_STREAM); + + adConfig = { outstream: false }; + placement = getPlacement(adConfig); + expect(placement).to.be.equal(PLACEMENT.IN_STREAM); + }); + + it('should be undefined on outstream', function () { + let adConfig = { outstream: true }; + let placement = getPlacement(adConfig); + 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; + }); + }); +});