@applicaster/zapp-react-native-ui-components 14.0.18 → 14.0.19-alpha.1129407856
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/Components/MasterCell/DefaultComponents/LiveImage/__tests__/prepareEntry.test.ts +352 -0
- package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +136 -0
- package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +34 -16
- package/Components/PlayerContainer/PlayerContainer.tsx +0 -9
- package/Components/VideoLive/LiveImageManager.ts +163 -55
- package/Components/VideoLive/PlayerLiveImageComponent.tsx +29 -32
- package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +2 -17
- package/package.json +5 -5
- package/Components/PlayerContainer/ErrorDisplay/ErrorDisplay.tsx +0 -57
- package/Components/PlayerContainer/ErrorDisplay/index.ts +0 -9
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { prepareEntry } from "../index";
|
|
2
|
+
|
|
3
|
+
const makeEntry = (overrides = {}): any => ({
|
|
4
|
+
content: { src: "https://example.com/video.mp4", type: "video/mp4" },
|
|
5
|
+
extensions: {},
|
|
6
|
+
...overrides,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe("prepareEntry", () => {
|
|
10
|
+
it("should return null when entry is null or undefined", () => {
|
|
11
|
+
expect(prepareEntry(null)).toBeNull();
|
|
12
|
+
expect(prepareEntry(undefined)).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("preview_playback_override", () => {
|
|
16
|
+
it("should deep merge override with entry, override taking precedence", () => {
|
|
17
|
+
const override = {
|
|
18
|
+
link: { href: "https://video-preload.com/id", rel: "self" },
|
|
19
|
+
extensions: {
|
|
20
|
+
brightcove: { video_id: 1234 },
|
|
21
|
+
preview: true,
|
|
22
|
+
free: true,
|
|
23
|
+
requires_authentication: false,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const entry = makeEntry({
|
|
28
|
+
extensions: {
|
|
29
|
+
preview_playback_override: override,
|
|
30
|
+
existing_field: "keep-me",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const result = prepareEntry(entry);
|
|
35
|
+
|
|
36
|
+
expect(result.extensions.brightcove.video_id).toBe(1234);
|
|
37
|
+
expect(result.extensions.preview).toBe(true);
|
|
38
|
+
expect(result.extensions.existing_field).toBe("keep-me");
|
|
39
|
+
|
|
40
|
+
expect(result.link).toEqual({
|
|
41
|
+
href: "https://video-preload.com/id",
|
|
42
|
+
rel: "self",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result.content).toEqual(entry.content);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should override entry fields when both have the same key", () => {
|
|
49
|
+
const override = {
|
|
50
|
+
extensions: {
|
|
51
|
+
brightcove: { video_id: 9999 },
|
|
52
|
+
},
|
|
53
|
+
content: { src: "https://override.com/video.mp4" },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const entry = makeEntry({
|
|
57
|
+
extensions: {
|
|
58
|
+
preview_playback_override: override,
|
|
59
|
+
brightcove: { video_id: 1111, account_id: "abc" },
|
|
60
|
+
},
|
|
61
|
+
content: { src: "https://original.com/video.mp4", type: "video/mp4" },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const result = prepareEntry(entry);
|
|
65
|
+
|
|
66
|
+
expect(result.extensions.brightcove.video_id).toBe(9999);
|
|
67
|
+
expect(result.extensions.brightcove.account_id).toBe("abc");
|
|
68
|
+
expect(result.content.src).toBe("https://override.com/video.mp4");
|
|
69
|
+
expect(result.content.type).toBe("video/mp4");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should take priority over other playback sources", () => {
|
|
73
|
+
const override = {
|
|
74
|
+
link: { href: "https://override.com" },
|
|
75
|
+
extensions: { brightcove: { video_id: 5555 } },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const entry = makeEntry({
|
|
79
|
+
extensions: {
|
|
80
|
+
preview_playback_override: override,
|
|
81
|
+
brightcove: { preview_playback: "other-id" },
|
|
82
|
+
preview_playback: "https://other.com/video.mp4",
|
|
83
|
+
},
|
|
84
|
+
content: {
|
|
85
|
+
src: "https://example.com/video.mp4",
|
|
86
|
+
teaser: "https://teaser.com/video.mp4",
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const result = prepareEntry(entry);
|
|
91
|
+
|
|
92
|
+
expect(result.link).toEqual({ href: "https://override.com" });
|
|
93
|
+
expect(result.extensions.brightcove.video_id).toBe(5555);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("brightcove preview_playback", () => {
|
|
98
|
+
it("should return entry with brightcove video_id replaced by preview_playback", () => {
|
|
99
|
+
const entry = makeEntry({
|
|
100
|
+
extensions: {
|
|
101
|
+
brightcove: {
|
|
102
|
+
video_id: "original-id",
|
|
103
|
+
preview_playback: "preview-id",
|
|
104
|
+
},
|
|
105
|
+
some_other: "value",
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const result = prepareEntry(entry);
|
|
110
|
+
|
|
111
|
+
expect(result).toEqual({
|
|
112
|
+
...entry,
|
|
113
|
+
extensions: {
|
|
114
|
+
...entry.extensions,
|
|
115
|
+
brightcove: {
|
|
116
|
+
video_id: "preview-id",
|
|
117
|
+
preview_playback: "preview-id",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should preserve other extensions", () => {
|
|
124
|
+
const entry = makeEntry({
|
|
125
|
+
extensions: {
|
|
126
|
+
brightcove: { preview_playback: "preview-id" },
|
|
127
|
+
custom_field: "keep-me",
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const result = prepareEntry(entry);
|
|
132
|
+
|
|
133
|
+
expect(result.extensions.custom_field).toBe("keep-me");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("extensions.preview_playback (non-brightcove)", () => {
|
|
138
|
+
it("should return entry with content.src set to preview_playback URL", () => {
|
|
139
|
+
const entry = makeEntry({
|
|
140
|
+
extensions: { preview_playback: "https://preview.com/video.m3u8" },
|
|
141
|
+
content: { src: "https://original.com/video.mp4" },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const result = prepareEntry(entry);
|
|
145
|
+
|
|
146
|
+
expect(result.content.src).toBe("https://preview.com/video.m3u8");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should preserve other content fields", () => {
|
|
150
|
+
const entry = makeEntry({
|
|
151
|
+
extensions: { preview_playback: "https://preview.com/video.m3u8" },
|
|
152
|
+
content: { src: "https://original.com/video.mp4", type: "video/mp4" },
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const result = prepareEntry(entry);
|
|
156
|
+
|
|
157
|
+
expect(result.content.type).toBe("video/mp4");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("content.teaser", () => {
|
|
162
|
+
it("should return entry with content.src set to teaser URL", () => {
|
|
163
|
+
const entry = makeEntry({
|
|
164
|
+
extensions: {},
|
|
165
|
+
content: {
|
|
166
|
+
src: "https://original.com/video.mp4",
|
|
167
|
+
teaser: "https://teaser.com/teaser.mp4",
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const result = prepareEntry(entry);
|
|
172
|
+
|
|
173
|
+
expect(result.content.src).toBe("https://teaser.com/teaser.mp4");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("brightcove video_id (without preview_playback)", () => {
|
|
178
|
+
it("should return the entry as-is when brightcove video_id exists", () => {
|
|
179
|
+
const entry = makeEntry({
|
|
180
|
+
extensions: { brightcove: { video_id: "some-id" } },
|
|
181
|
+
content: { src: "" },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const result = prepareEntry(entry);
|
|
185
|
+
|
|
186
|
+
expect(result).toBe(entry);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("content.src fallback", () => {
|
|
191
|
+
it("should return null when content.src is missing", () => {
|
|
192
|
+
const entry = makeEntry({
|
|
193
|
+
extensions: {},
|
|
194
|
+
content: {},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(prepareEntry(entry)).toBeNull();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should return null when content.src is empty", () => {
|
|
201
|
+
const entry = makeEntry({
|
|
202
|
+
extensions: {},
|
|
203
|
+
content: { src: "" },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(prepareEntry(entry)).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should return entry when content.type starts with video", () => {
|
|
210
|
+
const entry = makeEntry({
|
|
211
|
+
extensions: {},
|
|
212
|
+
content: { src: "https://example.com/stream", type: "video/mp4" },
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(prepareEntry(entry)).toBe(entry);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should return entry when src has a known video extension", () => {
|
|
219
|
+
const extensions = ["m3u8", "mp4", "m4v", "mkv", "mov", "mpd", "ogv"];
|
|
220
|
+
|
|
221
|
+
for (const ext of extensions) {
|
|
222
|
+
const entry = makeEntry({
|
|
223
|
+
extensions: {},
|
|
224
|
+
content: { src: `https://example.com/video.${ext}` },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(prepareEntry(entry)).toBe(entry);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should return entry for audio extensions", () => {
|
|
232
|
+
const extensions = ["mp3", "oga", "opus"];
|
|
233
|
+
|
|
234
|
+
for (const ext of extensions) {
|
|
235
|
+
const entry = makeEntry({
|
|
236
|
+
extensions: {},
|
|
237
|
+
content: { src: `https://example.com/audio.${ext}` },
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(prepareEntry(entry)).toBe(entry);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("should return null for non-video/non-audio URLs", () => {
|
|
245
|
+
const entry = makeEntry({
|
|
246
|
+
extensions: {},
|
|
247
|
+
content: { src: "https://example.com/page.html" },
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(prepareEntry(entry)).toBeNull();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("should return null for URLs without extensions", () => {
|
|
254
|
+
const entry = makeEntry({
|
|
255
|
+
extensions: {},
|
|
256
|
+
content: { src: "https://example.com/stream" },
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(prepareEntry(entry)).toBeNull();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("priority order", () => {
|
|
264
|
+
it("should prefer brightcove.preview_playback over extensions.preview_playback", () => {
|
|
265
|
+
const entry = makeEntry({
|
|
266
|
+
extensions: {
|
|
267
|
+
brightcove: { preview_playback: "bc-preview" },
|
|
268
|
+
preview_playback: "ext-preview",
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const result = prepareEntry(entry);
|
|
273
|
+
|
|
274
|
+
expect(result.extensions.brightcove.video_id).toBe("bc-preview");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("should prefer extensions.preview_playback over content.teaser", () => {
|
|
278
|
+
const entry = makeEntry({
|
|
279
|
+
extensions: { preview_playback: "https://preview.com/video.mp4" },
|
|
280
|
+
content: {
|
|
281
|
+
src: "https://original.com/video.mp4",
|
|
282
|
+
teaser: "https://teaser.com/video.mp4",
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const result = prepareEntry(entry);
|
|
287
|
+
|
|
288
|
+
expect(result.content.src).toBe("https://preview.com/video.mp4");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("should prefer preview_playback_override over all other sources", () => {
|
|
292
|
+
const override = {
|
|
293
|
+
extensions: { brightcove: { video_id: "override-id" } },
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const entry = makeEntry({
|
|
297
|
+
extensions: {
|
|
298
|
+
preview_playback_override: override,
|
|
299
|
+
brightcove: { preview_playback: "bc-preview", video_id: "original" },
|
|
300
|
+
preview_playback: "https://ext-preview.com/video.mp4",
|
|
301
|
+
},
|
|
302
|
+
content: {
|
|
303
|
+
src: "https://original.com/video.mp4",
|
|
304
|
+
teaser: "https://teaser.com/video.mp4",
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const result = prepareEntry(entry);
|
|
309
|
+
|
|
310
|
+
expect(result.extensions.brightcove.video_id).toBe("override-id");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("should prefer content.teaser over brightcove.video_id", () => {
|
|
314
|
+
const entry = makeEntry({
|
|
315
|
+
extensions: { brightcove: { video_id: "some-id" } },
|
|
316
|
+
content: {
|
|
317
|
+
src: "https://original.com/video.mp4",
|
|
318
|
+
teaser: "https://teaser.com/teaser.mp4",
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const result = prepareEntry(entry);
|
|
323
|
+
|
|
324
|
+
expect(result.content.src).toBe("https://teaser.com/teaser.mp4");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("should prefer content.teaser over content.src URL check", () => {
|
|
328
|
+
const entry = makeEntry({
|
|
329
|
+
extensions: {},
|
|
330
|
+
content: {
|
|
331
|
+
src: "https://original.com/video.mp4",
|
|
332
|
+
teaser: "https://teaser.com/teaser.m3u8",
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const result = prepareEntry(entry);
|
|
337
|
+
|
|
338
|
+
expect(result.content.src).toBe("https://teaser.com/teaser.m3u8");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("should prefer brightcove.video_id over content.src URL check", () => {
|
|
342
|
+
const entry = makeEntry({
|
|
343
|
+
extensions: { brightcove: { video_id: "bc-id" } },
|
|
344
|
+
content: { src: "https://example.com/page.html" },
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const result = prepareEntry(entry);
|
|
348
|
+
|
|
349
|
+
expect(result).toBe(entry);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { findPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
|
|
2
|
+
import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
|
|
3
|
+
import { loggerLiveImageManager } from "../../../VideoLive/loggerHelper";
|
|
4
|
+
|
|
5
|
+
const { log_debug, log_error } = loggerLiveImageManager;
|
|
6
|
+
|
|
7
|
+
export type PreloadHookConfig = ZappPreloadPlugins & {
|
|
8
|
+
configuration: any;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function runHookWithCallback(
|
|
12
|
+
invoke: (
|
|
13
|
+
payload: any,
|
|
14
|
+
callback: (result: hookCallbackArgs) => void,
|
|
15
|
+
abortCallback: () => void
|
|
16
|
+
) => void,
|
|
17
|
+
payload: any
|
|
18
|
+
): Promise<{ payload: any; aborted: boolean }> {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const abortCallback = () => {
|
|
21
|
+
resolve({ payload, aborted: true });
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
invoke(
|
|
26
|
+
payload,
|
|
27
|
+
({
|
|
28
|
+
success,
|
|
29
|
+
error,
|
|
30
|
+
payload: resultPayload,
|
|
31
|
+
abort,
|
|
32
|
+
}: hookCallbackArgs) => {
|
|
33
|
+
if (error) {
|
|
34
|
+
reject(error);
|
|
35
|
+
} else if (!success || abort) {
|
|
36
|
+
resolve({ payload: resultPayload ?? payload, aborted: true });
|
|
37
|
+
} else {
|
|
38
|
+
resolve({ payload: resultPayload ?? payload, aborted: false });
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
abortCallback
|
|
42
|
+
);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
log_error(`Error running preload hook: ${error.message}`, { error });
|
|
45
|
+
reject(error);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// TODO: Move this function to a more generic place,
|
|
51
|
+
// as it can be used by other components that want to run preload hooks
|
|
52
|
+
// and add test coverage for it
|
|
53
|
+
export async function executePreloadHooks({
|
|
54
|
+
preloadHooks,
|
|
55
|
+
entry,
|
|
56
|
+
}: {
|
|
57
|
+
preloadHooks: PreloadHookConfig[];
|
|
58
|
+
entry: ZappEntry;
|
|
59
|
+
}): Promise<ZappEntry | null> {
|
|
60
|
+
if (!preloadHooks?.length) {
|
|
61
|
+
return entry;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const sortedHooks = [...preloadHooks].sort(
|
|
65
|
+
(a, b) => (a.weight || 0) - (b.weight || 0)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const hookNames = sortedHooks.map((hook) => hook.identifier).join(", ");
|
|
69
|
+
|
|
70
|
+
log_debug(
|
|
71
|
+
`Preload hook sequence: ${hookNames}, entry: ${entry.id} - ${entry.title}`
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
let payload: any = entry;
|
|
75
|
+
|
|
76
|
+
let needsAbort = false;
|
|
77
|
+
const plugins = appStore.get("plugins");
|
|
78
|
+
|
|
79
|
+
for (const hookConfig of sortedHooks) {
|
|
80
|
+
const plugin = findPluginByIdentifier(hookConfig.identifier, plugins, true);
|
|
81
|
+
|
|
82
|
+
if (!plugin?.module) {
|
|
83
|
+
log_debug(
|
|
84
|
+
`Preload hook plugin not found: ${hookConfig.identifier}, skipping`
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const { module: hookModule } = plugin;
|
|
91
|
+
|
|
92
|
+
if (hookModule.skipHook?.(payload)) {
|
|
93
|
+
log_debug(`Skipping hook: ${hookConfig.identifier}`);
|
|
94
|
+
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (hookModule.runInBackground) {
|
|
99
|
+
log_debug(
|
|
100
|
+
`Running background hook: ${hookConfig.identifier}, entry: ${payload?.id} - ${payload?.title}`
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const result = await runHookWithCallback(
|
|
104
|
+
(item, callback, abortCallback) =>
|
|
105
|
+
hookModule
|
|
106
|
+
.runInBackground(item, callback, hookConfig, abortCallback)
|
|
107
|
+
?.catch?.((error: Error) => {
|
|
108
|
+
log_error(
|
|
109
|
+
`Error running background preload hook: ${hookConfig.identifier}, error: ${error.message}`,
|
|
110
|
+
{ error }
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
callback({ success: false, payload: item, error });
|
|
114
|
+
}),
|
|
115
|
+
payload
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
payload = result.payload;
|
|
119
|
+
needsAbort = result.aborted;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (needsAbort) {
|
|
123
|
+
log_debug(
|
|
124
|
+
`Preload hook requested abort: ${hookConfig.identifier}, stopping chain`
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (needsAbort) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return payload;
|
|
136
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import
|
|
2
|
+
import merge from "lodash/merge";
|
|
3
|
+
import { clone } from "@applicaster/zapp-react-native-utils/utils";
|
|
3
4
|
import { Platform, View, ViewStyle } from "react-native";
|
|
4
5
|
import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
|
|
5
6
|
import { isTV, isWeb } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
@@ -83,30 +84,22 @@ const removeAdsFromEntry = (entry) => {
|
|
|
83
84
|
return entry;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
const newEntry =
|
|
87
|
+
const newEntry = clone(entry);
|
|
87
88
|
|
|
88
89
|
delete newEntry.extensions.video_ads;
|
|
89
90
|
|
|
90
91
|
return newEntry;
|
|
91
92
|
};
|
|
92
93
|
|
|
93
|
-
const prepareEntry = (entry) => {
|
|
94
|
+
export const prepareEntry = (entry: ZappEntry) => {
|
|
94
95
|
if (!entry) {
|
|
95
96
|
return null;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
...entry,
|
|
101
|
-
content: { ...entry?.content, src: entry?.extensions?.preview_playback },
|
|
102
|
-
};
|
|
103
|
-
}
|
|
99
|
+
const previewPlaybackOverride = entry.extensions?.preview_playback_override;
|
|
104
100
|
|
|
105
|
-
if (
|
|
106
|
-
return {
|
|
107
|
-
...entry,
|
|
108
|
-
content: { ...entry?.content, src: entry?.content?.teaser },
|
|
109
|
-
};
|
|
101
|
+
if (previewPlaybackOverride) {
|
|
102
|
+
return merge({}, entry, previewPlaybackOverride);
|
|
110
103
|
}
|
|
111
104
|
|
|
112
105
|
const previewPlayback = entry.extensions?.brightcove?.preview_playback;
|
|
@@ -124,6 +117,20 @@ const prepareEntry = (entry) => {
|
|
|
124
117
|
};
|
|
125
118
|
}
|
|
126
119
|
|
|
120
|
+
if (entry.extensions?.preview_playback) {
|
|
121
|
+
return {
|
|
122
|
+
...entry,
|
|
123
|
+
content: { ...entry?.content, src: entry?.extensions?.preview_playback },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (entry.content?.teaser) {
|
|
128
|
+
return {
|
|
129
|
+
...entry,
|
|
130
|
+
content: { ...entry?.content, src: entry?.content?.teaser },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
127
134
|
if (entry.extensions?.brightcove?.video_id) {
|
|
128
135
|
return entry;
|
|
129
136
|
}
|
|
@@ -167,14 +174,24 @@ function getFocusedState(
|
|
|
167
174
|
const getPlayerConfig = (player_screen_id, actionIdentifier) => {
|
|
168
175
|
if (player_screen_id) {
|
|
169
176
|
const rivers = appStore.get("rivers");
|
|
177
|
+
const plugins = appStore.get("plugins");
|
|
170
178
|
const playerScreen = rivers?.[player_screen_id] ?? null;
|
|
171
179
|
|
|
180
|
+
const preloadHooks = playerScreen?.hooks?.preload_plugins?.map((hook) => {
|
|
181
|
+
const configuration =
|
|
182
|
+
plugins.find((plugin) => plugin.identifier === hook.identifier)
|
|
183
|
+
?.configuration || {};
|
|
184
|
+
|
|
185
|
+
return { ...hook, configuration, ...rivers[hook.screen_id] };
|
|
186
|
+
});
|
|
187
|
+
|
|
172
188
|
// TODO: Check is it a player later
|
|
173
189
|
|
|
174
190
|
// TODO: Add more dict if needed from the screen component, styles, data etc
|
|
175
191
|
return {
|
|
176
192
|
playerPluginId: playerScreen?.type ?? DEFAULT_PLAYER_IDENTIFIER,
|
|
177
193
|
screenConfig: playerScreen?.general,
|
|
194
|
+
preloadHooks,
|
|
178
195
|
};
|
|
179
196
|
}
|
|
180
197
|
|
|
@@ -229,7 +246,7 @@ const LiveImageComponent = (props: LiveImageProps) => {
|
|
|
229
246
|
|
|
230
247
|
const isCurrentlyFocused = useTrackCurrentAutoScrollingElement(componentId);
|
|
231
248
|
|
|
232
|
-
const { playerPluginId, screenConfig } =
|
|
249
|
+
const { playerPluginId, screenConfig, preloadHooks } =
|
|
233
250
|
getPlayerConfig(player_screen_id, actionIdentifier) ?? {};
|
|
234
251
|
|
|
235
252
|
const playableEntry = playerPluginId ? getPlayableEntry(item) : null;
|
|
@@ -239,8 +256,8 @@ const LiveImageComponent = (props: LiveImageProps) => {
|
|
|
239
256
|
|
|
240
257
|
const isShouldRender =
|
|
241
258
|
playerPlugin &&
|
|
242
|
-
getFocusedState(state, componentType, isCurrentlyFocused) &&
|
|
243
259
|
playableEntry &&
|
|
260
|
+
getFocusedState(state, componentType, isCurrentlyFocused) &&
|
|
244
261
|
cellUUID &&
|
|
245
262
|
isSupportedTVForLiveImage() &&
|
|
246
263
|
!isScreenReaderEnabled;
|
|
@@ -259,6 +276,7 @@ const LiveImageComponent = (props: LiveImageProps) => {
|
|
|
259
276
|
imageKey={imageKey}
|
|
260
277
|
item={playableEntry}
|
|
261
278
|
audioMutedByDefault={audio_muted_by_default}
|
|
279
|
+
preloadHooks={preloadHooks}
|
|
262
280
|
/>
|
|
263
281
|
</View>
|
|
264
282
|
) : null}
|
|
@@ -46,7 +46,6 @@ import {
|
|
|
46
46
|
PlayerContainerContextProvider,
|
|
47
47
|
} from "./PlayerContainerContext";
|
|
48
48
|
import { FocusableGroup } from "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup";
|
|
49
|
-
import { ErrorDisplay } from "./ErrorDisplay";
|
|
50
49
|
import { PlayerFocusableWrapperView } from "./WappersView/PlayerFocusableWrapperView";
|
|
51
50
|
import { FocusableGroupMainContainerId } from "./index";
|
|
52
51
|
import { isPlayable } from "@applicaster/zapp-react-native-utils/navigationUtils/itemTypeMatchers";
|
|
@@ -331,12 +330,6 @@ const PlayerContainerComponent = (props: Props) => {
|
|
|
331
330
|
playerContainerLogger.error(errorObj);
|
|
332
331
|
|
|
333
332
|
setState({ error: errorObj });
|
|
334
|
-
|
|
335
|
-
if (!isTvOS) {
|
|
336
|
-
setTimeout(() => {
|
|
337
|
-
close();
|
|
338
|
-
}, 800);
|
|
339
|
-
}
|
|
340
333
|
},
|
|
341
334
|
[close]
|
|
342
335
|
);
|
|
@@ -694,8 +687,6 @@ const PlayerContainerComponent = (props: Props) => {
|
|
|
694
687
|
</Player>
|
|
695
688
|
) : null}
|
|
696
689
|
</PlayerFocusableWrapperView>
|
|
697
|
-
|
|
698
|
-
{state.error ? <ErrorDisplay error={state.error} /> : null}
|
|
699
690
|
</View>
|
|
700
691
|
{/* Components container */}
|
|
701
692
|
{isInlineTV && context.showComponentsContainer ? (
|
|
@@ -4,9 +4,14 @@ import {
|
|
|
4
4
|
playerManager,
|
|
5
5
|
} from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
|
|
6
6
|
import { setUserCellPlayerMutedPreference } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/userCellPlayerMutedPreference";
|
|
7
|
+
import { playerFactory } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerFactory";
|
|
8
|
+
import { PlayerRole } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/conts";
|
|
7
9
|
import { loggerLiveImageManager } from "./loggerHelper";
|
|
8
10
|
import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
9
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
executePreloadHooks,
|
|
13
|
+
PreloadHookConfig,
|
|
14
|
+
} from "../MasterCell/DefaultComponents/LiveImage/executePreloadHooks";
|
|
10
15
|
|
|
11
16
|
const TIMEOUT_FOR_DELAY_CHECK_PLAYER_POSITION = 500; // ms
|
|
12
17
|
|
|
@@ -40,13 +45,20 @@ type Position = {
|
|
|
40
45
|
right: number;
|
|
41
46
|
};
|
|
42
47
|
|
|
48
|
+
type PlayerFactoryConfig = {
|
|
49
|
+
player: any; // React ref for native view
|
|
50
|
+
playerId: string;
|
|
51
|
+
muted: boolean;
|
|
52
|
+
playerPluginId: string;
|
|
53
|
+
screenConfig: Record<string, any>;
|
|
54
|
+
entry: ZappEntry; // original entry, used as fallback if no hooks
|
|
55
|
+
};
|
|
56
|
+
|
|
43
57
|
type LiveImageProps = {
|
|
44
|
-
player: Player;
|
|
45
58
|
playerId: string;
|
|
46
59
|
setMode?: (type: LiveImageType) => void;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// type: string;
|
|
60
|
+
preloadHooks?: PreloadHookConfig[];
|
|
61
|
+
factoryConfig: PlayerFactoryConfig;
|
|
50
62
|
};
|
|
51
63
|
|
|
52
64
|
// Disabled because we have only unmute button but no play/pause state anymore
|
|
@@ -85,7 +97,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
85
97
|
|
|
86
98
|
public register = (item: LiveImage): (() => void) => {
|
|
87
99
|
this.items.push(item);
|
|
88
|
-
log_debug(`register: live image ${
|
|
100
|
+
log_debug(`register: live image ${item.playerId}`);
|
|
89
101
|
|
|
90
102
|
// TV only Start playing video once registered
|
|
91
103
|
if (isTV()) {
|
|
@@ -96,15 +108,13 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
96
108
|
};
|
|
97
109
|
|
|
98
110
|
public unregister = (item: LiveImage) => {
|
|
99
|
-
log_debug(`unregister: live-image ${
|
|
111
|
+
log_debug(`unregister: live-image ${item.playerId}`);
|
|
100
112
|
|
|
101
113
|
if (this.currentlyPlaying === item) {
|
|
102
114
|
this.currentlyPlaying = null;
|
|
103
115
|
|
|
104
116
|
log_debug(
|
|
105
|
-
`unregister: currently playing live-image was destroyed, ${
|
|
106
|
-
item.player
|
|
107
|
-
)}`
|
|
117
|
+
`unregister: currently playing live-image was destroyed, ${item.playerId}`
|
|
108
118
|
);
|
|
109
119
|
|
|
110
120
|
// TODO: Maybe start another one
|
|
@@ -118,7 +128,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
118
128
|
item,
|
|
119
129
|
playerId: this.currentlyPlaying?.playerId,
|
|
120
130
|
primaryPlayerId: this.primaryPlayer?.playerId,
|
|
121
|
-
entry: item.getPlayer()
|
|
131
|
+
entry: item.getPlayer()?.getEntry(),
|
|
122
132
|
},
|
|
123
133
|
});
|
|
124
134
|
};
|
|
@@ -130,9 +140,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
130
140
|
|
|
131
141
|
public onViewportEnter = (item: LiveImage) => {
|
|
132
142
|
log_debug(
|
|
133
|
-
`onViewportEnter: live-image ${playerInfo(
|
|
134
|
-
item.player
|
|
135
|
-
)}, primary ${playerInfo(this.primaryPlayer)}`
|
|
143
|
+
`onViewportEnter: live-image ${item.playerId}, primary ${playerInfo(this.primaryPlayer)}`
|
|
136
144
|
);
|
|
137
145
|
|
|
138
146
|
if (!isTV()) {
|
|
@@ -154,9 +162,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
154
162
|
|
|
155
163
|
public onViewportLeave = (item: LiveImage) => {
|
|
156
164
|
log_debug(
|
|
157
|
-
`onViewportLeave: live-image playerId
|
|
158
|
-
item.player
|
|
159
|
-
)}, primary ${playerInfo(this.primaryPlayer)}`
|
|
165
|
+
`onViewportLeave: live-image ${item.playerId}, primary ${playerInfo(this.primaryPlayer)}`
|
|
160
166
|
);
|
|
161
167
|
|
|
162
168
|
this.pauseItem(item);
|
|
@@ -190,20 +196,27 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
190
196
|
this.items.find((i) => i.playerId === playerId) || null;
|
|
191
197
|
|
|
192
198
|
private pauseItem = (item: LiveImage) => {
|
|
193
|
-
log_debug(`pauseItem: live-image ${
|
|
199
|
+
log_debug(`pauseItem: live-image ${item.playerId}`);
|
|
200
|
+
|
|
201
|
+
if (!item.player) {
|
|
202
|
+
// Player not yet created (e.g. hooks still running) — just reset mode
|
|
203
|
+
item.setMode?.(LiveImageType.Image);
|
|
204
|
+
|
|
205
|
+
if (item === this.currentlyPlaying) {
|
|
206
|
+
this.currentlyPlaying = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
194
211
|
|
|
195
212
|
if (!item.player.playerState.isReadyToPlay) {
|
|
196
|
-
log_debug(
|
|
197
|
-
`playItem: live-image not ready, will start playback after loading, ${playerInfo(
|
|
198
|
-
item.player
|
|
199
|
-
)}`
|
|
200
|
-
);
|
|
213
|
+
log_debug(`pauseItem: live-image not ready, ${item.playerId}`);
|
|
201
214
|
} else {
|
|
202
|
-
item.player
|
|
215
|
+
item.player.pause();
|
|
203
216
|
}
|
|
204
217
|
|
|
205
218
|
// Fake close event, because we unmount native view
|
|
206
|
-
item.player
|
|
219
|
+
item.player.onPlayerClose();
|
|
207
220
|
item.setMode?.(LiveImageType.Image);
|
|
208
221
|
|
|
209
222
|
if (item === this.currentlyPlaying) {
|
|
@@ -213,9 +226,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
213
226
|
|
|
214
227
|
public playLiveImage = (item: LiveImage) => {
|
|
215
228
|
log_debug(
|
|
216
|
-
`playLiveImage: live-image ${playerInfo(
|
|
217
|
-
item.player
|
|
218
|
-
)}, primary ${playerInfo(this.primaryPlayer)}`
|
|
229
|
+
`playLiveImage: live-image ${item.playerId}, primary ${playerInfo(this.primaryPlayer)}`
|
|
219
230
|
);
|
|
220
231
|
|
|
221
232
|
if (this.primaryPlayer) {
|
|
@@ -223,7 +234,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
223
234
|
}
|
|
224
235
|
|
|
225
236
|
if (this.currentlyPlaying) {
|
|
226
|
-
if (this.currentlyPlaying
|
|
237
|
+
if (this.currentlyPlaying.playerId === item.playerId) {
|
|
227
238
|
return;
|
|
228
239
|
} else {
|
|
229
240
|
this.pauseItem(this.currentlyPlaying);
|
|
@@ -231,18 +242,42 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
231
242
|
}
|
|
232
243
|
|
|
233
244
|
this.currentlyPlaying = item;
|
|
234
|
-
item.setMode?.(LiveImageType.Video);
|
|
235
245
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
246
|
+
item
|
|
247
|
+
.prepareForPlayback()
|
|
248
|
+
.then((result) => {
|
|
249
|
+
if (!result) {
|
|
250
|
+
log_error(
|
|
251
|
+
`Failed to prepare live image ${item.playerId} for playback: prepareForPlayback returned false`
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
this.currentlyPlaying = null;
|
|
255
|
+
item.setMode?.(LiveImageType.Image);
|
|
256
|
+
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Guard: item might have been replaced while hooks were running
|
|
261
|
+
if (this.currentlyPlaying !== item) return;
|
|
262
|
+
|
|
263
|
+
item.setMode?.(LiveImageType.Video);
|
|
264
|
+
|
|
265
|
+
if (item.player?.playerState.isReadyToPlay) {
|
|
266
|
+
item.player.play();
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
.catch((error) => {
|
|
270
|
+
log_error(
|
|
271
|
+
`Failed to prepare live image ${item.playerId} for playback: ${error?.message}`
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
this.onLiveImageError(item, error);
|
|
275
|
+
});
|
|
239
276
|
};
|
|
240
277
|
|
|
241
278
|
public pauseLiveImage = (item: LiveImage) => {
|
|
242
279
|
log_debug(
|
|
243
|
-
`pauseLiveImage: live-image playerId
|
|
244
|
-
item.player
|
|
245
|
-
)}, primary ${playerInfo(this.primaryPlayer)}`
|
|
280
|
+
`pauseLiveImage: live-image ${item.playerId}, primary ${playerInfo(this.primaryPlayer)}`
|
|
246
281
|
);
|
|
247
282
|
|
|
248
283
|
this.pauseItem(item);
|
|
@@ -258,7 +293,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
258
293
|
|
|
259
294
|
setUserCellPlayerMutedPreference(true);
|
|
260
295
|
|
|
261
|
-
this.items.forEach((liveImage) => liveImage.player
|
|
296
|
+
this.items.forEach((liveImage) => liveImage.player?.mute());
|
|
262
297
|
};
|
|
263
298
|
|
|
264
299
|
public unmuteAll = () => {
|
|
@@ -266,16 +301,14 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
266
301
|
|
|
267
302
|
setUserCellPlayerMutedPreference(false);
|
|
268
303
|
|
|
269
|
-
this.items.forEach((liveImage) => liveImage.player
|
|
304
|
+
this.items.forEach((liveImage) => liveImage.player?.unmute());
|
|
270
305
|
};
|
|
271
306
|
|
|
272
307
|
public checkPlayerPosition = (item: LiveImage) => {
|
|
273
308
|
this.cancelCheckPlayerPositionTimeout();
|
|
274
309
|
|
|
275
310
|
log_debug(
|
|
276
|
-
`checkPlayerPosition: live-image playerId
|
|
277
|
-
item.player
|
|
278
|
-
)}, primary ${playerInfo(this.primaryPlayer)}`
|
|
311
|
+
`checkPlayerPosition: live-image ${item.playerId}, primary ${playerInfo(this.primaryPlayer)}`
|
|
279
312
|
);
|
|
280
313
|
|
|
281
314
|
const playerItem = this.findNextPlayableItem();
|
|
@@ -391,7 +424,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
391
424
|
item,
|
|
392
425
|
playerId: this.currentlyPlaying?.playerId,
|
|
393
426
|
primaryPlayerId: this.primaryPlayer?.playerId,
|
|
394
|
-
entry: item.getPlayer()
|
|
427
|
+
entry: item.getPlayer()?.getEntry(),
|
|
395
428
|
},
|
|
396
429
|
});
|
|
397
430
|
};
|
|
@@ -426,7 +459,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
426
459
|
item,
|
|
427
460
|
playerId: this.currentlyPlaying?.playerId,
|
|
428
461
|
primaryPlayerId: this.primaryPlayer?.playerId,
|
|
429
|
-
entry: item.getPlayer()
|
|
462
|
+
entry: item.getPlayer()?.getEntry(),
|
|
430
463
|
},
|
|
431
464
|
});
|
|
432
465
|
};
|
|
@@ -448,7 +481,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
448
481
|
this.currentlyPlaying = null;
|
|
449
482
|
|
|
450
483
|
log_debug(
|
|
451
|
-
`onLiveImageError:
|
|
484
|
+
`onLiveImageError: currentItem: ${currentItem.playerId} was removed`
|
|
452
485
|
);
|
|
453
486
|
|
|
454
487
|
// TODO: ...Maybe player some other item
|
|
@@ -460,8 +493,8 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
460
493
|
item,
|
|
461
494
|
error,
|
|
462
495
|
playerId: currentItem?.playerId,
|
|
463
|
-
primaryPlayerId:
|
|
464
|
-
entry: item.getPlayer()
|
|
496
|
+
primaryPlayerId: this.primaryPlayer?.playerId,
|
|
497
|
+
entry: item.getPlayer()?.getEntry(),
|
|
465
498
|
},
|
|
466
499
|
});
|
|
467
500
|
};
|
|
@@ -503,9 +536,8 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
503
536
|
LiveImageManager.instance;
|
|
504
537
|
|
|
505
538
|
export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
|
|
506
|
-
public player: Player;
|
|
507
|
-
public setMode
|
|
508
|
-
// Will be replaced with rects
|
|
539
|
+
public player: Player | null = null;
|
|
540
|
+
public setMode?: (type: LiveImageType) => void;
|
|
509
541
|
public isFullyVisible: boolean = false;
|
|
510
542
|
public position: Position = {
|
|
511
543
|
centerX: 0,
|
|
@@ -517,17 +549,93 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
|
|
|
517
549
|
};
|
|
518
550
|
|
|
519
551
|
readonly playerId: string;
|
|
520
|
-
|
|
552
|
+
public component: any = null;
|
|
553
|
+
|
|
554
|
+
private factoryConfig: PlayerFactoryConfig;
|
|
555
|
+
public preloadHooks?: PreloadHookConfig[];
|
|
556
|
+
public processedEntry: ZappEntry | null = null;
|
|
557
|
+
private _preparePromise: Promise<boolean> | null = null;
|
|
521
558
|
|
|
522
559
|
constructor(props: LiveImageProps) {
|
|
523
|
-
this.player = props.player;
|
|
524
560
|
this.setMode = props.setMode;
|
|
525
|
-
this.playerId =
|
|
526
|
-
this.
|
|
527
|
-
this.
|
|
561
|
+
this.playerId = props.playerId;
|
|
562
|
+
this.preloadHooks = props.preloadHooks;
|
|
563
|
+
this.factoryConfig = props.factoryConfig;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async prepareForPlayback(): Promise<boolean> {
|
|
567
|
+
// Already prepared — player exists
|
|
568
|
+
if (this.player) {
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Deduplicate: if preparation is already in flight, await the same promise
|
|
573
|
+
if (this._preparePromise) {
|
|
574
|
+
return this._preparePromise;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
this._preparePromise = (async (): Promise<boolean> => {
|
|
578
|
+
// 1. Run hooks if configured
|
|
579
|
+
let entry = this.factoryConfig.entry;
|
|
580
|
+
|
|
581
|
+
if (this.preloadHooks?.length) {
|
|
582
|
+
const result = await executePreloadHooks({
|
|
583
|
+
preloadHooks: this.preloadHooks,
|
|
584
|
+
entry,
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
if (result) {
|
|
588
|
+
this.processedEntry = result;
|
|
589
|
+
entry = result;
|
|
590
|
+
} else {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// 2. Create the player with the correct entry
|
|
596
|
+
const factoryItem = playerFactory({
|
|
597
|
+
player: this.factoryConfig.player,
|
|
598
|
+
playerId: this.factoryConfig.playerId,
|
|
599
|
+
autoplay: false,
|
|
600
|
+
entry,
|
|
601
|
+
muted: this.factoryConfig.muted,
|
|
602
|
+
playerPluginId: this.factoryConfig.playerPluginId,
|
|
603
|
+
screenConfig: this.factoryConfig.screenConfig,
|
|
604
|
+
playerRole: PlayerRole.Cell,
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
if (!factoryItem) {
|
|
608
|
+
throw new Error("Player factory returned null");
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
this.player = factoryItem.controller;
|
|
612
|
+
this.component = factoryItem.Component;
|
|
613
|
+
|
|
614
|
+
// 3. Register callbacks — player now exists
|
|
615
|
+
this.player.addListener({ id: "live-image", listener: this });
|
|
616
|
+
|
|
617
|
+
return true;
|
|
618
|
+
})()
|
|
619
|
+
.then((result) => {
|
|
620
|
+
this._preparePromise = null;
|
|
621
|
+
|
|
622
|
+
return result;
|
|
623
|
+
})
|
|
624
|
+
.catch((error) => {
|
|
625
|
+
this._preparePromise = null;
|
|
626
|
+
|
|
627
|
+
log_error(
|
|
628
|
+
`prepareForPlayback: live-image ${this.playerId}, error preparing for playback: ${error?.message}`,
|
|
629
|
+
{ error }
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
throw error;
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
return this._preparePromise;
|
|
528
636
|
}
|
|
529
637
|
|
|
530
|
-
public getPlayer = (): Player => {
|
|
638
|
+
public getPlayer = (): Player | null => {
|
|
531
639
|
return this.player;
|
|
532
640
|
};
|
|
533
641
|
|
|
@@ -7,8 +7,6 @@ import { AppState, AppStateStatus, View } from "react-native";
|
|
|
7
7
|
import { TrackedView } from "../TrackedView";
|
|
8
8
|
import { useDimensions } from "@applicaster/zapp-react-native-utils/reactHooks/layout";
|
|
9
9
|
|
|
10
|
-
import { playerFactory } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerFactory";
|
|
11
|
-
|
|
12
10
|
import {
|
|
13
11
|
isApplePlatform,
|
|
14
12
|
isTV,
|
|
@@ -22,15 +20,15 @@ import { AnimatedInOut } from "@applicaster/zapp-react-native-ui-components/Comp
|
|
|
22
20
|
import { overlayFadeIn } from "./animationUtils";
|
|
23
21
|
import { loggerLiveImageComponent } from "./loggerHelper";
|
|
24
22
|
import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
|
|
25
|
-
import { PlayerRole } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/conts";
|
|
26
23
|
import { getAutoplaySettings } from "./utils";
|
|
27
24
|
import { isString } from "@applicaster/zapp-react-native-utils/stringUtils";
|
|
28
25
|
import { BufferAnimation } from "../PlayerContainer/BufferAnimation";
|
|
26
|
+
import { PreloadHookConfig } from "../MasterCell/DefaultComponents/LiveImage/executePreloadHooks";
|
|
29
27
|
|
|
30
28
|
const { log_error, log_debug } = loggerLiveImageComponent;
|
|
31
29
|
|
|
32
|
-
const isMeasurement = (
|
|
33
|
-
isString(
|
|
30
|
+
const isMeasurement = (itemId: string | number | null): boolean =>
|
|
31
|
+
isString(itemId) && itemId.startsWith("pre-measurement-");
|
|
34
32
|
|
|
35
33
|
// Pixels by which the view can slightly extend outside the viewport and still be considered fully visible.
|
|
36
34
|
const CLIP_THRESHOLD = 10;
|
|
@@ -44,6 +42,7 @@ type Props = {
|
|
|
44
42
|
screenConfig: Record<string, any>;
|
|
45
43
|
audioMutedByDefault?: boolean;
|
|
46
44
|
uri: string; // cover image url
|
|
45
|
+
preloadHooks?: PreloadHookConfig[];
|
|
47
46
|
};
|
|
48
47
|
|
|
49
48
|
const PlayerLiveImageComponent = (props: Props) => {
|
|
@@ -116,43 +115,35 @@ const PlayerLiveImageComponent = (props: Props) => {
|
|
|
116
115
|
}
|
|
117
116
|
|
|
118
117
|
entryToViewIdMapping.current[nativeTag] = { itemId: item.id };
|
|
119
|
-
}, [
|
|
118
|
+
}, [item.id, mode]);
|
|
120
119
|
|
|
121
|
-
const { screenConfig, playerPluginId } = props;
|
|
120
|
+
const { screenConfig, playerPluginId, preloadHooks } = props;
|
|
122
121
|
|
|
123
122
|
const liveImageItem: LiveImage = React.useMemo(() => {
|
|
124
|
-
|
|
125
|
-
player: ref,
|
|
123
|
+
return new LiveImage({
|
|
126
124
|
playerId,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
playerPluginId: playerPluginId,
|
|
131
|
-
screenConfig: screenConfig,
|
|
132
|
-
playerRole: PlayerRole.Cell,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
if (playerFactoryItem) {
|
|
136
|
-
return new LiveImage({
|
|
125
|
+
preloadHooks,
|
|
126
|
+
factoryConfig: {
|
|
127
|
+
player: ref,
|
|
137
128
|
playerId,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
}, [playerId,
|
|
129
|
+
muted,
|
|
130
|
+
playerPluginId: playerPluginId,
|
|
131
|
+
screenConfig: screenConfig,
|
|
132
|
+
entry: item,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}, [playerId, preloadHooks, muted, playerPluginId, screenConfig, item]);
|
|
145
136
|
|
|
146
137
|
React.useEffect(() => {
|
|
147
138
|
liveImageItem.setMode = setModeDebounced;
|
|
148
139
|
}, [setModeDebounced, liveImageItem]);
|
|
149
140
|
|
|
141
|
+
// todo: no need for it to be react, can be moved into `player.addListener`
|
|
150
142
|
const { start, end } = React.useMemo(
|
|
151
143
|
() => getAutoplaySettings(item),
|
|
152
144
|
[item.id]
|
|
153
145
|
);
|
|
154
146
|
|
|
155
|
-
const controller = liveImageItem.getPlayer();
|
|
156
147
|
const player = usePlayer(playerId);
|
|
157
148
|
|
|
158
149
|
const _assignRoot = (component) => {
|
|
@@ -169,7 +160,7 @@ const PlayerLiveImageComponent = (props: Props) => {
|
|
|
169
160
|
|
|
170
161
|
React.useEffect(() => {
|
|
171
162
|
// FIXME - find a more elegant way to disable live-image on cell for measurement
|
|
172
|
-
if (isMeasurement(item)) {
|
|
163
|
+
if (isMeasurement(item.id)) {
|
|
173
164
|
return;
|
|
174
165
|
}
|
|
175
166
|
|
|
@@ -253,7 +244,13 @@ const PlayerLiveImageComponent = (props: Props) => {
|
|
|
253
244
|
}, [item.id]);
|
|
254
245
|
|
|
255
246
|
React.useEffect(() => {
|
|
256
|
-
if (isMeasurement(item) || !playerManager) {
|
|
247
|
+
if (isMeasurement(item.id) || !playerManager) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const controller = liveImageItem.getPlayer();
|
|
252
|
+
|
|
253
|
+
if (!controller) {
|
|
257
254
|
return;
|
|
258
255
|
}
|
|
259
256
|
|
|
@@ -273,7 +270,7 @@ const PlayerLiveImageComponent = (props: Props) => {
|
|
|
273
270
|
playerManager.unregisterPlayer(playerId);
|
|
274
271
|
};
|
|
275
272
|
}
|
|
276
|
-
}, [liveImageItem, playerId,
|
|
273
|
+
}, [liveImageItem, playerId, mode, item.id]);
|
|
277
274
|
|
|
278
275
|
const onPositionUpdated = React.useCallback(
|
|
279
276
|
(data) => {
|
|
@@ -338,11 +335,11 @@ const PlayerLiveImageComponent = (props: Props) => {
|
|
|
338
335
|
<Player
|
|
339
336
|
autoplay={false}
|
|
340
337
|
ref={_assignRoot}
|
|
341
|
-
entry={item} // Must be passed first in list
|
|
338
|
+
entry={liveImageItem.processedEntry || item} // Must be passed first in list
|
|
342
339
|
style={videoStyles}
|
|
343
340
|
playerId={playerId}
|
|
344
341
|
muted={muted}
|
|
345
|
-
listener={
|
|
342
|
+
listener={liveImageItem.getPlayer()?.getListener()}
|
|
346
343
|
resizeMode={"cover"}
|
|
347
344
|
{...platformSpecificProps}
|
|
348
345
|
/>
|
|
@@ -120,29 +120,14 @@ describe("PlayerLiveImageComponent", () => {
|
|
|
120
120
|
// TODO: implement this test
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
-
it("should register the player
|
|
123
|
+
it("should not register the player at mount (lazy creation)", () => {
|
|
124
124
|
render(
|
|
125
125
|
<Wrapper>
|
|
126
126
|
<PlayerLiveImage {...defaultProps} />
|
|
127
127
|
</Wrapper>
|
|
128
128
|
);
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
defaultProps.playerId
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
expect(isPlayerRegistered).toBe(true);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("should unregister the player for normal item", () => {
|
|
138
|
-
const component = render(
|
|
139
|
-
<Wrapper>
|
|
140
|
-
<PlayerLiveImage {...defaultProps} />
|
|
141
|
-
</Wrapper>
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
component.unmount();
|
|
145
|
-
|
|
130
|
+
// Player is not registered until playback starts (lazy creation)
|
|
146
131
|
const isPlayerRegistered = playerManager.isPlayerRegistered(
|
|
147
132
|
defaultProps.playerId
|
|
148
133
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-ui-components",
|
|
3
|
-
"version": "14.0.
|
|
3
|
+
"version": "14.0.19-alpha.1129407856",
|
|
4
4
|
"description": "Applicaster Zapp React Native ui components for the Quick Brick App",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
},
|
|
29
29
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@applicaster/applicaster-types": "14.0.
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "14.0.
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "14.0.
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "14.0.
|
|
31
|
+
"@applicaster/applicaster-types": "14.0.19-alpha.1129407856",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "14.0.19-alpha.1129407856",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "14.0.19-alpha.1129407856",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "14.0.19-alpha.1129407856",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"url": "^0.11.0",
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import * as R from "ramda";
|
|
3
|
-
import { Text, TextStyle, View, ViewStyle } from "react-native";
|
|
4
|
-
|
|
5
|
-
import { getLocalizations } from "@applicaster/zapp-react-native-utils/localizationUtils";
|
|
6
|
-
import { getAppStylesColor } from "@applicaster/zapp-react-native-utils/stylesUtils";
|
|
7
|
-
import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
|
|
8
|
-
import { styleKeys } from "@applicaster/zapp-react-native-utils/styleKeysUtils";
|
|
9
|
-
|
|
10
|
-
type Props = {
|
|
11
|
-
styles: {};
|
|
12
|
-
error: {};
|
|
13
|
-
remoteConfigurations: { localizations: {} };
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const defaultAppStyles = {
|
|
17
|
-
loading_error_label: {
|
|
18
|
-
color: "#aaa",
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const textStyles = (appStyles = defaultAppStyles): TextStyle => ({
|
|
23
|
-
color: getAppStylesColor("loading_error_label", appStyles),
|
|
24
|
-
fontSize: 36,
|
|
25
|
-
textAlign: "center",
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const errorStyles = ({ backgroundColor }): ViewStyle => ({
|
|
29
|
-
flex: 1,
|
|
30
|
-
width: "100%",
|
|
31
|
-
height: "100%",
|
|
32
|
-
justifyContent: "center",
|
|
33
|
-
alignItems: "center",
|
|
34
|
-
position: "absolute",
|
|
35
|
-
zIndex: 100,
|
|
36
|
-
backgroundColor,
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
export function ErrorDisplayComponent({
|
|
40
|
-
styles,
|
|
41
|
-
remoteConfigurations: { localizations },
|
|
42
|
-
}: Props) {
|
|
43
|
-
const theme = useTheme();
|
|
44
|
-
const backgroundColor = theme?.app_background_color;
|
|
45
|
-
|
|
46
|
-
const { stream_error_message = "Cannot play stream" } = getLocalizations({
|
|
47
|
-
localizations,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const appStyles = R.prop(styleKeys.style_namespace, styles);
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<View style={errorStyles({ backgroundColor })}>
|
|
54
|
-
<Text style={textStyles(appStyles)}>{stream_error_message}</Text>
|
|
55
|
-
</View>
|
|
56
|
-
);
|
|
57
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import * as R from "ramda";
|
|
2
|
-
|
|
3
|
-
import { connectToStore } from "@applicaster/zapp-react-native-redux/utils/connectToStore";
|
|
4
|
-
|
|
5
|
-
import { ErrorDisplayComponent } from "./ErrorDisplay";
|
|
6
|
-
|
|
7
|
-
export const ErrorDisplay = R.compose(
|
|
8
|
-
connectToStore(R.pick(["remoteConfigurations"]))
|
|
9
|
-
)(ErrorDisplayComponent);
|