Commit graph Ratatoskr-receiver/plugin/include
Author SHA1 Message Date
Meta Vens
ba47b0daec The receiver dials the producer
RatatoskrReceiver is a CultNet RUDP client towards the advertisement's
media_endpoint, known to the producer by its receiver id as the connect
payload. It redials every second until answered, pings, and treats 4 s of
silence or an explicit disconnect as a detach that it reports and redials
from. Audio arrives on its own channel. Nothing on the consumer's host
listens, so it needs no firewall rule or port forward.

The request no longer carries a receiver endpoint; the OBS source asks
first, then dials. ratatoskr_receiver_local_port is replaced by
ratatoskr_receiver_attached, and the "This host, as the producer reaches
it" setting is gone. CultLib pin c84cb2e.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 23:03:30 +02:00
Meta Vens
8bee11a866 The OBS source, and the mesh side it needs
A new source type, "CultMesh Media Stream". Its properties pull every
gamecult.media_stream_advertisement Odin holds and offer each stream's
video sources, audio sources and codecs from the advertisement, with
bitrate and latency budget; activating publishes a
gamecult.media_stream_request naming this receiver's endpoint. Nothing
in the picker comes from configuration or from Muninn.

The core grows the mesh half (catalog.rs): pull the advertisements,
publish a request, read the producer's answer, all over cultmesh-rs and
tested against an Odin-shaped RUDP catalog server on loopback. The C ABI
gains the catalog and request calls, a video relay that copies each
whole access unit to a loopback UDP port as a raw byte stream, and the
producer's presentation time on every payload.

The plugin itself owns only the OBS lifecycle. Video is drawn from a
private ffmpeg_source child reading the relay — the arrangement the
previous plugin proved in the field, minus the 2,500 lines of transport
it hand-rolled underneath. Audio is PCM straight to
obs_source_output_audio.

Built against a libobs-only build of obs-studio 32.2.2 with obs-deps
2026-07-15 under .sdk/ (git-ignored; the README says how). Installed to
the per-user OBS plugins directory. Not yet exercised with a live stream.

  cargo test -p ratatoskr-core -> 31 passed
  abi_smoke.exe ratatoskr_core.dll -> ABI SMOKE PASSED

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 22:23:05 +02:00
Meta Vens
01e8df4072 Tell the producer what was lost
The return path. A frame still waiting is asked for by chunk after 64 ms,
then every 32 ms, three times, on the previous receiver's field-tested
cadence; a repair request never asks for a keyframe, since it exists so
that one is not needed. A frame given up on is reported late, and once
per 500 ms a keyframe is requested, because whatever depended on it
cannot be decoded either. Records are built by CultLib's
build_receiver_feedback and sent down the producer's own session on the
media channel; with nobody attached they are counted as not sent rather
than lost quietly.

The loopback test runs the whole path over real sockets: a producer
connects, loses one chunk, is asked for exactly that chunk with no
keyframe request, and when the frame ages out hears it was late and
that a keyframe is wanted.

  cargo test -p ratatoskr-core -> 28 passed

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 21:39:33 +02:00
Meta Vens
261818408a Reassemble video here, where the renderer is
Chunks and parity in, whole access units out. The erasure code is the
contract's: XOR stripe parity as gamecult.media_video_parity_shard.v2
describes it, one recovered chunk per stripe. What this crate adds is
receiver policy the contract cannot carry — giving up on an incomplete
frame after 250 ms on this clock or under pressure from 64 newer ones,
remembering completed frames so late chunks do not reopen them, and
holding everything back after a loss until the next keyframe.

The C ABI now hands a renderer whole access units; RATATOSKR_KIND_VIDEO
means that and VIDEO_PARITY is gone, since no renderer has any use for a
parity shard. ratatoskr_receiver_video_stats reports completed frames,
chunks given back by parity, and frames given up on.

The C++ receiver's GF(256) block solver reads fields the v2 record does
not carry and belongs to the July-era muninn.* wire; it stays in Mimir.
Its audio erasure code had no producer on any contract.

  cargo test -p ratatoskr-core -> 20 passed

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 21:21:47 +02:00
Meta Vens
1d5d1e6aa7 Decode the envelope instead of handing over opaque bytes
The media wire envelope now lives in CultLib, so this side can use it rather
than reimplement it. The receiver decodes each media-channel payload into a
GameCultMediaWireRecord, and the ABI carries a kind alongside the bytes so a
caller routes video and audio without parsing anything itself. That was the
last piece stopping this repo from reading a real stream.

A payload that arrives and fails to decode becomes Undecodable and increments a
counter reachable from C. It is not dropped. A consumer that silently discards
what it cannot parse leaves a producer no way to discover its stream is
unreadable — which is exactly how the previous receiver and sender drifted apart
while both were being maintained.

Feedback records decode but yield no payload: they travel the other way and
carry no media, so a caller asking for payloads is not handed one.

Pinned to CultLib a3e6877 on claude/cultmesh-media-stream-contract, which is a
branch rather than main. That pin moves when the branch does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 02:03:41 +02:00
Meta Vens
58210c99fd The squirrel, carrying messages it does not own
Ratatoskr subscribes to a CultMesh media stream and hands frames to a renderer.
First lowering target is OBS.

It exists because the receiver it replaces hand-rolled CultNet RUDP in C++
inside Mimir: acknowledgement, fragment reassembly, sockets, cultmesh:// URL
parsing, wire framing and two erasure codes, in 973 lines of headers linking no
CultLib. That made the receiving end of a protocol a separate implementation
from the sending end, in another language and another repository, and every
CultNet fix from August and September reached cultnet-rs and cultnet-ts without
reaching it.

So the constraint this repo holds is that it implements no transport. It
consumes CultNet. A sequence number or an ACK mask appearing here means
something belongs upstream in CultLib instead, where both ends can share it.

What works: the core opens a subscription over cultnet-rs, drains it, and hands
payloads across a C ABI. Nine Rust tests and fourteen C checks, the latter
loading the cdylib and verifying the header tells the truth rather than assuming
it. The MSVC staticlib is what the plugin will link; the C smoke test loads the
cdylib dynamically so the ABI is checkable with any compiler.

What does not: no reassembly, no FEC, no receiver feedback, and no plugin. It
cannot decode a real Muninn stream yet, because the wire envelope
(MuninnMediaWireRecord, encode_media_wire_record) still lives in Muninn and
needs to move to CultLib where both ends can reach it. README says so plainly
rather than implying more than exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 01:41:30 +02:00