--- type: log topics: [streaming, latency, performance] status: living related: [measurement, downscale-tuning] --- # Streaming latency engineering log — tier1 goal: feels-like-direct responsiveness A running, append-only engineering log for tapflow's glass-to-glass latency work on tier1 (localhost + LAN). It records pipeline analysis, measurements, attempts, and decisions in chronological order — useful context for anyone touching the streaming render path. Unlike `.work/` (local, throwaway) it is committed and kept. > **Goal:** on tier1 (localhost + LAN), reach **localhost-JPEG-class responsiveness** (glass-to-glass > latency where operating the simulator/emulator in the browser feels nearly as direct as touching > it locally). This is the start of tapflow's core value: no-install, low-friction, low-latency > remote control. > > **Where it stands:** MSE was dropped for a 2-tier decoder path — WebCodecs on secure contexts > (HTTPS/localhost), WASM (tinyh264) on plain HTTP — reaching localhost-JPEG-class latency with no > media-element buffer. relay drop-to-keyframe removes tearing under LAN backpressure, and H.264 is > now the negotiated default. Same pipeline on iOS and Android. > > **Measurement tools:** `TAPFLOW_STREAM_METRICS=1` (agent throughput) · relay `ws backpressure` > (drops) · the per-stage `?perf=1` panel (capture→display). Environment: LAN + HTTP / localhost, > iPhone 16 Pro, macOS. --- ## 1. The problem — two demands at once Feels-like-direct responsiveness needs **both**: 1. **Low latency** (responsiveness) — the screen follows the instant you swipe. 2. **No drops** (smoothness) — no tearing from backpressure drops. So far no path has delivered both at once: | Path | Latency | Drops (bandwidth) | |------|------|--------------| | **JPEG** | ✅ low (frame-independent, instant decode) | ❌ large → LAN scroll relay drops 16–27/s (tearing) | | **H.264** | ❌ high (encoder + HW-decoder pipeline; on LAN, + MSE buffer) | ✅ small → drops 1–11/s | → **Only localhost-JPEG meets the bar.** LAN-JPEG tears; H.264 (local/LAN) misses the bar on latency. --- ## 2. Pipeline & bottleneck (glass-to-glass) | Stage | JPEG | H.264 (ours) | Cloud gaming | |------|------|--------------|------------------| | Touch send (browser→relay→agent→HID) | same | same | same | | Simulator scroll render | same (simulator-inherent) | same | — | | Capture (30fps polling) | 0–33ms | 0–33ms | — | | Encoder pipe | 0 (instant) | VT (minimized with MaxFrameDelayCount=0) | NVENC low-latency preset | | **Transport + buffer** | instant | **LAN = MSE media buffer ← the core bottleneck** | UDP/WebRTC mini jitter buffer | | Decoder | createImageBitmap, instant | WebCodecs (low-latency) / **MSE (buffered)** | HW low-latency decode | **Key insights:** - The bottleneck is not "H.264 vs JPEG" but the **decode/transport path**. **Cloud gaming hitting glass-to-glass <50ms with H.264 is the existence proof** — H.264 can deliver low bandwidth and low latency together. - **The real problem is using MSE on LAN-HTTP.** MSE is buffering by nature, so it is structural latency. Cloud gaming does not use MSE. - **Why we were stuck with MSE = the secure-context wall:** WebCodecs (zero-buffer, HW) only works on HTTPS/localhost → LAN-HTTP is non-secure, so it gets demoted to MSE. --- ## 3. Decoder tier model (decided — 2-tier, MSE removed) `pickDecoder` selects automatically per environment. **MSE is fully removed** and simplified to 2 tiers — WebCodecs if secure, else WASM. **Same pipeline** on iOS and Android: | Environment | Decoder | Characteristics | |------|--------|------| | HTTPS / localhost (secure) | WebCodecs | HW, lowest latency, all profiles | | **HTTP (non-secure)** | **WASM (tinyh264)** ✅ measured PASS | CPU, low-latency, zero-buffer, baseline only | | WebGL2/WASM unavailable | null (notice) | — | > **Why MSE was removed (2026-06-02):** MSE has a structural ~235ms from the `