Documentation > Backend > Models Reference
This reference documents all data models used in SpiritStream's Rust backend. Models are defined as Rust structs with serde serialization for IPC communication.
Understanding how models relate to each other is key to working with SpiritStream's data layer:
Profile
├── id, name, incoming_url
└── output_groups: Vec<OutputGroup>
├── id, name
├── video: VideoSettings
│ └── codec, resolution, bitrate, fps, preset...
├── audio: AudioSettings
│ └── codec, bitrate, sample_rate, channels
├── container: ContainerSettings
│ └── format
└── stream_targets: Vec<StreamTarget>
└── id, platform, name, url, stream_key
The hierarchy reflects how streaming actually works: a Profile captures your complete streaming setup (where video comes from, how it's encoded, where it goes). Each OutputGroup represents one FFmpeg process—all targets in a group share the same encoding, which is efficient but means different quality levels require separate groups.
The top-level configuration entity representing a streaming setup. A user typically has multiple profiles for different scenarios: "Gaming Stream" for high-action content at 60fps, "Just Chatting" for lower bitrate talking-head streams, "Recording Only" for local capture without RTMP output.
Profiles are stored as individual JSON files in the app data directory, optionally encrypted with a password. The incoming_url is where SpiritStream receives video—usually from OBS via a local RTMP server or a capture card.
// apps/desktop/src-tauri/src/models/profile.rs
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Profile {
pub id: String,
pub name: String,
pub incoming_url: String,
pub output_groups: Vec<OutputGroup>,
}| Field | Type | Description |
|---|---|---|
id |
String |
UUID identifier |
name |
String |
User-friendly profile name |
incoming_url |
String |
RTMP ingest URL (e.g., rtmp://localhost:1935/live/stream) |
output_groups |
Vec<OutputGroup> |
List of output configurations |
TypeScript Equivalent:
interface Profile {
id: string;
name: string;
incomingUrl: string;
outputGroups: OutputGroup[];
}An output configuration specifying encoding settings and stream targets. The key design decision here is that all targets in a group share identical encoding. FFmpeg encodes once and pushes the same stream to multiple destinations, which is CPU-efficient but inflexible.
When to use multiple output groups:
- Different quality levels (1080p for YouTube, 720p for Twitch)
- Different frame rates (60fps for gaming, 30fps for mobile viewers)
- Platform-specific encoding (one uses NVENC, another uses x264)
When to use one output group with multiple targets:
- Same quality to all platforms
- Simulcasting the exact same stream everywhere
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OutputGroup {
pub id: String,
pub name: String,
pub video: VideoSettings,
pub audio: AudioSettings,
pub container: ContainerSettings,
pub stream_targets: Vec<StreamTarget>,
}| Field | Type | Description |
|---|---|---|
id |
String |
UUID identifier |
name |
String |
Group name (e.g., "Main Output") |
video |
VideoSettings |
Video encoding settings |
audio |
AudioSettings |
Audio encoding settings |
container |
ContainerSettings |
Container format settings |
stream_targets |
Vec<StreamTarget> |
RTMP destinations |
TypeScript Equivalent:
interface OutputGroup {
id: string;
name: string;
video: VideoSettings;
audio: AudioSettings;
container: ContainerSettings;
streamTargets: StreamTarget[];
}Video encoding configuration. These settings map directly to FFmpeg arguments—understanding them means understanding FFmpeg's encoding pipeline.
The codec field is the most important choice. Hardware encoders (h264_nvenc, h264_qsv, h264_amf) offload work to your GPU, while software encoders (libx264) use CPU. The "copy" codec passes video through unchanged—useful when your input is already encoded correctly and you just want to relay it.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoSettings {
pub codec: String,
pub width: u32,
pub height: u32,
pub bitrate: u32,
pub fps: u32,
pub preset: Option<String>,
pub profile: Option<String>,
pub keyframe_interval: Option<u32>,
}| Field | Type | Description |
|---|---|---|
codec |
String |
FFmpeg codec name (e.g., libx264, h264_nvenc, copy) |
width |
u32 |
Output width in pixels |
height |
u32 |
Output height in pixels |
bitrate |
u32 |
Video bitrate in kbps |
fps |
u32 |
Frames per second |
preset |
Option<String> |
Encoder preset (e.g., veryfast) |
profile |
Option<String> |
H.264 profile (e.g., high) |
keyframe_interval |
Option<u32> |
Keyframe interval in seconds |
Passthrough Mode:
When codec is "copy", video is passed through without re-encoding.
Audio encoding configuration. Audio is simpler than video—AAC is the universal choice for RTMP streaming, and the settings rarely need adjustment beyond bitrate. The "copy" codec works well when your source is already AAC-encoded (common from OBS).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AudioSettings {
pub codec: String,
pub bitrate: u32,
pub sample_rate: u32,
pub channels: u32,
}| Field | Type | Description |
|---|---|---|
codec |
String |
FFmpeg audio codec (e.g., aac, copy) |
bitrate |
u32 |
Audio bitrate in kbps |
sample_rate |
u32 |
Sample rate in Hz (e.g., 48000) |
channels |
u32 |
Number of channels (1=mono, 2=stereo) |
Output container format settings. For RTMP streaming, this is almost always "flv"—it's the only container format the RTMP protocol supports. This model exists for future extensibility (recording to MP4, HLS output, etc.) rather than current flexibility.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContainerSettings {
pub format: String,
}| Field | Type | Description |
|---|---|---|
format |
String |
Container format (typically flv for RTMP) |
RTMP streaming destination. Each target represents one place your stream goes—a Twitch channel, a YouTube broadcast, a custom RTMP server. The service field references a Platform enum variant that determines platform-specific behavior (URL handling, stream key placement, log redaction).
Security note: Stream keys are sensitive credentials. They're encrypted at rest when profiles are password-protected, and the stream_key field supports environment variable references (${MY_STREAM_KEY}) for users who prefer not to store keys in profile files at all.
// apps/desktop/src-tauri/src/models/stream_target.rs
// Platform enum auto-generated from data/streaming-platforms.json at build time
include!(concat!(env!("OUT_DIR"), "/generated_platforms.rs"));
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StreamTarget {
pub id: String,
#[serde(default)]
pub service: Platform,
#[serde(default)]
pub name: String,
pub url: String,
pub stream_key: String,
}| Field | Type | Description |
|---|---|---|
id |
String |
UUID identifier |
service |
Platform |
Auto-generated enum from platform registry (80+ platforms) |
name |
String |
Display name (e.g., "My Twitch Channel") |
url |
String |
RTMP server URL |
stream_key |
String |
Stream key (can be env var reference: ${VAR_NAME}) |
Platform Enum:
The Platform enum is auto-generated at build time from data/streaming-platforms.json. This means you won't find the enum definition in source code—it's created by build.rs and included via the include! macro. Common values include:
Platform::Youtube- YouTube LivePlatform::Twitch- TwitchPlatform::Kick- KickPlatform::FacebookLive- Facebook LivePlatform::Custom- Custom RTMP server- Plus 75+ additional platforms from OBS's rtmp-services
See Platform Registry for the full list and how to add new platforms.
Statistics models capture runtime data that doesn't persist—they're populated from FFmpeg's progress output and pushed to the frontend for live monitoring.
Real-time streaming statistics parsed from FFmpeg's stderr output. These values update multiple times per second during an active stream and help users detect problems: dropping frames indicates encoding overload, bitrate fluctuation suggests network issues, and speed below 1.0 means encoding can't keep up with real-time.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StreamStats {
pub group_id: String,
pub frame: u64,
pub fps: f64,
pub bitrate: f64,
pub speed: f64,
pub size: u64,
pub time: f64,
pub dropped_frames: u64,
pub dup_frames: u64,
}| Field | Type | Description |
|---|---|---|
group_id |
String |
Output group identifier |
frame |
u64 |
Current frame number |
fps |
f64 |
Current frames per second |
bitrate |
f64 |
Current bitrate in kbps |
speed |
f64 |
Encoding speed (1.0 = real-time) |
size |
u64 |
Total bytes written |
time |
f64 |
Elapsed time in seconds |
dropped_frames |
u64 |
Number of dropped frames |
dup_frames |
u64 |
Number of duplicated frames |
Settings models store user preferences that apply across all profiles. They're persisted separately from profiles so changing your theme doesn't require re-saving your streaming configuration.
Application-wide settings stored in a single JSON file. The last_profile field enables "remember what I was doing" behavior on app launch.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Settings {
pub language: String,
pub theme: String,
pub start_minimized: bool,
pub show_notifications: bool,
pub ffmpeg_path: Option<String>,
pub auto_download_ffmpeg: bool,
pub last_profile: Option<String>,
}| Field | Type | Description |
|---|---|---|
language |
String |
UI language code (e.g., en, es) |
theme |
String |
Theme setting (light, dark, system) |
start_minimized |
bool |
Start app minimized |
show_notifications |
bool |
Show desktop notifications |
ffmpeg_path |
Option<String> |
Custom FFmpeg path |
auto_download_ffmpeg |
bool |
Auto-download FFmpeg if missing |
last_profile |
Option<String> |
Last used profile name |
Encoder models describe what's available on the user's system. They're populated at runtime by querying FFmpeg, since available encoders depend on hardware (NVIDIA GPU for NVENC, Intel CPU for QSV, etc.).
Information about an available encoder. The encoder_type distinction matters for UI—hardware encoders are generally preferred for streaming because they don't compete with games for CPU resources.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EncoderInfo {
pub name: String,
pub display_name: String,
pub encoder_type: EncoderType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum EncoderType {
Software,
Hardware,
}| Field | Type | Description |
|---|---|---|
name |
String |
FFmpeg encoder name |
display_name |
String |
User-friendly name |
encoder_type |
EncoderType |
Software or hardware |
Platform models provide metadata about streaming services. They're used for UI conveniences (auto-filling URLs, displaying the right icon) and security (intelligently redacting stream keys in logs based on each platform's URL structure).
Different platforms embed stream keys in URLs differently. This enum captures the two strategies SpiritStream needs to handle:
// apps/desktop/src-tauri/src/services/platform_registry.rs
pub enum StreamKeyPlacement {
/// Append stream key to URL (e.g., rtmp://server/app/{key})
Append,
/// Replace {stream_key} template in URL (e.g., rtmp://server/{stream_key}/live)
InUrlTemplate,
}| Variant | Example | Used By |
|---|---|---|
Append |
rtmp://live.twitch.tv/app/ + KEY |
Twitch, YouTube, most platforms |
InUrlTemplate |
rtmp://server/{stream_key}/live |
Restream, some custom setups |
Platform-specific configuration loaded from the JSON registry. Each platform has a PlatformConfig that determines how URLs are built, normalized, and how stream keys are masked in logs.
pub struct PlatformConfig {
/// Display name
pub name: &'static str,
/// Default RTMP server URL (may contain {stream_key} template)
pub default_server: &'static str,
/// Stream key placement strategy
pub placement: StreamKeyPlacement,
/// Default app path for URL normalization (e.g., "app", "live2")
pub default_app_path: Option<&'static str>,
/// Stream key position in URL path for redaction (0 = don't mask)
pub stream_key_position: usize,
}| Field | Type | Description |
|---|---|---|
name |
&'static str |
Human-readable platform name |
default_server |
&'static str |
Default RTMP URL, may include {stream_key} placeholder |
placement |
StreamKeyPlacement |
How stream key is added to URL |
default_app_path |
Option<&'static str> |
Path segment for URL normalization |
stream_key_position |
usize |
Which path segment contains the key (for log redaction) |
Key Methods:
normalize_url()- Fixes URLs missing required path segments (e.g., Kick requires/app)redact_url()- Masks stream keys in URLs for safe loggingbuild_url_with_key()- Constructs final RTMP URL with stream key
See Platform Registry for implementation details and how to add new platforms.
Default values are chosen to work out-of-the-box for common streaming scenarios. They prioritize compatibility (1080p60 works everywhere) and safety (copy mode avoids CPU load until the user explicitly chooses an encoder).
New profiles start with a local RTMP URL and one empty output group. The localhost:1935 URL assumes users are streaming from OBS to SpiritStream on the same machine—the most common setup.
impl Default for Profile {
fn default() -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
name: "New Profile".to_string(),
incoming_url: "rtmp://localhost:1935/live/stream".to_string(),
output_groups: vec![OutputGroup::default()],
}
}
}impl Default for OutputGroup {
fn default() -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
name: "Main Output".to_string(),
video: VideoSettings::default(),
audio: AudioSettings::default(),
container: ContainerSettings::default(),
stream_targets: vec![],
}
}
}The "copy" codec default means new output groups pass video through without re-encoding—zero CPU impact until you explicitly choose an encoder. The other defaults (1080p60, 6000kbps, 2-second keyframes) match Twitch's recommended settings and work well on most platforms.
impl Default for VideoSettings {
fn default() -> Self {
Self {
codec: "copy".to_string(),
width: 1920,
height: 1080,
bitrate: 6000,
fps: 60,
preset: Some("veryfast".to_string()), // Fast encoding, good quality
profile: Some("high".to_string()), // H.264 High profile for quality
keyframe_interval: Some(2), // 2 seconds for platform compatibility
}
}
}Audio defaults to copy mode (passthrough) with standard streaming settings. 48kHz stereo at 160kbps is the sweet spot for voice and music—higher bitrates offer diminishing returns for streaming audio.
impl Default for AudioSettings {
fn default() -> Self {
Self {
codec: "copy".to_string(),
bitrate: 160,
sample_rate: 48000, // Professional audio standard
channels: 2, // Stereo
}
}
}All models use serde for JSON serialization. This serves two purposes: persisting profiles to disk and sending data across the Tauri IPC boundary to the React frontend. The same struct definition works for both—no separate DTOs needed.
// Serialize to JSON
let json = serde_json::to_string_pretty(&profile)?;
// Deserialize from JSON
let profile: Profile = serde_json::from_str(&json)?;Rust uses snake_case for fields; JavaScript uses camelCase. The #[serde(rename_all = "camelCase")] attribute handles this automatically—write idiomatic Rust, and the frontend receives idiomatic JavaScript:
| Rust | JSON |
|---|---|
incoming_url |
incomingUrl |
stream_targets |
streamTargets |
sample_rate |
sampleRate |
Models can implement validation methods that check business rules before the data reaches services. This catches problems early—a profile with no output groups is invalid regardless of what operation you're trying to perform on it.
impl Profile {
pub fn validate(&self) -> Result<(), ValidationError> {
if self.name.is_empty() {
return Err(ValidationError::EmptyName);
}
if self.output_groups.is_empty() {
return Err(ValidationError::NoOutputGroups);
}
for group in &self.output_groups {
group.validate()?;
}
Ok(())
}
}Related: Rust Overview | Services Layer | Tauri Commands