prebid/Prebid.js

View on GitHub
modules/jwplayerVideoProvider.js

Summary

Maintainability
F
1 wk
Test Coverage
import {
  PROTOCOLS, API_FRAMEWORKS, VIDEO_MIME_TYPE, PLAYBACK_METHODS, PLACEMENT, VPAID_MIME_TYPE, AD_POSITION
} from '../libraries/video/constants/ortb.js';
import {
  SETUP_COMPLETE, SETUP_FAILED, DESTROYED, AD_REQUEST, AD_BREAK_START, AD_LOADED, AD_STARTED, AD_IMPRESSION, AD_PLAY,
  AD_TIME, AD_PAUSE, AD_CLICK, AD_SKIPPED, AD_ERROR, AD_COMPLETE, AD_BREAK_END, PLAYLIST, PLAYBACK_REQUEST,
  AUTOSTART_BLOCKED, PLAY_ATTEMPT_FAILED, CONTENT_LOADED, PLAY, PAUSE, BUFFER, TIME, SEEK_START, SEEK_END, MUTE, VOLUME,
  RENDITION_UPDATE, ERROR, COMPLETE, PLAYLIST_COMPLETE, FULLSCREEN, PLAYER_RESIZE, VIEWABLE, CAST
} from '../libraries/video/constants/events.js';
import { PLAYBACK_MODE } from '../libraries/video/constants/constants.js';
import stateFactory from '../libraries/video/shared/state.js';
import { JWPLAYER_VENDOR } from '../libraries/video/constants/vendorCodes.js';
import { getEventHandler } from '../libraries/video/shared/eventHandler.js';
import { submodule } from '../src/hook.js';

/**
 * @constructor
 * @param {videoProviderConfig} config
 * @param {Object} jwplayer_ - JW Player global factory
 * @param {State} adState_
 * @param {State} timeState_
 * @param {CallbackStorage} callbackStorage_
 * @param {Object} utils
 * @returns {VideoProvider}
 */
