Refactor queue management and transport control across all renderer backends

This commit refactors queue management and transport control to use a unified approach across all renderer backends (UPnP, OpenHome, Arylic TCP, Chromecast, LinkPlay). Key changes include:

1. Introduces `RendererBackend` and `QueueTransportControl` traits to provide consistent queue access and transport control operations
2. Moves queue management from the `MusicRenderer` struct to individual backend implementations
3. Implements `play_from_queue`, `play_next`, `play_previous`, and `play_from_index` methods in all backends
4. Simplifies `MusicRenderer` methods to delegate to backend-specific implementations
5. Removes direct queue access methods from `MusicRenderer` and centralizes queue operations in backend traits
6. Updates all backend implementations (UPnP, OpenHome, Arylic TCP, Chromecast, LinkPlay) to implement the new queue transport control traits

This change provides a more consistent and maintainable way to handle queue operations across different renderer types, ensuring that queue management and playback navigation work uniformly regardless of the underlying backend.
This commit is contained in:
2026-01-11 00:05:32 +01:00
parent 7f10f55086
commit c3a9e3ea7b
8 changed files with 1192 additions and 270 deletions

View File

@@ -640,7 +640,7 @@ impl ControlPoint {
})?;
// Get queue length before clearing
let removed = renderer.get_queue().upcoming_len().unwrap_or(0);
let removed = renderer.upcoming_len().unwrap_or(0);
renderer.clear_queue()?;
@@ -697,11 +697,8 @@ impl ControlPoint {
ControlPointError::SnapshotError(format!("Renderer {} not found", renderer_id.0))
})?;
let new_len = {
let mut queue = renderer.get_queue_mut();
queue.enqueue_items(items, mode)?;
queue.upcoming_len()?
};
renderer.enqueue_items(items, mode)?;
let new_len = renderer.upcoming_len()?;
debug!(
renderer = renderer_id.0.as_str(),
@@ -789,7 +786,6 @@ impl ControlPoint {
// Get queue from renderer (works for all backends)
let queue_snapshot = renderer
.get_queue()
.queue_snapshot()
.map_err(|e| anyhow!("Failed to get queue snapshot: {}", e))?;
@@ -955,7 +951,7 @@ impl ControlPoint {
})?;
// Use generic queue access (works for all backends)
let Some((item, remaining)) = renderer.get_queue_mut().peek_current()? else {
let Some((item, remaining)) = renderer.peek_current()? else {
debug!(
renderer = renderer_id.0.as_str(),
"play_current_from_queue: queue is empty or no current item"
@@ -975,9 +971,8 @@ impl ControlPoint {
// when renderer sends Stopped event during SetAVTransportURI
renderer.set_playback_source(PlaybackSource::None);
// Start playback
let didl_metadata = playback_item_to_didl(&item);
if let Err(err) = renderer.play_uri(&item.uri, &didl_metadata) {
// Start playback using play_from_queue which preserves the queue
if let Err(err) = renderer.play_from_queue() {
error!(
renderer = renderer_id.0.as_str(),
error = %err,
@@ -1008,61 +1003,50 @@ impl ControlPoint {
ControlPointError::SnapshotError(format!("Renderer {} not found", renderer_id.0))
})?;
// Dequeue next item
let Some((item, remaining_after)) = renderer.get_queue_mut().dequeue_next()? else {
// Check if queue is empty before trying to play next
if renderer.len()? == 0 {
debug!(
renderer = renderer_id.0.as_str(),
"play_next_from_queue: queue is empty"
);
renderer.set_playback_source(PlaybackSource::None);
return Ok(());
};
debug!(
renderer = renderer_id.0.as_str(),
queue_after = remaining_after,
uri = item.uri.as_str(),
"Dequeued next playback item"
);
}
// Temporarily disable auto-advance to prevent race condition
// when renderer sends Stopped event during SetAVTransportURI
renderer.set_playback_source(PlaybackSource::None);
// Start playback
let didl_metadata = playback_item_to_didl(&item);
if let Err(err) = renderer.play_uri(&item.uri, &didl_metadata) {
// Use the backend's play_next which handles queue advancement correctly for each backend type
if let Err(err) = renderer.play_next_from_queue() {
error!(
renderer = renderer_id.0.as_str(),
error = %err,
"Failed to start playback for queued item"
"Failed to play next item from queue"
);
// Try to requeue the item
let _ = renderer
.get_queue_mut()
.enqueue_items(vec![item.clone()], EnqueueMode::InsertAfterCurrent);
renderer.set_playback_source(PlaybackSource::None);
return Err(err);
}
// Save metadata for current track
let metadata = playback_item_track_metadata(&item);
renderer.set_last_metadata(Some(metadata));
// Get current item metadata for tracking
if let Some((item, _)) = renderer.peek_current()? {
let metadata = playback_item_track_metadata(&item);
renderer.set_last_metadata(Some(metadata));
}
renderer.set_playback_source(PlaybackSource::FromQueue);
debug!(
renderer = renderer_id.0.as_str(),
queue_len = remaining_after,
"Started playback from queue"
"Playing next item from queue"
);
// Prefetch next track if supported
self.prefetch_next_track(&renderer, renderer_id);
// Emit QueueUpdated event
let queue_length = renderer.len().unwrap_or(0);
self.emit_renderer_event(RendererEvent::QueueUpdated {
id: renderer_id.clone(),
queue_length: remaining_after,
queue_length,
});
Ok(())
@@ -1076,7 +1060,7 @@ impl ControlPoint {
}
// Get the next item from the queue using peek_current
let Ok(Some((_, remaining))) = renderer.get_queue_mut().peek_current() else {
let Ok(Some((_, remaining))) = renderer.peek_current() else {
return;
};
@@ -1084,7 +1068,7 @@ impl ControlPoint {
return;
}
let queue_snapshot = match renderer.get_queue().queue_snapshot() {
let queue_snapshot = match renderer.queue_snapshot() {
Ok(snapshot) => snapshot,
Err(_) => return,
};
@@ -1119,37 +1103,26 @@ impl ControlPoint {
ControlPointError::SnapshotError(format!("Renderer {} not found", renderer_id.0))
})?;
// Set queue index
renderer.get_queue_mut().set_index(Some(index))?;
let Some((item, remaining)) = renderer.get_queue_mut().peek_current()? else {
// Check if index is valid
if index >= renderer.len()? {
debug!(
renderer = renderer_id.0.as_str(),
index, "play_queue_index: no item at index"
index, "play_queue_index: index out of bounds"
);
renderer.set_playback_source(PlaybackSource::None);
return Ok(());
};
debug!(
renderer = renderer_id.0.as_str(),
index,
queue_len = remaining + 1,
uri = item.uri.as_str(),
"Playing item at index from queue"
);
}
// Temporarily disable auto-advance to prevent race condition
renderer.set_playback_source(PlaybackSource::None);
// Start playback
let didl_metadata = playback_item_to_didl(&item);
if let Err(err) = renderer.play_uri(&item.uri, &didl_metadata) {
// Use the backend's play_from_index which handles everything correctly
if let Err(err) = renderer.play_from_index(index) {
error!(
renderer = renderer_id.0.as_str(),
index,
error = %err,
"Failed to start playback for queued item at index"
"Failed to play from queue index"
);
renderer.set_playback_source(PlaybackSource::None);
return Err(err);
@@ -1157,14 +1130,14 @@ impl ControlPoint {
info!(
renderer = renderer_id.0.as_str(),
index,
uri = item.uri.as_str(),
"Queue playback started at index"
index, "Queue playback started at index"
);
// Save metadata
let metadata = playback_item_track_metadata(&item);
renderer.set_last_metadata(Some(metadata));
// Get current item metadata for tracking
if let Some((item, _)) = renderer.peek_current()? {
let metadata = playback_item_track_metadata(&item);
renderer.set_last_metadata(Some(metadata));
}
renderer.set_playback_source(PlaybackSource::FromQueue);
Ok(())
@@ -1270,7 +1243,7 @@ impl ControlPoint {
// Clear the local queue (detach binding + clear runtime queue structure)
self.detach_playlist_binding(renderer_id, "attach_new_playlist");
renderer.get_queue_mut().clear_queue()?;
renderer.clear_queue()?;
debug!(
renderer = renderer_id.0.as_str(),

View File

@@ -1,3 +1,4 @@
use std::sync::{Arc, Mutex};
use std::time::Duration;
use serde::Deserialize;
@@ -13,10 +14,13 @@ use crate::linkplay_client::extract_linkplay_host;
use crate::model::{PlaybackState, RendererInfo};
use crate::music_renderer::RendererFromMediaRendererInfo;
use crate::music_renderer::capabilities::{
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, TransportControl, VolumeControl,
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend,
TransportControl, VolumeControl,
};
use crate::music_renderer::musicrenderer::MusicRendererBackend;
use crate::music_renderer::time_utils::{format_hhmmss, ms_to_seconds, parse_hhmmss_strict};
use crate::queue::MusicQueue;
use crate::queue::{EnqueueMode, PlaybackItem, QueueBackend, QueueSnapshot};
/// Raw response from Arylic MCU+PINFGET command
#[derive(Debug, Deserialize)]
@@ -40,6 +44,7 @@ pub struct ArylicTcpRenderer {
host: String,
port: u16,
timeout: Duration,
queue: Arc<Mutex<MusicQueue>>,
}
impl ArylicTcpRenderer {
@@ -111,10 +116,13 @@ impl RendererFromMediaRendererInfo for ArylicTcpRenderer {
))
})?;
let queue = Arc::new(Mutex::new(MusicQueue::from_renderer_info(info)?));
Ok(Self {
host,
port: ARYLIC_TCP_PORT,
timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
queue,
})
}
@@ -209,6 +217,135 @@ impl PlaybackPosition for ArylicTcpRenderer {
}
}
impl RendererBackend for ArylicTcpRenderer {
fn queue(&self) -> &Arc<Mutex<MusicQueue>> {
&self.queue
}
}
impl QueueTransportControl for ArylicTcpRenderer {
fn play_from_queue(&self) -> Result<(), ControlPointError> {
let mut queue = self.queue.lock().unwrap();
let current_index = match queue.current_index()? {
Some(idx) => idx,
None => {
if queue.len()? > 0 {
queue.set_index(Some(0))?;
0
} else {
return Err(ControlPointError::QueueError("Queue is empty".into()));
}
}
};
let item = queue
.get_item(current_index)?
.ok_or_else(|| ControlPointError::QueueError("Current item not found".into()))?;
let uri = item.uri.clone();
drop(queue);
self.play_uri(&uri, "")
}
fn play_next(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.advance()? {
return Err(ControlPointError::QueueError("No next track".into()));
}
}
self.play_from_queue()
}
fn play_previous(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.rewind()? {
return Err(ControlPointError::QueueError("No previous track".into()));
}
}
self.play_from_queue()
}
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
queue.set_index(Some(index))?;
}
self.play_from_queue()
}
}
impl QueueBackend for ArylicTcpRenderer {
fn len(&self) -> Result<usize, ControlPointError> {
self.queue.lock().unwrap().len()
}
fn track_ids(&self) -> Result<Vec<u32>, ControlPointError> {
self.queue.lock().unwrap().track_ids()
}
fn id_to_position(&self, id: u32) -> Result<usize, ControlPointError> {
self.queue.lock().unwrap().id_to_position(id)
}
fn position_to_id(&self, id: usize) -> Result<u32, ControlPointError> {
self.queue.lock().unwrap().position_to_id(id)
}
fn current_track(&self) -> Result<Option<u32>, ControlPointError> {
self.queue.lock().unwrap().current_track()
}
fn current_index(&self) -> Result<Option<usize>, ControlPointError> {
self.queue.lock().unwrap().current_index()
}
fn queue_snapshot(&self) -> Result<QueueSnapshot, ControlPointError> {
self.queue.lock().unwrap().queue_snapshot()
}
fn set_index(&mut self, index: Option<usize>) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().set_index(index)
}
fn replace_queue(
&mut self,
items: Vec<PlaybackItem>,
current_index: Option<usize>,
) -> Result<(), ControlPointError> {
self.queue
.lock()
.unwrap()
.replace_queue(items, current_index)
}
fn sync_queue(&mut self, items: Vec<PlaybackItem>) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().sync_queue(items)
}
fn get_item(&self, index: usize) -> Result<Option<PlaybackItem>, ControlPointError> {
self.queue.lock().unwrap().get_item(index)
}
fn replace_item(&mut self, index: usize, item: PlaybackItem) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().replace_item(index, item)
}
fn enqueue_items(
&mut self,
items: Vec<PlaybackItem>,
mode: EnqueueMode,
) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().enqueue_items(items, mode)
}
}
#[derive(Debug)]
struct ArylicPlaybackInfo {
status_raw: String,

View File

@@ -1,8 +1,36 @@
// pmocontrol/src/capabilities.rs
use anyhow::Result;
use std::sync::{Arc, Mutex};
use crate::queue::MusicQueue;
use crate::{errors::ControlPointError, model::PlaybackState};
/// Backend-specific operations for renderers.
///
/// This trait provides access to backend-specific resources like the queue.
pub trait RendererBackend {
/// Returns a reference to the queue associated with this backend.
fn queue(&self) -> &Arc<Mutex<MusicQueue>>;
}
/// Queue-aware transport control operations.
///
/// These operations combine queue management with transport control,
/// allowing navigation (next/previous) and track selection from the queue.
pub trait QueueTransportControl {
/// Play the next track from the queue.
fn play_next(&self) -> Result<(), ControlPointError>;
/// Play the previous track from the queue.
fn play_previous(&self) -> Result<(), ControlPointError>;
/// Play from the queue at the current index (or initialize to 0 if not set).
fn play_from_queue(&self) -> Result<(), ControlPointError>;
/// Play from a specific index in the queue.
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError>;
}
/// Logical playback position across backends.
///
/// Times peuvent être soit en secondes, soit en "HH:MM:SS" selon ce que

View File

@@ -17,16 +17,19 @@ use std::thread::JoinHandle;
use tracing::debug;
use crate::DeviceIdentity;
use crate::music_renderer::capabilities::{
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, TransportControl,
VolumeControl,
use crate::discovery::chromecast_discovery::{
extract_host_from_location, extract_port_from_location,
};
use crate::music_renderer::time_utils::{format_hhmmss_f64, parse_hhmmss_strict};
use crate::discovery::chromecast_discovery::{extract_host_from_location, extract_port_from_location};
use crate::errors::ControlPointError;
use crate::model::{PlaybackState, RendererInfo};
use crate::music_renderer::RendererFromMediaRendererInfo;
use crate::music_renderer::capabilities::{
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend,
TransportControl, VolumeControl,
};
use crate::music_renderer::musicrenderer::MusicRendererBackend;
use crate::music_renderer::time_utils::{format_hhmmss_f64, parse_hhmmss_strict};
use crate::queue::{EnqueueMode, MusicQueue, PlaybackItem, QueueBackend, QueueSnapshot};
use rust_cast::{
CastDevice, ChannelMessage,
@@ -55,6 +58,7 @@ pub struct ChromecastRenderer {
/// Handle to the active heartbeat thread, if any.
/// Wrapped in Arc<Mutex> to allow cloning and proper thread lifecycle management.
thread_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
queue: Arc<Mutex<MusicQueue>>,
}
impl std::fmt::Debug for ChromecastRenderer {
@@ -88,20 +92,22 @@ fn connect_to_device<'a>(host: &'a str, port: u16) -> Result<CastDevice<'a>, Con
// Ensure rustls crypto provider is initialized before any TLS connection
ensure_crypto_provider_initialized();
let device = CastDevice::connect_without_host_verification(host, port)
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to connect to Chromecast: {}", e)))?;
let device = CastDevice::connect_without_host_verification(host, port).map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to connect to Chromecast: {}", e))
})?;
device
.connection
.connect(DEFAULT_DESTINATION_ID.to_string())
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to connect channel: {}", e)))?;
.map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to connect channel: {}", e))
})?;
// Send initial gre to establish heartbeat communication
// This is critical per rust_caster.rs example
device
.heartbeat
.ping()
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to send initial heartbeat ping: {}", e)))?;
device.heartbeat.ping().map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to send initial heartbeat ping: {}", e))
})?;
Ok(device)
}
@@ -116,7 +122,6 @@ fn map_player_state(player_state: &CastPlayerState) -> PlaybackState {
}
}
impl RendererFromMediaRendererInfo for ChromecastRenderer {
fn from_renderer_info(info: &RendererInfo) -> Result<Self, ControlPointError> {
tracing::info!(
@@ -125,13 +130,18 @@ impl RendererFromMediaRendererInfo for ChromecastRenderer {
info.friendly_name()
);
let host = extract_host_from_location(info.location())
.ok_or_else(|| ControlPointError::ChromecastError(format!("Invalid Chromecast location: {}", info.location())))?;
let host = extract_host_from_location(info.location()).ok_or_else(|| {
ControlPointError::ChromecastError(format!(
"Invalid Chromecast location: {}",
info.location()
))
})?;
let port = extract_port_from_location(info.location()).unwrap_or(DEFAULT_CHROMECAST_PORT);
let stop_signal = Arc::new(Mutex::new(false));
let thread_handle = Arc::new(Mutex::new(None));
let queue = Arc::new(Mutex::new(MusicQueue::from_renderer_info(info)?));
tracing::info!(
"ChromecastRenderer created for {} with host={} port={}",
@@ -145,13 +155,13 @@ impl RendererFromMediaRendererInfo for ChromecastRenderer {
port,
stop_signal,
thread_handle,
queue,
})
}
fn to_backend(self) -> MusicRendererBackend {
MusicRendererBackend::Chromecast(self)
}
}
impl TransportControl for ChromecastRenderer {
@@ -308,10 +318,9 @@ impl TransportControl for ChromecastRenderer {
let device = connect_to_device(&self.host, self.port)?;
// Get receiver status to find the active app
let status = device
.receiver
.get_status()
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)))?;
let status = device.receiver.get_status().map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e))
})?;
let app = status
.applications
@@ -322,13 +331,17 @@ impl TransportControl for ChromecastRenderer {
device
.connection
.connect(app.transport_id.as_str())
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)))?;
.map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e))
})?;
// Get media status
let media_status = device
.media
.get_status(app.transport_id.as_str(), None)
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)))?;
.map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to get media status: {}", e))
})?;
let media_entry = media_status
.entries
@@ -349,10 +362,9 @@ impl TransportControl for ChromecastRenderer {
let device = connect_to_device(&self.host, self.port)?;
let status = device
.receiver
.get_status()
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)))?;
let status = device.receiver.get_status().map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e))
})?;
let app = status
.applications
@@ -362,12 +374,16 @@ impl TransportControl for ChromecastRenderer {
device
.connection
.connect(app.transport_id.as_str())
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)))?;
.map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e))
})?;
let media_status = device
.media
.get_status(app.transport_id.as_str(), None)
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)))?;
.map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to get media status: {}", e))
})?;
let media_entry = media_status
.entries
@@ -397,10 +413,9 @@ impl TransportControl for ChromecastRenderer {
// Also send stop command to the device
let device = connect_to_device(&self.host, self.port)?;
let status = device
.receiver
.get_status()
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)))?;
let status = device.receiver.get_status().map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e))
})?;
let app = status
.applications
@@ -410,12 +425,16 @@ impl TransportControl for ChromecastRenderer {
device
.connection
.connect(app.transport_id.as_str())
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)))?;
.map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e))
})?;
let media_status = device
.media
.get_status(app.transport_id.as_str(), None)
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)))?;
.map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to get media status: {}", e))
})?;
let media_entry = media_status
.entries
@@ -437,10 +456,9 @@ impl TransportControl for ChromecastRenderer {
let device = connect_to_device(&self.host, self.port)?;
let status = device
.receiver
.get_status()
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)))?;
let status = device.receiver.get_status().map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e))
})?;
let app = status
.applications
@@ -450,12 +468,16 @@ impl TransportControl for ChromecastRenderer {
device
.connection
.connect(app.transport_id.as_str())
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)))?;
.map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e))
})?;
let media_status = device
.media
.get_status(app.transport_id.as_str(), None)
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)))?;
.map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to get media status: {}", e))
})?;
let media_entry = media_status
.entries
@@ -481,10 +503,9 @@ impl PlaybackStatus for ChromecastRenderer {
let device = connect_to_device(&self.host, self.port)?;
// Get receiver status to find the active app
let status = device
.receiver
.get_status()
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)))?;
let status = device.receiver.get_status().map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e))
})?;
// If no app is running, return NoMedia
let app = match status.applications.first() {
@@ -496,13 +517,17 @@ impl PlaybackStatus for ChromecastRenderer {
device
.connection
.connect(app.transport_id.as_str())
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)))?;
.map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e))
})?;
// Get media status
let media_status = device
.media
.get_status(app.transport_id.as_str(), None)
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)))?;
.map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to get media status: {}", e))
})?;
// If no media entry, return NoMedia
let media_entry = match media_status.entries.first() {
@@ -519,10 +544,9 @@ impl PlaybackPosition for ChromecastRenderer {
let device = connect_to_device(&self.host, self.port)?;
// Get receiver status to find the active app
let status = device
.receiver
.get_status()
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)))?;
let status = device.receiver.get_status().map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e))
})?;
let app = status
.applications
@@ -533,13 +557,17 @@ impl PlaybackPosition for ChromecastRenderer {
device
.connection
.connect(app.transport_id.as_str())
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e)))?;
.map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to connect to app: {}", e))
})?;
// Get media status
let media_status = device
.media
.get_status(app.transport_id.as_str(), None)
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to get media status: {}", e)))?;
.map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to get media status: {}", e))
})?;
let media_entry = media_status
.entries
@@ -635,10 +663,9 @@ impl VolumeControl for ChromecastRenderer {
fn volume(&self) -> Result<u16, ControlPointError> {
let device = connect_to_device(&self.host, self.port)?;
let status = device
.receiver
.get_status()
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)))?;
let status = device.receiver.get_status().map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e))
})?;
if let Some(level) = status.volume.level {
Ok((level * 100.0) as u16)
@@ -653,10 +680,9 @@ impl VolumeControl for ChromecastRenderer {
let device = connect_to_device(&self.host, self.port)?;
let level = (volume as f32) / 100.0;
device
.receiver
.set_volume(level)
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to set volume: {}", e)))?;
device.receiver.set_volume(level).map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to set volume: {}", e))
})?;
Ok(())
}
@@ -664,10 +690,9 @@ impl VolumeControl for ChromecastRenderer {
fn mute(&self) -> Result<bool, ControlPointError> {
let device = connect_to_device(&self.host, self.port)?;
let status = device
.receiver
.get_status()
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)))?;
let status = device.receiver.get_status().map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e))
})?;
Ok(status.volume.muted.unwrap_or(false))
}
@@ -677,11 +702,139 @@ impl VolumeControl for ChromecastRenderer {
let device = connect_to_device(&self.host, self.port)?;
device
.receiver
.set_volume(mute)
.map_err(|e| ControlPointError::ChromecastError(format!("Failed to set mute: {}", e)))?;
device.receiver.set_volume(mute).map_err(|e| {
ControlPointError::ChromecastError(format!("Failed to set mute: {}", e))
})?;
Ok(())
}
}
impl RendererBackend for ChromecastRenderer {
fn queue(&self) -> &Arc<Mutex<MusicQueue>> {
&self.queue
}
}
impl QueueTransportControl for ChromecastRenderer {
fn play_from_queue(&self) -> Result<(), ControlPointError> {
let mut queue = self.queue.lock().unwrap();
let current_index = match queue.current_index()? {
Some(idx) => idx,
None => {
if queue.len()? > 0 {
queue.set_index(Some(0))?;
0
} else {
return Err(ControlPointError::QueueError("Queue is empty".into()));
}
}
};
let item = queue
.get_item(current_index)?
.ok_or_else(|| ControlPointError::QueueError("Current item not found".into()))?;
let uri = item.uri.clone();
drop(queue);
self.play_uri(&uri, "")
}
fn play_next(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.advance()? {
return Err(ControlPointError::QueueError("No next track".into()));
}
}
self.play_from_queue()
}
fn play_previous(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.rewind()? {
return Err(ControlPointError::QueueError("No previous track".into()));
}
}
self.play_from_queue()
}
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
queue.set_index(Some(index))?;
}
self.play_from_queue()
}
}
impl QueueBackend for ChromecastRenderer {
fn len(&self) -> Result<usize, ControlPointError> {
self.queue.lock().unwrap().len()
}
fn track_ids(&self) -> Result<Vec<u32>, ControlPointError> {
self.queue.lock().unwrap().track_ids()
}
fn id_to_position(&self, id: u32) -> Result<usize, ControlPointError> {
self.queue.lock().unwrap().id_to_position(id)
}
fn position_to_id(&self, id: usize) -> Result<u32, ControlPointError> {
self.queue.lock().unwrap().position_to_id(id)
}
fn current_track(&self) -> Result<Option<u32>, ControlPointError> {
self.queue.lock().unwrap().current_track()
}
fn current_index(&self) -> Result<Option<usize>, ControlPointError> {
self.queue.lock().unwrap().current_index()
}
fn queue_snapshot(&self) -> Result<QueueSnapshot, ControlPointError> {
self.queue.lock().unwrap().queue_snapshot()
}
fn set_index(&mut self, index: Option<usize>) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().set_index(index)
}
fn replace_queue(
&mut self,
items: Vec<PlaybackItem>,
current_index: Option<usize>,
) -> Result<(), ControlPointError> {
self.queue
.lock()
.unwrap()
.replace_queue(items, current_index)
}
fn sync_queue(&mut self, items: Vec<PlaybackItem>) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().sync_queue(items)
}
fn get_item(&self, index: usize) -> Result<Option<PlaybackItem>, ControlPointError> {
self.queue.lock().unwrap().get_item(index)
}
fn replace_item(&mut self, index: usize, item: PlaybackItem) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().replace_item(index, item)
}
fn enqueue_items(
&mut self,
items: Vec<PlaybackItem>,
mode: EnqueueMode,
) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().enqueue_items(items, mode)
}
}

