Skip to content

Commit 3ff95b3

Browse files
authored
Add OpenIRL stream server type (#189)
1 parent c373140 commit 3ff95b3

File tree

3 files changed

+162
-0
lines changed

3 files changed

+162
-0
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,19 @@ Note: `application`, `key` and `publisher` are optional fields. Use either `appl
648648
},
649649
```
650650

651+
---
652+
653+
### Using OpenIRL
654+
655+
```JSON
656+
"streamServer": {
657+
"type": "OpenIRL",
658+
"statsUrl": "http://hostname:8080/stats/play_38f01143fc4049c5836d7f7dcaf1a31f",
659+
},
660+
```
661+
662+
- `statsUrl`: URL to OpenIRL stats page
663+
651664
</details>
652665

653666
## Depends on

src/stream_servers/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub mod nginx;
1212
pub mod nimble;
1313
pub mod nms;
1414
pub mod obs;
15+
pub mod openirl;
1516
pub mod rist;
1617
pub mod sls;
1718
pub mod xiu;
@@ -23,6 +24,7 @@ pub use nginx::Nginx;
2324
pub use nimble::Nimble;
2425
pub use nms::NodeMediaServer;
2526
pub use obs::Obs;
27+
pub use openirl::OpenIRL;
2628
pub use rist::Rist;
2729
pub use sls::SrtLiveServer;
2830
pub use xiu::Xiu;

src/stream_servers/openirl.rs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
use async_trait::async_trait;
2+
use serde::{Deserialize, Serialize};
3+
use serde_json::Value;
4+
use tracing::{error, trace};
5+
6+
use super::{Bsl, StreamServersCommands, SwitchLogic, default_reqwest_client};
7+
use crate::switcher::{SwitchType, Triggers};
8+
9+
#[derive(Deserialize, Debug)]
10+
pub struct Stat {
11+
pub bitrate: u32,
12+
pub buffer: u32,
13+
pub dropped_pkts: u64,
14+
pub latency: u16,
15+
pub rtt: f64,
16+
pub uptime: u64,
17+
}
18+
19+
#[derive(Debug, Serialize, Deserialize)]
20+
#[serde(rename_all = "camelCase")]
21+
pub struct OpenIRL {
22+
/// URL to the OpenIRL stats page (ex; http://127.0.0.1:8080/stats/play_38f01143fc4049c5836d7f7dcaf1a31f )
23+
pub stats_url: String,
24+
25+
/// Client to make HTTP requests with
26+
#[serde(skip, default = "default_reqwest_client")]
27+
pub client: reqwest::Client,
28+
}
29+
30+
impl OpenIRL {
31+
pub async fn get_stats(&self) -> Option<Stat> {
32+
let res = match self.client.get(&self.stats_url).send().await {
33+
Ok(res) => res,
34+
Err(e) => {
35+
error!("Stats page is unreachable, {}", e);
36+
return None;
37+
}
38+
};
39+
40+
if res.status() != reqwest::StatusCode::OK {
41+
error!("Error accessing stats page ({})", self.stats_url);
42+
return None;
43+
}
44+
45+
let text = res.text().await.ok()?;
46+
let data: Value = serde_json::from_str(&text).ok()?;
47+
48+
// Check if "publisher" field exists - if not, stream is offline
49+
let publisher = match data.get("publisher") {
50+
Some(publisher) => publisher,
51+
None => {
52+
// Publisher is offline - return None (standard pattern)
53+
return None;
54+
}
55+
};
56+
57+
let stream: Stat = match serde_json::from_value(publisher.to_owned()) {
58+
Ok(stats) => stats,
59+
Err(error) => {
60+
trace!("{}", &data);
61+
error!("Error parsing stats ({}) {}", self.stats_url, error);
62+
return None;
63+
}
64+
};
65+
66+
trace!("{:#?}", stream);
67+
Some(stream)
68+
}
69+
}
70+
71+
#[async_trait]
72+
#[typetag::serde]
73+
impl SwitchLogic for OpenIRL {
74+
/// Which scene to switch to
75+
async fn switch(&self, triggers: &Triggers) -> SwitchType {
76+
let stats = match self.get_stats().await {
77+
Some(b) => b,
78+
None => return SwitchType::Offline,
79+
};
80+
81+
if let Some(offline) = triggers.offline {
82+
if stats.bitrate > 0 && stats.bitrate <= offline.into() {
83+
return SwitchType::Offline;
84+
}
85+
}
86+
87+
if let Some(rtt_offline) = triggers.rtt_offline {
88+
if stats.rtt >= rtt_offline.into() {
89+
return SwitchType::Offline;
90+
}
91+
}
92+
93+
if stats.bitrate == 0 {
94+
return SwitchType::Offline;
95+
}
96+
97+
if stats.bitrate == 1 {
98+
return SwitchType::Previous;
99+
}
100+
101+
if let Some(low) = triggers.low {
102+
if stats.bitrate <= low.into() {
103+
return SwitchType::Low;
104+
}
105+
}
106+
107+
if let Some(rtt) = triggers.rtt {
108+
if stats.rtt >= rtt.into() {
109+
return SwitchType::Low;
110+
}
111+
}
112+
113+
return SwitchType::Normal;
114+
}
115+
}
116+
117+
#[async_trait]
118+
#[typetag::serde]
119+
impl StreamServersCommands for OpenIRL {
120+
async fn bitrate(&self) -> super::Bitrate {
121+
let stats = match self.get_stats().await {
122+
Some(stats) => stats,
123+
None => return super::Bitrate { message: None },
124+
};
125+
126+
let message = format!("{} Kbps, {} ms", stats.bitrate, stats.rtt.round());
127+
super::Bitrate {
128+
message: Some(message),
129+
}
130+
}
131+
132+
async fn source_info(&self) -> Option<String> {
133+
let stats = self.get_stats().await?;
134+
135+
let bitrate = format!("{} Kbps, {} ms at {} ms latency", stats.bitrate, stats.rtt.round(), stats.latency);
136+
let dropped = format!("dropped {} packets", stats.dropped_pkts);
137+
138+
Some(format!("{} | {}", bitrate, dropped))
139+
}
140+
}
141+
142+
#[typetag::serde]
143+
impl Bsl for OpenIRL {
144+
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
145+
self
146+
}
147+
}

0 commit comments

Comments
 (0)