Add a ArylicTCPRenderer

This commit is contained in:
2025-11-30 23:28:30 +01:00
parent 0e70a91ff9
commit 14f6b0b379
18 changed files with 942 additions and 182 deletions

View File

@@ -0,0 +1,119 @@
use std::io;
use std::thread;
use std::time::Duration;
use pmocontrol::{
ControlPoint, DeviceRegistryRead, MusicRenderer, PlaybackPosition, PlaybackPositionInfo,
PlaybackState, PlaybackStatus, TransportControl, VolumeControl,
};
fn main() -> io::Result<()> {
let _ = tracing_subscriber::fmt::try_init();
println!("Starting PMOMusic Arylic TCP demo...");
let cp = ControlPoint::spawn(5)?;
println!("Waiting 5 seconds for SSDP discovery...");
thread::sleep(Duration::from_secs(5));
let registry = cp.registry();
let renderers = {
let reg = registry.read().unwrap();
reg.list_renderers()
};
if renderers.is_empty() {
println!("No renderers discovered.");
return Ok(());
}
println!("Discovered renderers:");
for (idx, info) in renderers.iter().enumerate() {
println!(
" [{}] {} | model={} | Arylic TCP={} | LinkPlay HTTP={}",
idx,
info.friendly_name,
info.model_name,
info.capabilities.has_arylic_tcp,
info.capabilities.has_linkplay_http
);
}
let arylic_renderers: Vec<MusicRenderer> = {
let reg = registry.read().unwrap();
renderers
.iter()
.filter(|info| info.capabilities.has_arylic_tcp)
.filter_map(|info| MusicRenderer::from_registry_info(info.clone(), &reg))
.collect()
};
if arylic_renderers.is_empty() {
println!("\nNo Arylic TCP-capable renderers detected.");
return Ok(());
}
println!("\nArylic TCP renderer status:");
for renderer in arylic_renderers {
println!("- {} ({})", renderer.friendly_name(), renderer.id().0);
match renderer.playback_state() {
Ok(state) => println!(" state: {}", format_playback_state(&state)),
Err(err) => println!(" state: error: {}", err),
}
match renderer.playback_position() {
Ok(pos) => println!(" position: {}", format_position(&pos)),
Err(err) => println!(" position: error: {}", err),
}
match renderer.volume() {
Ok(vol) => println!(" volume: {}", vol),
Err(err) => println!(" volume: error: {}", err),
}
match renderer.mute() {
Ok(mute) => println!(" mute: {}", mute),
Err(err) => println!(" mute: error: {}", err),
}
println!(" attempting pause/play test...");
if let Err(err) = renderer.pause() {
println!(" pause error: {}", err);
} else {
thread::sleep(Duration::from_millis(500));
println!(" pause OK");
}
if let Err(err) = renderer.play() {
println!(" play error: {}", err);
} else {
println!(" play OK");
}
println!();
}
Ok(())
}
fn format_playback_state(state: &PlaybackState) -> String {
match state {
PlaybackState::Stopped => "Stopped".to_string(),
PlaybackState::Playing => "Playing".to_string(),
PlaybackState::Paused => "Paused".to_string(),
PlaybackState::Transitioning => "Transitioning".to_string(),
PlaybackState::NoMedia => "NoMedia".to_string(),
PlaybackState::Unknown(raw) => format!("Unknown({})", raw),
}
}
fn format_position(pos: &PlaybackPositionInfo) -> String {
let rel = pos.rel_time.as_deref().unwrap_or("-");
let dur = pos.track_duration.as_deref().unwrap_or("-");
let track = pos
.track
.map(|t| t.to_string())
.unwrap_or_else(|| "-".to_string());
format!("track={} rel_time={} duration={}", track, rel, dur)
}

View File

