Skip to main content

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.

Overview

The Playout Buffer is a critical component for managing media playback in MOQT applications. While Moqtail doesn’t provide a built-in playout buffer implementation (to allow for application-specific customization), this guide covers the concepts and patterns for implementing an effective playout buffer.

Core Concepts

Purpose

A playout buffer:
  • Absorbs network jitter and packet reordering
  • Maintains smooth playback despite variable delivery times
  • Handles out-of-order group and object arrival
  • Manages buffer levels to balance latency and smoothness

Buffer Architecture

interface PlayoutBuffer {
  // Add received objects to the buffer
  push(object: MOQTObject): void
  
  // Pull next object for playback
  pull(): MOQTObject | null
  
  // Check buffer status
  level(): number
  isEmpty(): boolean
  isFull(): boolean
  
  // Buffer management
  clear(): void
  setTarget(ms: number): void
}

Implementation Patterns

Basic Buffer Implementation

import { Location } from 'moqtail'

interface BufferedObject {
  location: Location
  data: Uint8Array
  timestamp: number
}

class SimplePlayoutBuffer {
  private buffer: Map<string, BufferedObject> = new Map()
  private targetLatency: number
  private currentGroup: bigint = 0n
  private currentObject: bigint = 0n
  
  constructor(targetLatencyMs: number = 500) {
    this.targetLatency = targetLatencyMs
  }
  
  push(location: Location, data: Uint8Array): void {
    const key = `${location.group}:${location.object}`
    this.buffer.set(key, {
      location,
      data,
      timestamp: Date.now()
    })
  }
  
  pull(): BufferedObject | null {
    const key = `${this.currentGroup}:${this.currentObject}`
    const obj = this.buffer.get(key)
    
    if (!obj) return null
    
    // Check if we should wait for more buffering
    const age = Date.now() - obj.timestamp
    if (age < this.targetLatency && this.buffer.size < 10) {
      return null  // Wait for more objects
    }
    
    this.buffer.delete(key)
    this.currentObject++
    return obj
  }
  
  level(): number {
    return this.buffer.size
  }
  
  isEmpty(): boolean {
    return this.buffer.size === 0
  }
  
  clear(): void {
    this.buffer.clear()
  }
  
  setTarget(ms: number): void {
    this.targetLatency = ms
  }
}

Priority Queue Buffer

Handle out-of-order delivery with a priority queue:
import { Location } from 'moqtail'

interface QueuedObject {
  location: Location
  data: Uint8Array
  receivedAt: number
}

class PriorityPlayoutBuffer {
  private queue: QueuedObject[] = []
  private targetLatency: number
  private lastPlayedLocation?: Location
  
  constructor(targetLatencyMs: number = 500) {
    this.targetLatency = targetLatencyMs
  }
  
  push(location: Location, data: Uint8Array): void {
    const obj: QueuedObject = {
      location,
      data,
      receivedAt: Date.now()
    }
    
    // Insert in sorted order
    let index = this.queue.findIndex(item => 
      this.compareLocations(location, item.location) < 0
    )
    
    if (index === -1) {
      this.queue.push(obj)
    } else {
      this.queue.splice(index, 0, obj)
    }
  }
  
  pull(): Uint8Array | null {
    if (this.queue.length === 0) return null
    
    const next = this.queue[0]
    const age = Date.now() - next.receivedAt
    
    // Adaptive latency: wait longer if buffer is low
    const adaptiveLatency = this.queue.length < 5 
      ? this.targetLatency * 1.5 
      : this.targetLatency
    
    if (age < adaptiveLatency) {
      return null  // Not ready yet
    }
    
    this.queue.shift()
    this.lastPlayedLocation = next.location
    return next.data
  }
  
  private compareLocations(a: Location, b: Location): number {
    if (a.group !== b.group) {
      return a.group < b.group ? -1 : 1
    }
    return a.object < b.object ? -1 : (a.object > b.object ? 1 : 0)
  }
  
  level(): number {
    return this.queue.length
  }
  
