Skip to content

Commit 1f1bcd8

Browse files
committed
Align catalog metadata and add forward publish mode
1 parent 92d73df commit 1f1bcd8

11 files changed

Lines changed: 527 additions & 83 deletions

File tree

README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ It is buildable and testable today, but it is not yet a full interoperable MOQT
1515

1616
- Parses fragmented MP4 input with `ftyp` + `moov` + `moof`/`mdat`
1717
- Remuxes non-fragmented MP4 input into synthesized fragmented media objects
18-
- Extracts basic track metadata and codec identifiers from MP4 sample tables
18+
- Extracts track metadata and RFC 6381 codec identifiers from MP4 sample tables
1919
- Builds a publish plan consisting of initialization and media objects
2020
- Emits planned objects to disk for inspection
2121
- Keeps fragmented input on a zero-copy fast path where possible
@@ -161,9 +161,20 @@ Use `--emit-dir` to inspect the emitted catalog and media objects on disk:
161161
The output directory should currently contain:
162162

163163
- `catalog.json`
164-
- one `.mp4` object file per media object
164+
- one `*_init.mp4` file per media track
165+
- one `*_media.mp4` file per emitted media object
166+
- one `*_probe.mp4` file per emitted media object for direct `ffprobe` use
165167
- `publish-plan.txt`
166168

169+
The current catalog format includes:
170+
171+
- `role` with values such as `video` and `audio`
172+
- RFC 6381 `codec` strings such as `avc1.64000C` and `mp4a.40.2`
173+
- `renderGroup` and `isLive`
174+
- `width` and `height` for video tracks
175+
- `sampleRate` and `channelCount` for audio tracks
176+
- base64-encoded codec `initData` per track
177+
167178
### Relay interoperability test
168179

169180
To attempt a live publish against a relay:
@@ -173,6 +184,7 @@ OPENMOQ_PICOQUIC_TRACE=1 ./build/openmoq-publisher \
173184
--input sample.mp4 \
174185
--endpoint moqt://interop-relay.cloudflare.mediaoverquic.com:443/moq \
175186
--namespace interop \
187+
--forward 0 \
176188
--insecure
177189
```
178190

@@ -181,7 +193,7 @@ Current status as of March 12, 2026:
181193
- QUIC handshake succeeds against `draft-14.cloudflare.mediaoverquic.com:443` and `interop-relay.cloudflare.mediaoverquic.com:443`
182194
- `CLIENT_SETUP` succeeds and the client prints the negotiated connection ID to stdout after setup
183195
- `PUBLISH_NAMESPACE` is accepted with `PUBLISH_NAMESPACE_OK`
184-
- the current client then waits for inbound `SUBSCRIBE_NAMESPACE` / `SUBSCRIBE`
196+
- with `--forward 0`, the current client then waits for inbound `SUBSCRIBE_NAMESPACE` / `SUBSCRIBE`
185197
- those Cloudflare endpoints did not issue subscriptions during these tests, so the publish attempt timed out waiting for control-stream data
186198

187199
The older `moq-relay.red5.net:8443` endpoint behaved worse in earlier testing: setup did not complete reliably, and repeated malformed-control testing was reported to crash the relay. Prefer the Cloudflare endpoints for routine interop checks.
@@ -229,6 +241,7 @@ Transport-oriented CLI flags are also present now:
229241
--input sample.mp4 \
230242
--endpoint localhost:4433 \
231243
--namespace media \
244+
--forward 0 \
232245
--alpn moq-00 \
233246
--insecure
234247
```
@@ -240,6 +253,7 @@ Current status:
240253
- `--endpoint` now enters the real picoquic-backed transport path when the project is built with local picoquic and picotls support
241254
- the local picoquic loopback handshake works, including object publication over QUIC streams
242255
- `--namespace` lets you choose the advertised track namespace during transport tests
256+
- `--forward 0|1` selects whether the publisher waits for `SUBSCRIBE` (`0`) or immediately sends `PUBLISH` requests and forwards objects after namespace announce (`1`)
243257
- after setup completes, the CLI prints `connection_id=<hex>` to stdout
244258
- interoperability against external relays is partially working at setup and namespace announce, but not yet at end-to-end subscription delivery
245259

include/openmoq/publisher/cli_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ struct CliOptions {
1616
transport::TlsConfig tls;
1717
DraftVersion draft_version = DraftVersion::kDraft14;
1818
std::string track_namespace = "media";
19+
bool forward = false;
1920
bool dump_plan = false;
2021
};
2122

include/openmoq/publisher/mp4_box.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,14 @@ struct TrackDescription {
2525
std::uint32_t track_id = 0;
2626
std::string handler_type;
2727
std::string codec;
28+
std::string sample_entry_type;
2829
std::string track_name;
30+
std::uint32_t timescale = 0;
31+
std::uint32_t width = 0;
32+
std::uint32_t height = 0;
33+
std::uint32_t channel_count = 0;
34+
std::uint32_t sample_rate = 0;
35+
double frame_rate = 0.0;
2936
};
3037

