Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/moq-dev/moq/llms.txt

Use this file to discover all available pages before exploring further.

Watching Streams

This guide covers how to subscribe to and watch Moq broadcasts, from simple web components to custom players.

Quick Start: Web Component

The easiest way to watch a Moq stream is with the <moq-watch> Web Component:
<!DOCTYPE html>
<html>
  <head>
    <script type="module">
      import '@moq/watch/ui'
    </script>
  </head>
  <body>
    <moq-watch 
      url="https://relay.moq.dev/demo" 
      name="bbb"
    ></moq-watch>
  </body>
</html>
This provides a complete player with:
  • Video/audio playback
  • Quality selection
  • Volume control
  • Playback statistics
  • Error handling
The @moq/watch/ui import includes a SolidJS-based UI. For a headless component without UI, use @moq/watch/element.

Installation

npm
npm install @moq/watch @moq/hang @moq/lite
bun
bun add @moq/watch @moq/hang @moq/lite
pnpm
pnpm add @moq/watch @moq/hang @moq/lite

Web Component API

Basic Usage

<moq-watch 
  url="https://relay.example.com/demo"
  name="my-stream"
  autoplay
></moq-watch>

Attributes

AttributeTypeDescription
urlstringWebTransport URL of the relay
namestringName/path of the broadcast
autoplaybooleanStart playing automatically
mutedbooleanStart muted
controlsbooleanShow player controls (default: true)

Methods

const watch = document.querySelector('moq-watch')

// Play/pause
await watch.play()
await watch.pause()

// Volume control
watch.volume = 0.5  // 0.0 to 1.0
watch.muted = true

// Quality selection
const tracks = watch.getTracks()
watch.selectTrack('video/720p')

// Get statistics
const stats = watch.getStats()
console.log('Bitrate:', stats.bitrate)
console.log('Latency:', stats.latency)

Events

const watch = document.querySelector('moq-watch')

// Playback events
watch.addEventListener('play', () => console.log('Playing'))
watch.addEventListener('pause', () => console.log('Paused'))
watch.addEventListener('ended', () => console.log('Ended'))

// Connection events
watch.addEventListener('connected', () => console.log('Connected'))
watch.addEventListener('disconnected', () => console.log('Disconnected'))

// Error events
watch.addEventListener('error', (e) => console.error('Error:', e.detail))

// Track events
watch.addEventListener('tracks', (e) => console.log('Tracks:', e.detail))

JavaScript API

For custom player implementation:

Basic Example

import { Client } from '@moq/lite'
import { Catalog, Watch } from '@moq/hang'

// Connect to relay
const client = await Client.connect('https://relay.example.com/demo')

// Subscribe to broadcast
const broadcast = await client.subscribe('my-stream')

// Read catalog to discover tracks
const catalog = await Catalog.fetch(broadcast)
console.log('Available tracks:', catalog.tracks())

// Create watcher
const watch = new Watch(broadcast, catalog)

// Attach to video element
const video = document.querySelector('video')
watch.attach(video)

// Start playback
await watch.play()

Custom Track Selection

// Get available video tracks
const videoTracks = catalog.tracks()
  .filter(t => t.kind === 'video')
  .sort((a, b) => b.bitrate - a.bitrate)

console.log('Available qualities:')
for (const track of videoTracks) {
  console.log(`  ${track.name}: ${track.width}x${track.height} @ ${track.bitrate}bps`)
}

// Select specific quality
await watch.selectTrack('video/720p')

// Or select by resolution
const hd = videoTracks.find(t => t.height >= 720)
if (hd) {
  await watch.selectTrack(hd.name)
}

Manual Decoding

For complete control:
import { Client } from '@moq/lite'
import { Catalog } from '@moq/hang'

const client = await Client.connect('https://relay.example.com/demo')
const broadcast = await client.subscribe('my-stream')
const catalog = await Catalog.fetch(broadcast)

// Subscribe to specific track
const track = broadcast.getTrack('video/1080p')

// Setup decoder
const decoder = new VideoDecoder({
  output: (frame) => {
    // Render frame to canvas or video element
    ctx.drawImage(frame, 0, 0)
    frame.close()
  },
  error: (e) => console.error('Decode error:', e)
})

// Get codec info from catalog
const trackInfo = catalog.tracks().find(t => t.name === 'video/1080p')
decoder.configure({
  codec: trackInfo.codec,
  codedWidth: trackInfo.width,
  codedHeight: trackInfo.height
})

// Decode frames
for await (const group of track.groups()) {
  for await (const frame of group.frames()) {
    const chunk = new EncodedVideoChunk({
      type: frame.isKeyframe ? 'key' : 'delta',
      timestamp: frame.timestamp,
      data: frame.data
    })
    decoder.decode(chunk)
  }
}

Adaptive Streaming

Automatic Quality Selection

import { AdaptiveSelector } from '@moq/watch'

const selector = new AdaptiveSelector(watch)

