Centricular

Expertise, Straight from the Source



Devlog

Read about our latest work!

Getting low-latency video from a media pipeline into a web browser has historically meant reaching for WebRTC. WebRTC works, but it has a lot of pieces: SDP negotiation, ICE connectivity checks, and a stack of protocols designed for peer-to-peer calling. WebRTC is built for two-way media between peers that may sit behind NAT, and it needs a separate signalling server.

For a client-to-server setup where the server just pushes video to the browser, a simpler solution might be possible, albeit with some trade-offs discussed later.

WebTransport is a newer browser API designed for this kind of use case and as an improvement on WebSockets. But to appreciate what WebTransport brings, it helps to first understand the layer beneath it, which is QUIC.

Why QUIC

Most web traffic today runs on TCP, the protocol that carries HTTP/1.1 and HTTP/2. TCP guarantees ordered, reliable delivery of a single byte stream. The whole connection is one ordered sequence, and TCP stalls the connection if any packets are lost, even if later packets arrived intact. This is called head-of-line blocking, and for web pages and file downloads it's required. Lost packets are re-sent, and a download just completes a little later.

Real-time media is different. Frames arrive continuously, and one lost packet holds up every frame behind it while the network retransmits. A video player would rather skip damaged content than freeze, but TCP gives it no way to do that.

QUIC is a transport protocol originally designed by Google and later standardized by the IETF. It runs on top of UDP and brings several things that can make it a better fit for streaming.

First, QUIC avoids head-of-line blocking between streams. QUIC separates transmission order from delivery order: packet numbers record what was sent, while stream offsets record what was received. Data from multiple streams is interleaved into QUIC packets on the wire, which keeps transmission efficient. When a packet is lost, only the streams that carried data in that packet stall. All other streams keep making progress (RFC 9000 Section 13). Put each media frame on its own stream, and a dropped packet can't block unrelated frames.

Streams aren't the only option. QUIC also has an optional datagram extension where media can travel as datagrams instead of using a stream. A datagram is a self-contained message that QUIC sends at most once. If it's lost, it's lost. There is no retransmission and no ordering, so the receiver hands each datagram to the application as it arrives, and lost ones leave gaps between the messages that did arrive. A datagram must fit inside a single QUIC packet, and what goes into it, one frame or several, is up to the application.

Second, QUIC has TLS 1.3 encryption built in. All traffic is encrypted, and there is no plain-text mode. Connection setup is faster because the transport and TLS handshakes happen in a single round trip, instead of setting up TCP first and then doing TLS on top. It still has TLS overhead, and more overhead than plain TCP, but it trades latency for security.

Third, QUIC supports connection migration (keeping a session alive when you switch from Wi-Fi to cellular).

QUIC becomes the base protocol, and HTTP/3 maps HTTP traffic onto QUIC. WebTransport builds on top of HTTP/3, and that gives browsers a way to open a QUIC connection to a server and send data as either reliable streams or unreliable datagrams, all from JavaScript.

Why WebTransport instead of WebRTC or WebSockets

WebRTC has been the default way to get video and audio into the browser. It works, and it's supported everywhere. The problem is that WebRTC is built for two-way media between peers that may sit anywhere on the internet, behind NAT, on unknown networks. Even when your two "peers" are just your own server and your own browser, you still go through the offer/answer dance with SDP to negotiate media properties, gather ICE candidates, and often deploy a TURN server for NAT traversal. On top of that, you need a signalling server just to exchange the SDP and ICE candidates in the first place. All of that infrastructure solves problems that don't exist when the server is a known host running in a data center.

WebTransport sidesteps all of that complexity. It's client to server by design, and there is no SDP, no ICE, no offer/answer exchange, and no signalling server. The browser opens a QUIC connection and starts sending and receiving data. For media delivery, you get streams and optional datagrams. An application can send video frames over either.

One trade-off mentioned in the introduction is that skipping SDP means there is no built-in media negotiation. Both ends must agree on the format out of band, which for this demo means hard-coding the codec, resolution, and bitrate in the two applications. Depending on the application, this might or might not be an acceptable trade-off, and you may have to implement format negotiation yourself.

WebTransport can also serve as an alternative to WebSockets, which browser applications commonly use for live data. A WebSocket provides one reliable, ordered, bidirectional message channel. WebTransport, running over QUIC, provides multiple independent reliable streams plus optional unreliable datagrams within a single encrypted connection. With a conventional TCP-based WebSocket, packet loss can cause head-of-line blocking, delaying later messages on the connection. WebTransport avoids head-of-line blocking between streams, while its datagrams can be delivered without waiting for retransmission, at the cost of possible loss and reordering.

WebTransport still requires TLS certificates, but that's the same work a WebRTC deployment already does. WebRTC needs TLS for its signalling server whenever it isn't localhost, while the certificates on its peer-to-peer DTLS connection are usually self-signed, auto-generated ones, validated only by comparing fingerprints exchanged over SDP. WebTransport validates the server certificate on the QUIC connection itself, so there is no additional certificate setup beyond what the deployment already has.

