npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

stinky-moq-js

v0.1.22

Published

A TypeScript library for Media over Quic (MoQ) with session management and simple API

Downloads

543

Readme

Stinky MoQ JS

A TypeScript library for Media over Quic (MoQ) with session management, automatic reconnection, and simple API for browser applications. Built on top of @kixelated/moq using the moq-lite-draft-04 specification.

Features

  • 🔄 Infinite Reconnection: Persistent reconnection with configurable delays
  • 🔍 Broadcast Discovery: Automatic discovery of available broadcasts via announcements
  • 📡 Session Management: Handle MoQ relay connections with namespace support
  • 📤 Broadcasting: Send binary data to MoQ tracks with status reporting
  • 📥 Subscriptions: Receive binary data from MoQ tracks with automatic retry
  • 🎯 Type Safe: Full TypeScript support with comprehensive type definitions
  • 🌐 WebTransport Only: Uses modern WebTransport for optimal performance
  • 🔁 Bidirectional Communication: Full pub/sub support for two-way data exchange

Installation

npm install stinky-moq-js

Quick Start

import { MoqSessionBroadcaster, MoqSessionSubscriber } from 'stinky-moq-js';

// Create a broadcaster
const broadcaster = new MoqSessionBroadcaster(
  {
    relayUrl: 'https://relay.quic.video',
    namespace: 'my-app/user-123'
  },
  [
    { trackName: 'video', priority: 10, type: 'video' },
    { trackName: 'audio', priority: 5, type: 'audio' }
  ]
);

// Create a subscriber
const subscriber = new MoqSessionSubscriber(
  {
    relayUrl: 'https://relay.quic.video',
    namespace: 'my-app/user-456'
  },
  [
    { trackName: 'video', priority: 10 },
    { trackName: 'audio', priority: 5 }
  ]
);

// Listen to events
broadcaster.on('stateChange', (status) => {
  console.log('Broadcaster state:', status.state);
});

subscriber.on('data', (trackName, data) => {
  console.log(`Received ${data.length} bytes from ${trackName}`);
});

// Connect
await broadcaster.connect();
await subscriber.connect();

// Send data
broadcaster.send('video', videoData, true);

Working with Broadcasts

// Create a broadcaster with track configurations
const broadcaster = new MoqSessionBroadcaster(
  {
    relayUrl: 'https://relay.quic.video',
    namespace: 'my-app/user-123'
  },
  [
    { trackName: 'video', priority: 10, type: 'video' },
    { trackName: 'audio', priority: 5, type: 'audio' }
  ]
);

// Listen to broadcast events
broadcaster.on('broadcastStateChange', (trackName, status) => {
  console.log(`Broadcast ${trackName} state:`, status.state);
  console.log('Bytes sent:', status.bytesSent);
});

broadcaster.on('broadcastError', (trackName, error) => {
  console.error(`Broadcast error on ${trackName}:`, error.message);
});

// Connect and send data
await broadcaster.connect();

// Send binary data (newGroup: true for keyframes/new groups)
const videoData = new Uint8Array([1, 2, 3, 4, 5]);
broCreate a subscriber with track configurations
const subscriber = new MoqSessionSubscriber(
  {
    relayUrl: 'https://relay.quic.video',
    namespace: 'my-app/user-456'
  },
  [
    { trackName: 'video', priority: 10 },
    { trackName: 'audio', priority: 5, retry: { delay: 2000 } }
  ]
);

// Listen to incoming data
subscriber.on('data', (trackName, data) => {
  console.log('Received data from:', trackName);
  console.log('Data length:', data.length);
});

// Listen to subscription status
subscriber.on('subscriptionStateChange', (trackName, status) => {
  console.log(`Subscription ${trackName} state:`, status.state);
  console.log('Bytes received:', status.bytesReceived);
});

subscriber.on('subscriptionError', (trackName, error) => {
  console.error(`Subscription error on ${trackName}:`, error.message);
});

// Connect to start receiving data
await subscriber.connect( console.log('Bytes received:', status.bytesReceived);
  console.log('Retry attempts:', status.retryAttempts);
});

session.on('subscriptionError', (trackName, error) => {
  console.error(`Subscription error on ${trackName}:`, error.message);
});

Configuration

Session Configuration

interface MoQSessionConfig {
  relayUrl: string;              // URL of the MoQ relay server
  namespace: string;             // MoQ namespace (broadcast name)
  reconnection?: {
    delay?: number;              // Delay between attempts in ms (default: 1000)
  };
  connectionTimeout?: number;    // Connection timeout in ms (default: 10000)
  discoveryOnly?: boolean;       // (Subscriber only) Only discover broadcasts, 
                                 // don't auto-subscribe (default: false)
}

Broadcast Configuration

interface BroadcastConfig {
  trackName: string;    // Track name for the broadcast
  priority?: number;    // Priority for the track (default: 0)
  type: 'video' | 'audio' | 'data';  // Type of track
}

Subscription Configuration

