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;