diff --git a/integrationExamples/videoModule/index.html b/integrationExamples/videoModule/index.html new file mode 100644 index 00000000000..3deb6689263 --- /dev/null +++ b/integrationExamples/videoModule/index.html @@ -0,0 +1,113 @@ + + + + + + + + + + + +

Prebid.js Test

+
Div-1
+
+
Div-2
+
+ + + + diff --git a/modules/.submodules.json b/modules/.submodules.json index 555e8adaefa..c4d279e771a 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -52,5 +52,9 @@ "fpdModule": [ "enrichmentFpdModule", "validationFpdModule" + ], + "videoModule": [ + "coreVideo", + "jwplayerVideoProvider" ] } diff --git a/modules/videoModule/submodules/jwplayerVideoProvider.js b/modules/jwplayerVideoProvider.js similarity index 98% rename from modules/videoModule/submodules/jwplayerVideoProvider.js rename to modules/jwplayerVideoProvider.js index 8350739d895..3bd47060107 100644 --- a/modules/videoModule/submodules/jwplayerVideoProvider.js +++ b/modules/jwplayerVideoProvider.js @@ -1,15 +1,15 @@ import { PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, PLACEMENT, VPAID_MIME_TYPE -} from '../constants/ortb.js'; +} from './videoModule/constants/ortb.js'; import { SETUP_COMPLETE, SETUP_FAILED, DESTROYED, AD_REQUEST, AD_BREAK_START, AD_LOADED, AD_STARTED, AD_IMPRESSION, AD_PLAY, AD_TIME, AD_PAUSE, AD_CLICK, AD_SKIPPED, AD_ERROR, AD_COMPLETE, AD_BREAK_END, PLAYLIST, PLAYBACK_REQUEST, AUTOSTART_BLOCKED, PLAY_ATTEMPT_FAILED, CONTENT_LOADED, PLAY, PAUSE, BUFFER, TIME, SEEK_START, SEEK_END, MUTE, VOLUME, RENDITION_UPDATE, ERROR, COMPLETE, PLAYLIST_COMPLETE, FULLSCREEN, PLAYER_RESIZE, VIEWABLE, CAST, PLAYBACK_MODE -} from '../constants/events.js'; -import stateFactory from '../shared/state.js'; -import { JWPLAYER_VENDOR } from '../constants/vendorCodes.js'; -import { vendorDirectory } from '../vendorDirectory'; +} from './videoModule/constants/events.js'; +import stateFactory from './videoModule/shared/state.js'; +import { JWPLAYER_VENDOR } from './videoModule/constants/vendorCodes.js'; +import { vendorDirectory } from './videoModule/vendorDirectory.js'; export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callbackStorage_, utils) { const jwplayer = jwplayer_; diff --git a/modules/videoModule/constants/events.js b/modules/videoModule/constants/events.js index fbf8444f4a9..c1a4e2ca7d5 100644 --- a/modules/videoModule/constants/events.js +++ b/modules/videoModule/constants/events.js @@ -43,6 +43,14 @@ export const PLAYER_RESIZE = 'playerResize'; export const VIEWABLE = 'viewable'; export const CAST = 'cast'; +export const allVideoEvents = [ + SETUP_COMPLETE, SETUP_FAILED, DESTROYED, AD_REQUEST, AD_BREAK_START, AD_LOADED, AD_STARTED, + AD_IMPRESSION, AD_PLAY, AD_TIME, AD_PAUSE, AD_CLICK, AD_SKIPPED, AD_ERROR, AD_COMPLETE, AD_BREAK_END, PLAYLIST, + PLAYBACK_REQUEST, AUTOSTART_BLOCKED, PLAY_ATTEMPT_FAILED, CONTENT_LOADED, PLAY, PAUSE, BUFFER, TIME, SEEK_START, + SEEK_END, MUTE, VOLUME, RENDITION_UPDATE, ERROR, COMPLETE, PLAYLIST_COMPLETE, FULLSCREEN, PLAYER_RESIZE, VIEWABLE, + CAST +]; + // Param options export const PLAYBACK_MODE = { VOD: 0, diff --git a/modules/videoModule/coreVideo.js b/modules/videoModule/coreVideo.js index 5288dfd9f52..36b0a90f4ff 100644 --- a/modules/videoModule/coreVideo.js +++ b/modules/videoModule/coreVideo.js @@ -1,17 +1,22 @@ -import { vendorDirectory } from './vendorDirectory'; +import { vendorDirectory } from './vendorDirectory.js'; export function VideoCore(submoduleBuilder_) { const submodules = {}; const submoduleBuilder = submoduleBuilder_; function registerProvider(providerConfig) { + const divId = providerConfig.divId; + if (submodules[divId]) { + return; + } + let submodule; try { submodule = submoduleBuilder.build(providerConfig); } catch (e) { throw e; } - submodules[providerConfig.divId] = submodule; + submodules[divId] = submodule; } function getOrtbParams(divId) { @@ -58,7 +63,7 @@ export function VideoSubmoduleBuilder(vendorDirectory_) { } const submodule = submoduleFactory(providerConfig); - submodule.init && submodule.init(); + submodule && submodule.init && submodule.init(); return submodule; } diff --git a/modules/videoModule/index.js b/modules/videoModule/index.js new file mode 100644 index 00000000000..774704a4c32 --- /dev/null +++ b/modules/videoModule/index.js @@ -0,0 +1,57 @@ +import {config} from '../../src/config.js'; +import events from '../../src/events.js'; +import { allVideoEvents } from './constants/events.js'; +import CONSTANTS from '../../src/constants.json'; +import { videoCoreFactory } from './coreVideo.js'; + +events.addEvents(allVideoEvents); + +export function PbVideo(videoCore_, getConfig_, pbGlobal_, pbEvents_, videoEvents_) { + const videoCore = videoCore_; + const getConfig = getConfig_; + const pbGlobal = pbGlobal_; + const requestBids = pbGlobal.requestBids; + const pbEvents = pbEvents_; + const videoEvents = videoEvents_; + + function init() { + getConfig('video', ({ video }) => { + video.providers.forEach(provider => { + try { + videoCore.registerProvider(provider); + videoCore.onEvents(videoEvents, (type, payload) => { + pbEvents.emit(type, payload); + }, provider.divId); + } catch (e) {} + }); + }); + + requestBids.before(enrichAdUnits, 40); + + pbEvents.on(CONSTANTS.EVENTS.AUCTION_END, function(auctionResult) { + // TODO: requires AdServer Module. + // get ad tag from adServer - auctionResult.winningBids + // coreVideo.setAdTagUrl(adTag, divId); + }); + } + + function enrichAdUnits(nextFn, bidRequest) { + const adUnits = bidRequest.adUnits || pbGlobal.adUnits || []; + adUnits.forEach(adUnit => { + const oRtbParams = videoCore.getOrtbParams(adUnit.video.divId); + adUnit.mediaTypes.video = Object.assign({}, adUnit.mediaTypes.video, oRtbParams); + }); + return nextFn.call(this, bidRequest); + } + + return { init }; +} + +function pbVideoFactory() { + const videoCore = videoCoreFactory(); + const pbVideo = PbVideo(videoCore, config.getConfig, $$PREBID_GLOBAL$$, events, allVideoEvents); + pbVideo.init(); + return pbVideo; +} + +pbVideoFactory(); diff --git a/src/events.js b/src/events.js index 8749ddf206b..e52ac18b383 100644 --- a/src/events.js +++ b/src/events.js @@ -133,6 +133,10 @@ module.exports = (function () { return _handlers; }; + _public.addEvents = function (events) { + allEvents.concat(events); + } + /** * This method can return a copy of all the events fired * @return {Array} array of events fired diff --git a/test/spec/modules/videoModule/pbVideo_spec.js b/test/spec/modules/videoModule/pbVideo_spec.js new file mode 100644 index 00000000000..10d4c5e494e --- /dev/null +++ b/test/spec/modules/videoModule/pbVideo_spec.js @@ -0,0 +1,127 @@ +import { expect } from 'chai'; +import { PbVideo } from 'modules/videoModule/index.js'; + +let ortbParamsMock; +let videoCoreMock; +let getConfigMock; +let requestBidsMock; +let pbGlobalMock; +let pbEventsMock; +let videoEventsMock; + +function resetTestVars() { + ortbParamsMock = { + 'video': {}, + 'content': {} + } + videoCoreMock = { + registerProvider: sinon.spy(), + onEvents: sinon.spy(), + getOrtbParams: () => ortbParamsMock + }; + getConfigMock = () => {}; + requestBidsMock = { + before: sinon.spy() + }; + pbGlobalMock = { + requestBids: requestBidsMock + }; + pbEventsMock = { + emit: sinon.spy(), + on: sinon.spy() + }; + videoEventsMock = []; +} + +let pbVideoFactory = (videoCore, getConfig, pbGlobal, pbEvents, videoEvents) => { + const pbVideo = PbVideo( + videoCore || videoCoreMock, + getConfig || getConfigMock, + pbGlobal || pbGlobalMock, + pbEvents || pbEventsMock, + videoEvents || videoEventsMock + ); + pbVideo.init(); + return pbVideo; +} + +describe('Prebid Video', function () { + beforeEach(() => resetTestVars()); + + describe('Setting video to config', function () { + let providers = [{ divId: 'div1' }, { divId: 'div2' }]; + let getConfigCallback; + let getConfig = (video, callback) => { + getConfigCallback = callback; + }; + + beforeEach(() => { + pbVideoFactory(null, getConfig); + getConfigCallback({ video: { providers } }); + }); + + it('Should register providers', function () { + expect(videoCoreMock.registerProvider.calledTwice).to.be.true; + }); + + it('Should register events', function () { + expect(videoCoreMock.onEvents.calledTwice).to.be.true; + const onEventsSpy = videoCoreMock.onEvents; + expect(onEventsSpy.getCall(0).args[2]).to.be.equal('div1'); + expect(onEventsSpy.getCall(1).args[2]).to.be.equal('div2'); + }); + + describe('Event triggering', function () { + it('Should emit events off of Prebid\'s Events', function () { + let eventHandler; + const videoCore = Object.assign({}, videoCoreMock, { + onEvents: (events, eventHandler_) => eventHandler = eventHandler_ + }); + pbVideoFactory(videoCore, getConfig); + getConfigCallback({ video: { providers } }); + const expectedType = 'test_event'; + const expectedPayload = {'test': 'data'}; + eventHandler(expectedType, expectedPayload); + expect(pbEventsMock.emit.calledOnce).to.be.true; + expect(pbEventsMock.emit.getCall(0).args[0]).to.be.equal(expectedType); + expect(pbEventsMock.emit.getCall(0).args[1]).to.be.equal(expectedPayload); + }); + }); + }); + + describe('Ad unit Enrichment', function () { + it('registers before:bidRequest hook', function () { + const pbVideo = pbVideoFactory(); + expect(requestBidsMock.before.calledOnce).to.be.true; + }); + + it('requests oRtb params and writes them to ad unit', function() { + const getOrtbParamsSpy = sinon.spy(videoCoreMock, 'getOrtbParams'); + let beforeBidRequestCallback; + const requestBids = { + before: callback_ => beforeBidRequestCallback = callback_ + }; + + const pbVideo = pbVideoFactory(null, null, { requestBids }); + expect(beforeBidRequestCallback).to.not.be.undefined; + const nextFn = sinon.spy(); + const adUnits = [{ + code: 'ad1', + mediaTypes: { + video: {} + }, + video: { divId: 'divId' } + }]; + beforeBidRequestCallback(nextFn, { adUnits }); + expect(getOrtbParamsSpy.calledOnce).to.be.true; + const adUnit = adUnits[0]; + expect(adUnit.mediaTypes.video).to.have.property('video'); + expect(adUnit.mediaTypes.video).to.have.property('content'); + expect(nextFn.calledOnce).to.be.true; + }); + }); + + describe('Ad tag injection', function () { + // TODO: requires adServer to be implemented + }); +}); diff --git a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js index 5d6f4eeb18d..16b143cf706 100644 --- a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js +++ b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js @@ -4,7 +4,7 @@ import { timeStateFactory, callbackStorageFactory, utils -} from 'modules/videoModule/submodules/jwplayerVideoProvider'; +} from 'modules/jwplayerVideoProvider'; import { PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, PLACEMENT, VPAID_MIME_TYPE