Skip to content

Commit f5f5f6c

Browse files
committed
Make SAP tracks opt-in
1 parent 729b770 commit f5f5f6c

8 files changed

Lines changed: 182 additions & 49 deletions

File tree

README.md

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ It packages MP4 input into CMSF-style publishable objects, supports MOQT draft-s
1212
- Builds a publish plan with catalog, SAP event timeline, and media objects
1313
- Emits generated objects and catalog metadata to disk for inspection
1414
- Supports a configurable track namespace, optional paced publication, and draft-aware MOQT control/object encoding
15+
- Can optionally add per-track SAP event timeline metadata with a CLI flag
1516
- Includes packaging, CLI, and MOQT session tests through CTest
1617

1718
## Design overview
@@ -102,6 +103,34 @@ Useful CMake options:
102103
- `-DOPENMOQ_PICOQUIC_SOURCE_DIR=/path/to/picoquic`
103104
- `-DOPENMOQ_RUN_PICOQUIC_SMOKE_TESTS=ON|OFF`
104105

106+
## Quick Start
107+
108+
If you already have a sample MP4 and just want to see what the publisher does, these are the most useful first commands:
109+
110+
Inspect the publish plan with the default settings:
111+
112+
```bash
113+
./build/openmoq-publisher --input sample.mp4 --dump-plan
114+
```
115+
116+
Inspect the same input with SAP event timeline metadata enabled:
117+
118+
```bash
119+
./build/openmoq-publisher --input sample.mp4 --sap --dump-plan
120+
```
121+
122+
Emit the catalog and media objects to disk:
123+
124+
```bash
125+
./build/openmoq-publisher --input sample.mp4 --emit-dir out/
126+
```
127+
128+
Stream the input over stdin instead of reading it from a file path:
129+
130+
```bash
131+
cat sample.mp4 | ./build/openmoq-publisher --input - --dump-plan
132+
```
133+
105134
## How To Test
106135

107136
### Packaging and session tests
@@ -130,6 +159,7 @@ Publish-plan numbering note:
130159
- `group_id` is allocated per track, not across all tracks, so interleaved audio and video fragments can both use `0, 1, 2, ...`
131160
- by default, `object_id` advances within a group when CMAF content is split into multiple MOQT objects for lower latency
132161
- `--coalesce-cmaf-chunks` forces `object_id = 0` for the current one-object-per-group fallback
162+
- SAP event timeline tracks are disabled by default; add `--sap` when you want catalog and metadata objects for `*_sap` tracks
133163

134164
If you want the packaging and transport tests without the CLI target, use the secondary build tree:
135165

@@ -162,13 +192,26 @@ Input source note:
162192
- `--input <path>` reads an MP4 from a regular file
163193
- `--input -` reads the MP4 byte stream from standard input, which allows `cat`, `ffmpeg`, or other producer pipelines to feed the publisher directly
164194
- fragmented and progressive MP4 inputs are both supported through either source type
195+
- SAP event timeline tracks are disabled by default; add `--sap` to include `*_sap` metadata tracks and objects in the publish plan
165196

166197
Use `--dump-plan` to inspect the generated publish plan without touching the network:
167198

168199
```bash
169200
./build/openmoq-publisher --input sample.mp4 --draft 14 --dump-plan
170201
```
171202

203+
Include SAP event timeline output explicitly:
204+
205+
```bash
206+
./build/openmoq-publisher --input sample.mp4 --draft 14 --sap --dump-plan
207+
```
208+
209+
The difference is:
210+
211+
- default output includes the `catalog` object plus media objects
212+
- `--sap` additionally creates `*_sap` metadata tracks and objects
213+
- this affects both `--dump-plan` output and emitted files under `--emit-dir`
214+
172215
Use stdin when the source is already being produced by another command:
173216

174217
```bash
@@ -192,15 +235,24 @@ Use `--emit-dir` to inspect the emitted catalog and media objects on disk:
192235
./build/openmoq-publisher --input sample.mp4 --draft 14 --emit-dir out/
193236
```
194237

238+
Emit the same plan with SAP metadata enabled:
239+
240+
```bash
241+
./build/openmoq-publisher --input sample.mp4 --draft 14 --sap --emit-dir out/
242+
```
243+
195244
The output directory should contain:
196245

