diff --git a/src/providers/dataAggregator.ts b/src/providers/data-aggregator.ts similarity index 90% rename from src/providers/dataAggregator.ts rename to src/providers/data-aggregator.ts index 2122eec..f70b63b 100644 --- a/src/providers/dataAggregator.ts +++ b/src/providers/data-aggregator.ts @@ -6,11 +6,12 @@ interface DataAggregatorProps { export class DataAggregator { private _data: any[] = []; private _timerId: NodeJS.Timeout | null = null; - private _throttleTimeout = 250; + private _throttleTimeout: number; private _onTimeoutFn: (data: any) => void; constructor(options: DataAggregatorProps) { this._onTimeoutFn = options.onTimeoutFn; + this._throttleTimeout = options.throttleTimeout || 350; } public addData(data: any): void { diff --git a/src/providers/live/live-provider.ts b/src/providers/live/live-provider.ts index 1353016..8095019 100644 --- a/src/providers/live/live-provider.ts +++ b/src/providers/live/live-provider.ts @@ -77,7 +77,9 @@ export class LiveProvider extends Provider { this._id3Timestamp = id3Timestamp; } - if (!id3Data.clipId || !id3Data.setId) return; + if (!id3Data.clipId || !id3Data.setId) { + return; + } const [partType, originalEntryId, clipStartTimestamp] = id3Data.clipId.split('-'); const cuepointOffset = this._getSimuliveCuepointOffset( @@ -86,7 +88,9 @@ export class LiveProvider extends Provider { id3Data.clipId, id3TagCues[id3TagCues.length - 1].startTime ); - if (cuepointOffset === null) return; + if (cuepointOffset === null) { + return; + } const textTracks = [...(this._player.getVideoElement().textTracks as any)]; const cuepointsTrack = textTracks.find(t => t.label === 'CuePoints'); @@ -401,7 +405,9 @@ export class LiveProvider extends Provider { requests.push({loader: HotspotLoader, params: {entryId: originalEntryId}}); } - if (!requests.length) return; + if (!requests.length) { + return; + } this._player.provider.doRequest(requests).then((data: Map) => { if (!data) { @@ -409,10 +415,10 @@ export class LiveProvider extends Provider { return; } if (data.has(ThumbLoader.id)) { - this._handleThumbResponse(data, cuepointOffset); + this._handleThumbResponse(data, {cuepointOffset, useDataAggregator: false, usePendingQueManager: true}); } if (data.has(HotspotLoader.id)) { - this._handleHotspotResponse(data, cuepointOffset); + this._handleHotspotResponse(data, {cuepointOffset, useDataAggregator: false, usePendingQueManager: true}); } }); } diff --git a/src/providers/pending-cues-manager.ts b/src/providers/pending-cues-manager.ts new file mode 100644 index 0000000..0e01d2e --- /dev/null +++ b/src/providers/pending-cues-manager.ts @@ -0,0 +1,87 @@ +import Player = KalturaPlayerTypes.Player; +import EventManager = KalturaPlayerTypes.EventManager; +import {CuePoint} from '../types'; + +interface PendingCuesManagerProps { + timeUpdateDelta?: number; + player: Player; + eventManager: EventManager; +} + +export class PendingCuesManager { + protected _eventManager: EventManager; + protected _player: Player; + private _timeUpdateDelta: number; + private _lastPositionCuePointsPushed: number = 0; + private _pendingCuePointsData: CuePoint[][] = []; + private _activeListener = false; + + constructor(options: PendingCuesManagerProps) { + this._player = options.player; + this._timeUpdateDelta = options.timeUpdateDelta || 400; + this._eventManager = options.eventManager; + } + + public addCuePoint(cp: CuePoint[]): void { + this._addListener(); + this._pendingCuePointsData.push(cp); + } + + public checkCues(): void { + this._pushCuePointsToPlayer(); + } + + private _pushCuePointsToPlayer(): void { + for (const pendingCuePoints of this._pendingCuePointsData) { + let cpToAdd: any[] = []; + for (let index = 0; index < pendingCuePoints.length; index++) { + const cp = pendingCuePoints[index]; + if (Math.floor(cp.startTime) <= this._player.currentTime) { + cpToAdd.push(cp); + } else { + // next cue points will have greater start time, no need to continue the loop + break; + } + } + // remove cue points from pending array, that are going to be pushed + pendingCuePoints.splice(0, cpToAdd.length); + if (cpToAdd.length) { + this._player.cuePointManager.addCuePoints(cpToAdd); + } + } + + if (this._pendingCuePointsData.every(pendingCues => pendingCues.length === 0)) { + // clear _pendingCuePointsData and remove listener if pending arrays are empty + this._pendingCuePointsData = []; + this._removeListener(); + } + } + + private _onTimeUpdate = (): void => { + if (this._player.currentTime * 1000 - this._lastPositionCuePointsPushed >= this._timeUpdateDelta) { + this._pushCuePointsToPlayer(); + // Update the last time that cue points were pushed to player + this._lastPositionCuePointsPushed = this._player.currentTime * 1000; + } + }; + + private _addListener = (): void => { + if (!this._activeListener) { + this._eventManager.listen(this._player, this._player.Event.TIME_UPDATE, this._onTimeUpdate); + this._activeListener = true; + } + }; + + private _removeListener = (): void => { + if (this._activeListener) { + this._eventManager.unlisten(this._player, this._player.Event.TIME_UPDATE, this._onTimeUpdate); + this._activeListener = false; + } + }; + + public destroy(): void { + this._lastPositionCuePointsPushed = 0; + this._pendingCuePointsData = []; + this._removeListener(); + } +} diff --git a/src/providers/provider.ts b/src/providers/provider.ts index 2caa704..f7b9ae7 100644 --- a/src/providers/provider.ts +++ b/src/providers/provider.ts @@ -6,7 +6,13 @@ import EventManager = KalturaPlayerTypes.EventManager; import {KalturaHotspotCuePoint, KalturaThumbCuePoint} from './vod/response-types'; import {HotspotLoader, ThumbLoader, ThumbUrlLoader} from './common/'; import {makeAssetUrl, generateThumb, sortArrayBy} from './utils'; -import {DataAggregator} from './dataAggregator'; +import {DataAggregator} from './data-aggregator'; +import {PendingCuesManager} from './pending-cues-manager'; + +export interface AddCuePointToPlayerOptions { + useDataAggregator?: boolean; + usePendingQueManager?: boolean; +} export interface ProviderRequest { loader: Function; @@ -19,6 +25,7 @@ export class Provider { protected _logger: Logger; public cuePointManager: CuePointManager | null = null; private _dataAggregator: DataAggregator | null = null; + private _pendingCuesManager: PendingCuesManager | null = null; constructor(player: Player, eventManager: EventManager, logger: Logger, types: CuepointTypeMap) { this._types = types; @@ -26,26 +33,19 @@ export class Provider { this._player = player; this._eventManager = eventManager; this._logger = logger; - if (this._useDataAggregator()) { + + if (this._player.isLive() && !this._player.isDvr()) { // for live entry without DVR use additional processing of cues (filter out cues behind Live Edge) - const onTimeoutFn = (collectedData: CuePoint[]) => { - // filter out duplicates - const collectedDataMap = new Map(); - collectedData.forEach(cuePoint => { - collectedDataMap.set(cuePoint.id, cuePoint); - }); - // for stream witout DVR filter out cues behind Live Edge - collectedDataMap.forEach(cue => { - if (cue.endTime === Number.MAX_SAFE_INTEGER) { - this._player.cuePointManager.addCuePoints([cue]); - } - }); - }; - this._dataAggregator = new DataAggregator({onTimeoutFn}); + this._initDataAggregator(); } + + this._pendingCuesManager = new PendingCuesManager({player: this._player, eventManager: this._eventManager}); } - protected _addCuePointToPlayer(cuePoints: any[], useDataAggregator = Boolean(this._dataAggregator)) { + protected _addCuePointToPlayer( + cuePoints: any[], + {useDataAggregator = Boolean(this._dataAggregator), usePendingQueManager = this._isPreventSeek()}: AddCuePointToPlayerOptions = {} + ) { if (!cuePoints.length) { return; } @@ -62,15 +62,13 @@ export class Provider { playerCuePoints.forEach(cuePoint => { this._dataAggregator!.addData(cuePoint); }); + } else if (usePendingQueManager) { + this._pendingCuesManager?.addCuePoint(playerCuePoints); } else { this._player.cuePointManager.addCuePoints(playerCuePoints); } } - protected _addCuePointsData(cp: any[], useDataAggregator = false): void { - this._addCuePointToPlayer(cp, useDataAggregator); - } - protected _shiftCuePoints(cuePoints: any[], seekFrom: number): void { cuePoints.forEach((cp: any) => { cp.startTime = cp.startTime - seekFrom; @@ -118,7 +116,7 @@ export class Provider { }); } - protected _handleThumbResponse(data: Map, cuepointOffset: number = 0) { + protected _handleThumbResponse(data: Map, {cuepointOffset = 0, ...options}: any = {}) { const replaceAssetUrl = (baseThumbAssetUrl: string) => (thumbCuePoint: KalturaThumbCuePoint) => { return makeAssetUrl(baseThumbAssetUrl, thumbCuePoint.assetId); }; @@ -143,7 +141,7 @@ export class Provider { cuePoints = sortArrayBy(cuePoints, 'startTime'); cuePoints = this._fixCuePointsEndTime(cuePoints); cuePoints = this._filterAndShiftCuePoints(cuePoints); - this._addCuePointsData(cuePoints, false); + this._addCuePointToPlayer(cuePoints, options); }; const thumbCuePointsLoader: ThumbLoader = data.get(ThumbLoader.id); const thumbCuePoints: Array = thumbCuePointsLoader?.response.thumbCuePoints || []; @@ -202,7 +200,7 @@ export class Provider { } } - protected _handleHotspotResponse(data: Map, cuepointOffset: number = 0) { + protected _handleHotspotResponse(data: Map, {cuepointOffset = 0, ...options}: any = {}) { const createCuePointList = (hotspotCuePoints: Array) => { return hotspotCuePoints.map((hotspotCuePoint: KalturaHotspotCuePoint) => { return { @@ -223,12 +221,35 @@ export class Provider { let cuePoints = createCuePointList(hotspotCuePoints); cuePoints = this._filterAndShiftCuePoints(cuePoints); cuePoints = sortArrayBy(cuePoints, 'startTime', 'createdAt'); - this._addCuePointsData(cuePoints, false); + this._addCuePointToPlayer(cuePoints, options); } } - private _useDataAggregator() { - return this._player.isLive() && !this._player.isDvr(); + protected _isPreventSeek(): boolean { + return this._player.ui.store.getState().seekbar.isPreventSeek; + } + + protected _maybeForcePushingCuePoints = () => { + if (this._isPreventSeek() && this._player.paused) { + this._pendingCuesManager?.checkCues(); + } + }; + + private _initDataAggregator() { + const onTimeoutFn = (collectedData: CuePoint[]) => { + // filter out duplicates + const collectedDataMap = new Map(); + collectedData.forEach(cuePoint => { + collectedDataMap.set(cuePoint.id, cuePoint); + }); + // for stream witout DVR filter out cues behind Live Edge + collectedDataMap.forEach(cue => { + if (cue.endTime === Number.MAX_SAFE_INTEGER) { + this._player.cuePointManager.addCuePoints([cue]); + } + }); + }; + this._dataAggregator = new DataAggregator({onTimeoutFn}); } public destroy() { @@ -239,5 +260,9 @@ export class Provider { this._dataAggregator.destroy(); this._dataAggregator = null; } + if (this._pendingCuesManager) { + this._pendingCuesManager.destroy(); + this._pendingCuesManager = null; + } } } diff --git a/src/providers/vod/vod-provider.ts b/src/providers/vod/vod-provider.ts index c243d34..25da41e 100644 --- a/src/providers/vod/vod-provider.ts +++ b/src/providers/vod/vod-provider.ts @@ -1,6 +1,6 @@ import {Provider, ProviderRequest} from '../provider'; import {ThumbLoader} from '../common/thumb-loader'; -import {KalturaQuizQuestionCuePoint, KalturaThumbCuePoint, KalturaCodeCuePoint, KalturaHotspotCuePoint, KalturaCaption} from './response-types'; +import {KalturaQuizQuestionCuePoint, KalturaCodeCuePoint, KalturaCaption} from './response-types'; import {KalturaCuePointType, KalturaThumbCuePointSubType, CuepointTypeMap} from '../../types'; import Player = KalturaPlayerTypes.Player; import Logger = KalturaPlayerTypes.Logger; @@ -12,12 +12,9 @@ import {QuizQuestionLoader} from './quiz-question-loader'; import {HotspotLoader} from '../common/hotspot-loader'; import {CaptionLoader} from './caption-loader'; -const TIME_UPDATE_DELTA_MS: number = 400; export class VodProvider extends Provider { private _fetchedCaptionKeys: Array = []; private _fetchingCaptionKey: string | null = null; - private _pendingCuePointsData: any[] = []; - private _lastPositionCuePointsPushed: number = 0; constructor(player: Player, eventManager: EventManager, logger: Logger, types: CuepointTypeMap) { super(player, eventManager, logger, types); @@ -32,47 +29,6 @@ export class VodProvider extends Provider { // handle change of caption track this._eventManager.listen(this._player, this._player.Event.TEXT_TRACK_CHANGED, this._handleLanguageChange); } - if (this._isPreventSeek()) { - this._eventManager.listen(this._player, this._player.Event.TIME_UPDATE, this._onTimeUpdate); - } - } - - private _isPreventSeek(): boolean { - return this._player.ui.store.getState().seekbar.isPreventSeek; - } - - private _onTimeUpdate = (): void => { - if (this._player.currentTime * 1000 - this._lastPositionCuePointsPushed >= TIME_UPDATE_DELTA_MS) { - this._pushCuePointsToPlayer(); - // Update the last time that cue points were pushed to player - this._lastPositionCuePointsPushed = this._player.currentTime * 1000; - } - }; - - private _pushCuePointsToPlayer(): void { - for (const pendingCuePoints of this._pendingCuePointsData) { - let cpToAdd: any[] = []; - for (let index = 0; index < pendingCuePoints.length; index++) { - const cp = pendingCuePoints[index]; - if (Math.floor(cp.startTime) <= this._player.currentTime) { - cpToAdd.push(cp); - } else { - // next cue points will have greater start time, no need to continue the loop - break; - } - } - // remove cue points from pending array, that are going to be pushed - pendingCuePoints.splice(0, cpToAdd.length); - this._addCuePointToPlayer(cpToAdd); - } - } - - protected _addCuePointsData(cp: any[]): void { - if (this._isPreventSeek()) { - this._pendingCuePointsData.push(cp); - } else { - super._addCuePointsData(cp); - } } private _removeListeners() { @@ -80,9 +36,6 @@ export class VodProvider extends Provider { this._eventManager.unlisten(this._player, this._player.Event.TEXT_TRACK_ADDED, this._handleLanguageChange); this._eventManager.unlisten(this._player, this._player.Event.TEXT_TRACK_CHANGED, this._handleLanguageChange); } - if (this._isPreventSeek()) { - this._eventManager.unlisten(this._player, this._player.Event.TIME_UPDATE, this._onTimeUpdate); - } } private _fetchVodData() { @@ -154,12 +107,6 @@ export class VodProvider extends Provider { } }; - private _maybeForcePushingCuePoints = () => { - if (this._isPreventSeek() && this._player.paused) { - this._pushCuePointsToPlayer(); - } - }; - private _loadCaptions = (captonSource: KalturaCaptionSource) => { const captionKey = `${captonSource.language}-${captonSource.label}`; if (this._fetchedCaptionKeys.includes(captionKey) || this._fetchingCaptionKey === captionKey) { @@ -200,7 +147,7 @@ export class VodProvider extends Provider { // filter empty captions cuePoints = cuePoints.filter(cue => cue.text); cuePoints = this._filterAndShiftCuePoints(cuePoints); - this._addCuePointsData(cuePoints); + this._addCuePointToPlayer(cuePoints); // mark captions as fetched this._fetchedCaptionKeys.push(captionKey); // after captions are loaded, might need to manually push cue points @@ -288,7 +235,5 @@ export class VodProvider extends Provider { this._fetchedCaptionKeys = []; this._fetchingCaptionKey = null; this._removeListeners(); - this._pendingCuePointsData = []; - this._lastPositionCuePointsPushed = 0; } } diff --git a/test/src/providers/live/live-provider.spec.js b/test/src/providers/live/live-provider.spec.js index ce82d7d..ad1a3fe 100644 --- a/test/src/providers/live/live-provider.spec.js +++ b/test/src/providers/live/live-provider.spec.js @@ -128,7 +128,12 @@ describe('Check Live provider', () => { currentTime: 5, config: {session: {ks: 'test_ks'}}, provider: {env: {serviceUrl: 'test_url'}}, - cuePointManager: {addCuePoints} + cuePointManager: {addCuePoints}, + ui: { + store: { + getState: () => ({seekbar: {isPreventSeek: false}}) + } + } }; liveProvider._currentTimeLive = 100; liveProvider._baseThumbAssetUrl = 'http://test.te/thumbAssetId/1_initialId/test_ks'; @@ -168,7 +173,12 @@ describe('Check Live provider', () => { liveProvider._player = { currentTime: 5, cuePointManager: {addCuePoints}, - isDvr: () => true + isDvr: () => true, + ui: { + store: { + getState: () => ({seekbar: {isPreventSeek: false}}) + } + } }; liveProvider._currentTimeLive = 100; liveProvider._prepareViewChangeCuePoints({