View File

@@ -1,28 +1,33 @@
use std::fmt;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use ureq::Agent;
use crate::DeviceIdentity;
use crate::linkplay_client::{LinkPlayStatus, build_agent, extract_linkplay_host, fetch_status_for_host, percent_encode};
use crate::music_renderer::capabilities::{
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, TransportControl,
VolumeControl,
};
use crate::music_renderer::time_utils::{parse_hhmmss_strict};
use crate::errors::ControlPointError;
use crate::model::{RendererInfo, PlaybackState};
use crate::linkplay_client::{
LinkPlayStatus, build_agent, extract_linkplay_host, fetch_status_for_host, percent_encode,
};
use crate::model::{PlaybackState, RendererInfo};
use crate::music_renderer::RendererFromMediaRendererInfo;
use crate::music_renderer::capabilities::{
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend,
TransportControl, VolumeControl,
};
use crate::music_renderer::musicrenderer::MusicRendererBackend;
use crate::music_renderer::time_utils::parse_hhmmss_strict;
use crate::queue::MusicQueue;
use crate::queue::{EnqueueMode, PlaybackItem, QueueBackend, QueueSnapshot};
const DEFAULT_HTTP_TIMEOUT_SECS: u64 = 3;
/// Renderer backend for devices exposing the LinkPlay HTTP API.
#[derive(Clone)]
pub struct LinkPlayRenderer {
host: String,
timeout: Duration,
queue: Arc<Mutex<MusicQueue>>,
}
impl fmt::Debug for LinkPlayRenderer {
@@ -34,7 +39,6 @@ impl fmt::Debug for LinkPlayRenderer {
}
impl LinkPlayRenderer {
fn agent(&self) -> Agent {
build_agent(self.timeout)
}
@@ -44,10 +48,12 @@ impl LinkPlayRenderer {
"http://{}/httpapi.asp?command=setPlayerCmd:{}",
self.host, command
);
self.agent()
.get(&url)
.call()
.map_err(|_| ControlPointError::ArilycTcpError(format!("LinkPlay command {} failed for {}", command, self.host)))?;
self.agent().get(&url).call().map_err(|_| {
ControlPointError::ArilycTcpError(format!(
"LinkPlay command {} failed for {}",
command, self.host
))
})?;
Ok(())
}
@@ -58,20 +64,25 @@ impl LinkPlayRenderer {
impl RendererFromMediaRendererInfo for LinkPlayRenderer {
fn from_renderer_info(info: &RendererInfo) -> Result<Self, ControlPointError> {
let host = extract_linkplay_host(&info.location())
.ok_or_else(|| ControlPointError::LinkPlayError(format!("Renderer {} has no valid LOCATION host", info.udn())))?;
let host = extract_linkplay_host(&info.location()).ok_or_else(|| {
ControlPointError::LinkPlayError(format!(
"Renderer {} has no valid LOCATION host",
info.udn()
))
})?;
let queue = Arc::new(Mutex::new(MusicQueue::from_renderer_info(info)?));
Ok(Self {
host,
timeout: Duration::from_secs(DEFAULT_HTTP_TIMEOUT_SECS),
queue,
})
}
fn to_backend(self) -> MusicRendererBackend {
MusicRendererBackend::LinkPlay(self)
}
}
impl TransportControl for LinkPlayRenderer {
@@ -129,6 +140,131 @@ impl PlaybackPosition for LinkPlayRenderer {
}
}
impl RendererBackend for LinkPlayRenderer {
fn queue(&self) -> &Arc<Mutex<MusicQueue>> {
&self.queue
}
}
impl QueueTransportControl for LinkPlayRenderer {
fn play_from_queue(&self) -> Result<(), ControlPointError> {
let mut queue = self.queue.lock().unwrap();
let current_index = match queue.current_index()? {
Some(idx) => idx,
None => {
if queue.len()? > 0 {
queue.set_index(Some(0))?;
0
} else {
return Err(ControlPointError::QueueError("Queue is empty".into()));
}
}
};
let item = queue
.get_item(current_index)?
.ok_or_else(|| ControlPointError::QueueError("Current item not found".into()))?;
let uri = item.uri.clone();
drop(queue);
self.play_uri(&uri, "")
}
fn play_next(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.advance()? {
return Err(ControlPointError::QueueError("No next track".into()));
}
}
self.play_from_queue()
}
fn play_previous(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.rewind()? {
return Err(ControlPointError::QueueError("No previous track".into()));
}
}
self.play_from_queue()
}
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
queue.set_index(Some(index))?;
}
self.play_from_queue()
}
}
impl QueueBackend for LinkPlayRenderer {
fn len(&self) -> Result<usize, ControlPointError> {
self.queue.lock().unwrap().len()
}
fn track_ids(&self) -> Result<Vec<u32>, ControlPointError> {
self.queue.lock().unwrap().track_ids()
}
fn id_to_position(&self, id: u32) -> Result<usize, ControlPointError> {
self.queue.lock().unwrap().id_to_position(id)
}
fn position_to_id(&self, id: usize) -> Result<u32, ControlPointError> {
self.queue.lock().unwrap().position_to_id(id)
}
fn current_track(&self) -> Result<Option<u32>, ControlPointError> {
self.queue.lock().unwrap().current_track()
}
fn current_index(&self) -> Result<Option<usize>, ControlPointError> {
self.queue.lock().unwrap().current_index()
}
fn queue_snapshot(&self) -> Result<QueueSnapshot, ControlPointError> {
self.queue.lock().unwrap().queue_snapshot()
}
fn set_index(&mut self, index: Option<usize>) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().set_index(index)
}
fn replace_queue(
&mut self,
items: Vec<PlaybackItem>,
current_index: Option<usize>,
) -> Result<(), ControlPointError> {
self.queue
.lock()
.unwrap()
.replace_queue(items, current_index)
}
fn sync_queue(&mut self, items: Vec<PlaybackItem>) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().sync_queue(items)
}
fn get_item(&self, index: usize) -> Result<Option<PlaybackItem>, ControlPointError> {
self.queue.lock().unwrap().get_item(index)
}
fn replace_item(&mut self, index: usize, item: PlaybackItem) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().replace_item(index, item)
}
fn enqueue_items(
&mut self,
items: Vec<PlaybackItem>,
mode: EnqueueMode,
) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().enqueue_items(items, mode)
}
}

