vmix_http/
client.rs

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        // Add Function parameter
48        url.query_pairs_mut().append_pair("Function", function);
49
50        // Add all parameters from HashMap
51        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        // HTTP API doesn't have direct TALLY command, so we need to derive it from XML state
77        // This simulates the TCP TALLY response by analyzing the XML state
78        let vmix_state = self.get_xml_state().await?;
79        let mut tally_map = HashMap::new();
80
81        // Parse active and preview inputs to determine tally states
82        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        // Populate tally data for all inputs (up to 1000 as per vMix spec)
86        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
125// Helper function for common vMix functions
126impl 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", &params).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", &params).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", &params).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", &params).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
185// HttpVmixClientはマルチスレッド環境で安全に使用できる
186unsafe 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}