export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callbackStorage_, utils, sharedUtils) {
  const jwplayer = jwplayer_;
  let player = null;
  let playerVersion = null;
  const playerConfig = config.playerConfig;
  const divId = config.divId;
  let adState = adState_;
  let timeState = timeState_;
  let callbackStorage = callbackStorage_;
  let pendingSeek = {};
  let supportedMediaTypes = null;
  let minimumSupportedPlayerVersion = '8.20.1';
  let setupCompleteCallbacks = [];
  let setupFailedCallbacks = [];
  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({ code: -1 }); // TODO: come up with error code schema- player is absent
      return;
    }

    playerVersion = jwplayer.version;

    if (playerVersion < minimumSupportedPlayerVersion) {
      triggerSetupFailure({ code: -2 }); // TODO: come up with error code schema - version not supported
      return;
    }

    if (!document.getElementById(divId)) {
      triggerSetupFailure({ code: -3 }); // TODO: come up with error code schema - missing div id
      return;
    }

    player = jwplayer(divId);
    if (!player || !player.getState) {
      triggerSetupFailure({ code: -4 }); // TODO: come up with error code schema - factory function failure
    } else if (player.getState() === undefined) {
      setupPlayer(playerConfig);
    } else {
      triggerSetupComplete();
    }
  }

  function getId() {
    return divId;
  }

  function getOrtbVideo() {
    if (!player) {
      return;
    }

    const config = player.getConfig() || {};
    const adConfig = config.advertising || {};
    supportedMediaTypes = supportedMediaTypes || utils.getSupportedMediaTypes(MEDIA_TYPES);

    const video = {
      mimes: supportedMediaTypes,
      protocols: [
        PROTOCOLS.VAST_2_0,
        PROTOCOLS.VAST_3_0,
        PROTOCOLS.VAST_4_0,
        PROTOCOLS.VAST_2_0_WRAPPER,
        PROTOCOLS.VAST_3_0_WRAPPER,
        PROTOCOLS.VAST_4_0_WRAPPER
      ],
      h: player.getHeight(), // TODO does player call need optimization ?
      w: player.getWidth(), // TODO does player call need optimization ?
      startdelay: utils.getStartDelay(),
      placement: utils.getPlacement(adConfig, player),
      // linearity is omitted because both forms are supported.
      // sequence - TODO not yet supported
      battr: adConfig.battr,
      maxextended: -1, // extension is allowed, and there is no time limit imposed.
      boxingallowed: 1,
      playbackmethod: [ utils.getPlaybackMethod(config) ],
      playbackend: 1, // TODO: need to account for floating player - https://developer.jwplayer.com/jwplayer/docs/jw8-embed-an-outstream-player , https://developer.jwplayer.com/jwplayer/docs/jw8-player-configuration-reference#section-float-on-scroll
      // companionad - TODO add in future version
      // companiontype - TODO add in future version
      // minbitrate - TODO add in future version
      // maxbitrate - TODO add in future version
      // delivery - omitted because all are supported.
      // minduration - Is there value to specifying ?
      // maxduration - Is there value to specifying ?
      api: [
        API_FRAMEWORKS.VPAID_2_0
      ],
    };

    if (utils.isOmidSupported(adConfig.adClient)) {
      video.api.push(API_FRAMEWORKS.OMID_1_0);
    }

    Object.assign(video, utils.getSkipParams(adConfig));

    if (player.getFullscreen()) { // TODO does player call need optimization ?
      // only specify ad position when in Fullscreen since computational cost is low
      // ad position options are listed in oRTB 2.5 section 5.4
      // https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf
      video.pos = AD_POSITION.FULL_SCREEN; // TODO make constant in oRTB
    }

    return video;
  }

  function getOrtbContent() {
    if (!player) {
      return;
    }

    const item = player.getPlaylistItem() || {}; // TODO does player call need optimization ?
    let { duration, playbackMode } = timeState.getState();
    if (duration === undefined) {
      duration = player.getDuration();
    }

    const content = {
      url: item.file,
      title: item.title,
      cat: item.iabCategories,
      keywords: item.tags,
      len: duration,
      embeddable: 1
    };

    if (playbackMode !== undefined) {
      content.livestream = Math.min(playbackMode, 1);
    }

    const mediaId = item.mediaid;
    if (mediaId) {
      content.id = 'jw_' + mediaId;
    }

    const jwpseg = item.jwpseg;
    const dataSegment = utils.getSegments(jwpseg);
    const contentDatum = utils.getContentDatum(mediaId, dataSegment);
    if (contentDatum) {
      content.data = [contentDatum];
    }

    const isoLanguageCode = utils.getIsoLanguageCode(player);
    if (isoLanguageCode) {
      content.language = isoLanguageCode;
    }

    return content;
  }

  function setAdTagUrl(adTagUrl, options) {
    if (!player) {
      return;
    }

    player.playAd(adTagUrl || options.adXml, options);
  }

  function onEvent(externalEventName, callback, basePayload) {
    if (externalEventName === SETUP_COMPLETE) {
      setupCompleteCallbacks.push(callback);
      return;
    }

    if (externalEventName === SETUP_FAILED) {
      setupFailedCallbacks.push(callback);
      return;
    }

    if (!player) {
      return;
    }

    let getEventPayload;

    switch (externalEventName) {
      case AD_REQUEST:
      case AD_PLAY:
      case AD_PAUSE:
        getEventPayload = e => ({ adTagUrl: e.tag });
        break;

      case AD_BREAK_START:
        getEventPayload = e => {
          timeState.clearState();
          return { offset: e.adPosition };
        };
        break;

      case AD_LOADED:
        getEventPayload = e => {
          adState.updateForEvent(e);
          const adConfig = player.getConfig().advertising;
          adState.updateState(utils.getSkipParams(adConfig));
          return adState.getState();
        };
        break;

      case AD_STARTED:
        // JW Player adImpression fires when the ad starts, regardless of viewability.
        getEventPayload = () => adState.getState();
        break;

      case AD_IMPRESSION:
      case AD_CLICK:
        getEventPayload = () => Object.assign({}, adState.getState(), timeState.getState());
        break;

      case AD_TIME:
        getEventPayload = e => {
          timeState.updateForEvent(e);
          return {
            adTagUrl: e.tag,
            time: e.position,
            duration: e.duration,
          };
        };
        break;

      case AD_SKIPPED:
        getEventPayload = e => {
          adState.clearState();
          return {
            time: e.position,
            duration: e.duration,
          };
        };
        break;

      case AD_ERROR:
        getEventPayload = e => {
          const extraPayload = Object.assign({
            playerErrorCode: e.adErrorCode,
            vastErrorCode: e.code,
            errorMessage: e.message,
            sourceError: e.sourceError
            // timeout
          }, adState.getState(), timeState.getState());
          adState.clearState();
          return extraPayload;
        };
        break;

      case AD_COMPLETE:
        getEventPayload = e => {
          adState.clearState();
          return { adTagUrl: e.tag };
        };
        break;

      case AD_BREAK_END:
        getEventPayload = e => ({ offset: e.adPosition });
        break;

      case PLAYLIST:
        getEventPayload = e => {
          const playlistItemCount = e.playlist.length;
          return {
            playlistItemCount,
            autostart: player.getConfig().autostart
          };
        };
        break;

      case PLAYBACK_REQUEST:
        getEventPayload = e => ({ playReason: e.playReason });
        break;

      case AUTOSTART_BLOCKED:
        getEventPayload = e => ({
          sourceError: e.error,
          errorCode: e.code,
          errorMessage: e.message
        });
        break;

      case PLAY_ATTEMPT_FAILED:
        getEventPayload = e => ({
          playReason: e.playReason,
          sourceError: e.sourceError,
          errorCode: e.code,
          errorMessage: e.message
        });
        break;

      case CONTENT_LOADED:
        getEventPayload = e => {
          const { item, index } = e;
          return {
            contentId: item.mediaid,
            contentUrl: item.file, // cover other sources ? util ?
            title: item.title,
            description: item.description,
            playlistIndex: index,
            contentTags: item.tags
          };
        };
        break;

      case BUFFER:
        getEventPayload = () => timeState.getState();
        break;

      case TIME:
        getEventPayload = e => {
          timeState.updateForEvent(e);
          return {
            position: e.position,
            duration: e.duration
          };
        };
        break;

      case SEEK_START:
        getEventPayload = e => {
          const duration = e.duration;
          const offset = e.offset;
          pendingSeek = {
            duration,
            offset
          };
          return {
            position: e.position,
            destination: offset,
            duration: duration
          };
        }
        break;

      case SEEK_END:
        getEventPayload = () => {
          const extraPayload = {
            position: pendingSeek.offset,
            duration: pendingSeek.duration
          };
          pendingSeek = {};
          return extraPayload;
        };
        break;

      case MUTE:
        getEventPayload = e => ({ mute: e.mute });
        break;

      case VOLUME:
        getEventPayload = e => ({ volumePercentage: e.volume });
        break;

      case RENDITION_UPDATE:
        getEventPayload = e => {
          const bitrate = e.bitrate;
          const level = e.level;
          return {
            videoReportedBitrate: bitrate,
            audioReportedBitrate: bitrate,
            encodedVideoWidth: level.width,
            encodedVideoHeight: level.height,
            videoFramerate: e.frameRate
          };
        };
        break;

      case ERROR:
        getEventPayload = e => ({
          sourceError: e.sourceError,
          errorCode: e.code,
          errorMessage: e.message,
        });
        break;

      case COMPLETE:
        getEventPayload = e => timeState.clearState();
        break;

      case FULLSCREEN:
        getEventPayload = e => ({ fullscreen: e.fullscreen });
        break;

      case PLAYER_RESIZE:
        getEventPayload = e => ({
          height: e.height,
          width: e.width,
        });
        break;

      case VIEWABLE:
        getEventPayload = e => ({
          viewable: e.viewable,
          viewabilityPercentage: player.getPercentViewable() * 100,
        });
        break;

      case CAST:
        getEventPayload = e => ({ casting: e.active });
        break;

      case PLAY:
      case PAUSE:
      case PLAYLIST_COMPLETE:
      case DESTROYED:
        break;

      default:
        return;
    }

    const jwEventName = utils.getJwEvent(externalEventName);
    const eventHandler = getEventHandler(externalEventName, callback, basePayload, getEventPayload)
    player.on(jwEventName, eventHandler);
    callbackStorage.storeCallback(externalEventName, eventHandler, callback);
  }

  function offEvent(event, callback) {
    const jwEvent = utils.getJwEvent(event);
    if (!callback) {
      player.off(jwEvent);
      return;
    }

    const eventHandler = callbackStorage.getCallback(event, callback);
    if (!eventHandler) {
      return;
    }

    player.off(jwEvent, eventHandler);
  }

  function destroy() {
    if (!player) {
      return;
    }
    player.remove();
    player = null;
  }

  return {
    init,
    getId,
    getOrtbVideo,
    getOrtbContent,
    setAdTagUrl,
    onEvent,
    offEvent,
    destroy
  };

  function setupPlayer(config) {
    if (!config) {
      return;
    }
    player.setup(utils.getJwConfig(config)).on('ready', triggerSetupComplete).on('setupError', triggerSetupFailure);
  }

  function triggerSetupComplete() {
    if (!setupCompleteCallbacks.length) {
      return;
    }

    const payload = getSetupCompletePayload();
    setupCompleteCallbacks.forEach(callback => callback(SETUP_COMPLETE, payload));
    setupCompleteCallbacks = [];
  }

  function getSetupCompletePayload() {
    return {
      divId,
      playerVersion,
      type: SETUP_COMPLETE,
      viewable: player.getViewable(),
      viewabilityPercentage: player.getPercentViewable() * 100,
      mute: player.getMute(),
      volumePercentage: player.getVolume()
    };
  }

  function triggerSetupFailure(e) {
    if (!setupFailedCallbacks.length) {
      return;
    }

    const payload = {
      divId,
      playerVersion,
      type: SETUP_FAILED,
      errorCode: e.code,
      errorMessage: e.message,
      sourceError: e.sourceError
    };

    setupFailedCallbacks.forEach(callback => callback(SETUP_FAILED, payload));
    setupFailedCallbacks = [];
  }
}