View File

@@ -14,7 +14,8 @@ use crate::model::{PlaybackSource, PlaybackState, RendererInfo, RendererProtocol
use crate::music_renderer::RendererFromMediaRendererInfo;
use crate::music_renderer::arylic_tcp::ArylicTcpRenderer;
use crate::music_renderer::capabilities::{
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, TransportControl, VolumeControl,
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend,
TransportControl, VolumeControl,
};
use crate::music_renderer::chromecast_renderer::ChromecastRenderer;
use crate::music_renderer::linkplay_renderer::LinkPlayRenderer;
@@ -87,7 +88,6 @@ pub struct MusicRenderer {
info: RendererInfo,
connection: Arc<Mutex<DeviceConnectionState>>,
backend: Arc<Mutex<MusicRendererBackend>>,
queue: Arc<Mutex<MusicQueue>>,
playlist_binding: Arc<Mutex<Option<PlaylistBinding>>>,
state: Arc<Mutex<MusicRendererState>>,
}
@@ -96,7 +96,6 @@ impl MusicRenderer {
pub fn new(
info: RendererInfo,
backend: Arc<Mutex<MusicRendererBackend>>,
queue: Arc<Mutex<MusicQueue>>,
) -> Arc<MusicRenderer> {
let connection = DeviceConnectionState::new();
@@ -104,7 +103,6 @@ impl MusicRenderer {
info,
connection: Arc::new(Mutex::new(connection)),
backend,
queue,
playlist_binding: Arc::new(Mutex::new(None)),
state: Arc::new(Mutex::new(MusicRendererState::default())),
};
@@ -115,13 +113,11 @@ impl MusicRenderer {
pub fn from_renderer_info(info: &RendererInfo) -> Result<MusicRenderer, ControlPointError> {
let connection = Arc::new(Mutex::new(DeviceConnectionState::new()));
let backend = MusicRendererBackend::make_from_renderer_info(info)?;
let queue = MusicQueue::make_from_renderer_info(info)?;
let renderer = MusicRenderer {
info: info.clone(),
connection,
backend,
queue,
playlist_binding: Arc::new(Mutex::new(None)),
state: Arc::new(Mutex::new(MusicRendererState::default())),
};
@@ -187,9 +183,9 @@ impl MusicRenderer {
/// Prepare the renderer for attaching a new playlist by clearing the queue and stopping playback.
pub fn clear_for_playlist_attach(&self) -> Result<(), ControlPointError> {
// Clear the queue first
self.queue
self.backend
.lock()
.expect("Queue mutex poisoned")
.expect("Backend mutex poisoned")
.clear_queue()?;
// Then stop playback (ignore errors if already stopped)
@@ -210,9 +206,9 @@ impl MusicRenderer {
/// Get the current queue snapshot.
pub fn queue_snapshot(&self) -> Result<QueueSnapshot, ControlPointError> {
let mut snapshot = self
.queue
.backend
.lock()
.expect("Queue mutex poisoned")
.expect("Backend mutex poisoned")
.queue_snapshot()?;
// Enrich snapshot with playlist_id from binding if available
@@ -232,72 +228,42 @@ impl MusicRenderer {
/// Get the current queue item without advancing.
/// Returns the item and count of remaining items after current.
pub fn peek_current(&self) -> Result<Option<(PlaybackItem, usize)>, ControlPointError> {
self.queue
self.backend
.lock()
.expect("Queue mutex poisoned")
.expect("Backend mutex poisoned")
.peek_current()
}
/// Get the count of items remaining after the current index.
pub fn upcoming_len(&self) -> Result<usize, ControlPointError> {
self.queue
self.backend
.lock()
.expect("Queue mutex poisoned")
.expect("Backend mutex poisoned")
.upcoming_len()
}
/// Play the current item from the queue.
pub fn play_current_from_queue(&self) -> Result<(), ControlPointError> {
let mut queue = self.queue.lock().expect("Queue mutex poisoned");
let backend = self.backend.lock().expect("Backend mutex poisoned");
// Get the current item from the queue
let (item, _remaining) = queue.peek_current()?.ok_or_else(|| {
ControlPointError::QueueError("Queue is empty, cannot play current".to_string())
})?;
// Build DIDL metadata if available
let metadata_xml = item
.metadata
.as_ref()
.map(|m| build_didl_lite_metadata(m, &item.uri, &item.protocol_info))
.unwrap_or_default();
// Play the URI using the backend
backend.play_uri(&item.uri, &metadata_xml)?;
Ok(())
self.backend
.lock()
.expect("Backend mutex poisoned")
.play_from_queue()
}
/// Advance to and play the next item from the queue.
pub fn play_next_from_queue(&self) -> Result<(), ControlPointError> {
let mut queue = self.queue.lock().expect("Queue mutex poisoned");
let backend = self.backend.lock().expect("Backend mutex poisoned");
// Dequeue the next item
let (item, _remaining) = queue
.dequeue_next()?
.ok_or_else(|| ControlPointError::QueueError("No next item in queue".to_string()))?;
// Build DIDL metadata if available
let metadata_xml = item
.metadata
.as_ref()
.map(|m| build_didl_lite_metadata(m, &item.uri, &item.protocol_info))
.unwrap_or_default();
// Play the URI using the backend
backend.play_uri(&item.uri, &metadata_xml)?;
Ok(())
}
/// Transport control: play URI with metadata
pub fn play_uri(&self, uri: &str, metadata: &str) -> Result<(), ControlPointError> {
self.backend
.lock()
.expect("Backend mutex poisoned")
.play_uri(uri, metadata)
.play_next()
}
/// Play from a specific index in the queue.
pub fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
self.backend
.lock()
.expect("Backend mutex poisoned")
.play_from_index(index)
}
/// Transport control: play
@@ -306,18 +272,16 @@ impl MusicRenderer {
/// joue le track courant de la queue automatiquement (comportement unifié pour tous les backends).
pub fn play(&self) -> Result<(), ControlPointError> {
// Vérifier si on a une queue non vide
let queue = self.queue.lock().expect("Queue mutex poisoned");
let queue_not_empty = queue.len().unwrap_or(0) > 0;
drop(queue); // Libérer le lock avant l'appel au backend
let backend = self.backend.lock().expect("Backend mutex poisoned");
let queue_not_empty = backend.len().unwrap_or(0) > 0;
if queue_not_empty {
// Si on a des items dans la queue, jouer le track courant (ou le premier si aucun n'est sélectionné)
// peek_current() initialise automatiquement l'index à 0 si nécessaire
// Cela fonctionne pour tous les backends (UPnP interne, OpenHome, etc.)
self.play_current_from_queue()
backend.play_from_queue()
} else {
// Queue vide : déléguer au backend (reprend la lecture en cours, etc.)
self.backend.lock().expect("Backend mutex poisoned").play()
backend.play()
}
}
@@ -506,14 +470,20 @@ impl MusicRenderer {
.unwrap_or(false)
}
/// Returns the number of items in the queue.
pub fn len(&self) -> Result<usize, ControlPointError> {
let backend = self.backend.lock().expect("Backend mutex poisoned");
backend.len()
}
/// Add items to the queue using the specified enqueue mode.
pub fn enqueue_items(
&self,
items: Vec<PlaybackItem>,
mode: EnqueueMode,
) -> Result<(), ControlPointError> {
let mut queue = self.queue.lock().expect("Queue mutex poisoned");
queue.enqueue_items(items, mode)
let mut backend = self.backend.lock().expect("Backend mutex poisoned");
backend.enqueue_items(items, mode)
}
/// Synchronize the queue with new items while preserving the current track.
@@ -523,21 +493,33 @@ impl MusicRenderer {
/// - If the current track is NOT in the new items, it's preserved as the first item
/// - If there's no current track, the queue is simply replaced
pub fn sync_queue(&self, items: Vec<PlaybackItem>) -> Result<(), ControlPointError> {
let mut queue = self.queue.lock().expect("Queue mutex poisoned");
queue.sync_queue(items)
let mut backend = self.backend.lock().expect("Backend mutex poisoned");
backend.sync_queue(items)
}
/// Set the current queue index (for advanced use).
/// Note: This does NOT start playback. Use select_queue_track() to play.
pub fn set_queue_index(&self, index: Option<usize>) -> Result<(), ControlPointError> {
let mut queue = self.queue.lock().expect("Queue mutex poisoned");
queue.set_index(index)
let mut backend = self.backend.lock().expect("Backend mutex poisoned");
backend.set_index(index)
}
/// Clears the renderer's queue using the generic QueueBackend trait.
pub fn clear_queue(&self) -> Result<(), ControlPointError> {
let mut queue = self.queue.lock().expect("Queue mutex poisoned");
queue.clear_queue()
let mut backend = self.backend.lock().expect("Backend mutex poisoned");
backend.clear_queue()
}
/// Dequeues and returns the next item from the queue.
pub fn dequeue_next(&self) -> Result<Option<(PlaybackItem, usize)>, ControlPointError> {
let mut backend = self.backend.lock().expect("Backend mutex poisoned");
backend.dequeue_next()
}
/// Sets the current index in the queue.
pub fn set_index(&self, index: Option<usize>) -> Result<(), ControlPointError> {
let mut backend = self.backend.lock().expect("Backend mutex poisoned");
backend.set_index(index)
}
/// Replaces the entire queue with new items and sets the current index.
@@ -547,8 +529,8 @@ impl MusicRenderer {
items: Vec<PlaybackItem>,
current_index: Option<usize>,
) -> Result<(), ControlPointError> {
let mut queue = self.queue.lock().expect("Queue mutex poisoned");
queue.replace_queue(items, current_index)
let mut backend = self.backend.lock().expect("Backend mutex poisoned");
backend.replace_queue(items, current_index)
}
/// Adds a track to the queue.
@@ -578,34 +560,14 @@ impl MusicRenderer {
/// Converts the track ID to a position using the generic QueueBackend trait,
/// then plays the track.
pub fn select_queue_track(&self, track_id: u32) -> Result<(), ControlPointError> {
let queue = self.queue.lock().expect("Queue mutex poisoned");
// Convert track_id to index
let index = {
let backend = self.backend.lock().expect("Backend mutex poisoned");
backend.id_to_position(track_id)?
};
// Convert track ID to position using generic QueueBackend trait
let position = queue.id_to_position(track_id)?;
drop(queue);
// Set the index
let mut queue = self.queue.lock().expect("Queue mutex poisoned");
queue.set_index(Some(position))?;
// Get the item to play
let item = queue
.get_item(position)?
.ok_or_else(|| ControlPointError::QueueError("Track not found".to_string()))?;
drop(queue);
// Build metadata XML
let metadata_xml = item
.metadata
.as_ref()
.map(|m| build_didl_lite_metadata(m, &item.uri, &item.protocol_info))
.unwrap_or_default();
// Play the item using TransportControl
let backend = self.backend.lock().expect("Backend mutex poisoned");
backend.play_uri(&item.uri, &metadata_xml)?;
Ok(())
// Play from that index
self.play_from_index(index)
}
/// Synchronizes the queue state with the backend.
@@ -613,9 +575,9 @@ impl MusicRenderer {
/// For backends with persistent queues (OpenHome), this refreshes the local view.
/// For others, this is essentially a no-op (just reads the current state).
pub fn sync_queue_state(&self) -> Result<(), ControlPointError> {
let queue = self.queue.lock().expect("Queue mutex poisoned");
let backend = self.backend.lock().expect("Backend mutex poisoned");
// Calling queue_snapshot() triggers a refresh for backends that need it
let _ = queue.queue_snapshot()?;
let _ = backend.queue_snapshot()?;
Ok(())
}
@@ -623,29 +585,25 @@ impl MusicRenderer {
///
/// This is primarily for backends with persistent queues (OpenHome).
pub fn play_current_from_backend_queue(&self) -> Result<(), ControlPointError> {
let queue = self.queue.lock().expect("Queue mutex poisoned");
let backend = self.backend.lock().expect("Backend mutex poisoned");
// Get current track ID using generic QueueBackend trait
let track_id = queue
let track_id = backend
.current_track()?
.ok_or_else(|| ControlPointError::QueueError("No current track".to_string()))?;
drop(queue);
drop(backend);
// Play it using select_queue_track
self.select_queue_track(track_id)
}
/// Returns a reference to the queue (read-only access via lock).
/// This is a convenience method to avoid repetitive `queue.lock().unwrap()` patterns.
pub fn get_queue(&self) -> std::sync::MutexGuard<'_, MusicQueue> {
self.queue.lock().unwrap()
}
/// Returns a mutable reference to the queue (write access via lock).
/// This is a convenience method to avoid repetitive `queue.lock().unwrap()` patterns.
pub fn get_queue_mut(&self) -> std::sync::MutexGuard<'_, MusicQueue> {
self.queue.lock().unwrap()
/// Plays from the queue at the current position.
///
/// Uses the backend's play_from_queue which preserves the queue for all backends.
pub fn play_from_queue(&self) -> Result<(), ControlPointError> {
let backend = self.backend.lock().expect("Backend mutex poisoned");
backend.play_from_queue()
}
// --- Playback State Management ---
@@ -771,7 +729,11 @@ impl MusicRenderer {
}
/// Helper function to build DIDL-Lite metadata XML from TrackMetadata
fn build_didl_lite_metadata(metadata: &TrackMetadata, uri: &str, protocol_info: &str) -> String {
pub(crate) fn build_didl_lite_metadata(
metadata: &TrackMetadata,
uri: &str,
protocol_info: &str,
) -> String {
format!(
r#"<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">
<item id="0" parentID="-1" restricted="1">
@@ -1087,3 +1049,217 @@ impl PlaybackPosition for MusicRendererBackend {
}
}
}
impl RendererBackend for MusicRendererBackend {
fn queue(&self) -> &Arc<Mutex<MusicQueue>> {
match self {
MusicRendererBackend::Upnp(r) => r.queue(),
MusicRendererBackend::OpenHome(r) => r.queue(),
MusicRendererBackend::LinkPlay(r) => r.queue(),
MusicRendererBackend::ArylicTcp(r) => r.queue(),
MusicRendererBackend::Chromecast(cc) => cc.queue(),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.queue(),
}
}
}
impl QueueTransportControl for MusicRendererBackend {
fn play_from_queue(&self) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.play_from_queue(),
MusicRendererBackend::OpenHome(r) => r.play_from_queue(),
MusicRendererBackend::LinkPlay(r) => r.play_from_queue(),
MusicRendererBackend::ArylicTcp(r) => r.play_from_queue(),
MusicRendererBackend::Chromecast(cc) => cc.play_from_queue(),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.play_from_queue(),
}
}
fn play_next(&self) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.play_next(),
MusicRendererBackend::OpenHome(r) => r.play_next(),
MusicRendererBackend::LinkPlay(r) => r.play_next(),
MusicRendererBackend::ArylicTcp(r) => r.play_next(),
MusicRendererBackend::Chromecast(cc) => cc.play_next(),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.play_next(),
}
}
fn play_previous(&self) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.play_previous(),
MusicRendererBackend::OpenHome(r) => r.play_previous(),
MusicRendererBackend::LinkPlay(r) => r.play_previous(),
MusicRendererBackend::ArylicTcp(r) => r.play_previous(),
MusicRendererBackend::Chromecast(cc) => cc.play_previous(),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.play_previous(),
}
}
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.play_from_index(index),
MusicRendererBackend::OpenHome(r) => r.play_from_index(index),
MusicRendererBackend::LinkPlay(r) => r.play_from_index(index),
MusicRendererBackend::ArylicTcp(r) => r.play_from_index(index),
MusicRendererBackend::Chromecast(cc) => cc.play_from_index(index),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.play_from_index(index),
}
}
}
impl QueueBackend for MusicRendererBackend {
fn len(&self) -> Result<usize, ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.len(),
MusicRendererBackend::OpenHome(r) => r.len(),
MusicRendererBackend::LinkPlay(r) => r.len(),
MusicRendererBackend::ArylicTcp(r) => r.len(),
MusicRendererBackend::Chromecast(cc) => cc.len(),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.len(),
}
}
fn track_ids(&self) -> Result<Vec<u32>, ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.track_ids(),
MusicRendererBackend::OpenHome(r) => r.track_ids(),
MusicRendererBackend::LinkPlay(r) => r.track_ids(),
MusicRendererBackend::ArylicTcp(r) => r.track_ids(),
MusicRendererBackend::Chromecast(cc) => cc.track_ids(),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.track_ids(),
}
}
fn id_to_position(&self, id: u32) -> Result<usize, ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.id_to_position(id),
MusicRendererBackend::OpenHome(r) => r.id_to_position(id),
MusicRendererBackend::LinkPlay(r) => r.id_to_position(id),
MusicRendererBackend::ArylicTcp(r) => r.id_to_position(id),
MusicRendererBackend::Chromecast(cc) => cc.id_to_position(id),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.id_to_position(id),
}
}
fn position_to_id(&self, id: usize) -> Result<u32, ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.position_to_id(id),
MusicRendererBackend::OpenHome(r) => r.position_to_id(id),
MusicRendererBackend::LinkPlay(r) => r.position_to_id(id),
MusicRendererBackend::ArylicTcp(r) => r.position_to_id(id),
MusicRendererBackend::Chromecast(cc) => cc.position_to_id(id),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.position_to_id(id),
}
}
fn current_track(&self) -> Result<Option<u32>, ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.current_track(),
MusicRendererBackend::OpenHome(r) => r.current_track(),
MusicRendererBackend::LinkPlay(r) => r.current_track(),
MusicRendererBackend::ArylicTcp(r) => r.current_track(),
MusicRendererBackend::Chromecast(cc) => cc.current_track(),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.current_track(),
}
}
fn current_index(&self) -> Result<Option<usize>, ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.current_index(),
MusicRendererBackend::OpenHome(r) => r.current_index(),
MusicRendererBackend::LinkPlay(r) => r.current_index(),
MusicRendererBackend::ArylicTcp(r) => r.current_index(),
MusicRendererBackend::Chromecast(cc) => cc.current_index(),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.current_index(),
}
}
fn queue_snapshot(&self) -> Result<QueueSnapshot, ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.queue_snapshot(),
MusicRendererBackend::OpenHome(r) => r.queue_snapshot(),
MusicRendererBackend::LinkPlay(r) => r.queue_snapshot(),
MusicRendererBackend::ArylicTcp(r) => r.queue_snapshot(),
MusicRendererBackend::Chromecast(cc) => cc.queue_snapshot(),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.queue_snapshot(),
}
}
fn set_index(&mut self, index: Option<usize>) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.set_index(index),
MusicRendererBackend::OpenHome(r) => r.set_index(index),
MusicRendererBackend::LinkPlay(r) => r.set_index(index),
MusicRendererBackend::ArylicTcp(r) => r.set_index(index),
MusicRendererBackend::Chromecast(cc) => cc.set_index(index),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.set_index(index),
}
}
fn replace_queue(
&mut self,
items: Vec<PlaybackItem>,
current_index: Option<usize>,
) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.replace_queue(items, current_index),
MusicRendererBackend::OpenHome(r) => r.replace_queue(items, current_index),
MusicRendererBackend::LinkPlay(r) => r.replace_queue(items, current_index),
MusicRendererBackend::ArylicTcp(r) => r.replace_queue(items, current_index),
MusicRendererBackend::Chromecast(cc) => cc.replace_queue(items, current_index),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => {
upnp.replace_queue(items, current_index)
}
}
}
fn sync_queue(&mut self, items: Vec<PlaybackItem>) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.sync_queue(items),
MusicRendererBackend::OpenHome(r) => r.sync_queue(items),
MusicRendererBackend::LinkPlay(r) => r.sync_queue(items),
MusicRendererBackend::ArylicTcp(r) => r.sync_queue(items),
MusicRendererBackend::Chromecast(cc) => cc.sync_queue(items),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.sync_queue(items),
}
}
fn get_item(&self, index: usize) -> Result<Option<PlaybackItem>, ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.get_item(index),
MusicRendererBackend::OpenHome(r) => r.get_item(index),
MusicRendererBackend::LinkPlay(r) => r.get_item(index),
MusicRendererBackend::ArylicTcp(r) => r.get_item(index),
MusicRendererBackend::Chromecast(cc) => cc.get_item(index),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.get_item(index),
}
}
fn replace_item(&mut self, index: usize, item: PlaybackItem) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.replace_item(index, item),
MusicRendererBackend::OpenHome(r) => r.replace_item(index, item),
MusicRendererBackend::LinkPlay(r) => r.replace_item(index, item),
MusicRendererBackend::ArylicTcp(r) => r.replace_item(index, item),
MusicRendererBackend::Chromecast(cc) => cc.replace_item(index, item),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.replace_item(index, item),
}
}
fn enqueue_items(
&mut self,
items: Vec<PlaybackItem>,
mode: EnqueueMode,
) -> Result<(), ControlPointError> {
match self {
MusicRendererBackend::Upnp(r) => r.enqueue_items(items, mode),
MusicRendererBackend::OpenHome(r) => r.enqueue_items(items, mode),
MusicRendererBackend::LinkPlay(r) => r.enqueue_items(items, mode),
MusicRendererBackend::ArylicTcp(r) => r.enqueue_items(items, mode),
MusicRendererBackend::Chromecast(cc) => cc.enqueue_items(items, mode),
MusicRendererBackend::HybridUpnpArylic { upnp, .. } => upnp.enqueue_items(items, mode),
}
}
}

