# 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 unpredictibly (at worst) 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](PROTOCOL.md).

More information at the [SLAY Tools](https://slay.tools/) home page.

---

## 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
- Surfaces short operator messages (MSG) that receivers send back over the control channel

## 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.

---

## Usage

```toml
[dependencies]
slay-slm = "0.4"
```

Or, if you want to live on the bleeding edge:

```toml
[dependencies]
slay-slm = { git = "https://git.c64.org/slayradio-public/slay-slm.git", branch = "main" }
```

```rust
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();
```

A receiver (for example the OBS plugin's "Send message to operator" button) can
send short operator messages back over the control channel. Drain them from your
status-polling loop:

```rust
for msg in sender.take_messages() {
    println!("operator: {msg}");
}
```

`take_messages()` returns the messages received since the last call, oldest
first, and empties the queue.

`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-<token>`); on Windows it is a page-file-backed Named File Mapping (`Local\slm-<token>`), where `<token>` is a 16-hex-char FNV-1a-64 hash of the sender name -- a fixed length, so the name stays within macOS's 31-char `shm_open` limit. 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](PROTOCOL.md) for the full wire format specification, packet layouts, session lifecycle, and SHM region layout. The C header files in the [slm-wire-for-obs](https://git.c64.org/slayradio-public/slm-wire-for-obs) repo (`slm_protocol.h`, `slm_shm.h`) are the normative cross-language reference.
