Files
Piik/docs/research/cross-platform-client-runtime.md
T
AAA 01821ae7ac fix(connection): retry startup and clarify sharing status
Public invitations could stop after an early tunnel-process exit, and connection progress and Viewer status did not consistently describe the current attempt or Host state. This maintenance release adds bounded startup retries, aligns enhanced NAT retry feedback across entry paths, and fixes camera-preview and room-entry layout.

The existing route owner and published contracts remain in place. Four-attempt progress is advertised and opted into; older readers retain supported messages. Interaction/chat experiments are outside this PR.

Validation: npm run check (74 files, 1072 tests and production build), npm run check:go (including Windows capture compile), documentation/repository hygiene, 10 production Viewer-entry discovery cases, and a real public-link gate with remote HTTP/WebSocket access and shutdown cleanup all passed. Existing device/network evidence limits remain documented.

## Release notes

### 中文

这是一次维护更新,重点改善公网邀请启动、连接重试反馈和分享状态提示,并修正两处界面布局问题。

- 公网邀请服务在连接就绪前意外退出时,会自动重试,最多重试 3 次;所有尝试仍共用 30 秒启动预算。
- 增强穿透连接在首次尝试失败后最多重试 3 次,重试进度显示为 1/3、2/3、3/3。邀请链接和房间号加入都支持新版进度提示。
- 修正观众状态提示:房主离线时不再误报“没有可用的媒体线路”,服务器连接恢复与媒体恢复也会分别呈现。
- 修正电视内房间号输入界面的居中位置,以及摄像头选择预览被裁切的问题。
- 尚未确认来源声音状态时,不再提前提示“未分享来源声音”。

本次保持现有版本兼容;新版重试进度需要页面与服务端均支持。额外重试不能保证所有网络都能直连。

### English

This maintenance update improves public invitation startup, connection retry feedback, and sharing status messages, with two layout fixes.

- If the public invitation service exits before connecting, Piik retries up to three times within the existing shared 30-second startup budget.
- Enhanced NAT traversal now allows up to three retries after the initial attempt, shown as 1/3, 2/3, and 3/3. Both invitation links and room-code entry support the updated progress display.
- Viewer status now distinguishes an offline host from an unavailable media route, and reports server reconnection separately from media recovery.
- Fixed the alignment of room-code entry inside the TV and cropping in camera-selection previews.
- The missing-source-audio notice now waits until the source's audio availability is known.

Existing version compatibility is preserved. Updated retry progress requires support from both the page and server. Additional retries cannot guarantee a direct connection on every network.
2026-09-23 22:27:56 +08:00

15 KiB

Cross-Platform App Runtime Research

  • Reviewed: 2026-09-04
  • Scope: loopback control and self-contained packaging boundary
  • Status: App and Local composition implemented; Windows native Host video and a cross-NAT media gate proved

Reusable Evidence

Project Observed boundary Applicable lesson
Discord RPC Bounded local endpoints and an explicit handshake Discovery and session negotiation can stay small
OBS WebSocket Versioned Hello/Identify before commands Reject unknown protocol shapes instead of adding compatibility readers
Syncthing Go process with a loopback Browser GUI by default A native entry and system-Browser UI are a practical cross-platform pair
Sunshine Native streaming host configured through a localhost Browser UI Native media does not require a second desktop UI
LocalSend One peer exposes a local HTTP service without an external server LAN operation can be self-contained, while network reachability remains physical

These are architecture references only. No implementation or copyleft source is copied.

A read-only inventory of locally installed streaming clients adds one practical constraint. GameViewer separates its UI, supervised media service, codec capability detector, streamer, health process, and updater. Discord keeps its Chromium UI separate from a native voice/media module and updater. Oopz ships Flutter, a second WebView, FFmpeg, Agora RTC, and Tencent LiteAV together; that stack is substantially larger and duplicates media ownership. Piik follows the first two products' process separation but not their UI runtimes, and avoids Oopz's parallel RTC stacks: one Go process, one Pion media core, and one thin system capture sidecar per platform remain sufficient. No binary code or private application data was copied or inspected.

Current Control Boundary

The App binds IPv4 loopback on a bounded range (39721-39730). /health returns the protocol version, service identity, selected port, and an instanceToken. /control admits one WebSocket session whose subprotocol contains that same value. The first frame must be hello; subsequent requests are limited to capability discovery, source preview, and one generation-fenced share's media controls.