Another trade-off compared to WebRTC is that WebRTC has built-in congestion control, bandwidth estimation, and forward error correction. That allows it to adapt to the network while playing, in a way that WebTransport doesn't out of the box. If the transmission bitrate is too high for the path, a stream backs up and latency grows just like TCP, while datagrams simply lose packets. Efforts like RTP over QUIC and Media over QUIC can address this bandwidth adaptation limitation, however.

WebTransport demo

The demo shows how a GStreamer pipeline can stream H.264 video directly into a browser using WebTransport. The setup has two parts: a Rust server that runs the GStreamer pipeline, and a React web app that receives and displays the video.

GStreamer

The server runs a pipeline that looks like this:

    videotestsrc ! videorate ! videoscale ! video/x-raw,width=640,height=360,framerate=15/1 ! \
    queue ! x264enc bitrate=600 speed-preset=ultrafast tune=zerolatency key-int-max=15 ! \
    h264parse config-interval=-1 ! video/x-h264,stream-format=byte-stream,alignment=au,profile=constrained-baseline ! \
    quinnwtsink

videotestsrc generates a test pattern, which gets scaled to 640x360 at 15 fps. x264enc encodes it to H.264 constrained baseline (a profile that all major browsers can decode) at a bitrate of 600 kbit/sec. The encoded frames then go into quinnwtsink, the GStreamer element that acts as a WebTransport server. The connection must be fast enough to carry this stream.

quinnwtsink is part of the gst-plugin-quinn plugin in gst-plugins-rs. Under the hood it uses the Rust quinn crate for the QUIC transport and web-transport-quinn for the WebTransport protocol layer. It listens for incoming QUIC connections and, once a client connects, sends the H.264 encoded data over a bidirectional stream.

QUIC can work with HTTP-based proxies when combined with MASQUE. The QUIC DATAGRAM extension provides a building block for carrying UDP payloads through a QUIC connection, and MASQUE's CONNECT-UDP protocol uses HTTP/3 to build UDP proxying on top of it. Cloudflare's post on QUIC proxying covers the details. For a media pipeline, this means an end-to-end QUIC connection, and therefore its streams, can traverse a compatible proxy without being converted to TCP, preserving QUIC's transport properties.

One detail worth mentioning: the browser's WebCodecs API needs the frame type (keyframe or delta) and a timestamp for each encoded video chunk. WebCodecs is the browser's API for decoding media natively. The chunks go through the browser's media stack, which may be a hardware or a software decoder. The demo prepends a small 6-byte header to each frame with the frame type and the payload size before handing it off to quinnwtsink. The payload size is there because QUIC streams don't preserve frame boundaries. A read from the stream can return half a frame, a whole frame, or pieces of several frames, so the application buffers incoming bytes and uses the size field to cut out complete frames.

Browser

The browser part is a React app that connects to the GStreamer server using the WebTransport API. WebTransport is supported on all major browsers. For details, see browser compatibility on MDN. Browser-specific differences may affect behaviour; this demo has been tested only with Chromium.

When the user hits connect, the browser opens a WebTransport session to https://localhost:4433 and creates a bidirectional stream. As data arrives, the app reassembles complete frames, then feeds each frame to the browser's WebCodecs VideoDecoder. The decoder outputs raw frames, which get drawn onto a <canvas> element.

The whole pipeline, from GStreamer's test pattern generator to pixels on the canvas, runs with a single WebTransport connection.

Running the demo

The demo is under net/quinn/examples/ in gst-plugins-rs. It has two pieces:

  • webtransport_webcodec.rs: the Rust server running the GStreamer pipeline
  • webtransport-webcodec-browser/: the React front-end

Since WebTransport requires TLS, you need a certificate. For local testing, a self-signed certificate works if you launch Chrome or Chromium with the right flags (--origin-to-force-quic-on and --ignore-certificate-errors-spki-list). The README in the demo directory walks through the exact steps.

Once the server is running, start the React dev server (pnpm run dev), open localhost:3001 in a Chromium-based browser, and click connect. You should see the GStreamer test pattern appear on the canvas.

Conclusion

In the presence of network latency, a WebTransport stream behaves like TCP. There are two sources of latency under packet loss. First, a lost packet stalls the stream until retransmission, and nothing in the transport tells the media pipeline to skip the damaged frame. Second, QUIC's congestion control reduces the sending rate under loss, and a fixed-bitrate pipeline can fall behind real-time. This is in comparison to WebRTC, which handles both with media-aware congestion control and forward error correction, so a video call can survive loss gracefully.

The datagram API avoids the first part of the problem: a lost datagram is dropped immediately, and the receiver just sees a gap. Congestion control still applies, however, so under sustained loss the sender slows down. If the application doesn't adapt its bitrate to match, the network continues dropping data. With WebTransport, your application gets to choose how to handle this, but it does have to handle it.

