Skip to content

moq-net: runtime Timescale/Timestamp; container::Frame keeps source scale#1473

Open
kixelated wants to merge 12 commits into
mainfrom
claude/gifted-bardeen-6ae3dd
Open

moq-net: runtime Timescale/Timestamp; container::Frame keeps source scale#1473
kixelated wants to merge 12 commits into
mainfrom
claude/gifted-bardeen-6ae3dd

Conversation

@kixelated
Copy link
Copy Markdown
Collaborator

Summary

Ports the Timestamp / Timescale types from #1439 (without the wire-format, SUBSCRIBE_OK, or async-subscribe changes) so moq_mux::container::Frame can carry its source format's native scale instead of forcing a lossy detour through microseconds.

What changed

New types in moq-net::model::time:

  • Timescale newtype around u64 with named constants UNKNOWN / SECOND / MILLI / MICRO / NANO.
  • Timestamp { value, scale } carries scale at runtime. Arithmetic panics on scale mismatch; as_* / convert return TimeOverflow when the scale is unspecified.
  • Time = Timescale<1000> alias is gone; jitter fields on the hang catalog move to Option<Timestamp>.

container::Frame.timestamp preserves the importer's scale:

  • fMP4 import stamps frames at the source mdhd.timescale instead of converting to micros; export rescales once at encode_fragment rather than the legacy as_micros() * scale / 1_000_000 round-trip.
  • MKV import keeps NANO; export rescales to MILLI at the wire boundary.
  • Legacy and LOC encoders convert to MICRO at the wire boundary (legacy has no on-wire scale; LOC's catalog convention is micros). LOC decode preserves any per-frame timescale property.
  • jitter::MinFrameDuration normalizes its result to MILLI for the catalog field.

Drive-by:

  • moq-net::coding is now pub and VarInt::{encode_quic, decode_quic} are public so hang's wire format can encode without reaching lite::Version.
  • Container producer/consumer's latency/duration checks compare in nanoseconds via the fallible API.
  • libmoq, moq-ffi, moq-gst updated for the new fallible as_* / Duration::try_from shapes.

Where lossy conversion still happens (intentional)

Site Scale Why
hang::container::Frame::encode MICRO Legacy wire has no on-wire scale tag
loc::Wire::write MICRO LOC catalog convention
mkv::export.feed_frame MILLI MKV's TimestampScale default
fmp4::encode_fragment track mdhd.timescale One rescale, not two
jitter::MinFrameDuration::observe MILLI Catalog jitter field convention

Heads-up for reviewer

The hang catalog jitter field changes its JSON shape: was a bare integer via the transparent Timescale<1000>(VarInt) newtype, now a {value, scale} object via the default derive on Timestamp. The existing round-trip test only covers jitter: None so it didn't catch this. A custom serde impl would be a focused follow-up if wire compatibility matters.

Test plan

  • cargo test --workspace --exclude moq-gst — all tests pass (141 in moq-mux, including fmp4/mkv round-trips)
  • just check — clippy, fmt, doc, shear, sort all clean
  • moq-gst couldn't be compiled locally without gstreamer-1.0 pkg-config. Edits there are mechanical (one fallible Duration::try_from); CI will verify

🤖 Generated with Claude Code

kixelated and others added 2 commits May 23, 2026 15:17
Ports the Timestamp/Timescale types from #1439 (without the wire-format,
SUBSCRIBE_OK, or async-subscribe changes) so container::Frame can carry
its source format's native scale instead of forcing a lossy detour
through microseconds.

- `Timescale` newtype around u64 with UNKNOWN/SECOND/MILLI/MICRO/NANO
  constants.
- `Timestamp { value, scale }` carries scale at runtime; arithmetic
  panics on scale mismatch, `as_*`/`convert` return TimeOverflow on
  unspecified scales.
- The old `Time = Timescale<1000>` alias is gone; jitter fields move to
  `Option<Timestamp>`.

container::Frame.timestamp now preserves the importer's scale:
- fmp4 import stamps frames at the source `mdhd.timescale` instead of
  converting to micros, and export rescales once at `encode_fragment`
  rather than the legacy `as_micros() * scale / 1_000_000` round-trip.
