@applicaster/zapp-react-native-utils 16.0.0-alpha.2663208063 → 16.0.0-alpha.2710214351

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.
Files changed (138) hide show
  1. package/actionUtils/index.ts +3 -1
  2. package/actionsExecutor/ActionExecutor.ts +10 -1
  3. package/actionsExecutor/ActionExecutorContext.tsx +29 -1
  4. package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
  5. package/actionsExecutor/__tests__/StorageActions.test.ts +98 -0
  6. package/actionsExecutor/__tests__/feedDecorator.test.ts +91 -0
  7. package/actionsExecutor/__tests__/screenToggleFlag.test.ts +90 -0
  8. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +3 -0
  9. package/actionsExecutor/actions/__tests__/goBack.test.ts +62 -0
  10. package/actionsExecutor/actions/__tests__/goHome.test.ts +19 -0
  11. package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +267 -0
  12. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +24 -0
  13. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +273 -1
  14. package/actionsExecutor/actions/__tests__/presentScreen.test.ts +127 -0
  15. package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
  16. package/actionsExecutor/actions/__tests__/showToast.test.ts +2 -2
  17. package/actionsExecutor/actions/dismissBottomSheet.ts +2 -1
  18. package/actionsExecutor/actions/goBack.ts +60 -0
  19. package/actionsExecutor/actions/goHome.ts +31 -0
  20. package/actionsExecutor/actions/index.ts +8 -0
  21. package/actionsExecutor/actions/navigateToScreen.ts +43 -45
  22. package/actionsExecutor/actions/openBottomSheet.ts +36 -16
  23. package/actionsExecutor/actions/presentScreen.ts +78 -0
  24. package/actionsExecutor/actions/screenToggleFlag.ts +1 -3
  25. package/actionsExecutor/actions/showAlert.ts +51 -0
  26. package/actionsExecutor/actions/showToast.ts +2 -2
  27. package/actionsExecutor/consts.ts +4 -0
  28. package/actionsExecutor/feedDecorator.ts +12 -6
  29. package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +1 -22
  30. package/appUtils/contextKeysManager/__tests__/getKey/success/fullSearch.test.ts +1 -25
  31. package/appUtils/contextKeysManager/__tests__/getKey/success/searchInSecure.test.ts +1 -7
  32. package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +0 -25
  33. package/appUtils/contextKeysManager/__tests__/removeKey/success.test.ts +0 -6
  34. package/appUtils/contextKeysManager/__tests__/setKey/failure/failedToSaveInStorage.test.ts +0 -40
  35. package/appUtils/contextKeysManager/__tests__/setKey/{success/saveItemAndReference.test.ts → success.test.ts} +2 -42
  36. package/appUtils/contextKeysManager/index.ts +28 -129
  37. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +4 -11
  38. package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
  39. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +9 -2
  40. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +4 -1
  41. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +4 -1
  42. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +21 -2
  43. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +4 -1
  44. package/appUtils/playerManager/OverlayObserver/utils.ts +9 -7
  45. package/appUtils/playerManager/__tests__/playerDiagnostics.test.ts +62 -0
  46. package/appUtils/playerManager/__tests__/playerNative.test.ts +114 -0
  47. package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
  48. package/appUtils/playerManager/__tests__/playerToolbox.test.ts +281 -0
  49. package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
  50. package/appUtils/playerManager/const.ts +13 -0
  51. package/appUtils/playerManager/index.ts +7 -0
  52. package/appUtils/playerManager/player.ts +25 -0
  53. package/appUtils/playerManager/playerNative.ts +27 -3
  54. package/appUtils/playerManager/playerNativeCommand.ts +3 -6
  55. package/appUtils/playerManager/playerToolbox.ts +196 -0
  56. package/appUtils/playerManager/usePlayerControllerSetup.tsx +6 -0
  57. package/arrayUtils/__tests__/reorderByIds.test.ts +13 -0
  58. package/arrayUtils/index.ts +26 -43
  59. package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
  60. package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
  61. package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
  62. package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
  63. package/cellUtils/index.ts +70 -3
  64. package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
  65. package/configurationUtils/__tests__/modalBlocksParser.test.ts +297 -1
  66. package/configurationUtils/manifestKeyParser.ts +12 -1
  67. package/configurationUtils/modalBlocksParser.ts +78 -4
  68. package/entryActions/__tests__/buildEntryActions.test.ts +235 -0
  69. package/entryActions/aliasPresentation.ts +108 -0
  70. package/entryActions/buildEntryActions.ts +167 -0
  71. package/entryActions/index.ts +15 -0
  72. package/entryActions/types.ts +39 -0
  73. package/focusManager/FocusManager.ts +26 -0
  74. package/focusManager/__tests__/FocusManager.test.ts +9 -0
  75. package/localizationUtils/index.ts +12 -6
  76. package/logger/__tests__/xrayToolbox.test.ts +69 -0
  77. package/logger/xrayToolbox.ts +67 -0
  78. package/manifestUtils/_internals/index.js +8 -3
  79. package/manifestUtils/defaultManifestConfigurations/generalContent.js +6 -0
  80. package/manifestUtils/defaultManifestConfigurations/player.js +30 -1
  81. package/manifestUtils/keys.js +7 -2
  82. package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
  83. package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
  84. package/manifestUtils/mobileAction/groups/defaults.js +2 -1
  85. package/modalState/ModalOrchestrator.ts +23 -9
  86. package/modalState/RemoteEditableCollection.ts +9 -1
  87. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +39 -0
  88. package/modalState/__tests__/RemoteEditableCollection.test.ts +90 -1
  89. package/modalState/__tests__/index.test.ts +30 -1
  90. package/modalState/__tests__/useModalStoreState.test.ts +142 -0
  91. package/modalState/index.ts +8 -1
  92. package/modalState/store.ts +9 -2
  93. package/modalState/types.ts +42 -2
  94. package/modalState/useBottomSheetContent.ts +24 -17
  95. package/navigationUtils/xrayNavigator.ts +12 -0
  96. package/package.json +3 -2
  97. package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
  98. package/playerUtils/index.ts +17 -1
  99. package/playerUtils/resolvePlayerActions.ts +71 -0
  100. package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
  101. package/pluginUtils/index.ts +29 -0
  102. package/quickBrickAssets/__tests__/index.test.ts +18 -0
  103. package/quickBrickAssets/index.ts +35 -0
  104. package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
  105. package/reactHooks/actions/index.ts +74 -3
  106. package/reactHooks/cell-click/__tests__/index.test.js +64 -0
  107. package/reactHooks/cell-click/index.ts +21 -14
  108. package/reactHooks/connection/index.tsx +8 -5
  109. package/reactHooks/feed/__tests__/feedInvalidation.test.tsx +338 -0
  110. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +8 -2
  111. package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
  112. package/reactHooks/feed/useFeedLoader.tsx +28 -1
  113. package/reactHooks/feed/useLoadPipesDataDispatch.ts +12 -11
  114. package/reactHooks/feed/useMarkPipesDataStale.ts +68 -23
  115. package/reactHooks/navigation/__tests__/index.test.tsx +176 -1
  116. package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
  117. package/reactHooks/navigation/index.ts +2 -0
  118. package/reactHooks/navigation/usePresentScreen.ts +157 -0
  119. package/reactHooks/navigation/useRoute.ts +22 -5
  120. package/reactHooks/usePluginConfiguration.ts +84 -46
  121. package/riverComponetsMeasurementProvider/index.tsx +3 -1
  122. package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
  123. package/uiActionsRegistrator/__tests__/subscribe.test.ts +133 -0
  124. package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
  125. package/uiActionsRegistrator/index.ts +25 -203
  126. package/uiActionsRegistrator/registry.ts +351 -0
  127. package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
  128. package/uiActionsRegistrator/usableActionItems.ts +66 -0
  129. package/appUtils/contextKeysManager/__tests__/getKey/success/byReference.test.ts +0 -56
  130. package/appUtils/contextKeysManager/__tests__/getKeyByReference/failure.test.ts +0 -265
  131. package/appUtils/contextKeysManager/__tests__/getKeyByReference/success.test.ts +0 -303
  132. package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/failure.test.ts +0 -82
  133. package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/success.test.ts +0 -41
  134. package/appUtils/contextKeysManager/__tests__/setKey/success/saveItemButNotReference.test.ts +0 -208
  135. package/appUtils/contextKeysManager/__tests__/setReferenceForKey/failure.test.ts +0 -93
  136. package/appUtils/contextKeysManager/__tests__/setReferenceForKey/success.test.ts +0 -45
  137. package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
  138. package/playerUtils/getPlayerActionButtons.ts +0 -17
