diff --git a/.env.example b/.env.example index 3e7a5bc..a06eca9 100644 --- a/.env.example +++ b/.env.example @@ -8,14 +8,15 @@ PUBLIC_BASE_URL=http://localhost:8787 ALLOWED_ORIGINS=http://localhost:8787 # Optional site access password in every environment. Unset or empty allows # entry without a site password; room ownership and invitations still apply. -# A non-empty value must contain 8-128 visible ASCII bytes (0x21-0x7e). +# Non-empty values are matched exactly; spaces and Unicode are supported. SITE_ACCESS_PASSWORD= # Rooms persist in rooms.sqlite under the working directory by default (also # when blank). Set :memory: explicitly for process-memory rooms. Deployments # should use an access-restricted absolute path outside the release directory, # preserving the existing database and its writable parent across updates. ROOM_DATABASE_PATH= -MAX_VIEWERS_PER_ROOM=8 +# Viewers per room, excluding the Host. Valid range: 1..20; default: 20. +MAX_VIEWERS_PER_ROOM=20 # Bounded standard-WebRTC relay/SFU controller is active for every room. # One steady outbound media-copy capacity for every non-server endpoint. # A peer child or Host SFU publication consumes one copy. Valid values: 1, 2, 3. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45afc96..9314547 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,9 +96,9 @@ Confirm runner inclusion when adding or moving a test; a file's presence alone does not establish coverage. - `npm run check` owns deterministic Web type-check, unit, and build acceptance. -- `npm run check:client` owns Go formatting, unit tests, vet, three-platform - builds, and the Windows capture compile/probe when run on Windows. The server - core is Go, so its acceptance runs here. It builds the Vite client first when +- `npm run check:client` owns Go formatting, unit tests, vet, Windows/Linux + builds, Darwin builds on macOS, and the current platform's capture compile/probe. + The server core is Go, so its acceptance runs here. It builds the Vite client first when that output is missing, because both binaries embed it. CI invokes these same package commands rather than rebuilding their steps in YAML. - `npm run check:client-race` checks port mapping, media-edge ownership and their diff --git a/README.md b/README.md index 57f5746..3f36167 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Website · Download · Gitee mirror · - Try the demo · + Use online · Documentation

