fcast_sender_sdk/
lib.rs

1//! # FCast Sender SDK
2//!
3//! An all in one SDK for casting media to [FCast], [Chromecast] and [Google
4//! Cast] receiver devices.
5//!
6//! ## Supported languages
7//!
8//! + Rust
9//! + Kotlin
10//! + Swift
11//!
12//! ## Features
13//!
14//! + Automatic discovery of devices on the network via [mDNS]
15//! + HTTP file server for easy casting of local media files
16//!
17//! ## Example usage
18//!
19//! ```no_run
20//! use std::sync::Arc;
21//!
22//! use fcast_sender_sdk::context::CastContext;
23//! use fcast_sender_sdk::device::{
24//!     ApplicationInfo, DeviceConnectionState, DeviceEventHandler, DeviceInfo, KeyEvent,
25//!     MediaEvent, LoadRequest, PlaybackState, ProtocolType, Source,
26//! };
27//! use fcast_sender_sdk::{DeviceDiscovererEventHandler, IpAddr};
28//!
29//! struct DevEventHandler {}
30//!
31//! impl DeviceEventHandler for DevEventHandler {
32//!     fn connection_state_changed(&self, state: DeviceConnectionState) {
33//!         println!("Connection state changed: {state:?}");
34//!     }
35//!
36//!     fn volume_changed(&self, volume: f64) {
37//!         println!("Volume changed: {volume}");
38//!     }
39//!
40//!     fn time_changed(&self, time: f64) {
41//!         println!("Time changed: {time}");
42//!     }
43//!
44//!     fn playback_state_changed(&self, state: PlaybackState) {
45//!         println!("Playback state changed: {state:?}");
46//!     }
47//!
48//!     fn duration_changed(&self, duration: f64) {
49//!         println!("Duration changed: {duration}");
50//!     }
51//!
52//!     fn speed_changed(&self, speed: f64) {
53//!         println!("Speed changed: {speed}");
54//!     }
55//!
56//!     fn source_changed(&self, source: Source) {
57//!         println!("Source changed: {source:?}");
58//!     }
59//!
60//!     fn key_event(&self, event: KeyEvent) {
61//!         println!("Key event: {event:?}");
62//!     }
63//!
64//!     fn media_event(&self, event: MediaEvent) {
65//!         println!("Media event: {event:?}");
66//!     }
67//!
68//!     fn playback_error(&self, message: String) {
69//!         println!("Playback error: {message}");
70//!     }
71//! }
72//!
73//! struct DiscovererEventHandler {}
74//!
75//! impl DeviceDiscovererEventHandler for DiscovererEventHandler {
76//!     fn device_available(&self, device_info: DeviceInfo) {
77//!         println!("Device available: {device_info:?}");
78//!     }
79//!
80//!     fn device_removed(&self, device_name: String) {
81//!         println!("Device removed: {device_name}");
82//!     }
83//!
84//!     fn device_changed(&self, device_info: DeviceInfo) {
85//!         println!("Device changed: {device_info:?}");
86//!     }
87//! }
88//!
89//! let ctx = CastContext::new().unwrap();
90//!
91//! ctx.start_discovery(Arc::new(DiscovererEventHandler {}));
92//!
93//! let dev = ctx.create_device_from_info(DeviceInfo {
94//!     name: "FCast device".to_owned(),
95//!     protocol: ProtocolType::FCast,
96//!     addresses: vec![IpAddr::v4(127, 0, 0, 1)],
97//!     port: 46899,
98//! });
99//!
100//! dev.connect(None, Arc::new(DevEventHandler {}), 1000)
101//!     .unwrap();
102//!
103//! dev.load(LoadRequest::Video {
104//!     content_type: "video/mp4".to_string(),
105//!     url: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
106//!         .to_string(),
107//!     resume_position: 0.0,
108//!     speed: None,
109//!     volume: None,
110//!     metadata: None,
111//!     request_headers: None,
112//! })
113//! .unwrap();
114//! ```
115//!
116//! [FCast]: https://fcast.org/
117//! [Chromecast]: https://en.wikipedia.org/wiki/Chromecast
118//! [Google Cast]: https://www.android.com/better-together/#cast
119//! [mDNS]: https://en.wikipedia.org/wiki/Multicast_DNS
120
121#[cfg(feature = "chromecast")]
122pub mod chromecast;
123#[cfg(any(feature = "http-file-server", any_protocol))]
124pub mod context;
125#[cfg(all(any_protocol, feature = "discovery"))]
126pub mod discovery;
127#[cfg(feature = "fcast")]
128pub mod fcast;
129#[cfg(feature = "chromecast")]
130pub(crate) mod googlecast_protocol;
131#[cfg(feature = "http-file-server")]
132pub(crate) mod http;
133pub(crate) mod utils;
134
135#[cfg(feature = "http-file-server")]
136pub mod file_server;
137
138/// Event handler for device discovery.
139#[cfg(all(any_protocol, feature = "discovery_types"))]
140#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))]
141pub trait DeviceDiscovererEventHandler: Send + Sync {
142    /// Called when a device is found.
143    fn device_available(&self, device_info: device::DeviceInfo);
144    /// Called when a device is removed or lost.
145    fn device_removed(&self, device_name: String);
146    /// Called when a device has changed.
147    ///
148    /// The `name` field of `device_info` will correspond to a device announced
149    /// from `device_available`.
150    fn device_changed(&self, device_info: device::DeviceInfo);
151}
152
153#[cfg(all(feature = "discovery", any_protocol))]
154use std::future::Future;
155
156#[cfg(any(feature = "http-file-server", any_protocol))]
157use tokio::runtime;
158#[cfg(any_protocol)]
159pub mod device;
160#[cfg(any_protocol)]
161use std::str::FromStr;
162
163#[cfg(feature = "uniffi")]
164uniffi::setup_scaffolding!();
165
166#[cfg(any(feature = "discovery", feature = "http-file-server", any_protocol))]
167#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
168#[cfg_attr(feature = "uniffi", uniffi(flat_error))]
169#[derive(thiserror::Error, Debug)]
170pub enum AsyncRuntimeError {
171    #[error("failed to build")]
172    FailedToBuild(#[from] std::io::Error),
173}
174
175#[cfg(any(feature = "http-file-server", any_protocol))]
176pub(crate) enum AsyncRuntime {
177    Handle(runtime::Handle),
178    Runtime(runtime::Runtime),
179}
180
181#[cfg(any(feature = "http-file-server", any_protocol))]
182impl AsyncRuntime {
183    pub fn new(threads: Option<usize>, name: &str) -> Result<Self, AsyncRuntimeError> {
184        Ok(match runtime::Handle::try_current() {
185            Ok(handle) => Self::Handle(handle),
186            Err(_) => Self::Runtime({
187                if let Some(threads) = threads {
188                    runtime::Builder::new_multi_thread()
189                        .worker_threads(threads)
190                        .enable_all()
191                        .thread_name(name)
192                        .build()?
193                } else {
194                    runtime::Builder::new_multi_thread()
195                        .enable_all()
196                        .thread_name(name)
197                        .build()?
198                }
199            }),
200        })
201    }
202
203    #[cfg(all(feature = "discovery", any_protocol))]
204    pub fn spawn<F>(&self, future: F) -> tokio::task::JoinHandle<F::Output>
205    where
206        F: Future + Send + 'static,
207        F::Output: Send + 'static,
208    {
209        match self {
210            AsyncRuntime::Handle(h) => h.spawn(future),
211            AsyncRuntime::Runtime(rt) => rt.spawn(future),
212        }
213    }
214
215    pub fn handle(&self) -> runtime::Handle {
216        match self {
217            AsyncRuntime::Handle(handle) => handle.clone(),
218            AsyncRuntime::Runtime(runtime) => runtime.handle().clone(),
219        }
220    }
221}
222
223// UniFFI does not support std::net::IpAddr
224#[cfg(any_protocol)]
225#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
226#[derive(Copy, Clone, Debug, PartialEq, Eq)]
227pub enum IpAddr {
228    V4 {
229        o1: u8,
230        o2: u8,
231        o3: u8,
232        o4: u8,
233    },
234    V6 {
235        // UniFFI will not accept [u8; 16] here...
236        o1: u8,
237        o2: u8,
238        o3: u8,
239        o4: u8,
240        o5: u8,
241        o6: u8,
242        o7: u8,
243        o8: u8,
244        o9: u8,
245        o10: u8,
246        o11: u8,
247        o12: u8,
248        o13: u8,
249        o14: u8,
250        o15: u8,
251        o16: u8,
252        scope_id: u32,
253    },
254}
255
256#[cfg(any_protocol)]
257impl IpAddr {
258    pub fn v4(o1: u8, o2: u8, o3: u8, o4: u8) -> Self {
259        Self::V4 { o1, o2, o3, o4 }
260    }
261}
262
263#[cfg(any_protocol)]
264#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
265#[cfg_attr(feature = "uniffi", uniffi(flat_error))]
266#[derive(thiserror::Error, Debug)]
267pub enum ParseIpAddrError {
268    #[error("failed to parse address")]
269    FailedToParse(#[from] std::net::AddrParseError),
270}
271
272#[allow(dead_code)]
273#[cfg(any_protocol)]
274#[cfg_attr(feature = "uniffi", uniffi::export)]
275fn try_ip_addr_from_str(s: &str) -> Result<IpAddr, ParseIpAddrError> {
276    Ok(IpAddr::from(&std::net::IpAddr::from_str(
277        s.trim_matches(['[', ']']),
278    )?))
279}
280
281#[allow(dead_code)]
282#[cfg(any_protocol)]
283#[cfg_attr(feature = "uniffi", uniffi::export)]
284pub fn url_format_ip_addr(addr: &IpAddr) -> String {
285    match addr {
286        IpAddr::V4 { o1, o2, o3, o4 } => format!("{o1}.{o2}.{o3}.{o4}"),
287        IpAddr::V6 {
288            o1,
289            o2,
290            o3,
291            o4,
292            o5,
293            o6,
294            o7,
295            o8,
296            o9,
297            o10,
298            o11,
299            o12,
300            o13,
301            o14,
302            o15,
303            o16,
304            scope_id,
305        } => {
306            let addr = std::net::Ipv6Addr::from_bits(u128::from_be_bytes([
307                *o1, *o2, *o3, *o4, *o5, *o6, *o7, *o8, *o9, *o10, *o11, *o12, *o13, *o14, *o15,
308                *o16,
309            ]));
310            format!("[{addr}%{scope_id}]")
311        }
312    }
313}
314
315#[allow(dead_code)]
316#[cfg(any_protocol)]
317#[cfg_attr(feature = "uniffi", uniffi::export)]
318fn octets_from_ip_addr(addr: &IpAddr) -> Vec<u8> {
319    match addr {
320        IpAddr::V4 { o1, o2, o3, o4 } => vec![*o1, *o2, *o3, *o4],
321        IpAddr::V6 {
322            o1,
323            o2,
324            o3,
325            o4,
326            o5,
327            o6,
328            o7,
329            o8,
330            o9,
331            o10,
332            o11,
333            o12,
334            o13,
335            o14,
336            o15,
337            o16,
338            ..
339        } => {
340            vec![
341                *o1, *o2, *o3, *o4, *o5, *o6, *o7, *o8, *o9, *o10, *o11, *o12, *o13, *o14, *o15,
342                *o16,
343            ]
344        }
345    }
346}
347
348#[cfg(any_protocol)]
349impl From<&std::net::IpAddr> for IpAddr {
350    fn from(value: &std::net::IpAddr) -> Self {
351        match value {
352            std::net::IpAddr::V4(v4) => {
353                let octets = v4.octets();
354                Self::V4 {
355                    o1: octets[0],
356                    o2: octets[1],
357                    o3: octets[2],
358                    o4: octets[3],
359                }
360            }
361            std::net::IpAddr::V6(v6) => {
362                let octets = v6.octets();
363                Self::V6 {
364                    o1: octets[0],
365                    o2: octets[1],
366                    o3: octets[2],
367                    o4: octets[3],
368                    o5: octets[4],
369                    o6: octets[5],
370                    o7: octets[6],
371                    o8: octets[7],
372                    o9: octets[8],
373                    o10: octets[9],
374                    o11: octets[10],
375                    o12: octets[11],
376                    o13: octets[12],
377                    o14: octets[13],
378                    o15: octets[14],
379                    o16: octets[15],
380                    scope_id: 0,
381                }
382            }
383        }
384    }
385}
386
387#[cfg(any_protocol)]
388impl From<&IpAddr> for std::net::IpAddr {
389    fn from(value: &IpAddr) -> Self {
390        match value {
391            IpAddr::V4 { o1, o2, o3, o4 } => {
392                std::net::IpAddr::V4(std::net::Ipv4Addr::new(*o1, *o2, *o3, *o4))
393            }
394            IpAddr::V6 {
395                o1,
396                o2,
397                o3,
398                o4,
399                o5,
400                o6,
401                o7,
402                o8,
403                o9,
404                o10,
405                o11,
406                o12,
407                o13,
408                o14,
409                o15,
410                o16,
411                ..
412            } => std::net::IpAddr::V6(std::net::Ipv6Addr::from_bits(u128::from_be_bytes([
413                *o1, *o2, *o3, *o4, *o5, *o6, *o7, *o8, *o9, *o10, *o11, *o12, *o13, *o14, *o15,
414                *o16,
415            ]))),
416        }
417    }
418}
419
420#[cfg(any_protocol)]
421impl From<std::net::IpAddr> for IpAddr {
422    fn from(value: std::net::IpAddr) -> Self {
423        Self::from(&value)
424    }
425}
426
427#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
428#[cfg(all(
429    any(target_os = "android", target_os = "ios", feature = "_uniffi_csharp"),
430    feature = "logging"
431))]
432#[derive(Clone, Debug, PartialEq, Eq)]
433pub enum LogLevelFilter {
434    Debug,
435    Info,
436}
437
438#[cfg(all(
439    any(target_os = "android", target_os = "ios", feature = "_uniffi_csharp"),
440    feature = "logging"
441))]
442impl LogLevelFilter {
443    pub fn to_log_compat(&self) -> log::LevelFilter {
444        match self {
445            LogLevelFilter::Debug => log::LevelFilter::Debug,
446            LogLevelFilter::Info => log::LevelFilter::Debug,
447        }
448    }
449}
450
451#[cfg(all(target_os = "android", feature = "logging"))]
452#[cfg_attr(feature = "uniffi", uniffi::export)]
453pub fn init_logger(level_filter: LogLevelFilter) {
454    log_panics::init();
455    android_logger::init_once(
456        android_logger::Config::default().with_max_level(level_filter.to_log_compat()),
457    );
458}
459
460#[cfg(all(target_os = "ios", feature = "logging"))]
461#[cfg_attr(feature = "uniffi", uniffi::export)]
462pub fn init_logger(level_filter: LogLevelFilter) {
463    env_logger::Builder::new()
464        .filter(None, level_filter.to_log_compat())
465        .init();
466}
467
468#[cfg(all(feature = "_uniffi_csharp", feature = "logging"))]
469#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
470pub enum LogLevel {
471    Error,
472    Warn,
473    Info,
474    Debug,
475    Trace,
476}
477
478#[cfg(all(feature = "_uniffi_csharp", feature = "logging"))]
479#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))]
480pub trait LogHandler: Send + Sync {
481    fn log(&self, level: LogLevel, tag: String, message: String);
482}
483
484#[cfg(all(feature = "_uniffi_csharp", feature = "logging"))]
485pub struct CustomLogger {
486    handler: std::sync::Arc<dyn LogHandler>,
487}
488
489#[cfg(all(feature = "_uniffi_csharp", feature = "logging"))]
490impl CustomLogger {
491    pub fn init(handler: std::sync::Arc<dyn LogHandler>) -> anyhow::Result<()> {
492        log::set_max_level(log::LevelFilter::Debug);
493        Ok(log::set_boxed_logger(Box::new(Self { handler }))?)
494    }
495}
496
497#[cfg(all(feature = "_uniffi_csharp", feature = "logging"))]
498impl log::Log for CustomLogger {
499    fn enabled(&self, _metadata: &log::Metadata) -> bool {
500        true
501    }
502
503    fn log(&self, record: &log::Record<'_>) {
504        self.handler.log(
505            match record.level() {
506                log::Level::Error => LogLevel::Error,
507                log::Level::Warn => LogLevel::Warn,
508                log::Level::Info => LogLevel::Info,
509                log::Level::Debug => LogLevel::Debug,
510                log::Level::Trace => LogLevel::Trace,
511            },
512            record.module_path().unwrap_or("n/a").to_string(),
513            record.args().to_string(),
514        );
515    }
516
517    fn flush(&self) {}
518}
519
520#[cfg(all(feature = "_uniffi_csharp", feature = "logging"))]
521#[cfg_attr(feature = "uniffi", uniffi::export)]
522pub fn init_custom_logger(handler: std::sync::Arc<dyn LogHandler>) {
523    let _ = CustomLogger::init(handler);
524}
525
526#[cfg(test)]
527mod tests {
528    use crate::AsyncRuntime;
529
530    #[tokio::test]
531    async fn async_runtime_spawn() {
532        let rt = AsyncRuntime::new(Some(1), "test-runtime").unwrap();
533        let jh = rt.spawn(async {
534            async fn test() -> u8 {
535                0
536            }
537            test().await
538        });
539        assert_eq!(jh.await.unwrap(), 0u8);
540    }
541}