Source: utils/SRGLetterboxConfiguration.js

import videojs from 'video.js';
import hotkeys from './PlayerHotKeysService.js';
import DataProviderService
  from '../dataProvider/services/DataProviderService.js';
import * as Events from './Events.js';
import * as SRGSettings from './SRGSettings.js';
import SRGSettingActions from './SRGSettingActions.js';
import SRGLetterboxComponents from './SRGLetterboxComponents.js';
import LocalStorage from './LocalStorage.js';
import PlayerUtils from './PlayerUtils.js';
import SRGAnalytics from '../analytics/SRGAnalytics.js';
import SRGLetterboxLayout from './SRGLetterboxLayout.js';
import SRGMenuStrategy from './SRGMenuStrategy.js';
import ERRORTYPE from '../errors/errors.js';
/**
 * The configuration is fully handled by SRGLetterbox user should not override videojs options.
 * @private
 */

class SRGLetterboxConfiguration {
  static initializePlayer(configuration = {}, version) {
    const defaultConfiguration = SRGLetterboxConfiguration.defaultLetterboxConfiguration;

    const mergedConfiguration = videojs.mergeOptions(defaultConfiguration, configuration);

    const {
      analytics,
      ilHost,
      language,
      storageNamespace,
      ...playerConfiguration
    } = mergedConfiguration;

    const playerOptions = videojs.mergeOptions(
      SRGLetterboxConfiguration.defaultVideoJSOptions(playerConfiguration),
      {
        language,
        storageNamespace,
      },
    );

    const player = SRGLetterboxConfiguration.createPlayer(playerOptions);
    SRGLetterboxConfiguration.initializeServices(player, {
      analytics, ilHost, version,
    });

    const { components, ...global } = playerConfiguration;

    SRGLetterboxConfiguration.fireActions(player, global);
    SRGLetterboxConfiguration.handleComponentsVisibility(player, components);

    return player;
  }

  static initializeServices(player, {
    analytics, ilHost, version,
  }) {
    const debug = SRGLetterboxConfiguration.getSetting(player, SRGSettings.DEBUG);

    player.options({
      SRGProviders: {
        analytics: analytics && new SRGAnalytics(player, version, {
          tagCommanderScriptURL: SRGLetterboxConfiguration
            .getSetting(player, SRGSettings.TAG_COMMANDER_SCRIPT_URL),
          debug,
        }),
        dataService: new DataProviderService(ilHost),
        menuStrategy: new SRGMenuStrategy(player, SRGLetterboxConfiguration),
        layoutManager: SRGLetterboxLayout(player),
      },
      version,
      ilHost,
    });
  }

  static updateConfiguration(player, { components = {}, ...configuration } = {}) {
    const playerConfiguration = SRGLetterboxConfiguration.getPlayerConfiguration(player);

    Object.keys(playerConfiguration).forEach((key) => {
      const configurationItem = configuration[key];

      if (configurationItem !== undefined) {
        const settingConfiguration = { [key]: configurationItem };

        SRGLetterboxConfiguration.clearPlayerSetting(player, key);
        SRGLetterboxConfiguration.updateSettingConfiguration(player, settingConfiguration);
        SRGLetterboxConfiguration.fireActions(player, settingConfiguration);
      }
    });

    player.trigger(Events.CONFIGURATION_CHANGE);

    SRGLetterboxConfiguration.handleComponentsVisibility(player, components);
  }

  static fireActions(player, settings) {
    Object.keys(settings).forEach((key) => {
      const action = SRGLetterboxConfiguration.getSettingAction(key);
      if (action) {
        const settingValue = SRGLetterboxConfiguration.getComputedValue(
          player,
          key,
          settings[key],
        );

        action(settingValue, player);
      }
      player.trigger({
        type: Events.SETTING_CHANGE,
        data: key,
      });
    });
  }

  static handleComponentsVisibility(player, components) {
    Object.keys(components).forEach((key) => {
      const componentPath = SRGLetterboxComponents[key];

      if (componentPath) {
        const visibility = components[key];

        SRGLetterboxConfiguration.setComponentVisibility(player, componentPath, visibility);
        SRGLetterboxConfiguration.updateSettingConfiguration(player, {
          components: {
            [key]: visibility,
          },
        });
      }
    });
  }

