@applicaster/zapp-react-native-utils 13.0.0-rc.15 → 13.0.0-rc.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,7 +14,7 @@ export class AnalyticPlayerListener
14
14
  static analyticsListenerID = "analytics-listener";
15
15
  private analyticsTracker: PlayerAnalyticsTracker;
16
16
  private player: Player;
17
-
17
+ private isSeeking: boolean = false;
18
18
  private onVideoLoadCalled; // android only
19
19
 
20
20
  constructor(props: Props) {
@@ -40,10 +40,27 @@ export class AnalyticPlayerListener
40
40
  };
41
41
 
42
42
  onPlayerPause = (event) => {
43
+ if (this.player?.seekController?.isSeeking()) {
44
+ this.isSeeking = true;
45
+
46
+ return;
47
+ }
48
+
43
49
  this.handleAnalyticEvent(PLAYBACK_EVENT.pause, event);
44
50
  };
45
51
 
46
52
  onPlayerResume = (event) => {
53
+ if (this.player?.seekController?.isSeeking()) {
54
+ return;
55
+ }
56
+
57
+ if (this.isSeeking) {
58
+ // Fallback due to player resume event been async and arriving with delays
59
+ this.isSeeking = false;
60
+
61
+ return;
62
+ }
63
+
47
64
  this.handleAnalyticEvent(PLAYBACK_EVENT.play, event);
48
65
  };
49
66
 
@@ -11,8 +11,8 @@ import {
11
11
  haveSameParentBeforeRoot,
12
12
  } from "./treeDataStructure/Utils";
13
13
  import { subscriber } from "../../functionUtils";
14
- import * as FOCUS_EVENTS from "./events";
15
14
  import { coreLogger } from "../../logger";
15
+ import { ACTION } from "./utils/enums";
16
16
  const logger = coreLogger.addSubsystem("focusManager");
17
17
 