interface SubscriptionConfig {
  trackName: string;    // Track name to subscribe to
  priority?: number;    // Priority for the subscription (default: 0)
  retry?: {
    delay?: number;     // Delay between retries in ms (default: 2000)
  };
}

Status Monitoring

Session Status

const status = broadcaster.status;  // or subscriber.status
console.log({
  state: status.state,              // SessionState enum
  reconnectAttempts: status.reconnectAttempts,
  lastError: status.lastError,
  connectedAt: status.connectedAt
});

Broadcast Status

// Broadcasters track individual broadcast status
broadcaster.on('broadcastStateChange', (trackName, status) => {
  console.log({
    state: status.state,         // BroadcastState enum
    trackName: status.trackName,
    bytesSent: status.bytesSent,
    lastError: status.lastError
  });
});

Subscription Status

// Subscribers track individual subscription status
subscriber.on('subscriptionStateChange', (trackName, status) => {
  console.log({
    state: status.state,              // SubscriptionState enum
    trackName: status.trackName,
    bytesReceived: status.bytesReceived,
    lastError: status.lastError
  });
});

States

Session States

  • DISCONNECTED: Not connected to relay
  • CONNECTING: Attempting to connect
  • CONNECTED: Successfully connected
  • RECONNECTING: Attempting to reconnect after disconnect
  • FAILED: Connection failed completely

Broadcast States

  • IDLE: Not broadcasting
  • BROADCASTING: Actively broadcasting
  • ERROR: Error occurred

Subscription States

  • PENDING: Waiting to subscribe
  • IDLE: Not subscribed
  • SUBSCRIBED: Successfully subscribed and receiving data
  • FAILED: Subscription failed

Error Handling

