Skip to content

feat(moq)!: migrate MoQ stack to moq-native 0.19 / hang 0.20 / moq-net + @moq/* majors - #675

Merged
streamer45 merged 18 commits into
mainfrom
devin/1787430379-moq-stack-migration
Aug 23, 2026
Merged

streamer45 merged 18 commits into
mainfrom
devin/1787430379-moq-stack-migration

Conversation

@staging-devin-ai-integration

@staging-devin-ai-integration staging-devin-ai-integration Bot commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Breaking MoQ stack migration for v0.6.0, deferred from chore(deps): compatible dependency refresh for v0.6.0 #674 (which stayed semver-compatible). Rust moves to moq-native 0.19.12 / hang 0.20.6 / moq-net 0.2.13 — upstream split the old moq-lite transport into moq-net + qmux + web-transport 0.12. UI moves to the coordinated @moq/* majors (@moq/hang 0.4, @moq/publish 0.4, @moq/watch 0.5, @moq/net 0.3, @moq/signals 0.2).
  • Upstream protocol/API changes adapted, not papered over: broadcast discovery is now announcement-based (origin.Consumer.announced_broadcast) instead of direct get_broadcast; track subscription is async; frames carry their timestamp in hang::container::Frame (the removed Timestamp::decode path); catalogs use Catalog::default_track() / to_json() / from_slice; server TLS fingerprints come from server.certificates().fingerprints() (tls_info() removed). Pull/peer nodes gained announcement-timeout + reconnect handling to preserve the previous late-publisher semantics.
  • UI stream wiring rewritten for the signal-based @moq/* API (streamStoreHelpers.ts): shared Publish.Broadcast with named rendition encoders (video/hd, audio/data), Watch.Broadcast/sources/decoders constructed from signals, and thin compatibility handles (VideoRendererHandle, AudioEmitterHandle) so views keep the videoRenderer.canvas.set(el) contract. Canvas aspect ratio remains derived from decoded dimensions (verified 1:1 below, no hardcoded 4:3/16:9). Pipeline/runtime state stays WebSocket-driven via the session store — no REST snapshots added. Bun overrides pin a single @moq/net/@moq/hang instance (duplicate nominal types otherwise break typechecking).
  • Compatibility/release risk: wire format follows current upstream moq/hang drafts, so v0.6.0 publishers/subscribers interoperate with current @moq/* clients and relays but not with pre-migration (v0.5.x moq-lite generation) peers. No StreamKit config changes: env vars, skit.toml keys, YAML node configs, and sample pipelines are unchanged.
  • Regression coverage added for announcement-based discovery, async late-track attachment, catalog parse/update cancellation safety, and mid-group abort recovery in pull/peer nodes; UI store/helper tests rewritten against the new API (81 helper tests).

Review & Validation

  • Protocol semantics: announcement-based discovery in pull.rs/peer/mod.rs preserves the old "wait for late publisher, reconnect on loss" behavior (timeout → default pin; closed announcement channel → reconnect).
  • UI publish path: shared broadcast + named encoders in streamStoreHelpers.ts matches upstream @moq/publish 0.4 expectations (audio gated until video catalog present).
  • Renderer handle keeps canvas binding/aspect behavior identical in StreamView.tsx / OutputPreviewPanel.tsx (no view changes needed).
  • Confirm the interop stance (no v0.5.x ↔ v0.6.0 MoQ compat) is acceptable for the release notes.

Validation performed:

  • just lint-ui, just test-ui (84 files / 1359 tests pass; the two useCompositorLayers*.perf unhandled-WS warnings reproduce identically on main), Rust fmt/clippy clean, targeted MoQ tests (199 pass), full just test Rust suite green, REUSE compliant.
  • Local no-TLS golden path E2E (Vite UI :3045 → skit :4545): Screen Share (MoQ) publish → server VP9 decode → compositor (edited to 1080×1080) → VP9 encode → MoQ watch. Publish connected, watch canvas rendered live frames with advancing timecode, canvas aspect = 1080/1080 (source-derived), zero @moq/* console errors.

Rendered 1:1 MoQ output canvas with compositor overlay

Session connected + LIVE

Session LIVE and connected

Liveness (later frame, advanced timecode)

Frame B

Benchmarks (crates/engine/benches/ — none touch MoQ; programmatic pipelines checked for node-interface changes, none needed; run to confirm no engine regression):

compositor_pipeline: 90 frames 640x480 VP9/WebM — 350.8 fps mean, 2.85 ms/frame, validation header=OK size=OK

Notes

  • Oneshot sample: not applicable — MoQ push/pull/peer are dynamic-only transport nodes (live QUIC sessions); they are not registered for oneshot mode, so no valid oneshot pipeline can exercise them. Existing dynamic samples (samples/pipelines/dynamic/moq*.yml, video_moq_*.yml) exercise the changed nodes unchanged.

  • Relationship to chore(deps): compatible dependency refresh for v0.6.0 #674: based directly on main; independent of chore(deps): compatible dependency refresh for v0.6.0 #674. If chore(deps): compatible dependency refresh for v0.6.0 #674 merges first, expect a trivial Cargo.lock/bun.lock conflict only.

  • The justfile license-check recipe's --metadata-path placement is rejected by current cargo-deny 0.20.x global-arg parsing (pre-existing; licenses pass when invoked as cargo deny --metadata-path … check licenses). Left untouched as out of scope.

  • Auth-mode E2E adaptation: upstream moq-native now accepts the WebTransport session before the gateway validates ?jwt=, so an auth-enabled server closes tokenless connections after establishment instead of rejecting the handshake (the old behavior the stream E2E tests relied on to skip). The auth-mode CI run therefore saw a transient "connected" state and failed. The stream tests now mint a MoQ token via the admin API and fill the Stream view token input, so E2E_AUTH=1 runs exercise the full authenticated MoQ path end-to-end (verified locally: 4/4 pass in both auth and no-auth modes).

Link to Devin session: https://staging.itsdev.in/sessions/c880623ccc5e4c3fb25fe20a785832bc
Open in Devin Desktop: https://staging.itsdev.in/desktop/session/c880623ccc5e4c3fb25fe20a785832bc?variant=devin-insiders
Requested by: @streamer45


Devin Review

Status Commit
🟢 Reviewed 52647e1
Devin Review (Staging)

…oq-net 0.2

Replace moq-lite with moq-net module-scoped APIs (origin/broadcast/track/
group/frame/announce), async track subscription, announcement-based
runtime discovery, hang 0.20 container frame encode/decode, and the new
moq-native certificates()/fingerprints() TLS API.

Signed-off-by: streamkit-devin <devin@streamkit.dev>
Signed-off-by: streamkit-devin <devin@streamkit.dev>
…sed)

Signed-off-by: streamkit-devin <devin@streamkit.dev>
@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@codecov

codecov Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.93651% with 129 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.32%. Comparing base (a8e8c32) to head (52647e1).

Files with missing lines Patch % Lines
crates/nodes/src/transport/moq/pull.rs 77.38% 64 Missing ⚠️
ui/src/stores/streamStoreHelpers.ts 76.11% 32 Missing ⚠️
crates/nodes/src/transport/moq/peer/mod.rs 84.61% 24 Missing ⚠️
crates/nodes/src/transport/moq/push.rs 80.95% 8 Missing ⚠️
apps/skit/src/server/moq.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #675      +/-   ##
==========================================
+ Coverage   85.28%   85.32%   +0.04%     
==========================================
  Files         249      249              
  Lines       75982    76190     +208     
  Branches     2444     2329     -115     
==========================================
+ Hits        64799    65010     +211     
+ Misses      11177    11174       -3     
  Partials        6        6              
Flag Coverage Δ
backend 85.36% <84.40%> (+0.03%) ⬆️
ui 84.94% <76.11%> (+0.14%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
core 85.79% <ø> (ø)
engine 83.76% <ø> (ø)
api 91.14% <ø> (ø)
nodes 84.96% <84.54%> (+0.05%) ⬆️
server 85.27% <0.00%> (+0.02%) ⬆️
plugin-native 84.79% <ø> (ø)
plugin-wasm 95.41% <ø> (ø)
ui-services 86.54% <76.11%> (+0.24%) ⬆️
ui-components 70.18% <ø> (ø)
Files with missing lines Coverage Δ
apps/skit/src/auth/moq.rs 100.00% <ø> (ø)
crates/nodes/src/transport/moq/catalog_consumer.rs 100.00% <100.00%> (ø)
crates/nodes/src/transport/moq/mod.rs 96.21% <100.00%> (+0.69%) ⬆️
crates/nodes/src/transport/moq/ordered_producer.rs 97.54% <100.00%> (+0.01%) ⬆️
crates/nodes/src/transport/moq/peer/config.rs 98.94% <ø> (ø)
ui/src/stores/streamStore.ts 70.42% <ø> (ø)
apps/skit/src/server/moq.rs 55.27% <0.00%> (+1.14%) ⬆️
crates/nodes/src/transport/moq/push.rs 87.86% <80.95%> (+0.04%) ⬆️
crates/nodes/src/transport/moq/peer/mod.rs 80.82% <84.61%> (+0.08%) ⬆️
ui/src/stores/streamStoreHelpers.ts 67.61% <76.11%> (+4.74%) ⬆️
... and 1 more

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

staging-devin-ai-integration[bot]

This comment was marked as resolved.

…ontrol messages

Also drop the unused direct @moq/hang dependency (kept pinned via a Bun
override for transitive dedupe) to satisfy knip.

Signed-off-by: streamkit-devin <devin@streamkit.dev>
staging-devin-ai-integration[bot]

This comment was marked as resolved.

Resolves RUSTSEC-2026-0258 (h2 unbounded empty DATA frames) and
RUSTSEC-2026-0222 / RUSTSEC-2026-0223 (wasmtime) flagged by
cargo deny check advisories in CI.

Signed-off-by: streamkit-devin <devin@streamkit.dev>
Raises patch coverage on the new streamStoreHelpers handle classes
introduced by the MoQ stack migration.

Signed-off-by: streamkit-devin <devin@streamkit.dev>
staging-devin-ai-integration[bot]

This comment was marked as resolved.

The announcement future is recreated after each non-shutdown control
message; moq-net's AnnounceConsumer replays the currently active
broadcast set as initial announcements, so a broadcast announced before
the restart is still observed. Pin that behavior with a test.

Signed-off-by: streamkit-devin <devin@streamkit.dev>
Each catalog snapshot is written to its own single-frame group; a
consumer keeping pace must observe every successive update.

Signed-off-by: streamkit-devin <devin@streamkit.dev>
staging-devin-ai-integration[bot]

This comment was marked as resolved.

…fails

Signed-off-by: streamkit-devin <devin@streamkit.dev>
Signed-off-by: streamkit-devin <devin@streamkit.dev>
Signed-off-by: streamkit-devin <devin@streamkit.dev>
staging-devin-ai-integration[bot]

This comment was marked as resolved.

moq-native now accepts the WebTransport session before the gateway
validates the ?jwt= parameter, so an auth-enabled server closes
tokenless connections after establishment instead of rejecting the
handshake. The stream tests previously relied on that pre-establishment
rejection to skip; now they mint a MoQ token via the admin API and fill
the Stream view token input so auth-mode runs exercise the full
authenticated path. Also make the 'Disconnected' status locator exact to
avoid strict-mode collisions with the disconnect error banner.

Signed-off-by: streamkit-devin <devin@streamkit.dev>
staging-devin-ai-integration[bot]

This comment was marked as resolved.

A biased select ensures an unread catalog snapshot in the held group
cannot be displaced when the next group is already available.

Signed-off-by: streamkit-devin <devin@streamkit.dev>

@staging-devin-ai-integration staging-devin-ai-integration Bot left a comment •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 3 new potential issues.

Devin Review (Staging)
Debug

Playground

Comment on lines +98 to +104
/// Publish one catalog snapshot as its own single-frame group.
pub(super) fn write_catalog_json(
producer: &mut moq_net::track::Producer,
json: impl Into<bytes::Bytes>,
) -> Result<(), moq_net::Error> {
producer.write_frame(moq_net::Timestamp::now(), json.into())
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

📝 Info: Catalog updates depend on one-group-per-frame semantics

write_catalog_json writes a single frame directly on the producer rather than the old append-group/finish sequence. Observing each republished catalog depends on write_frame opening a fresh group per call, since CatalogConsumer reads one frame per group and discards the rest. New tests exercise this against the real library; note the coupling for future moq_net bumps.

Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct — the vendored consumer reads one frame per group, so each catalog snapshot must land in its own group. moq_net::track::Producer::write_frame documents exactly that (each call appends a fresh group), and this is also the semantic upstream hang's own catalog publishing relies on. The back-to-back republish tests run against the real library, so a future moq_net bump that changed this would fail them immediately.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct — moq_net::track::Producer::write_frame is documented as "Create a group with a single frame", so each catalog republish opens a fresh group, matching CatalogConsumer's one-frame-per-group read. The catalog republish tests run against the real library and would catch a semantic change on future moq_net bumps. Noting the coupling here for reviewers; no change needed.

Comment thread ui/src/stores/streamStoreHelpers.ts
Comment thread ui/src/stores/streamStoreHelpers.ts
Signed-off-by: streamkit-devin <devin@streamkit.dev>
@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Rebase reminder: inherit #678's advertised-tag fetch path, then bump moq_relay_ref to moq-relay-v0.14.12. That relay release uses the same moq-native 0.19 / moq-net 0.2 / hang 0.20 stack as this migration; rerun the relay E2E job after resolving the expected workflow/justfile conflict.

Written by Devin

@staging-devin-ai-integration staging-devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

Devin Review (Staging)
Debug

Playground

Comment on lines +728 to +768
function createPublishEncoders(
broadcast: Publish.Broadcast,
capture: Publish.Video.Capture | null,
microphone: MicrophoneHandle | null,
audioEnabledInitially: boolean,
encoderConfig: Publish.Video.EncoderProps['config']
): {
audio: { enabled: Signal<boolean>; encoder: Publish.Audio.Encoder } | null;
video: Publish.Video.Encoder | null;
} {
let audio: { enabled: Signal<boolean>; encoder: Publish.Audio.Encoder } | null = null;
try {
if (microphone) {
const audioEnabled = new Signal(audioEnabledInitially);
audio = {
enabled: audioEnabled,
encoder: new Publish.Audio.Encoder(AUDIO_TRACK_NAME, {
broadcast,
enabled: audioEnabled,
source: microphone.source,
}),
};
}

const video = capture
? new Publish.Video.Encoder(VIDEO_TRACK_NAME, {
broadcast,
capture,
enabled: true,
config: encoderConfig,
})
: null;

return { audio, video };
} catch (e) {
audio?.encoder.close();
capture?.close();
broadcast.close();
throw e;
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔍 Publish teardown ordering across new handle types

The rewrite splits the old monolithic Publish.Broadcast into PublishHandle owning broadcast/capture/encoders, plus renderer/emitter handles wrapping signals. createPublishEncoders closes capture and broadcast when an encoder constructor throws, and setupPublishPath closes again on its outer catch. Worth confirming these overlapping failure paths never double-close or leak a resource.

Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Verified — the failure paths are mutually exclusive, so no double-close or leak:

  • If the Publish.Broadcast ctor throws, only capture exists; the inner guard closes it and createPublishEncoders never runs.
  • If an encoder ctor throws, createPublishEncoders's catch closes audio.encoder/capture/broadcast exactly once; publish is still undefined, so the outer catch's publish?.close() is a no-op.
  • Once PublishHandle is constructed (e.g. the catalog wait throws), the encoders' catch can no longer fire, and the outer catch closes everything once via publish.close().

The outer catch's shutdownMediaSource(microphone/camera/screen) targets the source handles, which are distinct objects from the encoder/capture wrappers closed above, so there's no overlap there either. Leaving the thread open in case you want a different ownership structure.

@streamer45
streamer45 merged commit 717444b into main Aug 23, 2026
30 checks passed
@streamer45
streamer45 deleted the devin/1787430379-moq-stack-migration branch August 23, 2026 13:37
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