2025-11-30 10:54:01 +01:00
|
|
|
// examples/music_renderer_demo.rs
|
|
|
|
|
//
|
|
|
|
|
// End-to-end demo using the `MusicRenderer` façade:
|
|
|
|
|
// - SSDP discovery via `ControlPoint`
|
|
|
|
|
// - selection of a renderer
|
|
|
|
|
// - generic `TransportControl` + `VolumeControl` + `PlaybackStatus` + `PlaybackPosition`
|
|
|
|
|
// - optional UPnP-specific inspection (TransportInfo, ConnectionManager)
|
|
|
|
|
//
|
|
|
|
|
// Build and run (from pmocontrol crate root):
|
|
|
|
|
// cargo run --example music_renderer_demo -- [index] [uri]
|
|
|
|
|
//
|
|
|
|
|
// index: optional 0-based renderer index (default: 0)
|
|
|
|
|
// uri : optional URI to play (default: Radio Paradise FLAC)
|
2025-11-30 09:26:33 +01:00
|
|
|
|
2025-11-30 10:54:01 +01:00
|
|
|
use anyhow::Result;
|
|
|
|
|
use pmocontrol::PlaybackPosition;
|
2025-12-05 21:59:43 +01:00
|
|
|
use pmocontrol::model::RendererInfo;
|
|
|
|
|
use pmocontrol::openhome_client::{OhInfoClient, OhPlaylistClient, OhTimeClient};
|
2025-11-30 10:54:01 +01:00
|
|
|
use pmocontrol::{
|
|
|
|
|
ControlPoint, MusicRenderer, PlaybackState, PlaybackStatus, RendererCapabilities,
|
|
|
|
|
RendererProtocol, TransportControl, VolumeControl,
|
|
|
|
|
};
|
2025-11-30 09:26:33 +01:00
|
|
|
use std::env;
|
|
|
|
|
use std::thread;
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
2025-11-30 10:54:01 +01:00
|
|
|
// Default URI if none is provided on the CLI.
|
2025-11-30 23:28:30 +01:00
|
|
|
const DEFAULT_TEST_URI: &str = "https://audio-fb.radioparadise.com/chan/1/x/1117/4/g/1117-3.flac";
|
2025-11-30 09:26:33 +01:00
|
|
|
|
2025-11-30 10:54:01 +01:00
|
|
|
// Extra wait after play_uri() so slow renderers (e.g. Arylic H50) have time
|
|
|
|
|
// to prefetch and actually start playback.
|
|
|
|
|
const AFTER_PLAY_WAIT_SECS: u64 = 15;
|
2025-11-30 09:26:33 +01:00
|
|
|
|
2025-11-30 10:54:01 +01:00
|
|
|
fn main() -> Result<()> {
|
|
|
|
|
// 1. Start control point and let discovery run a bit
|
2025-11-30 09:26:33 +01:00
|
|
|
let cp = ControlPoint::spawn(5)?;
|
|
|
|
|
thread::sleep(Duration::from_secs(5));
|
|
|
|
|
|
2025-11-30 10:54:01 +01:00
|
|
|
// 2. Snapshot of logical music renderers
|
|
|
|
|
let mut renderers: Vec<MusicRenderer> = cp.list_music_renderers();
|
2025-11-30 09:26:33 +01:00
|
|
|
|
2025-11-30 10:54:01 +01:00
|
|
|
// Filter out the in-dev PMOMusic renderer (if present)
|
2025-11-30 09:26:33 +01:00
|
|
|
renderers.retain(|r| {
|
2025-11-30 10:54:01 +01:00
|
|
|
let name = r.friendly_name().to_ascii_lowercase();
|
|
|
|
|
!name.contains("pmomusic audio renderer")
|
2025-11-30 09:26:33 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if renderers.is_empty() {
|
2025-11-30 10:54:01 +01:00
|
|
|
println!("No valid music renderer discovered.");
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. CLI args: [index] [uri]
|
|
|
|
|
let args: Vec<String> = env::args().skip(1).collect();
|
|
|
|
|
|
|
|
|
|
let selected_index: usize = if !args.is_empty() {
|
|
|
|
|
args[0].parse().unwrap_or(0)
|
|
|
|
|
} else {
|
|
|
|
|
0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let maybe_uri: Option<String> = if args.len() >= 2 {
|
|
|
|
|
Some(args[1].clone())
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Bounds check
|
|
|
|
|
if selected_index >= renderers.len() {
|
|
|
|
|
println!(
|
|
|
|
|
"Renderer index {} out of range ({} available).",
|
|
|
|
|
selected_index,
|
|
|
|
|
renderers.len()
|
|
|
|
|
);
|
2025-11-30 09:26:33 +01:00
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-30 10:54:01 +01:00
|
|
|
// 4. List renderers with basic info
|
|
|
|
|
println!("Discovered MusicRenderers:");
|
2025-11-30 09:26:33 +01:00
|
|
|
for (idx, r) in renderers.iter().enumerate() {
|
2025-11-30 10:54:01 +01:00
|
|
|
let info = r.info();
|
2025-11-30 09:26:33 +01:00
|
|
|
println!(
|
2025-11-30 10:54:01 +01:00
|
|
|
" [{}] {} | model={} | udn={} | location={}",
|
|
|
|
|
idx, info.friendly_name, info.model_name, info.udn, info.location
|
2025-11-30 09:26:33 +01:00
|
|
|
);
|
2025-11-30 14:43:29 +01:00
|
|
|
print_backend(" ", r);
|
2025-11-30 10:54:01 +01:00
|
|
|
print_capabilities(" ", &info.capabilities, &info.protocol);
|
2025-12-05 21:59:43 +01:00
|
|
|
print_openhome_details(" ", info);
|
2025-11-30 09:26:33 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-30 10:54:01 +01:00
|
|
|
// 5. Select renderer
|
|
|
|
|
let renderer = &renderers[selected_index];
|
|
|
|
|
let info = renderer.info();
|
|
|
|
|
|
|
|
|
|
println!("\nSelected renderer (index {}):", selected_index);
|
|
|
|
|
println!(" Name : {}", info.friendly_name);
|
|
|
|
|
println!(" Model : {}", info.model_name);
|
|
|
|
|
println!(" Manufacturer: {}", info.manufacturer);
|
|
|
|
|
println!(" UDN : {}", info.udn);
|
|
|
|
|
println!(" Location : {}", info.location);
|
|
|
|
|
println!(" Protocol : {:?}", info.protocol);
|
2025-11-30 14:43:29 +01:00
|
|
|
print_backend(" ", renderer);
|
2025-11-30 10:54:01 +01:00
|
|
|
print_capabilities(" ", &info.capabilities, &info.protocol);
|
2025-12-05 21:59:43 +01:00
|
|
|
print_openhome_details(" ", info);
|
2025-11-30 10:54:01 +01:00
|
|
|
|
|
|
|
|
if let Some(upnp) = renderer.as_upnp() {
|
|
|
|
|
println!(
|
|
|
|
|
" [UPnP] AVTransport control URL : {}",
|
2025-11-30 23:28:30 +01:00
|
|
|
upnp.info
|
2025-11-30 10:54:01 +01:00
|
|
|
.avtransport_control_url
|
|
|
|
|
.as_deref()
|
|
|
|
|
.unwrap_or("<none>")
|
|
|
|
|
);
|
|
|
|
|
println!(
|
|
|
|
|
" [UPnP] AVTransport service type: {}",
|
2025-11-30 23:28:30 +01:00
|
|
|
upnp.info
|
2025-11-30 10:54:01 +01:00
|
|
|
.avtransport_service_type
|
|
|
|
|
.as_deref()
|
|
|
|
|
.unwrap_or("<none>")
|
|
|
|
|
);
|
|
|
|
|
println!(
|
|
|
|
|
" [UPnP] RenderingControl control URL : {}",
|
2025-11-30 23:28:30 +01:00
|
|
|
upnp.info
|
2025-11-30 10:54:01 +01:00
|
|
|
.rendering_control_control_url
|
|
|
|
|
.as_deref()
|
|
|
|
|
.unwrap_or("<none>")
|
|
|
|
|
);
|
|
|
|
|
println!(
|
|
|
|
|
" [UPnP] RenderingControl service type: {}",
|
2025-11-30 23:28:30 +01:00
|
|
|
upnp.info
|
2025-11-30 10:54:01 +01:00
|
|
|
.rendering_control_service_type
|
|
|
|
|
.as_deref()
|
|
|
|
|
.unwrap_or("<none>")
|
|
|
|
|
);
|
|
|
|
|
println!(
|
|
|
|
|
" [UPnP] ConnectionManager control URL : {}",
|
2025-11-30 23:28:30 +01:00
|
|
|
upnp.info
|
2025-11-30 10:54:01 +01:00
|
|
|
.connection_manager_control_url
|
|
|
|
|
.as_deref()
|
|
|
|
|
.unwrap_or("<none>")
|
|
|
|
|
);
|
|
|
|
|
println!(
|
|
|
|
|
" [UPnP] ConnectionManager service type: {}",
|
2025-11-30 23:28:30 +01:00
|
|
|
upnp.info
|
2025-11-30 10:54:01 +01:00
|
|
|
.connection_manager_service_type
|
|
|
|
|
.as_deref()
|
|
|
|
|
.unwrap_or("<none>")
|
|
|
|
|
);
|
2025-11-30 09:26:33 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-30 10:54:01 +01:00
|
|
|
// 6. Initial state dump (generic + UPnP-specific)
|
|
|
|
|
dump_renderer_state(renderer, "Initial state")?;
|
2025-11-30 09:26:33 +01:00
|
|
|
|
2025-11-30 10:54:01 +01:00
|
|
|
// 7. Play URI via logical façade
|
|
|
|
|
let uri = maybe_uri.unwrap_or_else(|| DEFAULT_TEST_URI.to_string());
|
|
|
|
|
let meta = ""; // or full DIDL-Lite
|
|
|
|
|
|
|
|
|
|
println!("\nCalling play_uri on music renderer...");
|
|
|
|
|
if let Err(e) = renderer.play_uri(&uri, meta) {
|
|
|
|
|
println!(" play_uri failed: {e}");
|
2025-11-30 09:26:33 +01:00
|
|
|
} else {
|
2025-11-30 10:54:01 +01:00
|
|
|
println!(" play_uri: OK");
|
2025-11-30 09:26:33 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-30 10:54:01 +01:00
|
|
|
println!(
|
|
|
|
|
"Waiting {}s to let the renderer prefetch and start playback...",
|
|
|
|
|
AFTER_PLAY_WAIT_SECS
|
|
|
|
|
);
|
|
|
|
|
thread::sleep(Duration::from_secs(AFTER_PLAY_WAIT_SECS));
|
|
|
|
|
dump_renderer_state(renderer, "After play_uri")?;
|
|
|
|
|
|
|
|
|
|
// 8. Short progress polling using the PlaybackPosition façade
|
|
|
|
|
progress_monitor(renderer, "Progress while playing", 8, 3);
|
|
|
|
|
|
|
|
|
|
// 9. Seek (if supported)
|
|
|
|
|
println!("\nCalling seek_rel_time(\"00:01:00\")...");
|
|
|
|
|
if let Err(e) = renderer.seek_rel_time("00:01:00") {
|
|
|
|
|
println!(" seek_rel_time failed: {e}");
|
|
|
|
|
} else {
|
|
|
|
|
println!(" seek_rel_time: OK");
|
|
|
|
|
thread::sleep(Duration::from_secs(2));
|
|
|
|
|
dump_renderer_state(renderer, "After seek_rel_time")?;
|
|
|
|
|
|
|
|
|
|
// 9b. Second progress loop after seek: verify RelTime advances
|
|
|
|
|
progress_monitor(renderer, "Progress after seek", 8, 3);
|
2025-11-30 09:26:33 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-30 10:54:01 +01:00
|
|
|
// 10. Volume dance (if supported)
|
|
|
|
|
println!("\nProbing volume control via music façade...");
|
|
|
|
|
if let Err(e) = volume_demo(renderer) {
|
|
|
|
|
println!(" Volume control not fully usable: {e}");
|
|
|
|
|
}
|
2025-11-30 09:26:33 +01:00
|
|
|
|
2025-11-30 10:54:01 +01:00
|
|
|
// 11. Pause then Stop
|
|
|
|
|
println!("\nCalling pause() via music façade...");
|
|
|
|
|
if let Err(e) = renderer.pause() {
|
|
|
|
|
println!(" pause failed: {e}");
|
|
|
|
|
} else {
|
|
|
|
|
println!(" pause: OK");
|
|
|
|
|
thread::sleep(Duration::from_secs(2));
|
|
|
|
|
dump_renderer_state(renderer, "After pause")?;
|
2025-11-30 09:26:33 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-30 10:54:01 +01:00
|
|
|
println!("\nCalling stop() via music façade...");
|
|
|
|
|
if let Err(e) = renderer.stop() {
|
|
|
|
|
println!(" stop failed: {e}");
|
|
|
|
|
} else {
|
|
|
|
|
println!(" stop: OK");
|
|
|
|
|
dump_renderer_state(renderer, "After stop")?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
println!("\nDone.");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn print_capabilities(prefix: &str, caps: &RendererCapabilities, proto: &RendererProtocol) {
|
|
|
|
|
println!("{prefix}Capabilities:");
|
2025-11-30 14:43:29 +01:00
|
|
|
println!("{prefix} Protocol : {:?}", proto);
|
|
|
|
|
println!("{prefix} AVTransport : {}", caps.has_avtransport);
|
|
|
|
|
println!("{prefix} RendControl : {}", caps.has_rendering_control);
|
|
|
|
|
println!("{prefix} ConnManager : {}", caps.has_connection_manager);
|
|
|
|
|
println!("{prefix} LinkPlay HTTP : {}", caps.has_linkplay_http);
|
2025-11-30 23:28:30 +01:00
|
|
|
println!("{prefix} Arylic TCP : {}", caps.has_arylic_tcp);
|
2025-11-30 14:43:29 +01:00
|
|
|
println!("{prefix} OH Playlist : {}", caps.has_oh_playlist);
|
|
|
|
|
println!("{prefix} OH Volume : {}", caps.has_oh_volume);
|
|
|
|
|
println!("{prefix} OH Info : {}", caps.has_oh_info);
|
|
|
|
|
println!("{prefix} OH Time : {}", caps.has_oh_time);
|
|
|
|
|
println!("{prefix} OH Radio : {}", caps.has_oh_radio);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-05 21:59:43 +01:00
|
|
|
fn print_openhome_details(prefix: &str, info: &RendererInfo) {
|
2025-12-05 19:01:33 +01:00
|
|
|
if !info.capabilities.has_oh_playlist
|
|
|
|
|
&& !info.capabilities.has_oh_info
|
|
|
|
|
&& !info.capabilities.has_oh_time
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-05 21:59:43 +01:00
|
|
|
let playlist_client = build_playlist_client(info);
|
|
|
|
|
let info_client = build_info_client(info);
|
|
|
|
|
let time_client = build_time_client(info);
|
2025-12-05 19:01:33 +01:00
|
|
|
|
|
|
|
|
if playlist_client.is_none() && info_client.is_none() && time_client.is_none() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
println!("{prefix}OpenHome:");
|
|
|
|
|
|
|
|
|
|
if let Some(client) = playlist_client {
|
|
|
|
|
match client.id_array() {
|
|
|
|
|
Ok(ids) => println!("{prefix} Playlist tracks : {}", ids.len()),
|
|
|
|
|
Err(err) => println!("{prefix} Playlist tracks : <error {err}>"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(client) = info_client {
|
|
|
|
|
match client.transport_state() {
|
|
|
|
|
Ok(state) => {
|
|
|
|
|
let logical = map_openhome_state(&state);
|
2025-12-05 21:59:43 +01:00
|
|
|
println!("{prefix} Transport state : {} ({:?})", state, logical);
|
2025-12-05 19:01:33 +01:00
|
|
|
}
|
|
|
|
|
Err(err) => println!("{prefix} Transport state : <error {err}>"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(client) = time_client {
|
|
|
|
|
match client.position() {
|
|
|
|
|
Ok(pos) => println!(
|
|
|
|
|
"{prefix} Position : {}/{} (tracks={})",
|
|
|
|
|
format_seconds(pos.elapsed_secs),
|
|
|
|
|
format_seconds(pos.duration_secs),
|
|
|
|
|
pos.track_count
|
|
|
|
|
),
|
|
|
|
|
Err(err) => println!("{prefix} Position : <error {err}>"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-30 14:43:29 +01:00
|
|
|
fn print_backend(prefix: &str, renderer: &MusicRenderer) {
|
|
|
|
|
let backend = match renderer {
|
|
|
|
|
MusicRenderer::Upnp(_) => "UpnpRenderer (UPnP AV / DLNA)",
|
|
|
|
|
MusicRenderer::LinkPlay(_) => "LinkPlayRenderer (LinkPlay HTTP)",
|
2025-11-30 23:28:30 +01:00
|
|
|
MusicRenderer::ArylicTcp(_) => "ArylicTcpRenderer (ARylic TCP Protocol)",
|
2025-12-01 19:55:55 +01:00
|
|
|
MusicRenderer::HybridUpnpArylic { .. } => {
|
|
|
|
|
"Hybrid UpnpArylicRenderer (UPnP AV / DLNA + ARylic TCP Protocol)"
|
|
|
|
|
}
|
2025-12-05 21:59:43 +01:00
|
|
|
MusicRenderer::OpenHome(_) => "OpenHomeRenderer (native OpenHome stack)",
|
2025-11-30 14:43:29 +01:00
|
|
|
};
|
|
|
|
|
println!("{prefix}Backend : {backend}");
|
2025-11-30 10:54:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn dump_renderer_state(renderer: &MusicRenderer, label: &str) -> Result<()> {
|
|
|
|
|
println!("\n[{label}]");
|
|
|
|
|
|
|
|
|
|
if let Ok(state) = renderer.playback_state() {
|
|
|
|
|
println!(" Playback state (music): {:?}", state);
|
|
|
|
|
} else {
|
|
|
|
|
println!(" Playback state (music): <unavailable>");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generic volume façade
|
|
|
|
|
match renderer.volume() {
|
|
|
|
|
Ok(v) => println!(" Volume (music) : {}", v),
|
|
|
|
|
Err(e) => println!(" Volume not available: {e}"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match renderer.mute() {
|
|
|
|
|
Ok(m) => println!(" Mute : {}", m),
|
|
|
|
|
Err(e) => println!(" Mute state unknown : {e}"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Ok(pos) = renderer.playback_position() {
|
|
|
|
|
println!(" Position info:");
|
|
|
|
|
println!(" Track : {:?}", pos.track);
|
|
|
|
|
println!(" Duration : {:?}", pos.track_duration);
|
|
|
|
|
println!(" RelTime : {:?}", pos.rel_time);
|
|
|
|
|
println!(" AbsTime : {:?}", pos.abs_time);
|
|
|
|
|
} else {
|
|
|
|
|
println!(" Position info: <unavailable>");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Optional UPnP-specific TransportInfo
|
|
|
|
|
if let Some(upnp) = renderer.as_upnp() {
|
|
|
|
|
if upnp.has_avtransport() {
|
|
|
|
|
match upnp.avtransport() {
|
|
|
|
|
Ok(avt) => match avt.get_transport_info(0) {
|
|
|
|
|
Ok(info) => {
|
|
|
|
|
println!(" [UPnP] TransportInfo:");
|
|
|
|
|
println!(" State : {}", info.current_transport_state);
|
|
|
|
|
println!(" Status : {}", info.current_transport_status);
|
|
|
|
|
println!(" Speed : {}", info.current_speed);
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
println!(" [UPnP] TransportInfo unavailable: {e}");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Err(e) => println!(" [UPnP] No AVTransport client: {e}"),
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
println!(" [UPnP] AVTransport not present on this renderer.");
|
2025-11-30 09:26:33 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-30 10:54:01 +01:00
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-11-30 09:26:33 +01:00
|
|
|
|
2025-11-30 23:28:30 +01:00
|
|
|
fn progress_monitor(renderer: &MusicRenderer, label: &str, iterations: usize, interval_secs: u64) {
|
2025-11-30 10:54:01 +01:00
|
|
|
println!(
|
|
|
|
|
"\n[{label}] polling playback state/position {} times (every {} s)...",
|
|
|
|
|
iterations, interval_secs
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for i in 0..iterations {
|
|
|
|
|
if let Ok(state) = renderer.playback_state() {
|
|
|
|
|
print!(" Sample {:02}: state={:?}", i + 1, state);
|
|
|
|
|
} else {
|
|
|
|
|
print!(" Sample {:02}: state=<unavailable>", i + 1);
|
2025-11-30 09:26:33 +01:00
|
|
|
}
|
2025-11-30 10:54:01 +01:00
|
|
|
|
|
|
|
|
if let Ok(pos) = renderer.playback_position() {
|
|
|
|
|
println!(
|
|
|
|
|
" | track={:?}, rel={:?}, dur={:?}",
|
|
|
|
|
pos.track, pos.rel_time, pos.track_duration
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
println!(" | position=<unavailable>");
|
2025-11-30 09:26:33 +01:00
|
|
|
}
|
2025-11-30 10:54:01 +01:00
|
|
|
|
|
|
|
|
thread::sleep(Duration::from_secs(interval_secs));
|
2025-11-30 09:26:33 +01:00
|
|
|
}
|
2025-11-30 10:54:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn volume_demo(renderer: &MusicRenderer) -> Result<()> {
|
|
|
|
|
// Try to get current volume
|
|
|
|
|
let original = renderer.volume()?;
|
|
|
|
|
println!(" Current music volume : {}", original);
|
|
|
|
|
|
|
|
|
|
// Try mute toggle
|
|
|
|
|
let muted = renderer.mute()?;
|
|
|
|
|
println!(" Current mute state : {}", muted);
|
|
|
|
|
|
|
|
|
|
println!(" Setting mute = true...");
|
|
|
|
|
renderer.set_mute(true)?;
|
|
|
|
|
thread::sleep(Duration::from_secs(1));
|
|
|
|
|
println!(" Mute now: {}", renderer.mute()?);
|
|
|
|
|
|
|
|
|
|
println!(" Restoring mute = {}", muted);
|
|
|
|
|
renderer.set_mute(muted)?;
|
|
|
|
|
thread::sleep(Duration::from_millis(500));
|
|
|
|
|
|
|
|
|
|
// Small volume bump if possible
|
2025-11-30 23:28:30 +01:00
|
|
|
let new_volume = original.saturating_add(10).min(u16::MAX);
|
2025-11-30 10:54:01 +01:00
|
|
|
println!(" Bumping volume to : {}", new_volume);
|
|
|
|
|
renderer.set_volume(new_volume)?;
|
|
|
|
|
println!(" Volume after bump : {}", renderer.volume()?);
|
2025-11-30 23:28:30 +01:00
|
|
|
thread::sleep(Duration::from_secs(5));
|
2025-11-30 10:54:01 +01:00
|
|
|
|
|
|
|
|
println!(" Restoring original volume: {}", original);
|
2025-11-30 23:28:30 +01:00
|
|
|
println!(" Volume after reset : {}", renderer.volume()?);
|
2025-11-30 10:54:01 +01:00
|
|
|
renderer.set_volume(original)?;
|
2025-11-30 23:28:30 +01:00
|
|
|
thread::sleep(Duration::from_secs(5));
|
2025-11-30 09:26:33 +01:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-12-05 21:59:43 +01:00
|
|
|
|
|
|
|
|
fn build_playlist_client(info: &RendererInfo) -> Option<OhPlaylistClient> {
|
|
|
|
|
let control_url = info.oh_playlist_control_url.as_ref()?;
|
|
|
|
|
let service_type = info.oh_playlist_service_type.as_ref()?;
|
|
|
|
|
Some(OhPlaylistClient::new(
|
|
|
|
|
control_url.clone(),
|
|
|
|
|
service_type.clone(),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_info_client(info: &RendererInfo) -> Option<OhInfoClient> {
|
|
|
|
|
let control_url = info.oh_info_control_url.as_ref()?;
|
|
|
|
|
let service_type = info.oh_info_service_type.as_ref()?;
|
|
|
|
|
Some(OhInfoClient::new(control_url.clone(), service_type.clone()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_time_client(info: &RendererInfo) -> Option<OhTimeClient> {
|
|
|
|
|
let control_url = info.oh_time_control_url.as_ref()?;
|
|
|
|
|
let service_type = info.oh_time_service_type.as_ref()?;
|
|
|
|
|
Some(OhTimeClient::new(control_url.clone(), service_type.clone()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn map_openhome_state(raw: &str) -> PlaybackState {
|
|
|
|
|
match raw.trim().to_ascii_uppercase().as_str() {
|
|
|
|
|
"PLAYING" => PlaybackState::Playing,
|
|
|
|
|
"PAUSED" | "PAUSED_PLAYBACK" => PlaybackState::Paused,
|
|
|
|
|
"STOPPED" => PlaybackState::Stopped,
|
|
|
|
|
"BUFFERING" | "TRANSITIONING" => PlaybackState::Transitioning,
|
|
|
|
|
other => PlaybackState::Unknown(other.to_string()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn format_seconds(seconds: u32) -> String {
|
|
|
|
|
let hours = seconds / 3600;
|
|
|
|
|
let minutes = (seconds % 3600) / 60;
|
|
|
|
|
let secs = seconds % 60;
|
|
|
|
|
format!("{hours:02}:{minutes:02}:{secs:02}")
|
|
|
|
|
}
|