/**
 * @param {videoProviderConfig} config
 * @param {sharedUtils} sharedUtils
 * @returns {VideoProvider}
 */
const jwplayerSubmoduleFactory = function (config, sharedUtils) {
  const adState = adStateFactory();
  const timeState = timeStateFactory();
  const callbackStorage = callbackStorageFactory();
  return JWPlayerProvider(config, window.jwplayer, adState, timeState, callbackStorage, utils, sharedUtils);
}

jwplayerSubmoduleFactory.vendorCode = JWPLAYER_VENDOR;
submodule('video', jwplayerSubmoduleFactory);
export default jwplayerSubmoduleFactory;

// HELPERS

export const utils = {
  getJwConfig: function(config) {
    if (!config) {
      return;
    }

    const params = config.params || {};
    const jwConfig = params.vendorConfig || {};
    if (jwConfig.autostart === undefined && config.autoStart !== undefined) {
      jwConfig.autostart = config.autoStart;
    }

    if (jwConfig.mute === undefined && config.mute !== undefined) {
      jwConfig.mute = config.mute;
    }

    if (!jwConfig.key && config.licenseKey !== undefined) {
      jwConfig.key = config.licenseKey;
    }

    if (config.setupAds === false) {
      return jwConfig;
    }

    const advertising = jwConfig.advertising || { client: 'vast' };
    if (!jwConfig.file && !jwConfig.playlist && !jwConfig.source) {
      // TODO verify accuracy
      advertising.outstream = true;
    }

    const bids = advertising.bids || {};
    bids.prebid = true;
    advertising.bids = bids;

    jwConfig.advertising = advertising;
    return jwConfig;
  },

  getJwEvent: function(eventName) {
    switch (eventName) {
      case SETUP_COMPLETE:
        return 'ready';

      case SETUP_FAILED:
        return 'setupError';

      case DESTROYED:
        return 'remove';

      case AD_STARTED:
        return AD_IMPRESSION;

      case AD_IMPRESSION:
        return 'adViewableImpression';

      case PLAYBACK_REQUEST:
        return 'playAttempt';

      case AUTOSTART_BLOCKED:
        return 'autostartNotAllowed';

      case CONTENT_LOADED:
        return 'playlistItem';

      case SEEK_START:
        return 'seek';

      case SEEK_END:
        return 'seeked';

      case RENDITION_UPDATE:
        return 'visualQuality';

      case PLAYER_RESIZE:
        return 'resize';

      default:
        return eventName;
    }
  },

  getSkipParams: function(adConfig) {
    const skipParams = {};
    const skipoffset = adConfig.skipoffset;
    if (skipoffset !== undefined) {
      const skippable = skipoffset >= 0;
      skipParams.skip = skippable ? 1 : 0;
      if (skippable) {
        skipParams.skipmin = skipoffset + 2;
        skipParams.skipafter = skipoffset;
      }
    }
    return skipParams;
  },

  getSupportedMediaTypes: function(mediaTypes = []) {
    const el = document.createElement('video');
    return mediaTypes
      .filter(mediaType => el.canPlayType(mediaType))
      .concat(VPAID_MIME_TYPE); // Always allow VPAIDs.
  },

  getStartDelay: function() {
    // todo calculate
    // need to know which ad we are bidding on
    // Might have to implement and set in Pb-video ; would required ad unit as param.
  },

  /**
   * Determine the ad placement
   * @param {Object} adConfig
   * @param {Object} player
   * @return {PLACEMENT|OrtbVideoParams.placement|undefined}
   */
  getPlacement: function(adConfig, player) {
    if (!adConfig.outstream) {
      // https://developer.jwplayer.com/jwplayer/docs/jw8-embed-an-outstream-player for more info on outstream
      return PLACEMENT.INSTREAM;
    }

    if (player.getFloating()) {
      return PLACEMENT.FLOATING;
    }

    const placement = adConfig.placement;
    if (!placement) {
      return;
    }

    return PLACEMENT[placement.toUpperCase()];
  },

  getPlaybackMethod: function({ autoplay, mute, autoplayAdsMuted }) {
    if (autoplay) {
      // Determine whether player is going to start muted.
      const isMuted = mute || autoplayAdsMuted; // todo autoplayAdsMuted only applies to preRoll
      return isMuted ? PLAYBACK_METHODS.AUTOPLAY_MUTED : PLAYBACK_METHODS.AUTOPLAY;
    }
    /*
     TODO
      could support the following with float player:
      5 Initiates on Entering Viewport with Sound On
      6 Initiates on Entering Viewport with Sound Off by Default
     */
    return PLAYBACK_METHODS.CLICK_TO_PLAY;
  },

  /**
   * Indicates if Omid is supported
   *
   * @param {string} adClient - The identifier of the ad plugin requesting the bid
   * @returns {boolean} - support of omid
   */
  isOmidSupported: function(adClient) {
    const omidIsLoaded = window.OmidSessionClient !== undefined;
    return omidIsLoaded && adClient === 'vast';
  },

  /**
   * Gets ISO 639 language code of current audio track.
   * @param {Object} player
   * @returns {string|undefined} ISO 639 language code.
   */
  getIsoLanguageCode: function(player) {
    const audioTracks = player.getAudioTracks();
    if (!audioTracks || !audioTracks.length) {
      return;
    }

    const currentTrackIndex = Math.max(player.getCurrentAudioTrack() || 0, 0); // returns -1 when there are no alternative tracks.
    const audioTrack = audioTracks[currentTrackIndex];
    return audioTrack && audioTrack.language;
  },

  /**
   * Converts an array of jwpsegs into an array of data segments compliant with the oRTB content.data[index].segment
   * @param {[String]} jwpsegs - jwplayer contextual targeting segments
   * @return {[Object]|undefined} list of data segments compliant with the oRTB content.data[index].segment spec
   */
  getSegments: function (jwpsegs) {
    if (!jwpsegs || !jwpsegs.length) {
      return;
    }

    const formattedSegments = jwpsegs.reduce((convertedSegments, rawSegment) => {
      convertedSegments.push({
        id: rawSegment,
        value: rawSegment
      });
      return convertedSegments;
    }, []);

    return formattedSegments;
  },

  /**
   * Creates an object compliant with the oRTB content.data[index] spec.
   * @param {String} mediaId - content identifier
   * @param {[Object]} segments - list of data segments compliant with the oRTB content.data[index].segment spec
   * @return {Object} - Object compliant with the oRTB content.data[index] spec.
   */
  getContentDatum: function (mediaId, segments) {
    if (!mediaId && !segments) {
      return;
    }

    const contentData = {
      name: 'jwplayer.com',
      ext: {}
    };

    if (mediaId) {
      contentData.ext.cids = [mediaId];
    }

    if (segments) {
      contentData.segment = segments;
      contentData.ext.segtax = 502;
    }

    return contentData;
  }
}

