@applicaster/quick-brick-player 16.0.0-alpha.1151561937 → 16.0.0-alpha.1212303401
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.
|
|
3
|
+
"version": "16.0.0-alpha.1212303401",
|
|
4
4
|
"description": "Quick Brick Player",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@applicaster/quick-brick-mobile-transport-controls": "16.0.0-rc.60",
|
|
39
|
-
"@applicaster/quick-brick-tv-transport-controls": "16.0.0-rc.
|
|
40
|
-
"@applicaster/zapp-react-native-tvos-app": "16.0.0-alpha.
|
|
41
|
-
"@applicaster/zapp-react-native-ui-components": "16.0.0-alpha.
|
|
42
|
-
"@applicaster/zapp-react-native-utils": "16.0.0-alpha.
|
|
39
|
+
"@applicaster/quick-brick-tv-transport-controls": "16.0.0-rc.69",
|
|
40
|
+
"@applicaster/zapp-react-native-tvos-app": "16.0.0-alpha.1212303401",
|
|
41
|
+
"@applicaster/zapp-react-native-ui-components": "16.0.0-alpha.1212303401",
|
|
42
|
+
"@applicaster/zapp-react-native-utils": "16.0.0-alpha.1212303401",
|
|
43
43
|
"query-string": "7.1.3",
|
|
44
44
|
"shaka-player": "4.3.5",
|
|
45
45
|
"typeface-montserrat": "^0.0.54",
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import VideoPlayer from "../index";
|
|
2
|
+
import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests for the track-selection reconciliation logic in `onTracksChanged`.
|
|
6
|
+
*
|
|
7
|
+
* The handler uses `resolveReportedTrackId` to prevent an infinite feedback
|
|
8
|
+
* loop: a transient native "nothing selected" (reported id === undefined)
|
|
9
|
+
* must NOT clear controlled state when the previous track is still available,
|
|
10
|
+
* while genuine track changes must still propagate.
|
|
11
|
+
*
|
|
12
|
+
* Instances are constructed directly (no render) to keep these tests fast and
|
|
13
|
+
* focused on the instance method, mirroring the pattern used in
|
|
14
|
+
* `getNativeProps.test.ts`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
jest.mock("@applicaster/zapp-react-native-utils/appUtils", () => ({
|
|
18
|
+
...jest.requireActual("@applicaster/zapp-react-native-utils/appUtils"),
|
|
19
|
+
playerManager: {
|
|
20
|
+
getPlayerWithId: jest.fn(),
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
jest.mock(
|
|
25
|
+
"@applicaster/zapp-react-native-utils/playerUtils/configurationUtils",
|
|
26
|
+
() => ({
|
|
27
|
+
parseLanguageTracks: jest.fn(),
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
import { parseLanguageTracks } from "@applicaster/zapp-react-native-utils/playerUtils/configurationUtils";
|
|
32
|
+
|
|
33
|
+
const baseProps = {
|
|
34
|
+
entry: { id: "1", title: "Entry Title", summary: "Entry Summary" },
|
|
35
|
+
pluginConfiguration: {},
|
|
36
|
+
docked: false,
|
|
37
|
+
fullscreen: false,
|
|
38
|
+
inline: false,
|
|
39
|
+
isModal: false,
|
|
40
|
+
isTabletPortrait: false,
|
|
41
|
+
muted: false,
|
|
42
|
+
playerId: "player-1",
|
|
43
|
+
PlayerComponent: "QuickBrickDefaultPlayerView",
|
|
44
|
+
} as unknown as ConstructorParameters<typeof VideoPlayer>[0];
|
|
45
|
+
|
|
46
|
+
const audioTrack = { id: "en-audio", type: "audio" };
|
|
47
|
+
const textTrack = { id: "en-text", type: "text" };
|
|
48
|
+
|
|
49
|
+
/** Helper: call onTracksChanged with a mocked parseLanguageTracks result. */
|
|
50
|
+
function callOnTracksChanged(
|
|
51
|
+
instance: VideoPlayer,
|
|
52
|
+
parsed: {
|
|
53
|
+
audioTracks?: { id: string; type: string }[];
|
|
54
|
+
textTracks?: { id: string; type: string }[];
|
|
55
|
+
audioTrackId?: string | undefined;
|
|
56
|
+
textTrackId?: string | undefined;
|
|
57
|
+
}
|
|
58
|
+
) {
|
|
59
|
+
(parseLanguageTracks as jest.Mock).mockReturnValue({
|
|
60
|
+
audioTracks: [],
|
|
61
|
+
textTracks: [],
|
|
62
|
+
audioTrackId: undefined,
|
|
63
|
+
textTrackId: undefined,
|
|
64
|
+
...parsed,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
instance.onTracksChanged({ nativeEvent: {} });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe("onTracksChanged — resolveReportedTrackId reconciliation", () => {
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
(playerManager.getPlayerWithId as jest.Mock).mockReturnValue(null);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("sets audioTrackId and textTrackId when native reports real selections", () => {
|
|
76
|
+
const instance = new VideoPlayer(baseProps);
|
|
77
|
+
const setState = jest.spyOn(instance, "setState");
|
|
78
|
+
|
|
79
|
+
callOnTracksChanged(instance, {
|
|
80
|
+
audioTracks: [audioTrack],
|
|
81
|
+
textTracks: [textTrack],
|
|
82
|
+
audioTrackId: audioTrack.id,
|
|
83
|
+
textTrackId: textTrack.id,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(setState).toHaveBeenCalledWith(
|
|
87
|
+
expect.objectContaining({
|
|
88
|
+
audioTrackId: audioTrack.id,
|
|
89
|
+
textTrackId: textTrack.id,
|
|
90
|
+
}),
|
|
91
|
+
expect.any(Function)
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("does NOT clear audioTrackId when native transiently reports undefined and the track is still in the list", () => {
|
|
96
|
+
const instance = new VideoPlayer(baseProps);
|
|
97
|
+
// Establish prior selection state without a React reconciler.
|
|
98
|
+
instance.state = { ...instance.state, audioTrackId: audioTrack.id };
|
|
99
|
+
|
|
100
|
+
const setState = jest.spyOn(instance, "setState");
|
|
101
|
+
|
|
102
|
+
callOnTracksChanged(instance, {
|
|
103
|
+
audioTracks: [audioTrack],
|
|
104
|
+
textTracks: [],
|
|
105
|
+
audioTrackId: undefined, // transient native blip — nothing selected
|
|
106
|
+
textTrackId: undefined,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// State must NOT change because the resolved id equals the current id.
|
|
110
|
+
expect(setState).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("does NOT clear textTrackId when native transiently reports undefined and the track is still in the list", () => {
|
|
114
|
+
const instance = new VideoPlayer(baseProps);
|
|
115
|
+
instance.state = { ...instance.state, textTrackId: textTrack.id };
|
|
116
|
+
|
|
117
|
+
const setState = jest.spyOn(instance, "setState");
|
|
118
|
+
|
|
119
|
+
callOnTracksChanged(instance, {
|
|
120
|
+
audioTracks: [],
|
|
121
|
+
textTracks: [textTrack],
|
|
122
|
+
audioTrackId: undefined,
|
|
123
|
+
textTrackId: undefined,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(setState).not.toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("clears the id when the previously-selected track is no longer present in the list", () => {
|
|
130
|
+
const instance = new VideoPlayer(baseProps);
|
|
131
|
+
instance.state = { ...instance.state, audioTrackId: audioTrack.id };
|
|
132
|
+
|
|
133
|
+
const setState = jest.spyOn(instance, "setState");
|
|
134
|
+
|
|
135
|
+
// Track is gone from the available list.
|
|
136
|
+
callOnTracksChanged(instance, {
|
|
137
|
+
audioTracks: [], // track not available anymore
|
|
138
|
+
textTracks: [],
|
|
139
|
+
audioTrackId: undefined,
|
|
140
|
+
textTrackId: undefined,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(setState).toHaveBeenCalledWith(
|
|
144
|
+
expect.objectContaining({ audioTrackId: null }),
|
|
145
|
+
expect.any(Function)
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("propagates a genuine track change to a different id", () => {
|
|
150
|
+
const newTrack = { id: "fr-audio", type: "audio" };
|
|
151
|
+
const instance = new VideoPlayer(baseProps);
|
|
152
|
+
instance.state = { ...instance.state, audioTrackId: audioTrack.id };
|
|
153
|
+
|
|
154
|
+
const setState = jest.spyOn(instance, "setState");
|
|
155
|
+
|
|
156
|
+
callOnTracksChanged(instance, {
|
|
157
|
+
audioTracks: [audioTrack, newTrack],
|
|
158
|
+
textTracks: [],
|
|
159
|
+
audioTrackId: newTrack.id,
|
|
160
|
+
textTrackId: undefined,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(setState).toHaveBeenCalledWith(
|
|
164
|
+
expect.objectContaining({ audioTrackId: newTrack.id }),
|
|
165
|
+
expect.any(Function)
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("calls the listener with resolved ids even when state does not change", () => {
|
|
170
|
+
const instance = new VideoPlayer(baseProps);
|
|
171
|
+
instance.state = { ...instance.state, audioTrackId: audioTrack.id };
|
|
172
|
+
|
|
173
|
+
const onTracksChangedListener = jest.fn();
|
|
174
|
+
|
|
175
|
+
(instance as any).props = {
|
|
176
|
+
...instance.props,
|
|
177
|
+
listener: { onTracksChanged: onTracksChangedListener },
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
callOnTracksChanged(instance, {
|
|
181
|
+
audioTracks: [audioTrack],
|
|
182
|
+
textTracks: [],
|
|
183
|
+
audioTrackId: undefined, // transient blip
|
|
184
|
+
textTrackId: undefined,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// notifyListener is called with the resolved (non-null) audioTrackId
|
|
188
|
+
expect(onTracksChangedListener).toHaveBeenCalledWith(
|
|
189
|
+
expect.objectContaining({ audioTrackId: audioTrack.id })
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
});
|
package/src/Player/index.tsx
CHANGED
|
@@ -148,6 +148,38 @@ type VideoPlayerProps = QuickBrickPlayer.PlayerProps & {
|
|
|
148
148
|
children?: React.ReactNode;
|
|
149
149
|
};
|
|
150
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
|
+
|
|
151
183
|
export default class VideoPlayer extends React.Component<
|
|
152
184
|
VideoPlayerProps,
|
|
153
185
|
QuickBrickPlayer.NativePlayerState & {
|
|
@@ -697,20 +729,50 @@ export default class VideoPlayer extends React.Component<
|
|
|
697
729
|
const { textTracks, audioTracks, audioTrackId, textTrackId } =
|
|
698
730
|
parseLanguageTracks(nativeEvent);
|
|
699
731
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
732
|
+
// Resolve the reported selection against the current controlled state so a
|
|
733
|
+
// transient native "nothing selected" doesn't get echoed back as a command
|
|
734
|
+
// and re-trigger native selection (infinite loop, see resolveReportedTrackId).
|
|
735
|
+
const nextAudioTrackId = resolveReportedTrackId(
|
|
736
|
+
audioTrackId,
|
|
737
|
+
this.state.audioTrackId,
|
|
738
|
+
audioTracks
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
const nextTextTrackId = resolveReportedTrackId(
|
|
742
|
+
textTrackId,
|
|
743
|
+
this.state.textTrackId,
|
|
744
|
+
textTracks
|
|
713
745
|
);
|
|
746
|
+
|
|
747
|
+
const notifyListener = () =>
|
|
748
|
+
this.props?.listener?.onTracksChanged?.({
|
|
749
|
+
textTracks,
|
|
750
|
+
audioTracks,
|
|
751
|
+
audioTrackId: nextAudioTrackId,
|
|
752
|
+
textTrackId: nextTextTrackId,
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// Only update state (and thus re-send the controlled props) on a real change.
|
|
756
|
+
// Re-rendering with identical prop values would otherwise keep the native
|
|
757
|
+
// event/prop echo alive.
|
|
758
|
+
const stateChanges: {
|
|
759
|
+
audioTrackId?: string | null;
|
|
760
|
+
textTrackId?: string | null;
|
|
761
|
+
} = {};
|
|
762
|
+
|
|
763
|
+
if (nextAudioTrackId !== this.state.audioTrackId) {
|
|
764
|
+
stateChanges.audioTrackId = nextAudioTrackId;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (nextTextTrackId !== this.state.textTrackId) {
|
|
768
|
+
stateChanges.textTrackId = nextTextTrackId;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (Object.keys(stateChanges).length > 0) {
|
|
772
|
+
this.setState(stateChanges as any, notifyListener);
|
|
773
|
+
} else {
|
|
774
|
+
notifyListener();
|
|
775
|
+
}
|
|
714
776
|
};
|
|
715
777
|
|
|
716
778
|
onVideoProgress = ({ nativeEvent }) => {
|