Skip to content

Commit

Permalink
fix(FEC-13786): Use pending-cues manager for simulive (#72)
Browse files Browse the repository at this point in the history
* fix(FEC-13786): use pending-cues manager for simulive

* fix tests
  • Loading branch information
semarche-kaltura authored Apr 17, 2024
1 parent 05c3fc2 commit 733beee
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
16 changes: 11 additions & 5 deletions src/providers/live/live-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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');
Expand Down Expand Up @@ -401,18 +405,20 @@ 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<string, any>) => {
if (!data) {
this._logger.warn("Simulive cue points doRequest doesn't have data");
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});
}
});
}
Expand Down
87 changes: 87 additions & 0 deletions src/providers/pending-cues-manager.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
79 changes: 52 additions & 27 deletions src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,33 +25,27 @@ 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;
this._logger = logger;
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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -118,7 +116,7 @@ export class Provider {
});
}

protected _handleThumbResponse(data: Map<string, any>, cuepointOffset: number = 0) {
protected _handleThumbResponse(data: Map<string, any>, {cuepointOffset = 0, ...options}: any = {}) {
const replaceAssetUrl = (baseThumbAssetUrl: string) => (thumbCuePoint: KalturaThumbCuePoint) => {
return makeAssetUrl(baseThumbAssetUrl, thumbCuePoint.assetId);
};
Expand All @@ -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<KalturaThumbCuePoint> = thumbCuePointsLoader?.response.thumbCuePoints || [];
Expand Down Expand Up @@ -202,7 +200,7 @@ export class Provider {
}
}

protected _handleHotspotResponse(data: Map<string, any>, cuepointOffset: number = 0) {
protected _handleHotspotResponse(data: Map<string, any>, {cuepointOffset = 0, ...options}: any = {}) {
const createCuePointList = (hotspotCuePoints: Array<KalturaHotspotCuePoint>) => {
return hotspotCuePoints.map((hotspotCuePoint: KalturaHotspotCuePoint) => {
return {
Expand All @@ -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() {
Expand All @@ -239,5 +260,9 @@ export class Provider {
this._dataAggregator.destroy();
this._dataAggregator = null;
}
if (this._pendingCuesManager) {
this._pendingCuesManager.destroy();
this._pendingCuesManager = null;
}
}
}
Loading

0 comments on commit 733beee

Please sign in to comment.