Skip to content

fix(transcode): preserve portrait geometry with short-side rungs - #5232

Merged
kixelated merged 11 commits into
mainfrom
quest/m1/portrait-ladder
Oct 12, 2026
Merged

kixelated merged 11 commits into
mainfrom
quest/m1/portrait-ladder

Conversation

@kixelated

@kixelated kixelated commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A 360-pixel rung from 1080x1920 portrait input became 202x360, spending a landscape bitrate on a much smaller picture. Rungs should size the coded short side, producing 360x640 while preserving rotation metadata.

The strict portrait end-to-end harness (publish, transcode, <moq-watch> in Chromium, failing on any page or console error) also exposed two JS bugs and one harness race:

  • subscribe stream ended without SUBSCRIBE_END surfaced as an uncaught page error when a superseded catalog subscription closed. Reproduced 3/3 on the previous head.
  • After merging fix(net): bind JS consumes to the serving announcement #5254/fix(net): refuse new requests from retired sources #5294, the superseded subscription instead ends with a plain Error("broadcast instance retired"), which @moq/watch logs with console.error. Reproduced 4/4.
  • Under load, a CLI peer falls back to WebSocket and then upgrades to QUIC. On lite-06 that replaces the epochless instance, so the transcoder's source request fails as unroutable and the process exits.

Approach

Transcode. Resolve both dimensions from the source's coded short side, round for I420 chroma, and apply the no-upscale and below-source bitrate checks to that side. Preserve catalog rotation. CLI help, README and doc/bin/cli.md describe --rung size:bitrate as short-side pixels.

@moq/signals race. Root cause of the missing-END error. race returned as soon as an input had already settled, without observing the rest. The lite subscriber races [closed, producer.closed, updates.then(() => closed)]. If the producer had already closed, the third input was never observed, so the response reader's rejection after the local close went unhandled. race now observes every input like Promise.race, still through one shared reaction per promise. The same applies to an already-aborted effect.race. Confirmed in the browser: patching only race on the old head turned 3/3 failures into 3/3 passes. A unit regression test fails without the fix.

@moq/net retirement. A retired instance now aborts with StreamError(StreamCode.Unroutable), mirroring Rust's Error::Unroutable, so players treat it as the instance going offline rather than as a failure. A regression assertion in consume.test.ts fails without the fix.

Harness. The portrait relay accepts moq-lite-07-wip and moq-lite-06. The CLI publisher and transcoder negotiate lite-07, whose epochs carry an initial request across a session replacement, per the decision in #5333. Chromium stays on lite-06. Page errors are still rejected. Nothing waits, retries or disables a fallback. The portrait quest is complete and removed.

Validation: just check and just test interop --all pass, and just test portrait passed 5/5 on a loaded machine.

Public API

Breaking, Rust moq-transcode:

  • Rung.height is now Rung.size. Rung::new(size, bitrate) takes coded short-side pixels.
  • ladder::Error::Empty { size, .. } and ladder::Error::Unordered { size, below_size, .. } replace the height/below_height fields.

Behavior only:

  • moq transcode --rung size:bitrate: the syntax is unchanged, but the value is now the short side. A portrait source gets the intended larger picture, and landscape output is unchanged.
  • @moq/net: requests on a retired broadcast instance reject with StreamError code Unroutable instead of a plain Error. Signatures are unchanged.
  • @moq/signals race: no longer leaves a losing input unobserved. Signatures are unchanged.

Wire: none. Catalog format and media framing are unchanged. Catalog dimension values for portrait sources change. No JS or binding exposes Rung.

Decisions

Rung field name (maintainer):

  1. ✅ size, documented as short-side pixels
  2. height (misdescribes portrait input)
  3. short_side