The instance token distinguishes a newly discovered process from a stale or unrelated listener. It is visible through health and stdout and is not an authentication secret. Host and Origin checks plus Chromium's Local Network Access permission form the Browser boundary.

The current protocol advertises only the native capabilities and generation-fenced media commands that have consumers. It carries no room credentials or route policy; those stay in the Browser/server protocol.

App Composition

The App and Hosted Piik run the same server core, so the local package is one executable carrying the built Browser assets plus the optional capture and tunnel sidecars; the App README owns that layout. In Local mode the App runs the server in its own process with explicit local configuration. In Site mode it starts no server, and the system Browser opens the saved Site while the same process remains its loopback native-media owner. The system Browser remains the UI.

Official Go process APIs require every started child to be waited and provide a bounded WaitDelay for cancellation and stuck I/O. That guidance now covers the capture and cloudflared sidecars only: the room authority is no longer a child process, and Local shutdown is one bounded in-process sequence that ends rooms before closing signaling and HTTP. Windows is physically exercised; macOS and Linux remain package gates.

Local And Hosted Reachability

A local page completed discovery and the full control exchange in Chromium 151.0.7922.138. The hosted Piik origin completed it after Chromium received its loopback-network permission; without that permission the cross-origin request was blocked. The gate checks both expected outcomes.

Run npm run probe:app-loopback with PIIK_CLIENT_LOOPBACK_GATE=true, CHROME_PATH, PIIK_CLIENT_EXE, and PIIK_CLIENT_GATE_SITE_URL pointing to the reachable HTTPS Site being tested. Use PIIK_CLIENT_GATE_GRANT_LOOPBACK=true for the permission-granted run; omit it for the permission-blocked run. The gate uses an isolated App configuration and Browser profile, and rejects navigation failures or redirects to another origin as acceptance evidence.

A self-contained LAN room uses a localhost Host URL and a selected LAN IPv4 Viewer URL backed by the same local App process. The App persists an optional user-chosen access password and passes it only to its Host page in a consumed fragment; a blank value leaves the Local site open. Explicit --link mode launches the packaged Cloudflare Quick Tunnel against that same local HTTP server and supplies its random HTTPS origin to the local server configuration before it starts listening. The public path therefore reuses the exact frontend, WebSocket signaling, RoomStore, and Viewer grant instead of adding a rendezvous or second client protocol. Cloudflare carries control traffic; WebRTC media uses public STUN and remains P2P-only.

UI Runtime Boundary

The current App opens the system Browser rather than embedding another Web runtime. A lightweight launcher chooses the authority mode, then navigates to the same Host page, where an App-launched Host can select an exact native window. This keeps the existing UI, Site cookies, WebRTC behavior, Browser updates, and permission model intact. Syncthing and Sunshine establish this as a practical native-process plus Browser-UI deployment shape.

An embedded shell remains a decision gate rather than a rejected category. Current WebView2 exposes getDisplayMedia screen-capture events and its Evergreen runtime updates independently; WebKitGTK exposes display capture and uses GStreamer for WebRTC. Electron instead bundles one Chromium and Node but makes the App responsible for shipping those updates and adds substantial package size. These facts do not prove equivalent Piik media behavior.

Only a reproduced Browser-launch failure should open a comparative physical gate for Electron, Tauri, Wails, Neutralino, or another shell. That gate must compare actual capture, audio, H.264/VP8 sender implementation, 1080p cadence, relay, background behavior, and resource use on every target platform. The result selects one UI runtime; it does not create parallel products.

Current Evidence

The runs below are pre-port evidence: they exercised the package that bundled a Node runtime and an extracted application tree. Their capture, discovery, media, and network results still apply; every package composition, startup, and shutdown claim must be re-verified on the single-binary App before that build is accepted.

