From 17be9cbd137d88295dd99c89d806960f6e1b9f5a Mon Sep 17 00:00:00 2001 From: karimJWP Date: Wed, 4 Aug 2021 14:15:11 -0400 Subject: [PATCH 01/17] adds vendor consts --- modules/videoModule/constants/events.js | 44 + modules/videoModule/constants/ortb.js | 50 + modules/videoModule/constants/vendorCodes.js | 2 + modules/videoModule/shared/state.js | 21 + .../submodules/jwplayerVideoProvider.js | 891 ++++++++++++++++++ 5 files changed, 1008 insertions(+) create mode 100644 modules/videoModule/constants/events.js create mode 100644 modules/videoModule/constants/ortb.js create mode 100644 modules/videoModule/constants/vendorCodes.js create mode 100644 modules/videoModule/shared/state.js create mode 100644 modules/videoModule/submodules/jwplayerVideoProvider.js diff --git a/modules/videoModule/constants/events.js b/modules/videoModule/constants/events.js new file mode 100644 index 00000000000..32399703de2 --- /dev/null +++ b/modules/videoModule/constants/events.js @@ -0,0 +1,44 @@ +// 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'; diff --git a/modules/videoModule/constants/ortb.js b/modules/videoModule/constants/ortb.js new file mode 100644 index 00000000000..629d35145b2 --- /dev/null +++ b/modules/videoModule/constants/ortb.js @@ -0,0 +1,50 @@ + + +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' +}; + +/* +ORTB 2.5 section 5.10 - Playback Methods + */ +export const PLAYBACK_METHODS = { // Spec 5.10. + 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 = { // Spec 5.8. + // 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 = { // Spec 5.6. + 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/shared/state.js b/modules/videoModule/shared/state.js new file mode 100644 index 00000000000..d3f45d7b50b --- /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..8acef158a0a --- /dev/null +++ b/modules/videoModule/submodules/jwplayerVideoProvider.js @@ -0,0 +1,891 @@ +import { + PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, +} from "../constants/ortb"; +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 "../constants/events"; +import stateFactory from "../shared/state"; +import { JWPLAYER_VENDOR } from "../constants/vendorCodes"; + +export function jwplayerProviderFactory(config, jwplayer_, adState_, timeState_, callbackStorage_) { + 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 = null; + 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 { + const payload = getSetupCompletePayload(); + setupCompleteCallback && setupCompleteCallback(SETUP_COMPLETE, payload); + } + pendingSeek = {}; + } + + function getId() { + return divId; + } + + function getOrtbParams() { + if (!player) { + return; + } + const config = player.getConfig(); + const adConfig = config.advertising || {}; + supportedMediaTypes = supportedMediaTypes || filterCanPlay(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: getStartDelay(), + placement: getPlacement(adConfig), + // linearity is omitted because both forms are supported. + // sequence + // battr + maxextended: -1, + boxingallowed: 1, + playbackmethod: [ getPlaybackMethod(config) ], + playbackend: 1, + // companionad - todo add in future version + api: [ + API_FRAMEWORKS.VPAID_2_0 + ], + }; + + if (isOmidSupported(adConfig.adClient)) { + video.api.push(API_FRAMEWORKS.OMID_1_0); + } + + Object.assign(video, 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; + } + + 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? + // keywords? + len: duration, + livestream: Math.min(playbackMode, 1) + }; + + return { + video, + content + } + } + + function renderAd(adTagUrl) { + 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 eventHandler = callbackStorage.getCallback(event, callback); + const jwEvent = getJWPlayerEvent(event); + player.off(jwEvent, eventHandler); + }); + } + + function getJWPlayerEvent(eventName) { + switch(eventName) { + case SETUP_COMPLETE: + return 'ready'; + break; + + case SETUP_FAILED: + return 'setupError'; + break; + + case DESTROYED: + return 'remove'; + break; + + case AD_STARTED: + return AD_IMPRESSION; + break; + + case AD_IMPRESSION: + return 'adViewableImpression'; + break; + + case PLAYBACK_REQUEST: + return 'playAttempt'; + break; + + case AUTOSTART_BLOCKED: + return 'autostartNotAllowed'; + break; + + case CONTENT_LOADED: + return 'playlistItem'; + break; + + case SEEK_START: + return 'seek'; + break; + + case SEEK_END: + return 'seeked'; + break; + + case RENDITION_UPDATE: + return 'visualQuality'; + break; + + case PLAYER_RESIZE: + return 'resize'; + break; + + default: + return eventName; + } + } + + function destroy() { + if (!player) { + return; + } + player.remove(); + player = null; + } + + return { + init, + getId, + getOrtbParams, + renderAd, + onEvents, + offEvents, + destroy + }; + + function setupPlayer(config) { + if (!config) { + return; + } + player.setup(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, // come up with code + 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(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: playerConfig.autostart // TODO: autostart could be defined in specific player config + }); + 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, + // Content Tags (Required - nullable) + }); + 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 (Required) + }); + 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: + console.log('early exit'); + return; + } + callbackStorage.storeCallback(type, eventHandler, callback); + } + + // UTILS + + function getJwConfig(config) { + if (!config || !config.params) { + return; + } + const jwConfig = config.params.vendorConfig || {}; + if (jwConfig.autostart === undefined) { + jwConfig.autostart = config.autostart; + } + + if (jwConfig.mute === undefined) { + jwConfig.mute = config.mute; + } + + if (!jwConfig.key) { + jwConfig.key = config.licenseKey; + } + + const advertising = jwConfig.advertising || {}; + if (!jwConfig.file && !jwConfig.playlist && !jwConfig.source) { + advertising.outstream = true; + advertising.client = advertising.client || 'vast'; + } + + jwConfig.advertising = advertising; + return jwConfig; + } + + function getSkipParams(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; + } + + function filterCanPlay(mediaTypes = []) { + const el = document.createElement('video'); + return mediaTypes + .filter(mediaType => el.canPlayType(mediaType)) + .concat('application/javascript'); // Always allow VPAIDs. + } + + function getStartDelay() { + // todo calculate + } + + function getPlacement(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 1; + } + } + + function getPlaybackMethod({ 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 + */ + function isOmidSupported(adClient) { + const omidIsLoaded = window.OmidSessionClient !== undefined; + return omidIsLoaded && adClient === 'vast'; + } +} + +const jwplayerSubmoduleFactory = function (config) { + const adState = adStateFactory(); + const timeState = timeStateFactory(); + const callbackStorage = callbackStorageFactory(); + return jwplayerProviderFactory(config, window.jwplayer, adState, timeState, callbackStorage); +} +jwplayerSubmoduleFactory.vendorCode = JWPLAYER_VENDOR; + +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 analytics plugin, 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 1; + break; + + case 'banner': + return 2; + break; + + case 'article': + return 3; + break; + + case 'feed': + return 4; + break; + + case 'interstitial': + case 'slider': + case 'floating': + return 5; + } + } + + 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) { + let playbackMode; + if (duration > 0) { + playbackMode = 0; //vod + } else if (duration < 0) { + playbackMode = 2; //dvr + } else { + playbackMode = 1; //live + } + return playbackMode; + } + + return timeState; +} + +window.jwplayerVideoFactory = jwplayerSubmoduleFactory; +export default jwplayerSubmoduleFactory; From d59658f46555a085c477cee278f75b89da61a6fa Mon Sep 17 00:00:00 2001 From: karimJWP Date: Wed, 4 Aug 2021 14:24:24 -0400 Subject: [PATCH 02/17] registers jwplayer submodule --- .../submodules/jwplayerVideoProvider.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/modules/videoModule/submodules/jwplayerVideoProvider.js b/modules/videoModule/submodules/jwplayerVideoProvider.js index 8acef158a0a..be2691ec24b 100644 --- a/modules/videoModule/submodules/jwplayerVideoProvider.js +++ b/modules/videoModule/submodules/jwplayerVideoProvider.js @@ -9,8 +9,9 @@ import { } from "../constants/events"; import stateFactory from "../shared/state"; import { JWPLAYER_VENDOR } from "../constants/vendorCodes"; +import { submodule } from '../../../src/hook.js'; -export function jwplayerProviderFactory(config, jwplayer_, adState_, timeState_, callbackStorage_) { +export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callbackStorage_) { const jwplayer = jwplayer_; let player = null; let playerVersion = null; @@ -754,9 +755,14 @@ const jwplayerSubmoduleFactory = function (config) { const adState = adStateFactory(); const timeState = timeStateFactory(); const callbackStorage = callbackStorageFactory(); - return jwplayerProviderFactory(config, window.jwplayer, adState, timeState, callbackStorage); + return JWPlayerProvider(config, window.jwplayer, adState, timeState, callbackStorage); } + jwplayerSubmoduleFactory.vendorCode = JWPLAYER_VENDOR; +export default jwplayerSubmoduleFactory; +submodule('video', jwplayerSubmoduleFactory); + +// HELPERS export function callbackStorageFactory() { let storage = {}; @@ -813,7 +819,7 @@ export function adStateFactory() { linear: event.linear, vastVersion: event.vastversion, // campaignId: - creativeUrl: event.mediaFile, // TODO: per analytics plugin, mediafile might be object w/ file property. verify + creativeUrl: event.mediaFile, // TODO: per AP, mediafile might be object w/ file property. verify adId: event.adId, universalAdId: event.universalAdId, creativeId: event.creativeAdId, @@ -886,6 +892,3 @@ export function timeStateFactory() { return timeState; } - -window.jwplayerVideoFactory = jwplayerSubmoduleFactory; -export default jwplayerSubmoduleFactory; From 8fc9df47f5fcaaf44e8250e7dc612b2ce54a82ff Mon Sep 17 00:00:00 2001 From: karimJWP Date: Wed, 4 Aug 2021 15:01:57 -0400 Subject: [PATCH 03/17] renames render ad --- modules/videoModule/coreVideo.js | 4 +- .../submodules/jwplayerVideoProvider.js | 134 +++++++++--------- 2 files changed, 69 insertions(+), 69 deletions(-) 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/submodules/jwplayerVideoProvider.js b/modules/videoModule/submodules/jwplayerVideoProvider.js index be2691ec24b..46b7b3bd6a5 100644 --- a/modules/videoModule/submodules/jwplayerVideoProvider.js +++ b/modules/videoModule/submodules/jwplayerVideoProvider.js @@ -20,7 +20,7 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba let adState = adState_; let timeState = timeState_; let callbackStorage = callbackStorage_; - let pendingSeek = null; + let pendingSeek = {}; let supportedMediaTypes = null; let minimumSupportedPlayerVersion = '8.20.1'; let setupCompleteCallback = null; @@ -53,7 +53,6 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba const payload = getSetupCompletePayload(); setupCompleteCallback && setupCompleteCallback(SETUP_COMPLETE, payload); } - pendingSeek = {}; } function getId() { @@ -83,13 +82,13 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba startdelay: getStartDelay(), placement: getPlacement(adConfig), // linearity is omitted because both forms are supported. - // sequence - // battr + // sequence - TODO not yet supported + battr: adConfig.battr, maxextended: -1, boxingallowed: 1, playbackmethod: [ getPlaybackMethod(config) ], playbackend: 1, - // companionad - todo add in future version + // companionad - TODO add in future version api: [ API_FRAMEWORKS.VPAID_2_0 ], @@ -114,8 +113,8 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba id: item.mediaid, url: item.file, title: item.title, - // cat? - // keywords? + cat: item.iabCategories, + keywords: item.tags, len: duration, livestream: Math.min(playbackMode, 1) }; @@ -126,7 +125,7 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba } } - function renderAd(adTagUrl) { + function setAdTagUrl(adTagUrl) { player.playAd(adTagUrl); } @@ -159,61 +158,6 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba }); } - function getJWPlayerEvent(eventName) { - switch(eventName) { - case SETUP_COMPLETE: - return 'ready'; - break; - - case SETUP_FAILED: - return 'setupError'; - break; - - case DESTROYED: - return 'remove'; - break; - - case AD_STARTED: - return AD_IMPRESSION; - break; - - case AD_IMPRESSION: - return 'adViewableImpression'; - break; - - case PLAYBACK_REQUEST: - return 'playAttempt'; - break; - - case AUTOSTART_BLOCKED: - return 'autostartNotAllowed'; - break; - - case CONTENT_LOADED: - return 'playlistItem'; - break; - - case SEEK_START: - return 'seek'; - break; - - case SEEK_END: - return 'seeked'; - break; - - case RENDITION_UPDATE: - return 'visualQuality'; - break; - - case PLAYER_RESIZE: - return 'resize'; - break; - - default: - return eventName; - } - } - function destroy() { if (!player) { return; @@ -226,7 +170,7 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba init, getId, getOrtbParams, - renderAd, + setAdTagUrl, onEvents, offEvents, destroy @@ -260,7 +204,7 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba divId, playerVersion, type: SETUP_FAILED, - errorCode, // come up with code + errorCode, errorMessage: '', sourceError: null }; @@ -493,7 +437,7 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba title: item.title, description: item.description, playlistIndex: index, - // Content Tags (Required - nullable) + contentTags: item.tags }); callback(type, payload); }; @@ -589,7 +533,7 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba audioReportedBitrate: bitrate, encodedVideoWidth: level.width, encodedVideoHeight: level.height, - // videoFramerate (Required) + videoFramerate: e.frameRate }); callback(type, payload); }; @@ -662,7 +606,6 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba break; default: - console.log('early exit'); return; } callbackStorage.storeCallback(type, eventHandler, callback); @@ -697,6 +640,61 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba return jwConfig; } + function getJWPlayerEvent(eventName) { + switch(eventName) { + case SETUP_COMPLETE: + return 'ready'; + break; + + case SETUP_FAILED: + return 'setupError'; + break; + + case DESTROYED: + return 'remove'; + break; + + case AD_STARTED: + return AD_IMPRESSION; + break; + + case AD_IMPRESSION: + return 'adViewableImpression'; + break; + + case PLAYBACK_REQUEST: + return 'playAttempt'; + break; + + case AUTOSTART_BLOCKED: + return 'autostartNotAllowed'; + break; + + case CONTENT_LOADED: + return 'playlistItem'; + break; + + case SEEK_START: + return 'seek'; + break; + + case SEEK_END: + return 'seeked'; + break; + + case RENDITION_UPDATE: + return 'visualQuality'; + break; + + case PLAYER_RESIZE: + return 'resize'; + break; + + default: + return eventName; + } + } + function getSkipParams(adConfig) { const skipParams = {}; const skipoffset = adConfig.skipoffset; From a56a3d5fef9a897614995ec73dbc639055472b33 Mon Sep 17 00:00:00 2001 From: karimJWP Date: Wed, 4 Aug 2021 15:34:37 -0400 Subject: [PATCH 04/17] checks config autostart --- modules/videoModule/submodules/jwplayerVideoProvider.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/videoModule/submodules/jwplayerVideoProvider.js b/modules/videoModule/submodules/jwplayerVideoProvider.js index 46b7b3bd6a5..c54d9896ff3 100644 --- a/modules/videoModule/submodules/jwplayerVideoProvider.js +++ b/modules/videoModule/submodules/jwplayerVideoProvider.js @@ -388,7 +388,7 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba const playlistItemCount = e.playlist.length; Object.assign(payload, { playlistItemCount, - autostart: playerConfig.autostart // TODO: autostart could be defined in specific player config + autostart: player.getConfig().autostart }); callback(type, payload); }; @@ -718,6 +718,8 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba function getStartDelay() { // 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. } function getPlacement(adConfig) { From fba491058e6962f27283ccc4e16e95d4e58b6e55 Mon Sep 17 00:00:00 2001 From: karimJWP Date: Wed, 4 Aug 2021 16:02:43 -0400 Subject: [PATCH 05/17] moves private functions to a util obj --- .../submodules/jwplayerVideoProvider.js | 124 +++++++++--------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/modules/videoModule/submodules/jwplayerVideoProvider.js b/modules/videoModule/submodules/jwplayerVideoProvider.js index c54d9896ff3..b965ed1648e 100644 --- a/modules/videoModule/submodules/jwplayerVideoProvider.js +++ b/modules/videoModule/submodules/jwplayerVideoProvider.js @@ -11,7 +11,7 @@ import stateFactory from "../shared/state"; import { JWPLAYER_VENDOR } from "../constants/vendorCodes"; import { submodule } from '../../../src/hook.js'; -export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callbackStorage_) { +export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callbackStorage_, utils) { const jwplayer = jwplayer_; let player = null; let playerVersion = null; @@ -65,7 +65,7 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba } const config = player.getConfig(); const adConfig = config.advertising || {}; - supportedMediaTypes = supportedMediaTypes || filterCanPlay(MEDIA_TYPES); + supportedMediaTypes = supportedMediaTypes || utils.getSupportedMediaTypes(MEDIA_TYPES); const video = { mimes: supportedMediaTypes, @@ -79,14 +79,14 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba ], h: player.getHeight(), // TODO does player call need optimization ? w: player.getWidth(), // TODO does player call need optimization ? - startdelay: getStartDelay(), - placement: getPlacement(adConfig), + 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: [ getPlaybackMethod(config) ], + playbackmethod: [ utils.getPlaybackMethod(config) ], playbackend: 1, // companionad - TODO add in future version api: [ @@ -94,11 +94,11 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba ], }; - if (isOmidSupported(adConfig.adClient)) { + if (utils.isOmidSupported(adConfig.adClient)) { video.api.push(API_FRAMEWORKS.OMID_1_0); } - Object.assign(video, getSkipParams(adConfig)); + 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 @@ -153,7 +153,7 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba function offEvents(events, callback) { events.forEach(event => { const eventHandler = callbackStorage.getCallback(event, callback); - const jwEvent = getJWPlayerEvent(event); + const jwEvent = utils.getJwEvent(event); player.off(jwEvent, eventHandler); }); } @@ -180,7 +180,7 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba if (!config) { return; } - player.setup(getJwConfig(config)); + player.setup(utils.getJwConfig(config)); } function getSetupCompletePayload() { @@ -278,7 +278,7 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba eventHandler = e => { adState.updateForEvent(e); const adConfig = player.getConfig().advertising; - adState.updateState(getSkipParams(adConfig)); + adState.updateState(utils.getSkipParams(adConfig)); Object.assign(payload, adState.getState()); callback(type, payload); }; @@ -610,37 +610,50 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba } callbackStorage.storeCallback(type, eventHandler, callback); } +} - // UTILS +const jwplayerSubmoduleFactory = function (config) { + const adState = adStateFactory(); + const timeState = timeStateFactory(); + const callbackStorage = callbackStorageFactory(); + return JWPlayerProvider(config, window.jwplayer, adState, timeState, callbackStorage, utils); +} - function getJwConfig(config) { - if (!config || !config.params) { - return; - } - const jwConfig = config.params.vendorConfig || {}; - if (jwConfig.autostart === undefined) { - jwConfig.autostart = config.autostart; - } +jwplayerSubmoduleFactory.vendorCode = JWPLAYER_VENDOR; +export default jwplayerSubmoduleFactory; +submodule('video', jwplayerSubmoduleFactory); - if (jwConfig.mute === undefined) { - jwConfig.mute = config.mute; - } +// HELPERS - if (!jwConfig.key) { - jwConfig.key = config.licenseKey; - } +export const utils = { + getJwConfig: function(config) { + if (!config || !config.params) { + return; + } + const jwConfig = config.params.vendorConfig || {}; + if (jwConfig.autostart === undefined) { + jwConfig.autostart = config.autostart; + } - const advertising = jwConfig.advertising || {}; - if (!jwConfig.file && !jwConfig.playlist && !jwConfig.source) { - advertising.outstream = true; - advertising.client = advertising.client || 'vast'; - } + if (jwConfig.mute === undefined) { + jwConfig.mute = config.mute; + } + + if (!jwConfig.key) { + jwConfig.key = config.licenseKey; + } - jwConfig.advertising = advertising; - return jwConfig; + const advertising = jwConfig.advertising || {}; + if (!jwConfig.file && !jwConfig.playlist && !jwConfig.source) { + advertising.outstream = true; + advertising.client = advertising.client || 'vast'; } - function getJWPlayerEvent(eventName) { + jwConfig.advertising = advertising; + return jwConfig; +}, + + getJwEvent: function(eventName) { switch(eventName) { case SETUP_COMPLETE: return 'ready'; @@ -693,9 +706,9 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba default: return eventName; } - } + }, - function getSkipParams(adConfig) { + getSkipParams: function(adConfig) { const skipParams = {}; const skipoffset = adConfig.skipoffset; if (skipoffset !== undefined) { @@ -707,37 +720,37 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba } } return skipParams; - } + }, - function filterCanPlay(mediaTypes = []) { - const el = document.createElement('video'); - return mediaTypes - .filter(mediaType => el.canPlayType(mediaType)) - .concat('application/javascript'); // Always allow VPAIDs. - } + getSupportedMediaTypes: function(mediaTypes = []) { + const el = document.createElement('video'); + return mediaTypes + .filter(mediaType => el.canPlayType(mediaType)) + .concat('application/javascript'); // Always allow VPAIDs. +}, - function getStartDelay() { + 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. - } + }, - function getPlacement(adConfig) { + 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 1; } - } + }, - function getPlaybackMethod({ autoplay, mute, autoplayAdsMuted }) { + 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 @@ -745,25 +758,12 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba * @param {string=} adClient - The identifier of the ad plugin requesting the bid * @returns {boolean} - support of omid */ - function isOmidSupported(adClient) { + isOmidSupported: function(adClient) { const omidIsLoaded = window.OmidSessionClient !== undefined; return omidIsLoaded && adClient === 'vast'; } } -const jwplayerSubmoduleFactory = function (config) { - const adState = adStateFactory(); - const timeState = timeStateFactory(); - const callbackStorage = callbackStorageFactory(); - return JWPlayerProvider(config, window.jwplayer, adState, timeState, callbackStorage); -} - -jwplayerSubmoduleFactory.vendorCode = JWPLAYER_VENDOR; -export default jwplayerSubmoduleFactory; -submodule('video', jwplayerSubmoduleFactory); - -// HELPERS - export function callbackStorageFactory() { let storage = {}; From da2f7cc94c3012a12a06ce5c12f6b67daa867cde Mon Sep 17 00:00:00 2001 From: karimJWP Date: Thu, 5 Aug 2021 15:25:12 -0400 Subject: [PATCH 06/17] introduces ad optimization --- .../submodules/jwplayerVideoProvider.js | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/modules/videoModule/submodules/jwplayerVideoProvider.js b/modules/videoModule/submodules/jwplayerVideoProvider.js index b965ed1648e..b46637d3486 100644 --- a/modules/videoModule/submodules/jwplayerVideoProvider.js +++ b/modules/videoModule/submodules/jwplayerVideoProvider.js @@ -627,31 +627,36 @@ submodule('video', jwplayerSubmoduleFactory); export const utils = { getJwConfig: function(config) { - if (!config || !config.params) { - return; - } - const jwConfig = config.params.vendorConfig || {}; - if (jwConfig.autostart === undefined) { - jwConfig.autostart = config.autostart; - } + if (!config) { + return; + } - if (jwConfig.mute === undefined) { - jwConfig.mute = config.mute; - } + const params = config.params; + const jwConfig = params.vendorConfig || {}; + if (jwConfig.autostart === undefined && config.autoStart !== undefined) { + jwConfig.autostart = config.autoStart; + } - if (!jwConfig.key) { - jwConfig.key = config.licenseKey; - } + if (jwConfig.mute === undefined && config.mute !== undefined) { + jwConfig.mute = config.mute; + } - const advertising = jwConfig.advertising || {}; - if (!jwConfig.file && !jwConfig.playlist && !jwConfig.source) { - advertising.outstream = true; - advertising.client = advertising.client || 'vast'; - } + if (!jwConfig.key && config.licenseKey !== undefined) { + jwConfig.key = config.licenseKey; + } - jwConfig.advertising = advertising; - return jwConfig; -}, + if (params.adOptimization === false) { + return jwConfig; + } + + const advertising = jwConfig.advertising || { client: 'vast' }; + if (!jwConfig.file && !jwConfig.playlist && !jwConfig.source) { + advertising.outstream = true; + } + + jwConfig.advertising = advertising; + return jwConfig; + }, getJwEvent: function(eventName) { switch(eventName) { From d30dd858bdb6cf6b397142a9f7c45164b44cf739 Mon Sep 17 00:00:00 2001 From: karimJWP Date: Mon, 9 Aug 2021 15:27:47 -0400 Subject: [PATCH 07/17] tests state.js --- modules/videoModule/constants/ortb.js | 53 +++++++++---------- modules/videoModule/shared/state.js | 30 +++++------ .../submodules/jwplayerVideoProvider.js | 52 +++++++----------- .../modules/videoModule/coreVideo_spec.js | 2 +- .../modules/videoModule/shared/state_spec.js | 26 +++++++++ 5 files changed, 86 insertions(+), 77 deletions(-) create mode 100644 test/spec/modules/videoModule/shared/state_spec.js diff --git a/modules/videoModule/constants/ortb.js b/modules/videoModule/constants/ortb.js index 629d35145b2..7e400d6d1af 100644 --- a/modules/videoModule/constants/ortb.js +++ b/modules/videoModule/constants/ortb.js @@ -1,50 +1,49 @@ - 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' + 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' }; /* ORTB 2.5 section 5.10 - Playback Methods */ -export const PLAYBACK_METHODS = { // Spec 5.10. - AUTOPLAY: 1, - AUTOPLAY_MUTED: 2, - CLICK_TO_PLAY: 3, - CLICK_TO_PLAY_MUTED: 4, - VIEWABLE: 5, - VIEWABLE_MUTED: 6 +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 = { // Spec 5.8. - // 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 +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 = { // Spec 5.6. - VPAID_1_0: 1, - VPAID_2_0: 2, - OMID_1_0: 7 +export const API_FRAMEWORKS = { + VPAID_1_0: 1, + VPAID_2_0: 2, + OMID_1_0: 7 }; diff --git a/modules/videoModule/shared/state.js b/modules/videoModule/shared/state.js index d3f45d7b50b..e96f8321c60 100644 --- a/modules/videoModule/shared/state.js +++ b/modules/videoModule/shared/state.js @@ -1,21 +1,21 @@ export default function stateFactory() { - let state = {}; + let state = {}; - function updateState(stateUpdate) { - Object.assign(state, stateUpdate); - } + function updateState(stateUpdate) { + Object.assign(state, stateUpdate); + } - function getState() { - return state; - } + function getState() { + return state; + } - function clearState() { - state = {}; - } + function clearState() { + state = {}; + } - return { - updateState, - getState, - clearState - }; + return { + updateState, + getState, + clearState + }; } diff --git a/modules/videoModule/submodules/jwplayerVideoProvider.js b/modules/videoModule/submodules/jwplayerVideoProvider.js index b46637d3486..e5186dacc7d 100644 --- a/modules/videoModule/submodules/jwplayerVideoProvider.js +++ b/modules/videoModule/submodules/jwplayerVideoProvider.js @@ -1,14 +1,14 @@ import { PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, -} from "../constants/ortb"; +} 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 -} from "../constants/events"; -import stateFactory from "../shared/state"; -import { JWPLAYER_VENDOR } from "../constants/vendorCodes"; +} 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) { @@ -240,8 +240,8 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba player && player.on('setupError', eventHandler); break; - default: - return; + default: + return; } callbackStorage.storeCallback(type, eventHandler, callback); } @@ -296,9 +296,9 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba case AD_IMPRESSION: eventHandler = () => { - Object.assign(payload, adState.getState(), timeState.getState()); - callback(type, payload); - }; + Object.assign(payload, adState.getState(), timeState.getState()); + callback(type, payload); + }; player.on('adViewableImpression', eventHandler); break; @@ -353,7 +353,7 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba case AD_ERROR: eventHandler = e => { - Object.assign( payload, { + Object.assign(payload, { playerErrorCode: e.adErrorCode, vastErrorCode: e.code, errorMessage: e.message, @@ -631,7 +631,7 @@ export const utils = { return; } - const params = config.params; + const params = config.params || {}; const jwConfig = params.vendorConfig || {}; if (jwConfig.autostart === undefined && config.autoStart !== undefined) { jwConfig.autostart = config.autoStart; @@ -659,54 +659,42 @@ export const utils = { }, getJwEvent: function(eventName) { - switch(eventName) { + switch (eventName) { case SETUP_COMPLETE: return 'ready'; - break; case SETUP_FAILED: return 'setupError'; - break; case DESTROYED: return 'remove'; - break; case AD_STARTED: return AD_IMPRESSION; - break; case AD_IMPRESSION: return 'adViewableImpression'; - break; case PLAYBACK_REQUEST: return 'playAttempt'; - break; case AUTOSTART_BLOCKED: return 'autostartNotAllowed'; - break; case CONTENT_LOADED: return 'playlistItem'; - break; case SEEK_START: return 'seek'; - break; case SEEK_END: return 'seeked'; - break; case RENDITION_UPDATE: return 'visualQuality'; - break; case PLAYER_RESIZE: return 'resize'; - break; default: return eventName; @@ -728,11 +716,11 @@ export const utils = { }, getSupportedMediaTypes: function(mediaTypes = []) { - const el = document.createElement('video'); - return mediaTypes + const el = document.createElement('video'); + return mediaTypes .filter(mediaType => el.canPlayType(mediaType)) .concat('application/javascript'); // Always allow VPAIDs. -}, + }, getStartDelay: function() { // todo calculate @@ -845,19 +833,15 @@ export function adStateFactory() { switch (placement) { case 'instream': return 1; - break; case 'banner': return 2; - break; case 'article': return 3; - break; case 'feed': return 4; - break; case 'interstitial': case 'slider': @@ -886,11 +870,11 @@ export function timeStateFactory() { function getPlaybackMode(duration) { let playbackMode; if (duration > 0) { - playbackMode = 0; //vod + playbackMode = 0; // vod } else if (duration < 0) { - playbackMode = 2; //dvr + playbackMode = 2; // dvr } else { - playbackMode = 1; //live + playbackMode = 1; // live } return playbackMode; } 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; + }); +}); From 4c30d71698a9c729caa7c40b59100d627693b1f6 Mon Sep 17 00:00:00 2001 From: karimJWP Date: Mon, 9 Aug 2021 16:19:25 -0400 Subject: [PATCH 08/17] introduces submodule tests --- modules/videoModule/constants/ortb.js | 10 ++ .../submodules/jwplayerVideoProvider.js | 6 +- .../submodules/jwplayerVideoProvider_spec.js | 159 ++++++++++++++++++ 3 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js diff --git a/modules/videoModule/constants/ortb.js b/modules/videoModule/constants/ortb.js index 7e400d6d1af..36ce574a2b3 100644 --- a/modules/videoModule/constants/ortb.js +++ b/modules/videoModule/constants/ortb.js @@ -13,6 +13,16 @@ export const VIDEO_MIME_TYPE = { 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 +}; + /* ORTB 2.5 section 5.10 - Playback Methods */ diff --git a/modules/videoModule/submodules/jwplayerVideoProvider.js b/modules/videoModule/submodules/jwplayerVideoProvider.js index e5186dacc7d..b2f95ad943a 100644 --- a/modules/videoModule/submodules/jwplayerVideoProvider.js +++ b/modules/videoModule/submodules/jwplayerVideoProvider.js @@ -1,5 +1,5 @@ import { - PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, + 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, @@ -719,7 +719,7 @@ export const utils = { const el = document.createElement('video'); return mediaTypes .filter(mediaType => el.canPlayType(mediaType)) - .concat('application/javascript'); // Always allow VPAIDs. + .concat(VPAID_MIME_TYPE); // Always allow VPAIDs. }, getStartDelay: function() { @@ -732,7 +732,7 @@ export const utils = { // 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 1; + return PLACEMENT.IN_STREAM; } }, 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..d8e62e706b7 --- /dev/null +++ b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js @@ -0,0 +1,159 @@ +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' + +describe('JWPlayerProvider', function () { + +}); + +describe('adStateFactory', function () { + +}); + +describe('timeStateFactory', function () { + +}); + +describe('callbackStorageFactory', function () { + +}); +describe('utils', function () { + 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; + }); + }); +}); From 300b8e0499dc5c5c398e159636baa5a34d527078 Mon Sep 17 00:00:00 2001 From: karimJWP Date: Mon, 9 Aug 2021 17:19:13 -0400 Subject: [PATCH 09/17] populates utils tests --- .../submodules/jwplayerVideoProvider.js | 1 + .../submodules/jwplayerVideoProvider_spec.js | 101 ++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/modules/videoModule/submodules/jwplayerVideoProvider.js b/modules/videoModule/submodules/jwplayerVideoProvider.js index b2f95ad943a..627361d87c5 100644 --- a/modules/videoModule/submodules/jwplayerVideoProvider.js +++ b/modules/videoModule/submodules/jwplayerVideoProvider.js @@ -651,6 +651,7 @@ export const utils = { const advertising = jwConfig.advertising || { client: 'vast' }; if (!jwConfig.file && !jwConfig.playlist && !jwConfig.source) { + // TODO verify accuracy advertising.outstream = true; } diff --git a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js index d8e62e706b7..a091499ab8c 100644 --- a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js +++ b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js @@ -26,6 +26,107 @@ describe('callbackStorageFactory', function () { }); 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; From 4a796b6a96b6b6d8c87b9fa5ea31580a4e56b80d Mon Sep 17 00:00:00 2001 From: karimJWP Date: Mon, 9 Aug 2021 17:52:12 -0400 Subject: [PATCH 10/17] tests callback storage --- .../submodules/jwplayerVideoProvider_spec.js | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js index a091499ab8c..36c1460b70c 100644 --- a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js +++ b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js @@ -23,7 +23,45 @@ describe('timeStateFactory', function () { }); 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 () { From 5eeec6c51199e899975cb455dea42792ff9f757f Mon Sep 17 00:00:00 2001 From: karimJWP Date: Mon, 9 Aug 2021 20:24:10 -0400 Subject: [PATCH 11/17] tests ad and time states --- modules/videoModule/constants/events.js | 7 + modules/videoModule/constants/ortb.js | 9 +- .../submodules/jwplayerVideoProvider.js | 22 +-- .../submodules/jwplayerVideoProvider_spec.js | 186 +++++++++++++++++- 4 files changed, 210 insertions(+), 14 deletions(-) diff --git a/modules/videoModule/constants/events.js b/modules/videoModule/constants/events.js index 32399703de2..fbf8444f4a9 100644 --- a/modules/videoModule/constants/events.js +++ b/modules/videoModule/constants/events.js @@ -42,3 +42,10 @@ 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 index 36ce574a2b3..e2de8912884 100644 --- a/modules/videoModule/constants/ortb.js +++ b/modules/videoModule/constants/ortb.js @@ -20,7 +20,14 @@ export const VPAID_MIME_TYPE = JS_APP_MIME_TYPE; ORTB 2.5 section 5.9 - Video Placement Types */ export const PLACEMENT = { - IN_STREAM: 1 + IN_STREAM: 1, + BANNER: 2, + ARTICLE: 3, + FEED: 4, + INTERSTITIAL: 5, + SLIDER: 5, + FLOATING: 5, + INTERSTITIAL_SLIDER_FLOATING: 5 }; /* diff --git a/modules/videoModule/submodules/jwplayerVideoProvider.js b/modules/videoModule/submodules/jwplayerVideoProvider.js index 627361d87c5..586010f8867 100644 --- a/modules/videoModule/submodules/jwplayerVideoProvider.js +++ b/modules/videoModule/submodules/jwplayerVideoProvider.js @@ -5,7 +5,7 @@ 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 + 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'; @@ -833,21 +833,21 @@ export function adStateFactory() { function convertPlacementToOrtbCode(placement) { switch (placement) { case 'instream': - return 1; + return PLACEMENT.IN_STREAM; case 'banner': - return 2; + return PLACEMENT.BANNER; case 'article': - return 3; + return PLACEMENT.ARTICLE; case 'feed': - return 4; + return PLACEMENT.FEED; case 'interstitial': case 'slider': case 'floating': - return 5; + return PLACEMENT.INTERSTITIAL_SLIDER_FLOATING; } } @@ -869,15 +869,13 @@ export function timeStateFactory() { timeState.updateForEvent = updateForEvent; function getPlaybackMode(duration) { - let playbackMode; if (duration > 0) { - playbackMode = 0; // vod + return PLAYBACK_MODE.VOD; } else if (duration < 0) { - playbackMode = 2; // dvr - } else { - playbackMode = 1; // live + return PLAYBACK_MODE.DVR; } - return playbackMode; + + return PLAYBACK_MODE.LIVE; } return timeState; diff --git a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js index 36c1460b70c..b4c88aa0020 100644 --- a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js +++ b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js @@ -8,18 +8,201 @@ import { import { PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, PLACEMENT, VPAID_MIME_TYPE -} from 'modules/videoModule/constants/ortb.js' +} from 'modules/videoModule/constants/ortb.js'; + +import { + PLAYBACK_MODE +} from 'modules/videoModule/constants/events.js' describe('JWPlayerProvider', function () { }); 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 () { @@ -63,6 +246,7 @@ describe('callbackStorageFactory', function () { expect(callbackStorage.getCallback('event', callback1)).to.be.undefined; }); }); + describe('utils', function () { describe('getJwConfig', function () { const getJwConfig = utils.getJwConfig; From 528684b03a350d238c7236ac8f2f05a31b9bf3d9 Mon Sep 17 00:00:00 2001 From: karimJWP Date: Tue, 10 Aug 2021 15:42:10 -0400 Subject: [PATCH 12/17] introduces provider tests: --- .../submodules/jwplayerVideoProvider_spec.js | 113 +++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js index b4c88aa0020..f47eeee69f0 100644 --- a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js +++ b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js @@ -11,11 +11,122 @@ import { } from 'modules/videoModule/constants/ortb.js'; import { - PLAYBACK_MODE + PLAYBACK_MODE, SETUP_FAILED } 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 () { + // JWPlayerProvider(config, jwplayer_, adState_, timeState_, callbackStorage_, utils) + + describe('init', function () { + let config; + let jwplayerMock; + let adState; + let timeState; + let callbackStorage; + let utilsMock; + + beforeEach(() => { + config = {}; + jwplayerMock = getPlayerMock(); + 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; + }); + + it('should trigger failure when jwplayer version is under min supported version', function () { + + }); + + it('should instantiate the player when uninstantied', function () { + + }); + + it('should trigger setup complete when player is already instantied', function () { + + }); + }); + + 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 () { + + }); + + describe('setAdTagUrl', function () { + }); + + describe('events', function () { + + }); + + 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 () { From e88f19e05e7df749931013405e8023b189247489 Mon Sep 17 00:00:00 2001 From: karimJWP Date: Tue, 10 Aug 2021 16:29:58 -0400 Subject: [PATCH 13/17] tests init --- .../submodules/jwplayerVideoProvider_spec.js | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js index f47eeee69f0..f6e9c576a15 100644 --- a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js +++ b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js @@ -11,7 +11,7 @@ import { } from 'modules/videoModule/constants/ortb.js'; import { - PLAYBACK_MODE, SETUP_FAILED + PLAYBACK_MODE, SETUP_COMPLETE, SETUP_FAILED } from 'modules/videoModule/constants/events.js' function getPlayerMock() { @@ -60,7 +60,6 @@ describe('JWPlayerProvider', function () { describe('init', function () { let config; - let jwplayerMock; let adState; let timeState; let callbackStorage; @@ -68,7 +67,6 @@ describe('JWPlayerProvider', function () { beforeEach(() => { config = {}; - jwplayerMock = getPlayerMock(); adState = adStateFactory(); timeState = timeStateFactory(); callbackStorage = callbackStorageFactory(); @@ -81,18 +79,48 @@ describe('JWPlayerProvider', function () { 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; }); }); From 684b408690c2f08d91f598b0696b97cb64c77187 Mon Sep 17 00:00:00 2001 From: karimJWP Date: Tue, 10 Aug 2021 18:15:59 -0400 Subject: [PATCH 14/17] tests get ortb --- .../submodules/jwplayerVideoProvider.js | 8 +- .../submodules/jwplayerVideoProvider_spec.js | 115 +++++++++++++++++- 2 files changed, 119 insertions(+), 4 deletions(-) diff --git a/modules/videoModule/submodules/jwplayerVideoProvider.js b/modules/videoModule/submodules/jwplayerVideoProvider.js index 586010f8867..56e08b9e8bf 100644 --- a/modules/videoModule/submodules/jwplayerVideoProvider.js +++ b/modules/videoModule/submodules/jwplayerVideoProvider.js @@ -50,8 +50,7 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba if (player.getState() === undefined) { setupPlayer(playerConfig); } else { - const payload = getSetupCompletePayload(); - setupCompleteCallback && setupCompleteCallback(SETUP_COMPLETE, payload); + setupCompleteCallback && setupCompleteCallback(SETUP_COMPLETE, getSetupCompletePayload()); } } @@ -104,7 +103,7 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba // 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; + video.pos = 7; // TODO make constant in oRTB } const item = player.getPlaylistItem() || {}; // TODO does player call need optimization ? @@ -126,6 +125,9 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba } function setAdTagUrl(adTagUrl) { + if (!player) { + return; + } player.playAd(adTagUrl); } diff --git a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js index f6e9c576a15..f9cd9cb2b6d 100644 --- a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js +++ b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js @@ -132,11 +132,124 @@ describe('JWPlayerProvider', function () { }); 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 () { From 6a3472472a0157eb34dfb087970bae2e9f6fc673 Mon Sep 17 00:00:00 2001 From: karimJWP Date: Wed, 11 Aug 2021 10:54:48 -0400 Subject: [PATCH 15/17] tests event listeners --- .../submodules/jwplayerVideoProvider_spec.js | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js index f9cd9cb2b6d..5d6f4eeb18d 100644 --- a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js +++ b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js @@ -11,7 +11,7 @@ import { } from 'modules/videoModule/constants/ortb.js'; import { - PLAYBACK_MODE, SETUP_COMPLETE, SETUP_FAILED + PLAYBACK_MODE, SETUP_COMPLETE, SETUP_FAILED, PLAY, AD_IMPRESSION } from 'modules/videoModule/constants/events.js' function getPlayerMock() { @@ -56,8 +56,6 @@ function getUtilsMock() { } describe('JWPlayerProvider', function () { - // JWPlayerProvider(config, jwplayer_, adState_, timeState_, callbackStorage_, utils) - describe('init', function () { let config; let adState; @@ -253,7 +251,30 @@ describe('JWPlayerProvider', function () { }); 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 () { From af91e7efbee7ba8ccbd56fdd4e1921a7a02cc0ca Mon Sep 17 00:00:00 2001 From: karimJWP Date: Fri, 13 Aug 2021 15:58:06 -0400 Subject: [PATCH 16/17] supports removing all event handlers when explicit --- modules/videoModule/submodules/jwplayerVideoProvider.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/videoModule/submodules/jwplayerVideoProvider.js b/modules/videoModule/submodules/jwplayerVideoProvider.js index 56e08b9e8bf..a5db12025b4 100644 --- a/modules/videoModule/submodules/jwplayerVideoProvider.js +++ b/modules/videoModule/submodules/jwplayerVideoProvider.js @@ -154,7 +154,12 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba function offEvents(events, callback) { events.forEach(event => { - const eventHandler = callbackStorage.getCallback(event, callback); + const eventHandler = callback && callbackStorage.getCallback(event, callback); + if (!eventHandler && callback) { + // skip this iteration when event handler not found. + // support scenario where callback is undefined, as it means turn off all listeners. + return; + } const jwEvent = utils.getJwEvent(event); player.off(jwEvent, eventHandler); }); From ab889aedea14304b6703206f3c45147912e55ea7 Mon Sep 17 00:00:00 2001 From: karimJWP Date: Fri, 13 Aug 2021 16:01:35 -0400 Subject: [PATCH 17/17] splits clear all event listeners from early return --- .../videoModule/submodules/jwplayerVideoProvider.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/modules/videoModule/submodules/jwplayerVideoProvider.js b/modules/videoModule/submodules/jwplayerVideoProvider.js index a5db12025b4..7d74f1c65c3 100644 --- a/modules/videoModule/submodules/jwplayerVideoProvider.js +++ b/modules/videoModule/submodules/jwplayerVideoProvider.js @@ -154,13 +154,18 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba function offEvents(events, callback) { events.forEach(event => { - const eventHandler = callback && callbackStorage.getCallback(event, callback); - if (!eventHandler && callback) { + 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. - // support scenario where callback is undefined, as it means turn off all listeners. return; } - const jwEvent = utils.getJwEvent(event); + player.off(jwEvent, eventHandler); }); }