1use crate::traits::VmixApiClient;
2use anyhow::Result;
3use async_trait::async_trait;
4use reqwest::Client;
5use std::{collections::HashMap, net::SocketAddr, time::Duration};
6use vmix_core::Vmix;
7use vmix_tcp::{InputNumber, TallyData};
8
9#[derive(Debug, Clone)]
10pub struct HttpVmixClient {
11 base_url: String,
12 client: Client,
13}
14
15impl HttpVmixClient {
16 pub fn new(addr: SocketAddr, timeout: Duration) -> Self {
17 let client = Client::builder()
18 .timeout(timeout)
19 .build()
20 .expect("Failed to create HTTP client");
21
22 Self {
23 base_url: format!("http://{}/api", addr),
24 client,
25 }
26 }
27
28 pub fn new_with_host_port(host: &str, port: u16, timeout: Duration) -> Self {
29 let client = Client::builder()
30 .timeout(timeout)
31 .build()
32 .expect("Failed to create HTTP client");
33
34 Self {
35 base_url: format!("http://{}:{}/api", host, port),
36 client,
37 }
38 }
39
40 pub async fn execute_function(
41 &self,
42 function: &str,
43 params: &HashMap<String, String>,
44 ) -> Result<()> {
45 let mut url = reqwest::Url::parse(&self.base_url)?;
46
47 url.query_pairs_mut().append_pair("Function", function);
49
50 for (key, value) in params {
52 url.query_pairs_mut().append_pair(key, value);
53 }
54
55 let response = self.client.get(url.as_str()).send().await?;
56
57 if response.status().is_success() {
58 Ok(())
59 } else {
60 Err(anyhow::anyhow!(
61 "HTTP request failed with status: {}",
62 response.status()
63 ))
64 }
65 }
66
67 pub async fn get_xml_state(&self) -> Result<Vmix> {
68 let response = self.client.get(&self.base_url).send().await?;
69
70 let xml_text = response.text().await?;
71 let vmix_data: Vmix = vmix_core::from_str(&xml_text)?;
72 Ok(vmix_data)
73 }
74
75 pub async fn get_tally_data(&self) -> Result<HashMap<InputNumber, TallyData>> {
76 let vmix_state = self.get_xml_state().await?;
79 let mut tally_map = HashMap::new();
80
81 let active_input: InputNumber = vmix_state.active.parse().unwrap_or(0);
83 let preview_input: InputNumber = vmix_state.preview.parse().unwrap_or(0);
84
85 for input in &vmix_state.inputs.input {
87 let input_number: InputNumber = input.number.parse().unwrap_or(0);
88
89 let tally_state = if input_number == active_input {
90 TallyData::PROGRAM
91 } else if input_number == preview_input {
92 TallyData::PREVIEW
93 } else {
94 TallyData::OFF
95 };
96
97 tally_map.insert(input_number, tally_state);
98 }
99
100 Ok(tally_map)
101 }
102
103 pub async fn is_connected(&self) -> bool {
104 match self.client.get(&self.base_url).send().await {
105 Ok(response) => response.status().is_success(),
106 Err(_) => false,
107 }
108 }
109
110 pub async fn get_active_input(&self) -> Result<InputNumber> {
111 let vmix_data = self.get_xml_state().await?;
112 Ok(vmix_data.active.parse().unwrap_or(0))
113 }
114
115 pub async fn get_preview_input(&self) -> Result<InputNumber> {
116 let vmix_data = self.get_xml_state().await?;
117 Ok(vmix_data.preview.parse().unwrap_or(0))
118 }
119
120 pub fn get_base_url(&self) -> &str {
121 &self.base_url
122 }
123}
124
125impl HttpVmixClient {
127 pub async fn cut(&self) -> Result<()> {
128 self.execute_function("Cut", &HashMap::new()).await
129 }
130
131 pub async fn fade(&self, duration_ms: Option<u32>) -> Result<()> {
132 let mut params = HashMap::new();
133 if let Some(duration) = duration_ms {
134 params.insert("Duration".to_string(), duration.to_string());
135 }
136 self.execute_function("Fade", ¶ms).await
137 }
138
139 pub async fn preview_input(&self, input: InputNumber) -> Result<()> {
140 let mut params = HashMap::new();
141 params.insert("Input".to_string(), input.to_string());
142 self.execute_function("PreviewInput", ¶ms).await
143 }
144
145 pub async fn active_input(&self, input: InputNumber) -> Result<()> {
146 let mut params = HashMap::new();
147 params.insert("Input".to_string(), input.to_string());
148 self.execute_function("ActiveInput", ¶ms).await
149 }
150
151 pub async fn set_text(
152 &self,
153 input: InputNumber,
154 selected_name: &str,
155 value: &str,
156 ) -> Result<()> {
157 let mut params = HashMap::new();
158 params.insert("Input".to_string(), input.to_string());
159 params.insert("SelectedName".to_string(), selected_name.to_string());
160 params.insert("Value".to_string(), value.to_string());
161 self.execute_function("SetText", ¶ms).await
162 }
163
164 pub async fn start_recording(&self) -> Result<()> {
165 self.execute_function("StartRecording", &HashMap::new())
166 .await
167 }
168
169 pub async fn stop_recording(&self) -> Result<()> {
170 self.execute_function("StopRecording", &HashMap::new())
171 .await
172 }
173
174 pub async fn start_streaming(&self) -> Result<()> {
175 self.execute_function("StartStreaming", &HashMap::new())
176 .await
177 }
178
179 pub async fn stop_streaming(&self) -> Result<()> {
180 self.execute_function("StopStreaming", &HashMap::new())
181 .await
182 }
183}
184
185unsafe impl Send for HttpVmixClient {}
187unsafe impl Sync for HttpVmixClient {}
188
189#[async_trait]
190impl VmixApiClient for HttpVmixClient {
191 async fn execute_function(
192 &self,
193 function: &str,
194 params: &HashMap<String, String>,
195 ) -> Result<()> {
196 self.execute_function(function, params).await
197 }
198
199 async fn get_xml_state(&self) -> Result<Vmix> {
200 self.get_xml_state().await
201 }
202
203 async fn get_tally_data(&self) -> Result<HashMap<InputNumber, TallyData>> {
204 self.get_tally_data().await
205 }
206
207 async fn is_connected(&self) -> bool {
208 self.is_connected().await
209 }
210
211 async fn get_active_input(&self) -> Result<InputNumber> {
212 self.get_active_input().await
213 }
214
215 async fn get_preview_input(&self) -> Result<InputNumber> {
216 self.get_preview_input().await
217 }
218}