MoQ Streaming with Wowza Streaming Engine and Claude AI
At the beginning of this year, I was researching how to take the CMAF output that Wowza Streaming Engine already produces, map it correctly into the Media over QUIC (MoQ) object model, and do it in a way that fits Streaming Engine’s extension patterns, all without modifying any core code. I went into this project thinking the hard part would be understanding MoQ. In reality, the hardest part was architecture.
Claude made this dramatically faster, but not because it wrote the code. The real acceleration came from using Claude’s planning workflow to converge on a thoughtful design before generating anything. Once the plan was correct, Claude produced 8,000+ lines and I only touched about 100 by hand. Mainly, I only intervened to handle edge cases, tighten integration, and align with my preferences. To be clear: this is a proof of concept, not a production-ready release. It would need significantly more testing and hardening before I would be comfortable shipping it. But as a demonstration that the architecture is viable and the protocol mapping is correct, it absolutely delivers.
Learn how I took CMAF fMP4 produced by Wowza’s CMAF packager and delivered it over MOQT (Media over QUIC Transport) using the CMSF CMAF-over-MoQ mapping in Wowza Streaming Engine.
Streaming Over QUIC/MoQ vs HTTP
MoQ is built on QUIC and targets low-latency, congestion-aware media distribution. Here is a quick visualization to compare the TCP handshake with QIUC:

If you’re coming from HLS/DASH muscle memory, your brain wants to design an origin where the clients request playlists, then request segments. That is a PULL model. MoQ (specifically MOQT) is different. Data always flows from publisher to subscriber, it is never pulled. Understanding the two paths that trigger that push is crucial.
In the subscriber-initiated path the subscriber sends SUBSCRIBE for a specific track and the publisher responds by pushing Objects as they become available. In the publisher-initiated path (added in recent drafts), the publisher sends PUBLISH to offer a track, the subscriber accepts, and objects flow the same way. Either way, there is no polling an origin for a new segment URL. The publisher pushes, relays fan out, and subscribers receive.
That single conceptual inversion drives everything from buffering strategy to threading model to how you wire the CMAF packager into the transport.

Fundamental Principles of MoQ/QUIC
Tracks, Groups, & Objects
MOQT structures media delivery in a four-level hierarchy.
- A Track is a sequence of Groups
- A Group is a temporal sequence of Objects and serves as a join point, where a new or rejoining subscriber can start receiving without waiting for earlier data
Each Object within a Group belongs to exactly one Subgroup, which maps to a single QUIC unidirectional stream and preserves ordering for dependent frames. Objects are the basic addressable unit: a byte payload identified by Group ID and Object ID. On the wire, each track is identified by a compact numeric Track Alias assigned during subscription so that data stream headers stay small.
Namespaces
A Track Namespace is an ordered tuple of up to 32 opaque binary fields. Combined with a Track Name, it forms the Full Track Name that uniquely identifies a track in the session. Namespaces are designed for prefix-based discovery: a relay or subscriber can subscribe to a prefix and be notified about every track published beneath it. Four namespace-level messages exist:
- PUBLISH_NAMESPACE (publisher advertises tracks under a namespace)
- SUBSCRIBE_NAMESPACE (subscriber requests matching namespaces)
- NAMESPACE (relay response carrying namespace details)
- NAMESPACE_DONE (publisher signals the namespace is ending)
For this project each stream gets its own namespace tuple, [com.wowza, <applicationName>, <streamName>] for example. That way, the catalog and tracks for every stream are fully isolated and prefix-discoverable.
Catalogs
A catalog is an MOQT Object used by publishers to advertise tracks and by subscribers to consume them. This is how the player discovers the tracks. It can be opaque to relays and end-to-end encrypted. It also includes track namespaces/names, relationships, selection properties, and initialization data.
The common catalog format can be found here: https://moq-wg.github.io/catalog-format/draft-ietf-moq-catalogformat.html
Why Java Extensibility in Wowza Streaming Engine Is A Game-Changer
It’s worth reiterating that I built a new delivery protocol path (MoQ/CMSF over QUIC) without changing any core code in Wowza Streaming Engine. Most media servers do not let you do this. They might expose configuration knobs, or maybe an HTTP plugin surface. But adding an entirely new push-based protocol that integrates with packaging, stream lifecycle, and fan-out logic (without forking the server) usually isn’t possible.
Wowza Streaming Engine is different because it was designed to be extended this way. The architecture explicitly frames the Java API as a way to extend server functionality, and server-side modules as Java classes that load when an application starts and provide much of the functionality that controls the streaming process.

