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 loop
  • select_live() – Factory function that instantiates the correct platform adapter
  • bili_start_live() / bili_stop_live() – Control Bilibili streaming via REST API
  • ffmpeg() – 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 containing BiliLive, Twitch, Youtube, Platform, and optional Gotify sections
  • load_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:

  • Live trait – Requires get_status(), room(), get_real_m3u8_url(), and set_room() methods
  • select_live() – Factory function that returns a boxed dyn Live based on the platform configuration 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/gql with specific SHA256 hashes
  • Twitch::get_status() – Returns true when res["data"]["user"]["stream"]["type"] == "live"
  • Twitch::get_real_m3u8_url() – Invokes yt-dlp to 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 to get_youtube_live_status() for HTML parsing
  • Youtube::get_real_m3u8_url() – Uses yt-dlp to 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:

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →