UNPKG

ima-ad-player

Version:

Yet another Google IMA video ad player.

533 lines (435 loc) 17.5 kB
// ima-player.js import {makeNum, makeDefault} from './utils' import Observable from './observable' /* global google */ export default class ImaPlayer { constructor(options) { this._configure(options) this._evt = new Observable() this._adRequesting = false this._adRequested = false // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.ImaSdkSettings#setVpaidMode this._o.vpaidMode && google.ima.settings.setVpaidMode(this._resolvedVpaidMode) // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.ImaSdkSettings#setLocale this._o.locale && google.ima.settings.setLocale(this._o.locale) // Assumes the display container and video element are correctly positioned and sized // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side#html this._adDisplayContainer = new google.ima.AdDisplayContainer(this._o.displayContainer, this._o.video, this._o.clickTracking) this._adDisplayContainerInit = false } _configure(o) { this._o = { displayContainer: o.displayContainer, video: o.video, tag: o.tag, } // VPAID mode will be ima SDK default (if not set) if (o.vpaidMode) { this._o.vpaidMode = makeNum(o.vpaidMode, undefined) } if (o.maxDuration) { this._o.maxDuration = makeNum(o.maxDuration, undefined) } // Default is undefined this._o.locale = o.locale // Default is undefined or alternative video ad click element // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdDisplayContainer this._o.clickTracking = o.clickTracking // Default is undefined or object this._o.adsRequestOptions = o.adsRequestOptions // Default is undefined or object this._o.adsRenderingOptions = o.adsRenderingOptions // Default is to let IMA SDK handle non-linear display duration this._o.nonLinearMaxDuration = makeNum(o.nonLinearMaxDuration, -1) // Assumes by default that the playback is consented by user this._o.adWillAutoPlay = !!makeDefault(o.adWillAutoPlay, true) this._o.adWillPlayMuted = !!makeDefault(o.adWillPlayMuted, false) // Default is undefined this._o.continuousPlayback = o.continuousPlayback // Default is to tell the SDK NOT to save and restore content video state this._o.restoreVideo = !!makeDefault(o.restoreVideo, false) } _setProperties(target, properties) { for (let prop in properties) { if (typeof target[prop] !== 'undefined') { target[prop] = properties[prop] } } } static get vpaidMode() { return { DISABLED: 0, ENABLED: 1, INSECURE: 2 } } get _resolvedVpaidMode() { if (this._o.vpaidMode === ImaPlayer.vpaidMode.DISABLED) { return google.ima.ImaSdkSettings.VpaidMode.DISABLED } if (this._o.vpaidMode === ImaPlayer.vpaidMode.INSECURE) { return google.ima.ImaSdkSettings.VpaidMode.INSECURE } // Default to SECURE mode return google.ima.ImaSdkSettings.VpaidMode.ENABLED } on(name, cb) { this._evt.subscribe(name, cb) } off(name, cb = null) { if (cb === null) { this._evt.unsubscribeAll(name) } else { this._evt.unsubscribe(name, cb) } } play() { this._dispatch('ad_play_intent') this._adPlayIntent = true this.initAdDisplayContainer() this._requestAd() } request(options) { this._dispatch('ad_request_intent', options) this._requestAd(options) } resize(width, height, viewMode = null) { if (this._adsManager) { // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsManager#resize viewMode || (viewMode = google.ima.ViewMode.NORMAL) this._adsManager.resize(width, height, viewMode) } } setVolume(volume) { // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsManager#setVolume this._adsManager && this._adsManager.setVolume(volume) } getVolume() { // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsManager#getVolume return this._adsManager ? this._adsManager.getVolume() : null } discardAdBreak() { // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsManager#discardAdBreak this._adsManager && this._adsManager.discardAdBreak() } pause() { // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsManager#pause this._adsManager && this._adsManager.pause() } resume() { // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsManager#resume this._adsManager && this._adsManager.resume() } skip() { // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsManager#skip this._adsManager && this._adsManager.skip() } updateAdsRenderingSettings(adsRenderingSettings) { // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsManager#updateAdsRenderingSettings this._adsManager && this._adsManager.updateAdsRenderingSettings(adsRenderingSettings) } configureAdsManager(content, adsRenderingSettings) { // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsManager#configureAdsManager this._adsManager && this._adsManager.configureAdsManager(content, adsRenderingSettings) } focus() { // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsManager#focus this._adsManager && this._adsManager.focus() } getDisplayContainer() { return this._o.displayContainer } getCuePoints() { // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsManager#getCuePoints return this._adsManager ? this._adsManager.getCuePoints() : null } getAdSkippableState() { // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsManager#getAdSkippableState return this._adsManager ? this._adsManager.getAdSkippableState() : null } getRemainingTime() { // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsManager#getRemainingTime return this._adsManager ? this._adsManager.getRemainingTime() : null } isCustomClickTrackingUsed() { // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsManager#isCustomClickTrackingUsed return this._adsManager ? this._adsManager.isCustomClickTrackingUsed() : null } isCustomPlaybackUsed() { // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsManager#isCustomPlaybackUsed return this._adsManager ? this._adsManager.isCustomPlaybackUsed() : null } setAdWillAutoPlay(autoPlay) { this._o.adWillAutoPlay = autoPlay } setAdWillPlayMuted(muted) { this._o.adWillPlayMuted = muted } setContinuousPlayback(continuousPlayback) { this._o.continuousPlayback = continuousPlayback } stop() { this._dispatch('ad_stop_intent') this._stop() } ended() { // Signals the video content is finished. // This will allow to play post-roll ads (if any) this._adsLoader && this._adsLoader.contentComplete() } initAdDisplayContainer() { // Must be done via a user interaction if (! this._adDisplayContainerInit) { this._adDisplayContainer.initialize() this._adDisplayContainerInit = true } } destroy(unsubscribeEvents = true) { this._adsManager && this._adsManager.stop() this._endAd() unsubscribeEvents && this._evt.unsubscribeAll() this._destroyAdsManager() this._destroyAdsLoader() this._destroyAdDisplayContainer() this._destroyOptions() } _destroyAdsLoader() { if (this._adsLoader) { this._adsLoader.destroy() this._adsLoader = null delete this._adsLoader } } _destroyAdsManager() { if (this._adsManager) { this._adsManager.destroy() this._adsManager = null delete this._adsManager } } _destroyAdDisplayContainer() { if (this._adDisplayContainer) { this._adDisplayContainer.destroy() this._adDisplayContainer = null delete this._adDisplayContainer } } _destroyOptions() { this._o = null delete this._o } _stop() { this._dispatch('ad_stop') if (this._adsManager) { // Signal ads manager to stop and get back to content this._adsManager.stop() } else { this._endAd() } } _makeAdsLoader() { this._adsLoader = new google.ima.AdsLoader(this._adDisplayContainer) this._adsLoader.addEventListener( google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, (e) => { this._onAdsManagerLoaded(e) } ) this._adsLoader.addEventListener( google.ima.AdErrorEvent.Type.AD_ERROR, (e) => { this._adRequested = false this._onAdError(e) } ) } _requestAd(options) { // Check if ad request is pending if (this._adRequesting) { // Ad will autostart if play method called return } // Check if ad already requested (pre-request) if (this._adRequested) { // Start ad only if play method called if (this._adPlayIntent) { this._playAd() } return } this._adRequesting = true if (! this._adsLoader) { this._makeAdsLoader() } let adsRequest = new google.ima.AdsRequest() // Set ad request default settings adsRequest.adTagUrl = this._o.tag adsRequest.linearAdSlotWidth = this._o.video.offsetWidth adsRequest.linearAdSlotHeight = this._o.video.offsetHeight adsRequest.nonLinearAdSlotWidth = this._o.video.offsetWidth adsRequest.nonLinearAdSlotHeight = this._o.video.offsetHeight adsRequest.setAdWillAutoPlay(this._o.adWillAutoPlay) adsRequest.setAdWillPlayMuted(this._o.adWillPlayMuted) if (this._o.continuousPlayback !== undefined) { // Internally set AdsRequest.videoContinuousPlay to "0" if undefined, "1" if false, "2" if true adsRequest.setContinuousPlayback(this._o.continuousPlayback) } // Assumes that ad request options is an object with ads request properties // defined in the IMA SDK documentation (will override default settings) // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsRequest let adsRequestOptions = options ? options : this._o.adsRequestOptions if (adsRequestOptions) { this._setProperties(adsRequest, adsRequestOptions) } this._dispatch('ad_request', adsRequest) // The requestAds() method triggers _onAdsManagerLoaded() or _onAdError() this._adsLoader.requestAds(adsRequest) } _bindAdsManagerEvents() { this._adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (e) => { this._onAdError(e) }) this._adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, (e) => { this._adEnded = false this._dispatch('content_pause_requested', e) this._dispatch('ad_begin') // "content_pause_requested" event alias }) this._adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, (e) => { this._dispatch('content_resume_requested', e) this._endAd() }) this._adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, (e) => { this._dispatch('started', e) let ad = e.getAd() if (ad.isLinear()) { this._o.maxDuration && this._startMaxDurationTimer() } else { // Signal non-linear ad scenario let duration = this._o.nonLinearMaxDuration this._dispatch('ad_non_linear', {ad, duration}) // By default, IMA SDK will automatically close non-linear ad (after 45 seconds ?) if (this._o.nonLinearMaxDuration > 0) { setTimeout(() => { this._adsManager && this._adsManager.stop() }, this._o.nonLinearMaxDuration) } // Ends to play/resume content video this._endAd() } }) this._adsManager.addEventListener(google.ima.AdEvent.Type.ALL_ADS_COMPLETED, (e) => { this._adRequested = false this._dispatch('all_ads_completed', e) }) let adEvents = { 'ad_break_ready': google.ima.AdEvent.Type.AD_BREAK_READY, 'ad_buffering': google.ima.AdEvent.Type.AD_BUFFERING, 'ad_metadata': google.ima.AdEvent.Type.AD_METADATA, 'ad_progress': google.ima.AdEvent.Type.AD_PROGRESS, 'click': google.ima.AdEvent.Type.CLICK, 'complete': google.ima.AdEvent.Type.COMPLETE, 'duration_change': google.ima.AdEvent.Type.DURATION_CHANGE, 'first_quartile': google.ima.AdEvent.Type.FIRST_QUARTILE, 'impression': google.ima.AdEvent.Type.IMPRESSION, 'interaction': google.ima.AdEvent.Type.INTERACTION, 'linear_changed': google.ima.AdEvent.Type.LINEAR_CHANGED, 'loaded': google.ima.AdEvent.Type.LOADED, 'log': google.ima.AdEvent.Type.LOG, 'midpoint': google.ima.AdEvent.Type.MIDPOINT, 'paused': google.ima.AdEvent.Type.PAUSED, 'resumed': google.ima.AdEvent.Type.RESUMED, 'skippable_state_changed': google.ima.AdEvent.Type.SKIPPABLE_STATE_CHANGED, 'skipped': google.ima.AdEvent.Type.SKIPPED, 'third_quartile': google.ima.AdEvent.Type.THIRD_QUARTILE, 'user_close': google.ima.AdEvent.Type.USER_CLOSE, 'video_clicked': google.ima.AdEvent.Type.VIDEO_CLICKED, 'video_icon_clicked': google.ima.AdEvent.Type.VIDEO_ICON_CLICKED, 'volume_changed': google.ima.AdEvent.Type.VOLUME_CHANGED, 'volume_muted': google.ima.AdEvent.Type.VOLUME_MUTED, } // Not documented, may be unavailable in the future google.ima.AdEvent.Type.AD_CAN_PLAY && (adEvents.ad_can_play = google.ima.AdEvent.Type.AD_CAN_PLAY) google.ima.AdEvent.Type.VIEWABLE_IMPRESSION && (adEvents.viewable_impression = google.ima.AdEvent.Type.VIEWABLE_IMPRESSION) for (let adEvent in adEvents) { this._adsManager.addEventListener(adEvents[adEvent], (e) => { this._dispatch(adEvent, e) }) } } _onAdsManagerLoaded(adsManagerLoadedEvent) { this._dispatch('ads_manager_loaded', adsManagerLoadedEvent) // Create default ads rendering settings let adsRenderingSettings = new google.ima.AdsRenderingSettings() adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = this._o.restoreVideo // Assumes that ads rendering options is an object with ads rendering settings properties // defined in the IMA SDK documentation (will override default settings) // https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdsRenderingSettings if (this._o.adsRenderingOptions) { this._setProperties(adsRenderingSettings, this._o.adsRenderingOptions) } this._dispatch('ads_rendering_settings', adsRenderingSettings) this._destroyAdsManager() this._adsManager = adsManagerLoadedEvent.getAdsManager(this._o.video, adsRenderingSettings) this._bindAdsManagerEvents() this._dispatch('ads_manager', this._adsManager) // Ad is ready to be played this._adRequesting = false this._adRequested = true if (this._adPlayIntent) { this._playAd() } } _onMaxDuration() { this._dispatch('error', new Error('Maximum duration of ' + this._o.maxDuration + ' ms reached')) this._stop() } _startMaxDurationTimer() { this._maxDurationTimer = setTimeout(() => { this._onMaxDuration() }, this._o.maxDuration) } _resetMaxDurationTimer() { if (typeof this._maxDurationTimer === 'number') { clearTimeout(this._maxDurationTimer) this._maxDurationTimer = undefined } } _onAdError(adErrorEvent) { // google.ima.AdErrorEvent : https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdErrorEvent // google.ima.AdError : https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdError // console.log('onAdError: ' + adErrorEvent.getError()) this._dispatch('ad_error', adErrorEvent) this._endAd() } _playAd() { try { this._dispatch('ad_play') this._adEnded = false this._adsManager.init( this._o.video.offsetWidth, this._o.video.offsetHeight, google.ima.ViewMode.NORMAL ) this._adsManager.start() } catch (e) { // console.log('adsManager catched error', e) this._dispatch('error', e) this._endAd() } } _dispatch(name, e) { this._evt.notify(name, { name: name, data: e, target: this, }) } _endAd() { if (this._adEnded) { return } this._adEnded = true this._adPlayIntent = false this._adRequesting = false this._resetMaxDurationTimer() this._dispatch('ad_end') } }