# How the Bilistream Project Is Structured: A Rust RTMP Forwarding Architecture

> Explore the Rust RTMP forwarding architecture of the Bilistream project, featuring modular configs, trait-based plugins, and a polling loop for seamless YouTube/Twitch stream forwarding to Bilibili.

- Repository: [InitCool/bilistream](https://github.com/limitcool/bilistream)
- Tags: architecture
- Published: 2026-03-06

---

**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`](https://github.com/limitcool/bilistream/blob/main/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`](https://github.com/limitcool/bilistream/blob/main/docker-compose.yaml), while [`README.md`](https://github.com/limitcool/bilistream/blob/main/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`](https://github.com/limitcool/bilistream/blob/main/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`](https://github.com/limitcool/bilistream/blob/main/config.yaml) using `serde_yaml`, providing type-safe access to settings throughout the application.

### src/push.rs: Notification System

[`src/push.rs`](https://github.com/limitcool/bilistream/blob/main/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`](https://github.com/limitcool/bilistream/blob/main/src/plugins/mod.rs) re-exports concrete implementations for clean imports.

### src/plugins/live.rs: The Core Abstraction

[`src/plugins/live.rs`](https://github.com/limitcool/bilistream/blob/main/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 live
- `room()` – Returns the room identifier as a string slice
- `get_real_m3u8_url()` – Fetches the actual media URL for ffmpeg consumption
- `set_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`](https://github.com/limitcool/bilistream/blob/main/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:

1. **Initialization** – [`main.rs`](https://github.com/limitcool/bilistream/blob/main/main.rs) sets up tracing and loads configuration from [`config.yaml`](https://github.com/limitcool/bilistream/blob/main/config.yaml)
2. **Platform Selection** – `select_live` in [`live.rs`](https://github.com/limitcool/bilistream/blob/main/live.rs) constructs the appropriate struct (`Youtube::new`, `Twitch::new`, etc.)
3. **Polling Loop** – The endless loop in [`main.rs`](https://github.com/limitcool/bilistream/blob/main/main.rs) performs 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
4. **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`:

```rust
// 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`](https://github.com/limitcool/bilistream/blob/main/src/plugins/live.rs):

```rust
"MyStream" => Ok(Box::new(MyStream {
    room: cfg.mystream.room.clone(),
    client: client.clone(),
})),

```

### Using the Library Programmatically

```rust
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

```rust
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-dlp` for stream URL extraction and `ffmpeg` for 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`](https://github.com/limitcool/bilistream/blob/main/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`](https://github.com/limitcool/bilistream/blob/main/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`](https://github.com/limitcool/bilistream/blob/main/src/config.rs), which defines structs like `Config`, `BiliLive`, and `GotifyConfig`. The `load_config` function uses `serde_yaml` to deserialize [`config.yaml`](https://github.com/limitcool/bilistream/blob/main/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`](https://github.com/limitcool/bilistream/blob/main/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`](https://github.com/limitcool/bilistream/blob/main/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`](https://github.com/limitcool/bilistream/blob/main/src/plugins/live.rs) by matching against a new platform identifier in the configuration file.