The CMAF-to-MoQ Relay Architecture That Worked

One of the things I want to highlight here is that AI-generated code doesn’t mean unstructured code. I had Claude follow a deliberate, reviewable architecture throughout. Every class has a single responsibility, the package hierarchy mirrors the layered design, and the naming conventions are consistent with existing Wowza patterns. This is what generated code looks like when you start with a solid plan:
com.wowza.wms.moqstreaming/
├── module/
│ └── ModuleMoQStreaming.java # WSE Module entry point (ModulePushPublish-style)
├── relay/
│ ├── MoQRelayPublisher.java # Manages poller and publisher
│ └── MoQRelaySession.java # Manages transport implementation
├── transport
│ └── MoQSession.java # MOQT session (control + data) streams)
├── protocol/
│ ├── MoQVarInt.java
│ ├── MoQMessageCodec.java # encodes MOQT messages
│ ├── SubscriptionManager.java
│ └── TrackAliasRegistry.java
├── publisher/
│ ├── MoQCmafPublisher.java # emits objects to publisher
│ ├── MoQSegmentPoller.java # poll loop (Stream Target-like)
├─ catalog/
│ ├── MoQCatalogTrack.java # catalog as a track/object
│ └── CmsfCatalogBuilder.java # CMSF/MSF JSON
└── config/
└── MoQConfig.java
Notice how the layers map cleanly to the design concerns.
- module/ is the WSE lifecycle hook
- relay/ owns the publishing to the relay, and protocol provider abstraction
- transport/ abstracts the session
- protocol/ handles wire encoding
- publisher/ bridges CMAF to MoQ
- catalog/ builds the CMSF JSON
- config/ holds all the config variables
By generating sections of code at a time per the plans, I could review one layer at a time before moving to the next.
The MoQ Workflow In Practice
Step 1: Publish a namespace per stream
Create a separate namespace and catalog per stream to enable prefix-based discovery. My namespace schema looked like: publisherId/com.wowza/<application>/<streamName>
Step 2: Catalog all available tracks
The per-stream catalog lists video tracks (alternate bitrates/resolutions), audio tracks (including alternate audio), and optionally captions/data tracks. To map CMAF to CMSF to MOQT, I treated CMSF as a checklist: init segment becomes catalog initData, fragments (moof+mdat) become Objects, and segment/GOP boundaries map to Group boundaries.
Step 3: The subscriber subscribes
The client subscribes to the namespace and is delivered the catalog to discover available tracks. It sends a SUBSCRIBE message for each audio or video track it wants. On receipt of SUBSCRIBE, the publisher begins pushing Objects for that track as they are produced. The publisher-initiated PUBLISH path (where the origin offers a track and the subscriber accepts) is equally valid in MOQT and worth exploring in a future iteration.
Step 4: Push objects
The publisher begins pushing CMAF-derived objects to the relay and subscribers, only after subscriptions exist.
Evaluating QUIC Transport: quiche4j vs Kwik
I built a transport abstraction so I could swap QUIC backends and measure performance without rewriting the publisher.
The first option was quiche4j (a Java wrapper) with Cloudflare quiche. You will need to fork the quiche4j repo in order to make improvements. First, update quiche4j to track newer quiche versions. Claude helped with that work. Claude also proved to be quite capable of adding WebTransport to the quiche4j implementation, needing only a solid plan and a few iterations to be successful.
The second option was Kwik (pure Java QUIC) with WebTransport over HTTP/3 and Flupke (a WT layer on top of Kwik). Kwik worked out of the box, and Flupke needed only a couple of small changes.
What Worked Well
Use Plan Mode Before Code Mode
The single highest-leverage decision was using Claude’s planning mode before writing a single line of Java. Feeding it the Wowza API docs, the MOQT draft, the CMSF draft, and the catalog format spec all at once let it reason about the full system. Each plan iteration was fast (minutes, not hours), cheap to throw away, and far less painful than refactoring generated code later. By taking a plan and refining the approach by the fifth or sixth iteration, I had something that felt designed rather than cobbled together.
Use Constraints As Creative Fuel
Giving Claude explicit constraints like ‘you may not modify any core Wowza code’ was not a limitation. With this clarity, Claude immediately looked for the existing extension points (modules, push-publish patterns, the CMAF packager API) and used them correctly. Vague prompts produce vague designs, whereas concrete guardrails produce concrete solutions.
Generate Layered, Reviewable Code Blocks
Generating one package layer at a time meant I could review and approve each layer before moving on. I could tackle protocol codec first, then transport abstraction, then publisher bridge, then catalog builder. The package tree was the skeleton that made the review process straightforward. Reviewing a ton of generated code sounds intimidating until you realize it was delivered in 10-12 bounded, focused chunks that you can read in under an hour.
Streamline Tedious Compliance Work
Updating quiche4j to stay current with the upstream Cloudflare quiche C library is exactly the kind of work that is important, time-consuming, and deeply unpleasant. Hunting FFI signature diffs, regenerating JNI bindings, and verifying native-library ABI compatibility are time-consuming. Claude handled the mechanical parts of that diff-and-patch loop without complaint, and it never lost context across the session. I estimate it saved two or three days of work on that task alone.
Navigate Specs Easily
IETF drafts are dense. Having Claude synthesize the relevant sections of the MOQT transport draft, the CMSF mapping rules, and the catalog format spec before mapping them directly to Java class responsibilities compressed what would have been a week of careful reading into an afternoon. That said, I could not have built this without reasonably deep knowledge of the various specs. You do need to check the work. The resulting code faithfully followed the spec naming (PUBLISH_NAMESPACE, SUBSCRIBE, GROUP boundaries, initData in catalog) because Claude had read the source material, not just inferred from example code.
What Was Harder Than Expected
The Push/Pull Inversion Is A Genuine Mental Model Shift
Even knowing intellectually that MoQ is publish/subscribe and not HTTP pull, I kept designing things in pull mode by reflex. The polling loop (MoQSegmentPoller) is a visible manifestation of this: it works, but it might not be the best or most efficient approach. Claude flagged this tension early, but fully internalizing the implications (subscription-gated production, backpressure flowing upstream, latency variability) took longer than the code did.
The MOQT and CMSF Draft Specs Are Moving Targets
Because the MOQT and CMSF drafts are living documents, message types, varint encodings, and wire-format details changed between the draft versions Claude had access to and the versions I was targeting. This is the nature of working with pre-RFC specifications. The mitigation is to keep the codec layer (MoQVarInt, MoQMessageCodec) thin and independently testable so you can update wire format without touching business logic.
Native Library Packaging (quiche4j)
Getting the Cloudflare quiche native library (.so on Linux, .dylib on macOS, .dll on Windows) packaged correctly alongside the JNI wrapper and deployed into Wowza’s lib/ and extras/native/ directories was fiddly. Claude could generate the Maven POM changes and the System.loadLibrary() scaffolding. But, actually validating that the right native binary matched the JNI interface required real environment testing. This is a class of problem where AI assistance ends at the code boundary.
Interoperability Validation Requires Real Peers
Claude can generate unit tests for varint encoding and message framing, and those are valuable. But validating that the catalog is correctly parsed by a reference MoQ client (moq-rs, moq-js), that ALPN negotiation succeeds, and that a relay correctly fans out subscriptions requires running the full stack. No amount of AI-generated test coverage can substitute end-to-end integration testing against real peer implementations.
This proof of concept demonstrates the architecture works, but it still requires sustained load testing, failure-mode validation, and security hardening of the QUIC/TLS surface. It needs significantly broader interoperability testing against real-world MoQ relay and player implementations, especially across latency targets. Treat it as a validated starting point, not a finished product, and test it before deploying it to production.
Advice For Developing Modules Using Generative AI Agents
1. Invest In The Prompt, Not The Code
The quality of your architecture plan is the ceiling for the quality of the generated code. I had a definite advantage of being very familiar with Wowza Streaming Engine, and an increasing familiarity with MoQ.
Do your research! Spend a couple hours writing a thorough context prompt with existing patterns you want reused, specs you want followed, constraints that are non-negotiable, and the exact deliverable description. Do this before you ask for any code to be written. It may feel slow, but it pays for itself on the first plan revision.
2. Give Claude The Primary Sources, Not Summaries
Pointing Claude directly at the IETF draft URLs (not blog post summaries) produced code that used correct spec terminology, correct message names, and correct wire-format constants. When the source of truth is authoritative, the generated code inherits that authority. This matters especially for pre-RFC drafts where secondary sources lag or get details wrong.
3. Define “Done” Before Starting
For a proof of concept, successfully compiling is nowhere near complete. Define your acceptance bar early and write it into the plan prompt. For this project, it was ‘a reference client receives a catalog, subscribes to a track, and gets correctly ordered CMAF objects through a relay, which can be played out as a viewable video stream.’ That criterion kept Claude from optimizing for the wrong thing, and kept me from declaring victory too early. If you are aiming for production, raise the bar further still.
4. Keep The Transport Layer Swappable From Day One
Building the MoqTransportProvider abstraction into the plan before I had decided between quiche4j and Kwik was one of the best structural decisions in the project. It let me benchmark both libraries against real streaming workloads without refactoring the publisher layer. If you are exploring a new protocol stack, design for replaceability. The first library you try probably will not be the one you ship.
5. Review Layers, Not Lines
Trying to review 8,000 lines of generated code linearly is not realistic. Reviewing the protocol codec package (four classes, clear spec mapping) is realistic. Reviewing the publisher package (three classes, clear data-flow responsibility) is realistic. Design the package structure so that each sub-package is a reviewable unit. Review it before asking Claude to generate the next layer, then review the big picture at the end.
6. Know Where AI Help Ends
Claude is excellent at navigating large API surfaces, synthesizing spec documents, generating boilerplate-heavy code, and doing mechanical compliance work like FFI updates. It is less useful for validating native binary ABI compatibility, end-to-end interop testing with real peer stacks, and tuning performance under actual streaming load. Know the boundaries, staff accordingly, and do not be surprised when the last 100 lines you write by hand are the most important ones.
Try MoQ Streaming with Wowza Streaming Engine
If you’re looking to bring MoQ delivery into your streaming stack, the hard part isn’t writing the code, it’s knowing that your media server can actually accommodate a fundamentally new protocol path. Wowza Streaming Engine’s Java API extensibility means you can build a full CMAF-to-MOQT publishing workflow as a module, reuse the existing packager and push-publish patterns, swap QUIC transports underneath, and ship it alongside everything else the server already does.
The architecture is viable, the extension points are there, and the path from proof of concept to production is a matter of hardening, not rearchitecting. See how Wowza Streaming Engine fits your MoQ workflow, contact a Wowza streaming expert for a demo.

FAQs
What are the main advantages of MoQ over WebRTC for streaming?
MoQ is designed for scalable, many-to-many delivery using a publish/subscribe model, while WebRTC is optimized for real-time peer-to-peer communication. MoQ handles fan-out through relays more efficiently, making it better suited for large-scale live streaming, whereas WebRTC excels in ultra-low-latency interactive use cases like video calls.
Can MoQ streaming work with existing CDN infrastructure?
Not directly. Traditional CDNs are built around HTTP-based pull delivery models (like HLS and DASH). MoQ requires a push-based architecture with relay nodes that understand QUIC and the MoQ protocol. However, hybrid approaches may emerge where CDNs integrate MoQ-compatible edge services.
How does MoQ handle packet loss and network congestion?
MoQ leverages QUIC’s built-in congestion control and loss recovery mechanisms. Unlike TCP, QUIC avoids head-of-line blocking by using independent streams, which allows media objects to continue flowing even if some packets are delayed or lost. This improves playback stability under fluctuating network conditions.
Is encryption required when using MoQ?
Yes. MoQ runs over QUIC, which requires TLS 1.3 encryption by default. This ensures that all media delivery is encrypted in transit, providing a secure baseline without requiring additional configuration.