18
18
  const isFocusEnabled = (focusableItem): boolean => {
@@ -167,13 +167,82 @@ export const focusManager = (function () {
167
167
  }
168
168
 
169
169
  /**
170
- * returns the current focusable's group
171
- * @private
172
- * @returns {React.Component} the current focus
170
+ * Computes the difference in parent groups between two nodes.
171
+ * It returns the IDs of parents that are present in the current node but not in the previous node,
172
+ * as well as the IDs of parents that are present in the previous node but not in the current node.
173
+ *
174
+ * @param {string} currentNodeID - The ID of the current node.
175
+ * @param {string} previousNodeID - The ID of the previous node.
176
+ * @returns {Object} An object containing the differences in parent IDs:
177
+ * - {Array<string>} currentNodeParentsIDs: Parent IDs present in the current node but not in the previous node.
178
+ * - {Array<string>} previousNodeParentsIDs: Parent IDs present in the previous node but not in the current node.
173
179
  */
174
- function getCurrentGroup() {
175
- return getGroupById(R.path(["props", "groupId"], getCurrentFocus()));
176
- }
180
+ const getParentDiffs = (currentNodeID, previousNodeID) => {
181
+ const currentNodeParentsIDs =
182
+ focusableTree.getParentGroupIds(currentNodeID);
183
+
184
+ const previousNodeParentsIDs =
185
+ focusableTree.getParentGroupIds(previousNodeID);
186
+
187
+ return {
188
+ currentNodeParentsIDs: R.difference(
189
+ currentNodeParentsIDs,
190
+ previousNodeParentsIDs
191
+ ),
192
+ previousNodeParentsIDs: R.difference(
193
+ previousNodeParentsIDs,
194
+ currentNodeParentsIDs
195
+ ),
196
+ };
197
+ };
198
+
199
+ /**
200
+ * Updates the focus state of nodes identified by their IDs.
201
+ * For each ID, if `setFocus` is true, the node will receive focus;
202
+ * otherwise, it will lose focus.
203
+ *
204
+ * @param {Array<string>} ids - An array of node IDs to update.
205
+ * @param {boolean} setFocus - A flag indicating whether to set focus (true) or blur (false) on the nodes.
206
+ */
207
+ const updateNodeFocus = (ids, action) => {
208
+ if (!ids || ids.length === 0) {
209
+ return; // Nothing to do
210
+ }
211
+
212
+ const startNode = focusableTree.findInTree(ids[0]);
213
+
214
+ if (!startNode) {
215
+ return; // Start node not found
216
+ }
217
+
218
+ const endNodeId = ids[ids.length - 1]; // Get the ID of the end node
219
+
220
+ let currentNode = startNode;
221
+
222
+ // Function to apply the action (focus or blur)
223
+ const applyAction = (node) => {
224
+ if (node && node.component) {
225
+ if (action === "focus") {
226
+ node.component.setFocus();
227
+ } else if (action === "blur") {
228
+ node.component.setBlur();
229
+ }
230
+ }
231
+ };
232
+
233
+ // Apply action to the starting node
234
+ applyAction(currentNode);
235
+
236
+ // Traverse upwards
237
+ while (currentNode && currentNode.id !== endNodeId) {
238
+ currentNode = currentNode.parent;
239
+
240
+ if (currentNode) {
241
+ // check if current node exists, as parent might be root
242
+ applyAction(currentNode);
243
+ }
244
+ }
245
+ };
177
246
 
178
247
  /**
179
248
  * Sets the current focus to the provided id. If there is an existing focus, it will
@@ -181,34 +250,29 @@ export const focusManager = (function () {
181
250
  * @private
182
251
  * @param {String} id of the focusable to set as currentFocus
183
252
  * @param {Object} direction of the navigation, which led to this focus change
184
- * @param {Object} options
185
- * @param {Boolean} options.groupFocusedChanged tells wether the focus has moved
186
253
  * to another group or not. defaults to false
187
254
  */
188
- function setFocus(
189
- id: string,
190
- direction?: FocusManager.Web.Direction,
191
- options?: Partial<{ groupFocusedChanged: boolean }>
192
- ) {
255
+ function setFocus(id: string, direction?: FocusManager.Web.Direction) {
193
256
  if (focusDisabled) return false;
194
257
 
195
- const { groupFocusedChanged = false } = options || {};
196
-
197
258
  // due to optimisiation it's recommanded to set currentFocusNode before setFocus
198
259
  // is called, so there will be no delays on tree search.
199
260
  // If currentFocusNode is null or undefined we still want it to findInTree
200
261
  if (currentFocusNode?.id !== id) {
262
+ const { currentNodeParentsIDs, previousNodeParentsIDs } = getParentDiffs(
263
+ id,
264
+ currentFocusNode?.id
265
+ );
266
+
267
+ // Set focus on current node parents and blur on previous node parents
268
+ updateNodeFocus(currentNodeParentsIDs, ACTION.FOCUS);
269
+ updateNodeFocus(previousNodeParentsIDs, ACTION.BLUR);
270
+
201
271
  currentFocusNode = focusableTree.findInTree(id);
202
272
  }
203
273
 
204
274
  setLastFocusOnParentNode(currentFocusNode);
205
275
 
206
- const currentGroup = getCurrentGroup();
207
-
208
- if (groupFocusedChanged && currentGroup) {
209
- currentGroup.hasLostFocus(currentGroup, direction);
210
- }
211
-
212
276
  focus(direction);
213
277
  }
214
278
 
@@ -251,6 +315,7 @@ export const focusManager = (function () {
251
315
  !focusableItem.isGroup &&
252
316
  isFocusEnabled(focusableItem)
253
317
  ) {
318
+ // TODO: Move this to setFocus method
254
319
  blur();
255
320
 
256
321
  logger.log({
@@ -261,7 +326,7 @@ export const focusManager = (function () {
261
326
  });
262
327
 
263
328
  currentFocusNode = focusCandidate;
264
- focusableItem.willReceiveFocus(focusableItem, null);
329
+
265
330
  focusableItem && setFocus(focusCandidate.id, null);
266
331
 
267
332
  return { success: true };
@@ -448,20 +513,10 @@ export const focusManager = (function () {
448
513
  const nextRect = nextFocusable.component.getRect();
449
514
 
450
515
  if (nextRect && nextRect.width > 0) {
451
- nextFocusable.component.willReceiveFocus(
452
- nextFocusable.component,
453
- direction
454
- );
455
-
516
+ // TODO: Move this to setFocus method
456
517
  blur(direction);
457
518
 
458
- const groupChanged =
459
- nextFocusable.component.props.groupId !==
460
- currentFocusNode?.component?.props?.groupId;
461
-
462
- setFocus(nextFocusable.id, direction, {
463
- groupFocusedChanged: groupChanged,
464
- });
519
+ setFocus(nextFocusable.id, direction);
465
520
  }
466
521
  } else {
467
522
  currentFocusNode.component.failedLostFocus(currentFocusNode, direction);
@@ -481,34 +536,6 @@ export const focusManager = (function () {
481
536
 
482
537
  const { on, invokeHandler, removeHandler } = eventHandler;
483
538
 
484
- on(FOCUS_EVENTS.FOCUS, (focusable, direction) => {
485
- if (focusable.props.groupId) {
486
- const parent = focusableTree.findInTree(focusable.props.groupId);
487
- parent?.component?.onFocus(focusable.component, direction);
488
- }
489
- });
490
-
491
- on(FOCUS_EVENTS.BLUR, (focusable, direction) => {
492
- if (focusable.props.groupId) {
493
- const parent = focusableTree.findInTree(focusable.props.groupId);
494
- parent?.component?.onBlur(focusable.component, direction);
495
- }
496
- });
497
-
498
- on(FOCUS_EVENTS.HAS_LOST_FOCUS, (focusable, direction) => {
499
- if (focusable.props.groupId) {
500
- const parent = focusableTree.findInTree(focusable.props.groupId);
501
- parent?.component?.hasLostFocus(parent.component, direction);
502
- }
503
- });
504
-
505
- on(FOCUS_EVENTS.WILL_RECEIVE_FOCUS, (focusable, direction) => {
506
- if (focusable.props.groupId) {
507
- const parent = focusableTree.findInTree(focusable.props.groupId);
508
- parent?.component?.willReceiveFocus(focusable.component, direction);
509
- }
510
- });
511
-
512
539
  /**
513
540
  * this is the list of the functions available externally
514
541
  * when importing the focus manager
@@ -231,4 +231,27 @@ export class Tree {
231
231
 
232
232
  return retVal;
233
233
  }
234
+
235
+ /**
236
+ * Gets the IDs of all parent groups for a given node id.
237
+ * @public
238
+ * @param {String} nodeId - The id of the node to get parent IDs for.
239
+ * @returns {Array} An array of parent group IDs or an empty array if the node is not found.
240
+ */
241
+ getParentGroupIds(nodeId) {
242
+ const parentIds = [];
243
+ let currentNode = this.findInTree(nodeId);
244
+
245
+ if (!currentNode) {
246
+ return []; // Node not found, return an empty array
247
+ }
248
+
249
+ // Traverse up the tree, collecting parent IDs
250
+ while (currentNode.parent && currentNode.parent.id !== "root") {
251
+ parentIds.push(currentNode.parent.id);
252
+ currentNode = currentNode.parent;
253
+ }
254
+
255
+ return parentIds;
256
+ }
234
257
  }
@@ -0,0 +1,4 @@
1
+ export enum ACTION {
2
+ FOCUS = "focus",
3
+ BLUR = "blur",
4
+ }
@@ -77,6 +77,10 @@ const web = {
77
77
  Exit: { code: "Exit", keyCode: null },
78
78
  Escape: { code: "Escape", keyCode: 27 },
79
79
  EscapeDevice: { code: "Escape", keyCode: 27 },
80
+ // For Debugging
81
+ Z: { code: "KeyZ", keyCode: 90 }, // Hardware Backward Button
82
+ X: { code: "KeyX", keyCode: 88 }, // Hardware Enter Button
83
+ C: { code: "KeyC", keyCode: 67 }, // Hardware Forward Button
80
84
  };
81
85
 
82
86
  const android_tv = {
@@ -0,0 +1,68 @@
1
+ export type SeekKeyHandlerListener = {
2
+ onRewindPress: () => boolean;
3
+ onForwardPress: () => boolean;
4
+ onRewindPressOut: () => void;
5
+ onForwardPressOut: () => void;
6
+ };
7
+
8
+ export class KeyInputHandler {
9
+ private static instance: KeyInputHandler;
10
+
11
+ public static getInstance() {
12
+ if (!KeyInputHandler.instance) {
13
+ KeyInputHandler.instance = new KeyInputHandler();
14
+ }
15
+
16
+ return KeyInputHandler.instance;
17
+ }
18
+
19
+ constructor() {
20
+ this.listeners = [];
21
+ }
22
+
23
+ private listeners: SeekKeyHandlerListener[] = [];
24
+
25
+ addListener = (listener: SeekKeyHandlerListener) => {
26
+ this.listeners.splice(0, 0, listener);
27
+
28
+ return () => this.removeListener(listener);
29
+ };
30
+
31
+ removeListener = (listener: SeekKeyHandlerListener) => {
32
+ this.listeners = this.listeners.filter((l) => l !== listener);
33
+ };
34
+
35
+ onRewindPress = () => {
36
+ for (const element of this.listeners) {
37
+ if (element.onRewindPress()) {
38
+ break;
39
+ }
40
+ }
41
+ };
42
+
43
+ onForwardPress = () => {
44
+ for (const element of this.listeners) {
45
+ if (element.onForwardPress()) {
46
+ break;
47
+ }
48
+ }
49
+ };
50
+
51
+ // TODO: Event should be send to listeners who handled down event
52
+ onRewindPressOut = () => {
53
+ this.listeners.forEach((listener) => listener.onRewindPressOut());
54
+ };
55
+
56
+ onForwardPressOut = () => {
57
+ this.listeners.forEach((listener) => listener.onForwardPressOut());
58
+ };
59
+
60
+ getSeekMethods = () => {
61
+ return {
62
+ onForwardPress: this.onForwardPress,
63
+ onRewindPress: this.onRewindPress,
64
+ onForwardPressOut: this.onForwardPressOut,
65
+ onRewindPressOut: this.onRewindPressOut,
66
+ };
67
+ };
68
+ }
@@ -5,6 +5,7 @@ import { createLogger, utilsLogger } from "../../logger";
5
5
  import { getBoolFromConfigValue } from "../../configurationUtils";
6
6
  import { PlayerRole, SharedPlayerCallBacksKeys } from "./conts";
7
7
  import { OverlaysObserver } from "./OverlayObserver/OverlaysObserver";
8
+ import { TVSeekController } from "../../reactHooks/player/TVSeekControlller/TVSeekController";
8
9
 
9
10
  const logger = createLogger({
10
11
  category: "PlayerController",
@@ -49,6 +50,8 @@ export class Player {
49
50
  [key: string]: QuickBrickPlayer.SharedPlayerCallBacks;
50
51
  } = {};
51
52
 
53
+ public seekController?: TVSeekController | null;
54
+
52
55
  get entry(): ZappEntry | null {
53
56
  return this._entry;
54
57
  }
@@ -59,6 +62,8 @@ export class Player {
59
62
  }
60
63
 
61
64
  constructor(props) {
65
+ this.seekController = null;
66
+
62
67
  this.playerState = {
63
68
  // We want isLive null by default to be able not to show controls if we do not know type
64
69
  isLive: null,
@@ -91,6 +96,8 @@ export class Player {
91
96
  this.onVideoProgress = this.onVideoProgress.bind(this);
92
97
  this.onVideoLoad = this.onVideoLoad.bind(this);
93
98
  this.isFullScreenSupported = this.isFullScreenSupported.bind(this);
99
+ this.onPlayerSeekComplete = this.onPlayerSeekComplete.bind(this);
100
+
94
101
  this.entryObservable = null;
95
102
  }
96
103
 
@@ -321,7 +328,7 @@ export class Player {
321
328
  * You should use dispatchPlayerEvent at PlayerContainer or call playerManager.invokeHandler directly
322
329
  * This allows you to use one method for everything.
323
330
  */
324
- onSeekStart = (event: QuickBrickPlayer.OnSeekStartEvent) => {
331
+ onPlayerSeekStart = (event: QuickBrickPlayer.OnSeekStartEvent) => {
325
332
  const { isLive, seekableDuration, currentTime, duration, seekTime } = event;
326
333
 
327
334
  this.setPlaybackState({ currentTime, duration, seekableDuration, isLive });
@@ -336,10 +343,11 @@ export class Player {
336
343
  });
337
344
  };
338
345
 
339
- onSeekComplete = (event: QuickBrickPlayer.OnSeekCompleteEvent) => {
346
+ onPlayerSeekComplete = (event: QuickBrickPlayer.OnSeekCompleteEvent) => {
340
347
  const { isLive, seekableDuration, currentTime, duration } = event;
341
348
 
342
349
  this.setPlaybackState({ currentTime, duration, seekableDuration, isLive });
350
+
343
351
  this.playerState.seekPosition = null;
344
352
 
345
353
  if (!isLive) {
@@ -620,14 +628,18 @@ export class Player {
620
628
  selectTrack = (_track) => {};
621
629
 
622
630
  protected notifyPlayHeadPositionUpdate = () => {
631
+ const currentTime = this.getPosition();
632
+
623
633
  const event = {
624
- currentTime: this.getPosition(),
634
+ currentTime,
625
635
  duration: this.getDuration(),
626
636
  item: this.getEntry(),
627
637
  seekableDuration: this.playerState.seekableDuration,
628
638
  isLive: this.isLive(),
629
639
  };
630
640
 
641
+ this.seekController?.onVideoProgress(currentTime);
642
+
631
643
  this.invokeListenersUpdate({
632
644
  callbackName: SharedPlayerCallBacksKeys.OnVideoProgress,
633
645
  event,
@@ -653,8 +665,8 @@ export class Player {
653
665
  };
654
666
 
655
667
  public getListener = (): QuickBrickPlayer.SharedPlayerCallBacks => ({
656
- onPlayerSeekStart: this.onSeekStart,
657
- onPlayerSeekComplete: this.onSeekComplete,
668
+ onPlayerSeekStart: this.onPlayerSeekStart,
669
+ onPlayerSeekComplete: this.onPlayerSeekComplete,
658
670
  onVideoEnd: this.onVideoEnd,
659
671
  onError: this.onError,
660
672
  onVideoError: this.onVideoError,
@@ -222,9 +222,10 @@ export class PlayerNative extends Player {
222
222
 
223
223
  onVideoLoad = (event) => {
224
224
  if (this.startPosition) {
225
- super.onVideoLoad({ ...event, ignoreContinueWatching: true });
226
225
  this.seekTo(this.startPosition);
227
226
  this.startPosition = null;
227
+
228
+ super.onVideoLoad({ ...event, ignoreContinueWatching: true });
228
229
  } else {
229
230
  super.onVideoLoad(event);
230
231
  }
@@ -39,7 +39,7 @@ export const usePlayerControllerSetup = ({
39
39
  ? { supports_native_controls: true, ...config }
40
40
  : config;
41
41
 
42
- const newPlayerController = new ControllerType({
42
+ const combinedProps = {
43
43
  player: ref,
44
44
  listener: props as any,
45
45
  config: newConfig,
@@ -47,9 +47,9 @@ export const usePlayerControllerSetup = ({
47
47
  entry,
48
48
  playerPluginId: pluginId,
49
49
  playerRole: PlayerRole.Primary,
50
- });
50
+ };
51
51
 
52
- setPlayerController(newPlayerController);
52
+ setPlayerController(new ControllerType(combinedProps));
53
53
  }, [playerId, entry?.id]);
54
54
 
55
55
  React.useEffect(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "13.0.0-rc.15",
3
+ "version": "13.0.0-rc.16",
4
4
  "description": "Applicaster Zapp React Native utilities package",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "13.0.0-rc.15",
31
+ "@applicaster/applicaster-types": "13.0.0-rc.16",
32
32
  "buffer": "^5.2.1",
33
33
  "camelize": "^1.0.0",
34
34
  "create-react-context": "^0.2.3",
@@ -0,0 +1,32 @@
1
+ import { MAX_SEEK_TIME, SEEK_TYPE, SKIP_TIME_BASE } from "../const";
2
+
3
+ export interface ISeekControllerDelegate {
4
+ handleDelayedSeek(offset: number): void;
5
+ handleSeek(offset: number): void;
6
+ handleSeekComplete(): void;
7
+ handlePlayerPause(): void;
8
+ }
9
+
10
+ export interface ISeekController {
11
+ title: string;
12
+ onStartSeek(seekType: SEEK_TYPE): void;
13
+ onSeekEnd(): void;
14
+ reset(): void;
15
+ }
16
+
17
+ export class SeekIncrementalBaseStrategy {
18
+ private skipMultiplier = 1;
19
+
20
+ getSkipTime = () =>
21
+ Math.min(MAX_SEEK_TIME, SKIP_TIME_BASE * this.skipMultiplier);
22
+
23
+ increment = () => {
24
+ this.skipMultiplier++;
25
+ };
26
+
27
+ getSkipMultiplier = () => this.skipMultiplier;
28
+
29
+ reset = () => {
30
+ this.skipMultiplier = 1;
31
+ };
32
+ }
@@ -0,0 +1,71 @@
1
+ import {
2
+ ISeekController,
3
+ ISeekControllerDelegate,
4
+ SeekIncrementalBaseStrategy,
5
+ } from "./ISeekController";
6
+ import { SEEK_TYPE } from "../const";
7
+ import { log_debug, log_error, log_verbose } from "./TVSeekController";
8
+ import { interval, Subscription } from "rxjs";
9
+ import { tap } from "rxjs/operators";
10
+
11
+ interface LongPressConfig {
12
+ TIMER_INTERVAL_MS: number;
13
+ }
14
+
15
+ export class LongPressSeekController implements ISeekController {
16
+ private subscription: Subscription | null = null;
17
+ readonly title = "LongPressSeek";
18
+ constructor(
19
+ private seekControllerDelegate: ISeekControllerDelegate,
20
+ private seekIncrementalStrategy: SeekIncrementalBaseStrategy,
21
+ private config: LongPressConfig
22
+ ) {}
23
+
24
+ onStartSeek(seekType: SEEK_TYPE) {
25
+ this.cleanUpSubscription();
26
+
27
+ log_debug(`${this.title}: start long press seek type: ${seekType}.`);
28
+
29
+ this.seekControllerDelegate.handlePlayerPause();
30
+
31
+ this.subscription = interval(this.config.TIMER_INTERVAL_MS)
32
+ .pipe(
33
+ tap(() => {
34
+ this.seekControllerDelegate.handleDelayedSeek(
35
+ this.seekIncrementalStrategy.getSkipTime()
36
+ );
37
+
38
+ log_debug(
39
+ `${
40
+ this.title
41
+ }: handle delayed seek type: ${seekType} - multiplier: ${this.seekIncrementalStrategy.getSkipMultiplier()}, currentSkipAmount: ${this.seekIncrementalStrategy.getSkipTime()}.`
42
+ );
43
+
44
+ this.seekIncrementalStrategy.increment();
45
+ })
46
+ )
47
+ .subscribe({
48
+ complete: () =>
49
+ log_verbose(`${this.title}: Long press subscription complete`),
50
+ error: (err) =>
51
+ log_error(`${this.title}: Long press subscription error`, err),
52
+ });
53
+ }
54
+
55
+ onSeekEnd() {
56
+ this.cleanUpSubscription();
57
+ this.seekControllerDelegate.handleSeekComplete();
58
+ }
59
+
60
+ reset() {
61
+ this.seekIncrementalStrategy.reset();
62
+ this.cleanUpSubscription();
63
+ }
64
+
65
+ private cleanUpSubscription() {
66
+ if (this.subscription) {
67
+ this.subscription.unsubscribe();
68
+ this.subscription = null;
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,258 @@
1
+ import { BehaviorSubject } from "rxjs";
2
+ import { TapSeekController } from "./TapSeekController";
3
+ import { LongPressSeekController } from "./LongPressSeekController";
4
+ import { SEEK_TYPE } from "../const";
5
+ import { createLogger } from "../../../logger";
6
+ import { Player } from "../../../appUtils/playerManager/player";
7
+ import { SeekKeyHandlerListener } from "../../../appUtils/keyInputHandler/KeyInputHandler";
8
+ import {
9
+ ISeekController,
10
+ ISeekControllerDelegate,
11
+ SeekIncrementalBaseStrategy,
12
+ } from "./ISeekController";
13
+
14
+ export const { log_warning, log_debug, log_info, log_error, log_verbose } =
15
+ createLogger({
16
+ category: "General",
17
+ subsystem: "TVSeekController",
18
+ });
19
+
20
+ export enum SEEK_STRATEGY {
21
+ TAP_SEEK = "TAP_SEEK",
22
+ AUTO_SEEK = "AUTO_SEEK",
23
+ }
24
+
25
+ const CONFIG_TAP_SEEK = {
26
+ CLICKS_BEFORE_PAUSE: 0,
27
+ };
28
+
29
+ const CONFIG_LONG_PRESS = {
30
+ TIMER_INTERVAL_MS: 200,
31
+ };
32
+
33
+ const LONG_PRESS_THRESHOLD = 500;
34
+ const SEEK_END_TIMEOUT_DURATION = 700;
35
+ // prettier-ignore
36
+
37
+ export class TVSeekController
38
+ implements SeekKeyHandlerListener, ISeekControllerDelegate {
39
+ public delayedSeekPosition: number | null = null;
40
+ private readonly playerController: Player;
41
+ private delayedSeekProgressSubject = new BehaviorSubject<number | null>(null);
42
+ isPlayerInitiallyPaused: null | boolean = null;
43
+ currentSeekType: SEEK_TYPE | null = null;
44
+
45
+ private currentSeekController: ISeekController;
46
+ private readonly tapSeekController: TapSeekController;
47
+ private readonly longPressSeekController: LongPressSeekController;
48
+ private longPressThresholdTimeout: any | null = null;
49
+ private seekEndTimeout: any | null = null;
50
+
51
+ constructor(playerController: Player) {
52
+ this.playerController = playerController;
53
+
54
+ this.tapSeekController = new TapSeekController(
55
+ this,
56
+ new SeekIncrementalBaseStrategy(),
57
+ CONFIG_TAP_SEEK
58
+ );
59
+
60
+ this.longPressSeekController = new LongPressSeekController(
61
+ this,
62
+ new SeekIncrementalBaseStrategy(),
63
+ CONFIG_LONG_PRESS
64
+ );
65
+
66
+ this.currentSeekController = this.tapSeekController;
67
+ }
68
+
69
+ getDelayedSeekProgressObservable = () => {
70
+ return this.delayedSeekProgressSubject;
71
+ };
72
+
73
+ setStrategy = (seekStrategy: SEEK_STRATEGY) => {
74
+ switch (seekStrategy) {
75
+ case SEEK_STRATEGY.TAP_SEEK:
76
+ this.currentSeekController = this.tapSeekController;
77
+ log_debug("TVSeekController: setStrategy - TAP_SEEK");
78
+
79
+ break;
80
+ case SEEK_STRATEGY.AUTO_SEEK:
81
+ this.currentSeekController = this.longPressSeekController;
82
+ log_debug("TVSeekController: setStrategy - AUTO_SEEK");
83
+
84
+ break;
85
+ default:
86
+ }
87
+ };
88
+
89
+ handleStartSeek = (seekType: SEEK_TYPE) => {
90
+ this.currentSeekType = seekType;
91
+
92
+ if (this.isPlayerInitiallyPaused === null) {
93
+ this.isPlayerInitiallyPaused = this.playerController.isPaused();
94
+ }
95
+
96
+ this.clearLongPressThresholdTimeout();
97
+ this.clearSeekCompleteTimeout();
98
+ this.setStrategy(SEEK_STRATEGY.TAP_SEEK);
99
+ this.currentSeekController.onStartSeek(seekType);
100
+
101
+ this.longPressThresholdTimeout = setTimeout(() => {
102
+ this.setStrategy(SEEK_STRATEGY.AUTO_SEEK);
103
+ this.currentSeekController.onStartSeek(seekType);
104
+ }, LONG_PRESS_THRESHOLD);
105
+ };
106
+
107
+ onForwardPress = (): boolean => {
108
+ this.handleStartSeek(SEEK_TYPE.FORWARD);
109
+
110
+ return true;
111
+ };
112
+
113
+ onRewindPress = (): boolean => {
114
+ this.handleStartSeek(SEEK_TYPE.REWIND);
115
+
116
+ return true;
117
+ };
118
+
119
+ onSeekEnd = () => {
120
+ this.clearLongPressThresholdTimeout();
121
+ this.currentSeekController.onSeekEnd();
122
+ };
123
+
124
+ onRewindPressOut = () => {
125
+ this.onSeekEnd();
126
+ };
127
+
128
+ onForwardPressOut = () => {
129
+ this.onSeekEnd();
130
+ };
131
+
132
+ onVideoProgress = (position: number) => {
133
+ if (this.delayedSeekPosition !== null) {
134
+ return;
135
+ }
136
+
137
+ this.delayedSeekProgressSubject.next(position);
138
+ };
139
+
140
+ handleDelayedSeek = (offset: number = 0) => {
141
+ this.handlePlayerPause();
142
+
143
+ const currentPos =
144
+ this.delayedSeekPosition === null
145
+ ? this.playerController.getPosition()
146
+ : this.delayedSeekPosition;
147
+
148
+ let targetPos = currentPos;
149
+
150
+ if (this.currentSeekType === SEEK_TYPE.FORWARD) {
151
+ targetPos = Math.min(
152
+ currentPos + offset,
153
+ this.playerController.getSeekableDuration()
154
+ );
155
+ } else if (this.currentSeekType === SEEK_TYPE.REWIND) {
156
+ targetPos = Math.max(0, currentPos - offset);
157
+ } else {
158
+ log_warning(
159
+ `TVSeekController: handleDelayedSeek - invalid seek type: ${this.currentSeekType}`
160
+ );
161
+ }
162
+
163
+ log_debug(
164
+ `TVSeekController: handleDelayedSeek - targetPos: ${targetPos}, skipTime: ${offset}`
165
+ );
166
+
167
+ const prevValue = this.delayedSeekProgressSubject.getValue();
168
+
169
+ if (prevValue !== null) {
170
+ const delta = Math.abs(prevValue - targetPos);
171
+
172
+ if (delta < 1) {
173
+ return;
174
+ }
175
+ }
176
+
177
+ this.delayedSeekPosition = targetPos;
178
+ this.delayedSeekProgressSubject.next(targetPos);
179
+ };
180
+
181
+ handleSeekTo = (newPosition: number) => {
182
+ this.playerController.seekTo(newPosition);
183
+
184
+ if (!this.isPlayerInitiallyPaused) {
185
+ this.playerController.play();
186
+ }
187
+ };
188
+
189
+ handleSeek = (offset: number) => {
190
+ this.playerController[this.currentSeekType](offset);
191
+
192
+ if (!this.isPlayerInitiallyPaused) {
193
+ this.playerController.play();
194
+ }
195
+ };
196
+
197
+ handleSeekComplete = () => {
198
+ this.clearSeekCompleteTimeout();
199
+
200
+ this.seekEndTimeout = setTimeout(() => {
201
+ if (this.delayedSeekPosition !== null) {
202
+ this.handleSeekTo(this.delayedSeekPosition);
203
+ }
204
+
205
+ log_debug(
206
+ `handleSeekComplete: Seek finished, type: ${this.currentSeekController.title}, seekPosition: ${this.delayedSeekPosition}. Reset all states and seek position.`
207
+ );
208
+
209
+ this.resetAll();
210
+ }, SEEK_END_TIMEOUT_DURATION);
211
+ };
212
+
213
+ isSeeking = (): boolean => {
214
+ return this.delayedSeekPosition !== null;
215
+ };
216
+
217
+ handlePlayerPause = () => {
218
+ if (this.playerController.isPaused()) {
219
+ return;
220
+ }
221
+
222
+ this.playerController.pause();
223
+ };
224
+
225
+ clearSeekCompleteTimeout = () => {
226
+ if (this.seekEndTimeout === null) {
227
+ return;
228
+ }
229
+
230
+ clearTimeout(this.seekEndTimeout);
231
+ this.seekEndTimeout = null;
232
+ };
233
+
234
+ clearLongPressThresholdTimeout = () => {
235
+ if (this.longPressThresholdTimeout === null) {
236
+ return;
237
+ }
238
+
239
+ clearTimeout(this.longPressThresholdTimeout);
240
+ this.longPressThresholdTimeout = null;
241
+ };
242
+
243
+ resetAll() {
244
+ log_debug("TVSeekController: Reset all states");
245
+ this.currentSeekType = null;
246
+
247
+ this.delayedSeekPosition = null;
248
+ this.isPlayerInitiallyPaused = null;
249
+
250
+ this.clearSeekCompleteTimeout();
251
+ this.clearLongPressThresholdTimeout();
252
+
253
+ this.tapSeekController.reset();
254
+ this.longPressSeekController.reset();
255
+
256
+ this.setStrategy(SEEK_STRATEGY.TAP_SEEK);
257
+ }
258
+ }
@@ -0,0 +1,53 @@
1
+ import {
2
+ ISeekController,
3
+ ISeekControllerDelegate,
4
+ SeekIncrementalBaseStrategy,
5
+ } from "./ISeekController";
6
+ import { SEEK_TYPE } from "../const";
7
+ import { log_debug } from "./TVSeekController";
8
+
9
+ interface TapPressConfig {
10
+ CLICKS_BEFORE_PAUSE: number;
11
+ }
12
+
13
+ export class TapSeekController implements ISeekController {
14
+ readonly title = "TapSeek";
15
+ constructor(
16
+ private seekControllerDelegate: ISeekControllerDelegate,
17
+ private seekIncrementalStrategy: SeekIncrementalBaseStrategy,
18
+ private config: TapPressConfig
19
+ ) {}
20
+
21
+ onStartSeek = (seekType: SEEK_TYPE) => {
22
+ log_debug(`${this.title}: start tap seek type: ${seekType}.`);
23
+
24
+ const skipTime = this.seekIncrementalStrategy.getSkipTime();
25
+ const seekMultiplier = this.seekIncrementalStrategy.getSkipMultiplier();
26
+
27
+ if (seekMultiplier > this.config.CLICKS_BEFORE_PAUSE) {
28
+ log_debug(
29
+ `${this.title}: handle delayed seek type:${seekType} - skipTime: ${skipTime}, seekMultiplier: ${seekMultiplier}.`
30
+ );
31
+
32
+ this.seekControllerDelegate.handleDelayedSeek(skipTime);
33
+ } else {
34
+ log_debug(
35
+ `${this.title}: handle seek type:${seekType} - skipTime: ${skipTime}.`
36
+ );
37
+
38
+ this.seekControllerDelegate.handleSeek(skipTime);
39
+ }
40
+
41
+ this.seekIncrementalStrategy.increment();
42
+ };
43
+
44
+ onSeekEnd = () => {
45
+ this.seekControllerDelegate.handleSeekComplete();
46
+ };
47
+
48
+ reset = () => {
49
+ log_debug("{this.title}: Reset states");
50
+
51
+ this.seekIncrementalStrategy.reset();
52
+ };
53
+ }
@@ -1,9 +1,5 @@
1
1
  export const SKIP_TIME_BASE = 10;
2
2
 
3
- export const ON_PRESS_RESET_DURATION = 2000;
4
-
5
- export const ON_HOLD_INTERVAL = 500;
6
-
7
3
  export const MAX_SEEK_TIME = 600;
8
4
 
9
5
  export enum SEEK_TYPE {
@@ -1,7 +1 @@
1
1
  export { useLanguageData } from "./useLanguageData";
2
-
3
- export { useAutoSeek } from "./useAutoSeek";
4
-
5
- export { useTapSeek } from "./useTapSeek";
6
-
7
- export { useSeek, withUseSeek } from "./useSeek";
@@ -1,87 +0,0 @@
1
- import React, { useEffect, useState } from "react";
2
- import { usePrevious } from "@applicaster/zapp-react-native-utils/reactHooks/utils";
3
- import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
4
- import {
5
- MAX_SEEK_TIME,
6
- ON_HOLD_INTERVAL,
7
- SEEK_TYPE,
8
- SKIP_TIME_BASE,
9
- } from "./const";
10
- import { noop } from "../../functionUtils";
11
-
12
- export const useAutoSeek = (resetHUDTimer = noop) => {
13
- const [isRunning, setIsRunning] = useState<boolean>(false);
14
- const [direction, setDirection] = useState<Nullable<SEEK_TYPE>>(null);
15
- const [multiplier, setMultiplier] = useState(1);
16
- const intervalTimer = React.useRef(null);
17
- const prevRunning = usePrevious(isRunning);
18
-
19
- const clearIntervalTimer = () => {
20
- try {
21
- clearInterval(intervalTimer.current);
22
- } catch (err) {
23
- intervalTimer.current = null;
24
- }
25
- };
26
-
27
- const setIntervalTimer = (cb) => {
28
- clearIntervalTimer();
29
- intervalTimer.current = setInterval(cb, ON_HOLD_INTERVAL);
30
- };
31
-
32
- const resetState = () => {
33
- setIsRunning(false);
34
- setDirection(null);
35
- setMultiplier(1);
36
- clearIntervalTimer();
37
- };
38
-
39
- useEffect(() => {
40
- const player = playerManager.getInstanceController();
41
-
42
- if (!player) return;
43
-
44
- if (direction) {
45
- player[direction](Math.min(MAX_SEEK_TIME, SKIP_TIME_BASE * multiplier));
46
- resetHUDTimer();
47
- }
48
- }, [multiplier, direction]);
49
-
50
- useEffect(() => {
51
- if (isRunning && isRunning !== prevRunning) {
52
- setIntervalTimer(() => {
53
- setMultiplier((prev) => {
54
- return prev + 1;
55
- });
56
- });
57
- } else if (!isRunning && isRunning !== prevRunning) {
58
- resetState();
59
- }
60
-
61
- return () => {
62
- clearIntervalTimer();
63
- };
64
- }, [isRunning, direction]);
65
-
66
- const autoSeekStop = React.useCallback(() => {
67
- resetState();
68
- }, []);
69
-
70
- const autoSeekStart = React.useCallback(
71
- (eventType: SEEK_TYPE) => {
72
- // Check if there is another session running
73
- if (isRunning) {
74
- autoSeekStop();
75
- }
76
-
77
- setDirection(eventType);
78
- setIsRunning(true);
79
- },
80
- [isRunning]
81
- );
82
-
83
- return React.useMemo(
84
- () => ({ autoSeekStart, autoSeekStop }),
85
- [autoSeekStart, autoSeekStop]
86
- );
87
- };
@@ -1,66 +0,0 @@
1
- import React from "react";
2
- import { SEEK_TYPE } from "./const";
3
- import { useTapSeek } from "./useTapSeek";
4
- import { useAutoSeek } from "./useAutoSeek";
5
- import { noop } from "../../functionUtils";
6
-
7
- export const useSeek = (resetHUDTimer = noop) => {
8
- const handlePressEvent = useTapSeek();
9
- const { autoSeekStart, autoSeekStop } = useAutoSeek(resetHUDTimer);
10
-
11
- const onForwardPress = () => {
12
- handlePressEvent(SEEK_TYPE.FORWARD);
13
- };
14
-
15
- const onRewindPress = () => {
16
- handlePressEvent(SEEK_TYPE.REWIND);
17
- };
18
-
19
- const onForwardPressIn = () => {
20
- autoSeekStart(SEEK_TYPE.FORWARD);
21
- };
22
-
23
- const onForwardPressOut = () => {
24
- autoSeekStop();
25
- };
26
-
27
- const onRewindPressIn = () => {
28
- autoSeekStart(SEEK_TYPE.REWIND);
29
- };
30
-
31
- const onRewindLongPress = () => {
32
- autoSeekStart(SEEK_TYPE.REWIND);
33
- };
34
-
35
- const onForwardLongPress = () => {
36
- autoSeekStart(SEEK_TYPE.FORWARD);
37
- };
38
-
39
- const onRewindPressOut = () => {
40
- autoSeekStop();
41
- };
42
-
43
- return React.useMemo(
44
- () => ({
45
- onForwardPress,
46
- onRewindPress,
47
- onForwardPressIn,
48
- onForwardPressOut,
49
- onRewindPressIn,
50
- onRewindPressOut,
51
- onRewindLongPress,
52
- onForwardLongPress,
53
- }),
54
- [handlePressEvent, autoSeekStart]
55
- );
56
- };
57
-
58
- export const withUseSeek = (Component) =>
59
- Object.assign(
60
- (props) => {
61
- const seekMethods = useSeek(props?.resetHUDTimer);
62
-
63
- return <Component {...props} seekMethods={seekMethods} />;
64
- },
65
- { displayName: "WithUseSeek" }
66
- );
@@ -1,58 +0,0 @@
1
- import { useCallback, useRef } from "react";
2
- import * as R from "ramda";
3
- import {
4
- MAX_SEEK_TIME,
5
- ON_PRESS_RESET_DURATION,
6
- SEEK_TYPE,
7
- SKIP_TIME_BASE,
8
- } from "./const";
9
- import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils";
10
-
11
- type SeekType = SEEK_TYPE | null;
12
-
13
- const getSkipTime = (multiplier) => {
14
- const skipTime = R.min(MAX_SEEK_TIME, SKIP_TIME_BASE * multiplier);
15
-
16
- return skipTime;
17
- };
18
-
19
- export const useTapSeek = () => {
20
- const onPressMultiplier = useRef(1);
21
- const lastPressTime = useRef(0);
22
- const currentOnPressEventType = useRef<SeekType>(null);
23
-
24
- const resetMultiplier = useCallback(() => {
25
- onPressMultiplier.current = 1;
26
- }, []);
27
-
28
- const handlePressEvent = useCallback(
29
- (eventType) => {
30
- const player = playerManager.getInstanceController();
31
- if (!player) return;
32
-
33
- const now = Date.now();
34
- const timeSinceLastPress = now - lastPressTime.current;
35
- const isSameEventType = eventType === currentOnPressEventType.current;
36
-
37
- const isWithinResetDuration =
38
- timeSinceLastPress <= ON_PRESS_RESET_DURATION;
39
-
40
- const shouldIncreaseMultiplier = isSameEventType && isWithinResetDuration;
41
-
42
- if (shouldIncreaseMultiplier) {
43
- onPressMultiplier.current += 1;
44
- } else {
45
- resetMultiplier();
46
- }
47
-
48
- const seekTime = getSkipTime(onPressMultiplier.current);
49
- player[eventType](seekTime);
50
-
51
- lastPressTime.current = now;
52
- currentOnPressEventType.current = eventType;
53
- },
54
- [resetMultiplier]
55
- );
56
-
57
- return handlePressEvent;
58
- };