react-x11 2.32.0 → 2.34.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/package.json +1 -1
- package/src/Reconciler.js +5 -0
- package/src/a11y.js +4 -0
- package/src/appcontext.js +16 -1
- package/src/cocoa/app.js +54 -1
- package/src/cocoa/fonts.js +30 -6
- package/src/cocoa/player.js +177 -0
- package/src/cocoa/promotion.js +4 -0
- package/src/cocoa/sprites.js +188 -34
- package/src/cocoa/video.js +122 -0
- package/src/host.js +1 -0
- package/src/index.d.ts +14 -1
- package/src/index.js +7 -0
- package/src/mediaplayback.js +37 -0
- package/src/node.d.ts +25 -2
- package/src/nodes/fit.js +46 -0
- package/src/nodes/image.js +37 -6
- package/src/nodes/kinds.js +1 -0
- package/src/nodes/layouthost.js +1 -0
- package/src/nodes/video.js +641 -0
- package/src/styles.js +19 -4
- package/src/types/elements.d.ts +54 -0
- package/src/types/nodes.d.ts +19 -0
- package/src/types/style.d.ts +11 -0
- package/src/types/video.d.ts +123 -0
- package/src/videoframes.js +375 -0
- package/src/videohooks.js +29 -0
package/src/cocoa/sprites.js
CHANGED
|
@@ -17,16 +17,33 @@
|
|
|
17
17
|
// A part's content is a raster the presenter paints by calling the part's
|
|
18
18
|
// own `paint`, at opacity 1 and untransformed, and paints again when its
|
|
19
19
|
// `version` changes; its opacity and its transform are the layer's, and so
|
|
20
|
-
// are its animations, keyframes the bridge hands to Core Animation.
|
|
20
|
+
// are its animations, keyframes the bridge hands to Core Animation. Or it is
|
|
21
|
+
// a **source**, the part's `contents` — a `<video>`'s frames, or its player —
|
|
22
|
+
// which the presenter does not paint at all: the source shows itself on the
|
|
23
|
+
// layer (src/cocoa/video.js, src/cocoa/player.js), between frames and on its
|
|
24
|
+
// own clock, and the layer is exactly the part's rect. A source this bridge
|
|
25
|
+
// cannot show is a part declined, drawn by its element like any other. A
|
|
21
26
|
// transform is CSS's matrix, which the bridge takes from
|
|
22
27
|
// `@windowkit/appkit` 0.19 (`transformForms()`) along with the negative
|
|
23
28
|
// delay a part joined half way through needs, so that is the bridge sprites
|
|
24
29
|
// ask for: on an older one nothing is asked and every part is drawn by its
|
|
25
30
|
// element.
|
|
31
|
+
//
|
|
32
|
+
// A part may be inside another of its element's (`parent`): a spinner in a
|
|
33
|
+
// card that fades. Its layer is lifted inside the parent's, so it fades,
|
|
34
|
+
// turns and is cut with it, and the parent's raster leaves it out — the
|
|
35
|
+
// presenter hands the parent's `paint` the keys of its children lifted with
|
|
36
|
+
// it, and paints it again whenever they change, in the frame they do. The
|
|
37
|
+
// parent was asked about everywhere it can be, so a child is asked about
|
|
38
|
+
// nothing more where it stays inside the parent's raster, and is its
|
|
39
|
+
// element's to draw where it does not, or where its parent is not lifted.
|
|
26
40
|
|
|
27
41
|
import { RASTER_PAD, RasterState } from './presenter.js';
|
|
28
|
-
import {
|
|
42
|
+
import { CocoaPlayer, PlayerLift } from './player.js';
|
|
43
|
+
import { VideoLift, canLiftVideo } from './video.js';
|
|
44
|
+
import { intersectRects, rectContains } from '../nodes/rects.js';
|
|
29
45
|
import { DEV } from '../nodes/util.js';
|
|
46
|
+
import { isVideoFrames } from '../videoframes.js';
|
|
30
47
|
|
|
31
48
|
// Past this many device pixels on a side a part is drawn by its element: a
|
|
32
49
|
// layer's contents are a bitmap, and a page-sized one is a page-sized
|
|
@@ -39,6 +56,11 @@ const AFTER_CHILDREN = Number.MAX_SAFE_INTEGER;
|
|
|
39
56
|
|
|
40
57
|
const IDENTITY = Object.freeze([1, 0, 0, 1, 0, 0]);
|
|
41
58
|
|
|
59
|
+
const ORIGIN = Object.freeze({ x: 0, y: 0 });
|
|
60
|
+
|
|
61
|
+
// what a part with no part lifted inside it is handed
|
|
62
|
+
const NO_KIDS = Object.freeze(new Set());
|
|
63
|
+
|
|
42
64
|
// what a part's raster holds before its first paint
|
|
43
65
|
const UNPAINTED = Symbol('unpainted');
|
|
44
66
|
|
|
@@ -105,12 +127,29 @@ function isAnimation(a) {
|
|
|
105
127
|
return true;
|
|
106
128
|
}
|
|
107
129
|
|
|
108
|
-
/** Is `sprite` a part as `Node.sprites()` documents one?
|
|
130
|
+
/** Is `sprite` a part as `Node.sprites()` documents one? Either it paints
|
|
131
|
+
* or it has a source to show, never both. */
|
|
109
132
|
function isSprite(sprite) {
|
|
110
133
|
if (sprite == null || typeof sprite.key !== 'string') return false;
|
|
111
|
-
if (!isRect(sprite.rect)
|
|
134
|
+
if (!isRect(sprite.rect)) return false;
|
|
135
|
+
if (
|
|
136
|
+
sprite.contents != null
|
|
137
|
+
? sprite.paint !== undefined
|
|
138
|
+
: typeof sprite.paint !== 'function'
|
|
139
|
+
) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
112
142
|
if (sprite.reach !== undefined && !isRect(sprite.reach)) return false;
|
|
113
143
|
if (sprite.clip !== undefined && !isRect(sprite.clip)) return false;
|
|
144
|
+
if (sprite.parent !== undefined && typeof sprite.parent !== 'string') {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (
|
|
148
|
+
sprite.clipRadius !== undefined &&
|
|
149
|
+
!(finite(sprite.clipRadius) && sprite.clipRadius >= 0)
|
|
150
|
+
) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
114
153
|
if (sprite.opacity !== undefined && !finite(sprite.opacity)) return false;
|
|
115
154
|
if (sprite.transform !== undefined && !isMatrix(sprite.transform)) {
|
|
116
155
|
return false;
|
|
@@ -204,8 +243,11 @@ function extentOf(sprite) {
|
|
|
204
243
|
}
|
|
205
244
|
|
|
206
245
|
/** The raster a part's content is painted into: its reach, out to whole
|
|
207
|
-
* pixels and padded for antialiasing, untransformed.
|
|
246
|
+
* pixels and padded for antialiasing, untransformed. A source's layer is
|
|
247
|
+
* the part's rect, which the element has already put on whole pixels so
|
|
248
|
+
* that the hole it leaves and the layer meet. */
|
|
208
249
|
function rasterRectOf(sprite) {
|
|
250
|
+
if (sprite.contents != null) return sprite.rect;
|
|
209
251
|
const reach = sprite.reach ?? sprite.rect;
|
|
210
252
|
const x = Math.floor(reach.x) - RASTER_PAD;
|
|
211
253
|
const y = Math.floor(reach.y) - RASTER_PAD;
|
|
@@ -236,6 +278,8 @@ export class SpriteLayers {
|
|
|
236
278
|
this.supported = Boolean(
|
|
237
279
|
this.native.transformForms?.()?.includes?.('matrix'),
|
|
238
280
|
);
|
|
281
|
+
// and a video's frames on a layer need its video surfaces
|
|
282
|
+
this.videos = canLiftVideo(this.native);
|
|
239
283
|
this.hosts = new Map(); // element -> Map(key -> state)
|
|
240
284
|
this._warned = new WeakSet();
|
|
241
285
|
}
|
|
@@ -304,31 +348,82 @@ export class SpriteLayers {
|
|
|
304
348
|
continue;
|
|
305
349
|
}
|
|
306
350
|
if (now.has(sprite.key)) continue;
|
|
351
|
+
// inside another of the element's parts: lifted with it or not at
|
|
352
|
+
// all, its parent earlier in the list, and neither of them a source
|
|
353
|
+
let parent = null;
|
|
354
|
+
if (sprite.parent !== undefined) {
|
|
355
|
+
parent = now.get(sprite.parent) ?? null;
|
|
356
|
+
if (!parent || parent.contents !== null || sprite.contents != null) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const contents = sprite.contents ?? null;
|
|
361
|
+
// a source this bridge cannot show: the element draws it
|
|
362
|
+
if (contents !== null && !this._canShow(contents)) continue;
|
|
307
363
|
const raster = rasterRectOf(sprite);
|
|
308
|
-
|
|
364
|
+
// a raster's limit; a source's surface is its own size, whatever
|
|
365
|
+
// the layer is scaled to
|
|
366
|
+
if (
|
|
367
|
+
contents === null &&
|
|
368
|
+
(raster.width > MAX_SIDE || raster.height > MAX_SIDE)
|
|
369
|
+
) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
309
372
|
// what shows of everywhere it can be: inside its own clip, and the
|
|
310
373
|
// clips of its element's ancestors, which cut its layer (`_clear`)
|
|
311
|
-
const cut = {
|
|
374
|
+
const cut = {
|
|
375
|
+
clip: sprite.clip ?? null,
|
|
376
|
+
radius: sprite.clip ? (sprite.clipRadius ?? 0) : 0,
|
|
377
|
+
bounds: null,
|
|
378
|
+
};
|
|
312
379
|
const extent = extentOf(sprite);
|
|
313
380
|
const bounds = cut.clip ? intersectRects(extent, cut.clip) : extent;
|
|
314
|
-
if (!bounds
|
|
381
|
+
if (!bounds) continue;
|
|
382
|
+
if (parent) {
|
|
383
|
+
// the parent was asked about everywhere it can be, and its layer
|
|
384
|
+
// carries this one: nothing more to ask where it stays inside the
|
|
385
|
+
// parent's raster, and its element draws it where it does not
|
|
386
|
+
if (!rectContains(rasterRectOf(parent.sprite), bounds)) continue;
|
|
387
|
+
cut.bounds = bounds;
|
|
388
|
+
} else if (!this.promotion._clear(host, bounds, cut)) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
315
391
|
const shown = cut.bounds;
|
|
316
392
|
let state = was?.get(sprite.key);
|
|
317
393
|
// a clip that comes or goes is a layer in a box or out of one: lifted
|
|
318
|
-
// again, rather than a running layer moved between parents
|
|
319
|
-
|
|
394
|
+
// again, rather than a running layer moved between parents — and so
|
|
395
|
+
// is a part that shows another source, or stops painting to show
|
|
396
|
+
// one, and one in another layer than it was
|
|
397
|
+
if (
|
|
398
|
+
state &&
|
|
399
|
+
(Boolean(state.clip) !== Boolean(cut.clip) ||
|
|
400
|
+
state.contents !== contents ||
|
|
401
|
+
state.parent !== parent)
|
|
402
|
+
) {
|
|
320
403
|
this._drop(state, root);
|
|
321
404
|
state = null;
|
|
322
405
|
}
|
|
323
406
|
const lifting = !state;
|
|
324
|
-
if (lifting)
|
|
407
|
+
if (lifting) {
|
|
408
|
+
state = this._lift(
|
|
409
|
+
host,
|
|
410
|
+
sprite.key,
|
|
411
|
+
cut.clip !== null,
|
|
412
|
+
contents,
|
|
413
|
+
parent,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
325
416
|
state.sprite = sprite;
|
|
326
417
|
state.clip = cut.clip;
|
|
327
|
-
state.
|
|
418
|
+
state.radius = cut.radius;
|
|
419
|
+
state.order = parent ? null : [...place, AFTER_CHILDREN, index];
|
|
420
|
+
state.kids = [];
|
|
421
|
+
parent?.kids.push(state);
|
|
328
422
|
// the bitmap under where it shows repaints without it, from this
|
|
329
423
|
// frame on; where it went since, it is the element's own claims
|
|
330
|
-
// that move the hole
|
|
331
|
-
|
|
424
|
+
// that move the hole. A child is under its parent's layer, whose
|
|
425
|
+
// raster leaves it out instead (`sync`).
|
|
426
|
+
if (lifting && !parent) this.promotion._claim(root, shown);
|
|
332
427
|
state.extent = shown;
|
|
333
428
|
now.set(sprite.key, state);
|
|
334
429
|
}
|
|
@@ -347,11 +442,12 @@ export class SpriteLayers {
|
|
|
347
442
|
return changed;
|
|
348
443
|
}
|
|
349
444
|
|
|
350
|
-
/** Every lifted part, with its place in the paint
|
|
445
|
+
/** Every lifted part above the bitmap, with its place in the paint
|
|
446
|
+
* order: not one inside another's layer, which goes with it (`sync`). */
|
|
351
447
|
entries() {
|
|
352
448
|
const out = [];
|
|
353
449
|
for (const lifted of this.hosts.values()) {
|
|
354
|
-
for (const state of lifted.values()) out.push(state);
|
|
450
|
+
for (const state of lifted.values()) if (!state.parent) out.push(state);
|
|
355
451
|
}
|
|
356
452
|
return out;
|
|
357
453
|
}
|
|
@@ -370,25 +466,39 @@ export class SpriteLayers {
|
|
|
370
466
|
}
|
|
371
467
|
}
|
|
372
468
|
|
|
373
|
-
|
|
469
|
+
/** Can this bridge show `contents` on a layer by itself? A player always
|
|
470
|
+
* can: it was made by this bridge, with the layer it plays into. */
|
|
471
|
+
_canShow(contents) {
|
|
472
|
+
if (contents instanceof CocoaPlayer) return !contents.released;
|
|
473
|
+
return isVideoFrames(contents) && this.videos;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
_lift(host, key, clipped, contents = null, parent = null) {
|
|
374
477
|
const layer = this.native.createLayer();
|
|
375
478
|
// a part cut to a clip is in a box of the clip's size that masks to it,
|
|
376
|
-
// and the box is what stands among the layers above the bitmap
|
|
479
|
+
// and the box is what stands among the layers above the bitmap — or
|
|
480
|
+
// among its parent's children, for a part inside another's layer
|
|
481
|
+
const above = parent ? parent.layer : this.promotion.rootVisual.layer;
|
|
377
482
|
let box = null;
|
|
378
483
|
if (clipped) {
|
|
379
484
|
box = this.native.createLayer();
|
|
380
|
-
this.native.addSublayer(
|
|
485
|
+
this.native.addSublayer(above, box);
|
|
381
486
|
this.native.addSublayer(box, layer);
|
|
382
487
|
} else {
|
|
383
|
-
this.native.addSublayer(
|
|
488
|
+
this.native.addSublayer(above, layer);
|
|
384
489
|
}
|
|
385
490
|
return {
|
|
386
491
|
host,
|
|
387
492
|
key,
|
|
388
493
|
layer,
|
|
389
494
|
box,
|
|
495
|
+
// the part whose layer this one is in, and the parts in this one's
|
|
496
|
+
parent,
|
|
497
|
+
kids: [],
|
|
498
|
+
paintedKids: '',
|
|
390
499
|
boxProps: {},
|
|
391
500
|
clip: null, // device pixels, window coordinates
|
|
501
|
+
radius: 0, // the clip's corners, device pixels
|
|
392
502
|
raster: new RasterState(),
|
|
393
503
|
painted: UNPAINTED,
|
|
394
504
|
rect: null, // the raster's rect, device pixels, window coordinates
|
|
@@ -402,6 +512,14 @@ export class SpriteLayers {
|
|
|
402
512
|
sprite: null,
|
|
403
513
|
extent: null,
|
|
404
514
|
order: null,
|
|
515
|
+
// what shows itself on the layer, for a part with a source
|
|
516
|
+
contents,
|
|
517
|
+
lift:
|
|
518
|
+
contents === null
|
|
519
|
+
? null
|
|
520
|
+
: contents instanceof CocoaPlayer
|
|
521
|
+
? new PlayerLift(this.native, contents, layer)
|
|
522
|
+
: new VideoLift(this.native, contents, layer),
|
|
405
523
|
};
|
|
406
524
|
}
|
|
407
525
|
|
|
@@ -409,11 +527,14 @@ export class SpriteLayers {
|
|
|
409
527
|
* forgotten, and the bitmap under where it could be repaints with the
|
|
410
528
|
* element drawing it again. */
|
|
411
529
|
_drop(state, root) {
|
|
530
|
+
state.lift?.release();
|
|
412
531
|
this.native.removeFromSuperlayer(state.box ?? state.layer);
|
|
413
532
|
state.raster.release(this.native);
|
|
414
533
|
for (const run of state.runs.values()) this._ends().delete(run.caId);
|
|
415
534
|
state.runs.clear();
|
|
416
|
-
|
|
535
|
+
// a child's is under its parent's layer, which paints it again or goes
|
|
536
|
+
// with it
|
|
537
|
+
if (!state.host.destroyed && state.extent && !state.parent) {
|
|
417
538
|
this.promotion._claim(root, state.extent);
|
|
418
539
|
}
|
|
419
540
|
}
|
|
@@ -425,39 +546,62 @@ export class SpriteLayers {
|
|
|
425
546
|
const rect = rasterRectOf(sprite);
|
|
426
547
|
const reach = sprite.reach ?? sprite.rect;
|
|
427
548
|
const phase = `${reach.x - Math.floor(reach.x)},${reach.y - Math.floor(reach.y)}`;
|
|
549
|
+
// the parts lifted inside it, which its raster leaves out
|
|
550
|
+
const kids = state.kids.map((kid) => kid.key).join('\n');
|
|
428
551
|
// painted again for a new version, and for a raster of another size or
|
|
429
|
-
// with the part at another fraction of a pixel
|
|
430
|
-
// is the layer's alone
|
|
552
|
+
// with the part at another fraction of a pixel, or with other parts
|
|
553
|
+
// lifted inside it; a move by whole pixels is the layer's alone. A
|
|
554
|
+
// source paints nothing: it shows itself.
|
|
431
555
|
if (
|
|
432
|
-
state.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
556
|
+
!state.lift &&
|
|
557
|
+
(state.painted === UNPAINTED ||
|
|
558
|
+
sprite.version !== state.painted ||
|
|
559
|
+
state.rect?.width !== rect.width ||
|
|
560
|
+
state.rect?.height !== rect.height ||
|
|
561
|
+
state.phase !== phase ||
|
|
562
|
+
state.paintedKids !== kids)
|
|
437
563
|
) {
|
|
438
564
|
this._paint(state, sprite, rect);
|
|
439
565
|
state.phase = phase;
|
|
566
|
+
state.paintedKids = kids;
|
|
440
567
|
}
|
|
441
568
|
state.rect = rect;
|
|
442
569
|
const origin = originOf(sprite);
|
|
443
570
|
const m = sprite.transform ?? IDENTITY;
|
|
571
|
+
// where its layer's coordinates start: the window's corner, or the
|
|
572
|
+
// corner of its parent's raster, for a part inside another's layer
|
|
573
|
+
const base = state.parent?.rect ?? ORIGIN;
|
|
444
574
|
// in its box, where it is cut to a clip: placed from the clip's corner,
|
|
445
575
|
// and the box at the part's place among the layers
|
|
446
576
|
const clip = state.clip;
|
|
447
577
|
if (state.box) {
|
|
448
|
-
const frame = [
|
|
578
|
+
const frame = [
|
|
579
|
+
(clip.x - base.x) / s,
|
|
580
|
+
(clip.y - base.y) / s,
|
|
581
|
+
clip.width / s,
|
|
582
|
+
clip.height / s,
|
|
583
|
+
];
|
|
584
|
+
// its corners rounded where the clip's are: a circle's arc, as wide
|
|
585
|
+
// as the box allows
|
|
586
|
+
const cornerRadius =
|
|
587
|
+
Math.min(state.radius, clip.width / 2, clip.height / 2) / s;
|
|
449
588
|
const was = state.boxProps;
|
|
450
|
-
if (
|
|
589
|
+
if (
|
|
590
|
+
!sameMatrix(was.frame, frame) ||
|
|
591
|
+
was.zPosition !== z ||
|
|
592
|
+
was.cornerRadius !== cornerRadius
|
|
593
|
+
) {
|
|
451
594
|
this.native.setLayerProps(state.box, {
|
|
452
595
|
frame,
|
|
453
596
|
masksToBounds: true,
|
|
597
|
+
cornerRadius,
|
|
454
598
|
zPosition: z,
|
|
455
599
|
});
|
|
456
|
-
state.boxProps = { frame, zPosition: z };
|
|
600
|
+
state.boxProps = { frame, zPosition: z, cornerRadius };
|
|
457
601
|
}
|
|
458
602
|
}
|
|
459
|
-
const ox = state.box ? clip.x :
|
|
460
|
-
const oy = state.box ? clip.y :
|
|
603
|
+
const ox = state.box ? clip.x : base.x;
|
|
604
|
+
const oy = state.box ? clip.y : base.y;
|
|
461
605
|
const next = {
|
|
462
606
|
bounds: [0, 0, rect.width / s, rect.height / s],
|
|
463
607
|
anchorPoint: [
|
|
@@ -490,8 +634,14 @@ export class SpriteLayers {
|
|
|
490
634
|
}
|
|
491
635
|
if (any) this.native.setLayerProps(state.layer, out);
|
|
492
636
|
state.props = next;
|
|
637
|
+
// a source that fills the layer with a layer of its own
|
|
638
|
+
state.lift?.layout?.(next.bounds);
|
|
493
639
|
// after the model went out, in the same transaction
|
|
494
640
|
this._syncAnimations(state, sprite);
|
|
641
|
+
// the parts inside it, in the element's order, over its raster
|
|
642
|
+
for (let i = 0; i < state.kids.length; i++) {
|
|
643
|
+
this.sync(state.kids[i], i + 1);
|
|
644
|
+
}
|
|
495
645
|
}
|
|
496
646
|
|
|
497
647
|
_paint(state, sprite, rect) {
|
|
@@ -508,7 +658,10 @@ export class SpriteLayers {
|
|
|
508
658
|
ctx.translate(-rect.x, -rect.y);
|
|
509
659
|
// a part paints whole: there is no pass to cull it by
|
|
510
660
|
if (root) root._paintDamage = null;
|
|
511
|
-
sprite.paint(
|
|
661
|
+
sprite.paint(
|
|
662
|
+
ctx,
|
|
663
|
+
state.kids.length ? new Set(state.kids.map((kid) => kid.key)) : NO_KIDS,
|
|
664
|
+
);
|
|
512
665
|
} catch (err) {
|
|
513
666
|
this._warn(
|
|
514
667
|
state.host,
|
|
@@ -595,6 +748,7 @@ export class SpriteLayers {
|
|
|
595
748
|
destroy() {
|
|
596
749
|
for (const lifted of this.hosts.values()) {
|
|
597
750
|
for (const state of lifted.values()) {
|
|
751
|
+
state.lift?.release();
|
|
598
752
|
this.native.removeFromSuperlayer(state.box ?? state.layer);
|
|
599
753
|
state.raster.release(this.native);
|
|
600
754
|
for (const run of state.runs.values()) this._ends().delete(run.caId);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// A video's frames on a layer of their own (docs/architecture/video.md §7):
|
|
2
|
+
// what the surface presenter's sprites (src/cocoa/sprites.js) do with a part
|
|
3
|
+
// whose `contents` is a `VideoFrames` sink instead of a `paint`.
|
|
4
|
+
//
|
|
5
|
+
// Every frame goes into a video surface — an IOSurface the render server
|
|
6
|
+
// scans out as it is, NV12 for a YCbCr sink and BGRA for a BGRA one
|
|
7
|
+
// (@windowkit/appkit's createVideoSurface) — and the layer is repointed at
|
|
8
|
+
// it, inside the `push` that delivered it: a layer change of its own, with
|
|
9
|
+
// no react-x11 frame, no paint and nothing for the frame pacer to price. At
|
|
10
|
+
// 1080p that is the copy in (0.07ms for NV12, 0.27ms for an I420 frame
|
|
11
|
+
// interleaved on the way) and a transaction.
|
|
12
|
+
//
|
|
13
|
+
// The surfaces are a ring, because a surface the render server is still
|
|
14
|
+
// reading — the one on screen, and in threaded mode the one a queued frame
|
|
15
|
+
// has not taken off the layer yet — is torn by a write. A frame goes into
|
|
16
|
+
// one `videoSurfaceIsInUse` says nothing reads, a new one is made while the
|
|
17
|
+
// ring is short, and with every one of them busy the frame waits for the
|
|
18
|
+
// next push or a short retry: the newest frame is the frame, and a dropped
|
|
19
|
+
// one is the cheapest thing to lose.
|
|
20
|
+
//
|
|
21
|
+
// The verbs are asked for by name (`canLiftVideo`), and a bridge without
|
|
22
|
+
// them lifts nothing: the part is declined and the element draws its frames,
|
|
23
|
+
// which is what it does on every other backend.
|
|
24
|
+
|
|
25
|
+
// Two would do in pump mode — one on screen, one being written — and a
|
|
26
|
+
// worker's queued flip holds a third.
|
|
27
|
+
const RING_MAX = 4;
|
|
28
|
+
// With the whole ring busy, how soon to try the newest frame again: about a
|
|
29
|
+
// display frame, since that is how often the render server lets one go.
|
|
30
|
+
const RETRY_MS = 8;
|
|
31
|
+
|
|
32
|
+
/** Has `native` the verbs a lifted video needs? */
|
|
33
|
+
export function canLiftVideo(native) {
|
|
34
|
+
return (
|
|
35
|
+
typeof native?.createVideoSurface === 'function' &&
|
|
36
|
+
typeof native.writeVideoSurface === 'function' &&
|
|
37
|
+
typeof native.videoSurfaceIsInUse === 'function' &&
|
|
38
|
+
typeof native.setLayerContentsIOSurface === 'function'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A sink's frames on `layer`, from the frame it holds now until `release()`.
|
|
44
|
+
* Placing the layer — its bounds, its place among the others, the box that
|
|
45
|
+
* cuts it to a clip — is the presenter's; this only ever sets what it shows.
|
|
46
|
+
*/
|
|
47
|
+
export class VideoLift {
|
|
48
|
+
constructor(native, sink, layer) {
|
|
49
|
+
this.native = native;
|
|
50
|
+
this.sink = sink;
|
|
51
|
+
this.layer = layer;
|
|
52
|
+
this.ring = []; // { handle, id }
|
|
53
|
+
this.current = null;
|
|
54
|
+
this.shown = 0; // the version on the layer
|
|
55
|
+
this.released = false;
|
|
56
|
+
this._retry = null;
|
|
57
|
+
this._unsubscribe = sink._subscribe((frame) => this._show(frame));
|
|
58
|
+
if (sink.frame) this._show(sink.frame);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A surface nothing reads, or a new one while the ring is short. */
|
|
62
|
+
_free() {
|
|
63
|
+
const native = this.native;
|
|
64
|
+
for (const s of this.ring) {
|
|
65
|
+
if (s !== this.current && !native.videoSurfaceIsInUse(s.handle)) {
|
|
66
|
+
return s;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (this.ring.length >= RING_MAX) return null;
|
|
70
|
+
const sink = this.sink;
|
|
71
|
+
const made = native.createVideoSurface(sink.width, sink.height, {
|
|
72
|
+
// a YCbCr frame stays YCbCr, I420 interleaved by the write; the
|
|
73
|
+
// render server converts it, in the colour the sink says it is
|
|
74
|
+
format: sink.format === 'BGRA' ? 'BGRA' : 'NV12',
|
|
75
|
+
colorSpace: sink.colorSpace,
|
|
76
|
+
range: sink.range,
|
|
77
|
+
});
|
|
78
|
+
const s = { handle: made.handle, id: made.iosurfaceId };
|
|
79
|
+
this.ring.push(s);
|
|
80
|
+
return s;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_show(frame) {
|
|
84
|
+
if (this.released || frame === null) return;
|
|
85
|
+
const s = this._free();
|
|
86
|
+
if (!s) {
|
|
87
|
+
this._again();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.native.writeVideoSurface(s.handle, this.sink.format, frame.planes, {
|
|
91
|
+
strides: frame.strides,
|
|
92
|
+
});
|
|
93
|
+
// its own transaction, actions off, in pump mode; a command of its own
|
|
94
|
+
// from a worker
|
|
95
|
+
this.native.setLayerContentsIOSurface(this.layer, s.id);
|
|
96
|
+
this.current = s;
|
|
97
|
+
this.shown = frame.version;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_again() {
|
|
101
|
+
if (this._retry) return;
|
|
102
|
+
this._retry = setTimeout(() => {
|
|
103
|
+
this._retry = null;
|
|
104
|
+
const frame = this.sink.frame;
|
|
105
|
+
if (frame && frame.version !== this.shown) this._show(frame);
|
|
106
|
+
}, RETRY_MS);
|
|
107
|
+
this._retry.unref?.();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Off the layer: no more frames, and the surfaces freed — the layer
|
|
111
|
+
* holds the one it shows until the presenter takes it away. */
|
|
112
|
+
release() {
|
|
113
|
+
if (this.released) return;
|
|
114
|
+
this.released = true;
|
|
115
|
+
this._unsubscribe();
|
|
116
|
+
if (this._retry) clearTimeout(this._retry);
|
|
117
|
+
this._retry = null;
|
|
118
|
+
for (const s of this.ring) this.native.releaseVideoSurface?.(s.handle);
|
|
119
|
+
this.ring = [];
|
|
120
|
+
this.current = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/host.js
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export * from './types/tray.js';
|
|
|
32
32
|
export * from './types/permissions.js';
|
|
33
33
|
export * from './types/notifications.js';
|
|
34
34
|
export * from './types/desktopcalendar.js';
|
|
35
|
+
export * from './types/video.js';
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* The XID of the X11 window a ref points at, or `null` if there is not one
|
|
@@ -220,9 +221,21 @@ export function useClipboard(): Clipboard;
|
|
|
220
221
|
* above everything the X server draws: nothing can be drawn over one there.
|
|
221
222
|
* Ask it before handing a surface its HUD. A property of the backend and the
|
|
222
223
|
* display, settled before the first render.
|
|
224
|
+
*
|
|
225
|
+
* `'mediaPlayback'` is whether this display plays a file or URL itself — a
|
|
226
|
+
* platform player, AVFoundation on macOS over a bridge with its player verbs
|
|
227
|
+
* — which `<video src>` needs. False on X11 and Wayland for good; ask it
|
|
228
|
+
* before rendering a `<video src>`, which refuses with one `onError` and
|
|
229
|
+
* shows its poster where it is false. `<video frames>` needs no capability.
|
|
230
|
+
* A property of the backend, and it never changes.
|
|
223
231
|
*/
|
|
224
232
|
export type SupportsFeature =
|
|
225
|
-
|
|
233
|
+
| 'transparency'
|
|
234
|
+
| 'shaders'
|
|
235
|
+
| 'nativeControls'
|
|
236
|
+
| 'embedding'
|
|
237
|
+
| 'glOverlay'
|
|
238
|
+
| 'mediaPlayback';
|
|
226
239
|
|
|
227
240
|
/**
|
|
228
241
|
* Can this **display** do something? `'transparency'` is true when the
|
package/src/index.js
CHANGED
|
@@ -75,6 +75,13 @@ export {
|
|
|
75
75
|
export { useDesktopCalendarEvents } from './desktopcalendarhooks.js';
|
|
76
76
|
export { parseUriList } from './transfer.js';
|
|
77
77
|
export { useApp, useClipboard, useSupports } from './appcontext.js';
|
|
78
|
+
export {
|
|
79
|
+
createVideoFrames,
|
|
80
|
+
VIDEO_FORMATS,
|
|
81
|
+
VideoFrames,
|
|
82
|
+
} from './videoframes.js';
|
|
83
|
+
export { useVideoFrames } from './videohooks.js';
|
|
84
|
+
export { NoMediaPlaybackError } from './mediaplayback.js';
|
|
78
85
|
export { BusUnavailableError, closeBus, sessionBus, systemBus } from './bus.js';
|
|
79
86
|
export { announce } from './a11y.js';
|
|
80
87
|
// the standard Undo/Cut/Copy/Paste menu, for an element that edits or
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Whether this display plays a file or URL itself — `<video src>` — and the
|
|
2
|
+
// error a `<video src>` reports where it does not.
|
|
3
|
+
//
|
|
4
|
+
// `<video>` asks before it opens anything, and `useSupports('mediaPlayback')`
|
|
5
|
+
// asks for a component deciding what to render. One function for both, so
|
|
6
|
+
// the two cannot disagree (#531's rule, src/embedding.js's shape). A feature
|
|
7
|
+
// test on the app, not a question about which backend this is: the answer is
|
|
8
|
+
// yes where the app can make a player — the Cocoa app over a bridge with
|
|
9
|
+
// AVFoundation's player verbs — and no on X11 and Wayland for good, where no
|
|
10
|
+
// display-server player exists and the application decodes into `frames`.
|
|
11
|
+
//
|
|
12
|
+
// A leaf module on purpose: it imports nothing of ours, so both
|
|
13
|
+
// `appcontext.js` and the node layer can use it.
|
|
14
|
+
|
|
15
|
+
/** Can `app` play a `<video src>` with a platform player? */
|
|
16
|
+
export function canPlayMedia(app) {
|
|
17
|
+
return typeof app?.createPlayer === 'function';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `<video src>` where this backend has no platform player — the error
|
|
22
|
+
* `onError` is handed, so an application can tell it from a file that would
|
|
23
|
+
* not play.
|
|
24
|
+
*/
|
|
25
|
+
export class NoMediaPlaybackError extends Error {
|
|
26
|
+
constructor() {
|
|
27
|
+
super(
|
|
28
|
+
'react-x11: <video src> needs a platform media player, and this ' +
|
|
29
|
+
'backend has none — so the poster is shown instead. Ask ' +
|
|
30
|
+
"useSupports('mediaPlayback') before rendering one, and give a " +
|
|
31
|
+
'backend without a player frames you decode yourself: <video ' +
|
|
32
|
+
'frames={useVideoFrames(…)}> works everywhere (docs/elements.md#video).',
|
|
33
|
+
);
|
|
34
|
+
this.name = 'NoMediaPlaybackError';
|
|
35
|
+
this.code = 'ENOMEDIAPLAYBACK';
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/node.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
MouseEvent,
|
|
25
25
|
WheelEvent,
|
|
26
26
|
} from './types/events.js';
|
|
27
|
+
import type { VideoFrames } from './types/video.js';
|
|
27
28
|
|
|
28
29
|
/** A gradient, as `createLinearGradient` answers with. */
|
|
29
30
|
export interface CanvasGradientLike {
|
|
@@ -289,9 +290,31 @@ export interface Sprite {
|
|
|
289
290
|
* every ancestor with square corners (a rounded one has to hold all of
|
|
290
291
|
* it). */
|
|
291
292
|
clip?: Rect;
|
|
293
|
+
/** The radius of `clip`'s corners, in device pixels: a circular arc at
|
|
294
|
+
* each of the four, as a card with a `borderRadius` cuts what it holds.
|
|
295
|
+
* 0 when left out. A clipping ancestor with square corners that would
|
|
296
|
+
* cut the clip again keeps the part with its element, since one box
|
|
297
|
+
* cannot take both. */
|
|
298
|
+
clipRadius?: number;
|
|
292
299
|
/** Draws the part where it is in the window, as `paint` would, but at
|
|
293
|
-
* opacity 1 and untransformed
|
|
294
|
-
|
|
300
|
+
* opacity 1 and untransformed — and without the parts lifted inside it
|
|
301
|
+
* (`parent`), whose keys the presenter hands it as `children`. A part
|
|
302
|
+
* with `contents` has none. */
|
|
303
|
+
paint?(ctx: Context2D, children?: ReadonlySet<string>): void;
|
|
304
|
+
/** The key of another of the element's parts this one is inside, earlier
|
|
305
|
+
* in the list: its layer goes in that part's layer, fading, turning and
|
|
306
|
+
* cut with it, and is lifted only with it, and only where everywhere it
|
|
307
|
+
* can be is inside that part's raster. Its `rect`, `origin` and `clip`
|
|
308
|
+
* are the window's, with its parent untransformed. */
|
|
309
|
+
parent?: string;
|
|
310
|
+
/**
|
|
311
|
+
* A source that shows itself on the part's layer instead of a paint — a
|
|
312
|
+
* `VideoFrames` sink, whose every push reaches the layer with no frame of
|
|
313
|
+
* the window's own. `rect` is the picture, on whole pixels; a presenter
|
|
314
|
+
* that cannot show the source declines the part, and the element draws
|
|
315
|
+
* the current frame itself (docs/extending.md).
|
|
316
|
+
*/
|
|
317
|
+
contents?: VideoFrames;
|
|
295
318
|
/** Compared with `===` from frame to frame; a change paints the part
|
|
296
319
|
* again. Left out, it is painted once. */
|
|
297
320
|
version?: unknown;
|