@@ -152,17 +152,12 @@ fn format_playback_state(state: &PlaybackState) -> String {
/// Affichage lisible d'un PlaybackPositionInfo.
fn format_position(pos: &PlaybackPositionInfo) -> String {
let track = pos.track.map(|t| t.to_string()).unwrap_or_else(|| "-".to_string());
let rel = pos
.rel_time
.as_deref()
.unwrap_or("-")
.to_string();
let dur = pos
.track_duration
.as_deref()
.unwrap_or("-")
.to_string();
let track = pos
.track
.map(|t| t.to_string())
.unwrap_or_else(|| "-".to_string());
let rel = pos.rel_time.as_deref().unwrap_or("-").to_string();
let dur = pos.track_duration.as_deref().unwrap_or("-").to_string();
format!("track={} rel_time={} duration={}", track, rel, dur)
}

View File

@@ -23,8 +23,7 @@ use std::thread;
use std::time::Duration;
// Default URI if none is provided on the CLI.
const DEFAULT_TEST_URI: &str =
"https://audio-fb.radioparadise.com/chan/1/x/1117/4/g/1117-3.flac";
const DEFAULT_TEST_URI: &str = "https://audio-fb.radioparadise.com/chan/1/x/1117/4/g/1117-3.flac";
// Extra wait after play_uri() so slow renderers (e.g. Arylic H50) have time
// to prefetch and actually start playback.
@@ -103,48 +102,42 @@ fn main() -> Result<()> {
if let Some(upnp) = renderer.as_upnp() {
println!(
" [UPnP] AVTransport control URL : {}",
upnp
.info
upnp.info
.avtransport_control_url
.as_deref()
.unwrap_or("<none>")
);
println!(
" [UPnP] AVTransport service type: {}",
upnp
.info
upnp.info
.avtransport_service_type
.as_deref()
.unwrap_or("<none>")
);
println!(
" [UPnP] RenderingControl control URL : {}",
upnp
.info
upnp.info
.rendering_control_control_url
.as_deref()
.unwrap_or("<none>")
);
println!(
" [UPnP] RenderingControl service type: {}",
upnp
.info
upnp.info
.rendering_control_service_type
.as_deref()
.unwrap_or("<none>")
);
println!(
" [UPnP] ConnectionManager control URL : {}",
upnp
.info
upnp.info
.connection_manager_control_url
.as_deref()
.unwrap_or("<none>")
);
println!(
" [UPnP] ConnectionManager service type: {}",
upnp
.info
upnp.info
.connection_manager_service_type
.as_deref()
.unwrap_or("<none>")
@@ -223,6 +216,7 @@ fn print_capabilities(prefix: &str, caps: &RendererCapabilities, proto: &Rendere
println!("{prefix} RendControl : {}", caps.has_rendering_control);
println!("{prefix} ConnManager : {}", caps.has_connection_manager);
println!("{prefix} LinkPlay HTTP : {}", caps.has_linkplay_http);
println!("{prefix} Arylic TCP : {}", caps.has_arylic_tcp);
println!("{prefix} OH Playlist : {}", caps.has_oh_playlist);
println!("{prefix} OH Volume : {}", caps.has_oh_volume);
println!("{prefix} OH Info : {}", caps.has_oh_info);
@@ -234,6 +228,8 @@ fn print_backend(prefix: &str, renderer: &MusicRenderer) {
let backend = match renderer {
MusicRenderer::Upnp(_) => "UpnpRenderer (UPnP AV / DLNA)",
MusicRenderer::LinkPlay(_) => "LinkPlayRenderer (LinkPlay HTTP)",
MusicRenderer::ArylicTcp(_) => "ArylicTcpRenderer (ARylic TCP Protocol)",
MusicRenderer::HybridUpnpArylic{..} => "Hybrid UpnpArylicRenderer (UPnP AV / DLNA + ARylic TCP Protocol)",
};
println!("{prefix}Backend : {backend}");
}
@@ -293,12 +289,7 @@ fn dump_renderer_state(renderer: &MusicRenderer, label: &str) -> Result<()> {
Ok(())
}
fn progress_monitor(
renderer: &MusicRenderer,
label: &str,
iterations: usize,
interval_secs: u64,
) {
fn progress_monitor(renderer: &MusicRenderer, label: &str, iterations: usize, interval_secs: u64) {
println!(
"\n[{label}] polling playback state/position {} times (every {} s)...",
iterations, interval_secs
@@ -343,14 +334,16 @@ fn volume_demo(renderer: &MusicRenderer) -> Result<()> {
thread::sleep(Duration::from_millis(500));
// Small volume bump if possible
let new_volume = original.saturating_add(5).min(u16::MAX);
let new_volume = original.saturating_add(10).min(u16::MAX);
println!(" Bumping volume to : {}", new_volume);
renderer.set_volume(new_volume)?;
thread::sleep(Duration::from_secs(1));
println!(" Volume after bump : {}", renderer.volume()?);
thread::sleep(Duration::from_secs(5));
println!(" Restoring original volume: {}", original);
println!(" Volume after reset : {}", renderer.volume()?);
renderer.set_volume(original)?;
thread::sleep(Duration::from_secs(5));
Ok(())
}

View File

@@ -1,4 +1,4 @@
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use pmocontrol::{ControlPoint, DeviceRegistryRead, RendererInfo};
use std::env;
use std::io::{self, Write};
@@ -66,11 +66,7 @@ fn main() -> Result<()> {
for (idx, r) in renderers.iter().enumerate() {
println!(
" [{}] {} | model={} | udn={} | location={}",
idx,
r.friendly_name,
r.model_name,
r.udn,
r.location,
idx, r.friendly_name, r.model_name, r.udn, r.location,
);
}