react-x11 2.32.0 → 2.33.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 +97 -18
- 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 +17 -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/package.json
CHANGED
package/src/Reconciler.js
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from './nodes/scope.js';
|
|
34
34
|
import { TextNode, TextChunkNode, setTextStripBelow } from './nodes/text.js';
|
|
35
35
|
import { ImageNode } from './nodes/image.js';
|
|
36
|
+
import { VideoNode } from './nodes/video.js';
|
|
36
37
|
import { CanvasNode } from './nodes/canvas.js';
|
|
37
38
|
import { TextInputNode } from './nodes/textinput.js';
|
|
38
39
|
import { TextAreaNode } from './nodes/textarea.js';
|
|
@@ -102,6 +103,7 @@ export const HOST_TYPES = [
|
|
|
102
103
|
'box',
|
|
103
104
|
'text',
|
|
104
105
|
'image',
|
|
106
|
+
'video',
|
|
105
107
|
'canvas',
|
|
106
108
|
'textinput',
|
|
107
109
|
'textarea',
|
|
@@ -316,6 +318,9 @@ const HostConfig = {
|
|
|
316
318
|
case 'image':
|
|
317
319
|
node = new ImageNode(props, rootContainer);
|
|
318
320
|
break;
|
|
321
|
+
case 'video':
|
|
322
|
+
node = new VideoNode(props, rootContainer);
|
|
323
|
+
break;
|
|
319
324
|
case 'canvas':
|
|
320
325
|
node = new CanvasNode(props, rootContainer);
|
|
321
326
|
break;
|
package/src/a11y.js
CHANGED
|
@@ -123,6 +123,7 @@ export const ATSPI_ROLE = Object.freeze({
|
|
|
123
123
|
INFO_BAR: 102,
|
|
124
124
|
LEVEL_BAR: 103,
|
|
125
125
|
BLOCK_QUOTE: 105,
|
|
126
|
+
VIDEO: 107,
|
|
126
127
|
ARTICLE: 109,
|
|
127
128
|
LANDMARK: 110,
|
|
128
129
|
LOG: 111,
|
|
@@ -307,6 +308,9 @@ const KIND_ROLES = {
|
|
|
307
308
|
box: ATSPI_ROLE.FILLER,
|
|
308
309
|
text: ATSPI_ROLE.LABEL,
|
|
309
310
|
image: ATSPI_ROLE.IMAGE,
|
|
311
|
+
// AT-SPI's own role for one, which Chromium and Firefox give `<video>` too;
|
|
312
|
+
// ARIA has none, so there is no web role name to write in its place
|
|
313
|
+
video: ATSPI_ROLE.VIDEO,
|
|
310
314
|
canvas: ATSPI_ROLE.DRAWING_AREA,
|
|
311
315
|
textinput: ATSPI_ROLE.ENTRY,
|
|
312
316
|
textarea: ATSPI_ROLE.ENTRY,
|
package/src/appcontext.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
watchCompositing,
|
|
28
28
|
} from './compositing.js';
|
|
29
29
|
import { canEmbed } from './embedding.js';
|
|
30
|
+
import { canPlayMedia } from './mediaplayback.js';
|
|
30
31
|
import { hasDirectGL, watchDirectGL } from './glbackend.js';
|
|
31
32
|
import { canOverlay } from './gloverlay.js';
|
|
32
33
|
|
|
@@ -59,7 +60,7 @@ export function useApp() {
|
|
|
59
60
|
return app;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
// 'nativeControls', 'embedding' and '
|
|
63
|
+
// 'nativeControls', 'embedding', 'glOverlay' and 'mediaPlayback' are properties of the
|
|
63
64
|
// backend — and 'glOverlay' of the X server too, which `createRoot` asks
|
|
64
65
|
// before the first render (src/gloverlay.js) — decided before anything
|
|
65
66
|
// renders and never changing after, so their subscription has nothing to
|
|
@@ -81,6 +82,7 @@ const FEATURES = {
|
|
|
81
82
|
},
|
|
82
83
|
embedding: { watch: NEVER_CHANGES, read: canEmbed },
|
|
83
84
|
glOverlay: { watch: NEVER_CHANGES, read: canOverlay },
|
|
85
|
+
mediaPlayback: { watch: NEVER_CHANGES, read: canPlayMedia },
|
|
84
86
|
// Everything here is a property of the **display**: what the server, the
|
|
85
87
|
// compositor and the drawing pipeline can do. A desktop's own surfaces --
|
|
86
88
|
// a tray, a launcher icon and what hangs off it, a notification daemon --
|
|
@@ -183,6 +185,19 @@ const FEATURES = {
|
|
|
183
185
|
*
|
|
184
186
|
* A property of the backend and the display, settled before the first
|
|
185
187
|
* render, and it never changes.
|
|
188
|
+
*
|
|
189
|
+
* `'mediaPlayback'` is true when this display plays a file or URL itself —
|
|
190
|
+
* a platform player, AVFoundation on macOS — which is what `<video src>`
|
|
191
|
+
* needs. It is false on X11 and Wayland for good, where no display-server
|
|
192
|
+
* player exists, and the question to ask before rendering a `<video src>`,
|
|
193
|
+
* which refuses with one `onError` and shows its poster where the answer is
|
|
194
|
+
* no. `<video frames>`, a sink the application decodes into, needs no
|
|
195
|
+
* capability: it works everywhere, at a cost that differs.
|
|
196
|
+
*
|
|
197
|
+
* ```jsx
|
|
198
|
+
* const playback = useSupports('mediaPlayback');
|
|
199
|
+
* {playback ? <video src={file} autoPlay /> : <video frames={decoded} />}
|
|
200
|
+
* ```
|
|
186
201
|
*/
|
|
187
202
|
export function useSupports(feature) {
|
|
188
203
|
const app = useApp();
|
package/src/cocoa/app.js
CHANGED
|
@@ -39,6 +39,7 @@ import { CocoaColorSampler } from './screencolor.js';
|
|
|
39
39
|
import { CocoaFilePanels } from './filepanels.js';
|
|
40
40
|
import { CocoaFontManager } from './fonts.js';
|
|
41
41
|
import { releaseImageUpload } from '../backend/context2d.js';
|
|
42
|
+
import { CocoaPlayer, canPlay } from './player.js';
|
|
42
43
|
import { CocoaSurface } from './surface.js';
|
|
43
44
|
import { CocoaSymbols } from './symbols.js';
|
|
44
45
|
import { CocoaWindow, FRAME_SLACK_MS } from './window.js';
|
|
@@ -217,6 +218,16 @@ export class CocoaApp {
|
|
|
217
218
|
// The tray items (src/cocoa/statusitem.js), by the bridge's handle —
|
|
218
219
|
// which is what a click event names them by.
|
|
219
220
|
this._statusItems = new Map();
|
|
221
|
+
// `<video src>`'s players (src/cocoa/player.js), by the bridge's id — what
|
|
222
|
+
// a `player-*` event names them by. `createPlayer` is there only over a
|
|
223
|
+
// bridge that plays (@windowkit/appkit's player verbs): its presence is
|
|
224
|
+
// what `useSupports('mediaPlayback')` and `<video src>` ask
|
|
225
|
+
// (src/mediaplayback.js), so an app over an older bridge, or a test's
|
|
226
|
+
// fake without them, refuses `src` the way X11 does.
|
|
227
|
+
this._players = new Map();
|
|
228
|
+
if (canPlay(native)) {
|
|
229
|
+
this.createPlayer = (src, options) => new CocoaPlayer(this, src, options);
|
|
230
|
+
}
|
|
220
231
|
|
|
221
232
|
// 'surface' (the measured default) or 'layers' — the retained CALayer
|
|
222
233
|
// presenter, opt-in while docs/macos.md's measure-first gate is open.
|
|
@@ -245,7 +256,7 @@ export class CocoaApp {
|
|
|
245
256
|
// the app's own bridge, so an app over a fake one (the tests) needs no
|
|
246
257
|
// real bridge on the machine — the manager's default loads it only when
|
|
247
258
|
// it is built standalone
|
|
248
|
-
this.fonts = new CocoaFontManager(native);
|
|
259
|
+
this.fonts = new CocoaFontManager(native, { scale: this.scale });
|
|
249
260
|
// `<image src={{ symbol }}>`'s names are SF Symbols here (src/symbols.js)
|
|
250
261
|
this.symbols = new CocoaSymbols(native);
|
|
251
262
|
|
|
@@ -598,6 +609,42 @@ export class CocoaApp {
|
|
|
598
609
|
return new CocoaSurface(this, options);
|
|
599
610
|
}
|
|
600
611
|
|
|
612
|
+
/**
|
|
613
|
+
* The formats a `VideoFrames` sink is cheapest in here, first to last
|
|
614
|
+
* (`preferredFormats`, src/videoframes.js). Over a bridge with video
|
|
615
|
+
* surfaces a YCbCr frame goes onto a layer as it is and into a bitmap
|
|
616
|
+
* through the bridge's own conversion, so NV12 — what a hardware decoder
|
|
617
|
+
* emits — is first; without them every frame is converted in JavaScript on
|
|
618
|
+
* its way to a `putImageData`, and BGRA is the one that costs least.
|
|
619
|
+
*/
|
|
620
|
+
videoFormats() {
|
|
621
|
+
if (typeof this._native.writeVideoSurface !== 'function') return ['BGRA'];
|
|
622
|
+
return ['NV12', 'I420', 'BGRA'];
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* A `<video>`'s frame into `surface` — its drawn presentation, a
|
|
627
|
+
* `CocoaSurface` at the frame's size — in the bridge's code: a copy for
|
|
628
|
+
* BGRA, and for YCbCr a conversion that keeps the colours Core Animation
|
|
629
|
+
* shows the same frame in on a layer (`writeVideoSurface`), so a video
|
|
630
|
+
* moving between the two does not change shade. False over a bridge
|
|
631
|
+
* without the verb, and the frame is converted in JavaScript instead
|
|
632
|
+
* (src/nodes/video.js).
|
|
633
|
+
*/
|
|
634
|
+
writeVideoFrame(surface, sink, frame) {
|
|
635
|
+
const native = this._native;
|
|
636
|
+
const handle = surface?._surfaceHandle;
|
|
637
|
+
if (typeof native.writeVideoSurface !== 'function' || !handle) return false;
|
|
638
|
+
native.writeVideoSurface(handle, sink.format, frame.planes, {
|
|
639
|
+
strides: frame.strides,
|
|
640
|
+
width: sink.width,
|
|
641
|
+
height: sink.height,
|
|
642
|
+
colorSpace: sink.colorSpace,
|
|
643
|
+
range: sink.range,
|
|
644
|
+
});
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
|
|
601
648
|
/**
|
|
602
649
|
* The release seam for an `Image`'s upload (`freeImage`, src/imagesource.js).
|
|
603
650
|
* `ctx.drawImage(image)` here composites from a CG bitmap made for the
|
|
@@ -1332,6 +1379,12 @@ export class CocoaApp {
|
|
|
1332
1379
|
return this._window(ev)?._created(ev);
|
|
1333
1380
|
case 'surface-released':
|
|
1334
1381
|
return this._routeSurfaceReleased(ev);
|
|
1382
|
+
case 'player-metadata':
|
|
1383
|
+
case 'player-state':
|
|
1384
|
+
case 'player-time':
|
|
1385
|
+
case 'player-ended':
|
|
1386
|
+
case 'player-error':
|
|
1387
|
+
return this._players.get(ev.id)?._event(ev);
|
|
1335
1388
|
default:
|
|
1336
1389
|
return undefined;
|
|
1337
1390
|
}
|
package/src/cocoa/fonts.js
CHANGED
|
@@ -735,9 +735,25 @@ function spacedFeatures(features, spacing) {
|
|
|
735
735
|
const KEPT_TYPESETTER_UNITS = 1 << 20;
|
|
736
736
|
|
|
737
737
|
export class CocoaFontManager {
|
|
738
|
-
/**
|
|
739
|
-
|
|
738
|
+
/**
|
|
739
|
+
* @param native the @windowkit/appkit module; the tests hand in a fake
|
|
740
|
+
* @param options.scale the app's backing scale. A size this engine is
|
|
741
|
+
* handed is in device pixels, and CoreText reads some of a face's data
|
|
742
|
+
* by point size — San Francisco's optical size and tracking, Apple Color
|
|
743
|
+
* Emoji's `trak` — so a font made at the device size is the face as it
|
|
744
|
+
* is set at twice the size on a 2x display: a 13px label 8% narrower
|
|
745
|
+
* than AppKit sets it, a 19px emoji 1em wide where it is 23pt at 19pt.
|
|
746
|
+
* The bridge makes a font at `size / scale` points under a matrix of
|
|
747
|
+
* `scale` instead (windowkit/appkit#106), and answers in device pixels
|
|
748
|
+
* as before. A bridge older than that ignores the scale and sets the
|
|
749
|
+
* text as it always did.
|
|
750
|
+
*/
|
|
751
|
+
constructor(native = loadNative(), { scale = 1 } = {}) {
|
|
740
752
|
this._native = native;
|
|
753
|
+
// the trailing argument cgFontWithSize and fontByPostScriptName take,
|
|
754
|
+
// and nothing at 1x, so a 1x app asks the bridge what it always asked
|
|
755
|
+
this._scale = scale > 0 && Number.isFinite(scale) ? scale : 1;
|
|
756
|
+
this._scaleArgs = this._scale === 1 ? [] : [this._scale];
|
|
741
757
|
this._fonts = new Map(); // family|weight|italic|size -> handle
|
|
742
758
|
// handle -> features key -> the handle with those features set
|
|
743
759
|
this._featured = new WeakMap();
|
|
@@ -895,8 +911,15 @@ export class CocoaFontManager {
|
|
|
895
911
|
const sizedKey = `${key}|${size}|${variations ? JSON.stringify(variations) : ''}`;
|
|
896
912
|
let handle = this._sized.get(sizedKey);
|
|
897
913
|
if (handle) return handle;
|
|
898
|
-
if (entry.ps)
|
|
899
|
-
|
|
914
|
+
if (entry.ps) {
|
|
915
|
+
handle = this._native.fontByPostScriptName(
|
|
916
|
+
entry.ps,
|
|
917
|
+
size,
|
|
918
|
+
...this._scaleArgs,
|
|
919
|
+
);
|
|
920
|
+
} else if (entry.cg) {
|
|
921
|
+
handle = this._native.cgFontWithSize(entry.cg, size, ...this._scaleArgs);
|
|
922
|
+
}
|
|
900
923
|
if (!handle) return null;
|
|
901
924
|
handle = this._withVariations(handle, variations);
|
|
902
925
|
this._sized.set(sizedKey, handle);
|
|
@@ -1020,7 +1043,7 @@ export class CocoaFontManager {
|
|
|
1020
1043
|
best = face;
|
|
1021
1044
|
}
|
|
1022
1045
|
}
|
|
1023
|
-
handle = this._native.cgFontWithSize(best.cg, size);
|
|
1046
|
+
handle = this._native.cgFontWithSize(best.cg, size, ...this._scaleArgs);
|
|
1024
1047
|
break;
|
|
1025
1048
|
}
|
|
1026
1049
|
handle ??= this._native.matchFont({
|
|
@@ -1028,6 +1051,7 @@ export class CocoaFontManager {
|
|
|
1028
1051
|
size,
|
|
1029
1052
|
weight,
|
|
1030
1053
|
italic,
|
|
1054
|
+
...(this._scale === 1 ? null : { scale: this._scale }),
|
|
1031
1055
|
});
|
|
1032
1056
|
this._fonts.set(key, handle);
|
|
1033
1057
|
}
|
|
@@ -1115,7 +1139,7 @@ export class CocoaFontManager {
|
|
|
1115
1139
|
_registeredFace(reg) {
|
|
1116
1140
|
if (!reg.face) {
|
|
1117
1141
|
reg.face = new CocoaFace(this, 'registered', (size) =>
|
|
1118
|
-
this._native.cgFontWithSize(reg.cg, size),
|
|
1142
|
+
this._native.cgFontWithSize(reg.cg, size, ...this._scaleArgs),
|
|
1119
1143
|
);
|
|
1120
1144
|
reg.face.key = `registered:${reg.face.postscriptName}`;
|
|
1121
1145
|
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// `<video src>` on macOS (docs/architecture/video.md §3.2, §9): a file or URL
|
|
2
|
+
// played by AVFoundation through @windowkit/appkit's player verbs — the
|
|
3
|
+
// platform's decoder, audio, seeking and HLS — with the picture on the
|
|
4
|
+
// player's own AVPlayerLayer.
|
|
5
|
+
//
|
|
6
|
+
// The player is what a `<video>`'s sprite part shows as its `contents`, the
|
|
7
|
+
// same way a `VideoFrames` sink is: lifted (src/cocoa/sprites.js), the
|
|
8
|
+
// AVPlayerLayer goes inside the part's layer and fills it, and AVFoundation
|
|
9
|
+
// puts every frame on the screen with nothing of ours in the way; declined,
|
|
10
|
+
// the element draws the frame showing, which `copyFrame` converts into a
|
|
11
|
+
// surface of its own in the colours the layer shows it in. Everything else —
|
|
12
|
+
// state, time, the end, failure — comes back as the bridge's `player-*`
|
|
13
|
+
// events, routed here by id (`CocoaApp._route`).
|
|
14
|
+
|
|
15
|
+
/** Has `native` the verbs a player needs? */
|
|
16
|
+
export function canPlay(native) {
|
|
17
|
+
return (
|
|
18
|
+
typeof native?.createPlayer === 'function' &&
|
|
19
|
+
typeof native.playerSet === 'function' &&
|
|
20
|
+
typeof native.playerSeek === 'function' &&
|
|
21
|
+
typeof native.playerCopyFrame === 'function' &&
|
|
22
|
+
typeof native.releasePlayer === 'function'
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One file or URL, playing or not. `width`/`height` are the picture's once
|
|
28
|
+
* the item is ready (0 before), `duration` its length in seconds (`Infinity`
|
|
29
|
+
* for a live stream, NaN before it is known).
|
|
30
|
+
*/
|
|
31
|
+
export class CocoaPlayer {
|
|
32
|
+
constructor(app, src, options = {}) {
|
|
33
|
+
this.app = app;
|
|
34
|
+
this._native = app._native;
|
|
35
|
+
const made = this._native.createPlayer(String(src), {
|
|
36
|
+
autoPlay: Boolean(options.autoPlay),
|
|
37
|
+
loop: Boolean(options.loop),
|
|
38
|
+
muted: Boolean(options.muted),
|
|
39
|
+
volume: typeof options.volume === 'number' ? options.volume : 1,
|
|
40
|
+
rate: typeof options.playbackRate === 'number' ? options.playbackRate : 1,
|
|
41
|
+
});
|
|
42
|
+
this.id = made.id;
|
|
43
|
+
/** the AVPlayerLayer, a layer handle */
|
|
44
|
+
this.layer = made.layer;
|
|
45
|
+
this.width = 0;
|
|
46
|
+
this.height = 0;
|
|
47
|
+
this.duration = NaN;
|
|
48
|
+
this.currentTime = 0;
|
|
49
|
+
this.playing = false;
|
|
50
|
+
this.ended = false;
|
|
51
|
+
this.error = null;
|
|
52
|
+
this.released = false;
|
|
53
|
+
this._listeners = new Set();
|
|
54
|
+
app._players.set(this.id, this);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A `player-*` event from the bridge: the state it says, then whoever
|
|
58
|
+
* listens. */
|
|
59
|
+
_event(ev) {
|
|
60
|
+
if (this.released) return;
|
|
61
|
+
switch (ev.type) {
|
|
62
|
+
case 'player-metadata':
|
|
63
|
+
this.width = ev.width;
|
|
64
|
+
this.height = ev.height;
|
|
65
|
+
this.duration = ev.duration;
|
|
66
|
+
break;
|
|
67
|
+
case 'player-state':
|
|
68
|
+
// AVFoundation reports every move of its time control — waiting,
|
|
69
|
+
// playing, a loop's restart — and HTML's `play` and `pause` are the
|
|
70
|
+
// moves between paused and not
|
|
71
|
+
if (Boolean(ev.playing) === this.playing) return;
|
|
72
|
+
this.playing = Boolean(ev.playing);
|
|
73
|
+
if (this.playing) this.ended = false;
|
|
74
|
+
break;
|
|
75
|
+
case 'player-time':
|
|
76
|
+
this.currentTime = ev.currentTime;
|
|
77
|
+
break;
|
|
78
|
+
case 'player-ended':
|
|
79
|
+
this.ended = true;
|
|
80
|
+
this.playing = false;
|
|
81
|
+
break;
|
|
82
|
+
case 'player-error':
|
|
83
|
+
this.error = new Error(
|
|
84
|
+
`react-x11: <video src> could not be played: ${ev.message}`,
|
|
85
|
+
);
|
|
86
|
+
break;
|
|
87
|
+
default:
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for (const fn of [...this._listeners]) fn(ev);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `fn(event)` for every event; answers the unsubscribe. */
|
|
94
|
+
_subscribe(fn) {
|
|
95
|
+
this._listeners.add(fn);
|
|
96
|
+
return () => this._listeners.delete(fn);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** What `paused`, `playbackRate`, `volume`, `muted` and `loop` say now;
|
|
100
|
+
* what is left out stays. */
|
|
101
|
+
set({ paused, playbackRate, volume, muted, loop } = {}) {
|
|
102
|
+
if (this.released) return;
|
|
103
|
+
const settings = {};
|
|
104
|
+
if (typeof paused === 'boolean') settings.paused = paused;
|
|
105
|
+
if (typeof playbackRate === 'number' && playbackRate > 0) {
|
|
106
|
+
settings.rate = playbackRate;
|
|
107
|
+
}
|
|
108
|
+
if (typeof volume === 'number') settings.volume = volume;
|
|
109
|
+
if (typeof muted === 'boolean') settings.muted = muted;
|
|
110
|
+
if (typeof loop === 'boolean') settings.loop = loop;
|
|
111
|
+
this._native.playerSet(this.id, settings);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
play() {
|
|
115
|
+
// HTML plays an ended item from its start
|
|
116
|
+
if (this.ended) this.seek(0);
|
|
117
|
+
this.set({ paused: false });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
pause() {
|
|
121
|
+
this.set({ paused: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
seek(seconds) {
|
|
125
|
+
if (this.released || !Number.isFinite(seconds)) return;
|
|
126
|
+
this.currentTime = Math.max(0, seconds);
|
|
127
|
+
this._native.playerSeek(this.id, this.currentTime);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The frame showing, into `surface` (a `CocoaSurface` of the picture's
|
|
132
|
+
* size): `{ width, height, time, written }`, or null when nothing is newer
|
|
133
|
+
* than the last frame copied. `written` false means the surface is not the
|
|
134
|
+
* frame's size, which the caller remakes.
|
|
135
|
+
*/
|
|
136
|
+
copyFrame(surface) {
|
|
137
|
+
if (this.released || !surface?._surfaceHandle) return null;
|
|
138
|
+
return this._native.playerCopyFrame(this.id, surface._surfaceHandle);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
release() {
|
|
142
|
+
if (this.released) return;
|
|
143
|
+
this.released = true;
|
|
144
|
+
this._listeners.clear();
|
|
145
|
+
this.app._players.delete(this.id);
|
|
146
|
+
this._native.releasePlayer(this.id);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* A player's picture on a sprite part's layer: its AVPlayerLayer inside the
|
|
152
|
+
* part's, filling it. The bridge plays into it on AVFoundation's clock; this
|
|
153
|
+
* only places it, and takes it out again when the part comes down.
|
|
154
|
+
*/
|
|
155
|
+
export class PlayerLift {
|
|
156
|
+
constructor(native, player, layer) {
|
|
157
|
+
this.native = native;
|
|
158
|
+
this.player = player;
|
|
159
|
+
this.layer = layer;
|
|
160
|
+
this._bounds = null;
|
|
161
|
+
native.addSublayer(layer, player.layer);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The part's layer is `bounds` (points) now: the picture fills it. */
|
|
165
|
+
layout(bounds) {
|
|
166
|
+
const was = this._bounds;
|
|
167
|
+
if (was && was[2] === bounds[2] && was[3] === bounds[3]) return;
|
|
168
|
+
this._bounds = bounds;
|
|
169
|
+
this.native.setLayerProps(this.player.layer, {
|
|
170
|
+
frame: [0, 0, bounds[2], bounds[3]],
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
release() {
|
|
175
|
+
this.native.removeFromSuperlayer(this.player.layer);
|
|
176
|
+
}
|
|
177
|
+
}
|
package/src/cocoa/promotion.js
CHANGED
|
@@ -615,6 +615,10 @@ export class CocoaPromotion {
|
|
|
615
615
|
!(parent.style?.borderRadius > 0)
|
|
616
616
|
) {
|
|
617
617
|
const inner = insetRect(parent.abs, borderReach(parent));
|
|
618
|
+
// a clip with round corners cut again is no box's shape
|
|
619
|
+
if (cut.radius > 0 && cut.clip && !containsRect(inner, cut.clip)) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
618
622
|
cut.clip = cut.clip ? intersectRects(cut.clip, inner) : inner;
|
|
619
623
|
bounds = cut.clip && intersectRects(bounds, cut.clip);
|
|
620
624
|
if (!bounds) return false;
|