197246
- `catalog.json`
198-
- one `*_sap_g*_o*.json` file per emitted SAP event timeline object
199247
- one `*_init.mp4` file per media track
200248
- one `*_media.mp4` file per emitted media object
201249
- one `*_probe.mp4` file per emitted media object for direct `ffprobe` use
202250
- `publish-plan.txt`
203251

252+
When `--sap` is enabled, the output directory also contains:
253+
254+
- one `*_sap_g*_o*.json` file per emitted SAP event timeline object
255+
204256
The catalog format includes:
205257

206258
- `role` with values such as `video` and `audio`
@@ -211,6 +263,13 @@ The catalog format includes:
211263
- `sampleRate` and `channelCount` for audio tracks
212264
- base64-encoded per-track CMAF initialization segment (`ftyp` + `moov`) in `initData`
213265

266+
When `--sap` is enabled, `catalog.json` also includes:
267+
268+
- one `*_sap` track per media track
269+
- `packaging: "eventtimeline"`
270+
- `eventType: "org.ietf.moq.cmsf.sap"`
271+
- `depends` pointing back to the corresponding media track
272+
214273
### Relay interoperability test
215274

216275
To attempt a live publish against a relay:
@@ -226,6 +285,20 @@ OPENMOQ_PICOQUIC_TRACE=1 ./build/openmoq-publisher \
226285
--insecure
227286
```
228287

288+
Publish the same stream with SAP timeline tracks included:
289+
290+
```bash
291+
OPENMOQ_PICOQUIC_TRACE=1 ./build/openmoq-publisher \
292+
--input sample.mp4 \
293+
--endpoint moqt://interop-relay.cloudflare.mediaoverquic.com:443/moq \
294+
--namespace interop \
295+
--forward 0 \
296+
--timeout 10 \
297+
--paced \
298+
--sap \
299+
--insecure
300+
```
301+
229302
If you need to connect to a relay by IP while still presenting the relay hostname in TLS SNI:
230303

231304
```bash
@@ -244,6 +317,7 @@ Behavior notes:
244317
- `--forward 0` waits for inbound `SUBSCRIBE` requests before sending matching media objects
245318
- with `--forward 0`, subscribers are still expected to request tracks explicitly; by default that includes subscribing to `catalog` if they need track discovery
246319
- `--publish-catalog` keeps `--forward 0` for media tracks but proactively publishes the `catalog` track through the normal `PUBLISH` / `PUBLISH_OK` path so downstream consumers can discover available tracks without first subscribing to `catalog`
320+
- `--sap` adds per-track `*_sap` event timeline tracks and metadata objects; by default those tracks are not created
247321
- media packaging defaults to lower-latency split MOQT objects per group when chunk/sample boundaries are available
248322
- `--coalesce-cmaf-chunks` disables that split and falls back to one media object per group
249323
- when multiple tracks are subscribed, matching objects are served in publish-plan order so time-aligned audio/video stay interleaved instead of draining one track before the next
@@ -272,12 +346,24 @@ Inspect the publish plan for an already fragmented MP4:
272346
./build/openmoq-publisher --input sample-fragmented.mp4 --draft 14 --dump-plan
273347
```
274348

349+
Inspect the same fragmented MP4 with SAP metadata enabled:
350+
351+
```bash
352+
./build/openmoq-publisher --input sample-fragmented.mp4 --draft 14 --sap --dump-plan
353+
```
354+
275355
Emit object payloads for a progressive MP4 after remux:
276356

277357
```bash
278358
./build/openmoq-publisher --input sample-progressive.mp4 --draft 14 --emit-dir out/
279359
```
280360

361+
Emit a progressive MP4 after remux with SAP metadata enabled:
362+
363+
```bash
364+
./build/openmoq-publisher --input sample-progressive.mp4 --draft 14 --sap --emit-dir out/
365+
```
366+
281367
Try the draft-16 compatibility profile:
282368

283369
```bash
@@ -297,6 +383,20 @@ Transport-oriented CLI flags are also present now:
297383
--insecure
298384
```
299385

386+
If you want subscribers or downstream tools to see SAP event timeline tracks, add `--sap`:
387+
388+
```bash
389+
./build/openmoq-publisher \
390+
--input sample.mp4 \
391+
--endpoint localhost:4433 \
392+
--namespace media \
393+
--forward 0 \
394+
--timeout 3 \
395+
--paced \
396+
--sap \
397+
--insecure
398+
```
399+
300400
The same CLI accepts stdin for transport publishing as well:
301401

302402
```bash
@@ -315,6 +415,11 @@ Chunk/object mapping:
315415
- default behavior is lower-latency split publication, which emits multiple MOQT objects in the same group when CMAF chunk/sample boundaries are available
316416
- `--coalesce-cmaf-chunks` restores one media object per group
317417

