@applicaster/quick-brick-player 16.0.0-rc.10 → 16.0.0-rc.101

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 (33) 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 +87 -0
  5. package/src/Player/AudioLayer/Layout/Controls/index.tsx +34 -14
  6. package/src/Player/AudioLayer/Layout/Controls/utils.ts +40 -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/Layout/TransportInfo/__tests__/index.test.tsx +257 -0
  12. package/src/Player/AudioLayer/Layout/TransportInfo/index.tsx +69 -8
  13. package/src/Player/AudioLayer/PlaylistNavigationController.ts +354 -0
  14. package/src/Player/AudioLayer/SleepTimerManager.ts +5 -1
  15. package/src/Player/AudioLayer/SpeedManager.ts +4 -0
  16. package/src/Player/AudioLayer/__tests__/AudioDescription.test.tsx +147 -0
  17. package/src/Player/AudioLayer/__tests__/AudioPlayerWrapper.test.tsx +130 -0
  18. package/src/Player/AudioLayer/__tests__/PlaylistNavigationController.test.ts +528 -0
  19. package/src/Player/AudioLayer/__tests__/SleepTimerManager.test.ts +78 -0
  20. package/src/Player/AudioLayer/__tests__/SpeedManager.test.ts +57 -0
  21. package/src/Player/AudioLayer/__tests__/playerActions.test.ts +369 -0
  22. package/src/Player/AudioLayer/__tests__/usePlaylistNavigation.test.ts +539 -0
  23. package/src/Player/AudioLayer/playerActions.ts +294 -0
  24. package/src/Player/AudioLayer/usePlaylistNavigation.ts +148 -0
  25. package/src/Player/AudioLayer/useSleepTimerModal.ts +20 -57
  26. package/src/Player/PlayerModal/VideoPlayerModal.tsx +18 -1
  27. package/src/Player/PlayerModal/__tests__/VideoPlayerModal.test.tsx +160 -0
  28. package/src/Player/__tests__/getNativeProps.test.ts +115 -0
  29. package/src/Player/__tests__/onTracksChanged.test.ts +192 -0
  30. package/src/Player/__tests__/toNativeMetadata.test.ts +77 -0
  31. package/src/Player/index.tsx +168 -40
  32. package/src/Player/AudioLayer/components/SleepTimerModalHeader/SleepTimerModalHeader.tsx +0 -66
  33. package/src/Player/AudioLayer/components/SleepTimerModalHeader/index.ts +0 -1
@@ -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;
@@ -96,6 +148,38 @@ type VideoPlayerProps = QuickBrickPlayer.PlayerProps & {
96
148
  children?: React.ReactNode;
97
149
  };
98
150
 
151
+ /**
152
+ * `textTrackId`/`audioTrackId` are both the value the native player *reports* as
153
+ * currently selected AND the *controlled prop* that drives native selection.
154
+ * Because of that dual role, reflecting a reported value straight back down turns
155
+ * a status update into a command.
156
+ *
157
+ * The native player can momentarily report "nothing selected" (index -1, which
158
+ * `parseLanguageTracks` maps to `undefined`) during normal fMP4/ABR segment
159
+ * transitions. Pushing that back down as a "disable"/"clear" command flips native
160
+ * state, which emits a fresh onTracksChanged, which we would echo again — an
161
+ * infinite feedback loop.
162
+ *
163
+ * Keep the current selection when the reported value is a transient drop and the
164
+ * chosen track is still available. A genuine user "Off"/change already updates the
165
+ * controlled state first, so this only ignores unsolicited native status blips.
166
+ */
167
+ const resolveReportedTrackId = (
168
+ reportedId: string | null | undefined,
169
+ currentId: string | null,
170
+ tracks: { id: string }[] = []
171
+ ): string | null => {
172
+ if (reportedId) {
173
+ return reportedId;
174
+ }
175
+
176
+ if (currentId && tracks.some((track) => track?.id === currentId)) {
177
+ return currentId;
178
+ }
179
+
180
+ return null;
181
+ };
182
+
99
183
  export default class VideoPlayer extends React.Component<
