Skip to content

mux: emit avcC/hvcC in catalog description for inline-SPS/PPS importers#1437

Closed
kixelated wants to merge 4 commits into
mainfrom
avcc-hvcc-emission
Closed

mux: emit avcC/hvcC in catalog description for inline-SPS/PPS importers#1437
kixelated wants to merge 4 commits into
mainfrom
avcc-hvcc-emission

Conversation

@kixelated
Copy link
Copy Markdown
Collaborator

Summary

The Avc3 and Hev1 importers cache parameter set NALs for re-insertion before keyframes, but until now they left the catalog description field empty. Downstream consumers that need an out-of-band AVCDecoderConfigurationRecord / HEVCDecoderConfigurationRecord (KVS via MKV CodecPrivate; CMAF muxers; anything not willing to scrape the bitstream) had no way to get one from an avc3/hev1 broadcast.

This PR:

  • Builds an avcC from cached SPS+PPS the first time both are observed, sets it as the rendition's description, and republishes the catalog. Same flow for hvcC once VPS+SPS+PPS are all cached.
  • Reorders the NAL handlers so the cache update precedes init() — that way the description builder sees the latest NAL, not the previous one.
  • In Hev1, splits "codec changed" (which requires a new track) from "description appeared" (in-place rendition update) so subscribers don't re-fetch the track when only the description becomes available.

The avcC layout follows ISO/IEC 14496-15 §5.3.3.1.2 (standard, no high-profile extension fields — those are inferable from the inline SPS we ship anyway). The hvcC layout follows §8.3.3; chroma/bit-depth/temporal-id fields come from the parsed SPS.

Test plan

  • cargo test --lib on moq-mux — 84 tests pass
  • Verified end-to-end pilot rebuild against a downstream consumer that requires description populated (KVS MKV bridge in quartermaster/moq-pilot worker)
  • Manual: confirm the description appears in a live catalog snapshot and that consumers using description (not just inline NALs) can decode

🤖 Generated with Claude Code

The Avc3 and Hev1 importers cache parameter set NALs but previously left
the catalog `description` field empty, leaving downstream consumers to
synthesize an AVCDecoderConfigurationRecord (or HEVC equivalent) on their
own. KVS- and CMAF-style muxers that need codec-private bytes had no
out-of-band record to work from and would either reject the rendition or
require keyframe scraping.

Build the configuration record once both SPS+PPS (or VPS+SPS+PPS for
H.265) have been observed and republish the catalog with the populated
description. The cache update order changes so init() sees the latest
NAL before deciding whether the catalog needs republishing.

For Hev1, splitting "codec changed" (which requires a new track) from
"description appeared" (in-place rendition update) keeps subscribers
from re-fetching the track every time the description becomes available.

avcC builder emits the standard ISO/IEC 14496-15 §5.3.3.1.2 layout
without the high-profile extension fields; players that need them
re-derive from the SPS we ship inline. hvcC builder pulls
chroma/bit-depth/temporal-id fields from the parsed SPS per §8.3.3.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

Review Change Stack

Walkthrough

This PR adds codec-specific description fields to H.264 and HEVC renditions by populating them with configuration records (avcC for AVC3 and hvcC for HEV1). Avc3 now caches SPS before initial init(), re-parses cached SPS when PPS arrives, stores a BroadcastProducer to allow track replacement, and builds avcC when SPS+PPS are present. Hev1 gates VPS/SPS/PPS caching and computes hvcC when VPS+SPS+PPS exist; init() now reinitializes tracks only when codec-bearing fields differ (same_codec). Both modules include helpers and tests to serialize/validate the config records.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: emitting avcC/hvcC configuration records in catalog descriptions for the Avc3 and Hev1 importers.
Description check ✅ Passed The description comprehensively explains the changes, rationale, and test plan. It clearly relates to the changeset across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch avcc-hvcc-emission
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch avcc-hvcc-emission

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 and usage tips.

Three follow-ups on top of the avcC/hvcC description emission:

1. Avc3 now drops + recreates its track when the codec config actually
   changes (resolution/profile flip), matching Hev1's existing behavior.
   Description-only updates (PPS arriving after SPS) keep the existing
   track via a shared `same_codec` predicate hoisted into `import::mod`.
   Previously the avc3 track persisted across codec changes and mixed
   incompatible samples — a pre-existing bug surfaced by this PR's
   description-emission flow.

2. `build_avcc` and `build_hvcc` now error if any NAL exceeds the 16-bit
   length field in their respective configuration records, instead of
   silently truncating. SPS/PPS/VPS NALs are normally a few dozen bytes
   but range/SCC extensions can produce larger ones.

3. `build_hvcc` parses its own SPS internally instead of accepting an
   already-parsed `SpsNALUnit`, simplifying both the call site and the
   tests.

4. Unit tests for the new code paths: avcC layout, oversize rejection
   for both records, and array-record structure for hvcC.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rs/moq-mux/src/import/avc3.rs (1)

401-441: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid emitting incomplete avcC for high profiles.

build_avcc always writes the baseline layout, but profiles like 100/110/122/144 require the extra avcC extension fields. Emitting the shorter form for those profiles can produce an invalid description for strict consumers. Either append the required extension fields or fail fast for those profile IDs.

Suggested guard (fail fast until extension bytes are implemented)
 fn build_avcc(sps_nal: &[u8], pps_nal: &[u8]) -> anyhow::Result<Bytes> {
 	use bytes::BufMut;
@@
 	let profile_idc = sps_nal.get(1).copied().unwrap_or(0);
 	let constraints = sps_nal.get(2).copied().unwrap_or(0);
 	let level_idc = sps_nal.get(3).copied().unwrap_or(0);
+	anyhow::ensure!(
+		!matches!(profile_idc, 100 | 110 | 122 | 144),
+		"avcC high-profile extension fields are not implemented (profile_idc={profile_idc})"
+	);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rs/moq-mux/src/import/avc3.rs` around lines 401 - 441, build_avcc currently
always emits the baseline avcC layout but must fail for high profiles that
require the avcC extension fields; after extracting profile_idc (in function
build_avcc) add a guard that checks if profile_idc is one of the high-profile
IDs (e.g. 100, 110, 122, 144 — include other known high profiles if needed) and
return an error (using anyhow::bail or anyhow::ensure) with a clear message like
"profile X requires avcC extension fields; not implemented" so we fail fast
until the extension bytes are implemented; place this check right after the
profile_idc/constraints/level_idc extraction and before allocating
out/serializing the avcC fields.
🧹 Nitpick comments (1)
rs/moq-mux/src/import/hev1.rs (1)

525-557: ⚡ Quick win

Add one positive hvcC layout test in addition to overflow tests.

The new tests only cover error paths. A single “known input → exact hvcC bytes/fields” assertion would protect the bitfield packing and array ordering from regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rs/moq-mux/src/import/hev1.rs` around lines 525 - 557, Add a positive unit
test in the existing tests module (in hev1.rs) that calls build_hvcc with a
small, known VPS/SPS/PPS triplet (use the same test harness as the overflow
tests) and asserts success and that the returned hvcC bytes/fields match an
exact expected layout; specifically reference build_hvcc and the tests mod,
construct deterministic vps/sps/pps vectors, call build_hvcc(...).unwrap(), and
compare the resulting byte vector (or parsed hvcC struct fields) against a
precomputed expected byte slice to lock in bitfield packing and array ordering.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@rs/moq-mux/src/import/avc3.rs`:
- Around line 401-441: build_avcc currently always emits the baseline avcC
layout but must fail for high profiles that require the avcC extension fields;
after extracting profile_idc (in function build_avcc) add a guard that checks if
profile_idc is one of the high-profile IDs (e.g. 100, 110, 122, 144 — include
other known high profiles if needed) and return an error (using anyhow::bail or
anyhow::ensure) with a clear message like "profile X requires avcC extension
fields; not implemented" so we fail fast until the extension bytes are
implemented; place this check right after the profile_idc/constraints/level_idc
extraction and before allocating out/serializing the avcC fields.

---

Nitpick comments:
In `@rs/moq-mux/src/import/hev1.rs`:
- Around line 525-557: Add a positive unit test in the existing tests module (in
hev1.rs) that calls build_hvcc with a small, known VPS/SPS/PPS triplet (use the
same test harness as the overflow tests) and asserts success and that the
returned hvcC bytes/fields match an exact expected layout; specifically
reference build_hvcc and the tests mod, construct deterministic vps/sps/pps
vectors, call build_hvcc(...).unwrap(), and compare the resulting byte vector
(or parsed hvcC struct fields) against a precomputed expected byte slice to lock
in bitfield packing and array ordering.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 80bb2ef3-45c4-4901-a66f-44034d05a2cc

📥 Commits

Reviewing files that changed from the base of the PR and between 181a0df and cf2af34.

📒 Files selected for processing (3)
  • rs/moq-mux/src/import/avc3.rs
  • rs/moq-mux/src/import/hev1.rs
  • rs/moq-mux/src/import/mod.rs

Comment thread rs/moq-mux/src/import/avc3.rs Outdated
tracing::debug!(name = ?old_name, "codec changed; replacing track");
catalog.video.renditions.remove(&old_name);

let new_track = self.broadcast.unique_track(".avc3")?;
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.

We should make this before removing the old one right?

Comment thread rs/moq-mux/src/import/hev1.rs Outdated
catalog.video.renditions.remove(&track.name);
}

let track = self.broadcast.unique_track(".hev1")?;
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.

We should do this before removing right?

Per review on #1437: if `unique_track(...)` errors after we've already
removed the old rendition (avc3) or taken the old track (hev1), the
catalog is left in a half-state and the old track is lost.

Reorder both importers so the replacement track is constructed first
and the catalog mutations only run once we have it in hand. Also lifts
the fallible network call out from under the catalog lock.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
rs/moq-mux/src/import/hev1.rs (1)

540-565: 💤 Low value

Consider adding a layout verification test for hvcC.

Unlike avc3.rs which has avcc_layout_matches_iso_14496_15, this file only tests overflow rejection. A layout test with a minimal valid SPS would improve confidence in the hvcC byte layout. This would require crafting a parseable SPS NAL, which is more involved than avc3's simple byte extraction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rs/moq-mux/src/import/hev1.rs` around lines 540 - 565, Add a layout
verification unit test for hvcC similar to avcc_layout_matches_iso_14496_15:
create a new test (e.g., hvcc_layout_matches_iso_14496_15) that uses build_hvcc
with a minimal valid VPS/SPS/PPS byte sequences (construct a parseable SPS NAL
rather than just arbitrary bytes), then assert the produced hvcC byte layout
matches the ISO/IEC 14496-15 expected structure (or round-trip by parsing the
output with your hvcC parser). Locate build_hvcc and the existing tests
hvcc_errors_on_oversized_vps/sps/pps to mirror their pattern and ensure the SPS
chosen is a minimal valid HEVC SPS so the test verifies layout correctness
rather than just overflow rejection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@rs/moq-mux/src/import/hev1.rs`:
- Around line 540-565: Add a layout verification unit test for hvcC similar to
avcc_layout_matches_iso_14496_15: create a new test (e.g.,
hvcc_layout_matches_iso_14496_15) that uses build_hvcc with a minimal valid
VPS/SPS/PPS byte sequences (construct a parseable SPS NAL rather than just
arbitrary bytes), then assert the produced hvcC byte layout matches the ISO/IEC
14496-15 expected structure (or round-trip by parsing the output with your hvcC
parser). Locate build_hvcc and the existing tests
hvcc_errors_on_oversized_vps/sps/pps to mirror their pattern and ensure the SPS
chosen is a minimal valid HEVC SPS so the test verifies layout correctness
rather than just overflow rejection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1812920c-4d59-44eb-988a-3f942c2e6a2f

📥 Commits

Reviewing files that changed from the base of the PR and between cf2af34 and 8032f7a.

📒 Files selected for processing (2)
  • rs/moq-mux/src/import/avc3.rs
  • rs/moq-mux/src/import/hev1.rs

… description

Once we ship an avcC/hvcC in the catalog description, the bitstream
shouldn't redundantly carry the parameter sets — that's the avc1/hvc1
contract. Previously the importers were a hybrid: out-of-band avcC plus
inline NALs plus Annex-B framing, which matches no codec convention
cleanly.

This commit commits to avc1/hvc1:
- SPS/PPS (and VPS for HEVC) are cached for description but stripped
  from sample data; the defensive insertion before IDR slices is gone.
- Sample data is length-prefixed (4-byte big-endian NAL length) instead
  of Annex-B start codes, matching `lengthSizeMinusOne = 3` in the
  configuration records.
- `H264.inline` and `H265.in_band` flip to false, so the catalog codec
  string becomes `avc1.*` / `hvc1.*`.
- The `contains_sps`/`contains_pps`/`contains_vps` flags and the unused
  `START_CODE` constant in `annexb` go away.

Importer struct names (`Avc3`, `Hev1`) describe the INPUT format
(Annex-B with inline parameter sets) — the output is now consistently
avc1/hvc1.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
rs/moq-mux/src/import/hev1.rs (1)

337-341: ⚡ Quick win

Dead code: contains_vps, contains_sps, contains_pps flags are never set.

The Frame struct includes contains_vps, contains_sps, and contains_pps fields that are reset in maybe_start_frame() but never set to true anywhere. These appear to be remnants from the prior implementation that inserted parameter sets before keyframes. Consider removing them.

♻️ Proposed cleanup

In Frame struct:

 struct Frame {
 	chunks: BytesMut,
 	contains_idr: bool,
 	contains_slice: bool,
-	contains_vps: bool,
-	contains_sps: bool,
-	contains_pps: bool,
 }

In maybe_start_frame():

 	self.current.contains_idr = false;
 	self.current.contains_slice = false;
-	self.current.contains_vps = false;
-	self.current.contains_sps = false;
-	self.current.contains_pps = false;

Also applies to: 450-457

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rs/moq-mux/src/import/hev1.rs` around lines 337 - 341, Frame contains dead
boolean flags contains_vps, contains_sps, contains_pps that are reset in
maybe_start_frame() but never set or used elsewhere; remove these fields from
the Frame struct and delete their resets in maybe_start_frame() (and the
duplicated reset block around the other occurrence), then run a repo-wide search
to remove any remaining references to contains_vps/contains_sps/contains_pps and
adjust any logic that assumed their presence (ensure only contains_idr and
contains_slice remain as needed).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@rs/moq-mux/src/import/hev1.rs`:
- Around line 337-341: Frame contains dead boolean flags contains_vps,
contains_sps, contains_pps that are reset in maybe_start_frame() but never set
or used elsewhere; remove these fields from the Frame struct and delete their
resets in maybe_start_frame() (and the duplicated reset block around the other
occurrence), then run a repo-wide search to remove any remaining references to
contains_vps/contains_sps/contains_pps and adjust any logic that assumed their
presence (ensure only contains_idr and contains_slice remain as needed).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9cba06e6-315a-4845-8051-397371440044

📥 Commits

Reviewing files that changed from the base of the PR and between 8032f7a and 73f8a73.

📒 Files selected for processing (3)
  • rs/moq-mux/src/import/annexb.rs
  • rs/moq-mux/src/import/avc3.rs
  • rs/moq-mux/src/import/hev1.rs
💤 Files with no reviewable changes (1)
  • rs/moq-mux/src/import/annexb.rs

@kixelated kixelated marked this pull request as draft May 21, 2026 22:16
@kixelated
Copy link
Copy Markdown
Collaborator Author

Closing — review feedback led us to the right architectural conclusion: the avc3/hev1 importers shouldn't transcode their own output. If a downstream consumer needs avc1/hvc1 (length-prefixed samples + out-of-band avcC/hvcC), that's the consumer's transcode and belongs in an exporter layer. Codec-aware writers like the upcoming export::Mkv are the right home for that conversion, where length-prefixing and CodecPrivate emission can share code with the format itself.

Not landing the avcC/hvcC emission in the importers means consumers that today need an out-of-band record will get it from the export layer instead.

Re-open or salvage commits if useful for the MKV exporter work.

@kixelated kixelated closed this May 21, 2026
kixelated pushed a commit that referenced this pull request May 21, 2026
Addresses the qsx0004 / KVS pilot blockers from #1438 review:

1. **Avc3/Hev1 sources now work end-to-end.** The MKV exporter no longer
   demands a populated catalog `description`. New `export::Avc1` /
   `export::Hvc1` helpers cache inline parameter sets per track, synthesize
   `AVCDecoderConfigurationRecord` / `HEVCDecoderConfigurationRecord`, strip
   SPS/PPS/VPS from sample data, and length-prefix the remaining NALs.
   `Container::Legacy` sources with avc1/hvc1 shape (description populated)
   pass through unchanged.

   Per the close comment on #1437, the codec-shape transcode belongs in the
   exporter (codec-aware writer), not the importer. These helpers live in
   `export/` because that's their natural home; they're public so other
   container exporters (e.g. fmp4) can reuse them later.

2. **Header emission is deferred.** When any video track lacks a codec
   config (Avc3 with description=None), the file header waits until each
   transmuxer has observed SPS+PPS (typically the first keyframe). After
   that, the header is emitted with CodecPrivate populated; sample bytes
   that arrived during the wait are already transformed and flow normally.

3. **Configurable fragment duration.** `Mkv::with_fragment_duration(d)`
   batches frames into Clusters of roughly `d` (default 2s) and rolls on
   video keyframes or i16 timestamp overflow. `Duration::ZERO` keeps the
   old one-cluster-per-frame behavior. This unblocks KVS PutMedia, which
   throttles at much lower fragment rates than per-frame.

4. **Graceful close on dropped producer.** Both catalog and track polls
   now treat `moq_net::Error::Dropped` as EOS. Producers that drop without
   calling `finish()` no longer surface as exporter errors.

Restructuring:
- `pub use mkv::*;` tightened to `pub use mkv::Mkv;` (and `Fmp4`, `Avc1`,
  `Hvc1`); only intended types are re-exported from `export`.
- `mkv_test.rs` moved under `export/test/` to match the import-side layout.
- `import/annexb` is now `pub(crate)` so the export-side transmuxers can
  share its NAL iterator.

New tests:
- `export_avc3_source_synthesizes_avcc_and_length_prefixes` — end-to-end
  proof that an Avc3 broadcast (inline=true, no description, Annex-B
  samples) yields valid MKV with avcC in CodecPrivate and length-prefixed
  blocks.
- `export_fragment_duration_batches_blocks` — 5 frames within 100ms batch
  into a single Cluster when fragment_duration=2s.
- `avc1_*` / `hvc1_*` unit tests for the transmuxer state machine
  (parameter caching, codec config build, passthrough, idempotency).

Mid-stream Tracks updates are still a known limitation; out of scope here.

https://claude.ai/code/session_013rPJFCfTMyf71MqzqAXo45
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