Overview
The StreamingServer model manages communication with Stremio’s streaming server for torrent streaming, device casting, transcoding, and network configuration.
Structure
pub struct StreamingServer {
pub selected : Selected ,
pub settings : Loadable < Settings , EnvError >,
pub base_url : Option < Url >,
pub remote_url : Option < Url >,
pub playback_devices : Loadable < Vec < PlaybackDevice >, EnvError >,
pub network_info : Loadable < NetworkInfo , EnvError >,
pub device_info : Loadable < DeviceInfo , EnvError >,
pub torrent : Option <( InfoHash , Loadable < ResourcePath , EnvError >)>,
pub statistics : Option < Loadable < Statistics , EnvError >>,
}
Selected
Tracks current server connection:
pub struct Selected {
pub transport_url : Url ,
pub statistics : Option < StatisticsRequest >,
}
Settings
Server configuration:
pub struct Settings {
pub cache_size : Option < f64 >,
pub bt_max_connections : u64 ,
pub bt_handshake_timeout : u64 ,
pub bt_request_timeout : u64 ,
pub bt_download_speed_soft_limit : f64 ,
pub bt_download_speed_hard_limit : f64 ,
pub bt_min_peers_for_stable : u64 ,
pub remote_https : Option < String >,
pub proxy_streams_enabled : bool ,
pub transcode_profile : Option < String >,
}
Update Settings
Msg :: Action ( Action :: StreamingServer (
ActionStreamingServer :: UpdateSettings ( settings )
))
Validate Settings
Ensures settings are valid and server is ready
Send to Server
POSTs settings to /settings endpoint
Update Remote URL
If remote_https is set, fetches HTTPS endpoint
Implementation:
fn set_settings<E: Env + 'static>(url: &Url, settings: &Settings) -> Effect {
let body = Body {
cache_size: settings.cache_size.to_owned(),
bt_max_connections: settings.bt_max_connections,
bt_handshake_timeout: settings.bt_handshake_timeout,
bt_request_timeout: settings.bt_request_timeout,
bt_download_speed_soft_limit: settings.bt_download_speed_soft_limit,
bt_download_speed_hard_limit: settings.bt_download_speed_hard_limit,
bt_min_peers_for_stable: settings.bt_min_peers_for_stable,
remote_https: settings.remote_https.to_owned(),
proxy_streams_enabled: settings.proxy_streams_enabled,
transcode_profile: settings.transcode_profile.to_owned(),
};
let endpoint = url.join("settings").expect("url builder failed");
let request = Request::post(endpoint.as_str())
.header(http::header::CONTENT_TYPE, "application/json")
.body(body)
.expect("request builder failed");
EffectFuture::Concurrent(
E::fetch::<_, SuccessResponse>(request)
.map(move |result| {
Msg::Internal(Internal::StreamingServerUpdateSettingsResult(
url.clone(),
result,
))
})
)
}
Torrent Management
Create Torrent from Magnet
Msg :: Action ( Action :: StreamingServer (
ActionStreamingServer :: CreateTorrent (
CreateTorrentArgs :: Magnet ( magnet_url )
)
))
Parse Magnet
Extracts info hash and tracker list
Create on Server
Sends magnet to streaming server
Generate Resource Path
Creates bt:{infohash} path for playback
Magnet Parsing:
fn parse_magnet(magnet: &Url) -> Result<(InfoHash, Vec<String>), MagnetError> {
let (magnet, hash_vec) = decode_btih(magnet.as_str())?;
let mut hash: [u8; 20] = [0_u8; 20];
if hash_vec.len() != 20 {
return Err(MagnetError::NotAMagnetURL);
}
hash.copy_from_slice(&hash_vec[..20]);
let info_hash = InfoHash::new(hash);
let announce = magnet.trackers();
Ok((info_hash, announce.to_vec()))
}
fn decode_btih(magnet_uri: &str) -> Result<(Magnet, Vec<u8>), Box<dyn std::error::Error>> {
let magnet = Magnet::new(magnet_uri)?;
let hash_str = magnet.hash().ok_or("No hash found")?;
let decoded = match hash_str.len() {
32 | 52 => data_encoding::BASE32.decode(hash_str.as_bytes())?,
40 | 64 if hash_str.chars().all(|c| c.is_ascii_hexdigit()) => {
hex::decode(hash_str.as_bytes())?
}
_ => return Err(format!("Unrecognized hash format: {}", hash_str).into()),
};
Ok((magnet, decoded))
}
Create Torrent from File
Msg :: Action ( Action :: StreamingServer (
ActionStreamingServer :: CreateTorrent (
CreateTorrentArgs :: File ( torrent_bytes )
)
))
Torrent Parsing:
fn parse_torrent(torrent: &[u8]) -> Result<(InfoHash, Vec<String>), serde_bencode::Error> {
#[derive(Deserialize)]
struct TorrentFile {
info: serde_bencode::value::Value,
#[serde(default)]
announce: Option<String>,
#[serde(default, rename = "announce-list")]
announce_list: Option<Vec<Vec<String>>>,
}
let torrent_file = serde_bencode::from_bytes::<TorrentFile>(torrent)?;
let info_bytes = serde_bencode::to_bytes(&torrent_file.info)?;
let mut hasher = Sha1::new();
hasher.update(info_bytes);
let info_hash = InfoHash::new(hasher.finalize().into());
let mut announce = vec![];
if let Some(a) = torrent_file.announce {
announce.push(a);
}
if let Some(lists) = torrent_file.announce_list {
for list in lists {
announce.extend(list);
}
}
announce.dedup();
Ok((info_hash, announce))
}
Torrent State
After successful creation:
self . torrent = Some (( info_hash , Loadable :: Ready ( ResourcePath {
resource : "meta" . to_owned (),
r#type : "other" . to_owned (),
id : format! ( "bt:{}" , info_hash ),
extra : vec! [],
})));
Statistics
Monitor torrent download progress:
pub struct StatisticsRequest {
pub info_hash : String ,
pub file_idx : Option < usize >,
}
pub struct Statistics {
pub info_hash : String ,
pub downloaded : u64 ,
pub uploaded : u64 ,
pub download_speed : f64 ,
pub upload_speed : f64 ,
pub peers : u64 ,
pub seeders : u64 ,
}
Get Statistics
Msg :: Action ( Action :: StreamingServer (
ActionStreamingServer :: GetStatistics ( StatisticsRequest {
info_hash : "abc123..." . to_string (),
file_idx : Some ( 0 ),
})
))
Implementation:
fn get_torrent_statistics<E: Env + 'static>(
url: &Url,
request: &StatisticsRequest,
) -> Effect {
let fetch_fut = async move {
let req = TorrentStatisticsRequest {
server_url: url.clone(),
request: request.clone(),
};
let statistics: Option<Statistics> = E::fetch(req.into()).await?;
Ok(statistics)
};
EffectFuture::Concurrent(
fetch_fut
.map(move |result| {
Msg::Internal(Internal::StreamingServerStatisticsResult(
(url.clone(), request.clone()),
result,
))
})
)
}
Statistics return None when torrent is fully downloaded, signaling completion.
Device Casting
Playback Devices
pub struct PlaybackDevice {
pub id : String ,
pub name : String ,
pub r#type : String , // "chromecast", "dlna", "airplay", etc.
}
Loaded on initialization from /casting endpoint.
Play on Device
pub struct PlayOnDeviceArgs {
pub device : String , // Device ID
pub source : String , // Playback URL
pub time : Option < u64 >, // Start time in ms
}
Msg :: Action ( Action :: StreamingServer (
ActionStreamingServer :: PlayOnDevice ( PlayOnDeviceArgs {
device : "chromecast-living-room" . to_string (),
source : "http://localhost:11470/stream.mp4" . to_string (),
time : Some ( 120000 ), // Start at 2 minutes
})
))
Validation:
match Url::parse(&args.source).is_ok() {
true => match &mut self.playback_devices {
Loadable::Ready(devices) => {
let device_exists = devices
.iter()
.any(|d| d.id == args.device);
if device_exists {
Effects::one(play_on_device::<E>(&self.selected.transport_url, args))
} else {
Effects::none().unchanged()
}
}
_ => Effects::none().unchanged(),
},
_ => Effects::none().unchanged(),
}
pub struct NetworkInfo {
pub local_ip : String ,
pub public_ip : String ,
pub port : u16 ,
}
pub struct DeviceInfo {
pub name : String ,
pub os : String ,
pub version : String ,
}
Both loaded on initialization.
Remote HTTPS
For remote access with HTTPS:
Configure Remote IP
Set settings.remote_https to public IP address
Get HTTPS Endpoint
Calls /get-https?authKey=...&ipAddress=...
Update Remote URL
Sets remote_url to HTTPS domain
fn get_https_endpoint<E: Env + 'static>(
url: &Url,
auth_key: &AuthKey,
ip_address: &String,
) -> Effect {
let endpoint = url.join(&format!(
"/get-https?authKey={}&ipAddress={}",
auth_key, ip_address,
)).expect("url builder failed");
let request = Request::get(endpoint.as_str())
.body(())
.expect("request builder failed");
EffectFuture::Concurrent(
E::fetch::<_, GetHTTPSResponse>(request)
.map(move |result| {
Msg::Internal(Internal::StreamingServerGetHTTPSResult(
url.clone(),
result,
))
})
)
}
fn update_remote_url<E: Env + 'static>(
remote_url: &mut Option<Url>,
selected: &Selected,
settings: &Settings,
ctx: &Ctx,
) -> Effects {
match (settings.remote_https.as_ref(), ctx.profile.auth_key()) {
(Some(ip_address), Some(auth_key)) if !ip_address.is_empty() => {
Effects::one(get_https_endpoint::<E>(
&selected.transport_url,
auth_key,
ip_address,
))
}
_ => eq_update(remote_url, None),
}
}
Initialization
On model creation:
impl StreamingServer {
pub fn new<E: Env + 'static>(profile: &Profile) -> (Self, Effects) {
let effects = Effects::many(vec![
get_settings::<E>(&profile.settings.streaming_server_url),
get_playback_devices::<E>(&profile.settings.streaming_server_url),
get_network_info::<E>(&profile.settings.streaming_server_url),
get_device_info::<E>(&profile.settings.streaming_server_url),
]);
(
Self {
selected: Selected {
transport_url: profile.settings.streaming_server_url.to_owned(),
statistics: None,
},
settings: Loadable::Loading,
base_url: None,
remote_url: None,
playback_devices: Loadable::Loading,
network_info: Loadable::Loading,
device_info: Loadable::Loading,
torrent: None,
statistics: None,
},
effects.unchanged(),
)
}
}
Reload
Refresh all server data:
Msg :: Action ( Action :: StreamingServer ( ActionStreamingServer :: Reload ))
Clears current state and re-fetches settings, devices, and network info.
Usage Example
use stremio_core :: models :: streaming_server :: StreamingServer ;
use stremio_core :: runtime :: msg :: { Action , ActionStreamingServer , CreateTorrentArgs };
// Initialize
let ( server , effects ) = StreamingServer :: new :: < MyEnv >( & ctx . profile);
// Create torrent from magnet
let magnet = Url :: parse (
"magnet:?xt=urn:btih:abc123...&tr=udp://tracker.example.com:80"
) . unwrap ();
runtime . dispatch ( Msg :: Action (
Action :: StreamingServer (
ActionStreamingServer :: CreateTorrent (
CreateTorrentArgs :: Magnet ( magnet )
)
)
));
// Wait for torrent to be created
// server.torrent will contain (InfoHash, ResourcePath)
// Monitor download progress
if let Some (( info_hash , _ )) = & server . torrent {
runtime . dispatch ( Msg :: Action (
Action :: StreamingServer (
ActionStreamingServer :: GetStatistics ( StatisticsRequest {
info_hash : info_hash . to_string (),
file_idx : Some ( 0 ),
})
)
));
}
// Play on Chromecast
if let Some ( chromecast ) = server . playback_devices
. as_ref ()
. and_then ( | devices | devices . iter () . find ( | d | d . r# type == "chromecast" ))
{
runtime . dispatch ( Msg :: Action (
Action :: StreamingServer (
ActionStreamingServer :: PlayOnDevice ( PlayOnDeviceArgs {
device : chromecast . id . clone (),
source : "http://localhost:11470/stream.mp4" . to_string (),
time : None ,
})
)
));
}
Best Practices
Statistics should be polled periodically (e.g., every 1-2 seconds) during active downloads. When statistics return None, the download is complete.
When profile.settings.streaming_server_url changes, the StreamingServer model automatically reloads all data from the new server.
If any initial request fails (settings, devices, network), all loadables are set to error state. Use Reload action to retry.