Repository navigation
Conversation
…point `LiveBuilder::with_moq` panicked when the node ran on another endpoint than the builder's. The doc now says which endpoint to use. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The microphone publication and the MicrophoneConfig export were not gated on capture, so the crate failed to compile on its own without it. Workspace builds hid this through feature unification, but a standalone build of iroh-live-relay or iroh-live-media-android, which take the crate without default features, failed. The helpers only the device paths use are gated with them, so every single feature builds without warnings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Explanations of history and of the obvious go, and the rest says what a caller needs in fewer words. The README matches the current API. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comments that restated GL calls and the decorative banners go, the crate docs list the renderer, and the letterbox comments no longer swap pillarbox and letterbox. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Six status updates in the video task each reset the rendition, the decoder and the switch in their own way; PlayerStatus::clear_video does it once. Latency validates itself instead of going through a free function and a one-line PlayerConfig method. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The link monitor read QUIC path statistics for direct sessions and moq-tokio's session statistics for relay links, in two monitors with two reading types, and kept per-path history for a sliding one-second goodput, a sliding two-second loss rate, a two-bucket windowed round trip minimum, a count of distinct round trip readings, and counters nothing read. moq-net's `Session::stats` gives both kinds of link the same figures, the peer's delivery estimate included, so one monitor now reads them and one sampler keeps fifteen seconds of readings: the minimum over all of them, loss and goodput against the newest one two seconds old. `LinkSample` keeps what adaptation and the overlay read: round trip, its minimum, loss, goodput, delivery, path generation, and whether the path is relayed. The facade's signals count changes of the serving link and its path instead of offsetting generations. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
VideoSource::spawn and AudioSource::push assembled their inner state by hand, and AudioSource had a second constructor only to pass a demand counter in; both now take the source's own counter for the sender. The microphone config's feature check and its resolve step no longer carry the same error twice. The rpicam configs lose getters and a constructor that only restated their fields inside the module. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
iroh-moq carries MoQ for anything, and what the preset tunes (BBR3, so moq-net's send-rate estimate tracks the link) is about MoQ, not media. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Probing, track creation and producer setup each wrapped the error, reported it on the rendition and kept it as the last failure in their own copy of the same five lines. The raw and pre-encoded paths also kept their own copy of the frame, byte and rate counting, which is now EncodeStats::record. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Eight tests spelled out the same timeout-and-watch loop, two pushed frames with the same loop, and six turned off hardware encoding with the same struct update. Each is now one helper. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pacing was a struct around one rate meter with one method; it is a function taking the meter. The pacing future uses n0-future's BoxFuture. CatalogProducer only renamed moq-mux's catalog producer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A failed dial that a relay could stand in for awaited the relay route inside the select branch that saw the failure. The branches now only pick the outcome, and the one wait on the relay route follows them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sessions and relay links spawned their two origin drivers the same way, and now call `Origins::run`. `Subscription::session` and `Subscription::link` share one lookup of the serving link, `hop_for` and the relayed first hop share the masking in `hop_from`, and the transport's dial and accept wrap errors with one function. `AudienceKind` converts from `Audience` with `From`, the `PeerSet` alias, `Incoming::close` and `Moq::tasks` go, and a publication's closed task is no longer an `Option` that is always set. The origin tests use the harness in `tests/common` instead of a copy of it, and test origins come from moq-tokio's `origin::spawn_config`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comments that told the history of a bug, carried review ids, or said what the next line does are gone. The rest says what a caller needs in shorter sentences, and keeps the measured numbers and platform quirks behind the defaults. Four docs that no longer matched the code are corrected: AudioSource::file does not check the codec before it returns, the pattern generator's bands are cut in sixteenths, the playout clock wakes a wait when a frame becomes due sooner, and the crate docs no longer link capture-only items from ungated docs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The gossip announcement and the public `RoomPeer` carried the same two fields, with a hand-written conversion from one to the other. The announcement now wraps a `RoomPeer` next to its `left` flag, and the actor keeps `RoomPeer`s, so the published state is a clone. This changes the announcement's wire format, which has no earlier release to meet. The one-use `KvEntry` alias, whose doc described a type that no longer exists, is inlined. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`IrohSessions::alpns` rebuilt `iroh_moq::alpns()` by hand, the URL name extraction repeated `requested_name`, and a pull's identity-checked removal existed twice. `RelayServer`, a struct that held only the data directory, is one `secret_key` function, and the HTTP handlers take the certificates as their state directly. The pull module's ticket tests repeated `iroh-live`'s own and go. The relay no longer depends on `web-transport-iroh` or `http`, nor its tests on `rand`, and test origins come from moq-tokio's `origin::spawn`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every doc comment, inline comment and the README are cut to what a caller needs and worded plainly. The crate docs list the main types and keep cancellation safety as a compact table. Gone: history notes, notes on versioning of integration points, field docs that restate the field, and a node test comment about the bare-name alias cut earlier. Every doc summary is one line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The crate docs, constants and room methods say what a caller needs in plain words, and the test docs fit their summaries on one line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The crate docs, comments, test docs and README are shorter and plain. The `--bind` help no longer claims it binds iroh, which binds its own socket. Gone: a pull comment about the old path layout, a test doc about a Playwright failure, and box-drawing characters in test comments. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`Moq`, `MoqConfig`, `Session`, `Subscription`, `Incoming` and the relay's `PullState` had hand-written `Debug` impls; derive_more's attributes say the same in a line or two each. `Shared::hop`, which only the old `Moq` impl read, goes: the route table knows its hop. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Stage 2 of v2-redux (plans/v2-redux). Six conflicts, resolved as follows: - iroh-live/Cargo.toml and lib.rs docs: the rooms crate is iroh-live-rooms (transport side); the relay-links feature is gone (transport side) and so is test-util (media side). - iroh-live/src/network.rs: the transport side's simplified signal closure; it only fills public NetworkSample fields, so it fits the media side's type. - The LocalBroadcast doc in iroh-live-media, docs/architecture/publish.md and docs/guide/moq.md: the media side's wording, since the catalog metadata section is cut, with the rooms crate's new name. Verified: cargo make check-all, the full suite (321 passed, 4 skipped), and Android clippy with -D warnings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The CLI's `LocalPreview` was `iroh_live_egui::VideoView` with an optional stream: a `FrameView`, a repaint task per stream and `set_frames`. `VideoView::new` and `set_frames` take `impl Into<Option<VideoFrames>>`, so a view can wait without a stream, and the CLI's copy and its second `wake_on_frame` go. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The CLI's `record` and `watch` each carried the same `check_rendition`, and the media crate made the same check in `Recording::start` and the selector. `Catalog::video_rendition` serves all four, and `Error::UnknownRendition` now lists the names the catalog has, which is what the CLI printed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The CLI's recorder and the egui overlay each had a `format_bytes`, in different units. `bytesize` is already in the tree through moq-relay. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`LatencyArg::latency` returns the player's `Latency` instead of two durations the caller put back together, and its three constant tests become one. `OpenedVideo` derives `Debug`; `audio_encoding` loses a parameter it ignored; the Android bridge uses n0-future's `AbortOnDropHandle` instead of its own; the Pi's `print_stats` is gated on the feature that calls it instead of allowing dead code. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-video moq-video publishes the names its backends answer to as `encode::NAMES` and `decode::NAMES`. The CLI spelled them out again as two enums, with a test to catch the copies drifting. One `Backend` type now parses a strategy or one of upstream's names, so a new backend upstream is a choice here without a change. `DecoderArg::from_kind` had no caller. `WatchArgs` is boxed like the other commands' arguments, since a `Backend` is larger than the enum it replaces. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`irl call` and the Android demo each carried the same `Call`: the `call`
broadcast name, dialing a peer, and subscribing to its side over the
session. The two have to agree for a phone to call a desktop, so the
convention now lives in `iroh_live::{CALL, Call}` and both use it. A
test in the e2e suite covers the dial.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Five callers read `IROH_SECRET` by hand (the CLI, both demos, both examples), and the relay and `irl run` each loaded or created a key file. `EndpointOptions::from_env` and `secret_key_file` in iroh-moq replace them. `irl run` now stores its keys like the relay does, as 32 bytes in a file only its user can read, where it wrote hex with default permissions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three callers built a ticket by hand instead of calling `Live::ticket`. Three set a Pi camera's keyframe interval to its frame rate, which `RpicamConfig::new` already does. The balanced latency preset restated `Latency::default`. Waiting for the first catalog goes through n0-watcher's `initialized` instead of a hand-written loop, in the CLI and the Pi demo. `irl devices` and the scan screen share one check for `rpicam-vid` on `PATH`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`ScanView`, `FileImport` and `TicketQr` had hand-written `Debug` impls that printed less than a derive does, or skipped fields a derive can skip. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…track moq-net ends a track with `Error::Dropped` when its last producer goes away without a finish or an abort. That is a publisher leaving, most often one in the same process whose task stopped. The player took it for a broken track: it marked the video slot `Failed` and backed off the rendition, so a route that carried the same rendition name could not take over at once. `CleanClose` now counts `Dropped` with the cancel codes. The track ends like a clean end, and the player waits for the next route or catalog. `a_player_keeps_playing_when_its_route_goes` aborts the task that holds the producers, so this is the likely cause of its macOS flake. Over the wire a dropped producer arrives as an internal error, which stays a failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…path A URL carries the requested name percent-encoded. A name with a space or a non-ASCII character reached the ticket parser and the mount as `my%20cam`, so the relay pulled and mounted the wrong path and a browser viewer never got the broadcast. `percent-encoding` was already in the tree through `url`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The module docs said dropping `run`'s future stops everything it started. The tasks that dial and hold a pull are detached spawns and run until the pull retires. `resolve_again` also does not look at claims, so a pull nobody reads redials for up to about 90 seconds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without `--locked`, `cargo install` resolves the dependencies anew and ignores `Cargo.lock`. With moq pinned by git rev, that can build a tree nobody has tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A viewer must keep playing when the publisher switches its video source, both when an application feeds another device into one pushed source and when it calls `set_video` with a new source. The tests run in-process and across two iroh nodes, with a camera-like source whose timestamps start at zero and a test pattern on the process clock. Two of them fail today. A pushed source fed from the camera after the pattern fails the rendition with a timestamp rewind. A `set_video` replacement shows the new source only after about 2.2 seconds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An application that pushes another device into the same source breaks its timeline: a camera stamps from zero after a test pattern on the process clock. The publisher anchored the timeline once, so the camera frames went back and moq-mux refused them as a rewind. That failed the rendition, and nothing brought it back. `Timeline` now anchors a timeline that goes back, or jumps far past the broadcast clock, at the clock again, and the encoder cuts a group there. Audio uses the same mapping. A `set_video` replacement finished its tracks and created new ones of the same names. The player saw the old track end after the catalog changed and waited two seconds before it asked again. The replaced task now hands each rendition's track to its successor, so a viewer keeps its subscription and sees the new source from its first keyframe. A size change that needs another decoder still moves the rendition to a new track name, as moq-mux does, so the encoder follows the demand of the renamed track. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frando
marked this pull request as ready for review
November 4, 2026 22:38
…source frame The broadcast moves every source timestamp onto its own clock, so a subscriber that reports which frame it worked on cannot name the frame the publisher knows. An application that matched results to frames had to paint a frame id into the pixels to get around this. The video timeline now records the source timestamp of the last 1024 frames it published, and `LocalBroadcast::video_source_timestamp` returns it for a published timestamp. A subscriber echoes the decoded frame's timestamp, and the publisher looks up its own frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A player plays audio to a device or decodes it into nothing, so an application that analyses a broadcast's audio had to open its own moq-audio decoder and follow the catalog itself. `Player::audio_frames` returns an `AudioFrames` stream of the frames the audio task decodes: interleaved f32 samples with their timestamp and format. It runs beside the `AudioTarget`, so `AudioTarget::null` decodes without playing. The stream follows reopened tracks and new routes, and a reader more than a second behind skips the oldest frames instead of holding the decoder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt or per dial iroh-live did not name its congestion controllers, so an application that compared them depended on noq-proto and set the transport config after `EndpointOptions::apply` installed BBR3. Switching controllers also meant binding a new endpoint. `Congestion` names BBR3, CUBIC and NewReno and builds the transport config for one. `EndpointOptions::congestion` sets it for the whole endpoint, BBR3 by default as before. iroh 1.2 takes a transport config per connection, so `ConnectOptions::congestion` dials one session with another controller through the new `dial_with`, and switching is a close and a new dial. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An application that publishes a control value or a stream of results had to write those tracks by hand: one frame per group, a walk over groups and frames to read them, and the value repeated every second so a late reader converged. moq-json's snapshot track already does this. Every update supersedes the last, `Consumer::latest` skips to the newest value, and the track keeps its newest group, so a late reader gets the current value at once. iroh-live now re-exports it as `iroh_live::json`, with an example of a value written on one node and read on another. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A view takes frames from its stream and only hands back an image, so an application that labels the picture with its timestamp or size, or matches it to a result, had to keep a second subscription to the same frames and guess which one was on screen. `VideoView::last_frame` returns the frame `render` last took. It stays across `set_frames`, as the picture on screen does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The decoder reads the track and decodes it in one call, so the time between a frame landing and its picture leaving the decoder was all one number. An application that measures how long a picture waited could not tell the network from the decoder. moq-net stamps each frame when it lands, but the container layer drops the stamp before the decoder sees it. Each decoder now gets a watcher on the same track, with the same subscription so moq-net serves both from one upstream request. It decodes only the container and keeps the arrival of the last 256 frames by timestamp. `FrameTiming` gains `arrived`, set for pictures, and `Player::video_timing(pts)` returns the timing of one presented picture, so a frame from `Player::video` can be looked up by its timestamp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er the player drops A handle keeps up to a second of decoded frames. After the player drops it still returns those, then ends; the test expected it to end at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to moq-ext-access Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Grants now have a base per link and ticket paths per peer. A claim widens
every session with the peer and narrows again when the last claim drops.
Also adds Node::{id, grant_for, closed}, NodeConfig::owner, and richer
NotGranted, NotOwner and TooManyGrants errors.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…dd LiveLayout BroadcastTicket moves into iroh-live-node as ilv1:<id>/<path> with the peer's own segment written as ~. Node::subscribe_ticket claims the path for its publisher while the subscription lives, so a default-config viewer works. The facade gains layout::LiveLayout and media::remote_broadcast, and the old free functions delegate to them. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…path The pull node starts from a base grant of nothing and each pull holds a claim_path guard, checked by an owner function: only the canonical live and rooms paths of the ticket's peer are pulled. Tickets are read from the raw URL path before percent decoding. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The CLI call window now runs on iroh_live::calls, and the room command mounts rooms next to the node on its own router. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e config functions Callers hold a Node and name a LiveLayout. The Android demo and the CLI place calls through iroh_live::calls, tickets use the ilv1 scheme, and the relay's RelayConfig is now iroh_live_relay::Config. The pi-zero e-paper QR uses error correction level L so an ilv1 ticket keeps its 33 modules. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Hops are plain ids and publishing goes through origin::Route, announce events are a Present/Absent listing, follow reports a path going offline, encoders take a join time instead of a GOP, and moq-net, moq-json and moq-room enable their async features. A live session now applies a widened ticket grant a moment after the claim, which the claim_path docs and the grants test say. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Documents undocumented items with one-line summaries, renames vague tests, moves function-local imports to the top, uses LinkId for link ids inside iroh-live-node, tightens READMEs and DEVELOPMENT.md, and drops stale statements. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- iroh-live-node: hop ids are a crate-private Hop type instead of raw u64s. - pi-zero: every unsafe block has a SAFETY comment, the GL helpers are unsafe fns with a documented contract, and dead helpers are gone. Each texture keeps its own allocated size: the U and V planes shared one, so the first V upload hit a texture that was never allocated, and so did a switch between RGBA and I420. The fb-demo renders on its own thread, as run_drm does, since its EGL context is current on one thread only. The console ioctls are named constants. - moq-ext-sources: LocalTask::released exists only with the capture feature, its only user. - calls: ring changes the audience under the state lock, as a hang-up does, so the two cannot lose each other's update. The docs say a hung-up peer rings again only after it withdraws and offers its side again. - A line-by-line review of the facade, rooms, relay, player, media-core, adaptive, publisher, sources and egui crates: stale docs after the moq update (GOP, V4L2 timeouts, announcements), redundant comments, shared helpers in tests and in moq-ext-adaptive, and #[must_use] where dropping a value is a bug. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR ports iroh-live to moq main and gives it a new public API in every library crate. The node now takes any path, and the
live/layout, calls and rooms sit on top of it as optional parts. Generic media and access code moves intomoq-ext-*crates without an iroh dependency, so it can potentially be upstreamed. #75 was closed in favor of this PR, so its redesign is part of this diff.Why
moq main now does much of what iroh-live built by hand. Its origin picks routes and fails over between them. moq-mux has rendition helpers and encoder rate control, moq-audio has an adaptive jitter buffer, and moq-room is a room layer over announcements. Main also dropped moq-tokio's iroh backend (moq-dev/moq#7345), so iroh-live is now the only place where moq runs over iroh.
That splits the work in two. What is specific to iroh stays here: the transport, the node with its route table and per-peer access, tickets, gossip rooms and the relay. Everything generic moves into
moq-ext-*crates that know nothing about iroh.On
main, the API was built around one convention.Livedecided the path layout, the grants, the endpoint and the router, and calls and rooms were methods on it. An application with its own path scheme, its own access rules or its own router had to work around the facade. This PR turns that around:iroh_live::Nodepublishes and subscribes at any path. Nothing in the node mentionslive/.layout::LiveLayoutis the<root>/<peer>/<name>scheme, and every use of it names it. An application with another scheme calls the node with its own paths.Calls::new(&node, &layout)andRooms::new(&node)stand side by side. Neither knows the other, and the node knows neither.NodeConfig::default()lets peers read and publish nothing. Subscribing to a ticket grants exactly that ticket's path, to exactly its publisher, for as long as the subscription lives.moq main is not released, so the workspace depends on it by git rev (
moq-dev/moq@5ac2db31d, 2026-11-08). The README says so up front and gives the git dependency line, since iroh-live is not on crates.io either.How it fits together
mainiroh-livelayout::LiveLayout,calls,EndpointOptions,media::remote_broadcast, and re-exports of the restiroh-live-nodeiroh-moqNode: one route table fed by direct sessions and relay links, publications, grants and ticket claims, admission,BroadcastTicketmoq-transport-irohiroh-moqMoqPreset, link statisticsiroh-live-roomsiroh-roomsiroh-live-relayiroh-live-cliirlbinarymoq-ext-accessGrant,SessionRequest,Audience<P>,visible_pathsand the token wire convention, generic over the peer idmoq-ext-adaptivemoq-mediamoq-ext-playermoq-mediaRemoteBroadcastandPlayer: rendition switches, adaptation, A/V sync, stats, recordingmoq-ext-publishermoq-mediaLocalBroadcast: one source encoded into a rendition laddermoq-ext-sourcesmoq-mediamoq-ext-media-coremoq-ext-eguimoq-media-eguiThe Android EGL renderer and JNI helpers moved into
demos/android/rust, their only user. The thin wrappers inmoq-mediaare gone: the crates usemoq_audio::playback::Engine,moq_audio::encode::Control, hang'sCatalog,moq_net::bandwidth::Rateandmoq_video::FrameRatedirectly.Using it
Publish and play
On
main,Live::publishreturned a broadcast with one video and one audio slot, and a subscriber got one decoder per track:Now the application binds the endpoint, owns the router, and names the layout it publishes under:
The broadcast is created in the node's route table, and peers read it without a copy.
layout.publish_with(&node, name, audience, fill)runsfillbefore the announcement, so a reader never finds the broadcast without its catalog, and returns thePublicationwith whatfillbuilt.On
main, the adaptation state machine, the playout clock andreopen_decoderwere public, and the caller had to coordinate them. NowPlayerConfigsets each behaviour with one value:RenditionMode(auto, pinned or off),Latency, the decoder kind and an optionalAudioTarget.Player::status()reports the rendition on screen, a pending switch and errors. A switch opens a second decoder that takes over once it catches up, so the picture does not go blank.An application with a path scheme of its own skips the layout and gives the node a grant:
iroh-live/examples/moq_only.rsdoes exactly this.Tickets
A
BroadcastTicketis a publisher's endpoint id and the full path of one of its broadcasts. It says nothing about layouts, so it works with any scheme.Publication::ticket()andLiveLayout::ticket(peer, name)build one.The string form is
ilv1:<peer>/<path>. The peer is the endpoint id in lowercase base32. A path segment equal to the peer's own id is written~, and other bytes outside[A-Za-z0-9._-]are percent-encoded:That is about 72 characters, short enough for a QR code on a small screen. A ticket holds no socket addresses: the subscriber finds the publisher by its id over pkarr, DNS and mDNS.
Ticket grants
Node::subscribe_ticketworks on a node with the default config, which lets no peer publish anything. It does that by claiming the ticket's path for the ticket's publisher:Node::claim_path(peer, path). The claim widens what that peer may publish into this node by that one path, on every session with the peer, including ones that start later.subscribe(path, Reach::Both(peer)).A claim is only allowed if the peer owns the path. By default that means exactly one segment of the path is the peer's endpoint id in its canonical spelling, and no other segment parses as an endpoint id. So a claimed path names its publisher and no one else, and it can never overlap another peer's paths.
NodeConfig::ownerreplaces the rule for a node that knows better, as the relay does. Claims are capped at 64 paths and 16 KiB per peer.Each session's grant is now two layers: a base set by
NodeConfig::grant, manual admission,ConnectOptions::grantorSession::set_grant, and the peer's claimed paths on top.Session::grant()returns the effective grant,Session::base_grant()the base. Aset_grantcannot remove a claimed path, and a claim cannot undo a narrowing of any other path.Calls
On
main, each side published atcalls/<own id>, which every session of the node could see, andCall::closeclosed the whole session.iroh_live::callsis now a service over a layout's call path:Ringing a peer adds it to the audience of this node's side, so the side appears in that peer's route table, and that is the ring. Answering rings back and subscribes. Two peers that call each other at once end up in one call. Dropping a
Callhangs up: the peer loses the side, and the session stays.calls.ringing()is the level-triggered set of peers that ring now.events()reports the edges, best-effort, and replays the current ringing set first. One side serves every call in turn, so a camera and a preview keep running between calls. The wire protocol is the oneirl calland the Android demo already speak.Rooms
On
main, a room subscribed to every broadcast of every member and sent events, chat included. Now the membership is a watchedRoomState, and the application subscribes to the broadcasts it wants:Room broadcasts live at
rooms/<topic>/<member>/<name>, and only members see them. Chat is not part of the library:irl roomsends it onmoq-room's chat track, which@moq/roomalso reads.The node: routing and access control
On
main,iroh-moqhad no shared route table: each session had its own origin, and a broadcast came only from the session with its publisher. That prevented forgery, but a broadcast could not come through a relay. The node now follows moq main's model and keeps no routing logic of its own:Link::Peerwith the peer's hop. Relay links announce asLink::Upstreamwith a higher cost. moq moves a subscription between a direct and a relay route of one stream without ending it, andRemoteBroadcastfollows a publisher that restarts as a new stream.Unauthorized.LiveLayout::configlets each peer publish only under<root>/<its id>/, so no peer can publish in the name of another, and pinned subscribes can tell peers apart by the routes' hops.Admission::Manualhands incoming sessions toNode::accept, for example after a token check.attach_relaytakes aniroh://orhttps://URL and redials with backoff.Everyonepublications go to the relay,Peerspublications never do. A relay is trusted with every path it forwards.The relay
iroh-live-relayserves browsers through moq-relay and pulls broadcasts from iroh publishers into its cluster without copying them. Its pull node uses the cluster origin as its route table (NodeConfig::table) and a base grant of nothing. Each pull holds aclaim_pathguard for the ticket's path, so the publisher's session announces exactly that path into the cluster and nothing else. The pull node'sNodeConfig::owneraccepts only a peer's canonicallive/<peer>/<name>orrooms/<topic>/<peer>/<name>path, so a ticket that names another peer's path is refused before anything is dialed. A browser reads the pull under the ticket string it asked for, which the relay parses from the raw URL path before percent-decoding.Transport
moq main owns the transport interface (
moq_net::transport::poll), and moq-tokio implements it on its own QUIC stack.moq-transport-irohdoes the same over iroh'sConnectionand streams, with no web-transport crates. Priorities, datagrams, close and reset codes, metrics and wire counters are backed.send_group,probeandreceive_windowreturnNone, since iroh has nothing to back them with. A send stream dropped beforefinishis reset, so the peer never takes a truncated stream for a complete one.WebTransport over HTTP/3 on iroh is gone. Its only job was carrying the relay token in the URL. The token now travels in the MoQ SETUP, and for moq-lite-06, whose SETUP has no token field, also as
?jwt=in the setup path;moq_ext_access::tokenwrites both andSessionRequest::token()reads them. A dial offers only the newest moq ALPN, because iroh's router sorts its ALPNs and TLS would otherwise settle on the oldest version.Breaking changes
main's wire formats, room format or path layout, nor with peers that dial WebTransport over HTTP/3 on iroh. Broadcasts move from bare names tolive/<id>/<name>, and calls fromcalls/<id>tolive/<id>/call.BroadcastTicket { peer, path }in the node, writtenilv1:....LiveTicket, theiroh-live:form and the olderEndpointAddrform no longer parse, so printed QR codes need re-issuing.Live,LiveBuilder,CallOffer,Call, the facade's free config functions,Subscription::mediaand the per-trackVideoTrack(usePlayer),MoqSessionandIncomingSessionStream(useSessionandNode::accept),RoomEventand room chat, the Android Camera2 bridge (useVideoSource::push), and themoq-ext-media-androidcrate.MoqtoNode,Live::remote_broadcasttomedia::remote_broadcast, and the relay'sRelayConfigtoiroh_live_relay::Config. The node'sRelayConfigforattach_relaykeeps its name.Router::shutdownshuts down the node and every service mounted on the router, then closes the endpoint.SessionRequestlost its HTTP/3 headers;SessionRequest::token()returns the SETUP token.Default;#[non_exhaustive]and the builders it forced are gone.iroh-livehas no default features (mainhadcapture,renderandsound-server).fullturns onmedia,capture,render,sound-serverandrecord.media,rooms,fileandrecordare new.irl record --latencyis now--max-age.LICENSE-MITheld the Apache text since the first commit; it now holds the MIT license.Known limits
claim_pathon an existing session can still be refused.subscribe_ticketon a fresh session is not affected.ringing()stays correct.Session::subscribeattributes a route correctly only while grants do not overlap. The layout's grants and the claim ownership rule make sure of that.Faileduntil the nextset_audio.cargo ndk -t arm64-v8a check -p iroh-live-android.Testing
CI runs
cargo checkwith default, all and no default features, plus the relay alone and the facade withoutmedia. It runs clippy with-D warnings, nextest andcargo fmt --checkon Linux and macOS, the Playwright suite (browser to iroh, iroh to browser through a relay link, pull mode), and release builds for Android, Windows and aarch64.At the head of this branch, every crate's test suite passes locally with all features, including the patchbay tests for adaptation on impaired links (
cargo make test-patchbay). New tests cover ticket grants and claims (forged paths, narrowing on drop,set_grantprecedence, limits, concurrent claims), the ticket format, the relay's pull ownership, and calls, including interop with a peer that speaks the raw path protocol as today'sirl calldoes.Suggested reading order
moq-ext-access: the grant, audience and token types.moq-transport-iroh/src/transport.rsandstream.rs: the poll transport on iroh.iroh-live-node:node.rs,publish.rs,state.rs(authorization and the two grant layers),claim.rs,ticket.rs,session.rs,route.rs, thenrelay.rs.iroh-live/src/layout.rsandcalls.rs.iroh-live-relay/src/pull.rs.iroh-live-rooms/src/room.rs.moq-ext-adaptive, thenmoq-ext-playerandmoq-ext-publisher.You can skim
iroh-live-cli, the demos anddocs/. Using iroh-live has complete programs for every case above.🤖 Generated with Claude Code