@applicaster/quick-brick-player 16.0.0-alpha.4004661200 → 16.0.0-alpha.4065452341

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/quick-brick-player",
3
- "version": "16.0.0-alpha.4004661200",
3
+ "version": "16.0.0-alpha.4065452341",
4
4
  "description": "Quick Brick Player",
5
5
  "main": "./src/index.ts",
6
6
  "types": "index.d.ts",
@@ -37,9 +37,9 @@
37
37
  "dependencies": {
38
38
  "@applicaster/quick-brick-mobile-transport-controls": "16.0.0-rc.73",
39
39
  "@applicaster/quick-brick-tv-transport-controls": "16.0.0-rc.69",
40
- "@applicaster/zapp-react-native-tvos-app": "16.0.0-alpha.4004661200",
41
- "@applicaster/zapp-react-native-ui-components": "16.0.0-alpha.4004661200",
42
- "@applicaster/zapp-react-native-utils": "16.0.0-alpha.4004661200",
40
+ "@applicaster/zapp-react-native-tvos-app": "16.0.0-alpha.4065452341",
41
+ "@applicaster/zapp-react-native-ui-components": "16.0.0-alpha.4065452341",
42
+ "@applicaster/zapp-react-native-utils": "16.0.0-alpha.4065452341",
43
43
  "query-string": "7.1.3",
44
44
  "shaka-player": "4.3.5",
45
45
  "typeface-montserrat": "^0.0.54",
@@ -23,7 +23,6 @@ import {
23
23
  PLAYER_ACTION_IDENTIFIERS,
24
24
  registerPlayerActions,
25
25
  } from "./playerActions";
26
- import { usePlayerStateForEntry } from "./usePlayerStateForEntry";
27
26
 
28
27
  const { log_debug } = createLogger({
29
28
  subsystem: "UIComponent",
@@ -240,10 +239,6 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
240
239
  player?.seekTo(0);
241
240
  }, [player]);
242
241
 
243
- // Only the layout gets the entry-scoped position: the callbacks above act on
244
- // the live player and must keep reading its real state.
245
- const layoutPlayerState = usePlayerStateForEntry(entry, playerState);
246
-
247
242
  const layoutStateMemo = React.useMemo(
248
243
  () => ({
249
244
  inline: layoutState.inline,
@@ -264,7 +259,7 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
264
259
  <View style={Styles.playerContainer} onLayout={onLayout}>
265
260
  <Layout
266
261
  entry={entry}
267
- playerState={layoutPlayerState}
262
+ playerState={playerState}
268
263
  layoutState={layoutStateMemo}
269
264
  value={value}
270
265
  onPlaybackButtonPress={onPlaybackButtonPress}
@@ -43,9 +43,6 @@ const stateAt = (
43
43
 
44
44
  const NOT_SEEKABLE = 0;
45
45
 
46
- /** Whatever the modal is currently presenting as; a playlist step must keep it. */
47
- const MODE = "MAXIMIZED";
48
-
49
46
  const renderPlaylistNavigation = async (
50
47
  entry: ZappEntry,
51
48
  playerState: QuickBrickPlayer.PlayerState
@@ -65,10 +62,7 @@ describe("usePlaylistNavigation", () => {
65
62
  replaceTop = jest.fn();
66
63
  seekTo = jest.fn();
67
64
 
68
- useNavigationMock.mockReturnValue({
69
- replaceTop,
70
- videoModalState: { mode: MODE },
71
- });
65
+ useNavigationMock.mockReturnValue({ replaceTop });
72
66
 
73
67
  usePlayerMock.mockReturnValue({ seekTo });
74
68
 
@@ -131,29 +125,7 @@ describe("usePlaylistNavigation", () => {
131
125
  await act(async () => result.current.goNext());
132
126
 
133
127
  expect(loadFeedEntryMock).toHaveBeenCalledWith(NEXT_URL, entry);
134
- expect(replaceTop).toHaveBeenCalledWith(target, { mode: MODE });
135
- });
136
-
137
- it("keeps the modal's presentation mode instead of resetting it", async () => {
138
- // `replaceTop` lands on OPEN_VIDEO_MODAL, which forces MAXIMIZED unless a
139
- // mode is supplied - a track change must not re-present the player.
140
- useNavigationMock.mockReturnValue({
141
- replaceTop,
142
- videoModalState: { mode: "MINIMIZED" },
143
- });
144
-
145
- loadFeedEntryMock.mockResolvedValue({ id: "next" } as ZappEntry);
146
-
147
- const { result } = await renderPlaylistNavigation(
148
- entryWith({ play_next_feed_url: NEXT_URL }),
149
- stateAt(0)
150
- );
151
-
152
- await act(async () => result.current.goNext());
153
-
154
- expect(replaceTop).toHaveBeenCalledWith(expect.anything(), {
155
- mode: "MINIMIZED",
156
- });
128
+ expect(replaceTop).toHaveBeenCalledWith(target);
157
129
  });
158
130
 
159
131
  it("does nothing when there is no next url", async () => {
@@ -207,7 +179,7 @@ describe("usePlaylistNavigation", () => {
207
179
  await act(async () => result.current.goPrevious());
208
180
 
209
181
  expect(seekTo).not.toHaveBeenCalled();
210
- expect(replaceTop).toHaveBeenCalledWith(target, { mode: MODE });
182
+ expect(replaceTop).toHaveBeenCalledWith(target);
211
183
  });
212
184
 
213
185
  it("restarts the track when there is no previous entry to move to", async () => {
@@ -278,7 +250,7 @@ describe("usePlaylistNavigation", () => {
278
250
  await act(async () => result.current.goPrevious());
279
251
 
280
252
  expect(seekTo).not.toHaveBeenCalled();
281
- expect(replaceTop).toHaveBeenCalledWith(target, { mode: MODE });
253
+ expect(replaceTop).toHaveBeenCalledWith(target);
282
254
  });
283
255
 
284
256
  it("restarts past the threshold when the stream is seekable", async () => {
@@ -36,11 +36,6 @@ export const usePlaylistNavigation = (
36
36
  // seeking would miss the receiver while casting.
37
37
  const player = usePlayer();
38
38
 
39
- // `replaceTop` reaches the video modal through OPEN_VIDEO_MODAL, which resets
40
- // the presentation mode unless one is passed. Stepping through a playlist
41
- // changes what is playing, never how the player is presented.
42
- const mode = navigator.videoModalState?.mode;
43
-
44
39
  const [nextUrl, setNextUrl] = React.useState<string | null>(null);
45
40
 
46
41
  React.useEffect(() => {
@@ -93,7 +88,7 @@ export const usePlaylistNavigation = (
93
88
  targetId: target.id,
94
89
  });
95
90
 
96
- navigator.replaceTop(target, { mode });
91
+ navigator.replaceTop(target);
97
92
  } catch (error) {
98
93
  log_error("navigateTo: could not play the neighbouring entry", {
99
94
  error,
@@ -104,7 +99,7 @@ export const usePlaylistNavigation = (
104
99
  isNavigatingRef.current = false;
105
100
  }
106
101
  },
107
- [entry, navigator, mode]
102
+ [entry, navigator]
108
103
  );
109
104
 
110
105
  const goNext = React.useCallback(async () => {
@@ -1,111 +0,0 @@
1
- import { renderHook, act } from "@testing-library/react-native";
2
- import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
3
-
4
- import { usePlayerStateForEntry } from "../usePlayerStateForEntry";
5
-
6
- jest.mock(
7
- "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer",
8
- () => ({ usePlayer: jest.fn() })
9
- );
10
-
11
- const usePlayerMock = usePlayer as jest.Mock;
12
-
13
- const entryWith = (id: string) => ({ id }) as ZappEntry;
14
-
15
- const stateAt = (currentTime: number) =>
16
- ({
17
- currentTime,
18
- duration: 600,
19
- seekableDuration: 600,
20
- isPaused: false,
21
- }) as QuickBrickPlayer.PlayerState;
22
-
23
- describe("usePlayerStateForEntry", () => {
24
- /** Fires the player's `onVideoLoad`, the way a loaded source would. */
25
- let load: () => void;
26
- let removeListener: jest.Mock;
27
-
28
- beforeEach(() => {
29
- removeListener = jest.fn();
30
-
31
- usePlayerMock.mockReturnValue({
32
- addListener: ({ listener }) => {
33
- load = () => act(() => listener.onVideoLoad());
34
-
35
- return removeListener;
36
- },
37
- });
38
- });
39
-
40
- it("reports the start of the track until the source has loaded", () => {
41
- const { result } = renderHook(() =>
42
- usePlayerStateForEntry(entryWith("track-1"), stateAt(42))
43
- );
44
-
45
- expect(result.current.currentTime).toBe(0);
46
- expect(result.current.duration).toBe(0);
47
- expect(result.current.seekableDuration).toBe(0);
48
- });
49
-
50
- it("passes the player's position through once the entry has loaded", () => {
51
- const { result } = renderHook(() =>
52
- usePlayerStateForEntry(entryWith("track-1"), stateAt(42))
53
- );
54
-
55
- load();
56
-
57
- expect(result.current.currentTime).toBe(42);
58
- expect(result.current.duration).toBe(600);
59
- });
60
-
61
- it("resets to the start when the entry changes, before the new source loads", () => {
62
- // The player object survives the entry change, so it is still reporting the
63
- // outgoing track's position here. Showing it would leave the progress bar
64
- // parked at the previous track's elapsed time.
65
- const { result, rerender } = renderHook(
66
- ({ entry }) => usePlayerStateForEntry(entry, stateAt(42)),
67
- { initialProps: { entry: entryWith("track-1") } }
68
- );
69
-
70
- load();
71
-
72
- rerender({ entry: entryWith("track-2") });
73
-
74
- expect(result.current.currentTime).toBe(0);
75
- });
76
-
77
- it("follows the player again once the new source loads", () => {
78
- const { result, rerender } = renderHook(
79
- ({ entry, state }) => usePlayerStateForEntry(entry, state),
80
- {
81
- initialProps: { entry: entryWith("track-1"), state: stateAt(42) },
82
- }
83
- );
84
-
85
- load();
86
- rerender({ entry: entryWith("track-2"), state: stateAt(3) });
87
- load();
88
-
89
- expect(result.current.currentTime).toBe(3);
90
- });
91
-
92
- it("leaves everything except the position untouched", () => {
93
- const { result } = renderHook(() =>
94
- usePlayerStateForEntry(entryWith("track-1"), stateAt(42))
95
- );
96
-
97
- // The transport still needs to know it is playing while the bar reads zero,
98
- // which is what keeps the controls visible rather than reloading.
99
- expect(result.current.isPaused).toBe(false);
100
- });
101
-
102
- it("drops its subscription on unmount", () => {
103
- const { unmount } = renderHook(() =>
104
- usePlayerStateForEntry(entryWith("track-1"), stateAt(42))
105
- );
106
-
107
- unmount();
108
-
109
- expect(removeListener).toHaveBeenCalled();
110
- });
111
- });
@@ -1,63 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
4
-
5
- /** What the transport reads before the player has loaded anything. */
6
- const AT_START = {
7
- currentTime: 0,
8
- duration: 0,
9
- seekableDuration: 0,
10
- };
11
-
12
- /**
13
- * Scopes the player's reported position to the entry currently on screen.
14
- *
15
- * The player object outlives an item change - that is what keeps the player
16
- * shell mounted across a playlist step - so between the source swap and the
17
- * new track's `onVideoLoad` it still reports the outgoing track's position.
18
- * Rendering that would leave the progress bar sitting at the previous track's
19
- * elapsed time. Reporting zero instead resets the bar for the new item without
20
- * ever unmounting it.
21
- */
22
- export const usePlayerStateForEntry = (
23
- entry: ZappEntry,
24
- playerState: QuickBrickPlayer.PlayerState
25
- ): QuickBrickPlayer.PlayerState => {
26
- const player = usePlayer();
27
- const entryId = entry?.id;
28
-
29
- // `addListener` keys on the id and refuses a duplicate, so a shared constant
30
- // would leave a second player silently never hearing that its source loaded.
31
- const listenerId = `audio-player-loaded-entry-${React.useId()}`;
32
-
33
- const [loadedEntryId, setLoadedEntryId] =
34
- React.useState<Nullable<ZappEntry["id"]>>(null);
35
-
36
- // Read through a ref so the subscription can span entry changes: re-binding
37
- // the listener on every change would race the load event it is waiting for.
38
- const entryIdRef = React.useRef(entryId);
39
-
40
- entryIdRef.current = entryId;
41
-
42
- React.useEffect(() => {
43
- if (!player) {
44
- return;
45
- }
46
-
47
- const unsubscribe = player.addListener({
48
- id: listenerId,
49
- listener: {
50
- onVideoLoad: () => setLoadedEntryId(entryIdRef.current),
51
- },
52
- });
53
-
54
- return () => unsubscribe?.();
55
- }, [player, listenerId]);
56
-
57
- const isLoaded = loadedEntryId === entryId;
58
-
59
- return React.useMemo(
60
- () => (isLoaded ? playerState : { ...playerState, ...AT_START }),
61
- [isLoaded, playerState]
62
- );
63
- };