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
30 changes: 15 additions & 15 deletions cpp/obs/src/moq-source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ static void on_video_frame(void *user_data, int32_t frame_id)
if (ctx->shutting_down.load() || ctx->consume < 0) {
// Shutting down or disconnected: drop the frame.
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
moq_consume_frame_free(frame_id);
return;
}
pthread_mutex_unlock(&ctx->mutex);
Expand Down Expand Up @@ -1020,7 +1020,7 @@ static void moq_source_decode_frame(struct moq_source *ctx, int32_t frame_id)
{
// Fast path: check atomic flag before taking lock
if (ctx->shutting_down.load()) {
moq_consume_frame_close(frame_id);
moq_consume_frame_free(frame_id);
return;
}

Expand All @@ -1029,15 +1029,15 @@ static void moq_source_decode_frame(struct moq_source *ctx, int32_t frame_id)
// Double-check after acquiring lock (may have changed)
if (ctx->shutting_down.load()) {
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
moq_consume_frame_free(frame_id);
return;
}

// Check if decoder is still valid (may have been destroyed during reconnect)
// Note: sws_ctx and frame_buffer may be NULL on first frame - they're created dynamically
if (!ctx->codec_ctx) {
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
moq_consume_frame_free(frame_id);
return;
}

Expand All @@ -1046,7 +1046,7 @@ static void moq_source_decode_frame(struct moq_source *ctx, int32_t frame_id)
if (moq_consume_frame(frame_id, &frame_data) < 0) {
LOG_ERROR("Failed to get frame data");
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
moq_consume_frame_free(frame_id);
return;
}

Expand All @@ -1058,7 +1058,7 @@ static void moq_source_decode_frame(struct moq_source *ctx, int32_t frame_id)
ctx->frames_waiting_for_keyframe);
}
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
moq_consume_frame_free(frame_id);
return;
}

Expand All @@ -1079,7 +1079,7 @@ static void moq_source_decode_frame(struct moq_source *ctx, int32_t frame_id)
AVPacket *packet = av_packet_alloc();
if (!packet) {
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
moq_consume_frame_free(frame_id);
return;
}

Expand Down Expand Up @@ -1110,15 +1110,15 @@ static void moq_source_decode_frame(struct moq_source *ctx, int32_t frame_id)
}
}
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
moq_consume_frame_free(frame_id);
return;
}

// Receive decoded frames
AVFrame *frame = av_frame_alloc();
if (!frame) {
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
moq_consume_frame_free(frame_id);
return;
}

Expand All @@ -1143,7 +1143,7 @@ static void moq_source_decode_frame(struct moq_source *ctx, int32_t frame_id)
}
av_frame_free(&frame);
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
moq_consume_frame_free(frame_id);
return;
}

Expand Down Expand Up @@ -1173,7 +1173,7 @@ static void moq_source_decode_frame(struct moq_source *ctx, int32_t frame_id)
LOG_ERROR("Invalid decoded frame dimensions: %dx%d", frame->width, frame->height);
av_frame_free(&frame);
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
moq_consume_frame_free(frame_id);
return;
}

Expand All @@ -1182,7 +1182,7 @@ static void moq_source_decode_frame(struct moq_source *ctx, int32_t frame_id)
LOG_ERROR("Invalid decoded frame pixel format: %d", decoded_pix_fmt);
av_frame_free(&frame);
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
moq_consume_frame_free(frame_id);
return;
}

Expand All @@ -1203,7 +1203,7 @@ static void moq_source_decode_frame(struct moq_source *ctx, int32_t frame_id)
: "unknown");
av_frame_free(&frame);
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
moq_consume_frame_free(frame_id);
return;
}

Expand All @@ -1216,7 +1216,7 @@ static void moq_source_decode_frame(struct moq_source *ctx, int32_t frame_id)
sws_freeContext(new_sws_ctx);
av_frame_free(&frame);
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
moq_consume_frame_free(frame_id);
return;
}

Expand Down Expand Up @@ -1252,7 +1252,7 @@ static void moq_source_decode_frame(struct moq_source *ctx, int32_t frame_id)

av_frame_free(&frame);
pthread_mutex_unlock(&ctx->mutex);
moq_consume_frame_close(frame_id);
moq_consume_frame_free(frame_id);
}

// Registration function
Expand Down
2 changes: 1 addition & 1 deletion doc/concept/layer/hang.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ The catalog is a JSON document published through the merge-patch snapshot helper
A base consumer ignores them; an extension reads its own section and treats its absence as "not present".
In TypeScript, build an extended schema with `z.extend(Catalog.RootSchema, { scte35: ... })`.
In Rust, either flatten the catalog into your own struct with `#[serde(flatten)]` for typed access, or read sections untyped from an `Extra` catalog, which keeps unknown keys as raw JSON (`catalog.section("scte35")`). The `()` default drops sections it doesn't model.
The FFI bindings always use the untyped form, one JSON string per section keyed by name (`catalog.sections["scte35"]` in Python, `moq_catalog_get_section()` / `moq_catalog_section_at()` in C).
The FFI bindings always use the untyped form, one JSON string per section keyed by name (`catalog.sections["scte35"]` in Python, `moq_consume_catalog_section()` / `moq_consume_catalog_section_at()` in C).
- **Writing**: the catalog producer holds one shared document.
Each owner edits only its own keys and publishes: `producer.mutate(c => { c.scte35 = ... })` in TypeScript; the `Deref`/`DerefMut` lock guard from `producer.lock()` for a typed Rust extension, or `producer.set_section("scte35", value)` for an untyped one; `broadcast.set_catalog_section("scte35", value)` in Python; `moq_publish_catalog_section()` in C.
Every edit starts from the latest value, so the base media sections and any extension sections compose instead of clobbering one another.
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/c/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ moq_publish_json_snapshot_update(json, value, strlen(value));

// Subscribe: on_value fires with a value ID for each update; read it, then release it.
int32_t task = moq_consume_json_snapshot(consume, "status", strlen("status"), &config, on_value, user_data);
// In on_value: struct moq_json_value v; moq_consume_json_value(id, &v); ... moq_consume_json_value_close(id);
// In on_value: struct moq_json_value v; moq_consume_json_value(id, &v); ... moq_consume_json_value_free(id);
```

`compression` must match on the producer and subscriber. The consumer callback follows the same lifetime contract as every other (see above): release `user_data` on the terminal `<= 0` call.
Expand Down
18 changes: 16 additions & 2 deletions rs/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,25 @@ Follow the root `poll_*` conventions: collapse `Poll::Pending => Poll::Pending`

## Version matching

`moq_net::Version` is `#[non_exhaustive]`, splitting `Lite(lite::Version)` and `Ietf(ietf::Version)` (`version.rs`). When matching on a `Version` (or the inner draft enums), default to the **newest** draft so future versions fall forward; list older versions explicitly:
`moq_net::Version` is `#[non_exhaustive]`, splitting `Lite(lite::Version)` and `Ietf(ietf::Version)` (`version.rs`). The inner `lite::Version` / `ietf::Version` payloads are crate-private, so outside `moq-net` you branch on the accessors rather than on variants: `is_lite()` / `is_ietf()` for the protocol family, and `alpn()` / `code()` for the specific draft.

```rust
// Outside the crate: family first, then the ALPN string for a specific draft.
if version.is_lite() {
// moq-lite behavior
} else {
match version.alpn() {
"moqt-15" | "moqt-16" => { /* old behavior */ }
_ => { /* newest / draft-17+ behavior */ }
}
}
```

Inside `moq-net`, match the inner draft enums directly. Either way, default to the **newest** draft so future versions fall forward, and list older versions explicitly:

```rust
match version {
Version::Draft14 | Version::Draft15 | Version::Draft16 => { /* old behavior */ }
ietf::Version::Draft14 | ietf::Version::Draft15 | ietf::Version::Draft16 => { /* old behavior */ }
_ => { /* newest / draft-17+ behavior */ }
}
```
Expand Down
19 changes: 19 additions & 0 deletions rs/hang/src/catalog/video/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,12 @@ pub struct VideoConfig {
///
/// This allows you to stretch/shrink pixels of the video.
/// If not provided, the display aspect ratio is 1:1
///
/// The `displayRatio*` aliases decode catalogs from publishers predating the
/// rename to `displayAspect*`; the current name is what we emit.
#[serde(alias = "displayRatioWidth")]
pub display_aspect_width: Option<u32>,
#[serde(alias = "displayRatioHeight")]
pub display_aspect_height: Option<u32>,

// TODO color space
Expand Down Expand Up @@ -222,4 +227,18 @@ mod test {
assert!(encoded.get("displayRatioWidth").is_none());
assert!(encoded.get("displayRatioHeight").is_none());
}

#[test]
fn decodes_legacy_display_ratio_keys() {
// A catalog serialized by a pre-0.20 publisher used displayRatio*; the
// alias keeps the aspect ratio from being silently dropped.
let json = serde_json::json!({
"codec": "avc1.640028",
"displayRatioWidth": 16,
"displayRatioHeight": 9,
});
let config: VideoConfig = serde_json::from_value(json).expect("failed to decode legacy keys");
assert_eq!(config.display_aspect_width, Some(16));
assert_eq!(config.display_aspect_height, Some(9));
}
}
1 change: 0 additions & 1 deletion rs/kio/src/consumer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ impl<T> Consumer<T> {
pub async fn wait<F, R>(&self, mut f: F) -> Result<R, Closed>
where
F: FnMut(&Ref<'_, T>) -> Poll<R> + Unpin,
R: Unpin,
{
// The `Ref` is dropped here inside the closure, releasing the lock before the
// caller ever sees the result.
Expand Down
22 changes: 16 additions & 6 deletions rs/kio/src/waiter.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use std::{
cell::OnceCell,
fmt,
future::Future,
marker::PhantomData,
pin::Pin,
sync::{Arc, Weak},
sync::{Arc, OnceLock, Weak},
task::{Context, Poll, Waker},
};

Expand All @@ -27,15 +26,15 @@ pub struct Waiter {
// The shared handle downgraded into every list this waiter registers with. Created on the
// first `register` (a poll that never parks never allocates it), then reused so multiple
// lists in one poll share a single allocation whose `Weak`s die together when the waiter drops.
shared: OnceCell<Arc<Waker>>,
shared: OnceLock<Arc<Waker>>,
}

impl Waiter {
/// Create a new waiter from an async [`Waker`].
pub fn new(waker: Waker) -> Self {
Self {
waker,
shared: OnceCell::new(),
shared: OnceLock::new(),
}
}

Expand Down Expand Up @@ -102,8 +101,8 @@ impl WaiterList {
if self.entries[self.cursor].strong_count() == 0 {
// Reuse the dead slot in place. Each Waiter owns a
// unique Arc<Waker>, so strong_count == 0 uniquely
// identifies a slot whose owner has been dropped —
// no will_wake / pointer comparison needed.
// identifies a slot whose owner has been dropped.
// No will_wake / pointer comparison needed.
self.entries[self.cursor] = new_weak;
return;
}
Expand Down Expand Up @@ -203,6 +202,17 @@ mod tests {
assert_eq!(waiter.poll_future(boxed.as_mut()), Poll::Ready(9));
}

// `Waiter` is shared behind `&self` across threads, so the lazily allocated
// `shared` handle must use a thread-safe cell. A `!Sync` waiter silently
// infects `Pending` and `Shared`, and through them every moq-net consumer.
const fn assert_sync<T: Sync>() {}

const _: () = {
assert_sync::<Waiter>();
assert_sync::<crate::Pending<crate::Consumer<u32>>>();
assert_sync::<crate::Shared<u32>>();
};

#[test]
fn wait_output_need_not_be_unpin() {
struct NotUnpin(#[allow(dead_code)] std::marker::PhantomPinned);
Expand Down
4 changes: 2 additions & 2 deletions rs/libmoq/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@ int32_t moq_consume_audio_close(uint32_t track);

// Consuming: Frames
int32_t moq_consume_frame(uint32_t frame, moq_frame *dst);
int32_t moq_consume_frame_close(uint32_t frame);
int32_t moq_consume_frame_free(uint32_t frame);
int32_t moq_consume_track(uint32_t broadcast, const char *name, uintptr_t name_len, void (*on_frame)(void *user_data, int32_t frame), void *user_data);
int32_t moq_consume_track_frame(uint32_t frame, moq_frame *dst);
int32_t moq_consume_track_frame_close(uint32_t frame);
int32_t moq_consume_track_frame_free(uint32_t frame);
int32_t moq_consume_track_close(uint32_t track);
```

Expand Down
Loading
Loading