How the Bilistream Project Is Structured: A Rust RTMP Forwarding Architecture
Bilistream is a Rust application that forwards live-stream URLs from YouTube or Twitch to a Bilibili RTMP endpoint using ffmpeg, organized into a standard Cargo layout with modular configuration files, trait-based plugins, and a continuous polling loop.
The bilistream project structure follows conventional Rust patterns while implementing a flexible plugin system for multi-platform stream aggregation. As an open-source tool developed by limitcool, it isolates platform-specific logic behind a common trait interface, making it straightforward to extend with new streaming providers. Understanding this architecture reveals how the codebase manages configuration loading, asynchronous polling, and external process orchestration.
Directory Layout and Cargo Configuration
The repository adheres to a classic Cargo project structure. At the root level, Cargo.toml declares the crate metadata, version, and dependencies including reqwest, tokio, async-trait, and gotify. Container support is provided via Dockerfile and docker-compose.yaml, while README.md contains usage instructions.
The src/ directory contains the application logic organized into modules:
bilistream/
├── Cargo.toml
├── Dockerfile
├── docker-compose.yaml
├── README.md
├── config.yaml
└── src/
├── main.rs
├── config.rs
├── push.rs
└── plugins/
├── mod.rs
├── live.rs
├── twitch.rs
├── youtube.rs
└── response.rs
Core Source Files and Responsibilities
src/main.rs: The Entry Point and Event Loop
src/main.rs serves as the application entry point. It initializes the tracing subscriber for logging, loads the YAML configuration via load_config, and selects the appropriate live-stream implementation through select_live. The file contains the primary polling loop that continuously checks stream status and manages the ffmpeg lifecycle.
When a stream goes live, the main loop optionally triggers Gotify notifications, starts the BiliBili live session via bili_start_live, and invokes the local ffmpeg wrapper. If the remote stream ends, it may call bili_stop_live to terminate the broadcast. The loop sleeps for cfg.interval seconds between checks.
src/config.rs: Configuration Management
This module defines strongly-typed configuration structs including Config, BiliLive, TwitchC, YoutubeC, and GotifyConfig. The load_config function deserializes config.yaml using serde_yaml, providing type-safe access to settings throughout the application.
src/push.rs: Notification System
src/push.rs implements a thin wrapper around the gotify crate. It creates a client from the configured URL and token, then fires messages to announce stream events. The main polling loop calls this helper when broadcasting begins.
The Plugin Architecture and Live Trait
The src/plugins/ directory implements a trait-driven design that abstracts platform differences. src/plugins/mod.rs re-exports concrete implementations for clean imports.
src/plugins/live.rs: The Core Abstraction
src/plugins/live.rs defines the Live async trait, which is the cornerstone of the bilistream project structure. This trait specifies four required methods:
get_status()– Returns a boolean indicating if the stream is currently liveroom()– Returns the room identifier as a string sliceget_real_m3u8_url()– Fetches the actual media URL for ffmpeg consumptionset_room()– Updates the room identifier
The same file contains the select_live factory function. This matcher reads cfg.platform and returns a boxed trait object (Box<dyn Live>) containing the appropriate concrete implementation: Youtube, Twitch, or a preview mode. It also houses helper functions for extracting YouTube IDs by scraping HTML pages.
Platform-Specific Implementations
src/plugins/youtube.rs: YouTube Integration
This module implements the Live trait for YouTube. The get_status method delegates to get_youtube_live_status, which scrapes the live page HTML to detect broadcast state. The get_real_m3u8_url method delegates to yt-dlp to extract the direct stream URL.
src/plugins/twitch.rs: Twitch Integration
src/plugins/twitch.rs provides the Twitch implementation. It queries Twitch's GraphQL endpoint to determine stream status and uses yt-dlp (or a manual token-based HLS request) to retrieve the media URL. The module supports authenticated streams through an optional cookies configuration field.
src/plugins/response.rs
This file currently contains unused response utilities but remains part of the plugin structure for potential future extensions.
Execution Flow and Runtime Behavior
The application follows a deterministic lifecycle:
- Initialization –
main.rssets up tracing and loads configuration fromconfig.yaml - Platform Selection –
select_liveinlive.rsconstructs the appropriate struct (Youtube::new,Twitch::new, etc.) - Polling Loop – The endless loop in
main.rsperforms the following:- Calls
r.get_status()to check remote stream state - If live: Sends Gotify notification, ensures BiliBili session is active, invokes ffmpeg with the RTMP URL/Key and the m3u8 URL from
r.get_real_m3u8_url() - If stream ended: optionally calls
bili_stop_live() - Sleeps for the configured interval
- Calls
- FFmpeg Management – The ffmpeg wrapper builds a command line that streams the retrieved media URL to the BiliBili RTMP endpoint. It recursively restarts on non-zero exit codes to ensure high availability.
Practical Code Examples
Adding a New Platform
To extend the bilistream project structure with a new provider, implement the Live trait and register it in select_live:
// src/plugins/mystream.rs
use async_trait::async_trait;
use super::Live;
use reqwest_middleware::ClientWithMiddleware;
use std::error::Error;
pub struct MyStream {
room: String,
client: ClientWithMiddleware,
}
#[async_trait]
impl Live for MyStream {
async fn get_status(&self) -> Result<bool, Box<dyn Error>> {
Ok(self.client
.get(&format!("https://api.mystream.com/{}/status", self.room))
.send()
.await?
.json::<serde_json::Value>()
.await?["live"]
.as_bool()
.unwrap_or(false))
}
fn room(&self) -> &str { &self.room }
async fn get_real_m3u8_url(&self) -> Result<String, Box<dyn Error>> {
let mut cmd = std::process::Command::new("yt-dlp");
cmd.arg("-g")
.arg(format!("https://www.mystream.com/{}", self.room));
let out = cmd.output()?;
Ok(String::from_utf8(out.stdout)?.trim().to_string())
}
fn set_room(&mut self, room: &str) {
self.room = room.to_owned();
}
}
Then register in src/plugins/live.rs:
"MyStream" => Ok(Box::new(MyStream {
room: cfg.mystream.room.clone(),
client: client.clone(),
})),
Using the Library Programmatically
use bilistream::config::load_config;
use bilistream::plugins::select_live;
use std::path::Path;
#[tokio::main]
async fn main() {
let cfg = load_config(Path::new("./config.yaml")).unwrap();
let live = select_live(cfg.clone()).await.unwrap();
println!("{} is live? {}", live.room(), live.get_status().await.unwrap());
}
Sending Manual Notifications
use bilistream::push::send_gotify_notification;
use bilistream::config::GotifyConfig;
#[tokio::main]
async fn main() {
let cfg = GotifyConfig {
url: "https://push.example.com".to_string(),
token: "YOUR_TOKEN".to_string(),
};
send_gotify_notification(&cfg, "Stream started", "Bilistream").await;
}
Summary
- The bilistream project structure follows a standard Cargo layout with clear separation between configuration, core logic, and platform-specific plugins.
- src/main.rs orchestrates the polling loop and ffmpeg invocation, while src/config.rs handles type-safe YAML deserialization.
- The Live trait in src/plugins/live.rs abstracts platform differences, enabling support for YouTube and Twitch through interchangeable implementations.
- External dependencies include
yt-dlpfor stream URL extraction andffmpegfor RTMP forwarding. - The architecture supports containerized deployment via included Docker configurations.
Frequently Asked Questions
What is the purpose of the Live trait in the bilistream architecture?
The Live trait defines a common interface for all streaming platforms, specifying four methods: get_status, room, get_real_m3u8_url, and set_room. This abstraction allows main.rs to handle YouTube, Twitch, and future platforms polymorphically without modification to the core polling logic. According to the source code in src/plugins/live.rs, this design isolates platform-specific networking and parsing code behind a unified async interface.
How does bilistream load and validate configuration?
Configuration management occurs in src/config.rs, which defines structs like Config, BiliLive, and GotifyConfig. The load_config function uses serde_yaml to deserialize config.yaml into these strongly-typed structures at startup. This approach ensures that missing or malformed configuration fields are caught immediately when the application initializes, preventing runtime errors during stream forwarding operations.
Which external tools are required to run bilistream?
The application depends on ffmpeg for RTMP stream forwarding and yt-dlp for extracting direct media URLs from YouTube and Twitch. These are not Rust crate dependencies but external binaries that must be present in the system PATH. The ffmpeg wrapper in src/main.rs builds command lines that combine the source m3u8 URL (from yt-dlp) with the BiliBili RTMP endpoint credentials.
How can I add support for a new streaming platform?
To add a new platform, create a new file in src/plugins/ (e.g., mystream.rs) and implement the Live trait for your platform-specific struct. You must implement the four required trait methods to check stream status and retrieve media URLs. Finally, register your implementation in the select_live factory function within src/plugins/live.rs by matching against a new platform identifier in the configuration file.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →