Skip to content

feat(ffi): expose generic CMAF muxing - #2765

Draft
Qizot wants to merge 1 commit into
moq-dev:mainfrom
Qizot:feat/cmaf-muxer-api
Draft

feat(ffi): expose generic CMAF muxing#2765
Qizot wants to merge 1 commit into
moq-dev:mainfrom
Qizot:feat/cmaf-muxer-api

Conversation

@Qizot

@Qizot Qizot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Let applications package selected encoded audio or video frames into standalone CMAF without owning a live subscription.
  • Rebase independently selected fragments onto an explicit presentation origin, preserve exact sample durations, and return matching initialization and media data together.
  • Resolve inline H.264 and H.265 configuration before emission, and reject batches that cross a codec reconfiguration boundary so one init segment always describes its fragment.
  • Expose the same single-rendition muxer through Rust, UniFFI, C, Swift, Kotlin, Python, and Go.

Public API changes

  • Add fmp4::Output, Muxer::fragment_rebased, Muxer::mux, and Error::CodecConfigChanged to moq-mux.
  • Add MoqCmafTrack, MoqCmafConfig, MoqCmafOutput, and MoqCmafMuxer to moq-ffi, plus optional duration_us metadata on MoqMediaFrame.
  • Add the moq_cmaf_* configuration, frame, output, creation, muxing, initialization, and close surface to libmoq.
  • Add idiomatic CMAF muxer wrappers for Swift, Kotlin, Python, and Go. These changes are additive and target main.

Test plan

  • cargo fmt --all -- --check
  • cargo check -p moq-mux -p moq-ffi -p libmoq
  • cargo nextest run --all-targets -p moq-mux -p moq-ffi -p libmoq --no-fail-fast (607 passed)

(Written by GPT-5)

Add transport-independent frame muxing with explicit timeline origins, exact sample durations, atomic initialization and fragment output, and codec reconfiguration boundaries. Expose the same API through UniFFI, C, Swift, Kotlin, Python, and Go.

Co-Authored-By: Codex <codex@openai.com>
@Qizot
Qizot marked this pull request as draft August 12, 2026 09:16
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Added CMAF muxing to the Rust fMP4 engine, including timestamp rebasing, codec metadata handling, configuration boundaries, and sample durations. Added C and UniFFI bindings with lifecycle and error handling. Added Go, Kotlin, Python, and Swift APIs with tests. Added documentation for CMAF packaging workflows and output behavior.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: exposing generic CMAF muxing through FFI.
Description check ✅ Passed The description accurately summarizes generic CMAF muxing, codec handling, APIs, bindings, and tests added by the changeset.
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.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

🧹 Nitpick comments (4)
py/moq-rs/moq/cmaf.py (1)

27-51: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Expose deterministic CMAF muxer cleanup in every wrapper.

CmafMuxer and CMAFMuxer wrap a native MoqCmafMuxer, but neither wrapper exposes close. Add explicit cleanup in both APIs. Add a Python context manager so callers can reliably release the handle.

  • py/moq-rs/moq/cmaf.py#L27-L51: add close() and context-manager cleanup that delegates to the FFI handle.
  • swift/Sources/Moq/CMAF.swift#L12-L61: add a public close() method that delegates to the FFI handle.

The Kotlin wrapper already exposes AutoCloseable.close() for the same FFI handle.

(Written by CodeRabbit)

🤖 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 `@py/moq-rs/moq/cmaf.py` around lines 27 - 51, Expose deterministic cleanup for
both CMAF wrappers: in py/moq-rs/moq/cmaf.py, add CmafMuxer.close() delegating
to the native _ffi handle and implement context-manager entry/exit cleanup; in
swift/Sources/Moq/CMAF.swift, add the public CMAFMuxer.close() method delegating
to its FFI handle. Ensure both wrappers release the underlying MoqCmafMuxer
explicitly.
kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt (1)

91-117: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Verify the timestamp rebasing result, not only fragment presence.

Each test passes a frame timestamp equal to the configured origin. Each test then checks only for non-empty output. A wrapper that forwards 0 as the origin would still pass. Decode or inspect the fragment tfdt and assert that the first sample is rebased to zero.

  • kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt#L91-L117: assert the fragment decode time is zero after muxing.
  • py/moq-rs/tests/test_local.py#L102-L127: assert the fragment decode time is zero after muxing.
  • swift/Tests/MoqTests/SmokeTests.swift#L174-L199: assert the fragment decode time is zero after muxing.

The supplied native muxer test demonstrates the required timestamp check.

(Written by CodeRabbit)

🤖 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 `@kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt` around lines 91 -
117, The muxer smoke tests only verify fragment presence and must also verify
timestamp rebasing. In kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt
lines 91-117, py/moq-rs/tests/test_local.py lines 102-127, and
swift/Tests/MoqTests/SmokeTests.swift lines 174-199, inspect or decode the
generated fragment’s tfdt and assert that the first sample decode time is zero
after using the configured origin; retain the existing initialization and
fragment presence assertions.
rs/libmoq/src/test.rs (1)

127-174: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the rebased timestamp.

The test only verifies that a fragment exists. A muxer that ignores origin_us also passes this test. Decode the fragment and assert that its first timestamp is zero.

As per coding guidelines, add a regression test that fails without the fix. (Written by CodeRabbit)

🤖 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/libmoq/src/test.rs` around lines 127 - 174, The
cmaf_muxer_constructs_and_rebases_frames test must verify origin rebasing, not
just fragment creation. Decode output.fragment using the existing CMAF/fragment
parsing utilities, assert the first decoded sample timestamp is zero, and keep
the test configured with origin_us and timestamp_us both set to 10_000_000 so it
fails when rebasing is ignored.

Source: Coding guidelines

rs/moq-mux/src/container/fmp4/muxer.rs (1)

307-323: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Hoist the origin conversion out of the per-frame loop when the scale repeats.

fragment_rebased recomputes moq_net::Timestamp::try_from(origin)? on every iteration, and converts it to frame.timestamp.scale() each time. Frames in one batch normally share a scale, so the conversion repeats without effect. Caching the converted origin per scale keeps behavior identical and removes the repeated work.

♻️ Proposed refactor
 	pub fn fragment_rebased(&self, sequence: u32, origin: Duration, frames: &[Frame]) -> crate::Result<Bytes> {
+		let origin = moq_net::Timestamp::try_from(origin)?;
 		let mut rebased = Vec::with_capacity(frames.len());
+		let mut cached: Option<(moq_net::Timescale, moq_net::Timestamp)> = None;
 		for frame in frames {
-			let track_origin = moq_net::Timestamp::try_from(origin)?.convert(frame.timestamp.scale())?;
+			let scale = frame.timestamp.scale();
+			let track_origin = match cached {
+				Some((cached_scale, value)) if cached_scale == scale => value,
+				_ => {
+					let value = origin.convert(scale)?;
+					cached = Some((scale, value));
+					value
+				}
+			};
 			let timestamp = frame.timestamp.checked_sub(track_origin)?;
🤖 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/fmp4/muxer.rs` around lines 307 - 323, Update
fragment_rebased to cache the converted origin per timestamp scale instead of
recomputing Timestamp::try_from(origin) and convert for every frame; reuse the
cached value when consecutive or repeated frame.timestamp.scale() values match,
while preserving the existing checked subtraction and fragment_owned behavior.
🤖 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 `@doc/lib/c/index.md`:
- Around line 279-295: After the moq_cmaf_video call, return from the example or
otherwise skip frame setup and moq_cmaf_mux when muxer is negative. Keep the
existing moq_error reporting, and allow the mux path to run only after
successful muxer creation.

In `@rs/libmoq/src/cmaf.rs`:
- Around line 205-208: Update the frame-size handling in moq_cmaf_video so a
non-zero coded_width combined with a zero coded_height, or vice versa, returns
Error::InvalidCode. Preserve assigning both values when they are non-zero and
continue treating both-zero dimensions as unknown.

In `@rs/moq-ffi/src/cmaf.rs`:
- Around line 20-28: Add a Rust-only MoqCmafConfig::new(track) constructor that
initializes track and sets origin_us to zero, while retaining the existing
#[uniffi(default = 0)] annotation for foreign callers.

In `@rs/moq-ffi/src/test.rs`:
- Around line 492-511: Strengthen the fragment metadata assertion in the muxer
test by capturing traf.trun[0].entries.len() and the inferred duration of the
second entry alongside the existing sequence, track, decode-time, and
first-duration values. Update the expected fragment_info tuple to require
exactly two samples and the expected second-entry duration, preserving the
existing checks.

---

Nitpick comments:
In `@kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt`:
- Around line 91-117: The muxer smoke tests only verify fragment presence and
must also verify timestamp rebasing. In
kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt lines 91-117,
py/moq-rs/tests/test_local.py lines 102-127, and
swift/Tests/MoqTests/SmokeTests.swift lines 174-199, inspect or decode the
generated fragment’s tfdt and assert that the first sample decode time is zero
after using the configured origin; retain the existing initialization and
fragment presence assertions.

In `@py/moq-rs/moq/cmaf.py`:
- Around line 27-51: Expose deterministic cleanup for both CMAF wrappers: in
py/moq-rs/moq/cmaf.py, add CmafMuxer.close() delegating to the native _ffi
handle and implement context-manager entry/exit cleanup; in
swift/Sources/Moq/CMAF.swift, add the public CMAFMuxer.close() method delegating
to its FFI handle. Ensure both wrappers release the underlying MoqCmafMuxer
explicitly.

In `@rs/libmoq/src/test.rs`:
- Around line 127-174: The cmaf_muxer_constructs_and_rebases_frames test must
verify origin rebasing, not just fragment creation. Decode output.fragment using
the existing CMAF/fragment parsing utilities, assert the first decoded sample
timestamp is zero, and keep the test configured with origin_us and timestamp_us
both set to 10_000_000 so it fails when rebasing is ignored.

In `@rs/moq-mux/src/container/fmp4/muxer.rs`:
- Around line 307-323: Update fragment_rebased to cache the converted origin per
timestamp scale instead of recomputing Timestamp::try_from(origin) and convert
for every frame; reuse the cached value when consecutive or repeated
frame.timestamp.scale() values match, while preserving the existing checked
subtraction and fragment_owned behavior.
🪄 Autofix

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 Plus

Run ID: 44b7ce17-6365-496f-9c25-6cdc3a2df962

📥 Commits

Reviewing files that changed from the base of the PR and between 9040cfe and 116d181.

📒 Files selected for processing (30)
  • doc/lib/c/index.md
  • doc/lib/go/moq.md
  • doc/lib/kt/moq.md
  • doc/lib/py/moq-rs.md
  • doc/lib/swift/moq.md
  • go/wrapper/moq/cmaf.go
  • go/wrapper/moq/moq_test.go
  • go/wrapper/moq/types.go
  • kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Cmaf.kt
  • kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt
  • py/moq-rs/moq/__init__.py
  • py/moq-rs/moq/cmaf.py
  • py/moq-rs/tests/test_local.py
  • rs/libmoq/src/cmaf.rs
  • rs/libmoq/src/lib.rs
  • rs/libmoq/src/state.rs
  • rs/libmoq/src/test.rs
  • rs/moq-ffi/src/cmaf.rs
  • rs/moq-ffi/src/consumer.rs
  • rs/moq-ffi/src/lib.rs
  • rs/moq-ffi/src/media.rs
  • rs/moq-ffi/src/test.rs
  • rs/moq-mux/src/codec/h264/mod.rs
  • rs/moq-mux/src/codec/h265/mod.rs
  • rs/moq-mux/src/container/fmp4/export.rs
  • rs/moq-mux/src/container/fmp4/mod.rs
  • rs/moq-mux/src/container/fmp4/muxer.rs
  • rs/moq-mux/src/container/source.rs
  • swift/Sources/Moq/CMAF.swift
  • swift/Tests/MoqTests/SmokeTests.swift

Comment thread doc/lib/c/index.md
Comment on lines +279 to +295
int32_t muxer = moq_cmaf_video(&config);
if (muxer < 0) {
fprintf(stderr, "muxer creation failed: %s\n", moq_error());
}

struct moq_cmaf_frame frame = {
.payload = payload,
.payload_size = payload_size,
.timestamp_us = timestamp_us,
.keyframe = true,
.duration_us = duration_us,
.has_duration = true,
};
struct moq_cmaf_output output = {0};
if (moq_cmaf_mux(muxer, sequence, &frame, 1, &output) < 0) {
fprintf(stderr, "mux failed: %s\n", moq_error());
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Stop after CMAF muxer creation fails.

If moq_cmaf_video returns a negative handle, Line 293 still passes that handle to moq_cmaf_mux. Exit the example or guard the mux call after reporting the creation error.

(Written by CodeRabbit)

🤖 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 `@doc/lib/c/index.md` around lines 279 - 295, After the moq_cmaf_video call,
return from the example or otherwise skip frame setup and moq_cmaf_mux when
muxer is negative. Keep the existing moq_error reporting, and allow the mux path
to run only after successful muxer creation.

Comment thread rs/libmoq/src/cmaf.rs
Comment on lines +205 to +208
if raw.coded_width != 0 && raw.coded_height != 0 {
config.coded_width = Some(raw.coded_width);
config.coded_height = Some(raw.coded_height);
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

A partially specified frame size is dropped without notice.

moq_cmaf_video applies coded_width and coded_height only when both values are non-zero. Each field is documented as "zero when unknown" on its own, so a caller that supplies only a width gets neither value, and no error. Returning Error::InvalidCode for a half-specified size makes the contract explicit.

🐛 Proposed fix
-		if raw.coded_width != 0 && raw.coded_height != 0 {
-			config.coded_width = Some(raw.coded_width);
-			config.coded_height = Some(raw.coded_height);
-		}
+		match (raw.coded_width, raw.coded_height) {
+			(0, 0) => {}
+			(width, height) if width != 0 && height != 0 => {
+				config.coded_width = Some(width);
+				config.coded_height = Some(height);
+			}
+			// A half-specified size is a caller mistake, not an unknown size.
+			_ => return Err(Error::InvalidCode),
+		}
📝 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
if raw.coded_width != 0 && raw.coded_height != 0 {
config.coded_width = Some(raw.coded_width);
config.coded_height = Some(raw.coded_height);
}
match (raw.coded_width, raw.coded_height) {
(0, 0) => {}
(width, height) if width != 0 && height != 0 => {
config.coded_width = Some(width);
config.coded_height = Some(height);
}
// A half-specified size is a caller mistake, not an unknown size.
_ => return Err(Error::InvalidCode),
}
🤖 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/libmoq/src/cmaf.rs` around lines 205 - 208, Update the frame-size handling
in moq_cmaf_video so a non-zero coded_width combined with a zero coded_height,
or vice versa, returns Error::InvalidCode. Preserve assigning both values when
they are non-zero and continue treating both-zero dimensions as unknown.

Comment thread rs/moq-ffi/src/cmaf.rs
Comment on lines +20 to +28
/// Options for constructing a single-rendition CMAF muxer.
#[derive(uniffi::Record)]
pub struct MoqCmafConfig {
/// The encoded rendition to package.
pub track: MoqCmafTrack,
/// Timestamp subtracted from every sample so independently fetched fragments share a timeline.
#[uniffi(default = 0)]
pub origin_us: 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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does UniFFI 0.31.0 support Rust #[non_exhaustive] on a type deriving uniffi::Record?

💡 Result:

UniFFI 0.31.0 does not support using the Rust #[non_exhaustive] attribute on a type deriving uniffi::Record. While UniFFI has added specific support for non-exhaustive enums (using a separate [NonExhaustive] attribute in UDL or through related proc-macro mechanisms) [1][2][3], this support does not extend to records. UniFFI Records are strictly structured data consisting of a fixed set of named fields, and the current proc-macro implementation for uniffi::Record does not accommodate the semantic flexibility or forward-compatibility requirements implied by the Rust #[non_exhaustive] attribute [4][5]. If you need to handle evolving data structures with UniFFI, the library typically recommends managing versioning through explicit field defaults [5][6] or defining wrapper types that manage compatibility between different versions of the data.

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- cmaf.rs ---'
cat -n rs/moq-ffi/src/cmaf.rs | sed -n '1,180p'
printf '%s\n' '--- moq-ffi manifests ---'
rg -n -C 3 'uniffi|version' rs/moq-ffi/Cargo.toml Cargo.toml rs -g 'Cargo.toml' | head -160
printf '%s\n' '--- comparable records and constructors ---'
rg -n -C 5 '#\[derive\(uniffi::Record\)|#\[uniffi::export\]|pub struct Moq.*Config|fn new\(' rs/moq-ffi rs/libmoq -g '*.rs' | head -240
printf '%s\n' '--- rust tool availability ---'
rustc --version 2>/dev/null || true
cargo --version 2>/dev/null || true

Repository: moq-dev/moq

Length of output: 28195


🌐 Web query:

site:github.com/mozilla/uniffi-rs 0.31.0 non_exhaustive Record Rust attribute

💡 Result:

The term "non_exhaustive" in the context of the UniFFI project refers to the Rust attribute #[non_exhaustive] [1], which is used to indicate that an enum or struct may have additional variants or fields added in future versions [1][2]. There is no specific UniFFI attribute named "non_exhaustive record". Rather, UniFFI handles non_exhaustive types in the following ways: 1. Standard Rust Attribute: UniFFI respects the standard library #[non_exhaustive] attribute applied to Rust enums or structs [1][2]. 2. UDL Support: When working with UniFFI's Interface Definition Language (UDL), there is a [NonExhaustive] UDL attribute used to inform UniFFI that a type (typically an enum) defined in an upstream crate is marked as non-exhaustive [1][2]. This is necessary because UniFFI must generate a default arm when matching against these variants to prevent compilation errors in foreign language bindings [1]. 3. Records: UniFFI provides a #[derive(uniffi::Record)] macro to expose Rust structs as records (dictionaries) [3]. While #[non_exhaustive] can be applied to Rust structs, the uniffi::Record derive macro primarily focuses on mapping named fields [3]. UniFFI v0.31.0 is a released version (as of January 2026), but it does not introduce a specific "non_exhaustive record" feature [4]. The support for non_exhaustive types has been part of UniFFI's evolution to handle remote types from other crates correctly [1][5][2]. [1][2][4]

Citations:


🌐 Web query:

site:mozilla.github.io/uniffi-rs/0.31 records non_exhaustive uniffi::Record

💡 Result:

UniFFI does not provide a non_exhaustive attribute for the uniffi::Record derive macro. The non_exhaustive attribute is specifically supported for enumerations in UniFFI, particularly when using the Interface Definition Language (UDL) [1]. For enums defined in external crates that are marked as #[non_exhaustive], UniFFI provides a [NonExhaustive] attribute in UDL to facilitate handling them by generating a default arm for matching [1]. This is necessary because UniFFI requires exhaustive matching during the encoding and decoding process [2]. If you are working with structs using the #[derive(uniffi::Record)] macro, there is no corresponding non_exhaustive mechanism [3][4]. UniFFI records require a defined structure that must be consistent between Rust and the generated bindings [3]. If you encounter compatibility issues with external structs, it is generally recommended to use a wrapper (such as a newtype) or a custom type converter, though these approaches involve additional implementation effort [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
import json
import urllib.request

base = "https://api.github.com/repos/mozilla/uniffi-rs"
paths = [
    "/git/trees/v0.31.0?recursive=1",
    "/contents/uniffi_macros/src/record.rs?ref=v0.31.0",
    "/contents/uniffi_bindgen/src?ref=v0.31.0",
]
for path in paths:
    print(f"--- {path} ---")
    req = urllib.request.Request(
        base + path,
        headers={"Accept": "application/vnd.github+json", "User-Agent": "review-inspection"},
    )
    try:
        with urllib.request.urlopen(req, timeout=15) as response:
            data = json.load(response)
        if path.startswith("/git/trees"):
            for item in data.get("tree", []):
                name = item["path"].lower()
                if "record" in name or "non_exhaustive" in name or "changelog" in name:
                    print(item["path"])
        else:
            print(json.dumps(data, indent=2)[:12000])
    except Exception as exc:
        print(type(exc).__name__ + ": " + str(exc))
PY

Repository: moq-dev/moq

Length of output: 730


🌐 Web query:

UniFFI 0.31.0 #[non_exhaustive] #[derive(uniffi::Record)] compile struct

💡 Result:

In UniFFI 0.31.0, the #[non_exhaustive] attribute is primarily supported for enums defined in UDL, where it is handled via the [NonExhaustive] UDL attribute to allow UniFFI to generate a default arm for matching variants [1][2]. Regarding structs (records) marked with #[derive(uniffi::Record)], UniFFI does not have a corresponding #[non_exhaustive] feature for structs defined as records [3][4]. The #[non_exhaustive] attribute on a Rust struct is a language-level feature that prevents construction and pattern matching outside the crate defining the struct. Because UniFFI's #[derive(uniffi::Record)] macro requires full access to the struct's fields to generate the necessary FFI scaffolding (lift/lower implementations), using #[non_exhaustive] on a struct intended to be a UniFFI record will typically lead to compilation errors [3][5]. If you are encountering a compile error, it is likely because the derive macro cannot inspect the fields of the #[non_exhaustive] struct. To expose such a type through UniFFI, you must typically use a "wrapper" pattern: 1. Define a separate, non-exhaustive-compatible struct or type for your public API. 2. Implement a conversion (using something like a custom type or a manual mapping) between the external #[non_exhaustive] struct and a UniFFI-compatible record that you control [6]. UniFFI's internal metadata does support a non_exhaustive flag for enums [7][8], but this does not extend to providing equivalent support for non-exhaustive structs derived as records [3][5].

Citations:


Add a Rust constructor for MoqCmafConfig.

UniFFI records do not support #[non_exhaustive]. Add MoqCmafConfig::new(track) with origin_us set to zero, and retain the UniFFI field default for foreign callers.

(Written by CodeRabbit)

🤖 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-ffi/src/cmaf.rs` around lines 20 - 28, Add a Rust-only
MoqCmafConfig::new(track) constructor that initializes track and sets origin_us
to zero, while retaining the existing #[uniffi(default = 0)] annotation for
foreign callers.

Source: Coding guidelines

Comment thread rs/moq-ffi/src/test.rs
Comment on lines +492 to +511
let video_fragment = muxer.mux(12, first_video).unwrap().fragment.unwrap();
let fragment_info = [video_fragment]
.into_iter()
.map(|fragment| {
let mut cursor = std::io::Cursor::new(fragment.as_slice());
while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor).unwrap() {
if let mp4_atom::Any::Moof(moof) = atom {
let traf = &moof.traf[0];
return (
moof.mfhd.sequence_number,
traf.tfhd.track_id,
traf.tfdt.as_ref().unwrap().base_media_decode_time,
traf.trun[0].entries[0].duration,
);
}
}
panic!("fragment missing moof")
})
.collect::<Vec<_>>();
assert_eq!(fragment_info, [(12, 1, 0, Some(510))]);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the sample count so a dropped or duplicated sample fails the test.

The test supplies two frames but inspects only traf.trun[0].entries[0]. If the muxer dropped the second sample, or emitted an extra one, the assertion still passes. Capturing traf.trun[0].entries.len() closes that gap. Asserting the second entry's inferred duration also pins the gap-inference behavior that the delta frame exercises.

💚 Proposed test strengthening
 			while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor).unwrap() {
 				if let mp4_atom::Any::Moof(moof) = atom {
 					let traf = &moof.traf[0];
 					return (
 						moof.mfhd.sequence_number,
 						traf.tfhd.track_id,
 						traf.tfdt.as_ref().unwrap().base_media_decode_time,
+						traf.trun[0].entries.len(),
 						traf.trun[0].entries[0].duration,
 					);
 				}
 			}
 			panic!("fragment missing moof")
 		})
 		.collect::<Vec<_>>();
