@vectoriox/iox-ui 5.76.0 → 5.77.0
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/fesm2022/vectoriox-iox-ui-media-core.mjs +145 -33
- package/fesm2022/vectoriox-iox-ui-media-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-scene-core.mjs +29 -1
- package/fesm2022/vectoriox-iox-ui-scene-core.mjs.map +1 -1
- package/media-core/index.d.ts +38 -7
- package/package.json +1 -1
- package/scene-core/index.d.ts +19 -2
- package/src/media-core/frames.ts +143 -33
- package/src/scene-core/scene-edit.ts +35 -0
|
@@ -20,8 +20,12 @@ var FrameExtension;
|
|
|
20
20
|
FrameExtension["Webp"] = "webp";
|
|
21
21
|
FrameExtension["Jpeg"] = "jpg";
|
|
22
22
|
})(FrameExtension || (FrameExtension = {}));
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The most pictures one set may hold. A film's smoothness is its density, pictures per screen of scrolling: 210 over 18
|
|
25
|
+
* screens stepped on Doron's iPhone as a glide slowed, 1050 did not (2026-11-05). Memory is not what limits it: the player
|
|
26
|
+
* keeps the downloaded files (about 10 kB a phone picture) and only a window of them unpacked (`unpackedFrameCount`).
|
|
27
|
+
*/
|
|
28
|
+
const MAX_FRAMES = 1200;
|
|
25
29
|
/** The most one picture may weigh when uploaded. */
|
|
26
30
|
const MAX_FRAME_BYTES = 3 * 1024 * 1024;
|
|
27
31
|
/** A sequence's name in storage: what the browser generates and what the server accepts in a key. */
|
|
@@ -140,14 +144,38 @@ function nearestLoadedFrame(loaded, index) {
|
|
|
140
144
|
}
|
|
141
145
|
return -1;
|
|
142
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* How much unpacked picture one player holds at once. A browser keeps few of 300 unpacked pictures (a wide set is 1.7 GB
|
|
149
|
+
* unpacked) and unpacked again on the page's own thread, at the moment of drawing, 22 ms a 1600×900 picture: longer than a
|
|
150
|
+
* whole frame, so the film and the page beside it skipped a beat for every new picture (Doron's recording, 2026-11-04).
|
|
151
|
+
*/
|
|
152
|
+
const FRAMES_UNPACKED_BYTES = 96 * 1024 * 1024;
|
|
153
|
+
const MIN_UNPACKED = 6;
|
|
154
|
+
const MAX_UNPACKED = 24;
|
|
155
|
+
/** How many pictures of `width`×`height` a player holds unpacked. */
|
|
156
|
+
function unpackedFrameCount(width, height) {
|
|
157
|
+
return Math.min(MAX_UNPACKED, Math.max(MIN_UNPACKED, Math.floor(FRAMES_UNPACKED_BYTES / (width * height * 4))));
|
|
158
|
+
}
|
|
159
|
+
/** The `size` pictures to hold unpacked around `position`, nearest first, each side in turn starting ahead. */
|
|
160
|
+
function framesToHold(position, count, size) {
|
|
161
|
+
const from = Math.min(count - 1, Math.max(0, Math.floor(position)));
|
|
162
|
+
const held = [from];
|
|
163
|
+
for (let distance = 1; held.length < Math.min(size, count); distance++) {
|
|
164
|
+
if (from + distance < count)
|
|
165
|
+
held.push(from + distance);
|
|
166
|
+
if (from - distance >= 0 && held.length < size)
|
|
167
|
+
held.push(from - distance);
|
|
168
|
+
}
|
|
169
|
+
return held;
|
|
170
|
+
}
|
|
143
171
|
/** The longest side of a wide picture and of a phone picture. A phone's is not its width: a landscape video covers a
|
|
144
172
|
* tall stage by its height, so a picture sized to the phone's width would be enlarged five times. */
|
|
145
173
|
const WIDE_LONG_SIDE = 1600;
|
|
146
174
|
const PHONE_LONG_SIDE = 1100;
|
|
147
|
-
/** How many pictures a second of video becomes, before the cap. */
|
|
148
|
-
const VIDEO_FRAMES_PER_SECOND =
|
|
149
|
-
/** A video's cap:
|
|
150
|
-
const VIDEO_MAX_FRAMES =
|
|
175
|
+
/** How many pictures a second of video becomes, before the cap: what most recordings hold, so few are the same twice. */
|
|
176
|
+
const VIDEO_FRAMES_PER_SECOND = 30;
|
|
177
|
+
/** A video's cap: half a minute at that rate. A set of pictures is the author's own count, up to `MAX_FRAMES`. */
|
|
178
|
+
const VIDEO_MAX_FRAMES = 900;
|
|
151
179
|
/** `width`×`height` fitted inside `longSide`, never enlarged. */
|
|
152
180
|
function fittedFrameSize(width, height, longSide) {
|
|
153
181
|
const scale = Math.min(1, longSide / Math.max(width, height));
|
|
@@ -165,10 +193,6 @@ function sampledFrameIndexes(sourceCount, count) {
|
|
|
165
193
|
return [0];
|
|
166
194
|
return Array.from({ length: count }, (_, i) => Math.round((i * (sourceCount - 1)) / (count - 1)));
|
|
167
195
|
}
|
|
168
|
-
/** A phone plays half the pictures: it has less memory, and half is plenty on a small screen. */
|
|
169
|
-
function phoneFrameCount(wideCount) {
|
|
170
|
-
return Math.max(2, Math.ceil(wideCount / 2));
|
|
171
|
-
}
|
|
172
196
|
const NATURAL = new Intl.Collator('en', { numeric: true, sensitivity: 'base' });
|
|
173
197
|
/** Rendered frames in their real order: `frame2` before `frame10`. */
|
|
174
198
|
function sortFrameFiles(files) {
|
|
@@ -183,6 +207,7 @@ var FramesClass;
|
|
|
183
207
|
FramesClass["Canvas"] = "iox-video__frames";
|
|
184
208
|
})(FramesClass || (FramesClass = {}));
|
|
185
209
|
const LOAD_CONCURRENCY = 6;
|
|
210
|
+
const UNPACK_CONCURRENCY = 4;
|
|
186
211
|
const NEAR_MARGIN = '100% 0px';
|
|
187
212
|
/** Under these the visitor keeps the poster: nobody on a metered 2G connection asked to download a film. */
|
|
188
213
|
const SLOW_CONNECTIONS = new Set(['slow-2g', '2g']);
|
|
@@ -190,6 +215,11 @@ const SLOW_CONNECTIONS = new Set(['slow-2g', '2g']);
|
|
|
190
215
|
* The player: draws the picture nearest what the scroll asked for on a canvas laid over the poster, and nothing until a
|
|
191
216
|
* first picture has arrived, so the poster is what a visitor sees until then, and for good if the pictures never come.
|
|
192
217
|
* Browser only. It writes and never reads, so the driver can call it in its write phase.
|
|
218
|
+
*
|
|
219
|
+
* Each picture is downloaded as a file and unpacked off the page's thread (`createImageBitmap` of the file), a window of
|
|
220
|
+
* them around where the film stands, so drawing one never waits for it. A browser that cannot (no `fetch` or
|
|
221
|
+
* `createImageBitmap`, or storage that refuses the download) plays from `<img>` pictures, as before: it works, and pays the
|
|
222
|
+
* unpacking on the page's thread.
|
|
193
223
|
*/
|
|
194
224
|
class FramePlayer {
|
|
195
225
|
constructor(el, sequence, options = {}) {
|
|
@@ -204,13 +234,21 @@ class FramePlayer {
|
|
|
204
234
|
this.loading = 0;
|
|
205
235
|
this.started = false;
|
|
206
236
|
this.destroyed = false;
|
|
237
|
+
this.held = new Set();
|
|
238
|
+
this.unpacked = new Map();
|
|
239
|
+
this.unpacking = new Set();
|
|
207
240
|
this.pending = new Set();
|
|
208
241
|
this.host = options.host ?? window;
|
|
209
242
|
const { set, info } = frameSetFor(sequence, !!options.phone);
|
|
210
243
|
this.set = set;
|
|
211
244
|
this.info = info;
|
|
212
|
-
this.
|
|
245
|
+
this.ready = new Array(info.count).fill(null);
|
|
246
|
+
this.files = new Array(info.count).fill(null);
|
|
213
247
|
this.order = frameLoadOrder(info.count);
|
|
248
|
+
this.holdCount = unpackedFrameCount(info.width, info.height);
|
|
249
|
+
this.fromFiles = typeof this.host.fetch === 'function' && typeof this.host.createImageBitmap === 'function';
|
|
250
|
+
const Controller = this.host.AbortController;
|
|
251
|
+
this.downloads = this.fromFiles && Controller ? new Controller() : null;
|
|
214
252
|
if (!this.allowed())
|
|
215
253
|
return;
|
|
216
254
|
const Observer = this.host.IntersectionObserver;
|
|
@@ -232,6 +270,7 @@ class FramePlayer {
|
|
|
232
270
|
if (played === this.played)
|
|
233
271
|
return;
|
|
234
272
|
this.played = played;
|
|
273
|
+
this.hold();
|
|
235
274
|
this.draw();
|
|
236
275
|
}
|
|
237
276
|
/** Whether a re-render took the canvas away with the markup it sat in. */
|
|
@@ -243,12 +282,16 @@ class FramePlayer {
|
|
|
243
282
|
this.el.removeAttribute(FRAMES_SHOWING_ATTRIBUTE);
|
|
244
283
|
this.observer?.disconnect();
|
|
245
284
|
this.observer = null;
|
|
285
|
+
this.downloads?.abort();
|
|
246
286
|
for (const image of this.pending) {
|
|
247
287
|
image.onload = null;
|
|
248
288
|
image.onerror = null;
|
|
249
289
|
image.src = '';
|
|
250
290
|
}
|
|
251
291
|
this.pending.clear();
|
|
292
|
+
for (const bitmap of this.unpacked.values())
|
|
293
|
+
bitmap.close();
|
|
294
|
+
this.unpacked.clear();
|
|
252
295
|
this.canvas?.remove();
|
|
253
296
|
this.canvas = null;
|
|
254
297
|
this.context = null;
|
|
@@ -267,26 +310,55 @@ class FramePlayer {
|
|
|
267
310
|
}
|
|
268
311
|
pump() {
|
|
269
312
|
while (!this.destroyed && this.loading < LOAD_CONCURRENCY && this.nextToLoad < this.order.length) {
|
|
270
|
-
this.
|
|
313
|
+
const index = this.order[this.nextToLoad++];
|
|
314
|
+
if (this.fromFiles)
|
|
315
|
+
this.download(index);
|
|
316
|
+
else
|
|
317
|
+
this.loadImage(index);
|
|
271
318
|
}
|
|
272
319
|
}
|
|
273
|
-
|
|
320
|
+
loaded(arrived) {
|
|
321
|
+
this.loading--;
|
|
322
|
+
if (this.destroyed)
|
|
323
|
+
return;
|
|
324
|
+
if (arrived)
|
|
325
|
+
this.draw();
|
|
326
|
+
this.pump();
|
|
327
|
+
}
|
|
328
|
+
download(index) {
|
|
329
|
+
this.loading++;
|
|
330
|
+
const url = frameUrl(this.sequence, this.set, index);
|
|
331
|
+
this.host.fetch(url, this.downloads ? { signal: this.downloads.signal } : undefined)
|
|
332
|
+
.then(response => (response.ok ? response.blob() : null))
|
|
333
|
+
.then(file => {
|
|
334
|
+
if (this.destroyed)
|
|
335
|
+
return;
|
|
336
|
+
// A picture that failed is a gap: its neighbours stand in.
|
|
337
|
+
this.files[index] = file;
|
|
338
|
+
this.loading--;
|
|
339
|
+
this.hold();
|
|
340
|
+
this.pump();
|
|
341
|
+
}, () => {
|
|
342
|
+
if (this.destroyed)
|
|
343
|
+
return;
|
|
344
|
+
// Storage that refuses a download from the page's address still serves an <img>: play from those.
|
|
345
|
+
this.fromFiles = false;
|
|
346
|
+
this.loading--;
|
|
347
|
+
this.loadImage(index);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
loadImage(index) {
|
|
274
351
|
const image = this.host.document.createElement('img');
|
|
275
352
|
image.decoding = 'async';
|
|
276
353
|
this.loading++;
|
|
277
354
|
this.pending.add(image);
|
|
278
355
|
const done = (arrived) => {
|
|
279
356
|
this.pending.delete(image);
|
|
280
|
-
this.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (arrived) {
|
|
284
|
-
this.images[index] = image;
|
|
285
|
-
this.draw();
|
|
286
|
-
}
|
|
287
|
-
this.pump();
|
|
357
|
+
if (!this.destroyed && arrived)
|
|
358
|
+
this.ready[index] = image;
|
|
359
|
+
this.loaded(arrived);
|
|
288
360
|
};
|
|
289
|
-
// Decoded before it counts, or
|
|
361
|
+
// Decoded before it counts, or its first draw decodes on the driver's frame.
|
|
290
362
|
image.onload = () => {
|
|
291
363
|
const decoded = image.decode?.();
|
|
292
364
|
if (decoded)
|
|
@@ -294,19 +366,58 @@ class FramePlayer {
|
|
|
294
366
|
else
|
|
295
367
|
done(true);
|
|
296
368
|
};
|
|
297
|
-
// A picture that failed is a gap: its neighbours stand in.
|
|
298
369
|
image.onerror = () => done(false);
|
|
299
370
|
image.src = frameUrl(this.sequence, this.set, index);
|
|
300
371
|
}
|
|
372
|
+
/** Unpack the pictures around where the film stands, nearest first, and let go of the ones it has left. */
|
|
373
|
+
hold() {
|
|
374
|
+
if (this.destroyed)
|
|
375
|
+
return;
|
|
376
|
+
const wanted = framesToHold(framePosition(this.played ?? 0, this.info.count), this.info.count, this.holdCount);
|
|
377
|
+
this.held = new Set(wanted);
|
|
378
|
+
for (const [index, bitmap] of this.unpacked) {
|
|
379
|
+
if (this.held.has(index))
|
|
380
|
+
continue;
|
|
381
|
+
bitmap.close();
|
|
382
|
+
this.unpacked.delete(index);
|
|
383
|
+
this.ready[index] = null;
|
|
384
|
+
}
|
|
385
|
+
for (const index of wanted) {
|
|
386
|
+
if (this.unpacking.size >= UNPACK_CONCURRENCY)
|
|
387
|
+
return;
|
|
388
|
+
if (this.files[index] && !this.ready[index] && !this.unpacking.has(index))
|
|
389
|
+
this.unpack(index);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
unpack(index) {
|
|
393
|
+
this.unpacking.add(index);
|
|
394
|
+
this.host.createImageBitmap(this.files[index]).then(bitmap => {
|
|
395
|
+
this.unpacking.delete(index);
|
|
396
|
+
if (this.destroyed || !this.held.has(index)) {
|
|
397
|
+
bitmap.close();
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
this.unpacked.set(index, bitmap);
|
|
401
|
+
this.ready[index] = bitmap;
|
|
402
|
+
this.draw();
|
|
403
|
+
}
|
|
404
|
+
this.hold();
|
|
405
|
+
}, () => {
|
|
406
|
+
// A file that will not unpack is a gap, like one that never came.
|
|
407
|
+
this.unpacking.delete(index);
|
|
408
|
+
this.files[index] = null;
|
|
409
|
+
this.hold();
|
|
410
|
+
});
|
|
411
|
+
}
|
|
301
412
|
draw() {
|
|
302
413
|
if (this.destroyed || this.played === null)
|
|
303
414
|
return;
|
|
304
|
-
const blend = frameBlendAt(this.
|
|
415
|
+
const blend = frameBlendAt(this.ready, framePosition(this.played, this.info.count));
|
|
305
416
|
const last = this.drawn;
|
|
306
417
|
if (!blend || (last && last.from === blend.from && last.to === blend.to && last.mix === blend.mix))
|
|
307
418
|
return;
|
|
308
|
-
const
|
|
309
|
-
if (!this.ensureCanvas(
|
|
419
|
+
const picture = this.ready[blend.from];
|
|
420
|
+
if (!this.ensureCanvas(picture))
|
|
310
421
|
return;
|
|
311
422
|
const context = this.context;
|
|
312
423
|
const { width, height } = this.canvas;
|
|
@@ -317,28 +428,29 @@ class FramePlayer {
|
|
|
317
428
|
// a time instead of moving.
|
|
318
429
|
context.globalCompositeOperation = 'lighter';
|
|
319
430
|
context.globalAlpha = 1 - blend.mix;
|
|
320
|
-
context.drawImage(
|
|
431
|
+
context.drawImage(picture, 0, 0, width, height);
|
|
321
432
|
context.globalAlpha = blend.mix;
|
|
322
|
-
context.drawImage(this.
|
|
433
|
+
context.drawImage(this.ready[blend.to], 0, 0, width, height);
|
|
323
434
|
context.globalAlpha = 1;
|
|
324
435
|
context.globalCompositeOperation = 'source-over';
|
|
325
436
|
}
|
|
326
437
|
else {
|
|
327
|
-
context.drawImage(
|
|
438
|
+
context.drawImage(picture, 0, 0, width, height);
|
|
328
439
|
}
|
|
329
440
|
// Over a transparent film the poster would show through: a second, stiller subject behind the moving one.
|
|
330
441
|
if (!this.drawn)
|
|
331
442
|
this.el.setAttribute(FRAMES_SHOWING_ATTRIBUTE, '');
|
|
332
443
|
this.drawn = blend;
|
|
333
444
|
}
|
|
334
|
-
ensureCanvas(
|
|
445
|
+
ensureCanvas(picture) {
|
|
335
446
|
if (this.canvas)
|
|
336
447
|
return !!this.context;
|
|
448
|
+
const size = picture;
|
|
337
449
|
const canvas = this.host.document.createElement('canvas');
|
|
338
450
|
canvas.className = FramesClass.Canvas;
|
|
339
451
|
canvas.setAttribute('aria-hidden', 'true');
|
|
340
|
-
canvas.width =
|
|
341
|
-
canvas.height =
|
|
452
|
+
canvas.width = size.naturalWidth || Number(size.width) || this.info.width;
|
|
453
|
+
canvas.height = size.naturalHeight || Number(size.height) || this.info.height;
|
|
342
454
|
this.context = canvas.getContext('2d');
|
|
343
455
|
if (!this.context)
|
|
344
456
|
return false;
|
|
@@ -968,5 +1080,5 @@ function videoRuntimeOf(el) {
|
|
|
968
1080
|
* Generated bundle index. Do not edit.
|
|
969
1081
|
*/
|
|
970
1082
|
|
|
971
|
-
export { CONSENT_GRANT_EVENT, CONSENT_PREFERENCES_EVENT, FRAMES_PHONE_MAX_WIDTH, FRAMES_SHOWING_ATTRIBUTE, FRAME_BLEND_STEPS, FRAME_SEQUENCE_ID, FrameExtension, FramePlayer, FrameSet, FramesClass, MAX_FRAMES, MAX_FRAME_BYTES, PHONE_LONG_SIDE, VIDEO_EMBED_ORIGINS, VIDEO_NODE_TYPE, VIDEO_PROVIDER_COMPANY, VIDEO_PROVIDER_LABEL, VIDEO_WORDS, VideoAttribute, VideoClass, VideoIcon, VideoKind, VideoMode, VideoRuntime, VideoSource, VideoTrait, WIDE_LONG_SIDE, attachVideoAt, attachVideos, documentConsentTransport, fittedFrameSize, frameBlendAt, frameFileName, frameIndexAt, frameLoadOrder, framePosition, frameSetFor, frameSummary, frameUrl, isThirdPartyPoster, nearestLoadedFrame, normalizeFrameSequence, parseVideoUrl,
|
|
1083
|
+
export { CONSENT_GRANT_EVENT, CONSENT_PREFERENCES_EVENT, FRAMES_PHONE_MAX_WIDTH, FRAMES_SHOWING_ATTRIBUTE, FRAMES_UNPACKED_BYTES, FRAME_BLEND_STEPS, FRAME_SEQUENCE_ID, FrameExtension, FramePlayer, FrameSet, FramesClass, MAX_FRAMES, MAX_FRAME_BYTES, PHONE_LONG_SIDE, VIDEO_EMBED_ORIGINS, VIDEO_NODE_TYPE, VIDEO_PROVIDER_COMPANY, VIDEO_PROVIDER_LABEL, VIDEO_WORDS, VideoAttribute, VideoClass, VideoIcon, VideoKind, VideoMode, VideoRuntime, VideoSource, VideoTrait, WIDE_LONG_SIDE, attachVideoAt, attachVideos, documentConsentTransport, fittedFrameSize, frameBlendAt, frameFileName, frameIndexAt, frameLoadOrder, framePosition, frameSetFor, frameSummary, frameUrl, framesToHold, isThirdPartyPoster, nearestLoadedFrame, normalizeFrameSequence, parseVideoUrl, phoneFrameSrcset, planVideo, readConsentRecord, sampledFrameIndexes, serializeVideoAttributes, sortFrameFiles, unpackedFrameCount, videoConsentCategory, videoEmbedUrl, videoFrameCount, videoInnerHtml, videoPlayLabel, videoPosterFileName, videoProp, videoRefOf, videoRootAttributes, videoRuntimeOf, videoSourceOf, videoThumbnailRequest, videoWords };
|
|
972
1084
|
//# sourceMappingURL=vectoriox-iox-ui-media-core.mjs.map
|