- mkv import keeps NANO; export rescales to MILLI at the wire boundary.
- legacy and LOC encoders convert to MICRO at the wire boundary
  (legacy has no on-wire scale; LOC's catalog convention is micros).
  LOC decode preserves any per-frame timescale property.
- jitter::MinFrameDuration normalizes its result to MILLI for the
  catalog field.

Drive-by:
- moq-net's `coding` module is now `pub` and `VarInt::{encode_quic,
  decode_quic}` are public so hang's wire format can encode without
  reaching `lite::Version`.
- Container producer/consumer's latency/duration checks compare in
  nanoseconds via the fallible API.
- libmoq, moq-ffi, moq-gst updated for the new fallible `as_*` /
  `Duration::try_from` shapes.

`cargo test --workspace --exclude moq-gst` passes (all 141 moq-mux
tests); `just check` clean. moq-gst couldn't be built locally without
gstreamer-1.0 pkg-config; the edits there are mechanical.

Note: the hang catalog `jitter` field changes its JSON shape (was a
bare integer via the transparent Timescale<1000>(VarInt) newtype, now a
`{value, scale}` object via the default derive on Timestamp). The
existing round-trip test only covers `jitter: None` so it didn't catch
this; a custom serde impl would be a follow-up if wire compatibility
matters.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit changed the catalog jitter field from
`Option<moq_net::Time>` (transparently a `VarInt` → JSON integer) to
`Option<moq_net::Timestamp>`, which derive-serialized as
`{"value": N, "scale": 1000}` and broke the on-wire shape.

Switch the field to `std::time::Duration` and apply a small
`duration_millis` serde adapter so it serializes as a bare integer
number of milliseconds, matching the historical format. Sub-ms
precision is truncated, which is fine for jitter.

`MinFrameDuration::observe` now returns `Option<Duration>` directly via
`Duration::try_from(timestamp)`. The fmp4 importer's internal jitter
accumulator stays at the source scale and converts once at the catalog
write boundary.

Adds a round-trip test that pins the integer-ms shape so this regresses
loudly if the serde adapter is ever dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 23, 2026

Review Change Stack

Caution

Review failed

Failed to post review comments

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

<review_stack_artifact>

</review_stack_artifact>

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: runtime Timescale/Timestamp implementation in moq-net with container::Frame preserving source scale.
Description check ✅ Passed The description is well-related to the changeset, providing detailed context about the new types, how different formats handle timestamps, intentional lossy conversions, and test coverage.
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
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/gifted-bardeen-6ae3dd

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.

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.

Actionable comments posted: 2

🤖 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.

Inline comments:
In `@rs/moq-net/src/model/time.rs`:
- Around line 285-305: checked_add and checked_sub currently only reject
mismatched scales, allowing two Timescale::UNKNOWN values to combine; update
both functions (checked_add, checked_sub) to also return Err(TimeOverflow) when
either operand's scale is unspecified by checking for the Timescale::UNKNOWN
variant on self.scale (or rhs.scale) before doing the numeric
checked_add/checked_sub; keep the existing mismatched-scale check and overflow
handling but add the early UNKNOWN-scale rejection to prevent arithmetic on
unspecified scales.
- Around line 370-384: The current Ord::cmp for Timestamp falls back to
raw-value ordering when scales differ, causing incorrect ordering for known
mixed scales; update Timestamp::cmp to detect when both scales are known
(neither is Timescale::UNKNOWN) and compare by first normalizing both values to
a common timescale rather than directly comparing raw value. Locate the impl Ord
for Timestamp and its cmp method and implement a safe rescaling: compute a
common target scale (e.g., the finer/smaller unit or an LCM of the two scales),
convert self.value and other.value into that common scale using checked integer
arithmetic or by promoting to a wider integer type to avoid overflow, then
compare the rescaled values; retain the debug_assert for scale compatibility
with UNKNOWN, and only use raw-value fallback when one side is
Timescale::UNKNOWN or when rescaling is provably impossible (handle that path
explicitly).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d4be9501-c815-499f-9c0b-ddd4fe83f5ee

📥 Commits

Reviewing files that changed from the base of the PR and between 88b9225 and f6754b2.

📒 Files selected for processing (21)
  • rs/hang/src/catalog/audio/mod.rs
  • rs/hang/src/catalog/video/mod.rs
  • rs/hang/src/container/frame.rs
  • rs/libmoq/src/consume.rs
  • rs/moq-ffi/src/consumer.rs
  • rs/moq-gst/src/source/imp.rs
  • rs/moq-mux/src/catalog/hang/producer.rs
  • rs/moq-mux/src/catalog/msf/consumer.rs
  • rs/moq-mux/src/container/consumer.rs
  • rs/moq-mux/src/container/fmp4/export.rs
  • rs/moq-mux/src/container/fmp4/import.rs
  • rs/moq-mux/src/container/fmp4/mod.rs
  • rs/moq-mux/src/container/jitter.rs
  • rs/moq-mux/src/container/loc/mod.rs
  • rs/moq-mux/src/container/mkv/export.rs
  • rs/moq-mux/src/container/mkv/import.rs
  • rs/moq-mux/src/container/mod.rs
  • rs/moq-mux/src/container/producer.rs
  • rs/moq-net/src/coding/varint.rs
  • rs/moq-net/src/lib.rs
  • rs/moq-net/src/model/time.rs

Comment thread rs/moq-net/src/model/time.rs
Comment thread rs/moq-net/src/model/time.rs
kixelated and others added 3 commits May 23, 2026 15:27
`serde_with` already provides `DurationMilliSeconds<u64>` which does
the same Option<Duration> ↔ integer-ms conversion. Drop the hand-rolled
`duration_millis` adapter and use the library helper instead.

The integer-ms round-trip test from the previous commit still passes,
so the on-wire shape is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes for issues CodeRabbit flagged on #1473:

1. `checked_add` / `checked_sub` now reject [`Timescale::UNKNOWN`]
   operands instead of treating them like any matching scale. Without
   this, wire-decoded timestamps that haven't had a scale attached yet
   would silently combine into "valid" arithmetic results.

2. `Ord::cmp` now cross-normalizes when both scales are known, using
   128-bit cross-multiplication (`a*s2 vs b*s1`). The previous raw-value
   fallback was a release-build hazard: `1s < 2ms` would compare as
   `1 < 2` once the `debug_assert!` got compiled out. This matters for
   the three `min_by_key(|ts| *ts)` sites in fmp4/mkv exporters, which
   pick the next track to emit across per-track frames that now carry
   their own native scale.

UNKNOWN-scale sentinel comparisons (`Timestamp::ZERO` / `Timestamp::MAX`)
keep their raw-value fallback so existing `unwrap_or(ZERO/MAX)` patterns
still work.

Adds tests covering both fixes plus a mixed-scale sort regression test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-6ae3dd

# Conflicts:
#	rs/moq-mux/src/catalog/hang/producer.rs
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.

Actionable comments posted: 2

🤖 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.

Inline comments:
In `@rs/moq-mux/src/catalog/msf/consumer.rs`:
- Around line 223-226: The jitter mapping currently converts filtered finite
non-negative f64 values with Duration::from_millis(v as u64), which saturates
huge values instead of rejecting them; update the jitter mapping in the consumer
code (the iterator producing jitter: track.jitter.filter(...).map(...)) to use
Duration::try_from_secs_f64(v / 1000.0) and drop entries where the conversion
fails (i.e., map -> FilterMap or filter_map with try_from_secs_f64 returning
Err), and make the same change for both the audio and video jitter mappings so
oversized/invalid MSF jitter values are rejected rather than clamped to
u64::MAX.

In `@rs/moq-net/src/model/time.rs`:
- Around line 371-392: The current Ord::cmp for Timestamp normalizes scales and
can return Ordering::Equal for different (value, scale) pairs, violating the
Ord/Eq contract; update fn cmp(&self, other: &Self) to, after the cross-multiply
comparison, if lhs.cmp(&rhs) == Ordering::Equal, fall back to a deterministic
tie-breaker that mirrors the derived field-wise equality: compare (self.scale.0,
self.value.into_inner()) against (other.scale.0, other.value.into_inner()) and
return that Ordering so cmp returns Equal iff both scale and value fields are
equal (ensuring consistency with PartialEq/Eq/Hash and methods like
from_secs/from_millis).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4f82456e-ce7b-4df5-b53b-42d6976bff36

📥 Commits

Reviewing files that changed from the base of the PR and between f6754b2 and 5ac1cd9.

📒 Files selected for processing (8)
  • rs/hang/src/catalog/audio/mod.rs
  • rs/hang/src/catalog/root.rs
  • rs/hang/src/catalog/video/mod.rs
  • rs/moq-mux/src/catalog/hang/producer.rs
  • rs/moq-mux/src/catalog/msf/consumer.rs
  • rs/moq-mux/src/container/fmp4/import.rs
  • rs/moq-mux/src/container/jitter.rs
  • rs/moq-net/src/model/time.rs

Comment thread rs/moq-mux/src/catalog/msf/consumer.rs Outdated
Comment thread rs/moq-net/src/model/time.rs
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.

♻️ Duplicate comments (2)
rs/moq-mux/src/catalog/msf/consumer.rs (2)

223-226: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace saturating cast with fallible conversion to reject invalid jitter values.

The as u64 cast saturates oversized finite f64 jitter to u64::MAX milliseconds (≈292 million years) instead of treating the catalog value as invalid. Use Duration::try_from_secs_f64(v / 1000.0) with .and_then() to reject unrepresentable values.

🔧 Proposed fix
 		jitter: track
 			.jitter
 			.filter(|v| v.is_finite() && *v >= 0.0)
-			.map(|v| std::time::Duration::from_millis(v as u64)),
+			.and_then(|v| std::time::Duration::try_from_secs_f64(v / 1000.0).ok()),
 	}))
🤖 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/catalog/msf/consumer.rs` around lines 223 - 226, The current
mapping of jitter uses a saturating cast (v as u64) which turns huge finite f64
values into u64::MAX; change the chain on track.jitter (the filter/map for
jitter) to perform a fallible conversion instead: after the existing filter(|v|
v.is_finite() && *v >= 0.0) replace map(|v| std::time::Duration::from_millis(v
as u64)) with an and_then (or map followed by .ok()) that calls
std::time::Duration::try_from_secs_f64(v / 1000.0) and converts the Result to an
Option (e.g. .ok()) so unrepresentable jitter values are rejected rather than
saturated.