  static setComponentVisibility(player, componentPath, isVisible) {
    const component = PlayerUtils.getComponent(componentPath, player);

    if (!component) {
      return;
    }

    component.options({
      isComponentVisible: isVisible,
    });

    if (isVisible) {
      component.show();
    } else {
      component.hide();
    }
  }

  static updateSettingConfiguration(player, settingConfiguration) {
    player.options({
      SRGPlayerConfiguration: {
        ...settingConfiguration,
      },
    });
  }

  static clearPlayerSetting(player, key) {
    player.options({
      srgPlayerSettings: {
        [key]: undefined,
      },
    });
  }

  /**
   * @name PlayerConfiguration
   *
   * @type {Object}
   * @property {Boolean} analytics allows to enable or disable the SRG SSR analytics
   * @property {String} chromeCastReceiver set a custom Chrome Cast Receiver app id
   * @property {String} container query selector of the HTMLElement where the player will be created
   * @property {Boolean} debug debug mode to show logs in dev console through videojs logs
   * @property {Array<Object>} endScreenPlaylist allows to provide a custom end screen playlist, must be an array with 18 items
   * @property {String} endScreenPlaylist.urn urn of the media
   * @property {String} endScreenPlaylist.id id of the media
   * @property {String} endScreenPlaylist.title title of the media
   * @property {String} endScreenPlaylist.showTitle title of the show
   * @property {String} endScreenPlaylist.description description of the media
   * @property {Number} endScreenPlaylist.duration duration of the media
   * @property {String} endScreenPlaylist.imageUrl image URL of the media
   * @property {String} endScreenPlaylist.imageTitle image title of the media
   * @property {String} endScreenPlaylist.vendor the BU name
   * @property {String} endScreenPlaylist.mediaType type of the media
   * @property {Number} endScreenPlaylist.pendingSeek defines the media start time
   * @property {Boolean} endScreenPlaylist.standalone defines whether the segment should standalone
   * @property {Object} [userReport] allows to control the display of the error report button
   * @property {Array} userReport.errors list of errors that are handled
   * @property {String} [userReport.email=undefined] email used to send the error report
   * @property {Boolean} fillMode set the layout: (true for fill, false for fluid)
   * @property {Boolean} [nextEpisodeRecommendation=false] allows to use the recommendation service provided by Playfff
   * @property {Boolean} [noUI=false] allows to disable the player's UI
   * @property {Object} playerFocus allows to set the focus to the player at initialization to use hotkeys
   * @property {Boolean} recommendations allows to show or hide the recommendation screen
   * @property {String} storageNamespace defines the player's localStorage namespace
   * @property {String} tagCommanderScriptURL defines the tagCommander script URL
   *
   * @property {Object} components lists all component that can be shown or hidden.
   *                               The value trop allow to show the component when false will hide it
   * @property {Boolean} components.audioDescription
   * @property {Boolean} components.controlBar
   * @property {Boolean} components.header
   * @property {Boolean} components.sharing
   * @property {Boolean} components.title
   * @property {Boolean} components.thumbnails
   * @property {Boolean} components.playButton
   * @property {Boolean} components.subdivisions
   *
   * @property {Object} audioTrackLanguage allows to control the audio track language user experience
   * @property {Object} autoplay allows to control the autoplay user experience
   * @property {Object} continuousPlayback allows to control the continuous playback user experience
   * @property {Object} debugMenu allows to display the debug option in the setting menu
   * @property {Object} hd allows to control the resource quality
   * @property {Object} mute allows to control the volume user experience
   * @property {Object} playbackRate allows to control the playback rate user experience
   * @property {Object} textTrackLanguage allows to control the subtitle user experience
   * @property {Object} textTrackSizeLevel allows to control the subtitle size level
   * @property {Object} volume allows to control the volume user experience
   */
  static getPlayerConfiguration(player) {
    return player.options().SRGPlayerConfiguration;
  }

