Skip to content

Add Low Overhead Container (LOC) frame format support#1388

Open
kixelated wants to merge 1 commit into
mainfrom
claude/implement-moq-loc-bBz8k
Open

Add Low Overhead Container (LOC) frame format support#1388
kixelated wants to merge 1 commit into
mainfrom
claude/implement-moq-loc-bBz8k

Conversation

@kixelated
Copy link
Copy Markdown
Collaborator

Implements support for the Low Overhead Container (LOC) frame format defined in draft-ietf-moq-loc across the Rust and JavaScript stacks.

Summary

This PR adds complete encode/decode support for LOC frames, a lightweight container format that wraps codec bitstreams with minimal overhead. Each LOC frame consists of a small property block (containing timestamp and optional per-frame timescale) followed by the raw codec payload.

Key Changes

Rust Implementation:

  • New moq-loc crate providing encode() and decode() functions for LOC frame wire format
    • Handles QUIC-style varint encoding for properties
    • Supports delta-encoded property type IDs (0x06 timestamp, 0x08 timescale)
    • Comprehensive error handling and validation
    • Full test coverage including roundtrip, per-frame timescale, and error cases
  • Integration into moq-mux container abstraction via new Loc struct
  • Updated catalog container enum to support LOC with configurable timescale (defaults to 1,000,000 microseconds)

JavaScript Implementation:

  • New @moq/loc package with Format (decoder) and Producer (encoder) classes
    • Mirrors Rust API with TypeScript types
    • Handles timescale conversion between frame timestamps and microseconds
    • Supports both Uint8Array and custom Source interface for flexible payload handling
    • Full test coverage matching Rust test scenarios
  • Integration into hang catalog and watch player for audio/video decoding

Integration:

  • Updated moq-mux error handling to include LOC errors
  • Modified hang catalog schema to support LOC container type with optional timescale property
  • Updated watch player (audio/video decoders and MSE backends) to instantiate appropriate LOC decoder based on catalog configuration
  • Added LOC preference in audio source selection (prioritized after legacy, before CMAF)

Implementation Details

  • Timescale handling: Frames can specify per-frame timescale (0x08 property) or fall back to catalog default
  • Property encoding: Uses delta-encoding for type IDs to minimize wire size
  • Even-typed properties carry varint values; odd-typed properties carry length-prefixed bytes
  • Unknown properties are silently skipped on decode, never emitted on encode
  • Timestamp conversion properly scales between different timescale units while maintaining precision

https://claude.ai/code/session_01K5qt8CV2TU1hciusTu6P5r

Adds support for the Low Overhead Container (LOC) defined in
draft-ietf-moq-loc, alongside the existing Legacy and CMAF containers.

- New rs/moq-loc and js/loc packages handle LOC frame encode/decode
  (timestamp + optional per-frame timescale + codec bitstream).
- hang catalog Container gains a Loc { timescale } variant. The
  timescale is optional and defaults to 1_000_000 (microseconds).
- moq-mux container::Hang dispatches Loc through the runtime-dispatched
  Container trait, picking the per-frame 0x08 timescale when present
  and falling back to the catalog value otherwise.
- js/watch decoders pick Container.Loc.Format when the catalog
  container kind is "loc"; the audio source selector prefers loc
  over cmaf when both are offered.

LOC is opt-in. Importers continue to emit Legacy/CMAF, so users only
get LOC by selecting it in their catalog.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

Walkthrough

This PR introduces Low Overhead Container (LOC) support as a new frame format alongside existing Legacy and CMAF containers. The implementation spans Rust (rs/moq-loc crate with encode/decode functions) and TypeScript (js/loc package with Format and Producer classes), updating both catalog schemas to recognize the "loc" container kind. The Rust runtime (moq-mux) and TypeScript runtime (watch) are wired to use LOC format dynamically based on catalog configuration, with FMP4 export validation ensuring CMAF-only constraints remain intact.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main change: adding Low Overhead Container (LOC) frame format support across Rust and JavaScript stacks, which aligns with all the changeset modifications.
Description check ✅ Passed The PR description comprehensively explains the purpose, key changes, and implementation details of adding LOC support, directly relating to the changeset across both Rust and JavaScript implementations.
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 claude/implement-moq-loc-bBz8k
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/implement-moq-loc-bBz8k

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: 3