270-273: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace saturating cast with fallible conversion to reject invalid jitter values.

Same issue as the video jitter mapping: as u64 saturates instead of rejecting oversized values.

🔧 Proposed fix
 		jitter: track
 			.jitter
 			.filter(|v| v.is_finite() && *v >= 0.0)
-			.map(|v| std::time::Duration::from_millis(v as u64)),
+			.and_then(|v| std::time::Duration::try_from_secs_f64(v / 1000.0).ok()),
 	}))
🤖 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/catalog/msf/consumer.rs` around lines 270 - 273, The jitter
mapping currently uses a saturating cast (`v as u64`) which silently clamps
oversized values; update the closure in the `jitter:
track.jitter.filter(...).map(...)` pipeline to perform a fallible conversion
instead: after checking `v.is_finite()` and `*v >= 0.0`, also require `*v <=
u64::MAX as f64` (or use an explicit check against `f64::from(u64::MAX)`), then
convert with a safe cast (e.g., `let ms = *v as u64` only after the bound check)
and call `std::time::Duration::from_millis(ms)`; if the value exceeds the bound,
drop it (keep it out of the iterator) so invalid/oversized jitters are rejected
rather than saturated.
🤖 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.

Duplicate comments:
In `@rs/moq-mux/src/catalog/msf/consumer.rs`:
- Around line 223-226: The current mapping of jitter uses a saturating cast (v
as u64) which turns huge finite f64 values into u64::MAX; change the chain on
track.jitter (the filter/map for jitter) to perform a fallible conversion
instead: after the existing filter(|v| v.is_finite() && *v >= 0.0) replace
map(|v| std::time::Duration::from_millis(v as u64)) with an and_then (or map
followed by .ok()) that calls std::time::Duration::try_from_secs_f64(v / 1000.0)
and converts the Result to an Option (e.g. .ok()) so unrepresentable jitter
values are rejected rather than saturated.
- Around line 270-273: The jitter mapping currently uses a saturating cast (`v
as u64`) which silently clamps oversized values; update the closure in the
`jitter: track.jitter.filter(...).map(...)` pipeline to perform a fallible
conversion instead: after checking `v.is_finite()` and `*v >= 0.0`, also require
`*v <= u64::MAX as f64` (or use an explicit check against
`f64::from(u64::MAX)`), then convert with a safe cast (e.g., `let ms = *v as
u64` only after the bound check) and call
`std::time::Duration::from_millis(ms)`; if the value exceeds the bound, drop it
(keep it out of the iterator) so invalid/oversized jitters are rejected rather
than saturated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 82e325c2-c5f2-4acc-a9b4-5b93d1d1377c

📥 Commits

Reviewing files that changed from the base of the PR and between 5ac1cd9 and 8673fa9.

📒 Files selected for processing (4)
  • rs/moq-mux/src/catalog/hang/producer.rs
  • rs/moq-mux/src/catalog/msf/consumer.rs
  • rs/moq-mux/src/container/fmp4/export.rs
  • rs/moq-mux/src/container/mkv/export.rs

kixelated and others added 5 commits May 23, 2026 15:49
Mirror the same pattern hang uses: a `std::time::Duration` field with a
`serde_with` adapter, instead of a bare `Option<f64>`. The wire format
stays a fractional JSON number of ms via
`DurationMilliSecondsWithFrac<f64>` (existing JS consumers and the
`15.5` round-trip test are unaffected); only the Rust type changes.

Lets the moq-mux ↔ MSF bridges drop their explicit
`Duration → f64 → Duration` conversions: both sides are now
`Option<Duration>`, so the bridge is just `dst.jitter = src.jitter`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jitter isn't in the MSF/CMSF drafts at all, so there's no wire-format
to preserve. Use `DurationMilliSeconds<u64>` (integer ms) like the hang
catalog instead of `DurationMilliSecondsWithFrac<f64>`. Sub-ms
precision isn't meaningful for jitter and the two catalogs now agree
on shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When two timestamps cross-mult to the same temporal value but have
different (value, scale) representations (e.g. `from_secs(1)` and
`from_millis(1000)`), cmp used to return Ordering::Equal while the
derived PartialEq/Eq said they were unequal. That violates Rust's
Ord/Eq contract and would break ordered-collection keys.

Add a `(scale, value)` tie-break after the cross-multiply so cmp
returns Equal iff the fields are equal, matching derived Eq.

Test updated: `from_secs(1).cmp(&from_millis(1000))` now asserts
`!= Equal`, and adds the round-trip `cmp(a, b) == Equal iff a == b`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two cleanups:

1. **moq-mux::container**: removed the `pub use moq_net::{Timescale, Timestamp};`
   re-export and the `pub const TIMESCALE: Timescale = Timescale::MICRO;`
   constant. Containers now carry their own native timescale (fmp4 uses
   `mdhd.timescale`, mkv uses nanoseconds, legacy/LOC use microseconds),
   so there's no single "moq-mux container timescale" worth advertising.
   Call sites use `moq_net::{Timestamp, Timescale}` directly. LOC's
   microsecond fallback moves into a private `DEFAULT_TIMESCALE` const
   in `loc/mod.rs` since that's the only place that needed it.

2. **moq-loc**: drop the hand-rolled QUIC varint implementation and use
   `moq_net::coding::VarInt::{encode_quic, decode_quic}` instead.
   Adds `moq-net` as a dep, maps moq-net's `DecodeError` /
   `EncodeError` / `BoundsExceeded` into the local `Error` enum,
   keeps the existing public API and all 7 tests passing unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups from review:

**From<Timestamp> for Duration**: revert the TryFrom I introduced
during the port back to an infallible From, panicking on
Timescale::UNKNOWN. The only unspecified-scale timestamps are the
ZERO/MAX sentinels, which nobody converts to Duration. Lets the
container::Producer latency check drop its as_nanos() / Result-juggling
in favour of a clean `(last - first).into() >= self.latency` shape, and
cleans up the .try_into().ok() callers in container::Consumer,
container::jitter, fmp4 import, and moq-gst source.

**coding module**: switch `pub mod coding` back to private and just
re-export `VarInt` at the top level alongside the existing
`BoundsExceeded`/`DecodeError`/`EncodeError`. Exposing the whole module
just for VarInt leaked size/stream/reader/writer/version internals that
shouldn't be part of moq-net's public surface. Downstream callers
(hang, moq-loc) now use `moq_net::VarInt` directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (2)
rs/moq-net/src/model/time.rs (1)

135-145: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject Timescale::UNKNOWN as the target in from_scale.

from_scale(_, _, Timescale::UNKNOWN) currently returns 0/? for any input because the multiply uses 0 and the division still succeeds. That silently discards the original timestamp and contradicts the “UNKNOWN-scale conversions fail” contract.

Suggested fix
 pub const fn from_scale(value: u64, source: Timescale, target: Timescale) -> Result<Self, TimeOverflow> {
-	if source.0 == 0 {
+	if source.0 == 0 || target.0 == 0 {
 		return Err(TimeOverflow);
 	}
 	match (value as u128).checked_mul(target.0 as u128) {
 		Some(scaled) => match VarInt::from_u128(scaled / source.0 as u128) {
 			Some(value) => Ok(Self { value, scale: target }),
 			None => Err(TimeOverflow),
 		},
 		None => Err(TimeOverflow),
 	}
 }
🤖 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-net/src/model/time.rs` around lines 135 - 145, from_scale currently
lets Timescale::UNKNOWN through as the target because its underlying .0==0
causes the multiply to produce 0 and silently drop the timestamp; update the
from_scale function to explicitly reject Timescale::UNKNOWN by checking target.0
== 0 (similar to the existing source.0 == 0 check) and returning
Err(TimeOverflow) when true, so conversions to an unknown scale fail; modify the
function around the top checks (in from_scale) to return Err(TimeOverflow) if
target.0 == 0 before doing the checked_mul/VarInt conversion.
rs/moq-mux/src/container/producer.rs (1)

