// Player module - Complete playback control system // TRACES: UR-003, UR-004, UR-005, UR-019, UR-023, UR-026 | // IR-003, IR-004, IR-006, IR-008 | // DR-001, DR-004, DR-005, DR-009, DR-028, DR-029, DR-047 pub mod autoplay; pub mod backend; pub mod events; pub mod media; pub mod queue; pub mod seek; pub mod session; pub mod sleep_timer; pub mod state; #[cfg(test)] mod mpv_backend_test; // Platform-specific backends #[cfg(target_os = "android")] pub mod android; #[cfg(target_os = "linux")] pub mod mpv_backend; // Re-export commonly used types pub use autoplay::{AutoplayDecision, AutoplaySettings}; pub use backend::{NullBackend, PlayerBackend, PlayerError}; pub use events::{PlayerEventEmitter, PlayerStatusEvent, TauriEventEmitter}; pub use media::{MediaItem, MediaSource, MediaType, QueueContext}; pub use queue::{QueueManager, RepeatMode}; pub use seek::{determine_video_seek_strategy, VideoSeekStrategy}; pub use session::{MediaSessionManager, MediaSessionType}; pub use sleep_timer::{SleepTimerMode, SleepTimerState}; pub use state::{EndReason, PlayerState}; // Re-export platform-specific backends #[cfg(target_os = "android")] pub use android::ExoPlayerBackend; #[cfg(target_os = "linux")] pub use mpv_backend::MpvBackend; #[cfg(target_os = "android")] pub use android::{ MediaCommandHandler, RemoteVolumeHandler, enable_remote_volume, disable_remote_volume, set_media_command_handler, set_remote_volume_handler, get_detected_codecs, }; /// Metadata for the lockscreen / media notification. /// /// Used to drive the Android MediaSession from Rust in remote (cast) mode, where /// the local ExoPlayer is idle and so can't supply now-playing info. The session /// poller fills this in from the remote Jellyfin session and pushes it to the /// notification so the lockscreen stays in sync while casting. #[derive(Debug, Clone)] pub struct LockscreenMetadata { pub title: String, pub artist: String, pub album: Option, /// Track duration in milliseconds. pub duration_ms: i64, /// Current playback position in milliseconds. pub position_ms: i64, pub is_playing: bool, } /// Push now-playing metadata to the Android lockscreen. No-op off Android, so the /// session poller can call it unconditionally and stay platform-agnostic. pub fn update_lockscreen_metadata(_meta: &LockscreenMetadata) -> Result<(), String> { #[cfg(target_os = "android")] { return android::update_lockscreen_metadata(_meta); } #[cfg(not(target_os = "android"))] { Ok(()) } } use crate::utils::lock::MutexSafe; use log::{debug, error, warn}; use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::sync::Mutex as TokioMutex; use crate::jellyfin::JellyfinClient; use crate::settings::AudioSettings; use crate::repository::MediaRepository; use crate::playback_reporting::{PlaybackReporter, EventThrottler, PlaybackOperation, PlaybackContext}; /// Central player controller that coordinates playback pub struct PlayerController { backend: Arc>>, queue: Arc>, jellyfin_client: Arc>>, muted: bool, // Sleep timer state sleep_timer: Arc>, // Autoplay settings autoplay_settings: Arc>, // Repository for fetching next episodes repository: Arc>>>, // Event emitter for notifications event_emitter: Arc>>>, // Countdown cancellation handle countdown_cancel: Arc>>>>, // Playback reporting (dual sync: local DB + server) playback_reporter: Arc>>, // Event throttler to prevent spam from position updates // Will be used when position update hooks are added to backends #[allow(dead_code)] position_throttler: Arc, // End reason tracking for autoplay decision making end_reason: Arc>>, // Auto-play episode counter (session-based, resets on manual play) autoplay_episode_count: Arc>, } impl PlayerController { pub fn new( backend: Box, playback_reporter: Arc>>, position_throttler: Arc, ) -> Self { let controller = Self { backend: Arc::new(Mutex::new(backend)), queue: Arc::new(Mutex::new(QueueManager::new())), jellyfin_client: Arc::new(Mutex::new(None)), muted: false, sleep_timer: Arc::new(Mutex::new(SleepTimerState::default())), autoplay_settings: Arc::new(Mutex::new(AutoplaySettings::default())), repository: Arc::new(Mutex::new(None)), event_emitter: Arc::new(Mutex::new(None)), countdown_cancel: Arc::new(Mutex::new(None)), playback_reporter, position_throttler, end_reason: Arc::new(Mutex::new(None)), autoplay_episode_count: Arc::new(Mutex::new(0)), }; // Start background timer thread for sleep timer countdown controller.start_timer_thread(); controller } /// Configure the Jellyfin API client for automatic playback reporting pub fn set_jellyfin_client(&self, client: Option) { let mut jellyfin = self.jellyfin_client.lock_safe(); *jellyfin = client; log::info!("[PlayerController] Jellyfin client configured: {}", jellyfin.is_some()); } /// Get a reference to the Jellyfin client (for remote session control) pub fn jellyfin_client(&self) -> Arc>> { self.jellyfin_client.clone() } /// Configure the playback reporter for dual sync (local DB + server). /// Called from `player_configure_jellyfin` on login/restore/reauth. pub async fn set_playback_reporter(&self, reporter: Option) { let mut reporter_guard = self.playback_reporter.lock().await; *reporter_guard = reporter; log::info!("[PlayerController] Playback reporter configured: {}", reporter_guard.is_some()); } /// Get a reference to the playback reporter (for backend position updates) /// Will be used when position update hooks are added to backends #[allow(dead_code)] pub fn playback_reporter(&self) -> Arc>> { self.playback_reporter.clone() } /// Get a reference to the position throttler (for backend position updates) /// Will be used when position update hooks are added to backends #[allow(dead_code)] pub fn position_throttler(&self) -> Arc { self.position_throttler.clone() } /// Set the end reason for the next playback end event fn set_end_reason(&self, reason: EndReason) { log::debug!("[PlayerController] Setting end reason: {:?}", reason); *self.end_reason.lock_safe() = Some(reason); } /// Get and clear the current end reason fn take_end_reason(&self) -> Option { self.end_reason.lock_safe().take() } /// Increment autoplay episode counter. Returns true if limit is reached. fn increment_autoplay_count(&self) -> bool { let max = self.autoplay_settings.lock_safe().max_episodes; if max == 0 { // Unlimited return false; } let mut count = self.autoplay_episode_count.lock_safe(); *count += 1; debug!("[PlayerController] Autoplay episode count: {}/{}", *count, max); *count >= max } /// Reset autoplay episode counter (called on manual play actions) fn reset_autoplay_count(&self) { let mut count = self.autoplay_episode_count.lock_safe(); if *count > 0 { debug!("[PlayerController] Resetting autoplay episode counter (was {})", *count); } *count = 0; } /// Load and play a single item (also sets the queue to contain only this item) pub fn play_item(&self, item: MediaItem) -> Result<(), PlayerError> { debug!("[PlayerController] play_item: {}", item.title); // Reset autoplay counter on manual play self.reset_autoplay_count(); // Update queue with this single item { let mut queue = self.queue.lock_safe(); queue.set_queue(vec![item.clone()], 0); } // Load and play the item self.load_and_play(&item)?; Ok(()) } /// Set the current queue item without loading it into the playback backend. /// /// Used on platforms where video is rendered outside the native backend /// (Linux WebKitGTK HTML5