// Session errors
session.on('error', (error) => {
  console.error('Session error:', error.message);
broadcaster.on('error', (error) => {
  console.error('Session error:', error.message);
});

// Broadcast errors (per track)
broadcaster.on('broadcastError', (trackName, error) => {
  console.error(`Broadcast error on ${trackName}:`, error.message);
});

// Subscription errors (per track)
subscriber

Cleanup

// Remove all broadcasts and subscriptions
session.updateBroadcasts([]);  // Empty array removes all
sesDisconnect and cleanup
broadcaster.disconnect();
subscriber.dispose
## Broadcast Discovery

The library automatically discovers available broadcasts via MoQ announcements. This is essential for building multi-user applications like video chat rooms.

### Discovery-Only Mode (Room-Level Discovery)

Use `discoveryOnly: true` to discover broadcasts without automatically subscribing. This is perfect for room-level discovery where you want to find all users in a room and then selectively subscribe to them:

```typescript
// Create a discovery subscriber for a room
const roomDiscovery = new MoqSessionSubscriber(
  {
    relayUrl: 'https://relay.quic.video',
    namespace: 'lobby',  // Room prefix
    discoveryOnly: true   // Only discover, don't auto-subscribe
  },
  [] // No subscriptions needed for discovery
);

// Listen for user announcements
roomDiscovery.on('broadcastAnnounced', (announcement) => {
  console.log('User broadcast:', announcement.path);  // e.g., "lobby/user/abc123"
  console.log('Is active:', announcement.active);
  
  if (announcement.active) {
    // Manually create a subscription to this user's tracks
    const userSubscriber = new MoqSessionSubscriber(
      {
        relayUrl: 'https://relay.quic.video',
        namespace: announcement.path,  // e.g., "lobby/user/abc123"
        discoveryOnly: false
      },
      [
        { trackName: 'video', priority: 10 },
        { trackName: 'audio', priority: 5 }
      ]
    );
    
    await userSubscriber.connect();
  } else {
    // User left - clean up their subscription
    console.log('User left:', announcement.path);
  }
});

await roomDiscovery.connect();

Multi-User Video Chat Room Example

Here's a complete example of building a video chat room with discovery:

// Namespace structure: {room}/user/{userId}
const roomName = 'lobby';
const myUserId = 'user-123';

// 1. Create a broadcaster for your own tracks
const myBroadcaster = new MoqSessionBroadcaster(
  {
    relayUrl: 'https://relay.quic.video',
    namespace: `${roomName}/user/${myUserId}`
  },
  [
    { trackName: 'video', priority: 10, type: 'video' },
    { trackName: 'audio', priority: 5, type: 'audio' }
  ]
);

await myBroadcaster.connect();

// 2. Create a discovery subscriber to find other users
const roomDiscovery = new MoqSessionSubscriber(
  {
    relayUrl: 'https://relay.quic.video',
    namespace: roomName,
    discoveryOnly: true
  },
  []
);

// Track active users
const activeUsers = new Map();

roomDiscovery.on('broadcastAnnounced', async (announcement) => {
  const userPath = announcement.path;  // e.g., "lobby/user/xyz789"
  
  // Skip our own broadcast
  if (userPath === `${roomName}/user/${myUserId}`) return;
  
  if (announcement.active) {
    // New user joined - subscribe to their tracks
    const userSubscriber = new MoqSessionSubscriber(
      {
        relayUrl: 'https://relay.quic.video',
        namespace: userPath
      },
      [
        { trackName: 'video', priority: 10 },
        { trackName: 'audio', priority: 5 }
      ]
    );
    
    userSubscriber.on('data', (trackName, data) => {
      console.log(`Received ${trackName} from ${userPath}:`, data.length, 'bytes');
      // Process video/audio data
    });
    
    await userSubscriber.connect();
    activeUsers.set(userPath, userSubscriber);
    
    console.log(`User joined: ${userPath}`);
  } else {
    // User left - clean up
    const userSubscriber = activeUsers.get(userPath);
    if (userSubscriber) {
      userSubscriber.dispose();
      activeUsers.delete(userPath);
      console.log(`User left: ${userPath}`);
    }
  }
});

await roomDiscovery.connect();

// 3. Broadcast your video/audio
myBroadcaster.send('video', videoData, true);
myBroadcaster.send('audio', audioData, false);

How Discovery Works

The discovery mechanism uses the MoQ announced() method under the hood:

  • Discovery uses announced(prefix): Listens for all broadcasts matching a prefix (e.g., lobby)
  • Subscriptions use consume(path): Subscribes to tracks from a specific broadcast path (e.g., lobby/user/abc123)
  • These are separate operations: Discovery doesn't require consuming, allowing efficient room-level discovery
  • Active/Inactive events: Announcements include an active flag - true when a broadcast starts, false when it ends

Dynamic Track Management

You can add or remove subscriptions dynamically:

// Add a subscription
subscriber.addSubscription({ trackName: 'chat', priority: 0 });

// Remove a subscription
subscriber.removeSubscription('chat');

// Update all subscriptions at once
subscriber.updateSubscriptions([
  { trackName: 'video', priority: 10 },
  { trackName: 'audio', priority: 5 }
]);

Advanced Usage

Infinite Retry Logic

Both reconnection and subscription retries run infinitely with configurable delays:

// Session with custom reconnection delay
const subscriber = new MoqSessionSubscriber(
  {
    relayUrl: 'https://relay.quic.video',
    namespace: 'persistent-app',
    reconnection: {
      delay: 5000  // Wait 5 seconds between reconnection attempts
    }
  },
  [
    {
      trackName: 'important-track',
      retry: {
        delay: 3000  // Wait 3 seconds between retry attempts
      }
    }
  ]
);

Bidirectional Communication

Set up full bidirectional communication between two applications:

// App A: Publisher and Subscriber
const sessionA = new MoQSession({
  relayUrl: 'wss://relay.quic.video',
  namespace: 'app-a'
});

await sessionA.connect();

// Set up tracks
sessionA.updateBroadcasts([{ trackName: 'data-from-a' }]);
sessionA.updateSubscriptions([{ trackName: 'data-from-b' }]);

// Listen for data from App B
sessionA.on('data', (trackName, data) => {
  if (trackName === 'data-from-b') {
    console.log('App A received data from App B:', data);
  }
});

// App B: Subscriber and Publisher
const sessionB = new MoQSession({
  relayUrl: 'wss://relay.quic.video',
  namespace: 'app-b'
});users:

```typescript
// User A: Broadcaster and Subscriber
const broadcasterA = new MoqSessionBroadcaster(
  {
    relayUrl: 'https://relay.quic.video',
    namespace: 'app/user-a'
  },
  [
    { trackName: 'video', priority: 10, type: 'video' },
    { trackName: 'audio', priority: 5, type: 'audio' }
  ]
);

const subscriberA = new MoqSessionSubscriber(
  {
    relayUrl: 'https://relay.quic.video',
    namespace: 'app/user-b'
  },
  [
    { trackName: 'video', priority: 10 },
    { trackName: 'audio', priority: 5 }
  ]
);

// User B: Broadcaster and Subscriber
const broadcasterB = new MoqSessionBroadcaster(
  {
    relayUrl: 'https://relay.quic.video',
    namespace: 'app/user-b'
  },
  [
    { trackName: 'video', priority: 10, type: 'video' },
    { trackName: 'audio', priority: 5, type: 'audio' }
  ]
);

const subscriberB = new MoqSessionSubscriber(
  {
    relayUrl: 'https://relay.quic.video',
    namespace: 'app/user-a'
  },
  [
    { trackName: 'video', priority: 10 },
    { trackName: 'audio', priority: 5 }
  ]
);

// Connect all sessions
await Promise.all([
  broadcasterA.connect(),
  subscriberA.connect(),
  broadcasterB.connect(),
  subscriberB.connect()
]);

// Listen for incoming data
subscriberA.on('data', (trackName, data) => {
  console.log('User A received:', trackName, data.length);
});

subscriberB.on('data', (trackName, data) => {
  console.log('User B received:', trackName, data.length);
});

// Send data
broadcasterA.send('video', videoDataA, true);
broadcasterB.send('video', videoDataB, true