100
184
  VideoPlayerProps,
101
185
  QuickBrickPlayer.NativePlayerState & {
@@ -164,20 +248,10 @@ export default class VideoPlayer extends React.Component<
164
248
  nativePlayerIsReady: false,
165
249
  };
166
250
 
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
251
  constructor(props) {
179
252
  super(props);
180
253
  this.onVideoLoadCalled = false; // android
254
+ PlayerAnalyticsManager.getInstance();
181
255
 
182
256
  this.takeParentStyle = isWeb()
183
257
  ? { width: "100%", flex: "1 1 auto", objectFit: "contain" }
@@ -191,9 +265,6 @@ export default class VideoPlayer extends React.Component<
191
265
  componentWillUnmount() {
192
266
  this.playbackStarted = false;
193
267
 
194
- this.analyticsTracker.handleAnalyticEvent(GENERAL_EVENT.session_end);
195
- this.onPlayerClosed();
196
-
197
268
  if (isAndroidPlatform()) {
198
269
  this.disablePiP();
199
270
  }
@@ -201,7 +272,6 @@ export default class VideoPlayer extends React.Component<
201
272
 
202
273
  componentDidMount() {
203
274
  this.playbackStarted = false;
204
- this.onPlayerPresented();
205
275
  }
206
276
 
207
277
  shouldComponentUpdate(nextProps) {
@@ -328,6 +398,22 @@ export default class VideoPlayer extends React.Component<
328
398
  RNPlayerManager?.[this.playerPluginId]?.stopBackgroundPlayback?.();
329
399
  };
330
400
 
401
+ command = (command: string, userInfo?: Record<string, any>) => {
402
+ if (
403
+ this.isReactNativeMethodsSupported() &&
404
+ RNPlayerManager?.[this.playerPluginId]?.command &&
405
+ this._root
406
+ ) {
407
+ const node = findNodeHandle(this._root);
408
+
409
+ if (!node) {
410
+ return;
411
+ }
412
+
413
+ RNPlayerManager?.[this.playerPluginId]?.command(node, command, userInfo);
414
+ }
415
+ };
416
+
331
417
  forward = (seekTime = SEEK_TIME) => {
332
418
  const currentTime = this.state.currentTime;
333
419
 
@@ -445,14 +531,6 @@ export default class VideoPlayer extends React.Component<
445
531
  });
446
532
  };
