---
url: https://doc.moq.dev/bin/gstreamer.md
description: moqsink and moqsrc elements
---

# GStreamer Plugin

Two elements: **moqsink** publishes a pipeline to a relay, **moqsrc**
subscribes to a broadcast and exposes one source pad per rendition.

```bash
# Inspect (Nix bundles the plugin with gst-launch)
nix shell github:moq-dev/moq/release#moq-gst --command gst-inspect-1.0 moq

# Play the public test broadcast
nix shell github:moq-dev/moq/release#moq-gst --command gst-launch-1.0 -e \
  moqsrc name=s url=https://cdn.moq.dev/demo broadcast=bbb.hang \
  s.video_0 ! queue ! decodebin3 ! videoconvert ! autovideosink \
  s.audio_0 ! queue ! decodebin3 ! audioconvert ! autoaudiosink

# Publish a test pattern
gst-launch-1.0 -e videotestsrc is-live=true ! x264enc tune=zerolatency ! h264parse \
  ! video/x-h264,stream-format=byte-stream,alignment=au ! mux.sink_0 \
  moqsink name=mux url=https://cdn.moq.dev/anon broadcast=<your-name>.hang sink_0::encoder=true
```

Install via `apt install gstreamer1.0-moq` or `dnf install gstreamer1-moq`
([Install](/setup/install)), or build with `cargo build -p moq-gst` and point
`GST_PLUGIN_PATH_1_0` at the output. `http://` URLs pin the relay's
certificate fingerprint automatically, so local development needs no TLS setup.
That scheme is for localhost only: the fingerprint is fetched unauthenticated
and the WebSocket fallback runs as cleartext `ws://`, so use `https://` for
anything else.

## moqsink

| Codec | Caps |
| --- | --- |
| H.264, H.265, AV1, VP8, VP9 | `video/x-h264`, `video/x-h265`, `video/x-av1`, `video/x-vp8`, `video/x-vp9` |
| AAC, MP3, Opus | `audio/mpeg`, `audio/x-opus` |
| Captions | `text/x-raw` (one WebVTT cue per buffer, PTS is the cue start and the buffer duration its end) |
| Opaque data | `application/octet-stream` (raw bytes on a named track, one group per buffer) |

A `text/x-raw` pad is how captions get in: ffmpeg cannot mux a subtitle track
into fragmented MP4, so `moq import fmp4` can't carry one, while a demuxer that
resolves timed text (`qtdemux` on a 3GPP timed-text track) can feed the pad
directly. A cue with no duration is dropped rather than left on screen.

Each `sink_%u` request pad is one track. Pad properties: `track` names it
(default: after the codec), `container=loc` publishes it as
[LOC](/concept/standard#loc) instead of the legacy hang container,
`encoder=true` marks it as fed by a local encoder, and
`track-status`/`track-error` report its lifecycle. Element properties:
`url`, `broadcast`, `tls-disable-verify`, `quic-idle-timeout`,
`quic-keep-alive`, and read-only `status`, `connected`, `moq-version`, and
`estimated-send-rate`, `estimated-recv-rate`, `connection-stats`, and
`sessions`. The sink reconnects for as long as the pipeline runs and only
reports `failed` on an answer redialing can't change, such as a rejected token.

`connection-stats` is null while disconnected. While connected it is a
`GstStructure` named `moq-connection-stats` containing the transport metrics
available from the active backend. Missing metrics are omitted rather than
reported as zero. Its possible `guint64` fields are `rtt-us`,
`estimated-send-rate-bps`, `estimated-recv-rate-bps`, `bytes-sent`,
`bytes-received`, `bytes-lost`, `packets-sent`, `packets-received`, and
`packets-lost`. Poll the property for current counters; property notification
marks connection and disconnection edges.

`sessions` is a `GstStructure` named `moq-sessions` with `guint64` fields
`started` and `ended`, the cumulative connect and disconnect counts for the
current element session in the same shape as the relay's sessions track. One
read returns both counters from the same instant, so `started - ended` is 1
while connected and 0 otherwise, reconnects are `started - 1` once `started`
is at least 1, and a rate is the delta over any window you sample. Unlike
`status`, a connection that drops before you poll still moves both counters.

Set `encoder=true` on audio and video pads a local encoder feeds
(`x264enc`, `opusenc`, ...). The pad then measures how late each frame reaches
the sink behind its running time and raises the catalog `jitter` by the spread,
and `delay` by how far it trails the earliest such pad, so players buffer for an
encoder that delivers irregularly or behind the others. Leave it off, the
default, for file, demuxed, and network media: their arrival reflects the disk
or the network, not the original encoder, and a GStreamer segment cannot tell
the two apart. Text and opaque pads refuse it.

After a pause, flush, or changed TIME segment, the next media buffer starts a new
timeline epoch and resets the handoff baseline. The pause does not inflate
advertised jitter, and previously measured maxima remain. Resumed timestamps
must continue forward on the broadcast media clock.

A video pad joining mid-GOP drops delta frames until its first keyframe. If a
source rewinds below the producer's live edge without signalling a break, the
pad drops that frame and waits for a keyframe at or beyond the live edge. It
keeps the rendition alive and preserves the media timeline; it does not shift
rewound timestamps forward.

## moqsrc

Pads are named by kind and appear as the catalog announces renditions:
`video_0`, `video_1`, `audio_0`. Link the pad you want by name; the terse
`moqsrc ! decodebin3` form links only the first pad offered, which may be
audio. Each pad sends EOS when its rendition ends. A rendition that leaves the
catalog keeps its pad until its track ends, while one whose format changes is
replaced by a new pad. Properties: `url`, `broadcast`, `tls-disable-verify`.

Debug with `GST_DEBUG=*:4` for GStreamer and `RUST_LOG=debug` for the plugin.
