diff --git a/package.json b/package.json index fd0e7523f..f1dba4783 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@playkit-js/playkit-js-providers": "2.40.5", "@playkit-js/playkit-js-ui": "0.79.1-canary.0-24968c4", "hls.js": "^1.5.8", - "shaka-player": "4.7.0" + "shaka-player": "4.8.11" }, "devDependencies": { "@babel/core": "^7.22.20", diff --git a/src/common/playlist/playlist-manager.ts b/src/common/playlist/playlist-manager.ts index 6b61ae6d2..f43ee2ca6 100644 --- a/src/common/playlist/playlist-manager.ts +++ b/src/common/playlist/playlist-manager.ts @@ -11,6 +11,8 @@ import { Playlist } from './playlist'; import { PlaylistItem } from './playlist-item'; import { mergeProviderPluginsConfig } from '../utils/setup-helpers'; import { PlaylistOptions, PlaylistCountdownOptions, KalturaPlayerConfig, PluginsConfig, KPPlaylistObject, PlaylistConfigObject } from '../../types'; +import { addClientTag, addReferrer, addStartAndEndTime, updateSessionIdInUrl } from '../utils/kaltura-params'; +import { SessionIdGenerator } from '../utils/session-id-generator'; /** * @class PlaylistManager @@ -332,6 +334,25 @@ class PlaylistManager { } this._player.configure({ playback }); this._playlist.activeItemIndex = index; + + const promises: Promise[] = []; + + if (this._playlist.items[index - 1]) { + promises.push(this.prepareEntry(index - 1)); + } + if (this._playlist.items[index + 1]) { + promises.push(this.prepareEntry(index + 1)); + } + + Promise.all(promises).then((mediaConfigs) => { + let cachedUrls = []; + for (const mediaConfig of mediaConfigs) { + 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); @@ -365,6 +386,21 @@ class PlaylistManager { 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( @@ -388,6 +424,32 @@ class PlaylistManager { public destroy(): void { this._eventManager.destroy(); } + + private prepareEntry(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) => { + 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); + }); + } } export { PlaylistManager }; diff --git a/src/common/utils/kaltura-params.ts b/src/common/utils/kaltura-params.ts index 42fc7bcbf..f1bc4217a 100644 --- a/src/common/utils/kaltura-params.ts +++ b/src/common/utils/kaltura-params.ts @@ -2,6 +2,7 @@ import { PKSourcesConfigObject, PlayerStreamTypes, StreamType, Utils } from '@pl import { getServerUIConf } from './setup-helpers'; import { KalturaPlayer } from '../../kaltura-player'; import { PartialKPOptionsObject } from '../../types'; +import { SessionIdGenerator } from './session-id-generator'; const PLAY_MANIFEST = 'playmanifest/'; const PLAY_SESSION_ID = 'playSessionId='; @@ -28,6 +29,9 @@ function handleSessionId(player: KalturaPlayer, playerConfig: PartialKPOptionsOb } else { // on first playback addSessionId(playerConfig); + if (player?.playlist?.items?.length && playerConfig.sources?.id) { + player.sessionIdCache?.set(playerConfig.sources.id, playerConfig.session!.id as string); + } } } @@ -37,9 +41,7 @@ function handleSessionId(player: KalturaPlayer, playerConfig: PartialKPOptionsOb * @private */ function addSessionId(playerConfig: PartialKPOptionsObject): void { - const primaryGUID = Utils.Generator.guid(); - const secondGUID = Utils.Generator.guid(); - setSessionId(playerConfig, primaryGUID + ':' + secondGUID); + setSessionId(playerConfig, SessionIdGenerator.next()); } /** @@ -49,19 +51,18 @@ function addSessionId(playerConfig: PartialKPOptionsObject): void { * @private */ function updateSessionId(player: KalturaPlayer, playerConfig: PartialKPOptionsObject): void { - const secondGuidInSessionIdRegex = /:((?:[a-z0-9]|-)*)/i; - const secondGuidInSessionId = secondGuidInSessionIdRegex.exec( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - player.config.session.id - ); - if (secondGuidInSessionId && secondGuidInSessionId[1]) { - setSessionId( - playerConfig, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - player.config.session.id.replace(secondGuidInSessionId[1], Utils.Generator.guid()) - ); + const entryId = playerConfig.sources?.id; + + if (!player?.playlist?.items?.length) { + setSessionId(playerConfig, SessionIdGenerator.next()); + } else if (entryId) { + if (player.sessionIdCache?.get(entryId)) { + setSessionId(playerConfig, player.sessionIdCache?.get(entryId)); + } else { + const sessionId = SessionIdGenerator.next(); + player.sessionIdCache?.set(entryId, sessionId); + setSessionId(playerConfig, sessionId); + } } } @@ -83,13 +84,16 @@ function setSessionId(playerConfig: PartialKPOptionsObject, sessionId: string): * @return {string} - the url with the new sessionId * @private */ -function updateSessionIdInUrl(url: string, sessionId?: string, paramName: string = PLAY_SESSION_ID): string { +function updateSessionIdInUrl(player: KalturaPlayer | null, url: string, sessionId?: string, paramName: string = PLAY_SESSION_ID): string { if (sessionId) { const sessionIdInUrlRegex = new RegExp(paramName + '((?:[a-z0-9]|-)*:(?:[a-z0-9]|-)*)', 'i'); const sessionIdInUrl = sessionIdInUrlRegex.exec(url); + // this url has session id (has already been played) if (sessionIdInUrl && sessionIdInUrl[1]) { - // this url has session id (has already been played) - url = url.replace(sessionIdInUrl[1], sessionId); + // session id should be the same for the same entry + if (!player?.playlist?.items?.length) { + url = url.replace(sessionIdInUrl[1], sessionId); + } } else { url += getQueryStringParamDelimiter(url) + paramName + sessionId; } @@ -201,7 +205,7 @@ function addKalturaParams(player: KalturaPlayer, playerConfig: PartialKPOptionsO // @ts-ignore !source.localSource ) { - source.url = updateSessionIdInUrl(source.url, sessionId); + source.url = updateSessionIdInUrl(player, source.url, sessionId); source.url = addReferrer(source.url); source.url = addClientTag(source.url, productVersion); source.url = addStartAndEndTime(source.url, sources); @@ -209,7 +213,7 @@ function addKalturaParams(player: KalturaPlayer, playerConfig: PartialKPOptionsO if (source['drmData'] && source['drmData'].length) { source['drmData'].forEach((drmData) => { if (typeof drmData.licenseUrl === 'string' && [UDRM_DOMAIN, CUSTOM_DATA, SIGNATURE].every((t) => drmData.licenseUrl.includes(t))) { - drmData.licenseUrl = updateSessionIdInUrl(drmData.licenseUrl, sessionId, DRM_SESSION_ID); + drmData.licenseUrl = updateSessionIdInUrl(player, drmData.licenseUrl, sessionId, DRM_SESSION_ID); drmData.licenseUrl = addClientTag(drmData.licenseUrl, productVersion); drmData.licenseUrl = addReferrer(drmData.licenseUrl); drmData.licenseUrl = addUIConfId(drmData.licenseUrl, playerConfig); @@ -221,4 +225,4 @@ function addKalturaParams(player: KalturaPlayer, playerConfig: PartialKPOptionsO }); } -export { addKalturaParams, handleSessionId, updateSessionIdInUrl, getReferrer, addReferrer, addClientTag, addUIConfId }; +export { addKalturaParams, handleSessionId, updateSessionIdInUrl, getReferrer, addReferrer, addClientTag, addUIConfId, addStartAndEndTime }; diff --git a/src/common/utils/session-id-cache.ts b/src/common/utils/session-id-cache.ts new file mode 100644 index 000000000..77431812f --- /dev/null +++ b/src/common/utils/session-id-cache.ts @@ -0,0 +1,17 @@ +class SessionIdCache { + private cache = new Map(); + + public set(key: string, value: string): void { + this.cache.set(key, value); + } + + public get(key: string): string { + return this.cache.get(key); + } + + public clear(): void { + this.cache.clear(); + } +} + +export { SessionIdCache }; diff --git a/src/common/utils/session-id-generator.ts b/src/common/utils/session-id-generator.ts new file mode 100644 index 000000000..8061c5482 --- /dev/null +++ b/src/common/utils/session-id-generator.ts @@ -0,0 +1,28 @@ +import { Utils } from '@playkit-js/playkit-js'; + +class SessionIdGenerator { + private static _value: string = ''; + + private static init(): void { + SessionIdGenerator._value = `${Utils.Generator.guid()}:${Utils.Generator.guid()}`; + } + + public static next(): string { + if (!SessionIdGenerator._value) { + this.init(); + return SessionIdGenerator._value; + } + + const next = SessionIdGenerator._value; + + const secondGuidInSessionIdRegex = /:((?:[a-z0-9]|-)*)/i; + const secondGuidInSessionId = secondGuidInSessionIdRegex.exec(next); + if (secondGuidInSessionId && secondGuidInSessionId[1]) { + SessionIdGenerator._value = next.replace(secondGuidInSessionId[1], Utils.Generator.guid()); + } + + return next; + } +} + +export { SessionIdGenerator }; diff --git a/src/common/utils/setup-helpers.ts b/src/common/utils/setup-helpers.ts index 68f246d29..89dfaa51a 100644 --- a/src/common/utils/setup-helpers.ts +++ b/src/common/utils/setup-helpers.ts @@ -23,6 +23,7 @@ import SessionStorageManager from '../storage/session-storage-manager'; import { BaseStorageManager } from '../storage/base-storage-manager'; import { BasePlugin } from '../plugins'; import { KalturaPlayerConfig, LegacyPartialKPOptionsObject, PartialKPOptionsObject, PluginsConfig, PlaybackConfig } from '../../types'; +import { SessionIdGenerator } from './session-id-generator'; const setupMessages: Array = []; const CONTAINER_CLASS_NAME: string = 'kaltura-player-container'; @@ -99,7 +100,7 @@ function validateProviderConfig(options: KalturaPlayerConfig): void { source.url = addProductVersion(source.url, productVersion); source.url = addReferrer(source.url); source.url = addClientTag(source.url, productVersion); - source.url = updateSessionIdInUrl(source.url, Utils.Generator.guid() + ':' + Utils.Generator.guid()); + source.url = updateSessionIdInUrl(null, source.url, SessionIdGenerator.next()); navigator.sendBeacon && navigator.sendBeacon(source.url); } } diff --git a/src/kaltura-player.ts b/src/kaltura-player.ts index 0743787ff..b2deaad30 100644 --- a/src/kaltura-player.ts +++ b/src/kaltura-player.ts @@ -77,6 +77,7 @@ import { MediaCapabilitiesObject } from './types'; import getErrorCategory from './common/utils/error-helper'; +import { SessionIdCache } from './common/utils/session-id-cache'; export class KalturaPlayer extends FakeEventTarget { private static _logger: any = getLogger('KalturaPlayer' + Utils.Generator.uniqueId(5)); @@ -106,10 +107,12 @@ export class KalturaPlayer extends FakeEventTarget { private _serviceProvider: ServiceProvider; private _isVisible: boolean = false; private _autoPaused: boolean = false; + private _sessionIdCache: SessionIdCache | null = null; constructor(options: KalturaPlayerConfig) { super(); const { sources, plugins } = options; + this._sessionIdCache = new SessionIdCache(); this._configEvaluator = new ConfigEvaluator(); this._configEvaluator.evaluatePluginsConfig(plugins, options); this._playbackStart = false; @@ -369,6 +372,7 @@ export class KalturaPlayer extends FakeEventTarget { if (targetContainer && targetContainer.parentNode) { Utils.Dom.removeChild(targetContainer.parentNode, targetContainer); } + this._sessionIdCache?.clear(); } public isLive(): boolean { @@ -1166,4 +1170,12 @@ export class KalturaPlayer extends FakeEventTarget { public async getMediaCapabilities(hevcConfig?: HEVCConfigObject): Promise { return getMediaCapabilities(hevcConfig); } + + public setCachedUrls(urls: string[]): void { + this._localPlayer.setCachedUrls(urls); + } + + public get sessionIdCache(): SessionIdCache | null { + return this._sessionIdCache; + } } diff --git a/tests/e2e/common/utils/kaltura-params.spec.ts b/tests/e2e/common/utils/kaltura-params.spec.ts index 00cd185af..7edcd2523 100644 --- a/tests/e2e/common/utils/kaltura-params.spec.ts +++ b/tests/e2e/common/utils/kaltura-params.spec.ts @@ -9,6 +9,7 @@ import { handleSessionId, updateSessionIdInUrl } from '../../../../src/common/utils/kaltura-params'; +import { SessionIdGenerator } from '../../../../src/common/utils/session-id-generator'; class Player { public set sessionId(s) { @@ -147,22 +148,104 @@ describe('addKalturaParams', () => { describe('handleSessionId', () => { const sessionIdRegex = /(?:[a-z0-9]|-)*:(?:[a-z0-9]|-)*/i; - it('should add the player session id', () => { - player.config = { session: {} }; + + it('should generate add a new session id', () => { + const nextSessionId = '5cc03aa6-c58f-3220-b548-2a698aa54830:33e6d80e-63b3-108a-091d-ccc15998f85b'; + SessionIdGenerator._value = nextSessionId; + player.config = { + session: { + id: '' + } + }; + player.playlist = { + items: [] + }; + sessionIdRegex.test(player.config.session.id).should.be.false; handleSessionId(player, player.config); sessionIdRegex.test(player.config.session.id).should.be.true; + expect(player.config.session.id).to.equal(nextSessionId); }); - it('should update the player session id', () => { + it('should update existing session id if not in playlist mode and there is no active source', () => { + const nextSessionId = '5cc03aa6-c58f-3220-b548-2a698aa54830:33e6d80e-63b3-108a-091d-ccc15998f85b'; + SessionIdGenerator._value = nextSessionId; player.config = { session: { - id: '5cc03aa6-c58f-3220-b548-2a698aa54830:33e6d80e-63b3-108a-091d-ccc15998f85b' + id: 'abc' + } + }; + player.playlist = { + items: [] + }; + sessionIdRegex.test(player.config.session.id).should.be.false; + handleSessionId(player, player.config); + sessionIdRegex.test(player.config.session.id).should.be.true; + expect(player.config.session.id).to.equal(nextSessionId); + }); + + it('should not update session id if in playlist mode and there is no active entry', () => { + SessionIdGenerator._value = '5cc03aa6-c58f-3220-b548-2a698aa54830:33e6d80e-63b3-108a-091d-ccc15998f85b'; + player.config = { + session: { + id: 'abc' + } + }; + player.playlist = { + items: [1] + }; + sessionIdRegex.test(player.config.session.id).should.be.false; + handleSessionId(player, player.config); + sessionIdRegex.test(player.config.session.id).should.be.false; + }); + it('should cache session id when generating a new id in playlist mode', () => { + const nextSessionId = '5cc03aa6-c58f-3220-b548-2a698aa54830:33e6d80e-63b3-108a-091d-ccc15998f85b'; + SessionIdGenerator._value = nextSessionId; + player.config = { + session: { + id: '' + }, + sources: { + id: '123' + } + }; + player.playlist = { + items: [1] + }; + player.sessionIdCache = new Map(); + + sessionIdRegex.test(player.config.session.id).should.be.false; + handleSessionId(player, player.config); + sessionIdRegex.test(player.config.session.id).should.be.true; + player.config.session.id = 'abc'; + sessionIdRegex.test(player.config.session.id).should.be.false; + handleSessionId(player, player.config); + sessionIdRegex.test(player.config.session.id).should.be.true; + expect(player.config.session.id).to.equal(nextSessionId); + }); + it('should cache session id if in playlist mode and there is an active entry', () => { + const nextSessionId = '5cc03aa6-c58f-3220-b548-2a698aa54830:33e6d80e-63b3-108a-091d-ccc15998f85b'; + SessionIdGenerator._value = nextSessionId; + player.config = { + session: { + id: 'abc' + }, + sources: { + id: '123' } }; + player.playlist = { + items: [1] + }; + player.sessionIdCache = new Map(); + + sessionIdRegex.test(player.config.session.id).should.be.false; handleSessionId(player, player.config); sessionIdRegex.test(player.config.session.id).should.be.true; - (player.config.session.id.indexOf('5cc03aa6-c58f-3220-b548-2a698aa54830:') > -1).should.be.true; - (player.config.session.id.indexOf('33e6d80e-63b3-108a-091d-ccc15998f85b') > -1).should.be.false; + player.config.session.id = 'def'; + sessionIdRegex.test(player.config.session.id).should.be.false; + handleSessionId(player, player.config); + sessionIdRegex.test(player.config.session.id).should.be.true; + expect(player.config.session.id).to.equal(nextSessionId); }); }); @@ -174,7 +257,7 @@ describe('updateSessionIdInUrl', () => { id: '5cc03aa6-c58f-3220-b548-2a698aa54830:33e6d80e-63b3-108a-091d-ccc15998f85b' } }; - source.url = updateSessionIdInUrl(source.url, player.config.session.id); + source.url = updateSessionIdInUrl(null, source.url, player.config.session.id); source.url.should.be.equal('a/b/c/playmanifest/source?playSessionId=' + player.config.session.id); }); @@ -185,7 +268,7 @@ describe('updateSessionIdInUrl', () => { id: '5cc03aa6-c58f-3220-b548-2a698aa54830:33e6d80e-63b3-108a-091d-ccc15998f85b' } }; - source.url = updateSessionIdInUrl(source.url, player.config.session.id); + source.url = updateSessionIdInUrl(null, source.url, player.config.session.id); source.url.should.be.equal('a/b/c/playmanifest/source?a&playSessionId=' + player.config.session.id); }); @@ -198,7 +281,7 @@ describe('updateSessionIdInUrl', () => { id: '5cc03aa6-c58f-3220-b548-2a698aa54830:33e6d80e-63b3-108a-091d-ccc15998f85b' } }; - source.url = updateSessionIdInUrl(source.url, player.config.session.id); + source.url = updateSessionIdInUrl(null, source.url, player.config.session.id); source.url.should.be.equal('a/b/c/playmanifest/source?playSessionId=' + player.config.session.id); }); @@ -211,7 +294,7 @@ describe('updateSessionIdInUrl', () => { id: '5cc03aa6-c58f-3220-b548-2a698aa54830:33e6d80e-63b3-108a-091d-ccc15998f85b' } }; - source.url = updateSessionIdInUrl(source.url, player.config.session.id); + source.url = updateSessionIdInUrl(null, source.url, player.config.session.id); source.url.should.be.equal('a/b/c/playmanifest/source?a&playSessionId=' + player.config.session.id); }); @@ -224,9 +307,26 @@ describe('updateSessionIdInUrl', () => { id: '5cc03aa6-c58f-3220-b548-2a698aa54830:33e6d80e-63b3-108a-091d-ccc15998f85b' } }; - source.url = updateSessionIdInUrl(source.url, player.config.session.id, 'testId='); + source.url = updateSessionIdInUrl(null, source.url, player.config.session.id, 'testId='); source.url.should.be.equal('a/b/c/playmanifest/source?testId=' + player.config.session.id); }); + it('should not update session id in url if we are in playlist mode', () => { + const source = { + url: 'a/b/c/playmanifest/source?a&playSessionId=5cc03aa6-c58f-3220-b548-2a698aa54830:b5391ed8-be5d-3a71-e157-f23a1b434121' + }; + player.config = { + session: { + id: '5cc03aa6-c58f-3220-b548-2a698aa54830:33e6d80e-63b3-108a-091d-ccc15998f85b' + } + }; + const playerMock = { + playlist: { + items: [1] + } + }; + source.url = updateSessionIdInUrl(playerMock, source.url, player.config.session.id, 'testId='); + source.url.should.not.be.equal('a/b/c/playmanifest/source?testId=' + player.config.session.id); + }); }); describe('addReferrer', () => { diff --git a/yarn.lock b/yarn.lock index aaa329e45..7f35efd82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5199,10 +5199,10 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -shaka-player@4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/shaka-player/-/shaka-player-4.7.0.tgz#5bb0a60c1b7c2a8a3c2d1ff82e632c3c000219c3" - integrity sha512-utR9hKMt8GiGv7EDC8/nh8F1c4KeVGa4Wd8k6h+g2Ylks0m9//kvxvXkQnYAGJRtdql/CJC9Ur8YQ/G+kTwoiQ== +shaka-player@4.8.11: + version "4.8.11" + resolved "https://registry.yarnpkg.com/shaka-player/-/shaka-player-4.8.11.tgz#f67df889ac859875a2f53645840c39b67b568192" + integrity sha512-PXrizP6bWi6Gzjk5B7aSfBiU81di1kPd8LEBIzdaNvwINjZSMYL+lDzEWeLTIoyZCjb3l1WoJJTGLex8mD3dmg== dependencies: eme-encryption-scheme-polyfill "^2.1.1"