use oxicast::{CastApp, CastClient, CastEvent, MediaInfo, PlayerState, StreamType};
use std::time::Duration;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "oxicast=debug,device_test=info".parse().unwrap()),
)
.init();
let ip = std::env::args().nth(1).unwrap_or_else(|| {
eprintln!("Usage: cargo run --example device_test --all-features -- <CHROMECAST_IP>");
eprintln!();
eprintln!("Trying mDNS discovery...");
String::new()
});
let (ip, port) = if ip.is_empty() {
match discover_device().await {
Some(d) => {
println!("Found: {} at {}:{}", d.name, d.ip, d.port);
(d.ip.to_string(), d.port)
}
None => {
eprintln!("No devices found. Pass IP manually.");
std::process::exit(1);
}
}
} else {
(ip, 8009)
};
let mut passed = 0u32;
let mut failed = 0u32;
macro_rules! step {
($name:expr, $body:expr) => {{
print!(" {} ... ", $name);
match $body {
Ok(val) => {
println!("OK");
passed += 1;
Some(val)
}
Err(e) => {
println!("FAIL: {e}");
failed += 1;
None
}
}
}};
}
println!("\n=== oxicast device test against {ip}:{port} ===\n");
let client = match step!("Connect", CastClient::connect(&ip, port).await) {
Some(c) => c,
None => {
println!("\nCannot continue without connection.");
std::process::exit(1);
}
};
assert!(client.is_connected(), "should be connected");
if let Some(status) = step!("Get receiver status", client.receiver_status().await) {
println!(" Volume: {:.0}%, muted: {}", status.volume.level * 100.0, status.volume.muted);
println!(" Apps: {}", status.applications.len());
}
let _app = step!(
"Launch Default Media Receiver",
client.launch_app(&CastApp::DefaultMediaReceiver).await
);
let media_arg = std::env::args().nth(2).unwrap_or_else(|| "hls".into());
let media = match media_arg.as_str() {
"hls" => {
println!(" Using: Mux HLS test stream (H264/AAC, up to 1080p)");
MediaInfo::new(
"https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8",
"application/x-mpegURL",
)
.stream_type(StreamType::Buffered)
}
"mp4" => {
println!(" Using: TU Ilmenau Big Buck Bunny MP4 (H264, 1080p)");
MediaInfo::new(
"https://avtshare01.rz.tu-ilmenau.de/avt-vqdb-uhd-1/test_1/segments/bigbuck_bunny_8bit_15000kbps_1080p_60.0fps_h264.mp4",
"video/mp4",
)
.stream_type(StreamType::Buffered)
}
url => {
println!(" Using: custom URL {url}");
let content_type =
if url.contains(".m3u8") { "application/x-mpegURL" } else { "video/mp4" };
MediaInfo::new(url, content_type).stream_type(StreamType::Buffered)
}
};
let load_result =
step!("Load media (Big Buck Bunny)", client.load_media(&media, true, 0.0, None).await);
if let Some(ref status) = load_result {
println!(" State: {:?}, Duration: {:?}s", status.player_state, status.duration);
}
if load_result.is_some() {
print!(" Wait for PLAYING ... ");
let playing = wait_for_state(&client, PlayerState::Playing, Duration::from_secs(15)).await;
if playing {
println!("OK");
passed += 1;
} else {
println!("FAIL: timeout");
failed += 1;
}
}
step!("Pause", client.pause().await);
tokio::time::sleep(Duration::from_secs(1)).await;
step!("Play (resume)", client.play().await);
tokio::time::sleep(Duration::from_secs(1)).await;
if let Some(status) = step!("Seek to 60s", client.seek(60.0).await) {
println!(" Position after seek: {:.1}s", status.current_time);
}
if let Some(Some(s)) = step!("Get media status", client.media_status().await) {
println!(" State: {:?}, Position: {:.1}s", s.player_state, s.current_time);
}
step!("Set volume to 30%", client.set_volume(0.3).await);
tokio::time::sleep(Duration::from_millis(500)).await;
step!("Restore volume to 80%", client.set_volume(0.8).await);
{
print!(" Watch channels have data ... ");
let media_rx = client.watch_media_status();
let recv_rx = client.watch_receiver_status();
if media_rx.borrow().is_some() && recv_rx.borrow().is_some() {
println!("OK");
passed += 1;
} else {
println!(
"FAIL (media={}, receiver={})",
media_rx.borrow().is_some(),
recv_rx.borrow().is_some()
);
failed += 1;
}
}
step!("Stop media", client.stop_media().await);
tokio::time::sleep(Duration::from_millis(500)).await;
step!("Disconnect", client.disconnect().await);
assert!(!client.is_connected(), "should be disconnected");
println!("\n=== Results: {passed} passed, {failed} failed ===\n");
if failed > 0 {
std::process::exit(1);
}
}
async fn wait_for_state(client: &CastClient, target: PlayerState, timeout: Duration) -> bool {
let deadline = tokio::time::Instant::now() + timeout;
loop {
tokio::select! {
Some(event) = client.next_event() => {
if let CastEvent::MediaStatusChanged(s) = event {
if s.player_state == target {
return true;
}
}
}
_ = tokio::time::sleep_until(deadline) => {
return false;
}
}
}
}
async fn discover_device() -> Option<oxicast::DeviceInfo> {
let devices = oxicast::discovery::discover_devices(Duration::from_secs(3)).await.ok()?;
devices.into_iter().next()
}