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
5 changes: 3 additions & 2 deletions doc/concept/moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,9 @@ moq-lite 07 receiver. Since moq-lite 05 and 06 send times look like media time,
a receiver drops every timestamp it gets over them and the track arrives
untimed.
Timestamps received over moq-transport, whose peers may stamp arrival times,
are not checked and are only passed on over moq-transport: a relay serves that
track to moq-lite untimed.
are not checked. Rust relays preserve them across moq-lite 07 when both peers
negotiate the [ietf extension](/draft/moq-ietf), which carries their origin and
keeps them unchecked. A moq-lite peer without it receives that track untimed.

A publisher may compress a track, which pays off for repetitive data such as
JSON and not for media. Each group's frames become one DEFLATE stream
Expand Down
7 changes: 7 additions & 0 deletions drafts/draft-lcurley-moq-ietf.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ The Version also gives each property its meaning: drafts 18 and 19 serialize a b
Other properties are opaque to this extension.

## Timestamps {#timestamps}
A relay MUST preserve a relayed track's Timescale and timestamps across a negotiated hop, except in a listing ({{track}}).
A nonzero Version identifies timestamps from MoQ Transport, which may be arrival times and need not rise with Group Sequence.
The receiver MUST NOT apply moq-lite's group-start rewind check to such a track; a Version of zero retains that check.
Without this extension, a relay MUST serve a MoQ Transport track untimed as {{moql}} requires.

On a timed track, the FRAME's Timestamp, or the datagram's for a datagram, is the object's time, and nothing else carries it.
The Property Block MUST NOT contain a mutable Timestamp (0x10) there; a receiver MUST treat one as malformed.
A relay forwarding the object to {{moqt}} restates the time as the mutable Timestamp, unless a Timestamp inside the Immutable Properties already says it.
Expand Down Expand Up @@ -194,6 +199,7 @@ The receiver parses the keys inside the Immutable Properties from its value, whi
It SHOULD read them from the value when needed rather than hold them beside it, as a list of IDs can cost several times the value.

A TRACKS_ADD ({{moql}} Tracks Stream) embeds TRACK_INFO without TRACK_PROPERTIES, so when the extension is negotiated a subscriber MUST NOT skip the Track Stream for a track it learned from a listing.
Lacking the Version, a TRACKS_ADD cannot mark a {{moqt}} track's timestamps as unchecked ({{timestamps}}), so it lists that track untimed even when the extension is negotiated.


