@applicaster/quick-brick-player 16.0.0-rc.7 → 16.0.0-rc.70

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 (26) hide show
  1. package/package.json +6 -6
  2. package/src/Player/AudioLayer/AudioDescription.tsx +46 -11
  3. package/src/Player/AudioLayer/AudioPlayerWrapper.tsx +38 -48
  4. package/src/Player/AudioLayer/Layout/Controls/__tests__/utils.test.ts +66 -0
  5. package/src/Player/AudioLayer/Layout/Controls/index.tsx +34 -14
  6. package/src/Player/AudioLayer/Layout/Controls/utils.ts +26 -0
  7. package/src/Player/AudioLayer/Layout/DockedControls/index.tsx +1 -13
  8. package/src/Player/AudioLayer/Layout/MobileLayout.tsx +0 -4
  9. package/src/Player/AudioLayer/Layout/TabletLandscapeLayout.tsx +0 -4
  10. package/src/Player/AudioLayer/Layout/TabletPortraitLayout.tsx +0 -4
  11. package/src/Player/AudioLayer/SleepTimerManager.ts +5 -1
  12. package/src/Player/AudioLayer/SpeedManager.ts +4 -0
  13. package/src/Player/AudioLayer/__tests__/AudioDescription.test.tsx +147 -0
  14. package/src/Player/AudioLayer/__tests__/AudioPlayerWrapper.test.tsx +130 -0
  15. package/src/Player/AudioLayer/__tests__/SleepTimerManager.test.ts +78 -0
  16. package/src/Player/AudioLayer/__tests__/SpeedManager.test.ts +57 -0
  17. package/src/Player/AudioLayer/__tests__/playerActions.test.ts +342 -0
  18. package/src/Player/AudioLayer/__tests__/usePlaylistNavigation.test.ts +387 -0
  19. package/src/Player/AudioLayer/playerActions.ts +277 -0
  20. package/src/Player/AudioLayer/usePlaylistNavigation.ts +134 -0
  21. package/src/Player/AudioLayer/useSleepTimerModal.ts +20 -57
  22. package/src/Player/__tests__/getNativeProps.test.ts +115 -0
  23. package/src/Player/__tests__/toNativeMetadata.test.ts +77 -0
  24. package/src/Player/index.tsx +72 -27
  25. package/src/Player/AudioLayer/components/SleepTimerModalHeader/SleepTimerModalHeader.tsx +0 -66
  26. package/src/Player/AudioLayer/components/SleepTimerModalHeader/index.ts +0 -1