418+
SAP metadata:
419+
420+
- by default the catalog only includes media tracks plus the top-level `catalog` object
421+
- `--sap` adds per-track `*_sap` event timeline entries to the catalog and emits matching metadata objects
422+
318423
ALPN selection:
319424

320425
- draft-14 defaults to `moq-00`
@@ -324,7 +429,7 @@ ALPN selection:
324429
Catalog note:
325430

326431
- `catalog.json` uses the CMSF-style `role` field such as `video` and `audio`
327-
- `catalog.json` also advertises per-track SAP event timeline tracks using CMSF `eventtimeline` metadata
432+
- with `--sap`, `catalog.json` also advertises per-track SAP event timeline tracks using CMSF `eventtimeline` metadata
328433
- `publish-plan.txt` and `--dump-plan` still print an internal debug `kind=` label for object type (`catalog`, `metadata`, or `media`); that debug label is not part of the catalog spec
329434

330435
## Creating Fragmented MP4 with FFmpeg

include/openmoq/publisher/cli_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ struct CliOptions {
3030
bool endpoint_alpn_overridden = false;
3131
bool forward = false;
3232
bool publish_catalog = false;
33+
bool include_sap = false;
3334
bool split_cmaf_chunks = true;
3435
bool paced = false;
3536
bool dump_plan = false;

include/openmoq/publisher/cmsf_packager.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ struct PublishPlan {
4040
std::vector<CmsfObject> objects;
4141
};
4242

43-
PublishPlan build_publish_plan(const SegmentedMp4& segmented_mp4, DraftVersion version);
43+
PublishPlan build_publish_plan(const SegmentedMp4& segmented_mp4, DraftVersion version, bool include_sap = false);
4444
std::string render_publish_plan(const PublishPlan& plan);
4545
PublishPlan materialize_publish_plan(const PublishPlan& plan, std::span<const std::uint8_t> bytes);
4646
void emit_plan_objects(const PublishPlan& plan,

src/cli_options.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ CliOptions parse_cli_options(int argc, char** argv) {
121121
options.forward = parse_forward_flag(require_value("--forward"));
122122
} else if (argument == "--publish-catalog") {
123123
options.publish_catalog = true;
124+
} else if (argument == "--sap") {
125+
options.include_sap = true;
124126
} else if (argument == "--coalesce-cmaf-chunks" || argument == "--coalesce-cmaf-chunk") {
125127
options.split_cmaf_chunks = false;
126128
} else if (argument == "--timeout") {
@@ -155,7 +157,7 @@ CliOptions parse_cli_options(int argc, char** argv) {
155157
std::string build_usage(const char* argv0) {
156158
return std::string("Usage: ") + argv0 +
157159
" --input <mp4|-> [--draft 14|16] [--namespace <value>] [--forward 0|1] [--timeout <seconds>]"
158-
" [--publish-catalog] [--coalesce-cmaf-chunks|--coalesce-cmaf-chunk] [--paced] [--dump-plan] [--emit-dir <dir>]"
160+
" [--publish-catalog] [--sap] [--coalesce-cmaf-chunks|--coalesce-cmaf-chunk] [--paced] [--dump-plan] [--emit-dir <dir>]"
159161
" [--endpoint host:port|moqt://host:port/path] [--alpn value] [--sni value]"
160162
" [--cert file] [--key file] [--ca file] [--insecure]";
161163
}

src/cmsf_packager.cpp

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -420,21 +420,23 @@ std::vector<std::uint8_t> build_track_codec_init_data(std::span<const std::uint8
420420

421421
} // namespace
422422

423-
PublishPlan build_publish_plan(const SegmentedMp4& segmented_mp4, DraftVersion version) {
423+
PublishPlan build_publish_plan(const SegmentedMp4& segmented_mp4, DraftVersion version, bool include_sap) {
424424
std::vector<TrackDescription> tracks = segmented_mp4.tracks;
425-
std::uint32_t synthetic_track_id = next_synthetic_track_id(tracks);
426-
for (const auto& media_track : segmented_mp4.tracks) {
427-
tracks.push_back(TrackDescription{
428-
.track_id = synthetic_track_id++,
429-
.handler_type = "meta",
430-
.codec = {},
431-
.sample_entry_type = "eventtimeline",
432-
.track_name = sap_track_name(media_track.track_name),
433-
.packaging = "eventtimeline",
434-
.event_type = "org.ietf.moq.cmsf.sap",
435-
.mime_type = "application/json",
436-
.depends = {media_track.track_name},
437-
});
425+
if (include_sap) {
426+
std::uint32_t synthetic_track_id = next_synthetic_track_id(tracks);
427+
for (const auto& media_track : segmented_mp4.tracks) {
428+
tracks.push_back(TrackDescription{
429+
.track_id = synthetic_track_id++,
430+
.handler_type = "meta",
431+
.codec = {},
432+
.sample_entry_type = "eventtimeline",
433+
.track_name = sap_track_name(media_track.track_name),
434+
.packaging = "eventtimeline",
435+
.event_type = "org.ietf.moq.cmsf.sap",
436+
.mime_type = "application/json",
437+
.depends = {media_track.track_name},
438+
});
439+
}
438440
}
439441
tracks.insert(tracks.begin(), TrackDescription{
440442
.track_id = 0,
@@ -511,18 +513,20 @@ PublishPlan build_publish_plan(const SegmentedMp4& segmented_mp4, DraftVersion v
511513
});
512514
}
513515

514-
for (const auto& media_track : segmented_mp4.tracks) {
515-
if (find_track_by_name(plan.tracks, sap_track_name(media_track.track_name)) == nullptr) {
516-
continue;
516+
if (include_sap) {
517+
for (const auto& media_track : segmented_mp4.tracks) {
518+
if (find_track_by_name(plan.tracks, sap_track_name(media_track.track_name)) == nullptr) {
519+
continue;
520+
}
521+
plan.objects.push_back({
522+
.kind = CmsfObjectKind::kMetadata,
523+
.track_name = sap_track_name(media_track.track_name),
524+
.group_id = 0,
525+
.object_id = 0,
526+
.payload = {},
527+
.owned_payload = build_sap_timeline_payload(segmented_mp4, media_track),
528+
});
517529
}
518-
plan.objects.push_back({
519-
.kind = CmsfObjectKind::kMetadata,
520-
.track_name = sap_track_name(media_track.track_name),
521-
.group_id = 0,
522-
.object_id = 0,
523-
.payload = {},
524-
.owned_payload = build_sap_timeline_payload(segmented_mp4, media_track),
525-
});
526530
}
527531

528532
return plan;

src/main.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ int main(int argc, char** argv) {
1919
const SegmentedMp4 segmented_mp4 = segment_for_cmaf(parsed_mp4,
2020
options.split_cmaf_chunks ? CmafObjectMode::kSplit
2121
: CmafObjectMode::kCoalesced);
22-
const PublishPlan plan = build_publish_plan(segmented_mp4, options.draft_version);
22+
const PublishPlan plan = build_publish_plan(segmented_mp4, options.draft_version, options.include_sap);
2323

2424
if (options.dump_plan || !options.emit_dir.has_value()) {
2525
std::cout << render_publish_plan(plan);

tests/cli_options_test.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ int main() {
4242
"expected file input to remain the default input source kind");
4343
ok &= expect(options.input_source.path == "sample.mp4",
4444
"expected file input path to be preserved");
45+
ok &= expect(!options.include_sap, "expected SAP track creation to be disabled by default");
4546
}
4647

4748
{
@@ -74,6 +75,12 @@ int main() {
7475
ok &= expect(options.publish_catalog, "expected --publish-catalog to enable proactive catalog publish");
7576
}
7677

78+
{
79+
const CliOptions options =
80+
parse({"openmoq-publisher", "--input", "sample.mp4", "--sap"});
81+
ok &= expect(options.include_sap, "expected --sap to enable SAP track creation");
82+
}
83+
7784
{
7885
const CliOptions options =
7986
parse({"openmoq-publisher", "--input", "sample.mp4", "--coalesce-cmaf-chunks"});

0 commit comments

Comments
 (0)