# Datagrams {#datagrams}
Expand Down Expand Up @@ -258,6 +264,7 @@ A high, distinctive value is used to avoid collisions with the low values {{moql

## Since draft-lcurley-moq-ietf-00 (in progress)

- Preserve relayed track timestamps and their unchecked MoQ Transport provenance across negotiated hops.
- Initial version.

# Acknowledgments
Expand Down
8 changes: 5 additions & 3 deletions drafts/draft-lcurley-moq-lite.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,11 @@ Each frame of a timed Track carries a presentation timestamp expressed in the Tr
A Track without a `Timescale` is untimed: its frames and datagrams carry no timestamp.
A timestamp MUST be less than 2^61: lite-05 and lite-06 zigzag a Group's first [Timestamp Delta](#frame) into a QUIC varint, which carries no more, and the bound lets a relay forward any timestamp to any version.
A receiver treats a larger timestamp as a malformed Track, even one it discards.
A publisher MUST NOT start a timed Group's frame 0 earlier than that of any lower-sequence Group it publishes.
Unless a negotiated extension specifies otherwise, a publisher MUST NOT start a timed Group's frame 0 earlier than that of any lower-sequence Group it publishes.
lite-05 and lite-06 cannot encode an untimed Track, so a publisher on those versions declares a `Timescale` of 1000 for it and stamps every frame and datagram with its send time in milliseconds.
A receiver cannot tell such send times from presentation timestamps, and they need not rise with sequence, so it discards every timestamp received on those versions and treats the Track as untimed.
On later versions a receiver MAY check a received Group against this rule, refusing a violating Group's stream (STOP_SENDING) with TIMESTAMP_MISMATCH.
A relay that cannot vouch for a Track's timestamps, such as ones received over another protocol, MUST serve the Track untimed.
A relay that cannot vouch for a Track's timestamps, such as ones received over another protocol, MUST serve the Track untimed unless a negotiated extension identifies their unchecked provenance.

## Positions {#positions}
A Position is a (Group Sequence, Frame Index) pair identifying one frame within a Track.
Expand Down Expand Up @@ -1768,6 +1768,8 @@ The `Message Length` describes the payload size on the wire.

## moq-lite-07

- Allow a negotiated extension to preserve timestamps with explicitly unchecked provenance.

- Added Padding Stream (0x2), read and discarded incrementally by either endpoint.

- Named 128 the midpoint priority for an unset Subscriber or Publisher Priority.
Expand All @@ -1778,7 +1780,7 @@ The `Message Length` describes the payload size on the wire.

- Replaced ANNOUNCE_OK Active Count with an ordered, empty SYNCED event (0x4), marking the initial set without retaining routes that end before delivery.

- A publisher must not start a timed Group's frame 0 earlier than a lower-sequence Group's, and a receiver may refuse a Group that does. A receiver discards the timestamps it receives over lite-05/06, which may be send times, and a relay serves a Track whose timestamps it cannot vouch for untimed.
- A publisher must not start a timed Group's frame 0 earlier than a lower-sequence Group's, and a receiver may refuse a Group that does. A receiver discards the timestamps it receives over lite-05/06, which may be send times, and a relay serves a Track whose timestamps it cannot vouch for untimed unless a negotiated extension identifies their unchecked provenance.

- Bounded every timestamp below 2^61, since a lite-05/06 Group's first Timestamp Delta carries no more, so a relay never holds one it cannot forward. A larger one is a malformed Track.

Expand Down
2 changes: 1 addition & 1 deletion js/net/src/ietf/subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1227,7 +1227,7 @@ export class Subscriber {
priority: fromWire(ok.properties.priority ?? ABSENT_WIRE),
maxAge: maxCacheDuration === undefined ? undefined : Milli(Number(maxCacheDuration)),
// A moq-transport peer may stamp arrival times, and nothing on the wire says it didn't.
unchecked: ok.properties.timescale != null,
ietf: version,
});
const producer = staged.producer;
const subscription: Subscription = { track: producer, tail: new Tail() };
Expand Down
3 changes: 2 additions & 1 deletion js/net/src/lite/publisher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Producer as BroadcastProducer } from "../broadcast.ts";
import { fromTransport, SessionCode, StreamCode, StreamError, toStreamCode } from "../error.ts";
import { Producer as GroupProducer } from "../group.ts";
import { randomHop } from "../hop.ts";
import { Version as IetfVersion } from "../ietf/version.ts";
import { hooks } from "../internal.ts";
import { createMockTransportPair, textFrame } from "../mock.ts";
import { Producer as OriginProducer } from "../origin.ts";
Expand Down Expand Up @@ -555,7 +556,7 @@ test("lite draft-06: an unchecked track's fetched frames carry their send time",
const publisher = new Publisher(pair.server, Version.DRAFT_06, randomHop(), origin.consume());

const broadcast = publish(origin, Path.from("test"));
const track = broadcast.createTrack("video", { timescale: Timescale.MICRO, unchecked: true });
const track = broadcast.createTrack("video", { timescale: Timescale.MICRO, ietf: IetfVersion.DRAFT_22 });

const group = new GroupProducer(0);
group.writeFrame(textFrame("hello", Timestamp.fromMillis(36_000_000)));
Expand Down
4 changes: 3 additions & 1 deletion js/net/src/lite/track_stream.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// An open Track Stream is interest in the track: the subscriber keeps it open until its
// SUBSCRIBE is answered, and the publisher keeps the track wanted until the subscriber closes it.

import { describe, expect, test } from "bun:test";
import { Milli } from "@moq/clock";
import { randomHop } from "../hop.ts";
import { Version as IetfVersion } from "../ietf/version.ts";
import { createMockTransportPair } from "../mock.ts";
import { Producer as OriginProducer } from "../origin.ts";
import * as Path from "../path.ts";
Expand Down Expand Up @@ -396,7 +398,7 @@ describe.each([Version.DRAFT_05, Version.DRAFT_06, Version.DRAFT_07])("%s", (ver
for (;;) {
const request = await wireOf(broadcast).requested();
if (!request) return;
request.accept({ timescale: Timescale.MICRO, unchecked: true });
request.accept({ timescale: Timescale.MICRO, ietf: IetfVersion.DRAFT_22 });
}
})();

Expand Down
17 changes: 17 additions & 0 deletions js/net/src/track.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Manual, Milli, Nano } from "@moq/clock";
import { Producer as BroadcastProducer } from "./broadcast.ts";
import { ProtocolViolation, StreamCode, StreamError, TimestampMismatch, TooFarBehind } from "./error.ts";
import { Producer as GroupProducer, MAX_GROUP_FRAMES } from "./group.ts";
import { Version as IetfVersion } from "./ietf/version.ts";
import { hooks } from "./internal.ts";
import { textFrame } from "./mock.ts";
import { Timescale, Timestamp } from "./time.ts";
Expand Down Expand Up @@ -2977,6 +2978,22 @@ test("an endless track with gaps remembers only those within the budget", () =>
}
});

