import { FakeEvent, Utils, EventManager, getLogger, MediaType } from '@playkit-js/playkit-js'; import { ProviderEntryListObject, ProviderMediaInfoObject, ProviderPlaylistMetadataObject, ProviderPlaylistObject } from '@playkit-js/playkit-js-providers/types'; import { KalturaPlayer } from '../../kaltura-player'; import { PlaylistEventType } from './playlist-event-type'; import { Playlist } from './playlist'; import { PlaylistItem } from './playlist-item'; import { mergeProviderPluginsConfig } from '../utils/setup-helpers'; import { PlaylistOptions, PlaylistCountdownOptions, KalturaPlayerConfig, PluginsConfig, KPPlaylistObject, PlaylistConfigObject, SourcesConfig } from '../../types'; import { addClientTag, addReferrer, addStartAndEndTime, updateSessionIdInUrl } from '../utils/kaltura-params'; import { SessionIdGenerator } from '../utils/session-id-generator'; /** * @class PlaylistManager * @param {KalturaPlayer} player - The player instance * @param {KPOptionsObject} options - The player config object */ class PlaylistManager { private static _logger: any = getLogger('PlaylistManager'); private readonly _player: KalturaPlayer; private _eventManager: EventManager; private _playlist: Playlist; private readonly _options: PlaylistOptions; private readonly _countdown: PlaylistCountdownOptions; private _playerOptions: KalturaPlayerConfig; private _mediaInfoList: Array; private _appPluginConfig: PluginsConfig; constructor(player: KalturaPlayer, options: KalturaPlayerConfig) { this._player = player; this._eventManager = new EventManager(); this._playlist = new Playlist(); this._options = { autoContinue: true, loop: false, imageDuration: 5, documentDuration: 10 }; this._countdown = { duration: 10, showing: true }; this._mediaInfoList = []; this._playerOptions = options; this._appPluginConfig = {}; } /** * Config the playlist * @param {KPPlaylistObject} [config] - The playlist config * @param {ProviderEntryListObject} [entryList] - Entry list * @returns {void} * @instance * @memberof PlaylistManager */ public configure(config: KPPlaylistObject, entryList?: ProviderEntryListObject): void { if (config) { this._playlist.configure(config, Utils.Object.getPropertyPath(this._player.sources, 'options')); Utils.Object.mergeDeep(this._options, config.options); Utils.Object.mergeDeep(this._countdown, config.countdown); if (config.items && config.items.find((item) => !!item.sources)) { this._mediaInfoList = config.items.map((item, index) => { return entryList && entryList.entries && typeof entryList.entries[index] === 'object' ? entryList.entries[index] : { entryId: item.sources.id }; }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this._player.dispatchEvent(new FakeEvent(PlaylistEventType.PLAYLIST_LOADED, { playlist: this })); this._addBindings(); const startPlaylistAtEntryId = config.options?.startAtEntryId; let wasEntryIdSet = false; if (startPlaylistAtEntryId && typeof startPlaylistAtEntryId === 'string') { const entryToPlay: PlaylistItem | undefined = this._playlist.items.find((item: PlaylistItem) => item.sources.id === startPlaylistAtEntryId); if (entryToPlay) { wasEntryIdSet = true; this.playItem(entryToPlay.index); } } if (!wasEntryIdSet) { this.playNext(); } } } } /** * Load a playlist * @param {KPPlaylistObject} playlistData - The playlist data * @param {KPPlaylistConfigObject} [playlistConfig] - The playlist config * @param {ProviderEntryListObject} [entryList] - Entry list * @returns {void} * @instance * @memberof PlaylistManager */ public load(playlistData: ProviderPlaylistObject, playlistConfig: PlaylistConfigObject, entryList?: ProviderEntryListObject): void { const mergedPlaylistData: KPPlaylistObject = this._getMergedPlaylistData(playlistData, playlistConfig); this.configure(mergedPlaylistData, entryList); } /** * Reset the playlist * @returns {void} * @instance * @memberof PlaylistManager */ public reset(): void { this._eventManager.removeAll(); this._playlist = new Playlist(); this._mediaInfoList = []; } /** * Play the next item * @returns {void} * @instance * @memberof PlaylistManager */ public playNext(playlistGotError: boolean = false): void { if (playlistGotError) { this._player.clearReset(); } PlaylistManager._logger.debug('playNext'); const next = this._playlist.getNext(true); if (next) { this._setItem(next); } } /** * Play the previous item * @returns {void} * @instance * @memberof PlaylistManager */ public playPrev(): void { PlaylistManager._logger.debug('playPrev'); const prev = this._playlist.prev; if (prev) { this._setItem(prev); } } /** * Play a specific item * @param {number} index - The index of the item to play * @returns {void} * @instance * @memberof PlaylistManager */ public playItem(index: number): void { PlaylistManager._logger.debug(`playItem(${index})`); const item = this._playlist.items[index]; if (item) { this._setItem(item); } } /** * Playlist items * @type {Array} * @instance * @memberof PlaylistManager */ public get items(): Array { return this._playlist.items; } /** * Current item * @type {?PlaylistItem} * @instance * @memberof PlaylistManager */ public get current(): PlaylistItem | undefined { return this._playlist.current; } /** * Next item * @type {?PlaylistItem} * @instance * @memberof PlaylistManager */ public get next(): PlaylistItem | undefined { return this._playlist.getNext(this._options.loop); } /** * Previous item * @type {?PlaylistItem} * @instance * @memberof PlaylistManager */ public get prev(): PlaylistItem | undefined { return this._playlist.prev; } /** * Playlist id * @type {string} * @instance * @memberof PlaylistManager */ public get id(): string { return this._playlist.id; } /** * Playlist metadata * @type {ProviderPlaylistMetadataObject} * @instance * @memberof PlaylistManager */ public get metadata(): ProviderPlaylistMetadataObject { return this._playlist.metadata; } /** * Playlist poster * @type {?string} * @instance * @memberof PlaylistManager */ public get poster(): string | undefined { return this._playlist.poster; } /** * Playlist countdown * @type {KPPlaylistCountdownOptions} * @instance * @memberof PlaylistManager */ public get countdown(): PlaylistCountdownOptions { if (this._playlist.current && this._playlist.current.config) { const mergedConfig: PlaylistCountdownOptions = { duration: 10, showing: true }; Utils.Object.mergeDeep(mergedConfig, this._countdown, this._playlist.current.config.countdown); return mergedConfig; } return this._countdown; } /** * Playlist options * @type {KPPlaylistOptions} * @instance * @memberof PlaylistManager */ public get options(): PlaylistOptions { return this._options; } private _getMergedPlaylistData(playlistData: ProviderPlaylistObject, playlistConfig: PlaylistConfigObject): KPPlaylistObject { const mergedPlaylistData: KPPlaylistObject = { id: playlistData.id, metadata: playlistData.metadata, poster: playlistData.poster, options: playlistConfig ? playlistConfig.options : this._options, countdown: playlistConfig ? playlistConfig.countdown : this.countdown, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore items: playlistData.items.map((item, index) => { const itemData = Utils.Object.copyDeep(item); // keep original media source data itemData.sources.mediaEntryType = item?.sources?.type; ['sources.dvr', 'sources.type'].forEach((omitKey) => { // use medias source data instead of playlist source data Utils.Object.deletePropertyPath(itemData, omitKey); }); Utils.Object.mergeDeep( itemData.sources, playlistConfig && playlistConfig.items && playlistConfig.items[index] && playlistConfig.items[index].sources ); if (Array.isArray(itemData.sources.poster)) { this._player.updateKalturaPoster(itemData.sources, item.sources, this._player.dimensions); } else if (this._player.shouldAddKs() && typeof itemData.sources.poster === 'string' && this._player.getKs()) { const ksSegment = `/ks/${this._player.getKs()}`; // Avoid appending KS multiple times if this mapping runs more than once if (!itemData.sources.poster.includes('/ks/')) { itemData.sources.poster += ksSegment; } } return { sources: itemData.sources, config: playlistConfig && playlistConfig.items && playlistConfig.items[index] && playlistConfig.items[index].config }; }) }; return mergedPlaylistData; } private _addBindings(): void { this._eventManager.listen(this._player, this._player.Event.Core.PLAYBACK_ENDED, () => this._onPlaybackEnded()); this._eventManager.listen(this._player, this._player.Event.Core.CHANGE_SOURCE_STARTED, () => this._onChangeSourceStarted()); } private _onPlaybackEnded(): void { const nextItem = this._playlist.getNext(false); if (!nextItem) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this._player.dispatchEvent(new FakeEvent(PlaylistEventType.PLAYLIST_ENDED)); } if (this._playerOptions.ui.disable || !this.countdown.showing) { if ((nextItem && this._options.autoContinue) || this._options.loop) { this.playNext(); } } } private _onChangeSourceStarted(): void { if (this._player.isImage()) { this._player.configure({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore sources: { duration: this._options.imageDuration } }); } else if (this._player.isDocument()) { this._player.configure({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore sources: { duration: this._options.documentDuration } }); } } private _setItem(activeItem: PlaylistItem): Promise { const { index } = activeItem; PlaylistManager._logger.debug(`Playing item number ${index}`, activeItem); const playback: any = { loop: false }; if (this._playlist.current) { if (this._player._playbackStart) { // from the second item onwards playback['autoplay'] = true; } else { playback['autoplay'] = !!this._playerOptions.playback['autoplay']; } } this._player.configure({ playback }); this._playlist.activeItemIndex = index; Promise.all(this.getEntryPromises([index - 1, index + 1])).then((mediaConfigs) => { let cachedUrls = []; for (const mediaConfig of mediaConfigs) { if (mediaConfig.sources.dash?.length) { cachedUrls = cachedUrls.concat(mediaConfig.sources.dash.map((dashSource) => dashSource.url)); } } this._player.setCachedUrls(cachedUrls); }); if (activeItem.isPlayable()) { this._resetProviderPluginsConfig(); const mergedPluginsConfigAndFromApp = mergeProviderPluginsConfig(activeItem.plugins, this._player.config.plugins); const providerPlugins = mergedPluginsConfigAndFromApp[0]; this._appPluginConfig = mergedPluginsConfigAndFromApp[1]; const media = { session: this._player.config.session, plugins: providerPlugins, sources: activeItem.sources }; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this._player.setMedia(media); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this._player.dispatchEvent( new FakeEvent(PlaylistEventType.PLAYLIST_ITEM_CHANGED, { index, activeItem, entryId: this._mediaInfoList[index]?.entryId || activeItem.sources?.id }) ); return Promise.resolve(); } else { if (this._mediaInfoList[index]) { this._resetProviderPluginsConfig(); const media = { sources: activeItem.sources }; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this._player.setMedia(media); return this._player.loadMedia(this._mediaInfoList[index]).then((mediaConfig) => { this._playlist.updateItemSources(index, mediaConfig.sources); this._playlist.updateItemPlugins(index, mediaConfig.plugins); let sessionId = this._player.sessionIdCache?.get(mediaConfig.sources.id); if (!sessionId) { sessionId = SessionIdGenerator.next(); this._player.sessionIdCache?.set(mediaConfig.sources.id, sessionId); } mediaConfig.sources.dash = mediaConfig.sources.dash.map((source) => { source.url = updateSessionIdInUrl(this._player, source.url, sessionId); source.url = addReferrer(source.url); source.url = addClientTag(source.url, mediaConfig.productVersion); source.url = addStartAndEndTime(source.url, mediaConfig.sources); return source; }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this._player.dispatchEvent( new FakeEvent(PlaylistEventType.PLAYLIST_ITEM_CHANGED, { index, activeItem, entryId: this._mediaInfoList[index].entryId }) ); }); } } return Promise.reject(); } private _resetProviderPluginsConfig(): void { this._player.configure({ plugins: this._appPluginConfig }); this._appPluginConfig = {}; } public destroy(): void { this._eventManager.destroy(); } private getEntryPromise(index: number): Promise { if (this._playlist.items[index].isPlayable()) return Promise.resolve({ sources: this._playlist.items[index].sources }); return this._player.provider.getMediaConfig(this._mediaInfoList[index]).then((providerMediaConfig) => { if (this._player.shouldAddKs() && this._player.getKs()) { const poster = providerMediaConfig.sources.poster; if (typeof poster === 'string') { providerMediaConfig.sources.poster = `${poster}/ks/${this._player.getKs()}`; } } const mediaConfig = Utils.Object.copyDeep(providerMediaConfig); this._playlist.updateItemSources(index, mediaConfig.sources); this._playlist.updateItemPlugins(index, mediaConfig.plugins); let sessionId = this._player.sessionIdCache?.get(mediaConfig.sources.id); if (!sessionId) { sessionId = SessionIdGenerator.next(); this._player.sessionIdCache?.set(mediaConfig.sources.id, sessionId); } mediaConfig.sources.dash = mediaConfig.sources.dash.map((source) => { source.url = updateSessionIdInUrl(this._player, source.url, sessionId); source.url = addReferrer(source.url); source.url = addClientTag(source.url, mediaConfig.productVersion); source.url = addStartAndEndTime(source.url, mediaConfig.sources); return source; }); return Promise.resolve(mediaConfig); }); } private getEntryPromises(indexes: number[]): Promise[] { const result: Promise[] = []; for (const index of indexes) { const item = this._playlist.items[index]; if (!item?.sources) continue; const { type, mediaEntryType } = item.sources as SourcesConfig & { mediaEntryType?: string }; if (type === MediaType.VOD || mediaEntryType === MediaType.VOD) { result.push(this.getEntryPromise(index)); } } return result; } } export { PlaylistManager };