🧹 Nitpick comments (6)
rs/hang/src/catalog/container.rs (1)

43-45: ⚡ Quick win

Replace repeated default literal with a named constant.

The default LOC timescale literal is duplicated. Centralizing it avoids drift between implementation and tests.

Suggested refactor
+const DEFAULT_LOC_TIMESCALE: u64 = 1_000_000;
+
 fn default_loc_timescale() -> u64 {
-	1_000_000
+	DEFAULT_LOC_TIMESCALE
 }
@@
 	#[test]
 	fn loc_default_timescale() {
 		let parsed: Container = serde_json::from_str(r#"{"kind":"loc"}"#).unwrap();
-		assert_eq!(parsed, Container::Loc { timescale: 1_000_000 });
+		assert_eq!(parsed, Container::Loc { timescale: DEFAULT_LOC_TIMESCALE });
 	}

As per coding guidelines, "Avoid using magic numbers; use named constants instead".

Also applies to: 54-55

🤖 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/hang/src/catalog/container.rs` around lines 43 - 45, The code uses the
literal 1_000_000 in default_loc_timescale (and again at the other location
noted) — replace these magic-number occurrences with a single named constant
(e.g. DEFAULT_LOC_TIMESCALE) declared at module scope and return that constant
from the function default_loc_timescale and use it wherever the literal
currently appears (including the other occurrence referenced around lines 54-55
and in related tests) so the value is centralized and stays in sync.
js/watch/src/video/mse.ts (1)

151-154: ⚡ Quick win

Extract the repeated LOC/Legacy format selection into a shared helper.

The same conditional is copy-pasted across four consumer-creation sites:

  • js/watch/src/video/mse.ts (here)
  • js/watch/src/video/decoder.ts
  • js/watch/src/audio/mse.ts
  • js/watch/src/audio/decoder.ts

Adding another non-CMAF container kind would require updating all four locations. A single helper isolates the decision.

♻️ Suggested refactor

In a shared container utility (e.g., js/watch/src/container.ts):

+ import * as Catalog from "@moq/hang/catalog";
+ import * as Container from "@moq/hang/container";
+
+ export function legacyContainerFormat(
+     container: Catalog.LegacyContainer | Catalog.LocContainer,
+ ): Container.Format {
+     if (container.kind === "loc") {
+         return new Container.Loc.Format({ timescale: container.timescale });
+     }
+     return new Container.Legacy.Format();
+ }

Then at each call site:

- const format =
-     config.container.kind === "loc"
-         ? new Container.Loc.Format({ timescale: config.container.timescale })
-         : new Container.Legacy.Format();
+ const format = legacyContainerFormat(config.container);
🤖 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 `@js/watch/src/video/mse.ts` around lines 151 - 154, Extract the duplicated
LOC/Legacy format selection into a single helper function (e.g.,
legacyContainerFormat) that accepts a Catalog.LegacyContainer |
Catalog.LocContainer and returns a Container.Format; implement the LOC branch to
return new Container.Loc.Format({ timescale: container.timescale }) and the
other branch to return new Container.Legacy.Format(), export it from a shared
module (suggested js/watch/src/container.ts), and replace each inline
conditional (the ternary constructing new Container.Loc.Format or new
Container.Legacy.Format in mse.ts, decoder.ts, audio/mse.ts, audio/decoder.ts)
with a call to legacyContainerFormat(config.container).
js/loc/src/index.test.ts (1)

1-104: ⚡ Quick win

Consider adding a Producer / roundtrip test.

The Rust side has a roundtrip test pairing encode and decode. On the JS side only Format.decode is exercised here; the new Producer encoder has no test coverage. A simple Producer.encode(...) -> Format.decode(...) roundtrip would catch encoder-side regressions (varint framing, property emission, length prefix) symmetrically with Rust.

🤖 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 `@js/loc/src/index.test.ts` around lines 1 - 104, Add a Producer/roundtrip test
that encodes a frame with Producer.encode and immediately decodes it with
Format.decode to assert fields match; specifically create a Producer instance
(with at least one timescale scenario and a payload), call Producer.encode(...)
to get the encoded buffer, pass that buffer to new Format().decode(...) and
assert timestamp, data, keyframe and timescale-derived conversion are identical
to the original inputs—this will exercise Producer.encode, Format.decode and
varint/length-prefix framing together (refer to Producer.encode and
Format.decode to locate code).
rs/moq-loc/src/lib.rs (2)

76-127: 💤 Low value

LGTM with one small note on delta-encoding strictness.

Decoding correctly enforces properties_length bounds, treats the first type as absolute (delta from 0), uses delta accumulation thereafter, splits even (varint value) vs odd (length-prefixed bytes) types, and surfaces MissingTimestamp precisely. The unit tests cover roundtrip, per-frame timescale, video-config skip, missing timestamp, and overflowing length.

One minor nit: a delta of 0 (after the first property) is silently accepted, which would mean two properties of the same type ID. The draft requires strictly ascending property IDs, so rejecting delta == 0 past the first KVP would catch malformed encoders. Optional and not blocking.

🤖 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-loc/src/lib.rs` around lines 76 - 127, In decode, enforce strictly
ascending property IDs by rejecting a delta of 0 after the first property: after
computing delta and determining abs (inside the while loop in function decode
where prev_type and first are used), if first is false and delta == 0 return
Err(Error::MalformedProperties); keep allowing delta==0 for the very first entry
(the absolute initial type). This ensures duplicate property IDs are treated as
malformed per the draft while preserving current handling of the initial
absolute type.

146-159: 💤 Low value

Consider refactoring to use VarInt directly if a plain varint helper is exposed in moq_lite.

The current use of Timescale<1> works correctly, but it's semantically a time abstraction (despite the dimensionless scale) used only for varint encoding. A dedicated varint interface would be clearer. Note that VarInt's encode_quic and decode_quic methods are currently private; refactoring would require either exposing public varint helpers in moq_lite or switching to the trait-based interface (which requires a Version parameter that the current code avoids).

🤖 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-loc/src/lib.rs` around lines 146 - 159, The read_varint/write_varint
helpers currently use Timescale::<1> as a varint encoder/decoder; refactor them
to use a dedicated VarInt interface instead: replace
Timescale::<1>::decode/encode usage in read_varint and write_varint with
moq_lite's VarInt public helper (or newly-exposed functions) so the functions
call VarInt::decode_quic/encode_quic (or a public wrapper like
varint::decode/encode) rather than the time abstraction; if VarInt's
encode_quic/decode_quic remain private, add a small public varint helper in
moq_lite (or implement the trait-based VarInt API with an explicit Version
parameter) and update read_varint/write_varint to call that helper, mapping
moq_lite errors to Error the same way Timescale did.
js/loc/src/index.ts (1)

115-124: ⚡ Quick win

Remove redundant ?. on #group — it's always non-null here.

After the if (keyframe) / else if (!this.#group) block, this.#group is guaranteed to be defined: the keyframe branch assigns it; the non-keyframe branch throws if it is null. The optional chaining on line 123 silently swallows the writeFrame call in any future scenario where the invariant is accidentally broken, masking the bug instead of surfacing it.

♻️ Proposed fix
-    this.#group?.writeFrame(this.#encode(data, timestamp));
+    this.#group.writeFrame(this.#encode(data, timestamp));
🤖 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 `@js/loc/src/index.ts` around lines 115 - 124, The encode method uses optional
chaining on this.#group when calling writeFrame, which is redundant because
after the if (keyframe) / else if (!this.#group) checks the `#group` field is
guaranteed non-null; replace the optional call with a direct call to
this.#group.writeFrame(...) so failures surface instead of being silently
ignored—modify encode (and keep the existing use of this.#track.appendGroup()
and this.#encode(...)) to call writeFrame on `#group` without ?. to enforce the
invariant.
🤖 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 `@js/loc/src/index.ts`:
- Around line 157-160: The close method currently calls this.#track.close(err)
before this.#group?.close(), which breaks moq-lite's teardown order; change the
teardown to call this.#group?.close() first, then call this.#track.close(err) so
the group can finalize its signals before the parent track is closed (update the
close method where it references this.#group and this.#track).

In `@js/watch/src/video/mse.ts`:
- Around line 155-158: The inline comment near the Container.Consumer
construction (where the variable format may be Loc.Format or Legacy.Format) is
stale — update it to describe both behaviours: that Loc.Format is selected with
an explicit timescale while Legacy.Format assumes a microsecond timescale
implicitly; modify the comment above the Container.Consumer(...) invocation
(referencing format and Container.Consumer) to clearly state these two cases and
also update the equivalent comment in the audio MSE implementation to match.

In `@rs/hang/src/catalog/container.rs`:
- Around line 34-40: The timescale field currently accepts 0 which is invalid;
add a custom deserializer (e.g., fn non_zero_timescale<'de, D>(deserializer: D)
-> Result<u64, D::Error>) that reads the u64 and returns
Err(D::Error::custom("timescale must be non-zero")) when value == 0, then
annotate the field with #[serde(default = "default_loc_timescale",
deserialize_with = "non_zero_timescale")]; apply the same change to the other
timescale field referenced in lines 43-45 so deserialization rejects zero at the
boundary instead of allowing invalid values into Loc (and the other struct).

---

Nitpick comments:
In `@js/loc/src/index.test.ts`:
- Around line 1-104: Add a Producer/roundtrip test that encodes a frame with
Producer.encode and immediately decodes it with Format.decode to assert fields
match; specifically create a Producer instance (with at least one timescale
scenario and a payload), call Producer.encode(...) to get the encoded buffer,
pass that buffer to new Format().decode(...) and assert timestamp, data,
keyframe and timescale-derived conversion are identical to the original
inputs—this will exercise Producer.encode, Format.decode and
varint/length-prefix framing together (refer to Producer.encode and
Format.decode to locate code).

In `@js/loc/src/index.ts`:
- Around line 115-124: The encode method uses optional chaining on this.#group
when calling writeFrame, which is redundant because after the if (keyframe) /
else if (!this.#group) checks the `#group` field is guaranteed non-null; replace
the optional call with a direct call to this.#group.writeFrame(...) so failures
surface instead of being silently ignored—modify encode (and keep the existing
use of this.#track.appendGroup() and this.#encode(...)) to call writeFrame on
`#group` without ?. to enforce the invariant.

In `@js/watch/src/video/mse.ts`:
- Around line 151-154: Extract the duplicated LOC/Legacy format selection into a
single helper function (e.g., legacyContainerFormat) that accepts a
Catalog.LegacyContainer | Catalog.LocContainer and returns a Container.Format;
implement the LOC branch to return new Container.Loc.Format({ timescale:
container.timescale }) and the other branch to return new
Container.Legacy.Format(), export it from a shared module (suggested
js/watch/src/container.ts), and replace each inline conditional (the ternary
constructing new Container.Loc.Format or new Container.Legacy.Format in mse.ts,
decoder.ts, audio/mse.ts, audio/decoder.ts) with a call to
legacyContainerFormat(config.container).

In `@rs/hang/src/catalog/container.rs`:
- Around line 43-45: The code uses the literal 1_000_000 in
default_loc_timescale (and again at the other location noted) — replace these
magic-number occurrences with a single named constant (e.g.
DEFAULT_LOC_TIMESCALE) declared at module scope and return that constant from
the function default_loc_timescale and use it wherever the literal currently
appears (including the other occurrence referenced around lines 54-55 and in
related tests) so the value is centralized and stays in sync.

In `@rs/moq-loc/src/lib.rs`:
- Around line 76-127: In decode, enforce strictly ascending property IDs by
rejecting a delta of 0 after the first property: after computing delta and
determining abs (inside the while loop in function decode where prev_type and
first are used), if first is false and delta == 0 return
Err(Error::MalformedProperties); keep allowing delta==0 for the very first entry
(the absolute initial type). This ensures duplicate property IDs are treated as
malformed per the draft while preserving current handling of the initial
absolute type.
- Around line 146-159: The read_varint/write_varint helpers currently use
Timescale::<1> as a varint encoder/decoder; refactor them to use a dedicated
VarInt interface instead: replace Timescale::<1>::decode/encode usage in
read_varint and write_varint with moq_lite's VarInt public helper (or
newly-exposed functions) so the functions call VarInt::decode_quic/encode_quic
(or a public wrapper like varint::decode/encode) rather than the time
abstraction; if VarInt's encode_quic/decode_quic remain private, add a small
public varint helper in moq_lite (or implement the trait-based VarInt API with
an explicit Version parameter) and update read_varint/write_varint to call that
helper, mapping moq_lite errors to Error the same way Timescale did.
🪄 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: a11ce08f-22a4-4238-b96d-19133a0c0108

📥 Commits

Reviewing files that changed from the base of the PR and between 9b1665f and ff915e1.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • Cargo.toml
  • js/hang/package.json
  • js/hang/src/catalog/container.ts
  • js/hang/src/container/index.ts
  • js/loc/package.json
  • js/loc/src/index.test.ts
  • js/loc/src/index.ts
  • js/loc/tsconfig.json
  • js/watch/src/audio/decoder.ts
  • js/watch/src/audio/mse.ts
  • js/watch/src/audio/source.ts
  • js/watch/src/video/decoder.ts
  • js/watch/src/video/mse.ts
  • package.json
  • rs/hang/src/catalog/container.rs
  • rs/moq-loc/CHANGELOG.md
  • rs/moq-loc/Cargo.toml
  • rs/moq-loc/README.md
  • rs/moq-loc/src/lib.rs
  • rs/moq-mux/Cargo.toml
  • rs/moq-mux/src/container/hang.rs
  • rs/moq-mux/src/container/loc.rs
  • rs/moq-mux/src/container/mod.rs
  • rs/moq-mux/src/error.rs
  • rs/moq-mux/src/export/fmp4.rs

Comment thread js/loc/src/index.ts
Comment on lines +157 to +160
close(err?: Error) {
this.#track.close(err);
this.#group?.close();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

moq-lite Track Group close teardown order JavaScript

💡 Result:

In moq-lite’s JavaScript/TypeScript API (the @moq/lite package), the teardown/close order for a published track is: 1) close each Group (via group.close) after writing frames, then 2) close the Track (via track.close) after all groups are done. Evidence from the official example: - The publisher example creates a group, writes frames, then “Mark the group as complete” with group.close, and only after that “Mark the track as complete (optional)” with track.close [1]. Conceptual protocol alignment: - A Group is the in-order unit of frames and is closed when “finished or aborted” [2]. - A Track consists of groups delivered out-of-order “until closed/cancelled” [2]. So, for a clean shutdown when publishing in JS: - group.close first (for the currently-appending group) - track.close next (after the last group) Notes: - The example marks track.close as optional; depending on application/relay behavior you may also rely on end-of-stream/track completion semantics, but the documented teardown pattern is group.close then track.close [1].

Citations:


Close the group before the track to ensure proper finalization.

The code currently calls this.#track.close(err) before this.#group?.close(). According to moq-lite's documented teardown pattern, groups must be closed before their parent track. Closing the track first prevents the group from properly finalizing any pending signals (such as group boundaries), resulting in lost or dropped data.

🐛 Proposed fix: reverse teardown order
 close(err?: Error) {
-    this.#track.close(err);
     this.#group?.close();
+    this.#track.close(err);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
close(err?: Error) {
this.#track.close(err);
this.#group?.close();
}
close(err?: Error) {
this.#group?.close();
this.#track.close(err);
}
🤖 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 `@js/loc/src/index.ts` around lines 157 - 160, The close method currently calls
this.#track.close(err) before this.#group?.close(), which breaks moq-lite's
teardown order; change the teardown to call this.#group?.close() first, then
call this.#track.close(err) so the group can finalize its signals before the
parent track is closed (update the close method where it references this.#group
and this.#track).

Comment thread js/watch/src/video/mse.ts
Comment on lines 155 to +158
// Create consumer that reorders groups/frames up to the provided latency.
// Legacy container uses microsecond timescale implicitly.
const consumer = new Container.Consumer(data, {
format: new Container.Legacy.Format(),
format,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale comment — update to reflect both LOC and Legacy formats.

Line 156 ("Legacy container uses microsecond timescale implicitly.") was accurate when only Legacy.Format was constructed here, but the block now conditionally selects Loc.Format with an explicit timescale. The comment no longer reflects the current state of the code. The same stale comment exists in js/watch/src/audio/mse.ts line 148.

✏️ Suggested fix
- // Legacy container uses microsecond timescale implicitly.
+ // LOC uses the catalog timescale; Legacy uses microsecond timescale implicitly.
  const consumer = new Container.Consumer(data, {

As per coding guidelines: "Comments must reflect the current state of the code, not its history."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Create consumer that reorders groups/frames up to the provided latency.
// Legacy container uses microsecond timescale implicitly.
const consumer = new Container.Consumer(data, {
format: new Container.Legacy.Format(),
format,
// Create consumer that reorders groups/frames up to the provided latency.
// LOC uses the catalog timescale; Legacy uses microsecond timescale implicitly.
const consumer = new Container.Consumer(data, {
format,
🤖 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 `@js/watch/src/video/mse.ts` around lines 155 - 158, The inline comment near
the Container.Consumer construction (where the variable format may be Loc.Format
or Legacy.Format) is stale — update it to describe both behaviours: that
Loc.Format is selected with an explicit timescale while Legacy.Format assumes a
microsecond timescale implicitly; modify the comment above the
Container.Consumer(...) invocation (referencing format and Container.Consumer)
to clearly state these two cases and also update the equivalent comment in the
audio MSE implementation to match.

Comment on lines +34 to +40
Loc {
/// Catalog-level timescale (units per second) used when a LOC frame
/// omits its own 0x08 timescale property. Defaults to 1_000_000
/// (microseconds).
#[serde(default = "default_loc_timescale")]
timescale: u64,
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject zero LOC timescale at deserialization boundary.

timescale: 0 currently parses successfully. That value is invalid for units-per-second and can later break timestamp conversion paths.

Suggested fix
 	Loc {
 		/// Catalog-level timescale (units per second) used when a LOC frame
 		/// omits its own 0x08 timescale property. Defaults to 1_000_000
 		/// (microseconds).
-		#[serde(default = "default_loc_timescale")]
+		#[serde(
+			default = "default_loc_timescale",
+			deserialize_with = "deserialize_loc_timescale"
+		)]
 		timescale: u64,
 	},
 }
 
 fn default_loc_timescale() -> u64 {
 	1_000_000
 }
+
+fn deserialize_loc_timescale<'de, D>(deserializer: D) -> Result<u64, D::Error>
+where
+	D: serde::Deserializer<'de>,
+{
+	let timescale = u64::deserialize(deserializer)?;
+	if timescale == 0 {
+		return Err(serde::de::Error::custom("LOC timescale must be > 0"));
+	}
+	Ok(timescale)
+}

Also applies to: 43-45

🤖 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/hang/src/catalog/container.rs` around lines 34 - 40, The timescale field
currently accepts 0 which is invalid; add a custom deserializer (e.g., fn
non_zero_timescale<'de, D>(deserializer: D) -> Result<u64, D::Error>) that reads
the u64 and returns Err(D::Error::custom("timescale must be non-zero")) when
value == 0, then annotate the field with #[serde(default =
"default_loc_timescale", deserialize_with = "non_zero_timescale")]; apply the
same change to the other timescale field referenced in lines 43-45 so
deserialization rejects zero at the boundary instead of allowing invalid values
into Loc (and the other struct).

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.

2 participants