When latency matters, WebTransport datagrams are one option. The RTP over QUIC draft maps RTP and RTCP packets onto QUIC streams, datagrams, or a mixture of both, and leaves the choice to the application. Media over QUIC can be another option.

This demo is a starting point. With quinnwtsink acting as the server and the browser handling decoding and rendering, you can replace videotestsrc with a real video source (a camera, a file, a network stream) and stream it into a web app without WebRTC. Support for multiple streams is also provided by the quinnquicmux and quinnquicdemux elements, and an example is included upstream.

There are rough edges. WebTransport is still relatively new, and the API surface in quinnwtsink is minimal. QUIC has mandatory transport-level congestion control, but quinnwtsink doesn't expose it as of this writing, and the demo does no application-level congestion control or bitrate adaptation either.

The plugin also supports RTP over QUIC upstream, and work on Media over QUIC is in progress.

If you have ever wrestled with WebRTC when all you needed was a simple server-to-browser push, approaches utilising QUIC might serve your use case.


Centricular

Meet us at IBC 2026!


The International Broadcasting Convention takes place between 11th and 14th September this year, and Centricular will be there with Tim and Mathieu stationed at Booth FT30 in Hall 14!

If you're working on something with GStreamer, come by and tell us about it. The same goes if you're stuck on some multimedia problem, or have an idea for a project and aren't quite sure yet what the right way to build it is. We’re always glad to meet other people working in multimedia, so even if you just fancy a chat about what you’re up to, come and say hi.

We’ll be there for the whole show, so find us at Booth 14.FT30 if you’re around. See you in Amsterdam!


Taruntej Kanakamalla

GStreamer Rust plugins in the Yocto Project


Using GStreamer's Rust plugins (gst-plugins-rs) on Embedded Linux just became easier, thanks to the new bitbake recipe that is now a part of OpenEmbedded-Core (OE-Core).

Yocto project and OpenEmbedded

The Yocto project has been the de facto provider of build infrastructure for creating custom Linux images, especially for embedded systems, regardless of the hardware architecture.

The OpenEmbedded build framework is the build system used by the Yocto project. It uses Bitbake to execute various build related tasks such as fetching and compiling the code, installing and packaging of the binaries etc. Bitbake executes these tasks according to the instructions provided in the form of a recipe (.bb) and other metadata files (e.g., .bbappend, .inc, .bbclass).

OE-Core contains metadata that comprises foundational recipes, classes, and associated files that are meant to be commonly-used by many different OpenEmbedded-derived systems, including the Yocto Project.

A recipe for Gstreamer Rust plugins

bitbake recipes for GStreamer have already existed in the OE-Core layer for a long time. These recipes build plugins and libraries belonging to various submodules of GStreamer.

We created a new recipe for the GStreamer Rust plugins, which was merged into OE-Core recently.

This recipe was inspired by a pre-existing patch submitted by Bartosz Golaszewski 4 years ago, but significant changes were needed. Some of them are listed below.

  • Use dependencies only from crates.io

    For reproducibility, cargo.bbclass uses the --frozen flag to ensure that cargo doesn't update Cargo.lock and doesn't fetch anything from the network at build time. The necessary fetching is done by bitbake in an earlier step do_fetch, which downloads source code and other files based on the path(s) listed in SRC_URI. Refer “file download support” for more information.

    To add all the plugin dependencies to SRC_URI, we execute update_crates, which is a one-time task that auto-generates a <recipe>-update-crates.inc file. The update_crates function captures the URLs and checksums of the packages present in Cargo.lock, but only those that use the crates.io registry. It ignores all other (for e.g. git-based source) packages.

    By default, some of the dependencies of gst-plugins-rs, such as gstreamer-rs and gtk-rs, point to Git repositories in the manifest (Cargo.toml) and lock (Cargo.lock) files. As a result, they are not added to SRC_URI automatically and are not downloaded during the do_fetch stage unless we add them manually. However, as pointed out in a review comment, all the dependencies should be in the auto-generated gstreamer1.0-plguins-rs-update-crates.inc file and should not be manually specified in SRC_URI.

    To address this problem, starting with gst-plugins-rs release 0.15.3, we changed the root manifest file (Cargo.toml) for workspace dependencies like gstreamer-rs, gtk-rs etc from git repositories to the corresponding crates.io package names.

    It's also worth noting that the plugins gst-plugin-ffv1 and gst-plugin-flavors have been skipped in the recipe because they are not part of gst-plugins-rs releases, and hence do not have crates.io packages.

  • Use cargo-c.bbclass instead of cargo.bbclass

    Switching to cargo-c helped to generate and ship C-ABI compatible libraries and pkg-config files. It also required a minor fix in cargo_c.bbclass to always specify the path to the directory where the library files should be installed, because the default value <prefix>/lib is not the correct lib directory for some targets.

  • Skip recipe for 32-bit x86 platforms without SSE

    The ring crate, which is pulled as a dependency by various plugins, fails on 32-bit x86 platform builds that do not have SSE support. These plugins include reqwest, rswebrtc, quinn etc. It is not trivial to fix this without moving away from the ring dependency, so taking into account the small number of affected users, we decided to skip the recipe for these platforms.