@@ -0,0 +1,134 @@
1
+ import * as React from "react";
2
+
3
+ import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
4
+ import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
5
+ import {
6
+ loadFeedEntry,
7
+ retrieveFeedUrl,
8
+ retrievePlayPreviousFeedUrl,
9
+ } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/OverlayObserver/utils";
10
+ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
11
+
12
+ const { log_error, log_info } = createLogger({
13
+ subsystem: "Player",
14
+ category: "usePlaylistNavigation",
15
+ });
16
+
17
+ const RESTART_THRESHOLD_SECONDS = 3;
18
+
19
+ export type PlaylistNavigation = {
20
+ hasNext: boolean;
21
+ hasPrevious: boolean;
22
+ goNext: () => Promise<void>;
23
+ goPrevious: () => Promise<void>;
24
+ };
25
+
26
+ /**
27
+ * Each entry points at the feed holding its neighbour, so the chain walks
28
+ * itself and needs no client-side queue.
29
+ */
30
+ export const usePlaylistNavigation = (
31
+ entry: ZappEntry,
32
+ playerState: QuickBrickPlayer.PlayerState
33
+ ): PlaylistNavigation => {
34
+ const navigator = useNavigation();
35
+ // Not playerManager.getActivePlayer(): that is always the local player, so
36
+ // seeking would miss the receiver while casting.
37
+ const player = usePlayer();
38
+
39
+ const [nextUrl, setNextUrl] = React.useState<string | null>(null);
40
+
41
+ React.useEffect(() => {
42
+ let cancelled = false;
43
+
44
+ retrieveFeedUrl(entry)
45
+ .then((url) => {
46
+ if (!cancelled) {
47
+ setNextUrl(url ?? null);
48
+ }
49
+ })
50
+ .catch((error) => {
51
+ log_error(
52
+ "usePlaylistNavigation: could not resolve the next feed url",
53
+ { error }
54
+ );
55
+
56
+ if (!cancelled) {
57
+ setNextUrl(null);
58
+ }
59
+ });
60
+
61
+ return () => {
62
+ cancelled = true;
63
+ };
64
+ }, [entry]);
65
+
66
+ const previousUrl = retrievePlayPreviousFeedUrl(entry);
67
+ const currentTime = playerState?.currentTime ?? 0;
68
+
69
+ const canRestart = (playerState?.seekableDuration ?? 0) > 0;
70
+
71
+ const isNavigatingRef = React.useRef(false);
72
+
73
+ const navigateTo = React.useCallback(
74
+ async (feedUrl: string) => {
75
+ // A second tap mid-flight resolves to the same target, since `entry` has
76
+ // not been replaced yet.
77
+ if (isNavigatingRef.current) {
78
+ return;
79
+ }
80
+
81
+ isNavigatingRef.current = true;
82
+
83
+ try {
84
+ const target = await loadFeedEntry(feedUrl, entry);
85
+
86
+ log_info(`navigateTo: playing "${target.title}", id: ${target.id}`, {
87
+ feedUrl,
88
+ targetId: target.id,
89
+ });
90
+
91
+ navigator.replaceTop(target);
92
+ } catch (error) {
93
+ log_error("navigateTo: could not play the neighbouring entry", {
94
+ error,
95
+ feedUrl,
96
+ });
97
+ } finally {
98
+ // Released on failure too, so one bad request cannot latch the control.
99
+ isNavigatingRef.current = false;
100
+ }
101
+ },
102
+ [entry, navigator]
103
+ );
104
+
105
+ const goNext = React.useCallback(async () => {
106
+ if (!nextUrl) return;
107
+
108
+ await navigateTo(nextUrl);
109
+ }, [nextUrl, navigateTo]);
110
+
111
+ const goPrevious = React.useCallback(async () => {
112
+ // Restarting belongs to the stream, not the chain — so it stays available
113
+ // at the head, where there is nothing to step back to.
114
+ const shouldRestart =
115
+ canRestart && (currentTime > RESTART_THRESHOLD_SECONDS || !previousUrl);
116
+
117
+ if (shouldRestart) {
118
+ player?.seekTo(0);
119
+
120
+ return;
121
+ }
122
+
123
+ if (!previousUrl) return;
124
+
125
+ await navigateTo(previousUrl);
126
+ }, [previousUrl, currentTime, canRestart, navigateTo, player]);
127
+
128
+ return {
129
+ hasNext: Boolean(nextUrl),
130
+ hasPrevious: Boolean(previousUrl) || canRestart,
131
+ goNext,
132
+ goPrevious,
133
+ };
134
+ };
@@ -1,30 +1,19 @@
1
1
  import { usePlayerState } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerState";
2
+ import { uiActionsRegistry } from "@applicaster/zapp-react-native-utils/uiActionsRegistrator";
2
3
  import { useCallback, useEffect, useMemo, useState } from "react";
3
4
  import {
4
5
  SLEEP_TIMER_OFF,
5
6
  SleepTimerController,
6
7
  SleepTimerState,
7
8
  } from "./SleepTimerManager";
8
-
9
- import { openBottomSheetModal } from "@applicaster/zapp-react-native-utils/modalState";
10
-
11
- import { SleepTimerModalHeader } from "./components/SleepTimerModalHeader";
12
- import { getSleepTimerOptions } from "../Const/sleepTimerOptions";
13
9
  import {
14
- getBottomSheetModalLabelProps,
15
- getBottomSheetModalIconProps,
16
- getBottomSheetModalItemProps,
17
- getBottomSheetModalSelectedItemIcon,
18
- selectSleepTitle,
19
- selectSleepOffLabel,
20
- } from "../Utils";
10
+ PLAYER_ACTION_IDENTIFIERS,
11
+ registerPlayerActions,
12
+ } from "./playerActions";
21
13
 