93-102: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use buffered min/max PTS for the latency span.

Frame.timestamp can move backward in decode order for B-frames, so last.saturating_sub(first) can shrink and miss the configured latency threshold even though the buffered window already exceeds it. This check should use the buffered min/max timestamp instead of just the first/last entries.

🤖 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/container/producer.rs` around lines 93 - 102, The latency
check currently compares the first and last buffered frame timestamps which can
be wrong for B-frames; change it to compute the buffered minimum and maximum
timestamp across self.buffer (e.g. iterate self.buffer.iter().map(|f|
f.timestamp.as_nanos()) and collect only Ok values), ensure you have at least
two valid timestamps, compute min and max from those values, and then compare
max.saturating_sub(min) >= self.latency.as_nanos(); update the branch that
references self.buffer.first()/last() to use these min/max values and
conservatively skip the check if any timestamps are Err or fewer than two valid
samples.
🤖 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.

Inline comments:
In `@rs/moq-loc/src/lib.rs`:
- Around line 71-95: Error conversions should use thiserror's #[from] for the
simple BoundsExceeded => OutOfRange mapping: modify the Error enum so the
OutOfRange variant carries a BoundsExceeded (or add a dedicated
OutOfRangeFromBounds variant) annotated with #[from] so thiserror derives the
From<BoundsExceeded> impl automatically, then remove the manual impl
From<BoundsExceeded> for Error; keep the current manual impls for DecodeError
and EncodeError (DecodeError::Short => ShortBuffer, other =>
MalformedProperties; EncodeError::Short => ShortBuffer, other => OutOfRange)
because those intentionally collapse cases and should remain as-is.

In `@rs/moq-mux/src/container/mod.rs`:
- Around line 41-45: The comment about LOC timescale is outdated:
loc::Wire::poll_read() now preserves a per-frame LOC timescale, so
Frame.timestamp may not always be in microseconds. Update the doc comment in
container/mod.rs to say that LOC frames default to microseconds only when the
per-frame timescale is absent (or when re-encoding to the LOC wire convention),
and mention that loc::Wire::poll_read() preserves/propagates the explicit
per-frame timescale so exporters can re-emit without forcing micros.

---

Outside diff comments:
In `@rs/moq-mux/src/container/producer.rs`:
- Around line 93-102: The latency check currently compares the first and last
buffered frame timestamps which can be wrong for B-frames; change it to compute
the buffered minimum and maximum timestamp across self.buffer (e.g. iterate
self.buffer.iter().map(|f| f.timestamp.as_nanos()) and collect only Ok values),
ensure you have at least two valid timestamps, compute min and max from those
values, and then compare max.saturating_sub(min) >= self.latency.as_nanos();
update the branch that references self.buffer.first()/last() to use these
min/max values and conservatively skip the check if any timestamps are Err or
fewer than two valid samples.

In `@rs/moq-net/src/model/time.rs`:
- Around line 135-145: from_scale currently lets Timescale::UNKNOWN through as
the target because its underlying .0==0 causes the multiply to produce 0 and
silently drop the timestamp; update the from_scale function to explicitly reject
Timescale::UNKNOWN by checking target.0 == 0 (similar to the existing source.0
== 0 check) and returning Err(TimeOverflow) when true, so conversions to an
unknown scale fail; modify the function around the top checks (in from_scale) to
return Err(TimeOverflow) if target.0 == 0 before doing the checked_mul/VarInt
conversion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7fbc3154-1f8b-475d-9bc3-27eb11165b73

📥 Commits

Reviewing files that changed from the base of the PR and between 8673fa9 and 8a2f3eb.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • rs/hang/examples/video.rs
  • rs/moq-loc/Cargo.toml
  • rs/moq-loc/src/lib.rs
  • rs/moq-msf/src/lib.rs
  • rs/moq-mux/src/catalog/hang/producer.rs
  • rs/moq-mux/src/catalog/msf/consumer.rs
  • rs/moq-mux/src/codec/aac/import.rs
  • rs/moq-mux/src/codec/av1/import.rs
  • rs/moq-mux/src/codec/h264/import.rs
  • rs/moq-mux/src/codec/h265/import.rs
  • rs/moq-mux/src/codec/opus/import.rs
  • rs/moq-mux/src/container/consumer.rs
  • rs/moq-mux/src/container/fmp4/export.rs
  • rs/moq-mux/src/container/fmp4/export_test.rs
  • rs/moq-mux/src/container/fmp4/import.rs
  • rs/moq-mux/src/container/fmp4/mod.rs
  • rs/moq-mux/src/container/jitter.rs
  • rs/moq-mux/src/container/loc/mod.rs
  • rs/moq-mux/src/container/mkv/export_test.rs
  • rs/moq-mux/src/container/mkv/import.rs
  • rs/moq-mux/src/container/mod.rs
  • rs/moq-mux/src/container/producer.rs
  • rs/moq-mux/src/import.rs
  • rs/moq-net/src/model/time.rs
✅ Files skipped from review due to trivial changes (1)
  • rs/moq-mux/src/container/mkv/export_test.rs

Comment thread rs/moq-loc/src/lib.rs Outdated
Comment thread rs/moq-mux/src/container/mod.rs Outdated
Four fixes from review:

1. **Timestamp::from_scale**: reject UNKNOWN as the *target* scale, not
   just the source. Previously `from_scale(_, MILLI, UNKNOWN)` would
   multiply by 0 and silently return a 0/?-scale timestamp, contradicting
   the "UNKNOWN-scale conversions fail" contract. Test added.

2. **container::Producer + fmp4::should_flush**: latency check now uses
   min/max PTS across the buffered window rather than first/last. Frames
   within a track are in *decode* order; with B-frames the PTS can move
   backwards, so `last - first` can shrink and miss the latency
   threshold. Min/max correctly captures the presentation span.

3. **container::Frame doc**: clarified that LOC frames preserve any
   per-frame wire timescale on decode rather than always landing as
   micros, matching what `loc::Wire::poll_read` actually does.

4. **moq-loc Error**: the simple BoundsExceeded -> OutOfRange mapping
   moves to thiserror's `#[from]`. DecodeError/EncodeError keep their
   manual impls (they intentionally collapse Short vs everything-else).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kixelated kixelated enabled auto-merge (squash) May 23, 2026 23:20
@kixelated kixelated disabled auto-merge May 23, 2026 23:25
…-6ae3dd

# Conflicts:
#	rs/moq-mux/src/catalog/hang/producer.rs
#	rs/moq-mux/src/catalog/msf/consumer.rs
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