On Windows with Chrome for Testing 151.0.7922.138:

  • the Local App started its built static application and reached /healthz;
  • the Host fragment was removed, existing SiteAccess became authenticated, and the ordinary Host page loaded;
  • the room API generated an invitation from the selected LAN origin;
  • one synthetic Host and three Viewers formed a P2P-only tree with one relay, every Viewer advanced decoded frames, and no SFU publication appeared; and
  • Enter-driven App shutdown released the Browser, App, Node, loopback and application ports, and disposable profile.
  • the immutable App release was assembled with Node 24.19.0 and the Go App from the same full revision; that packaged directory passed the Local and Site gates, while a deliberately mismatched app/REVISION was rejected before a listener started.
  • the explicit target assembler produced Windows amd64, Linux amd64, and macOS arm64 outputs from one clean revision. The Linux package ran its bundled Node Local authority on Ubuntu, and its full package exposed and then closed a public link reachable from another network. The macOS App and Node inputs are both Mach-O arm64; execution there remains unproved.
  • the native Host path created a Local room and a remote Pion Viewer received 30 packets over a selected srflx-to-srflx pair; the reverse SSH link in that gate carried signaling only.
  • a Windows App created an accountless Quick Tunnel, generated its ordinary invitation on that origin, and served the Viewer page plus /signal WebSocket upgrade to an independent Linux host; a native Host then repeatedly delivered 30+ H.264 RTP packets to a Linux Pion Viewer over selected direct paths using a reflexive candidate. Stopping the App closed the URL.

The one-link gate used the official Windows amd64 cloudflared 2026.8.3 asset with SHA-256 83e726ed18ea78c5ad5213c4c3a3a27051393950d2bc8ed4de69bec12d14eaae. The sidecar remains an explicit package input rather than a linked App dependency.

The peer topology gate now treats native getStats() RTP identity and frame totals as its portable core proof. Detailed Piik quality metrics enrich a development run when its source module is served, but their absence on a static deployment no longer erases valid native RTCStats.

Public Invitation Startup

The pinned cloudflared Quick Tunnel defaults to explicit QUIC, which disables its HTTP/2 fallback. Piik requests --protocol auto and --max-edge-addr-retries 0: try QUIC once, then let cloudflared fall back within the existing 30-second startup budget. This affects HTTP/WebSocket control only; media routes, tunnel identity and ordered shutdown keep their existing owners. DNS failures, API rejection and networks blocking both transports can still fail. The preceding Quick Tunnel allocation is a single HTTPS request with a 15-second dependency timeout; failure exits before transport fallback applies. The App's pre-readiness exit message and exit code do not identify that cause. Match the same run's public-tunnel dependency diagnostics before classifying a report. Piik retries a child that exits before readiness, up to four starts with a one-second pause between starts, all within the same 30-second acquisition deadline. The previous child, output readers and temporary config retire before the next start. Cancellation and local setup errors stop acquisition; a ready tunnel retains its original identity and explicit ordered-close owner.

A 2026-09-21 Windows check used the packaged binary with an isolated UDP sink and TCP forwarder. The previous arguments timed out at 30 seconds without trying TCP. The candidate connected in about 11 seconds, served the public page and echoed a WebSocket message, then joined the child on close. The ordinary-network App link gate also serves the invitation page and upgrades its WebSocket from an external US host, then confirms App exit and link retirement. These bounded checks do not establish regional reachability, uninterrupted connector availability or the cause of reports without matching diagnostics. Upstream Quick Tunnel setup and protocol fallback own the dependency behavior. An early-exit fixture confirms fourth-start recovery and bounded exhaustion; these checks do not identify a reporter's API or network failure or guarantee four starts when earlier attempts consume the deadline.

HTTP 1033 means Cloudflare cannot find a healthy tunnel connector. App readiness requires both the assigned origin and a registered connection; later reconnection belongs to cloudflared, while process exit ends that App's public-link runtime. This HTTP failure precedes WebRTC candidate exchange and is distinct from media-route exhaustion on an already reachable site.

Remaining Gates

  • Run the exact clean-revision package on macOS; Linux amd64 is proved.
  • Prove a physical second-device LAN Viewer on desktop, Android, and iOS.
  • Prove first-run Browser local-network permission outside CDP automation.
  • Record no-STUN mDNS behavior on ordinary and AP-isolated LANs.
  • Exercise the native Host on a configured Site and representative Browsers; the local SFU bridge is proved in isolation, while full Site recovery, representative audio, and non-Windows capture remain undefined.
  • Exercise one-link Browser media on a physical second device; the current gate proves the complete public control path and independent Pion media, not decoded Browser frames.

Sources