Low-latency protocol for media. SHM and UDP implementation.
  • Rust 99.6%
  • Shell 0.4%
Find a file
Slaygon 27ea5d7c44 Replace typographic characters with ASCII equivalents
The crate moved to the public slayradio-public org. Per project
convention, source and docs use only keyboard-typable ASCII so terms
stay searchable and typeable. Converts em/en dashes (-> --/-), arrows
(-> ->), the micro sign (-> us), multiply (-> x), and approx (-> ~=)
across comments and README. No behavior change.
2026-06-21 17:43:06 +02:00
src Replace typographic characters with ASCII equivalents 2026-06-21 17:43:06 +02:00
.gitignore Initial commit 2026-05-14 15:33:33 +02:00
build.sh feat(protocol): add sender_ts_ns to wire and SHM frames (protocol v3) 2026-05-31 11:51:23 +02:00
Cargo.lock UDP pacing adjusted to match actual reality 2026-05-21 01:34:30 +02:00
Cargo.toml chore: add MIT OR Apache-2.0 dual license 2026-06-21 15:58:15 +02:00
LICENSE-APACHE chore: add MIT OR Apache-2.0 dual license 2026-06-21 15:58:15 +02:00
LICENSE-MIT chore: add MIT OR Apache-2.0 dual license 2026-06-21 15:58:15 +02:00
PROTOCOL.md docs: note v4 discovery version gate and CAPS re-send 2026-06-16 00:59:05 +02:00
README.md Replace typographic characters with ASCII equivalents 2026-06-21 17:43:06 +02:00

slay-slm

Rust implementation of the SLM (SLAY Media) protocol sender -- a low-latency protocol for media.

It is an attempt to get as close to real-time interaction between an external "VJ" application, like a soundboard or video clip player, and OBS as possible. Initial attempts were made using NDI, but that added several hundred milliseconds (at best) of latency between hitting "play" and actually getting the content through to OBS, which is why this project exists.

Handles multicast discovery, the HELLO/CAPS handshake, UDP frame delivery, and shared memory transport (POSIX on Unix, Named File Mappings on Windows). Wire format is defined in PROTOCOL.md.


What it does

  • Broadcasts multicast ANNOUNCE packets (~1 Hz) so receivers can find the sender
  • Accepts HELLO from receivers and replies with CAPS (stream parameters)
  • Sends NV12 video frames and f32le audio frames to all registered receivers over UDP
  • Writes frames into a shared memory ring buffer for same-machine receivers (no UDP overhead)
  • Tracks receiver liveness and expires stale entries automatically

What it does not do

Frame production is the caller's responsibility. This crate accepts raw NV12 bytes and f32le PCM samples; it does not decode video or audio.

Planned protocol extensions

Two new packet types are planned for a receiver->sender feedback channel (#21):

Value Name Direction Purpose
0x30 PKT_TALLY receiver -> sender 1-byte tally state: 0 = off, 1 = preview, 2 = program (on-air)
0x31 PKT_MSG receiver -> sender UTF-8 freetext message (max 255 bytes) from operator to remote sender

Both are sent by OBS as UDP unicast to the sender's ctrl_port (already carried in every ANNOUNCE packet), so no new socket is needed on either side. PKT_TALLY is re-sent on every HELLO so a reconnecting sender gets the current state immediately.

This feedback channel also provides the foundation for the dynamic bitrate adaptation design in #10.


Usage

[dependencies]
slay-slm = { git = "https://git.c64.org/slayradio/slay-slm.git", branch = "main" }
use slay_slm::{SlmConfig, SlmSender};

let sender = SlmSender::new(SlmConfig {
    name:       "My Source".into(),
    width:      1920,
    height:     1080,
    fps_num:    30,
    fps_den:    1,
    compress:   true,   // LZ4 on the UDP video path
    audio_only: false,
})?;

// In your frame loop (synchronous; use block_in_place from async):
sender.send_video(timestamp_ns, &nv12_bytes);
sender.send_audio(timestamp_ns, &f32le_samples);

// Between clips:
sender.clear_disconnect();   // before starting
sender.send_disconnect();    // after ending, so the receiver flushes its frame queue
sender.update_heartbeat();   // call from idle loops to stay alive

// On shutdown:
sender.send_bye();

SlmSender is Clone and backed by an Arc, so you can share a handle between your feed loop and a status-polling thread without wrapping it yourself.

All send methods are synchronous. When calling from an async tokio task, wrap them in tokio::task::block_in_place.


Platform notes

Feature Linux / macOS Windows
UDP transport yes yes
SHM transport yes (POSIX shm_open) yes (Win32 Named File Mapping)

The shared memory layout is byte-for-byte identical on both platforms; only the OS primitives differ. On Unix the region is a POSIX named SHM object (/slm-<name>); on Windows it is a page-file-backed Named File Mapping (Local\slm-<name>). Both use named semaphores for wake-up signalling.

The Unix path is gated behind #[cfg(unix)] (requires librt on Linux, linked automatically via libc). The Windows path is gated behind #[cfg(windows)] (requires windows-sys). On any other platform the transport is a no-op stub and all frames go over UDP only.


Protocol

See PROTOCOL.md for the full wire format specification, packet layouts, session lifecycle, and SHM region layout. The C header files in the obs-slm-source repo (slm_protocol.h, slm_shm.h) are the normative cross-language reference.