523 lines
25 KiB
Markdown
523 lines
25 KiB
Markdown
# 09 — Hardware Acceleration, Encoding, WebCodecs, GPU Textures
|
||
|
||
This chapter covers the encoding/decoding/WASM pipeline in the MIT
|
||
approach. **Critical changes from Greenfield**:
|
||
- **No x264enc** (GPL-2.0+) — replaced with `vaapih264enc`/`nvh264enc`/`openh264enc`
|
||
- **No FFmpeg Wasm** (GPL-3.0 in Greenfield's build) — replaced with WebCodecs primary + OpenH264 BSD fallback
|
||
- **Hardware decode enabled** (Greenfield disables it with a FIXME)
|
||
- **Adaptive bitrate** (consumes `EncoderFeedback` data Greenfield produces but doesn't use)
|
||
|
||
## What's reused (MIT)
|
||
|
||
### Browser side
|
||
|
||
| Component | License | Role |
|
||
|---|---|---|
|
||
| `@gfld/compositor-wasm` | MIT | pixman 0.42.2 + xkbcommon 1.5.0 WASM (region math + keymap) |
|
||
|
||
### Vendored C (MIT, used in Python proxy via cffi)
|
||
|
||
| File | Lines | Role |
|
||
|---|---|---|
|
||
| `westfield-dmabuf.c` | ~200 | DMA-BUF import via EGL/GBM |
|
||
| `westfield-egl.c` | ~900 | EGL zero-copy setup (EGLDisplay, EGLContext, EGLImage) |
|
||
| `wlr_drm.c` | ~244 | `wl_drm` global impl |
|
||
| `wlr_linux_dmabuf_v1.c` | ~1,110 | `zwp_linux_dmabuf_v1` global impl |
|
||
| `drm_format_set.c` | ~239 | DRM format helpers |
|
||
|
||
## What's replaced (proprietary, clean-room)
|
||
|
||
### Browser side
|
||
|
||
| Component | Replaces | Why |
|
||
|---|---|---|
|
||
| Custom `WebCodecsFrameDecoder.ts` | Greenfield's `webcodec-buffer-decoder.ts` (AGPL) | Clean-room; **enables hardware decode** (Greenfield disables with FIXME) |
|
||
| Custom `OpenH264FrameDecoder.ts` | Greenfield's `wasm-buffer-decoder.ts` + `H264NALDecoder.worker.ts` (AGPL) + `@gfld/compositor-ffmpeg-h264` (GPL) | Clean-room; uses **OpenH264 BSD Wasm** instead of FFmpeg GPL Wasm |
|
||
| Custom `YUVA2RGBA` / `RGBXandA2RGBA` shaders | Greenfield's (AGPL) | Clean-room; **WebGL2** (Greenfield uses WebGL1) |
|
||
| Custom `Renderer` / `Scene` / `SceneShader` | Greenfield's (AGPL) | Clean-room; **OffscreenCanvas + Web Workers** (Greenfield is main-thread) |
|
||
|
||
### Python side
|
||
|
||
| Component | Replaces | Why |
|
||
|---|---|---|
|
||
| Custom `gst_encoder.py` (~700 LOC) | Greenfield's `proxy-encoding-addon.node` (AGPL, 290 lines C) + `libproxy-encoding.so` (AGPL, ~1400 lines C) | Pure Python via PyGObject. Same GStreamer, no N-API addon. |
|
||
| `vaapih264enc` / `nvh264enc` / `openh264enc` | `x264enc` (GPL) | GPL-free H.264 encoding |
|
||
|
||
## Encoder selection (per-frame, deterministic)
|
||
|
||
Same logic as Greenfield, but **without x264enc**:
|
||
|
||
| name | element | when used | split_alpha | coded-size multiple | License |
|
||
|---|---|---|---|---|---|
|
||
| `vaapih264` | `vaapih264enc` | >256×256 + ARGB/XRGB/dmabuf, Intel/AMD GPU | true (parallel alpha H.264 stream) | 128 | LGPL + MIT (libva) |
|
||
| `nvh264` | `nvh264enc` | same, NVIDIA GPU | true | 128 | BSD plugin + proprietary NVENC blob |
|
||
| `openh264` | `openh264enc` | same, no GPU (CPU fallback) | true | 128 | BSD-2-Clause (libopenh264) |
|
||
| `png` | `pngenc` | ≤256×256 + ARGB/XRGB/dmabuf | false (PNG has native alpha) | 16 | LGPL |
|
||
|
||
**Rule**: pixel_count ≤ 256×256 → PNG; otherwise the configured H.264
|
||
encoder (selected by `--encoder=vaapih264|nvh264|openh264`).
|
||
|
||
**NOTE**: This means the **default encoder is hardware-accelerated**
|
||
(vaapih264 on Intel/AMD, nvh264 on NVIDIA). Greenfield defaults to x264
|
||
(software, GPL).
|
||
|
||
## Canonical H.264 pipeline (Python equivalent)
|
||
|
||
Same GStreamer pipeline as Greenfield, ported to PyGObject:
|
||
|
||
```python
|
||
# gst_encoder.py (clean-room, ~700 LOC)
|
||
import gi
|
||
gi.require_version("Gst", "1.0")
|
||
gi.require_version("GstGL", "1.0")
|
||
gi.require_version("GstEGL", "1.0") # if available
|
||
from gi.repository import Gst, GstGL, GLib
|
||
|
||
class H264Encoder:
|
||
def __init__(self, egl_display_ptr, egl_context_ptr, encoder_type="vaapih264"):
|
||
# Create GLDisplay + GLContext from existing EGL context (zero-copy)
|
||
self.gl_display = GstGL.GLDisplayEGL.new_with_egl_display(egl_display_ptr)
|
||
self.gl_context = GstGL.GLContext.new_wrapped(
|
||
self.gl_display,
|
||
egl_context_ptr,
|
||
GstGL.GLPlatform.EGL,
|
||
GstGL.GLLAPI.GLES2,
|
||
)
|
||
|
||
# Encoder pipeline
|
||
encoder_element = {
|
||
"vaapih264": "vaapih264enc",
|
||
"nvh264": "nvh264enc",
|
||
"openh264": "openh264enc",
|
||
}[encoder_type]
|
||
|
||
self.pipeline = Gst.parse_launch(
|
||
f"appsrc name=src ! "
|
||
f"glupload ! glcolorconvert ! "
|
||
f"glshader name=scale ! " # pads to macroblock multiple
|
||
f"glcolorconvert ! video/x-raw(memory:GLMemory),format=NV12 ! "
|
||
f"gldownload ! queue ! "
|
||
f"{encoder_element} tune=zerolatency bitrate=12000 vbv-buf-capacity=160 "
|
||
f"bframes=0 rc-lookahead=0 sliced-threads byte-stream=true ! "
|
||
f"video/x-h264,profile=high,stream-format=byte-stream,alignment=au ! "
|
||
f"appsink name=sink"
|
||
)
|
||
|
||
# Wire up appsink callback (zero-copy external Buffers)
|
||
sink = self.pipeline.get_by_name("sink")
|
||
sink.emit("set-property", "emit-signals", True)
|
||
sink.connect("new-sample", self._on_new_sample)
|
||
|
||
# Alpha pipeline (separate, for transparent windows)
|
||
self.alpha_pipeline = self._build_alpha_pipeline(encoder_element)
|
||
|
||
# GLib MainLoop in separate thread (GStreamer requires it)
|
||
self.glib_loop = GLib.MainLoop()
|
||
self.glib_context = GLib.MainContext.new()
|
||
GLib.MainContext.push_thread_default(self.glib_context)
|
||
import threading
|
||
self.glib_thread = threading.Thread(target=self.glib_loop.run, daemon=True)
|
||
self.glib_thread.start()
|
||
|
||
def encode_frame(self, buffer_id, content_serial, creation_serial, dma_buf_fd, w, h):
|
||
# Push buffer into appsrc (zero-copy via GstBuffer wrapping the dmabuf)
|
||
# ... marshalled from asyncio via GLib.idle_add
|
||
|
||
def _on_new_sample(self, appsink):
|
||
sample = appsink.emit("pull-sample")
|
||
buffer = sample.get_buffer()
|
||
# Marshal back to asyncio
|
||
asyncio.run_coroutine_threadsafe(
|
||
self._deliver_frame(buffer, self.current_buffer_id),
|
||
self.asyncio_loop,
|
||
)
|
||
```
|
||
|
||
### Bitrates (same as Greenfield)
|
||
|
||
- Opaque: 12000 (vaapih264 / nvh264 / openh264)
|
||
- Alpha: 1200 (10% of opaque)
|
||
|
||
### Adaptive bitrate (NEW — Greenfield lacks)
|
||
|
||
Consume `EncoderFeedback` data (refreshInterval + decode duration):
|
||
|
||
```python
|
||
# adaptive_bitrate.py (clean-room, ~150 LOC)
|
||
class AdaptiveBitrate:
|
||
def __init__(self, encoder):
|
||
self.encoder = encoder
|
||
|
||
def on_feedback(self, refresh_interval_ms, avg_decode_ms):
|
||
# If decode is taking too long, reduce bitrate
|
||
if avg_decode_ms > refresh_interval_ms * 0.8:
|
||
new_bitrate = max(self.current_bitrate * 0.7, MIN_BITRATE)
|
||
self.encoder.set_property("bitrate", new_bitrate)
|
||
# If decode is fast and we have headroom, increase bitrate
|
||
elif avg_decode_ms < refresh_interval_ms * 0.3:
|
||
new_bitrate = min(self.current_bitrate * 1.3, MAX_BITRATE)
|
||
self.encoder.set_property("bitrate", new_bitrate)
|
||
```
|
||
|
||
## Wire format (REUSED from Greenfield)
|
||
|
||
Same binary format as Greenfield (see `../greenfield-analysis/09-hardware-accel.md`):
|
||
|
||
```
|
||
struct packed props {
|
||
u32 buffer_id, creation_serial, content_serial,
|
||
u32 frame_encoding_type (0=h264, 1=png),
|
||
u32 width, height, coded_width, coded_height
|
||
}
|
||
u32 opaque_length
|
||
u8 opaque[opaque_length] // H.264 AU or PNG blob
|
||
u32 alpha_length // 0 if no alpha split
|
||
u8 alpha[alpha_length] // H.264 AU of alpha-only stream
|
||
```
|
||
|
||
## Browser decode (clean-room TS)
|
||
|
||
### Preferred path: WebCodecs `VideoDecoder` (with HW decode enabled)
|
||
|
||
```typescript
|
||
// WebCodecsFrameDecoder.ts (clean-room, ~250 LOC)
|
||
export class WebCodecsFrameDecoder implements FrameDecoder {
|
||
private opaqueDecoders: Map<SurfaceId, VideoDecoder> = new Map();
|
||
private alphaDecoders: Map<SurfaceId, VideoDecoder> = new Map();
|
||
|
||
async isSupported(): Promise<boolean> {
|
||
const config = {
|
||
codec: 'avc1.64001f',
|
||
optimizeForLatency: true,
|
||
// NEW: enable hardware acceleration (Greenfield disables this)
|
||
hardwareAcceleration: 'prefer-hardware',
|
||
};
|
||
const result = await VideoDecoder.isConfigSupported(config);
|
||
return result.supported;
|
||
}
|
||
|
||
async decode(surface: Surface, encodedFrame: EncodedFrame): Promise<DecodedFrame> {
|
||
let opaqueDecoder = this.opaqueDecoders.get(surface.id);
|
||
if (!opaqueDecoder) {
|
||
opaqueDecoder = new VideoDecoder({
|
||
output: (frame) => this.onDecoded(surface, frame),
|
||
error: (e) => this.onError(surface, e),
|
||
});
|
||
opaqueDecoder.configure({
|
||
codec: 'avc1.64001f',
|
||
optimizeForLatency: true,
|
||
hardwareAcceleration: 'prefer-hardware', // NEW
|
||
});
|
||
this.opaqueDecoders.set(surface.id, opaqueDecoder);
|
||
}
|
||
|
||
const chunk = new EncodedVideoChunk({
|
||
type: encodedFrame.isKeyframe ? 'key' : 'delta',
|
||
data: encodedFrame.opaque,
|
||
timestamp: encodedFrame.timestamp,
|
||
});
|
||
opaqueDecoder.decode(chunk);
|
||
|
||
// Alpha path (if present)
|
||
if (encodedFrame.alpha) {
|
||
// Same pattern for alphaDecoder
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**Key difference from Greenfield**: We use `hardwareAcceleration:
|
||
'prefer-hardware'` (Greenfield uses `'prefer-software'` with a FIXME
|
||
about "frame offset is wrong"). This needs testing, but should give
|
||
significant CPU savings.
|
||
|
||
### Fallback path: OpenH264 BSD Wasm (NOT FFmpeg — FFmpeg is GPL)
|
||
|
||
```typescript
|
||
// OpenH264FrameDecoder.ts (clean-room, ~250 LOC)
|
||
// Uses OpenH264 BSD-2-Clause Wasm build (Cisco's binary release or self-built)
|
||
// Available from: https://github.com/cisco/openh264 (BSD-2-Clause)
|
||
```
|
||
|
||
OpenH264 is BSD-2-Clause — fully proprietary-compatible. Less featureful
|
||
than FFmpeg (no VP9/AV1), but sufficient for H.264 fallback.
|
||
|
||
## WebGL2 upload (clean-room TS, modernized)
|
||
|
||
WebGL2 with `desynchronized:true` + OffscreenCanvas in Web Worker.
|
||
|
||
Per Scene (canvas/output), three GL helpers (same as Greenfield, but
|
||
WebGL2 instead of WebGL1):
|
||
|
||
### `YUVA2RGBA`
|
||
|
||
FBO pass: uploads Y/U/V/alphaY as **4× LUMINANCE textures** (WebGL1
|
||
compatible) OR **1× R8UI texture array** (WebGL2, more efficient),
|
||
renders into the surface's RGBA texture via `YUVA2RGBAShader` (BT.601
|
||
coefficients).
|
||
|
||
### `RGBXandA2RGBA`
|
||
|
||
FBO pass for the `DualPlaneRGBA*` WebCodecs outputs (opaque + alpha
|
||
planes composited via `RGBandA2RGBAShader`).
|
||
|
||
### `SceneShader`
|
||
|
||
Final pass: draws every view's RGBA texture to the canvas with
|
||
`gl.blendFunc(ONE, ONE_MINUS_SRC_ALPHA)`.
|
||
|
||
**NEW**: WebGL2 `texStorage2D` for immutable textures (faster upload).
|
||
**NEW**: `VideoFrame` directly as a WebGL texture source (zero-copy from
|
||
WebCodecs to GPU, no `copyTo` needed).
|
||
|
||
## End-to-end pipeline
|
||
|
||
```
|
||
┌──────────────────── NATIVE CLIENT PROCESS ────────────────────┐
|
||
│ App renders via GTK/Qt/raw EGL into │
|
||
│ ┌────────────────────────────────────┐ │
|
||
│ │ wl_shm buffer (ARGB8888/XRGB8888)│ ─── or ─────────┐ │
|
||
│ │ zwp_linux_dmabuf_v1 (DMA-BUF) │ │ │
|
||
│ │ wl_drm create_prime_buffer │ │ │
|
||
│ └────────────────────────────────────┘ │ │
|
||
└────────────────────────┬──────────────────────────────────┼────┘
|
||
│ Wayland socket (UNIX domain) │
|
||
▼ │
|
||
┌────────────────────── PYTHON PROXY (aiohttp + pywayland + GStreamer)─┐
|
||
│ pywayland (Apache-2.0) │
|
||
│ └─ Display → Client → Resource (buffer) │
|
||
│ └─ display.init_shm(), init_drm("/dev/dri/renderD128") │
|
||
│ └─ Vendored westfield-egl.c (MIT, via cffi): │
|
||
│ EGLDevice + EGLDisplay + EGLContext │
|
||
│ │
|
||
│ gst_encoder.py (~700 LOC Python, clean-room) │
|
||
│ ┌─ frame_buffer {type: SHM|DMA, w, h, fmt} │
|
||
│ │ │
|
||
│ │ GLib MainLoop in separate thread (GLib.idle_add) │
|
||
│ │ │ │
|
||
│ │ ▼ │
|
||
│ │ gst_parse_launch: │
|
||
│ │ ┌─ Encoder selection (per-frame): │
|
||
│ │ │ pixel_count ≤ 256×256 ─► PNG pipeline │
|
||
│ │ │ > 256×256 + ARGB/XRGB/dmabuf ─► configured h264 │
|
||
│ │ │ │
|
||
│ │ │ H.264 pipelines (parallel, NO x264 — GPL): │
|
||
│ │ │ opaque: appsrc → glupload → glcolorconvert → │
|
||
│ │ │ glshader(scale→coded_size) → │
|
||
│ │ │ glcolorconvert(NV12) → gldownload → │
|
||
│ │ │ queue → vaapih264enc|nvh264enc|openh264enc │
|
||
│ │ │ → byte-stream AU → appsink │
|
||
│ │ │ alpha: same source, fragment shader │
|
||
│ │ │ `vec4(pix.a,pix.a,pix.a,0.)` → │
|
||
│ │ │ second H.264 stream │
|
||
│ │ │ │
|
||
│ │ │ PNG pipeline (single): │
|
||
│ │ │ appsrc → glupload → glcolorconvert → glshader → │
|
||
│ │ │ RGBA → gldownload → pngenc → appsink │
|
||
│ │ │ │
|
||
│ │ └─ asyncio.run_coroutine_threadsafe for cross-thread │
|
||
│ │ │
|
||
│ └─ surface_buffer_encoding.py │
|
||
│ └─ frame_data_channel.send(buffer) │
|
||
│ │
|
||
│ channel.py: │
|
||
│ ARQChannel (kcp PyPI package, MIT, over WebSocket) │
|
||
│ SimpleChannel (raw WebSocket, for FEEDBACK only) │
|
||
│ ▲ │
|
||
│ │ frame_feedback.py: 16-bit rAF interval + 16-bit avg │
|
||
│ │ decode duration → used to pace wl_frame.done AND │
|
||
│ │ drive adaptive_bitrate.py (NEW vs Greenfield) │
|
||
└───────┼───────────────────────────────────────────────────────┘
|
||
│ 4 logical channels per client: PROTOCOL / FRAME / XWM / FEEDBACK
|
||
▼
|
||
┌────────────────────── BROWSER (Custom Compositor TS, clean-room) ──────┐
|
||
│ WebSocket → ReconnectingWebSocket (vendored MIT) │
|
||
│ ↓ │
|
||
│ channel.ts: ARQChannel.input(kcp) → onMessage │
|
||
│ ↓ │
|
||
│ RemoteAppLauncher.setupFrameDataChannel → │
|
||
│ BufferStream.deliverContentToBufferStream (parse serials) │
|
||
│ ↓ │
|
||
│ StreamingBuffer.getContents(surface, serial) │
|
||
│ ↓ │
|
||
│ Session.frameDecoder.decode(surface, encodedFrame) │
|
||
│ │ │
|
||
│ ├─ PNG path (both decoders): │
|
||
│ │ new Blob([opaque], {type:'image/png'}) → │
|
||
│ │ createImageBitmap(blob, sx, sy, w, h) // crop padding │
|
||
│ │ → SinglePlane { bitmap: ImageBitmap } │
|
||
│ │ │
|
||
│ └─ H.264 path: │
|
||
│ (preferred) WebCodecs VideoDecoder │
|
||
│ config: avc1.64001f, prefer-hardware (NEW!), │
|
||
│ optimizeForLatency │
|
||
│ opaqueDecoder + alphaDecoder per surface │
|
||
│ EncodedVideoChunk {type:key|delta, data:nal} │
|
||
│ → VideoFrame.output → │
|
||
│ if I420: copyTo → DualPlaneYUVAArrayBuffer │
|
||
│ else: keep VideoFrame → DualPlaneRGBAVideoFrame │
|
||
│ (fallback) OpenH264 BSD Wasm in 2 Web Workers │
|
||
│ (NOT FFmpeg — GPL contamination) │
|
||
│ worker.postMessage(nal, [nal.buffer]) // transferable │
|
||
│ OpenH264._decode → I420 planes │
|
||
│ postMessage back with [y.buffer, u.buffer, v.buffer] │
|
||
│ → DualPlaneYUVASplitBuffer │
|
||
│ │
|
||
│ Renderer.render() (rAF-driven, in Web Worker via OffscreenCanvas)│
|
||
│ ↓ │
|
||
│ Scene.render(viewStack) │
|
||
│ ├─ Scene['image/png'] → texImage2D(ImageBitmap) │
|
||
│ ├─ Scene['image/bitmap'] (web-bitmapbuf path) → texImage2D │
|
||
│ └─ Scene['video/h264']: │
|
||
│ YUVA2RGBA.convertYUVASplitBufferInto / │
|
||
│ convertYUVAArrayBufferInto: │
|
||
│ Y/U/V/alphaY → 4× LUMINANCE textures (WebGL1 compat) │
|
||
│ OR 1× R8UI texture array (WebGL2, more efficient) │
|
||
│ bind FBO → renderState.texture (RGBA) as color attach │
|
||
│ YUVA2RGBAShader (BT.601) → drawArrays → RGBA in tex │
|
||
│ or RGBXandA2RGBA.convertInto (WebCodecs RGBA): │
|
||
│ VideoFrame/ImageBitmap → texImage2D directly │
|
||
│ ↓ │
|
||
│ SceneShader.draw(): drawArrays with surface's RGBA texture, │
|
||
│ blend gl.ONE / gl.ONE_MINUS_SRC_ALPHA, desynchronized canvas │
|
||
│ ↓ │
|
||
│ Browser composites canvas into the page │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
|
||
|
||
Same-frame web app fast path (web-bitmapbuf, REUSED):
|
||
┌───────────────┐ MessagePort.postMessage([view, ...fds], ┌──────────────┐
|
||
│ Web app │ ──► [imageBitmap]) ──────────────────────────► │ Compositor │
|
||
│ (iframe) │ web_bitmapbuf_factory.create_buffer(id, fd) │ WebConnHandl │
|
||
└───────────────┘ → wl_buffer → ImageBitmapBuffer └──────────────┘
|
||
(no encoding, no WebSocket, no GStreamer)
|
||
```
|
||
|
||
## Key answers (quick reference)
|
||
|
||
### Why H.264 over PNG?
|
||
|
||
H.264 at 12 Mbps × 1/60s ≈ 25 KB/frame for 1080p, vs PNG 100 KB – 3 MB.
|
||
PNG wins for ≤256×256 because H.264 I-frame overhead and alpha-split
|
||
doubling dominate at small sizes.
|
||
|
||
### WebCodecs?
|
||
|
||
Yes — preferred path. **Hardware decode enabled** (Greenfield disables
|
||
with FIXME). OpenH264 BSD Wasm is the fallback (NOT FFmpeg — GPL).
|
||
|
||
### Trade-offs?
|
||
|
||
- H.264 = small wire / decode cost
|
||
- PNG = medium wire / browser-native fast decode (`createImageBitmap`)
|
||
- raw SHM = **not used** between proxy and browser — every surface commit
|
||
is encoded.
|
||
|
||
### Per-surface policy?
|
||
|
||
No — per-frame deterministic. Only configurable knob is
|
||
`--encoder=vaapih264|nvh264|openh264`. PNG is always automatic fallback.
|
||
|
||
### Cursors?
|
||
|
||
Always PNG (32×32 ≤ 256×256). Decoded to ImageBitmap and set as the CSS
|
||
cursor via `setBrowserCursor` — never enters the WebGL compositor.
|
||
|
||
### Alpha pre-multiplication?
|
||
|
||
Implicit in shaders (same as Greenfield). Works acceptably for typical
|
||
Wayland content.
|
||
|
||
### OffscreenCanvas / Web Workers?
|
||
|
||
**Yes** — Render Worker (WebGL2 + OffscreenCanvas) + Decode Worker
|
||
(WebCodecs VideoDecoder). Greenfield uses only 2 workers for the WASM
|
||
fallback path; our MIT approach uses Workers for ALL rendering.
|
||
|
||
### Adaptive bitrate (NEW)?
|
||
|
||
**Yes** — `adaptive_bitrate.py` consumes `EncoderFeedback` data and
|
||
adjusts `vaapih264enc`/`nvh264enc`/`openh264enc` bitrate at runtime.
|
||
Greenfield produces the feedback data but doesn't use it.
|
||
|
||
## Notable design trade-offs for the MIT approach
|
||
|
||
1. **Hardware-accelerated encoding by default** (vaapih264 on Intel/AMD,
|
||
nvh264 on NVIDIA). Greenfield defaults to x264 (software, GPL).
|
||
2. **Hardware-accelerated decoding by default** (WebCodecs
|
||
`prefer-hardware`). Greenfield uses `prefer-software` with a FIXME.
|
||
3. **No GPL anywhere in the encoding/decoding pipeline** (no x264, no
|
||
FFmpeg Wasm).
|
||
4. **Adaptive bitrate** consumes the feedback data Greenfield ignores.
|
||
5. **OpenH264 BSD Wasm** as fallback (Cisco's binary release or
|
||
self-built). Sufficient for H.264 fallback when WebCodecs is
|
||
unavailable.
|
||
6. **OffscreenCanvas + Web Workers** for all rendering (Greenfield is
|
||
main-thread).
|
||
7. **WebGL2** instead of WebGL1 (uniform buffer objects, simplified
|
||
shaders).
|
||
8. **VideoFrame directly as WebGL texture source** (zero-copy from
|
||
WebCodecs to GPU, no `copyTo` needed).
|
||
9. **Same alpha-split H.264 approach** as Greenfield (doubles cost for
|
||
transparent windows) — could be replaced with AV1 with native alpha
|
||
support in the future.
|
||
10. **Future: AV1/VP9 support** via WebCodecs (when browser support is
|
||
widespread) — sidesteps H.264 MPEG-LA patent royalties entirely.
|
||
|
||
## File reference
|
||
|
||
### Browser side (custom, clean-room TS)
|
||
|
||
| Path | Role |
|
||
|---|---|
|
||
| `packages/compositor/src/transport/decoders/WebCodecsFrameDecoder.ts` | WebCodecs VideoDecoder (HW-accelerated) |
|
||
| `packages/compositor/src/transport/decoders/OpenH264FrameDecoder.ts` | OpenH264 BSD Wasm fallback |
|
||
| `packages/compositor/src/transport/decoders/FrameDecoder.ts` | Decoder interface |
|
||
| `packages/compositor/src/transport/EncodedFrame.ts` | Wire format parser for encoded frames |
|
||
| `packages/compositor/src/transport/EncoderFeedback.ts` | Sends 4-byte decode feedback at 1 Hz |
|
||
| `packages/compositor/src/transport/StreamingBuffer.ts` | Per-surface encoded-frame buffer |
|
||
| `packages/compositor/src/transport/BufferStream.ts` | Manages serial → frame promise resolution |
|
||
| `packages/compositor/src/render/Renderer.ts` | 4-phase render pipeline (WebGL2 + OffscreenCanvas) |
|
||
| `packages/compositor/src/render/Scene.ts` | One per output, manages view stack + textures |
|
||
| `packages/compositor/src/render/YUVA2RGBA.ts` | YUVA→RGBA FBO pass |
|
||
| `packages/compositor/src/render/YUVA2RGBAShader.ts` | BT.601 YUVA→RGBA shader |
|
||
| `packages/compositor/src/render/RGBXandA2RGBA.ts` | RGB+Alpha → RGBA FBO pass (WebCodecs output) |
|
||
|
||
### Browser side (REUSED MIT)
|
||
|
||
| Path | Role |
|
||
|---|---|
|
||
| `libs/compositor-wasm/src/index.ts` | pixman + xkbcommon WASM (region math + keymap) |
|
||
|
||
### Python side (custom, clean-room)
|
||
|
||
| Path | Role |
|
||
|---|---|
|
||
| `proxy/encoding/gst_encoder.py` | GStreamer H.264/PNG encoder (~700 LOC) |
|
||
| `proxy/encoding/adaptive_bitrate.py` | Adaptive bitrate controller (NEW) |
|
||
| `proxy/surface_buffer_encoding.py` | Encoding pipeline orchestration |
|
||
| `proxy/proxy_buffer.py` | Proxy-side buffer representation |
|
||
| `proxy/frame_feedback.py` | Receives 4-byte decode feedback |
|
||
| `proxy/encoding/glib_thread.py` | GLib MainLoop in separate thread + asyncio marshalling |
|
||
|
||
### Vendored Greenfield C files (MIT, used via cffi)
|
||
|
||
| Path | Role |
|
||
|---|---|
|
||
| `native/westfield-egl.c` | EGL zero-copy setup |
|
||
| `native/westfield-dmabuf.c` | DMA-BUF import via EGL/GBM |
|
||
| `native/wlr_drm.c` | `wl_drm` global impl |
|
||
| `native/wlr_linux_dmabuf_v1.c` | `zwp_linux_dmabuf_v1` global impl |
|
||
| `native/drm_format_set.c` | DRM format helpers |
|
||
|
||
### Reused (LGPL/BSD/MIT)
|
||
|
||
| Component | Role |
|
||
|---|---|
|
||
| PyGObject (`gi.repository.Gst`) | GStreamer Python bindings (LGPL-2.1+) |
|
||
| GStreamer core | Media pipeline framework (LGPL-2.0+) |
|
||
| gst-plugins-base, gst-plugins-good | PNG, appsrc, appsink, videoconvert (LGPL) |
|
||
| gst-vaapi | `vaapih264enc` for Intel/AMD (LGPL + MIT libva) |
|
||
| gst-plugins-bad (BSD subset) | `nvh264enc` for NVIDIA, `openh264enc` for CPU fallback |
|
||
| libopenh264 | OpenH264 BSD Wasm fallback decoder (BSD-2-Clause) |
|