// Enable adaptive streaming
selector.enable({
  // Target buffer size (ms)
  targetBuffer: 2000,
  
  // Bandwidth estimation window (ms)
  bandwidthWindow: 10000,
  
  // Switch up threshold (% of bandwidth)
  switchUpThreshold: 0.8,
  
  // Switch down threshold (% of bandwidth)
  switchDownThreshold: 1.2
})

// Monitor quality changes
selector.addEventListener('qualitychange', (e) => {
  console.log('Switched to:', e.detail.track)
})

Manual Quality Control

// Get available qualities
const qualities = catalog.tracks()
  .filter(t => t.kind === 'video')
  .map(t => ({
    name: t.name,
    label: `${t.height}p`,
    bitrate: t.bitrate
  }))

// Let user select
const select = document.querySelector('#quality-select')
for (const q of qualities) {
  const option = document.createElement('option')
  option.value = q.name
  option.textContent = q.label
  select.appendChild(option)
}

select.addEventListener('change', async () => {
  await watch.selectTrack(select.value)
})

Statistics & Monitoring

Real-time Statistics

// Get current stats
const stats = watch.getStats()

console.log({
  // Playback
  currentTime: stats.currentTime,
  buffered: stats.buffered,
  
  // Network
  bitrate: stats.bitrate,
  bandwidth: stats.bandwidth,
  
  // Quality
  droppedFrames: stats.droppedFrames,
  decodedFrames: stats.decodedFrames,
  
  // Latency
  latency: stats.latency,
  jitter: stats.jitter
})

// Monitor continuously
setInterval(() => {
  const stats = watch.getStats()
  updateUI(stats)
}, 1000)

Performance Monitoring

// Track quality changes
let qualityChanges = 0
watch.addEventListener('qualitychange', () => {
  qualityChanges++
})

// Track buffering events
let bufferingEvents = 0
let bufferingTime = 0
watch.addEventListener('waiting', () => {
  bufferingEvents++
  bufferStart = Date.now()
})
watch.addEventListener('playing', () => {
  if (bufferStart) {
    bufferingTime += Date.now() - bufferStart
  }
})

// Report metrics
setInterval(() => {
  analytics.track('playback_quality', {
    quality_changes: qualityChanges,
    buffering_events: bufferingEvents,
    buffering_time_ms: bufferingTime,
    average_bitrate: watch.getStats().bitrate
  })
}, 60000)

Authentication

Watch protected broadcasts with JWT tokens:
// Generate token (server-side or CLI)
// moq-token --key secret.jwk sign --root demo --subscribe my-stream

const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'

// Include in URL
const url = `https://relay.example.com/demo?jwt=${token}`
const client = await Client.connect(url)
See Authentication for detailed setup.

Advanced Topics

Multi-track Playback

Play video, audio, and subtitles together:
const broadcast = await client.subscribe('my-stream')
const catalog = await Catalog.fetch(broadcast)

// Create separate watchers for each type
const videoWatch = new Watch(broadcast, catalog)
const audioWatch = new Watch(broadcast, catalog)

// Select tracks
await videoWatch.selectTrack('video/1080p')
await audioWatch.selectTrack('audio/en')

// Sync to same video element
const video = document.querySelector('video')
videoWatch.attachVideo(video)
audioWatch.attachAudio(video)

// Play both
await Promise.all([
  videoWatch.play(),
  audioWatch.play()
])

Time-shift / DVR

If the relay caches content, you can seek backward:
// Check if DVR is available
const dvr = watch.getDVRWindow()
if (dvr) {
  console.log(`Can seek back ${dvr.duration}ms`)
  
  // Seek backward
  await watch.seek(watch.currentTime - 30000) // 30s back
}

Thumbnail Preview

Generate thumbnails for seeking:
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')

const decoder = new VideoDecoder({
  output: (frame) => {
    // Draw first frame of each GOP as thumbnail
    if (frame.isKeyframe) {
      canvas.width = frame.displayWidth
      canvas.height = frame.displayHeight
      ctx.drawImage(frame, 0, 0)
      
      const thumbnail = canvas.toDataURL()
      saveThumbnail(frame.timestamp, thumbnail)
    }
    frame.close()
  },
  error: console.error
})

Troubleshooting

  • Check browser console for errors
  • Verify WebTransport is supported (Chrome 97+, Edge 97+)
  • Check relay URL is correct
  • Verify broadcast name exists
  • Check network connectivity
  • Verify JWT token is valid
  • Check token has subscribe permission
  • Ensure token hasn’t expired
  • Check URL includes ?jwt= parameter
  • Check network bandwidth
  • Try lower quality rendition
  • Check CPU usage (decoding overhead)
  • Enable hardware acceleration
  • Check relay distance (use closer relay)
  • Verify publisher is using low-latency settings
  • Disable quality auto-switching temporarily
  • Check for network congestion
  • Ensure both use same clock reference
  • Check timestamp alignment in catalog
  • Verify decoder latency is similar

Next Steps

Publishing

Learn how to publish media streams

Authentication

Setup JWT authentication

hang Protocol

Understand the media layer

moq-lite Protocol

Learn about the transport layer

Build docs developers (and LLMs) love