Skip to content

Latest commit

 

History

History
1197 lines (953 loc) · 130 KB

README.md

File metadata and controls

1197 lines (953 loc) · 130 KB

🌊 Rivulet

Modern Screen Recording & Streaming Software

CI License Rust Status Platform OpenSSF Best Practices

A Rust recording and streaming engine — built for performance, safety, reliability, and embeddability

Features • Installation • Bedienungsanleitung • Hilfe • Erster Stream • Roadmap • Contributing

Rivulet


📚 Repositories

Rivulet spans more than one codebase. All repositories are public and MIT licensed:

Repository Content Status & intent
thoser666/Rivulet The application and engine: GUI, rivulet-core engine, capture/audio/streaming crates, installers, workflows, and all versioned documentation (docs/) Primary codebase, active. Every release is built from this repository.
thoser666/Rivulet.wiki The end-user wiki (bilingual EN/DE how-tos, platform tips, FAQ) Documentation companion, active, not compiled into releases. Synced from this repository by the wiki-translation workflow; boundaries with the versioned docs are defined in docs/wiki-content-policy.md.

🏆 Achievements


🎯 Vision

Rivulet is not an OBS clone. It is an embeddable, deterministic recording & streaming engine written in Rust, with a modern GUI on top. It provides the OBS core feature set (capture, encoding, audio, streaming, dual output) while turning OBS's architectural gaps into its own strengths: automation instead of interactivity, a library instead of a monolith, a modern render path instead of legacy OpenGL/D3D, and a stable plugin ABI instead of version-sensitive C DLLs.

Why Rust?

  • 🔒 Memory Safety - No segfaults, no data races
  • ⚡ Performance - Zero-cost abstractions
  • 🛡️ Reliability - Catch bugs at compile time
  • 🌍 Cross-Platform - Write once, run everywhere

🧭 Positioning: OBS weaknesses as Rivulet strengths

OBS strength OBS weakness Rivulet's answer
Powerful, mature feature set Interactive-first: no headless/CI/render-farm use, hard to automate Deterministic pipeline, headless CLI, testable engine (M7)
Monolithic app + libobs libobs is not a clean library API; embedding it into products is painful rivulet-core as a normal, semver-stable crate (M8)
Plugin ecosystem C/C++ plugins against the libobs ABI, version-sensitive, can crash the app WASM plugin runtime + temporary OBS compat mode (M5)
Powerful renderer OpenGL/D3D legacy, hard to modernize WebGPU/Zero-copy from the ground up (M9)
Streaming foundation Core is RTMP/FLV, low-latency protocols are fiddly WebRTC/WHIP & SRT/RIST as first-class citizens (M3)
Windows-first Uneven platform parity (macOS/Linux weaker) Parity as a release blocker, not an afterthought (M5)

Long-term Goals (v1.0+)

  • Feature Parity with OBS Studio (core feature set)
  • Temporary OBS plugin compatibility as a bridge, long-term WASM plugin ecosystem
  • Modern Architecture - Clean, maintainable codebase
  • Deterministic & Embeddable - Engine as a library, headless-capable, CI-friendly
  • Active Community - Open development, regular updates
  • Internationalization - UI and docs are language-neutral; locale files drive all visible strings

✨ Features

✅ Currently Available

  • Screen Capture - Capture your primary monitor in real-time
  • Window Capture - Capture a single application window (games, etc.) in addition to full monitors, with a window picker in the GUI (Linux & Windows); the Record view provides a live thumbnail so the target can be verified before starting
  • Camera Capture - Capture from USB/webcam devices via GStreamer's device provider API on all three platforms — Linux (v4l2 → PipeWire), Windows (Media Foundation → Kernel Streaming → DirectShow, virtual cameras included) and macOS (AVFoundation); the picker fills at startup and again on 🔄, and an empty list says “No camera found.” / „Keine Kamera gefunden.“ instead of staying silently blank; dropdown selection of available cameras with configurable resolution and FPS; engine API list_cameras() / start_camera_capture() — see docs/camera-capture.md
  • Game Capture - Capture a specific game window using Windows Graphics Capture (Windows) or xcap window capture (Linux), with a checkbox toggle and window picker in the GUI. On Windows the picker identifies installed games locally from Steam, Epic Games Store, GOG, Origin/EA app and Battle.net (registry keys + launcher manifests, no storefront API, no telemetry) and groups them by launcher, with the running game offered first and the raw window list kept as an "Other windows" fallback — see docs/game-detection.md
  • Scene Organisation - Folders (parent scenes), color coding, and search/filter for managing multiple scenes; engine API supports parent UUID and color u32 ARGB on Scene struct; engine API list_game_windows() / start_game_capture() (window listing uses xdotool on Linux)
  • Multi-Monitor Selection - Pick any connected monitor from a dropdown instead of being limited to the primary display (Linux & Windows)
  • Region Capture - Record a rectangular area of a monitor with an interactive drag selector over a live preview, plus precise X/Y/W/H inputs and a one-click "full monitor" reset (Linux & Windows)
  • Screen + Audio Recording (Linux GUI) - Full recording flow in the GUI: monitor selection, start/stop, recording timer; system audio + microphone are captured and mixed into the MP4
  • Video Encoding - H.264/H.265/VP9 encoding via GStreamer with codec selection UI; hardware-accelerated encoders (NVENC, QuickSync, AMF) with automatic detection and software fallback
  • Hardware Encoding - NVIDIA NVENC (nvh264enc), Intel QuickSync (qsvh264enc) and AMD AMF (amfh264enc) with automatic detection of the best available encoder and fallback to software x264; engine API set_video_encoder(VideoEncoder) / set_video_bitrate(kbps)
  • Audio Capture (engine, Linux) - Desktop sound and microphone, mixed in real time via GStreamer (48 kHz stereo, per-source volume); usable via the rivulet-audio crate and the record_screen_audio example
  • Audio Mixer UI (Linux GUI) - Start/stop audio capture with live level meter (dB), per-source volume sliders for system/mic
  • Separate Audio Tracks - System and microphone output as separate tracks in the MP4 (via the "Separate Tracks" option, Linux GUI); engine API push_audio_track(frame, AudioTrack)
  • RTMPS Streaming - Live to Twitch, Kick, YouTube or any custom RTMP/RTMPS ingest (H.264+AAC over FLV); StreamSettings presets, engine APIs set_stream_settings + start_streaming (pure stream) or start_local_recording (dual output); example stream_rtmps
  • Dual Output - Record locally and stream simultaneously; once encoded, split via tee into the MP4 file and the FLV/RTMPS sink (enabled by configuring both a local recording and stream settings)
  • Embeddable Session API - Record from your own Rust code without starting the GUI: RecordingJob validates the configuration up front (typed, field-named errors), runs the session from the frames and audio you push in, streams Started/Progress/Paused/Resumed/Stopped events, and finalizes the output on stop() or on Drop; file, stream or dual-output shapes with an inspectable pipeline description (stream keys redacted before logging); runnable example session_embed, which CI executes with the other examples
  • Stream Health & Network Stats - Live status (Connecting/Good/Warning/Poor) with sent/dropped frame counters, bitrate (kbps) and FPS over a sliding window; derived from drop ratio (>5% Poor, >1% Warning), throughput collapse and stalls; engine API stream_stats(), polled in stream_rtmps
  • Recording Performance Metrics - Live FPS, encoder load (%) and output file size during a recording, measured via GStreamer pad probes (per-frame encode duration paired by PTS, filesink byte counter); engine API recording_stats(), shown in the GUI next to the recording timer
  • Hotkeys - F9 toggles recording, F10 pauses/resumes, F11 mutes/unmutes audio; pause skips video frames while capture keeps running, mute drops audio frames; hotkey hints shown in the UI and on the Start Recording button
  • Recording Presets - Resolution and FPS presets (Original, 1080p60, 1080p30, 720p60, 720p30, 480p30) with automatic videoscale/videorate insertion, per-preset bitrate defaults, and encoder-aware caps; engine API set_preset(RecordingPreset), selectable in the GUI
  • Recording Overlay - Burn-in timer (HH:MM:SS) and live FPS counter onto the recorded video via GStreamer textoverlay; toggle in the GUI, engine API set_overlay_enabled(bool) + update_overlay_text(&str)
  • Replay Buffer / Instant Replay - Ring buffer of the last N seconds (H.264+AAC, keyframe-aligned) kept while recording; save the clip as a standalone MP4 via the F12 hotkey or the GUI button; engine API set_replay_duration() / save_replay()
  • Auto-Update - Checks the GitHub Releases API on startup and manually for newer versions, downloads the matching platform package (MSI / AppImage / DMG) with a live progress bar and launches the installer; Windows upgrades retain one Control Panel entry and update the launcher target; the alpha channel keeps up with every feature push. On Windows a detached rivulet-updater watchdog takes over the install: it waits for the running GUI (and launcher) to fully terminate — so the executing files are no longer locked — and only then runs msiexec to completion. This prevents the classic "only the registry is updated but the old files stay" failure.
  • Live Workspace - One page for a running show: encoder preview, the scene dock (multi-select included) and the inline audio strip under a bar with recording, streaming and Studio Take, so switching scenes or checking the picture never means leaving the tab; the Record, Scenes and Stream tabs keep the detailed configuration
  • Recording Live Preview - The Record view shows a throttled thumbnail of the selected monitor or window before recording and continues with the actual encoder-bound frames while recording; the panel reports whether it is ready, waiting for the first frame, or active
  • Sidebar Navigation - Clean, DaVinci Resolve-style UI with a collapsible left sidebar (see docs/ui-design.md)
    • Record - Main recording controls and preview
    • Mixer - Audio mixer with live level meter and volume sliders
    • Scenes - Scene composition (M2, planned)
    • Stream - Meld-style single broadcast page (streaming controls, chat dock, status/health, audio)
    • Assistant - AI chat assistant (M10, planned)
    • Settings - All configuration options (recording, codec, presets, hotkeys, replay buffer, language)
  • Customizable Settings
    • Recording presets (1080p60, 720p30, ...) or original resolution
    • Codec selection (H.264, H.265, VP9)
    • Custom output path with file picker
    • Auto-timestamped filenames
  • Cross-Platform - Windows, macOS, and Linux support (via xcap)
  • Modern UI - Clean interface built with egui
  • Internationalized UI - All visible strings are driven by locale files (English by default, German included)
  • Activity status & Discord Rich Presence - The UI exposes a privacy-safe “Rivulet nimmt auf / streamt” activity model and an opt-out, non-blocking Discord Rich Presence adapter (see docs/activity-status.md); stream keys, URLs, paths, and window titles are never included

📖 Bedienungsanleitung und Wiki

Der zentrale Rivulet User Guide erklärt Installation, Navigation, Aufnahme, Quellen, Live-Vorschau, Szenen, Streaming, Updates und Fehlerdiagnose. Der Weg von der ersten Szene bis zum gesendeten Bild steht im Tutorial Von der Szene zu Aufnahme und Stream.