3138
struct ParsedMp4 {

include/openmoq/publisher/transport/moqt_session.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ namespace openmoq::publisher::transport {
1313

1414
class MoqtSession {
1515
public:
16-
explicit MoqtSession(PublisherTransport& transport, std::string track_namespace = "media");
16+
explicit MoqtSession(PublisherTransport& transport,
17+
std::string track_namespace = "media",
18+
bool auto_forward = false);
1719

1820
TransportStatus connect(const EndpointConfig& endpoint, const TlsConfig& tls);
1921
TransportStatus publish(const openmoq::publisher::PublishPlan& plan);
@@ -26,6 +28,7 @@ class MoqtSession {
2628

2729
PublisherTransport& transport_;
2830
std::string track_namespace_;
31+
bool auto_forward_ = false;
2932
std::optional<EndpointConfig> endpoint_;
3033
std::uint64_t control_stream_id_ = 0;
3134
std::uint64_t peer_max_request_id_ = 0;

src/cli_options.cpp

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ transport::EndpointConfig parse_endpoint(std::string_view value) {
4949
return endpoint;
5050
}
5151

52+
bool parse_forward_flag(std::string_view value) {
53+
if (value == "0") {
54+
return false;
55+
}
56+
if (value == "1") {
57+
return true;
58+
}
59+
throw std::runtime_error("unsupported --forward value: expected 0 or 1");
60+
}
61+
5262
} // namespace
5363

5464
CliOptions parse_cli_options(int argc, char** argv) {
@@ -86,6 +96,8 @@ CliOptions parse_cli_options(int argc, char** argv) {
8696
options.draft_version = parse_draft(require_value("--draft"));
8797
} else if (argument == "--namespace") {
8898
options.track_namespace = std::string(require_value("--namespace"));
99+
} else if (argument == "--forward") {
100+
options.forward = parse_forward_flag(require_value("--forward"));
89101
} else if (argument == "--emit-dir") {
90102
options.emit_dir = std::filesystem::path(require_value("--emit-dir"));
91103
} else if (argument == "--dump-plan") {
@@ -113,7 +125,7 @@ CliOptions parse_cli_options(int argc, char** argv) {
113125

114126
std::string build_usage(const char* argv0) {
115127
return std::string("Usage: ") + argv0 +
116-
" --input <mp4> [--draft 14|16] [--namespace <value>] [--dump-plan] [--emit-dir <dir>]"
128+
" --input <mp4> [--draft 14|16] [--namespace <value>] [--forward 0|1] [--dump-plan] [--emit-dir <dir>]"
117129
" [--endpoint host:port|moqt://host:port/path] [--alpn value]"
118130
" [--cert file] [--key file] [--ca file] [--insecure]";
119131
}

src/cmsf_packager.cpp

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <fstream>
66
#include <iomanip>
77
#include <algorithm>
8+
#include <cmath>
89
#include <map>
910
#include <sstream>
1011
#include <stdexcept>
@@ -122,7 +123,7 @@ std::string base64_encode(std::span<const std::uint8_t> bytes) {
122123
return encoded;
123124
}
124125

125-
std::string track_kind(std::string_view handler_type) {
126+
std::string track_role(std::string_view handler_type) {
126127
if (handler_type == "vide") {
127128
return "video";
128129
}
@@ -132,6 +133,17 @@ std::string track_kind(std::string_view handler_type) {
132133
return "data";
133134
}
134135

136+
std::string json_number(double value) {
137+
std::ostringstream out;
138+
const double rounded = std::round(value);
139+
if (std::fabs(value - rounded) < 0.0005) {
140+
out << static_cast<long long>(rounded);
141+
} else {
142+
out << std::fixed << std::setprecision(3) << value;
143+
}
144+
return out.str();
145+
}
146+
135147
std::uint32_t track_id_from_trak(const Mp4Box& trak, std::span<const std::uint8_t> bytes) {
136148
const Mp4Box* tkhd = find_child_box(trak, "tkhd");
137149
if (tkhd == nullptr || tkhd->payload.size < 20) {
@@ -305,6 +317,7 @@ PublishPlan build_publish_plan(const SegmentedMp4& segmented_mp4, DraftVersion v
305317
.track_id = 0,
306318
.handler_type = "meta",
307319
.codec = "catalog",
320+
.sample_entry_type = "catalog",
308321
.track_name = "catalog",
309322
});
310323

@@ -343,11 +356,24 @@ PublishPlan build_publish_plan(const SegmentedMp4& segmented_mp4, DraftVersion v
343356
catalog << '{'
344357
<< "\"name\":\"" << json_escape(track.track_name) << "\","
345358
<< "\"id\":" << track.track_id << ','
346-
<< "\"kind\":\"" << track_kind(track.handler_type) << "\","
347-
<< "\"handler\":\"" << json_escape(track.handler_type) << "\","
359+
<< "\"role\":\"" << track_role(track.handler_type) << "\","
348360
<< "\"codec\":\"" << json_escape(track.codec) << "\","
349361
<< "\"packaging\":\"cmaf\","
350-
<< "\"initData\":\"" << init_data_by_track.at(track.track_name) << "\""
362+
<< "\"renderGroup\":1,"
363+
<< "\"isLive\":false,";
364+
if (track.handler_type == "vide") {
365+
catalog << "\"width\":" << track.width << ','
366+
<< "\"height\":" << track.height;
367+
if (track.frame_rate > 0.0) {
368+
catalog << ','
369+
<< "\"frameRate\":" << json_number(track.frame_rate);
370+
}
371+
catalog << ',';
372+
} else if (track.handler_type == "soun") {
373+
catalog << "\"sampleRate\":" << track.sample_rate << ','
374+
<< "\"channelCount\":" << track.channel_count << ',';
375+
}
376+
catalog << "\"initData\":\"" << init_data_by_track.at(track.track_name) << "\""
351377
<< '}';
352378
}
353379
catalog << "]}";

src/main.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ int main(int argc, char** argv) {
3030

3131
const PublishPlan materialized_plan = materialize_publish_plan(plan, parsed_mp4.bytes);
3232
PicoquicClient transport;
33-
MoqtSession session(transport, options.track_namespace);
33+
MoqtSession session(transport, options.track_namespace, options.forward);
3434

3535
TransportStatus status = session.connect(*options.endpoint, options.tls);
3636
if (!status.ok) {

0 commit comments

Comments
 (0)