red5pro/moq-playa
GitHub: red5pro/moq-playa
Stars: 13 | Forks: 0
# Red5 Playa – Modular MOQ Player Framework for Scalable Real-Time Streaming
Reference implementation of **Media over QUIC Transport (MoQT)** in TypeScript — the next-generation live media streaming protocol built on WebTransport.
Full stack from WebTransport to viewport, with two integration paths:
- **`@moqt/*`** — Reference implementation toolkit: protocol, playback, browser adapters
- **`@playa/player`** — Batteries-included drop-in player built on `@moqt/*`
## Quick Start
### `@playa/player` — Drop-in Player
import { Player } from '@playa/player';
const player = new Player(document.getElementById('container')!, {
url: 'https://relay.example.com/moq',
namespace: 'live/broadcast',
});
await player.load();
player.play();
### React / Custom DOM
const canvasRef = useRef(null);
const videoRef = useRef(null);
const player = new Player(null, {
url: 'https://relay.example.com/moq',
namespace: 'live/broadcast',
canvas: canvasRef.current!, // WebCodecs path
video: videoRef.current!, // MSE/CMAF fallback path
});
await player.load();
player.play();
When elements are supplied directly the Player never touches the DOM — no `appendChild`, no `hidden` toggling, no style mutations.
### `@moqt/player` — Protocol-Level API
import { MoqtPlayer } from '@moqt/player';
import { MoqtConnection } from '@moqt/webtransport';
import {
createWebTransport, WebCodecsVideoDecoder, CanvasRenderer,
WebCodecsAudioDecoder, WebAudioOutput,
} from '@moqt/browser';
const player = new MoqtPlayer({
url: 'https://relay.example.com/moq',
namespace: 'live/broadcast',
draftVersion: 16,
createTransport: createWebTransport(),
createConnection: () => new MoqtConnection(16),
createVideoDecoder: () => new WebCodecsVideoDecoder(),
createRenderer: () => new CanvasRenderer(canvas),
createAudioDecoder: () => new WebCodecsAudioDecoder(),
createAudioOutput: () => new WebAudioOutput(),
});
player.on('catalog_received', ({ catalog }) => { /* inspect tracks */ });
player.on('first_frame', () => { /* start your UI */ });
player.on('error', ({ error }) => { /* structured error with severity + code */ });
await player.load();
player.play();
## Package Structure
packages/
transport/ @moqt/transport — Sans-I/O protocol core (draft-14 + draft-16)
webtransport/ @moqt/webtransport — MoQT connection over WebTransport
loc/ @moqt/loc — Low Overhead Container (CaptureTimestamp, VideoFrameMarking)
msf/ @moqt/msf — MSF catalog parsing, track selection, timeline
playback/ @moqt/playback — Jitter buffer, A/V sync, decoder state, gap detection
player/ @moqt/player — Player orchestrator (connect, catalog, subscribe, decode, render)
browser/ @moqt/browser — Browser adapters (WebCodecs, Canvas, WebAudio, MSE)
playa/ @playa/player — Batteries-included player with simple API
### Architecture
The playback core (`@moqt/playback`) has **no browser dependencies**. It produces `DecoderCommand` and `PlaybackEvent` objects. Browser adapters (`@moqt/browser`) consume these. This separation enables testing in Node.js without WebCodecs/Canvas/WebAudio.
WebTransport ──► @moqt/transport ──► @moqt/player ──► @moqt/playback
│
DecoderCommand│PlaybackEvent
▼
@moqt/browser (browser)
WebCodecs / Canvas / WebAudio / MSE
## `@playa/player` API
const player = new Player(container, options);
// Lifecycle
await player.load(); // connect, subscribe to catalog, subscribe to tracks
player.play(); // start rendering
player.pause(); // pause rendering
await player.seek(30_000); // seek to 30s (VOD only, requires timeline track)
player.destroy(); // tear down connection and clean up
// State
player.state // 'idle' | 'loading' | 'playing' | 'paused' | 'ended' | 'error'
player.currentTime // ms
player.duration // ms, undefined for live
player.seekable // true when timeline track is available
player.volume // 0–1
player.muted // boolean
player.levels // available video quality levels
player.audioTracks // available audio tracks
player.currentLevel // active level index
player.activeMediaType // 'canvas' | 'video' — which element is rendering
// Quality (async — resolves when switch commits)
await player.setQuality(index); // manual quality switch (disables ABR)
await player.setQuality('auto'); // re-enable ABR
player.levels; // available quality levels
// Events
player.on('ready', ({ levels, audioTracks }) => { ... });
player.on('timeupdate', ({ currentTime }) => { ... });
player.on('durationchange', ({ duration }) => { ... });
player.on('seeking', ({ targetTime }) => { ... });
player.on('seeked', ({ actualTime }) => { ... });
player.on('qualitychange', ({ level }) => { ... });
player.on('stall', ({ duration }) => { ... });
player.on('error', ({ error }) => { ... });
player.on('statechange', ({ from, to }) => { ... });
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `url` | string | — | WebTransport relay URL |
| `namespace` | string | — | Track namespace (e.g. `live/broadcast`) |
| `draftVersion` | 14 \| 16 | 16 | MOQT draft version |
| `certHash` | ArrayBuffer | — | SHA-256 hash for self-signed certs |
| `autoplay` | boolean | false | Start playback after load |
| `volume` | number | 1 | Initial volume 0–1 |
| `muted` | boolean | false | Start muted |
| `targetLatencyMs` | number | — | Live edge target latency |
| `autoQuality` | boolean | true | Enable ABR |
| `startLevel` | number \| 'auto' \| 'lowest' | 'auto' | Initial quality level |
| `maxResolution` | `{width, height}` | — | Cap video quality |
| `canvas` | HTMLCanvasElement | — | Caller-owned canvas (framework mode) |
| `video` | HTMLVideoElement | — | Caller-owned video element (framework mode) |
## `@moqt/player` MoqtPlayer API
// Hooks — intercept and override decisions
player.hooks.beforeSubscribe.use(async (intent, next) => {
if (shouldSkip(intent.trackName)) return; // cancel
return next(intent); // or return next(modifiedIntent);
});
player.hooks.beforeQualitySwitch.use(async (intent, next) => {
if (networkIsBad()) return; // suppress switch
return next(intent);
});
player.hooks.onRecovery.use(async (action, next) => {
if (action.type === 'quality_down') return; // suppress quality drop
return next(action);
});
// Extension points
player.on('media_object', ({ mediaType, groupId, objectId, payload }) => { ... });
player.on('decoder_command', ({ command }) => { ... }); // every WebCodecs command
player.on('namespace_discovered', ({ namespaceSuffix }) => { ... });
player.on('sap_event', ({ entries }) => { ... }); // CMAF seek points
player.on('catch_up_changed', ({ active, rate, latencyMs }) => { ... });
### Key config options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `draftVersion` | 14 \| 16 | 16 | Protocol version (14 for moq-rs / Red5 compat) |
| `maxRequestId` | number | 100 | Initial MOQT MAX_REQUEST_ID (auto-replenished) |
| `knownTracks` | object | — | Pre-known codec metadata for TTFF optimization |
| `catalog` | `{tracks}` | — | Inject catalog externally, skip catalog subscription |
| `targetLatencyMs` | number | — | Live catch-up target |
| `maxCatchUpRate` | number | 1.0 | Max playback rate for catch-up |
| `objectTransform` | function | — | Per-object transform (e.g. decryption) |
| `extensionParser` | function | — | Custom LOC extension parser |
| `onQlogEvent` | function | — | qlog event stream |
| `logLevel` | string | 'none' | Logging: 'none' \| 'error' \| 'warn' \| 'info' \| 'debug' |
### Draft version selection
Browser WebTransport may expose `transport.protocol` (e.g., `'moqt-16'`), enabling automatic draft detection. Node/polyfill WebTransport typically has `protocol` undefined — the connection defaults to **draft 16**.
For **draft-14 relays** (moq-rs, Red5, moqtail), you must explicitly specify the version:
const conn = new MoqtConnection(14); // required — CLIENT_SETUP is draft-specific
After `publishNamespace(ns)`, wait for acceptance via `onMessage` before calling `publishNamespaceDone(requestId)`:
- **v16**: `REQUEST_OK` with the matching `requestId`
- **v14**: `PUBLISH_NAMESPACE_OK` with the matching `requestId`
Do not use a fixed sleep. If `onClose` fires before acceptance, treat the operation as failed.
### Transport robustness
- **MAX_REQUEST_ID sliding window** — auto-replenishes as subscriptions are consumed; starts at 100, extends by 1000 per window
- **Stream limit handling** — `createUnidirectionalStream()` failures caught and surfaced as non-fatal `MoqtConnectionError` (relevant to relays with WT_MAX_STREAMS limits)
- **REQUESTS_BLOCKED** — handled; peer notified via MAX_REQUEST_ID when blocked
## Running the Examples
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Start the dev server (examples at http://localhost:5173)
cd examples && npx vite dev
Example pages:
| Path | Description |
|------|-------------|
| `/player/` | Full-featured player with stats overlay, quality selector, settings |
| `/simple/` | Minimal player — connect, play, done |
| `/connect/` | Protocol explorer — raw message log |
| `/catalog/` | Catalog browser |
| `/broadcast/` | Publisher example |
| `/video/` | Video-only player |
## Testing
# Run all tests (2400+ tests across all packages)
pnpm test
# Watch mode
pnpm test:watch
# Type check
npx tsc --noEmit -p packages/browser/tsconfig.json
## Docs
- [Catalog Testing](docs/catalog-testing.md) — Integration harness for validating catalog subscription against a live relay
## Related Content
- [Learn more about Playa player](https://www.red5.net/blog/consensus-on-a-moq-media-layer-player-framework/#the-playa-connection)
- [Start streaming with MOQ ](https://www.red5.net/media-over-quic-moq/)
## Author
Raymond Lucke and the Red5 Team
## License
MIT
标签:自动化攻击