  static getSettingConfiguration(player, key) {
    const playerConfiguration = SRGLetterboxConfiguration.getPlayerConfiguration(player);

    return playerConfiguration[key];
  }

  static getSetting(player, key) {
    const settingConfiguration = SRGLetterboxConfiguration.getSettingConfiguration(player, key);

    return SRGLetterboxConfiguration.getComputedValue(player, key, settingConfiguration);
  }

  static isConfigurationForced(settingConfiguration, key) {
    const configurationExists = SRGLetterboxConfiguration
      .defaultLetterboxConfiguration[key] !== undefined;
    const hasDefaultValue = (settingConfiguration || {}).default !== undefined;

    return configurationExists && !hasDefaultValue;
  }

  static setSetting(player, key, value) {
    const settingConfiguration = SRGLetterboxConfiguration.getSettingConfiguration(player, key);


    if (SRGLetterboxConfiguration.isConfigurationForced(settingConfiguration, key)) {
      return;
    }

    const setting = {
      [key]: value,
    };

    player.options({
      srgPlayerSettings: setting,
    });

    if (settingConfiguration.storage) {
      LocalStorage.setItem(player.options().storageNamespace, key, value);
    }

    SRGLetterboxConfiguration.fireActions(player, setting);
  }

  static getComputedValue(player, key, settingConfiguration) {
    const setting = player.options().srgPlayerSettings[key];

    if (setting !== undefined) {
      return setting;
    }

    if (settingConfiguration === null
      || typeof settingConfiguration !== 'object'
      || !(typeof settingConfiguration === 'object' && 'default' in settingConfiguration)) {
      return settingConfiguration;
    }

    const { default: defaultValue, storage } = settingConfiguration;

    if (!storage) {
      return defaultValue;
    }

    const storageValue = LocalStorage.getItem(player.options().storageNamespace, key);

    return storageValue !== undefined ? storageValue : defaultValue;
  }

  static getSettingAction(key) {
    return SRGSettingActions[key];
  }