Missing SUBSCRIBE_END page error (maintainer: fix at the source, don't relax the harness):

  1. ✅ Fix race so every input is observed, as Promise.race does
  2. Catch the rejection at the lite subscriber call site only
  3. Stop rejecting page errors in the harness

Retired-instance error type:

  1. ✅ StreamError(Unroutable) in @moq/net, mirroring Rust
  2. Special-case the message in @moq/watch

Harness across a WebSocket-to-QUIC upgrade:

  1. ✅ Negotiate lite-07 on the CLI peers, matching test(net): preserve identity during initial catalog acquisition #5333's identity decision
  2. Disable the CLI WebSocket fallback in the harness
  3. Make lite-06 acquisition survive the replacement (test(net): preserve identity during initial catalog acquisition #5333 refused this)

Follow-ups

  • Chromium does not advertise lite-07 by default, so a browser that itself falls back to WebSocket and upgrades can still lose an initial request on lite-06. That is the existing transport-upgrade questline, not this PR.
  • No benchmark was added for race. The common path, where no input has already settled, is unchanged apart from one branch. The decided path now does one WeakMap lookup per remaining input instead of returning early.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 10 commits October 10, 2026 09:45
Co-Authored-By: GPT-6 <noreply@openai.com>
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
Retain the portrait quest while strict browser validation exposes the subscription cancellation failure.

Co-Authored-By: GPT-6 Astra <noreply@openai.com>
A retired instance aborted its requests with a plain Error, which players
log as an unexpected failure. Rust ends them with Unroutable; mirror it so
watch treats a superseded instance as going offline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
race() returned as soon as an input had already settled, without observing
the rest. A loser that rejected later, such as a subscription's response
reader after a local close, surfaced as an unhandled rejection in the
browser. Observe every input like Promise.race, keeping one shared reaction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A CLI peer that falls back to WebSocket and then upgrades to QUIC replaces
its session. On lite-06 that is a new epochless instance, so the source and
derivative catalog requests fail as unroutable by design. Lite-07 epochs
carry them across the upgrade. The strict browser run now passes, so the
portrait quest is complete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review October 12, 2026 02:52
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

(Written by Claude Opus 5.5)

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 5 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8e9978a3-b938-4d7d-a2b2-c71f1a32fab2

📥 Commits

Reviewing files that changed from the base of the PR and between 62d727c and d78aa1b.


📒 Files selected for processing (20)
  • .github/workflows/interop.yml
  • doc/bin/cli.md
  • js/net/src/consume.test.ts
  • js/net/src/consume.ts
  • js/signals/src/index.test.ts
  • js/signals/src/index.ts
  • quest/m1/README.md
  • quest/m1/portrait-ladder.md
  • rs/moq-cli/src/transcode.rs
  • rs/moq-transcode/README.md
  • rs/moq-transcode/src/active.rs
  • rs/moq-transcode/src/catalog.rs
  • rs/moq-transcode/src/config.rs
  • rs/moq-transcode/src/ladder.rs
  • rs/moq-transcode/src/pipeline.rs
  • rs/moq-transcode/src/rung.rs
  • test/interop/clients/js/portrait.ts
  • test/justfile
  • test/portrait/relay.toml
  • test/portrait/run.sh

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-12T03:00:04.419976Z d78aa1b Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed the full current PR at b146695 against base 62d727c; no earlier review with this attribution was present.

No actionable correctness findings in the current changes. The direction is sound: scaling both coded dimensions from the short side keeps portrait and rotated-landscape geometry consistent while retaining the no-upscale/bitrate filters. Observing race losers in the shared primitive and giving retired instances a typed Unroutable error address the underlying lifecycle behavior rather than special-casing the player. The existing shared promise reaction remains useful for repeated races; a bare Promise.race replacement would reintroduce accumulating reactions on long-lived inputs.

The geometry/rotation unit coverage and strict browser harness fit the change. Verification was GitHub-only static inspection of the patch and relevant surrounding code; I did not execute tests. Exact-head CI, including Check and Interop, was still running when checked. No independent review findings were present at publication.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b14669573d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +129 to +130
- name: Portrait transcode
run: nix develop --command just test portrait

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the portrait suite independent after setup

Unlike the surrounding suites, this step has no explicit if, so GitHub Actions applies the implicit success() condition. If any earlier independently-run suite fails during a nightly or manual run, the portrait test is skipped instead of reporting its own result, contrary to the workflow's stated independent-suite behavior. Give it the same !cancelled() && steps.setup.outcome == 'success' condition as the adjacent test steps.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, fixed in d78aa1b: the portrait step now uses the same !cancelled() && steps.setup.outcome == 'success' condition as its neighbours.

(Written by Claude Opus 5.5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

(Written by Claude Opus 5.5)

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed d78aa1b against our previous b146695 review. The base is unchanged; the only delta is .github/workflows/interop.yml:130.

The added condition fixes the independent Codex finding at #5232 (comment): portrait now runs after an earlier suite fails, while still requiring successful setup and honoring cancellation. This is the minimal fix and matches adjacent suites. No new actionable findings; our previous review had no open findings.

Verification: GitHub-only static comparison and surrounding workflow inspection. I did not execute the workflow or tests.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: d78aa1b414

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@kixelated
kixelated added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit ca89952 Oct 12, 2026
9 of 10 checks passed
@kixelated
kixelated deleted the quest/m1/portrait-ladder branch October 12, 2026 03:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant