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
10 changes: 6 additions & 4 deletions dart/moq/lib/src/durations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ extension TrackInfoDuration on MoqTrackInfo {

/// Duration view over a raw frame's presentation time.
extension FrameDuration on MoqFrame {
/// Presentation timestamp.
Duration get timestamp => Duration(microseconds: timestampUs);
/// Presentation timestamp, or null for an untimed frame.
Duration? get timestamp =>
timestampUs == null ? null : Duration(microseconds: timestampUs!);
}

/// Duration view over a media frame's presentation time.
Expand All @@ -50,6 +51,7 @@ extension MediaFrameDuration on MoqMediaFrame {

/// Duration view over a datagram's presentation time.
extension DatagramDuration on MoqDatagram {
/// Presentation timestamp.
Duration get timestamp => Duration(microseconds: timestampUs);
/// Presentation timestamp, or null for an untimed datagram.
Duration? get timestamp =>
timestampUs == null ? null : Duration(microseconds: timestampUs!);
}
2 changes: 1 addition & 1 deletion dart/moq/test/moq_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ void main() {

final producer = track.appendGroup();
producer.writeFrame(
frame: MoqFrame(payload: utf8.encode('dart round trip')),
frame: MoqFrame(payload: utf8.encode('dart round trip'), timestampUs: 0),
);
producer.finish();

Expand Down
36 changes: 19 additions & 17 deletions dart/moq_ffi/lib/src/moq.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import "dart:ffi";
import "dart:io" show Platform, File, Directory;
import "dart:isolate";
import "dart:typed_data";

import "package:ffi/ffi.dart";

import "uniffi_runtime.dart";
export "uniffi_runtime.dart";

Expand Down Expand Up @@ -825,9 +823,13 @@ class FfiConverterMoqContainerInit {

class MoqDatagram {
final int sequence;
final int timestampUs;
final int? timestampUs;
final Uint8List payload;
MoqDatagram({this.sequence = 0, this.timestampUs = 0, required this.payload});
MoqDatagram({
this.sequence = 0,
this.timestampUs = null,
required this.payload,
});
}

class FfiConverterMoqDatagram {
Expand All @@ -842,7 +844,7 @@ class FfiConverterMoqDatagram {
);
final sequence = sequence_lifted.value;
new_offset += sequence_lifted.bytesRead;
final timestampUs_lifted = FfiConverterUInt64.read(
final timestampUs_lifted = FfiConverterOptionalUInt64.read(
Uint8List.view(buf.buffer, new_offset),
);
final timestampUs = timestampUs_lifted.value;
Expand All @@ -865,7 +867,7 @@ class FfiConverterMoqDatagram {
static RustBuffer lower(MoqDatagram value) {
final total_length =
FfiConverterUInt64.allocationSize(value.sequence) +
FfiConverterUInt64.allocationSize(value.timestampUs) +
FfiConverterOptionalUInt64.allocationSize(value.timestampUs) +
FfiConverterUint8List.allocationSize(value.payload) +
0;
final buf = Uint8List(total_length);
Expand All @@ -879,7 +881,7 @@ class FfiConverterMoqDatagram {
value.sequence,
Uint8List.view(buf.buffer, new_offset),
);
new_offset += FfiConverterUInt64.write(
new_offset += FfiConverterOptionalUInt64.write(
value.timestampUs,
Uint8List.view(buf.buffer, new_offset),
);
Expand All @@ -892,7 +894,7 @@ class FfiConverterMoqDatagram {

static int allocationSize(MoqDatagram value) {
return FfiConverterUInt64.allocationSize(value.sequence) +
FfiConverterUInt64.allocationSize(value.timestampUs) +
FfiConverterOptionalUInt64.allocationSize(value.timestampUs) +
FfiConverterUint8List.allocationSize(value.payload) +
0;
}
Expand Down Expand Up @@ -959,8 +961,8 @@ class FfiConverterMoqDimensions {

class MoqFrame {
final Uint8List payload;
final int timestampUs;
MoqFrame({required this.payload, this.timestampUs = 0});
final int? timestampUs;
MoqFrame({required this.payload, this.timestampUs = null});
}

class FfiConverterMoqFrame {
Expand All @@ -975,7 +977,7 @@ class FfiConverterMoqFrame {
);
final payload = payload_lifted.value;
new_offset += payload_lifted.bytesRead;
final timestampUs_lifted = FfiConverterUInt64.read(
final timestampUs_lifted = FfiConverterOptionalUInt64.read(
Uint8List.view(buf.buffer, new_offset),
);
final timestampUs = timestampUs_lifted.value;
Expand All @@ -989,7 +991,7 @@ class FfiConverterMoqFrame {
static RustBuffer lower(MoqFrame value) {
final total_length =
FfiConverterUint8List.allocationSize(value.payload) +
FfiConverterUInt64.allocationSize(value.timestampUs) +
FfiConverterOptionalUInt64.allocationSize(value.timestampUs) +
0;
final buf = Uint8List(total_length);
write(value, buf);
Expand All @@ -1002,7 +1004,7 @@ class FfiConverterMoqFrame {
value.payload,
Uint8List.view(buf.buffer, new_offset),
);
new_offset += FfiConverterUInt64.write(
new_offset += FfiConverterOptionalUInt64.write(
value.timestampUs,
Uint8List.view(buf.buffer, new_offset),
);
Expand All @@ -1011,7 +1013,7 @@ class FfiConverterMoqFrame {

static int allocationSize(MoqFrame value) {
return FfiConverterUint8List.allocationSize(value.payload) +
FfiConverterUInt64.allocationSize(value.timestampUs) +
FfiConverterOptionalUInt64.allocationSize(value.timestampUs) +
0;
}
}
Expand Down Expand Up @@ -12850,7 +12852,7 @@ void _checkApiChecksums() {
if (uniffi_moq_ffi_checksum_method_moqgroupproducer_sequence() != 21067) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqgroupproducer_write_frame() != 2442) {
if (uniffi_moq_ffi_checksum_method_moqgroupproducer_write_frame() != 51857) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqgrouprequest_abort() != 26970) {
Expand Down Expand Up @@ -12913,7 +12915,7 @@ void _checkApiChecksums() {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqtrackproducer_append_datagram() !=
6272) {
31895) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqtrackproducer_append_group() != 45225) {
Expand Down Expand Up @@ -12946,7 +12948,7 @@ void _checkApiChecksums() {
if (uniffi_moq_ffi_checksum_method_moqtrackproducer_used() != 19906) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqtrackproducer_write_frame() != 18663) {
if (uniffi_moq_ffi_checksum_method_moqtrackproducer_write_frame() != 1418) {
throw UniffiInternalError.panicked("UniFFI API checksum mismatch");
}
if (uniffi_moq_ffi_checksum_method_moqtrackrequest_abort() != 62713) {
Expand Down
2 changes: 1 addition & 1 deletion dart/moq_ffi/test/leak_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ void main() {
final track = broadcast.publishTrack(name: 'frames', info: null);
final consumer = track.consume(subscription: null);
final producer = track.appendGroup();
producer.writeFrame(frame: MoqFrame(payload: payload));
producer.writeFrame(frame: MoqFrame(payload: payload, timestampUs: 0));
producer.finish();
final group = await consumer.nextGroup();
final frame = await group!.readFrame();
Expand Down
4 changes: 3 additions & 1 deletion dart/moq_ffi/test/smoke_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ void main() {
final nextGroup = consumer.nextGroup();

final producer = track.appendGroup();
producer.writeFrame(frame: MoqFrame(payload: utf8.encode('dart')));
producer.writeFrame(
frame: MoqFrame(payload: utf8.encode('dart'), timestampUs: 0),
);
producer.finish();

final group = await nextGroup;
Expand Down
4 changes: 3 additions & 1 deletion doc/bin/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,9 @@ Every track gets its own timeline, stored in spans cut at group boundaries
between 2s and 10s. The catalog and every text, JSON, and binary track are
sparse data, so each of their groups is stored as soon as it finishes, and a
group that never closes is stored in pieces as it grows. It refuses a rendition served
from another broadcast, and one that returns after the catalog dropped it. The
from another broadcast, one that returns after the catalog dropped it, and an
untimed track, which has no timestamps to record (any track over moq-lite before
05, or moq-transport without `TIMESCALE`, such as drafts 14–16). The
stage ends once the broadcast does. A store URL that already holds a
recording is continued: each track resumes after its newest stored span. `--retention 1h` keeps only the last hour (a DVR),
deleting expired objects, and timeline objects no longer needed to recover it,
Expand Down
10 changes: 10 additions & 0 deletions doc/concept/moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,16 @@ anything on its own. Both ends apply it: the publisher skips a group rather
than sending it, and the subscriber skips it again as it reads, since the
publisher only ever sees the most tolerant budget across its subscribers.

A track is all timed or all untimed: it declares a timescale or none, and a
frame that doesn't match is refused. An untimed track stays untimed at every
hop: no receiver fills in arrival time, which would change whenever a route
fails over. An untimed group is never too old, so the cache's own expiry is what
reclaims it, and a new subscriber to an untimed track starts at its latest group
unless it names a start. Tracks from lite before 05, or from moq-transport
without a `TIMESCALE` (every track on drafts 14–16), arrive untimed. Lite cannot
mark a track untimed yet, so a lite-05 or later publisher sends each frame of an
untimed track with its send time.

A route failover is invisible to max delay: a group open across the change carries
on from the new route at the frame where the old one stopped, and a group only a
replaced route that went quiet still holds is given up once it falls a full
Expand Down
12 changes: 10 additions & 2 deletions doc/concept/standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,16 @@ before `FETCH_OK`, and aborts an incomplete group instead of caching it as whole
Drafts 14–16 use `FETCH_CANCEL`; drafts 17–19 stop and reset the request stream.
A range touching several groups is refused with `NOT_SUPPORTED`, as is
any `FETCH` on draft-20 and later, which moved the range into
`LOCATION_FILTER`. A standalone `FETCH`
carries no timestamps, since no `SUBSCRIBE_OK` declared a timescale for it.
`LOCATION_FILTER`. A standalone `FETCH` keeps each object's Timestamp, in the
units the track's `SUBSCRIBE_OK` declares. `FETCH_OK` doesn't declare them yet,
so a reader that never subscribed reads the objects untimed.

A track whose `SUBSCRIBE_OK` declares no `TIMESCALE` is untimed: its objects
arrive untimed, and an object-scope Timescale or a Timestamp on them is
ignored, as on imquic's LOC objects. A relay serves such a track downstream
untimed, without a `TIMESCALE` of its own. On a track that declares one, an
object without a Timestamp is malformed. Drafts 14–16 can't carry `TIMESCALE`,
so every track there is untimed.

On drafts 14–19, the Rust publisher also serves relative and absolute joining
`FETCH` requests for `NextObject` subscriptions, for the subscription group's
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 @@ -52,7 +52,7 @@ pkg-config file included.
- **Server.** `moq_server_listen` binds before it returns (a bad address or certificate fails there) and hands each incoming session to `on_request` as a request handle. Read `moq_session_request_path` and `_query` to route and authenticate, then `moq_session_request_accept` (a session handle, with origins like `moq_session_connect`) or `moq_session_request_reject` with an HTTP-style code (401 and 403 become the protocol's unauthorized close). An accepted session reports `1` once SETUP completes and never reconnects. `moq_server_addr` reports an ephemeral port and `moq_server_fingerprints` the hashes a client pins for a `tls_generate` certificate. `moq_server_close` stops listening; its terminal callback fires once the sockets are released.
- **Demand.** A watcher on a published track (`moq_publish_track_demand`, `moq_publish_media_demand`, `moq_encode_video_demand`, `moq_encode_audio_demand`) calls `on_demand` with `MOQ_DEMAND_USED` or `MOQ_DEMAND_UNUSED` right away and again on every change, so an encoder on a battery-powered device runs only while someone is watching. The first call is the current state, so a track that went unused before the watcher existed still reports it. `moq_publish_demand_cancel` stops it; the terminal callback still fires. A container has no single demand and is refused. Demand follows the last real subscriber: an origin that served the track drops its source copy on the unused edge, so a cache linger never delays it. Only a relay keeps what it already delivered warm for 30 seconds, and a returning subscriber is served that cache only once the publisher confirms it is still current.
- **Requests.** `moq_publish_dynamic` serves subscriptions to tracks the broadcast never declared: each arrives as a request handle, read its name with `moq_track_request_name`, then `moq_track_request_accept` (a raw track handle), `moq_track_request_video` / `_audio` (the media handle `moq_publish_video` / `_audio` return), or `moq_track_request_abort` with an application code the subscriber sees. Without a live handler an unknown name is refused. `moq_publish_track_dynamic` does the same for fetches of groups a track no longer has cached, delivered as `moq_group_request_*` (`sequence`, `priority`, `frame_start`); `moq_group_request_accept` starts the producer at `frame_start` so written frames keep their group indices. Register it with `moq_track_request_dynamic` before accepting a track that was itself requested by a fetch, so that pending group survives the transition. Both handlers stop with `moq_publish_dynamic_cancel`.
- **Everything the bindings can do** ([list](/lib/#what-every-binding-can-do)): media publish and consume with the catalog managed for you, raw pixels and PCM with the codec inside (`moq_encode_video`, `moq_encode_audio`, and the `moq_decode_*` mirrors), raw tracks with timestamps and datagrams, JSON and binary data tracks (snapshot or stream, each advertised in the catalog for as long as it lives), group fetch, catalog sections, shared video properties, and stalled hints. The three advertising operations are `moq_origin_create_broadcast` (unannounced producer, invisible to everyone), `moq_publish_announce` / `moq_publish_unannounce` (exact-path advertisement), `moq_publish_close` (ends the broadcast for good and releases its handle), and `moq_origin_dynamic` (a claim over a path prefix and everything beneath it; `""` for everything). A route is a capability, not an inventory. `moq_origin_announced` takes a `moq_announce_config` (NULL or zeroed for everything): a literal prefix and an optional relative pattern filter; each `moq_announce_event` has a `kind` (`MOQ_ANNOUNCE_KIND_START`, `_UPDATE`, or `_END`); `prefix` stays relative to the origin, while `captures` reports what each wildcard matched when `has_captures` is true. Paths with a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts); set `hidden` or name the dot segment in `prefix` to list them.
- **Everything the bindings can do** ([list](/lib/#what-every-binding-can-do)): media publish and consume with the catalog managed for you, raw pixels and PCM with the codec inside (`moq_encode_video`, `moq_encode_audio`, and the `moq_decode_*` mirrors), raw tracks with timestamps and datagrams (one read from an untimed track has `timestamp_present` false), JSON and binary data tracks (snapshot or stream, each advertised in the catalog for as long as it lives), group fetch, catalog sections, shared video properties, and stalled hints. The three advertising operations are `moq_origin_create_broadcast` (unannounced producer, invisible to everyone), `moq_publish_announce` / `moq_publish_unannounce` (exact-path advertisement), `moq_publish_close` (ends the broadcast for good and releases its handle), and `moq_origin_dynamic` (a claim over a path prefix and everything beneath it; `""` for everything). A route is a capability, not an inventory. `moq_origin_announced` takes a `moq_announce_config` (NULL or zeroed for everything): a literal prefix and an optional relative pattern filter; each `moq_announce_event` has a `kind` (`MOQ_ANNOUNCE_KIND_START`, `_UPDATE`, or `_END`); `prefix` stays relative to the origin, while `captures` reports what each wildcard matched when `has_captures` is true. Paths with a `.`-prefixed segment below the prefix are [hidden](/concept/moq-lite#hidden-broadcasts); set `hidden` or name the dot segment in `prefix` to list them.

```c
moq_client_config config;
Expand Down
2 changes: 2 additions & 0 deletions doc/lib/dart/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,6 @@ not the same as zero. `rttUs` is microseconds; the `rtt` extension reads it as a

Raw track publisher metadata has an optional maximum age. Omitting it imposes no publisher age limit; zero keeps the live edge. Local cache limits still apply, and media imports explicitly retain 30 seconds. See [publisher retention](/concept/moq-lite).

A raw `MoqFrame` or `MoqDatagram` has a null `timestampUs` (and `timestamp`) when it was read from an untimed track; see [untimed tracks](/concept/moq-lite#subscriptions). A raw track you publish is always timed.

Await `session.shutdown()` or `moq.close()` to drain finished tracks before disconnecting. These futures fail if delivery has not completed within one second. `session.cancel(code: 0)` remains immediate. Finish or abort live tracks before shutdown. IETF media streams are not drained yet.
5 changes: 4 additions & 1 deletion doc/lib/go/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ for event, err := range announced.All(ctx) {
// opusInit, packet, pts, and rgba come from your encoder or capture source.
broadcast, _ := client.CreateBroadcast("my-stream.hang")
audio, _ := broadcast.PublishAudio(moq.AudioFormatOpus, opusInit)
_ = audio.WriteFrame(moq.Frame{Payload: packet, TimestampUs: 20_000})
pts := uint64(20_000)
_ = audio.WriteFrame(moq.Frame{Payload: packet, TimestampUs: &pts})

track := "camera"
video, _ := broadcast.EncodeVideo(
Expand Down Expand Up @@ -181,4 +182,6 @@ available, which is not the same as zero.

Raw track publisher metadata has an optional maximum age. Omitting it imposes no publisher age limit; zero keeps the live edge. Local cache limits still apply, and media imports explicitly retain 30 seconds. See [publisher retention](/concept/moq-lite).

A raw `Frame` or `Datagram` has a nil `TimestampUs` when it was read from an untimed track; see [untimed tracks](/concept/moq-lite#subscriptions). A raw track you publish is always timed.

`session.Shutdown(ctx)` drains finished tracks and returns a delivery error if the one-second deadline expires. Cancelling the context aborts immediately. `client.Close()` waits for shutdown and returns the same error; `session.Cancel(code)` remains immediate. Finish or abort live tracks before shutdown. IETF media streams are not drained yet.
2 changes: 2 additions & 0 deletions doc/lib/kt/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,6 @@ reads it as a `kotlin.time.Duration`.

Raw track publisher metadata has an optional maximum age. Omitting it imposes no publisher age limit; zero keeps the live edge. Local cache limits still apply, and media imports explicitly retain 30 seconds. See [publisher retention](/concept/moq-lite).

A raw `Frame` or `Datagram` has a null `timestampUs` (and `timestamp`) when it was read from an untimed track; see [untimed tracks](/concept/moq-lite#subscriptions). A raw track you publish is always timed.

Call suspending `session.shutdown()` or `moq.shutdown()` to drain finished tracks before disconnecting. They throw if delivery has not completed within one second. Finish or abort live tracks first. `cancel(0u)` and synchronous `Moq.close()` remain immediate; `use { }` therefore cancels on exit. IETF media streams are not drained yet.
2 changes: 2 additions & 0 deletions doc/lib/py/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,6 @@ not the same as zero.

Raw track publisher metadata has an optional maximum age. Omitting it imposes no publisher age limit; zero keeps the live edge. Local cache limits still apply, and media imports explicitly retain 30 seconds. See [publisher retention](/concept/moq-lite).

A raw `Frame` or `Datagram` has `timestamp_us` of `None` when it was read from an untimed track; see [untimed tracks](/concept/moq-lite#subscriptions). A raw track you publish is always timed.

Await `session.shutdown()` to drain finished tracks before disconnecting. It raises if delivery has not completed within one second. `cancel(code)` stays immediate. Session and client async context managers await shutdown on a clean exit and cancel on an error, so the body's exception survives; finish or abort live tracks first. IETF media streams are not drained yet.
Loading
Loading