  /**
   * Represents the default video.js player options.
   *
   * @returns {Object} JSON object
   */
  static defaultVideoJSOptions(SRGPlayerConfiguration) {
    return {
      children: {
        warningMessageComponent: true,
        mediaLoader: true,
        liveTracker: true,
        headerComponent: true,
        posterImage: true,
        overlayComponent: true,
        endScreen: true,
        continuousPlaybackScreen: true,
        textTrackDisplay: true,
        textTrackSettings: true,
        loadingSpinner: true,
        errorDisplay: true,
        resizeManager: true,
        imageCopyright: true,
        skipCredits: true,
        controlBar: {
          children: {
            playToggle: true,
            backwardButton: true,
            forwardButton: true,
            volumePanel: true,
            currentTimeDisplay: true,
            timeDivider: true,
            durationDisplay: true,
            progressControl: {
              seekBar: {
                playProgressBar: {
                  timeTooltip: false,
                },
              },
              thumbnailSeeking: true,
            },
            liveDisplay: true,
            seekToLive: true,
            remainingTimeDisplay: true,
            customControlSpacer: true,
            chaptersButton: true,
            descriptionButton: true,
            subtitlesMenu: true,
            settingsMenu: true,
            pictureInPictureToggle: true,
            airplayButton: true,
            chromecastToggleComponent: true,
            fullscreenToggle: true,
            playbackRateMenu: true,
            SRGSSRButtonComponent: true,
          },
        },
        subdivisionsContainer: true,
      },
      controls: true,
      // enableSourceset: true, // Enable the sourceset event, overrides the currentSource
      html5: {
        vhs: {
          overrideNative: !videojs.browser.IS_SAFARI,
          useForcedSubtitles: true,
        },
        dash: {
          updateSettings: {
            streaming: {
              timeShiftBuffer: {
                calcFromSegmentTimeline: true,
              },
            },
          },
        },
      },
      playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 2],
      liveTracker: {
        trackingThreshold: 120,
        liveTolerance: 15,
      },
      liveui: true,
      preload: 'auto', // Should preload media chunks
      responsive: true,
      techOrder: ['html5'],
      SRGProviders: {},
      SRGPlayerConfiguration,
      srgPlayerSettings: {},
      srgTextTrackSizes: [1, 1.3, 1.6],
      userActions: {
        hotkeys,
      },
      breakpoints: {
        tiny: 210,
        xsmall: 320,
        small: 425,
        medium: 768,
        large: 960,
        xlarge: 1440,
        huge: Infinity,
      },
    };
  }

  /**
   * @name LetterboxConfiguration
   *
   * @type {Object}
   * @property {Boolean} analytics allows to enable or disable the SRG SSR analytics
   * @property {String} [chromeCastReceiver=1AC2931D] set a custom Chrome Cast Receiver app id
   * @property {String} container query selector of the HTMLElement where the player will be created
   * @property {Boolean} [debug=false] debug mode to show logs in dev console through videojs logs
   * @property {Array<Object>} [endScreenPlaylist=false] allows to provide a custom end screen playlist, must be an array with 18 items
   * @property {String} endScreenPlaylist.urn urn of the media
   * @property {String} endScreenPlaylist.id id of the media
   * @property {String} endScreenPlaylist.title title of the media
   * @property {String} endScreenPlaylist.showTitle title of the show
   * @property {String} endScreenPlaylist.description description of the media
   * @property {Number} endScreenPlaylist.duration duration of the media
   * @property {String} endScreenPlaylist.imageUrl image URL of the media
   * @property {String} endScreenPlaylist.imageTitle image title of the media
   * @property {String} endScreenPlaylist.vendor the BU name
   * @property {String} endScreenPlaylist.mediaType type of the media
   * @property {Number} endScreenPlaylist.pendingSeek defines the media start time
   * @property {Boolean} endScreenPlaylist.standalone defines whether the segment should standalone
   * @property {Object} [userReport] allows to control the display of the error report button
   * @property {Array} userReport.errors list of errors that are handled
   * @property {String} [userReport.email=undefined] email used to send the error report
   * @property {Boolean} [fillMode=true] set the layout: (true for fill, false for fluid)
   * @property {String} [ilHost=il.srgssr.ch] Integration layer server hostname to use for URN resolution
   * @property {String} [language=en] set the player's default language, supported values en, fr, de, it and rm only
   * @property {Boolean} [nextEpisodeRecommendation=false] allows to use the recommendation service provided by Playfff
   * @property {Boolean} [noUI=false] allows to disable the player's UI
   * @property {Object} [playerFocus=undefined] allows to set the focus to the player at initialization to use hotkeys.
   *                                            By default __playerFocus__ is set to __undefined__ which means the focus method will be called without __preventScroll__ object.
   *                                            See [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/focus) documentation for further details.
   * @property {Boolean} [recommendations=true] allows to show or hide the recommendation screen
   * @property {String} [storageNamespace=srgssr/letterbox/userStorage] defines the player's localStorage namespace
   * @property {String} [tagCommanderScriptURL=//colibri-js.akamaized.net/penguin/tc_SRGGD_11.js] defines the tagCommander script URL
   * @property {Int} [warningMessageTimeout=6000] sets the timeout in milliseconds after what the message will be hidden
   *
   * @property {Object} components lists all component that can be shown or hidden.
   *                               The value trop allow to show the component when false will hide it
   * @property {Boolean} [components.audioDescription=true]
   * @property {Boolean} [components.controlBar=true]
   * @property {Boolean} [components.header=true]
   * @property {Boolean} [components.sharing=true]
   * @property {Boolean} [components.title=true]
   * @property {Boolean} [components.thumbnails=true]
   * @property {Boolean} [components.playButton=true]
   * @property {Boolean} [components.subdivisions=true]
   *
   * @property {Object} [audioTrackLanguage={default:false, storage:true}] allows to control the audio track language user experience
   * @property {Object} [autoplay={default:false, storage:true}] allows to control the autoplay user experience
   * @property {Object} [continuousPlayback={default:false, storage:true}] allows to control the continuous playback user experience
   * @property {Object} [debugMenu=false] allows to display the debug option in the setting menu
   * @property {Object} [hd={default:true, storage:true}] allows to control the resource quality
   * @property {Object} [muted={default:false, storage:true}] allows to control the volume user experience
   * @property {Object} [playbackRate={default:1, storage:true}] allows to control the playback rate user experience
   * @property {Object} [textTrackLanguage={default:false, storage:true}] allows to control the subtitle user experience
   * @property {Object} [textTrackSizeLevel={default:1, storage:true}] allows to control the subtitle size level
   * @property {Object} [volume={default:1, storage:true}] allows to control the volume user experience
   */
  static get defaultLetterboxConfiguration() {
    return {
      analytics: true,
      container: undefined,
      chromeCastReceiver: '1AC2931D',
      debug: false,
      endScreenPlaylist: false,
      fillMode: true,
      ilHost: 'il.srgssr.ch',
      language: 'en',
      nextEpisodeRecommendation: false,
      noUI: false,
      playerFocus: undefined,
      recommendations: true,
      storageNamespace: 'srgssr/letterbox/userStorage',
      tagCommanderScriptURL: '//colibri-js.akamaized.net/penguin/tc_SRGGD_11.js',
      userReport: {
        errors: [
          ERRORTYPE.ERROR_BLOCKING_REASON_GEOBLOCK.type,
          ERRORTYPE.ERROR_BLOCKING_REASON_UNKNOWN.type,
          ERRORTYPE.ERROR_UNKNOWN.type,
        ],
        emailTo: undefined,
      },
      warningMessageTimeout: 6000,
      components: {
        audioDescription: true,
        controlBar: true,
        header: true,
        sharing: true,
        title: true,
        thumbnails: true,
        playButton: true,
        subdivisions: true,
      },
      audioTrackLanguage: {
        default: false,
        storage: true,
      },
      autoplay: {
        default: false,
        storage: true,
      },
      continuousPlayback: {
        default: true,
        storage: true,
      },
      debugMenu: false,
      hd: {
        default: true,
        storage: true,
      },
      muted: {
        default: false,
        storage: true,
      },
      playbackRate: {
        default: 1,
        storage: true,
      },
      textTrackLanguage: {
        default: false,
        storage: true,
      },
      textTrackSizeLevel: {
        default: 1,
        storage: true,
      },
      volume: {
        default: 1,
        storage: true,
      },
    };
  }

  /**
   * Represents the default video element filled with some default video.js CSS classes.
   *
   * @returns {HTMLVideoElement} HTML video element
   */
  static defaultVideo() {
    const video = document.createElement('video');

    video.classList.add('video-js');
    video.classList.add('vjs-srgssr-skin');
    video.classList.add('vjs-fluid');
    video.classList.add('vjs-show-big-play-button-on-pause');

    return video;
  }

  /**
   * Connect this instance to a given element and initialize videojs player.
   *
   * The current configuration (see setXXX methods) is applied to this player.
   *
   * @param containerElement
   *  DOM element to attach this player to (id string, class string or a DOM element).
   *  If this is a string and not an id or string, a new div element is created
   * @returns {Node} DOM Element the player has been attached to
   */
  static createPlayer(playerOptions) {
    const { SRGPlayerConfiguration: { container = '' } = {} } = playerOptions;
    const isContainerTypeValid = typeof container === 'string';
    const isContainerTypeNode = container instanceof Element;

    if ((!isContainerTypeNode && !isContainerTypeValid)
      || (isContainerTypeValid && container.trim().length === 0)
    ) {
      throw new Error('The container must be a valid selector or a DOM element');
    }

    const element = isContainerTypeNode ? container : document.querySelector(container);

    if (!element) {
      throw new Error(`The container ${container} is not found`);
    }

    const video = SRGLetterboxConfiguration.defaultVideo();

    if (videojs.browser.IS_IPHONE) {
      video.setAttribute('playsinline', '');
    }

    element.classList.add('letterbox-web');
    element.appendChild(video);

    // eslint-disable-next-line new-cap
    return new videojs(video, playerOptions);
  }
}

export default SRGLetterboxConfiguration;