Skip to main content

hang Media Layer

hang is a WebCodecs-compatible media encoding layer built on top of moq-lite. It handles codecs, containers, and media-specific metadata, making it easy to stream audio and video.
Think of hang as HLS/DASH for Moq - it defines how media is packaged and described, while moq-lite is the HTTP that delivers it.

Why hang?

While moq-lite provides generic pub/sub transport, media streaming has specific needs:
  • Codec information: What codec? What parameters?
  • Timing information: When should this frame be displayed?
  • Track relationships: How do audio and video tracks relate?
  • Rendition selection: Which quality level should I subscribe to?
hang provides standardized answers to these questions, making it easy to build interoperable media applications.

Architecture

Each hang broadcast consists of:

1. Catalog Track

A special JSON track that describes the broadcast:
{
  "tracks": [
    {
      "name": "video/1080p",
      "kind": "video",
      "codec": "avc1.64002a",
      "width": 1920,
      "height": 1080,
      "framerate": 30,
      "bitrate": 5000000
    },
    {
      "name": "audio/en",
      "kind": "audio",
      "codec": "mp4a.40.2",
      "sampleRate": 48000,
      "channelCount": 2,
      "bitrate": 128000
    }
  ]
}
Key features:
  • Live updates: Catalog is a live track that updates as renditions change
  • WebCodecs compatible: Codec strings work directly with WebCodecs APIs
  • Discovery: Subscribers learn about available tracks before subscribing
  • Metadata: Includes dimensions, bitrates, languages, etc.

2. Media Tracks

Individual tracks for each rendition, containing containerized frames:
  • video/1080p - 1080p video rendition
  • video/720p - 720p video rendition
  • audio/en - English audio track
  • audio/es - Spanish audio track

Container Formats

hang supports two container formats for wrapping codec payloads:

Legacy Container

Simple format: timestamp + codec payload
┌─────────────┬──────────────────┐
│  Timestamp  │  Codec Bitstream │
│  (8 bytes)  │  (variable)      │
└─────────────┴──────────────────┘
Use cases:
  • Simple streaming scenarios
  • Custom pipelines
  • Low overhead

CMAF Container

Fragmented MP4 (fMP4) format: moof + mdat pairs
┌──────────┬──────────┐
│   moof   │   mdat   │
│ (header) │ (payload)│
└──────────┴──────────┘
Use cases:
  • Standards compliance
  • Integration with existing tools (FFmpeg, GStreamer)
  • Industry compatibility
Use CMAF for maximum compatibility with existing media pipelines and tools.

Working with hang

Publishing Media

use hang::{Catalog, catalog::Track};
use moq_lite::Origin;

// Create origin and broadcast
let origin = Origin::new();
let broadcast = origin.create("my-stream");

// Create catalog
let mut catalog = Catalog::new();

// Add video track to catalog
catalog.add(Track {
    name: "video/1080p".into(),
    kind: "video".into(),
    codec: "avc1.64002a".into(),
    width: Some(1920),
    height: Some(1080),
    framerate: Some(30.0),
    bitrate: Some(5_000_000),
    ..Default::default()
});

// Publish catalog
catalog.publish(&broadcast).await?;

// Create and publish video track
let mut video_track = broadcast.create_track("video/1080p");

// Publish groups (e.g., GOPs)
for gop in video_encoder {
    let mut group = video_track.create_group(gop.timestamp);
    for frame in gop.frames {
        group.write(frame.data).await?;
    }
    group.close().await?;
}

Subscribing to Media

use hang::{CatalogConsumer, container::Frame};

// Subscribe to broadcast
let broadcast = session.subscribe("my-stream").await?;

// Read catalog
let catalog = CatalogConsumer::new(&broadcast).await?;

// Get available tracks
let tracks = catalog.tracks();
for track in tracks {
    println!("Track: {} ({})", track.name, track.codec);
}

// Subscribe to specific track
let video_track = catalog.subscribe("video/1080p").await?;

// Decode frames
while let Some(frame) = video_track.next_frame().await? {
    let timestamp = frame.timestamp();
    let data = frame.payload();
    
    // Send to decoder (WebCodecs, FFmpeg, etc.)
    decoder.decode(data, timestamp)?;
}

Catalog Updates

The catalog is a live track that can be updated during streaming:
// Add a new rendition mid-stream
catalog.add(Track {
    name: "video/480p".into(),
    kind: "video".into(),
    codec: "avc1.64001e".into(),
    width: Some(854),
    height: Some(480),
    framerate: Some(30.0),
    bitrate: Some(1_500_000),
    ..Default::default()
});

// Update catalog
catalog.publish(&broadcast).await?;
Subscribers receive the update and can switch to the new rendition.

Integration with Media Tools

hang is designed to integrate seamlessly with existing media tools:

FFmpeg

Generate CMAF-formatted content:
ffmpeg -i input.mp4 \
  -c copy \
  -f mp4 \
  -movflags cmaf+separate_moof+delay_moov+skip_trailer+frag_every_frame \
  - | moq publish --format fmp4

GStreamer

Use the hang-gst plugin:
gst-launch-1.0 videotestsrc ! hangsink url=https://relay.example.com name=test

WebCodecs

Direct integration with browser encoding/decoding:
const encoder = new VideoEncoder({
  output: (chunk) => {
    // chunk is already in the right format
    publishToHang(chunk)
  },
  error: (e) => console.error(e)
})

encoder.configure({
  codec: 'avc1.64002a',
  width: 1920,
  height: 1080,
  bitrate: 5_000_000,
  framerate: 30
})

Track Naming Conventions

While hang doesn’t enforce naming, these conventions are recommended:
  • video/{resolution} - e.g., video/1080p, video/720p
  • audio/{language} - e.g., audio/en, audio/es
  • subtitle/{language} - e.g., subtitle/en, subtitle/es
  • Custom tracks - e.g., chat, telemetry, metadata

Libraries

Crate: hang (docs.rs)
[dependencies]
hang = "0.15.1"
moq-lite = "0.15.0"
Also includes:
  • moq-mux - Import from fMP4, HLS, CMAF
  • moq-cli - Command-line publisher

Next Steps

Publishing Guide

Learn how to publish media with hang

Watching Guide

Subscribe to and play hang broadcasts

moq-lite Protocol

Understand the underlying transport

Architecture

See how hang fits in the stack

Build docs developers (and LLMs) love