-	assert_eq!(fragment_info, [(12, 1, 0, Some(510))]);
+	assert_eq!(fragment_info, [(12, 1, 0, 2, Some(510))]);
📝 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
let video_fragment = muxer.mux(12, first_video).unwrap().fragment.unwrap();
let fragment_info = [video_fragment]
.into_iter()
.map(|fragment| {
let mut cursor = std::io::Cursor::new(fragment.as_slice());
while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor).unwrap() {
if let mp4_atom::Any::Moof(moof) = atom {
let traf = &moof.traf[0];
return (
moof.mfhd.sequence_number,
traf.tfhd.track_id,
traf.tfdt.as_ref().unwrap().base_media_decode_time,
traf.trun[0].entries[0].duration,
);
}
}
panic!("fragment missing moof")
})
.collect::<Vec<_>>();
assert_eq!(fragment_info, [(12, 1, 0, Some(510))]);
let video_fragment = muxer.mux(12, first_video).unwrap().fragment.unwrap();
let fragment_info = [video_fragment]
.into_iter()
.map(|fragment| {
let mut cursor = std::io::Cursor::new(fragment.as_slice());
while let Some(atom) = mp4_atom::Any::decode_maybe(&mut cursor).unwrap() {
if let mp4_atom::Any::Moof(moof) = atom {
let traf = &moof.traf[0];
return (
moof.mfhd.sequence_number,
traf.tfhd.track_id,
traf.tfdt.as_ref().unwrap().base_media_decode_time,
traf.trun[0].entries.len(),
traf.trun[0].entries[0].duration,
);
}
}
panic!("fragment missing moof")
})
.collect::<Vec<_>>();
assert_eq!(fragment_info, [(12, 1, 0, 2, Some(510))]);
🤖 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-ffi/src/test.rs` around lines 492 - 511, Strengthen the fragment
metadata assertion in the muxer test by capturing traf.trun[0].entries.len() and
the inferred duration of the second entry alongside the existing sequence,
track, decode-time, and first-duration values. Update the expected fragment_info
tuple to require exactly two samples and the expected second-entry duration,
preserving the existing checks.

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