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
26 changes: 14 additions & 12 deletions doc/bin/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,25 +449,27 @@ and writes its packets back out unchanged, from its first group on, each run
between two PCRs `--delay` after the first one's time on the source clock. The
output is exactly as conformant as the source, and two exporters emit the same
bytes. A run that arrives after its deadline is dropped and counted, leaving a
continuity error, and the rest carry on. It takes neither `--linger`,
continuity error, and the rest carry on. It takes neither `--follow`,
`--stitch` nor `--mux-rate`. Only the hang catalog names the track, so an
`.msf` broadcast needs `--catalog-format hang`.

A stdout export ends with the broadcast. `export ts --linger 10s` waits that
A stdout export ends with the broadcast. `export ts --follow 10s` waits that
long for the same publisher instance to come back, and carries on with the same
stream. A return that cannot be read yet, such as one still routed through a
dead session, keeps it waiting for the next announcement; if the linger runs
out first, it exits 1 with that error. A replacement (a restarted publisher, under a new
stream. A return that refuses its catalog keeps it waiting for the next
announcement; if the follow runs out first, it exits 1 with that refusal. One
that goes before serving, such as one still routed through a dead session, is
no return. A replacement (a restarted publisher, under a new
[epoch](/concept/moq-lite#publisher-epochs)) exits 1 unless `--stitch` follows
it as a full program switch: a new PMT from its catalog, with every PID flagging
the break. `--linger` also bounds a mid-stream stitch from the replacement
announcement through its first catalog snapshot; expiry exits 1. The default
`--linger 0s` follows only a replacement whose catalog is already available.
Without `--stitch` the export stays on the old publisher while it is
the break. A stitch waits for the replacement's first catalog snapshot until it
arrives or the replacement goes, keeping the old publisher playing meanwhile,
so even the default `--follow 0s` switches to a replacement that is still
starting. Without `--stitch` the export stays on the old publisher while it is
up. Only lite-07 and moq-transport draft 17+ sessions carry epochs, so on the
default version every return is a replacement. An export that fails while the broadcast stays up, such as on
a codec TS cannot carry, exits 1 without lingering. Only `ts` can mark a break,
so the other formats refuse `--linger`.
default version every return is a replacement. A failure other than the
publisher leaving, such as a codec TS cannot carry, exits 1 at once. Only `ts`
can mark a break, so the other formats refuse `--linger`, the released spelling
of `--follow`.

## Reports and feedback

Expand Down
6 changes: 2 additions & 4 deletions doc/bin/srt.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,9 @@ Each connection publishes under a fresh
[epoch](/concept/moq-lite#publisher-epochs), so an encoder that reconnects
while its stale connection is still open replaces it at once: viewers see the
broadcast restart, and their next subscribe reaches the new feed. An
`export srt` stream ends with its broadcast; `--linger` and `--stitch` follow a
`export srt` stream ends with its broadcast; `--follow` and `--stitch` follow a
return or a replacement on the same connection, as they do for
[`export ts`](/bin/cli#retention-and-latency). The linger also bounds a
mid-stream replacement's first catalog; expiry ends the export with an error.
With the default `--linger 0s`, that catalog must already be available.
[`export ts`](/bin/cli#retention-and-latency).

A multi-program feed is refused unless `--program`
picks one: `--program 2` imports program 2 alone, and `--program all`
Expand Down
7 changes: 3 additions & 4 deletions doc/setup/redundancy.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,13 @@ For MPEG-TS output, opt into following replacements:
```bash
moq --connect https://edge.example.com/anon \
--connect-version moq-transport-17 --broadcast event.hang \
export ts --stitch --linger 10s > event.ts
export ts --stitch --follow 10s > event.ts
```

`--stitch` follows a replacement as a program switch: its catalog supplies a
new PMT and every PID flags the discontinuity. Starting a backup can cause
this switch too. `--linger 10s` bounds waiting after an end and waiting for a
replacement's first catalog; choose that window for your application. It does
not make the switch hitless. Without `--stitch`, the TS export stays on the
this switch too. `--follow 10s` bounds waiting for a publisher after an end;
choose that window for your application. It does not make the switch hitless. Without `--stitch`, the TS export stays on the
old instance while it is up, then fails on a replacement. Other stdout export
formats end with their broadcast; they do not follow a replacement. TS
passthrough also cannot stitch. See [CLI retention and latency](/bin/cli#retention-and-latency).
Expand Down
3 changes: 1 addition & 2 deletions quest/m1/api-audit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ Rejected in planning:
`close` collides with `AutoCloseable`, and no single verb fits every
language's reserved names.
- A follow policy enum folding `linger` and `stitch`: one name per meaning
([TS export follows by cause](/quest/m1/broadcast-lifecycle/ts-follow.md)
renames the export wait `--follow`) fixes the confusion more cheaply.
(the export wait is `--follow`) fixes the confusion more cheaply.

## Required

Expand Down
1 change: 0 additions & 1 deletion quest/m1/broadcast-lifecycle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ timer on main at 1582fdc07d:
## Required

- [Stateless broadcasts](/quest/m1/broadcast-lifecycle/stateless.md) - `Finish`, its linger, and finished-only supersession are gone from moq-net, `@moq/net`, moq-ffi, moq-c, and every binding
- [TS export follows by cause](/quest/m1/broadcast-lifecycle/ts-follow.md) - `export ts|srt --linger` becomes `--follow <duration>`, a replacement's catalog has no separate deadline, and a track failure is classified by its cause instead of a 1s grace
- [Timer cleanups](/quest/m1/broadcast-lifecycle/timers.md) - `ConnectionProps.linger` is gone and stale linger references are fixed

## Related
Expand Down
39 changes: 0 additions & 39 deletions quest/m1/broadcast-lifecycle/ts-follow.md

This file was deleted.

2 changes: 1 addition & 1 deletion rs/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ quic-sim:
send-depth:
bash sh/rs/send-depth.sh

# Run moq-cli's `moq export` process tests, which feed a real relay and wait out lingers in real time.
# Run moq-cli's `moq export` process tests, which feed a real relay and wait out follows in real time.
[positional-arguments]
cli-export *args:
just rs test -p moq-cli --test integration --run-ignored only -E 'test(/^export::/)' "$@"
Expand Down
9 changes: 5 additions & 4 deletions rs/moq-cli/MIGRATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,14 @@ Rename `--max-age` to `--max-delay` on those exports.
Each frame is written that long after its decode time; the older staleness spellings are refused with `--delay` named.
Rename `--max-age` to `--delay`.

### `moq export ts --linger` follows only the same publisher instance
### `moq export ts --linger` is `--follow`, and follows only the same publisher instance

A restarted publisher exits 1 unless `--stitch` follows it as a program switch.
`moq export srt` takes the same two flags.
Rename `export ts --linger` to `--follow`; the old spelling stops the run naming it. `export srt` takes the same `--follow`.
A restarted publisher exits 1 unless `--stitch` follows it as a program switch, and a stitched replacement's first catalog is awaited until it arrives or the replacement goes, so `--follow 0s` no longer refuses a cold one.
A failure other than the publisher leaving, such as a codec TS cannot carry, exits at once rather than after a 1 s grace.

```bash
moq export ts --linger 10s --stitch ...
moq export ts --follow 10s --stitch ...
```

### `moq export fmp4` fixes its track set at the init segment
Expand Down
81 changes: 50 additions & 31 deletions rs/moq-cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,8 @@ impl Invocation {
}
if let Some(stdout) = export.sink.stdout() {
anyhow::ensure!(
stdout.linger.is_zero() || matches!(stdout.format, SubscribeFormat::Ts),
"--linger needs an output that can mark a restart, and only `export ts` can"
stdout.follow.is_zero() || matches!(stdout.format, SubscribeFormat::Ts),
"--linger is gone, and following a broadcast's return needs an output that can mark the restart: only `export ts --follow` can"
);
if matches!(stdout.format, SubscribeFormat::H264 | SubscribeFormat::H265) {
anyhow::ensure!(
Expand Down Expand Up @@ -1090,7 +1090,7 @@ impl ExportSink {
let container = |format, container: &Container| Stdout {
format,
max_delay: container.max_delay.into(),
linger: container.linger.into(),
follow: container.linger.into(),
stitch: false,
fragment_duration: None,
mux_rate: None,
Expand All @@ -1099,7 +1099,7 @@ impl ExportSink {
let buffered = |format, args: &Buffered| Stdout {
format,
max_delay: args.delay.into(),
linger: args.linger.into(),
follow: args.linger.into(),
stitch: false,
fragment_duration: None,
mux_rate: None,
Expand All @@ -1117,7 +1117,7 @@ impl ExportSink {
Self::Ts(args) => Stdout {
format: SubscribeFormat::Ts,
max_delay: args.delay.into(),
linger: args.linger.into(),
follow: args.follow.into(),
stitch: args.stitch,
fragment_duration: None,
mux_rate: args.mux_rate,
Expand All @@ -1137,7 +1137,9 @@ pub struct Stdout {
/// The staleness budget, also the FLV/MKV release delay and how far behind the source `ts`
/// runs (`--delay`).
pub max_delay: Duration,
pub linger: Duration,
/// How long to wait for the broadcast to come back (`ts` only; the others refuse their
/// hidden `--linger`).
pub follow: Duration,
/// Follow a replacement as a program switch (`ts` only).
pub stitch: bool,
pub fragment_duration: Option<Duration>,
Expand All @@ -1154,8 +1156,8 @@ pub struct Container {
#[usage(long, default = "500ms")]
pub max_delay: moq_tokio::cli::Duration,

/// Accepted only to refuse a nonzero value with a pointer to `ts`, the one format
/// that can mark where a returned broadcast restarts.
/// The released spelling of `export ts --follow`, accepted only to refuse a nonzero value:
/// only TS can mark where a returned broadcast restarts.
#[usage(long, default = "0s", hide = true)]
pub linger: moq_tokio::cli::Duration,

Expand Down Expand Up @@ -1190,7 +1192,8 @@ pub struct Buffered {
/// drops none as late, but still skips a stalled group once a newer one is ready.
#[usage(long, default = "500ms")]
pub delay: moq_tokio::cli::Duration,
/// Accepted only to refuse a nonzero value: only TS can mark a broadcast restart.
/// The released spelling of `export ts --follow`, accepted only to refuse a nonzero value:
/// only TS can mark a broadcast restart.
#[usage(long, default = "0s", hide = true)]
pub linger: moq_tokio::cli::Duration,
/// An older spelling of the staleness setting.
Expand Down Expand Up @@ -1238,12 +1241,12 @@ pub struct Transport {
#[usage(long, default = "1s")]
pub delay: moq_tokio::cli::Duration,

/// How long to wait for the same publisher instance to come back once it ends (e.g. `10s`),
/// then carry on with the same stream. A replacement exits 1 unless `--stitch` is passed.
/// An export that fails while the broadcast is still up exits without waiting.
/// Also bounds a stitched replacement through its first catalog; zero requires it already available.
/// How long to wait for the same publisher instance, or with `--stitch` a replacement, to
/// come back once the broadcast ends (e.g. `10s`), then carry on with the same stream.
/// Zero follows only an instance already serving. A replacement exits 1 unless `--stitch` is
/// passed. A failure other than the publisher leaving exits at once.
#[usage(long, default = "0s")]
pub linger: moq_tokio::cli::Duration,
pub follow: moq_tokio::cli::Duration,

/// Follow another publisher instance that replaces the broadcast, as a full program switch:
/// a new PMT from its catalog, flagged as a break on every PID.
Expand All @@ -1259,9 +1262,13 @@ pub struct Transport {
/// Write the multiplex an `import ts --passthrough` published back out byte-identical,
/// paced on its own PCR, `--delay` behind it. Packets that arrive later than that are
/// dropped, leaving a continuity error.
#[usage(long, conflicts("--linger", "--stitch", "--mux-rate"))]
#[usage(long, conflicts("--follow", "--stitch", "--mux-rate"))]
pub passthrough: bool,

/// The released spelling of [`Self::follow`].
#[usage(long = "linger", hide = true)]
linger: Option<moq_tokio::cli::Duration>,

/// The released spelling of [`Self::delay`].
#[usage(long = "max-age", hide = true)]
max_age: Option<moq_tokio::cli::Duration>,
Expand All @@ -1281,6 +1288,9 @@ impl Transport {
if self.latency_max.is_some() {
found.changed("--latency-max", None, "--delay", BEHIND);
}
if self.linger.is_some() {
found.flag("--linger", None, "--follow");
}
found
}
}
Expand Down Expand Up @@ -1318,20 +1328,29 @@ mod tests {
let _ = Stage::to_kdl();
}

/// Only TS can mark where a returned broadcast restarts, so only `export ts` may linger.
/// Only TS can mark where a returned broadcast restarts, so only `export ts` follows one.
/// The released `--linger` points there, as a rename on `ts` and a refusal elsewhere.
#[test]
fn linger_is_ts_only() {
let parse = |format: &str, linger: &str| {
Invocation::try_parse_from(["moq", "--connect", "http://relay", "export", format, "--linger", linger])
.unwrap()
fn follow_is_ts_only() {
let parse = |format: &str, flag: &str, value: &str| {
Invocation::try_parse_from(["moq", "--connect", "http://relay", "export", format, flag, value])
};
assert!(parse("ts", "10s").validate().is_ok());
for format in ["fmp4", "h264", "h265"] {
let err = parse(format, "10s").validate().unwrap_err().to_string();
assert!(err.contains("--linger"), "{format}: {err}");
assert!(parse("ts", "--follow", "10s").unwrap().validate().is_ok());
let Err(err) = parse("ts", "--linger", "10s") else {
panic!("the released spelling must not start a run");
};
assert!(err.to_string().contains("--linger -> --follow"), "{err}");
for format in ["fmp4", "mkv", "flv", "h264", "h265"] {
assert!(parse(format, "--follow", "10s").is_err(), "{format} takes no --follow");
let err = parse(format, "--linger", "10s")
.unwrap()
.validate()
.unwrap_err()
.to_string();
assert!(err.contains("export ts --follow"), "{format}: {err}");
assert!(
parse(format, "0s").validate().is_ok(),
"{format}: no linger is always fine"
parse(format, "--linger", "0s").unwrap().validate().is_ok(),
"{format}: a zero --linger is always fine"
);
}
}
Expand Down Expand Up @@ -1536,7 +1555,7 @@ mod tests {
let stdout = export.sink.stdout().unwrap();
assert!(stdout.passthrough);
assert_eq!(stdout.max_delay, Duration::from_secs(2));
for flags in [&["--stitch"][..], &["--linger", "10s"], &["--mux-rate", "20000000"]] {
for flags in [&["--stitch"][..], &["--follow", "10s"], &["--mux-rate", "20000000"]] {
assert!(parse(flags).is_err(), "{flags:?} must not start a run");
}
}
Expand Down Expand Up @@ -1777,16 +1796,16 @@ mod tests {
);
}

/// `export srt` follows its broadcast with the same `--linger` and `--stitch` as `export ts`.
/// `export srt` follows its broadcast with the same `--follow` and `--stitch` as `export ts`.
#[test]
fn export_srt_takes_linger_and_stitch() {
fn export_srt_takes_follow_and_stitch() {
let cli = Invocation::try_parse_from([
"moq",
"export",
"srt",
"--listen",
"[::]:9000",
"--linger",
"--follow",
"10s",
"--stitch",
])
Expand All @@ -1797,7 +1816,7 @@ mod tests {
let ExportSink::Srt(args) = &export.sink else {
panic!("an export srt stage");
};
assert_eq!(Duration::from(args.linger), Duration::from_secs(10));
assert_eq!(Duration::from(args.follow), Duration::from_secs(10));
assert!(args.stitch);
assert!(
Invocation::try_parse_from(["moq", "import", "srt", "--listen", "[::]:9000", "--stitch"]).is_err(),
Expand Down
Loading
Loading