Usage

By default, this recipe ships only the plugins marked essential and important, namely audiofx, closedcaption, file, fallbackswitch, tracers, threadshare, rtp, inter, isobmff, hlssink3, mpegtslive, reqwest, rtsp, webrtc, webrtc-signalling, videofx, webp.

If you want to install some or all of these in your image/rootfs, you can append those package names to IMAGE_INSTALL, f.ex.,

  • IMAGE_INSTALL:append = " gstreamer1.0-plugins-rs-meta" for all plugins in the default-members list, or
  • IMAGE_INSTALL:append = " gstreamer1.0-plugins-rs-audiofx gstreamer1.0-plugins-rs-webrtc gstreamer1.0-plugins-rs-reqwest" for specific plugins

You can also override the PACKAGECONFIG variable to add or remove the plugins from the default list.

Acknowledgements

Finally, a big thanks to the maintainers and reviewers from the OpenEmbedded community as well as my peers at Centricular for their valuable feedback on the patches.

Availability

This recipe is available in the master branch at moment and is expected to be part of the upcoming Yocto project release (Blacksail 6.1). This recipe will not be added to the upstream stable branches (Wrynose/Scarthgap) because Yocto policy dictates that features cannot be backported to a stable release.

If you are keen to use this recipe in the released Yocto branches, it should be straightforward on Wrynose (Yocto release 6.0). Cherry-pick the cargo-c and gstreamer1.0-plugins-rs patches, and use the License expression as per older syntax (expand the below block for the exact diff).

License expression change for Wrynose
diff --git a/meta/recipes-multimedia/gstreamer/gstreamer1.0-plugins-rs_0.15.3.bb b/meta/recipes-multimedia/gstreamer/gstreamer1.0-plugins-rs_0.15.3.bb
index 1800dfc543..af488e1587 100644
--- a/meta/recipes-multimedia/gstreamer/gstreamer1.0-plugins-rs_0.15.3.bb
+++ b/meta/recipes-multimedia/gstreamer/gstreamer1.0-plugins-rs_0.15.3.bb
@@ -1,7 +1,7 @@
SUMMARY = "GStreamer Rust Plugins"
HOMEPAGE = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"

-LICENSE = "Apache-2.0 OR MPL-2.0"
+LICENSE = "Apache-2.0 | MPL-2.0"

SRC_URI += "\
git://gitlab.freedesktop.org/gstreamer/gst-plugins-rs;protocol=https;tag=${PV};nobranch=1;name=default \

However, to make it work on Scarthgap (Yocto release 5.0), in addition to the above patches, further work is required - such as bumping up the rust and cargo family recipes to 1.92 (minimum supported version for gst-plugins-rs) or later, and fixing other dependents of the rust/cargo packages.

If you have questions, comments, or if you need any help using this recipe in your project, please feel free to get in touch.



RTSP stands for Real-Time Streaming Protocol, standardized by RFC 7826. It's commonly used to stream audio and video from IP cameras.

In GStreamer, RTSP handling was traditionally done by rtspsrc, a widely used and mature element, but it still had several architectural limitations that were hard to address within the existing codebase.

rtspsrc ties the server state to client/pipeline state. This resulted in issues with

  • Multicast playback
  • PAUSE being sent on error, immediately followed by tear down
  • Flushing seeks involved state changes causing glitches and possible deadlock

As a result, the element has been rewritten from scratch in Rust as rtspsrc2. See this GStreamer Conference Talk and README for more details.

Until now, some features were missing, so if your application relied on them, rtspsrc2 couldn't replace the original.

Many of these have now been implemented:

Authentication

Basic and Digest authentication are supported. Digest covers MD5, SHA-256 and SHA-512-256, so rtspsrc2 can connect to servers that require a username/password and can perform the challenge response handshake automatically.

TLS/TCP support

