@applicaster/zapp-react-native-ui-components 14.0.20-rc.1 → 14.0.20
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/VideoLive/LiveImageManager.ts +199 -54
- package/Components/VideoLive/PlayerLiveImageComponent.tsx +31 -33
- package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +2 -17
- package/package.json +5 -5
|
@@ -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}
|
|
@@ -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,21 @@ 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
|
-
|
|
60
|
+
preloadHooks?: PreloadHookConfig[];
|
|
61
|
+
factoryConfig: PlayerFactoryConfig;
|
|
62
|
+
tag: string;
|
|
50
63
|
};
|
|
51
64
|
|
|
52
65
|
// Disabled because we have only unmute button but no play/pause state anymore
|
|
@@ -85,7 +98,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
85
98
|
|
|
86
99
|
public register = (item: LiveImage): (() => void) => {
|
|
87
100
|
this.items.push(item);
|
|
88
|
-
log_debug(`register: live image ${
|
|
101
|
+
log_debug(`register: live image ${item.playerId} - ${item.tag}`);
|
|
89
102
|
|
|
90
103
|
// TV only Start playing video once registered
|
|
91
104
|
if (isTV()) {
|
|
@@ -96,15 +109,13 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
96
109
|
};
|
|
97
110
|
|
|
98
111
|
public unregister = (item: LiveImage) => {
|
|
99
|
-
log_debug(`unregister: live-image ${
|
|
112
|
+
log_debug(`unregister: live-image ${item.playerId} - ${item.tag}`);
|
|
100
113
|
|
|
101
114
|
if (this.currentlyPlaying === item) {
|
|
102
115
|
this.currentlyPlaying = null;
|
|
103
116
|
|
|
104
117
|
log_debug(
|
|
105
|
-
`unregister: currently playing live-image was destroyed, ${
|
|
106
|
-
item.player
|
|
107
|
-
)}`
|
|
118
|
+
`unregister: currently playing live-image was destroyed, ${item.playerId} - ${item.tag}`
|
|
108
119
|
);
|
|
109
120
|
|
|
110
121
|
// TODO: Maybe start another one
|
|
@@ -118,7 +129,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
118
129
|
item,
|
|
119
130
|
playerId: this.currentlyPlaying?.playerId,
|
|
120
131
|
primaryPlayerId: this.primaryPlayer?.playerId,
|
|
121
|
-
entry: item.getPlayer()
|
|
132
|
+
entry: item.getPlayer()?.getEntry(),
|
|
122
133
|
},
|
|
123
134
|
});
|
|
124
135
|
};
|
|
@@ -130,9 +141,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
130
141
|
|
|
131
142
|
public onViewportEnter = (item: LiveImage) => {
|
|
132
143
|
log_debug(
|
|
133
|
-
`onViewportEnter: live-image ${playerInfo(
|
|
134
|
-
item.player
|
|
135
|
-
)}, primary ${playerInfo(this.primaryPlayer)}`
|
|
144
|
+
`onViewportEnter: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)} - position:${item.positionToString()}`
|
|
136
145
|
);
|
|
137
146
|
|
|
138
147
|
if (!isTV()) {
|
|
@@ -154,9 +163,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
154
163
|
|
|
155
164
|
public onViewportLeave = (item: LiveImage) => {
|
|
156
165
|
log_debug(
|
|
157
|
-
`onViewportLeave: live-image playerId
|
|
158
|
-
item.player
|
|
159
|
-
)}, primary ${playerInfo(this.primaryPlayer)}`
|
|
166
|
+
`onViewportLeave: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)} - position:${item.positionToString()}`
|
|
160
167
|
);
|
|
161
168
|
|
|
162
169
|
this.pauseItem(item);
|
|
@@ -190,20 +197,29 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
190
197
|
this.items.find((i) => i.playerId === playerId) || null;
|
|
191
198
|
|
|
192
199
|
private pauseItem = (item: LiveImage) => {
|
|
193
|
-
log_debug(`pauseItem: live-image ${
|
|
200
|
+
log_debug(`pauseItem: live-image ${item.playerId} - ${item.tag}`);
|
|
201
|
+
|
|
202
|
+
if (!item.player) {
|
|
203
|
+
// Player not yet created (e.g. hooks still running) — just reset mode
|
|
204
|
+
item.setMode?.(LiveImageType.Image);
|
|
205
|
+
|
|
206
|
+
if (item === this.currentlyPlaying) {
|
|
207
|
+
this.currentlyPlaying = null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
194
212
|
|
|
195
213
|
if (!item.player.playerState.isReadyToPlay) {
|
|
196
214
|
log_debug(
|
|
197
|
-
`
|
|
198
|
-
item.player
|
|
199
|
-
)}`
|
|
215
|
+
`pauseItem: live-image not ready, ${item.playerId} - ${item.tag}`
|
|
200
216
|
);
|
|
201
217
|
} else {
|
|
202
|
-
item.player
|
|
218
|
+
item.player.pause();
|
|
203
219
|
}
|
|
204
220
|
|
|
205
221
|
// Fake close event, because we unmount native view
|
|
206
|
-
item.player
|
|
222
|
+
item.player.onPlayerClose();
|
|
207
223
|
item.setMode?.(LiveImageType.Image);
|
|
208
224
|
|
|
209
225
|
if (item === this.currentlyPlaying) {
|
|
@@ -213,9 +229,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
213
229
|
|
|
214
230
|
public playLiveImage = (item: LiveImage) => {
|
|
215
231
|
log_debug(
|
|
216
|
-
`playLiveImage: live-image ${playerInfo(
|
|
217
|
-
item.player
|
|
218
|
-
)}, primary ${playerInfo(this.primaryPlayer)}`
|
|
232
|
+
`playLiveImage: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)}`
|
|
219
233
|
);
|
|
220
234
|
|
|
221
235
|
if (this.primaryPlayer) {
|
|
@@ -223,7 +237,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
223
237
|
}
|
|
224
238
|
|
|
225
239
|
if (this.currentlyPlaying) {
|
|
226
|
-
if (this.currentlyPlaying
|
|
240
|
+
if (this.currentlyPlaying.playerId === item.playerId) {
|
|
227
241
|
return;
|
|
228
242
|
} else {
|
|
229
243
|
this.pauseItem(this.currentlyPlaying);
|
|
@@ -231,18 +245,42 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
231
245
|
}
|
|
232
246
|
|
|
233
247
|
this.currentlyPlaying = item;
|
|
234
|
-
item.setMode?.(LiveImageType.Video);
|
|
235
248
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
249
|
+
item
|
|
250
|
+
.prepareForPlayback()
|
|
251
|
+
.then((result) => {
|
|
252
|
+
if (!result) {
|
|
253
|
+
log_error(
|
|
254
|
+
`Failed to prepare live image ${item.playerId} - ${item.tag} for playback: prepareForPlayback returned false`
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
this.currentlyPlaying = null;
|
|
258
|
+
item.setMode?.(LiveImageType.Image);
|
|
259
|
+
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Guard: item might have been replaced while hooks were running
|
|
264
|
+
if (this.currentlyPlaying !== item) return;
|
|
265
|
+
|
|
266
|
+
item.setMode?.(LiveImageType.Video);
|
|
267
|
+
|
|
268
|
+
if (item.player?.playerState.isReadyToPlay) {
|
|
269
|
+
item.player.play();
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
.catch((error) => {
|
|
273
|
+
log_error(
|
|
274
|
+
`Failed to prepare live image ${item.playerId} - ${item.tag} for playback: ${error?.message}`
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
this.onLiveImageError(item, error);
|
|
278
|
+
});
|
|
239
279
|
};
|
|
240
280
|
|
|
241
281
|
public pauseLiveImage = (item: LiveImage) => {
|
|
242
282
|
log_debug(
|
|
243
|
-
`pauseLiveImage: live-image playerId
|
|
244
|
-
item.player
|
|
245
|
-
)}, primary ${playerInfo(this.primaryPlayer)}`
|
|
283
|
+
`pauseLiveImage: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)}`
|
|
246
284
|
);
|
|
247
285
|
|
|
248
286
|
this.pauseItem(item);
|
|
@@ -258,7 +296,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
258
296
|
|
|
259
297
|
setUserCellPlayerMutedPreference(true);
|
|
260
298
|
|
|
261
|
-
this.items.forEach((liveImage) => liveImage.player
|
|
299
|
+
this.items.forEach((liveImage) => liveImage.player?.mute());
|
|
262
300
|
};
|
|
263
301
|
|
|
264
302
|
public unmuteAll = () => {
|
|
@@ -266,16 +304,36 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
266
304
|
|
|
267
305
|
setUserCellPlayerMutedPreference(false);
|
|
268
306
|
|
|
269
|
-
this.items.forEach((liveImage) => liveImage.player
|
|
307
|
+
this.items.forEach((liveImage) => liveImage.player?.unmute());
|
|
270
308
|
};
|
|
271
309
|
|
|
272
|
-
public
|
|
310
|
+
public onViewPositionChanged = (item: LiveImage) => {
|
|
311
|
+
log_debug(
|
|
312
|
+
`onViewPositionChanged: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)} - position:${item.positionToString()}`
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
if (!isTV()) {
|
|
316
|
+
// mobile only
|
|
317
|
+
// we have to delay running checkPlayerPosition, because sometimes on fast scrolling we get wrong order onEnter, then onLeave.
|
|
318
|
+
// which could cause select wrong item to play
|
|
319
|
+
|
|
320
|
+
this.cancelCheckPlayerPositionTimeout();
|
|
321
|
+
|
|
322
|
+
this.checkPlayerPositionTimeout = setTimeout(() => {
|
|
323
|
+
this.cancelCheckPlayerPositionTimeout();
|
|
324
|
+
|
|
325
|
+
this.checkPlayerPosition(item);
|
|
326
|
+
}, TIMEOUT_FOR_DELAY_CHECK_PLAYER_POSITION);
|
|
327
|
+
} else {
|
|
328
|
+
this.checkPlayerPosition(item);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
private checkPlayerPosition = (item: LiveImage) => {
|
|
273
333
|
this.cancelCheckPlayerPositionTimeout();
|
|
274
334
|
|
|
275
335
|
log_debug(
|
|
276
|
-
`checkPlayerPosition: live-image playerId
|
|
277
|
-
item.player
|
|
278
|
-
)}, primary ${playerInfo(this.primaryPlayer)}`
|
|
336
|
+
`checkPlayerPosition: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)}`
|
|
279
337
|
);
|
|
280
338
|
|
|
281
339
|
const playerItem = this.findNextPlayableItem();
|
|
@@ -391,7 +449,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
391
449
|
item,
|
|
392
450
|
playerId: this.currentlyPlaying?.playerId,
|
|
393
451
|
primaryPlayerId: this.primaryPlayer?.playerId,
|
|
394
|
-
entry: item.getPlayer()
|
|
452
|
+
entry: item.getPlayer()?.getEntry(),
|
|
395
453
|
},
|
|
396
454
|
});
|
|
397
455
|
};
|
|
@@ -426,7 +484,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
426
484
|
item,
|
|
427
485
|
playerId: this.currentlyPlaying?.playerId,
|
|
428
486
|
primaryPlayerId: this.primaryPlayer?.playerId,
|
|
429
|
-
entry: item.getPlayer()
|
|
487
|
+
entry: item.getPlayer()?.getEntry(),
|
|
430
488
|
},
|
|
431
489
|
});
|
|
432
490
|
};
|
|
@@ -448,7 +506,7 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
448
506
|
this.currentlyPlaying = null;
|
|
449
507
|
|
|
450
508
|
log_debug(
|
|
451
|
-
`onLiveImageError:
|
|
509
|
+
`onLiveImageError: currentItem: ${currentItem.playerId} - ${currentItem.tag} was removed`
|
|
452
510
|
);
|
|
453
511
|
|
|
454
512
|
// TODO: ...Maybe player some other item
|
|
@@ -460,8 +518,8 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
460
518
|
item,
|
|
461
519
|
error,
|
|
462
520
|
playerId: currentItem?.playerId,
|
|
463
|
-
primaryPlayerId:
|
|
464
|
-
entry: item.getPlayer()
|
|
521
|
+
primaryPlayerId: this.primaryPlayer?.playerId,
|
|
522
|
+
entry: item.getPlayer()?.getEntry(),
|
|
465
523
|
},
|
|
466
524
|
});
|
|
467
525
|
};
|
|
@@ -503,10 +561,10 @@ export class LiveImageManager implements PlayerLifecycleListener {
|
|
|
503
561
|
LiveImageManager.instance;
|
|
504
562
|
|
|
505
563
|
export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
|
|
506
|
-
public player: Player;
|
|
507
|
-
public setMode
|
|
508
|
-
// Will be replaced with rects
|
|
564
|
+
public player: Player | null = null;
|
|
565
|
+
public setMode?: (type: LiveImageType) => void;
|
|
509
566
|
public isFullyVisible: boolean = false;
|
|
567
|
+
public tag: string;
|
|
510
568
|
public position: Position = {
|
|
511
569
|
centerX: 0,
|
|
512
570
|
centerY: 0,
|
|
@@ -516,18 +574,105 @@ export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
|
|
|
516
574
|
left: 0,
|
|
517
575
|
};
|
|
518
576
|
|
|
577
|
+
positionToString() {
|
|
578
|
+
if (!this.position) {
|
|
579
|
+
return "position not set";
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const { centerX, centerY, top, bottom, left, right } = this.position;
|
|
583
|
+
|
|
584
|
+
return `centerX: ${centerX.toFixed(2)}, centerY: ${centerY.toFixed(2)}, top: ${top.toFixed(2)}, bottom: ${bottom.toFixed(2)}, left: ${left.toFixed(2)}, right: ${right.toFixed(2)}`;
|
|
585
|
+
}
|
|
586
|
+
|
|
519
587
|
readonly playerId: string;
|
|
520
|
-
|
|
588
|
+
public component: any = null;
|
|
589
|
+
|
|
590
|
+
private factoryConfig: PlayerFactoryConfig;
|
|
591
|
+
public preloadHooks?: PreloadHookConfig[];
|
|
592
|
+
public processedEntry: ZappEntry | null = null;
|
|
593
|
+
private _preparePromise: Promise<boolean> | null = null;
|
|
521
594
|
|
|
522
595
|
constructor(props: LiveImageProps) {
|
|
523
|
-
this.player = props.player;
|
|
524
596
|
this.setMode = props.setMode;
|
|
525
|
-
this.playerId =
|
|
526
|
-
this.
|
|
527
|
-
this.
|
|
597
|
+
this.playerId = props.playerId;
|
|
598
|
+
this.preloadHooks = props.preloadHooks;
|
|
599
|
+
this.factoryConfig = props.factoryConfig;
|
|
600
|
+
this.tag = props.tag || "untagged";
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async prepareForPlayback(): Promise<boolean> {
|
|
604
|
+
// Already prepared — player exists
|
|
605
|
+
if (this.player) {
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Deduplicate: if preparation is already in flight, await the same promise
|
|
610
|
+
if (this._preparePromise) {
|
|
611
|
+
return this._preparePromise;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
this._preparePromise = (async (): Promise<boolean> => {
|
|
615
|
+
// 1. Run hooks if configured
|
|
616
|
+
let entry = this.factoryConfig.entry;
|
|
617
|
+
|
|
618
|
+
if (this.preloadHooks?.length) {
|
|
619
|
+
const result = await executePreloadHooks({
|
|
620
|
+
preloadHooks: this.preloadHooks,
|
|
621
|
+
entry,
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
if (result) {
|
|
625
|
+
this.processedEntry = result;
|
|
626
|
+
entry = result;
|
|
627
|
+
} else {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// 2. Create the player with the correct entry
|
|
633
|
+
const factoryItem = playerFactory({
|
|
634
|
+
player: this.factoryConfig.player,
|
|
635
|
+
playerId: this.factoryConfig.playerId,
|
|
636
|
+
autoplay: false,
|
|
637
|
+
entry,
|
|
638
|
+
muted: this.factoryConfig.muted,
|
|
639
|
+
playerPluginId: this.factoryConfig.playerPluginId,
|
|
640
|
+
screenConfig: this.factoryConfig.screenConfig,
|
|
641
|
+
playerRole: PlayerRole.Cell,
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
if (!factoryItem) {
|
|
645
|
+
throw new Error("Player factory returned null");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
this.player = factoryItem.controller;
|
|
649
|
+
this.component = factoryItem.Component;
|
|
650
|
+
|
|
651
|
+
// 3. Register callbacks — player now exists
|
|
652
|
+
this.player.addListener({ id: "live-image", listener: this });
|
|
653
|
+
|
|
654
|
+
return true;
|
|
655
|
+
})()
|
|
656
|
+
.then((result) => {
|
|
657
|
+
this._preparePromise = null;
|
|
658
|
+
|
|
659
|
+
return result;
|
|
660
|
+
})
|
|
661
|
+
.catch((error) => {
|
|
662
|
+
this._preparePromise = null;
|
|
663
|
+
|
|
664
|
+
log_error(
|
|
665
|
+
`prepareForPlayback: live-image ${this.playerId}, error preparing for playback: ${error?.message}`,
|
|
666
|
+
{ error }
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
throw error;
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
return this._preparePromise;
|
|
528
673
|
}
|
|
529
674
|
|
|
530
|
-
public getPlayer = (): Player => {
|
|
675
|
+
public getPlayer = (): Player | null => {
|
|
531
676
|
return this.player;
|
|
532
677
|
};
|
|
533
678
|
|
|
@@ -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,36 @@ 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
|
-
|
|
129
|
+
muted,
|
|
130
|
+
playerPluginId: playerPluginId,
|
|
131
|
+
screenConfig: screenConfig,
|
|
132
|
+
entry: item,
|
|
133
|
+
},
|
|
134
|
+
tag: item.title?.toString(),
|
|
135
|
+
});
|
|
136
|
+
}, [playerId, preloadHooks, muted, playerPluginId, screenConfig, item]);
|
|
145
137
|
|
|
146
138
|
React.useEffect(() => {
|
|
147
139
|
liveImageItem.setMode = setModeDebounced;
|
|
148
140
|
}, [setModeDebounced, liveImageItem]);
|
|
149
141
|
|
|
142
|
+
// todo: no need for it to be react, can be moved into `player.addListener`
|
|
150
143
|
const { start, end } = React.useMemo(
|
|
151
144
|
() => getAutoplaySettings(item),
|
|
152
145
|
[item.id]
|
|
153
146
|
);
|
|
154
147
|
|
|
155
|
-
const controller = liveImageItem.getPlayer();
|
|
156
148
|
const player = usePlayer(playerId);
|
|
157
149
|
|
|
158
150
|
const _assignRoot = (component) => {
|
|
@@ -169,7 +161,7 @@ const PlayerLiveImageComponent = (props: Props) => {
|
|
|
169
161
|
|
|
170
162
|
React.useEffect(() => {
|
|
171
163
|
// FIXME - find a more elegant way to disable live-image on cell for measurement
|
|
172
|
-
if (isMeasurement(item)) {
|
|
164
|
+
if (isMeasurement(item.id)) {
|
|
173
165
|
return;
|
|
174
166
|
}
|
|
175
167
|
|
|
@@ -253,7 +245,13 @@ const PlayerLiveImageComponent = (props: Props) => {
|
|
|
253
245
|
}, [item.id]);
|
|
254
246
|
|
|
255
247
|
React.useEffect(() => {
|
|
256
|
-
if (isMeasurement(item) || !playerManager) {
|
|
248
|
+
if (isMeasurement(item.id) || !playerManager) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const controller = liveImageItem.getPlayer();
|
|
253
|
+
|
|
254
|
+
if (!controller) {
|
|
257
255
|
return;
|
|
258
256
|
}
|
|
259
257
|
|
|
@@ -273,7 +271,7 @@ const PlayerLiveImageComponent = (props: Props) => {
|
|
|
273
271
|
playerManager.unregisterPlayer(playerId);
|
|
274
272
|
};
|
|
275
273
|
}
|
|
276
|
-
}, [liveImageItem, playerId,
|
|
274
|
+
}, [liveImageItem, playerId, mode, item.id]);
|
|
277
275
|
|
|
278
276
|
const onPositionUpdated = React.useCallback(
|
|
279
277
|
(data) => {
|
|
@@ -310,7 +308,7 @@ const PlayerLiveImageComponent = (props: Props) => {
|
|
|
310
308
|
? LiveImageManager.instance.onViewportEnter(liveImageItem)
|
|
311
309
|
: LiveImageManager.instance.onViewportLeave(liveImageItem);
|
|
312
310
|
} else {
|
|
313
|
-
LiveImageManager.instance.
|
|
311
|
+
LiveImageManager.instance.onViewPositionChanged(liveImageItem);
|
|
314
312
|
}
|
|
315
313
|
},
|
|
316
314
|
[liveImageItem, dimensions.height, dimensions.width]
|
|
@@ -338,11 +336,11 @@ const PlayerLiveImageComponent = (props: Props) => {
|
|
|
338
336
|
<Player
|
|
339
337
|
autoplay={false}
|
|
340
338
|
ref={_assignRoot}
|
|
341
|
-
entry={item} // Must be passed first in list
|
|
339
|
+
entry={liveImageItem.processedEntry || item} // Must be passed first in list
|
|
342
340
|
style={videoStyles}
|
|
343
341
|
playerId={playerId}
|
|
344
342
|
muted={muted}
|
|
345
|
-
listener={
|
|
343
|
+
listener={liveImageItem.getPlayer()?.getListener()}
|
|
346
344
|
resizeMode={"cover"}
|
|
347
345
|
{...platformSpecificProps}
|
|
348
346
|
/>
|
|
@@ -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.20
|
|
3
|
+
"version": "14.0.20",
|
|
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.20
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "14.0.20
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "14.0.20
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "14.0.20
|
|
31
|
+
"@applicaster/applicaster-types": "14.0.20",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "14.0.20",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "14.0.20",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "14.0.20",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"url": "^0.11.0",
|