@@ -0,0 +1,281 @@
1
+ const mockRegister = jest.fn();
2
+ let mockXray: Record<string, unknown> | null = {};
3
+
4
+ jest.mock("../../../logger", () => ({
5
+ getXray: () => mockXray,
6
+ }));
7
+
8
+ const loadToolbox = () => {
9
+ let module;
10
+
11
+ jest.isolateModules(() => {
12
+ module = require("../playerToolbox");
13
+ });
14
+
15
+ return module as typeof import("../playerToolbox");
16
+ };
17
+
18
+ const player = (overrides = {}) => ({
19
+ getSeekableDuration: () => 100,
20
+ getContentPosition: () => 40,
21
+ isLive: () => false,
22
+ getPlayNextEntry: () => null,
23
+ playNextHandler: jest.fn(),
24
+ seekTo: jest.fn(),
25
+ ...overrides,
26
+ });
27
+
28
+ const manager = (active) =>
29
+ ({ getActivePlayer: () => active, isCasting: () => false }) as any;
30
+
31
+ const registeredTools = async (active) => {
32
+ mockRegister.mockResolvedValue(undefined);
33
+ mockXray = { registerReactToolbox: mockRegister };
34
+ await loadToolbox().registerPlayerToolbox(manager(active));
35
+
36
+ const { tools } = mockRegister.mock.lastCall[0];
37
+
38
+ return Object.fromEntries(tools.map((tool) => [tool.name, tool.handler]));
39
+ };
40
+
41
+ describe("playerToolbox", () => {
42
+ beforeEach(() => {
43
+ mockRegister.mockReset();
44
+ });
45
+
46
+ it("does nothing when X-Ray has no React toolbox API", async () => {
47
+ mockXray = {};
48
+
49
+ await expect(
50
+ loadToolbox().registerPlayerToolbox(manager(null))
51
+ ).resolves.toBe(false);
52
+ });
53
+
54
+ it("does nothing when getXray returns null", async () => {
55
+ mockXray = null;
56
+
57
+ await expect(
58
+ loadToolbox().registerPlayerToolbox(manager(null))
59
+ ).resolves.toBe(false);
60
+ });
61
+
62
+ it("swallows a failed registration", async () => {
63
+ mockRegister.mockRejectedValue(new Error("no native bridge"));
64
+ mockXray = { registerReactToolbox: mockRegister };
65
+
66
+ await expect(
67
+ loadToolbox().registerPlayerToolbox(manager(null))
68
+ ).resolves.toBe(false);
69
+ });
70
+
71
+ it("seeks to an absolute position, from the end when negative, clamped", async () => {
72
+ const active = player();
73
+ const tools = await registeredTools(active);
74
+
75
+ expect(tools.seek({ position: 30 })).toEqual({ position: 30 });
76
+ expect(tools.seek({ position: -10 })).toEqual({ position: 90 });
77
+ expect(tools.seek({ position: 500 })).toEqual({ position: 100 });
78
+ expect(tools.seek({ position: -500 })).toEqual({ position: 0 });
79
+ expect(active.seekTo.mock.calls).toEqual([[30], [90], [100], [0]]);
80
+ });
81
+
82
+ it("offsets from the current position and refuses zero", async () => {
83
+ const tools = await registeredTools(player());
84
+
85
+ expect(tools.offset({ seconds: 15 })).toEqual({ from: 40, position: 55 });
86
+ expect(tools.offset({ seconds: -60 })).toEqual({ from: 40, position: 0 });
87
+
88
+ expect(() => tools.offset({ seconds: 0 })).toThrow(
89
+ "Ignored zero relative seek"
90
+ );
91
+ });
92
+
93
+ describe("state", () => {
94
+ const playing = (overrides = {}, entry: any = { id: "e1", title: "T" }) =>
95
+ player({
96
+ playerId: "p1",
97
+ playerRole: "Primary",
98
+ getEntry: () => entry,
99
+ getPosition: () => 40,
100
+ getDuration: () => 100,
101
+ getContentDuration: () => 100,
102
+ isReadyToPlay: () => true,
103
+ isPlaying: () => true,
104
+ isBuffering: () => false,
105
+ isAd: () => false,
106
+ isLive: () => false,
107
+ getIsMuted: () => false,
108
+ ...overrides,
109
+ });
110
+
111
+ const state = async (overrides = {}, entry?: any) =>
112
+ (await registeredTools(playing(overrides, entry))).state();
113
+
114
+ it("reports inactive when nothing plays", async () => {
115
+ expect((await registeredTools(null)).state()).toEqual({
116
+ active: false,
117
+ casting: false,
118
+ });
119
+ });
120
+
121
+ it("keeps the existing fields and adds the content clock for a VOD", async () => {
122
+ expect(await state()).toEqual({
123
+ active: true,
124
+ playerId: "p1",
125
+ entry: { id: "e1", title: "T" },
126
+ position: 40,
127
+ duration: 100,
128
+ playing: true,
129
+ buffering: false,
130
+ readyToPlay: true,
131
+ ad: false,
132
+ live: false,
133
+ muted: false,
134
+ role: "Primary",
135
+ plugin: null,
136
+ engine: null,
137
+ casting: false,
138
+ seekableDuration: 100,
139
+ contentPosition: 40,
140
+ contentDuration: 100,
141
+ drm: null,
142
+ lastError: null,
143
+ next: null,
144
+ });
145
+ });
146
+
147
+ it("reports the content's clock next to the ad's during a client-side ad", async () => {
148
+ const result = await state({
149
+ isAd: () => true,
150
+ getPosition: () => 5,
151
+ getDuration: () => 15,
152
+ getContentPosition: () => 300,
153
+ getContentDuration: () => 1800,
154
+ });
155
+
156
+ expect(result).toMatchObject({
157
+ ad: true,
158
+ position: 5,
159
+ duration: 15,
160
+ contentPosition: 300,
161
+ contentDuration: 1800,
162
+ });
163
+ });
164
+
165
+ it("reports non-finite numbers as null", async () => {
166
+ expect(
167
+ await state({
168
+ getSeekableDuration: () => NaN,
169
+ getContentPosition: () => null,
170
+ getContentDuration: () => Infinity,
171
+ })
172
+ ).toMatchObject({
173
+ seekableDuration: null,
174
+ contentPosition: null,
175
+ contentDuration: null,
176
+ });
177
+ });
178
+
179
+ it("reports the player plugin and its web engine", async () => {
180
+ expect(
181
+ await state({
182
+ playerPluginId: "QuickBrickPlayerPlugin",
183
+ engine: "hls.js",
184
+ })
185
+ ).toMatchObject({ plugin: "QuickBrickPlayerPlugin", engine: "hls.js" });
186
+ });
187
+
188
+ it("lists the entry's DRM key systems, or null", async () => {
189
+ const drm = { widevine: { license_url: "w" }, playready: {} };
190
+
191
+ expect((await state({}, { id: "e", extensions: { drm } })).drm).toEqual([
192
+ "widevine",
193
+ "playready",
194
+ ]);
195
+
196
+ expect((await state({}, { id: "e", extensions: { drm: {} } })).drm).toBe(
197
+ null
198
+ );
199
+
200
+ expect((await state({}, { id: "e", extensions: {} })).drm).toBe(null);
201
+ expect((await state({}, null)).drm).toBe(null);
202
+ });
203
+
204
+ it("reports no last error as null", async () => {
205
+ expect((await state({ lastError: null })).lastError).toBeNull();
206
+ });
207
+ });
208
+
209
+ describe("play next", () => {
210
+ const nextEntry = { id: "n1", title: "Next", extensions: {} };
211
+
212
+ const stateWith = async (overrides) =>
213
+ (
214
+ await registeredTools(
215
+ player({
216
+ getEntry: () => null,
217
+ getPosition: () => 0,
218
+ getDuration: () => 0,
219
+ getContentDuration: () => 0,
220
+ isReadyToPlay: () => true,
221
+ isPlaying: () => true,
222
+ isBuffering: () => false,
223
+ isAd: () => false,
224
+ getIsMuted: () => false,
225
+ ...overrides,
226
+ })
227
+ )
228
+ ).state();
229
+
230
+ it("reports the entry Play Next would play", async () => {
231
+ expect(
232
+ (await stateWith({ getPlayNextEntry: () => nextEntry })).next
233
+ ).toEqual({ id: "n1", title: "Next" });
234
+ });
235
+
236
+ it("reports no next entry as null", async () => {
237
+ expect((await stateWith({})).next).toBeNull();
238
+ });
239
+
240
+ it("plays the next entry through the player's Play Next action", async () => {
241
+ const active = player({ getPlayNextEntry: () => nextEntry });
242
+ const tools = await registeredTools(active);
243
+
244
+ expect(tools.next()).toEqual({ id: "n1", title: "Next" });
245
+ expect(active.playNextHandler).toHaveBeenCalledTimes(1);
246
+ });
247
+
248
+ it("fails without a next entry", async () => {
249
+ const active = player();
250
+ const tools = await registeredTools(active);
251
+
252
+ expect(() => tools.next()).toThrow("No next item");
253
+ expect(active.playNextHandler).not.toHaveBeenCalled();
254
+ });
255
+
256
+ it("fails when no Play Next action is registered", async () => {
257
+ const tools = await registeredTools(
258
+ player({
259
+ getPlayNextEntry: () => nextEntry,
260
+ playNextHandler: null,
261
+ })
262
+ );
263
+
264
+ expect(() => tools.next()).toThrow("No next item");
265
+ });
266
+
267
+ it("fails without an active player", async () => {
268
+ const tools = await registeredTools(null);
269
+
270
+ expect(() => tools.next()).toThrow("No active player");
271
+ });
272
+ });
273
+
274
+ it("refuses to seek a stream that is not seekable", async () => {
275
+ const tools = await registeredTools(
276
+ player({ getSeekableDuration: () => 0 })
277
+ );
278
+
279
+ expect(() => tools.seek({ position: 10 })).toThrow("Seek is not supported");
280
+ });
281
+ });
@@ -0,0 +1,62 @@
1
+ import { renderHook } from "@testing-library/react-native";
2
+
3
+ import { usePlayerControllerSetup } from "../usePlayerControllerSetup";
4
+ import { PlayerAnalyticsManager } from "../../../analyticsUtils/PlayerAnalyticsManager";
5
+
6
+ const calls: string[] = [];
7
+
8
+ jest.mock("../index", () => ({
9
+ playerManager: {
10
+ isPlayerRegistered: jest.fn(() => false),
11
+ registerPlayer: jest.fn(() => calls.push("registerPlayer")),
12
+ unregisterPlayer: jest.fn(),
13
+ },
14
+ }));
15
+
16
+ jest.mock("../../../analyticsUtils/PlayerAnalyticsManager", () => ({
17
+ PlayerAnalyticsManager: {
18
+ getInstance: jest.fn(() => calls.push("analyticsManager")),
19
+ },
20
+ }));
21
+
22
+ jest.mock("../../../reactHooks/useConfiguration", () => ({
23
+ useConfiguration: () => ({}),
24
+ }));
25
+
26
+ jest.mock("../../../playerUtils/useValidatePlayerConfig", () => ({
27
+ useValidatePlayerConfig: jest.fn(),
28
+ }));
29
+
30
+ jest.mock("../../../reactUtils", () => ({
31
+ isTV: () => false,
32
+ }));
33
+
34
+ jest.mock(
35
+ "@applicaster/zapp-react-native-ui-components/Helpers/Platform",
36
+ () => ({
37
+ isAppleTV: () => false,
38
+ })
39
+ );
40
+
41
+ class Controller {
42
+ destroy = jest.fn();
43
+ }
44
+
45
+ describe("usePlayerControllerSetup", () => {
46
+ it("creates the analytics manager before the player registers", () => {
47
+ renderHook(() =>
48
+ usePlayerControllerSetup({
49
+ props: { playerId: "player-1", entry: { id: "entry-1" } },
50
+ ControllerType: Controller,
51
+ ref: null,
52
+ manifest: {},
53
+ pluginId: "plugin",
54
+ })
55
+ );
56
+
57
+ // The manager attaches analytics to players as they register, so a player
58
+ // registered before it exists would send no player analytics events
59
+ expect(PlayerAnalyticsManager.getInstance).toHaveBeenCalled();
60
+ expect(calls).toEqual(["analyticsManager", "registerPlayer"]);
61
+ });
62
+ });
@@ -59,6 +59,19 @@ export enum SharedPlayerCallBacksKeys {
59
59
  // PIP
60
60
  OnPlayerDetached = "onPlayerDetached",
61
61
  OnPlayerAttached = "onPlayerAttached",
62
+
63
+ OnRemoteCommand = "onRemoteCommand",
64
+ }
65
+
66
+ // Ids native sends in onRemoteCommand (lock screen / Control Center / CarPlay buttons)
67
+ export enum RemoteCommand {
68
+ Play = "play",
69
+ Pause = "pause",
70
+ Stop = "stop",
71
+ NextTrack = "nextTrack",
72
+ PreviousTrack = "previousTrack",
73
+ SkipForward = "skipForward",
74
+ SkipBackward = "skipBackward",
62
75
  }