test("IETF provenance survives normalization on timed and untimed tracks", () => {
for (const timescale of [undefined, Timescale.MILLI]) {
const info = infoDefaults({ ietf: IetfVersion.DRAFT_22, timescale });
expect(info.ietf).toBe(IetfVersion.DRAFT_22);
expect(info.timescale).toBe(timescale);
}
const producer = new TrackProducer("ietf").accept({ timescale: Timescale.MILLI, ietf: IetfVersion.DRAFT_22 });
try {
stampGroup(producer, 0, 1000);
stampGroup(producer, 1, 500);
producer.writeFrame({ payload: enc.encode("arrival"), timestamp: Timestamp.fromMillis(250) });
} finally {
producer.close();
}
});

test("an appended rewind is refused and an equal start is not", () => {
const producer = new TrackProducer("test").accept({ timescale: Timescale.MILLI });
producer.writeFrame({ payload: enc.encode("a"), timestamp: Timestamp.fromMillis(1000) });
Expand Down
25 changes: 9 additions & 16 deletions js/net/src/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from "./error.ts";
import { Compression } from "./flate/group.ts";
import { type Frame, type Consumer as GroupConsumer, Producer as GroupProducer } from "./group.ts";
import type { IetfVersion } from "./ietf/version.ts";
import {
type DropRun,
dropAt,
Expand Down Expand Up @@ -130,15 +131,9 @@ export interface Info {
* snapshot or keyframe. `0` asks on any join after a delta. An integer in `0..2^32`.
*/
refresh?: number;
/**
* Whether this timed track's timestamps arrived over moq-transport, whose peers may stamp
* arrival times, so no group is checked for a rewind. A moq-lite hop serves the track
* untimed, since its receivers check every timestamp. Mirrors the Rust
* `track::Info::unchecked`.
*
* @internal
*/
unchecked?: boolean;
/** The original moq-transport publisher's draft, whose timestamps may rewind. @internal */
// private: passthrough
ietf?: IetfVersion;
}

// Normalize a duration for the wire, which carries it as an unsigned varint.
Expand Down Expand Up @@ -166,25 +161,23 @@ function priorityValue(value: number): number {
}

// private: passthrough
/** The timescale a moq-lite hop serves a track at: none for an {@link Info.unchecked} one. @internal */
/** The timescale a moq-lite hop serves a track at: none for an {@link Info.ietf} one. @internal */
export function liteTimescale(info: Info): Timescale | undefined {
return info.unchecked ? undefined : (info.timescale ?? undefined);
return info.ietf === undefined ? (info.timescale ?? undefined) : undefined;
}

/** Fill in any unset {@link Info} fields with their defaults. */
export function infoDefaults(info: Partial<Info> = {}): Info {
if (info.compress !== undefined && typeof info.compress !== "boolean")
throw new TypeError("compress must be a boolean");
const timescale = info.timescale == null ? undefined : Timescale(info.timescale);
// Only a timed track has timestamps to doubt.
if (info.unchecked && timescale === undefined) throw new RangeError("an untimed track can't be unchecked");
return {
compress: info.compress ?? false,
timescale,
maxAge: info.maxAge === undefined ? undefined : wireMillis("maxAge", info.maxAge),
priority: priorityValue(info.priority ?? DEFAULT_PRIORITY),
refresh: info.refresh == null ? undefined : refreshRatio(info.refresh),
...(info.unchecked ? { unchecked: true } : {}),
...(info.ietf === undefined ? {} : { ietf: info.ietf }),
};
}

Expand Down Expand Up @@ -1490,9 +1483,9 @@ export class Producer {
this.#publish(group, start, late);
}

// Whether this track's timestamps may rewind (see {@link Info.unchecked}).
// Whether this track's timestamps may rewind (see {@link Info.ietf}).
#unchecked(): boolean {
return this.#state.info.peek()?.unchecked === true;
return this.#state.info.peek()?.ietf !== undefined;
}

// Fan a datagram out to every live subscriber, dropping the oldest once the ring is full.
Expand Down
6 changes: 1 addition & 5 deletions quest/m1/ietf-extension/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# moq-transport passthrough
# [M] moq-transport passthrough

## Goal

Expand Down Expand Up @@ -82,10 +82,6 @@ Work owned by this README, after the children land:
- FETCH cases are skipped upstream. Section 8 stays out while PUBLISH is
declined.

## Required

- [Timed tracks cross lite-07](/quest/m1/ietf-extension/serve-timed.md) - moq-transport timestamps reach `ietf` extension peers over lite-07, with `unchecked` derived from `Info::ietf()`

## Related

- [#4926](https://github.com/moq-dev/moq/issues/4926) - the declined request for an object-level API; this carries the fields for relays only
Expand Down
26 changes: 0 additions & 26 deletions quest/m1/ietf-extension/serve-timed.md

This file was deleted.

10 changes: 4 additions & 6 deletions rs/moq-net/src/ietf/subscriber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,8 +520,6 @@ fn accepted_info(properties: &ietf::Properties, version: Version) -> track::Info
version,
values: properties.unknown.clone(),
}));
// A moq-transport peer may stamp arrival times, and nothing on the wire says it didn't.
info.unchecked = info.timescale.is_some();
info
}