@@ -50,11 +50,12 @@ watch in their browsers. **To watch:** open the invitation your friend sends you. Select **Play** if the picture does not start automatically. -**To share:** +**To share:** download **Piik App** to get started. To share without installing, +you can also create a room in the online version. | Start here | What you need to do | | --- | --- | -| [Try the online demo](https://demo.piik.tv) | Open it in a desktop browser and start sharing. | +| [Use Piik online](https://demo.piik.tv) | Open it in a desktop browser to create a room and share. | | [Download Piik App](https://piik.tv/#download) | Extract and open the App, choose **Public invite**, then **Open Piik** to create a temporary room. | | Use an existing Piik site | Open the address your friend or administrator provides. [Hosting your own](./docs/operations/self-hosting.md) is an advanced option. | @@ -76,13 +77,13 @@ Download a ZIP beginning with **`piik-app`**. The filename also identifies the p | `darwin-arm64` | Apple silicon; native capture requires macOS 13+ | | `linux-amd64` | Linux x64 | -[**Full setup guide →**](./docs/guide/getting-started.md) · [**Open the demo →**](https://demo.piik.tv) +[**Full setup guide →**](./docs/guide/getting-started.md) · [**Open online →**](https://demo.piik.tv) Browser capture requires HTTPS or `localhost`. Windows App and desktop browser sharing are the primary tested paths. The macOS and Linux apps have not yet been tested on physical devices; [test results and feedback are welcome](https://github.com/TNTcraftHIM/Piik/issues). P2P-only modes, including the App's temporary public -link and the demo, may not connect on restrictive networks. +link and the online version, may not connect on restrictive networks. ## Self-hosting diff --git a/README.zh-CN.md b/README.zh-CN.md index 02d1250..8520816 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -5,7 +5,7 @@ 官网 · 下载 · Gitee 镜像 · - 在线演示 · + 在线版 · 文档

@@ -48,11 +48,11 @@ Piik 是一个免费开源的屏幕共享工具,适合游戏围观、一起看 **观看分享:** 打开朋友发来的邀请链接。画面没有自动播放时,点击**播放**即可。 -**分享画面:** +**分享画面:** 推荐下载 **Piik App**。想免安装,也可以打开在线版直接创建房间。 | 从这里开始 | 需要做什么 | | --- | --- | -| [体验在线 Demo](https://demo.piik.tv) | 在电脑浏览器打开,开始分享。 | +| [使用在线版](https://demo.piik.tv) | 在电脑浏览器打开,直接创建房间并分享。 | | [下载 Piik App](https://piik.tv/?lang=zh-CN#download) | 解压并启动客户端,选择**公网邀请**,点击**进入 Piik**,创建临时房间。 | | 使用已有 Piik 站点 | 打开朋友或管理员提供的地址。[自行部署](./docs/operations/self-hosting.zh-CN.md)属于进阶操作。 | @@ -73,11 +73,11 @@ Piik 默认跟随系统语言,中文系统显示简体中文,其他语言显 | `darwin-arm64` | Apple 芯片;原生采集需 macOS 13 及以上 | | `linux-amd64` | Linux x64 | -[**完整使用教程 →**](./docs/guide/getting-started.zh-CN.md) · [**打开在线演示 →**](https://demo.piik.tv) +[**完整使用教程 →**](./docs/guide/getting-started.zh-CN.md) · [**打开在线版 →**](https://demo.piik.tv) 浏览器采集需要 HTTPS 或 `localhost`。目前主要测试 Windows 客户端和桌面浏览器分享。 macOS 和 Linux 客户端尚未经过实机测试,欢迎有设备的朋友试用并[反馈问题](https://github.com/TNTcraftHIM/Piik/issues)。 -客户端公网邀请和演示站使用纯 P2P 连接, +客户端公网邀请和在线版使用纯 P2P 连接, 在受限网络下可能无法连通。 ## 自行部署 diff --git a/cmd/piik-app/README.md b/cmd/piik-app/README.md index 80e563a..3867002 100644 --- a/cmd/piik-app/README.md +++ b/cmd/piik-app/README.md @@ -1,5 +1,7 @@ # Piik App +English · [简体中文](./README.zh-CN.md) · [Getting started](../../docs/guide/getting-started.md) + Piik App provides local rooms and native screen capture through the Piik web interface in your system Browser. @@ -11,6 +13,9 @@ Linux, or `Piik App.app` on macOS. The launcher opens in the system Browser. Linux native capture uses the system dependencies described in the [Linux capture guide](../../native/capture/linux/README.md). +If the page does not open automatically, open the address shown in the terminal +or press **O** there to retry. Keep the App running while using that page. + Windows App and browser sharing are the primary tested paths. The macOS and Linux apps have not yet been tested on physical devices; test results and [feedback](https://github.com/TNTcraftHIM/Piik/issues) are welcome. macOS native @@ -22,8 +27,11 @@ publication are separate steps in [deployment](../../docs/deployment.md). - With no mode argument, the App opens a small launcher in the system Browser. It selects Local, a temporary public HTTPS invitation, or a saved Site, then enters the normal Host page. -- The launcher remembers the Site address but keeps the per-run room source - separate. The App RPC starts before this choice and accepts the saved Site +- The launcher remembers the last mode confirmed with **Open Piik** and the saved + Site address. Without a saved mode, it selects Site when an address is saved, + otherwise Public invite; it waits for confirmation before starting. + [App configuration](../../docs/reference/configuration.md#piik-app-configuration) + owns the saved settings. The App RPC starts before this choice and accepts the saved Site alongside the Local Host origin. An App-opened Site remembers the opt-in in that Browser origin; later manually opened Host pages may reuse the running App. - The `--site`, `--local`, and `--link` flags select a mode for CI and development. @@ -80,7 +88,7 @@ persistent quality evidence and route replacement. See [media quality](../../docs/product/media-quality.md) for the implemented behavior and [status](../../docs/status.md) for its acceptance limits. -The App configuration keeps an optional Local site-access password. Leave it +The App configuration keeps an optional Local site passphrase. Leave it blank for an open Local site, or enter a password in the launcher before starting. When present, the App passes it to its own Host page in a URL fragment; the page uses the existing SiteAccess endpoint and @@ -138,15 +146,32 @@ log locations, export commands, retention and privacy boundaries. ## Development -The binary embeds the Browser assets, so build them before running it: +Use the Node/npm/Go versions in [Run from source](../../docs/README.md#run-from-source). +The binary embeds the Browser assets, so build them from the repository root first: ```sh +npm ci npm run build:client -go test ./... -go vet ./... +``` + +On Linux or macOS: + +```sh go run ./cmd/piik-app ``` +On Windows, build and run from a stable executable path for the system firewall: + +```powershell +go build -o build/dev/piik-app.exe ./cmd/piik-app +./build/dev/piik-app.exe +``` + +These commands build the App only. Choose **Local room** or a configured Site +for Browser capture. Native capture and **Public invite** need their helpers; +use `--capture-process` / `--tunnel-process` to select built helpers, or follow +[packaging](#packaging) for a complete bundle. + The repository-level entry used locally and by CI is: ```sh @@ -163,8 +188,8 @@ The Darwin App and peer gate build only on macOS with cgo enabled and an installed SDK; other hosts report that skipped platform explicitly. The pinned media dependency's Darwin CPU statistics use Mach APIs through cgo, so a Windows or Linux core check does not establish Darwin build acceptance. -Each target compiles its isolated capture process and validates its bounded -capability response. macOS additionally encodes one in-memory hardware H.264 +The check compiles the current platform's isolated capture process and validates +its bounded capability response. macOS additionally encodes one in-memory hardware H.264 IDR; Linux probes the Portal/PipeWire/GStreamer adapter. Real capture, GPU attribution, Browser decode, and public-network paths remain explicit physical gates rather than environment-dependent unit tests. @@ -198,9 +223,7 @@ verifies the pinned public-link helper, and assembles and checks the App: ```sh node scripts/package-app-release.mjs /outside/repository/app-release -node scripts/package-client-candidate.mjs \ - /outside/repository/app-release windows-amd64 \ - /outside/repository/client-candidate +node scripts/package-client-candidate.mjs /outside/repository/app-release windows-amd64 /outside/repository/client-candidate ``` Supported targets are `windows-amd64`, `linux-amd64`, and `darwin-arm64`. @@ -223,8 +246,9 @@ runtime/tunnel/cloudflared[.exe] # packages that support --link ``` The executable embeds the Browser assets of the consumed application release and -carries that same full Git revision as the package `REVISION`. No compatibility -reader accepts a mismatched private build. +carries that same full Git revision as the package `REVISION`. App/Site +interoperability follows the +[public compatibility contract](../../docs/reference/versioning.md#public-compatibility-promise). The Windows App embeds the shared Piik mark through the `cmd/piik-app/piik_windows_amd64.syso` resource; the platform @@ -236,8 +260,8 @@ with an ICNS resource; the raw Go executable remains available beside it. For a native Host smoke run, launch the App and select a window in the Host page. The App never guesses among multiple targets. The page still creates the room and sends the current SDP/ICE through the selected authority. A -configured Site supplies its normal Internet routing and SFU fallback. Without -a Site, the **Public invite** choice exposes the Local control surface while +configured Site supplies its normal Internet routing and any enabled SFU fallback. +Without a Site, the **Public invite** choice exposes the Local control surface while media remains P2P-only. ## Gates diff --git a/cmd/piik-app/README.zh-CN.md b/cmd/piik-app/README.zh-CN.md new file mode 100644 index 0000000..35755af --- /dev/null +++ b/cmd/piik-app/README.zh-CN.md @@ -0,0 +1,276 @@ +# Piik App + +[English](./README.md) · 简体中文 · [上手指南](../../docs/guide/getting-started.zh-CN.md) + +Piik App 通过系统浏览器中的 Piik 网页界面,提供本地房间与原生屏幕采集。 + +## 运行程序包 + +完整解压对应平台的程序包,保留 App 程序旁的 `runtime` 目录。 +Windows 运行 `piik-app.exe`,Linux 运行 `./piik-app`,macOS 打开 `Piik App.app`。 +启动页会在系统浏览器中打开。Linux 原生采集所需的系统依赖见 +[Linux 采集指南](../../native/capture/linux/README.md)。 + +网页没有自动打开时,可手动打开终端中显示的地址,或在终端按 **O** 重试。 +使用网页期间,请保持 App 运行。 + +目前主要测试 Windows 客户端和浏览器分享。macOS 与 Linux 客户端尚未经过实机测试, +欢迎试用并[反馈结果](https://github.com/TNTcraftHIM/Piik/issues)。macOS 原生采集需要 +Apple 芯片及 macOS 13 或更新版本。构建程序包与公开发布 Release 是两个独立步骤, +详见[部署与发布](../../docs/deployment.md)。 + +## 运行模式 + +- 不传模式参数时,App 在系统浏览器中打开启动页。选择本地房间、临时公网 HTTPS 邀请 + 或已保存的站点后,进入通常的房主页面。 +- 启动页会记住上次点击 **进入 Piik** 确认的模式,以及保存的站点地址。 + 没有已保存的模式时,有站点地址就默认选中站点,否则选中公网邀请;确认后才启动。 + 保存项的规则见 [App 配置](../../docs/reference/configuration.md#piik-app-configuration)。 + App RPC 在选择前就已启动,同时接受已保存站点和本地房主来源的访问。 + 通过 App 打开的站点会在该浏览器来源下记住启用 App 的选择,之后手动打开的房主页面 + 可以继续使用正在运行的 App。 +- `--site`、`--local` 和 `--link` 用于在 CI 或开发时直接选择模式。 +- 默认启动页打开后会检查官方发布,优先使用 GitHub,GitHub 不可用时使用 Gitee。 + 有对应平台的 ZIP 时直接打开下载地址,否则打开发布页。App 不会自行安装或替换程序。 + +本地模式的房间只保存在内存中,使用 P2P 中继,不启动 SFU 监听器,也不使用公网发现服务。 +普通本地模式适用于设备间可互相访问的局域网。**公网邀请** 模式运行程序包内的 +Cloudflare Tunnel 辅助进程,仅暴露已有的 HTTP/WebSocket 控制服务;同时使用一个普通公网 +STUN 地址和两个用于有限范围 NAT 探测的公网地址。浏览器与原生连接使用相同的预测规则; +原生 STUN、ICE 检查和媒体共用一个 Pion UDP mux。媒体不会经过 HTTP 隧道, +难以连通的媒体路径也没有 SFU 或 TURN 兜底。 + +本地邀请会自动选择唯一的活动地址或唯一的私有 IPv4 地址。仍有多个可能地址时, +请在启动页选择网卡和 IP。公网邀请和站点模式无需选择局域网地址。 +通过 `--local` 跳过启动页时,可用 `--lan-address

` 指定地址来解决多地址歧义。 +App 会在启动时再次检查所选地址;它只影响本地邀请链接,媒体路径仍由 ICE 独立选择。 + +系统浏览器始终是房主操作界面。通过 App 启动的房主页面同时提供浏览器标准采集弹窗, +以及平台上的具体采集目标列表,由用户明确选择。Windows 同样提供 VP8/Auto/H264 选择: +VP8 使用固定版本的 WebRTC/libvpx 编码器,H264 在 WebRTC 输出管线内使用硬件 +Media Foundation。Auto 会测量目标画质配置下的编码工作量,再为本次分享选定一种编码。 +Windows 使用 Graphics Capture 与 WASAPI;macOS 使用 ScreenCaptureKit、VideoToolbox +和 AudioToolbox;Linux 通过 ScreenCast Portal 选择来源,使用系统 PipeWire/GStreamer +硬件路径。 + +视频与音频共用房间路由和 PeerConnection。原生采集按房主当前的分辨率、帧率、 +视频与音频码率及画质偏好启动;分享中修改设置只替换这些稳定连接后的采集与编码代次。 +使用公网 STUN 的原生连接还会为其 Pion UDP socket 尝试一次有界的 PCP、UPnP 或 NAT-PMP +端口映射,纯局域网模式不会执行。路由器没有映射服务时,继续使用普通 ICE/STUN。 +映射不会建立中继,也不会让媒体经过 App 控制连接。配置的站点可以从共享编码源直接接收 +原生发布;本地与公网邀请模式仍只使用 P2P。普通网页房主保留浏览器采集路径,不主动探测 App。 + +原生 P2P 连接与内嵌 SFU 共用一套 Pion/LiveKit 媒体适配层,负责反馈、转发分配、 +有界发送节奏和恢复。原生父节点复用合适的 H.264/VP8 输出,仅在直接子节点需要时生成 +缺少的较低输出。兼容的子节点共用该输出,每条连接只接收为它选定的表示。 +SFU 发布将其请求的输出前缀与一个聚合上行预算结合使用。较低输出的限制不会替换原始输入, +也不会替换其他同级连接使用的较高输出。路由控制器负责持续质量证据与路由替换。 +具体行为见[媒体质量](../../docs/product/media-quality.md),验收边界见[当前状态](../../docs/status.md)。 + +App 配置保存可选的本地站点口令。留空表示本地站点开放进入;需要口令时,在启动页填写后再启动。 +设置口令后,App 通过 URL fragment 将它交给自己的房主页面,页面调用已有的 SiteAccess +接口,并在继续前移除 fragment。观众邀请仍使用既有的房间级授权。 + +终端显示当前模式、入口链接与启动状态,语言跟随启动页和已启用 App 的页面。 +按 `o` 重新打开浏览器,按 `q` 或 Ctrl+C 结束本地房间,停止本地服务与临时公网链接。 +纯文本输出时使用 Ctrl+C。关闭站点标签页后,App 仍会继续运行。 +App 在独立 Windows 控制台中启动时,启动或运行错误会保留在窗口中,按 Enter 后退出。 +正常关闭、在已有终端中运行、重定向输出或自动化运行时,会直接退出。 + +临时公网分享时,打开 App 启动页,选择 **公网邀请**,在随后打开的浏览器中创建房间, +发送通常的邀请链接即可。随机的 `trycloudflare.com` 来源仅在本次 App 运行期间有效, +下次启动会创建新链接。Cloudflare Quick Tunnels 不保证持续可用; +需要持久的控制服务或 SFU 兜底时,请使用配置好的站点。 + +## 问题排查 + +### Chromium WebRTC 连接 + +**为什么屏幕采集成功了,媒体连接却失败?** + +基于 Chromium 的浏览器可能通过浏览器设置、扩展或管理策略限制 WebRTC UDP。 +禁止未经代理的 UDP,甚至会阻止本机浏览器与 App 之间的媒体连接; +采集成功或页面能打开,并不能证明这条独立连接可用。 + +检查浏览器的 WebRTC/IP 处理策略,以及扩展中的 WebRTC 或 IP 泄露保护设置。 +恢复允许 WebRTC UDP 的策略,重新加载 Piik,再确认没有其他扩展或管理策略覆盖该选择。 +各浏览器的设置名称与可用选项不同。例如,Vivaldi 提供 +**Settings > Privacy and Security > WebRTC IP Handling > Broadcast IP for Best WebRTC Performance**。 +修改这项策略可能向 WebRTC 对端暴露网络地址,请不要同时关闭无关保护。 + +参考 [Chromium 扩展策略 API](https://developer.chrome.com/docs/extensions/reference/api/privacy#property-network)、 +[Vivaldi 设置说明](https://help.vivaldi.com/desktop/privacy/privacy-settings/)和 +[已核验的策略机制与实际案例](../../docs/research/native-client-lifecycle.md)。 + +### 诊断 + +在启动页开启 **诊断启动**,进入 Piik 并复现问题,再在交互终端按 `D` 导出本地 ZIP。 +启动页出现前就失败时,使用 `--debug`。导出不会停止分享或上传文件。 +浏览器诊断通过主题按钮后的 **诊断** 芯片图标开启:确认重新加载后,使用 **网页报告** 下载。 +调查浏览器与 App 配合的问题时,请提供同一次复现的两份报告。 +[诊断参考](../../docs/reference/configuration.md#diagnostics)说明日志位置、导出命令、保留规则与隐私边界。 + +## 开发 + +Node/npm/Go 版本见[从源码运行](../../docs/README.md#run-from-source)。 +程序会内嵌浏览器资源,请先在仓库根目录构建: + +```sh +npm ci +npm run build:client +``` + +Linux 或 macOS: + +```sh +go run ./cmd/piik-app +``` + +Windows 使用固定的可执行文件路径,便于系统防火墙识别: + +```powershell +go build -o build/dev/piik-app.exe ./cmd/piik-app +./build/dev/piik-app.exe +``` + +这些命令只构建 App。体验浏览器采集时,选择 **本地房间** 或配置好的站点。 +原生采集和 **公网邀请** 需要各自的辅助程序,可用 `--capture-process` / `--tunnel-process` +指定已构建的程序,或按[打包](#打包)步骤生成完整程序包。 + +本地与 CI 共用的仓库级检查入口是: + +```sh +npm run check:client +``` + +在 Windows 上,该入口将 App、采集及媒体测试程序写入已忽略的仓库 `build/client-check` +目录,并在下次运行时复用路径,保持系统防火墙识别的可执行文件身份稳定。 +这些文件是本地构建输出,不会打包或提交。 + +检查包括 Go 格式、单元测试、vet,以及禁用 cgo 的 Windows/Linux 交叉构建。 +Darwin App 和 peer gate 仅在安装 SDK 的 macOS 上启用 cgo 构建;其他系统会明确报告跳过该平台。 +固定版本的媒体依赖在 Darwin 上通过 cgo 调用 Mach API 获取 CPU 统计, +因此 Windows 或 Linux 上的核心检查不能证明 Darwin 构建通过。 +检查会编译当前平台的独立采集程序,并验证有界的能力响应。macOS 还会在内存中编码一个硬件 H.264 +IDR;Linux 会探测 Portal/PipeWire/GStreamer 适配层。真实采集、GPU 归因、浏览器解码 +和公网路径由明确的实机验证负责,不作为依赖环境的单元测试。 + +loopback 服务在 IPv4 回环地址的 `39721` 到 `39730` 中绑定第一个可用端口。 +`/health` 用于发现当前进程;`/control` 最多接纳两个相互独立、使用严格 v9 协议的控制会话。 +完成 `hello` 后,每个会话可以列出本地采集选项、管理一个按代次隔离的房主分享, +或接收一个原生观众来源及其数量受限的编码子连接。关闭会话只回收该会话的资源。 +公开的 `instanceToken` 用于区分发现的进程,不是身份认证凭据。 +共享 Go 服务负责房间权威状态,浏览器负责房间协议客户端,并通过控制会话协调原生媒体。 +即使本机没有可用的原生采集编码器,观众接收与中继仍可使用。 +采集辅助程序必须匹配 App 当前的探测与编码输出协议;候选打包脚本会先验证版本,再接受产物。 + +打包版本会在终端和诊断上下文中报告产品版本与源码 SHA。启动页用它们区分更新版本、 +同版本的不同构建,以及向开发构建提供的正式发布;更新链接不会安装程序或打断分享。 +这些含义由[版本规则](../../docs/reference/versioning.md)管理,自动发布由 +[GitHub 运维](../../docs/operations/github.md)说明。 + +## 打包 + +从干净的源码版本开始,构建应用发布产物,再在目标平台的操作系统上运行候选打包脚本。 +脚本会构建采集程序、验证固定版本的公网链接辅助程序,并组装和检查 App: + +```sh +node scripts/package-app-release.mjs /outside/repository/app-release +node scripts/package-client-candidate.mjs /outside/repository/app-release windows-amd64 /outside/repository/client-candidate +``` + +支持的目标为 `windows-amd64`、`linux-amd64` 和 `darwin-arm64`,请替换命令中的目标名称。 +Darwin 组装需要原生 macOS runner 与 SDK,并启用 cgo;Windows 与 Linux 组装禁用 cgo。 +结果是 ZIP 程序包和 SHA-256 文件。手动组装辅助程序、显式 CI 打包、Release 发布与更新, +见[部署与发布](../../docs/deployment.md);创建候选包不会将它发布。 + +解压后的目录包含: + +```text +piik-app[.exe] +REVISION +LICENSE +THIRD-PARTY-NOTICES.txt +runtime/native/piik-capture[.exe] # 支持原生媒体的程序包 +runtime/tunnel/cloudflared[.exe] # 支持 --link 的程序包 +``` + +App 内嵌所用应用发布产物的浏览器资源,程序内的完整 Git 版本与包内 `REVISION` 一致。 +App 与站点之间的互操作遵循[公开兼容规则](../../docs/reference/versioning.md#public-compatibility-promise)。 + +Windows App 通过 `cmd/piik-app/piik_windows_amd64.syso` 资源内嵌共享的 Piik 标识; +平台后缀使该 Windows 资源不参与 Linux 和 macOS 构建。 +Linux 程序包包含标准的 `share/applications` 桌面入口与 hicolor 图标。 +macOS 程序包包含一个带 ICNS 资源的轻量 `Piik App.app` 启动器,原始 Go 程序仍保留在它旁边。 + +执行原生房主冒烟检查时,启动 App 并在房主页面选择一个窗口;有多个目标时,App 不会代替用户猜选。 +页面仍通过选定的房间权威服务创建房间、发送当前 SDP/ICE。配置好的站点提供通常的公网路由, +以及已启用的 SFU 兜底。没有站点时,**公网邀请** 会暴露本地控制服务,媒体仍只使用 P2P。 + +## 验证入口 + +Local gate 会启动已打包进程,在 Chromium 中加载真实构建的页面,验证启动访问与局域网 +邀请构造,并检查进程和浏览器配置的完整清理。loopback gate 分别验证授予和未授予 +Chromium Local Network Access 权限时的站点访问。以下环境变量语法用于 POSIX shell; +Windows 请使用等效的环境变量设置方式。 + +```sh +PIIK_CLIENT_LOCAL_GATE=true \ +CHROME_PATH=/path/to/chrome \ +PIIK_CLIENT_EXE=/path/to/piik-app \ +PIIK_CLIENT_GATE_LAN_ADDRESS=192.168.1.10 \ +npm run gate:client-local + +PIIK_CLIENT_LOOPBACK_GATE=true \ +CHROME_PATH=/path/to/chrome \ +PIIK_CLIENT_EXE=/path/to/piik-app \ +npm run probe:client-loopback + +PIIK_CLIENT_MEDIA_GATE=true \ +CHROME_PATH=/path/to/chrome \ +PIIK_CLIENT_GATE_STUN_URLS=stun::3478 \ +npm run gate:client-media + +PIIK_CLIENT_NATIVE_HOST_GATE=true \ +CHROME_PATH=/path/to/chrome \ +PIIK_GO=/path/to/go \ +npm run gate:client-native-host + +PIIK_CLIENT_NATIVE_HOST_GATE=true \ +PIIK_CLIENT_CROSS_NAT_GATE=true \ +CHROME_PATH=/path/to/chrome \ +PIIK_GO=/path/to/go \ +PIIK_REMOTE_HOST= \ +PIIK_REMOTE_USER= \ +PIIK_REMOTE_SSH_KEY=/path/to/key \ +PIIK_CLIENT_GATE_STUN_URLS=stun::3478 \ +npm run gate:client-native-host + +PIIK_CLIENT_NATIVE_HOST_GATE=true \ +PIIK_CLIENT_LINK_MEDIA_GATE=true \ +CHROME_PATH=/path/to/chrome \ +PIIK_GO=/path/to/go \ +PIIK_CLOUDFLARED=/path/to/cloudflared \ +PIIK_REMOTE_HOST= \ +PIIK_REMOTE_USER= \ +PIIK_REMOTE_SSH_KEY=/path/to/key \ +npm run gate:client-native-host + +PIIK_CLIENT_LINK_GATE=true \ +PIIK_GO=/path/to/go \ +PIIK_CLOUDFLARED=/path/to/cloudflared \ +PIIK_REMOTE_HOST= \ +PIIK_REMOTE_USER= \ +PIIK_REMOTE_SSH_KEY=/path/to/key \ +npm run gate:client-link +``` + +loopback 健康响应分别报告视频、进程音频、系统音频和硬件 H.264 是否可用。 +Windows media gate 验证一个硬件 H.264 采集代次、共享 Pion 来源、浏览器解码、PLI 恢复和 +STUN 候选收集。native Host gate 验证房间创建及当前路由上的原生视频传输; +能力探测与目标系统支持时,也包含原生音频。 + +跨 NAT 变体仅将临时反向 SSH 路径用于信令,并要求选中的媒体候选对包含 `srflx` 或 `prflx`; +媒体不会经过 SSH。公网邀请媒体变体通过 App 临时公网来源传送相同信令, +并要求将媒体直接传给独立的 Linux 对端。原生 P2P 质量证据与内嵌 SFU 传输有各自的验证入口。 +macOS 和 Linux 采集仍需实机桌面与媒体验证,CI 编译及程序包冒烟检查不能替代这些验证。 diff --git a/docs/README.md b/docs/README.md index ef49dee..65adf20 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,14 +6,14 @@ | Task | Guide | | --- | --- | -| Try the online demo | [Online demo](https://demo.piik.tv) · [Demo steps](./guide/getting-started.md#try-the-online-demo) | +| Use Piik online | [Open the online site](https://demo.piik.tv) · [Online sharing guide](./guide/getting-started.md#use-piik-online) | | Watch a friend or share my first screen | [Getting started](./guide/getting-started.md) | | Choose an App mode or fix sound and connection trouble | [First-use help](./guide/getting-started.md#choose-an-app-mode) | -| Run Piik App, check platform requirements, or build a package | [App guide](../cmd/piik-app/README.md) | +| Run Piik App, check platform requirements, or build a package | [App guide](../cmd/piik-app/README.md) · [中文 App 指南](../cmd/piik-app/README.zh-CN.md) | | Try or change the source code | [Run from source](#run-from-source) | | Find a directory or the module responsible for a behavior | [Repository layout and module map](./reference/engineering.md#repository-layout) | | Host a site for my group | [Self-hosting](./operations/self-hosting.md) · [中文部署](./operations/self-hosting.zh-CN.md) | -| Keep a server running with systemd or Docker | [Service management](./operations/service-management.md) | +| Keep a server running with systemd or Docker | [Service management](./operations/service-management.md) · [中文服务管理](./operations/service-management.zh-CN.md) | | Set passwords, ports, or room storage | [Configuration](./reference/configuration.md) | | Update my server | [Server updates](./operations/self-hosting.md#keep-it-running-and-update) | | Build, publish or recover a maintained release | [Maintainer release tooling](./deployment.md) | @@ -21,7 +21,7 @@ | Translate Piik or improve wording | [Translation guide](./guide/translating.md) · [中文翻译教程](./guide/translating.zh-CN.md) | | Check release and platform readiness | [Current status](./status.md) | -The introduction, getting-started, self-hosting and translation guides are available in English and Chinese. +The introduction, getting-started, App, self-hosting, service-management and translation guides are available in English and Chinese. Technical references keep one shared version. ## Run from source diff --git a/docs/adr/0014-browser-node-local-encoding-pool.md b/docs/adr/0014-browser-node-local-encoding-pool.md index c5fb0bf..e967df7 100644 --- a/docs/adr/0014-browser-node-local-encoding-pool.md +++ b/docs/adr/0014-browser-node-local-encoding-pool.md @@ -63,7 +63,10 @@ in [ADR-0011](./0011-browser-assisted-native-fanout.md) remains independently ow stale completion, and release the old group only after the owning operation commits. Failure preserves the prior applied state. Pause synchronously gates borrowed payload, including pending output; stop cancels pending preparation - and releases resources. A quiet source is not evidence of failure. + and releases resources. Local transport startup is bounded so a stalled + producer yields to ordinary encoding before the outer route readiness + deadline. Both local connections must connect; once connected, a quiet or + paused source is not evidence of failure. 7. Switching requires an accepted recovery frame and compatible key/delta, dependency, codec and packetization metadata. Preserve ordering and discard obsolete data only at a valid recovery boundary. Carrier timestamps and diff --git a/docs/design/media-status.md b/docs/design/media-status.md index 118efc8..da15ed7 100644 --- a/docs/design/media-status.md +++ b/docs/design/media-status.md @@ -58,6 +58,12 @@ over a reused comic's default; text and pure-visual tooltips retain that context PeerConnection errors remain diagnostic observations, not persistent red warnings after media recovers. The control menu does not repeat the television status or aggregate the first weak outbound edge into a global quality warning. + The operation supplies its outcome tone and matching scene together. Neither + the presence of an illustration nor a default checkmark proves success; + partial application retains a limitation instead of claiming completion. +- Topology: unready edges use the blue pending treatment. Readiness alone cannot + distinguish initial connection from recovery, so this view does not invent a + warning. Confirmed interruption remains owned by the status projection. - Title: keeps the primary activity, with a compact warning for an applicable problem. Losing focus alone never means waiting for media. - Recovery: every projection is recomputed from current facts. A warning requires @@ -95,8 +101,8 @@ Participant status reads the current evidence directly; it adds no stored status, timer, threshold or score. Detailed sender/receiver metrics remain available in connection details and Debug with their original scope. -The preview labels supplied example quality facts for the displayed stream. -Examples do not authorize guessing unavailable production facts. +Previews render the production projection and metric components with explicit +sample inputs. They do not add quality facts absent from the real workflow. ## References diff --git a/docs/design/public-introduction.md b/docs/design/public-introduction.md index 9ba1904..8cd8530 100644 --- a/docs/design/public-introduction.md +++ b/docs/design/public-introduction.md @@ -89,8 +89,9 @@ The short comics illustrate choosing, inviting and joining, following the shared motion grammar and whole-control replay rule. The optional film starts only after an explicit action and has native buttons and a seek slider. Open it within the homepage, loading its single player on demand; closing it unloads -playback. The standalone page serves recording and direct links. Fullscreen -watching keeps touch controls available; hiding them is an explicit recording action. +playback. Keep one homepage playback entry; the player document also supports +local recording. Fullscreen watching keeps touch controls available; hiding +them is an explicit recording action. Both READMEs share the friendly opening and room illustration. Their feature, limitation and setup sections use the factual register in the copy guide. diff --git a/docs/design/visual-language.md b/docs/design/visual-language.md index 31e000c..3a399f5 100644 --- a/docs/design/visual-language.md +++ b/docs/design/visual-language.md @@ -55,15 +55,36 @@ second activation, outside tap, Escape or focus leaving. Action controls keep their direct click action and offer touch guidance on long-press. `Tooltip` owns both interactions; pages do not add their own open state or timers. +Add a tooltip when it explains an action, a limitation or a status that needs +context. Plain names, decorative participants and self-explanatory text do not +need another popup. Keep participant details on their existing click action and +state explanations on the dedicated status indicator. + +When a nickname is clipped, the shared tooltip reveals its complete literal +text in every language mode, without a comic or repeated role/status labels. +Show this only after detecting actual truncation; resize and name changes must +recheck it. Preserve hover, keyboard and touch-hold access, and the participant's +existing click action. A complete or wrapping name needs no extra focus stop or +popup. If a status tooltip already wraps the row, add the clipped name there +rather than nesting another tooltip. + Tooltips, state overlays and entry/error pages reuse the same illustrative scene in every mode. Chinese and English add a concise caption beneath it; pure-visual mode keeps the -scene and its playful pictograms as an explicit alternative. Names, addresses and other literal -values without a corresponding scene remain text. Illustration and caption +scene and its playful pictograms as an explicit alternative. Literal names and +addresses keep their exact text. An invitation explanation retains its link +scene; simply revealing a clipped name needs only the name. +Do not substitute an unrelated action just to fill a tooltip. Illustration and caption explain the same action or current fact, with one tone and motion owner. Mount scenes only while their tooltip or overlay is shown; captions remain readable throughout. +Metric icons and hints share the `metric-presentation.ts` definition in details +and overview headings. Frames, packets, jitter and audio repair have distinct +symbols; related time measurements keep a clock with a scene identifying the +measured stage. Illustrations explain a metric, never invent its current value, +threshold or quality verdict. + Shared buttons and chips use pill shapes, a quiet lower edge and a short lift on hover or keyboard focus, then compress on press. Inputs and option tiles share softer corners. Keep a distinct focus ring and a persistent selected @@ -118,6 +139,80 @@ Use differences in tone, spacing and name tags to keep people readable; retain only the few interior seams that clarify cushions. Apply this convention to the website and README cast as well as the product UI. +## Symbol Reference + +This reference applies to controls, status strips, comics, overlays, document +titles, placeholders and public product demonstrations. A symbol keeps its +basic meaning across those surfaces. Its nearby label identifies the affected +object; it must not silently acquire a different measurement or permission. +This reference describes functional meaning in context, not exclusive ownership +of every shape. Decorative lettering, welcome ornaments and shared screen +content do not become controls or measurements by using a familiar symbol. + +[Glyph](../../src/client/ui/icons.tsx) owns small-icon geometry and the accepted +`GlyphName` union. Use it for controls and embedded comic symbols; reuse `MiniTv`, +`Pawn`, `InviteLink` and the other shared scene objects for illustrations. Select +existing meanings before adding an icon. A new meaning needs a row here and a +rendered example in the existing catalogue. A typo or missing icon must fail +type checking rather than silently render a warning triangle. + +| Meaning / 含义 | Symbols | Use and boundary | +| --- | --- | --- | +| Participants and room / 人与房间 | `users`, `couch`, `door` | People, shared room, room entry. Use the shared pawn for a person and `HostMark` for Host authority. | +| Shared screen / 共享画面 | `tv`, `MiniTv`, 📺 | Media or its sharing source. The TV beside the Host name identifies the source; the crown on a person identifies the role. | +| Start and choose a source / 分享与选源 | `cast`, `share`, `switchSource` | Start sharing, capture a screen, replace the current capture source respectively. A captured window keeps window chrome or a display stand. | +| Capture source / 采集来源 | `window`, `display` | An application window or a whole display; these nouns remain distinct from the source-switch action. | +| Invitation URL / 邀请链接 | `link`, `InviteLink`, `linkOff` | A URL and its disabled/revoked form. Keep the chain when copying or rejecting it. Copying is local; a send-to-chat scene is a separate action. | +| Credential / 凭证 | `key`, password dots | Site passphrase or room password, identified by the field label. Room-code admission with a password shows both fields. | +| Restriction / 受限 | `lock` | Restricted entry, concealed password or a setting fixed by availability; show the affected object. A lock never promises anonymity or secure media. | +| Site and public entry / 站点与公开入口 | `globe` | Browser/Site entry or code-based public entry. It does not claim internet reachability. | +| Connections / 连接 | `network`, `branch`, `signal`, `wifiOff`, `server` | Topology, branching, signaling, disconnected signaling and the named server. A server on a media path means forwarding; a Site backend or control-link server alone does not imply SFU. An invitation chain does not represent signaling. | +| Playback / 播放 | `play`, `pause`, `stop` | Play/resume, pause, end. The control label states whether it affects local viewing or the Host's share. Stop is distinct from a paused or idle room. | +| Audio / 声音 | `speaker`, `speakerOff` | Audio and mute/no audio, qualified by the source or local playback label. A source track's presence does not establish delivered sound. | +| Size and viewing mode / 尺寸与观看模式 | `expand`, `contract`, `pip`, `pipExit`, `theater`, `theaterExit` | Expand/restore, enter/leave picture in picture, enter/leave theatre mode. The same size symbol can accompany a resolution measurement. | +| Local actions / 本地操作 | `copy`, `save`, `pencil`, `eye`, `eyeOff` | Copy, save, edit, reveal and conceal the named object. An action icon does not prove completion. | +| Repeat and disclosure / 重试与展开 | `refresh`, `chevron`, `arrowRight` | Repeat/refresh, expand/collapse, proceed/enter. Direction follows the control's action; no arrow alone establishes delivery. | +| Transfer and upgrade / 传输与更新 | `arrowUp`, `arrowDown` | Outbound/inbound or upload/download, qualified by the adjacent label. An explicit update label may use the upward arrow for upgrade; neither arrow is a connection-quality rating. | +| Diagnostics / 诊断 | `cpu` | Technical diagnostics in the labelled diagnostic control; in metrics the same processor identifies encoding work. The surrounding control or measured field states the scope. | +| Settings / 设置 | `sliders`, `mountain`, `balance`, `frames` | Settings, preserve image detail, balanced preference, preserve frame rate. The frame-rate preference uses the same frames as the measured value. | +| Progress and result / 进度与结果 | `loader`, `alert`, `check`, `x` | In progress, a problem, confirmation/positive result, cancellation/negative result. The status owner supplies severity; the shape alone cannot set it. | +| Theme / 主题 | `sun`, `moon` | Light/dark appearance in theme controls. A moon in an idle-room scene means rest, with the idle scene providing that context. | +| Welcome decoration / 开场装饰 | `gamepad`, `popcorn`, `clapperboard`, `trophy`, `gift`, `flag`, `heart`, `bulb`, `zap` | Games, snacks, film, achievement, gift, next stop, affection, discovery and energy. These may decorate the welcome line, never a measured speed, failure or connection verdict. | + +Metrics use [METRIC_PRESENTATION](../../src/client/components/living/metric-presentation.ts) +in both summaries and details. Labels, units and stage-specific comics distinguish +related values; views do not choose their own symbols. + +Topology hover and keyboard focus use neutral outlines; selection adds a heavier +outline and text weight. Connection colours remain owned by route/readiness +facts, so interacting with a node cannot look like a connection-state change. + +| Measurement / 指标 | Symbol | Boundary | +| --- | --- | --- | +| Frame rate / 帧率 | `frames` | Video frames per second, including capture/input frame rate. | +| Packet loss / 丢包 | `packetLoss` | Missing network packets, including audio packets. | +| Dropped frames / 丢帧 | `frameDrop` | Discarded video frames in the stated observation interval; never substitute packet loss or a cumulative total under the same interval label. | +| Jitter / 抖动 | `jitter` | Variation in arrival timing, including audio jitter. | +| Audio concealment / 音频补偿 | `audioRepair` | Missing audio concealed by the decoder. | +| Rate / 速率 | `gauge`, `speaker` for audio rate | Bitrate or outgoing rate; the unit remains visible. | +| Duration / 耗时 | `clock` | RTT, encode/decode time, freeze duration, playout or buffer time. The label and comic identify the measured stage. | +| Freeze count / 卡顿次数 | `pause` | Count of interrupted video progress, distinguished by its metric label from intentional pause. | +| Encoder and codec / 编码器与编码格式 | `cpu`, `puzzle` | Processing implementation and encoded format respectively. | + +Text-only surfaces retain the same meanings: + +| Surface | Standard and owner | +| --- | --- | +| Activity in document titles | [Title catalogs](../../src/client/locales/visual.ts): 📺 sharing/viewing, 🛋️ ready room, 💤 idle, ⏳ waiting/starting, ⏸️ paused, ▶️ waiting for playback, ⏹️ ended, ❗ unavailable. Secondary decorations cannot replace this leading meaning or imply new progress. | +| A problem beside an activity | [Media status](../../src/client/ui/media-status.ts) supplies ⚠️ from actual facts; [document-title](../../src/client/ui/document-title.ts) only composes and rotates titles. | +| Default names | [display-name](../../src/client/lib/display-name.ts) owns 📺 for the sharing source and 👤 for a Viewer in visual mode. Preserve user-entered emoji names as data; never infer role from their characters. [viewer-presence](../../src/client/lib/viewer-presence.ts) owns duplicate-name suffixes. | +| Visual-mode shortcut | ✦ in the language control and its documentation; not a success or quality mark. | +| Input placeholders | Four slots represent the four-digit room code. Password dots mask a field; their illustrated count imposes no password length. Numeric samples reuse the actual field's `--mono` typeface and weight. Real inputs retain localized labels. | +| Unknown values | `—` means unavailable/unknown, never zero or a healthy result. Keep exact values and units whenever known. | +| Capability values | `✓` / `✗` answer the adjacent boolean capability, not whether an entire route or share succeeded. | +| Shortened text | `…` means truncation. Preserve the full nickname/URL for reading and copying; never turn an exact value into a decorative code. | +| Welcome cipher | [WelcomeLine](../../src/client/components/living/WelcomeLine.tsx) owns the paired symbols and pixel words, tied to the same selected line. They are decoration, not hidden connection state. | + ## Semantic Colour And Shape Use the existing theme tokens in `src/client/styles.css`. The shared @@ -139,6 +234,12 @@ prospective action is not proof of success: opening details, switching theme, adjusting volume or offering P2P stays neutral. Actual copy feedback can change that same hint to green or red, using the existing operation result. +Review meaning at the call site. A current admission badge shows the credential +accepted by [room access](../product/rooms-access.md), not the act of switching +privacy. Password input is not proof of admission; copying writes to the local +clipboard and does not send to a friend. An audio track's presence does not +establish a locked control or delivery to another device. + Two-panel comics always read before/context on the left and result/current condition on the right. Only the result carries the semantic accent; a single panel carries the current condition. Tooltip outline and caret use that same @@ -158,7 +259,7 @@ possibility. A rejected PiP request is failure; lack of PiP support is a limitat | --- | --- | | Action demonstration (`demo`) | Play one meaningful transition when shown, then hold its result | | Work actually continuing (`progress`) | Repeat a gentle connection/recovery beat while that state exists | -| Settled success, failure, limitation, pause or idle (`still`) | Show the informative final pose immediately; no repeated stamping, shaking, blinking or celebration | +| Settled success, failure, limitation, pause or idle (`still`) | Keep the result readable while a related subject moves briefly, then holds; do not repeat the operation, retry or celebrate | Demonstrations and progress share the existing 3.2-second comic beat. The main action belongs in its first half, followed by a readable hold; small internal @@ -172,6 +273,21 @@ result or shows a misleading starting pose. Static styling is scoped to its SVG; one still tooltip must not freeze another progress scene. New keyframes must end in the documented result rather than reset for an obsolete loop. +Every comic surface, including overlays and error pages, needs visible motion +in its subject: a paused scene's steam rises, a loose cable sways, or packets +advance toward a bottleneck. Whole-scene entrance, blinking or sparkle alone +does not satisfy this. Settled result marks stay readable during that movement. +Changing the comic's meaning starts its animation afresh; changing a caption, +language or numeric value alone does not. The preview's reduced-motion setting +uses the same explicit final poses as the system setting. +Check scenes in isolation, including settled overrides and quick re-entry while +a tooltip is exiting, so another mounted scene cannot hide missing CSS or a +finished animation. Reduced-motion rules must outrank settled-motion overrides. +Also check the actual controls together, in every relevant property combination. +Judge motion at its displayed size: a changing transform on a tiny figure can +still look frozen. The default scene catalogue does not cover a Pill's settled +override or missing preview states by itself. + The tooltip panel itself uses the same restrained entrance/exit for every tone. Replay belongs to opening/hovering/focusing the whole control, not just its SVG. Ordinary controls keep their icons visible during hover and keyboard focus; @@ -201,8 +317,8 @@ for reduced motion. Product-state comics still follow the table above. Entry screens may carry one quiet, original welcome line in Chinese or English text mode. Pure-visual mode pairs familiar pictograms with static pixel -pseudo-lettering tied to the same line. This decoration is hidden from assistive -technology. The selected line stays stable during that visit, including mode +pseudo-lettering tied to the same line. Hide that visual cipher from assistive +technology; the ordinary text sentence remains readable. The selected line stays stable during that visit, including mode changes, and never replaces an action, error, loading message or connection progress. Keep it on one line, apart from the actions; primary controls take precedence when space is tight. @@ -233,3 +349,7 @@ distinguishes functional feedback from occasional expressive movement. [Web Animations start time](https://developer.mozilla.org/en-US/docs/Web/API/Animation/startTime) provides the native timeline alignment used by participant gestures. + +[Carbon tooltip guidance](https://carbondesignsystem.com/components/tooltip/usage/) +supports concise, contextual help where it adds information; required instructions +stay visible beside the action. diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index cd968dd..5536138 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -3,17 +3,15 @@ English · [简体中文](./getting-started.zh-CN.md) · [Back to Piik](../../README.md) Piik shares one person's screen with up to 20 invited viewers. This guide covers -watching, trying the online demo, using Piik App and sharing through an existing site. +watching, using Piik online, using Piik App and sharing through an existing site. -[Join a room](#join-a-friends-room) · [Online demo](#try-the-online-demo) · +[Join a room](#join-a-friends-room) · [Use online](#use-piik-online) · [Piik App](#share-with-piik-app) · [Existing site](#share-from-your-browser) · [Troubleshooting](#when-something-gets-in-the-way) -Viewers need a browser and an invitation. To try sharing, use the online demo -or **Public invite** in Piik App. - -If Piik shows icons only, choose **EN** in the header to show the labels -used below. **中** switches to Chinese; **✦** returns to the illustrations. +Viewers need a browser and an invitation. To start sharing, download Piik App +and choose **Public invite**. To share without installing, open +[Piik online](https://demo.piik.tv) and create a room in your browser. ## Join a friend's room @@ -46,7 +44,7 @@ you cannot join by code. If an old link no longer works, ask the host for the current invitation. App Local rooms and temporary public invitations end when that App run stops. -## Try the online demo +## Use Piik online 1. Open [demo.piik.tv](https://demo.piik.tv) in a desktop browser that supports screen sharing. @@ -56,7 +54,7 @@ App Local rooms and temporary public invitations end when that App run stops. second device or send it to a friend. 4. Select **Stop sharing** when finished. -The project hosts this site for you. It uses direct media connections (P2P); +The online version is provided by the project. Media travels only between participants (P2P); restrictive networks may load the page but block video. ## Share with Piik App @@ -89,7 +87,7 @@ See the [App guide](../../cmd/piik-app/README.md) for package and runtime detail | Mode | When to choose it | What to keep in mind | | --- | --- | --- | -| **Local room** | Friends are on the same local network. | If several **Local invitation addresses** appear, choose the interface and IP on your friends' network. Your computer must be reachable from theirs. A local site passphrase is optional. | +| **Local room** | Friends are on the same local network. | If several entries appear under **Local invitation address**, choose the interface and IP on your friends' network. Your computer must be reachable from theirs. A local site passphrase is optional. | | **Public invite** | Friends are elsewhere and you want a temporary invitation. | Needs Internet access. The public address lasts for this App run. | | **Connect to Site** | Your group already has a Piik site. | Enter its address; the App adds capture capabilities to the site's sharing page. | @@ -102,7 +100,7 @@ The [App guide](../../cmd/piik-app/README.md#modes) explains the connection setu ## Share from your browser You need an existing HTTPS site running Piik Server, provided by a friend or -administrator. If you do not have one, start with the [demo](#try-the-online-demo) +administrator. You can also use [Piik online](#use-piik-online) or [Piik App](#share-with-piik-app). Hosting a site yourself is an advanced option: follow the [self-hosting guide](../operations/self-hosting.md) for server, HTTPS and network setup. diff --git a/docs/guide/getting-started.zh-CN.md b/docs/guide/getting-started.zh-CN.md index 5fa3278..5191350 100644 --- a/docs/guide/getting-started.zh-CN.md +++ b/docs/guide/getting-started.zh-CN.md @@ -3,16 +3,14 @@ [English](./getting-started.md) · 简体中文 · [回到 Piik](../../README.zh-CN.md) Piik 支持一位房主分享屏幕,最多 20 位受邀观众通过浏览器观看。 -本指南介绍加入房间、体验在线 Demo、使用客户端,以及通过已有站点分享的方法。 +本指南介绍加入房间、使用在线版、使用客户端,以及通过已有站点分享的方法。 -[加入房间](#加入朋友的房间) · [在线 Demo](#体验在线-demo) · +[加入房间](#加入朋友的房间) · [在线版](#使用在线版) · [Piik App](#用-piik-app-分享) · [已有站点](#用浏览器分享) · [问题排查](#遇到问题了) 观众只需要浏览器和邀请链接。 -初次体验分享,可以使用在线 Demo 或客户端的 **公网邀请**。 - -如果 Piik 只显示图示,点击顶部的 **中** 即可显示下文提到的按钮文字。 -**EN** 切换到英文,**✦** 返回图示模式。 +分享画面推荐下载 Piik App,选择 **公网邀请**。 +想免安装,可以打开[在线版](https://demo.piik.tv),直接在浏览器里创建房间并分享。 ## 加入朋友的房间 @@ -43,14 +41,14 @@ Piik 支持一位房主分享屏幕,最多 20 位受邀观众通过浏览器 旧链接失效了,就请房主重新发一份当前邀请。 App 的本地房间和临时公网邀请会随本次 App 退出而结束。 -## 体验在线 Demo +## 使用在线版 1. 在支持屏幕共享的电脑浏览器中打开 [demo.piik.tv](https://demo.piik.tv)。 2. 点击 **开始分享**,选择屏幕、窗口或标签页;如需声音且浏览器提供该选项,启用音频。 3. 确认预览后,点击 **复制邀请链接**,用另一台设备打开,或发送给朋友。 -4. 体验结束后,点击 **停止分享**。 +4. 分享结束后,点击 **停止分享**。 -演示站由项目提供,画面通过设备间直连(P2P)传输。 +在线版由项目提供,画面仅通过设备间直连(P2P)传输。 在受限网络下,可能出现页面能打开、画面无法连接的情况。 ## 用 Piik App 分享 @@ -75,7 +73,7 @@ App 的本地房间和临时公网邀请会随本次 App 退出而结束。 目前主要测试 Windows 客户端和浏览器分享。macOS 和 Linux 客户端尚未经过实机测试, 欢迎有设备的朋友试用并[反馈问题](https://github.com/TNTcraftHIM/Piik/issues)。 Linux 原生采集还需要[系统组件](../../native/capture/linux/README.md)。 -程序包与启动参数的细节见 [App 指南](../../cmd/piik-app/README.md)。 +程序包与启动参数的细节见 [App 指南](../../cmd/piik-app/README.zh-CN.md)。 ### 选择 App 模式 @@ -88,12 +86,12 @@ Linux 原生采集还需要[系统组件](../../native/capture/linux/README.md) **网页能打开,不代表视频一定能连通。** 公网邀请为房间提供临时网页地址, 画面和声音仍靠参与者之间的连接传输。此模式没有媒体服务器兜底, 临时地址也不保证持续可用。所有模式都需要可用的 UDP 媒体路径。 -具体连接方式见 [App 指南](../../cmd/piik-app/README.md#modes)。 +具体连接方式见 [App 指南](../../cmd/piik-app/README.zh-CN.md#运行模式)。 ## 用浏览器分享 需要先有一个运行 Piik Server 的 HTTPS 站点,可以使用朋友或管理员提供的地址。 -还没有站点时,请先体验[在线 Demo](#体验在线-demo) 或使用[客户端](#用-piik-app-分享)。 +也可以使用项目提供的[在线版](#使用在线版)或[客户端](#用-piik-app-分享)。 自建站点属于进阶操作,服务器、HTTPS 和网络端口的配置步骤见[部署指南](../operations/self-hosting.zh-CN.md)。 1. 在电脑上打开 Piik 站点。如果站点设置了口令,先输入口令。 @@ -112,9 +110,9 @@ Linux 原生采集还需要[系统组件](../../native/capture/linux/README.md) | --- | --- | | 有画面,没声音 | 先取消视频静音。房主应选择支持分享声音的来源;如果开始时关闭了声音,请停止后重新开始并打开声音。通过 App 采集窗口或屏幕时,切换来源会保留原来的声音设置。 | | 没有屏幕选择弹窗 | 按系统提示允许浏览器或 App 录制屏幕。浏览器采集需要 HTTPS 或 `localhost`,请在电脑上尝试分享。 | -| App 启动失败 | 先查看页面和黑窗口中的具体原因。重新打开 App,在主题按钮后的芯片图标开启「诊断启动」再重试。启动失败时会自动导出报告,保存位置见黑窗口。 | +| App 启动失败 | 先查看页面和终端中的具体原因。重新打开 App,在主题按钮后的芯片图标开启「诊断启动」再重试。启动失败时会自动导出报告,保存位置见终端。 | | 本地邀请打不开 | 确认两台设备在同一网络且可以互相访问。访客 Wi-Fi 或防火墙规则可能阻止本地连接。 | -| 网页能打开,画面连不上 | 点击播放控制栏的 **重新连接**;仍然失败时,检查 [WebRTC 连接设置](../../cmd/piik-app/README.md#chromium-webrtc-connections),或尝试有媒体服务器兜底的站点。 | +| 网页能打开,画面连不上 | 点击播放控制栏的 **重新连接**;仍然失败时,检查 [WebRTC 连接设置](../../cmd/piik-app/README.zh-CN.md#chromium-webrtc-连接),或尝试有媒体服务器兜底的站点。 | | 休眠或挂起后分享中断 | 唤醒设备,回到分享标签页,必要时重新开始分享。浏览器或系统挂起可能中断采集和播放。 | 反馈问题时,请附上版本、系统与浏览器、预期结果和复现步骤。 diff --git a/docs/guide/translating.zh-CN.md b/docs/guide/translating.zh-CN.md index 5ea9cc9..90c0ea3 100644 --- a/docs/guide/translating.zh-CN.md +++ b/docs/guide/translating.zh-CN.md @@ -36,7 +36,7 @@ Piik 目前提供简体中文和英文。欢迎改进按钮措辞、校对教程 | 随机欢迎语、浏览器标签页的轮换标题 | 同一语言文件中的 `welcome.*` 及文件末尾的标题词库 | | 官网 | [页面正文](../../site/index.html)和[交互文案](../../site/main.js),按 `en`、`zh-CN` 成对组织 | | 宣传短片 | [短片源码说明](../../site/film/README.md),字幕、插画和播放控件有各自的中英文内容 | -| App 黑窗口 | [console.go](../../internal/app/console.go) 中的 `consoleCopy`,每项按英文、中文、纯视觉排列;第三项即使为空也需保留 | +| App 终端 | [console.go](../../internal/app/console.go) 中的 `consoleCopy`,每项按英文、中文、纯视觉排列;第三项即使为空也需保留 | | README 与入门教程 | [README](../../README.md)、[使用指南](./getting-started.md)、[部署指南](../operations/self-hosting.md),均有对应的 `.zh-CN.md` 文件 | App 与 Server 使用同一套网页操作界面。请修改源码,构建目录和发布包由工具生成。 @@ -64,7 +64,7 @@ App 与 Server 使用同一套网页操作界面。请修改源码,构建目 词库使用普通字符串,不解析 HTML 或 ICU 复数表达式。 如果目标语言需要复数变化或不同的句子组合,请给出具体例子,便于连同调用处一起调整。 -保留代码的引号、转义和逗号。命令、配置项、URL、下载文件名,以及 Go 黑窗口文案中的 `%s` +保留代码的引号、转义和逗号。命令、配置项、URL、下载文件名,以及 Go 终端文案中的 `%s` 等格式标记保持原样。链接显示文字和图片说明可以翻译;Markdown 标题变化后,要修正对应的页内链接。 许可证原文及第三方声明保持完整。 @@ -94,8 +94,8 @@ App 与 Server 使用同一套网页操作界面。请修改源码,构建目 用户保存的选择优先。 App、Server 网页界面及官网对中文地区和文字变体的默认选择,遵循 [统一的系统语言规则](../product/presentation-lifecycle.md#visual-language)。 -3. 检查完整界面与启动流程。新增的界面语言在 App 黑窗口中暂用英文; - 翻译黑窗口时,还需同步启动服务和 loopback 对语言的校验,相关位置见 +3. 检查完整界面与启动流程。新增的界面语言在 App 终端中暂用英文; + 翻译终端时,还需同步启动服务和 loopback 对语言的校验,相关位置见 [App 模块地图](../reference/engineering.md#module-map)。 4. 在 PR 中写清覆盖范围。官网、短片和文档可以分别翻译,新增语言时需同步各自的语言切换入口。 从右向左书写的语言还需要验证文字方向与布局;仅有词库并不代表已支持 RTL 排版。 diff --git a/docs/operations/self-hosting.md b/docs/operations/self-hosting.md index 1152dcb..3e86f31 100644 --- a/docs/operations/self-hosting.md +++ b/docs/operations/self-hosting.md @@ -76,6 +76,11 @@ working directory. A blank `SITE_ACCESS_PASSWORD` allows entry without a site passphrase; enter a passphrase to require one. Room invitations and access settings still apply. +`MAX_VIEWERS_PER_ROOM` sets the room's Viewer limit, excluding the Host. Choose +`1..20` and restart to apply it. More Viewers can require more network and relay +resources. See [room capacity](../reference/configuration.md#room-capacity) +for defaults and the difference from App rooms. + ### 2. Enable HTTPS Use your existing HTTPS reverse proxy to forward to `127.0.0.1:8787` with diff --git a/docs/operations/self-hosting.zh-CN.md b/docs/operations/self-hosting.zh-CN.md index 3d23586..eee754a 100644 --- a/docs/operations/self-hosting.zh-CN.md +++ b/docs/operations/self-hosting.zh-CN.md @@ -40,7 +40,7 @@ docker compose up -d 接着完成下方的 [HTTPS 配置](#2-配置-https)和[端口放行](#3-放行端口并检查)。 默认使用 P2P;如需 SFU 兜底,按 `.env` 中的说明启用即可。 请保留 `piik-data` 数据卷,房间数据和可选诊断文件都保存在其中。 -更新与备份见[容器维护说明](./service-management.md#container)。 +更新与备份见[容器维护说明](./service-management.zh-CN.md#容器)。 ## 对外提供服务 @@ -71,6 +71,10 @@ SITE_ACCESS_PASSWORD= `SITE_ACCESS_PASSWORD` 留空时,进入站点无须口令; 如需设置口令,填写你想使用的口令即可。房间邀请与加入权限仍由房主管理。 +`MAX_VIEWERS_PER_ROOM` 可设置每房观众上限,不含房主,支持 `1..20`,修改后重启生效。 +人数越多,对网络和转发资源的需求也可能增加。默认值及 App 房间的区别见 +[人数限制](../reference/configuration.md#room-capacity)。 + ### 2. 配置 HTTPS 如果已有 HTTPS 反向代理,将请求转发到 `127.0.0.1:8787`,并启用 WebSocket 支持。 @@ -102,7 +106,7 @@ TCP 8787 仅供本机反向代理访问。STUN 域名需要直接解析到服务 ## 长期运行与更新 -需要开机启动时,参阅 [systemd 与容器配置](./service-management.md)。 +需要开机启动时,参阅 [systemd 与容器配置](./service-management.zh-CN.md)。 更新时保留 `.env` 和 `rooms.sqlite`:停止服务、备份房间数据、替换新版程序,再启动并检查健康状态和房间访问。 涉及数据格式变化的版本,请先阅读发布说明。 diff --git a/docs/operations/service-management.md b/docs/operations/service-management.md index 2e28434..755b85a 100644 --- a/docs/operations/service-management.md +++ b/docs/operations/service-management.md @@ -1,5 +1,7 @@ # Service Management +English · [简体中文](./service-management.zh-CN.md) · [Documentation](../README.md) + For first installation, use the [short self-hosting guide](./self-hosting.md). This reference covers a persistent Linux service and container maintenance. @@ -93,8 +95,8 @@ The bare-metal `release-app.sh` wrapper does not manage containers. - The official Trickle ICE sample obtains an ordinary UDP `srflx` candidate from the configured STUN service and no relay candidate. - A direct room presents media on two networks when Peer ICE succeeds. -- A constrained route creates one managed Host publication and only admitted - subscriptions over embedded SFU UDP; Peer descendants remain ordinary P2P. +- When SFU is enabled, a constrained route creates one managed Host publication + and only admitted subscriptions over embedded SFU UDP; Peer descendants remain ordinary P2P. - Blocking all UDP reaches a clear bounded failure. - Restarting Piik in stable mode retains room authority while Browser sessions and media reconnect from fresh process state. diff --git a/docs/operations/service-management.zh-CN.md b/docs/operations/service-management.zh-CN.md new file mode 100644 index 0000000..c11fe79 --- /dev/null +++ b/docs/operations/service-management.zh-CN.md @@ -0,0 +1,91 @@ +# 服务管理 + +[English](./service-management.md) · 简体中文 · [文档导航](../README.md) + +首次安装请从[部署指南](./self-hosting.zh-CN.md)开始。本页介绍 Linux 常驻服务与容器维护。 + +## systemd + +[服务模板](../../deploy/systemd/piik.service.example)使用 `piik` 用户运行, +读取 `/etc/piik/piik.env`,启动 `/opt/piik/current/piik-server`。 +请创建服务账号,将解压后的发布产物放在该路径,或让 `current` 指向对应发布目录。 +将配置复制到 `/etc/piik/piik.env`,并设置 `ROOM_DATABASE_PATH=/var/lib/piik/rooms.sqlite`; +服务单元会创建这个状态目录。环境文件只应允许运维人员读取。 + +将模板安装为 `/etc/systemd/system/piik.service`,然后执行: + +```sh +sudo systemctl daemon-reload +sudo systemctl enable --now piik +sudo systemctl status piik +``` + +HTTPS 代理与 Piik 在同一主机时,让 HTTP 继续绑定回环地址。 +STUN 和可选的 SFU 使用各自的公网 UDP 监听器。已有 nginx 时,可修改 +[代理示例](../../deploy/nginx/piik.conf.example)中的域名与证书路径后使用。 + +## 容器 + +安装步骤见 [Docker Compose 配置](./self-hosting.zh-CN.md#使用-docker-compose)。 +镜像为 `ghcr.io/tntcrafthim/piik`,提供 `latest` 和对应产品版本标签(`vMAJOR.MINOR.PATCH`), +支持 **linux/amd64**。在 `.env` 中设置 `PIIK_IMAGE` 可固定版本或镜像摘要。 +更新需要手动执行;仅拉取镜像不会替换正在运行的容器。 + +[运行时 Dockerfile](../../deploy/container/Dockerfile)封装已验证的 Server 压缩包, +包括网页界面、许可证声明和 `REVISION`。固定版本的 +[Distroless 非 root 基础镜像](https://github.com/GoogleContainerTools/distroless) +包含 CA 证书,没有 shell 或包管理器。镜像的构建、检查和发布由 +[发布流程](../reference/versioning.md#container-distribution)管理。 + +使用既有的[生产配置](../reference/configuration.md)。容器内保留 `LISTEN_HOST=0.0.0.0`, +并继续使用主机已有的 HTTPS/WebSocket 代理。使用 Docker 端口映射且启用 SFU 时, +将 `SFU_PUBLIC_IP` 设为外部可达的服务器 IPv4 地址;容器私网候选无法从互联网访问。 +公布的 STUN 域名必须解析到服务器公网地址,UDP 必须能直接到达容器。 + +[Compose 文件](../../deploy/container/compose.yaml)将 HTTP 绑定到主机回环地址, +公布标准 UDP 端口,并启用自动重启。公布端口不会开启对应服务,可选监听器由 `.env` 控制。 +修改监听端口时,同时修改应用配置和端口映射。 +请使用主机上的代理,或明确调整代理容器的网络配置;代理容器自己的 `127.0.0.1` +无法访问另一个容器内的 Piik。 + +镜像中的 `/home/nonroot` 属于 UID/GID `65532:65532`,权限为 `0700`。 +新的命名数据卷继承该目录;已有数据卷或绑定挂载需要事先允许该身份写入。 +替换容器时保留这个数据卷。设置 `ROOM_DATABASE_PATH=:memory:` 可让房间权威状态只保存在内存中。 +诊断保持手动开启,使用 `/home/nonroot/logs`;[导出与保留](../reference/configuration.md#diagnostics) +由运维人员负责。不要将应用文件挂载为可写。 +启用诊断后,`docker compose kill --signal=SIGUSR1 piik` 会请求本地导出,不会停止容器; +ZIP 保存在挂载的日志目录中。 + +从主机与公网代理检查 `/healthz`,再按[运行验证](#运行验证)检查已配置的 STUN/SFU UDP, +以及房间和媒体行为。镜像内没有基于 shell 的健康检查。 +替换前保留旧镜像、环境配置与数据卷备份。在部署目录执行: + +```sh +docker compose pull +docker compose stop +docker compose cp piik:/home/nonroot ./piik-data-backup +docker compose up -d +docker compose logs --tail=50 piik +``` + +每次使用新的备份目录。复制 SQLite 前先停止 Piik,确保数据库和日志文件一致。 +单独保存 `.env`,并将配置和数据备份视为房间凭据加以保护。 +保留同一个 Compose 项目与目录,以便继续使用原命名数据卷; +`docker compose down --volumes` 会删除这些数据。 +回滚时,恢复之前的镜像设置与兼容配置,再重新创建服务。 +跨越存储格式边界前,先阅读发布说明。裸机部署脚本 `release-app.sh` 不管理容器。 + +## 运行验证 + +- 本地和公网来源的 `/healthz` 都返回 `{"status":"ok"}`。 +- Piik 与 HTTPS 代理处于运行状态,重启次数符合预期,日志和资源使用有界; + Piik 占有配置中的 STUN/SFU UDP 监听器。 +- 官方 Trickle ICE 示例能从配置的 STUN 服务取得普通 UDP `srflx` 候选,不出现 relay 候选。 +- Peer ICE 成功时,两个网络上的设备能在直连房间中显示媒体。 +- 启用 SFU 时,受限路由通过内嵌 SFU UDP 创建一个受管理的房主发布,且只接纳已授权订阅; + Peer 后代节点继续使用普通 P2P。 +- 阻断全部 UDP 后,在有限时间内进入明确的失败状态。 +- 使用持久化模式时,重启 Piik 保留房间权威状态;浏览器会话和媒体从新进程状态重新连接。 + +代表性网络、移动端、音频、20 位观众和长时间运行的证据由 +[验证状态](../verification-status.md)跟踪,不记录在本操作手册中。 diff --git a/docs/operations/website.md b/docs/operations/website.md index 7dd73b3..05c992e 100644 --- a/docs/operations/website.md +++ b/docs/operations/website.md @@ -51,12 +51,11 @@ unit provides the SQLite state directory. Room ownership, invitations and privat admission still apply. The [configuration reference](../reference/configuration.md) owns each setting and its bounds. -For updates, verify the release descriptor and manifest before placing a new -release under `/opt/piik/releases/`. Stop Piik, back up its SQLite state, switch -`current` to the verified release, and restart. Check local/public health, -WebSocket room access and STUN. Keep the previous release and state backup until -acceptance; restore both with Piik stopped if rollback is needed. The nginx-specific -maintainer wrapper does not manage this Caddy installation. +For updates, follow the [application release boundary](../deployment.md#release-boundary) +and its artifact verification, health checks and recovery rules. Routine application +rollback restores the previous executable while keeping current room data. This +Caddy installation needs a scoped updater; the nginx-specific maintainer wrapper +does not manage it. ## Preview diff --git a/docs/product/presentation-lifecycle.md b/docs/product/presentation-lifecycle.md index 41a469e..11686e5 100644 --- a/docs/product/presentation-lifecycle.md +++ b/docs/product/presentation-lifecycle.md @@ -32,6 +32,10 @@ Browser picker and any exact App-owned native windows. It never chooses a window automatically. A reproduced Browser-window, capture, or background failure is required before introducing an embedded Web runtime. +Failure to open the system Browser does not retire a ready App service. The +console keeps its entry address and retry action; service failure and explicit +exit still own shutdown. Browser-opening errors retain their underlying cause. + Server and App share one control layout and vocabulary. Deployment capabilities may fix a control on or off, but do not remove its place in the interface; disabled controls explain their reason in text and pure-visual modes. Real @@ -70,6 +74,9 @@ Chinese catalog across App, Server UI and website; this includes region and script tags such as `zh-CN`, `zh-TW`, `zh-HK`, `zh-Hans` and `zh-Hant`. Pure-visual mode remains an explicit choice. An explicit later choice is persisted and takes precedence over that default, including App launch handoff. +Explicit launcher choices apply only to the presentation domains the user chose. +Older launchers cannot distinguish defaults from explicit choices; updated Sites +preserve their saved preferences when receiving those unmarked hints. The App console uses its own supported presentation languages and falls back to English for additional UI languages. Text modes use concise copy. @@ -87,9 +94,11 @@ fullscreen includes the playback bar; Safari's video-only fullscreen uses system controls and native video events to reflect playback and exit. Identity, connection details and topology stay in the deck. -Replacement media bindings do not consult Viewer-local pause: when the Host is +Replacement video bindings do not consult Viewer-local pause: when the Host is playing, they attempt playback subject to browser autoplay permission. There is no separate remembered playback intent for source changes or reconnects. +Adding, replacing or removing audio on the same video binding updates only its +audio consumer; it preserves current-frame proof and Viewer-local playback. The bar spans the screen's lower edge. Narrow screens separate audio controls from window actions into two rows and retain 44px action targets. Playing video hides the bar diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 8c96f8d..5e519df 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -27,12 +27,28 @@ service secret store or an untracked access-restricted environment file. | `ALLOWED_ORIGINS` | Comma-separated exact HTTP(S) origins; wildcard is invalid. | | `SITE_ACCESS_PASSWORD` | Optional in every environment. Unset or empty allows entry without a site password. A configured value is matched exactly, including spaces and Unicode; there are no password length or character rules. General HTTP request limits still apply. Room ownership and Viewer admission remain independent. | | `ROOM_DATABASE_PATH` | Hosted defaults to `rooms.sqlite` in its working directory when unset or blank. An explicit absolute file path selects another SQLite file; `:memory:` opts into process-memory room authority. App Local remains in memory. | -| `MAX_VIEWERS_PER_ROOM` | `1..20`, default `8`. | +| `MAX_VIEWERS_PER_ROOM` | `1..20`, default `20`; excludes the Host. | | `ENDPOINT_MEDIA_COPY_CAPACITY` | Shared endpoint steady-copy cap `1..3`, default `2`. | | `STUN_URLS` | Comma-separated advertised `stun:` discovery URLs; at least one is required in production. These are not local bind addresses and may use an unproxied DNS name separate from the Web origin. | | `STUN_LISTEN_HOST` | Hosted IPv4 STUN bind address, default `0.0.0.0` when `STUN_URLS` is configured; independent of HTTP `LISTEN_HOST`. Local App construction creates no STUN listeners. | | `NAT_PREDICTION_ENABLED` | Optional bounded NAT prediction capability, default `false`; requires an ordinary `STUN_URLS` endpoint on UDP 3478. Hosted startup binds UDP 3479/3480 before advertising the capability. When unavailable, the visible NAT switch is locked off; ordinary ICE remains. Firewall reachability remains an operator requirement. | +### Room capacity + +`MAX_VIEWERS_PER_ROOM` counts authenticated Viewers per room, excluding the Host. +Self-hosted Server accepts `1..20` and defaults to `20`, matching the App and +deployment examples. Apply changes by restarting the +Server (with Compose, run `docker compose up -d` to recreate it after editing +`.env`). Restart interrupts active connections; excess Viewers receive `ROOM_FULL` +when joining again. App Local and public-invite modes stay fixed at `20`. + +Values above `20` are rejected at startup. This is also a wire-contract bound, +so increasing it in source requires a compatibility and capacity review. More +Viewers can add relay hops, routing wait and total media load. The setting does +not increase the per-endpoint copy cap or guarantee available bandwidth. + +### Media fallback + Automatic SFU fallback runs inside the Hosted process when `SFU_UDP_PORT` is set: | Variable | Contract | @@ -77,9 +93,13 @@ schema field. The user settings are: | `site` | Saved Piik Site origin. The launcher or `--site` updates it. | | `localAccessPassword` | Empty by default. Optional password for the App's Local room authority, with the same exact-match behavior as `SITE_ACCESS_PASSWORD`. It is separate from a hosted site's password. | -The launcher preselects a saved Site when present; otherwise it preselects the -public invitation link. Selecting a mode does not start it: the user confirms -with the launch button. Local mode remains available for the same network. +The launcher remembers the last mode confirmed with the launch button. This +optional preference lives beside the configuration as `client.json.mode` (or +`.mode`); removing it restores the initial selection without +changing saved settings. Older App versions ignore this file. Without a valid +preference, a saved Site is selected when present; otherwise the public +invitation link is selected. Choosing a mode does not start it until confirmed. +Local mode remains available for the same network. For Local mode, the launcher selects a sole active address or sole private IPv4 address automatically. With several choices it shows interface names and IPs; an ambiguous choice must be selected before launch. `--lan-address` preselects diff --git a/docs/reference/engineering.md b/docs/reference/engineering.md index 7d4471a..ef720c0 100644 --- a/docs/reference/engineering.md +++ b/docs/reference/engineering.md @@ -105,6 +105,7 @@ above rather than searching the tree. | --- | --- | | 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` | +| Received viewer metrics and expiry | `src/client/media/viewer-quality-evidence.ts` owns evidence rules; `viewer-quality-evidence-store.ts` owns received entries and timers for Host and relay views | | 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` | diff --git a/docs/reference/naming.md b/docs/reference/naming.md index ba13ada..1b6d8aa 100644 --- a/docs/reference/naming.md +++ b/docs/reference/naming.md @@ -62,6 +62,10 @@ are optional. Keep App and Server package names distinguishable at download link Public platform notes say what has been tested and invite useful feedback; internal phrases such as “this phase's acceptance target” belong in status. +Present the public online site as a usable entry for real rooms and sharing. +Offer the App download first and direct browser use alongside it; keep the +site's P2P-only deployment conditions in its detailed guide. + Use progressive disclosure on the website and in the film: | Information layer | Reader's question | Content | diff --git a/docs/status.md b/docs/status.md index 918bb3b..16baf14 100644 --- a/docs/status.md +++ b/docs/status.md @@ -1,6 +1,6 @@ # Current Status -Last updated: 2026-09-12 +Last updated: 2026-09-15 This is the compact execution/deployment index. Product modules own behavior, [verification status](./verification-status.md) owns unresolved physical limits, @@ -9,8 +9,8 @@ and Git/PRs own completed history. ## Accepted Release Contract - Browser/server v23, Native control v9 and capture v7 form the current contract. - The coordinated brand/protocol cutover requires matching Web/App/Server - artifacts and a reload of incompatible active pages. + [Versioning](./reference/versioning.md#public-compatibility-promise) owns + compatibility between published Web/App/Server versions. - Hosted Server defaults to SQLite schema 2. Room authority has no inactivity expiry; explicit replacement/deletion or grant rotation/revocation ends the corresponding authority. Explicit memory mode and App Local end rooms at @@ -24,12 +24,9 @@ and Git/PRs own completed history. ## Deployment Source, package identity, GitHub repository and local worktree paths use Piik. -Runtime code keeps only the Piik wire labels and stored keys. The coordinated -private cutover requires matching App/Server/Web packages; unavailable old -Browser credentials cannot be reconstructed from stored digests. +Runtime code keeps only the Piik wire labels and stored keys. The immutable release descriptor, runtime `REVISION` and operator deployment -record own exact identity and postflight results. Old active pages reload at -the signaling v23 boundary. +record own exact identity and postflight results. The existing deployment uses one Go process for Web, room authority/signaling, Binding-only STUN on UDP 3478/3479/3480 and optional SFU on UDP 7882. nginx owns diff --git a/docs/todo.md b/docs/todo.md index c94e4e5..053ce9f 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,43 +1,39 @@ # Current TODO Ledger -Last reviewed: 2026-09-14 +Last reviewed: 2026-09-15 Only **Now** is executable. Product modules own behavior; Git/PRs own completed history. A parked idea is not implementation authority. ## Now +- [ ] **Introduction motion and presentation boundaries.** Resolve the automatic + hero-loop control requirement against the accepted design without restoring + discarded gesture controls or silently removing animation. Review only the + remaining concrete layout/accessibility choices; distinguish decorative content + from functional symbols. Keep independent correctness repairs moving. - [ ] **Post-launch monitoring.** Collect App/Server feedback and verify the public downloads, container pulls and deployed services after product releases. Follow the [deployment runbook](./deployment.md) for the private service and keep public demo deployment separate. Preserve the owner's [device/network deferrals](./verification-status.md#candidate-evidence-boundary). -- [ ] **Browser sharing startup failure.** Reproduce [#399](https://github.com/TNTcraftHIM/Piik/issues/399): - the local encoding connection can remain connecting without producing frames - or entering the existing ordinary-sender fallback. Verify bounded startup and - retirement at that owner before changing room routing or ICE configuration. - [ ] **App startup feedback.** Verify [#396](https://github.com/TNTcraftHIM/Piik/issues/396) against the current release. Its older build predates the LAN selector, detailed launcher errors and Windows error-exit pause; those repairs do not establish resolution on the reporting machine. Use a current Debug report if startup still fails. -- [ ] **Chrome mirror download warning.** Verify the current Gitee download in - Chrome and resolve its download-source warning using the recorded verdict and - package checksums. Keep GitHub primary; ordinary link changes do not establish - that Safe Browsing has cleared the mirror. -- [ ] **App launch-mode preference.** Check why a saved Site keeps selecting - Site mode after the user chooses another mode. Review last-used-mode behavior - and its configuration owner before changing startup selection. -- [ ] **Display-name editing.** Reproduce the edit button immediately closing - or failing to open the editor. Check Host and Viewer forms, including native - submit behavior when React reuses action buttons, before changing signaling. -- [ ] **Theater centering.** Keep expansion and collapse centered. The current - width spring overshoots the viewport before settling; verify motion at wide - and narrow sizes without changing playback state. -- [ ] **Tooltip motion consistency.** Review all tooltip entries and explanatory - comics for missing motion across languages, themes and pointer/touch input. - Check trigger reuse and static rendering against the shared visual grammar; - resolve any intended change to settled-state motion in that owner. +- [ ] **Windows launcher exit after opening the page.** A user reports that the + mode-selection page opens, then the App console reports + `Piik App could not open its launcher: exit status 0xc0000005`. + Recheck on the reporting machine after the browser-handoff repair. The Windows + URL-handler crash itself still needs the affected build and process/dump + evidence; local checks cannot establish its underlying cause. +- [ ] **Tooltip placement around video.** After the current motion/meaning + acceptance, review placement that keeps the picture visible, including + side placement and restrained translucency. Keep neighbouring actions + reachable when a help popup remains open. Current behavior prefers above + (below for header controls) and flips for viewport space. Discuss the design + before changing placement rules. Keep fixes on a maintenance branch until acceptance. The public release is the compatibility baseline; private service deployment stays independent. @@ -86,6 +82,8 @@ not establish better connection success or speed; this note adds no retry policy candidate. Reopen C=3 structural-intent retention, SFU failure during unrelated prepare and multi-child evidence ownership only with current-contract reproductions. + When related behavior changes, compare the Browser/Native recovery budget; + preserve Native bridge versus network failure distinctions when sharing code. No new revision namespace, failure-state mirror or topology queue by default. 5. **Storage fault recovery.** Choose and verify a damaged-disk/COMMIT/ROLLBACK recovery policy before adding catch-and-continue or retries. This failure @@ -114,3 +112,13 @@ not establish better connection success or speed; this note adds no retry policy service. Sign Piik's executables before archive checksums are computed; signing improves publisher identity but does not guarantee that antivirus cloud scanning stops. Service selection and enrollment remain pending. +11. **Gitee download-source warning.** Paused by the owner. Keep GitHub primary + and retain Gitee; do not add a self-hosted mirror. Chrome still blocks the + Gitee attachment when Referer is removed. Reopen for new evidence or a + provider review; the warning remains unresolved. +12. **Windows capture border preference.** [#401](https://github.com/TNTcraftHIM/Piik/issues/401) + requests an optional borderless capture mode. First verify Windows' + permission-based `GraphicsCaptureAccess` / `IsBorderRequired` flow with the + packaged Native helper; keep ordinary capture working when unavailable or + denied. Browser capture indicators remain browser-owned. No new setting is + accepted yet. diff --git a/index.html b/index.html index e57d60c..3ff5ff9 100644 --- a/index.html +++ b/index.html @@ -13,8 +13,10 @@ try { theme = localStorage.getItem("piik:ui-theme"); } catch (e) {} var launch = new URLSearchParams(location.hash.slice(1)); var launchTheme = launch.get("piik-theme"); + var explicitTheme = (new URLSearchParams(location.search).get("piik-preferences") || "").split(",").includes("theme"); if (launch.get("piik-client") === "1" && - (launchTheme === "light" || launchTheme === "dark" || launchTheme === "system")) { + (launchTheme === "light" || launchTheme === "dark" || launchTheme === "system") && + (explicitTheme || (theme !== "light" && theme !== "dark"))) { theme = launchTheme === "system" ? systemTheme : launchTheme; } document.documentElement.dataset.theme = diff --git a/internal/app/app.go b/internal/app/app.go index bc11388..13b53b6 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -16,7 +16,6 @@ import ( "syscall" "time" - "github.com/TNTcraftHIM/Piik/internal/app/browser" appconfig "github.com/TNTcraftHIM/Piik/internal/app/config" "github.com/TNTcraftHIM/Piik/internal/app/lan" "github.com/TNTcraftHIM/Piik/internal/app/launcher" @@ -224,11 +223,16 @@ func runLauncher( control *loopback.Server, configureDiagnostics func(*Options, appconfig.Config) error, ) error { + mode, err := launcher.LoadMode(configPath) + if err != nil { + slog.Warn("Could not restore the last launcher mode", diagnostics.Error(err)) + } launch, err := launcher.Start( ctx, webassets.FS(), launcher.Options{ - Site: config.Site, Version: buildVersion(), Revision: BuildRevision, + DefaultMode: mode, + Site: config.Site, Version: buildVersion(), Revision: BuildRevision, LocalAccessPassword: config.LocalAccessPassword, Debug: options.Debug, LANAddress: options.LANAddress, }, @@ -246,9 +250,7 @@ func runLauncher( launchAddress.Scheme+"://"+launchAddress.Host, )) options.console.show(consoleView{state: "setup", entry: launch.URL()}) - if err = browser.Open(launch.URL()); err != nil { - return fmt.Errorf("Piik App could not open its launcher: %w", err) - } + options.console.send(openBrowser(launch.URL())) var selection launcher.Selection select { @@ -262,6 +264,9 @@ func runLauncher( return nil } options.console.setLanguage(selection.Language) + if err := launcher.SaveMode(configPath, selection.Mode); err != nil { + slog.Warn("Could not save the launcher mode", diagnostics.Error(err)) + } if selection.Mode == launcher.ModeSite { config.Site = selection.Site } else { @@ -390,21 +395,17 @@ func runSite(site string, options Options, control *loopback.Server) error { var err error slog.Debug("piik-client", "event", "mode", "mode", "site") targetURL := launchURL(site) - view := consoleView{mode: "site", state: "starting", entry: targetURL} + view := consoleView{mode: "site", state: "ready", entry: targetURL} options.console.show(view) if options.console.machine { fmt.Printf("Piik Site: %s\n", site) } if !options.DisableBrowser { - if err = browser.Open(targetURL); err != nil { - return fmt.Errorf("Piik App could not open the Site: %w", err) - } + options.console.send(openBrowser(targetURL)) } else if options.Ready != nil { options.Ready(targetURL) } - view.state = "ready" slog.Debug("piik-client", "event", "site-ready") - options.console.show(view) if err = <-control.Done(); err != nil { return fmt.Errorf("Piik App native control stopped unexpectedly: %w", err) } @@ -507,18 +508,16 @@ func runLocal(ctx context.Context, options Options, config appconfig.Config, fmt.Sprintf("http://127.0.0.1:%d/", options.Port), config.LocalAccessPassword, ) - if !options.DisableBrowser { - if err = browser.Open(launchURL); err != nil { - return fmt.Errorf("Piik App could not open the Local page: %w", err) - } - } else if options.Ready != nil { - options.Ready(launchURL) - } view.state, view.entry, view.invite = "ready", launchURL, publicOrigin if view.invite == "" { view.invite = fmt.Sprintf("http://%s:%d", selectedAddress, options.Port) } options.console.show(view) + if !options.DisableBrowser { + options.console.send(openBrowser(launchURL)) + } else if options.Ready != nil { + options.Ready(launchURL) + } var tunnelDone <-chan struct{} if tunnel != nil { tunnelDone = tunnel.Done() diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 2fa916a..53526ee 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -2,6 +2,8 @@ package app import ( "bytes" + "context" + "encoding/json" "errors" "fmt" "log" @@ -18,10 +20,89 @@ import ( appconfig "github.com/TNTcraftHIM/Piik/internal/app/config" "github.com/TNTcraftHIM/Piik/internal/app/lan" + "github.com/TNTcraftHIM/Piik/internal/app/launcher" "github.com/TNTcraftHIM/Piik/internal/app/loopback" serverconfig "github.com/TNTcraftHIM/Piik/internal/server/config" ) +func TestBrowserOpenFailureKeepsTheAppAvailable(t *testing.T) { + // An empty launcher search path makes the real browser.Open fail on every + // supported platform without changing the user's default browser or opening it. + t.Setenv("PATH", t.TempDir()) + t.Setenv("TERM", "dumb") + for _, mode := range []string{"launcher", "site"} { + t.Run(mode, func(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + control, err := loopback.Start(ctx, loopback.Options{}) + if err != nil { + t.Fatal(err) + } + defer control.Close() + console := newConsole(cancel, false) + options := Options{Port: 8787, console: console} + configPath := filepath.Join(t.TempDir(), "client.json") + config, err := appconfig.LoadOrCreate(configPath) + if err != nil { + t.Fatal(err) + } + done := make(chan error, 1) + go func() { + if mode == "site" { + done <- runSite("https://share.example", options, control) + } else { + done <- runLauncher(ctx, options, configPath, config, control, + func(*Options, appconfig.Config) error { return nil }) + } + }() + t.Cleanup(func() { + cancel() + select { + case err := <-done: + if err != nil { + t.Errorf("App exit: %v", err) + } + case <-time.After(5 * time.Second): + t.Error("App did not stop after cancellation") + } + }) + var view consoleView + deadline := time.After(3 * time.Second) + for view.browserError == nil { + console.mu.Lock() + view = console.plain.view + console.mu.Unlock() + select { + case <-deadline: + t.Fatal("browser failure was not reported") + case <-time.After(10 * time.Millisecond): + } + } + if view.entry == "" || view.state != "setup" && view.state != "ready" { + t.Fatalf("unusable App after browser failure: %+v", view) + } + if mode == "launcher" { + client := &http.Client{Timeout: 3 * time.Second} + origin := strings.TrimSuffix(view.entry, "/client") + response, err := client.Post(origin+"/api/client-launcher/launch", "application/json", + strings.NewReader(`{"mode":"site","language":"en","site":"https://share.example"}`)) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + var result struct{ Target string } + if json.NewDecoder(response.Body).Decode(&result) != nil || response.StatusCode != http.StatusOK || + result.Target != launchURL("https://share.example") { + t.Fatalf("manual launch failed: status=%d, target=%q", response.StatusCode, result.Target) + } + if saved, err := launcher.LoadMode(configPath); err != nil || saved != launcher.ModeSite { + t.Fatalf("manual selection was not saved: %q, %v", saved, err) + } + } + }) + } +} + func TestSavedSiteAllowsBrowserOriginAtNativeControl(t *testing.T) { for _, site := range []struct{ input, origin string }{ {"https://Share.Example:443", "https://share.example"}, diff --git a/internal/app/console.go b/internal/app/console.go index 6450f44..5e13e81 100644 --- a/internal/app/console.go +++ b/internal/app/console.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "log/slog" "math/rand/v2" "net/url" "os" @@ -15,6 +16,7 @@ import ( tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/TNTcraftHIM/Piik/internal/app/browser" + "github.com/TNTcraftHIM/Piik/internal/diagnostics" "github.com/charmbracelet/colorprofile" "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/term" @@ -24,13 +26,17 @@ type consoleView struct { mode, state, entry, invite string protected bool problem string + browserError error } type consoleLanguage string type consoleFinished struct{ err error } type consoleTick struct{} type consoleIdleWink struct{} -type consoleOpenResult struct{ err error } +type consoleOpenResult struct { + target string + err error +} type consoleDiagnostic string type consoleDebug struct { logPath string @@ -136,6 +142,16 @@ func (console *console) send(message tea.Msg) { func (console *console) show(view consoleView) { console.send(view) } func (console *console) setLanguage(language string) { console.send(consoleLanguage(language)) } +// Opening a page is a convenience action; the launcher and room services own +// readiness and shutdown even when the system URL handler fails. +func openBrowser(target string) consoleOpenResult { + err := browser.Open(target) + if err != nil { + slog.Warn("Could not open the system browser", diagnostics.Error(err)) + } + return consoleOpenResult{target: target, err: err} +} + func (console *console) finish(err error) error { console.send(consoleFinished{err}) if console.program != nil { @@ -177,6 +193,7 @@ func (model consoleModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { model.language = string(value) case consoleDiagnostic: model.view.problem = string(value) + model.view.browserError = nil case consoleDebug: model.debug = value case consoleExportResult: @@ -200,12 +217,13 @@ func (model consoleModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { model.frame, model.idleWink = 8, true } case consoleOpenResult: - if value.err != nil { - model.view.problem = model.text("openFailed") + if value.target == model.view.entry && !model.finished && model.view.state != "stopping" { + model.view.browserError = value.err } case consoleFinished: model.finished = true model.view.entry, model.view.invite = "", "" + model.view.browserError = nil model.view.state, model.view.problem = "stopped", "" if value.err != nil { model.view.state, model.view.problem = "failed", value.err.Error() @@ -216,9 +234,9 @@ func (model consoleModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { case "q", "ctrl+c": model.view = consoleView{mode: model.view.mode, state: "stopping"} model.cancel() - case "o": + case "o", "O": if target := model.view.entry; target != "" { - return model, func() tea.Msg { return consoleOpenResult{browser.Open(launchURL(consoleAddress(target)))} } + return model, func() tea.Msg { return openBrowser(target) } } case "d", "D": if model.debug.export != nil && !model.exporting && !model.finished && model.view.state != "stopping" { @@ -360,8 +378,12 @@ func (model consoleModel) content(styled bool) string { fmt.Fprintf(&out, "\n%s %s\n", muted.Render(model.text("access")), model.text(access)) } } - if model.view.problem != "" { - lines := strings.Split(ansi.Hardwrap(ansi.Strip(model.view.problem), width-4, true), "\n") + problem := model.view.problem + if model.view.browserError != nil { + problem = model.text("openFailed") + "\n" + model.view.browserError.Error() + } + if problem != "" { + lines := strings.Split(ansi.Hardwrap(ansi.Strip(problem), width-4, true), "\n") if len(lines) > 3 { lines = append(lines[:2], "...") } @@ -443,6 +465,8 @@ func consoleVisualToken(key string) string { return "○" case "failed", "error": return "!" + case "openFailed": + return "! ↗" case "open": return "○" case "password": diff --git a/internal/app/console_test.go b/internal/app/console_test.go index dc045c6..3745493 100644 --- a/internal/app/console_test.go +++ b/internal/app/console_test.go @@ -25,6 +25,42 @@ func TestConsoleStartsWithSystemLanguageAndAcceptsVisualSelection(t *testing.T) } } +func TestBrowserOpenFeedbackBelongsToTheCurrentEntry(t *testing.T) { + target := "http://127.0.0.1:8787/#client-access=private-secret" + failure := consoleOpenResult{target: target, err: errors.New("exit status 0xc0000005")} + model := consoleModel{width: 80, language: "en", view: consoleView{state: "ready", entry: target}} + updated, _ := model.Update(failure) + model = updated.(consoleModel) + if model.finished || model.view.state != "ready" || model.view.entry != target { + t.Fatal("browser failure retired a ready App") + } + for _, language := range []string{"en", "zh", "vis"} { + updated, _ = model.Update(consoleLanguage(language)) + model = updated.(consoleModel) + content := model.content(false) + if !strings.Contains(content, model.text("openFailed")) || !strings.Contains(content, "0xc0000005") || + strings.Contains(content, "private-secret") { + t.Fatalf("browser feedback lost its current language/error or exposed a credential: %s", content) + } + } + updated, _ = model.Update(consoleOpenResult{target: target}) + model = updated.(consoleModel) + if model.view.browserError != nil { + t.Fatal("successful retry retained the browser failure") + } + for _, next := range []tea.Msg{ + consoleView{state: "setup", entry: "http://127.0.0.1:12345/client"}, + consoleView{state: "stopping"}, + consoleFinished{err: errors.New("service failed")}, + } { + updated, _ = model.Update(next) + updated, _ = updated.(consoleModel).Update(failure) + if current := updated.(consoleModel); current.view.browserError != nil { + t.Fatal("late browser result overwrote the current App state") + } + } +} + func TestConsolePresentationAndShutdown(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/internal/app/launcher/preference.go b/internal/app/launcher/preference.go new file mode 100644 index 0000000..facb773 --- /dev/null +++ b/internal/app/launcher/preference.go @@ -0,0 +1,35 @@ +package launcher + +import ( + "errors" + "os" + "strings" +) + +// Keep UI preference outside client.json: published App versions reject +// unknown configuration fields. Missing preference preserves their default. +func LoadMode(configPath string) (Mode, error) { + payload, err := os.ReadFile(configPath + ".mode") + if errors.Is(err, os.ErrNotExist) { + return "", nil + } + if err != nil { + return "", err + } + mode := Mode(strings.TrimSpace(string(payload))) + if !validMode(mode) { + return "", errors.New("invalid saved launcher mode") + } + return mode, nil +} + +func SaveMode(configPath string, mode Mode) error { + if !validMode(mode) { + return errors.New("invalid launcher mode") + } + return os.WriteFile(configPath+".mode", []byte(string(mode)+"\n"), 0o600) +} + +func validMode(mode Mode) bool { + return mode == ModeLocal || mode == ModeLink || mode == ModeSite +} diff --git a/internal/app/launcher/preference_test.go b/internal/app/launcher/preference_test.go new file mode 100644 index 0000000..b478395 --- /dev/null +++ b/internal/app/launcher/preference_test.go @@ -0,0 +1,81 @@ +package launcher + +import ( + "encoding/json" + "net/http" + "os" + "path/filepath" + "strings" + "testing" + + appconfig "github.com/TNTcraftHIM/Piik/internal/app/config" +) + +func TestSavedModeSurvivesRelaunchWithoutChangingLegacyConfig(t *testing.T) { + path := filepath.Join(t.TempDir(), "client.json") + config, err := appconfig.LoadOrCreate(path) + if err != nil { + t.Fatal(err) + } + config.Site = "https://share.example" + if err := appconfig.Save(path, config); err != nil { + t.Fatal(err) + } + original, _ := os.ReadFile(path) + for _, test := range []struct { + chosen Mode + site string + want Mode + }{ + {ModeSite, config.Site, ModeSite}, + {ModeLocal, config.Site, ModeLocal}, + {ModeLink, config.Site, ModeLink}, + {ModeSite, "", ModeLink}, + } { + if err := SaveMode(path, test.chosen); err != nil { + t.Fatal(err) + } + mode, err := LoadMode(path) + if err != nil || mode != test.chosen { + t.Fatalf("restored mode = %q, %v", mode, err) + } + server, err := Start(t.Context(), appAssets(), Options{Site: test.site, DefaultMode: mode}) + if err != nil { + t.Fatal(err) + } + response, err := http.Get(strings.TrimSuffix(server.URL(), "/client") + "/api/client-launcher") + if err != nil { + t.Fatal(err) + } + var state struct { + DefaultMode Mode `json:"defaultMode"` + Site string `json:"site"` + } + err = json.NewDecoder(response.Body).Decode(&state) + response.Body.Close() + server.Close() + if err != nil || state.DefaultMode != test.want || state.Site != test.site { + t.Fatalf("launcher lost mode or saved Site: %+v, %v", state, err) + } + } + current, _ := os.ReadFile(path) + if string(current) != string(original) { + t.Fatal("launcher preference changed legacy configuration") + } +} + +func TestUnavailableModePreferenceDoesNotSupplyAnInvalidChoice(t *testing.T) { + path := filepath.Join(t.TempDir(), "client.json") + if mode, err := LoadMode(path); err != nil || mode != "" { + t.Fatalf("missing preference = %q, %v", mode, err) + } + if err := os.WriteFile(path+".mode", []byte("invalid"), 0o600); err != nil { + t.Fatal(err) + } + if mode, err := LoadMode(path); err == nil || mode != "" { + t.Fatalf("corrupt preference = %q, %v", mode, err) + } + if err := SaveMode(path, "invalid"); err == nil { + t.Fatal("saved an invalid mode") + } +} diff --git a/internal/app/launcher/server.go b/internal/app/launcher/server.go index 6545760..a63b0de 100644 --- a/internal/app/launcher/server.go +++ b/internal/app/launcher/server.go @@ -47,6 +47,7 @@ type Selection struct { } type Options struct { + DefaultMode Mode Site string Version string Revision string @@ -65,6 +66,7 @@ type Server struct { assets fs.FS static http.Handler site string + defaultMode Mode localAccessPassword string version string revision string @@ -115,6 +117,7 @@ func Start(parent context.Context, assets fs.FS, options Options) (*Server, erro assets: assets, static: http.FileServer(http.FS(assets)), site: normalizedSite, + defaultMode: options.DefaultMode, localAccessPassword: options.LocalAccessPassword, version: options.Version, revision: strings.TrimSpace(options.Revision), @@ -238,6 +241,9 @@ func (server *Server) handleState(response http.ResponseWriter, request *http.Re "debug": server.debug, "lan": map[string]any{"addresses": addresses, "selected": selected}, "defaultMode": func() Mode { + if validMode(server.defaultMode) && (server.defaultMode != ModeSite || server.site != "") { + return server.defaultMode + } if server.site != "" { return ModeSite } diff --git a/internal/server/config/config.go b/internal/server/config/config.go index 027e3d2..45cd04b 100644 --- a/internal/server/config/config.go +++ b/internal/server/config/config.go @@ -28,7 +28,7 @@ const ( // Bounds and defaults for Hosted configuration. const ( - defaultMaxViewersPerRoom = 8 + defaultMaxViewersPerRoom = protocol.MaxViewersPerRoomLimit defaultPort = 8787 maxPort = 65_535 ) diff --git a/internal/server/config/config_test.go b/internal/server/config/config_test.go index ea074a3..4b19ed8 100644 --- a/internal/server/config/config_test.go +++ b/internal/server/config/config_test.go @@ -84,7 +84,7 @@ func TestLoadDevelopmentDefaults(t *testing.T) { if config.NATPredictionSTUNURLs != nil { t.Errorf("NATPredictionSTUNURLs = %v, want nil (TS left it undefined)", config.NATPredictionSTUNURLs) } - if config.MaxViewersPerRoom != 8 { + if config.MaxViewersPerRoom != 20 { t.Errorf("MaxViewersPerRoom = %d", config.MaxViewersPerRoom) } if config.EndpointMediaCopyCapacity != 2 { @@ -203,7 +203,7 @@ func TestLoadAccepts(t *testing.T) { check func(*testing.T, Config) }{ {"default viewer limit", map[string]string{}, func(t *testing.T, c Config) { - if c.MaxViewersPerRoom != 8 { + if c.MaxViewersPerRoom != 20 { t.Errorf("MaxViewersPerRoom = %d", c.MaxViewersPerRoom) } }}, diff --git a/public/favicon.svg b/public/favicon.svg index 08bb32d..5b3406e 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1,5 +1,5 @@ - + diff --git a/site/film/README.md b/site/film/README.md index 564cdd5..1559095 100644 --- a/site/film/README.md +++ b/site/film/README.md @@ -1,10 +1,10 @@ # Piik film The homepage expands this optional, 76-second introduction in an iframe loaded -only on request. Closing it unloads playback; the adjacent **Open film page** link -opens the same player on its own page and carries the current language and theme. -Use `/film/?lang=en` for an English direct link. From the repository -root, run `npm run build:website` and `npm run preview:website`, then open +only on request. Closing it unloads playback; the frame follows the homepage's +language and theme. The player document also supports local recording and +focused checks. From the repository root, run `npm run build:website` and +`npm run preview:website`, then open `http://127.0.0.1:18890/film/`. Publish the static `build/site/` output. A project prefix such as `/Piik/film/` also works; GitHub Pages needs no application server. diff --git a/site/film/art.js b/site/film/art.js index 72e179e..4d87f78 100644 --- a/site/film/art.js +++ b/site/film/art.js @@ -211,7 +211,7 @@ export function createArt(svg, language, onUI = () => {}) { ${rect(-115,383,950,200,0,INK)}${text(57,538,say('LOW LATENCY.','低延迟。'),zh?160:106,PAPER)} ${sceneLabel(say('PIIK / FEATURES', 'PIIK / 产品特点'))} - ${text(65,793,say('A direct path, so friends can keep up.','画面优先直达朋友,精彩及时跟上。'),30,INK,'style="letter-spacing:0;font-weight:650"')} + ${text(65,793,say('A direct path first, so friends can keep up.','画面优先直达朋友,精彩及时跟上。'),30,INK,'style="letter-spacing:0;font-weight:650"')} ${field(YELLOW)} @@ -229,7 +229,7 @@ export function createArt(svg, language, onUI = () => {}) { ${rect(-110,388,945,197,0,INK)}${text(48,541,say('SHARING.','轻负担。'),zh?164:153,PAPER)} ${sceneLabel(say('PIIK / FEATURES', 'PIIK / 产品特点'))} - ${text(65,793,say('Lighter on resources, with more power left for games and creative work.','少占些资源,给游戏和创作多留些性能。'),30,INK,'style="letter-spacing:0;font-weight:650"')} + ${text(65,793,say('Reuse picture processing where supported, for efficient sharing with friends.','能共用的画面处理,就不重复做;多人分享也讲效率。'),30,INK,'style="letter-spacing:0;font-weight:650"')} ${field(INK)} diff --git a/site/film/film.css b/site/film/film.css index e16d14a..6e45846 100644 --- a/site/film/film.css +++ b/site/film/film.css @@ -7,6 +7,7 @@ body { margin:0; background:var(--wall); color:var(--ink); font:15px/1.6 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif; -webkit-font-smoothing:antialiased; } [hidden] { display:none !important; } html[lang="en"] [lang="zh-CN"], html[lang="zh-CN"] [lang="en"] { display:none; } +html .no-script [lang] { display:inline; } button,a,input { -webkit-tap-highlight-color:transparent; touch-action:manipulation; } button,a { color:inherit; } button { font:inherit; cursor:pointer; } @@ -18,7 +19,7 @@ button,a,input { outline-offset:5px; } .sr-only { position:absolute; width:1px; height:1px; margin:-1px; padding:0; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; } .masthead { width:calc(100% - 96px); max-width:1600px; margin:auto; height:90px; display:flex; align-items:center; justify-content:space-between; gap:20px; } .masthead nav { display:flex; align-items:center; gap:28px; font-size:13px; font-weight:650; } -.masthead nav a,.masthead button { min-height:44px; display:inline-flex; align-items:center; text-decoration:none; } +.masthead nav a,.masthead button,#language { min-height:44px; display:inline-flex; align-items:center; text-decoration:none; } .masthead button { font-size:12px; } .masthead nav :is(a,button):hover { color:var(--accent); } main { width:calc(100% - 64px); max-width:min(1664px,calc((100svh - 248px) * 16 / 9)); margin:auto; } diff --git a/site/film/index.html b/site/film/index.html index b3f1fc2..6ddd01f 100644 --- a/site/film/index.html +++ b/site/film/index.html @@ -17,7 +17,7 @@
Piik. -