22
- export const useSleepTimerModal = (player, configuration) => {
14
+ export const useSleepTimerModal = (player) => {
23
15
  const playerState = usePlayerState("audio-player-sleep-timer-modal");
24
16
 
25
- const title = configuration(selectSleepTitle);
26
- const offLabel = configuration(selectSleepOffLabel);
27
-
28
17
  const [sleepTimerManager, setSleepTimerManager] =
29
18
  useState<SleepTimerController | null>(null);
30
19
 
@@ -33,19 +22,22 @@ export const useSleepTimerModal = (player, configuration) => {
33
22
  );
34
23
 
35
24
  useEffect(() => {
36
- if (player) {
37
- setSleepTimerManager(new SleepTimerController(player));
38
- } else {
39
- setSleepTimerManager(null);
40
- }
25
+ setSleepTimerManager(player ? new SleepTimerController(player) : null);
41
26
  }, [player]);
42
27
 
43
28
  useEffect(() => {
44
29
  if (playerState?.isLive) {
45
- // Resetting a sleep timer when audio content is live (Timer not supported on live video)
46
- sleepTimerManager.setSleepTimer(SLEEP_TIMER_OFF).catch();
30
+ sleepTimerManager?.setSleepTimer(SLEEP_TIMER_OFF).catch(() => undefined);
31
+ }
32
+ }, [playerState?.isLive, sleepTimerManager]);
33
+
34
+ useEffect(() => {
35
+ if (!sleepTimerManager || playerState.isLive) {
36
+ return;
47
37
  }
48
- }, [playerState?.isLive]);
38
+
39
+ return registerPlayerActions({ sleepController: sleepTimerManager });
40
+ }, [sleepTimerManager, playerState.isLive]);
49
41
 
50
42
  useEffect(() => {
51
43
  if (!playerState.isReadyToPlay) {
@@ -64,40 +56,11 @@ export const useSleepTimerModal = (player, configuration) => {
64
56
  };
65
57
  }, [sleepTimerManager]);
66
58
 
67
- const onSleepSelect = useCallback(
68
- async (item) => {
69
- await sleepTimerManager?.setSleepTimer(item);
70
- },
71
- [sleepTimerManager]
72
- );
73
-
74
59
  const onSleepButtonPress = useCallback(() => {
75
- const timerDate = sleepTimerState.timerDate;
76
-
77
- const timerRemaining =
78
- timerDate !== 0 && +timerDate > Date.now() ? timerDate : null;
79
-
80
- openBottomSheetModal({
81
- modalBottomSheetContentProps: {
82
- headerComponent: SleepTimerModalHeader,
83
- items: getSleepTimerOptions(offLabel, timerRemaining),
84
- onPress: onSleepSelect,
85
- title,
86
- summary: timerRemaining,
87
- itemProps: getBottomSheetModalItemProps,
88
- iconProps: getBottomSheetModalIconProps,
89
- labelProps: getBottomSheetModalLabelProps,
90
- getSelectedItemIcon: getBottomSheetModalSelectedItemIcon,
91
- },
92
- options: {
93
- supportedOrientations: [
94
- "portrait",
95
- "landscape",
96
- "portrait-upside-down",
97
- ],
98
- },
99
- });
100
- }, [sleepTimerState.timerDate, sleepTimerState.timer, title, onSleepSelect]);
60
+ uiActionsRegistry
61
+ .getAction(PLAYER_ACTION_IDENTIFIERS.SLEEP_TIMER)
62
+ ?.invokeAction(player?.entry);
63
+ }, [player]);
101
64
 