Expand Down Expand Up @@ -12857,7 +12855,7 @@ mod stitch_tests {
}

/// Relay `script` through a track the ingress times in `timescale`, which the cache
/// normalizes to microseconds like [`accepted_info`], via one relay or two joined by a
/// retains on the publisher's grid like [`accepted_info`], via one relay or two joined by a
/// moq-lite `hop`.
async fn relay_passthrough_via(
script: Vec<u8>,
Expand Down Expand Up @@ -13045,15 +13043,15 @@ mod stitch_tests {

/// A relayed Timestamp leaves in the units the publisher declared: the cache holds the
/// track on the publisher's grid, so a copy of the same instance over another route
/// matches it. A moq-lite hop serves the unchecked track untimed, so the Timestamp
/// stops there and the other property survives.
/// matches it, including across a moq-lite hop that negotiated the `ietf` extension.
#[moq_clock::test]
async fn passthrough_keeps_the_publishers_timestamp_units() {
let version = Version::Draft22;
// Timestamp 42 (ms), then an unknown property that must survive beside it.
let properties = [0x10, 42, 0x70, 7];
let script = object_stream(version, true, &[(0, &properties, None)]);
for (hop, timestamp) in [(false, Timestamp::new(42, Timescale::MILLI).ok()), (true, None)] {
for hop in [false, true] {
let timestamp = Timestamp::new(42, Timescale::MILLI).ok();
let (bytes, log) =
relay_passthrough_via(script.clone(), version, version, None, Some(Timescale::MILLI), hop).await;
assert!(log.resets().is_empty(), "hop: {hop}");
Expand Down
Loading
Loading