First working POC
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
//! Session polling manager for remote playback control.
|
||||
//!
|
||||
//! Manages background polling of Jellyfin sessions with dynamic frequency adjustment
|
||||
//! based on playback mode and UI state. Eliminates duplicate pollers across browser tabs.
|
||||
|
||||
use log::{debug, info, warn};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::jellyfin::JellyfinClient;
|
||||
use crate::player::PlayerEventEmitter;
|
||||
use crate::playback_mode::{PlaybackMode, PlaybackModeManager};
|
||||
|
||||
/// Hint for adjusting poll frequency based on UI state
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PollingHint {
|
||||
/// CastButton is active and needs frequent updates (500ms)
|
||||
CastActive,
|
||||
/// CastButton is in discovery mode (15s)
|
||||
CastDiscovery,
|
||||
/// No special hint, use mode-based frequency (default)
|
||||
Normal,
|
||||
}
|
||||
|
||||
/// Manages background polling of Jellyfin sessions
|
||||
pub struct SessionPollerManager {
|
||||
jellyfin_client: Arc<Mutex<Option<JellyfinClient>>>,
|
||||
playback_mode_manager: Arc<PlaybackModeManager>,
|
||||
event_emitter: Arc<Mutex<Option<Arc<dyn PlayerEventEmitter>>>>,
|
||||
|
||||
// Polling state
|
||||
is_running: Arc<AtomicBool>,
|
||||
current_hint: Arc<RwLock<PollingHint>>,
|
||||
current_interval_ms: Arc<AtomicU64>,
|
||||
|
||||
// Thread handle (for cleanup)
|
||||
thread_handle: Arc<Mutex<Option<thread::JoinHandle<()>>>>,
|
||||
}
|
||||
|
||||
impl SessionPollerManager {
|
||||
/// Create a new SessionPollerManager
|
||||
pub fn new(
|
||||
jellyfin_client: Arc<Mutex<Option<JellyfinClient>>>,
|
||||
playback_mode_manager: Arc<PlaybackModeManager>,
|
||||
) -> Self {
|
||||
Self {
|
||||
jellyfin_client,
|
||||
playback_mode_manager,
|
||||
event_emitter: Arc::new(Mutex::new(None)),
|
||||
is_running: Arc::new(AtomicBool::new(false)),
|
||||
current_hint: Arc::new(RwLock::new(PollingHint::Normal)),
|
||||
current_interval_ms: Arc::new(AtomicU64::new(10000)), // Default 10s
|
||||
thread_handle: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set event emitter for broadcasting session updates
|
||||
pub fn set_event_emitter(&self, emitter: Arc<dyn PlayerEventEmitter>) {
|
||||
*self.event_emitter.lock().unwrap() = Some(emitter);
|
||||
}
|
||||
|
||||
/// Start the background polling thread
|
||||
pub fn start(&self) {
|
||||
if self.is_running.swap(true, Ordering::Relaxed) {
|
||||
warn!("[SessionPoller] Already running, ignoring start request");
|
||||
return;
|
||||
}
|
||||
|
||||
info!("[SessionPoller] Starting session polling");
|
||||
|
||||
// Clone Arc references for the thread
|
||||
let client = self.jellyfin_client.clone();
|
||||
let mode_manager = self.playback_mode_manager.clone();
|
||||
let emitter = self.event_emitter.clone();
|
||||
let is_running = self.is_running.clone();
|
||||
let hint = self.current_hint.clone();
|
||||
let interval_ms = self.current_interval_ms.clone();
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
// Create Tokio runtime for async operations in this thread
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
while is_running.load(Ordering::Relaxed) {
|
||||
// Calculate poll interval based on mode and hint
|
||||
let new_interval = Self::calculate_interval(
|
||||
&mode_manager.get_mode(),
|
||||
*hint.read().unwrap(),
|
||||
);
|
||||
|
||||
interval_ms.store(new_interval, Ordering::Relaxed);
|
||||
|
||||
debug!("[SessionPoller] Polling with interval: {}ms", new_interval);
|
||||
|
||||
// Fetch sessions
|
||||
let sessions_result = rt.block_on(async {
|
||||
let client_opt = client.lock().unwrap().clone();
|
||||
match client_opt {
|
||||
Some(c) => c.get_sessions().await,
|
||||
None => {
|
||||
debug!("[SessionPoller] Jellyfin client not configured, skipping poll");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Emit event if successful
|
||||
match sessions_result {
|
||||
Ok(sessions) => {
|
||||
debug!("[SessionPoller] Fetched {} sessions", sessions.len());
|
||||
if let Some(em) = emitter.lock().unwrap().as_ref() {
|
||||
em.emit(crate::player::PlayerStatusEvent::SessionsUpdated {
|
||||
sessions,
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("[SessionPoller] Failed to fetch sessions: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Sleep for the calculated interval
|
||||
thread::sleep(Duration::from_millis(new_interval));
|
||||
}
|
||||
|
||||
info!("[SessionPoller] Polling thread stopped");
|
||||
});
|
||||
|
||||
*self.thread_handle.lock().unwrap() = Some(handle);
|
||||
}
|
||||
|
||||
/// Stop the polling thread
|
||||
pub fn stop(&self) {
|
||||
info!("[SessionPoller] Stopping session polling");
|
||||
self.is_running.store(false, Ordering::Relaxed);
|
||||
|
||||
// Join the thread if possible (don't block indefinitely)
|
||||
if let Some(handle) = self.thread_handle.lock().unwrap().take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
|
||||
/// Set UI hint for polling frequency adjustment
|
||||
pub fn set_polling_hint(&self, hint: PollingHint) {
|
||||
debug!("[SessionPoller] Setting polling hint: {:?}", hint);
|
||||
*self.current_hint.write().unwrap() = hint;
|
||||
}
|
||||
|
||||
/// Calculate polling interval based on mode and hint
|
||||
fn calculate_interval(mode: &PlaybackMode, hint: PollingHint) -> u64 {
|
||||
match hint {
|
||||
PollingHint::CastActive => 500, // Very fast for active control
|
||||
PollingHint::CastDiscovery => 15000, // Slow discovery
|
||||
PollingHint::Normal => {
|
||||
match mode {
|
||||
PlaybackMode::Remote { .. } => 2000, // Fast in remote mode
|
||||
PlaybackMode::Local | PlaybackMode::Idle => 10000, // Default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Manually trigger a poll (for frontend refresh button)
|
||||
pub async fn poll_now(&self) -> Result<Vec<crate::jellyfin::client::SessionInfo>, String> {
|
||||
let client = self.jellyfin_client.lock().unwrap().clone()
|
||||
.ok_or("Jellyfin client not configured")?;
|
||||
|
||||
client.get_sessions().await
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SessionPollerManager {
|
||||
fn drop(&mut self) {
|
||||
self.stop();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Test polling interval calculation for different modes and hints
|
||||
#[test]
|
||||
fn test_calculate_interval() {
|
||||
// CastActive hint should always be 500ms regardless of mode
|
||||
assert_eq!(
|
||||
SessionPollerManager::calculate_interval(&PlaybackMode::Idle, PollingHint::CastActive),
|
||||
500
|
||||
);
|
||||
assert_eq!(
|
||||
SessionPollerManager::calculate_interval(&PlaybackMode::Local, PollingHint::CastActive),
|
||||
500
|
||||
);
|
||||
assert_eq!(
|
||||
SessionPollerManager::calculate_interval(
|
||||
&PlaybackMode::Remote { session_id: "test".to_string() },
|
||||
PollingHint::CastActive
|
||||
),
|
||||
500
|
||||
);
|
||||
|
||||
// CastDiscovery hint should always be 15s regardless of mode
|
||||
assert_eq!(
|
||||
SessionPollerManager::calculate_interval(&PlaybackMode::Idle, PollingHint::CastDiscovery),
|
||||
15000
|
||||
);
|
||||
assert_eq!(
|
||||
SessionPollerManager::calculate_interval(&PlaybackMode::Local, PollingHint::CastDiscovery),
|
||||
15000
|
||||
);
|
||||
assert_eq!(
|
||||
SessionPollerManager::calculate_interval(
|
||||
&PlaybackMode::Remote { session_id: "test".to_string() },
|
||||
PollingHint::CastDiscovery
|
||||
),
|
||||
15000
|
||||
);
|
||||
|
||||
// Normal hint should depend on mode
|
||||
// Idle and Local modes -> 10s
|
||||
assert_eq!(
|
||||
SessionPollerManager::calculate_interval(&PlaybackMode::Idle, PollingHint::Normal),
|
||||
10000
|
||||
);
|
||||
assert_eq!(
|
||||
SessionPollerManager::calculate_interval(&PlaybackMode::Local, PollingHint::Normal),
|
||||
10000
|
||||
);
|
||||
// Remote mode -> 2s
|
||||
assert_eq!(
|
||||
SessionPollerManager::calculate_interval(
|
||||
&PlaybackMode::Remote { session_id: "test".to_string() },
|
||||
PollingHint::Normal
|
||||
),
|
||||
2000
|
||||
);
|
||||
}
|
||||
|
||||
/// Test PollingHint enum equality
|
||||
#[test]
|
||||
fn test_polling_hint_equality() {
|
||||
assert_eq!(PollingHint::Normal, PollingHint::Normal);
|
||||
assert_eq!(PollingHint::CastActive, PollingHint::CastActive);
|
||||
assert_eq!(PollingHint::CastDiscovery, PollingHint::CastDiscovery);
|
||||
|
||||
assert_ne!(PollingHint::Normal, PollingHint::CastActive);
|
||||
assert_ne!(PollingHint::CastActive, PollingHint::CastDiscovery);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user