react-x11 2.52.0 → 2.54.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/README.md +10 -10
- package/docs/README.md +160 -314
- package/docs/accessibility.md +176 -229
- package/docs/appearance.md +77 -176
- package/docs/click-to-component.md +18 -51
- package/docs/clipboard.md +40 -59
- package/docs/components.md +518 -1036
- package/docs/dbus.md +61 -133
- package/docs/debugging.md +152 -245
- package/docs/desktop-calendar.md +51 -88
- package/docs/desktop.md +222 -347
- package/docs/devtools.md +50 -167
- package/docs/drag-and-drop.md +126 -204
- package/docs/ecosystem/animation.md +91 -125
- package/docs/ecosystem/data-fetching.md +38 -53
- package/docs/ecosystem/dev-tooling.md +159 -202
- package/docs/ecosystem/forms.md +61 -97
- package/docs/ecosystem/headless.md +64 -89
- package/docs/ecosystem/i18n.md +18 -21
- package/docs/ecosystem/icons.md +93 -126
- package/docs/ecosystem/layout.md +42 -52
- package/docs/ecosystem/routing.md +18 -29
- package/docs/ecosystem/state.md +71 -88
- package/docs/ecosystem/testing.md +131 -157
- package/docs/ecosystem/theming.md +119 -165
- package/docs/ecosystem.md +145 -179
- package/docs/elements.md +1096 -1774
- package/docs/embedding.md +83 -132
- package/docs/events.md +412 -695
- package/docs/extending.md +784 -1326
- package/docs/eyedropper.md +63 -122
- package/docs/filedialog.md +71 -107
- package/docs/frame.md +195 -339
- package/docs/gl.md +65 -62
- package/docs/globalmenu.md +63 -146
- package/docs/macos.md +531 -3234
- package/docs/notifications.md +33 -47
- package/docs/packaging.md +166 -309
- package/docs/permissions.md +9 -14
- package/docs/react-features.md +81 -106
- package/docs/remote.md +32 -76
- package/docs/scale.md +72 -186
- package/docs/styling.md +329 -547
- package/docs/system.md +200 -280
- package/docs/testing.md +63 -83
- package/docs/typescript.md +6 -48
- package/docs/uri-schemes.md +67 -107
- package/docs/wayland-backend.md +200 -459
- package/docs/windows-integrations.md +75 -131
- package/docs/windows.md +343 -1367
- package/package.json +3 -2
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/dnd.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/imagedecode-worker.js +32 -0
- package/src/imagedecode.js +140 -2
- package/src/layouts.js +1 -1
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/ntk.d.ts +12 -0
- package/src/pacing.js +1 -1
- package/src/refresh/index.js +2 -1
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +27 -0
- package/src/wayland/video.js +376 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
- package/docs/architecture/accessibility.md +0 -470
- package/docs/architecture/animation.md +0 -707
- package/docs/architecture/container-queries.md +0 -525
- package/docs/architecture/custom-layout.md +0 -334
- package/docs/architecture/drag-and-drop.md +0 -941
- package/docs/architecture/element-layer-contents.md +0 -622
- package/docs/architecture/frame-pacing.md +0 -255
- package/docs/architecture/grid-layout.md +0 -358
- package/docs/architecture/protocol-efficiency.md +0 -357
- package/docs/architecture/video.md +0 -1149
- package/docs/architecture/windowed-regions.md +0 -854
- package/docs/glx.md +0 -137
- package/docs/wayland.md +0 -545
- package/docs/windows-embedding.md +0 -271
- package/docs/windows-gl.md +0 -221
package/src/nodes/video.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// <video>: frames an application decodes (`frames`, a VideoFrames sink), or a
|
|
2
2
|
// file or URL the platform plays (`src`, where there is a player), fitted
|
|
3
|
-
// into the node's box (
|
|
3
|
+
// into the node's box (design/architecture/video.md §6–7).
|
|
4
4
|
//
|
|
5
5
|
// A drawn node like `<image>`: it joins yoga, it is in the paint order and it
|
|
6
6
|
// is hit like anything else — which is the whole point. A video in a form
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
// - **drawn** — everywhere else, and in every frame the presenter declines
|
|
19
19
|
// the part (something is painted over it, it fades, a rounded card does
|
|
20
20
|
// not hold it): the newest frame is copied into a surface of the node's
|
|
21
|
-
// own at paint, natively where the backend can (`
|
|
22
|
-
// through `frameToRGBA` where it cannot, and
|
|
23
|
-
// rect like an `<image>` whose pixels
|
|
21
|
+
// own at paint, natively where the backend can (`writeVideoFrame`,
|
|
22
|
+
// src/videobackend.js) and through `frameToRGBA` where it cannot, and
|
|
23
|
+
// composited at the picture's rect like an `<image>` whose pixels
|
|
24
|
+
// changed. A push claims the picture.
|
|
24
25
|
//
|
|
25
26
|
// `src` needs a platform player, which this backend has when
|
|
26
27
|
// `useSupports('mediaPlayback')` says so — the app's `createPlayer`, on macOS
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
} from '../imagesource.js';
|
|
42
43
|
import { canPlayMedia, NoMediaPlaybackError } from '../mediaplayback.js';
|
|
43
44
|
import { Surface } from '../offscreen.js';
|
|
45
|
+
import { videoBackendOf } from '../videobackend.js';
|
|
44
46
|
import { frameToRGBA, isVideoFrames } from '../videoframes.js';
|
|
45
47
|
import { fitRect, objectFitOf } from './fit.js';
|
|
46
48
|
import { intrinsicSize } from './layout.js';
|
|
@@ -592,10 +594,18 @@ export class VideoNode extends Node {
|
|
|
592
594
|
}
|
|
593
595
|
|
|
594
596
|
_upload(surface, sink, frame) {
|
|
595
|
-
// natively where the backend converts in its own code
|
|
596
|
-
// bridge's writeVideoSurface
|
|
597
|
-
// colour-matched to what a layer shows)
|
|
598
|
-
|
|
597
|
+
// natively where the backend converts in its own code: the Cocoa
|
|
598
|
+
// bridge's writeVideoSurface (a copy for BGRA, VideoToolbox for YCbCr,
|
|
599
|
+
// colour-matched to what a layer shows), the Wayland context's shader,
|
|
600
|
+
// an Xv port on X11 (src/videobackend.js)
|
|
601
|
+
if (
|
|
602
|
+
videoBackendOf(this.app)?.writeVideoFrame?.(surface, sink, frame, this)
|
|
603
|
+
) {
|
|
604
|
+
// a frame converted in JavaScript earlier — the first, on X11, before
|
|
605
|
+
// the window was mapped — left a buffer the native path does not need
|
|
606
|
+
this._rgba = null;
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
599
609
|
const w = sink.width;
|
|
600
610
|
const h = sink.height;
|
|
601
611
|
if (this._rgba?.width !== w || this._rgba?.height !== h) {
|
|
@@ -632,6 +642,7 @@ export class VideoNode extends Node {
|
|
|
632
642
|
this._surface = null;
|
|
633
643
|
this._rgba = null;
|
|
634
644
|
this._drawn = -1;
|
|
645
|
+
if (surface) videoBackendOf(this.app)?.releaseVideoFrame?.(surface);
|
|
635
646
|
try {
|
|
636
647
|
surface?.destroy();
|
|
637
648
|
} catch {
|
package/src/ntk.d.ts
CHANGED
|
@@ -115,8 +115,20 @@ export interface DecodedImage {
|
|
|
115
115
|
*/
|
|
116
116
|
export function decodeImageBytes(
|
|
117
117
|
bytes: Uint8Array,
|
|
118
|
+
options?: DecodeImageOptions,
|
|
118
119
|
): DecodedImage | Promise<DecodedImage>;
|
|
119
120
|
|
|
121
|
+
/** How `decodeImageBytes` decodes. */
|
|
122
|
+
export interface DecodeImageOptions {
|
|
123
|
+
/**
|
|
124
|
+
* Decode on a worker thread where the runtime decodes in JavaScript (Node),
|
|
125
|
+
* for bytes long enough to pay for the trip: the answer is then a promise
|
|
126
|
+
* for a PNG and a JPEG too. Under Bun every decode is off the thread
|
|
127
|
+
* already.
|
|
128
|
+
*/
|
|
129
|
+
offThread?: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
120
132
|
/** `code` values on a failed GL setup — see `<glarea onError>`. */
|
|
121
133
|
export const GLXError: {
|
|
122
134
|
NO_EXTENSION: 'GLX_NO_EXTENSION';
|
package/src/pacing.js
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
// Off by default: `'display'` is the built-in, and paints every frame the
|
|
50
50
|
// clock gives, after React's own batching — a UI answers its input as
|
|
51
51
|
// quickly as it can unless it says otherwise. `'adaptive'` is what a window
|
|
52
|
-
// that streams asks for.
|
|
52
|
+
// that streams asks for. design/architecture/frame-pacing.md is the account.
|
|
53
53
|
|
|
54
54
|
export const DEFAULT_FRAME_RATE = 'display';
|
|
55
55
|
|
package/src/refresh/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// react-x11/refresh — the runtime half of state-preserving hot reload
|
|
2
2
|
// (Fast Refresh). The loader half is ./loader.js; `react-x11/refresh/register`
|
|
3
|
-
// wires both up for `node --import`. See docs/ecosystem/dev-tooling.md
|
|
3
|
+
// wires both up for `node --import`. See docs/ecosystem/dev-tooling.md, and
|
|
4
|
+
// design/ecosystem/dev-tooling.md §"How it reloads" for the loader's rules.
|
|
4
5
|
//
|
|
5
6
|
// Import order is what this module exists to own:
|
|
6
7
|
//
|
package/src/types/video.d.ts
CHANGED
|
@@ -69,8 +69,11 @@ export class VideoFrames {
|
|
|
69
69
|
readonly colorSpace: VideoColorSpace;
|
|
70
70
|
readonly range: VideoRange;
|
|
71
71
|
/** This display's formats, cheapest first: what a decoder that can choose
|
|
72
|
-
* should emit (`ffmpeg -pix_fmt nv12`). NV12 leads
|
|
73
|
-
*
|
|
72
|
+
* should emit (`ffmpeg -pix_fmt nv12`). NV12 leads wherever something
|
|
73
|
+
* native takes it as it is — a layer on macOS, a shader on Wayland, an Xv
|
|
74
|
+
* port on X11 — and BGRA where a frame is converted in JavaScript. On X11
|
|
75
|
+
* it is BGRA, and not kept, until the port has answered, a few round
|
|
76
|
+
* trips after the first sink is made. */
|
|
74
77
|
readonly preferredFormats: readonly VideoFormat[];
|
|
75
78
|
/** Frames pushed so far. */
|
|
76
79
|
readonly version: number;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Who writes a `<video>`'s frames natively on an app, if anyone — the seam
|
|
2
|
+
// `VideoFrames.preferredFormats` and the drawn presentation ask through
|
|
3
|
+
// (design/architecture/video.md §6.3).
|
|
4
|
+
//
|
|
5
|
+
// The app itself where its backend has the verbs: the Cocoa app over the
|
|
6
|
+
// bridge's video surfaces, the Wayland app over its own GL context. On X11
|
|
7
|
+
// the app is ntk's, which knows nothing of sinks, so the answer is the Xv
|
|
8
|
+
// writer react-x11 keeps for the connection (src/xvideo.js). A feature test,
|
|
9
|
+
// not a question about which backend this is — `typeof app.xv` is an ntk
|
|
10
|
+
// with XVideo discovery. Null where nothing will, and every frame is
|
|
11
|
+
// converted in JavaScript (`frameToRGBA`, src/videoframes.js).
|
|
12
|
+
import { xvVideoFor } from './xvideo.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `{ videoFormats(), writeVideoFrame(surface, sink, frame, node),
|
|
16
|
+
* releaseVideoFrame?(surface), prepare?(sink) }` for `app`, or null.
|
|
17
|
+
*/
|
|
18
|
+
export function videoBackendOf(app) {
|
|
19
|
+
if (!app) return null;
|
|
20
|
+
if (
|
|
21
|
+
typeof app.writeVideoFrame === 'function' ||
|
|
22
|
+
typeof app.videoFormats === 'function'
|
|
23
|
+
) {
|
|
24
|
+
return app;
|
|
25
|
+
}
|
|
26
|
+
if (typeof app.xv === 'function') return xvVideoFor(app);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
package/src/videoframes.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// `VideoFrames`: a sink an application pushes decoded video frames into,
|
|
2
|
-
// and a `<video frames>` shows (
|
|
2
|
+
// and a `<video frames>` shows (design/architecture/video.md §6.3).
|
|
3
3
|
//
|
|
4
4
|
// react-x11 decodes nothing. Where the bytes come from — an `ffmpeg` child
|
|
5
5
|
// writing rawvideo to a pipe, a WASM decoder, an addon over libmpv — is the
|
|
@@ -33,12 +33,16 @@
|
|
|
33
33
|
// a decoder that can choose (`ffmpeg -pix_fmt`); one that cannot still
|
|
34
34
|
// works, with the conversion charged where it happens.
|
|
35
35
|
//
|
|
36
|
-
//
|
|
37
|
-
// (
|
|
38
|
-
//
|
|
36
|
+
// No node and no backend: the one question asked of the display is its
|
|
37
|
+
// formats, through `videoBackendOf` (src/videobackend.js) — the Cocoa app's
|
|
38
|
+
// answer from the bridge, the Wayland app's own, and on X11 the Xv writer's,
|
|
39
|
+
// which a sink starts asking for when it is made.
|
|
40
|
+
|
|
41
|
+
import { videoBackendOf } from './videobackend.js';
|
|
39
42
|
|
|
40
43
|
export const VIDEO_FORMATS = Object.freeze(['NV12', 'I420', 'BGRA']);
|
|
41
44
|
const COLOR_SPACES = ['bt709', 'bt601', 'bt2020'];
|
|
45
|
+
const BGRA_ONLY = Object.freeze(['BGRA']);
|
|
42
46
|
const RANGES = ['video', 'full'];
|
|
43
47
|
// The largest side a frame may have: a surface's limit on every backend, and
|
|
44
48
|
// a number past which a size is a bug rather than a video.
|
|
@@ -209,22 +213,30 @@ export class VideoFrames {
|
|
|
209
213
|
this._frame = null;
|
|
210
214
|
this._listeners = new Set();
|
|
211
215
|
this._preferred = null;
|
|
216
|
+
// On X11 the formats are a port's, and asking for one is round trips:
|
|
217
|
+
// started here, so they are known by the time a decoder that waits for
|
|
218
|
+
// anything at all — a probe of the file, a spawn — reads them.
|
|
219
|
+
videoBackendOf(app)?.prepare?.(this);
|
|
212
220
|
}
|
|
213
221
|
|
|
214
222
|
/**
|
|
215
223
|
* This display's formats, cheapest first: what a decoder that can choose
|
|
216
|
-
* should emit.
|
|
217
|
-
*
|
|
224
|
+
* should emit. YCbCr first wherever something native takes it as it is —
|
|
225
|
+
* a layer on Cocoa, a shader on Wayland, an Xv port on X11 — and BGRA
|
|
226
|
+
* first where a frame is converted in JavaScript on its way to a 2D
|
|
227
|
+
* context. On X11 the answer is BGRA until the port has answered, a few
|
|
228
|
+
* round trips after the first sink is made, and is not kept until then.
|
|
218
229
|
*/
|
|
219
230
|
get preferredFormats() {
|
|
220
|
-
if (this._preferred
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
231
|
+
if (this._preferred !== null) return this._preferred;
|
|
232
|
+
const asked = videoBackendOf(this.app)?.videoFormats?.();
|
|
233
|
+
// null: still being asked — answer for now, and ask again next time
|
|
234
|
+
if (asked === null) return BGRA_ONLY;
|
|
235
|
+
this._preferred = Object.freeze(
|
|
236
|
+
Array.isArray(asked) && asked.length !== 0
|
|
237
|
+
? asked.filter((f) => VIDEO_FORMATS.includes(f))
|
|
238
|
+
: ['BGRA'],
|
|
239
|
+
);
|
|
228
240
|
return this._preferred;
|
|
229
241
|
}
|
|
230
242
|
|
|
@@ -281,8 +293,9 @@ export const isVideoFrames = (v) => v instanceof VideoFrames;
|
|
|
281
293
|
|
|
282
294
|
// --- converting a frame where nothing native will ----------------------------
|
|
283
295
|
//
|
|
284
|
-
// The drawn path
|
|
285
|
-
// Cocoa bridge older than its video surfaces
|
|
296
|
+
// The drawn path where nothing native writes the frame (win32, an X server
|
|
297
|
+
// with no Xv adaptor, a Cocoa bridge older than its video surfaces, and on
|
|
298
|
+
// any backend a frame the native writer declines): the frame converted here to
|
|
286
299
|
// the straight RGBA `putImageData` takes. Not free — two million pixels a
|
|
287
300
|
// frame at 1080p — which is why `preferredFormats` says BGRA first there.
|
|
288
301
|
// The matrix and the range are the frame's; no transfer function is
|
package/src/wayland/app.js
CHANGED
|
@@ -30,6 +30,7 @@ import { releaseDevice } from './device.js';
|
|
|
30
30
|
import { WaylandSeat } from './seat.js';
|
|
31
31
|
import { WaylandBackendWindow } from './backendwindow.js';
|
|
32
32
|
import { WaylandSurface } from './surface.js';
|
|
33
|
+
import { releaseVideoFrame, writeVideoFrame } from './video.js';
|
|
33
34
|
import { InputRouter } from './input.js';
|
|
34
35
|
import { WaylandTouch } from './touch.js';
|
|
35
36
|
import { WaylandTablet } from './tablet.js';
|
|
@@ -440,6 +441,32 @@ export class WaylandApp extends EventEmitter {
|
|
|
440
441
|
return new WaylandSurface(this, options);
|
|
441
442
|
}
|
|
442
443
|
|
|
444
|
+
/**
|
|
445
|
+
* The formats a `VideoFrames` sink is cheapest in here
|
|
446
|
+
* (`preferredFormats`, src/videoframes.js): all three, YCbCr first. Every
|
|
447
|
+
* one goes up as textures as it is and is converted by a shader
|
|
448
|
+
* (src/wayland/video.js), and NV12 or I420 is less than half of BGRA's
|
|
449
|
+
* bytes on the way.
|
|
450
|
+
*/
|
|
451
|
+
videoFormats() {
|
|
452
|
+
return ['NV12', 'I420', 'BGRA'];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* A `<video>`'s frame into `surface`, its drawn presentation, on the GPU:
|
|
457
|
+
* planes as textures, the matrix in a fragment shader. False where the
|
|
458
|
+
* frame cannot be — a size past the GPU's textures — and the node
|
|
459
|
+
* converts it in JavaScript instead (src/nodes/video.js).
|
|
460
|
+
*/
|
|
461
|
+
writeVideoFrame(surface, sink, frame) {
|
|
462
|
+
return writeVideoFrame(this, surface, sink, frame);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** The node let `surface` go: its plane textures go too. */
|
|
466
|
+
releaseVideoFrame(surface) {
|
|
467
|
+
releaseVideoFrame(this, surface);
|
|
468
|
+
}
|
|
469
|
+
|
|
443
470
|
/**
|
|
444
471
|
* What the machine can do, as a promise like the X11 and Cocoa answers
|
|
445
472
|
* (src/glbackend.js chains on it): here the answer was settled when the
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// `<video frames>` on Wayland: a frame's planes go up as textures as the
|
|
2
|
+
// decoder made them, and the colour conversion is a fragment shader drawing
|
|
3
|
+
// into the `<video>`'s own surface (design/architecture/video.md §5).
|
|
4
|
+
//
|
|
5
|
+
// The backend's 2D context is already a GLES rasterizer, so this is the GLX
|
|
6
|
+
// transport of §2.3 made native: no conversion in JavaScript for any format,
|
|
7
|
+
// and NV12 and I420 cross into the GPU at 1.5 bytes a pixel. Y is an R8
|
|
8
|
+
// texture; NV12's chroma one RG8 texture at half size, I420's two R8 ones;
|
|
9
|
+
// BGRA one RGBA8 texture read back to front. The shader applies the sink's
|
|
10
|
+
// matrix and range exactly as `frameToRGBA` does — no transfer function,
|
|
11
|
+
// because nothing else on this backend is colour-managed either — so a frame
|
|
12
|
+
// drawn here and one converted in JavaScript are the same picture, chroma
|
|
13
|
+
// apart: sampled bilinearly here, as a player does, and by nearest there.
|
|
14
|
+
//
|
|
15
|
+
// What it writes into is the surface the drawn presentation already keeps
|
|
16
|
+
// (src/nodes/video.js): a render target, which the window's context then
|
|
17
|
+
// composites at the picture's rect like any `drawImage`. Nothing lifts on
|
|
18
|
+
// Wayland — no subsurfaces, by the backend's own design — so this is the
|
|
19
|
+
// whole presentation.
|
|
20
|
+
//
|
|
21
|
+
// The GL is shared with every 2D context on the app (device.js): whatever
|
|
22
|
+
// the device's owner still has buffered is drawn before the target is
|
|
23
|
+
// written, the shader's vertex state lives in a vertex array of its own,
|
|
24
|
+
// and the device is released afterwards so the next context to draw puts
|
|
25
|
+
// its own state back. `UNPACK_ROW_LENGTH`, which a decoder's padded rows
|
|
26
|
+
// need, is set back to 0 for the uploads that assume it.
|
|
27
|
+
|
|
28
|
+
import { flushDevice, releaseDevice } from './device.js';
|
|
29
|
+
|
|
30
|
+
const VERT = `
|
|
31
|
+
attribute vec2 aPos;
|
|
32
|
+
attribute vec2 aUV;
|
|
33
|
+
varying vec2 vUV;
|
|
34
|
+
void main() {
|
|
35
|
+
vUV = aUV;
|
|
36
|
+
gl_Position = vec4(aPos, 0.0, 1.0);
|
|
37
|
+
}`;
|
|
38
|
+
|
|
39
|
+
const FRAG = `
|
|
40
|
+
#ifdef GL_FRAGMENT_PRECISION_HIGH
|
|
41
|
+
precision highp float;
|
|
42
|
+
#else
|
|
43
|
+
precision mediump float;
|
|
44
|
+
#endif
|
|
45
|
+
varying vec2 vUV;
|
|
46
|
+
uniform int uLayout;
|
|
47
|
+
uniform sampler2D uY;
|
|
48
|
+
uniform sampler2D uC;
|
|
49
|
+
uniform sampler2D uV;
|
|
50
|
+
uniform vec2 uChroma;
|
|
51
|
+
uniform mat3 uMatrix;
|
|
52
|
+
uniform vec3 uOffset;
|
|
53
|
+
void main() {
|
|
54
|
+
if (uLayout == 0) {
|
|
55
|
+
// BGRA bytes uploaded as RGBA: blue is in r. Opaque, whatever the
|
|
56
|
+
// fourth byte held.
|
|
57
|
+
gl_FragColor = vec4(texture2D(uY, vUV).bgr, 1.0);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
float y = texture2D(uY, vUV).r;
|
|
61
|
+
vec2 cuv = vUV * uChroma;
|
|
62
|
+
vec2 c = uLayout == 1
|
|
63
|
+
? texture2D(uC, cuv).rg
|
|
64
|
+
: vec2(texture2D(uC, cuv).r, texture2D(uV, cuv).r);
|
|
65
|
+
vec3 rgb = uMatrix * (vec3(y, c) - uOffset);
|
|
66
|
+
gl_FragColor = vec4(clamp(rgb, 0.0, 1.0), 1.0);
|
|
67
|
+
}`;
|
|
68
|
+
|
|
69
|
+
const LAYOUT = { BGRA: 0, NV12: 1, I420: 2 };
|
|
70
|
+
|
|
71
|
+
// Kr and Kb, as src/videoframes.js has them
|
|
72
|
+
const MATRIX = {
|
|
73
|
+
bt709: [0.2126, 0.0722],
|
|
74
|
+
bt601: [0.299, 0.114],
|
|
75
|
+
bt2020: [0.2627, 0.0593],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const matrices = new Map();
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The column-major mat3 and the offset that take normalized Y'CbCr to
|
|
82
|
+
* R'G'B' for a matrix and a range — the same numbers `frameToRGBA`'s tables
|
|
83
|
+
* are made of, in units of 1 rather than of 255.
|
|
84
|
+
*/
|
|
85
|
+
export function videoMatrix(colorSpace, range) {
|
|
86
|
+
const key = `${colorSpace}/${range}`;
|
|
87
|
+
let m = matrices.get(key);
|
|
88
|
+
if (m) return m;
|
|
89
|
+
const [kr, kb] = MATRIX[colorSpace];
|
|
90
|
+
const kg = 1 - kr - kb;
|
|
91
|
+
const full = range === 'full';
|
|
92
|
+
const ys = full ? 1 : 255 / 219;
|
|
93
|
+
const cs = full ? 1 : 255 / 224;
|
|
94
|
+
m = {
|
|
95
|
+
matrix: Float32Array.of(
|
|
96
|
+
// the column Y' multiplies
|
|
97
|
+
ys,
|
|
98
|
+
ys,
|
|
99
|
+
ys,
|
|
100
|
+
// Cb
|
|
101
|
+
0,
|
|
102
|
+
-cs * ((2 * kb * (1 - kb)) / kg),
|
|
103
|
+
cs * 2 * (1 - kb),
|
|
104
|
+
// Cr
|
|
105
|
+
cs * 2 * (1 - kr),
|
|
106
|
+
-cs * ((2 * kr * (1 - kr)) / kg),
|
|
107
|
+
0,
|
|
108
|
+
),
|
|
109
|
+
offset: Float32Array.of(full ? 0 : 16 / 255, 128 / 255, 128 / 255),
|
|
110
|
+
};
|
|
111
|
+
matrices.set(key, m);
|
|
112
|
+
return m;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The planes of a frame as textures: format, size and the bytes per texel
|
|
116
|
+
* `UNPACK_ROW_LENGTH` counts a decoder's stride in. */
|
|
117
|
+
function planesOf(gl, format, width, height) {
|
|
118
|
+
const cw = Math.ceil(width / 2);
|
|
119
|
+
const ch = Math.ceil(height / 2);
|
|
120
|
+
const r8 = { internal: gl.R8, format: gl.RED, texel: 1 };
|
|
121
|
+
switch (format) {
|
|
122
|
+
case 'NV12':
|
|
123
|
+
return [
|
|
124
|
+
{ ...r8, width, height },
|
|
125
|
+
{ internal: gl.RG8, format: gl.RG, texel: 2, width: cw, height: ch },
|
|
126
|
+
];
|
|
127
|
+
case 'I420':
|
|
128
|
+
return [
|
|
129
|
+
{ ...r8, width, height },
|
|
130
|
+
{ ...r8, width: cw, height: ch },
|
|
131
|
+
{ ...r8, width: cw, height: ch },
|
|
132
|
+
];
|
|
133
|
+
case 'BGRA':
|
|
134
|
+
return [{ internal: gl.RGBA, format: gl.RGBA, texel: 4, width, height }];
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** One per GL device: the program, and the quad it draws. */
|
|
140
|
+
const programs = new WeakMap();
|
|
141
|
+
|
|
142
|
+
function programFor(gl) {
|
|
143
|
+
let p = programs.get(gl);
|
|
144
|
+
if (p) return p;
|
|
145
|
+
const vs = compile(gl, gl.VERTEX_SHADER, VERT, 'video vertex');
|
|
146
|
+
let fs;
|
|
147
|
+
try {
|
|
148
|
+
fs = compile(gl, gl.FRAGMENT_SHADER, FRAG, 'video fragment');
|
|
149
|
+
} catch (err) {
|
|
150
|
+
gl.deleteShader(vs);
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
const program = gl.createProgram();
|
|
154
|
+
gl.attachShader(program, vs);
|
|
155
|
+
gl.attachShader(program, fs);
|
|
156
|
+
gl.linkProgram(program);
|
|
157
|
+
const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
|
|
158
|
+
const log = linked ? '' : gl.getProgramInfoLog(program);
|
|
159
|
+
gl.deleteShader(vs);
|
|
160
|
+
gl.deleteShader(fs);
|
|
161
|
+
if (!linked) {
|
|
162
|
+
gl.deleteProgram(program);
|
|
163
|
+
throw new Error(`video program failed to link: ${log}`);
|
|
164
|
+
}
|
|
165
|
+
const u = (name) => gl.getUniformLocation(program, name);
|
|
166
|
+
const aPos = gl.getAttribLocation(program, 'aPos');
|
|
167
|
+
const aUV = gl.getAttribLocation(program, 'aUV');
|
|
168
|
+
// A strip over the whole target. The 2D context draws device row 0 at the
|
|
169
|
+
// top of clip space, so the frame's first row — texture row 0 — goes
|
|
170
|
+
// there: v = 0 at clip y = +1.
|
|
171
|
+
const buffer = gl.createBuffer();
|
|
172
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
173
|
+
gl.bufferData(
|
|
174
|
+
gl.ARRAY_BUFFER,
|
|
175
|
+
Float32Array.of(-1, 1, 0, 0, -1, -1, 0, 1, 1, 1, 1, 0, 1, -1, 1, 1),
|
|
176
|
+
gl.STATIC_DRAW,
|
|
177
|
+
);
|
|
178
|
+
// its own vertex array, so the 2D contexts' attribute state — on the
|
|
179
|
+
// default one, re-pointed every batch — is never this program's
|
|
180
|
+
const vao = gl.createVertexArray();
|
|
181
|
+
gl.bindVertexArray(vao);
|
|
182
|
+
gl.enableVertexAttribArray(aPos);
|
|
183
|
+
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 16, 0);
|
|
184
|
+
gl.enableVertexAttribArray(aUV);
|
|
185
|
+
gl.vertexAttribPointer(aUV, 2, gl.FLOAT, false, 16, 8);
|
|
186
|
+
gl.bindVertexArray(null);
|
|
187
|
+
p = {
|
|
188
|
+
program,
|
|
189
|
+
buffer,
|
|
190
|
+
vao,
|
|
191
|
+
maxSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
|
|
192
|
+
loc: {
|
|
193
|
+
uLayout: u('uLayout'),
|
|
194
|
+
uY: u('uY'),
|
|
195
|
+
uC: u('uC'),
|
|
196
|
+
uV: u('uV'),
|
|
197
|
+
uChroma: u('uChroma'),
|
|
198
|
+
uMatrix: u('uMatrix'),
|
|
199
|
+
uOffset: u('uOffset'),
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
programs.set(gl, p);
|
|
203
|
+
return p;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Each surface's plane textures, kept across frames of one size. */
|
|
207
|
+
const uploads = new WeakMap();
|
|
208
|
+
|
|
209
|
+
function texturesFor(gl, surface, format, width, height) {
|
|
210
|
+
let kept = uploads.get(surface);
|
|
211
|
+
if (
|
|
212
|
+
kept &&
|
|
213
|
+
kept.format === format &&
|
|
214
|
+
kept.width === width &&
|
|
215
|
+
kept.height === height
|
|
216
|
+
) {
|
|
217
|
+
return kept;
|
|
218
|
+
}
|
|
219
|
+
if (kept) deleteTextures(gl, kept);
|
|
220
|
+
const planes = planesOf(gl, format, width, height);
|
|
221
|
+
kept = {
|
|
222
|
+
format,
|
|
223
|
+
width,
|
|
224
|
+
height,
|
|
225
|
+
planes,
|
|
226
|
+
textures: planes.map(() => gl.createTexture()),
|
|
227
|
+
fresh: true,
|
|
228
|
+
};
|
|
229
|
+
uploads.set(surface, kept);
|
|
230
|
+
return kept;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function deleteTextures(gl, kept) {
|
|
234
|
+
for (const tex of kept.textures) gl.deleteTexture(tex);
|
|
235
|
+
kept.textures = [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* A plane's rows at a stride `UNPACK_ROW_LENGTH` cannot say — one that is
|
|
240
|
+
* not a whole number of texels — packed tight. Every decoder pads to whole
|
|
241
|
+
* pixels, so this is the case that should never come.
|
|
242
|
+
*/
|
|
243
|
+
function tight(data, stride, rowBytes, rows) {
|
|
244
|
+
const out = new Uint8Array(rowBytes * rows);
|
|
245
|
+
for (let r = 0; r < rows; r++) {
|
|
246
|
+
out.set(data.subarray(r * stride, r * stride + rowBytes), r * rowBytes);
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* `frame` from `sink` into `surface` (a WaylandSurface at the frame's size).
|
|
253
|
+
* False — and the frame is converted in JavaScript instead — where the
|
|
254
|
+
* surface is not one of this backend's, the app has no GL, or the frame is
|
|
255
|
+
* larger than a texture can be.
|
|
256
|
+
*/
|
|
257
|
+
export function writeVideoFrame(app, surface, sink, frame) {
|
|
258
|
+
const gl = app?.gl;
|
|
259
|
+
const target = surface?.target;
|
|
260
|
+
if (!gl || !target?.fbo || target.destroyed) return false;
|
|
261
|
+
if (!(sink.format in LAYOUT)) return false;
|
|
262
|
+
app.makeCurrent();
|
|
263
|
+
const p = programFor(gl);
|
|
264
|
+
if (sink.width > p.maxSize || sink.height > p.maxSize) return false;
|
|
265
|
+
// Anything a context still has buffered is drawn first: one of its draws
|
|
266
|
+
// may read this target, and must read the frame it was given (device.js).
|
|
267
|
+
flushDevice(gl);
|
|
268
|
+
const kept = texturesFor(gl, surface, sink.format, sink.width, sink.height);
|
|
269
|
+
try {
|
|
270
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
271
|
+
kept.planes.forEach((plane, i) => {
|
|
272
|
+
const rowBytes = plane.width * plane.texel;
|
|
273
|
+
const stride = frame.strides[i];
|
|
274
|
+
let data = frame.planes[i];
|
|
275
|
+
let rowLength = 0;
|
|
276
|
+
if (stride !== rowBytes) {
|
|
277
|
+
if (stride % plane.texel === 0) rowLength = stride / plane.texel;
|
|
278
|
+
else data = tight(data, stride, rowBytes, plane.height);
|
|
279
|
+
}
|
|
280
|
+
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, rowLength);
|
|
281
|
+
gl.activeTexture(gl.TEXTURE0 + i);
|
|
282
|
+
gl.bindTexture(gl.TEXTURE_2D, kept.textures[i]);
|
|
283
|
+
if (kept.fresh) {
|
|
284
|
+
// Y and BGRA meet the target texel for texel; chroma is half size,
|
|
285
|
+
// and read between its samples as a player reads it
|
|
286
|
+
const filter = i === 0 ? gl.NEAREST : gl.LINEAR;
|
|
287
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
|
|
288
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
|
|
289
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
290
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
291
|
+
gl.texImage2D(
|
|
292
|
+
gl.TEXTURE_2D,
|
|
293
|
+
0,
|
|
294
|
+
plane.internal,
|
|
295
|
+
plane.width,
|
|
296
|
+
plane.height,
|
|
297
|
+
0,
|
|
298
|
+
plane.format,
|
|
299
|
+
gl.UNSIGNED_BYTE,
|
|
300
|
+
data,
|
|
301
|
+
);
|
|
302
|
+
} else {
|
|
303
|
+
gl.texSubImage2D(
|
|
304
|
+
gl.TEXTURE_2D,
|
|
305
|
+
0,
|
|
306
|
+
0,
|
|
307
|
+
0,
|
|
308
|
+
plane.width,
|
|
309
|
+
plane.height,
|
|
310
|
+
plane.format,
|
|
311
|
+
gl.UNSIGNED_BYTE,
|
|
312
|
+
data,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
kept.fresh = false;
|
|
317
|
+
|
|
318
|
+
target.bind();
|
|
319
|
+
gl.disable(gl.BLEND);
|
|
320
|
+
gl.disable(gl.SCISSOR_TEST);
|
|
321
|
+
gl.disable(gl.STENCIL_TEST);
|
|
322
|
+
gl.disable(gl.DEPTH_TEST);
|
|
323
|
+
gl.disable(gl.CULL_FACE);
|
|
324
|
+
gl.colorMask(true, true, true, true);
|
|
325
|
+
gl.useProgram(p.program);
|
|
326
|
+
const { loc } = p;
|
|
327
|
+
gl.uniform1i(loc.uLayout, LAYOUT[sink.format]);
|
|
328
|
+
gl.uniform1i(loc.uY, 0);
|
|
329
|
+
gl.uniform1i(loc.uC, 1);
|
|
330
|
+
gl.uniform1i(loc.uV, 2);
|
|
331
|
+
// full-size texture coordinates onto a chroma plane rounded up: an odd
|
|
332
|
+
// frame's last chroma column covers one picture column, not two
|
|
333
|
+
gl.uniform2f(
|
|
334
|
+
loc.uChroma,
|
|
335
|
+
sink.width / (2 * Math.ceil(sink.width / 2)),
|
|
336
|
+
sink.height / (2 * Math.ceil(sink.height / 2)),
|
|
337
|
+
);
|
|
338
|
+
const { matrix, offset } = videoMatrix(sink.colorSpace, sink.range);
|
|
339
|
+
gl.uniformMatrix3fv(loc.uMatrix, false, matrix);
|
|
340
|
+
gl.uniform3fv(loc.uOffset, offset);
|
|
341
|
+
gl.bindVertexArray(p.vao);
|
|
342
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
343
|
+
gl.bindVertexArray(null);
|
|
344
|
+
} finally {
|
|
345
|
+
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 0);
|
|
346
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
347
|
+
// the program, the blend and the framebuffer were the 2D contexts' to
|
|
348
|
+
// say: the next one to draw says them again
|
|
349
|
+
releaseDevice(gl);
|
|
350
|
+
app.rebindWindowTarget?.();
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** `surface` is let go: its plane textures with it. */
|
|
356
|
+
export function releaseVideoFrame(app, surface) {
|
|
357
|
+
const kept = uploads.get(surface);
|
|
358
|
+
if (!kept) return;
|
|
359
|
+
uploads.delete(surface);
|
|
360
|
+
const gl = app?.gl;
|
|
361
|
+
if (!gl) return;
|
|
362
|
+
app.makeCurrent();
|
|
363
|
+
deleteTextures(gl, kept);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function compile(gl, type, src, what) {
|
|
367
|
+
const sh = gl.createShader(type);
|
|
368
|
+
gl.shaderSource(sh, src);
|
|
369
|
+
gl.compileShader(sh);
|
|
370
|
+
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
|
371
|
+
const log = gl.getShaderInfoLog(sh);
|
|
372
|
+
gl.deleteShader(sh);
|
|
373
|
+
throw new Error(`${what} shader failed to compile: ${log}`);
|
|
374
|
+
}
|
|
375
|
+
return sh;
|
|
376
|
+
}
|
package/src/win32/a11y.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// property reads — so answering from JS would pace the screen reader by the
|
|
8
8
|
// application's busiest moment. The bridge therefore *pushes*, and
|
|
9
9
|
// `windows/src/uia.cc` answers from its copy. The reasoning is
|
|
10
|
-
//
|
|
10
|
+
// design/windows.md §"Accessibility: UI Automation"; what matters here is the
|
|
11
11
|
// consequence: **this file's job is a diff, not an answer.**
|
|
12
12
|
//
|
|
13
13
|
// Everything it pushes comes from `src/a11y.js`, which is pure functions over
|
package/src/win32/app.js
CHANGED
|
@@ -362,7 +362,7 @@ export class Win32App {
|
|
|
362
362
|
// `embeddable` is "created, not shown; somebody else will place me", and
|
|
363
363
|
// on this backend that cannot be a window: a composition target stops
|
|
364
364
|
// presenting the moment its window becomes a child, so an HWND is not
|
|
365
|
-
// something another process can take (
|
|
365
|
+
// something another process can take (design/windows-embedding.md). What a
|
|
366
366
|
// host can take is the **buffer**, so an embeddable window here is the
|
|
367
367
|
// same shared surface a `<Frame>` pane draws into — whether this process
|
|
368
368
|
// was forked as a pane or is a guest somebody else started.
|
|
@@ -825,7 +825,7 @@ export class Win32App {
|
|
|
825
825
|
break;
|
|
826
826
|
}
|
|
827
827
|
case 'dpichanged':
|
|
828
|
-
// The renderer cannot re-scale a live window yet (
|
|
828
|
+
// The renderer cannot re-scale a live window yet (design/windows.md
|
|
829
829
|
// §Layout, open question 8), so this is recorded and not acted on.
|
|
830
830
|
wnd.emit('statechange', { scale: event.a / 96 });
|
|
831
831
|
break;
|