63
76
 
64
77
  export enum PlayerRole {
@@ -11,6 +11,7 @@ import {
11
11
  import { setUserLanguagePreference } from "./userLanguagePreference";
12
12
  import { PlayerRole } from "./const";
13
13
  import { RNPlayerManager } from "./nativePlayerManager";
14
+ import { registerPlayerToolbox } from "./playerToolbox";
14
15
 
15
16
  const { log_error, log_debug, log_warning } = createLogger({
16
17
  category: "General",
@@ -54,6 +55,7 @@ export class PlayerManager {
54
55
  public static getInstance() {
55
56
  if (!PlayerManager.instance) {
56
57
  PlayerManager.instance = new PlayerManager();
58
+ registerPlayerToolbox(PlayerManager.instance);
57
59
  }
58
60
 
59
61
  return PlayerManager.instance;
@@ -395,6 +397,11 @@ export class PlayerManager {
395
397
  return this.manager.removeHandler(event, handler);
396
398
  }
397
399
 
400
+ /** Whether anything handles `event` now, e.g. a mounted PlayerContainer for "toggleFullscreen". */
401
+ public hasHandler(event: string) {
402
+ return this.manager.handlers[event]?.length > 0;
403
+ }
404
+
398
405
  // TODO: should not be
399
406
  public invokeHandler(event, ...args) {
400
407
  return this.manager.invokeHandler(event, ...args);
@@ -76,6 +76,12 @@ export class Player {
76
76
  private overlaysObserver?: OverlaysObserver | null;
77
77
  private entryObservable?: BehaviorSubject<ZappEntry> | null;
78
78
  private contentSubject?: BehaviorSubject<PlayerContent> | null;
79
+ // Read by the X-Ray `player` toolbox: the last onError/onVideoError of the current entry,
80
+ // and the Play Next action of the PlayerContainer showing this player.
81
+ public lastError: unknown = null;
82
+ public playNextHandler: (() => void) | null = null;
83
+ // The web engine the default player picked (hls.js, dash.js, shaka, video.js); null elsewhere.
84
+ public engine: string | null = null;
79
85
 
80
86
  protected listeners: {
81
87
  [key: string]: QuickBrickPlayer.SharedPlayerCallBacks;
@@ -89,6 +95,7 @@ export class Player {
89
95
 
90
96
  set entry(newEntry: ZappEntry | null) {
91
97
  this._entry = newEntry;
98
+ this.lastError = null;
92
99
  // The live entry is scoped to the entry it overlays - see
93
100
  // `OverlaysObserver.clearLiveEntry`. No-op when there is no observer,
94
101
  // which is every non-live player.
@@ -271,6 +278,10 @@ export class Player {
271
278
 
272
279
  getEntry = (): ZappEntry | null => this.entry;
273
280
 
281
+ /** The entry Play Next would play now; null without one. Never creates the overlay observer. */
282
+ getPlayNextEntry = (): ZappEntry | null =>
283
+ this.overlaysObserver?.getPlayNextEntry() ?? null;
284
+
274
285
  logState = (text, additionalParams = {}) => {
275
286
  const isSeeking = this.playerState.seekPosition !== null;
276
287
 
@@ -492,6 +503,8 @@ export class Player {
492
503
  };
493
504
 
494
505
  onVideoError = (error: Error) => {
506
+ this.lastError = error;
507
+
495
508
  this.invokeListenersUpdate({
496
509
  callbackName: SharedPlayerCallBacksKeys.OnVideoError,
497
510
  event: error,
@@ -499,6 +512,8 @@ export class Player {
499
512
  };
500
513
 
501
514
  onError = (error: Error) => {
515
+ this.lastError = error;
516
+
502
517
  this.invokeListenersUpdate({
503
518
  callbackName: SharedPlayerCallBacksKeys.OnError,
504
519
  event: error,
@@ -746,6 +761,13 @@ export class Player {
746
761
  });
747
762
  };
748
763
 
764
+ onRemoteCommand = (event: QuickBrickPlayer.RemoteCommandEvent) => {
765
+ this.invokeListenersUpdate({
766
+ callbackName: SharedPlayerCallBacksKeys.OnRemoteCommand,
767
+ event,
768
+ });
769
+ };
770
+
749
771
  getTracksState = (): QuickBrickPlayer.TracksState =>
750
772
  this.playerState.trackState;
751
773
 
@@ -823,6 +845,8 @@ export class Player {
823
845
  // PIP
824
846
  onPlayerDetached: this.onPlayerDetached,
825
847
  onPlayerAttached: this.onPlayerAttached,
848
+
849
+ onRemoteCommand: this.onRemoteCommand,
826
850
  });
827
851
 
828
852
  play = () => {};
@@ -832,6 +856,7 @@ export class Player {
832
856
  setPlaybackRate = (_rate) => {};
833
857
  startSleepTimer = (_timestamp) => {};
834
858
  cancelSleepTimer = () => {};
859
+ command = (_command: string, _userInfo?: Record<string, any>) => {};
835
860
 
836
861
  getPluginConfiguration = () => null;
837
862
  appStateChange = (_appState, _previousAppState) => {};
@@ -6,6 +6,7 @@ import { findNodeHandle } from "react-native";
6
6
  import { PlayerRole } from "./const";
7
7
  import { toBooleanWithDefaultFalse } from "../../booleanUtils";
8
8
  import { RNPlayerManager } from "./nativePlayerManager";
9
+ import { isApplePlatform } from "../../reactUtils";
9
10
 
10
11
  const logger = createLogger({
11
12
  category: "PlayerNativeController",
@@ -27,6 +28,7 @@ interface PlayerMethods {
27
28
  appStateChange?: (appState: string, previousAppState: string) => void;
28
29
  closeNativePlayer?: () => void;
29
30
  togglePlayPause?: () => void;
31
+ command?: (command: string, userInfo?: Record<string, any>) => void;
30
32
  }
31
33
 
32
34
  type PlayerComponent = ReactComponent<PlayerRefProps> & PlayerMethods;
@@ -52,6 +54,7 @@ const PlayerModuleFuncNames = {
52
54
  startSleepTimer: "startSleepTimer",
53
55
  cancelSleepTimer: "cancelSleepTimer",
54
56
  getSleepTimerEnd: "getSleepTimerEnd",
57
+ command: "command",
55
58
  };
56
59
 
57
60
  export class PlayerNative extends Player {
@@ -206,6 +209,25 @@ export class PlayerNative extends Player {
206
209
  }
207
210
  };
208
211
 
212
+ command = (command: string, userInfo?: Record<string, any>) => {
213
+ if (isApplePlatform()) {
214
+ const module = this.getPlayerModule();
215
+ module?.command?.(command, this.playerId, userInfo);
216
+
217
+ return true;
218
+ }
219
+
220
+ if (
221
+ !this.invokeNativeFunction(
222
+ PlayerModuleFuncNames.command,
223
+ command,
224
+ userInfo
225
+ )
226
+ ) {
227
+ this.currentPlayerComponent()?.command?.(command, userInfo);
228
+ }
229
+ };
230
+
209
231
  disableBufferAnimation = (): boolean => false;
210
232
 
211
233
  selectTrack = (
@@ -296,9 +318,11 @@ export class PlayerNative extends Player {
296
318
  }, listeners);
297
319
  };
298
320
 
299
- isAudioItem = () =>
300
- this.getEntry()?.content?.type?.includes?.("audio") ||
301
- this.getEntry()?.type?.value === "audio";
321
+ isAudioItem = (): boolean =>
322
+ Boolean(
323
+ this.getEntry()?.content?.type?.includes?.("audio") ||
324
+ this.getEntry()?.type?.value?.includes?.("audio")
325
+ );
302
326
 
303
327
  isFullScreenSupported = (): boolean => {
304
328
  const config = this.getConfig();
@@ -1,14 +1,11 @@
1
1
  import { isApplePlatform } from "../../reactUtils";
2
2
  import { NativeModules } from "react-native";
3
3
 
4
- const NativePlayerModule = isApplePlatform()
5
- ? NativeModules.PlayerModule
6
- : NativeModules.QuickBrickDefaultPlayerModule;
7
-
8
4
  const PlayerNativeCommandTypes = {
9
5
  pause: "pause",
10
6
  clearPlayerData: "clearPlayerData",
11
7
  removeViewByNativeId: "removeViewByNativeId",
8
+ setPlayerControls: "setPlayerControls",
12
9
  };
13
10
 
14
11
  const PlayerNativeSendCommand = (
@@ -16,8 +13,8 @@ const PlayerNativeSendCommand = (
16
13
  playerId: string,
17
14
  userInfo?: Record<string, any>
18
15
  ) => {
19
- if (playerId && command) {
20
- NativePlayerModule?.command?.(command, playerId, userInfo);
16
+ if (isApplePlatform() && playerId && command) {
17
+ NativeModules.PlayerModule?.command?.(command, playerId, userInfo);
21
18
  }
22
19
  };
23
20