@applicaster/zapp-react-native-utils 16.0.0-rc.121 → 16.0.0-rc.122
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.
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Player } from "../player";
|
|
2
|
+
|
|
3
|
+
const entry = (id: string) => ({ id, title: id }) as unknown as ZappEntry;
|
|
4
|
+
|
|
5
|
+
const makePlayer = () => {
|
|
6
|
+
const player = new Player({ playerId: "test-player" });
|
|
7
|
+
player.entry = entry("first");
|
|
8
|
+
|
|
9
|
+
return player;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe("Player per-entry diagnostics", () => {
|
|
13
|
+
it("keeps the latest of onError and onVideoError, and clears it when a new entry loads", () => {
|
|
14
|
+
const player = makePlayer();
|
|
15
|
+
const second = { message: "second" };
|
|
16
|
+
|
|
17
|
+
player.onError({ message: "first" } as any);
|
|
18
|
+
player.onVideoError(second as any);
|
|
19
|
+
|
|
20
|
+
expect(player.lastError).toBe(second);
|
|
21
|
+
|
|
22
|
+
player.entry = entry("second");
|
|
23
|
+
|
|
24
|
+
expect(player.lastError).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("reads the next entry without creating the overlay observer", () => {
|
|
28
|
+
const player = makePlayer();
|
|
29
|
+
|
|
30
|
+
expect(player.getPlayNextEntry()).toBeNull();
|
|
31
|
+
|
|
32
|
+
jest
|
|
33
|
+
.spyOn(player.getOverlayObservable(), "getPlayNextEntry")
|
|
34
|
+
.mockReturnValue(entry("next"));
|
|
35
|
+
|
|
36
|
+
expect(player.getPlayNextEntry()).toEqual(entry("next"));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("keeps the content clock while a client-side ad plays", () => {
|
|
40
|
+
const player = makePlayer();
|
|
41
|
+
|
|
42
|
+
player.onVideoProgress({
|
|
43
|
+
currentTime: 300,
|
|
44
|
+
duration: 1800,
|
|
45
|
+
seekableDuration: 1800,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
player.onAdBegin({});
|
|
49
|
+
|
|
50
|
+
player.onVideoProgress({
|
|
51
|
+
currentTime: 5,
|
|
52
|
+
duration: 15,
|
|
53
|
+
seekableDuration: 15,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(player.getPosition()).toBe(5);
|
|
57
|
+
expect(player.getDuration()).toBe(15);
|
|
58
|
+
expect(player.getContentPosition()).toBe(300);
|
|
59
|
+
expect(player.getContentDuration()).toBe(1800);
|
|
60
|
+
expect(player.getSeekableDuration()).toBe(1800);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -18,6 +18,9 @@ const loadToolbox = () => {
|
|
|
18
18
|
const player = (overrides = {}) => ({
|
|
19
19
|
getSeekableDuration: () => 100,
|
|
20
20
|
getContentPosition: () => 40,
|
|
21
|
+
isLive: () => false,
|
|
22
|
+
getPlayNextEntry: () => null,
|
|
23
|
+
playNextHandler: jest.fn(),
|
|
21
24
|
seekTo: jest.fn(),
|
|
22
25
|
...overrides,
|
|
23
26
|
});
|
|
@@ -30,7 +33,7 @@ const registeredTools = async (active) => {
|
|
|
30
33
|
mockXray = { registerReactToolbox: mockRegister };
|
|
31
34
|
await loadToolbox().registerPlayerToolbox(manager(active));
|
|
32
35
|
|
|
33
|
-
const { tools } = mockRegister.mock.
|
|
36
|
+
const { tools } = mockRegister.mock.lastCall[0];
|
|
34
37
|
|
|
35
38
|
return Object.fromEntries(tools.map((tool) => [tool.name, tool.handler]));
|
|
36
39
|
};
|
|
@@ -87,6 +90,187 @@ describe("playerToolbox", () => {
|
|
|
87
90
|
);
|
|
88
91
|
});
|
|
89
92
|
|
|
93
|
+
describe("state", () => {
|
|
94
|
+
const playing = (overrides = {}, entry: any = { id: "e1", title: "T" }) =>
|
|
95
|
+
player({
|
|
96
|
+
playerId: "p1",
|
|
97
|
+
playerRole: "Primary",
|
|
98
|
+
getEntry: () => entry,
|
|
99
|
+
getPosition: () => 40,
|
|
100
|
+
getDuration: () => 100,
|
|
101
|
+
getContentDuration: () => 100,
|
|
102
|
+
isReadyToPlay: () => true,
|
|
103
|
+
isPlaying: () => true,
|
|
104
|
+
isBuffering: () => false,
|
|
105
|
+
isAd: () => false,
|
|
106
|
+
isLive: () => false,
|
|
107
|
+
getIsMuted: () => false,
|
|
108
|
+
...overrides,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const state = async (overrides = {}, entry?: any) =>
|
|
112
|
+
(await registeredTools(playing(overrides, entry))).state();
|
|
113
|
+
|
|
114
|
+
it("reports inactive when nothing plays", async () => {
|
|
115
|
+
expect((await registeredTools(null)).state()).toEqual({
|
|
116
|
+
active: false,
|
|
117
|
+
casting: false,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("keeps the existing fields and adds the content clock for a VOD", async () => {
|
|
122
|
+
expect(await state()).toEqual({
|
|
123
|
+
active: true,
|
|
124
|
+
playerId: "p1",
|
|
125
|
+
entry: { id: "e1", title: "T" },
|
|
126
|
+
position: 40,
|
|
127
|
+
duration: 100,
|
|
128
|
+
playing: true,
|
|
129
|
+
buffering: false,
|
|
130
|
+
readyToPlay: true,
|
|
131
|
+
ad: false,
|
|
132
|
+
live: false,
|
|
133
|
+
muted: false,
|
|
134
|
+
role: "Primary",
|
|
135
|
+
plugin: null,
|
|
136
|
+
engine: null,
|
|
137
|
+
casting: false,
|
|
138
|
+
seekableDuration: 100,
|
|
139
|
+
contentPosition: 40,
|
|
140
|
+
contentDuration: 100,
|
|
141
|
+
drm: null,
|
|
142
|
+
lastError: null,
|
|
143
|
+
next: null,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("reports the content's clock next to the ad's during a client-side ad", async () => {
|
|
148
|
+
const result = await state({
|
|
149
|
+
isAd: () => true,
|
|
150
|
+
getPosition: () => 5,
|
|
151
|
+
getDuration: () => 15,
|
|
152
|
+
getContentPosition: () => 300,
|
|
153
|
+
getContentDuration: () => 1800,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(result).toMatchObject({
|
|
157
|
+
ad: true,
|
|
158
|
+
position: 5,
|
|
159
|
+
duration: 15,
|
|
160
|
+
contentPosition: 300,
|
|
161
|
+
contentDuration: 1800,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("reports non-finite numbers as null", async () => {
|
|
166
|
+
expect(
|
|
167
|
+
await state({
|
|
168
|
+
getSeekableDuration: () => NaN,
|
|
169
|
+
getContentPosition: () => null,
|
|
170
|
+
getContentDuration: () => Infinity,
|
|
171
|
+
})
|
|
172
|
+
).toMatchObject({
|
|
173
|
+
seekableDuration: null,
|
|
174
|
+
contentPosition: null,
|
|
175
|
+
contentDuration: null,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("reports the player plugin and its web engine", async () => {
|
|
180
|
+
expect(
|
|
181
|
+
await state({
|
|
182
|
+
playerPluginId: "QuickBrickPlayerPlugin",
|
|
183
|
+
engine: "hls.js",
|
|
184
|
+
})
|
|
185
|
+
).toMatchObject({ plugin: "QuickBrickPlayerPlugin", engine: "hls.js" });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("lists the entry's DRM key systems, or null", async () => {
|
|
189
|
+
const drm = { widevine: { license_url: "w" }, playready: {} };
|
|
190
|
+
|
|
191
|
+
expect((await state({}, { id: "e", extensions: { drm } })).drm).toEqual([
|
|
192
|
+
"widevine",
|
|
193
|
+
"playready",
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
expect((await state({}, { id: "e", extensions: { drm: {} } })).drm).toBe(
|
|
197
|
+
null
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect((await state({}, { id: "e", extensions: {} })).drm).toBe(null);
|
|
201
|
+
expect((await state({}, null)).drm).toBe(null);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("reports no last error as null", async () => {
|
|
205
|
+
expect((await state({ lastError: null })).lastError).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("play next", () => {
|
|
210
|
+
const nextEntry = { id: "n1", title: "Next", extensions: {} };
|
|
211
|
+
|
|
212
|
+
const stateWith = async (overrides) =>
|
|
213
|
+
(
|
|
214
|
+
await registeredTools(
|
|
215
|
+
player({
|
|
216
|
+
getEntry: () => null,
|
|
217
|
+
getPosition: () => 0,
|
|
218
|
+
getDuration: () => 0,
|
|
219
|
+
getContentDuration: () => 0,
|
|
220
|
+
isReadyToPlay: () => true,
|
|
221
|
+
isPlaying: () => true,
|
|
222
|
+
isBuffering: () => false,
|
|
223
|
+
isAd: () => false,
|
|
224
|
+
getIsMuted: () => false,
|
|
225
|
+
...overrides,
|
|
226
|
+
})
|
|
227
|
+
)
|
|
228
|
+
).state();
|
|
229
|
+
|
|
230
|
+
it("reports the entry Play Next would play", async () => {
|
|
231
|
+
expect(
|
|
232
|
+
(await stateWith({ getPlayNextEntry: () => nextEntry })).next
|
|
233
|
+
).toEqual({ id: "n1", title: "Next" });
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("reports no next entry as null", async () => {
|
|
237
|
+
expect((await stateWith({})).next).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("plays the next entry through the player's Play Next action", async () => {
|
|
241
|
+
const active = player({ getPlayNextEntry: () => nextEntry });
|
|
242
|
+
const tools = await registeredTools(active);
|
|
243
|
+
|
|
244
|
+
expect(tools.next()).toEqual({ id: "n1", title: "Next" });
|
|
245
|
+
expect(active.playNextHandler).toHaveBeenCalledTimes(1);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("fails without a next entry", async () => {
|
|
249
|
+
const active = player();
|
|
250
|
+
const tools = await registeredTools(active);
|
|
251
|
+
|
|
252
|
+
expect(() => tools.next()).toThrow("No next item");
|
|
253
|
+
expect(active.playNextHandler).not.toHaveBeenCalled();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("fails when no Play Next action is registered", async () => {
|
|
257
|
+
const tools = await registeredTools(
|
|
258
|
+
player({
|
|
259
|
+
getPlayNextEntry: () => nextEntry,
|
|
260
|
+
playNextHandler: null,
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
expect(() => tools.next()).toThrow("No next item");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("fails without an active player", async () => {
|
|
268
|
+
const tools = await registeredTools(null);
|
|
269
|
+
|
|
270
|
+
expect(() => tools.next()).toThrow("No active player");
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
90
274
|
it("refuses to seek a stream that is not seekable", async () => {
|
|
91
275
|
const tools = await registeredTools(
|
|
92
276
|
player({ getSeekableDuration: () => 0 })
|
|
@@ -76,6 +76,12 @@ export class Player {
|
|
|
76
76
|
private overlaysObserver?: OverlaysObserver | null;
|
|
77
77
|
private entryObservable?: BehaviorSubject<ZappEntry> | null;
|
|
78
78
|
private contentSubject?: BehaviorSubject<PlayerContent> | null;
|
|
79
|
+
// Read by the X-Ray `player` toolbox: the last onError/onVideoError of the current entry,
|
|
80
|
+
// and the Play Next action of the PlayerContainer showing this player.
|
|
81
|
+
public lastError: unknown = null;
|
|
82
|
+
public playNextHandler: (() => void) | null = null;
|
|
83
|
+
// The web engine the default player picked (hls.js, dash.js, shaka, video.js); null elsewhere.
|
|
84
|
+
public engine: string | null = null;
|
|
79
85
|
|
|
80
86
|
protected listeners: {
|
|
81
87
|
[key: string]: QuickBrickPlayer.SharedPlayerCallBacks;
|
|
@@ -89,6 +95,7 @@ export class Player {
|
|
|
89
95
|
|
|
90
96
|
set entry(newEntry: ZappEntry | null) {
|
|
91
97
|
this._entry = newEntry;
|
|
98
|
+
this.lastError = null;
|
|
92
99
|
// The live entry is scoped to the entry it overlays - see
|
|
93
100
|
// `OverlaysObserver.clearLiveEntry`. No-op when there is no observer,
|
|
94
101
|
// which is every non-live player.
|
|
@@ -271,6 +278,10 @@ export class Player {
|
|
|
271
278
|
|
|
272
279
|
getEntry = (): ZappEntry | null => this.entry;
|
|
273
280
|
|
|
281
|
+
/** The entry Play Next would play now; null without one. Never creates the overlay observer. */
|
|
282
|
+
getPlayNextEntry = (): ZappEntry | null =>
|
|
283
|
+
this.overlaysObserver?.getPlayNextEntry() ?? null;
|
|
284
|
+
|
|
274
285
|
logState = (text, additionalParams = {}) => {
|
|
275
286
|
const isSeeking = this.playerState.seekPosition !== null;
|
|
276
287
|
|
|
@@ -492,6 +503,8 @@ export class Player {
|
|
|
492
503
|
};
|
|
493
504
|
|
|
494
505
|
onVideoError = (error: Error) => {
|
|
506
|
+
this.lastError = error;
|
|
507
|
+
|
|
495
508
|
this.invokeListenersUpdate({
|
|
496
509
|
callbackName: SharedPlayerCallBacksKeys.OnVideoError,
|
|
497
510
|
event: error,
|
|
@@ -499,6 +512,8 @@ export class Player {
|
|
|
499
512
|
};
|
|
500
513
|
|
|
501
514
|
onError = (error: Error) => {
|
|
515
|
+
this.lastError = error;
|
|
516
|
+
|
|
502
517
|
this.invokeListenersUpdate({
|
|
503
518
|
callbackName: SharedPlayerCallBacksKeys.OnError,
|
|
504
519
|
event: error,
|
|
@@ -14,6 +14,12 @@ const toNumber = (value: unknown) =>
|
|
|
14
14
|
|
|
15
15
|
const clamp = (value: number, max: number) => Math.min(Math.max(value, 0), max);
|
|
16
16
|
|
|
17
|
+
/** Key systems the entry declares (widevine, playready, fairplay...). */
|
|
18
|
+
const drmKeySystems = (drm: unknown) =>
|
|
19
|
+
drm && typeof drm === "object" && Object.keys(drm).length
|
|
20
|
+
? Object.keys(drm)
|
|
21
|
+
: null;
|
|
22
|
+
|
|
17
23
|
/** The range a seek can reach: the DVR window on live, the content on VOD. */
|
|
18
24
|
const requireSeekable = (player: {
|
|
19
25
|
getSeekableDuration: () => number | undefined;
|
|
@@ -38,14 +44,21 @@ const tools = (manager: PlayerManager): XrayToolSpec[] => {
|
|
|
38
44
|
{
|
|
39
45
|
name: "state",
|
|
40
46
|
description:
|
|
41
|
-
"The active player: entry, position and duration in seconds
|
|
42
|
-
"role (Primary, Cell, Chromecast) and casting
|
|
47
|
+
"The active player: entry, position and duration in seconds (the ad's while an ad plays), " +
|
|
48
|
+
"playing/buffering/ad/live/muted, role (Primary, Cell, Chromecast) and casting; in seconds, " +
|
|
49
|
+
"seekableDuration and contentPosition/contentDuration (the content's, even during ads); " +
|
|
50
|
+
"drm key systems, lastError {message} and next {id, title}, each null when absent; " +
|
|
51
|
+
"plugin (player plugin id) and engine (the default player's web engine, else null). " +
|
|
52
|
+
"active: false when nothing plays.",
|
|
43
53
|
handler: () => {
|
|
44
54
|
const player = manager.getActivePlayer();
|
|
45
55
|
|
|
46
56
|
if (!player) return { active: false, casting: manager.isCasting() };
|
|
47
57
|
|
|
48
58
|
const entry = player.getEntry();
|
|
59
|
+
const seekableDuration = toNumber(player.getSeekableDuration());
|
|
60
|
+
const next = player.getPlayNextEntry();
|
|
61
|
+
const error: any = player.lastError;
|
|
49
62
|
|
|
50
63
|
return {
|
|
51
64
|
active: true,
|
|
@@ -61,7 +74,18 @@ const tools = (manager: PlayerManager): XrayToolSpec[] => {
|
|
|
61
74
|
live: !!player.isLive(),
|
|
62
75
|
muted: !!player.getIsMuted(),
|
|
63
76
|
role: player.playerRole,
|
|
77
|
+
plugin: player.playerPluginId ?? null,
|
|
78
|
+
engine: player.engine ?? null,
|
|
64
79
|
casting: manager.isCasting(),
|
|
80
|
+
seekableDuration,
|
|
81
|
+
contentPosition: toNumber(player.getContentPosition()),
|
|
82
|
+
contentDuration: toNumber(player.getContentDuration()),
|
|
83
|
+
drm: drmKeySystems(entry?.extensions?.drm),
|
|
84
|
+
lastError:
|
|
85
|
+
error == null
|
|
86
|
+
? null
|
|
87
|
+
: { message: String(error?.message ?? error?.error ?? error) },
|
|
88
|
+
next: next ? { id: next.id, title: next.title } : null,
|
|
65
89
|
};
|
|
66
90
|
},
|
|
67
91
|
},
|
|
@@ -83,6 +107,21 @@ const tools = (manager: PlayerManager): XrayToolSpec[] => {
|
|
|
83
107
|
return "OK";
|
|
84
108
|
},
|
|
85
109
|
},
|
|
110
|
+
{
|
|
111
|
+
name: "next",
|
|
112
|
+
description:
|
|
113
|
+
"Play the next item, as the Play Next overlay does. Fails when there is none.",
|
|
114
|
+
handler: () => {
|
|
115
|
+
const player = requirePlayer();
|
|
116
|
+
const next = player.getPlayNextEntry();
|
|
117
|
+
|
|
118
|
+
if (!next || !player.playNextHandler) throw new Error("No next item");
|
|
119
|
+
|
|
120
|
+
player.playNextHandler();
|
|
121
|
+
|
|
122
|
+
return { id: next.id, title: next.title };
|
|
123
|
+
},
|
|
124
|
+
},
|
|
86
125
|
{
|
|
87
126
|
name: "seek",
|
|
88
127
|
description:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-utils",
|
|
3
|
-
"version": "16.0.0-rc.
|
|
3
|
+
"version": "16.0.0-rc.122",
|
|
4
4
|
"description": "Applicaster Zapp React Native utilities package",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@applicaster/applicaster-types": "16.0.0-rc.
|
|
30
|
+
"@applicaster/applicaster-types": "16.0.0-rc.122",
|
|
31
31
|
"buffer": "^5.2.1",
|
|
32
32
|
"camelize": "^1.0.0",
|
|
33
33
|
"dayjs": "^1.11.10",
|