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
|
@@ -1,1149 +0,0 @@
|
|
|
1
|
-
# Video: decoding a stream, capturing one, and putting the frames on the screen
|
|
2
|
-
|
|
3
|
-
_Design record, 2026-09-08, **reviewed 2026-10-02** against master at 65e323a
|
|
4
|
-
(react-x11 2.30.0, ntk ^8.17.8, `@windowkit/appkit` ^0.19.0,
|
|
5
|
-
`@windowkit/win32` ^0.0.6, `@windowkit/wayland` ^3.1.1, node-x11 4.2.3 in
|
|
6
|
-
the lockfile). §0.2 is what has been built since — track 1 of §12 and the
|
|
7
|
-
Cocoa half of track 2. §0.1 is what
|
|
8
|
-
moved in the month between the two dates and what it changed here; §6 is
|
|
9
|
-
the element, §7 the seam it rides, §8 the two consumers it is for, §12 what
|
|
10
|
-
can be built now and in what order, and §13 the spikes every number came
|
|
11
|
-
from — two of them new, and one of them overturning the conclusion the first
|
|
12
|
-
version of this record drew for macOS. The mechanism it extends is
|
|
13
|
-
[sprites](../extending.md#parts-of-your-drawing-on-layers-of-their-own),
|
|
14
|
-
which shipped where
|
|
15
|
-
[element-owned layer contents](element-layer-contents.md) was designed; the
|
|
16
|
-
constraint it runs into on X11 is the one
|
|
17
|
-
[protocol efficiency](protocol-efficiency.md) measures everywhere else._
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## 0. TL;DR
|
|
22
|
-
|
|
23
|
-
- **No X11 extension decodes video.** The one that did — XvMC — is dead,
|
|
24
|
-
and the things that replaced it (VA-API, VDPAU, NVDEC) are not
|
|
25
|
-
extensions at all but C libraries that borrow a `Display*`. On X11 the
|
|
26
|
-
decoder is always ours, in our process, and X's only job is moving
|
|
27
|
-
finished frames to the screen without a copy per pixel. Nothing about
|
|
28
|
-
that changed, and nothing will.
|
|
29
|
-
- **On Cocoa both halves are OS services.** VideoToolbox decodes in
|
|
30
|
-
hardware into `CVPixelBuffer`s that are already IOSurfaces, and
|
|
31
|
-
`AVPlayerLayer` is a `CALayer` that shows an `AVPlayer`, which takes a
|
|
32
|
-
URL and does the rest.
|
|
33
|
-
Measured: 1.87ms per 1080p H.264 frame, hardware, every frame
|
|
34
|
-
IOSurface-backed (§3.3).
|
|
35
|
-
- **The trap that decided the first version of this record is not a trap
|
|
36
|
-
on screen.** A `CALayer` whose `contents` is a YUV IOSurface rasterizes
|
|
37
|
-
_nothing_ through `render(in:)` — that measurement stands — and shows the
|
|
38
|
-
picture **on screen**, through the render server, for every two-plane
|
|
39
|
-
and packed layout tried: `420v`, `420f` and `2vuy` all render; only the
|
|
40
|
-
three-plane `y420` does not (§3.4, probed today through the window
|
|
41
|
-
server's own capture). So the frame sink on macOS needs no AV layer
|
|
42
|
-
class and no BGRA conversion: a plain layer and an NV12 surface, which
|
|
43
|
-
costs **0.40ms of CPU per 1080p frame** for the copy and the commit
|
|
44
|
-
(§3.4.1). `AVPlayerLayer` is for _sources_ — a URL, with audio, seeking
|
|
45
|
-
and HLS — not a workaround.
|
|
46
|
-
- **The seam this needs shipped, under another name.** #499's
|
|
47
|
-
`presentedSurface()` was never built; what landed is **sprites**
|
|
48
|
-
(#819/#821/#828): a node offers parts of its drawing, the surface
|
|
49
|
-
presenter lifts each onto a layer under promotion's rules — the same
|
|
50
|
-
z-order test, the same clip boxes, the same hole, the same "declining is
|
|
51
|
-
always safe" — and today every part's content is a raster the presenter
|
|
52
|
-
paints by calling the part's own `paint`. Video is one addition to that
|
|
53
|
-
vocabulary: **a part whose content is a source** rather than a paint
|
|
54
|
-
(§7). Everything else about lifting a video is already written and
|
|
55
|
-
tested.
|
|
56
|
-
- **So a cross-backend `<video>` is one element with two sources.**
|
|
57
|
-
`frames`, a sink the application feeds decoded frames into, on every
|
|
58
|
-
backend; and `src`, a URL the platform plays itself, on the backends that
|
|
59
|
-
have a platform player — AVFoundation now, Media Foundation when the
|
|
60
|
-
Windows half lands — refused with a typed error where there is none (§6).
|
|
61
|
-
Presented two ways everywhere: lifted onto a layer when the frame says it
|
|
62
|
-
can be, drawn into the window's bitmap when it cannot.
|
|
63
|
-
- **Two consumers, one element.** A `<video>` in a form is the element.
|
|
64
|
-
A `<video>` inside `@react-x11/components`' `<Html>` is the element too,
|
|
65
|
-
mounted beside the document at the box's rect the way `<Html>` already
|
|
66
|
-
mounts a core `<textinput>` for an `<input>` (§8) — not a second video
|
|
67
|
-
implementation inside the HTML renderer, and not a new seam.
|
|
68
|
-
- **Windows has the pieces and none of the design.** The win32 backend is
|
|
69
|
-
a DirectComposition visual tree per window with `<glarea>` on a visual of
|
|
70
|
-
its own and a pane host that already attaches a surface _from a handle_ —
|
|
71
|
-
which is precisely what `IMFMediaEngine`'s windowless swapchain hands
|
|
72
|
-
out. §4 is the plan and the list of things to probe on a Windows machine
|
|
73
|
-
before writing any of it.
|
|
74
|
-
- **The X11 numbers that set the budget still stand.** A 1080p frame
|
|
75
|
-
through core `PutImage` costs 33ms with a round trip (§2.5); SHM halves
|
|
76
|
-
it and is already in ntk; Xv is now in node-x11 **and in the lockfile**
|
|
77
|
-
(4.2.3), untouched by anything here; XQuartz still answers "no adaptors
|
|
78
|
-
present", so every Xv path still needs the drawn fallback by
|
|
79
|
-
construction.
|
|
80
|
-
|
|
81
|
-
### 0.1 Review, 2026-10-02: what moved
|
|
82
|
-
|
|
83
|
-
| the record said, 2026-09-08 | the tree says, 2026-10-02 | what it changes here |
|
|
84
|
-
| ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
85
|
-
| a video node is an element-owned layer on #499's `presentedSurface()`, which should answer a layer | #499 is closed, nothing of it built; **sprites** shipped instead (`src/nodes/sprites.js`, `src/cocoa/sprites.js`), a per-frame accessor in the shape of `opaqueRect()` whose parts are rasters painted by the presenter | §7: the seam is a sprite part with `contents`, not a new accessor |
|
|
86
|
-
| a `CALayer` draws no YUV IOSurface; use `AVSampleBufferDisplayLayer` or pay 2.1× the decode for BGRA | **on screen it draws every two-plane and packed layout**; the blank was `render(in:)`, a CPU path (§3.4) | the Cocoa frame sink is a plain layer and two bridge verbs, not an AV class; AV verbs are for `src` only |
|
|
87
|
-
| the implementing PR should first verify that the render server scans out a YUV surface | verified: `420v`, `420f`, `2vuy` render; `y420` and `f420` (three planes) do not; colour attachments change the matching | NV12 is the sink's native format on macOS; I420 is interleaved on the way in |
|
|
88
|
-
| the bridge should grow `createVideoLayer`, `enqueueSampleBuffer`, `flushVideoLayer` and a synchroniser (against `@windowkit/appkit` ^0.7.0) | 0.19.0: `createSurfaceIOSurface`, `surfaceFromIOSurfaceID`, `setLayerContentsIOSurface`, `surfaceIsInUse`, a `surface-released` event, `snapshotWindow` through `CGWindowListCreateImage`; still no CoreVideo, CoreMedia, VideoToolbox, AVKit or Metal linked | §9: the verb list, in two releases |
|
|
89
|
-
| whether `snapshotWindow` composites sublayers is open, and decides what the tests can assert | it is the window server's composite, and today's capture holds an `AVPlayerLayer`, an `AVSampleBufferDisplayLayer` and the YUV tiles | pixel tests on Cocoa go through `snapshotWindow`; no `render(in:)` |
|
|
90
|
-
| two backends | four: win32 (`src/win32/`, DirectComposition, `<glarea>` on a visual, a pane host over `CreateSurfaceFromHandle`) and Wayland (`src/wayland/`, dma-buf only, its own GLES rasterizer, no subsurfaces bound) | §4 and §5, new |
|
|
91
|
-
| Xv is a lockfile refresh away | the lockfile has node-x11 4.2.3 (`PutImage`, `ShmPutImage`, `QueryImageAttributes`, `StopVideo`, both notify selectors); ntk still has no Xv wrapper and the renderer calls none of it | the Xv rung is gated on nothing upstream; it is still work |
|
|
92
|
-
| camera capture is a separate element, later | unchanged — and the on-screen result makes both `AVCaptureVideoPreviewLayer` and a raw `2vuy` surface viable layer contents, where the first version doubted both | §3.5's conclusion softens; the element is still later |
|
|
93
|
-
| the ecosystem has nothing to play a video with | `@react-x11/components` has `<MediaPlayer>` (mpv or VLC embedded over `<foreign>`, X11 only, transport over mpv's JSON IPC) and `<Html>` lays a `<video>` out as its poster, or a 300×150 box; neither plays a frame on macOS | §8: what each becomes |
|
|
94
|
-
|
|
95
|
-
### 0.2 Built, 2026-10-02
|
|
96
|
-
|
|
97
|
-
- **Core** (§12 track 1): `VideoFrames` (`src/videoframes.js`,
|
|
98
|
-
`createVideoFrames`, `useVideoFrames`), `<video>` (`src/nodes/video.js`)
|
|
99
|
-
drawn on every backend through a `Surface` of its own, `objectFit` for
|
|
100
|
-
`<video>` and `<image>` (`src/nodes/fit.js`), `poster`, HTML's sizing,
|
|
101
|
-
`onLoadedMetadata`, the typed refusal of `src` (`NoMediaPlaybackError`)
|
|
102
|
-
and `useSupports('mediaPlayback')`, false everywhere until a player lands.
|
|
103
|
-
The JavaScript conversion (`frameToRGBA`) applies the matrix and the
|
|
104
|
-
range and no transfer function.
|
|
105
|
-
- **The `contents` sprite kind** (§7) in `src/cocoa/sprites.js`, and the
|
|
106
|
-
lift in `src/cocoa/video.js`: a ring of up to four video surfaces, each
|
|
107
|
-
push written and the layer repointed in the push. Measured on the real
|
|
108
|
-
bridge, threaded: sixty pushes, one window frame — the lift's.
|
|
109
|
-
- **`@windowkit/appkit` release A** (§9), with the shapes the work settled
|
|
110
|
-
on: `createVideoSurface(width, height, { format, colorSpace, range })`,
|
|
111
|
-
`writeVideoSurface(target, format, planes, { strides, … })` — whose target
|
|
112
|
-
may also be a 2D surface, which is the drawn presentation's whole native
|
|
113
|
-
half — `videoSurfaceIsInUse`, `releaseVideoSurface` and `videoFormats()`.
|
|
114
|
-
The surface holds the IOSurface alone, not the CVPixelBuffer it was made
|
|
115
|
-
through: a pixel buffer keeps a use count on its surface, and
|
|
116
|
-
`IOSurfaceIsInUse` would answer true for one nothing shows.
|
|
117
|
-
`videoSurfacePlanes` was not needed: the copy is 0.07ms at 1080p.
|
|
118
|
-
- **A colour finding the record did not have.** With CoreVideo's tags
|
|
119
|
-
alone, Core Animation shows a surface tagged BT.709 throughout with the
|
|
120
|
-
exact 709 curve — while `AVPlayerLayer`, VideoToolbox's pixel transfer,
|
|
121
|
-
Core Image, and Core Animation itself for any other tagging use Apple's
|
|
122
|
-
1.961 gamma: a video-range Y′=47 was sRGB 51 on such a layer and 40 under
|
|
123
|
-
`AVPlayerLayer` playing the same clip. So §3.4's "colour attachments are
|
|
124
|
-
read" was half the story: the bridge names each video surface's colour
|
|
125
|
-
space as the one CoreVideo makes of its tags, which puts a lifted frame on
|
|
126
|
-
the player's curve exactly, and every drawn frame goes through
|
|
127
|
-
VideoToolbox, about 1ms at 1080p. A frame lifted, drawn, and played by
|
|
128
|
-
`AVPlayerLayer` agree to within two levels on screen (the bridge's
|
|
129
|
-
`test/video-surface.js` and `test/player.js`).
|
|
130
|
-
|
|
131
|
-
Also: the presenter bench's `video` scenario and its gate rule (§9) — a
|
|
132
|
-
lifted sink holds to no window frame per push, a rule read as skipped over a
|
|
133
|
-
bridge without video surfaces. And **`<video src>` on macOS** (§12 track 3):
|
|
134
|
-
`@windowkit/appkit`'s `createPlayer`, `playerSet`, `playerSeek`,
|
|
135
|
-
`playerCopyFrame` and `releasePlayer` with the `player-*` events, and
|
|
136
|
-
`src/cocoa/player.js`, the player as a sprite part's `contents` — its
|
|
137
|
-
`AVPlayerLayer` lifted into the part, its frame copied into the element's
|
|
138
|
-
surface while it plays and something is over it. HTML's props, events and
|
|
139
|
-
imperative half; `useSupports('mediaPlayback')` true where the bridge plays.
|
|
140
|
-
Not built yet: the Windows and Wayland rungs.
|
|
141
|
-
|
|
142
|
-
## 1. The asymmetry, stated once
|
|
143
|
-
|
|
144
|
-
Every other feature in `docs/architecture/` has the same shape on every
|
|
145
|
-
backend: a rule, N implementations, one set of semantics. Video does not,
|
|
146
|
-
and pretending otherwise would produce an API that is a lie on half the
|
|
147
|
-
backends.
|
|
148
|
-
|
|
149
|
-
On macOS, "play this video" is a supported product: hand `AVPlayer` a URL,
|
|
150
|
-
attach `AVPlayerLayer`, and you have hardware decode, HLS, A/V sync,
|
|
151
|
-
seeking, HDR and colour management, none of it ours. On Windows the same
|
|
152
|
-
product is Media Foundation's `IMFMediaEngine`, whose event vocabulary is
|
|
153
|
-
HTML5's media element verbatim. On X11 and Wayland every one of those is a
|
|
154
|
-
component someone assembles — a demuxer, a decoder, a clock, a colour
|
|
155
|
-
conversion, a presentation path — and the display server contributes only
|
|
156
|
-
the last one, badly on X11 unless you leave the protocol for a native
|
|
157
|
-
library, and not at all on Wayland, which is a buffer handoff by design.
|
|
158
|
-
|
|
159
|
-
The design consequence is that the _element_ is shared and the _source_ is
|
|
160
|
-
not. A `<video>` whose only prop is a URL would work beautifully on Cocoa
|
|
161
|
-
and be a lie on X11, where nothing in this project can open a URL and
|
|
162
|
-
produce frames. A `<video>` whose only prop is a frame sink is honest
|
|
163
|
-
everywhere but throws away the whole of AVFoundation. §6 is the resolution:
|
|
164
|
-
both, with the frame sink as the primitive and the URL as the convenience
|
|
165
|
-
of the backends that have a player, asked for with `useSupports` and refused
|
|
166
|
-
with a typed error elsewhere.
|
|
167
|
-
|
|
168
|
-
## 2. X11
|
|
169
|
-
|
|
170
|
-
### 2.1 Xv, which does not decode
|
|
171
|
-
|
|
172
|
-
Xv is the extension people mean when they say "the video extension", and
|
|
173
|
-
the first thing to be clear about is that it is a **presentation**
|
|
174
|
-
extension. It converts colour, it scales, and it puts the result in a
|
|
175
|
-
drawable. It does not decode anything.
|
|
176
|
-
|
|
177
|
-
The model is ports on adaptors. `QueryAdaptors` on a window answers the
|
|
178
|
-
adaptors a screen has; each has ports, a type mask (`ImageMask` is the one
|
|
179
|
-
that matters — an adaptor that accepts images from client memory rather
|
|
180
|
-
than from a capture source), and the visuals it can draw into. You
|
|
181
|
-
`GrabPort` one, ask `ListImageFormats` for the FOURCCs it takes (`YV12`,
|
|
182
|
-
`I420`, `UYVY`, `YUY2`…), ask `QueryImageAttributes` for the plane pitches
|
|
183
|
-
and offsets at your size, and then push a frame per `PutImage` — or
|
|
184
|
-
`ShmPutImage`, which is the same request with a shared-memory segment in
|
|
185
|
-
place of the payload.
|
|
186
|
-
|
|
187
|
-
Two things it gives you that are worth the trouble:
|
|
188
|
-
|
|
189
|
-
- **The colour conversion is the server's.** You push planar YUV at
|
|
190
|
-
1.5 bytes per pixel; the driver produces RGB. For 1080p that is 3.1MB
|
|
191
|
-
per frame instead of 7.9MB, and no CPU of ours touches a pixel.
|
|
192
|
-
- **The scale is the server's too**, with `QueryBestSize` to ask what it
|
|
193
|
-
would prefer, and the drawable rectangle chosen per frame — so a resized
|
|
194
|
-
video costs nothing extra on our side.
|
|
195
|
-
|
|
196
|
-
And the caveats, which are why it is not simply the answer:
|
|
197
|
-
|
|
198
|
-
- **Overlay adaptors and compositing do not mix.** A hardware overlay
|
|
199
|
-
writes past the compositor; under a compositing WM you want a textured
|
|
200
|
-
adaptor, which every modern driver provides, but the distinction is not
|
|
201
|
-
visible in the protocol and the failure is a black or misplaced
|
|
202
|
-
rectangle.
|
|
203
|
-
- **There is no clock.** Xv presents when you ask. Pacing, A/V sync and
|
|
204
|
-
vsync are entirely yours — which is exactly the seam the
|
|
205
|
-
[frame pacer](frame-pacing.md) already owns for everything else.
|
|
206
|
-
- **It may not exist.** Measured on this machine: XQuartz advertises
|
|
207
|
-
`X-Video Extension version 2.2` and answers `no adaptors present`. The
|
|
208
|
-
extension is there and useless. Any Xv path needs a fallback by
|
|
209
|
-
construction, not as a nicety.
|
|
210
|
-
- **Where it draws.** A textured adaptor's `PutImage` lands in the
|
|
211
|
-
drawable it names. Named at our window it is overwritten by the next
|
|
212
|
-
present of ntk's backing pixmap, and named at a child window of its own
|
|
213
|
-
it stacks above everything drawn, which is the z-order flaw `<glarea>`
|
|
214
|
-
and `<MediaPlayer>` both live with. The honest target is the **backing
|
|
215
|
-
pixmap itself**, at the fitted rect, as a step of the paint pass — then
|
|
216
|
-
the frame composites in paint order and the present carries it. Whether
|
|
217
|
-
ntk will name that pixmap to a caller is a small seam to ask for, and
|
|
218
|
-
the question the Xv rung starts with.
|
|
219
|
-
|
|
220
|
-
### 2.2 The decode extensions, and what replaced them
|
|
221
|
-
|
|
222
|
-
**XvMC** is the one X extension that ever did decode: it took MPEG-2
|
|
223
|
-
macroblocks, IDCT and motion compensation onto the GPU. It is effectively
|
|
224
|
-
dead — a handful of drivers, MPEG-2 and some MPEG-4 only — and it is
|
|
225
|
-
unreachable from a protocol client anyway, because the client API is a
|
|
226
|
-
vendor `libXvMC*.so` the application dlopens, not wire requests. It is
|
|
227
|
-
mentioned here so that the next person who finds it in a list of
|
|
228
|
-
extensions does not spend an afternoon on it.
|
|
229
|
-
|
|
230
|
-
**VA-API, VDPAU, NVDEC** are what people actually use, and none of them
|
|
231
|
-
is an X extension. They are C libraries that use X only to obtain a
|
|
232
|
-
display and, optionally, to present: `vaPutSurface(Drawable)`,
|
|
233
|
-
`VdpPresentationQueueTargetCreateX11`. Underneath they go to DRI and the
|
|
234
|
-
kernel. A pure-JS protocol client cannot speak them at all; reaching them
|
|
235
|
-
means a native addon, at which point the addon may as well own decode and
|
|
236
|
-
presentation together.
|
|
237
|
-
|
|
238
|
-
The conclusion is the TL;DR's: **on X11 we decode, always.** In practice
|
|
239
|
-
that means ffmpeg through a native addon, a WASM decoder, or a subprocess
|
|
240
|
-
pipe — and that is a dependency decision (§10), not a rendering one.
|
|
241
|
-
|
|
242
|
-
### 2.3 The transports
|
|
243
|
-
|
|
244
|
-
Given frames in our memory, three ways to get them onto the screen, in
|
|
245
|
-
ascending order of how little they copy.
|
|
246
|
-
|
|
247
|
-
**MIT-SHM.** `ShmPutImage` moves a frame through shared memory instead of
|
|
248
|
-
the socket. ntk already has this — `ShmUploader` in
|
|
249
|
-
`node_modules/ntk/lib/shm-upload.js`, reached through the app's `shm`
|
|
250
|
-
getter and used by `putImageData` and by `Image` uploads: a pool of
|
|
251
|
-
segments rounded up to a 64KB quantum, the first upload of a size going
|
|
252
|
-
out as core `PutImage` while a segment warms behind it, and each segment
|
|
253
|
-
returned on `ShmCompletion`. Its own measurements put it at roughly 2× on
|
|
254
|
-
uploads, and it is available on XQuartz here. This is the floor of any X11
|
|
255
|
-
video path, it is already built, and **the frame sink's drawn presentation
|
|
256
|
-
gets it for free** by writing into an ntk `Surface` (§6.3).
|
|
257
|
-
|
|
258
|
-
**DRI3 + Present.** The modern zero-copy present: `PixmapFromBuffer`
|
|
259
|
-
imports a dma-buf fd as a Pixmap, `PresentPixmap` flips it with
|
|
260
|
-
`target_msc`/`divisor`/`remainder` for vsync, and
|
|
261
|
-
`PresentCompleteNotify`/`PresentIdleNotify` hand back both a frame clock
|
|
262
|
-
and buffer recycling. node-x11 ships `dri3.js` and `present.js`, and
|
|
263
|
-
`x11-dri` is already an optional dependency. The blocker is the direction:
|
|
264
|
-
the DRI3 _receive_ side — importing a buffer somebody else allocated — is
|
|
265
|
-
not wired, and that is precisely the direction a hardware decoder needs.
|
|
266
|
-
This is the right long-term path and the largest piece of work in this
|
|
267
|
-
document.
|
|
268
|
-
|
|
269
|
-
**GLX / direct GL.** Upload Y and CbCr as textures and convert in a
|
|
270
|
-
fragment shader. This works **today**, through `<glarea>`, with no new
|
|
271
|
-
protocol at all: see [gl.md](../gl.md) and the direct-GL notes in
|
|
272
|
-
[glx.md](../glx.md). It is the fastest thing to prototype and it costs a
|
|
273
|
-
`<glarea>`'s constraints — the node owns a real child window or layer, and
|
|
274
|
-
its pixels cannot be read back through the X path. It is also, as it
|
|
275
|
-
happens, the shape of the Wayland answer (§5), where the backend's whole
|
|
276
|
-
2D context is a GLES rasterizer.
|
|
277
|
-
|
|
278
|
-
### 2.4 What node-x11 has
|
|
279
|
-
|
|
280
|
-
Until 2026-09-07 `lib/ext/xv.js` implemented the half of Xv that asks
|
|
281
|
-
questions and none of the half that answers with pixels. That gap is
|
|
282
|
-
closed —
|
|
283
|
-
[sidorares/node-x11#294](https://github.com/sidorares/node-x11/issues/294)
|
|
284
|
-
was fixed by
|
|
285
|
-
[#295](https://github.com/sidorares/node-x11/pull/295) — and, since this
|
|
286
|
-
record was first written, **installed**: the lockfile carries 4.2.3, so
|
|
287
|
-
`npm ci` now gets the image path. The refresh the first version asked for
|
|
288
|
-
has happened.
|
|
289
|
-
|
|
290
|
-
| Xv minor opcode | 4.1.0 | 4.2.0+ |
|
|
291
|
-
| ----------------------------------------------- | ----- | ------- |
|
|
292
|
-
| 0–4 version, adaptors, encodings, port grab | yes | yes |
|
|
293
|
-
| 5–8 `PutVideo`/`PutStill`/`GetVideo`/`GetStill` | no | no |
|
|
294
|
-
| 9 `StopVideo` | no | **yes** |
|
|
295
|
-
| 10 `SelectVideoNotify` / 11 `SelectPortNotify` | no | **yes** |
|
|
296
|
-
| 12–16 best size, port attributes, image formats | yes | yes |
|
|
297
|
-
| 17 `QueryImageAttributes` | no | **yes** |
|
|
298
|
-
| 18 `PutImage` | no | **yes** |
|
|
299
|
-
| 19 `ShmPutImage` | no | **yes** |
|
|
300
|
-
|
|
301
|
-
5–8 remain absent on purpose — they are the capture-card half of the
|
|
302
|
-
extension, as dead as XvMC, and §3.5's answer to "where do camera frames
|
|
303
|
-
come from" is not Xv.
|
|
304
|
-
|
|
305
|
-
Two things about the implementation that bound what this document can
|
|
306
|
-
assume:
|
|
307
|
-
|
|
308
|
-
- **`PutImage` refuses a frame the connection cannot carry**, rather than
|
|
309
|
-
sending it and having the server drop the connection over a bad request
|
|
310
|
-
length. The 40-byte header plus a padded payload has to fit
|
|
311
|
-
`max_request_length`, which without BIG-REQUESTS is 256KB. A 1080p
|
|
312
|
-
`I420` frame is 3.1MB, so **`PutImage` alone cannot carry an HD frame on
|
|
313
|
-
a connection without BIG-REQUESTS**, and will say so instead of failing
|
|
314
|
-
mysteriously.
|
|
315
|
-
- **`ShmPutImage` sidesteps that entirely**, because its 52 bytes carry a
|
|
316
|
-
`SHMSEG` and no payload. So for HD it is not an optimisation over
|
|
317
|
-
`PutImage`; it is the path.
|
|
318
|
-
|
|
319
|
-
What is still missing is ours: ntk has no Xv wrapper (its only `xv`
|
|
320
|
-
substrings are `Xvfb` and `glxVisual`), and nothing in `src/` requires the
|
|
321
|
-
extension. The rung is gated on nobody but us (§12).
|
|
322
|
-
|
|
323
|
-
### 2.5 Measured: what the naive path costs
|
|
324
|
-
|
|
325
|
-
The question that decides whether "decode in-process and `PutImage` the
|
|
326
|
-
result" is even worth prototyping. A 1920×1080 frame at depth 24, pushed
|
|
327
|
-
to an offscreen pixmap with a `GetInputFocus` round trip after each so the
|
|
328
|
-
server has demonstrably consumed it, on XQuartz over a unix socket, M1 Pro:
|
|
329
|
-
|
|
330
|
-
| path | median | min | p95 |
|
|
331
|
-
| ------------------------------------- | ------- | ------- | ------- |
|
|
332
|
-
| core `PutImage`, 7.9 MiB BGRA, + sync | 33.19ms | 25.22ms | 48.26ms |
|
|
333
|
-
|
|
334
|
-
That is **238 MiB/s and a 30.1fps ceiling** with zero time left for
|
|
335
|
-
layout, paint, or the rest of the application — for one video, at 1080p,
|
|
336
|
-
on a local socket. It is a bound rather than a Linux number (XQuartz is
|
|
337
|
-
not a DRI-backed server), but it is the right order of magnitude for the
|
|
338
|
-
shape of the problem, and it prices the alternatives:
|
|
339
|
-
|
|
340
|
-
- `420v` instead of BGRA is 3.1MB instead of 7.9MB — **2.6× less to
|
|
341
|
-
push**, before SHM.
|
|
342
|
-
- SHM removes the socket copy; ntk's own figure is ~2× on uploads.
|
|
343
|
-
- Xv does both _and_ moves the colour conversion and the scale off our
|
|
344
|
-
CPU — which is why it was worth the upstream request even though the
|
|
345
|
-
extension is absent on XQuartz.
|
|
346
|
-
|
|
347
|
-
What follows for the element: on X11 the sink wants **BGRA frames**, and
|
|
348
|
-
says so (§6.3's `preferredFormats`), because a decoder that can emit BGRA
|
|
349
|
-
does the conversion in C for nothing, and one that cannot leaves it to a
|
|
350
|
-
JavaScript loop over two million pixels. At 720p — the size a video in a
|
|
351
|
-
form actually is — a BGRA frame is 3.7MB, which by the two numbers above
|
|
352
|
-
the SHM path puts on the screen in the order of ten milliseconds on
|
|
353
|
-
XQuartz — derived, not measured, and owed a measurement by the bench
|
|
354
|
-
scenario in §12. That is the honest X11
|
|
355
|
-
budget until the Xv rung, and a document that said otherwise would be
|
|
356
|
-
read by someone building a 4K player on a `PutImage`.
|
|
357
|
-
|
|
358
|
-
## 3. Cocoa
|
|
359
|
-
|
|
360
|
-
### 3.1 Decode
|
|
361
|
-
|
|
362
|
-
`VTDecompressionSession` is the primitive: build a
|
|
363
|
-
`CMVideoFormatDescription` from the stream's parameter sets, feed it
|
|
364
|
-
`CMSampleBuffer`s, get `CVPixelBuffer`s. Ask for IOSurface backing with
|
|
365
|
-
`kCVPixelBufferIOSurfacePropertiesKey` and every output buffer is a
|
|
366
|
-
surface the render server can already read.
|
|
367
|
-
|
|
368
|
-
Measured on this machine (macOS 15.2 then, M1 Pro),
|
|
369
|
-
`VTIsHardwareDecodeSupported`:
|
|
370
|
-
|
|
371
|
-
| codec | hardware |
|
|
372
|
-
| ---------- | -------- |
|
|
373
|
-
| H.264 | yes |
|
|
374
|
-
| HEVC | yes |
|
|
375
|
-
| ProRes 422 | yes |
|
|
376
|
-
| AV1 | no |
|
|
377
|
-
| VP9 | no |
|
|
378
|
-
|
|
379
|
-
AV1 is the one to plan around: hardware AV1 decode arrives with M3-class
|
|
380
|
-
silicon, so an AV1 stream on this machine is a software path or nothing.
|
|
381
|
-
|
|
382
|
-
Above VideoToolbox, AVFoundation supplies the parts we would otherwise
|
|
383
|
-
write: `AVAsset` + `AVAssetReader` for files frame by frame, `AVPlayer` /
|
|
384
|
-
`AVPlayerItem` for URLs including HLS,
|
|
385
|
-
`AVPlayerItemVideoOutput.copyPixelBuffer(forItemTime:)` to pull frames in
|
|
386
|
-
step with a display link, `AVCaptureVideoPreviewLayer` for a camera, and
|
|
387
|
-
ScreenCaptureKit for the screen.
|
|
388
|
-
|
|
389
|
-
### 3.2 The presentation ladder
|
|
390
|
-
|
|
391
|
-
Every rung is a `CALayer`, which is why all of them compose with layer
|
|
392
|
-
promotion and with the sprite order unchanged.
|
|
393
|
-
|
|
394
|
-
1. **`AVPlayerLayer`** — set `player`, set `videoGravity`, done. No pixel
|
|
395
|
-
code and no JS in the frame path at all. Everything about the source is
|
|
396
|
-
AVFoundation's. **The rung for `src`.**
|
|
397
|
-
2. **`AVSampleBufferDisplayLayer`** — enqueue `CMSampleBuffer`s from our
|
|
398
|
-
own demuxer or socket; it decodes and schedules them itself, and
|
|
399
|
-
`AVSampleBufferRenderSynchronizer` with a `CMTimebase` is how it is
|
|
400
|
-
synchronised against audio. The rung for "a compressed stream that is
|
|
401
|
-
not a file or a URL" — which no consumer in §8 has yet.
|
|
402
|
-
3. **A plain `CALayer` whose `contents` is the decoded IOSurface** — the
|
|
403
|
-
rung this repo is already standing on, since `setLayerContentsIOSurface`
|
|
404
|
-
is how `<glarea>`, the `<Frame>` pane host and the window swapchain all
|
|
405
|
-
present. **The rung for `frames`**, now that §3.4 says it takes YUV.
|
|
406
|
-
4. **`CAMetalLayer` + `CVMetalTextureCache`** — zero-copy `MTLTexture`
|
|
407
|
-
from the pixel buffer, full shader control, our own colour conversion
|
|
408
|
-
and scaling. Nothing here needs it.
|
|
409
|
-
5. `CAOpenGLLayer` — legacy, and `<glarea>`'s neighbourhood.
|
|
410
|
-
|
|
411
|
-
### 3.3 Measured
|
|
412
|
-
|
|
413
|
-
The 2026-09-08 spike encodes a synthetic sequence with VideoToolbox,
|
|
414
|
-
decodes it back, and times each step. 1920×1080 H.264, 60 frames, hardware
|
|
415
|
-
acceleration confirmed on the session, every output buffer IOSurface-backed:
|
|
416
|
-
|
|
417
|
-
| step | median |
|
|
418
|
-
| -------------------------------------------- | ------- |
|
|
419
|
-
| decode a 1080p H.264 frame, default output | 1.868ms |
|
|
420
|
-
| decode the same frame, output forced to BGRA | 3.917ms |
|
|
421
|
-
| `layer.contents = ioSurface` | 0.04µs |
|
|
422
|
-
|
|
423
|
-
At 1280×720 the same measurement is 1.143ms for H.264 and 0.932ms for
|
|
424
|
-
HEVC. The flip is **below the resolution of a microsecond timer**, because
|
|
425
|
-
the assignment itself is a pointer swap and the transaction commit is
|
|
426
|
-
where CA's cost lives.
|
|
427
|
-
|
|
428
|
-
`AVSampleBufferDisplayLayer` accepted all 30 compressed sample buffers it
|
|
429
|
-
was offered headless, with `status` rendering and no error.
|
|
430
|
-
|
|
431
|
-
### 3.4 The YUV surface: blank through `render(in:)`, drawn on screen
|
|
432
|
-
|
|
433
|
-
A hardware decoder's natural output is `420v` — bi-planar 8-bit
|
|
434
|
-
video-range YCbCr, two planes, 3.1MB for 1080p. Our swapchain surfaces are
|
|
435
|
-
BGRA. The obvious move is to hand the decoder's surface straight to the
|
|
436
|
-
existing `setLayerContentsIOSurface` verb.
|
|
437
|
-
|
|
438
|
-
The first version of this record measured that through
|
|
439
|
-
`CALayer.render(in:)` and got an empty box: 0 of 4096 pixels for a decoded
|
|
440
|
-
`420v` frame, 0 of 4096 for a captured packed `2vuy` one, against BGRA and
|
|
441
|
-
`CGImage` controls at 4096 of 4096 through the same probe. It read that as
|
|
442
|
-
"a `CALayer` takes any YUV IOSurface and draws none of them", recommended
|
|
443
|
-
AV layer classes for everything, and — to its credit — ended with "whether
|
|
444
|
-
the render server scans out a YUV surface on screen is not settled by it,
|
|
445
|
-
and that is the very first thing the implementing PR should check".
|
|
446
|
-
|
|
447
|
-
Checked today (`scripts/spikes/video/yuvscan.swift`, §13): ten tiles in a
|
|
448
|
-
borderless window, each a `CALayer` on screen, the window captured
|
|
449
|
-
through the window server (`screencapture -l`, the same source as the
|
|
450
|
-
bridge's `snapshotWindow`), and the centre pixel of each tile read back.
|
|
451
|
-
macOS 15.8.1, M1 Pro:
|
|
452
|
-
|
|
453
|
-
| layer `contents` | on screen, window-server capture | `render(in:)`, 2026-09-08 |
|
|
454
|
-
| ---------------------------------------------- | -------------------------------- | ------------------------- |
|
|
455
|
-
| BGRA IOSurface — control | blue, as filled | 4096 / 4096 px |
|
|
456
|
-
| `420v` IOSurface, 2 planes, video range | **green, as filled** | 0 / 4096 px |
|
|
457
|
-
| `420f` IOSurface, 2 planes, full range | **green, as filled** | — |
|
|
458
|
-
| `2vuy` IOSurface, 1 packed plane | **green, as filled** | 0 / 4096 px |
|
|
459
|
-
| `y420` IOSurface, 3 planes, video range | nothing: the tile is transparent | — |
|
|
460
|
-
| `f420` IOSurface, 3 planes, full range | nothing: the tile is transparent | — |
|
|
461
|
-
| `AVSampleBufferDisplayLayer`, 30 H.264 frames | red, as encoded | — |
|
|
462
|
-
| `AVPlayerLayer`, a `.mov` written by the probe | magenta, as encoded | — |
|
|
463
|
-
| `CGImage` — control | orange, as drawn | 4096 / 4096 px |
|
|
464
|
-
|
|
465
|
-
Three conclusions, and the first two overturn what was written here before:
|
|
466
|
-
|
|
467
|
-
- **The render server draws YUV layer contents.** `render(in:)` is a CPU
|
|
468
|
-
path that does not, which is a fact about testing, not about the screen
|
|
469
|
-
(§13 says what to test through instead). The "free" path is free.
|
|
470
|
-
- **Two planes or packed, not three.** Every bi-planar and packed layout
|
|
471
|
-
tried renders; the three-plane planar ones — what ffmpeg calls
|
|
472
|
-
`yuv420p`, the default output of nearly every decoder — draw nothing:
|
|
473
|
-
the layer is transparent and the window shows through.
|
|
474
|
-
So the sink's native format on macOS is **NV12**, and an I420 frame is
|
|
475
|
-
interleaved into one on the way in: a loop the bridge's write verb does
|
|
476
|
-
over 1.5 bytes per pixel, in the same pass as the copy it has to make
|
|
477
|
-
anyway.
|
|
478
|
-
- **Colour attachments are read, and change the picture.** A second
|
|
479
|
-
`420v` tile with a BT.601 matrix and an sRGB colour space attached
|
|
480
|
-
rendered a visibly lighter green than the bare one. The sink has to say
|
|
481
|
-
what its bytes mean (§6.3: `colorSpace`, `range`), and the bridge has to
|
|
482
|
-
attach it to the surface — the Generic-RGB default has bitten this
|
|
483
|
-
backend before (#820), and a video that is subtly the wrong colour is
|
|
484
|
-
the kind of defect nobody files.
|
|
485
|
-
|
|
486
|
-
The cost of the three ways out stays on record because two of them are
|
|
487
|
-
still what `src` and a compressed stream use, and because the number is
|
|
488
|
-
what proves the sink should not convert: forcing BGRA out of the decoder
|
|
489
|
-
costs **2.1× the decode** (3.917ms against 1.868ms) plus 8.3MB per surface
|
|
490
|
-
against 3.1MB; `AVSampleBufferDisplayLayer` takes YCbCr natively and brings
|
|
491
|
-
A/V sync; Metal is its own render loop.
|
|
492
|
-
|
|
493
|
-
#### 3.4.1 Measured: what the frame sink costs through a plain layer
|
|
494
|
-
|
|
495
|
-
The question §6.3 rests on: when decoded bytes arrive from a decoder — over
|
|
496
|
-
a pipe, from an addon, from a WASM module — what does it cost to show them
|
|
497
|
-
through rung 3? `scripts/spikes/video/sinkcost.swift` stands a `memcpy`
|
|
498
|
-
into a locked IOSurface in for the read, points a layer at it inside a
|
|
499
|
-
transaction, and paces the loop to 60Hz so the render server has a real
|
|
500
|
-
frame per iteration. 1920×1080, a ring of three surfaces, 240 frames,
|
|
501
|
-
process CPU from `getrusage`, two runs each:
|
|
502
|
-
|
|
503
|
-
| format | bytes per frame | CPU per frame | note |
|
|
504
|
-
| ----------- | --------------- | -------------- | ----------------------------------------- |
|
|
505
|
-
| NV12 (420v) | 3.11 MB | 0.40 – 0.43 ms | the copy and the commit; nothing converts |
|
|
506
|
-
| BGRA | 8.29 MB | 0.69 – 0.72 ms | the same, over 2.7× the bytes |
|
|
507
|
-
|
|
508
|
-
The wall time per iteration is the 60Hz pacing, not a cost. So a
|
|
509
|
-
1080p60 sink lifted onto a layer is **about 25ms of CPU per second**
|
|
510
|
-
in the process that feeds it, before the decoder; the decoder is the
|
|
511
|
-
whole budget, and the presentation is noise beside it. That is the number
|
|
512
|
-
that says the sink needs no JS frame per video frame and no pacer claim:
|
|
513
|
-
a `push()` on a lifted sink writes the surface and repoints the layer
|
|
514
|
-
right there, and react-x11's own frame clock is not involved until the
|
|
515
|
-
next frame re-decides the lift (§7).
|
|
516
|
-
|
|
517
|
-
### 3.5 Capture: the same stack, one step shorter
|
|
518
|
-
|
|
519
|
-
A webcam is the same set of API calls with the decode removed.
|
|
520
|
-
`AVCaptureSession` delivers `CMSampleBuffer`s carrying IOSurface-backed
|
|
521
|
-
`CVPixelBuffer`s — the same objects a `VTDecompressionSession` produces —
|
|
522
|
-
and `AVCaptureVideoPreviewLayer` is a `CALayer`, so rung 1 of §3.2 serves a
|
|
523
|
-
camera exactly as it serves a file. And since §3.4, so does rung 3: a
|
|
524
|
-
camera's native `2vuy` frame is a layer content the screen shows.
|
|
525
|
-
|
|
526
|
-
Three things separate capture from playback, and only one of them is work.
|
|
527
|
-
|
|
528
|
-
**The permission half is already built.** `camera` and `microphone` are
|
|
529
|
-
first-class kinds in [permissions.md](../permissions.md) —
|
|
530
|
-
`usePermission('camera')`, `permissionStatus`, `requestPermission`,
|
|
531
|
-
`openPrivacySettings` — translated in `src/cocoa/permissions.js` onto the
|
|
532
|
-
bridge's `authorizationStatus`/`requestAuthorization` verbs, which are
|
|
533
|
-
`AVCaptureDevice`'s underneath (the one place the bridge links
|
|
534
|
-
AVFoundation today). A capture element does not need to touch TCC at all.
|
|
535
|
-
|
|
536
|
-
**Discovery needs no authorization.** `AVCaptureDevice.DiscoverySession`
|
|
537
|
-
enumerates cameras and microphones, names included, while the status is
|
|
538
|
-
still `notDetermined` — no prompt, no indicator light. A device picker can
|
|
539
|
-
be built and rendered before consent; only the frames are gated.
|
|
540
|
-
|
|
541
|
-
**The conversion is not free, and this is the finding.** A camera's native
|
|
542
|
-
delivery is YUV — `2vuy` at the 720p preset here. Asking the output for
|
|
543
|
-
BGRA instead is one line and is the conventional capture configuration.
|
|
544
|
-
Measured, 1280×720, 60 frames per configuration, camera live:
|
|
545
|
-
|
|
546
|
-
| output configuration | delivered | process CPU | delivery interval | our callback |
|
|
547
|
-
| -------------------- | --------- | ------------- | ----------------- | ------------ |
|
|
548
|
-
| native, no settings | `2vuy` | 4.16ms/frame | 33.5ms | 0.054ms |
|
|
549
|
-
| `videoSettings` BGRA | `BGRA` | 11.12ms/frame | 33.0ms | 0.095ms |
|
|
550
|
-
|
|
551
|
-
**+6.96ms per frame, 2.67×**, charged to our own process rather than to the
|
|
552
|
-
camera daemon, on another thread — so it burns a core rather than dropping
|
|
553
|
-
frames, which is precisely the kind of cost the frame pacer cannot see.
|
|
554
|
-
The absolute figures amortize one-time session setup over 60 frames; the
|
|
555
|
-
delta is the trustworthy part.
|
|
556
|
-
|
|
557
|
-
So capture reaches the same conclusion as playback by the same route: do
|
|
558
|
-
not convert. Hand the session to `AVCaptureVideoPreviewLayer`, or hand its
|
|
559
|
-
native surfaces to a plain layer; both are on the screen.
|
|
560
|
-
|
|
561
|
-
**What the tests cannot do.** A running `AVCaptureVideoPreviewLayer` with a
|
|
562
|
-
live connection rasterized 0/4096 through `layer.render(in:)` — which, after
|
|
563
|
-
§3.4, is unsurprising rather than alarming. Capture pixel tests go through
|
|
564
|
-
the window server's capture like every other layer's, or through an
|
|
565
|
-
`AVCaptureVideoDataOutput` tap.
|
|
566
|
-
|
|
567
|
-
**One packaging finding, because it will cost someone an afternoon.** A
|
|
568
|
-
correctly signed `.app` carrying `NSCameraUsageDescription` and a
|
|
569
|
-
`CFBundleIdentifier`, executed directly from a shell, answered
|
|
570
|
-
`requestAccess` with `granted = false` while `authorizationStatus` stayed
|
|
571
|
-
`notDetermined` — a refusal that never prompted. Launched through
|
|
572
|
-
LaunchServices (`open`), so that the app is its own responsible process,
|
|
573
|
-
the same binary prompted and was granted. This is the same shape as the
|
|
574
|
-
notification rung's trap in [packaging.md](../packaging.md), and the same
|
|
575
|
-
rule applies: **a falsy grant with the status still `notDetermined` is not
|
|
576
|
-
a denial**, and code that reads it as one tells the user they declined
|
|
577
|
-
something they were never asked.
|
|
578
|
-
|
|
579
|
-
## 4. Windows
|
|
580
|
-
|
|
581
|
-
_Written on a Mac against the bridge's source at v0.0.6 and
|
|
582
|
-
[windows.md](../windows.md). Nothing below has been run; §4.3 is what to
|
|
583
|
-
run first._
|
|
584
|
-
|
|
585
|
-
### 4.1 What the backend has
|
|
586
|
-
|
|
587
|
-
A window on win32 is a DirectComposition **visual tree**: one target, one
|
|
588
|
-
root visual whose content is a virtual surface in `B8G8R8A8`, painted one
|
|
589
|
-
`BeginDraw` per damage rect and committed once per frame
|
|
590
|
-
([windows.md](../windows.md#rendering)). Under the root visual hang the
|
|
591
|
-
children an element can own today, each on a visual of its own:
|
|
592
|
-
|
|
593
|
-
- **`<glarea>`**, a composition swapchain filled through a WGL context and
|
|
594
|
-
`WGL_NV_DX_interop2`, added at the _bottom_ of the children
|
|
595
|
-
([windows-gl.md](../windows-gl.md)). A child HWND was built, measured
|
|
596
|
-
invisible in every variant, and abandoned
|
|
597
|
-
([windows-embedding.md](../windows-embedding.md)).
|
|
598
|
-
- **A `<Frame>` pane**, attached from a surface _handle_ another process
|
|
599
|
-
presents into: `DCompositionCreateSurfaceHandle` on the pane's side,
|
|
600
|
-
`CreateSurfaceFromHandle` and a visual on the host's
|
|
601
|
-
(`paneAttach`, `paneSetRect` with its clip).
|
|
602
|
-
- **Layers**, a premultiplied virtual surface on a visual at the _top_ of
|
|
603
|
-
the children, which is what `<glarea>`'s drawn children paint on
|
|
604
|
-
(`layerCreate`, `layerBeginDraw`, `layerScroll`).
|
|
605
|
-
|
|
606
|
-
What it does not have is a presenter that decides, per frame, whether an
|
|
607
|
-
element's content may sit on a visual of its own: the Windows retained tier
|
|
608
|
-
and promotion are a Phase 4 probe in windows.md, not code, and the sprite
|
|
609
|
-
seam is asked on macOS only. So on win32 today a visual is where
|
|
610
|
-
`<glarea>`'s is — above the 2D surface, under the layers, and under
|
|
611
|
-
whatever the 2D surface paints after it is not. There is no Media
|
|
612
|
-
Foundation, no YUV, no `DXGI_FORMAT_NV12` anywhere in the bridge, and
|
|
613
|
-
`windowPixels` reads the window back through `PrintWindow`.
|
|
614
|
-
|
|
615
|
-
### 4.2 The plan
|
|
616
|
-
|
|
617
|
-
**`src` is `IMFMediaEngine`**, the platform's player, which has two ways to
|
|
618
|
-
show a picture and the backend wants both, in this order:
|
|
619
|
-
|
|
620
|
-
1. **Frame-server mode first.** `TransferVideoFrame` copies the current
|
|
621
|
-
frame into a D3D11 texture on request; the backend draws that texture
|
|
622
|
-
into the window's surface at the fitted rect, in paint order, like an
|
|
623
|
-
`<image>` whose bytes changed. A copy per frame — but a GPU copy, and
|
|
624
|
-
correct under every tooltip, popup and fade, with no new z-order rule.
|
|
625
|
-
This is the drawn presentation of §7, and on Windows it is the one that
|
|
626
|
-
ships first because nothing there can yet decide the other.
|
|
627
|
-
2. **Windowless swapchain mode second.** `EnableWindowlessSwapchainMode`,
|
|
628
|
-
`GetVideoSwapchainHandle`, and the handle goes to the exact call the
|
|
629
|
-
pane host already makes — `CreateSurfaceFromHandle` onto a visual at
|
|
630
|
-
the element's rect. Zero copies, the engine presents on its own clock,
|
|
631
|
-
and the overlay planes can scan it out where the GPU has them. It
|
|
632
|
-
needs what `<glarea>` lacks: a per-frame answer to "is anything drawn
|
|
633
|
-
over this", which is the Windows half of promotion. Until then it is
|
|
634
|
-
`<glarea>`'s stacking, and a video element should not inherit that
|
|
635
|
-
quietly.
|
|
636
|
-
|
|
637
|
-
The engine's notifications — `LOADEDMETADATA`, `TIMEUPDATE`, `PLAYING`,
|
|
638
|
-
`PAUSE`, `ENDED`, `ERROR` — are the element's events already (§6.1); the
|
|
639
|
-
bridge forwards them as it forwards everything else, in batches, off the
|
|
640
|
-
engine's work queue onto Node's thread.
|
|
641
|
-
|
|
642
|
-
**`frames` is a texture.** A decoded frame arrives as bytes; the bridge
|
|
643
|
-
puts them in a `D3D11_USAGE_DYNAMIC` texture and the drawn presentation
|
|
644
|
-
composites it. BGRA is the floor. NV12 is the better transport —
|
|
645
|
-
`DXGI_FORMAT_NV12` textures are routine, and a flip-model composition
|
|
646
|
-
swapchain in NV12 on a visual of its own is what windows.md's rendering
|
|
647
|
-
section already names for video — but whether the adapter will create one
|
|
648
|
-
is a `CheckFormatSupport` question per machine, and the fallback is a
|
|
649
|
-
`ID3D11VideoProcessor` blit to BGRA. Both are probes, not plans.
|
|
650
|
-
|
|
651
|
-
### 4.3 What to probe first, on the Windows machine
|
|
652
|
-
|
|
653
|
-
In the order that decides the most for the least:
|
|
654
|
-
|
|
655
|
-
1. `IMFMediaEngine` from a bridge thread: create it from
|
|
656
|
-
`MFMediaEngineClassFactory` on the UI thread with a DXGI device manager
|
|
657
|
-
over the bridge's D3D11 device, load a file URL, confirm
|
|
658
|
-
`LOADEDMETADATA` and the frame size, and `TransferVideoFrame` into a
|
|
659
|
-
texture the 2D surface can draw. That is the whole drawn path.
|
|
660
|
-
2. `EnableWindowlessSwapchainMode` + `GetVideoSwapchainHandle` +
|
|
661
|
-
`CreateSurfaceFromHandle` on a visual: does the picture appear, does
|
|
662
|
-
`paneSetRect`'s clip apply to it, and does it respect the visual's
|
|
663
|
-
place among the children?
|
|
664
|
-
3. `windowPixels` over 1 and 2: does `PrintWindow` with full-content
|
|
665
|
-
rendering see a swapchain visual's frame? It decides what the tests can
|
|
666
|
-
assert without a capture tool.
|
|
667
|
-
4. `CheckFormatSupport(DXGI_FORMAT_NV12)` with `DISPLAY` and
|
|
668
|
-
`TEXTURE2D`, and a composition swapchain in NV12: on the GPU in the
|
|
669
|
-
machine and on WARP, since CI is WARP.
|
|
670
|
-
5. The cost of the frame-server copy at 1080p60: a GPU copy is cheap, and
|
|
671
|
-
the number is still owed before it is called cheap here.
|
|
672
|
-
6. Media Foundation's threading against the bridge's UI thread: the engine
|
|
673
|
-
wants a multithreaded apartment and a work queue; the UI thread is an
|
|
674
|
-
STA for OLE. Confirm the notify callback can post to the bridge's event
|
|
675
|
-
batch without a wait on the UI thread.
|
|
676
|
-
|
|
677
|
-
## 5. Wayland
|
|
678
|
-
|
|
679
|
-
The Wayland backend presents dma-bufs and nothing else: every frame is
|
|
680
|
-
drawn by its own GLES rasterizer into a GPU target and handed over through
|
|
681
|
-
`zwp_linux_dmabuf_v1`; `wl_shm` is bound for screen capture only; and
|
|
682
|
-
`wl_subcompositor` is never bound, by design — a subsurface would be a
|
|
683
|
-
second swapchain, a second frame clock and a second import per frame
|
|
684
|
-
([wayland-backend.md](../wayland-backend.md)). Its `<glarea>` is a
|
|
685
|
-
rectangle of the window's own target, not a surface of its own.
|
|
686
|
-
|
|
687
|
-
That makes its video story the shortest one here and, for the frame sink,
|
|
688
|
-
the cheapest: **the backend's 2D context is already a GL rasterizer**, so
|
|
689
|
-
decoded planes go up as textures and the colour conversion is a sampler in
|
|
690
|
-
a fragment shader — the GLX transport of §2.3, native to the backend rather
|
|
691
|
-
than bolted on, and with no conversion in JavaScript for any format. The
|
|
692
|
-
sink's drawn presentation on Wayland takes NV12, I420 and BGRA alike and
|
|
693
|
-
prefers whichever the decoder emits. Nothing lifts: there is no layer to
|
|
694
|
-
lift onto and no compositor-side scaler asked for, and `wp_viewporter` is
|
|
695
|
-
used for the window's fractional scale only.
|
|
696
|
-
|
|
697
|
-
`src` has no answer on Wayland for the same reason it has none on X11 — no
|
|
698
|
-
display-server player exists; GStreamer and PipeWire are libraries, and
|
|
699
|
-
the application that wants them brings them (§8.3) — so
|
|
700
|
-
`useSupports('mediaPlayback')` is false there and `<video src>` refuses the
|
|
701
|
-
way it does on X11. The zero-copy future is the same shape as X11's DRI3
|
|
702
|
-
one: a decoder that exports dma-buf fds (VA-API does) and a subsurface that
|
|
703
|
-
imports them, both of which the backend has deliberately not built, and
|
|
704
|
-
neither of which a form with a video in it needs.
|
|
705
|
-
|
|
706
|
-
## 6. The element
|
|
707
|
-
|
|
708
|
-
### 6.1 `<video>`
|
|
709
|
-
|
|
710
|
-
```jsx
|
|
711
|
-
// a file or URL the platform plays itself — AVFoundation today,
|
|
712
|
-
// Media Foundation when the Windows half lands
|
|
713
|
-
<video
|
|
714
|
-
src="file:///Users/me/clip.mp4"
|
|
715
|
-
autoPlay
|
|
716
|
-
muted
|
|
717
|
-
loop
|
|
718
|
-
style={{
|
|
719
|
-
width: 320,
|
|
720
|
-
height: 180,
|
|
721
|
-
objectFit: 'contain',
|
|
722
|
-
backgroundColor: '#000',
|
|
723
|
-
}}
|
|
724
|
-
onLoadedMetadata={({ width, height, duration }) => setDuration(duration)}
|
|
725
|
-
onTimeUpdate={({ currentTime }) => setAt(currentTime)}
|
|
726
|
-
/>;
|
|
727
|
-
|
|
728
|
-
// frames the application decodes — every backend
|
|
729
|
-
const frames = useVideoFrames({ width: 1280, height: 720, format: 'NV12' });
|
|
730
|
-
useEffect(() => {
|
|
731
|
-
const decoder = startDecoder(url, frames.preferredFormats);
|
|
732
|
-
decoder.on('frame', (planes, time) => frames.push(planes, { time }));
|
|
733
|
-
return () => decoder.stop();
|
|
734
|
-
}, [frames, url]);
|
|
735
|
-
<video frames={frames} style={{ width: '100%', aspectRatio: 16 / 9 }} />;
|
|
736
|
-
```
|
|
737
|
-
|
|
738
|
-
A `<video>` is a **drawn** node like `<image>` — it joins yoga, it is in
|
|
739
|
-
the paint order, it is hit-tested like anything — not a `drawn: false`
|
|
740
|
-
child window like `<glarea>` or `<foreign>`. That is the whole point of
|
|
741
|
-
§7: a video in a form sits _under_ the form's popups and tooltips and
|
|
742
|
-
_inside_ its scroll panes, and a child window can do neither.
|
|
743
|
-
|
|
744
|
-
| prop | |
|
|
745
|
-
| ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
746
|
-
| `src` | A path or URL, played by the platform. Where `useSupports('mediaPlayback')` is false the element reports one typed `onError` on a microtask and shows its `poster` — never a silent black box, the rule `<foreign>` set in #531. |
|
|
747
|
-
| `frames` | A `VideoFrames` sink (§6.3). Mutually exclusive with `src`, and passing both throws, the way `<image>`'s `src`/`picture`/`drawable` do. |
|
|
748
|
-
| `poster` | An `ImageSource`, shown until the first frame and whenever there is nothing to show. |
|
|
749
|
-
| `autoPlay`, `loop`, `muted`, `volume`, `playbackRate` | HTML's names and HTML's defaults: nothing plays until asked. Meaningful with `src`; a sink plays what it is pushed. |
|
|
750
|
-
| `paused` | Controlled when present, the shape `<MediaPlayer paused>` already has. Left out, the element is uncontrolled and the handle drives it. |
|
|
751
|
-
| `onLoadedMetadata({ width, height, duration })` | The stream's size is also the element's natural size, kept to its ratio when the style names one axis; before it is known, HTML's 300×150. |
|
|
752
|
-
| `onPlay`, `onPause`, `onEnded`, `onTimeUpdate`, `onError` | `onTimeUpdate` is coalesced to a few a second, as HTML's is, so a progress bar costs a render per tick and not per frame. |
|
|
753
|
-
| `ref` → `{ play(), pause(), seek(seconds), currentTime, duration }` | The imperative half, for the same reason `HTMLMediaElement` has one: a seek is an event, not a state. |
|
|
754
|
-
| `role`, `aria-label` | `video` by default — a role the accessibility model gains with the element, AT-SPI's `ROLE_VIDEO` beside `img`'s `IMAGE` — named by `aria-label`; the frames themselves are not described. |
|
|
755
|
-
|
|
756
|
-
Style, in `style` like everything else: `width`/`height` as any element;
|
|
757
|
-
`objectFit` — `'contain'` by default, `'cover'`, `'fill'` — decides where
|
|
758
|
-
the picture goes in the box, and `backgroundColor` is what shows in the
|
|
759
|
-
bars. `objectFit` is a new paint property and applies to `<image>` as well
|
|
760
|
-
in the same change, since an image stretched to its box has wanted it for
|
|
761
|
-
as long as there has been an `<image>`; `borderRadius` on a `<video>` is
|
|
762
|
-
legal and costs the drawn presentation on macOS (§7), which the docs say
|
|
763
|
-
in one sentence next to the prop.
|
|
764
|
-
|
|
765
|
-
### 6.2 What `useSupports('mediaPlayback')` answers
|
|
766
|
-
|
|
767
|
-
Whether `<video src>` plays on this display: true on Cocoa once the player
|
|
768
|
-
verbs ship, true on win32 once §4's first probe becomes code, false on X11
|
|
769
|
-
and Wayland for good, false on every backend until then. It is a property
|
|
770
|
-
of the display's pipeline, which is what `useSupports` answers; the
|
|
771
|
-
desktop-service shape (`{ available, backend, features }`) is for things
|
|
772
|
-
that come and go while the process runs, and a platform player does not.
|
|
773
|
-
`frames` needs no capability: it works everywhere, at a cost that differs.
|
|
774
|
-
|
|
775
|
-
### 6.3 `VideoFrames`, the sink
|
|
776
|
-
|
|
777
|
-
```ts
|
|
778
|
-
createVideoFrames(app, {
|
|
779
|
-
width, height,
|
|
780
|
-
format: 'NV12' | 'I420' | 'BGRA',
|
|
781
|
-
colorSpace?: 'bt709' | 'bt601' | 'bt2020', // default bt709
|
|
782
|
-
range?: 'video' | 'full', // default video
|
|
783
|
-
}): VideoFrames
|
|
784
|
-
|
|
785
|
-
interface VideoFrames {
|
|
786
|
-
readonly width: number; readonly height: number; readonly format: VideoFormat;
|
|
787
|
-
/** this backend's order of preference, for a decoder that can choose */
|
|
788
|
-
readonly preferredFormats: readonly VideoFormat[];
|
|
789
|
-
push(planes: Buffer | readonly Buffer[], opts?: { strides?: number[]; time?: number }): void;
|
|
790
|
-
close(): void;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
useVideoFrames(options): VideoFrames // stable across renders; new when size, format or colour changes; nothing to close
|
|
794
|
-
```
|
|
795
|
-
|
|
796
|
-
Three rules, each of which is a cost somewhere:
|
|
797
|
-
|
|
798
|
-
- **The newest frame is the frame.** A `push` while the last one is not yet
|
|
799
|
-
shown replaces it; nothing queues. A sink is a mirror of a decoder's
|
|
800
|
-
output, not a transport with back-pressure, and the decoder's own clock
|
|
801
|
-
is the pacing — exactly as a `<canvas>` animation's is.
|
|
802
|
-
- **Every format is accepted everywhere; the cheap one differs.** A decoder
|
|
803
|
-
that can choose reads `preferredFormats`; one that cannot still works,
|
|
804
|
-
with the conversion charged where it happens:
|
|
805
|
-
|
|
806
|
-
| backend | lifted | drawn | `preferredFormats` |
|
|
807
|
-
| ------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------- |
|
|
808
|
-
| Cocoa | NV12 as is; I420 interleaved on the write (bridge); BGRA as is | BGRA; NV12/I420 converted in the bridge by VideoToolbox, colour-matched to sRGB, about 1ms at 1080p | `NV12`, `I420`, `BGRA` |
|
|
809
|
-
| X11 | — | BGRA into an ntk `Surface` (SHM where the server has it); NV12/I420 converted in JS, which is not free at 1080p | `BGRA` |
|
|
810
|
-
| win32 | later: an NV12 swapchain on a visual (§4.2) | BGRA into a texture; NV12/I420 through the video processor, or in JS until then | `BGRA`, then `NV12` |
|
|
811
|
-
| Wayland | — | NV12, I420 and BGRA as textures; YUV sampled in the shader, no conversion anywhere | `NV12`, `I420`, `BGRA` |
|
|
812
|
-
|
|
813
|
-
- **`push` is not a frame.** On a lifted sink it writes a surface from the
|
|
814
|
-
ring and repoints the layer in a transaction of its own, and react-x11's
|
|
815
|
-
frame clock does not run — 0.4ms of CPU at 1080p (§3.4.1) and no claim
|
|
816
|
-
for the [pacer](frame-pacing.md) to price. On a drawn sink it claims the
|
|
817
|
-
element's content rect, bounded, and the next frame composites the new
|
|
818
|
-
bytes like an `<image>` whose `src` changed — priced like any claim, and
|
|
819
|
-
the first thing a profile of an X11 player shows.
|
|
820
|
-
|
|
821
|
-
Where the bytes come from is the application's business (§10), and the
|
|
822
|
-
sink's design keeps it that way: an `ffmpeg` child process writing
|
|
823
|
-
`rawvideo` to a pipe, a WASM decoder, a native addon over libmpv or VA-API
|
|
824
|
-
all end in the same `push`.
|
|
825
|
-
|
|
826
|
-
## 7. The seam: a sprite whose content is a source
|
|
827
|
-
|
|
828
|
-
Sprites ([extending.md](../extending.md#parts-of-your-drawing-on-layers-of-their-own),
|
|
829
|
-
`src/nodes/sprites.js`, `src/cocoa/sprites.js`) are how an element hands
|
|
830
|
-
parts of its drawing to the surface presenter, which lifts each onto a
|
|
831
|
-
`CALayer` above the window's bitmap under promotion's rules: asked every
|
|
832
|
-
frame after layout and before the damage is taken; refused when anything
|
|
833
|
-
painted after the element reaches into the part, when the element or an
|
|
834
|
-
ancestor fades, when a rounded clipping ancestor does not hold all of it,
|
|
835
|
-
when it sits inside a `<glarea>`, or when a side exceeds 8192 device
|
|
836
|
-
pixels; cut by a box layer with `masksToBounds` to every square clipping
|
|
837
|
-
ancestor; a hole in the element's own paint from the frame `spritesLifted`
|
|
838
|
-
says so; the bitmap under it claimed on the way onto a layer and off it.
|
|
839
|
-
Today a part's content is always a raster: the presenter calls the part's
|
|
840
|
-
`paint` into a surface of its own and hands that surface to the layer, and
|
|
841
|
-
paints again when the `version` changes.
|
|
842
|
-
|
|
843
|
-
A video is a part whose content the presenter does **not** paint:
|
|
844
|
-
|
|
845
|
-
```js
|
|
846
|
-
sprites() {
|
|
847
|
-
if (!this.source || !this.frameRect) return null;
|
|
848
|
-
return [{ key: 'video', rect: this.frameRect, clip: this.contentBox(), contents: this.source }];
|
|
849
|
-
}
|
|
850
|
-
```
|
|
851
|
-
|
|
852
|
-
- `contents` is a `VideoFrames` sink, or the player a `<video src>` made.
|
|
853
|
-
A part with `contents` has no `paint` and no `version`, and for now no
|
|
854
|
-
`animations` — nothing in §8 wants a video that fades on the render
|
|
855
|
-
server, and a fade on the element itself is a refusal, as it is for
|
|
856
|
-
every sprite.
|
|
857
|
-
- **Lifted**, the layer's content is the source's: for a sink, the ring
|
|
858
|
-
surface `push` last wrote, repointed by `push` itself between frames
|
|
859
|
-
(§6.3); for a player, the `AVPlayerLayer` the bridge made, added under
|
|
860
|
-
the clip box at the part's place in the one order every layer above the
|
|
861
|
-
bitmap shares. The element paints its bars and leaves the hole.
|
|
862
|
-
- **Declined**, the element draws the source's current frame itself:
|
|
863
|
-
`source.draw(ctx, rect)`, a `drawImage` of the sink's BGRA surface, or
|
|
864
|
-
of the frame the bridge pulls from the player on request
|
|
865
|
-
(`AVPlayerItemVideoOutput`, one verb, lazily made, §9). Every reason a
|
|
866
|
-
sprite is declined is a reason a video is drawn for a while — a tooltip
|
|
867
|
-
over it, a dialog's dim, a rounded card that does not hold it — and
|
|
868
|
-
since declining is decided per frame, the cost lasts exactly as long as
|
|
869
|
-
the reason.
|
|
870
|
-
- **Everything else is promotion's, unchanged**: the z-order test asked
|
|
871
|
-
at the part's extent, the clip boxes, the claim of the bitmap under it,
|
|
872
|
-
the one order with the promoted nodes. A `contents` part makes
|
|
873
|
-
`SpriteLayers` skip its raster and ask the source for a layer or a
|
|
874
|
-
surface, and that is the whole change on the presenter's side.
|
|
875
|
-
|
|
876
|
-
Why not a child window or layer of the element's own, the way `<glarea>`
|
|
877
|
-
has one: because a `<glarea>`'s layer sits at a fixed `zPosition` above
|
|
878
|
-
everything the presenter draws, is never clipped by an ancestor, and
|
|
879
|
-
cannot be drawn over — `<MediaPlayer>`'s own documentation explains that
|
|
880
|
-
its transport bar cannot be a sibling `<box>` for exactly this reason. A
|
|
881
|
-
video in a form is content, and content lives in the paint order. The
|
|
882
|
-
sprite seam is the one that lets a thing be on a layer when it can and in
|
|
883
|
-
the bitmap when it must, and that is the property this element needs
|
|
884
|
-
most.
|
|
885
|
-
|
|
886
|
-
Why not `Node._promoted`, promotion's own hole: because promotion lifts a
|
|
887
|
-
plain `<box>` for an animation it can express, and re-rasters the node's
|
|
888
|
-
children into the layer itself. A video's content is nobody's raster.
|
|
889
|
-
|
|
890
|
-
And why `<video>` is the first implementer and not only a consumer: the
|
|
891
|
-
element's one part is itself, which is the simplest possible sprite — and
|
|
892
|
-
it proves the `contents` kind for any registered element that wants to
|
|
893
|
-
show a live source inside its own drawing, a chart with a camera feed in
|
|
894
|
-
its corner being the obvious one. The **X11, Wayland and win32 presenters
|
|
895
|
-
never ask**, so the same element draws itself there as it always would.
|
|
896
|
-
|
|
897
|
-
## 8. The two consumers
|
|
898
|
-
|
|
899
|
-
### 8.1 A `<video>` in a form
|
|
900
|
-
|
|
901
|
-
The element of §6, with nothing between the application and it. On macOS,
|
|
902
|
-
`<video src>` with the player verbs: no JS in the frame path, audio and
|
|
903
|
-
seeking from AVFoundation, lifted under the form's own popups and tooltips
|
|
904
|
-
and drawn for the frames a dialog dims it. On X11, `useSupports` says no to
|
|
905
|
-
`src`, and the application decodes into `frames` — the ecosystem's job to
|
|
906
|
-
make that one line, below.
|
|
907
|
-
|
|
908
|
-
### 8.2 A `<video>` inside `<Html>`
|
|
909
|
-
|
|
910
|
-
`@react-x11/components`' `<Html>` is one registered element, `<htmlview>`,
|
|
911
|
-
that parses, lays out and paints a document into the window's own context.
|
|
912
|
-
Its `<video>` today is its `poster` as an image box, or a 300×150 frame
|
|
913
|
-
with nothing in it; its `<audio>` is nothing at all; and it has no hook by
|
|
914
|
-
which a host supplies a renderer for a tag. It lifts a CSS animation onto
|
|
915
|
-
a layer through the sprite seam (#620), and it mounts **real core widgets
|
|
916
|
-
beside the document** for form controls — a `<textinput>` at an `<input>`'s
|
|
917
|
-
rect, cut to the box's clip and faded by its opacity, reported through
|
|
918
|
-
`onControls` — on the rule its source states: _a drawn control is a picture
|
|
919
|
-
of a control_.
|
|
920
|
-
|
|
921
|
-
A drawn video is a picture of a video, and the same rule gives the same
|
|
922
|
-
answer. `<Html>` reports its media boxes the way it reports its controls —
|
|
923
|
-
rect, clip, opacity, and the attributes `src`, `poster`, `autoplay`,
|
|
924
|
-
`loop`, `muted` — and its React half mounts a core `<video>` beside the
|
|
925
|
-
document at each, absolutely positioned and clipped exactly as a widget is.
|
|
926
|
-
Everything the element knows applies: it lifts on macOS when its part is
|
|
927
|
-
clear, it draws when it is not, it scrolls with the pane because its `abs`
|
|
928
|
-
does, and `src` refuses where `useSupports` says so, which `<Html>` turns
|
|
929
|
-
into the poster it shows today. `onResource` grows a `kind: 'video'` so a
|
|
930
|
-
host that has a decoder can answer a URL with a `frames` sink instead,
|
|
931
|
-
and a host that has none declines and gets the poster.
|
|
932
|
-
|
|
933
|
-
What `<Html>` decides for itself, and should say in its docs: a video box
|
|
934
|
-
that something later in the document paints over — an overlay of controls,
|
|
935
|
-
a caption positioned across it — is declined and stays a poster, the same
|
|
936
|
-
`paintedAfter` rule its sprites already apply, because a sibling mounted
|
|
937
|
-
above the document would hide the overlay. HTML's `controls` attribute is
|
|
938
|
-
not drawn by `<Html>`: the transport bar an application wants is its own
|
|
939
|
-
React, over or under the box, the position `<MediaPlayer>`'s docs already
|
|
940
|
-
take. And `<audio>` is not this element: a sound with no box is a player
|
|
941
|
-
with no presentation, which is a different feature and a smaller one.
|
|
942
|
-
|
|
943
|
-
### 8.3 `<MediaPlayer>`, and where a decoder comes from
|
|
944
|
-
|
|
945
|
-
`<MediaPlayer>` embeds mpv or VLC over `<foreign>`, drives it over mpv's
|
|
946
|
-
JSON IPC, and is X11-only because `--wid` is an X mechanism; on macOS it
|
|
947
|
-
reports `unavailable`. It keeps doing that: an application that already
|
|
948
|
-
has mpv and an X display has a player with audio, subtitles and every
|
|
949
|
-
codec, and nothing here replaces it. What changes is its ceiling. Its
|
|
950
|
-
window stacks above everything drawn, so it cannot be scrolled under a
|
|
951
|
-
header, dimmed by a dialog or overlaid by a transport bar — and a backend
|
|
952
|
-
of it that decodes into a `<video frames>` sink instead of a window has
|
|
953
|
-
none of those limits, on every backend, at the cost of the audio path and
|
|
954
|
-
the sink's bytes. The shape of that backend is the ecosystem's decision,
|
|
955
|
-
not this record's; the two candidates are an `ffmpeg` child process
|
|
956
|
-
writing `rawvideo` to a pipe (pure JS, silent, every backend), and a native
|
|
957
|
-
addon over libmpv's software render API (audio, seeking, subtitles, every
|
|
958
|
-
backend, one prebuilt binary). Either one makes `<MediaPlayer>` the thing
|
|
959
|
-
a React developer reaches for and `<video>` the primitive it stands on,
|
|
960
|
-
which is the right order.
|
|
961
|
-
|
|
962
|
-
### 8.4 Camera, later
|
|
963
|
-
|
|
964
|
-
The same `frames` shape with the bytes from a capture session — on macOS
|
|
965
|
-
the `2vuy` surfaces §3.5 measured, as layer contents or as bytes — behind
|
|
966
|
-
a `createCameraFrames({ deviceId })` that owns device selection and the
|
|
967
|
-
session's lifetime. The permission half needs nothing new. A separate
|
|
968
|
-
piece of work, after this, on the same element.
|
|
969
|
-
|
|
970
|
-
## 9. Where the code goes
|
|
971
|
-
|
|
972
|
-
| piece | where |
|
|
973
|
-
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
974
|
-
| the element | `src/nodes/video.js` beside `image.js`, the kind in `src/nodes/kinds.js` and `src/Reconciler.js`, `VideoProps` in `src/types/elements.d.ts`, a line in `test/types/api.tsx`; `objectFit` in `src/styles.js` for `<video>` and `<image>` both |
|
|
975
|
-
| the sink | `src/videoframes.js` — pure: formats, the ring, the conversion policy, `preferredFormats` — with a presentation per backend behind it: ntk `Surface` on X11, `src/cocoa/video.js`, `src/win32/video.js`, `src/wayland/video.js` |
|
|
976
|
-
| the sprite kind | the `contents` field in `src/nodes/sprites.js`'s contract and `src/node.d.ts`; `src/cocoa/sprites.js` skips the raster for it; `src/cocoa/promotion.js` unchanged |
|
|
977
|
-
| the capability | `mediaPlayback` in `src/appcontext.js`'s `FEATURES` |
|
|
978
|
-
| the player, Cocoa | `src/cocoa/player.js` over the verbs below, in the shape of `src/cocoa/glarea.js` |
|
|
979
|
-
| the player, win32 | `src/win32/player.js` over `IMFMediaEngine`, after §4.3 |
|
|
980
|
-
| bridge verbs, `@windowkit/appkit` | release A, the sink: `createVideoSurface({ width, height, format, colorSpace, range })` → `{ handle, iosurfaceId }` with the colour attachments set; `writeVideoSurface(handle, planes)`, which interleaves I420 into NV12; an optional `videoSurfacePlanes(handle)` answering `Buffer`s over the locked planes so a pipe read lands in the surface with no copy at all. Reuse, unchanged: `setLayerContentsIOSurface`, `surfaceIsInUse`, `surface-released`, `releaseSurface`. Release B, the player: `createPlayer({ url })`, `playerSet(id, { rate, volume, muted, loop })`, `playerSeek(id, seconds)`, `playerLayer(id)` answering a layer handle `addSublayer` takes, `playerCopyFrame(id)` answering a BGRA surface for the drawn presentation, `releasePlayer(id)`; events `player-metadata`, `player-time`, `player-rate`, `player-ended`, `player-error`. Release C, capture, later |
|
|
981
|
-
| bridge verbs, `@windowkit/win32` | after §4.3: `mediaCreate`, `mediaSet`, `mediaSeek`, `mediaFrame` (frame-server), `mediaRelease` and the engine's events; later `mediaAttach` onto a visual, over the pane host's `CreateSurfaceFromHandle` |
|
|
982
|
-
| Xv | a thin wrapper in ntk over node-x11's requests, port and format negotiation, `ShmPutImage` as the HD path, a clean decline where `QueryAdaptors` answers none |
|
|
983
|
-
| docs | a `<video>` section in [elements.md](../elements.md); the ladder in [macos.md](../macos.md) and [windows.md](../windows.md); the `contents` part in [extending.md](../extending.md); this file is the record they point at |
|
|
984
|
-
| tests | `test/video.test.js` — pixels, fit, the poster, the refusal — on the mock and the in-process X server; `test/cocoa-video.test.js` over `test/helpers/cocoa-bridge.js` for which verbs a lift and a decline send; a real-bridge check through `snapshotWindow`; a sink scenario in `scripts/bench/protocol.js` and a rule in `presenters-gate.json` that a lifted sink paints no window frame per pushed frame |
|
|
985
|
-
| spikes | `scripts/spikes/video/` — `yuvscan.swift` and `sinkcost.swift`, this PR |
|
|
986
|
-
|
|
987
|
-
## 10. What is not here, and why
|
|
988
|
-
|
|
989
|
-
- **A decoder.** react-x11 will not ship one. On Cocoa and Windows `src`
|
|
990
|
-
does not need one; everywhere, `frames` takes what the application
|
|
991
|
-
decodes. Bundling ffmpeg would be the largest dependency in the project
|
|
992
|
-
by an order of magnitude, and would land on every user of a toolkit most
|
|
993
|
-
of whom want a button. §8.3 is where a decoder belongs.
|
|
994
|
-
- **Audio for `frames`, and therefore A/V sync.** A sink is silent frames,
|
|
995
|
-
on every backend, and says so. `src` has audio wherever it has a player.
|
|
996
|
-
A `frames` source that also carries audio is the libmpv shape of §8.3,
|
|
997
|
-
and its sync is the decoder's.
|
|
998
|
-
- **Seeking, buffering, HLS, DRM.** All the platform player's where there
|
|
999
|
-
is one and entirely absent otherwise. Anything the element exposes that
|
|
1000
|
-
only works with `src` is documented as `src`-only, in the way
|
|
1001
|
-
[macos.md](../macos.md) documents the rest of that backend's asymmetries.
|
|
1002
|
-
- **A compressed-stream source.** `AVSampleBufferDisplayLayer` and its
|
|
1003
|
-
synchroniser are the right rung for one, no consumer has asked, and the
|
|
1004
|
-
verb count would double. When one does, it is a third source prop on the
|
|
1005
|
-
same element and the same sprite.
|
|
1006
|
-
- **`<audio>`.** A player with no box is not this element.
|
|
1007
|
-
- **Fullscreen, picture-in-picture, the lock screen's now-playing.** Window
|
|
1008
|
-
state is `useWindowState()`'s; the rest is desktop integration with its
|
|
1009
|
-
own ladder.
|
|
1010
|
-
- **Reading a lifted video's pixels through the X path.** Like `<glarea>`,
|
|
1011
|
-
a lifted video is not in the window's bitmap. On macOS `snapshotWindow`
|
|
1012
|
-
sees it (§3.4); on X11 there is nothing lifted to miss.
|
|
1013
|
-
|
|
1014
|
-
## 11. Decisions the implementing PRs have to make, and the suggested answer
|
|
1015
|
-
|
|
1016
|
-
| question | suggested answer |
|
|
1017
|
-
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1018
|
-
| the seam | a sprite part with `contents` (§7), not a new accessor and not a child window; `<video>` offers itself as its one part |
|
|
1019
|
-
| the element's primitive | `frames`, a sink, on every backend; `src` is the convenience of the backends with a player |
|
|
1020
|
-
| `src` where there is no player | one typed `onError`, the poster stays, `useSupports('mediaPlayback')` to ask first — never a black box |
|
|
1021
|
-
| the sink's native format | NV12 on macOS (two planes render, three do not), BGRA on X11 and win32 until their fast paths exist, anything on Wayland; published as `preferredFormats` |
|
|
1022
|
-
| pixel format on Cocoa | YUV through a plain layer — **not** BGRA, measured at 2.1× the decode on playback and 2.67× the process CPU on capture for the same picture — and not an AV layer class for a sink, which §3.4 made unnecessary |
|
|
1023
|
-
| colour | the sink names `colorSpace` and `range`; the bridge attaches them to the surface; the defaults are BT.709 video range, and a test pins that the bare and the attached surface differ, since §3.4 saw them differ |
|
|
1024
|
-
| bridge verbs, Cocoa | two for the sink, six and five events for the player, in two releases (§9); the sink first, because it ships the whole element on macOS without AV |
|
|
1025
|
-
| the frame clock | a lifted sink's `push` is not a react-x11 frame and is not priced (§6.3); a drawn sink's is, exactly like any other claim; a player's layer costs the frame nothing |
|
|
1026
|
-
| the hole | the element's, through `spritesLifted`, as for every sprite; the bars are painted always, the picture only when drawn |
|
|
1027
|
-
| rounded video corners | legal, and the drawn presentation; say so beside the prop rather than mask a layer that cannot be |
|
|
1028
|
-
| X11 v1 | BGRA frames into an ntk `Surface` with SHM; Xv second, into the backing pixmap if ntk will name it; DRI3 import as the real answer |
|
|
1029
|
-
| Xv fallback | mandatory: `QueryAdaptors` answering none is every Mac, and the drawn path is the decline |
|
|
1030
|
-
| win32 v1 | `IMFMediaEngine` in frame-server mode, drawn in paint order; the swapchain visual waits for the Windows half of promotion |
|
|
1031
|
-
| tests | the fake bridge for what reaches the natives; `snapshotWindow` for pixels on Cocoa, never `render(in:)`; on X11 that the element paints the same with and without SHM; on every backend that a declined video and a lifted one show the same pixels |
|
|
1032
|
-
| `<Html>` | mounts a core `<video>` beside the document, as it mounts a `<textinput>`; declines a box something paints over; grows `onResource` by `kind: 'video'` |
|
|
1033
|
-
| camera | a `frames` source with device selection and a session, later, on the same element |
|
|
1034
|
-
|
|
1035
|
-
## 12. What can be built now, and in what order
|
|
1036
|
-
|
|
1037
|
-
Five tracks. The first ships on every backend with no bridge release and
|
|
1038
|
-
no upstream; the next two are two `@windowkit/appkit` releases; the fourth
|
|
1039
|
-
waits for a Windows machine; the fifth is the ecosystem's.
|
|
1040
|
-
|
|
1041
|
-
1. **Core, now.** `VideoFrames`, `<video frames>` drawn through an ntk
|
|
1042
|
-
`Surface` on X11 and the shared 2D dialect on Cocoa and win32, NV12 and
|
|
1043
|
-
I420 converted to BGRA where a backend cannot take them, `objectFit`
|
|
1044
|
-
for `<video>` and `<image>`, `poster`, the typed refusal of `src`,
|
|
1045
|
-
`useSupports('mediaPlayback')` answering false everywhere, the `contents`
|
|
1046
|
-
sprite kind in `src/cocoa/sprites.js` behind a feature test for the
|
|
1047
|
-
verbs that do not exist yet. Pixel tests on the mock and the in-process
|
|
1048
|
-
server; a 720p sink scenario in the protocol bench, which is what makes
|
|
1049
|
-
the SHM path's effect visible. The whole element is usable from this
|
|
1050
|
-
step: an application with `ffmpeg` on its `PATH` plays a file on X11
|
|
1051
|
-
and on macOS in forty lines, silently, at the drawn cost.
|
|
1052
|
-
2. **`@windowkit/appkit` release A — the sink's two verbs** (§9). Then the
|
|
1053
|
-
Cocoa presenter lifts a `<video frames>` and a 1080p60 feed costs the
|
|
1054
|
-
process 25ms of CPU a second (§3.4.1) and react-x11 no frames at all.
|
|
1055
|
-
The acceptance test is the gate rule: a lifted sink paints no window
|
|
1056
|
-
frame per pushed frame, over the fake bridge; and a real-bridge
|
|
1057
|
-
`snapshotWindow` that shows the pushed colour.
|
|
1058
|
-
3. **`@windowkit/appkit` release B — the player.** `<video src>` on macOS
|
|
1059
|
-
with audio, seeking and HLS from AVFoundation; `mediaPlayback` true
|
|
1060
|
-
there; the drawn presentation through `playerCopyFrame` so a dialog's
|
|
1061
|
-
dim or a rounded card still shows the picture. The first `<video>` a
|
|
1062
|
-
form developer meets is this one.
|
|
1063
|
-
4. **Windows, on the Windows machine.** §4.3's probes, then `src` through
|
|
1064
|
-
`IMFMediaEngine` in frame-server mode and `frames` through a dynamic
|
|
1065
|
-
texture, both drawn; the swapchain visual and the NV12 swapchain when
|
|
1066
|
-
the backend has the Windows half of promotion to decide them.
|
|
1067
|
-
5. **`@react-x11/components`.** `<Html>` reports media boxes and mounts
|
|
1068
|
-
`<video>`s; `onResource` learns `video`; `<MediaPlayer>` keeps mpv and
|
|
1069
|
-
VLC and grows a `frames` backend — `ffmpeg` first, since it is pure JS
|
|
1070
|
-
and silent is still a video, libmpv when someone wants audio on macOS.
|
|
1071
|
-
|
|
1072
|
-
After those, in order of value: the Xv rung on X11 (into the backing
|
|
1073
|
-
pixmap, if ntk will name it), the camera source, DRI3 import on X11 and
|
|
1074
|
-
a dma-buf subsurface on Wayland for the zero-copy decoders, and a
|
|
1075
|
-
compressed-stream source over `AVSampleBufferDisplayLayer` when a consumer
|
|
1076
|
-
appears.
|
|
1077
|
-
|
|
1078
|
-
## 13. The spikes
|
|
1079
|
-
|
|
1080
|
-
Every number above came from a program that uses the platform directly —
|
|
1081
|
-
no bridge, no renderer, no React — so that what it measures is the
|
|
1082
|
-
platform and not our use of it.
|
|
1083
|
-
|
|
1084
|
-
**`scripts/spikes/video/yuvscan.swift`** (2026-10-02) answers §3.4's
|
|
1085
|
-
on-screen question. It builds eight `CALayer`s in a borderless window:
|
|
1086
|
-
BGRA and `CGImage` controls, `420v`, `420f`, `2vuy`, `y420` and `f420`
|
|
1087
|
-
surfaces made through `CVPixelBufferCreate` with IOSurface backing and
|
|
1088
|
-
filled with one YCbCr colour per plane, an `AVSampleBufferDisplayLayer` fed
|
|
1089
|
-
thirty H.264 frames a `VTCompressionSession` encodes from solid red, and an
|
|
1090
|
-
`AVPlayerLayer` playing a `.mov` an `AVAssetWriter` writes from solid
|
|
1091
|
-
magenta. Two seconds in it captures the window through
|
|
1092
|
-
`screencapture -l <windowNumber>` — the window server's composite, the
|
|
1093
|
-
same source as `CGWindowListCreateImage` and therefore as the bridge's
|
|
1094
|
-
`snapshotWindow`, and the only capture a Swift program on this SDK is
|
|
1095
|
-
still allowed — and prints the centre pixel of each tile. Build and run
|
|
1096
|
-
with `swiftc -O -swift-version 5 -o yuvscan yuvscan.swift && ./yuvscan`;
|
|
1097
|
-
a capture that comes back empty is the Screen Recording permission, not
|
|
1098
|
-
the layer. The controls are the part that makes the YUV rows meaningful.
|
|
1099
|
-
|
|
1100
|
-
**`scripts/spikes/video/sinkcost.swift`** (2026-10-02) answers §3.4.1. It
|
|
1101
|
-
rotates three 1920×1080 surfaces, copies a prepared frame into each
|
|
1102
|
-
plane of the next one, repoints a layer at it inside a transaction, and
|
|
1103
|
-
paces to 60Hz; `getrusage` around 240 iterations gives the process CPU per
|
|
1104
|
-
frame, for NV12 and BGRA in turn, twice each. The `memcpy` stands in for
|
|
1105
|
-
the read from a decoder; a verb that answers `Buffer`s over the locked
|
|
1106
|
-
planes removes even that.
|
|
1107
|
-
|
|
1108
|
-
The **2026-09-08 spikes** behind §2.5, §3.1–3.3 and §3.5 are described
|
|
1109
|
-
there and were not committed; their shapes: encode synthetic frames with a
|
|
1110
|
-
`VTCompressionSession` to get a real stream with parameter sets, decode
|
|
1111
|
-
them back with and without BGRA forced, time each frame, check
|
|
1112
|
-
`CVPixelBufferGetIOSurface`, assign the surface to a layer two thousand
|
|
1113
|
-
times for the flip cost, probe rasterization with `render(in:)` against
|
|
1114
|
-
controls; push a 1920×1080 depth-24 image to an offscreen pixmap with core
|
|
1115
|
-
`PutImage` and a `GetInputFocus` round trip after each; run an
|
|
1116
|
-
`AVCaptureSession` at 1280×720 for 60 frames twice, native and BGRA, with
|
|
1117
|
-
`getrusage` around each run, from a bundle launched through
|
|
1118
|
-
LaunchServices so that the authorization request prompts, keeping nothing
|
|
1119
|
-
of what the camera saw.
|
|
1120
|
-
|
|
1121
|
-
## 14. Verdict
|
|
1122
|
-
|
|
1123
|
-
The Cocoa half got smaller in the month since this was first written, and
|
|
1124
|
-
for a good reason: the thing it was designed around turned out not to be
|
|
1125
|
-
there. A plain layer shows a YUV surface on screen, the seam that lifts
|
|
1126
|
-
part of an element onto a layer has shipped and been tested on something
|
|
1127
|
-
else, and the bridge can already address an IOSurface by id. What is left
|
|
1128
|
-
for a frame sink on macOS is two verbs and a field on a sprite; what is
|
|
1129
|
-
left for a URL is the player verbs, which are the first AVFoundation the
|
|
1130
|
-
bridge will link for anything but a permission prompt.
|
|
1131
|
-
|
|
1132
|
-
The X11 half is exactly as large as it was, and this record's main
|
|
1133
|
-
service there is still to say so before someone spends a week on it: the
|
|
1134
|
-
transport that exists in the protocol is capped near 30fps at 1080p, the
|
|
1135
|
-
one that would fix it is now installed and still absent on every Mac, and
|
|
1136
|
-
the one that is right is the largest piece of work here. A sink the
|
|
1137
|
-
application feeds, drawn through the surface every retained-surface
|
|
1138
|
-
element already draws through, is honest, is small, and is the version of
|
|
1139
|
-
this that ships on four backends at once.
|
|
1140
|
-
|
|
1141
|
-
Windows has every piece a player needs except a decision about stacking,
|
|
1142
|
-
which is promotion's to make there as it was on macOS, and a drawn
|
|
1143
|
-
presentation that needs no such decision. Wayland is a rasterizer, so its
|
|
1144
|
-
sink is a shader.
|
|
1145
|
-
|
|
1146
|
-
The thing not to build is still the API that hides the difference — and
|
|
1147
|
-
the thing not to build twice is the element: a video inside a document is
|
|
1148
|
-
the same `<video>` as one in a form, mounted where the document says it
|
|
1149
|
-
goes.
|