102
65
  return useMemo(
103
66
  () => ({
@@ -0,0 +1,115 @@
1
+ import VideoPlayer from "../index";
2
+ import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils";
3
+
4
+ /**
5
+ * Pins where `getNativeProps()`'s `metadata` object comes from: the player
6
+ * manager's content channel, looked up by `props.playerId`.
7
+ *
8
+ * This deliberately does NOT depend on tree position. An earlier version read
9
+ * the values from `PlayerContainerContext` via `static contextType`, which
10
+ * made native's props depend on `Player` being rendered inside
11
+ * `PlayerContainerContextProvider` - safe only as long as an exhaustive
12
+ * render-site search stayed true. Reading from the manager removes that whole
13
+ * class of fragility, so the tests below assert the value's source rather than
14
+ * a render-site invariant.
15
+ *
16
+ * Lookup is by id rather than `getActivePlayer()` because these props are
17
+ * handed to this component's own native view: they must describe that player
18
+ * even while a cast session is running.
19
+ *
20
+ * The instance is constructed directly (bypassing `render()`/mount) so this
21
+ * exercises the real `getNativeProps()` without paying for a full render of
22
+ * the ~37KB `VideoPlayer` class tree.
23
+ */
24
+
25
+ jest.mock("@applicaster/zapp-react-native-utils/appUtils", () => ({
26
+ ...jest.requireActual("@applicaster/zapp-react-native-utils/appUtils"),
27
+ playerManager: {
28
+ getPlayerWithId: jest.fn(),
29
+ },
30
+ }));
31
+
32
+ const entry = {
33
+ id: "1",
34
+ title: "Entry Title",
35
+ summary: "Entry Summary",
36
+ } as unknown as ZappEntry;
37
+
38
+ const baseProps = {
39
+ entry,
40
+ pluginConfiguration: {},
41
+ docked: false,
42
+ fullscreen: false,
43
+ inline: false,
44
+ isModal: false,
45
+ isTabletPortrait: false,
46
+ muted: false,
47
+ playerId: "player-1",
48
+ PlayerComponent: "QuickBrickDefaultPlayerView",
49
+ } as unknown as ConstructorParameters<typeof VideoPlayer>[0];
50
+
51
+ const publishContent = (
52
+ content: {
53
+ title: Option<string | number>;
54
+ summary: Option<string | number>;
55
+ } | null
56
+ ) => {
57
+ (playerManager.getPlayerWithId as jest.Mock).mockReturnValue(
58
+ content ? { getContent: () => content } : null
59
+ );
60
+ };
61
+
62
+ describe("Player's getNativeProps() reading Title/Subtitle from the manager", () => {
63
+ it("sends the title/summary the player published", () => {
64
+ publishContent({
65
+ title: "Resolved Title",
66
+ summary: "Resolved Summary",
67
+ });
68
+
69
+ const nativeProps = new VideoPlayer(baseProps).getNativeProps();
70
+
71
+ expect(nativeProps.metadata.title).toBe("Resolved Title");
72
+ expect(nativeProps.metadata.subtitle).toBe("Resolved Summary");
73
+ });
74
+
75
+ it("looks the player up by its own id, not the active player", () => {
76
+ publishContent({ title: "Resolved Title", summary: "Resolved Summary" });
77
+
78
+ new VideoPlayer(baseProps).getNativeProps();
79
+
80
+ expect(playerManager.getPlayerWithId).toHaveBeenCalledWith("player-1");
81
+ });
82
+
83
+ it("sends undefined when the player published no text", () => {
84
+ publishContent({ title: null, summary: null });
85
+
86
+ const nativeProps = new VideoPlayer(baseProps).getNativeProps();
87
+
88
+ expect(nativeProps.metadata.title).toBeUndefined();
89
+ expect(nativeProps.metadata.subtitle).toBeUndefined();
90
+ });
91
+
92
+ it("sends undefined rather than crashing when no player is registered", () => {
93
+ publishContent(null);
94
+
95
+ const nativeProps = new VideoPlayer(baseProps).getNativeProps();
96
+
97
+ expect(nativeProps.metadata.title).toBeUndefined();
98
+ expect(nativeProps.metadata.subtitle).toBeUndefined();
99
+ });
100
+
101
+ it("does not fall back to entry.title/entry.summary itself - the resolver owns that fallback", () => {
102
+ // `entry.title`/`entry.summary` are "Entry Title"/"Entry Summary" (see
103
+ // `baseProps`); with nothing published, nativeProps must NOT silently pick
104
+ // those up as a second, redundant fallback layer inside `Player`. The
105
+ // entry-field fallback lives in `resolvePlayerContentText`, reached
106
+ // through the player's own `getContent()`.
107
+ publishContent({ title: null, summary: null });
108
+
109
+ const nativeProps = new VideoPlayer(baseProps).getNativeProps();
110
+
111
+ expect(nativeProps.metadata.title).not.toBe(entry.title);
112
+ expect(nativeProps.metadata.subtitle).not.toBe(entry.summary);
113
+ expect(nativeProps.entry).toBe(entry);
114
+ });
115
+ });
@@ -0,0 +1,77 @@
1
+ import { toNativeMetadata } from "../index";
2
+
3
+ // `toNativeMetadata` used to be `resolveNativeTitleSubtitle`, which resolved
4
+ // Title/Subtitle from `entry` + `pluginConfiguration` itself (the dot-path /
5
+ // custom-key / fallback logic). That resolution now happens once in
6
+ // `Player.getContent()` (via `resolvePlayerTitleSubtitle`, already covered by
7
+ // `playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts` and
8
+ // `playerUtils/__tests__/contentDataKeys.test.ts` - custom-key resolution,
9
+ // fallback-when-unresolvable, and entry-non-mutation are all still asserted
10
+ // there). `toNativeMetadata` only adapts the already-resolved
11
+ // `player.getContent()` value into the `metadata` object the native bridge
12
+ // receives, so this file only covers that adaptation: the shape coercion, and
13
+ // the numeric coercion the native bridge specifically needs (it cannot render
14
+ // a number the way `<Text>` can).
15
+ describe("toNativeMetadata", () => {
16
+ it("returns undefined for both fields when title/summary are null (what the player publishes when there is nothing to show)", () => {
17
+ expect(toNativeMetadata({ title: null, summary: null })).toEqual({
18
+ title: undefined,
19
+ subtitle: undefined,
20
+ });
21
+ });
22
+
23
+ it("returns undefined for both fields when content itself is undefined", () => {
24
+ expect(toNativeMetadata(undefined)).toEqual({
25
+ title: undefined,
26
+ subtitle: undefined,
27
+ });
28
+ });
29
+
30
+ it("passes through string title/summary as-is", () => {
31
+ expect(
32
+ toNativeMetadata({
33
+ title: "Resolved Title",
34
+ summary: "Resolved Summary",
35
+ })
36
+ ).toEqual({
37
+ title: "Resolved Title",
38
+ subtitle: "Resolved Summary",
39
+ });
40
+ });
41
+
42
+ it("coerces a numeric title/summary to a string for the native bridge", () => {
43
+ expect(toNativeMetadata({ title: 0, summary: 42 })).toEqual({
44
+ title: "0",
45
+ subtitle: "42",
46
+ });
47
+ });
48
+
49
+ it("handles title and summary independently (one present, one absent)", () => {
50
+ expect(toNativeMetadata({ title: "Only Title", summary: null })).toEqual({
51
+ title: "Only Title",
52
+ subtitle: undefined,
53
+ });
54
+ });
55
+
56
+ it("passes the cover image url through", () => {
57
+ expect(
58
+ toNativeMetadata({
59
+ title: "A Title",
60
+ summary: "A Summary",
61
+ imageUrl: "https://img.test/base.jpg",
62
+ })
63
+ ).toEqual({
64
+ title: "A Title",
65
+ subtitle: "A Summary",
66
+ imageUrl: "https://img.test/base.jpg",
67
+ });
68
+ });
69
+
70
+ it("sends undefined rather than an empty string when there is no image", () => {
71
+ // Native treats an empty URI as a load attempt; `undefined` means "none".
72
+ expect(
73
+ toNativeMetadata({ title: "A Title", summary: null, imageUrl: null })
74
+ .imageUrl
75
+ ).toBeUndefined();
76
+ });
77
+ });
@@ -31,7 +31,6 @@ import {
31
31
  import { PlayNextOverlay } from "./PlayNextOverlay";
32
32
  import { PlayerImageBackground } from "@applicaster/zapp-react-native-ui-components/Components/PlayerImageBackground";
33
33
 
34
- import { GENERAL_EVENT } from "@applicaster/zapp-react-native-utils/analyticsUtils/events";
35
34
  import { parseLanguageTracks } from "@applicaster/zapp-react-native-utils/playerUtils/configurationUtils";
36
35
  import { DEFAULT_COLORS, SEEK_TIME } from "../utils/const";
37
36
  import { logger, getScreenAspectRatio } from "../utils";
@@ -41,7 +40,6 @@ import { getAllAccessibilityProps } from "@applicaster/zapp-react-native-utils/c
41
40
  import { PlayNextData } from "@applicaster/zapp-react-native-ui-components/Components/PlayerContainer/PlayerContainer";
42
41
  import { getTargetScreenDataFromEntry } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useTargetScreenData";
43
42
  import { PlayerAnalyticsManager } from "@applicaster/zapp-react-native-utils/analyticsUtils/PlayerAnalyticsManager";
44
- import { PlayerAnalyticsTrackerI } from "@applicaster/zapp-react-native-utils/analyticsUtils/playerAnalyticsTracker";
45
43
 
46
44
  import { RNPlayerManager } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/nativePlayerManager";
47
45
  import {
@@ -54,7 +52,7 @@ import { TVPlayerView } from "./PlayerView/TVPlayerView";
54
52
  import PlayerModal, { VideoPlayerModal } from "./PlayerModal";
55
53
  import { DockedControls } from "./AudioLayer/Layout/DockedControls";
56
54
  import { FlexImage } from "./AudioLayer/Layout/PlayerImage/FlexImage";
57
- import { PlayerError } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/conts";
55
+ import { PlayerError } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/const";
58
56
 
59
57
  const isTvOS = isTvOSPlatform();
60
58
 
@@ -71,6 +69,60 @@ const styles = StyleSheet.create({
71
69
  flexImage: { height: "100%", aspectRatio: 1, flex: -1 },
72
70
  });
73
71
 
72
+ /**
73
+ * Coerces the resolver's `Option<string | number>` result into what the
74
+ * native bridge expects: a string, or `undefined` when there is nothing to
75
+ * send.
76
+ */
77
+ const toNativeTextProp = (
78
+ value: Option<string | number>
79
+ ): string | undefined =>
80
+ value === null || value === undefined ? undefined : String(value);
81
+
82
+ export type NativePlayerMetadata = {
83
+ title: string | undefined;
84
+ subtitle: string | undefined;
85
+ imageUrl: string | undefined;
86
+ };
87
+
88
+ /**
89
+ * Adapts the player's resolved Title/Subtitle into the `metadata` object the
90
+ * *native* view receives.
91
+ *
92
+ * The resolution itself (dot-path / key-set logic, entry fallback) does not
93
+ * happen here - the player manager resolves it and publishes it on its content
94
+ * channel (`player.getContent()`). This function only coerces that value's
95
+ * `Option<string | number>` shape into what the native bridge needs: a string,
96
+ * or `undefined` when there is nothing to send.
97
+ *
98
+ * Travels as one `metadata` object rather than two flat props so native has a
99
+ * single place to look for player-facing text, and so adding a field later
100
+ * does not widen the bridge's prop surface again.
101
+ *
102
+ * This intentionally does not touch `entry` - analytics and the downloads
103
+ * feature read `entry.title` / `entry.summary` directly and must keep
104
+ * seeing the original feed values (the downloads feature requires the
105
+ * original entry title). The resolved (possibly customer-configured) text
106
+ * travels in `metadata` instead.
107
+ *
108
+ * Exported as a plain function (rather than inlined as a class method) so
109
+ * it can be unit tested directly without rendering the ~37KB `VideoPlayer`
110
+ * class component.
111
+ */
112
+ export const toNativeMetadata = (
113
+ content: Option<{
114
+ title: Option<string | number>;
115
+ summary: Option<string | number>;
116
+ imageUrl?: Option<string>;
117
+ }>
118
+ ): NativePlayerMetadata => ({
119
+ title: toNativeTextProp(content?.title),
120
+ subtitle: toNativeTextProp(content?.summary),
121
+ // Already a string when present; coerced through the same helper so an
122
+ // empty or nullish value reaches native as `undefined` rather than "".
123
+ imageUrl: toNativeTextProp(content?.imageUrl),
124
+ });
125
+
74
126
  type VideoPlayerProps = QuickBrickPlayer.PlayerProps & {
75
127
  isCasting?: boolean;
76
128
  playNextData?: PlayNextData;
@@ -164,20 +216,10 @@ export default class VideoPlayer extends React.Component<
164
216
  nativePlayerIsReady: false,
165
217
  };
166
218
 
167
- private _tracker: PlayerAnalyticsTrackerI;
168
- get analyticsTracker() {
169
- if (!this._tracker) {
170
- this._tracker = PlayerAnalyticsManager.getInstance().getTracker(
171
- this.props.playerId
172
- );
173
- }
174
-
175
- return this._tracker;
176
- }
177
-
178
219
  constructor(props) {
179
220
  super(props);
180
221
  this.onVideoLoadCalled = false; // android
222
+ PlayerAnalyticsManager.getInstance();
181
223
 
182
224
  this.takeParentStyle = isWeb()
183
225
  ? { width: "100%", flex: "1 1 auto", objectFit: "contain" }
@@ -191,9 +233,6 @@ export default class VideoPlayer extends React.Component<
191
233
  componentWillUnmount() {
192
234
  this.playbackStarted = false;
193
235
 
194
- this.analyticsTracker.handleAnalyticEvent(GENERAL_EVENT.session_end);
195
- this.onPlayerClosed();
196
-
197
236
  if (isAndroidPlatform()) {
198
237
  this.disablePiP();
199
238
  }
@@ -201,7 +240,6 @@ export default class VideoPlayer extends React.Component<
201
240
 
202
241
  componentDidMount() {
203
242
  this.playbackStarted = false;
204
- this.onPlayerPresented();
205
243
  }
206
244
 
207
245
  shouldComponentUpdate(nextProps) {
@@ -445,14 +483,6 @@ export default class VideoPlayer extends React.Component<
445
483
  });
446
484
  };
447
485
 
448
- onPlayerPresented = () => {
449
- this.analyticsTracker?.handleAnalyticEvent(GENERAL_EVENT.player_presented);
450
- };
451
-
452
- onPlayerClosed = () => {
453
- this.analyticsTracker?.handleAnalyticEvent(GENERAL_EVENT.player_closed);
454
- };
455
-
456
486
  onVideoEnd = () => {
457
487
  this.setState({ displayOverlay: !this.state.displayOverlay }, () => {
458
488
  if (this.state?.displayOverlay) {
@@ -1096,6 +1126,15 @@ export default class VideoPlayer extends React.Component<
1096
1126
  const liveCatchUpEnabled =
1097
1127
  this.props.pluginConfiguration?.liveCatchUpEnabled;
1098
1128
 
1129
+ // Look the player up by id rather than using the active player: these
1130
+ // props are handed to this component's own native player view, so they
1131
+ // must describe that player's content even while a cast session is
1132
+ // running (during which `usePlayer` swaps React consumers over to the
1133
+ // casting receiver).
1134
+ const metadata = toNativeMetadata(
1135
+ playerManager.getPlayerWithId(this.props.playerId)?.getContent()
1136
+ );
1137
+
1099
1138
  const nativeProps = {
1100
1139
  rate: undefined,
1101
1140
  entry,
@@ -1114,6 +1153,7 @@ export default class VideoPlayer extends React.Component<
1114
1153
  liveCatchUpEnabled,
1115
1154
  playerId: this.props.playerId,
1116
1155
  pictureInPictureEnabled,
1156
+ metadata,
1117
1157
  seekStep:
1118
1158
  (this.props.pluginConfiguration?.seek_duration || SEEK_TIME) * 1000,
1119
1159
  accessibilityProps: getAllAccessibilityProps(
@@ -1170,10 +1210,15 @@ export default class VideoPlayer extends React.Component<
1170
1210
  return null;
1171
1211
  }
1172
1212
 
1173
- const TransportControls = isTV()
1213
+ const isTvPlatform = isTV();
1214
+
1215
+ const TransportControls = isTvPlatform
1174
1216
  ? TransportControlsTV
1175
1217
  : TransportControlsMobile;
1176
1218
 
1219
+ // The mobile and TV transport controls read Title/Subtitle from the
1220
+ // player manager's content channel (via `usePlayerContent`) instead of
1221
+ // receiving them as props.
1177
1222
  const needToShowDefaultControls = !(
1178
1223
  (controls && !castAction?.state?.connected) ||
1179
1224
  this.getPlayNextData()
@@ -1,66 +0,0 @@
1
- import React from "react";
2
- import { ModalHeader } from "@applicaster/zapp-react-native-ui-components/Components/ModalComponent/Header";
3
- import { isNil } from "@applicaster/zapp-react-native-utils/utils";
4
-
5
- const formatTime = (milliseconds: number) => {
6
- const totalSeconds = Math.floor(milliseconds / 1000);
7
- const hours = Math.floor(totalSeconds / 3600);
8
- const minutes = Math.floor((totalSeconds % 3600) / 60);
9
- const seconds = totalSeconds % 60;
10
-
11
- if (milliseconds < 3600000) {
12
- return `${minutes.toString().padStart(2, "0")}:${seconds
13
- .toString()
14
- .padStart(2, "0")}`;
15
- }
16
-
17
- return `${hours.toString().padStart(2, "0")}:${minutes
18
- .toString()
19
- .padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
20
- };
21
-
22
- export const SleepTimerModalHeader = ({
23
- summary,
24
- ...props
25
- }: {
26
- summary?: string;
27
- }) => {
28
- const sleepTimerDate = +summary;
29
-
30
- const [timeLeft, setTimeLeft] = React.useState(
31
- sleepTimerDate !== 0 ? sleepTimerDate - Date.now() : 0
32
- );
33
-
34
- const showCounter = React.useMemo(() => sleepTimerDate > 0, [sleepTimerDate]);
35
-
36
- React.useLayoutEffect(() => {
37
- if (showCounter && sleepTimerDate !== 0) {
38
- setTimeLeft(sleepTimerDate - Date.now());
39
-
40
- const intervalId = setInterval(() => {
41
- setTimeLeft(() => {
42
- const newTimeLeft = sleepTimerDate - Date.now();
43
-
44
- if (newTimeLeft <= 0) {
45
- clearInterval(intervalId);
46
-
47
- return 0;
48
- } else {
49
- return newTimeLeft;
50
- }
51
- });
52
- }, 1000);
53
-
54
- return () => clearInterval(intervalId);
55
- } else {
56
- setTimeLeft(0);
57
- }
58
- }, [sleepTimerDate, showCounter]);
59
-
60
- return (
61
- <ModalHeader
62
- summary={!isNil(summary) ? formatTime(timeLeft) : summary}
63
- {...(props as any)}
64
- />
65
- );
66
- };
@@ -1 +0,0 @@
1
- export * from "./SleepTimerModalHeader";