@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.
- package/analyticsUtils/AnalyticPlayerListener.ts +18 -1
- package/appUtils/focusManager/index.ts +90 -63
- package/appUtils/focusManager/treeDataStructure/Tree/index.js +23 -0
- package/appUtils/focusManager/utils/enums.ts +4 -0
- package/appUtils/keyCodes/keys/keysByPlatform.ts +4 -0
- package/appUtils/keyInputHandler/KeyInputHandler.ts +68 -0
- package/appUtils/playerManager/player.ts +17 -5
- package/appUtils/playerManager/playerNative.ts +2 -1
- package/appUtils/playerManager/usePlayerControllerSetup.tsx +3 -3
- package/package.json +2 -2
- package/reactHooks/player/TVSeekControlller/ISeekController.ts +32 -0
- package/reactHooks/player/TVSeekControlller/LongPressSeekController.ts +71 -0
- package/reactHooks/player/TVSeekControlller/TVSeekController.ts +258 -0
- package/reactHooks/player/TVSeekControlller/TapSeekController.ts +53 -0
- package/reactHooks/player/const.ts +0 -4
- package/reactHooks/player/index.ts +0 -6
- package/reactHooks/player/useAutoSeek.tsx +0 -87
- package/reactHooks/player/useSeek.tsx +0 -66
- package/reactHooks/player/useTapSeek.tsx +0 -58
- /package/reactHooks/player/__tests__/{useAutoSeek.test.tsx → useAutoSeek._test.tsx} +0 -0
- /package/reactHooks/player/__tests__/{useTapSeek.test.ts → useTapSeek._test.ts} +0 -0
|
@@ -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
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
452
|
-
nextFocusable.component,
|
|
453
|
-
direction
|
|
454
|
-
);
|
|
455
|
-
|
|
516
|
+
// TODO: Move this to setFocus method
|
|
456
517
|
blur(direction);
|
|
457
518
|
|
|
458
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
657
|
-
onPlayerSeekComplete: this.
|
|
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
|
|
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(
|
|
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.
|
|
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.
|
|
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,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
|
-
};
|
|
File without changes
|
|
File without changes
|