Repository navigation
Conversation
The owner asked for a cloud version: streams that survive the computer, browser guests for talk shows, and the phone as camera and remote. The plan has Phase 0 (spikes) and four phases: the cloud holds solo streams, cloud-composed talk shows on LiveKit, the phone, and Max extras. It includes the Phase 1 wire contract for the desktop, the web and the private runner repo. The owner chose every recommended answer on 2026-10-16 and asked to start executing in draft PRs that are not merged. Plan 185 (the Max tier) lives in the private videorc-web repo. Co-authored-by: Claude <noreply@anthropic.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…deorc Max (plan 185 T4)
The capabilities answer gains two optional blocks, `plan` and `cloud`, read
with `#[serde(default)]` and `skip_serializing_if` on every Option so an
older web deploy and a newer one both load (the serde-null contract trap).
Max stays `tier = premium` everywhere; the desktop snapshot carries the plan
as an optional field beside the tier, never as a tier value. The signed token's
optional `plan` claim is read too, with the capabilities' plan as the fallback.
The upgrade-toast matcher also recognises "Videorc Max", with the prefix only:
a bare "max" is a limit ("max 5 destinations"). Shared fixtures carry the Max
capabilities answer, the Premium cloud block and both snapshots; Rust and the
strict TypeScript schemas validate the same JSON.
Co-authored-by: Claude <noreply@anthropic.com>
C2: cloud_show.rs owns the show. That covers the web client (create,
current, end, allowance), the control socket (hello with the vcs1
token, intents with acks, a ping every 5 s, reconnect with backoff) and
the state machine off/connecting/live/hold/resuming/ending/failed. The
RPCs are cloud.status (the only one a remote role may read),
cloud.settings.set, cloud.allowance.refresh, cloud.show.resume and
cloud.show.end. The event is cloud.state, a whole secret-free status.
Direct legs reopen only after the web confirms the show is gone,
because two pushes to one key make a platform drop both.
C3: in cloud mode the session's platform targets become one RTMPS feed
per orientation, {serverUrl}/{streamKey} at each orientation's encode,
lowest bitrate when targets differ. The platform targets go to the
runner in destinations.set, backend to runner, never through the
renderer. They are also listed after the feed rows in the session's
leg state, so FFmpeg leg positions stay exact. apply_cloud_leg_states
moves those rows with the runner's per-platform state.
C4/C5: Go Live asks the cloud first and streams directly, saying why
once, only when the account expected the cloud. A Basic user, or a
Premium user while the rollout switch is off, sees nothing new. Resume
after a relaunch fetches the ingest over the authenticated socket with
the new ingest.get intent (Wire shape, 2026-10-17): the web never
stores ingest keys and this computer never persists them.
Gates: cargo fmt, clippy -D warnings, targeted cloud_show and seam
tests (16), desktop vitest (4882 passed), typecheck, lint (0 errors),
format:check and smoke:streaming-secrets all pass.
Co-authored-by: Claude <noreply@anthropic.com>
…notice and the Go Live cloud step (plan 184 C4-C7) Contracts: cloud.status, cloud.settings.set, cloud.allowance.refresh, cloud.show.resume and cloud.show.end in the RPC map, the cloud.state event, and strict schemas for CloudStatus and CloudSettingsPatch. A shared fixture block (protocol-fixtures/high-risk-contracts.json, cloud) round-trips through both languages, so a renamed field or a null where the schema says absent fails in a unit test first. C6: Livestream -> Setup gains a Videorc Cloud section beside Output quality: "Stream through Videorc Cloud" with the month's hours, the image viewers see if this computer drops (the Be right back card or a takeover), and when the stream ends (5 min, 15 min, 30 min, 1 hour). Locked means disabled with one reason. A Basic account gets the Premium sentence with View Premium over switched-off controls. A paid account the cloud isn't on for yet says so with no Premium prompt. The page's session lock now names Videorc Cloud. C5: a show still live on our servers while this computer isn't feeding it puts "You're still live through Videorc Cloud." above Session, with the hold countdown. Resume is Go Live, which the backend binds to that show. End livestream asks first, like every in-app stop. C4: the Go Live primary reads "Connecting to Videorc Cloud..." while a cloud start runs. The provider holds the cloud status in its own context, so status ticks during a show re-render only what reads them. It toasts a direct fallback once per Go Live, and a lost show once. Errors go through toastUserError. Gates: typecheck, lint (0 errors; the one warning is on main), format: check, desktop vitest (476 files, 4900 passed), renderer asset budget (pass), smoke:start-labels, smoke:streaming-secrets, cargo fmt, clippy -D warnings, targeted cloud tests (25).
…e left (plan 184 C7)
The Stream Manager's dashboard carries the Videorc Cloud show for its own
session: cloud.state folds into LiveDashboardState.cloud (phase, and the
hold's seconds left), cleared by another session's status or the cloud
going off, and validated before main caches it.
Stream health stays one stat in the 32 px bar. Healthy through the cloud
it reads "Cloud · 6 Mbps". While the cloud holds the show it reads "Be
right back · 28 min left", which outranks the upload's own reconnecting
row, and while resuming "Cloud resuming". On hover the upload rows fold
into one Videorc Cloud row with the show's state ("On air", "Be right
back" with "Ends the stream in 28 min"), above the runner's per-platform
rows. A direct session reads exactly as before.
Gates: typecheck, lint (0 errors), format:check, desktop vitest (476
files, 4902 passed).
Max is Premium plus a plan (plan 185 D2): the tier stays premium and the entitlements snapshot's plan names it. The shell context carries the plan to the account menu's badge, which reads Max for a Max account and Premium otherwise; a stray plan never relabels Basic. Gates: typecheck, lint (0 errors), format:check, desktop vitest (476 files, 4902 passed).
… (plan 184) - The first welcome may take 20 s (was 5 s). A runner the create just spawned opens its socket only after its engine starts, and the node waits up to 15 s for it behind the hello. Before, a slow start fell back to direct as an unreachable node. - Creating a show gets 20 s (was 10 s). The web waits up to 8 s for the node, plus its own work. - A Go Live that meets a show its node already ended closes it on the web and starts a new one, once. Before, the web's stale row answered "show live", the desktop adopted it, the node said no such show, and Go Live failed until the reaper ran. A resumable show that ended while this computer was away now starts a new show on Go Live, instead of an error. Gates: cargo fmt --check, clippy -D warnings, cloud tests (27, two new).
…e runner together (plan 184 C8) The first run of the real dev app against a node: MediaMTX and the agent from the sibling videorc-cloud checkout on free ports, a fake control plane that mirrors the web's routes, node signatures and vcs1 tokens, and two local RTMP receivers. The feed cut is a TCP proxy that drops the RTMPS path (the plan's kill-the-encoder would end the recording too, and a MediaMTX kick is undone by the fifo's 2 s retry); the computer dies with SIGKILL of the whole app and a relaunch resumes the same show. ffprobe and ffmpeg check every receiver: streams, duration, the largest packet gap, the hold card during each drop and motion after each resume.
… setup (plan 184 C5b) The Studio's "You're still live through Videorc Cloud." notice ran the full Go Live: the dialog, the preflight, the metadata update and every platform prepare, so a YouTube sign-in destination got a new broadcast and key on each Resume, the runner moved its leg there, and the live broadcast and its VOD split. Resume is now its own start, `cloud.show.resume`, bound to the show by the backend. The renderer sends the saved destinations as rows only (no dialog, no preflight, no account check, no broadcast prepared or activated). The backend connects to the show, takes the ingest it kept or asks the node for it (`ingest.get`), feeds every orientation the show has that the session composes, sends the hold image only if the user picked one, and sends no `destinations.set`: the runner keeps every leg and key it holds. OAuth destinations without a key are rows the runner's leg states move, never a reason to refuse the start. A start that fails after binding the show makes it resumable again instead of leaving it bound to a session that never ran. A failed Resume says why under "Couldn't resume the stream". Go Live while a show is resumable still hands its freshly prepared destinations over: that is a new broadcast by choice. Tests: the backend's resume sends no destinations.set, asks only for the ingest without a hold image, works with no ready destination and feeds per ingest orientation; the renderer's Resume calls cloud.show.resume and never streamTargets.youtube.prepare, the preflight or the dialog; smoke:cloud-solo counts the runner's destinations.set after the Resume click and fails on any.
…me (plan 184 C1c) FFmpeg's `tls_verify` defaults to 0, so the feed to the node published to any certificate: whoever answered for the node's name (a spoofed DNS answer on hostile Wi-Fi) got the show's ingest key and could push their own video into the show. The control socket already verified; the feed did not. Every cloud feed leg now carries a TLS check in every output shape the legs take: the bridge's per-leg fifo (`-format_opts tls_verify=1:ca_file=…`, re-applied by the fifo on every reconnect), a tee slave's bracketed options, and the plain `-f flv` output's CLI options. With FFmpeg 8.1's OpenSSL backend (the macOS and Linux builds) `tls_verify=1` verifies the chain against `ca_file` and binds the URL's host name (`SSL_set1_host`); the Windows build's Schannel verifies both against the Windows store and ignores `ca_file`. OpenSSL's compiled-in default paths point nowhere on a user's machine, so the backend writes the roots it trusts beside the database (`cloud-feed-roots.pem`): Mozilla's store from webpki-root-certs, the same store the control socket and the API client trust. Dev builds may add one extra CA through `VIDEORC_CLOUD_FEED_CA_FILE`, read like `VIDEORC_API_BASE_URL` and never in a packaged build. A release build refuses an ingest address by IP, because FFmpeg skips the name check for a numeric host; a dev build warns. A roots bundle that cannot be written fails the start before anything reaches the web. Direct platform legs are unchanged. Measured with the local FFmpeg 8.1.1 through the fifo muxer: the right roots pass the handshake, a CA that did not sign the certificate fails it, and a certificate for another name fails it. smoke:cloud-solo now announces the node as `localhost` (a DNS name, so the name is checked), trusts the run's self-signed cert through the dev override, and runs those three FFmpeg publishes as a gate; it passed with checking on (feed cut held 1.53 s, back 4.81 s after the network; resume 5.74 s; receivers one connection, 34 ms gap).
…e's 0.9.0 (plan 184) `POST /api/cloud/shows` and the control socket's `hello` reported the backend crate's version. The web and the node read `client.version`, so they saw 0.9.0 for every desktop. It now comes from `desktop_app_version()`, which reads the `VIDEORC_APP_VERSION` Electron sets when it spawns the backend and falls back to the crate's version without it. The node agent logged `client="desktop 0.9.143"` for the smoke's dev app.
…art changes (plan 184) The runner now embeds its default hold card's PNGs with include_bytes! (videorc-cloud b632892f), so new art alone left the smoke on a stale binary: the staleness check only read .rs and Cargo.toml. It counts .png under crates/ too. The hold card check is unchanged. The new card, "Be right back" in Noto Sans on the dark base, reads 98.8% dark and 0.6% bright on the sampled 720p frame (the bitmap card read 99.2% and 0.4%). smoke:cloud-solo passed: the feed cut held in 1.53 s and came back 4.81 s after the network; the computer's death held in 1.65 s and was live 5.76 s after Resume; the card covered 99.6% and 100% of the hold windows on both receivers; largest gap 34 ms.
…ntory (plan 184 C5) The execution-policy inventory pins which RPCs own their session lifecycle instead of the generic mutation deadline. cloud.show.resume has taken that path since C5 (it is session.start bound to the live cloud show), but the pinned list never named it, so the test failed in CI on every push since. Full macOS backend suite: 3524 passed, 0 failed. clippy -D warnings clean. Co-Authored-By: Claude <noreply@anthropic.com>
…lan 184 C6b) `cloud.selftest.run` starts a test and answers at once; its progress and result ride `cloud.state` as `CloudStatus.test`. (A `.test.` segment would make the RPC admin-only and smoke-only in `backend_authority`, so it is `selftest`.) The test creates a `kind: "test"` show on the web, connects the control socket, and pushes 10 s of noisy `testsrc2` and a sine with the app's own FFmpeg at the stream quality's size, fps and bitrate, over RTMPS with the node's certificate checked (C1c). It reads the runner's feed kbps (the median of its 2 s readings) and live state, ends the show on the socket and the web, and kills its FFmpeg through the child it spawned. The verdict names one reason: a refused certificate, a push that never ran (FFmpeg missing: this computer's failure, never the network's), a feed that never arrived, a show the runner ended, an upload under 85% of video plus audio, or a push that failed. An upload the node never measured is unmeasured, never slow. The push lifts VideoToolbox's data-rate ceiling to 140% of the bitrate. At a stream leg's `-maxrate` equal to `-b:v` the noisy card settles near 70% of the bitrate (2,951 of 4,160 kbps at 720p30 on an M4), so every test would read as too slow; at 140% each 2 s window carried 94 to 102% at 720p30, 1080p60, 1440p60 and 1080x1920. FFmpeg's lines are kept redacted: the key replaced, whole or its secret part, then every URL token dropped, as a session's stderr tail is. The test never touches the Studio's session or the slot's show. It refuses while a session runs or finalizes and while a show is live or resumable. Any session start stops a running test without waiting; a cloud Go Live and a Resume wait for its show to end, because the web allows one open show per account. A 409 that names a test (one a crashed desktop left open; its `kind` is now parsed) is ended and the create runs once more, for Go Live and for a new test, never adopted like a live show. `CloudApiError::ShowLive` boxes its record, which keeps every `Result` it rides in small. The RPC is in the execution-policy inventory (default mutation) and round-trips the shared high-risk fixture in both languages (`cloud.statusTest`, `cloud.testRunParams`). Full macOS backend suite: 3534 passed, 0 failed (15 ignored). clippy -D warnings clean. Co-Authored-By: Claude <noreply@anthropic.com>
A "Test Videorc Cloud" row in the open Videorc Cloud section, a SettingsRow
in its grouped FieldGroup. Test is an outline button that reads "Testing…"
with a silent spinner while the test runs. The row's description says what
the test does ("Sends 10 seconds of test video. Nothing goes public."), the
step under way, then the result: "Videorc Cloud works from here. Live in
2.3 s. Upload held 4.1 of 4 Mbps." A failure the streamer acts on (a slow
upload, an unreachable cloud, a refused certificate, a blocked feed) is the
row's FieldError instead, with one fix each; a refusal (the hour's tests, the
cloud off or full) stays a plain line. The result is never a toast: only a
click the backend refuses reaches one, through toastUserError, as a warning.
Test locks with the section (its LockAlert says why), and is disabled with
one DisabledReason while a recording or livestream runs or a show is live or
resumable. It pushes the shape a show would carry (`cloudTestOutputs`): the
enabled horizontal destinations' stream quality at their lowest bitrate, or
the default stream profile before the first destination, plus the vertical
leg when it is armed.
Co-Authored-By: Claude <noreply@anthropic.com>
…st to its 30 s cap (plan 184 C6b) Before Go Live, `cloud.selftest.run` pushes 10 s to the same runner with no platforms, and must pass on loopback: live within 20 s and the upload at least 85% of the stream quality's bitrate. The fake web learns the test kind as the web has it (videorc-web 62d4f8ea): a solo show capped at 30 s to the node, never current, never metered, six an hour with 429 cloud-test-limit, and its call log now records what a create asked the node for, never the key. The gates: the test passed, the node was asked for a solo show capped at 30 s, the web saw the create and the end, current stayed empty while the push ran, the runner applied no destinations.set, the receivers took nothing, the desktop's own show never changed, and the node's usage report for the test was accepted and never metered. A new step holds the cap end to end: a test show nobody ends (the desktop died mid-test), fed by the smoke's own FFmpeg with the certificate check, must end `max-show` on the runner 30 s after its first feed frame, and the web must close it from the node's heartbeat. The leak scan now reads every launch's renderer events (it read only the last launch's) and every show's ingest key, the tests' too. Go Live's create and the shows created after the death are counted from Go Live on, so the test shows before it are not counted as this show's. A reused VIDEORC_SMOKE_OUTPUT_DIR no longer fails the receivers: their files are removed before they start, where an older run's file used to read as bytes from the first second. VIDEORC_CLOUD_SOLO_SHOTS_DIR=<dir> captures the Test row idle, running, with the result and after a test that failed (FFmpeg missing), each in dark and in light: onboarding is marked done with one reload before the test, and the sidebar's theme toggle flips the theme live, so the running state stays on screen. Pass on the final code (M4): the test was live 4.55 s after the click with the upload at 4,130 of 4,000 kbps (plus 160 audio, 99% of the total), the whole test 10.3 s; the unended test show ended max-show 29.999 s after it went live and the web closed it; the blocked test read no-feed; the feed cut held in 1.58 s and came back 3.10 s after the network; the death held in 1.60 s and was live 5.76 s after Resume, 1 show, 0 destinations.set after it; the hold card covered 100% of both hold windows on both receivers, largest gap 34 ms; Stop 94 ms; no secret in any launch's events or the app log. Co-Authored-By: Claude <noreply@anthropic.com>
C6b is done: the Test row, `cloud.selftest.run`, the web's test kind and the smoke's Test and cap steps, with the measured numbers. The Wire shape gains `kind: "test"`, 429 cloud-test-limit, and the 409 body's `kind`, which the desktop now reads. Co-Authored-By: Claude <noreply@anthropic.com>
…(plan 184) The spike's dated note, as Phase 0 asks: the macOS configure change (libsrt 1.5.7, static, one flag), the size delta (+0.66 MB ffmpeg, +0.68 MB ffprobe), the license check (FFmpeg stays LGPL v3 or later; the bundle adds the MPL-2.0 text and the UDT BSD notice), the Windows and Linux pins (libsrt present but 1.5.5, before the KMREQ CVE fix: re-pin to autobuild 2026-09-30), and the measurements: an SRT feed ready 0.4 s after the publisher starts against RTMPS's 1.6 s, 7.9% upload overhead against 4.7%, drops under 5 s kept, and 1% and 5% loss lost nothing at 1000 ms latency. SRT's stream id travels in the clear, so C1b keys an SRT feed by a passphrase per feed on a MediaMTX path per feed, never by the ingest key. Plan 184's Progress gains the S0c row. Co-Authored-By: Claude <noreply@anthropic.com>
…r ingest pair (plan 184) From the S0c note: SRT sends its stream id in the clear, so no secret may sit in a feed's path. Ingest on the node becomes one MediaMTX path per feed, `live/<showId>-<h|v>`, for both protocols: RTMPS carries the ingest key in the stream key's query, and SRT is keyed by a passphrase per show that the agent sets on the path through MediaMTX's API. The web mints the passphrase (never stored) and sets SRT's latency; each ingest pair, in the create response and in `ingest.get`, gains an optional `srt` block. C1b's slice now lists the build, leg, probe, redaction, health and smoke work the spike settled. Co-Authored-By: Claude <noreply@anthropic.com>
…per show and no catch-all path (plan 184) The smoke failed at the C1b node and web (runner a7a501c, web 7d8312b9), in the harness only: - The node requires srtPassphrase on POST /v1/shows (runner d636ba2, docs/wire-notes.md). The fake web never sent it, so every create got a 400 and the fake web answered cloud-node-unreachable: Test cloud read cant-reach. The fake web now does what web 5cbcbce2 does: one 32 base62 passphrase per show (a test's too) and srtLatencyMs, and the node's srt blocks pass through to the desktop with the web's checks (a malformed block is dropped by field name and fails the run). - The template has no catch-all path any more (runner d5eb542), so MediaMTX refuses the C1c probe's path right after the RTMP connect and FFmpeg says only "Input/output error": the right-roots probe read unknown. The probe runs at debug level and classifyFeedHandshake reads the RTMP lines, which FFmpeg prints only after the TLS handshake and its certificate check passed. - The template's SRT listener is :8890 on every interface; the run moves it to a free loopback port like the other listeners and tells the agent. - Every show's SRT passphrase joins the leak scan: the desktop gets it in the create answer and in ingest.get's ack, even while it feeds RTMPS. Pass on b6eba69 plus this change: Test live 4.61 s at 4,483 of 4,000 kbps; feed cut held 1.55 s, back 3.26 s; death held 1.66 s, Resume live 5.79 s, 1 show, 0 destinations.set; one connection for 62.1 s, gap 34 ms, hold card 99.9%; Stop 139 ms. No ingest key or passphrase in the app log, the renderer events or any node log. pnpm test:scripts 2445 pass, 0 fail. Co-Authored-By: Claude <noreply@anthropic.com>
…root-certs 1.0.10 (RUSTSEC-2026-0383, plan 184 C1c) C1c writes webpki-root-certs as cloud-feed-roots.pem, the roots the RTMPS feed checks a node's certificate against. 1.0.9 still bundles emSign ECC Root CA - G3; certificate paths through its distrusted intermediates, whose private keys are now public, would pass that check and hand over the ingest key. 1.0.10 removes the root (RUSTSEC-2026-0383). Cargo.lock only; origin/main does not depend on the crate. pnpm audit:rust now reports what origin/main's lockfile reports and nothing more: RUSTSEC-2026-0370/0371/0372 (zbus, unmaintained), 0381 (webpki-roots 1.0.7, the roots of the control socket and the API client) and 0382 (rustls-webpki 0.103.15). Gates: cargo clippy -p videorc-backend -- -D warnings clean; cargo test -p videorc-backend 3616 passed, 0 failed, 15 ignored (the_roots_bundle_is_ mozillas_store_plus_the_dev_ca included). Co-Authored-By: Claude <noreply@anthropic.com>
…ream key and srt blocks (plan 184 C1b)
Since C1b (runner 2b10405, web 5cbcbce2) the node's stream key is
<showId>-<h|v>?key=<ingestKey> and every ingest pair, in the create answer
and in ingest.get's ack, may carry an srt block. The tests still used the
old <showId>-h.<ingestKey> and no srt block, so nothing proved this desktop,
which feeds RTMPS only, reads what a C1b node and web send.
- A create answer in the Wire shape's C1b form parses, keeps the stream key
whole for {serverUrl}/{streamKey}, and its Debug shows neither the ingest
key, the SRT passphrase nor the token.
- The resume test's ingest.get ack carries an srt block per pair, and the
handover keeps both RTMPS stream keys whole.
- The cloud seam's fixtures use the keyed stream key, so the feed URL, its
certificate options and its redaction are checked with the key in the
query.
cargo test -p videorc-backend: 3616 passed, 0 failed, 15 ignored.
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…ost is told (plan 184)
Cloud test lane 2 B1: after a runner crash the agent restarts it in hold
without destinations (they live in memory only), and the desktop never
re-sent them, so every platform stayed dark while the rows still read live.
- The control loop reads the runner's new `destinationsSet` and `restarts`
(optional: an older runner shows a restart only as a state without legs).
After its own hand-over was acked, a runner without destinations gets the
same `destinations.set` again, once per socket; the runner keeps legs whose
id and URL are unchanged, so this never restarts a live leg.
- The session's rows go to Reconnecting while the legs are gone, instead of
keeping the old live rows. A show resumed after a relaunch holds no keys,
so its rows say to stop and go live again, unless the node gives the
runner its destinations back itself.
- `cloud.state` gains `restart: { at, destinationsResent }`; the renderer
says "Videorc Cloud restarted your show" once per restart, and once more
if the platforms could not be reconnected (a refused or silent re-send).
- The reconnect welcome now goes through the same state handler, so its
legs reach the session too.
Tests: the restart decision table, a restart over the real control loop
(re-send, rows, once per socket, legs back), a refused re-send, a node that
re-sent by itself, and the renderer toast. Existing tests now send the legs
a real runner keeps through a hold and a reconnect.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…4 Wire shape) Cloud test lane 5 S5: the control socket carries the show token and every platform key in `destinations.set`, and the desktop accepted `ws://` control URLs and plain `rtmp://` ingest in release builds too. - `control_url_problem`: a release build connects only to `wss://`; the connector refuses anything else before dialing. Dev builds keep `ws://` for the local agent the smokes run, with a warning. - `ingest_url_problem`: a release build feeds only `rtmps://` to a DNS name (the old IP-literal refusal moves here); dev builds allow plain RTMP and IPs with a warning. The feed builder in `recording.rs` uses it. - The ingest is now checked right after create, before the control socket opens: a node this computer can't feed securely never receives a key, the show ends on the web, and the Go Live streams directly with `cloud-node-unreachable` (D11). Both checks take the build kind as an argument, so the release branch is tested too (lane 5 noted only the dev branch ever ran). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…back (plan 184) Cloud test lane 3 R1: with `VIDEORC_CLOUD_DISABLED` on, the account's `cloud` block says not enabled, and the desktop only asked for a live show at launch when entitled. So a desktop relaunched during a rollback never learned which show to resume or end, and the runner held Be right back until its hold limit. The launch check now asks `GET /api/cloud/shows/current` whenever the account could own a show: entitled, or on a paid plan whose block says off. The web answers `current` during a rollback (videorc-web, C-W5). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…gnatures v2 first (plan 184) `smoke:cloud-solo` signs to the real node agent and checks its heartbeats, so the fake web must speak what the web and the agent now agree on (cloud test lanes 3 and 5): - Node signatures v2: HMAC-SHA256 over `v2\n<direction>\n<METHOD>\n<path>\n <timestamp>\n<raw body>`, `X-Videorc-Signature: v2=<hex>`. The fake signs `web-to-node` and verifies only `node-to-web` against the route it served; v1 is refused. The four shared vectors are pinned. - The web's replay guard (`node-request-replayed`) and its rule that a heartbeat signed no later than the newest applied one is `409 node-heartbeat-stale`; the forgotten-show rule reads the signed time. - Odd frame sizes are a 400 before any node is asked; a node that answered but refused is `502 cloud-node-rejected`, apart from an unreachable one. - A non-loopback agent must hand out `wss://` and `rtmps://`. - `setDisabled()` is the rollback switch: create is `503 cloud-disabled`, `current` still answers, the `cloud` block says off. The desktop's own mapping needs no change: `502 cloud-node-rejected` is a 5xx and streams directly, and `400 invalid-cloud-show` stays an error; the classification test now says so. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
D18 promises a warning at 11 hours before the runner ends any show at 12; cloud test lane 5 S8 found none. The cap is the runner's on-air clock, which `cloud.state` already carries as `liveSeconds`, so the renderer says once per show: "Your livestream ends in 60 min. A Videorc Cloud show runs 12 hours at most." A relaunch that meets a show already past 11 hours says it once with the time that is left. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… node by its driver Cloud test lane 1 E1 and E2, on ogre: - `cargo test -p videorc-backend` ran the real VAAPI encode probe on the first render node; on that boot `renderD128` was the AMD card, whose encoder hung and wedged the GPU until a reboot. Test builds now never probe a render node unless `VIDEORC_TEST_HARDWARE_PROBE=1` is set; they take the software fallback with a reason that says so. - `docs/linux-dev-loop.md` derived the pin from a hard-coded `renderD128`. The numbering changes between boots, so the snippet now finds the i915 node by its driver and refuses to pin without one. - A cold `videorc-backend` build after `cargo test` took 20 min 05 s on ogre and hit the smokes' 20-minute prebuild limit. The Linux default is 45 minutes; other platforms keep 20. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… (plan 184 D11) Cloud test lane 5 S8: the web now refuses an account's 13th show in an hour with `429 cloud-show-limit` and `resetAt` (failed creates included, after the one-open-show check). Classified as `Other`, it would have failed the Go Live instead of streaming directly. - `CloudApiError::ShowLimit` and the fallback reason `cloud-show-limit`: the Go Live streams directly, and the toast says "You've started this hour's Videorc Cloud shows. You're streaming directly from this computer for now." The shared schema knows the new reason. - The fake web mirrors the limit (tests keep their own six an hour). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… 184) Re-running `smoke:cloud-solo` on Linux (ogre, after the cloud test's GPU incident) failed after the feed cut: the runner held, but the feed never came back. The raw path (Linux, weak Windows) records and streams through one `tee`, whose RTMP slaves had no recovery (plan 184 says so: "no auto-recovery"). FFmpeg logged "Slave muxer #1 failed: Broken pipe, continuing with 1/2 slaves" and the session went on without its feed, so the show would hold until its limit. The encoded bridge (macOS) already wraps each leg in the fifo muxer, which is why the M4 runs came back. A cloud feed's tee slave now runs inside the fifo muxer (`use_fifo`) with the per-leg recovery options, shared as `FIFO_RECOVERY_OPTIONS` with the fifo output. `fifo_options` is a dictionary inside the slave's dictionary, so its separators are escaped twice. Proven with the bundled FFmpeg 8.1.2 on ogre against MediaMTX: a recording plus a feed through a cuttable proxy came back 0.04 s after an 8 s cut with the MKV intact, and over RTMPS a wrong CA is still refused on every reopen while the right one publishes. Direct platform legs are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…atform has (plan 184) The capped test show (C6b) publishes with the smoke's FFmpeg, hard-coded to `h264_videotoolbox`. On Linux that encoder doesn't exist, the publish never pushed a frame, and the test show ended `no-feed` instead of at its cap, failing the run. macOS keeps VideoToolbox; elsewhere it's libx264, which the node's runner already needs from the same FFmpeg. With this and the raw path's reconnecting feed, `smoke:cloud-solo` passes on Linux for the first time (ogre, i7-8750H, openh264 desktop encode, libx264 runner, the node at videorc-cloud 8c3d6a7): Test live 4.23 s at 4,251 of 4,000 kbps; the unended test ended max-show 30.04 s after it went live; feed cut held 1.64 s and back 2.92 s after the network; the death held 1.57 s, resumable 11.8 s after the relaunch, Resume live 6.36 s, 1 show, 0 destinations.set; Stop 1.34 s; both receivers one connection, largest gap 67 ms; no secret in the app log or the renderer events. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… plan 184) Cloud test round 2 on ogre: on the raw path (Linux, weak Windows) only the cloud feed's tee slave had recovery (6c10dc8). Direct platform legs still sat in the tee's queue-only fifo, so a platform that dropped the connection stayed gone for the rest of the session, and a lone stream with no recording used a plain `-f flv` output that ended for good on the first broken pipe (a cloud feed with recording off included). - Every RTMP tee slave now sets its own `fifo_options` with the per-leg recovery the encoded bridge uses (`FIFO_RECOVERY_OPTIONS`). The MKV slave keeps the tee's queue-only fifo, so a failing recording still aborts and is never reopened. - A lone stream uses the same fifo output as a bridge leg. The raw path's encode already carries `+global_header`, which the fifo needs: without it every reopen published a stream MediaMTX dropped again (seen on ogre). - The fifo's `Error opening <url>` / `Recovery successful` lines already drive the per-leg Reconnecting / Resumed rows (plan 161's monitor reads every session's stderr and attributes by URL). - `smoke:stream-leg-eof` records at the stream's own quality off macOS: the raw path's one-encode rule refused its 720p recording beside a 1080p stream, so it never ran on Linux. Proven with the bundled FFmpeg 8.1.2 against MediaMTX through a cuttable proxy: a lone fifo output came back once, cleanly, after a 6 s cut. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… 184) Cloud test round 2: a runner that restarts draws its default card until it gets the custom hold image again. The node now hands back the last one it relayed (videorc-cloud be842ef, `state.holdImageSet`, C-C11), but its memory goes with an agent restart. The show keeps the hold image it handed over, in memory only. On a detected runner restart (unless the runner already says `holdImageSet: true`), or whenever the runner says `holdImageSet: false`, the desktop sends it again after the destinations, at most once per socket. The hand-over socket sends it itself, as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… counts as the web does (plan 184 D11) - `prepare_session` against the web's `429 cloud-show-limit`: the Go Live streams directly with the reason `cloud-show-limit`, nothing connects, nothing is ended, and the next Go Live asks the web again. - The fake web no longer counts a create the node refused or never answered toward the 12 shows (or the 6 tests) an hour, as the web does since 52038614 (C-W6 update). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n FFmpeg can't count one (plan 184) Cloud test round 2: on the raw path (Linux, weak Windows) every output is a tee or fifo, FFmpeg's progress says `bitrate=N/A`, and Stream health read "Not reported yet" for the whole cloud show. The runner measures the feed as it arrives (`cloud.state.feeds[].kbps`). The dashboard's cloud block carries the connected feeds' sum as `feedKbps`, and the health stat uses it when FFmpeg reports no bitrate: "Cloud · 4.2 Mbps". FFmpeg's own count wins when there is one; a direct session reads as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With every raw-path leg recovering by itself (332e13f), `smoke:stream-leg-eof` showed the leg's bytes arriving again while its row stayed Reconnecting. The fifo's "Recovery successful" is an INFO line, and the raw path's session FFmpeg ran at `-loglevel warning`, so plan 161's monitor never saw it. A streaming raw-path session now logs at `level+info`, like the encoded bridge, through the same stderr relay (INFO stays out of the warning paths, the startup dump's URLs stay out of the logs). Recording-only sessions keep `warning`. `smoke:stream-leg-eof` on ogre: YouTube reported reconnecting 769 ms after its ingest closed, resumed 1.76 s after it came back, the new stream starts on a keyframe, Twitch stayed live, Stop 1.24 s mid-retry, the recording finalized. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…or smoke names a dark display On ogre the preview asks the desktop portal for a screen at launch; with no picker the request ends by itself within seconds, and a `session.start` inside that window is refused with the share-dialog copy (the app's right answer). `smoke:multistream` won that race by chance and `smoke:stream-leg-eof` lost it. `startSessionWhenScreenSettles` retries the start for up to 30 s while that is the only refusal, authorizing the output directory anew each time (a refused start spends the single-use capability). `smoke:freeform-editor` times every gesture to animation frames; a window the compositor never presents (ogre blanks its panel when the lid closes) got no frames and failed with a bare "CDP Runtime.evaluate timeout". It now says the display is dark. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft, do not merge. The owner asked to prepare this and keep working on it for a while (2026-10-16).
Plan 184 builds Videorc Cloud:
What's here (Phase 1, the solo show)
plans/184-videorc-cloud.mdis the plan: Phase 0 spikes, four phases, the Phase 1 wire contract and a Progress table.Backend (C1 to C3).
cloud_show.rsholds:cloud.status,cloud.settings.set,cloud.allowance.refresh,cloud.show.resumeandcloud.show.end, and the eventcloud.state.The
recording.rsseam swaps the session's platform targets for one RTMPS feed per orientation. It hands the platform keys to the runner backend to runner, never through the renderer.Go Live (C4).
Resume (C5). If a show is still live on our servers and this computer isn't feeding it, Studio says "You're still live through Videorc Cloud." with the hold countdown.
ingest.get), because the web never stores ingest keys.Livestream → Setup (C6). A Videorc Cloud section: the switch, the month's hours, the Be right back image (the card or a takeover) and the hold limit. It's locked with one reason for Basic (the Premium sentence) and for a paid account the cloud isn't on for yet.
Stream Manager (C7). Stream health reads "Cloud · 6 Mbps", "Be right back · 28 min left" or "Cloud resuming", with one Videorc Cloud row on hover above the runner's per-platform rows.
Plan 185 T4.
planandcloudblocks parse with a shared fixture in both languages.Cross-repo wire review (2026-10-17)
A read-only, field-by-field check of desktop, web and node found five mismatches. They're fixed in all three PRs.
welcome(was 5 s), because a fresh runner opens its socket only after its engine starts.smoke:cloud-solo(C8): the app, the node and the runner togetherpnpm smoke:cloud-soloruns the real dev app against MediaMTX, the node agent and the runner from a siblingvideorc-cloudcheckout (VIDEORC_CLOUD_DIR). It uses a fake control plane that mirrors the web's routes, node signatures andvcs1tokens, and two local RTMP receivers. Without the checkout it fails; it never skips.Fixes after C8
cloud.show.resume. It skips the dialog, the preflight and YouTube prepare, sends nodestinations.set, and the feeds come from the show's ingest, so a live YouTube broadcast keeps its key.tls_verify=1).VIDEORC_CLOUD_FEED_CA_FILE).client.versionis now the app's version (it was the backend crate's 0.9.0).Known gaps (in the plan)
wsswith a real certificate.Gates (last run,
76ec6426)cargo fmt --checkis clean,clippy -D warningspasses, and the targeted cloud and FFmpeg-argument tests pass (41). The Windows cross-check compiles, with no new warnings.smoke:cloud-solopasses, with the certificate check on.smoke:multistream,smoke:stream-leg-eof,smoke:streaming-secretsandsmoke:youtube-quotapass.smoke:recording-matrix: 18 of 18 pass.e9d22bf0:smoke:record-latencypasses, with warm click to recording at 71 ms (median).Companion PRs (also drafts, never merge)
plan-185-max-and-cloud: plan 185 (Videorc Max at $99, cloud hours, billing), the cloud control plane (W1 to W7), and T5's pricing,/maxand account pages behind switches.phase-1-runner: the per-show runner, the node agent and the wire protocol, with S0a measured on macOS.Blocked on the owner
*.live.videorc.com(I5).🤖 Generated with Claude Code