447
533
 
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
534
  onVideoEnd = () => {
457
535
  this.setState({ displayOverlay: !this.state.displayOverlay }, () => {
458
536
  if (this.state?.displayOverlay) {
@@ -473,6 +551,10 @@ export default class VideoPlayer extends React.Component<
473
551
  return this.props?.listener?.onError(newError);
474
552
  };
475
553
 
554
+ onRemoteCommand = ({ nativeEvent = null }) => {
555
+ this.props?.listener?.onRemoteCommand?.(nativeEvent);
556
+ };
557
+
476
558
  onPlayerPause = ({ nativeEvent = null }) => {
477
559
  this.setState({ paused: true });
478
560
  this.props?.listener?.onPlayerPause(nativeEvent);
@@ -667,20 +749,50 @@ export default class VideoPlayer extends React.Component<
667
749
  const { textTracks, audioTracks, audioTrackId, textTrackId } =
668
750
  parseLanguageTracks(nativeEvent);
669
751
 
670
- this.setState(
671
- {
672
- audioTrackId,
673
- textTrackId,
674
- },
675
- () => {
676
- this.props?.listener?.onTracksChanged?.({
677
- textTracks,
678
- audioTracks,
679
- audioTrackId,
680
- textTrackId,
681
- });
682
- }
752
+ // Resolve the reported selection against the current controlled state so a
753
+ // transient native "nothing selected" doesn't get echoed back as a command
754
+ // and re-trigger native selection (infinite loop, see resolveReportedTrackId).
755
+ const nextAudioTrackId = resolveReportedTrackId(
756
+ audioTrackId,
757
+ this.state.audioTrackId,
758
+ audioTracks
683
759
  );
760
+
761
+ const nextTextTrackId = resolveReportedTrackId(
762
+ textTrackId,
763
+ this.state.textTrackId,
764
+ textTracks
765
+ );
766
+
767
+ const notifyListener = () =>
768
+ this.props?.listener?.onTracksChanged?.({
769
+ textTracks,
770
+ audioTracks,
771
+ audioTrackId: nextAudioTrackId,
772
+ textTrackId: nextTextTrackId,
773
+ });
774
+
775
+ // Only update state (and thus re-send the controlled props) on a real change.
776
+ // Re-rendering with identical prop values would otherwise keep the native
777
+ // event/prop echo alive.
778
+ const stateChanges: {
779
+ audioTrackId?: string | null;
780
+ textTrackId?: string | null;
781
+ } = {};
782
+
783
+ if (nextAudioTrackId !== this.state.audioTrackId) {
784
+ stateChanges.audioTrackId = nextAudioTrackId;
785
+ }
786
+
787
+ if (nextTextTrackId !== this.state.textTrackId) {
788
+ stateChanges.textTrackId = nextTextTrackId;
789
+ }
790
+
791
+ if (Object.keys(stateChanges).length > 0) {
792
+ this.setState(stateChanges as any, notifyListener);
793
+ } else {
794
+ notifyListener();
795
+ }
684
796
  };
685
797
 
686
798
  onVideoProgress = ({ nativeEvent }) => {
@@ -977,6 +1089,7 @@ export default class VideoPlayer extends React.Component<
977
1089
  onPlayerResume: this.onPlayerResume,
978
1090
  onPlayerSeekComplete: this.onPlayerSeekComplete,
979
1091
  onPlayerSeekStart: this.onPlayerSeekStart,
1092
+ onRemoteCommand: this.onRemoteCommand,
980
1093
  };
981
1094
 
982
1095
  const lifecycleEvents = {
@@ -1096,6 +1209,15 @@ export default class VideoPlayer extends React.Component<
1096
1209
  const liveCatchUpEnabled =
1097
1210
  this.props.pluginConfiguration?.liveCatchUpEnabled;
1098
1211
 
1212
+ // Look the player up by id rather than using the active player: these
1213
+ // props are handed to this component's own native player view, so they
1214
+ // must describe that player's content even while a cast session is
1215
+ // running (during which `usePlayer` swaps React consumers over to the
1216
+ // casting receiver).
1217
+ const metadata = toNativeMetadata(
1218
+ playerManager.getPlayerWithId(this.props.playerId)?.getContent()
1219
+ );
1220
+
1099
1221
  const nativeProps = {
1100
1222
  rate: undefined,
1101
1223
  entry,
@@ -1114,6 +1236,7 @@ export default class VideoPlayer extends React.Component<
1114
1236
  liveCatchUpEnabled,
1115
1237
  playerId: this.props.playerId,
1116
1238
  pictureInPictureEnabled,
1239
+ metadata,
1117
1240
  seekStep:
1118
1241
  (this.props.pluginConfiguration?.seek_duration || SEEK_TIME) * 1000,
1119
1242
  accessibilityProps: getAllAccessibilityProps(
@@ -1170,10 +1293,15 @@ export default class VideoPlayer extends React.Component<
1170
1293
  return null;
1171
1294
  }
1172
1295
 
1173
- const TransportControls = isTV()
1296
+ const isTvPlatform = isTV();
1297
+
1298
+ const TransportControls = isTvPlatform
1174
1299
  ? TransportControlsTV
1175
1300
  : TransportControlsMobile;
1176
1301
 
1302
+ // The mobile and TV transport controls read Title/Subtitle from the
1303
+ // player manager's content channel (via `usePlayerContent`) instead of
1304
+ // receiving them as props.
1177
1305
  const needToShowDefaultControls = !(
1178
1306
  (controls && !castAction?.state?.connected) ||
1179
1307
  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";