rtspsrc2 can connect over TLS (the rtsps:// scheme) and supports client certificate authentication for servers that require it. Like rtspsrc, it also has tls-validation-flags property to ignore specific certificate errors like expired certificates.

HTTP tunnelling

Some networks block RTSP but allow HTTP. rtspsrc2 can tunnel RTSP over HTTP (rtsph://) and exposes an extra-http-request-headers property for custom headers (useful for proxies or authentication).

Keep alive

RTSP keep-alive is required because RTSP session state is entirely independent of the underlying transport. During active playback, RTCP serves as the primary proof of liveness. However, during paused states when media and RTCP traffic stop, explicit RTSP keep-alive requests are needed to prevent session timeout.

rtspsrc2 sends periodic keep-alive to prevent session timeout. This is enabled by default via do-rtsp-keep-alive. Disable it if you need compatibility with older servers.

Stream selection

This is one major feature that rtspsrc doesn't implement. rtspsrc2 is streams-aware.

When a source contains multiple streams (for example audio and video), rtspsrc2 exposes them as GstStream objects so applications can choose which streams to set up before playback using the StreamCollection API. For uses of this API by other elements, also see adaptive demuxers.

Consider an example scenario where the RTSP stream has audio and video. An application can choose to ignore audio and select only video via the stream collection API. rtspsrc2 then sends a SETUP request only for video and not audio. An example showing how this can be done in Rust can be seen here.

Unlinked pads

Linking of exposed source pads is now optional, for example, if you only link the video pad and leave the audio pad unlinked, rtspsrc2 handles that without error. Internally, it uses a flow combiner like the original rtspsrc, so selective linking works as expected.

GET/SET_PARAMETER

GET_PARAMETER and SET_PARAMETER requests are supported using signals similar to rtspsrc. These requests are used to retrieve or set the value of a parameter or a set of parameters for a presentation or stream specified by the URI.

Secure Real-time Transport Protocol (SRTP)

SRTP adds encryption and authentication to RTP streams. The RTSP source internally instantiates an RTP session manager element that handles the messages to and from the server, jitter removal, packet reordering along with providing a clock for the pipeline.

While rtspsrc uses rtpbin, rtspsrc2 can use rtpbin or the newer rtpbin2. rtpbin2 unlike rtpbin splits out the sender and receiver RTP session management in two separate elements rtpsend and rtprecv. See this GStreamer Conference Talk for understanding the motivations behind the new RTP elements. By setting the environment variable USE_RTP2=1, rtspsrc2 can use rtpsend and rtprecv.

Secure RTP (SRTP) as of this writing, is only supported when using rtspsrc2 with rtpbin, including MIKEY key exchange embedded in SDP.

With these additions, rtspsrc2 is much closer to being a drop-in replacement for rtspsrc. If you haven't tried it yet, now is a good time.

A GStreamer pipeline to test rtspsrc2 given a RTSP server address.

gst-launch-1.0 -e -vvv rtspsrc2 location=rtsp://some.server/url ! queue ! decodebin ! queue ! videoconvert ! autovideosink

The following key features still need to be implemented before rtspsrc2 achieves complete feature parity with rtspsrc.

  • Clock sync support, such as RFC 7273 (work is on-going to bring this to rtprecv)
  • Pause/seeking support with VOD
  • ONVIF back-channel support


A few days ago I wrote a new GStreamer element around llama.cpp. This element takes a text stream as input, passes it together with a configurable system prompt through a locally running LLM and then forwards the LLM's output. It can also keep a history of past inputs and outputs for more consistent outputs. Check the documentation of the element for everything that can be configured on it.

This can be used for all kinds of purposes involving text. The two most obvious ones are probably translation into different languages and rephrasing.

When compiling the plugin, make sure to select the correct backend. By default only the CPU backend is compiled in but depending on your GPU the Vulkan, ROCm or CUDA backends are going to provide much better performance. For example to compile with the Vulkan backend, use

$ cargo build --release --features vulkan

Translations

Together with the whisper speech-to-text element this would for example allow you to watch a movie with English audio, transcribe that text in real-time, then have the text translated from English to German and render German subtitles on top of the video.

An example pipeline for this would be the following:

$ gst-launch-1.0 filesrc location=movie.mp4 ! decodebin3 name=dbin \
    dbin. ! audio/x-raw ! tee name=audio-tee
    audio-tee. ! queue max-size-time=10000000000 max-size-buffers=0 max-size-bytes=0 ! audioconvert ! audioresample ! \
        whispertranscriber model-path=whisper-ggml-large-v3.bin model-preset=large-v3 chunk-duration=4000 ! \
        textaccumulate latency=0 ! queue max-size-time=10000000000 max-size-buffers=0 max-size-bytes=0 ! \
        llamacpp-texttransform model-path=Hunyuan-MT-7B.Q4_K_M.gguf system-prompt="Translate the following segments into German, without additional explanation." history-size=5 ! \
        textwrap columns=72 ! overlay.text_sink \
    dbin. ! queue max-size-time=10000000000 max-size-buffers=0 max-size-bytes=0 ! videoconvert ! \
        textoverlay name=overlay ! videoconvert ! autovideosink
    audio-tee. ! queue max-size-time=10000000000 max-size-buffers=0 max-size-bytes=0 ! audioconvert ! autoaudiosink

Running this on the famous speech at the end of The Great Dictator produces output such as the following:

charlie-english-german

Note that the plain transcribed text is rendered at the top and the corresponding translation is at the bottom.

This example also makes use of the textaccumulate element to collect full sentences or subclauses, and the textwrap element to wrap long lines so they fit on top of the video frames.

Collecting full sentences or subclauses before the text transformation can help the model with getting more context and providing better output in exchange for additional latency in live pipelines and additional buffering requirements in non-live pipelines.

Rephrasing

The same approach can also be used for rephrasing text. The following example is from the same movie and the system prompt now contained instructions to rephrase the English speech into "duck English".

charlie-english-duck

While this example is not too useful apart from the comedic effect, a useful application of this would be for example rephrasing in simpler and shorter English, or removing usage of jargon.

Models

Generally, any model that is supported by llama.cpp and that supports text input and output can be used for this element. It makes little sense to use a huge model but models with 1-10B are generally more than enough to give useful results while also not requiring too many resources.

The model that should be used can be selected via the model-path property. This expects a local GGUF file, which can be downloaded e.g. from Hugging Face. Models that are known to work well are

The examples above were all created with Hunyuan MT 7B or Qwen 3.5 9B at Q4_K_M (4 bit) quantization, and on my system via Vulkan on an AMD Radeon RX 9070 XT consumed no considerable processing resources for real-time processing and video memory corresponding to the size of the weights plus a small KV cache.

Censoring and safeguards

This all works well up to a certain point. Unfortunately most open weight LLMs come with safeguards built directly into the model weights instead of leaving it to the deployment layer. Depending on your input this might trigger:

charlie-english-censored

Translating subtitles of an R-rated movie, or a documentary about certain historical events, can easily trigger these safeguards. This is apparently also a problem for more serious usage like for translation and summarization of court documents at the Swiss Federal Supreme Court. To get around that, it is possible to use uncensored / abliterated models that can be created with the help of e.g. Heretic or downloaded directly from Hugging Face (e.g. this Qwen 3.5 9B variant). Another option to get around such censorship are prompt injection patterns such as Prompt Injection as Role Confusion but they're less reliable.

Future work

The element is in a decent state and is usable as-is, but there are a couple of future extensions I would like to work on:

  • Optimizing memory usage if the same model is used multiple times in the same process. The weights can be made to be in memory only once but right now every instance has a copy.

  • Optimizing KV cache utilization. Once the history of past inputs/outputs runs full, currently the whole KV cache is discarded. With this PR now merged it should be possible to improve this significantly such that it is not necessary to re-process the whole prompt on each new input.

  • Support for other llama.cpp elements that can work with different modalities, e.g. image or audio input and possibly combined with text input. This could, for example, be used to automatically generate scene descriptions from a low framerate video stream that is triggered on scene changes.

If any of these extensions would be useful to you, or if you have any questions, or are interested in other use-cases, please feel free to get in touch.



GStreamer's VideoToolbox-based decoder for macOS, iOS, and tvOS (vtdec) has recently gained support for decoding HEVC video with an alpha channel.

No special setup is required. When vtdec detects an HEVC+Alpha stream, it automatically negotiates an output format with alpha where possible. Existing pipelines should work without changes - if a non-alpha output format is selected, the alpha channel is replaced with a black background.

You can try it out yourself! Download this HEVC+Alpha sample file and run the following pipeline:

gst-launch-1.0 videotestsrc ! video/x-raw,width=1280,height=720 ! compositor name=comp ! autovideosink \
               filesrc location="phone-hevc-alpha.mp4" ! parsebin ! vtdec ! comp.

The sample video shows a phone rendered with a transparent background. In this pipeline, that background is replaced with the default videotestsrc SMPTE color bars. It should look something like this:

preview

This functionality opens up simpler compositing workflows. You can place HEVC+Alpha assets like this one on top of other content for overlays or effects without needing extra processing like chroma keying.

It's worth noting that another codec for videos with an alpha channel - VP8/VP9+Alpha - is also well supported in GStreamer via our libvpx, VA-API, V4L2 and D3D12 decoders.

If you need to encode your own HEVC+Alpha assets on macOS, you can already do so using vtenc_h265a, which we added to GStreamer a while ago.

HEVC with Alpha decoding via vtdec will be available in the upcoming GStreamer 1.29.2 development snapshot - feel free to give it a go and let us know if you encounter any problems.



TL;DR

There is a new webrtcbin2 rust plugin containing split webrtcsend and webrtcrecv elements for handling a WebRTC session. The highlights of webrtcbin2 are that it uses less threads per session by using rtpsend and rtprecv (also rust), implementing DTLS handling internally, using librice (ICE in rust), and handling signalling all within an async runtime. All of these components also share threads with other instances of webrtcsend and webrtcrecv allowing for an even further reduction in the amount of resources significantly improving scalability.

The landscape

When I originally wrote the webrtcbin GStreamer element almost 10 years ago, I did not completely envision the number of users that would come to use this code in some way shape or form. From HTTP based standards such as WHIP, and WHEP and the myriad of projects that use WebRTC in some way for ingest or egress. WebRTC is still one of the best ways to transport live video into a web browser for display. WebRTC's loose compatibility with the SIP ecosystem is also a driving force behind WebRTC's continued use.

Now, webrtcbin has definitely proved itself in situations that require a small number of sessions. Using webrtcbin for a mixing server (MCU) or even SFU with hundreds or even thousands of streams in a single application is still a tall ask. The biggest reason for this is the number of threads that are created for every WebRTC session.

Threads

  1. RTCP thread - rtpbin (used by webrtcbin) creates a thread per session essentially for handling timeouts required by RTCP.
  2. rtpjitterbuffer creates a thread per incoming stream in order to be able to handle timeouts and deal with late or missing RTP packets.
  3. dtlsenc - A thread whose sole purpose is for being able to handle DTLS timeouts.
  4. webrtcbin and signalling - A dedicated thread for handling signalling related changes such as SDP generation, applying remote SDPs, handling ICE candidates, etc.
  5. webrtcbin and ICE - ICE uses libnice on a dedicated ICE network thread per WebRTC session.
  6. Media streaming threads - One streaming thread for sending and receiving media data.

When an application requires many WebRTC sessions, the memory requirements and context switching overhead of having 5 extra threads per WebRTC session can limit the number of sessions that can be concurrently executed.

Pipeline loops

Another concern I had is that for the server mixing/forwarding use case, pipeline loops were almost a necessity due to the basic requirement that participants in a WebRTC call wanting to be able to see and listen to each other. The obvious answer to this problem is to split the pipeline and use some wormhole elements such as appsrc/appsink, intersink/intersrc, proxysrc/proxysink, etc.

What if? - webrtcbin2

With the benefit of hindsight, we can definitely improve on this situation and reduce the number of threads that is required by each additional WebRTC session. Let us go through the list from above.

Pipeline loops

In order to solve the problem of loops in the pipeline, I took a leaf out of the design we made for rtpbin2 and created separate webrtcsend and webrtcrecv elements that interact with a shared WebRTC session object by having the same id. This allows data to flow essentially in one direction without requiring any kind of loop in the pipeline graph.

For some background on why rtpbin2 was created, you can have a look at a previous post I wrote.

Threads

  1. RTCP thread - Amortised over multiple instances inside rtpbin2 using a tokio scheduler.
  2. Jitter buffer per stream - rtprecv (part of rtpbin2) uses the same tokio scheduler for RTCP handling as it does for handling timeouts and packets through the jitterbuffer introducing no extra threads.
  3. dtlsenc is no longer - DTLS is performed (using OpenSSL) directly just before/after ICE processing.
  4. webrtcsend/webrtcrecv and signalling - Signalling occurs on a tokio runtime shared across all instances of webrtcsend/webrtcrecv.
  5. webrtcsend/webrtcrecv and ICE - Uses librice on the same tokio runtime as webrtcsend/webrtcrecv.
  6. Media streaming thread - Same as webrtcbin. Can be amortised by using the threadshare elements.

If we count the number of threads saved, we can see that for every WebRTC session, at least 5 threads are no longer needed in the new design. At 100 sessions, that is roughly a 500 thread saving in both memory and contention.

Features of webrtcsend/webrtcrecv

While webrtcsend and webrtcrecv are functional and can successfully communicate with a web browser such as Chrome or Firefox, there are still some missing pieces. Some of the supported features include:

  • Audio and/or Video streaming. Data channels are not currently supported.
  • BUNDLE is supported and required for multiple media.
  • rtcp-mux is required.

A non exhaustive list of not yet supported features include:

  • Retransmissions and Forward Error Correction (rtpbin2 does not support this yet).
  • Data channels
  • Renegotiation
  • Statistics
  • TURN servers (librice supports it but not yet implemented in webrtcbin2)

All of these missing features are solveable with further implementation effort.

Example

A send and receive example using webrtcbin2 is available the upstream repository and can be used with this example web page. Just make sure that data channels are not enabled as they are currently not supported.

Closing

This work will be part of the upcoming GStreamer 1.29.2 development snapshot or can be built from the main branch of gst-plugins-rs.

Writing a mature WebRTC implementation is an endeavour that requires a fair bit of implementation effort to complete. If you would like to help make a secure, mature WebRTC implementation for GStreamer please get in touch.



New udpsrc2 element

Over the past few years, I have worked on a new GStreamer UDP source element. This is finally merged now and will be part of both the GStreamer 1.30.0 release and the gst-plugins-rs 0.16.0 release.

The old element uses GIO for networking, which is quite inefficient by design. The new implementation uses about 50% less CPU on my machine compared to the old element for a 3 Gbit/s stream.

As can be seen from the docs of the new element, it preserves the API of the old element. As such it should generally be possible to use it as a drop-in replacement.

In addition to performance improvements, the new element also includes various other improvements:

  • Support for faster packet receiving via Generic Receive Offload (GRO) on Linux, and for using recvmmsg() on platforms where it is available to significantly improve receive performance.

  • Complete support for multicast source filtering, including negative filters, and support for platforms that do not have APIs for the IGMPv3 SSM mechanism.

  • Always obtaining kernel-side packet receive times if available, which was opt-in in the old element due to GIO performance issues with socket control messages.

  • New preserve-packetization property that allows outputting multiple packets in the same buffer, which improves performance for formats like MPEG-TS where the UDP packetization is not necessary.

Give it a try with your pipelines and workloads and share your feedback or any issues you encounter.

In the future, io_uring support on Linux could be added for even better receive performance.

SMPTE ST2110 capture

While udpsrc2 is an improvement in general, its primary motivation is better SMPTE ST2110 support in GStreamer. The old element could not handle the packet rates typically used for such streams very well.

ST2110 defines a UDP/RTP-based set of standards for transmitting raw or very-high bitrate audio / video / ancillary data over Ethernet. It is intended as a replacement for SDI.

Related to this, we recently also merged some other improvements:

For all the new depayloaders there are also new, improved implementations of the corresponding payloaders available.

Together, these improvements enable reliable ST2110 stream capture in GStreamer.

An example pipeline putting it all together would look as follows:

$ gst-launch-1.0 \
    \ # Video capture pipeline part
    udpsrc2 address=239.255.64.20 port=16388 multicast-iface=enp15s0 buffer-size=20000000 caps='application/x-rtp, media=video, payload=96, clock-rate=90000, encoding-name=RAW, sampling=YCbCr-4:2:2, depth=10, width=1920, height=1080, exactframerate=60, colorimetry=BT709, pm=2110GPM, ssn=ST2110-20:2017, tp=2110TPN, a-sendonly="", a-ts-refclk="ptp=IEEE1588-2008:7C-2E-0D-FF-FE-1C-81-14:127", a-mediaclk="direct=0", ssrc-327995485-cname=E055FF0F3D6E4B349F7B786D8B6C837B' ! \
      rtprecv latency=0 ! queue max-size-bytes=0 max-size-buffers=0 max-size-time=500000000 ! rtpvrawdepay2 ! \
    \
    \ # Ancillary data capture pipeline part
    udpsrc2 address=239.255.64.20 port=16386 multicast-iface=enp15s0 buffer-size=20000000 caps='application/x-rtp, media=video, payload=98, clock-rate=90000, encoding-name=SMPTE291, vpid_code=138, a-sendonly="", a-ts-refclk="ptp=IEEE1588-2008:7C-2E-0D-FF-FE-1C-81-14:127", a-mediaclk="direct=0", ssrc-2672978631-cname=E055FF0F3D6E4B349F7B786D8B6C837B' ! \
      rtprecv latency=0 ! queue max-size-bytes=0 max-size-buffers=0 max-size-time=500000000 ! rtpsmpte291depay ! combiner.st2038 \
    \
    \ # Combination of video and ancillary data streams and output
    st2038combiner name=combiner start-time-selection=first ! videoconvert ! queue max-size-bytes=0 max-size-time=0 max-size-buffers=3 ! autovideosink \
    \
    \ # Audio capture and output pipeline part
    udpsrc2 address=239.255.64.20 port=16384 multicast-iface=enp15s0 buffer-size=20000000 caps='application/x-rtp, media=audio, payload=(int)97, clock-rate=48000, encoding-name=(string)L24, encoding-params=64, a-sendonly="", a-ptime=0.125, a-ts-refclk="ptp\=IEEE1588-2008:7C-2E-0D-FF-FE-1C-81-14:127", a-mediaclk="direct\=0", ssrc-603238248-cname=(string)E055FF0F3D6E4B349F7B786D8B6C837B' \
      rtprecv latency=0 ! queue max-size-bytes=0 max-size-buffers=0 max-size-time=500000000 ! rtpL24depay2 ! audioconvert ! autoaudiosink

This pipeline receives a 1080p60 4:2:2 YUV 10-bit video stream, ST291 ancillary data, and a 24-bit 48kHz 64-channel PCM audio stream. The video and ancillary data are combined to a single stream, and then both the combined video-ancillary stream and the audio are output.

rtprecv is used here for translating packet capture timestamps and RTP header timestamps to consistent GStreamer timestamps.

Ancillary data

The pipeline above captures all three streams and merges the ancillary data stream with the video. The ancillary data itself is not processed further.

One way to process the ancillary data further is to extract ST12 timecodes from it and overlay them over the video.

For this, insert the following elements before the video sink:

 ... ! timecodestamper source=ancillary-meta ancillary-meta-locations='8:2000,570:2000' \
     ! videoconvert ! timeoverlay time-mode=time-code \
     ! autovideosink

Here timecodes from ancillary data at positions (8,2000) and (570,2000) would be extracted and converted to GstVideoTimeCodeMeta on the video buffers.

We recently added support for extracting ST12 timecodes from ancillary meta as well.

The positions depend on the video signal standard in use and can be found in the ST12 specifications.