## Problem and outcome / 问题与结果 Self-hosting currently requires assembling a Server installation, while App port mappings can use the wrong external port or remain active after cancellation. This phase adds a verified Linux x64 Server image and Compose recipe, and repairs the existing NAT-PMP dependencies so mapping results, cancellation and cleanup follow the gateway response and request lifetime. The image wraps the exact verified Server archive and is tested before publication. Version tags retain their original image; `latest` follows the latest published GitHub release. The two scoped dependency replacements retain upstream source, tests and licenses, with their removal criteria documented in `internal/thirdparty/README.md`. Planned release: **v1.3.0** (compatible Docker distribution feature plus NAT fixes). ## Release notes ### 中文 - 新增 Linux x64 Docker 镜像与 Compose 部署方式,内置网页、信令和 STUN,可按需开启 SFU 兜底。提供部署、更新与备份教程。 - 修复 App 的 NAT-PMP 端口映射:正确使用路由器分配的外部端口,并在关闭时尝试删除映射。 - NAT-PMP 请求会随取消或超时结束,避免连接尝试结束后继续在后台重试。 已有配置和房间数据格式保持兼容,更新无需迁移。 ### English - Added a Linux x64 Docker image and Compose setup with the Web UI, signaling, STUN and optional SFU fallback, plus deployment, update and backup guides. - Fixed App NAT-PMP mappings to use the external port assigned by the router and attempt mapping removal on shutdown. - NAT-PMP requests now stop on cancellation or timeout, preventing retries from continuing after a connection attempt has ended. Existing configuration and room data formats remain compatible; no migration is required. ## Verification / 验证 [Candidate CI](https://github.com/TNTcraftHIM/Piik/actions/runs/34761969362) passed for `1d8de6c4690434cd2cf44fbdee4f45b1776432fa`. The final two files changed after that run are documentation only (`docs/todo.md` and `docs/verification-status.md`), with repository hygiene checked separately. - Web type checks, unit tests and production build; Go core tests, vet and target builds; NAT and media-edge race detection. - Actual packaged-image checks with Docker Compose: production startup, embedded Web, UDP STUN, optional SFU startup, retained room authority across recreation, diagnostic ZIP export, non-root/read-only execution and clean shutdown. - Windows x64, Linux x64 and macOS arm64 App packaging. - Scoped NAT dependency regressions cover assigned ports, renewal, deletion, cancellation, lost replies and PCPv6 composition using real local UDP endpoints. The publishing pipeline reruns the image checks for the merged release revision. First publication also requires making the GHCR package public and checking anonymous pulls of its version and `latest` tags. Representative physical routers, public-media networks and operator-specific HTTPS/firewall settings remain outside the container smoke test. ## Impact / 影响 No signaling, native-control or storage-format change. Existing App/Server installations remain compatible. Docker supports `linux/amd64` in this phase; its default is P2P, and SFU remains an explicit deployment option. Existing deployments keep their configuration. README, bilingual self-hosting guides, container maintenance, versioning, dependency provenance and NAT evidence are updated. Completed feedback is removed from TODO; ongoing network evidence remains tracked there.
16 KiB
Engineering And Interface Boundaries
This is the shared engineering convention for Browser, App and Server work. AGENTS.md owns task authority and hard product constraints; CONTRIBUTING.md owns the delivery workflow. The required ponytail guidance selects the smallest correct implementation. This file owns module responsibilities and interface discipline, not a second copy of the product contracts or wire fields.
Repository Layout
Keep conventional project entry points and tool-discovered configuration at the
root. Scope-specific configuration belongs with its owner: src/tsconfig.json
checks the Browser, scripts/tsconfig.json checks tooling/tests, and root
tsconfig.json only connects those projects.
| Location | What belongs here |
|---|---|
| Root README, contribution and agent files | Project introduction and shared working constraints; detailed documents live in docs/ |
Root manifests, lockfiles, Vite/Vitest config, index.html |
Go/npm entry points and Browser build/test startup; root LICENSE is authoritative |
src/client, src/shared |
Browser UI/media orchestration and its shared wire types; the same UI serves Hosted and App modes |
public/ |
Static assets shipped with that Browser application |
cmd/, internal/ |
Go executable composition and private application, server, media and diagnostics packages |
internal/thirdparty/ |
Scoped Go dependency repairs with pinned upstream provenance, licenses and tests; removal criteria live beside the source |
native/capture, native/fixtures, native/probes |
Platform capture implementations, synthetic workloads and standalone capability probes |
site/ |
Independently published static introduction; its assets do not implement product behavior |
scripts/ |
Development checks, browser/media gates, dependency assembly and release packaging/publishing tools |
tests/ |
TypeScript/tooling tests and cross-language fixtures; Go tests stay beside their packages |
deploy/ |
Operator scripts and container, proxy, service and certificate templates |
docs/ |
User guides, contracts, decisions and operations; documentation ownership defines each owner |
licenses/ |
Third-party license texts, pinned notice sources and their redistribution reference |
.github/, .githooks/, .agents/, .codex/ |
Platform automation and scoped agent/tool integration |
build/, node_modules/, coverage/ |
Ignored local output and dependencies; never current product truth |
site/ introduces Piik; src/client/ is Piik's interactive application.
internal/app/nativecapture adapts the capture contract in Go; native/capture/
implements it using platform APIs. scripts/ prepares and verifies artifacts;
deploy/ operates them. Move files with their actual consumers, and update paths
and checks together; a smaller root alone does not justify another abstraction.
Module Map
| Owner | Responsibility and dependency boundary |
|---|---|
cmd/piik-server, cmd/piik-app |
Thin entry points: defaults, startup and shutdown; compose the same Go room service |
internal/server/app |
HTTP, site access, runtime capabilities, static assets and listener lifecycle; room mutations go through signal |
internal/server/signal |
Authenticated command/effect owner; serializes room, session and route mutations and executes controller decisions |
internal/server/room |
Room authority, credentials and persistence; caller holds the signaling lock, durable writes precede in-memory changes |
internal/server/route |
Synchronous room graph and single-operation decisions; returns resource effects to signal, owns no locks or timers, and calls its optional diagnostic sink synchronously |
internal/server/sfu, internal/app/mediaedge |
Server forwarding and native peer adapters over the shared internal/media/forwarding; Pion/LiveKit own transport and adaptation |
internal/app |
Local/public-link/Site composition, loopback service, native sessions and App lifecycle; never a second room backend |
internal/app/nativecapture, native/capture/* |
App-side capture contract and minimal platform adapters; platform-specific capture/encoding stays here |
src/client/pages, lib, media, native |
Browser orchestration and endpoint adapters; bind current media and retire owned resources |
src/client/media/viewer-presentation.ts, ui/media-status.ts |
Playback facts and their derived status projection; UI indicators do not create recovery or quality policy |
src/client/components, locales, ui |
Shared interaction, visual primitives and localized copy; same product interface for Server and App |
internal/server/webassets |
One built Browser bundle embedded by both binaries; Node/Vite are development tooling |
Names follow naming. Architecture decisions explain why these boundaries exist: shared core, embedded media and Browser pooling. Directory names are a map, not a requirement to introduce another layer.
Contract Map
| Boundary | Schema/validation owners |
|---|---|
| Room HTTP and Browser/Server signaling | src/shared/protocol.ts and internal/server/protocol; HTTP dispatch in internal/server/app, WS dispatch in signal |
| Site access HTTP | src/client/lib/api.ts and internal/server/app/json.go; cookies and access behavior belong to rooms/access |
| Browser/App discovery and control | src/client/native/wire.ts, internal/app/loopback/protocol.go, internal/app/nativecontrol/wire.go |
| App/capture sidecar | internal/app/nativecapture and each platform capture adapter; probe/commands and encoded-frame envelope are distinct formats |
| Configuration and persistence | internal/server/config, internal/app/config, internal/server/room/database.go; operator semantics in configuration |
| Release and update metadata | Existing packaging scripts, src/client/lib/release-update.ts, cmd/piik-server/release.go; identity/compatibility in versioning, operations in deployment |
The shared tests/fixtures/wire-samples.json is consumed by TypeScript and Go
fixture tests. It covers room requests and signaling; it is not a universal
schema for all HTTP, Native or capture traffic. Change the actual two ends and
the relevant existing fixture/check together. Do not create another handwritten
field list in documentation, parallel DTO hierarchy or schema generator without
a demonstrated reduction in ownership or drift.
Runtime Lifecycles
Read each runtime as acquisition, the authoritative commit that makes its work visible, and the retirement that ends it. Revalidate the acquiring identity after every await or callback; presentation derives from the committed fact instead of mirroring it.
| Runtime | Acquire | Commit | Retire |
|---|---|---|---|
| Browser Host share | pages/HostPage.tsx captures one source and creates its peers and route |
current generation plus the owning identity: local-offer epoch, publisher slot revision, source-switch and quality-change tokens | disposeResources stops owned tracks, disposes peers and sends stop-sharing; a replaced source retires through retiringStreamRef |
| Browser Viewer route | pages/ViewerPage.tsx binds one parent peer (Browser or Native) and its relay children |
parent, connection and message identity, then playback facts | a new viewer generation retires the old binding; relay children follow activateChildren revisions |
| Browser relay fanout | webrtc/viewer-relay.ts prepares a child before it is needed |
activateChildren(revision, childPeerIds) commits the prepared set |
replaced or emptied sets disconnect the child peers they own |
| Browser local encoding pool | webrtc/host-peer.ts attaches a pool carrier when capture and APIs support it (ADR-0014) |
the carrier binding owns its pooled producer while attached | source change or disposal releases the carrier; unsupported APIs keep ordinary senders |
| App native session | the loopback service admits native control sessions and the Web UI selects the native path | session identity plus the explicit media-path choice | session end, share end or process exit retires that edge and its capture sidecar |
| Server room authority | signal authenticates one Host or Viewer command |
room writes durably before the in-memory commit; the signaling lock serializes mutations |
explicit replacement or deletion, grant rotation or revocation, or process exit for memory-mode rooms |
| Server route and SFU | signal feeds route one serial child operation; sfu/admission.go accepts one bounded publication |
one committed graph per controller and the forwarding publication | superseded assignments retire inside the same operation; deactivate retires the publication |
Source Index
Start a change at the owning entry point, then follow the interfaces listed above rather than searching the tree.
| Change | Start at |
|---|---|
| Host workflow, share lifecycle, notices | src/client/pages/HostPage.tsx, src/client/pages/host-page-notices.ts |
| Viewer workflow, playback, status projection | src/client/pages/ViewerPage.tsx, src/client/media/viewer-presentation.ts, src/client/ui/media-status.ts |
| Browser peer and route mechanics | src/client/webrtc/host-peer.ts, viewer-peer.ts, viewer-relay.ts, src/client/media/host-sfu-route.ts, viewer-sfu-route.ts, route-transition.ts |
| Capture, profiles and sender parameters | src/client/media/quality.ts, src/client/media/browser-encoding-pool.ts, browser-encoding-output.ts |
| SFU endpoints | src/client/sfu/publisher.ts, subscriber.ts, peer.ts, internal/server/sfu |
| Localized copy and failure facts | src/client/locales, src/client/ui/copy.ts, src/client/ui/media-failure.ts |
| Native bridge, capture and media edges | src/client/native, internal/app/loopback, internal/app/nativecontrol, nativehost, nativeviewer, nativecapture |
| Room authority, persistence and graph | internal/server/signal, internal/server/room, internal/server/route |
| Shared wire contract | src/shared/protocol.ts, internal/server/protocol, tests/fixtures/wire-samples.json |
| Diagnostics and logging | src/client/lib/debug.ts, internal/diagnostics, configuration |
| Configuration and deployment | internal/server/config, internal/app/config, configuration, deployment |
Implementation Rules
- Reuse a current owner, standard API or mature dependency before adding a mechanism. Extract around a responsibility shared by real callers; file size alone does not justify wrappers, a generic manager or a framework. Remove superseded paths in the same change; retain no unused legacy copy. Keep test-only state seeding and setup in test harnesses. Do not export mutable resources solely for tests; preserve the production construction path when verifying defaults or restoration.
- Review dependency direction for source/type imports as well as runtime imports. Shared types belong to a common owner. A type-only cycle is an ownership concern; it does not by itself establish a runtime or performance defect.
- State has one writer and a clear lifetime. Separate permission, capability, connection readiness, requested settings, applied settings and observation. Derive presentation from facts; do not synchronize parallel booleans or let a cached failure outlive its evidence. Cumulative observations need a valid baseline for the resource generation they describe; missing measurements are not zero. Serialized recovery reads current committed intent when it executes rather than replaying a queued snapshot.
- The operation that acquires a connection, clone, listener or queue owns its retirement. After an await/callback, and inside cleanup, validate the original operation/resource identity before changing current state. Cancellation, failure, missing observation and success must retain distinct meanings. A caller deadline does not cancel a dependency that ignores it. Keep late work bounded and serialize its cleanup; verify the dependency's actual lifetime.
- Keep decisions separate from effects. Respect documented lock ordering and revalidate after unlocked work. Local native capability never grants room authority; the shared Go service remains authoritative. Injected callbacks still execute in their caller's context; include their blocking I/O in the caller's lock and latency analysis.
- Validate input at the boundary, keep domain error meanings stable, and map them into shared localized UI. Versioning determines which extensions are compatible; an optional field is not automatically compatible. Retained media state carries failure facts; the presentation layer resolves localized copy during render. Keep raw exceptions in diagnostics. Transient event notices may resolve once when the event occurs. Values with a defined canonical form, such as HTTP origins, use one shared normalizer before storage and comparison. Verify the actual producer and consumer together instead of weakening an authorization comparison.
- Deployment configuration describes available services; room preferences request allowed behavior. The server enforces the policy even when a client requests more. The UI retains the same controls and explains locked choices. See configuration; avoid a second flag with the same job.
- Browser/App/Server share visual language and status projection. A new page, locale or deployment does not invent a new palette, status model or user-role metaphor.
- Keep package comments about current responsibilities and invariants. Historical
migration paths belong in ADR/research unless they explain a surviving constraint.
Support Windows, macOS and Linux; new text files use LF and new filenames use
ASCII, with format exceptions in
.gitattributes. Scripts use stable executable paths; physical confidence stays explicit in verification status. - Retained tools declare their direct package dependencies. Documented TypeScript tools join the normal type-check entry point; a transitive dependency or a one-off manual check is not an owned toolchain contract. Shipped templates use reserved example values. Checks provision isolated resources or require explicit targets; they must not depend on a maintainer's workstation, private deployment or machine-specific infrastructure names.
Ablation And Review
Judge a change by verified user value against implementation, maintenance, compatibility, runtime and failure cost. After a material module, remove any new state, dependency, branch or abstraction that the accepted behavior does not need. A small performance gain does not justify permanent complexity; do not reduce viewing quality or requested functionality merely to save lines.
A concrete reduction in duplicated writers, cross-module dependencies or repeated edit sites can justify structural refactoring without a reproduced runtime failure. Name that gain and its regression risk. A completed audit is bounded evidence, not a claim that every current module boundary is optimal.
Before changing production recovery or state, trace the real request producers and their guards. A test that bypasses disabled controls or writes private state does not alone establish a reachable product defect. Reference scans, including hits from tests and previews, are leads rather than proof of product use or absence. Follow the actual producer-to-consumer path and check the changed boundary; isolated helper tests can miss a mismatch between the two ends.
Trace acquisition, use, commit and retirement, including replacement and failure. For async ownership, check A starting, B replacing it, then A completing or cleaning up. Where compatibility is promised or being established for public release, check actual old/new readers as well as the happy path. Ordinary private changes do not require support for stale private clients. Run proportionate checks through the standard entry points. This discipline does not authorize an unrelated repository rewrite or a new test suite for every helper.