Core Modules of the Bilistream System: Architecture and Implementation Guide
Bilistream's architecture consists of seven specialized Rust modules—main.rs, config.rs, push.rs, and four plugin files—that handle configuration parsing, platform abstraction, status polling, and FFmpeg streaming to Bilibili.
Bilistream is a Rust-based automation tool that monitors live streams from YouTube, Twitch, or Bilibili preview channels and forwards them to Bilibili live rooms. Understanding the core modules of the bilistream system is essential for developers looking to extend platform support or debug streaming workflows. The codebase follows a clean modular design with clear separation between configuration management, platform-specific adapters, and the streaming pipeline.
Overview of the Bilistream Architecture
The system operates through a coordinated pipeline: configuration loading → platform selection → status polling → URL extraction → Bilibili control → FFmpeg streaming → optional notification. Each phase is handled by a dedicated module with well-defined interfaces, making the system extensible for additional platforms.
The architecture relies on the Live trait defined in src/plugins/live.rs to abstract platform differences, allowing src/main.rs to orchestrate the workflow without knowing implementation details of YouTube versus Twitch.
The Seven Core Modules
1. src/main.rs – Runtime Orchestrator
The primary entry point initializes the tracing system, loads YAML configuration, selects the appropriate platform implementation, and drives the main event loop. This module contains the FFmpeg wrapper and Bilibili start/stop logic.
Key functions include:
main()– Initializes the async runtime and enters the polling loopselect_live()– Factory function that instantiates the correct platform adapterbili_start_live()/bili_stop_live()– Control Bilibili streaming via REST APIffmpeg()– Launches external FFmpeg processes to push streams
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(fmt::layer())
.init();
let cfg = load_config(Path::new("./config.yaml")).unwrap();
let mut r = select_live(cfg.clone()).await.unwrap();
loop {
if r.get_status().await.unwrap_or(false) {
if let Some(ref gotify_config) = cfg.gotify {
send_gotify_notification(gotify_config,
&format!("{}开始直播", r.room()),
"bilistream").await;
}
// Bilibili stream handling and FFmpeg execution...
}
tokio::time::sleep(Duration::from_secs(cfg.interval)).await;
}
}
2. src/config.rs – Typed Configuration Management
This module defines the configuration schema using Serde and handles YAML deserialization. The Config struct maps directly to the configuration file structure with explicit renaming for field names.
Key components:
Config– Root struct containingBiliLive,Twitch,Youtube,Platform, and optionalGotifysectionsload_config()– Reads and parses the YAML configuration file
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
#[serde(rename = "BiliLive")] pub bililive: BiliLive,
#[serde(rename = "Twitch")] pub twitch: TwitchC,
#[serde(rename = "Interval")] pub interval: u64,
#[serde(rename = "Youtube")] pub youtube: YoutubeC,
#[serde(rename = "Platform")] pub platform: String,
}
3. src/push.rs – Gotify Notification Helper
Handles optional push notifications when streams go live. The module provides a thin wrapper around the Gotify client for alerting users to streaming events.
Key function:
send_gotify_notification()– Sends messages to a Gotify server with configurable URL and token
pub async fn send_gotify_notification(
config: &GotifyConfig,
message: &str,
title: &str,
) {
match GotifyClient::new(config.url.as_str(), &config.token) {
Ok(client) => match client.create_message(message).with_title(title).await {
Ok(_) => tracing::info!("Gotify通知发送成功"),
Err(e) => tracing::error!("Gotify通知发送失败: {}", e),
},
Err(e) => tracing::error!("Gotify客户端创建失败: {}", e),
}
}
4. src/plugins/live.rs – The Live Trait and Factory
The heart of the platform abstraction layer. This module defines the Live trait that all streaming platforms must implement, providing a uniform interface for status checking and URL extraction.
Key components:
Livetrait – Requiresget_status(),room(),get_real_m3u8_url(), andset_room()methodsselect_live()– Factory function that returns a boxeddyn Livebased on theplatformconfiguration value
#[async_trait]
pub trait Live {
async fn get_status(&self) -> Result<bool, Box<dyn Error>>;
fn room(&self) -> &str;
async fn get_real_m3u8_url(&self) -> Result<String, Box<dyn Error>>;
fn set_room(&mut self, room: &str);
}
5. src/plugins/twitch.rs – Twitch Platform Adapter
Implements the Live trait for Twitch streams. Uses the Twitch GraphQL API for status checks and yt-dlp for media URL extraction when native token-based methods are disabled.
Key implementation details:
- Status checking via
https://gql.twitch.tv/gqlwith specific SHA256 hashes Twitch::get_status()– Returnstruewhenres["data"]["user"]["stream"]["type"] == "live"Twitch::get_real_m3u8_url()– Invokesyt-dlpto extract the actual m3u8 URL
async fn get_status(&self) -> Result<bool, Box<dyn Error>> {
let j = json!({
"operationName":"StreamMetadata",
"variables":{"channelLogin": &self.room},
"extensions":{"persistedQuery":{ "version":1,
"sha256Hash":"1c719a40e481453e5c48d9bb585d971b8b372f8ebb105b17076722264dfa5b3e"}}
});
let res: serde_json::Value = self.client
.post("https://gql.twitch.tv/gql")
.header("Client-ID", "kimne78kx3ncx6brgo4mv6wki5h1ko")
.json(&j)
.send()
.await?
.json()
.await?;
Ok(res["data"]["user"]["stream"]["type"] == "live")
}
6. src/plugins/youtube.rs – YouTube Platform Adapter
Provides YouTube-specific implementations of the Live trait. Delegates status checking to HTML parsing functions and uses yt-dlp for URL extraction.
Key implementation details:
Youtube::get_status()– Delegates toget_youtube_live_status()for HTML parsingYoutube::get_real_m3u8_url()– Usesyt-dlpto obtain the m3u8 stream URL
async fn get_status(&self) -> Result<bool, Box<dyn Error>> {
get_youtube_live_status(&self.room).await
}
7. src/plugins/mod.rs and src/plugins/response.rs – Module Organization
src/plugins/mod.rs– Re-exports public symbols from the plugin submodules, providing a clean import interface (use bilistream::plugins::select_live)src/plugins/response.rs– Currently a placeholder module reserved for future response handling logic
Practical Implementation Examples
Selecting a Platform at Runtime
The select_live function in src/plugins/live.rs enables dynamic platform selection based on configuration without compile-time coupling:
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();
// Returns Box<dyn Live> - could be Twitch, Youtube, or Bilibili preview
let live = select_live(cfg.clone()).await.unwrap();
println!("Monitoring platform: {}", cfg.platform);
println!("Room/Channel ID: {}", live.room());
}
Sending Gotify Notifications
Integrate the notification module to alert external systems when streams start:
use bilistream::push::send_gotify_notification;
use bilistream::config::GotifyConfig;
#[tokio::main]
async fn main() {
let gotify = GotifyConfig {
url: "https://example.com/gotify".into(),
token: "YOUR_TOKEN".into(),
};
send_gotify_notification(&gotify, "Stream started!", "Bilistream")
.await;
}
Executing the FFmpeg Push Pipeline
The FFmpeg wrapper in src/main.rs handles the actual streaming process with automatic restart logic:
pub fn ffmpeg(rtmp_url: String, rtmp_key: String,
m3u8_url: String, ffmpeg_proxy: Option<String>) {
let cmd = format!("{}{}", rtmp_url, rtmp_key);
let mut command = Command::new("ffmpeg");
if let Some(proxy) = ffmpeg_proxy {
command.arg("-http_proxy").arg(proxy);
}
command.arg("-re")
.arg("-i").arg(m3u8_url)
.arg("-vcodec").arg("copy")
.arg("-acodec").arg("aac")
.arg("-f").arg("flv")
.arg(cmd);
// Execution and retry logic follows...
}
Summary
The core modules of the bilistream system form a cohesive pipeline for automated stream forwarding:
src/main.rsorchestrates the runtime loop, FFmpeg execution, and Bilibili API interactionssrc/config.rsprovides type-safe YAML configuration loading with Serdesrc/push.rsenables optional Gotify notifications for monitoring alertssrc/plugins/live.rsdefines theLivetrait andselect_livefactory for platform abstractionsrc/plugins/twitch.rsimplements Twitch-specific status checking via GraphQL andyt-dlpURL extractionsrc/plugins/youtube.rshandles YouTube stream detection and URL resolutionsrc/plugins/mod.rsorganizes public exports whileresponse.rsreserves extension points
This modular architecture allows developers to add new streaming platforms by implementing the Live trait without modifying the core orchestration logic in main.rs.
Frequently Asked Questions
What is the purpose of the Live trait in bilistream?
The Live trait defined in src/plugins/live.rs provides a platform-agnostic interface that abstracts the differences between YouTube, Twitch, and Bilibili preview streams. It requires four methods: get_status() for checking if a stream is live, room() for getting the channel identifier, get_real_m3u8_url() for extracting the actual stream URL, and set_room() for updating the channel. This abstraction allows src/main.rs to handle all platforms uniformly through dynamic dispatch via Box<dyn Live>.
How does bilistream handle different streaming platforms?
Bilistream uses a factory pattern implemented in the select_live() function within src/plugins/live.rs. This function inspects the platform field from the configuration and instantiates the appropriate concrete implementation—either Twitch from src/plugins/twitch.rs, Youtube from src/plugins/youtube.rs, or a Bilibili preview adapter. Each platform implements the Live trait methods differently: Twitch uses GraphQL API calls, while YouTube uses HTML parsing, but both expose the same interface to the main runtime loop.
Where is the FFmpeg streaming logic implemented?
The FFmpeg wrapper is implemented directly in src/main.rs within the ffmpeg() function. This function constructs a std::process::Command that invokes the external FFmpeg binary with arguments for RTMP pushing, including options for HTTP proxy configuration, codec copying (-vcodec copy), AAC audio encoding, and FLV formatting. The function includes recursive retry logic to restart the stream automatically if FFmpeg exits with a non-zero status code.
How is configuration loaded in the bilistream system?
Configuration loading is handled by src/config.rs, which uses the Serde library to deserialize YAML files into strongly-typed Rust structs. The load_config() function reads the file from a specified path (typically ./config.yaml) and maps it to the Config struct, which uses Serde attributes like #[serde(rename = "BiliLive")] to match YAML keys to Rust field names. This approach provides compile-time safety for configuration access throughout the application.
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 →