Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21291,7 +21291,7 @@ void _checkApiChecksums() {
if (uniffi_moq_ffi_checksum_method_moqmediatrackproducer_cut() != 54122) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqmediatrackproducer_demand() != 16488) {
if (uniffi_moq_ffi_checksum_method_moqmediatrackproducer_demand() != 51616) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqmediatrackproducer_discontinuity() !=
Expand Down Expand Up @@ -21644,7 +21644,7 @@ void _checkApiChecksums() {
if (uniffi_moq_ffi_checksum_method_moqvideoproducer_finish() != 40208) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqvideoproducer_reservation() != 25410) {
if (uniffi_moq_ffi_checksum_method_moqvideoproducer_reservation() != 1052) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqvideoproducer_set_bitrate() != 19081) {
Expand Down
8 changes: 4 additions & 4 deletions doc/concept/hang.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ every video rendition (display size, rotation, flip) sit at the section root.

A track name always means the same content, so a catalog update never
redefines a track. The fields a decoder can't change without reinitializing
are fixed: codec, description, container, display aspect, sample rate, and
channel count. Everything else, such as the coded size, `bitrate`,
are fixed: codec, description, container, display aspect, interlacing, sample
rate, and channel count. Everything else, such as the coded size, `bitrate`,
`framerate`, and `enabled`, updates in place; the picture resizes in band at a
keyframe unless a `description` pins it. A publisher that needs a new identity
publishes a new track name, or restarts the broadcast under a new
[epoch](/concept/moq-lite#publisher-epochs). A player can configure its
decoder once per track.
[epoch](/concept/moq-lite#publisher-epochs); the Rust and `@moq/publish`
publishers take a new name. A player can configure its decoder once per track.

A few things the catalog can express beyond decoder config:

Expand Down
4 changes: 4 additions & 0 deletions doc/lib/rs/moq-mux.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ arrived. The catalog clock never moves once something uses it: an importer that
starts after a data track, a capture, another importer, or a pinned clock
shifts its timestamps onto that clock instead, landing its first frame at now.
A `catalog::Timebase` gives renditions on one timestamp base one shared shift. A
stream whose decoder identity changes, such as an SPS with a new profile or level
or an audio header with a new sample rate, continues under a new track name
(`<name>/<n>` for the codec importers) while the old track ends; everything else,
such as the coded size, updates the rendition in place. A
timeline that rewinds ends the import. fMP4 export fixes its track
set at the init segment, so a new rendition or a changed configuration ends the
export. The [CLI page](/bin/cli#import) covers these and the MPEG-TS specifics:
Expand Down
1 change: 0 additions & 1 deletion quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ removed).
- [SUBSCRIBE honors the properties opt-out](/quest/m1/subscribe-timestamps.md) - the Rust publisher sends untimed objects to a SUBSCRIBE that declines Track Properties, and the Rust subscriber asks for them when it needs timestamps
- [JS FETCH timeout test](/quest/m1/js-fetch-timeout-test.md) - a JS joining FETCH still waiting on its SUBSCRIBE at the deadline is refused with TIMEOUT
- [IETF fill timeout](/quest/m1/ietf-fill-timeout.md) - Rust honors native FILL_TIMEOUT to bound upstream waiting on drafts 18 and 22, alongside local content-age enforcement
- [Rust identity enforcement](/quest/m1/catalog-identity-rs.md) - `moq-mux` refuses a redefined rendition, and every Rust publisher mints a new rendition name for a decoder identity change and updates everything else in place
- [IETF takeover](/quest/m1/ietf-takeover.md) - carry the shared takeover contract through the negotiated IETF epoch extension
- [Identity and recovery guide](/quest/m1/identity-guide.md) - explain epochs, pinned catalogs, lazy jobs, restart and version compatibility as they exist; takeover adds its own section
- [Native rung disable](/quest/m1/simulcast-rung-disable.md) - OBS stops sending the top renditions its grant cannot fund, as the Rust/FFI publishers do, and a shaped uplink through moq-ffi proves the resume
Expand Down
1 change: 0 additions & 1 deletion quest/m1/archive/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,5 @@ from a host. Each child fixes the stale docs it touches.

## Related

- [Rust identity enforcement](/quest/m1/catalog-identity-rs.md) - a track's definition never changes for its name, so the archived catalog describes every group of the tracks it lists
- [e2ee](/quest/m2/e2ee/README.md) - protected broadcasts are excluded initially
- [DVR rewind](/quest/m2/archive-dvr.md) - seeks back through the cache, then an archive, with time ranges
82 changes: 0 additions & 82 deletions quest/m1/catalog-identity-rs.md

This file was deleted.

9 changes: 3 additions & 6 deletions quest/m2/publish-publication.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,8 @@ d0337260a6). The state is spread over five name-keyed maps in `Broadcast`
(`#kinds`, `#names`, `#replacements`, `#served`, `#listed`), plus a
per-Broadcast `#advertised` WeakMap and three failure layers in each
encoder. Replace them with one publication object per network broadcast,
mirroring Rust's identity rules from
[Rust identity enforcement](/quest/m1/catalog-identity-rs.md).
mirroring Rust's identity rules (`hang::catalog::VideoConfig::same_identity`
and `AudioConfig::same_identity`, enforced by `moq-mux` since
[moq-dev/moq#7110](https://github.com/moq-dev/moq/pull/7110)).

Public API: none. Wire: none.

## Required

- [Rust identity enforcement](/quest/m1/catalog-identity-rs.md) - settles the identity contract this mirrors
15 changes: 15 additions & 0 deletions rs/hang/src/catalog/audio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,21 @@ impl AudioConfig {
hls: None,
}
}

/// Whether `other` keeps this config's track identity: the fields a consumer cannot change
/// without reinitializing its decoder or demuxer (`codec`, `description`, `container`,
/// `sample_rate`, and `channel_count`).
///
/// A track name always means the same content, so a publisher that changes any of them
/// publishes under a new name; every other field may update in place. See the draft's
/// [Track Identity](https://github.com/moq-dev/moq/blob/main/drafts/draft-lcurley-moq-hang.md#identity).
pub fn same_identity(&self, other: &Self) -> bool {
self.codec == other.codec
&& self.description == other.description
&& self.container == other.container
&& self.sample_rate == other.sample_rate
&& self.channel_count == other.channel_count
}
}

#[cfg(test)]
Expand Down
16 changes: 16 additions & 0 deletions rs/hang/src/catalog/video/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,22 @@ impl VideoConfig {
hls: None,
}
}

/// Whether `other` keeps this config's track identity: the fields a consumer cannot change
/// without reinitializing its decoder or demuxer (`codec`, `description`, `container`,
/// `display_aspect_width`, `display_aspect_height`, and `interlaced`).
///
/// A track name always means the same content, so a publisher that changes any of them
/// publishes under a new name; every other field may update in place. See the draft's
/// [Track Identity](https://github.com/moq-dev/moq/blob/main/drafts/draft-lcurley-moq-hang.md#identity).
pub fn same_identity(&self, other: &Self) -> bool {
self.codec == other.codec
&& self.description == other.description
&& self.container == other.container
&& self.display_aspect_width == other.display_aspect_width
&& self.display_aspect_height == other.display_aspect_height
&& self.interlaced == other.interlaced
}
}

fn is_zero(value: &i32) -> bool {
Expand Down
34 changes: 34 additions & 0 deletions rs/moq-audio/src/encode/producer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,15 @@ impl Producer {
/// Retune the live encoder to `bitrate`.
pub fn set_bitrate(&mut self, bitrate: moq_net::bandwidth::Rate) -> Result<(), Error> {
let encoder = self.encoder.as_mut().ok_or(moq_net::Error::Closed)?;
let previous = encoder.bitrate();
encoder.set_bitrate(bitrate)?;
// Advertise the configured target in place, so encoded-byte estimates cannot replace it.
if let Err(err) = self.track.set_bitrate(encoder.bitrate().as_bps()) {
// Keep the encoder on what the catalog still advertises. It accepted that rate before,
// so the publish error is the one to report.
let _ = encoder.set_bitrate(previous);
return Err(err.into());
}
self.bitrate = encoder.bitrate();
Ok(())
}
Expand Down Expand Up @@ -1332,6 +1340,32 @@ mod tests {
assert_eq!(second.track_name(), "1.opus");
}

#[tokio::test(start_paused = true)]
async fn retuning_updates_the_published_bitrate_in_place() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
let options = Options {
track: Some("audio".into()),
..Options::default()
};
let mut producer = Producer::new(&mut broadcast, catalog.clone(), Input::default(), &options).unwrap();
let before = catalog.snapshot().audio.renditions["audio"].clone();
let bitrate = moq_net::bandwidth::Rate::from_bps(64_000);
producer.set_bitrate(bitrate).unwrap();
let after = catalog.snapshot();
assert_eq!(after.audio.renditions.len(), 1);
let mut expected = before;
expected.bitrate = Some(64_000);
assert_eq!(after.audio.renditions["audio"], expected);
assert_eq!(producer.track_name(), "audio");
assert_eq!(producer.bitrate(), bitrate);
// Encoded-byte detection must not replace the newly configured target.
for timestamp in [0, 20_000, 40_000] {
producer.write(&pcm_frame(&vec![0.1; 1920], timestamp)).unwrap();
}
assert_eq!(catalog.snapshot().audio.renditions["audio"].bitrate, Some(64_000));
}

/// The rendition a producer publishes is the encoder's own catalog, so neither can drift.
#[tokio::test]
async fn publishes_the_encoder_catalog() {
Expand Down
7 changes: 7 additions & 0 deletions rs/moq-c/MIGRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ How to update your code for each breaking change, newest first.

## Unreleased

### Authored configs keep their identity ([#7110](https://github.com/moq-dev/moq/pull/7110))

`moq_publish_video_config` and `moq_publish_audio_config` return `MOQ_ERROR_MUX` for a repeat call that changes the rendition's `codec`, `description`, or `container`, or an audio `sample_rate` or `channel_count`.
Publish media with a new identity under a new name, and remove the old rendition.
Tracks published with `moq_publish_video` or `moq_publish_audio` take a new name such as `<name>/1` when the stream's decoder identity changes, so read the name from the catalog rather than caching it.
A frame passed to `moq_encode_video_frame` that moves the rendition to a new track, such as a resume at a ceiling past the declared level, reserves bandwidth against the new track, so call `moq_encode_video_reservation` again rather than holding the first handle.

### Paths with an empty segment are refused ([#6648](https://github.com/moq-dev/moq/pull/6648))

Every function taking a path, such as `moq_origin_create_broadcast` and `moq_origin_request`, returns an error for a path with a leading, trailing, or doubled `/` instead of collapsing it. Pass the canonical path, such as `room/cam`.
Expand Down
14 changes: 9 additions & 5 deletions rs/moq-c/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2613,10 +2613,12 @@ pub unsafe extern "C" fn moq_publish_video_properties(broadcast: u32, properties
/// reading a rendition out of a catalog, it writes one into the catalog of a
/// broadcast created with [moq_origin_create_broadcast]. The rendition is keyed by
/// `config.name`; calling this again with the same name replaces the rendition
/// you declared, so a config can be refined in place. It fails only when a
/// you declared, so a config can be refined in place. It fails when a
/// [moq_publish_video] track owns the name, since that track publishes and
/// retires its own rendition. The updated catalog is published to subscribers
/// automatically.
/// retires its own rendition, and when the replacement changes the track's
/// identity (`codec`, `description`, or `container`), since a track name always
/// means the same content: publish the changed media under a new name instead.
/// The updated catalog is published to subscribers automatically.
///
/// The struct fields are read as inputs:
/// - `name` / `codec` are required (NOT NULL terminated) string slices.
Expand Down Expand Up @@ -2663,8 +2665,10 @@ pub unsafe extern "C" fn moq_publish_video_config(broadcast: u32, config: *const
///
/// This is the producer counterpart to [moq_consume_audio_config]. The rendition
/// is keyed by `config.name`, on the same terms as [moq_publish_video_config]:
/// a repeat call replaces your own rendition, and a name a [moq_publish_audio]
/// track owns is refused. The updated catalog is published to subscribers
/// a repeat call replaces your own rendition, a name a [moq_publish_audio]
/// track owns is refused, and so is a replacement that changes the track's
/// identity (`codec`, `description`, `container`, `sample_rate`, or
/// `channel_count`). The updated catalog is published to subscribers
/// automatically.
///
/// The struct fields are read as inputs:
Expand Down
15 changes: 13 additions & 2 deletions rs/moq-c/src/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -672,13 +672,19 @@ impl Publish {

/// Insert or replace a caller-authored video rendition in the broadcast's catalog.
///
/// Errors if a media importer owns the name, since it publishes and retires its own rendition.
/// The catalog is republished automatically.
/// Errors if a media importer owns the name, since it publishes and retires its own rendition,
/// or if the config changes the rendition's track identity, which takes a new name. The catalog
/// is republished automatically.
pub fn video_config(&mut self, broadcast: Id, name: &str, config: hang::catalog::VideoConfig) -> Result<(), Error> {
let broadcast = self.broadcast_mut(broadcast)?;
if !broadcast.video.contains_key(name) && broadcast.catalog.is_claimed::<hang::catalog::VideoConfig>(name) {
return Err(Error::Hang(hang::Error::Duplicate(name.to_string())));
}
if let Some(previous) = broadcast.video.get(name)
&& !previous.same_identity(&config)
{
return Err(moq_mux::Error::IdentityChanged(name.to_string()).into());
}
broadcast.video.insert(name.to_string(), config.clone());
let mut catalog = broadcast.catalog.modify()?;
catalog.video.renditions.insert(name.to_string(), config);
Expand All @@ -694,6 +700,11 @@ impl Publish {
if !broadcast.audio.contains_key(name) && broadcast.catalog.is_claimed::<hang::catalog::AudioConfig>(name) {
return Err(Error::Hang(hang::Error::Duplicate(name.to_string())));
}
if let Some(previous) = broadcast.audio.get(name)
&& !previous.same_identity(&config)
{
return Err(moq_mux::Error::IdentityChanged(name.to_string()).into());
}
broadcast.audio.insert(name.to_string(), config.clone());
let mut catalog = broadcast.catalog.modify()?;
catalog.audio.renditions.insert(name.to_string(), config);
Expand Down
Loading
Loading