Für den praktischen Einstieg gibt es die Checkliste für den ersten Twitch-, YouTube- oder Kick-Stream sowie die technische [Stream-Setup- Dokumentation](docs/stream-setup.md

Für Einsteiger-How-tos, Plattformtipps, FAQ und Community-Workarounds ist das GitHub Wiki vorgesehen. Das Wiki ist bilingual (English/Deutsch) und bietet auf jeder Kernseite einen Sprachumschalter. Fehlende Sprachpaare werden automatisch durch den Workflow geprüft; der Ablauf ist in docs/wiki-translation-workflow.md beschrieben. Die Grenzen zwischen versionierter Repository-Doku und Wiki sind in docs/wiki-content-policy.md beschrieben. Das Wiki muss in den GitHub-Repository-Settings durch einen Administrator aktiviert werden.

🚀 Roadmap

Goal: OBS core parity and the architectural strengths OBS cannot offer (determinism, embeddability, modernity). Strategy: First a stable, embeddable engine, then scenes & streaming, then the differentiation features (M6–M11) as product identity. Priority: Gaming streamers are a primary audience — Game Capture (M2) and Replay Buffer (M4) are prioritized accordingly.

#1 Priority: Game capture (D3D/Vulkan/OpenGL hook, zero-overhead) — without this, Rivulet cannot replace OBS for gaming. Current: Window capture via WGC (Windows) / xcap (Linux) for gaming window selection. Roadmap: D3D11/D3D12 interposition (M3), Vulkan interposition (M3), OpenGL hook (M3) as architecture steps after M2.

UI development: new features should follow the navigation structure and egui conventions in docs/ui-design.md. Every milestone also uses the reusable docs/milestone-quality-gates.md for UI, usability, accessibility, reliability, platform, and resource-efficiency review. The ongoing UI audit, its CI-gated accessibility/screenshot contracts, and the accessibility-scanning backlog are tracked in docs/ui-audit.md. The cross-cutting resource goal and measurement method are documented in docs/resource-efficiency-goal.md. The M5 platform coverage — Windows/macOS/Linux feature parity and the explicitly labelled platform limitations — is documented in docs/platform-feature-matrix.md.

Milestone overview

Milestone Focus Status Target Open issues
M0 – Recording Foundation Capture, Encoding, Audio, GUI ✅ Done — M0
M1 – Solid Recording Audio tracks, Hardware encoding, QoL, Overlay ✅ Done (milestone closed) — M1
M2 – Scenes & Composition Scenes, Sources, Game Capture, Scene Organisation, Transitions, Studio Mode ✅ Complete (conditional UI/UX gate; milestone closed) — M2
M3 – Streaming RTMP/RTMPS, WebRTC/WHIP, SRT/RIST, Multitrack Video ✅ Complete (conditional: live interop follow-ups) — M3
M4 – Advanced Output & Capture Virtual Camera, Replay Buffer, Filters, Formats ✅ Complete (conditional: integration follow-ups; milestone closed) — M4
M5 – Ecosystem & Parity WASM Plugins, OBS Compat (descoped → M11), Platform Parity ✅ Complete (milestone closed; signing identities wait on certificate purchase #50, macOS live verification hardware-blocked, OBS compat descoped → M11 #147) — M5
M6 – Creator Toolkit & Interactivity Chat auto-clips, Restream, Remote, Multi-track Audio Routing ✅ Complete (milestone closed; see docs/m6-creator-toolkit-completion-report.md) — M6
M6.9 – Scene & Stream Polish Interims-Backlog: per-Platform Chat-Metadaten, Browser-Engine, Device-Picker, natives Scene-Compositing ✅ Complete (milestone closed; browser-source shipped as backend contract + Windows WebView2 POC — production adapter remains a follow-up) — M6.9
M7 – Automation & Determinism Headless CLI, CI Rendering, Reproducible Pipelines ✅ Complete (milestone closed; see docs/m7-automation-completion-report.md) — M7
M8 – Embeddable Engine & API Stable rivulet-core API, Docs, Tooling ✅ Delivered — M8
M9 – Modern Architecture WebGPU, Zero-copy, Compute Filters 📅 Planned — M9
M10 – AI Chat Assistant Local-first LLM Chat Bot for Streamers 📅 Planned — M10
M11 – Extensible UI & Plugin Platform Persisted layouts, view registry, declarative/WASM plugins, permissions, OBS compat bridge 📅 Planned — M11

Beta-Gate

Beta is not date-driven — it is a manual, criteria-based decision. The project leaves alpha and enters beta (tag vX.Y.Z-beta.1, see Releases) only when all of the following verifiable criteria are met:

# Criterion Verifiable via
1 M1 – Solid Recording complete ✅ All M1 items checked (audio filters/monitoring, hardware encoding, hotkeys, timer overlay, region capture, codec UI, presets)
2 M3 – Streaming complete All M3 checklist items checked (RTMP/RTMPS, dual output, adaptive bitrate, stream-key management, WebRTC/WHIP, SRT/RIST)
3 Platform parity (M5 release blocker) Recording + streaming verified on Windows and Linux; macOS capture parity tracked by the M5 roadmap checkbox
4 Code-signing secrets configured Windows: the WINDOWS_CERT_* pair or the four SIGNPATH_* secrets set in the repo so a beta build produces signed packages (the automation itself is already smoke-tested without secrets by signing-e2e.yml). macOS is outside the beta scope (v0.66.0 ships Windows and Linux), so the MACOS_*/APPLE_* secrets are only needed once macOS enters it (#50)
5 CI fully green on develop All checks pass: build, full test suite, actionlint, ShellCheck, action-pin checks, asset-drift check
6 No known release-blocking bugs No open issue marked as release blocker (the recording frame-loss and no-frame-timeout regressions are fixed)

Gatekeeper: The beta tag is set manually (see the Beta/RC/stable channel below). The roadmap checklist above is the source of truth for when; the release channel only describes how the tag is built and published.


✅ M0 – Recording Foundation

Status: Done

  • Screen capture (monitor, real-time)
  • H.264 video encoding (GStreamer)
  • Audio capture (system + microphone, mixed, 48 kHz stereo)
  • Audio/video synchronization
  • Audio mixer UI with live level meter and volume sliders
  • GUI recording with monitor selection and recording timer (Linux)
  • Skeleton: engine, recorder, scene/source models, plugin system (stubs)

✅ M1 – Solid Recording

Status: Done

Audio

  • Separate audio tracks (system/mic)
  • Audio filters (noise suppression, compressor, limiter)
  • Audio monitoring (source preview)

Performance

  • Hardware encoding (NVIDIA NVENC, Intel QuickSync, AMD AMF)
  • Automatic detection of the best encoder with fallback
  • Performance metrics (FPS, encode load, file size)

Updates

  • Auto-update (update check, download & install via GitHub Releases) — Windows installs run through a detached rivulet-updater watchdog that waits for the app to exit before invoking msiexec, so updated files replace the locked executables instead of leaving the old build behind.
  • Code signing (signing automation present, secrets needed — waiting on certificate purchase, see #50)

Quality of Life

  • Hotkeys (record, pause, mute)
  • Recording timer overlay and FPS counter
  • Region capture and multi-monitor selection
  • Codec selection UI (H264/H265/VP9)
  • Preset management (1080p60, 720p30, ...)

Goal: High-quality recording with audio as a solid foundation for scenes & streaming.


🎨 M2 – Scenes & Composition

Status: Complete (conditional UI/UX gate)

The core concept of OBS: scenes, sources, and transitions. Core scene organisation, collections/profiles, duplication, and source transforms are now implemented. Remaining M2 work is focused on composition UX, transitions, and multi-view/projectors. Studio Mode is now implemented with separate Preview/Program roles and a transition-aware Take action.

Priority for gaming streamers:

Game capture (D3D/Vulkan/OpenGL hook, zero-overhead) — #1 priority: without this, Rivulet cannot replace OBS for gaming. Split into work packages G1–G6:

  • G1 – Capture strategy spike — evaluate the capture approach per graphics API (DXGI Desktop Duplication, Vulkan layer, wgl hook; OBS backends as reference) and define the overhead budget (<1% frame time) and abort criteria. DoD: decision document in docs/ with per-API recommendation and feasibility assessment — docs/game-capture-strategy.md.
  • G2 – Windows DXGI backend — Desktop Duplication for fullscreen/exclusive fullscreen with Graphics Capture fallback; GPU-texture path instead of CPU copy; usable as a scene source. Backend done: rivulet-capture gains a DXGI Desktop Duplication source (DxgiDesktopDuplication) with a zero-copy GPU-texture path (shared-handle hand-off, no CPU readback in the hot path), BGRA→RGBA readback for the compatibility path, output enumeration, and the strategy's abort/fallback rules (access-lost → re-create, protected content → explicit "not capturable", timeout → retry). Unit tests run on every platform; a live-DXGI smoke test runs locally (cargo test -p rivulet-capture -- --ignored). The Windows GUI probes the backend at recording start and shows the active backend (Desktop Duplication vs. Graphics Capture fallback, with reason) during the recording. Scene-source wiring follows with S1 (Source abstraction). DoD: DX9/11/12 fullscreen captured via zero-copy path, tests + docs.
  • G3 – Vulkan hook — VK_LAYER-based capture (OBS vulkan-capture approach) for fullscreen Vulkan games. Layer done: rivulet-vulkan-layer — cdylib that the Vulkan loader discovers via VkLayer_rivulet_capture.json. Intercepts vkQueuePresentKHR → staging buffer readback (image transition, cmd_copy_image_to_buffer, HOST_VISIBLE map). Ash 0.38 for raw Vulkan API access. Layer negotiates via vkNegotiateLoaderLayerInterfaceVersion, forwards all calls to next layer/ICD. Capture pipeline initialized on vkCreateSwapchainKHR, frames captured on each present. Layer activation via VulkanLayerConfig — sets VK_LAYER_PATH + VK_INSTANCE_LAYERS for child process or current process. Frames transferred via shared memory IPC (capture_channel — FrameHeader protocol, ShmReader/ShmWriter). start_vulkan_layer_capture() in game_capture.rs provides channel-based frame reading. The Windows GUI now prefers the layer for fullscreen game capture: when the layer's shared-memory channel is available the frames flow straight into the recording pipeline, otherwise it falls back to Windows Graphics Capture on the window and shows the active backend + fallback reason in the recording view. DoD: Vulkan fullscreen captured within the overhead budget, tests + docs — implementation complete (layer, GUI wiring, fallback status, tests); the within-the-budget number itself is measured by G5 and still open.
  • G4 – OpenGL hook — wglSwapBuffers interception for OpenGL games. Backend done: rivulet-opengl-hook-dll (cdylib) hooks wglSwapBuffers via IAT patching, reads the back buffer via GDI GetDIBits, and writes BGRA frames to shared memory. Host-side rivulet-core/src/opengl_hook.rs provides DLL injection config, SHM reader, and start_opengl_hook_capture() (same poll-loop pattern as the Vulkan layer). BackendKind::OpenGLHook in rivulet-capture/src/backend.rs with i18n keys (EN/DE) and GUI status display (green for active, orange for fallback). The Windows GUI tries Vulkan → DXGI → OpenGL hook → WGC in order. Unit tests for frame header, SHM name, backend status, i18n keys, and error display. DoD: the within-the-overhead number is measured by G5.
  • G5 – Performance verification — FPS/frame-time comparison with vs. without capture, benchmarked against OBS, with a CI regression guard. DoD: benchmark script in scripts/, overhead budget verified for all three backends — done: rivulet-core/src/benchmark.rs (percentile calculation, budget checker, CI report) + scripts/g5-benchmark.py (CI gate, smoke test, JSON report validation). CI regression guard wired into Lints job. All 12 unit tests pass. Actual GPU measurements require running on real hardware; the framework and CI gate are in place. Resource-efficiency methodology and game-first targets are documented in docs/resource-efficiency-goal.md.
  • G6 – Linux fullscreen path (optional) — PipeWire/portal capture on Wayland with X11 fallback. DoD: fullscreen capture on Wayland and X11, tests + docs — done: rivulet-capture/src/pipewire_portal.rs (ashpd portal session + PipeWire frame capture), BackendKind::PipeWirePortal with i18n keys (EN/DE), GUI wires PipeWire portal as primary Linux capture with xcap fallback. 3 unit tests. All 11 capture tests + 16 i18n tests pass.
  • Game capture (window-based) — Windows Graphics Capture (Windows) / xcap (Linux), with checkbox toggle and window picker in the GUI. Issue #239: the window picker identifies real games instead of guessing from window size — installed Steam/Epic/GOG/Origin/EA/Battle.net games read from local manifests and registry keys, ranked by evidence (launcher running signal > foreground-window match > size heuristic), grouped by launcher with localized headers. Privacy: local reads only, no storefront API, no process inspection
  • Game capture live preview — show a live thumbnail of the selected game window in the GUI before recording starts, so the user can verify the correct window is targeted. Done: GamePreview grabs a frame via xcap on window selection and refreshes every 500ms, displayed as an egui::Texture below the game-window picker (Linux + Windows). Also fixed the Source dropdown leaking the selected window title. The window list refreshes live while the picker is open (selection preserved by id) and a manual 🔄 button forces an immediate re-scan.

Sources (image, text, webcam, browser/embedded Chromium, media, color, per-app audio). Split into work packages S1–S8:

  • S1 – Source abstraction — complete the Source trait, properties UI, and per-source transforms. Done: rivulet-core/src/source.rs — SourceKind enum (Image, Text, Webcam, Browser, Media, Color, GameCapture, ScreenCapture, Audio), Transform struct (x/y/width/height/rotation/opacity), Source struct with kind/transform/visibility/locked/z_order, SceneSource for per-scene transform overrides, SourceManager for source CRUD + scene bindings + z-order reordering. 36 unit tests + 18 i18n tests pass. DoD met: any source type can be added to a scene with a configurable transform.
  • S2 – Image source — PNG/JPEG/GIF, folder loop/slideshow. DoD: static and slideshow image sources usable in scenes, tests + docs. Done: rivulet-core/src/image_source.rs (10 unit tests, i18n keys EN/DE).
  • S3 – Text source — rich text, scrolling, outline/background. DoD: styled text source usable in scenes, tests + docs. Done: rivulet-core/src/text_source.rs (Rgba, FontWeight, TextAlign, ScrollDirection, TextSource with scrolling animation, 30 unit tests + i18n keys EN/DE).
  • S4 – Webcam as scene source — expose the existing camera capture as a scene source with a properties panel. DoD: webcam selectable per scene with resolution/framerate settings, tests + docs. Done: rivulet-core/src/webcam_source.rs (PixelFormat, Resolution, WebcamSource with resolution/framerate/mirror/timeout, 19 unit tests + i18n keys EN/DE).
  • S5a – Browser spike — evaluate platform webviews (WebView2 / WebKitGTK / WKWebView) and render into a GPU texture. DoD: spike document with per-platform recommendation and a rendered-frame proof. Done: docs/browser-source-spike.md (wry recommendation, GPU texture integration, risk analysis).
  • S5b – Browser source — URL, interaction, transparency, config UI. DoD: portable browser-source contract, bounded input queue, RGBA frame hand-off, configuration panel, tests + docs. Done: rivulet-core/src/browser_source.rs validates http(s)/file URLs, manages viewport/FPS/zoom/transparency/CSS, queues interaction events, validates rendered RGBA frames, and exposes a platform-neutral adapter trait for WebView2/WebKitGTK/WKWebView. The Scenes view provides the configuration panel; native webview adapter wiring landed in PR #227 (Windows wry/WebView2 adapter with real input forwarding + non-blocking frame poll + GUI backend tick).
  • S6 – Media source — video/audio file playback in scenes (loop, restart on scene entry, speed). DoD: media files play in scenes with controls, tests + docs. Tracked in #66. Done: rivulet-core/src/media_source.rs (MediaType, PlaybackMode, MediaSource with speed/volume/loop/restart, 26 unit tests + i18n EN/DE).
  • S7 – Color source — solid-color background/banner. DoD: color source usable in scenes, tests + docs. Tracked in #67. Done: rivulet-core/src/color_source.rs (ColorSource with hex parsing, opacity, scene-fill, 15 unit tests + i18n EN/DE).
  • S8 – Audio sources — per-app audio capture (Windows WASAPI loopback, Linux PipeWire) and audio input/output device capture as scene sources on Windows and Linux. DoD: per-app and device audio selectable per scene, tests + docs. Tracked in #68. Done: rivulet-core/src/audio_source.rs (AudioSourceKind: Application/Input/Output/Mixed, AudioSource with volume/mute, 16 unit tests + i18n EN/DE). Follow-up (#229): the device half is live on Windows — wasapi-out:<endpoint>/wasapi-in:<endpoint> device ids (DeviceTarget), rivulet-audio/src/device_capture.rs (list_audio_devices + AudioDeviceCapture: shared-mode render loopback for output endpoints, plain capture for inputs, 48 kHz stereo f32), and a Windows Mixer device picker with friendly names + default marker so virtual mixer channels (SteelSeries GG/Sonar) are selectable as sources. Follow-up (#231, Linux + macOS slices): the device picker + capture lifecycle are un-gated to Linux and macOS — Linux: pw-src:<node>/pw-mon:<node> device ids, rivulet-audio/src/device_capture_pw.rs (PipeWire registry enumeration of source nodes and sink monitors with default-metadata default markers; capture per target.object with stream.capture.sink = true for monitors, 48 kHz stereo f32); macOS: core-audio-in:<name> device ids, rivulet-audio/src/device_capture_macos.rs (cpal input enumeration with loopback-capable virtual drivers first, 48 kHz stereo f32 via worker-thread conversion; honest OutputDevice semantics — a render loopback needs a BlackHole-class driver, the picker shows the install hint). Same Mixer picker + live refresh on every platform.
  • Sources — camera capture (webcam via GStreamer)

GUI polish:

  • Glassmorphism panels — semi-transparent top menu bar and left sidebar (theme::glass_frame()) with rounded corners, desktop shows through with frosted-glass effect
  • Hover, focus & active accent strokes — shared theme::accent_button() / theme::paint_interaction_stroke() helpers apply the palette accent (1.5px at 60% alpha on hover, 2px opaque on focus/press) without overriding egui's disabled visuals
  • Preview fade-in animation — theme::preview_fade_alpha() wrapping ctx.animate_bool for smooth game-preview tint fade-in

Scene system:

  • Scene management (multiple scenes, switching, add/rename/remove, switch-back history) — SceneManager in rivulet-core/src/scene.rs, live Scenes view in the sidebar
  • Scene organisation (folders, search/filter, color coding) — top community request, active PRs in OBS; Scene model with parent UUIDs and ARGB color coding (folders via hierarchy), tested in rivulet-core/src/scene.rs
  • Undo/Redo (Ctrl+Z / Ctrl+Y) — scene add/remove/rename/switch operations are undoable; toolbar buttons are disabled when no history is available
  • Scene collections & profiles — named collection/profile context plus JSON import/export are available in the Scenes view; selected recording presets/codecs are applied when recording starts, while profile-specific preset storage remains follow-up work. Tracked in #69.
  • Duplicate scene / source — one-click scene duplication with fresh identity and copied properties, plus source duplication with no accidental bindings; scene-local binding duplication is available from the composition editor
  • Scene hotkeys & auto-switching — assign per-scene F1–F8 hotkeys and define window-title auto-switch rules; focus integration beyond title matching remains a follow-up
  • Sources — window capture (monitor + individual windows, Linux & Windows)
  • Source composition (layers, position, scaling, cropping — each scene stores its own layout per source; switching scenes auto-moves sources to their saved positions) — the Scenes view now provides a source/layer editor for per-scene transform, crop, visibility, lock, z-order, and duplication; native media rendering and transform copy/paste remain follow-up work
  • Transitions — Cut and configurable Fade transitions are selectable in the Scenes view, with a non-blocking progress indicator; stinger transitions remain open
  • Scene overlays — optional text overlay with localized controls in the Scenes view; image/PiP overlays remain follow-up work
  • Chroma key / green screen — per-source enablement and similarity/smoothness controls; renderer-specific activation remains documented as a follow-up
  • Studio mode (preview/program) — separate Preview and Program scene roles with a Take action using Cut/Fade transitions; composition edits target Preview while Program remains live
  • Multi-view & projectors — optional scene-grid mode and projector-preview lifecycle controls; native fullscreen second-display routing remains a platform follow-up
  • Scene snapshot — one-click export of the active scene or Studio Mode Preview as a PNG layout snapshot. Visible layers, scene-local transforms, opacity, visibility, z-order, collection, and profile are captured deterministically; native source-pixel rendering remains a follow-up when platform source renderers are connected. Done: rivulet-core/src/scene_snapshot.rs + localized Scenes-view export button; unit tests cover ordering, visibility, dimensions, and alpha blending.
  • Live scene composition — the active scene is composited into the frames that reach recording and streaming (toggle in the Scenes and Live views, on by default). Screen, game, webcam and browser layers carry real pixels; image/text/media/color/audio layers keep their layout tile until their renderers land (#217). Done: rivulet-core/src/live_compositor.rs, axis-aligned fast path in scene_snapshot.rs, submit_output_frame in the GUI, DE/EN i18n, docs and a ci_pinning guard. Tutorial: docs/scene-to-stream-tutorial.md.

Goal: The composable workspace OBS users expect.

M2 completion gate: ✅ Complete. The cross-platform UI/UX review followed docs/m2-ui-ux-review.md and the common/M2-specific criteria in docs/milestone-quality-gates.md. The recorded result is a conditional pass in docs/m2-ui-ux-review-report.md: zero Blocker/Critical findings and zero open High findings. Remaining Medium follow-ups (native projector routing, native source rendering, and native browser adapters) are explicitly assigned to M5/M9 and are not hidden. Issue #75 was moved to M5 and closed as the native projector follow-up.


📡 M3 – Streaming

Status: Complete (conditional)

M3 is functionally complete: the roadmap checklist is 15/15, and the completion gate is recorded in docs/m3-streaming-completion-report.md. Remaining live-integration evidence (real SFU WHIP handshake, SRT/RIST receiver interop, multitrack transport, VOD routing, NDI LAN, production resource baselines, and OS-backed credential storage) is explicitly assigned to follow-up milestones and must not be implied by beta parity.

  • RTMP/RTMPS client implementation (H.264+AAC, FLV muxing)
  • TLS-encrypted streaming (RTMPS) with certificate validation
  • Dual output (stream + recording in parallel; encoded once, split via tee)
  • Stream health and network stats (status via drop ratio/throughput, stream_stats() API)
  • Platform integrations (Twitch, Kick, YouTube via RTMPS; custom)
  • Custom RTMP server support (arbitrary rtmp:///rtmps:// URLs)
  • Stream key management and stream presets — validated Twitch/YouTube/Kick/custom endpoints, masked key display, TLS enforcement for platform presets, and Low/Standard/High/Custom quality presets; keys are never exposed by the UI masking API
  • Adaptive bitrate — bounded policy applies changed values to the active GStreamer encoder, reducing bitrate on Poor health and recovering on Good health; network monitoring and cooldown remain follow-up
  • Stream delay — configurable, bounded per-target packet delay integrated into fan-out branches; reconnect-aware buffering and backoff remain follow-up
  • Multitrack Video — bounded multi-representation configuration (1–4 tracks) and pipeline metadata; per-track encoder/transport negotiation remains a follow-up
  • WebRTC/WHIP as a first-class protocol (ultra-low-latency, SFU-compatible) — SDP offer/answer signaling with secure endpoint validation; WhipMediaSession now owns the deterministic session lifecycle (SdpOffer H.264/Opus offer generation, post_offer exchange, answer application, Idle→Negotiating→Live/Failed state tracking, and HTTP DELETE teardown on the resource URL), fully covered by offline unit and local-HTTP-mock tests. The live SFU/ICE/DTLS/SRTP media handshake remains the final integration evidence. See docs/whip-strategy-spike.md.; vision-approved in docs/obs-vision-roadmap.md
  • SRT/RIST configuration contract — validated protocol-specific endpoints, bounded latency, passphrase validation/redaction, and sink-fragment generation; live GStreamer transport/interoperability remains open
  • Multistreaming — GStreamer fan-out with one named branch/sink per validated RTMP/RTMPS target, up to four named destinations, independent target states, retry-target selection, duplicate-name protection, and secret masking; production bus monitoring/retry scheduling remains follow-up work. Tracked in #70.
  • NDI output — LAN contribution/monitoring destination, selectable in Settings → "NDI output (LAN)" and wired into the engine (#77): when enabled, every recording/streaming/dual-output session additionally publishes the encoded H.264 video as an NDI source (ndisink fan-out of the pre-mux video tee, playing alongside the replay buffer when both are on). NdiOutput validates source name/group, escapes hostile names so they cannot break the pipeline string, and probes ndisink availability (shown as a warning in Settings when the NewTek NDI plugin is missing). Off by default so a feed is never published unintentionally; audio-in-NDI and a standalone NDI-only session remain follow-ups. See docs/ndi.md.
  • VOD track — separate copyright-safe audio track (VodTrack on StreamSettings) for the Twitch VOD workflow while streaming: deterministic enabled/recorded model that is only ever active when both are set, so it can never silently leak into the live ingest; unit tests confirm the ivod flag signal and off-by-default behavior. In dual-output sessions an active VodTrack now emits a third, independent audio branch into the recording muxer (audio_src_vod, issue #78) — the live FLV path stays untouched. The routing UI is shipped and the streaming FLV mux stage carries the VOD-track marker (metadatacreator=Rivulet-ivod in the onMetaData; stock GStreamer cannot write a literal ivod AMF key — see the quality-gate design note).

Goal: Live streaming to the common platforms and low-latency protocols as native citizens instead of RTMP legacy.


🎥 M4 – Advanced Output & Capture

Status: 🚧 Done — milestone closed (all M4 roadmap bullets implemented, tested and documented). Integration follow-ups remain open and are tracked openly: VST3 hosting (loading a plugin into the audio graph — issue #96), multipart cloud upload + cloud GUI settings, VOD-track GStreamer routing, and Windows/macOS platform parity (a release blocker, not an afterthought). NDI routing itself is implemented (#77, see docs/ndi.md).

M4 completion gate: ✅ Complete (conditional). The output quality gate was recorded in docs/m4-output-quality-gate.md using the criteria in docs/milestone-quality-gates.md: zero Blocker/Critical findings; hardware-dependent resource-efficiency measurements are explicitly marked BLOCKED/N/A rather than hidden. The milestone M4 on GitHub is closed.

  • Replay buffer / instant replay — essential for gaming streamers (clip moments without recording the whole session); ring buffer (H.264+AAC) with instant replay save via F12 hotkey or GUI button
  • Virtual camera output — platform-neutral contract — validated format/resolution/FPS configuration and explicit Starting/Running/Stopping/Unavailable/Error lifecycle; platform driver integration and consumer smoke tests remain open
  • Video filters & effects (color correction, blur, sharpen) — VideoEffects (brightness/contrast/saturation/hue via videobalance, gaussianblur, cas sharpen) inserted between videoconvert and the encoder caps; availability-gated like the audio filters. LUT colour-grading and chroma-key refinement remain open (no portable .cube/lut3d path in core plugins; chroma key is available per source). Engine set_video_effects() + GUI section in the recording settings
  • Audio filters (noise gate, compressor, limiter, expander, gain, 10-band EQ; noise suppression via webrtcdsp) — AudioFilters extended with noise gate + expander (audiodynamic mode=expander), makeup gain (audioamplify, dB→linear) and a 10-band EQ (equalizer-10bands), on top of the existing webrtcdsp noise suppression, compressor and limiter; all availability-gated so a missing optional element is skipped instead of failing capture; per-source GUI toggles + sliders in the Mixer view
  • Audio ducking (sidechain compression — lower music/crowd while the mic speaks) — sidechain policy model with threshold/attenuation/attack/release + hysteresis (issue #79)
  • VST 3.x support — *VstPlugin/VstChain config contract plus deterministic bundle discovery from the platform-standard VST3 search directories (Windows Program Files/AppData, macOS /Library + ~/Library, Linux /usr/lib + ~/.vst3): validated .vst3 bundle paths, ordered per-track chain, availability probe — all testable without a plugin binary. Hosting contract shipped (Z96-1): VstHost trait with Loaded/Skipped results, skip-on-error semantics mirroring SkippedFilter (never fatal), ChainLoadResults summary. Windows COM host skeleton shipped (Z96-2): bundle resolution + LoadLibraryW staging through BundleResolve → FactoryObtain → ProcessorCreate → Loaded; non-Windows stubs skip cleanly. Skip-path tests cover every SkipReason without a plugin binary (Z96-3). Real audio routing through loaded plugins (process call into the engine chain) and the per-track GUI panel remain open follow-ups — see docs/vst3.md, docs/vst3-host-boundary.md (#96)
  • Master audio mix (output VU meter, master volume control, monitoring) — master volume applied to the whole mix after the sources are summed (a single output level on top of the per-source volumes), a labelled master-output VU meter showing the mixed level in dB, and per-source monitoring with its own volume; AudioConfig::master_volume + set_master_volume() on rivulet-audio, i18n EN/DE keys, GUI wiring on the Mixer view (Linux)
  • Additional recording formats (MKV, MOV, TS — crash-safe alternatives to MP4) — container selection on the recording pipeline (MP4 default preserves the codec-native muxer; MKV/MOV/TS opt into a crash-safe intermediate), GUI container picker, RecordingContainer/RemuxPlan on rivulet-core
  • Remux recordings (MKV/MOV → MP4 remux after stop, automatic or manual). Tracked in #71. — validated RemuxPlan, RemuxSettings with auto_remux_after_stop, and GStreamer remux execution via remux_to_mp4 after stop (element-availability gating); GUI toggle next to the container picker. See docs/recording-formats.md.
  • Recording file management (split by time/size, filename patterns, auto-record alongside stream) — FileNamePattern with validated {name}/{date}/{time}/{seq}/{stream} tokens, SplitBy time/size rules with RecordingSession part sequencing and auto_record_with_stream; GUI split/auto-record toggles and pattern-driven default filenames. Live splitting maps SplitBy onto splitmuxsink (max-size-time/max-size-bytes, %02d part numbering, crash-safe containers only). See docs/recording-files.md.
  • Advanced rate control (VBR, CQ, CQVBR, custom encoder options) — RateControl/RateControlMode (CBR default, VBR, constant-quality CQ, CQ-VBR with a bitrate cap) plus free-form extra encoder options folded into the encoder parse_launch fragment; full property mapping for software x264 (pass/quantizer/vbv-buf-capacity) and NVENC (rc-mode/max-bitrate/qp-const/const-quality), graceful fallback to average bitrate for QSV/AMF/VP9/software x265; engine.set_rate_control() and a GUI section in the recording settings
  • Multi-track audio export (system + microphone stored on separate tracks) — engine.set_separate_audio_tracks/set_audio_track_enabled build one avenc_aac branch per enabled source into the shared muxer; per-track export toggles in the Mixer view that are decoupled from the live mix and gated on capture; AudioTrack::label/i18n_key for display
  • Cloud integration (cloud recordings) — S3-compatible CloudRecording contract (endpoint/bucket/region/prefix + credentials) with validation, deterministic object-key builder, secret masking (custom Debug never prints the access key) and a working S3 PUT uploader (AWS Signature V4 via ureq) that uploads the finished recording after stop_recording when enabled. SigV4 verified against AWS test vectors; multipart + GUI settings remain follow-ups

Goal: The advanced output and production features of OBS.


🔌 M5 – Ecosystem & Platform Parity

Status: Complete (milestone closed; the remaining unchecked markers below are honest descopes and external waits, not engineering debt — see each annotation)

  • Plugin system (WASM-first) — the legacy "native Rust plugins" framing is retired: a native Rust plugin would just be a build of Rivulet itself, so the roadmap targets the WASM plugin system instead (manifest + sandboxed runtime + install/permission flow, shipped below); native-code extensibility remains available through the temporary OBS compatibility bridge (descoped to M11, see below)
  • WASM plugin runtime core (stable, sandboxed: plugins can never crash the app; long-term target plugin model) — RFC Phases 1–3 shipped: manifest parser/validator (plugin_manifest.rs), the wasmtime-based runtime (plugin_runtime.rs) with per-call fuel budgets, epoch-based wall-clock timeouts, the full Loaded → Initialized → Active → Inactive → Unloaded lifecycle and the core host imports, plus the install/permission flow (Phase 3): Settings → Plugins scans the install root, identifies publisher/type/host-API/sandbox status per bundle before activation, records per-capability user approvals (plugin_registry.rs, default-denial, persisted), and gates enabling on a completed review — secrets/capture stay denied for WASM — see docs/plugin-system-rfc.md
  • OBS plugin compatibility layer — moved to M11 (#147; descoped at M5 closure): the temporary native bridge (opt-in "Compatibility Mode", explicitly marked unsafe, loads libobs encoders/filters/sources without UI; Qt UI plugins out of scope) is a transition convenience for existing OBS users, not a requirement for Rivulet's own plugin ecosystem — with RFC Phases 1–3 shipped, the WASM plugin system is the real, user-facing model and the bridge belongs with the M11 plugin-platform work
  • obs-websocket compatibility — OBS WebSocket v5 server (JSON) for the Stream Deck / TouchPortal ecosystem remote control with the exact v5 wire names (StartRecord/ToggleRecord, StartStream/ToggleStream, ToggleInputMute, pause/unpause, replay buffer start/stop/toggle/save, studio mode): scene list/switch, source listing, recording/streaming/pause/mute/replay/studio control, optional password authentication and event subscriptions, verified end-to-end with a real WebSocket client plus a Stream Deck compatibility contract test (see docs/obs-websocket.md). #72
  • Windows/macOS feature parity — window capture on Windows exists; macOS recording implemented (xcap monitor/window video capture + cpal audio: microphone out of the box, system audio via a loopback driver like BlackHole; Record view with monitor/window/preset selection, start/stop, finalizing status; see docs/macos-recording.md); audio filters/monitoring on macOS remains a follow-up, and live on-device verification is hardware-blocked — the implementation is compile-checked in CI, but executing a real capture session needs access to a physical Mac (one manual session, not engineering work); see the scope note in docs/macos-recording.md
  • Source delete hotkey — delete the selected source/scene item (active scene, or Studio Mode Preview when enabled) via the delete_source action in the existing registry (OBS 32.2 parity; cross-platform, not macOS-only), rebindable in Settings → Hotkeys, default Delete; deliberately in-app-only (destructive, never OS-global, never fires while text input is focused; locked sources are kept) with the platform matrix updated honestly (see docs/hotkeys.md) #113
  • Installers (Windows MSI, macOS DMG, Linux AppImage) — automated in CI
  • Code signing (signing automation present, secrets needed — waiting on certificate purchase, see #50: SignPath Foundation declined the free tier (insufficient stars), so Windows needs an OV certificate (~$100–200/yr) and macOS needs Apple Developer ($99/yr); Linux GPG signing is already active)
  • Telemetry (opt-in, privacy-friendly) — privacy-first opt-in telemetry pipeline (Settings → Telemetry, off by default): identifier-free event model (enums/codes/booleans only — never titles, paths, URLs, stream keys or usernames), opting out clears pending events, bounded deterministic collector (rivulet-core::TelemetryReporter), and a TelemetrySink interface with no transport wired in the shipped build — nothing leaves the device; events are wired end-to-end (Startup, RecordingStop, RecordingError with classified category, SceneSwitch, ChatConnect); the client-side contract and policy are shipped and test-locked, an HTTPS ingestion backend remains a documented follow-up (see docs/telemetry.md) #127
  • Discord Rich Presence adapter — non-blocking activity updates using the Rivulet status model; explicit opt-out (persisted Settings toggle), no stream keys/URLs/paths/window titles, and graceful operation when Discord is unavailable (see docs/activity-status.md).
  • Multi-language support — all UI strings driven by locale tables (EN/DE, 528 keys each), Settings → Language picker with OS auto-detection on first launch, parity test prevents key drift (see docs/i18n.md)
  • MIDI device support — map controllers like the Korg NanoKontrol to scene switches, master-volume faders (CC 0-127), mute, and chroma-key toggles, with learn mode (capture the next moved control) and per-device presets (see docs/midi.md); the mapping/parse core is hardware-free and unit-tested, the GUI owns the midir device bridge
  • Global hotkeys & remapping UI — OBS-style hotkey settings: per-action rebinding (Settings → Hotkeys), OS-level registration that keeps working while the app is unfocused on Windows, with an honest platform matrix (see docs/hotkeys.md) #80
  • Multi-channel distribution rollout — tracked in docs/release-platforms.md; the rollout continues past the milestone (Stage 1 live, Stage 2 prepared and waiting on external package reviews — see the stage list below), it does not block M5:
    • Stage 1 – GitHub Releases: canonical artifacts, checksums, changelog, and updater source (already active; signing is enabled when release secrets are configured). Every release is also verified after publishing: the verify_release job downloads the published assets and checks them against the release's own SHA256SUMS (issue #190).
    • Stage 2 – WinGet + Flathub: first external channels after stable package identity, signing, Flatpak metadata, and review are complete.
  • WinGet preparation — signed MSI with a stable UpgradeCode plus a deterministic winget manifest generator/validator (packaging/windows/generate-winget-manifest.ps1, Pester-pinned) and a dry-run Distribution Readiness → winget job that renders and verifies the manifest against the real release MSI; the initial microsoft/winget-pkgs submission PR stays external.
    • Flathub preparation — Flatpak manifest with a pinned, cargo-offline build (packaging/flatpak/, cargo-sources.json generated from Cargo.lock by the official flatpak-cargo-generator — crate archives pinned by URL + SHA-256, cargo builds offline via vendored sources), honest sandbox permissions, and a CI job that builds/installs/flatpak-lints the bundle against org.freedesktop.Platform 25.08; the initial Flathub submission PR and its permissions/appstream review stay external.
    • Stage 3 – Homebrew Cask + Steam: macOS cask after notarization; Steam for Windows/macOS after beta stability, App/Depot setup, and SteamPipe verification.
    • Stage 4 – Microsoft Store: optional MSIX/Partner Center channel after the MSIX packaging decision.

Goal: OBS core parity across all platforms, with a plugin model structurally superior to OBS (WASM instead of a C ABI), plus OBS compat as a transition bridge.


🎬 M6 – Creator Toolkit & Interactivity

Status: Complete (all 4 features shipped and issues closed — chat auto-clips, multi-platform restream, mobile/HTTP remote companion, and multi-track audio routing; completion report in docs/m6-creator-toolkit-completion-report.md)

Differentiation: streamer-facing creator features that OBS leaves to third-party SaaS — chat-triggered clips, one-pipeline restreaming, and remote control — built on shipped building blocks (chat dock, replay buffer, obs-websocket, multi-target fan-out).

  • Chat-driven auto-clips — save replay-buffer highlight clips automatically when chat activity spikes (sliding-window message-rate detector, configurable threshold + window + cooldown) or on a !clip command (customizable command name); builds on the shipped chat dock (Twitch/Kick/YouTube) and M4 replay buffer; auto-clip toggle and settings in the Stream view, i18n (EN/DE)
  • Multi-platform restream — one pipeline to Twitch, YouTube, and Kick simultaneously via the Stream view restream section: add/remove targets with per-platform name, platform selector, ingest URL, and stream key (persisted), independent health per target (Connecting/Live/Degraded/Failed), wired into MultistreamSettings before every stream start; max 4 targets, duplicate-name protection, i18n (EN/DE)
  • Multi-track audio routing — capture individual app audio streams (game, Spotify, Discord) as separate named sources with independent filters and volume; route each source independently to Record and/or Stream outputs via a checkbox matrix; persists across restarts with i18n (EN+DE); per-platform capture: WASAPI per-app (Windows), PipeWire/PulseAudio per-app (Linux), system loopback fallback (macOS) — engine core + GUI mixer + Windows WASAPI, Linux PipeWire and macOS system-loopback fallback per-app capture implemented (Phases 1–5, issue #154): source/routing/filter types, per-source engine API with live volume/mute, versioned audio_routing_v1 persistence, routing-aware pipelines for record (per-source tracks), stream (mixed FLV track), and dual output, the Mixer-view routing matrix with per-source filter panel, shared inline mixers in the Record/Stream views, the Windows process-loopback backend and the Linux PipeWire backend (capture stream targeted at the app's sink-input node) with a unified process/node picker, and the macOS fallback (the system loopback mix delivered to every routed Application source, with a Mixer hint about the sharing semantics); resource budget verified with 6 routed sources + full filter chains (push p99 ≤ 60 µs, CPU delta ≈ 0, memory growth ≤ 3.2 MiB — docs/m6-audio-resource-report.md); all quality-gate criteria checked; see docs/m6-audio-routing.md; per-track buses (issue #242): OBS-style track model — up to 6 independently editable mixing buses (master gain/mute applied live without restart), per-source track membership, a streaming send-track selection, one AAC track per member bus in every container (MP4/MKV/MOV/TS, including the crash-safe remux), and the dedicated audio_tracks_v1 persistence
  • Mobile & HTTP remote companion — drive scenes, record, and stream from the phone or a browser on the LAN, building on the shipped obs-websocket v5 server (M5) with optional auth; self-contained mobile page (no CDN) served over HTTP with explicit LAN bind, LAN-requires-password policy, and a permission gate for remote stream start/stop (see docs/remote-companion.md)

Goal: The creator workflows that make a streamer choose Rivulet — clips appear when chat pops off, the stream reaches every platform at once, and the setup is controllable from the couch.


🎬 M6.9 – Scene & Stream Polish

Status: Complete (all 4 issues closed — per-platform stream metadata, device picker, native scene compositing, and the browser-source contract + Windows WebView2 POC; the interim backlog milestone #13 collected post-M6 follow-up work that belongs in neither M7 (Automation & Determinism) nor M8 (Embeddable Engine & API))

Complements the shipped M6 creator toolkit and the M2 scenes foundation with the scene/source depth and per-platform stream-metadata polish OBS users expect.

  • Per-platform stream title/game values (#214) — shipped in PR #220: the Stream-info editor has scope tabs (All platforms / Twitch / Kick / YouTube) with fully independent per-platform title + game drafts; Apply to all pushes each platform's own values in one batch (empty drafts are skipped, not failed) and platform tabs get a dedicated Apply to button
  • Native Browser-source engine (#215) — shipped in PRs #222, #223 and #227: the BrowserSourceBackend contract has a deterministic, headless reference implementation (SyntheticBrowserBackend) plus a prime_browser_backend/sync_browser_backend driver so the GUI↔backend wiring and the scene snapshot pipeline are fully testable, and the rivulet-browser crate ships a Windows adapter that embeds a real wry/WebView2 viewport captured via ICoreWebView2::CapturePreview, forwards pointer/keyboard input as Win32 message sequences (unit-tested mapping; no-op while interaction is disabled), toggles transparency via ICoreWebView2Controller2, and is driven from the Scenes view through a diff-based GUI tick (settings sync, input drain, non-blocking frame poll, egui preview texture); the #[ignore] spike test asserts a colour-pixel capture (GDI PrintWindow/BitBlt documented as a dead end); Linux/macOS adapters remain a follow-up (see docs/browser-source-spike.md)
  • Device/window/monitor picker in scene items (#216) — the scene add dialog shows a device picker for capture kinds (webcam/game/screen) backed by the same device tables as the record view; the chosen device is persisted on the source as device_id (camera:…/game:…/monitor:…)
  • Native scene compositing (#217) — SceneSnapshot now supports real RGBA SnapshotFrames per layer via attach_frames; the compositor rasterizes cropped, scaled, rotated, opacity-blended source pixels (render_rgba) instead of only color tiles, and the scene snapshot export feeds browser frames straight in (F-M2-002 follow-up)

Goal: ✅ Closed the two visible gaps that keep Rivulet from feeling "production-ready" to OBS users — per-platform stream metadata (✅ #214) and real scene composition (✅ #216–#217).


🤖 M7 – Automation & Determinism ("Render-First")

Status: Complete (conditional: one assigned follow-up) — spec: docs/m7-automation.md, 7 workstream issues on the M7 milestone (#186–#192), all closed and merged; completion review with evidence and follow-ups: docs/m7-automation-completion-report.md.

Differentiation pillar #1: OBS is interactive-first, Rivulet is deterministic.

  • Deterministic pipeline (controllable engine clock, reproducible output from the same inputs) — injectable clock (SystemClock/VirtualClock with advance/step/hold), identical container timestamps under the virtual clock, and every remaining nondeterminism source reported rather than hidden: rivulet record emits the active sources, the encoder, and separate reproducible/byte_reproducible flags in its stopped JSON event — #187
  • Headless CLI: capture/rendering without a GUI (rivulet record ...), usable as binary and library — shipped in rivulet-cli (rivulet record with TOML config, JSON status events, documented exit codes, graceful SIGINT/SIGTERM; library path via rivulet_cli::RecordJob) — #186
  • CI-friendly rendering: generate video from code (Remotion approach, native in Rust) — rivulet render --config scene.toml --frame 7 --png out.png renders frame N deterministically (byte-identical across runs), --video pushes frames through the engine on the virtual clock, and --config-dir scenes/ --out-dir renders/ --json drives a batch (one job per config, failures isolated, machine-readable summary with per-job status and relative output paths); the Render Smoke CI job runs the real binary end-to-end on a clean runner — #189
  • Reproducible distribution inputs: one tested SHA256SUMS generator plus post-publish verification — shipped in PR #285: the duplicated inline find | xargs sha256sum pipeline in both release workflows is replaced by scripts/release-manifest.py (generate / verify / --self-test, run on every push), and a new verify_release job re-downloads the published release with gh release download and verifies every published artifact against the manifest, so what CI hashed is proven to be what a user actually downloads. Covered: every build artifact, not the documentation images the release also attaches (see docs/release-platforms.md) — #190
  • Rebuild-and-compare: bit-reproducibility is measured, not claimed — .github/workflows/reproducible-build.yml builds the release ref twice per platform on two separate runners, both runs stage the payload with the same script the release uses (scripts/stage-release-payload.sh) and compare fingerprints (scripts/reproducible-build.py): a differing payload byte fails the job and names the file, container (ZIP/MSI/AppImage/DMG/.asc) and metadata drift is reported as documented, and an uncomparable pair (different toolchain, target, GStreamer version, commit or SOURCE_DATE_EPOCH) is reported as not comparable instead of passing. Normalization lives in one script (scripts/release-build-env.sh: --remap-path-prefix for checkout and Cargo home, SOURCE_DATE_EPOCH from the commit, /Brepro /DEBUG:NONE on Windows) consumed by the release build too, and a ci_pinning guard fails if the wiring or either script disappears — #288
  • Pipeline inspector/diagnostics tooling (analogous to gst-inspect, gst-launch), embedded in the engine — rivulet inspect --config recording.toml [--json] prints the pipeline the engine would build (same code path as a real run, secrets redacted) plus a capability report of local encoders/containers/capture backends/audio filters; rivulet record --dry-run reuses the same path; a failing run appends a machine-readable {"event": "failed", "stage": …} object naming the failing stage (usage/config/output/engine/finalize) alongside the human stderr diagnostic — #191
  • Scene-item copy/paste API — deterministic duplicate/paste of scene items within and across scenes, exposed scriptably (OBS 32.2 frontend-API parity), with undo integration and deterministic tests — SourceManager::copy_scene_item captures source plus binding verbatim (transforms, crop, lock, visibility, order); paste_scene_item duplicates into any scene with a " copy" suffix and, with with_deterministic_ids (UUIDv5 from source ID + target scene), makes scripted pastes idempotent; every paste lands on a paste-scoped undo/redo stack the GUI undo dispatch prefers, with copy/paste buttons and Scenes-view-scoped Ctrl+C/Ctrl+V — #192
  • Headless streaming over the CLI: publish without a GUI, usable as binary and library — rivulet stream --config streaming.toml targets Twitch/YouTube/Kick or a custom RTMP/RTMPS ingest, with the same JSON status events on stdout, diagnostics on stderr, documented exit codes and graceful SIGINT/SIGTERM stop as rivulet record; --dry-run probes the ingest and prints the plan without publishing (so a broken endpoint fails as config/output instead of mid-stream), the stream key is masked and the ingest URL reduced to scheme + authority in every report, and the library path is rivulet_cli::stream::StreamJob; an end-to-end test publishes video and audio to a loopback RTMP server — #289
  • Deterministic tests as first-class citizens (golden-frame tests, exact PTS/DTS verification) — rivulet_core::test_helpers provides GoldenFrame (frame index + pixel-count/max/mean delta + first differing coordinates on mismatch, Serialize for machine-readable CI output, PNG artifact for eyeballing) and Timestamps (assert_equals/assert_constant_interval/assert_monotonic/assert_dts_not_after_pts, each violation naming the offending index and both values); synthetic frames come from rivulet_core::source::TestVideoSource — #188

Goal: ✅ Delivered — "Video from code" and reproducible capture pipelines: the reason a developer/team cannot use OBS but can use Rivulet. Release artifacts are audited and download-verified (#190), an independent rebuild is compared against a second rebuild of the same commit on every release (#288), and headless streaming ships as rivulet stream (#289).


📦 M8 – Embeddable Engine & API

Status: Delivered (opened 2026-10-05, closed 2026-10-27 — all six workstream issues, #292, #293, #294, #295, #296 and #297, delivered). The M8 completion report records every gate criterion individually and names the four follow-ups the exit evidence left open: F-M8-003, F-M8-004, F-M8-005 and F-M8-006 — F-M8-005 (written deprecation and migration policy) is now closed by docs/api-migration.md, and F-M8-006 by the #345 deprecation.

Differentiation pillar #2: rivulet-core is a normal library, not a monolith with retrofitted API.

  • Stabilized public API for rivulet-core (semver 1.0, crate-style types) — #292. The #![warn(missing_docs)] half of that promise is a real crate attribute since #342: lib.rs and all 74 of its modules are documented, the counted #[allow(missing_docs)] list is empty, and ci_pinning fails if one is added back
  • Comprehensive API docs + examples (recording, streaming, dual output, encoder selection, engine lifecycle, audio routing, and frame delivery — session_embed pushes real frames through the in-process session API) — #293
  • In-process capture API: embed a recording feature in any Rust app (audio/video capture, encoding, file/stream) — #294 — typed config gate + GUI-free RecordingJob session entry point (push frames/audio, events, pause/resume, Drop-finalize) delivered, pinned by 29 tests in tests/recording_job.rs — including an end-to-end run of one session pushing audio+video into file and a loopback RTMP fixture (handshake → publish → counted media, no network) — and demonstrated by the session_embed example
  • Feature detection and runtime diagnostics as an API (detect_available_encoders_with_info(), encoder fallback, capture-backend detector) — #295 — structured detection (EncoderInfo/EncoderFallback/CaptureBackendInfo/FeatureInfo) with FEATURE_DETECTION_CACHE, non-localised why_not strings, and deterministic CI-friendly tests delivered; docs at docs/feature-detection.md. The older detect_encoder_availability() is deprecated and delegates to the codec-aware detect_encoder_availability_for_codec(), so the typed helper and the probe cannot disagree (#345)
  • Abstraction of capture backends (xcap, PipeWire, Metal/WGC) behind stable traits — #296 — CaptureBackend trait (Send+Sync, lifecycle, typed CaptureError) with documentation (docs/capture-backends.md); all three backends capture real frames on their own platform — XCapBackend on Linux/X11, PipeWireBackend through the xdg-desktop-portal screencast on Linux/Wayland, MetalWgcBackend through Windows Graphics Capture on Windows — and answer BackendUnavailable only off that platform (the macOS/Metal half is still open, F-M8-004); all_registered_backends_implement_trait test enforces coherence; existing E2E tests unchanged
  • Exit evidence (closes the milestone) — #297 — an external consumer crate (downstream-smoke/, no workspace membership, no internal paths) builds and runs against the cargo package artifact through scripts/run-downstream-smoke.sh in CI; cargo doc -p rivulet-core is warning-free under -D warnings (22 broken doc links fixed for it); docs/api-overview.md is generated from rivulet-core/src/lib.rs and checked on every push; docs/m8-automation-completion-report.md carries the delivery table, the per-criterion verdicts and the follow-ups

Example overview: the seven runnable examples live in rivulet-core/examples/ and are listed with what each one shows in the crate documentation — simple_record, simple_stream, dual_output, encoder_config, engine_lifecycle, audio_routing, session_embed. CI runs all seven (Run rivulet-core examples in the Build & Test job); building without running them does not count as delivered.

Exit evidence (#297). The milestone closes on evidence that comes from outside the crate rather than from its own prose:

  • External consumer: downstream-smoke/ is its own crate — not a workspace member, no internal source paths, rivulet-core as its only dependency. scripts/run-downstream-smoke.sh runs cargo package -p rivulet-core, unpacks the .crate cargo publish would upload, and builds and runs the consumer against that, so a public item that exists only in the working tree fails here. The consumer validates its configuration through the typed gate, records a video-only session end to end with frames it generates itself, repeats it with audio, checks the returned SessionReport against the bytes on disk, and queries structured feature detection. CI runs it with RIVULET_DOWNSTREAM_REQUIRE_AUDIO=1, so a host without the audio plugins cannot quietly turn the positive audio evidence into the typed refusal.
  • Docs build: cargo doc -p rivulet-core --no-deps runs with RUSTDOCFLAGS=-D warnings on all three platforms of the Build & Test matrix, so a cfg-gated item that exists only off the Linux runner is checked too. It failed on 22 broken intra-doc links before this step existed; the count is now zero, and the step fails on the first new one on any platform.
  • Example matrix: the Run rivulet-core examples (M8 #293) step executes all seven examples on all three platforms (see the example overview below).
  • Public API overview: docs/api-overview.md lists every pub mod, every crate-root re-export and every module that documents itself as experimental, generated by scripts/generate-api-overview.py and checked with --check in the Lints job, so the list cannot drift from lib.rs.

Where this stands. The promise of M8 is directional — "rivulet-core is a normal library" — and promises of that kind fail quietly: nothing breaks, the crate just becomes unusable again at some point nobody notices. The opening state is therefore recorded honestly rather than optimistically. When the milestone opened, rivulet-core sat at 0.65.0-alpha.55 with 66 public modules and no #![warn(missing_docs)] (the module count is that snapshot and has grown since; #![warn(missing_docs)] is a real crate attribute now, and #342 names the item documentation that is still missing), there was no examples/ crate, and feature detection is reachable through log lines and GUI labels rather than through the API. Under semver 0.x, every minor bump is breaking, so an external consumer can rely on nothing yet — that is what #292 is for.

Recommended first implementation step: #292 (stabilized public API). It gates the rest: examples, the in-process capture entry point and the backend traits all need a stable surface to be written against, and documenting a moving target is wasted work.

Goal: "Recording you can embed into your product" — the use case OBS is architecturally not built for.


⚡ M9 – Modern Architecture

Status: Planned — workstream issues opened: #360 (WebGPU renderer), #361 (compute filters), #362 (zero-copy capture), #363 (resource metrics), #364 (egui foundation). Scene-management follow-ups: #366 (multi-select and bulk move), #367 (OBS collection import), #368 (per-folder default transition).

Differentiation pillar #3: a modern render path from the ground up instead of OpenGL/D3D legacy.

  • WebGPU renderer (wgpu) for preview, compositing, and effects (#360)
  • Compute-based video/audio filters (chroma key, scaling, filters) instead of CPU/shader legacy (#361)
  • Zero-copy/GPU-direct capture paths (DMA-BUF, WGC/NVENC-direct, Metal IO) (#362)
  • Performance and footprint metrics (idle CPU/RAM, laptop battery) as CI checks (#363) — delivered: rivulet metrics drives real startup/idle/preview/record phases and scripts/resource-metrics.py samples the process from outside, gates idle CPU, RSS slope, memory growth, startup and headless frame time against a committed per-profile baseline, records the synthetic capture-on/off A/B, and marks GPU/battery as n/a/blocked with a reason where a shared runner cannot measure them. Method and thresholds: docs/resource-efficiency-goal.md
  • Modern UI foundation (egui) consistent across all platforms (#364)
  • Native scene management: folders, colour labels, reusable component scenes, search (#319) — delivered: scene model (folders, component flag, cycle guard, OBS-compatible legacy loading), scene dock (indented tree, per-collection fold, colour labels, drag & drop, component group) and search/filter. Spec: docs/scene-management.md, tutorial: docs/scene-management-tutorial.md
  • Scene multi-select and bulk move: drag, colour, kind and delete apply to a selection as one undo step (#366) — delivered: SceneSelection in rivulet_core::scene_dock (click, toggle and shift-range over the drawn rows, sticky filtering, Ctrl/Cmd+A, Esc, Delete), an atomic bulk move that refuses the whole batch and names the offending scene, and one-step bulk colour, kind and delete (confirmed with the count). Spec: docs/scene-management.md
  • OBS scene-collection importer, using the mapping documented in docs/scene-management.md (#367) — delivered: rivulet_core::obs_import maps scenes, groups and nested groups, scene items, transforms (with OBS alignment), crop, visibility, lock and device ids, and reports every difference as a typed ObsImportNote instead of dropping it. rivulet import-obs <collection.json> --dry-run prints the report, --out <rivulet-scenes.json> writes the scene tree atomically in the GUI's scene-collection format. Mapping, limits and tests: docs/scene-management.md
  • Per-folder default transition: a folder's override applies to everything inside it, otherwise the collection default (#368) — delivered: the folder's … menu sets a kind/duration default, SceneManager::resolve_transition resolves nearest ancestor folder → collection → app default, the map travels with the collection (serde(default), omitted while empty), setting/clearing is undoable, deleting a folder prunes its entry, and a studio Take plays the same resolved transition. Spec: docs/scene-management.md

Goal: Lightweight and future-proof — technically ahead where OBS has to retrofit.

Cross-cutting resource-efficiency goal: Rivulet follows a game-first performance policy: capture and streaming should preserve CPU/GPU/memory headroom for the game, with p99 frame-time, idle CPU, memory stability, and fallback evidence measured rather than inferred. See docs/resource-efficiency-goal.md.


🤖 M10 – AI Chat Assistant

Status: Planned

Differentiation pillar #4: OBS has no native chat; streamers use cloud SaaS. Rivulet ships a local-first AI chat assistant.

  • Chat adapters: Twitch (IRC/WebSocket), Kick (WebSocket), YouTube (Live API)
  • Platform compliance — the bot must satisfy each platform's hard constraints to stay connected and legal:
    • Twitch: OAuth token with chat:read + chat:edit; global rate limit of 20 messages/30 s for non-broadcaster/mod/VIP accounts (throttled token bucket, never burst); PONG on every PING; replies via reply-parent-msg-id (needs the twitch.tv/tags capability); surface a phone-verification hint when Twitch demands a verified bot account
    • Kick: sending uses the authenticated session token via the undocumented kick.com API/Pusher WebSocket — no documented rate limits, so the bot self-throttles conservatively per channel, and endpoint breakage must degrade to read-only/observer mode instead of failing the chat dock
    • YouTube: official sending needs the Live Streaming API (API key + OAuth youtube.force-ssl/youtube scope) with parentId replies and quota accounting (insert ≈ 200 units; daily budget 10 000); without/over quota the bot falls back to the read-only Innertube poller
  • LLM provider abstraction — local-first:
    • Ollama (http://localhost:11434) as default
    • llama.cpp server / in-process (llama_cpp2)
    • OpenAI-compatible endpoints (cloud, optional)
  • Personas: configurable system prompts per "personality" (TOML)
  • Custom commands (!so, !song, !setup, ...) — deterministic, no LLM needed
  • Context/memory: short chat history + streamer info
  • Moderation: toxicity filter as a pre-check before responding
  • Bot coexistence: detect other active bots in the same channel (e.g. the Vivid Android bot) and suppress double replies — shared reply cooldown, command-claiming, optional silent/observer mode
  • GUI panel (chat + bot settings) wired through the i18n layer
  • AI Creative Studio (Spark-like) — a local-first chat-driven creative assistant in the M10 panel, mirroring Meld Spark but fully local and free: the Ollama code model turns natural-language requests into HTML/CSS/JS overlays (alerts, lower thirds, countdown timers, goal trackers, leaderboards, chat overlays, starting-soon/BRB/ending screens, full scene packages) rendered through the shipped browser source, with live-event reactivity (follows/subs/raids/chat/!commands), scene/layer awareness, conversational refinement, per-message restore points, test-event firing, and a gallery; emote/asset generation is an additional local T2I pipeline (transparent platform kit + optional 7TV push) and alert-sound/voice generation an additional local T2S/TTS pipeline (Stable Audio Open Small SFX/jingles, Kokoro voice read-outs, assigned per alert kind); scope and research in docs/m10-ai-creative-studio.md
  • AI off-switches (Settings + Assistant tab) — all AI features are off by default and independently disableable: a global "Enable AI features" master switch (kills the chatbot, the creative studio, the emote/T2I generator, and the sound/voice generator — no model load, no workers spawned) plus per-feature toggles for "AI Chat Assistant", "AI Creative Studio (overlays)", "Emote/T2I generator", and "Sound/voice generator", with an optional "pause while live" runtime override (Go Live ⇒ models suspend, streaming stays unblocked); the switches persist (Settings serialization), are localized (EN/DE), and their default-off state is verified by tests — mirrors the M6 remote-companion permission-gating pattern. Settings stay lean: infrastructure lives in the Settings page (master switch, Ollama connection, T2I engine, resource budget), feature workflow lives on the Assistant tab (per-feature toggles, model choice, overlay folder, test events, gallery) — infrastructure in Settings, workflow on the Assistant tab

Goal: A private, subscription-free, API-free AI chat assistant that runs fully locally — the counter-position to cloud chat bots like StreamChatAI.

⚠️ Coexistence with Vivid: Rivulet (desktop) and Vivid (Android) can be used at the same time — e.g. Vivid streaming from the phone while Rivulet runs on the PC. The Rivulet bot must therefore never fight with the Vivid bot for the same chat: no duplicate replies, coordinated !-commands, and a per-channel configurable bot identity.

🧩 M11 – Extensible UI & Plugin Platform

Details, guardrails, and the completion gate: docs/extensible-ui-roadmap.md and docs/milestone-quality-gates.md.

The UI extensibility work is intentionally isolated from the streaming and capture milestones. It starts with a versioned, persistable layout and view registry, then adds declarative plugins before considering sandboxed WASM execution.

  • P1 – Persistable UI layout — versioned workspace/layout state, safe defaults, migrations, and no secrets or runtime handles.
  • P2 – View registry — stable view IDs for built-in and optional views; navigation, Help, and accessibility derive from the registry.
  • P3 – Declarative UI plugins — manifest-defined panels, menu items, help pages, enable/disable state, and API compatibility.
  • P4 – Permission model — explicit UI/network/filesystem/capture/audio/secrets capabilities; sensitive permissions denied by default.
  • P5 – Isolated plugin execution — preferably WASM, with timeouts, resource limits, and crash isolation. (The WASM runtime itself shipped early as M5 RFC Phases 1–3 — see the M5 section.)
  • P6 – Plugin quality gate — compatibility, migration, accessibility, UX, performance, security, and example-plugin checks.
  • OBS plugin compatibility bridge — descoped from M5 (#147): opt-in "Compatibility Mode" (explicitly marked as unsafe) loading native libobs plugins (encoders/filters/sources without UI; Qt UI plugins out of scope); the goal remains migration to the WASM plugin system.
  • Live translation showcase plugin — end-to-end demo of the plugin platform: a host-side STT (speech-to-text) transcript stream feeds a WASM plugin that translates in real time and renders bilingual subtitles burnable into the stream and recording (#252); requires the host_transcript_subscribe host import (STT stays host-side — Whisper-class models are too heavy for the WASM sandbox), a capability-gated network import for the translation API, and an overlay subtitle channel.

Definition of Done: The versioned layout survives restart, built-in and plugin views use one stable registry, plugins cannot access secrets without explicit capability approval, and a failing plugin cannot terminate the GUI.


🎯 Feature-Parity Checklist (vs. OBS)

OBS category Status
Capture sources (display, window, webcam) Partial (display + window + region, multi-monitor)
Game capture (D3D/Vulkan/OpenGL hooks) In progress (G2 DXGI done; G3 Vulkan layer + capture pipeline; G4 OpenGL hook; G5 perf verification done; G6 Linux PipeWire portal done)
Media & color sources Done (S6 + S7 done; S5b browser source open)
Image & text sources Done (S2 + S3 + S4 done)
Browser sources S5b contract/configuration done + Windows adapter (#227); Linux/macOS follow-up
Audio sources (microphone, per-app, devices) Done (S8 done)
Scenes & transitions In progress (scene management, collections/profiles, duplicate scene, Cut/Fade transitions, Studio Mode; stinger/multi-view open, M2)
Scene collections & profiles Partial (named collection/profile context; import/export and per-profile settings open, M2)
Studio mode & multi-view Partial (Studio Mode done; multi-view/projectors open, M2)
Source transforms & composition ✅ Done (M2 S1)
Hotkeys (incl. remapping, global) Partial (record/pause/mute/save-replay; remapping planned M5)
Source delete hotkey Open (M5: delete-source hotkey action on all platforms, OBS 32.2 parity)
Scene-item copy/paste (API) Done (M7 #192: scriptable copy/paste of scene items with paste-scoped undo + deterministic IDs, OBS 32.2 frontend-API parity)
Undo/Redo Done (M2)
Audio mixer (sources, tracks, filters) Partial (mixer, separate tracks, filters)
Video filters (color correction, LUT, blur, sharpen, chroma key) Partial (M4: color correction/blur/sharpen done; LUT grading + chroma-key refinement open)
Audio filters (noise gate, compressor, limiter, expander, gain, EQ) Done (M4: gate, compressor, limiter, expander, gain, 10-band EQ, NS)
Audio ducking (sidechain) Done (M4 sidechain policy with hysteresis)
VST 3.x support Partial (config contract + discovery + hosting contract + Windows COM skeleton; audio routing through loaded plugins open)
Recording & encoding Partial (H.264/H.265/VP9, HW + SW)
Replay buffer Done (ring buffer, F12 hotkey, save-to-MP4)
Remux & file management Done (M4: remux after stop, split, patterns, auto-record)
Virtual camera Partial (M4 platform-neutral contract + lifecycle; driver integration open)
Streaming (RTMP/RTMPS, platforms) Partial (RTMPS Twitch/Kick/YouTube; validated presets and key handling)
Multistreaming Implemented (per-target fan-out + GUI add/remove targets, independent health; see Stream view → Restream section)
Adaptive bitrate Partial (bounded policy implemented; live encoder reconfiguration remains open)
WebRTC/WHIP, SRT/RIST, NDI WHIP session/lifecycle+DELETE; SRT/RIST contracts done, NDI output wired into recording/streaming/dual pipelines as a selectable destination; live SFU/ICE/DTLS handshake + real NDI LAN interop verification open (needs the NewTek NDI runtime)
VOD track Done (config model + dual-output recording branch audio_src_vod + routing UI + streaming mux-stage marker, issue #78)
Multi-track audio Partial (2 local tracks; VOD-track model in M3)
Plugin ecosystem & OBS compatibility Open
obs-websocket / Streamdeck Implemented (M5)
Mobile remote & MIDI Implemented (M6: mobile/HTTP remote companion page with LAN bind, password policy, and permission-gated stream control, see docs/remote-companion.md; MIDI mapping shipped in M5)
Cloud & telemetry Partial (M4 S3 SigV4 PUT upload after stop; multipart + GUI settings open)
Discord Rich Presence Implemented (optional, privacy-safe activity status; M5, see docs/activity-status.md)
Chat dock & alerts Partial (M5/M6: combined multi-platform chat dock — one list, one send box, all connected platforms (Twitch IRC + Kick WebSocket + YouTube polling) with per-line platform badges, broadcast send with per-platform delivery feedback, and platform-routed replies, see docs/twitch-chat.md; follow/sub/donation/raid ingestion surfaced locally in the chat dock and in a dedicated combined alerts dock — one live list for all platforms with platform badges, bounded independently and clearable, fed by an opt-in loopback webhook receiver for Streamlabs + Twitch EventSub with HMAC verification, and a native outbound Twitch EventSub WebSocket transport (wss://, forwarder-free, no port) — see docs/alerts-ingest.md) #129
Multi-language support Implemented (EN/DE, 528 keys, parity-tested; see docs/i18n.md)
Platform parity (Windows/macOS) Open
AI chat assistant Open (M10)
AI creative studio (Spark-like overlays) Open (M10, planned: chat-driven local code-gen of browser-source overlays + emote/asset T2I; code-gen spike done — qwen2.5-coder:7b default on 8 GB GPUs, see docs/m10-ai-creative-studio.md + scripts/codegen-spike/)
AI off-switches Open (M10, planned: master switch + per-feature toggles + pause-while-live, default off, see docs/m10-ai-creative-studio.md)

The checklist is verified against the machine-readable OBS catalog scripts/obs-features.json by scripts/check-parity-checklist.py in CI — a new OBS capability without a matching checklist row fails the check.


🔄 OBS upstream feature monitoring

A weekly GitHub Actions workflow checks the latest OBS release notes for new feature candidates, evaluates their fit against Rivulet's machine-readable vision pillars, and publishes an advisory report plus a review queue in docs/obs-vision-candidates.md. Maintainer-approved candidates are recorded in the curated docs/obs-vision-roadmap.md before being promoted into the milestone roadmap. Run it locally with python3 scripts/check-obs-upstream.py --self-test or inspect docs/obs-upstream-check.md. Candidates require maintainer review before changing the parity checklist; features that do not fit the vision are not added merely for parity.

🧪 UI smoke tests

The cross-platform headless UI smoke contract runs on Linux, Windows, and macOS and checks navigation, keyboard guards, GUI-visible diagnostics, screenshot invariants, secret redaction, focus/hover semantics, and theme-contrast hooks:

cargo test -p rivulet-gui --test ui_smoke

See docs/ui-smoke-testing.md for the evidence workflow and the limitations of deterministic headless checks versus native visual/accessibility review. The optional activity-status contract is documented in docs/activity-status.md.

📺 Streaming Example

The stream_rtmps example (rivulet-audio) captures the screen together with mixed system/microphone audio and streams live via RTMPS to Twitch, Kick, YouTube, or any RTMP server:

cd rivulet-audio

# Twitch (default) or YouTube
RIVULET_STREAM_KEY="your-stream-key" \
RIVULET_STREAM_SECS=60 \
cargo run --example stream_rtmps

# Kick requires the IVS ingest endpoint
RIVULET_STREAM_URL="rtmps://<id>.global-contribute.live-video.net/app" \
RIVULET_STREAM_PLATFORM=kick \
RIVULET_STREAM_KEY="your-stream-key" \
cargo run --example stream_rtmps

# Any RTMP/RTMPS server (e.g. local)
RIVULET_STREAM_URL="rtmp://127.0.0.1:1935/live" \
RIVULET_STREAM_PLATFORM=custom \
RIVULET_STREAM_KEY="test" \
cargo run --example stream_rtmps

Environment variables:

Variable Meaning Required
RIVULET_STREAM_KEY Stream key from the platform dashboard Yes
RIVULET_STREAM_PLATFORM twitch (default) · kick · youtube · custom No
RIVULET_STREAM_URL Ingest URL (for custom and kick) depends on platform
RIVULET_STREAM_SECS Stream duration in seconds (default: 10) No
RIVULET_STREAM_RECORD_PATH Optional MP4 path for parallel recording (dual output) No

Every two seconds the example prints the live health status, e.g. [health] Good | 2500 kbps | 30.0 fps | 120 sent / 0 dropped.


📦 Release platforms

GitHub Releases is Rivulet's canonical, signed distribution channel. The recommended expansion path is WinGet for Windows and Flathub for Linux, followed by Homebrew Cask for macOS and Steam for the gaming audience; Microsoft Store/MSIX is a later option. All external channels must publish the same version and checksums as GitHub Releases and should initially be limited to beta/stable builds.

The dry-run-first workflow Distribution Readiness checks release assets for each planned channel without submitting anything externally. It runs on a weekly schedule (Mondays) against the newest published release and can be triggered manually per channel. See docs/release-platforms.md for prerequisites and activation checklists.

Every release — alpha, beta, RC, and stable — is integrity-checked twice. scripts/release-manifest.py generates the deterministic SHA256SUMS manifest before publishing, and the verify_release job then downloads the published assets again and verifies them against that manifest, covering every build artifact. Both channels run the script's self-test on each push. The manifest covers the build artifacts rather than literally every attached file: the workflows also attach documentation images by path, which are not build output.


🛠 Development

Repository hygiene

Which file types and directories are handled exclusively via .gitattributes and .gitignore (line endings, binary markers, tool caches, nested repos) is normative in docs/repo-hygiene.md. Keep the working tree clean before committing: never git add -A, stage only the files that belong to a work package, and mark new binary assets in .gitattributes.

Diagnostics and crash logs

Rivulet writes structured, ANSI-free logs to the per-user data directory under Rivulet/logs/, using one file per local calendar day:

  • Windows: %LOCALAPPDATA%\\Rivulet\\logs\\rivulet-YYYY-MM-DD.log
  • Linux: $XDG_DATA_HOME/Rivulet/logs/rivulet-YYYY-MM-DD.log (or the platform data directory)
  • macOS: the platform data directory under Rivulet/logs/

The default retention is 14 days. Set RIVULET_LOG_RETENTION_DAYS before starting the app to change it; values below one are clamped to one day. Crash reports are delimited by ===== RIVULET CRASH =====, making them easy to find and attach to bug reports. When reporting a crash, include the relevant daily log and the app version, but remove personal paths or stream keys first. If the app starts with an empty log, verify that you are inspecting today's file and run once with RUST_LOG=info; see docs/logging.md for startup troubleshooting and the logging fallback behavior.

Settings → Crash report builds that report for you: it extracts the crash blocks and the ~50 log lines before each one, redacts every stream URL (SRT passphrases included) and your home directory, copies the result to the clipboard, and opens a GitHub issue composer that is already filled in. Every redaction is counted in the report header, so you can see what was removed. Rivulet itself transmits nothing — there is no token and no sender, and what leaves the machine is only what you paste. See docs/logging.md for the manual procedure that remains the fallback.

Help → Crash reports → Upload previous crash report covers the case the Settings button cannot: a panic. The release build aborts on panic, so there is no window in which anything could be sent — the panic hook writes a small record to Rivulet/crashes/ instead, and the next launch offers it from that menu entry (greyed out when the last session was clean). The record goes through the same redaction, is delivered the same way, and is deleted once you have dealt with it. This build has no upload destination wired up, so the menu item currently behaves exactly like the Settings button; the CrashReportSink trait is the seam a reviewed transport would use later. Windows packages also include a dependency-free launcher that records pre-Rust failures and can opt in to WER crash dumps with RIVULET_ENABLE_CRASH_DUMPS=1.

Running the GUI locally (Windows)

The GUI binary imports WebView2Loader.dll (wry/webview2-com, browser-source adapter). Windows resolves import DLLs only next to the executable or on PATH — not from cargo's build-script output directory — so a binary without the loader next to it dies silently before any log line (stderr: error while loading shared libraries: api-ms-win-core-winrt-error-l1-1-0.dll).

The GUI build script colocates the x64 loader with the executable automatically on every Windows build (rivulet-gui/build.rs, same destination contract as the Vulkan layer manifest), so a plain cargo build/cargo test keeps the exe startable — also after cargo clean. If the build ever prints WebView2Loader.dll not found under …, webview2-com-sys has not built yet: build once more (the first cargo test -p rivulet-gui run does it), or copy manually as a fallback:

cp target/debug/build/webview2-com-sys-*/out/x64/WebView2Loader.dll target/debug/

The colocate contract is pinned by the build_outputs integration test in rivulet-gui/tests/.

Tests

Run the full test suite:

# Build tests and run them
cargo test --workspace

# Run tests for a single crate
cargo test -p rivulet-core

On Linux, the game-window enumerator has an additional opt-in integration test. The CI provisions xdotool and an Xvfb test display and runs:

RIVULET_TEST_XDOTOOL=1 cargo test -p rivulet-core \
  list_game_windows_finds_the_ci_window -- --nocapture

It asserts that list_game_windows() returns a non-empty list containing the known CI window. See docs/LINUX_BUILD.md for a local Xvfb setup. The CI Cargo cache stores registry data only; compiler artifacts in target/ are deliberately not cached to prevent stale E0460 failures after Rust or dependency updates.

The tests cover the pure data structures (AudioFrame, OutputSettings, RecordingSettings, ...), the engine's recording pipeline (synthetic video + audio to MP4, including separate audio tracks verified via the GStreamer Discoverer), the streaming pipelines (RTMPS/dual output parse and route audio correctly), stream health tracking (status derivation from drop ratio, bitrate/FPS collapse and stalls), the encoder/recorder lifecycle, the video encoder abstraction (element names, detection order, fallback, pipeline fragments, 4:2:0 input caps), the recording performance metrics (FPS estimation, encoder load from a sliding window, file byte accounting, reset semantics), the recording presets (resolution/FPS presets with caps and transform fragments), the recording overlay (textoverlay pipeline integration, enable/disable toggle, duration formatting), the scene system (Scene model with parent UUIDs and ARGB color coding; SceneManager with add/remove/rename, active-scene switching with undo history, and depth-first ordered listing), the updater (GitHub release parsing, version comparison, platform asset selection, HTTP fetch against a local test server) and the i18n layer (locale resolution and translation lookups), plus the release code-signing automation (rivulet-core/tests/ci_signing.rs — verifies the signing scripts exist, that signing is gated on secret-presence outputs rather than raw secrets in if: conditions, and that the Windows MSI is signed after it is built). The M8 suites cover the typed configuration gate per parameter (rivulet-core/tests/recording_config.rs), the public-API stability and encapsulation contract (rivulet-core/tests/api_stability.rs), the capture-backend behaviour contracts — exact error strings, the platform contract of every new() and the default-backend mapping (rivulet-core/tests/capture_backend.rs) — the GUI-free session entry point including events, pause/resume, stop counters and Drop-finalize (rivulet-core/tests/recording_job.rs), its camera and game-capture ports (rivulet-core/tests/camera.rs, rivulet-core/tests/game_capture.rs), and the CI pinning guards that fail when a delivery claim, an example list or an action pin drifts (rivulet-core/tests/ci_pinning.rs). Building and running the tests requires GStreamer (dev packages + plugins) and, on Linux, the LIBCLANG_PATH environment variable. The hardware-encoding end-to-end test only runs when the matching GPU plugin is present.

Linting & Formatting

# Check formatting
cargo fmt --all -- --check

# Lint the whole workspace (including tests and examples)
cargo clippy --workspace --all-targets -- -D warnings

The CI also runs actionlint to lint the GitHub Actions workflow files and ShellCheck on the macOS packaging scripts. All third-party GitHub Actions are pinned to a full commit SHA (with the upstream version in a trailing comment) so a repointed tag or branch cannot swap in a malicious commit; rivulet-core/tests/ci_pinning.rs enforces this and docs/ci-action-pins.md maps each pin to its upstream version for reviewing Dependabot updates (its table is generated from the workflows by scripts/generate-action-pins.py). Dependabot PRs are approved and auto-merged via .github/workflows/dependabot-auto-merge.yml

Dependency update ownership is split intentionally: Renovate handles grouped Cargo updates and the dependency dashboard (renovate.json), while Dependabot handles GitHub Actions SHA pins. This avoids duplicate update PRs; all updates remain subject to the required CI, security, and pinning checks. once the required checks (including the pinning tests) pass; this needs "Allow auto-merge" and branch protection enabled in the repo settings. A daily nightly job (scripts/check-action-pins.py) also compares every pinned SHA against upstream — failing on missed same-major updates and reporting newer majors (--fail-on-major makes those fatal). --json gives a machine-readable result and --comment a compact Markdown notification, published to the nightly run's step summary.

GitHub Security

The repository security baseline is enabled and enforced in two layers:

  • Secret Scanning + Push Protection: enabled in the GitHub repository settings. Pushes containing detected credentials are blocked before they reach the repository. Signing secrets and stream keys must still remain in GitHub Actions Secrets and must never be printed to logs.
  • CodeQL: .github/workflows/security.yml scans Rust on pushes to develop, pull requests, and the weekly scheduled run. Results are uploaded to GitHub Code Scanning.
  • Dependency Review: the same workflow reviews pull-request dependency changes and fails on high-severity advisories; it posts a summary to the PR.
  • Cargo Audit + Cargo Deny: the Security workflow checks RustSec advisories, yanked crates, licenses, dependency sources, and the checked-in deny.toml policy.
  • OpenSSF Scorecard: .github/workflows/scorecard.yml evaluates supply-chain practices weekly and on repository changes, publishes the signed result, and uploads a separate SARIF category to Code Scanning.

The workflow actions are pinned to commit SHAs and covered by rivulet-core/tests/ci_pinning.rs. Verify the repository-level settings with:

gh api repos/thoser666/rivulet --jq '.security_and_analysis'

The expected state is secret_scanning.status=enabled and secret_scanning_push_protection.status=enabled. These settings are managed by GitHub and cannot be represented fully in a repository file. See docs/security.md for incident handling and plan limitations.

Security vulnerabilities are handled via responsible disclosure, outlined in SECURITY.md: report privately through a security advisory, never in a public issue.

The develop branch ruleset requires CI, Security, OpenSSF Scorecard, CodeQL (rust), Dependency Review, and Pinning-Tests before merge and blocks direct updates for ordinary contributors. GitHub does not allow an Actions-only bypass on this personal repository, so the active exception is the broader administrator bypass needed by the release automation; see docs/security.md for details.

Beta-readiness is evaluated on every push by scripts/check-beta-gate.py (CI job Beta-Gate readiness): it parses the roadmap checkboxes for M1/M3 and the 3-OS CI build matrix, and checks the signing secrets, the latest CI run and open release-blocker issues via the GitHub API when a token is available. The verdict is published to the run's step summary. It is informational by default (exit 0 — the project is expected to be NOT READY while it is still in alpha); --fail turns any unmet or unverified criterion into exit 1 (e.g. as a gate for a beta tag), and --json/--comment give machine-readable or Markdown output. Note that a workflow GITHUB_TOKEN can never read Actions secrets, so the signing criterion stays unverified in CI by design and is confirmed locally with a PAT that has secret read access.

GUI

Run the desktop GUI with cargo run -p rivulet-gui (or the packaged binary). If a recording receives no frames within a timeout (default 5 seconds) it is aborted automatically with an error in the UI — the recording pipeline is only started by the first captured frame, so a capture that silently delivers nothing would otherwise look like a running recording while writing no file. On Windows the timeout only applies before the first frame (a static screen can legitimately pause); on Linux it also fires mid-recording, because xcap captures actively on every loop iteration and a frame gap therefore means the capture thread died or its source became unavailable. The timeout is configurable at startup:

# wait 30 seconds for the first frame before aborting
cargo run -p rivulet-gui -- --no-frame-timeout 30

Assets

Scene snapshots are exported from the Scenes view with Save scene snapshot. The export is a deterministic 1920×1080 RGBA8 PNG of the current scene layout: visible source layers are ordered by z-order and rendered with their transform and opacity. Since native source-pixel rendering is platform-specific and not yet connected to the scene compositor, source kinds use stable layout colors; this keeps exports reproducible and makes the remaining renderer integration explicit.

The README thumbnail (docs/thumbnail.png), the GitHub social preview (docs/social-preview.png), the OpenGraph fallback (docs/opengraph.png), the Linux AppImage icon (packaging/rivulet.png), the macOS app icon (packaging/rivulet.icns) and the Discord Rich Presence artwork plus app icon (docs/assets/rivulet-rich-presence-*.png, docs/assets/rivulet-app-icon-512.png) are generated from the logo by scripts/generate-assets.sh (requires ImageMagick and Python 3). Text uses the committed DejaVu Sans fonts and the resize filter is pinned; because text rasterization depends on the FreeType version, the canonical output is produced with a pinned ImageMagick (dpokidov/imagemagick:7.1.2-12, pinned by content digest sha256:87998ec1…53cee1). ImageMagick publishes no official container image, so the image is multi-arch (amd64/arm64) and the digest pin guards against tag mutation. Pull requests do not build that tool layer from snapshot.debian.org: the Assets tool image workflow (.github/workflows/assets-tool-image.yml) publishes it once, scripts/assets-tool-image.py pulls it by the digest pinned in scripts/assets-tool-image.txt (the job token carries packages: read for that), and only when the pull fails (or the pull fails) does it build scripts/assets-tool.Dockerfile — with 30s/120s/300s backoff and a deb.debian.org fallback, so a mirror outage cannot fail the asset comparison before it runs (issue #391).

The scripts/generate-assets-docker.sh wrapper runs that pinned image for you via Docker or Podman, so no local ImageMagick installation is needed:

scripts/generate-assets-docker.sh            # regenerate all assets
scripts/generate-assets-docker.sh --check    # regenerate + verify they match the committed files

CI runs the same pinned image and fails if the committed assets drift (scripts/check-assets.py), so logo or generator changes must be committed together with the regenerated output. There is no separate Python image: png2icns.py and check-assets.py use only the Python standard library and run under the python3 installed inside the pinned ImageMagick container.

To activate the social preview, upload docs/social-preview.png as the repository's Social preview image under Settings → General → Social preview on GitHub; it is used when repository and release links are shared (OpenGraph/og:image). docs/opengraph.png (1200×630) is the matching fallback for X, Facebook and LinkedIn cards.

GitHub exposes no public API for that Settings upload, so the release workflow attaches both images to every release instead: they are then available at a stable URL (releases/latest/download/opengraph.png) that a website can reference from its og:image meta tag.

For Discord, upload docs/assets/rivulet-rich-presence-1024.png under Rich Presence → Art Assets (asset name rivulet_logo) and docs/assets/rivulet-app-icon-512.png as the App Icon (General Information) — the artwork renders on the profile card and the app icon replaces the generic game-controller placeholder in the server member list (see docs/activity-status.md for details). The installers ship both files together with the setup documentation inside the package (Windows: discord\ in the install folder; Linux: usr/share/doc/rivulet/discord/ in the AppImage; macOS: Contents/Resources/discord/ in the app bundle), and the GitHub release attaches them as standalone downloads.

Note: On Linux, set LIBCLANG_PATH to your LLVM lib directory (e.g. export LIBCLANG_PATH=/usr/lib/llvm-14/lib) if the clang-sys build fails.


📦 Installation

Download

Get the latest release from GitHub Releases:

  • Windows: rivulet-windows-x86_64.msi (installer) or the portable .zip
  • macOS: rivulet-macos-aarch64.dmg (Apple Silicon) — Universal2/intel builds are planned
  • Linux: rivulet-linux-x86_64.AppImage (portable, no install needed)

Pick the package for your platform, download it, and run the installer / open the AppImage. See the Rivulet-Bedienungsanleitung for platform-specific first-launch notes (Windows screen-capture permission, Linux PipeWire/Wayland access).

Prerequisites

GStreamer (Core + gst-plugins-good/gst-plugins-bad/gst-plugins-ugly + gst-libav) is required by the engine for encoding, audio mixing, and streaming (H.264 + AAC).


📦 Releases

Releases are fully automated via GitHub Actions:

Alpha channel (release.yml)

Every push with a feat: or fix: commit (Conventional Commits) to develop produces a new alpha release:

Commit type SemVer bump Example
fix(...) Patch 0.20.0 → 0.20.1-alpha.1
feat(...) Minor 0.20.0 → 0.21.0-alpha.1
feat!(...) Major 0.x.y → 1.0.0-alpha.1
  1. Versioning: The next version is computed via SemVer from the feat:/fix: commits since the last tag (scripts/release-version.sh), plus -alpha.N (N = next free number). Cargo.toml and CHANGELOG.md are bumped and committed as chore(release): prepare vX.Y.Z-alpha.N.
  2. Tag: vX.Y.Z-alpha.N is set and pushed.
  3. Binaries: Three platforms (Linux x86_64, Windows x86_64, macOS aarch64) build release binaries from the tag.
  4. Packages:
    • Linux: AppImage (packaging/linux/build-appimage.sh)
    • Windows: portable ZIP + MSI (packaging/windows/) — the GStreamer runtime is bundled, so the bundle runs without a GStreamer installation
    • macOS: DMG (packaging/macos/build-dmg.sh)
  5. Release: A formal GitHub release (pre-release) with all artifacts is created.

Dependabot pushes are skipped (no release build for merged bot PRs).

Orphaned tags: if a platform build fails, the tag is still pushed but no GitHub Release is created. scripts/backfill-releases.sh --check detects such orphaned tags and backfills them from the original workflow artifacts; see docs/release-backfill.md.

Beta/RC/stable channel (ci.yml)

A manually set tag vX.Y.Z (without -alpha.*) builds the same packages and publishes a GitHub release. Tags with -beta.*/-rc.* are marked as pre-releases; stable tags as full releases.

Entering beta is gated on the Beta-Gate criteria defined in the Roadmap: the tag may only be set once all six criteria are met.

Code signing

Release packages are signed automatically when the matching secrets are configured; without secrets, unsigned packages are built (default). Signing runs only when all required secrets for a platform are present, so CI keeps working for forks and for the unsigned development builds.

  • Windows (packaging/windows/sign.ps1): the staged rivulet-gui.exe is signed before the portable ZIP and MSI are packaged (so the installer embeds the signed binary), and the resulting MSI is signed as well. Secrets:
    • WINDOWS_CERT_BASE64 — base64-encoded .pfx code signing certificate (certutil -encode cert.pfx cert.txt, then paste the body without the -----BEGIN/END CERTIFICATE----- lines).
    • WINDOWS_CERT_PASSWORD — password of the .pfx.
  • macOS (packaging/macos/sign-notarize.sh): the app bundle is codesigned with the hardened runtime, packaged into a DMG, then notarized and stapled via notarytool. Secrets:
    • MACOS_CERT_BASE64 — base64-encoded .p12 "Developer ID Application" certificate (base64 -i cert.p12).
    • MACOS_CERT_PASSWORD — password of the .p12.
    • APPLE_ID — Apple ID used for notarization.
    • APPLE_APP_PASSWORD — app-specific password for the Apple ID.
    • APPLE_TEAM_ID — Apple Developer Team ID.
  • Linux (packaging/linux/sign-gpg.sh): the AppImage is signed with a detached GPG signature (rivulet-linux-x86_64.AppImage.asc), as expected by distributions and package managers. Secrets:
    • LINUX_GPG_PRIVATE_KEY — ASCII-armored (or base64-encoded) GPG private key (gpg --armor --export-secret-key <key-id>).
    • LINUX_GPG_PASSPHRASE — passphrase of the key, if it has one (optional).

Add the secrets under Settings → Secrets and variables → Actions. Full maintainer setup steps (certificate export, Apple notarization, GPG key creation, verification, troubleshooting) are in docs/code-signing.md.

Free for open source: We applied at SignPath Foundation for a free OV-level Authenticode certificate, but the application was rejected (Oct 2026) due to insufficient GitHub stars / download volume for their reputation threshold. Re-application is possible once the project reaches higher visibility. Until then, Windows artifacts remain unsigned (the PFX path is the alternative for paid certificates). The four SIGNPATH_API_TOKEN, SIGNPATH_ORGANIZATION_ID, SIGNPATH_PROJECT_SLUG, and SIGNPATH_SIGNING_POLICY_SLUG secrets are still referenced in the workflow and will activate signing if the application is approved in the future. The Beta-Gate (criterion 4) accepts either the PFX pair or the SignPath set for Windows.

Setting up the signing secrets

All seven secrets are a Beta-Gate criterion (criterion 4): CI keeps working without them (unsigned packages are built), but a beta release must be signed. Configure them in the repository settings (Settings → Secrets and variables → Actions). The automation itself is smoke-tested on every push without secrets (.github/workflows/signing-e2e.yml), so the scripts are verified even before real certificates exist.

Windows (WINDOWS_CERT_*)

  1. Obtain a certificate. Buy a code-signing certificate from an issuer (DigiCert, Sectigo, GlobalSign, …). For a first smoke test you can create a self-signed one with PowerShell — note that Windows will not trust it (warning “unknown publisher”), so a purchased certificate is required for real users:
    New-SelfSignedCertificate -Type CodeSigningCert -Subject "CN=Rivulet" `
      -CertStoreLocation Cert:\CurrentUser\My
  2. Export it to a .pfx file (the .pfx password is stored as WINDOWS_CERT_PASSWORD):
    $thumb = (Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert).Thumbprint
    $pwd = Read-Host "PFX password" -AsSecureString
    Export-PfxCertificate -Cert "Cert:\CurrentUser\My\$thumb" `
      -FilePath cert.pfx -Password $pwd
  3. Base64-encode the .pfx into a single line:
    [Convert]::ToBase64String([IO.File]::ReadAllBytes("cert.pfx"))
    (Alternatively certutil -encode cert.pfx cert.txt and paste the body without the -----BEGIN/END CERTIFICATE----- lines.)
  4. Create the secrets: paste the base64 blob into WINDOWS_CERT_BASE64 and the password into WINDOWS_CERT_PASSWORD. packaging/windows/sign.ps1 decodes the blob, signs the staged rivulet-gui.exe before packaging, and signs the resulting MSI.

macOS (MACOS_CERT_* + APPLE_*)

  1. Obtain a certificate. In the Apple Developer portal (developer.apple.com → Certificates) create a Developer ID Application certificate, download it and install it into the macOS Keychain.
  2. Export it to a .p12 file: Keychain Access → right-click the certificate → Export… → format Personal Information Exchange (.p12). The export password is stored as MACOS_CERT_PASSWORD.
  3. Base64-encode the .p12 into a single line:
    base64 -i cert.p12
  4. Create the secrets: MACOS_CERT_BASE64 (the base64 blob) and MACOS_CERT_PASSWORD (the export password). codesign-app.sh imports the .p12 into a temporary keychain and signs the app bundle with the hardened runtime.
  5. Notarization credentials — create an app-specific password for your Apple ID (appleid.apple.com → Sign-In & Security → App-Specific Passwords):
    • APPLE_ID — your Apple ID (the sign-in email).
    • APPLE_APP_PASSWORD — the app-specific password (never your Apple ID password).
    • APPLE_TEAM_ID — your Team ID (developer.apple.com → Membership).
  6. (Optional) if your certificate identity is not Developer ID Application, set MACOS_SIGN_IDENTITY accordingly (defaults to Developer ID Application).

Verifying

  • gh secret list shows which names are configured.
  • scripts/check-beta-gate.py (run with a token that can read secrets) reports exactly which of the seven secrets are still missing — criterion 4 of the Beta-Gate.
  • With all secrets present, the next release build signs the packages; without them, unsigned packages are produced (the default).

Updater troubleshooting

See docs/updater-troubleshooting.md for Windows installer exit codes, update crash diagnostics, and recovery steps.

During a Windows update, the app spawns the detached rivulet-updater watchdog (rivulet-updater/src/bin/rivulet-updater.rs), passing the running process IDs with --wait-pid and the downloaded MSI with --install. The watchdog copies itself to the temp directory (so the launcher's file lock never blocks it), waits up to 60s for each PID to exit, then runs msiexec /i <msi> and waits for it to finish. Because the old executables have fully exited before the WiX MajorUpgrade runs RemoveExistingProducts, the new files replace the old ones instead of the installer bailing on locked in-use files.