/**
 * Tracks which functions are attached to events
 * @typedef CallbackStorage
 * @function storeCallback
 * @function getCallback
 * @function clearStorage
 */

/**
 * @returns {CallbackStorage}
 */
export function callbackStorageFactory() {
  let storage = {};

  function storeCallback(eventType, eventHandler, callback) {
    let eventHandlers = storage[eventType];
    if (!eventHandlers) {
      eventHandlers = storage[eventType] = {};
    }

    eventHandlers[callback] = eventHandler;
  }

  function getCallback(eventType, callback) {
    let eventHandlers = storage[eventType];
    if (!eventHandlers) {
      return;
    }

    const eventHandler = eventHandlers[callback];
    delete eventHandlers[callback];
    return eventHandler;
  }

  function clearStorage() {
    storage = {};
  }

  return {
    storeCallback,
    getCallback,
    clearStorage
  }
}

// STATE

/**
 * @returns {State}
 */
export function adStateFactory() {
  const adState = Object.assign({}, stateFactory());

  function updateForEvent(event) {
    const updates = {
      adTagUrl: event.tag,
      offset: event.adPosition,
      loadTime: event.timeLoading,
      vastAdId: event.id, // TODO: delete from spec!! seems JW Player specific
      adDescription: event.description,
      adServer: event.adsystem,
      adTitle: event.adtitle,
      advertiserId: event.advertiserId,
      advertiserName: event.advertiser,
      dealId: event.dealId,
      // adCategories
      linear: event.linear,
      vastVersion: event.vastversion,
      // campaignId:
      creativeUrl: event.mediaFile, // TODO: per AP, mediafile might be object w/ file property. verify
      adId: event.adId,
      universalAdId: event.universalAdId,
      creativeId: event.creativeAdId,
      creativeType: event.creativetype,
      redirectUrl: event.clickThroughUrl,
      adPlacementType: convertPlacementToOrtbCode(event.placement),
      waterfallIndex: event.witem,
      waterfallCount: event.wcount,
      adPodCount: event.podcount,
      adPodIndex: event.sequence,
      wrapperAdIds: event.wrapperAdIds
    };

    if (event.client === 'googima' && !updates.wrapperAdIds) {
      updates.wrapperAdIds = parseImaAdWrapperIds(event);
    }

    this.updateState(updates);
  }

  adState.updateForEvent = updateForEvent;

  function convertPlacementToOrtbCode(placement) {
    switch (placement) {
      case 'instream':
        return PLACEMENT.INSTREAM;

      case 'banner':
        return PLACEMENT.BANNER;

      case 'article':
        return PLACEMENT.ARTICLE;

      case 'feed':
        return PLACEMENT.FEED;

      case 'interstitial':
      case 'slider':
      case 'floating':
        return PLACEMENT.INTERSTITIAL_SLIDER_FLOATING;
    }
  }

  function parseImaAdWrapperIds(adEvent) {
    const ima = adEvent.ima;
    const ad = ima && ima.ad;
    if (!ad) {
      return;
    }

    const adProperties = Object.keys(ad);
    adProperties.forEach(property => {
      const value = ad[property];
      const wrapperIds = value.adWrapperIds;
      if (wrapperIds) {
        return wrapperIds;
      }
    });
  }

  return adState;
}

/**
 * @returns {State}
 */
export function timeStateFactory() {
  const timeState = Object.assign({}, stateFactory());

  function updateForEvent(event) {
    const { position, duration } = event;
    this.updateState({
      time: position,
      duration,
      playbackMode: getPlaybackMode(duration)
    });
  }

  timeState.updateForEvent = updateForEvent;

  function getPlaybackMode(duration) {
    if (duration > 0) {
      return PLAYBACK_MODE.VOD;
    } else if (duration < 0) {
      return PLAYBACK_MODE.DVR;
    }

    return PLAYBACK_MODE.LIVE;
  }

  return timeState;
}