Skip to content

Commit 2094349

Browse files
committed
Add configurable publish namespace
1 parent 7ecd52c commit 2094349

8 files changed

Lines changed: 70 additions & 45 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Build output
22
/build/
3+
/build-*/
34
/out/
45
/cmake-build-*/
56

@@ -17,3 +18,5 @@ Testing/
1718
# Tooling
1819
compile_commands.json
1920

21+
# Local test artifacts
22+
/tmp-*.mp4

README.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@ It is buildable and testable today, but it is not yet a full interoperable MOQT
2222
- Isolates MOQT draft-version mapping from the media packaging code
2323
- Builds a picoquic-backed QUIC transport path when local `picoquic` and `picotls` checkouts are available
2424
- Publishes a draft-aware control stream plus per-object streams in the current session layer
25+
- Supports a configurable published track namespace for relay and interop testing
2526

2627
## Current limitations
2728

28-
- No OpenMOQ relay interoperability coverage yet
29+
- External relay interoperability is still incomplete
2930
- Progressive MP4 remux support is intentionally narrow
3031
- Edit lists, richer interleaving cases, and broader timing edge cases are not fully handled yet
3132
- The current remux path synthesizes fragments from `stbl` sample tables but does not attempt a full general-purpose MP4 muxer implementation
32-
- The current MOQT control stream is still a contribution-oriented session mapping, not a finalized interoperable draft wire implementation
33+
- Current Cloudflare relay tests complete setup and namespace announcement, but do not yet result in inbound subscriptions
3334

3435
## Design overview
3536

@@ -124,7 +125,7 @@ This covers:
124125
- fragmented MP4 packaging
125126
- progressive MP4 remux into CMAF-style objects
126127
- MOQT setup encoding and decoding
127-
- binary publish control/object sequencing
128+
- binary namespace announcement plus subscribe-serving control/object sequencing
128129
- QUIC varint boundary coverage
129130

130131
### Picoquic loopback smoke test
@@ -170,19 +171,20 @@ To attempt a live publish against a relay:
170171
```bash
171172
OPENMOQ_PICOQUIC_TRACE=1 ./build/openmoq-publisher \
172173
--input sample.mp4 \
173-
--endpoint moqt://moq-relay.red5.net:8443/moq \
174+
--endpoint moqt://interop-relay.cloudflare.mediaoverquic.com:443/moq \
175+
--namespace interop \
174176
--insecure
175177
```
176178

177179
Current status as of March 12, 2026:
178180

179-
- QUIC handshake succeeds against `moq-relay.red5.net:8443`
180-
- the client sends `CLIENT_SETUP`
181-
- the client does not currently receive `SERVER_SETUP`
182-
- the publish attempt times out waiting for control-stream data
183-
- repeated tests against that relay have also been reported to crash the relay process
181+
- QUIC handshake succeeds against `draft-14.cloudflare.mediaoverquic.com:443` and `interop-relay.cloudflare.mediaoverquic.com:443`
182+
- `CLIENT_SETUP` succeeds and the client prints the negotiated connection ID to stdout after setup
183+
- `PUBLISH_NAMESPACE` is accepted with `PUBLISH_NAMESPACE_OK`
184+
- the current client then waits for inbound `SUBSCRIBE_NAMESPACE` / `SUBSCRIBE`
185+
- those Cloudflare endpoints did not issue subscriptions during these tests, so the publish attempt timed out waiting for control-stream data
184186

185-
Because of that, treat the external relay path as unsafe for routine testing until setup interoperability is resolved.
187+
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.
186188

187189
### Optional picoquic smoke test
188190

