@applicaster/zapp-react-native-utils 16.0.0-alpha.1833861900 → 16.0.0-alpha.1852433906
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/actionUtils/index.ts +3 -1
- package/actionsExecutor/ActionExecutor.ts +57 -12
- package/actionsExecutor/ActionExecutorContext.tsx +113 -314
- package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
- package/actionsExecutor/__tests__/StorageActions.test.ts +98 -0
- package/actionsExecutor/__tests__/feedDecorator.test.ts +91 -0
- package/actionsExecutor/__tests__/screenToggleFlag.test.ts +90 -0
- package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +25 -0
- package/actionsExecutor/actions/__tests__/goBack.test.ts +62 -0
- package/actionsExecutor/actions/__tests__/goHome.test.ts +19 -0
- package/actionsExecutor/actions/__tests__/loadItemsFromSource.test.ts +88 -0
- package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +267 -0
- package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +100 -0
- package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +372 -0
- package/actionsExecutor/actions/__tests__/presentScreen.test.ts +127 -0
- package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
- package/actionsExecutor/actions/__tests__/showToast.test.ts +88 -0
- package/actionsExecutor/actions/appRestart.ts +24 -0
- package/actionsExecutor/actions/confirmDialog.ts +53 -0
- package/actionsExecutor/actions/dismissBottomSheet.ts +23 -0
- package/actionsExecutor/actions/goBack.ts +60 -0
- package/actionsExecutor/actions/goHome.ts +31 -0
- package/actionsExecutor/actions/index.ts +42 -0
- package/actionsExecutor/actions/localStorageRemove.ts +27 -0
- package/actionsExecutor/actions/localStorageSet.ts +28 -0
- package/actionsExecutor/actions/localStorageToggleFlag.ts +28 -0
- package/actionsExecutor/actions/navigateToScreen.ts +121 -0
- package/actionsExecutor/actions/openBottomSheet.ts +340 -0
- package/actionsExecutor/actions/presentScreen.ts +78 -0
- package/actionsExecutor/actions/refreshComponent.ts +85 -0
- package/actionsExecutor/actions/screenSetVariable.ts +35 -0
- package/actionsExecutor/actions/screenToggleFlag.ts +36 -0
- package/actionsExecutor/actions/sendCloudEvent.ts +93 -0
- package/actionsExecutor/actions/sessionStorageRemove.ts +19 -0
- package/actionsExecutor/actions/sessionStorageSet.ts +20 -0
- package/actionsExecutor/actions/sessionStorageToggleFlag.ts +15 -0
- package/actionsExecutor/actions/showAlert.ts +51 -0
- package/actionsExecutor/actions/showToast.ts +87 -0
- package/actionsExecutor/actions/switchLayout.ts +40 -0
- package/actionsExecutor/consts.ts +27 -0
- package/actionsExecutor/feedDecorator.ts +12 -6
- package/actionsExecutor/types.ts +59 -0
- package/analyticsUtils/AnalyticsEvents/sendMenuClickEvent.ts +2 -2
- package/analyticsUtils/PlayerAnalyticsManager.ts +1 -1
- package/analyticsUtils/__tests__/analyticsMapper.test.ts +35 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testACP_events.json +1 -1
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testBlockUnlistedParams_rules.json +1 -1
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_events.json +4 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_rules.json +6 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEventRegex_events.json +3 -9
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_events.json +18 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_rules.json +16 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_events.json +4 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_rules.json +6 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_events.json +4 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_rules.json +6 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_events.json +4 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_rules.json +6 -0
- package/analyticsUtils/__tests__/fixtures/index.js +20 -0
- package/analyticsUtils/analyticsMapper.ts +4 -1
- package/appUtils/HooksManager/index.ts +9 -0
- package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +2 -23
- package/appUtils/contextKeysManager/__tests__/getKey/success/fullSearch.test.ts +1 -25
- package/appUtils/contextKeysManager/__tests__/getKey/success/searchInSecure.test.ts +1 -7
- package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +1 -26
- package/appUtils/contextKeysManager/__tests__/removeKey/success.test.ts +0 -6
- package/appUtils/contextKeysManager/__tests__/setKey/failure/failedToSaveInStorage.test.ts +0 -40
- package/appUtils/contextKeysManager/__tests__/setKey/failure/invalidKey.test.ts +4 -4
- package/appUtils/contextKeysManager/__tests__/setKey/{success/saveItemAndReference.test.ts → success.test.ts} +2 -42
- package/appUtils/contextKeysManager/index.ts +29 -130
- package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +142 -59
- package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
- package/appUtils/playerManager/OverlayObserver/__tests__/liveContent.test.ts +205 -0
- package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +56 -0
- package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +4 -0
- package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +4 -0
- package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +24 -0
- package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +4 -0
- package/appUtils/playerManager/OverlayObserver/utils.ts +51 -20
- package/appUtils/playerManager/__tests__/playerContent.test.ts +403 -0
- package/appUtils/playerManager/__tests__/playerDiagnostics.test.ts +62 -0
- package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
- package/appUtils/playerManager/__tests__/playerNative.test.ts +114 -0
- package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
- package/appUtils/playerManager/__tests__/playerToolbox.test.ts +281 -0
- package/appUtils/playerManager/__tests__/usePlayerContent.test.tsx +64 -0
- package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
- package/appUtils/playerManager/{conts.ts → const.ts} +33 -0
- package/appUtils/playerManager/index.ts +8 -1
- package/appUtils/playerManager/player.ts +140 -3
- package/appUtils/playerManager/playerFactory.ts +1 -1
- package/appUtils/playerManager/playerNative.ts +28 -4
- package/appUtils/playerManager/playerNativeCommand.ts +3 -6
- package/appUtils/playerManager/playerToolbox.ts +196 -0
- package/appUtils/playerManager/usePlayerContent.tsx +43 -0
- package/appUtils/playerManager/usePlayerControllerSetup.tsx +7 -1
- package/appUtils/playerManager/userLanguagePreference.ts +1 -1
- package/arrayUtils/__tests__/{anyThruthy.test.ts → anyTruthy.test.ts} +8 -0
- package/arrayUtils/__tests__/arrayUtils.test.ts +165 -108
- package/arrayUtils/__tests__/isEmptyArray.test.ts +11 -0
- package/arrayUtils/__tests__/isFilledArray.test.ts +12 -0
- package/arrayUtils/__tests__/isFirst.test.ts +17 -0
- package/arrayUtils/__tests__/isIndexInRange.test.ts +14 -0
- package/arrayUtils/__tests__/isLast.test.ts +31 -0
- package/arrayUtils/__tests__/makeListOf.test.ts +31 -0
- package/arrayUtils/__tests__/makeListOfIndexes.test.ts +20 -0
- package/arrayUtils/__tests__/reorderByIds.test.ts +63 -0
- package/arrayUtils/__tests__/sample.test.ts +61 -0
- package/arrayUtils/index.ts +119 -20
- package/bottomSheet/__tests__/scrollContext.test.tsx +35 -0
- package/bottomSheet/index.ts +17 -0
- package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
- package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
- package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
- package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
- package/cellUtils/index.ts +73 -8
- package/colorUtils/__tests__/isTransparentColor.test.ts +76 -0
- package/colorUtils/__tests__/isValidColor.test.ts +70 -0
- package/colorUtils/index.ts +50 -0
- package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
- package/configurationUtils/__tests__/modalBlocksParser.test.ts +403 -0
- package/configurationUtils/manifestKeyParser.ts +50 -10
- package/configurationUtils/modalBlocksParser.ts +252 -0
- package/entryActions/__tests__/buildEntryActions.test.ts +235 -0
- package/entryActions/aliasPresentation.ts +108 -0
- package/entryActions/buildEntryActions.ts +167 -0
- package/entryActions/index.ts +15 -0
- package/entryActions/types.ts +39 -0
- package/focusManager/FocusManager.ts +26 -0
- package/focusManager/__tests__/FocusManager.test.ts +9 -0
- package/localizationUtils/index.ts +12 -6
- package/logger/__tests__/xrayToolbox.test.ts +69 -0
- package/logger/xrayToolbox.ts +67 -0
- package/manifestUtils/__tests__/player.audioControls.test.js +158 -0
- package/manifestUtils/_internals/index.js +14 -3
- package/manifestUtils/defaultManifestConfigurations/generalContent.js +41 -0
- package/manifestUtils/defaultManifestConfigurations/player.js +357 -20
- package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +84 -0
- package/manifestUtils/fieldUtils/index.js +125 -0
- package/manifestUtils/index.js +3 -0
- package/manifestUtils/keys.js +16 -2
- package/manifestUtils/mobileAction/button/index.js +16 -0
- package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
- package/manifestUtils/mobileAction/container/index.js +3 -1
- package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
- package/manifestUtils/mobileAction/groups/defaults.js +5 -2
- package/manifestUtils/modalBlocks.js +304 -0
- package/manifestUtils/platformIsTV.js +1 -0
- package/modalState/ContentViewModel.ts +113 -0
- package/modalState/EditableCollection.ts +17 -0
- package/modalState/EditableCollectionRegistry.ts +51 -0
- package/modalState/ModalOrchestrator.ts +213 -0
- package/modalState/RemoteEditableCollection.ts +235 -0
- package/modalState/__tests__/ContentViewModel.editable.test.ts +69 -0
- package/modalState/__tests__/ContentViewModel.test.ts +165 -0
- package/modalState/__tests__/EditableCollectionRegistry.test.ts +112 -0
- package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +86 -0
- package/modalState/__tests__/RemoteEditableCollection.test.ts +415 -0
- package/modalState/__tests__/index.test.ts +30 -1
- package/modalState/__tests__/useBottomSheetContent.test.ts +349 -0
- package/modalState/__tests__/useModalStoreState.test.ts +142 -0
- package/modalState/index.ts +37 -83
- package/modalState/store.ts +109 -0
- package/modalState/types.ts +173 -0
- package/modalState/useBottomSheetContent.ts +109 -0
- package/navigationUtils/xrayNavigator.ts +12 -0
- package/numberUtils/__tests__/isMinusZero.test.ts +20 -0
- package/numberUtils/__tests__/isZero.test.ts +27 -0
- package/numberUtils/__tests__/requireNumber.test.ts +50 -0
- package/numberUtils/__tests__/toNumber.test.ts +27 -0
- package/numberUtils/__tests__/toNumberWithDefault.test.ts +64 -0
- package/numberUtils/__tests__/toNumberWithDefaultZero.test.ts +48 -0
- package/numberUtils/index.ts +27 -8
- package/package.json +3 -2
- package/pipesUtils/__tests__/buildUrlWithQuery.test.ts +33 -0
- package/pipesUtils/__tests__/withPipesEndpoint.test.tsx +56 -0
- package/pipesUtils/index.ts +1 -0
- package/pipesUtils/withPipesEndpoint.tsx +54 -0
- package/playerUtils/__tests__/contentDataKeys.test.ts +297 -0
- package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
- package/playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts +180 -0
- package/playerUtils/contentDataKeys.ts +134 -0
- package/playerUtils/index.ts +37 -16
- package/playerUtils/isAudioItem.ts +26 -0
- package/playerUtils/resolvePlayerActions.ts +71 -0
- package/playerUtils/resolvePlayerTitleSubtitle.ts +76 -0
- package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
- package/pluginUtils/index.ts +29 -0
- package/quickBrickAssets/__tests__/index.test.ts +18 -0
- package/quickBrickAssets/index.ts +35 -0
- package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
- package/reactHooks/actions/index.ts +124 -3
- package/reactHooks/cell-click/__tests__/index.test.js +64 -0
- package/reactHooks/cell-click/index.ts +39 -24
- package/reactHooks/connection/index.tsx +8 -5
- package/reactHooks/feed/__mocks__/useMarkPipesDataStale.ts +4 -0
- package/reactHooks/feed/__tests__/feedInvalidation.test.tsx +338 -0
- package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +8 -2
- package/reactHooks/feed/__tests__/useInflatedUrl.test.tsx +25 -0
- package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
- package/reactHooks/feed/index.ts +5 -1
- package/reactHooks/feed/useBatchLoading.ts +9 -1
- package/reactHooks/feed/useFeedLoader.tsx +28 -1
- package/reactHooks/feed/useLoadPipesDataDispatch.ts +12 -11
- package/reactHooks/feed/useMarkPipesDataStale.ts +97 -0
- package/reactHooks/index.ts +2 -0
- package/reactHooks/layout/index.ts +1 -1
- package/reactHooks/navigation/__mocks__/index.ts +4 -0
- package/reactHooks/navigation/__tests__/index.test.tsx +176 -1
- package/reactHooks/navigation/__tests__/useIsScreenActive.test.ts +42 -0
- package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
- package/reactHooks/navigation/index.ts +3 -1
- package/reactHooks/navigation/useIsScreenActive.ts +29 -8
- package/reactHooks/navigation/usePresentScreen.ts +157 -0
- package/reactHooks/navigation/useRoute.ts +22 -5
- package/reactHooks/state/useComponentScreenState.ts +1 -1
- package/reactHooks/usePluginConfiguration.ts +88 -47
- package/reactHooks/utils/__tests__/index.test.js +22 -2
- package/reactHooks/utils/index.ts +13 -2
- package/riverComponetsMeasurementProvider/index.tsx +15 -9
- package/stringUtils/__tests__/stringUtils.test.js +42 -0
- package/stringUtils/index.ts +2 -2
- package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
- package/uiActionsRegistrator/__tests__/subscribe.test.ts +133 -0
- package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
- package/uiActionsRegistrator/index.ts +25 -0
- package/uiActionsRegistrator/registry.ts +351 -0
- package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
- package/uiActionsRegistrator/usableActionItems.ts +66 -0
- package/zappFrameworkUtils/__tests__/localStorageHelper.test.ts +146 -0
- package/zappFrameworkUtils/localStorageHelper.ts +19 -15
- package/appUtils/contextKeysManager/__tests__/getKey/success/byReference.test.ts +0 -56
- package/appUtils/contextKeysManager/__tests__/getKeyByReference/failure.test.ts +0 -265
- package/appUtils/contextKeysManager/__tests__/getKeyByReference/success.test.ts +0 -303
- package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/failure.test.ts +0 -82
- package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/success.test.ts +0 -41
- package/appUtils/contextKeysManager/__tests__/setKey/success/saveItemButNotReference.test.ts +0 -208
- package/appUtils/contextKeysManager/__tests__/setReferenceForKey/failure.test.ts +0 -93
- package/appUtils/contextKeysManager/__tests__/setReferenceForKey/success.test.ts +0 -45
- package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +0 -56
- package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
- package/playerUtils/getPlayerActionButtons.ts +0 -17
- package/reactHooks/feed/__mocks__/usePipesCacheReset.ts +0 -1
- package/reactHooks/feed/usePipesCacheReset.ts +0 -44
- /package/reactHooks/actions/__tests__/{index.test.js → index.test.tsx} +0 -0
|
@@ -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,64 @@
|
|
|
1
|
+
import { renderHook, act } from "@testing-library/react-native";
|
|
2
|
+
import { BehaviorSubject } from "rxjs";
|
|
3
|
+
|
|
4
|
+
import { usePlayerContent } from "../usePlayerContent";
|
|
5
|
+
|
|
6
|
+
const mockPlayer: any = {
|
|
7
|
+
subject: null as BehaviorSubject<any> | null,
|
|
8
|
+
getContent: jest.fn(),
|
|
9
|
+
getContentObservable: jest.fn(),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
jest.mock("../usePlayer", () => ({
|
|
13
|
+
usePlayer: () => mockPlayer,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe("usePlayerContent", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
mockPlayer.subject = new BehaviorSubject({
|
|
19
|
+
title: "First Title",
|
|
20
|
+
summary: "First Summary",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
mockPlayer.getContent = jest.fn(() => mockPlayer.subject.getValue());
|
|
24
|
+
|
|
25
|
+
mockPlayer.getContentObservable = jest.fn(() =>
|
|
26
|
+
mockPlayer.subject.asObservable()
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns the player's current content", () => {
|
|
31
|
+
const { result } = renderHook(() => usePlayerContent());
|
|
32
|
+
|
|
33
|
+
expect(result.current).toEqual({
|
|
34
|
+
title: "First Title",
|
|
35
|
+
summary: "First Summary",
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("re-renders when the player publishes new content", () => {
|
|
40
|
+
const { result } = renderHook(() => usePlayerContent());
|
|
41
|
+
|
|
42
|
+
act(() => {
|
|
43
|
+
mockPlayer.subject.next({
|
|
44
|
+
title: "Second Title",
|
|
45
|
+
summary: "Second Summary",
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(result.current).toEqual({
|
|
50
|
+
title: "Second Title",
|
|
51
|
+
summary: "Second Summary",
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("unsubscribes on unmount", () => {
|
|
56
|
+
const { unmount } = renderHook(() => usePlayerContent());
|
|
57
|
+
|
|
58
|
+
expect(mockPlayer.subject.observed).toBe(true);
|
|
59
|
+
|
|
60
|
+
unmount();
|
|
61
|
+
|
|
62
|
+
expect(mockPlayer.subject.observed).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -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 {
|
|
@@ -67,3 +80,23 @@ export enum PlayerRole {
|
|
|
67
80
|
Chromecast = "Chromecast",
|
|
68
81
|
Unspecified = "Unspecified",
|
|
69
82
|
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The playback engine backing a web player. Published on
|
|
86
|
+
* `window.applicaster.playerType` when a player is created, so that plugins
|
|
87
|
+
* observing the player can tell which engine they are looking at instead of
|
|
88
|
+
* inferring it from the API the instance happens to expose.
|
|
89
|
+
*
|
|
90
|
+
* Keep in sync with `ApplicasterGlobals["playerType"]` in applicaster-types.
|
|
91
|
+
*/
|
|
92
|
+
export enum WebPlayerEngine {
|
|
93
|
+
Videojs = "videojs",
|
|
94
|
+
Hlsjs = "hlsjs",
|
|
95
|
+
Dashjs = "dashjs",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export enum PLAYER_EVENTS {
|
|
99
|
+
PlayNextTriggered = "play_next_triggered",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type PlayNextTriggeredEvent = { id: string };
|
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
unsubscribeToDeviceNetworkChanges,
|
|
10
10
|
} from "@applicaster/quick-brick-core/App/NetworkStatusProvider/utils";
|
|
11
11
|
import { setUserLanguagePreference } from "./userLanguagePreference";
|
|
12
|
-
import { PlayerRole } from "./
|
|
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);
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import * as R from "ramda";
|
|
2
|
-
import { BehaviorSubject } from "rxjs";
|
|
2
|
+
import { BehaviorSubject, Observable } from "rxjs";
|
|
3
|
+
import { distinctUntilChanged } from "rxjs/operators";
|
|
3
4
|
import { isEmptyOrNil } from "@applicaster/zapp-react-native-utils/cellUtils";
|
|
4
5
|
import { createLogger, utilsLogger } from "../../logger";
|
|
5
|
-
import {
|
|
6
|
-
|
|
6
|
+
import {
|
|
7
|
+
getBoolFromConfigValue,
|
|
8
|
+
imageSrcFromMediaItem,
|
|
9
|
+
} from "../../configurationUtils";
|
|
10
|
+
// Imported from the leaf modules rather than the `playerUtils` barrel: that
|
|
11
|
+
// barrel imports `playerManager`, so going through it would create a require
|
|
12
|
+
// cycle (playerManager/index -> player -> playerUtils/index -> playerManager).
|
|
13
|
+
import {
|
|
14
|
+
resolvePlayerTitleSubtitle,
|
|
15
|
+
getPlayerContentDataKeys,
|
|
16
|
+
} from "../../playerUtils/resolvePlayerTitleSubtitle";
|
|
17
|
+
import { isAudioItem } from "../../playerUtils/isAudioItem";
|
|
18
|
+
import { isTV } from "../../reactUtils";
|
|
19
|
+
import { PlayerRole, SharedPlayerCallBacksKeys } from "./const";
|
|
7
20
|
import { OverlaysObserver } from "./OverlayObserver/OverlaysObserver";
|
|
8
21
|
import { TVSeekController } from "../../reactHooks/player/TVSeekControlller/TVSeekController";
|
|
9
22
|
|
|
@@ -19,6 +32,23 @@ export declare type PlayerControllerListenerData = {
|
|
|
19
32
|
id: string;
|
|
20
33
|
listener: QuickBrickPlayer.SharedPlayerCallBacks;
|
|
21
34
|
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The player's resolved Title/Subtitle text, published by the player the same
|
|
38
|
+
* way duration and progress are. Consumers render these values as-is rather
|
|
39
|
+
* than reading `entry` or the plugin configuration themselves.
|
|
40
|
+
*/
|
|
41
|
+
export type PlayerContent = {
|
|
42
|
+
title: Option<string | number>;
|
|
43
|
+
summary: Option<string | number>;
|
|
44
|
+
/**
|
|
45
|
+
* Cover art for the content currently playing. Published alongside the text
|
|
46
|
+
* (rather than read from `entry` by whoever needs it) so it follows the same
|
|
47
|
+
* entry: on live audio that means the programme's own artwork, refreshed
|
|
48
|
+
* with every feed poll, instead of the channel's.
|
|
49
|
+
*/
|
|
50
|
+
imageUrl: Option<string>;
|
|
51
|
+
};
|
|
22
52
|
/**
|
|
23
53
|
* Player Instance Controller
|
|
24
54
|
* Instance exposes utility methods that will help set or get state
|
|
@@ -45,6 +75,13 @@ export class Player {
|
|
|
45
75
|
readonly playerRole: PlayerRole = PlayerRole.Unspecified;
|
|
46
76
|
private overlaysObserver?: OverlaysObserver | null;
|
|
47
77
|
private entryObservable?: BehaviorSubject<ZappEntry> | null;
|
|
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;
|
|
48
85
|
|
|
49
86
|
protected listeners: {
|
|
50
87
|
[key: string]: QuickBrickPlayer.SharedPlayerCallBacks;
|
|
@@ -58,7 +95,18 @@ export class Player {
|
|
|
58
95
|
|
|
59
96
|
set entry(newEntry: ZappEntry | null) {
|
|
60
97
|
this._entry = newEntry;
|
|
98
|
+
this.lastError = null;
|
|
99
|
+
// The live entry is scoped to the entry it overlays - see
|
|
100
|
+
// `OverlaysObserver.clearLiveEntry`. No-op when there is no observer,
|
|
101
|
+
// which is every non-live player.
|
|
102
|
+
this.overlaysObserver?.clearLiveEntry();
|
|
61
103
|
this.entryObservable?.next(newEntry);
|
|
104
|
+
// Optional chaining short-circuits the whole call expression, so
|
|
105
|
+
// `getContent()` is NOT evaluated while no subscriber exists. That
|
|
106
|
+
// matters: `PlayerNative` assigns `this.entry` in its constructor, before
|
|
107
|
+
// subclass arrow-function properties such as `getPluginConfiguration`
|
|
108
|
+
// have been assigned.
|
|
109
|
+
this.contentSubject?.next(this.getContent());
|
|
62
110
|
}
|
|
63
111
|
|
|
64
112
|
constructor(props) {
|
|
@@ -117,6 +165,77 @@ export class Player {
|
|
|
117
165
|
return this.entryObservable;
|
|
118
166
|
};
|
|
119
167
|
|
|
168
|
+
/**
|
|
169
|
+
* The player's resolved Title/Subtitle.
|
|
170
|
+
*
|
|
171
|
+
* Resolved on demand rather than cached: the plugin configuration arrives
|
|
172
|
+
* asynchronously on both the native and cast paths, so a value computed once
|
|
173
|
+
* at entry-set time would be stale for whichever arrives first.
|
|
174
|
+
*
|
|
175
|
+
* Every fallback (no config, empty/whitespace key, unresolvable key, key
|
|
176
|
+
* resolving to an empty string or to a non-string/non-number) lives in
|
|
177
|
+
* `resolvePlayerContentText` and is never reimplemented here.
|
|
178
|
+
*/
|
|
179
|
+
getContent = (): PlayerContent => {
|
|
180
|
+
// Live audio keeps playing one channel while the programme on air
|
|
181
|
+
// changes, so the observer's freshly-loaded programme entry wins when
|
|
182
|
+
// there is one. Read through `?.` so this never constructs an observer:
|
|
183
|
+
// a VOD or cast player has none and falls straight through to its entry.
|
|
184
|
+
const entry = this.overlaysObserver?.liveEntry ?? this.entry;
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
...resolvePlayerTitleSubtitle({
|
|
188
|
+
entry,
|
|
189
|
+
pluginConfiguration: this.getPluginConfiguration(),
|
|
190
|
+
dataKeys: getPlayerContentDataKeys({
|
|
191
|
+
isTV: isTV(),
|
|
192
|
+
isAudioContent: isAudioItem(entry),
|
|
193
|
+
}),
|
|
194
|
+
}),
|
|
195
|
+
// Same selection Android's `EntryHelpers.extractCoverImage` makes:
|
|
196
|
+
// prefer the `image_base` media item, otherwise the first one available.
|
|
197
|
+
// Reused rather than reimplemented so the two platforms cannot drift.
|
|
198
|
+
imageUrl: entry ? imageSrcFromMediaItem(entry, []) : undefined,
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Created lazily, like `entryObservable`. An eagerly created subject would
|
|
204
|
+
* resolve during `PlayerNative`'s constructor and call a not-yet-assigned
|
|
205
|
+
* `getPluginConfiguration`.
|
|
206
|
+
*
|
|
207
|
+
* The seed is produced by calling the resolver, never from raw entry fields:
|
|
208
|
+
* a `BehaviorSubject` replays its seed synchronously on subscribe, so a raw
|
|
209
|
+
* seed would overwrite correctly-resolved text at mount.
|
|
210
|
+
*/
|
|
211
|
+
getContentObservable = (): Observable<PlayerContent> => {
|
|
212
|
+
if (!this.contentSubject) {
|
|
213
|
+
this.contentSubject = new BehaviorSubject<PlayerContent>(
|
|
214
|
+
this.getContent()
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return this.contentSubject.asObservable().pipe(
|
|
219
|
+
// `resolvePlayerTitleSubtitle` returns a fresh object every call, so
|
|
220
|
+
// reference equality would never dedupe. Compare by value.
|
|
221
|
+
distinctUntilChanged(
|
|
222
|
+
(previous, next) =>
|
|
223
|
+
previous.title === next.title &&
|
|
224
|
+
previous.summary === next.summary &&
|
|
225
|
+
previous.imageUrl === next.imageUrl
|
|
226
|
+
)
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Re-resolves and publishes the current content. Called when something the
|
|
232
|
+
* resolution depends on changed without `entry` itself being reassigned -
|
|
233
|
+
* today that is the observer swapping in a newly-loaded live programme.
|
|
234
|
+
*/
|
|
235
|
+
publishContent = () => {
|
|
236
|
+
this.contentSubject?.next(this.getContent());
|
|
237
|
+
};
|
|
238
|
+
|
|
120
239
|
/**
|
|
121
240
|
* ---------------------------------------- PLAYER SETTERS GETTERS ----------------------------------------
|
|
122
241
|
* These methods let you get information about the content / ad playing
|
|
@@ -159,6 +278,10 @@ export class Player {
|
|
|
159
278
|
|
|
160
279
|
getEntry = (): ZappEntry | null => this.entry;
|
|
161
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
|
+
|
|
162
285
|
logState = (text, additionalParams = {}) => {
|
|
163
286
|
const isSeeking = this.playerState.seekPosition !== null;
|
|
164
287
|
|
|
@@ -380,6 +503,8 @@ export class Player {
|
|
|
380
503
|
};
|
|
381
504
|
|
|
382
505
|
onVideoError = (error: Error) => {
|
|
506
|
+
this.lastError = error;
|
|
507
|
+
|
|
383
508
|
this.invokeListenersUpdate({
|
|
384
509
|
callbackName: SharedPlayerCallBacksKeys.OnVideoError,
|
|
385
510
|
event: error,
|
|
@@ -387,6 +512,8 @@ export class Player {
|
|
|
387
512
|
};
|
|
388
513
|
|
|
389
514
|
onError = (error: Error) => {
|
|
515
|
+
this.lastError = error;
|
|
516
|
+
|
|
390
517
|
this.invokeListenersUpdate({
|
|
391
518
|
callbackName: SharedPlayerCallBacksKeys.OnError,
|
|
392
519
|
event: error,
|
|
@@ -634,6 +761,13 @@ export class Player {
|
|
|
634
761
|
});
|
|
635
762
|
};
|
|
636
763
|
|
|
764
|
+
onRemoteCommand = (event: QuickBrickPlayer.RemoteCommandEvent) => {
|
|
765
|
+
this.invokeListenersUpdate({
|
|
766
|
+
callbackName: SharedPlayerCallBacksKeys.OnRemoteCommand,
|
|
767
|
+
event,
|
|
768
|
+
});
|
|
769
|
+
};
|
|
770
|
+
|
|
637
771
|
getTracksState = (): QuickBrickPlayer.TracksState =>
|
|
638
772
|
this.playerState.trackState;
|
|
639
773
|
|
|
@@ -711,6 +845,8 @@ export class Player {
|
|
|
711
845
|
// PIP
|
|
712
846
|
onPlayerDetached: this.onPlayerDetached,
|
|
713
847
|
onPlayerAttached: this.onPlayerAttached,
|
|
848
|
+
|
|
849
|
+
onRemoteCommand: this.onRemoteCommand,
|
|
714
850
|
});
|
|
715
851
|
|
|
716
852
|
play = () => {};
|
|
@@ -720,6 +856,7 @@ export class Player {
|
|
|
720
856
|
setPlaybackRate = (_rate) => {};
|
|
721
857
|
startSleepTimer = (_timestamp) => {};
|
|
722
858
|
cancelSleepTimer = () => {};
|
|
859
|
+
command = (_command: string, _userInfo?: Record<string, any>) => {};
|
|
723
860
|
|
|
724
861
|
getPluginConfiguration = () => null;
|
|
725
862
|
appStateChange = (_appState, _previousAppState) => {};
|