@applicaster/quick-brick-player 16.0.0-rc.6 → 16.0.0-rc.60

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 +351 -0
  18. package/src/Player/AudioLayer/__tests__/usePlaylistNavigation.test.ts +387 -0
  19. package/src/Player/AudioLayer/components/SleepTimerModalHeader/SleepTimerModalHeader.tsx +9 -8
  20. package/src/Player/AudioLayer/components/SleepTimerModalHeader/__tests__/SleepTimerModalHeader.test.tsx +53 -0
  21. package/src/Player/AudioLayer/playerActions.ts +280 -0
  22. package/src/Player/AudioLayer/usePlaylistNavigation.ts +134 -0
  23. package/src/Player/AudioLayer/useSleepTimerModal.ts +20 -57
  24. package/src/Player/__tests__/getNativeProps.test.ts +115 -0
  25. package/src/Player/__tests__/toNativeMetadata.test.ts +77 -0
  26. package/src/Player/index.tsx +72 -27
@@ -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()