View File

@@ -1,6 +1,9 @@
use std::sync::{Arc, Mutex};
use crate::DeviceIdentity;
use crate::music_renderer::capabilities::{
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, TransportControl, VolumeControl,
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend,
TransportControl, VolumeControl,
};
use crate::music_renderer::time_utils::{format_hhmmss_u32, parse_time_flexible};
@@ -12,6 +15,7 @@ use crate::music_renderer::openhome::{
build_info_client, build_playlist_client, build_product_client, build_radio_client,
build_time_client, build_volume_client,
};
use crate::queue::{EnqueueMode, MusicQueue, PlaybackItem, QueueBackend, QueueSnapshot};
use crate::upnp_clients::{
OPENHOME_PLAYLIST_HEAD_ID, OhInfoClient, OhPlaylistClient, OhProductClient, OhRadioClient,
OhTimeClient, OhVolumeClient,
@@ -27,6 +31,7 @@ pub struct OpenHomeRenderer {
product_client: Option<OhProductClient>,
#[allow(dead_code)]
radio_client: Option<OhRadioClient>,
queue: Arc<Mutex<MusicQueue>>,
}
impl OpenHomeRenderer {
@@ -37,6 +42,7 @@ impl OpenHomeRenderer {
volume_client: Option<OhVolumeClient>,
product_client: Option<OhProductClient>,
radio_client: Option<OhRadioClient>,
queue: Arc<Mutex<MusicQueue>>,
) -> Self {
Self {
playlist,
@@ -45,6 +51,7 @@ impl OpenHomeRenderer {
volume_client,
product_client,
radio_client,
queue,
}
}
@@ -180,6 +187,9 @@ impl OpenHomeRenderer {
impl RendererFromMediaRendererInfo for OpenHomeRenderer {
fn from_renderer_info(info: &RendererInfo) -> Result<Self, ControlPointError> {
// Create the OpenHome queue
let queue = Arc::new(Mutex::new(MusicQueue::from_renderer_info(info)?));
let renderer = OpenHomeRenderer::new(
build_playlist_client(&info),
build_info_client(&info),
@@ -187,6 +197,7 @@ impl RendererFromMediaRendererInfo for OpenHomeRenderer {
build_volume_client(&info),
build_product_client(&info),
build_radio_client(&info),
queue,
);
if renderer.has_any_openhome_service() {
@@ -204,6 +215,12 @@ impl RendererFromMediaRendererInfo for OpenHomeRenderer {
}
}
impl RendererBackend for OpenHomeRenderer {
fn queue(&self) -> &Arc<Mutex<MusicQueue>> {
&self.queue
}
}
impl TransportControl for OpenHomeRenderer {
fn play_uri(&self, uri: &str, meta: &str) -> Result<(), ControlPointError> {
let playlist = self.playlist_client_for("play_uri")?;
@@ -330,3 +347,133 @@ pub(crate) fn map_openhome_state(raw: &str) -> PlaybackState {
other => PlaybackState::Unknown(other.to_string()),
}
}
impl QueueTransportControl for OpenHomeRenderer {
fn play_from_queue(&self) -> Result<(), ControlPointError> {
{
let queue = self.queue.lock().unwrap();
if queue.current_index()?.is_none() {
if queue.len()? > 0 {
drop(queue);
let mut queue = self.queue.lock().unwrap();
queue.set_index(Some(0))?;
} else {
return Err(ControlPointError::QueueError("Queue is empty".into()));
}
}
}
let playlist = self.playlist_client_for("play_from_queue")?;
playlist.play()
}
fn play_next(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.advance()? {
return Err(ControlPointError::QueueError("No next track".into()));
}
}
self.play_from_queue()
}
fn play_previous(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.rewind()? {
return Err(ControlPointError::QueueError("No previous track".into()));
}
}
self.play_from_queue()
}
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
// For OpenHome, we need to convert index to track_id
let track_id = {
let queue = self.queue.lock().unwrap();
queue.position_to_id(index)?
};
// Seek to the track by ID
let playlist = self.playlist_client_for("play_from_index")?;
playlist.seek_id(track_id)?;
// Update local queue index
{
let mut queue = self.queue.lock().unwrap();
queue.set_index(Some(index))?;
}
// Start playback
playlist.play()?;
Ok(())
}
}
impl QueueBackend for OpenHomeRenderer {
fn len(&self) -> Result<usize, ControlPointError> {
self.queue.lock().unwrap().len()
}
fn track_ids(&self) -> Result<Vec<u32>, ControlPointError> {
self.queue.lock().unwrap().track_ids()
}
fn id_to_position(&self, id: u32) -> Result<usize, ControlPointError> {
self.queue.lock().unwrap().id_to_position(id)
}
fn position_to_id(&self, id: usize) -> Result<u32, ControlPointError> {
self.queue.lock().unwrap().position_to_id(id)
}
fn current_track(&self) -> Result<Option<u32>, ControlPointError> {
self.queue.lock().unwrap().current_track()
}
fn current_index(&self) -> Result<Option<usize>, ControlPointError> {
self.queue.lock().unwrap().current_index()
}
fn queue_snapshot(&self) -> Result<QueueSnapshot, ControlPointError> {
self.queue.lock().unwrap().queue_snapshot()
}
fn set_index(&mut self, index: Option<usize>) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().set_index(index)
}
fn replace_queue(
&mut self,
items: Vec<PlaybackItem>,
current_index: Option<usize>,
) -> Result<(), ControlPointError> {
self.queue
.lock()
.unwrap()
.replace_queue(items, current_index)
}
fn sync_queue(&mut self, items: Vec<PlaybackItem>) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().sync_queue(items)
}
fn get_item(&self, index: usize) -> Result<Option<PlaybackItem>, ControlPointError> {
self.queue.lock().unwrap().get_item(index)
}
fn replace_item(&mut self, index: usize, item: PlaybackItem) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().replace_item(index, item)
}
fn enqueue_items(
&mut self,
items: Vec<PlaybackItem>,
mode: EnqueueMode,
) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().enqueue_items(items, mode)
}
}

View File

@@ -1,11 +1,14 @@
use std::sync::{Arc, Mutex};
use crate::errors::ControlPointError;
use crate::model::PlaybackState;
use crate::music_renderer::RendererFromMediaRendererInfo;
use crate::music_renderer::capabilities::{
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, TransportControl,
VolumeControl,
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend,
TransportControl, VolumeControl,
};
use crate::model::PlaybackState;
use crate::music_renderer::musicrenderer::MusicRendererBackend;
use crate::music_renderer::musicrenderer::{MusicRendererBackend, build_didl_lite_metadata};
use crate::queue::{EnqueueMode, MusicQueue, PlaybackItem, QueueBackend, QueueSnapshot};
use crate::upnp_clients::{
AvTransportClient, ConnectionInfo, ConnectionManagerClient, PositionInfo, ProtocolInfo,
RenderingControlClient,
@@ -19,6 +22,7 @@ pub struct UpnpRenderer {
rendering_control: Option<RenderingControlClient>,
connection_manager: Option<ConnectionManagerClient>,
has_avtransport_set_next: bool,
queue: Arc<Mutex<MusicQueue>>,
}
impl UpnpRenderer {
@@ -85,6 +89,24 @@ impl UpnpRenderer {
}
}
impl UpnpRenderer {
pub fn new(
avtransport: Option<AvTransportClient>,
rendering_control: Option<RenderingControlClient>,
connection_manager: Option<ConnectionManagerClient>,
has_avtransport_set_next: bool,
queue: Arc<Mutex<MusicQueue>>,
) -> Self {
Self {
avtransport,
rendering_control,
connection_manager,
has_avtransport_set_next,
queue,
}
}
}
impl RendererFromMediaRendererInfo for UpnpRenderer {
fn from_renderer_info(info: &RendererInfo) -> Result<Self, ControlPointError> {
// Prepare le service AVTTransport
@@ -122,11 +144,15 @@ impl RendererFromMediaRendererInfo for UpnpRenderer {
)));
}
// Create the internal queue
let queue = Arc::new(Mutex::new(MusicQueue::from_renderer_info(info)?));
Ok(Self {
avtransport,
rendering_control,
connection_manager,
has_avtransport_set_next: info.capabilities().has_avtransport_set_next(),
queue,
})
}
@@ -135,6 +161,152 @@ impl RendererFromMediaRendererInfo for UpnpRenderer {
}
}
impl RendererBackend for UpnpRenderer {
fn queue(&self) -> &Arc<Mutex<MusicQueue>> {
&self.queue
}
}
impl QueueTransportControl for UpnpRenderer {
fn play_from_queue(&self) -> Result<(), ControlPointError> {
let mut queue = self.queue.lock().unwrap();
// Get or initialize current index
let current_index = match queue.current_index()? {
Some(idx) => idx,
None => {
if queue.len()? > 0 {
queue.set_index(Some(0))?;
0
} else {
return Err(ControlPointError::QueueError("Queue is empty".into()));
}
}
};
// Get the item
let item = queue
.get_item(current_index)?
.ok_or_else(|| ControlPointError::QueueError("Current item not found".into()))?;
drop(queue);
// Build metadata - handle optional TrackMetadata
let metadata = if let Some(ref track_metadata) = item.metadata {
build_didl_lite_metadata(track_metadata, &item.uri, &item.protocol_info)
} else {
// Fallback to minimal DIDL-Lite if no metadata
format!(
r#"<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="0" parentID="-1" restricted="1"><res protocolInfo="{}">{}</res></item></DIDL-Lite>"#,
item.protocol_info, item.uri
)
};
// UPNP: SetAVTransportURI + Play
let avt = self.avtransport()?;
avt.set_av_transport_uri(&item.uri, &metadata)?;
avt.play(0, "1")?;
Ok(())
}
fn play_next(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.advance()? {
return Err(ControlPointError::QueueError("No next track".into()));
}
}
self.play_from_queue()
}
fn play_previous(&self) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
if !queue.rewind()? {
return Err(ControlPointError::QueueError("No previous track".into()));
}
}
self.play_from_queue()
}
fn play_from_index(&self, index: usize) -> Result<(), ControlPointError> {
{
let mut queue = self.queue.lock().unwrap();
queue.set_index(Some(index))?;
}
self.play_from_queue()
}
}
impl QueueBackend for UpnpRenderer {
fn len(&self) -> Result<usize, ControlPointError> {
self.queue.lock().unwrap().len()
}
fn track_ids(&self) -> Result<Vec<u32>, ControlPointError> {
self.queue.lock().unwrap().track_ids()
}
fn id_to_position(&self, id: u32) -> Result<usize, ControlPointError> {
self.queue.lock().unwrap().id_to_position(id)
}
fn position_to_id(&self, id: usize) -> Result<u32, ControlPointError> {
self.queue.lock().unwrap().position_to_id(id)
}
fn current_track(&self) -> Result<Option<u32>, ControlPointError> {
self.queue.lock().unwrap().current_track()
}
fn current_index(&self) -> Result<Option<usize>, ControlPointError> {
self.queue.lock().unwrap().current_index()
}
fn queue_snapshot(&self) -> Result<QueueSnapshot, ControlPointError> {
self.queue.lock().unwrap().queue_snapshot()
}
fn set_index(&mut self, index: Option<usize>) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().set_index(index)
}
fn replace_queue(
&mut self,
items: Vec<PlaybackItem>,
current_index: Option<usize>,
) -> Result<(), ControlPointError> {
self.queue
.lock()
.unwrap()
.replace_queue(items, current_index)
}
fn sync_queue(&mut self, items: Vec<PlaybackItem>) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().sync_queue(items)
}
fn get_item(&self, index: usize) -> Result<Option<PlaybackItem>, ControlPointError> {
self.queue.lock().unwrap().get_item(index)
}
fn replace_item(&mut self, index: usize, item: PlaybackItem) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().replace_item(index, item)
}
fn enqueue_items(
&mut self,
items: Vec<PlaybackItem>,
mode: EnqueueMode,
) -> Result<(), ControlPointError> {
self.queue.lock().unwrap().enqueue_items(items, mode)
}
}
/// Implémentation UPnP AV de `TransportControl` pour [`UpnpRenderer`].
///
/// Cette impl se base sur AVTransport (InstanceID = 0).