@@ -226,17 +228,20 @@ Transport-oriented CLI flags are also present now:
226228
./build/openmoq-publisher \
227229
--input sample.mp4 \
228230
--endpoint localhost:4433 \
231+
--namespace media \
229232
--alpn moq-00 \
230233
--insecure
231234
```
232235

233236
Current status:
234237

235238
- the packaging pipeline is fully usable today
236-
- the session layer now emits typed control messages for setup, namespace publication, and track publication
239+
- the session layer now emits typed control messages for setup, namespace publication, and subscription servicing
237240
- `--endpoint` now enters the real picoquic-backed transport path when the project is built with local picoquic and picotls support
238241
- the local picoquic loopback handshake works, including object publication over QUIC streams
239-
- interoperability against an external OpenMOQ-capable endpoint is still the next transport milestone
242+
- `--namespace` lets you choose the advertised track namespace during transport tests
243+
- after setup completes, the CLI prints `connection_id=<hex>` to stdout
244+
- interoperability against external relays is partially working at setup and namespace announce, but not yet at end-to-end subscription delivery
240245

241246
## Creating Fragmented MP4 with FFmpeg
242247

include/openmoq/publisher/cli_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ struct CliOptions {
1515
std::optional<transport::EndpointConfig> endpoint;
1616
transport::TlsConfig tls;
1717
DraftVersion draft_version = DraftVersion::kDraft14;
18+
std::string track_namespace = "media";
1819
bool dump_plan = false;
1920
};
2021

include/openmoq/publisher/transport/moqt_session.h

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,19 @@ namespace openmoq::publisher::transport {
1313

1414
class MoqtSession {
1515
public:
16-
explicit MoqtSession(PublisherTransport& transport);
16+
explicit MoqtSession(PublisherTransport& transport, std::string track_namespace = "media");
1717

1818
TransportStatus connect(const EndpointConfig& endpoint, const TlsConfig& tls);
1919
TransportStatus publish(const openmoq::publisher::PublishPlan& plan);
2020
TransportStatus close(std::uint64_t application_error_code = 0);
2121

2222
private:
23-
static constexpr std::string_view kDefaultTrackNamespace = "media";
24-
2523
TransportStatus ensure_setup(openmoq::publisher::DraftVersion draft);
2624
TransportStatus ensure_control_stream();
2725
TransportStatus write_frame(std::uint64_t stream_id, std::span<const std::uint8_t> frame, bool fin);
2826

2927
PublisherTransport& transport_;
28+
std::string track_namespace_;
3029
std::optional<EndpointConfig> endpoint_;
3130
std::uint64_t control_stream_id_ = 0;
3231
std::uint64_t peer_max_request_id_ = 0;

src/cli_options.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ CliOptions parse_cli_options(int argc, char** argv) {
8484
options.tls.insecure_skip_verify = true;
8585
} else if (argument == "--draft") {
8686
options.draft_version = parse_draft(require_value("--draft"));
87+
} else if (argument == "--namespace") {
88+
options.track_namespace = std::string(require_value("--namespace"));
8789
} else if (argument == "--emit-dir") {
8890
options.emit_dir = std::filesystem::path(require_value("--emit-dir"));
8991
} else if (argument == "--dump-plan") {
@@ -102,13 +104,16 @@ CliOptions parse_cli_options(int argc, char** argv) {
102104
if (options.endpoint.has_value() && options.endpoint->host.empty()) {
103105
throw std::runtime_error("--alpn requires --endpoint to be provided first");
104106
}
107+
if (options.track_namespace.empty()) {
108+
throw std::runtime_error("--namespace must not be empty");
109+
}
105110

106111
return options;
107112
}
108113

109114
std::string build_usage(const char* argv0) {
110115
return std::string("Usage: ") + argv0 +
111-
" --input <mp4> [--draft 14|16] [--dump-plan] [--emit-dir <dir>]"
116+
" --input <mp4> [--draft 14|16] [--namespace <value>] [--dump-plan] [--emit-dir <dir>]"
112117
" [--endpoint host:port|moqt://host:port/path] [--alpn value]"
113118
" [--cert file] [--key file] [--ca file] [--insecure]";
114119
}

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);
33+
MoqtSession session(transport, options.track_namespace);
3434

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

src/transport/moqt_session.cpp

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ namespace openmoq::publisher::transport {
1515

1616
namespace {
1717

18-
constexpr std::string_view kDefaultTrackNamespaceValue = "media";
19-
2018
bool control_message_complete(std::span<const std::uint8_t> bytes, std::size_t& message_size) {
2119
return next_control_message(bytes, message_size);
2220
}
@@ -183,14 +181,15 @@ TransportStatus serve_subscriptions(PublisherTransport& transport,
183181
std::uint64_t control_stream_id,
184182
const openmoq::publisher::PublishPlan& plan,
185183
const std::map<std::string, PublishedTrack>& tracks_by_name,
186-
openmoq::publisher::DraftVersion draft) {
184+
openmoq::publisher::DraftVersion draft,
185+
std::string_view track_namespace) {
187186
std::vector<std::uint8_t> buffer;
188187
std::set<std::uint64_t> completed_request_ids;
189188
bool fin = false;
190189
bool served_any_subscription = false;
191190
NamespaceMessage namespace_message{
192191
.draft = draft,
193-
.track_namespace = std::string(kDefaultTrackNamespaceValue),
192+
.track_namespace = std::string(track_namespace),
194193
.request_id = 0,
195194
};
196195

@@ -236,7 +235,7 @@ TransportStatus serve_subscriptions(PublisherTransport& transport,
236235
if (!decode_subscribe_namespace_message(message_bytes, subscribe_namespace)) {
237236
return TransportStatus::failure("received invalid SUBSCRIBE_NAMESPACE");
238237
}
239-
if (!namespace_prefix_matches(subscribe_namespace.track_namespace_prefix, kDefaultTrackNamespaceValue)) {
238+
if (!namespace_prefix_matches(subscribe_namespace.track_namespace_prefix, track_namespace)) {
240239
return TransportStatus::failure("peer requested unsupported namespace prefix");
241240
}
242241
const TransportStatus write_status =
@@ -255,7 +254,7 @@ TransportStatus serve_subscriptions(PublisherTransport& transport,
255254
if (!decode_subscribe_message(message_bytes, subscribe)) {
256255
return TransportStatus::failure("received invalid SUBSCRIBE");
257256
}
258-
if (!namespace_matches(subscribe.track_namespace, kDefaultTrackNamespaceValue)) {
257+
if (!namespace_matches(subscribe.track_namespace, track_namespace)) {
259258
return TransportStatus::failure("peer requested unsupported track namespace");
260259
}
261260

@@ -338,7 +337,8 @@ TransportStatus serve_subscriptions(PublisherTransport& transport,
338337

339338
} // namespace
340339

341-
MoqtSession::MoqtSession(PublisherTransport& transport) : transport_(transport) {}
340+
MoqtSession::MoqtSession(PublisherTransport& transport, std::string track_namespace)
341+
: transport_(transport), track_namespace_(std::move(track_namespace)) {}
342342

343343
TransportStatus MoqtSession::connect(const EndpointConfig& endpoint, const TlsConfig& tls) {
344344
endpoint_ = endpoint;
@@ -372,7 +372,7 @@ TransportStatus MoqtSession::publish(const openmoq::publisher::PublishPlan& plan
372372

373373
NamespaceMessage namespace_message{
374374
.draft = plan.draft.version,
375-
.track_namespace = std::string(kDefaultTrackNamespace),
375+
.track_namespace = track_namespace_,
376376
.request_id = 0,
377377
};
378378
status = write_frame(control_stream_id_, encode_namespace_message(namespace_message), false);
@@ -390,7 +390,7 @@ TransportStatus MoqtSession::publish(const openmoq::publisher::PublishPlan& plan
390390
tracks_by_name.emplace(track.name, track);
391391
}
392392

393-
return serve_subscriptions(transport_, control_stream_id_, plan, tracks_by_name, plan.draft.version);
393+
return serve_subscriptions(transport_, control_stream_id_, plan, tracks_by_name, plan.draft.version, track_namespace_);
394394
}
395395

396396
TransportStatus MoqtSession::close(std::uint64_t application_error_code) {

tests/moqt_session_test.cpp

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,13 @@ std::vector<std::uint8_t> encode_publish_namespace_ok_message(std::uint64_t requ
141141
return message;
142142
}
143143

144-
std::vector<std::uint8_t> encode_subscribe_namespace_message(std::uint64_t request_id) {
144+
std::vector<std::uint8_t> encode_subscribe_namespace_message(std::uint64_t request_id, std::string_view track_namespace) {
145145
std::vector<std::uint8_t> payload = encode_varint(request_id);
146146
const std::vector<std::uint8_t> tuple_len = encode_varint(1);
147-
const std::vector<std::uint8_t> component_len = encode_varint(5);
147+
const std::vector<std::uint8_t> component_len = encode_varint(track_namespace.size());
148148
payload.insert(payload.end(), tuple_len.begin(), tuple_len.end());
149149
payload.insert(payload.end(), component_len.begin(), component_len.end());
150-
payload.insert(payload.end(), {'m', 'e', 'd', 'i', 'a'});
150+
payload.insert(payload.end(), track_namespace.begin(), track_namespace.end());
151151
const std::vector<std::uint8_t> parameter_count = encode_varint(0);
152152
payload.insert(payload.end(), parameter_count.begin(), parameter_count.end());
153153

@@ -158,13 +158,15 @@ std::vector<std::uint8_t> encode_subscribe_namespace_message(std::uint64_t reque
158158
return message;
159159
}
160160

161-
std::vector<std::uint8_t> encode_subscribe_message(std::uint64_t request_id, std::string_view track_name) {
161+
std::vector<std::uint8_t> encode_subscribe_message(std::uint64_t request_id,
162+
std::string_view track_namespace,
163+
std::string_view track_name) {
162164
std::vector<std::uint8_t> payload = encode_varint(request_id);
163165
const std::vector<std::uint8_t> tuple_len = encode_varint(1);
164-
const std::vector<std::uint8_t> component_len = encode_varint(5);
166+
const std::vector<std::uint8_t> component_len = encode_varint(track_namespace.size());
165167
payload.insert(payload.end(), tuple_len.begin(), tuple_len.end());
166168
payload.insert(payload.end(), component_len.begin(), component_len.end());
167-
payload.insert(payload.end(), {'m', 'e', 'd', 'i', 'a'});
169+
payload.insert(payload.end(), track_namespace.begin(), track_namespace.end());
168170
const std::vector<std::uint8_t> track_name_length = encode_varint(track_name.size());
169171
payload.insert(payload.end(), track_name_length.begin(), track_name_length.end());
170172
payload.insert(payload.end(), track_name.begin(), track_name.end());
@@ -201,11 +203,13 @@ std::vector<std::uint8_t> encode_publish_ok_message(std::uint64_t request_id) {
201203
return message;
202204
}
203205

204-
void queue_subscribe_requests(MockTransport& transport, std::initializer_list<std::pair<std::uint64_t, std::string>> requests) {
206+
void queue_subscribe_requests(MockTransport& transport,
207+
std::string_view track_namespace,
208+
std::initializer_list<std::pair<std::uint64_t, std::string>> requests) {
205209
transport.reads[0].push_back(encode_publish_namespace_ok_message(0));
206-
transport.reads[0].push_back(encode_subscribe_namespace_message(1));
210+
transport.reads[0].push_back(encode_subscribe_namespace_message(1, track_namespace));
207211
for (const auto& [request_id, track_name] : requests) {
208-
transport.reads[0].push_back(encode_subscribe_message(request_id, track_name));
212+
transport.reads[0].push_back(encode_subscribe_message(request_id, track_namespace, track_name));
209213
}
210214
}
211215

@@ -350,14 +354,15 @@ PublishPlan make_span_backed_plan(DraftVersion draft) {
350354

351355
int main() {
352356
bool ok = true;
357+
constexpr std::string_view kTestTrackNamespace = "interop";
353358

354359
MockTransport transport;
355360
transport.reads[0].push_back(encode_server_setup_message({
356361
.draft = DraftVersion::kDraft14,
357362
.max_request_id = 8,
358363
}));
359-
queue_subscribe_requests(transport, {{2, "catalog"}, {4, "vide_1"}});
360-
MoqtSession session(transport);
364+
queue_subscribe_requests(transport, kTestTrackNamespace, {{2, "catalog"}, {4, "vide_1"}});
365+
MoqtSession session(transport, std::string(kTestTrackNamespace));
361366

362367
const EndpointConfig endpoint{
363368
.host = "example.com",
@@ -392,6 +397,9 @@ int main() {
392397
ok &= expect(path == "/", "expected draft-14 CLIENT_SETUP path");
393398
ok &= expect(max_request_id == 0, "expected draft-14 CLIENT_SETUP max_request_id");
394399
ok &= expect(message_type(transport.writes[1].bytes) == 0x06, "expected PUBLISH_NAMESPACE");
400+
ok &= expect(transport.writes[1].bytes == std::vector<std::uint8_t>({0x06, 0x00, 0x0b, 0x00, 0x01, 0x07, 0x69, 0x6e,
401+
0x74, 0x65, 0x72, 0x6f, 0x70, 0x00}),
402+
"expected namespace write to use the configured track namespace");
395403
ok &= expect(message_type(transport.writes[2].bytes) == 0x12, "expected SUBSCRIBE_NAMESPACE_OK");
396404
ok &= expect(message_type(transport.writes[3].bytes) == 0x04, "expected first SUBSCRIBE_OK");
397405
ok &= expect(transport.writes[4].stream_id == 2, "expected first object stream to be unidirectional stream 2");
@@ -406,12 +414,12 @@ int main() {
406414
ok &= expect(transport.writes[7].fin, "expected second object stream write to set FIN");
407415
ok &= expect(message_type(transport.writes[8].bytes) == 0x0b, "expected second PUBLISH_DONE");
408416
ok &= expect(message_type(transport.writes[9].bytes) == 0x09, "expected PUBLISH_NAMESPACE_DONE");
409-
ok &= expect(transport.writes[9].bytes == std::vector<std::uint8_t>({0x09, 0x00, 0x07, 0x01, 0x05, 0x6d, 0x65,
410-
0x64, 0x69, 0x61}),
411-
"expected draft-14 PUBLISH_NAMESPACE_DONE to contain only the track namespace");
417+
ok &= expect(transport.writes[9].bytes == std::vector<std::uint8_t>({0x09, 0x00, 0x09, 0x01, 0x07, 0x69, 0x6e,
418+
0x74, 0x65, 0x72, 0x6f, 0x70}),
419+
"expected draft-14 PUBLISH_NAMESPACE_DONE to contain the configured track namespace");
412420

413421
MockTransport failing_transport;
414-
MoqtSession failing_session(failing_transport);
422+
MoqtSession failing_session(failing_transport, std::string(kTestTrackNamespace));
415423
status = failing_session.connect(endpoint, tls);
416424
ok &= expect(status.ok, "expected second session connect to succeed");
417425

@@ -423,8 +431,8 @@ int main() {
423431
.draft = DraftVersion::kDraft16,
424432
.max_request_id = 8,
425433
}));
426-
queue_subscribe_requests(draft16_transport, {{2, "catalog"}, {4, "vide_1"}});
427-
MoqtSession draft16_session(draft16_transport);
434+
queue_subscribe_requests(draft16_transport, kTestTrackNamespace, {{2, "catalog"}, {4, "vide_1"}});
435+
MoqtSession draft16_session(draft16_transport, std::string(kTestTrackNamespace));
428436
status = draft16_session.connect(endpoint, tls);
429437
ok &= expect(status.ok, "expected draft-16 session connect to succeed");
430438

@@ -445,6 +453,10 @@ int main() {
445453
ok &= expect(path == "/", "expected draft-16 CLIENT_SETUP path");
446454
ok &= expect(max_request_id == 0, "expected draft-16 CLIENT_SETUP max_request_id");
447455
ok &= expect(message_type(draft16_transport.writes[1].bytes) == 0x06, "expected draft-16 PUBLISH_NAMESPACE");
456+
ok &= expect(draft16_transport.writes[1].bytes == std::vector<std::uint8_t>({0x06, 0x00, 0x0b, 0x00, 0x01, 0x07,
457+
0x69, 0x6e, 0x74, 0x65, 0x72, 0x6f,
458+
0x70, 0x00}),
459+
"expected draft-16 namespace write to use the configured track namespace");
448460
ok &= expect(message_type(draft16_transport.writes[2].bytes) == 0x12, "expected draft-16 SUBSCRIBE_NAMESPACE_OK");
449461
ok &= expect(message_type(draft16_transport.writes[3].bytes) == 0x04, "expected first draft-16 SUBSCRIBE_OK");
450462
ok &= expect(draft16_transport.writes[4].stream_id == 2, "expected first draft-16 object stream");
@@ -466,8 +478,8 @@ int main() {
466478
std::vector<std::uint8_t>(split_server_setup.begin(), split_server_setup.begin() + 3));
467479
segmented_transport.reads[0].push_back(
468480
std::vector<std::uint8_t>(split_server_setup.begin() + 3, split_server_setup.end()));
469-
queue_subscribe_requests(segmented_transport, {{2, "catalog"}, {4, "vide_1"}});
470-
MoqtSession segmented_session(segmented_transport);
481+
queue_subscribe_requests(segmented_transport, kTestTrackNamespace, {{2, "catalog"}, {4, "vide_1"}});
482+
MoqtSession segmented_session(segmented_transport, std::string(kTestTrackNamespace));
471483
status = segmented_session.connect(endpoint, tls);
472484
ok &= expect(status.ok, "expected segmented setup connect to succeed");
473485
status = segmented_session.publish(materialize_publish_plan(make_span_backed_plan(DraftVersion::kDraft14), source_bytes));

0 commit comments

Comments
 (0)