This project keeps draft-ietf-moq-transport-14 as the primary publisher profile and treats draft-ietf-moq-transport-16 as a secondary compatibility target.
- Namespace subscription responses are modeled as the dedicated
SUBSCRIBE_NAMESPACE_OKandSUBSCRIBE_NAMESPACE_ERRORflow. - Namespace overlap handling is documented against
NAMESPACE_PREFIX_OVERLAP. - Publisher-side namespace acceptance is modeled with draft-14 style
PUBLISH_NAMESPACE_OKandPUBLISH_NAMESPACE_ERROR. - Draft-14 control messages use a
u16outerLengthfield, includingPUBLISH,PUBLISH_OK, andPUBLISH_ERROR; only inner fields explicitly marked(i)remain QUIC varints.
- Some request/response handling moved to the generic
REQUEST_OKandREQUEST_ERRORflow. SUBSCRIBE_NAMESPACEuses au16outerLengthfield and carriesSubscribe Optionsbefore its message parameters.Subscribe Optionsaffects whether namespace advertisements, publish advertisements, or both are requested.- Message parameters in
SUBSCRIBE,PUBLISH_OK, and related draft-16 messages are delta-encoded key-value pairs. Even parameter types carry a varint value directly; odd parameter types carry a varint length followed by bytes. PUBLISH_OKapplies the draft-16 defaults when parameters are omitted:FORWARD=1,SUBSCRIBER_PRIORITY=128, no explicitGROUP_ORDER, and no subscription filter.- Namespace overlap handling is expressed through the generic request error path rather than the older dedicated response message family.
- Initialization data is represented either as a binary payload or as a dedicated MOQT track with one group and one object.
- Media objects follow the fast path of
styp? + moof + mdat, withmoof/mdatreused directly from fragmented MP4 input. - The current implementation uses one media object per fragment/group, which aligns with the fragment-to-group mapping in the current MOQT CMAF packaging draft.
- The catalog is a
draft-ietf-moq-msf-01version 1 document.versionis the String"1"; there is no rootformatfield. - Initialization data lives in the root
initDataListarray, referenced from each track byinitRef, and is serialized aftertracks. - Media tracks carry
packagingofcmafperdraft-ietf-moq-cmsf-01section 3.5.1, along withmaxGrpSapStartingTypeandmaxObjSapStartingType. - All catalog JSON is produced by
serialize_catalog()insrc/msf_catalog.cpp. Do not hand-assemble catalog JSON. - The
MsfCatalog::publish_tracksfield models the rootpublishTracksarray (MSF section 5.1.5), but no emitter populates it yet. - Catalog track lifecycle (MSF sections 5 and 11.3), owned by
CatalogPublisherinsrc/msf_catalog.cpp:- Object 0 of every group holds a full independent catalog; producing an independent catalog always starts a new group.
- Delta updates occupy object IDs 1 and above within the current group
(they never start a new group), and only
addandremoveoperations are emitted --cloneis not implemented, since it applies when a new track matches an existing one on every field except name, which no producer in this project generates. Scope: delta updates only happen at all insideMoqtSession's twopublish_live()overloads (the SRT-ingest and stdin-ingest live paths), the only callers that ever callCatalogPublisher::publish()more than once for a session. The batchpublish()plan path andpublish_live_objects()do not populateCatalogPublisher, so a broadcast run through either always gets a single static, one-shot catalog with no deltas. No delta is emitted on the wire today, even on the twopublish_live()paths: both build oneLiveCatalog/MsfCatalogsnapshot at startup and callcatalog_publisher_.publish()with that same immutable value on every SUBSCRIBE and republish tick, socatalogs_equal()short-circuits after the first call andpublish()always returns{}afterward. The add/remove differ and its object-ID/group-ID bookkeeping are exercised only from unit tests (tests/msf_catalog_test.cpp), not from any production code path. Making a delta actually reach the wire needs a live producer that detects a track add/remove mid-broadcast (SRT ingest noticing a track appear/disappear, or a DASH ingest reconfiguration) and callscatalog_publisher_.publish()again with an updatedMsfCatalog; wiring that producer is out of scope for the current phase. - Every catalog object, independent or delta, maps to MOQT sub-group 0.
- Section 5.3 freezes a track's attributes once its namespace-and-name
tuple has been declared: an attribute change on an existing track cannot
be expressed as a delta, so
CatalogPublisher::publish()falls back to a full independent catalog whenever it detects one, rather than silently dropping the change or emitting an invalid delta. CatalogPublisher::force_independent()re-emits the last published catalog as a fresh independent copy in a new group, for section 5.3's "periodically publish a new independent catalog" guidance and for section 5's cache-expiry republication;MoqtSessioncalls it onPublisherConfig::catalog_republish_intervalwhen configured non-zero (default zero preserves one-shot delivery). Scope: as with deltas, this is wired only inside the twopublish_live()overloads; the batchpublish()path andpublish_live_objects()never republish. Operator note: draft-ietf-moq-transport-19 section 10.11 states "A sender MUST NOT send PUBLISH_DONE until it has closed all streams it will ever open ... for a subscription."MoqtSession::end_broadcast()can open a further independent-catalog stream on the catalog track's alias at any point before the session ends, on every live session, regardless ofcatalog_republish_interval-- soMoqtSessionalways defers PUBLISH_DONE for the catalog subscription, not only when republication is enabled. It is sent once, when thepublish_live()polling loop actually winds down (natural end-of-input, or afterend_broadcast()has endedCatalogPublisherand no further stream can open), never immediately at SUBSCRIBE time. (An earlier revision of this document, and of the code, gated the deferral oncatalog_republish_intervalbeing non-zero; that left the default configuration -- republication off -- sending PUBLISH_DONE immediately, whichend_broadcast()could then violate by opening one more stream afterward. Fixed: the deferral no longer depends on that setting.) Operators should expect the catalog subscription to stay open, from the receiver's point of view, for the life of the broadcast rather than close immediately after the first catalog object.MoqtSession::end_broadcast()(MSF section 11.3) uses the sameCatalogPublisherto publish the final independent catalog when a live broadcast ends:isComplete: truewith an emptytracksarray forkTerminate, orisLive: false(withtrackDurationfor tracks whose duration is known) forkConvertToVod. This is wired only for the twoMoqtSession::publish_live()overloads, which are the only callers that populateCatalogPublisherin the first place; a session driven through the batchpublish()plan path or throughpublish_live_objects()does not populate it, andend_broadcast()correctly skips the final-catalog write there rather than guess which track alias is "catalog". Draft conflict on ordering: MSF section 11.3 lists SUBSCRIBE_DONE (i.e. PUBLISH_DONE, in transport terms) before the final catalog object in its end-of-broadcast bullet order. This implementation sends the final catalog object first and the catalog subscription's PUBLISH_DONE afterward -- the reverse of MSF's order -- because MOQT draft-ietf-moq-transport-19 section 10.11 forbids sending PUBLISH_DONE before every stream a subscription will ever open has closed, and the final catalog stream is one such stream. Where the two drafts disagree, the transport draft governs what may legally appear on the wire, so its ordering wins.
- Not implemented:
clonedelta operations, MSF sections 9/10 log and metrics tracks, and MSF section 12 compression signalling. CMSF section 4 content protection (see the dedicated section below) is implemented for the batch/VOD publish path and, for detection and signalling, the live paths that receive a real CMAF initialization segment (DASH CTE ingest and live stdin ingest); SRT ingest cannot carry CENC metadata. MSF section 11.1 URL parsing has shipped; see## MSF URLs and fragmentsbelow. - The MSFTS example (
examples/msfts-publisher) publishespackaging: "m2ts", which is not one of the values in MSF v1's Table 3; it is defined bydraft-gregoire-moq-msfts(examples/msfts-publisher/docs/) and is correct only for that draft's tracks. MSF v1 establishes no IANA registry for packaging values, so extensions add them by normative statement in their own document -- CMSF does exactly the same forcmaf. A validator that treats MSF Table 3 as a closed list would reject both. - That draft now tracks
draft-ietf-moq-msf-01. It previously referencedmsf-00, which differed in two ways that reach the wire: the catalogversionfield was a JSON Number rather than a String (MSF 5.1.1), and initialization data was a track-levelinitDatafield rather than the rootinitDataListplus a trackinitRef(MSF 5.1.7 and 5.2.13). The example always emitted the MSF v1 shapes, because it builds through the sharedserialize_catalog; it was the draft text that lagged. Them2ts*track fields are producer extensions permitted by MSF section 5 and collide with nothing.
draft-ietf-moq-cmsf-01 section 4. Implemented for the batch/VOD publish
path, and for detection and signalling on the live paths that receive a real
CMAF initialization segment (the DASH CTE ingest and the live stdin path);
the publisher never decrypts and never encrypts anywhere in this
project -- it detects and signals protection already present in its input.
-
Protection data lives at the catalog root as
contentProtections(CMSF 4.1.1), never duplicated onto a track. Each protected track instead carriescontentProtectionRefIDs(CMSF 4.1.2) pointing at the root entries byrefID.attach_content_protection(src/msf_catalog.cpp) reuses an existing root entry when its system ID and scheme already match, rather than emitting a duplicate, so multiple tracks sharing a KID share one entry. A protected track (TrackDescription::protectionpopulated) whose init segment carries nopsshsystem at all is refused bybuild_publish_plan(src/cmsf_packager.cpp) rather than silently emitted with nocontentProtectionsentry: CMSF 4.1.2 defines an absentcontentProtectionRefIDsas meaning the track is unprotected, so publishing one for genuinely encrypted content would assert the opposite of the truth.psshis only SHOULD-present (CMSF 4.1.1.4.5); ffmpeg's-encryption_scheme cenc-aes-ctris a real encoder that omits it entirely, so this is the ordinary case, not an exotic one. This also closes a second hole: without this refusal, an unrecognisedschmscheme on a pssh-less track would never reachvalidate_catalogat all, since nocontentProtectionsentry existed for it to reject. -
A protected track's
codecstring is always thefrmaoriginal format (e.g.avc1.64000C), not theencv/encasample-entry type that wraps it.sample_entry_typeseparately keeps the rawencv/encatype, so a consumer can distinguish "this track is protected" from "this track's codec". Resolving the codec throughfrmarequires reading the profile bytes (e.g.avcC) via the effective sample entry, not a bare hard-coded string. The same effective, frma-resolved type also gates geometry extraction (width/height for video, samplerate/channelConfig for audio):encv/encaare byte-for-byte a VisualSampleEntry/ AudioSampleEntry, so gating on the raw wrapper type would silently zero out a video track's dimensions and, worse, publish a wrong (zero) samplerate/channelConfig for an audio track, sincevalidate_trackmakes both MUST-present. -
CENC parameters (
scheme,default_KID,per_sample_iv_size,is_protected) come fromsinf/schm/schi/tencinside the encrypted sample entry (cenc.h'sparse_track_protection). A track whose protection boxes are absent or malformed is never advertised as protected -- protection detection fails closed. -
DRM system init data (
psshboxes, siblings oftrakundermoov) is extracted per system and becomescontentProtections[].pssh(the JSON key;psshBase64is only the C++ member name that holds it,MsfContentProtection::pssh_base64).--drm-configsupplies each system's deployment fields (laURL,certURL,robustness) from a JSON file parsed eagerly at CLI startup, so a malformed file fails before publishing begins rather than publishing with partial configuration. -
saiocorrection on the CTE ingest path (correct_saio_offsets,src/cmaf_segmenter.cpp): when the CTE path rebuilds a moof (e.g. to materializetrundefaults), the rebuild changes onlytrun's own size -- every other byte in the traf, moof, and mdat keeps its original position. Eachsaiooffset is therefore classified against two separate boundaries, not one:- Refusal boundary --
original_moof_size + mdat_size. An offset at or beyond this cannot be a moof-relative reference within a republished MOQT object at all; adjusting it would silently point somewhere meaningless and decrypt to garbage. It is refused (the fragment is rejected) rather than guessed at. - Shift boundary -- the original
trunbox's end offset, relative to the moof. Only an offset at or beyond this point actually moved, since the rebuild changed onlytrun's size: this is normallymdator asencplaced aftertrunin the traf. An offset below this point (insidetfhd, or inside asencplaced beforetrun-- both legal ISO/IEC 14496-12 orderings, and ffmpeg produces the latter) is left unchanged, not shifted.
A prior version of this function shifted every offset within the refusal boundary regardless of where it fell relative to
trun, which is only correct when every aux-info target happens to sit aftertrunin the traf; ffmpeg does not guarantee that ordering. The classification is a magnitude heuristic, not a semantic read of which box a saio entry targets: ISO/IEC 14496-12's tfhd flags0x000001(base-data-offset-present) and0x020000(default-base-is-moof) would give the exact answer instead, and were sanctioned as unnecessary for the CTE ingest path's actual inputs.Only the first
trafin a moof is rebuilt (and only itssaiocorrected for the delta); a multi-trafmoof copies every othertrafverbatim, including anysaioit carries, whose offsets would then be stale against the rebuilt moof's new size.materialize_live_trun_defaultstherefore refuses a multi-trafmoof if anytrafother than the first carries asaio, naming the limitation; a single-trafmoof, and a multi-trafmoof with nosaiooutside the first, are both handled as before. Correctingsaioacross multipletrafs is not implemented. - Refusal boundary --
-
Refused, not signalled: the progressive-remux path (
segment_for_cmaf's non-fragmented branch) synthesisesmoofboxes from scratch and cannot carrysenc,saiz, orsaio. Encrypted input there (any track with a populatedCencTrackProtection) is refused with an error naming the progressive-remux path and the offending track, rather than producing output that looks like valid CMAF but cannot be decrypted. -
Live paths: content protection is detected and signalled wherever a real CMAF initialization segment reaches the publisher.
MoqtSession::publish_live()'s stdin ingest andpublish_live_objects()'s DASH ingest both callattach_content_protectionwhen building their catalog -- viabuild_live_catalog(src/cmsf_packager.cpp) for stdin ingest, andbuild_catalog_locked(src/live_dash_ingest.cpp) for DASH ingest. Detection there is independent of--drm-config: it comes from the init segment'ssinf/schm/schi/tencboxes and the moov-levelpsshsiblings, exactly as in the batch path, so encrypted live input on these two paths is detected and signalled whether or not--drm-configwas supplied. A protected track whose init segment carries nopsshis refused on these live paths too, matching batch: CMSF 4.1.2 makes an absentcontentProtectionRefIDsmean the content is not protected, so publishing one for genuinely encrypted content would be an affirmative false claim.parse_cli_optionsstill refuses--drm-configcombined with--live-source srt. SRT carries MPEG-TS; the publisher synthesises a CMAF init segment from parsed elementary streams, so there is nosinf,tenc, orpsshbox for the publisher to detect. This is a property of the container, not unfinished work on the live paths, and the refusal message now explains that rather than describing it as a gap.--drm-configitself supplies only optional deployment fields (laURL,certURL,robustness) per DRM system; it has never determined whether a track is protected. No live path in the default build (-DOPENMOQ_USE_LIBMOQ_PUBLISHER=OFF, i.e. theMoqtSessionbackend) applies those deployment fields: detection and signalling work on the stdin and DASH live paths, butMoqtSessionhas no access toPublisherConfig::drm_systemsat all, solaURL/certURL/robustnessnever reach a live-built catalog on that backend. Only the batch/VOD path applies them. This holds on both backends. Building with-DOPENMOQ_USE_LIBMOQ_PUBLISHER=ONdoes not change it: that path passesconfig.drm_systemsintobuild_live_catalog, but consumes only the returnedtrack_initializationsand discardsmsf_catalog/catalog_payloadentirely, so the deployment fields have nothing to reach. The argument is passed for correctness should that path ever consume the built catalog; today it is inert. The same distinction holds at the library level:PublisherConfig::drm_systems(include/openmoq/publisher/publisher_api.h) supplies deployment fields to the paths that support them; an SDK consumer combining it with a live publish path on the default backend gets detection and signalling from the init segment as before, just without those deployment fields applied, and combining it with SRT ingest still yields nothing to detect, for the same container reason as the CLI.psshis parsed once per ingest path, from the full initialization segment held at registration (collect_pssh_systems,src/cmsf_packager.cpp), rather than re-parsed on every catalog build -- an efficiency choice, not a limitation.build_track_specific_init_segment(src/cmsf_packager.cpp:215-281) copies everymoovchild that is nottrakormvexverbatim into each per-track init segment,psshincluded, so a subscriber initialising a decoder from a track'sinitDatastill has thepsshit needs. -
Known limitation,
-DOPENMOQ_USE_LIBMOQ_PUBLISHER=ON: under this non-default backend, the stdin live path'sbuild_live_catalogresult (src/transport/libmoq_publisher.cpp) is used only for itstrack_initializations-- the per-track init segments handed tomoq_media_sender_add_track. Itsmsf_catalog/catalog_payloadare never read, because libmoq generates the catalog it actually publishes itself, frommoq_media_track_cfg_t/moq_media_sender_cfg_t. The libmoq service library does model content protection (moq_media_track_cfg_t'scontent_protection_ref_idsandmoq_media_sender_cfg_t'scontent_protections); the limitation is that this project's translation fromTrackDescription/CencTrackProtectionto those libmoq config structs does not populate them. So on this backend the stdin path gains the §4.1.2 refusal (still refuses a protected track with nopssh, since that check runs inbuild_live_catalogbefore the result is discarded) but emits nocontentProtectionsin the catalog libmoq actually sends. -
Not modelled: MoQ Secure Objects encryption fields (MSF 5.2.38-5.2.41) are a separate, LOC-packaged end-to-end encryption mechanism; CMSF uses CENC instead. The CMSF 4.1.1.4.4 Authorization URL field is also deliberately unmodelled -- the draft describes it but never names its JSON key.
draft-ietf-moq-msf-01 sections 11.1 (URL structure), 11.1.1 (reserved
fragment parameters), and 11.1.2 (namespace-name tuple encoding). Implemented
in src/msf_url.cpp and include/openmoq/publisher/msf_url.h, both parse and
build directions.
--urlconfigures the session endpoint, track namespace, and transport from one MSF URL (mutually exclusive with--endpoint/--namespace).--print-msf-urlsemits the broadcast's catalog URL only -- the catalog is the discovery entry point, and a client learns every media track from it, so there is no separate per-media-track URL to print.- All five reserved fragment parameters (
connection,c4m,wallclock-range,mediatime-range,location-range) parse into typed values. - URL-typed catalog fields are exempt from the 5.4 percent rule.
MsfUrlEntry::url,la_url, andcert_urlmay contain percent-encoding, because a license acquisition URL is legitimately percent-encoded under RFC 3986 and a strict reading of 5.4 would reject DRM configurations that work today. The exemption is confined to URL-typed fields. - The URL query is preserved.
--url's query string is folded into the endpoint path (path + "?" + query), the same way--endpointalready carries a query as part of its path. The fragment is never sent, per the draft, but the query is. - An MSF tuple element containing a literal slash is refused by
--url. The publisher'strack_namespaceis a flat string that the transport layer splits on/, so such an element cannot survive the round-trip. Refusing is preferred over silently producing a namespace with the wrong arity. c4mis parsed but not consumed. Nothing on the publish path uses a CAT token.- The
--urltrack name configures nothing. The publisher's catalog track name is the literal"catalog", hardcoded insrc/cmsf_packager.cpp. - Range parameters are parsed but not acted on. A publisher does not serve subclips.
- Union merges overlapping ranges only. Adjacent-but-disjoint ranges stay separate; the represented point set is identical either way. Merging is restricted to overlap because adjacency is undefined across location group boundaries, where an omitted end object means "through the end of that group."
- Section 5.4 is implemented only as emit-side validation. A
%in a catalog field value is refused unless it forms a well-formed%name%reference. There is no variable resolver, because resolution is client-side per 5.4.2 and this repository has no subscriber. - IPv6 authority literals are not supported.
moqt://[::1]:4433/p#msf:ns--tis refused, but the message says "port is not numeric" because the port split takes the first colon inside the brackets. The draft does not discuss IPv6; this is a known limitation with a misleading message, not a working case. - An odd run of hyphens, such as
a---b, is refused via "unescaped character" rather than the more on-point "more than one '--' delimiter". Every malformed input is still refused; only the message is less precise.
The code in this repository intentionally separates:
- MP4/CMAF packaging
- draft-version control-plane mapping
- future transport publication
That separation should make it practical to contribute the packaging path first and wire in a concrete OpenMOQ transport session afterwards.