Documentation Index
Fetch the complete documentation index at: https://mintlify.com/moqtail/moqtail/llms.txt
Use this file to discover all available pages before exploring further.
This guide will help you build a complete working example with both a subscriber and publisher using Moqtail.
Prerequisites
Before you begin, make sure you have:
- Node.js 18 or later installed
- A running MOQtail relay server (see Relay Server Setup)
- WebTransport-enabled browser (Chrome/Edge recommended)
Installation
First, install the Moqtail package:
Building a Subscriber
Let’s start by creating a subscriber that consumes live video content.
Establish WebTransport connection
Connect to your relay server using WebTransport:const url = 'https://localhost:4433'
const webTransport = new WebTransport(url)
await webTransport.ready
Initialize the MOQtail client
Create a client with role configuration:import { MOQtailClient, ClientSetup, RoleType } from 'moqtail/client'
const clientSetup = new ClientSetup(
[0], // Supported versions (Draft 14)
new SetupParameters()
.withRole(RoleType.Subscriber)
.withDeliveryTimeout(5000)
)
const client = await MOQtailClient.new(clientSetup, webTransport)
Subscribe to a track
Subscribe to live content and process incoming objects:import { Subscribe, FilterType, FullTrackName } from 'moqtail/model'
// Subscribe to latest content
const subscribe = Subscribe.newLatestObject(
client.nextClientRequestId,
1n, // trackAlias
FullTrackName.tryNew('live/conference', 'video'),
1n // subscriberId
)
const result = await client.subscribe(subscribe)
if (result instanceof SubscribeError) {
console.error('Subscription failed:', result.reasonPhrase)
return
}
// Process the stream
const reader = result.getReader()
while (true) {
const { done, value: object } = await reader.read()
if (done) break
console.log(`Received object ${object.objectId} from group ${object.groupId}`)
// Process the media data in object.payload
processMediaFrame(object.payload)
}
Building a Publisher
Now let’s create a publisher that produces live video content.
Connect and initialize
Set up the WebTransport connection and client with publisher role:const webTransport = new WebTransport('https://localhost:4433')
await webTransport.ready
const clientSetup = new ClientSetup(
[0],
new SetupParameters().withRole(RoleType.Publisher)
)
const client = await MOQtailClient.new(clientSetup, webTransport)
Create a content source
Set up a live content source with a stream of objects:import { LiveTrackSource, MoqtObject, Location } from 'moqtail'
// Create a stream of video frames
const frameStream = new ReadableStream({
async start(controller) {
let groupId = 0n
let objectId = 0n
// Simulate video encoding (replace with real encoder)
const interval = setInterval(() => {
const payload = encodeVideoFrame()
const object = MoqtObject.newWithPayload(
new Location(groupId, objectId),
payload,
1 // publisherPriority
)
controller.enqueue(object)
objectId++
// Start new group every 30 frames (keyframe interval)
if (objectId % 30n === 0n) {
groupId++
objectId = 0n
}
}, 33) // ~30fps
// Cleanup on stream cancellation
return () => clearInterval(interval)
}
})
const contentSource = new LiveTrackSource(frameStream)
Create and register the track
Define your track and add it to the client:import { Track, ObjectForwardingPreference } from 'moqtail'
const videoTrack: Track = {
fullTrackName: FullTrackName.tryNew('live/conference', 'video'),
trackAlias: 1n,
forwardingPreference: ObjectForwardingPreference.Subgroup,
contentSource: contentSource
}
client.addOrUpdateTrack(videoTrack)
Announce the namespace
Publish your namespace so subscribers can discover your tracks:import { PublishNamespace, Tuple } from 'moqtail/model'
const announce = new PublishNamespace(
client.nextClientRequestId,
Tuple.tryNew(['live', 'conference'])
)
const result = await client.publishNamespace(announce)
if (result instanceof PublishNamespaceError) {
console.error('Failed to announce:', result.reasonPhrase)
return
}
console.log('Successfully publishing namespace!')
Complete Example
Here’s a complete working example that combines both patterns:
Full Subscriber Implementation
import { MOQtailClient, ClientSetup, RoleType, SetupParameters } from 'moqtail/client'
import { Subscribe, FullTrackName, SubscribeError } from 'moqtail/model'
async function createSubscriber() {
// Connect to relay
const webTransport = new WebTransport('https://localhost:4433')
await webTransport.ready
// Initialize client
const clientSetup = new ClientSetup(
[0],
new SetupParameters()
.withRole(RoleType.Subscriber)
.withDeliveryTimeout(5000)
)
const client = await MOQtailClient.new(clientSetup, webTransport)
// Subscribe to latest content
const subscribe = Subscribe.newLatestObject(
client.nextClientRequestId,
1n,
FullTrackName.tryNew('live/conference', 'video'),
1n
)
const result = await client.subscribe(subscribe)
if (result instanceof SubscribeError) {
throw new Error(`Subscription failed: ${result.reasonPhrase}`)
}
// Process incoming objects
const reader = result.getReader()
try {
while (true) {
const { done, value: object } = await reader.read()
if (done) break
console.log(`Received object ${object.objectId} from group ${object.groupId}`)
// Process media data
await decodeAndRender(object.payload)
}
} finally {
reader.releaseLock()
}
}
createSubscriber().catch(console.error)
Full Publisher Implementation
import { MOQtailClient, ClientSetup, RoleType, SetupParameters } from 'moqtail/client'
import {
PublishNamespace,
Tuple,
FullTrackName,
PublishNamespaceError
} from 'moqtail/model'
import {
Track,
LiveTrackSource,
MoqtObject,
Location,
ObjectForwardingPreference
} from 'moqtail'
async function createPublisher() {
// Connect to relay
const webTransport = new WebTransport('https://localhost:4433')
await webTransport.ready
// Initialize client
const clientSetup = new ClientSetup(
[0],
new SetupParameters().withRole(RoleType.Publisher)
)
const client = await MOQtailClient.new(clientSetup, webTransport)
// Create live content stream
let groupId = 0n
let objectId = 0n
const frameStream = new ReadableStream({
start(controller) {
const interval = setInterval(() => {
const payload = captureAndEncodeFrame()
const object = MoqtObject.newWithPayload(
new Location(groupId, objectId),
payload,
1
)
controller.enqueue(object)
objectId++
// New GOP every 30 frames
if (objectId % 30n === 0n) {
groupId++
objectId = 0n
}
}, 33) // 30fps
return () => clearInterval(interval)
}
})
// Create and register track
const videoTrack: Track = {
fullTrackName: FullTrackName.tryNew('live/conference', 'video'),
trackAlias: 1n,
forwardingPreference: ObjectForwardingPreference.Subgroup,
contentSource: new LiveTrackSource(frameStream)
}
client.addOrUpdateTrack(videoTrack)
// Announce namespace
const announce = new PublishNamespace(
client.nextClientRequestId,
Tuple.tryNew(['live', 'conference'])
)
const result = await client.publishNamespace(announce)
if (result instanceof PublishNamespaceError) {
throw new Error(`Failed to announce: ${result.reasonPhrase}`)
}
console.log('Successfully publishing live content!')
}
createPublisher().catch(console.error)
Next Steps
Core Concepts
Understand tracks, objects, and the MOQT protocol
Publisher Guide
Learn advanced publishing patterns
Subscriber Guide
Master subscription and fetching strategies
API Reference
Explore the complete API documentation
Troubleshooting
WebTransport connection fails
Make sure:
- Your relay server is running on the correct port
- You have valid TLS certificates (see WebTransport Setup)
- Your browser trusts the certificates (for development, install the CA)
- The browser supports WebTransport (Chrome/Edge 97+)
Subscription returns no data
Check that:
- The publisher has announced the namespace
- The track name matches exactly (namespace and track name)
- The publisher’s track is active and generating objects
- Network connectivity between client and relay is stable
Objects arrive out of order
This is expected behavior for datagram delivery. To ensure ordering:
- Use
ObjectForwardingPreference.Subgroup for ordered delivery
- Implement a playout buffer (see Playout Buffer)
- Sort objects by their
Location (groupId, objectId)