  healthScore(): number {
    // 0 = empty, 1 = optimal, > 1 = overfilled
    const optimalSize = 10
    return this.queue.length / optimalSize
  }
}

Adaptive Buffer

Automatically adjust target latency based on network conditions:
import { NetworkTelemetry } from 'moqtail/util'

class AdaptivePlayoutBuffer {
  private buffer: Map<string, BufferedObject> = new Map()
  private telemetry: NetworkTelemetry
  private baseLatency: number
  private currentLatency: number
  
  constructor(baseLatencyMs: number = 500) {
    this.baseLatency = baseLatencyMs
    this.currentLatency = baseLatencyMs
    this.telemetry = new NetworkTelemetry(1000)
  }
  
  push(location: Location, data: Uint8Array, stats: { latency: number, size: number }): void {
    const key = `${location.group}:${location.object}`
    this.buffer.set(key, {
      location,
      data,
      timestamp: Date.now()
    })
    
    // Update telemetry
    this.telemetry.push(stats)
    
    // Adapt buffer target based on network conditions
    this.adaptLatency()
  }
  
  private adaptLatency(): void {
    const avgLatency = this.telemetry.latency
    const bufferHealth = this.buffer.size
    
    // Increase buffer if network is slow
    if (avgLatency > this.currentLatency * 0.8) {
      this.currentLatency = Math.min(
        this.currentLatency * 1.2,
        this.baseLatency * 3
      )
    }
    
    // Decrease buffer if network is fast and buffer is full
    if (avgLatency < this.currentLatency * 0.5 && bufferHealth > 15) {
      this.currentLatency = Math.max(
        this.currentLatency * 0.9,
        this.baseLatency
      )
    }
  }
  
  getStats(): { latency: number, throughput: number, bufferLatency: number } {
    return {
      latency: this.telemetry.latency,
      throughput: this.telemetry.throughput,
      bufferLatency: this.currentLatency
    }
  }
}

Integration with Subscribe

Feeding the Buffer

import { Subscribe, Location } from 'moqtail'

const buffer = new PriorityPlayoutBuffer(500)
const subscription = Subscribe.newLatestObject(
  requestId,
  fullTrackName,
  priority,
  groupOrder,
  true,
  []
)

// Handle incoming objects
subscription.onObject((object) => {
  const location = new Location(object.groupId, object.objectId)
  buffer.push(location, object.payload)
})

// Playback loop
setInterval(() => {
  const data = buffer.pull()
  if (data) {
    playFrame(data)
  }
}, 33)  // ~30fps

Buffer Strategies

Low-Latency Strategy

const lowLatencyBuffer = new SimplePlayoutBuffer(100)  // 100ms target
Best for:
  • Video conferencing
  • Live interactions
  • Gaming

Smooth Playback Strategy

const smoothBuffer = new SimplePlayoutBuffer(1000)  // 1s target
Best for:
  • Pre-recorded content
  • Unreliable networks
  • High-quality playback

Adaptive Strategy

const adaptiveBuffer = new AdaptivePlayoutBuffer(500)  // Start at 500ms
Best for:
  • Variable network conditions
  • Mobile applications
  • Long-duration streams

Monitoring and Debugging

Buffer Health Metrics

class BufferMonitor {
  private buffer: PlayoutBuffer
  private underrunCount = 0
  private overrunCount = 0
  
  constructor(buffer: PlayoutBuffer) {
    this.buffer = buffer
  }
  
  check(): BufferHealth {
    const level = this.buffer.level()
    
    if (level === 0) {
      this.underrunCount++
      return { status: 'underrun', level, underruns: this.underrunCount }
    }
    
    if (level > 50) {
      this.overrunCount++
      return { status: 'overrun', level, overruns: this.overrunCount }
    }
    
    return { status: 'healthy', level }
  }
}

Best Practices

Start with a conservative buffer size (500-1000ms) and adjust based on your application’s latency requirements.
Monitor buffer underruns and overruns. Frequent underruns indicate network issues or an insufficient buffer. Frequent overruns suggest the buffer is too large.
For live content, prioritize low latency. For VOD content, prioritize smooth playback.