Architecture remediation A/B/F: poison-tolerant locks, graceful backend init, doc fixes
Workstream A — poison-tolerant locking: - Add utils/lock.rs with MutexSafe/RwLockSafe extension traits that recover a poisoned std::sync lock instead of panicking, plus unit tests. - Replace all 153 .lock().unwrap() and 4 .read()/.write().unwrap() production sites with _safe variants across 14 files, eliminating the player crash-cascade class. Tokio async mutexes are unchanged. Workstream B — graceful backend init: - create_player_backend no longer panics when MPV/ExoPlayer fail to initialize; it falls back to NullBackend and emits a backend-init-failed event so the UI can show "playback unavailable" instead of the app crashing. Fatal DB-setup panics are kept. Workstream F — doc reconciliation: - Rewrite software-architecture.md's inaccurate "thin UI / ~800 lines" claims to reflect reality (~20.5k non-test frontend) and document the events+polling hybrid plus the new locking/backend-init behavior.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
//! This module provides a `PlayerBackend` implementation using Android's ExoPlayer
|
||||
//! through JNI calls to Kotlin code.
|
||||
|
||||
use crate::utils::lock::MutexSafe;
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
use tokio::sync::Mutex as TokioMutex;
|
||||
use log::debug;
|
||||
@@ -303,7 +304,7 @@ impl PlayerBackend for ExoPlayerBackend {
|
||||
|
||||
// Update local state
|
||||
{
|
||||
let mut state = self.shared_state.lock().unwrap();
|
||||
let mut state = self.shared_state.lock_safe();
|
||||
state.current_media = Some(media.clone());
|
||||
state.state = PlayerState::Loading {
|
||||
media: media.clone(),
|
||||
@@ -430,7 +431,7 @@ impl PlayerBackend for ExoPlayerBackend {
|
||||
|
||||
fn stop(&mut self) -> Result<(), PlayerError> {
|
||||
{
|
||||
let mut state = self.shared_state.lock().unwrap();
|
||||
let mut state = self.shared_state.lock_safe();
|
||||
state.state = PlayerState::Idle;
|
||||
state.is_loaded = false;
|
||||
state.current_media = None;
|
||||
@@ -479,24 +480,24 @@ impl PlayerBackend for ExoPlayerBackend {
|
||||
)
|
||||
.map_err(|e| PlayerError::playback_failed(format!("Failed to call setVolume: {}", e)))?;
|
||||
|
||||
self.shared_state.lock().unwrap().volume = clamped;
|
||||
self.shared_state.lock_safe().volume = clamped;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn position(&self) -> f64 {
|
||||
self.shared_state.lock().unwrap().position
|
||||
self.shared_state.lock_safe().position
|
||||
}
|
||||
|
||||
fn duration(&self) -> Option<f64> {
|
||||
self.shared_state.lock().unwrap().duration
|
||||
self.shared_state.lock_safe().duration
|
||||
}
|
||||
|
||||
fn state(&self) -> PlayerState {
|
||||
self.shared_state.lock().unwrap().state.clone()
|
||||
self.shared_state.lock_safe().state.clone()
|
||||
}
|
||||
|
||||
fn volume(&self) -> f32 {
|
||||
self.shared_state.lock().unwrap().volume
|
||||
self.shared_state.lock_safe().volume
|
||||
}
|
||||
|
||||
fn set_audio_track(&mut self, stream_index: i32) -> Result<(), PlayerError> {
|
||||
@@ -564,7 +565,7 @@ pub extern "system" fn Java_com_dtourolle_jellytau_player_JellyTauPlayer_nativeO
|
||||
|
||||
// Update state and get the preserved duration to emit
|
||||
let duration_to_emit = if let Some(state) = SHARED_STATE.get() {
|
||||
let mut state = state.lock().unwrap();
|
||||
let mut state = state.lock_safe();
|
||||
state.position = position;
|
||||
if duration > 0.0 {
|
||||
state.duration = Some(duration);
|
||||
@@ -606,7 +607,7 @@ pub extern "system" fn Java_com_dtourolle_jellytau_player_JellyTauPlayer_nativeO
|
||||
|
||||
// Update shared state
|
||||
if let Some(shared) = SHARED_STATE.get() {
|
||||
let mut shared = shared.lock().unwrap();
|
||||
let mut shared = shared.lock_safe();
|
||||
if let Some(media) = shared.current_media.clone() {
|
||||
let duration = shared.duration.unwrap_or(0.0);
|
||||
let position = shared.position;
|
||||
@@ -651,7 +652,7 @@ pub extern "system" fn Java_com_dtourolle_jellytau_player_JellyTauPlayer_nativeO
|
||||
duration: jdouble,
|
||||
) {
|
||||
if let Some(state) = SHARED_STATE.get() {
|
||||
let mut state = state.lock().unwrap();
|
||||
let mut state = state.lock_safe();
|
||||
state.duration = Some(duration);
|
||||
state.is_loaded = true;
|
||||
}
|
||||
@@ -692,7 +693,7 @@ pub extern "system" fn Java_com_dtourolle_jellytau_player_JellyTauPlayer_nativeO
|
||||
|
||||
// Log queue state before advancing
|
||||
let queue_info = {
|
||||
let queue = ctrl.queue.lock().unwrap();
|
||||
let queue = ctrl.queue.lock_safe();
|
||||
format!("current_index={:?}, len={}", queue.current_index(), queue.items().len())
|
||||
};
|
||||
log::debug!("[Autoplay] Queue state before next(): {}", queue_info);
|
||||
@@ -702,7 +703,7 @@ pub extern "system" fn Java_com_dtourolle_jellytau_player_JellyTauPlayer_nativeO
|
||||
log::info!("[Autoplay] Successfully advanced to next track");
|
||||
// Log queue state after advancing
|
||||
let queue_info = {
|
||||
let queue = ctrl.queue.lock().unwrap();
|
||||
let queue = ctrl.queue.lock_safe();
|
||||
format!("current_index={:?}, len={}", queue.current_index(), queue.items().len())
|
||||
};
|
||||
log::debug!("[Autoplay] Queue state after next(): {}", queue_info);
|
||||
@@ -811,7 +812,7 @@ pub extern "system" fn Java_com_dtourolle_jellytau_player_JellyTauPlayer_nativeO
|
||||
muted: jboolean,
|
||||
) {
|
||||
if let Some(state) = SHARED_STATE.get() {
|
||||
state.lock().unwrap().volume = volume;
|
||||
state.lock_safe().volume = volume;
|
||||
}
|
||||
|
||||
if let Some(emitter) = EVENT_EMITTER.get() {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
//!
|
||||
//! TRACES: UR-005, UR-019, UR-023, UR-026 | DR-001, DR-028, DR-047
|
||||
|
||||
#[cfg(test)]
|
||||
use crate::utils::lock::MutexSafe;
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
@@ -168,17 +170,17 @@ mod tests {
|
||||
}
|
||||
|
||||
pub fn events(&self) -> Vec<PlayerStatusEvent> {
|
||||
self.events.lock().unwrap().clone()
|
||||
self.events.lock_safe().clone()
|
||||
}
|
||||
|
||||
pub fn clear(&self) {
|
||||
self.events.lock().unwrap().clear();
|
||||
self.events.lock_safe().clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayerEventEmitter for TestEventEmitter {
|
||||
fn emit(&self, event: PlayerStatusEvent) {
|
||||
self.events.lock().unwrap().push(event);
|
||||
self.events.lock_safe().push(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+77
-76
@@ -44,6 +44,7 @@ pub use android::{
|
||||
set_media_command_handler, set_remote_volume_handler, get_detected_codecs,
|
||||
};
|
||||
|
||||
use crate::utils::lock::MutexSafe;
|
||||
use log::{debug, error, warn};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
@@ -121,7 +122,7 @@ impl PlayerController {
|
||||
|
||||
/// Configure the Jellyfin API client for automatic playback reporting
|
||||
pub fn set_jellyfin_client(&self, client: Option<JellyfinClient>) {
|
||||
let mut jellyfin = self.jellyfin_client.lock().unwrap();
|
||||
let mut jellyfin = self.jellyfin_client.lock_safe();
|
||||
*jellyfin = client;
|
||||
log::info!("[PlayerController] Jellyfin client configured: {}", jellyfin.is_some());
|
||||
}
|
||||
@@ -157,24 +158,24 @@ impl PlayerController {
|
||||
/// 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().unwrap() = Some(reason);
|
||||
*self.end_reason.lock_safe() = Some(reason);
|
||||
}
|
||||
|
||||
/// Get and clear the current end reason
|
||||
fn take_end_reason(&self) -> Option<EndReason> {
|
||||
self.end_reason.lock().unwrap().take()
|
||||
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().unwrap().max_episodes;
|
||||
let max = self.autoplay_settings.lock_safe().max_episodes;
|
||||
|
||||
if max == 0 {
|
||||
// Unlimited
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut count = self.autoplay_episode_count.lock().unwrap();
|
||||
let mut count = self.autoplay_episode_count.lock_safe();
|
||||
*count += 1;
|
||||
debug!("[PlayerController] Autoplay episode count: {}/{}", *count, max);
|
||||
|
||||
@@ -183,7 +184,7 @@ impl PlayerController {
|
||||
|
||||
/// Reset autoplay episode counter (called on manual play actions)
|
||||
fn reset_autoplay_count(&self) {
|
||||
let mut count = self.autoplay_episode_count.lock().unwrap();
|
||||
let mut count = self.autoplay_episode_count.lock_safe();
|
||||
if *count > 0 {
|
||||
debug!("[PlayerController] Resetting autoplay episode counter (was {})", *count);
|
||||
}
|
||||
@@ -199,7 +200,7 @@ impl PlayerController {
|
||||
|
||||
// Update queue with this single item
|
||||
{
|
||||
let mut queue = self.queue.lock().unwrap();
|
||||
let mut queue = self.queue.lock_safe();
|
||||
queue.set_queue(vec![item.clone()], 0);
|
||||
}
|
||||
|
||||
@@ -217,7 +218,7 @@ impl PlayerController {
|
||||
// Set end reason to NewTrackLoaded to prevent autoplay when MPV ends current track
|
||||
self.set_end_reason(EndReason::NewTrackLoaded);
|
||||
|
||||
let mut backend = self.backend.lock().unwrap();
|
||||
let mut backend = self.backend.lock_safe();
|
||||
backend.load(item)?;
|
||||
backend.play()?;
|
||||
drop(backend);
|
||||
@@ -297,12 +298,12 @@ impl PlayerController {
|
||||
self.reset_autoplay_count();
|
||||
|
||||
{
|
||||
let mut queue = self.queue.lock().unwrap();
|
||||
let mut queue = self.queue.lock_safe();
|
||||
queue.set_queue(items, start_index);
|
||||
}
|
||||
|
||||
// Play the current item (without modifying the queue we just set)
|
||||
if let Some(item) = self.queue.lock().unwrap().current().cloned() {
|
||||
if let Some(item) = self.queue.lock_safe().current().cloned() {
|
||||
self.load_and_play(&item)?;
|
||||
}
|
||||
|
||||
@@ -312,19 +313,19 @@ impl PlayerController {
|
||||
/// Play/resume playback
|
||||
pub fn play(&self) -> Result<(), PlayerError> {
|
||||
debug!("[PlayerController] play");
|
||||
let mut backend = self.backend.lock().unwrap();
|
||||
let mut backend = self.backend.lock_safe();
|
||||
backend.play()
|
||||
}
|
||||
|
||||
/// Pause playback
|
||||
pub fn pause(&self) -> Result<(), PlayerError> {
|
||||
let mut backend = self.backend.lock().unwrap();
|
||||
let mut backend = self.backend.lock_safe();
|
||||
backend.pause()
|
||||
}
|
||||
|
||||
/// Toggle play/pause
|
||||
pub fn toggle_playback(&self) -> Result<(), PlayerError> {
|
||||
let mut backend = self.backend.lock().unwrap();
|
||||
let mut backend = self.backend.lock_safe();
|
||||
if backend.state().is_playing() {
|
||||
backend.pause()
|
||||
} else {
|
||||
@@ -339,16 +340,16 @@ impl PlayerController {
|
||||
|
||||
// Get current playback info before stopping
|
||||
let jellyfin_id = {
|
||||
let queue = self.queue.lock().unwrap();
|
||||
let queue = self.queue.lock_safe();
|
||||
queue.current().and_then(|item| item.jellyfin_id().map(|s| s.to_string()))
|
||||
};
|
||||
|
||||
let position_ticks = {
|
||||
let backend = self.backend.lock().unwrap();
|
||||
let backend = self.backend.lock_safe();
|
||||
(backend.position() * 10_000_000.0) as i64
|
||||
};
|
||||
|
||||
let mut backend = self.backend.lock().unwrap();
|
||||
let mut backend = self.backend.lock_safe();
|
||||
backend.stop()?;
|
||||
drop(backend);
|
||||
|
||||
@@ -412,7 +413,7 @@ impl PlayerController {
|
||||
self.reset_autoplay_count();
|
||||
|
||||
let next_item = {
|
||||
let mut queue = self.queue.lock().unwrap();
|
||||
let mut queue = self.queue.lock_safe();
|
||||
queue.next().cloned()
|
||||
};
|
||||
|
||||
@@ -435,7 +436,7 @@ impl PlayerController {
|
||||
self.reset_autoplay_count();
|
||||
// If we're more than 3 seconds in, restart current track
|
||||
{
|
||||
let backend = self.backend.lock().unwrap();
|
||||
let backend = self.backend.lock_safe();
|
||||
if backend.position() > 3.0 {
|
||||
debug!("[PlayerController] previous: restarting current track (position > 3s)");
|
||||
drop(backend);
|
||||
@@ -444,7 +445,7 @@ impl PlayerController {
|
||||
}
|
||||
|
||||
let prev_item = {
|
||||
let mut queue = self.queue.lock().unwrap();
|
||||
let mut queue = self.queue.lock_safe();
|
||||
queue.previous().cloned()
|
||||
};
|
||||
|
||||
@@ -459,40 +460,40 @@ impl PlayerController {
|
||||
|
||||
/// Seek to a position in seconds
|
||||
pub fn seek(&self, position: f64) -> Result<(), PlayerError> {
|
||||
let mut backend = self.backend.lock().unwrap();
|
||||
let mut backend = self.backend.lock_safe();
|
||||
backend.seek(position)
|
||||
}
|
||||
|
||||
/// Set volume (0.0 - 1.0)
|
||||
pub fn set_volume(&self, volume: f32) -> Result<(), PlayerError> {
|
||||
self.backend.lock().unwrap().set_volume(volume)
|
||||
self.backend.lock_safe().set_volume(volume)
|
||||
}
|
||||
|
||||
/// Set the active audio track by stream index
|
||||
pub fn set_audio_track(&self, stream_index: i32) -> Result<(), PlayerError> {
|
||||
let mut backend = self.backend.lock().unwrap();
|
||||
let mut backend = self.backend.lock_safe();
|
||||
backend.set_audio_track(stream_index)
|
||||
}
|
||||
|
||||
/// Set the active subtitle track by stream index (None to disable subtitles)
|
||||
pub fn set_subtitle_track(&self, stream_index: Option<i32>) -> Result<(), PlayerError> {
|
||||
let mut backend = self.backend.lock().unwrap();
|
||||
let mut backend = self.backend.lock_safe();
|
||||
backend.set_subtitle_track(stream_index)
|
||||
}
|
||||
|
||||
/// Get current state
|
||||
pub fn state(&self) -> PlayerState {
|
||||
self.backend.lock().unwrap().state()
|
||||
self.backend.lock_safe().state()
|
||||
}
|
||||
|
||||
/// Get current position
|
||||
pub fn position(&self) -> f64 {
|
||||
self.backend.lock().unwrap().position()
|
||||
self.backend.lock_safe().position()
|
||||
}
|
||||
|
||||
/// Get duration
|
||||
pub fn duration(&self) -> Option<f64> {
|
||||
self.backend.lock().unwrap().duration()
|
||||
self.backend.lock_safe().duration()
|
||||
}
|
||||
|
||||
/// Get queue reference
|
||||
@@ -502,27 +503,27 @@ impl PlayerController {
|
||||
|
||||
/// Toggle shuffle
|
||||
pub fn toggle_shuffle(&self) {
|
||||
self.queue.lock().unwrap().toggle_shuffle();
|
||||
self.queue.lock_safe().toggle_shuffle();
|
||||
}
|
||||
|
||||
/// Cycle repeat mode
|
||||
pub fn cycle_repeat(&self) {
|
||||
self.queue.lock().unwrap().cycle_repeat();
|
||||
self.queue.lock_safe().cycle_repeat();
|
||||
}
|
||||
|
||||
/// Check if shuffle is enabled
|
||||
pub fn is_shuffle(&self) -> bool {
|
||||
self.queue.lock().unwrap().is_shuffle()
|
||||
self.queue.lock_safe().is_shuffle()
|
||||
}
|
||||
|
||||
/// Get repeat mode
|
||||
pub fn repeat_mode(&self) -> RepeatMode {
|
||||
self.queue.lock().unwrap().repeat_mode()
|
||||
self.queue.lock_safe().repeat_mode()
|
||||
}
|
||||
|
||||
/// Get current volume (0.0 - 1.0)
|
||||
pub fn volume(&self) -> f32 {
|
||||
self.backend.lock().unwrap().volume()
|
||||
self.backend.lock_safe().volume()
|
||||
}
|
||||
|
||||
/// Check if muted
|
||||
@@ -532,35 +533,35 @@ impl PlayerController {
|
||||
|
||||
/// Set audio settings (crossfade, gapless, normalization)
|
||||
pub fn set_audio_settings(&mut self, settings: &AudioSettings) -> Result<(), PlayerError> {
|
||||
self.backend.lock().unwrap().set_audio_settings(settings)
|
||||
self.backend.lock_safe().set_audio_settings(settings)
|
||||
}
|
||||
|
||||
/// Get current audio settings
|
||||
pub fn audio_settings(&self) -> AudioSettings {
|
||||
self.backend.lock().unwrap().audio_settings()
|
||||
self.backend.lock_safe().audio_settings()
|
||||
}
|
||||
|
||||
// ===== Sleep Timer Methods =====
|
||||
|
||||
/// Set the event emitter for notifications
|
||||
pub fn set_event_emitter(&self, emitter: Arc<dyn PlayerEventEmitter>) {
|
||||
let mut event_emitter = self.event_emitter.lock().unwrap();
|
||||
let mut event_emitter = self.event_emitter.lock_safe();
|
||||
*event_emitter = Some(emitter);
|
||||
}
|
||||
|
||||
/// Get the event emitter
|
||||
pub fn event_emitter(&self) -> Option<Arc<dyn PlayerEventEmitter>> {
|
||||
self.event_emitter.lock().unwrap().clone()
|
||||
self.event_emitter.lock_safe().clone()
|
||||
}
|
||||
|
||||
/// Get sleep timer state
|
||||
pub fn sleep_timer_state(&self) -> SleepTimerState {
|
||||
self.sleep_timer.lock().unwrap().clone()
|
||||
self.sleep_timer.lock_safe().clone()
|
||||
}
|
||||
|
||||
/// Set sleep timer mode (in-memory only, not persisted)
|
||||
pub fn set_sleep_timer(&self, mode: SleepTimerMode) {
|
||||
let mut timer = self.sleep_timer.lock().unwrap();
|
||||
let mut timer = self.sleep_timer.lock_safe();
|
||||
timer.mode = mode.clone();
|
||||
if let SleepTimerMode::Time { end_time } = mode {
|
||||
let now = chrono::Utc::now().timestamp_millis();
|
||||
@@ -589,7 +590,7 @@ impl PlayerController {
|
||||
loop {
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
|
||||
let mut timer = sleep_timer.lock().unwrap();
|
||||
let mut timer = sleep_timer.lock_safe();
|
||||
if timer.is_active() {
|
||||
timer.update_remaining_seconds();
|
||||
|
||||
@@ -599,7 +600,7 @@ impl PlayerController {
|
||||
timer.cancel();
|
||||
|
||||
// Emit cancelled state
|
||||
if let Some(emitter) = event_emitter.lock().unwrap().as_ref() {
|
||||
if let Some(emitter) = event_emitter.lock_safe().as_ref() {
|
||||
emitter.emit(PlayerStatusEvent::SleepTimerChanged {
|
||||
mode: SleepTimerMode::Off,
|
||||
remaining_seconds: 0,
|
||||
@@ -608,14 +609,14 @@ impl PlayerController {
|
||||
drop(timer);
|
||||
|
||||
// Stop the backend
|
||||
if let Err(e) = backend.lock().unwrap().stop() {
|
||||
if let Err(e) = backend.lock_safe().stop() {
|
||||
error!("[SleepTimer] Failed to stop playback: {}", e);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Emit update event
|
||||
if let Some(emitter) = event_emitter.lock().unwrap().as_ref() {
|
||||
if let Some(emitter) = event_emitter.lock_safe().as_ref() {
|
||||
emitter.emit(PlayerStatusEvent::SleepTimerChanged {
|
||||
mode: timer.mode.clone(),
|
||||
remaining_seconds: timer.remaining_seconds,
|
||||
@@ -629,9 +630,9 @@ impl PlayerController {
|
||||
|
||||
/// Emit sleep timer changed event to frontend
|
||||
fn emit_sleep_timer_changed(&self) {
|
||||
let timer = self.sleep_timer.lock().unwrap().clone();
|
||||
let timer = self.sleep_timer.lock_safe().clone();
|
||||
|
||||
if let Some(emitter) = self.event_emitter.lock().unwrap().as_ref() {
|
||||
if let Some(emitter) = self.event_emitter.lock_safe().as_ref() {
|
||||
emitter.emit(PlayerStatusEvent::SleepTimerChanged {
|
||||
mode: timer.mode,
|
||||
remaining_seconds: timer.remaining_seconds,
|
||||
@@ -641,12 +642,12 @@ impl PlayerController {
|
||||
|
||||
/// Emit queue changed event to frontend
|
||||
pub fn emit_queue_changed(&self) {
|
||||
let queue = self.queue.lock().unwrap();
|
||||
let queue = self.queue.lock_safe();
|
||||
|
||||
debug!("PlayerController::emit_queue_changed() - Emitting queue with {} items, current_index: {:?}",
|
||||
queue.items().len(), queue.current_index());
|
||||
|
||||
if let Some(emitter) = self.event_emitter.lock().unwrap().as_ref() {
|
||||
if let Some(emitter) = self.event_emitter.lock_safe().as_ref() {
|
||||
emitter.emit(PlayerStatusEvent::QueueChanged {
|
||||
items: queue.items().to_vec(),
|
||||
current_index: queue.current_index(),
|
||||
@@ -664,19 +665,19 @@ impl PlayerController {
|
||||
|
||||
/// Get autoplay settings
|
||||
pub fn autoplay_settings(&self) -> AutoplaySettings {
|
||||
self.autoplay_settings.lock().unwrap().clone()
|
||||
self.autoplay_settings.lock_safe().clone()
|
||||
}
|
||||
|
||||
/// Set autoplay settings (in-memory only, persistence handled by command layer)
|
||||
pub fn set_autoplay_settings(&self, settings: AutoplaySettings) {
|
||||
let validated = settings.with_validated_countdown();
|
||||
*self.autoplay_settings.lock().unwrap() = validated;
|
||||
*self.autoplay_settings.lock_safe() = validated;
|
||||
}
|
||||
|
||||
/// Cancel active autoplay countdown
|
||||
pub fn cancel_autoplay_countdown(&self) {
|
||||
if let Some(cancel_flag) = self.countdown_cancel.lock().unwrap().as_ref() {
|
||||
*cancel_flag.lock().unwrap() = true;
|
||||
if let Some(cancel_flag) = self.countdown_cancel.lock_safe().as_ref() {
|
||||
*cancel_flag.lock_safe() = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -719,7 +720,7 @@ impl PlayerController {
|
||||
}
|
||||
|
||||
let current_item = {
|
||||
let queue = self.queue.lock().unwrap();
|
||||
let queue = self.queue.lock_safe();
|
||||
queue.current().cloned()
|
||||
};
|
||||
|
||||
@@ -729,7 +730,7 @@ impl PlayerController {
|
||||
|
||||
// Check sleep timer state
|
||||
let timer_mode = {
|
||||
let timer = self.sleep_timer.lock().unwrap();
|
||||
let timer = self.sleep_timer.lock_safe();
|
||||
timer.mode.clone()
|
||||
};
|
||||
|
||||
@@ -739,14 +740,14 @@ impl PlayerController {
|
||||
let now = chrono::Utc::now().timestamp_millis();
|
||||
if now >= *end_time {
|
||||
debug!("[PlayerController] Time-based sleep timer expired at track boundary");
|
||||
self.sleep_timer.lock().unwrap().cancel();
|
||||
self.sleep_timer.lock_safe().cancel();
|
||||
self.emit_sleep_timer_changed();
|
||||
return Ok(AutoplayDecision::Stop);
|
||||
}
|
||||
}
|
||||
SleepTimerMode::EndOfTrack => {
|
||||
// Stop at end of track
|
||||
self.sleep_timer.lock().unwrap().cancel();
|
||||
self.sleep_timer.lock_safe().cancel();
|
||||
self.emit_sleep_timer_changed();
|
||||
return Ok(AutoplayDecision::Stop);
|
||||
}
|
||||
@@ -756,7 +757,7 @@ impl PlayerController {
|
||||
&& self.is_episode_item(¤t).await;
|
||||
|
||||
if is_episode {
|
||||
let should_stop = self.sleep_timer.lock().unwrap().decrement_episode();
|
||||
let should_stop = self.sleep_timer.lock_safe().decrement_episode();
|
||||
self.emit_sleep_timer_changed();
|
||||
|
||||
if should_stop {
|
||||
@@ -773,7 +774,7 @@ impl PlayerController {
|
||||
// Note: This path is typically not hit for HTML5 video (which uses on_video_playback_ended).
|
||||
// It's here for the Android ExoPlayer path where video items may be in the backend queue.
|
||||
if current.media_type == MediaType::Video && self.is_episode_item(¤t).await {
|
||||
let repo = self.repository.lock().unwrap().clone();
|
||||
let repo = self.repository.lock_safe().clone();
|
||||
let jellyfin_id = current.jellyfin_id().unwrap_or(¤t.id);
|
||||
let next_ep_result = if let Some(repo) = &repo {
|
||||
self.fetch_next_episode_for_item(jellyfin_id, repo).await?
|
||||
@@ -782,7 +783,7 @@ impl PlayerController {
|
||||
None
|
||||
};
|
||||
if let Some(next_ep) = next_ep_result {
|
||||
let settings = self.autoplay_settings.lock().unwrap().clone();
|
||||
let settings = self.autoplay_settings.lock_safe().clone();
|
||||
|
||||
// Check if auto-play episode limit is reached
|
||||
let limit_reached = self.increment_autoplay_count();
|
||||
@@ -803,7 +804,7 @@ impl PlayerController {
|
||||
|
||||
// For audio/movies, check if there's a next track in the queue
|
||||
let has_next = {
|
||||
let queue = self.queue.lock().unwrap();
|
||||
let queue = self.queue.lock_safe();
|
||||
queue.has_next()
|
||||
};
|
||||
|
||||
@@ -837,7 +838,7 @@ impl PlayerController {
|
||||
|
||||
// Check sleep timer state
|
||||
let timer_mode = {
|
||||
let timer = self.sleep_timer.lock().unwrap();
|
||||
let timer = self.sleep_timer.lock_safe();
|
||||
timer.mode.clone()
|
||||
};
|
||||
|
||||
@@ -846,18 +847,18 @@ impl PlayerController {
|
||||
let now = chrono::Utc::now().timestamp_millis();
|
||||
if now >= *end_time {
|
||||
debug!("[PlayerController] Time-based sleep timer expired at video end");
|
||||
self.sleep_timer.lock().unwrap().cancel();
|
||||
self.sleep_timer.lock_safe().cancel();
|
||||
self.emit_sleep_timer_changed();
|
||||
return Ok(AutoplayDecision::Stop);
|
||||
}
|
||||
}
|
||||
SleepTimerMode::EndOfTrack => {
|
||||
self.sleep_timer.lock().unwrap().cancel();
|
||||
self.sleep_timer.lock_safe().cancel();
|
||||
self.emit_sleep_timer_changed();
|
||||
return Ok(AutoplayDecision::Stop);
|
||||
}
|
||||
SleepTimerMode::Episodes { .. } => {
|
||||
let should_stop = self.sleep_timer.lock().unwrap().decrement_episode();
|
||||
let should_stop = self.sleep_timer.lock_safe().decrement_episode();
|
||||
self.emit_sleep_timer_changed();
|
||||
if should_stop {
|
||||
return Ok(AutoplayDecision::Stop);
|
||||
@@ -868,7 +869,7 @@ impl PlayerController {
|
||||
|
||||
// Fetch next episode for the video that just ended
|
||||
if let Some(next_ep) = self.fetch_next_episode_for_item(item_id, &repo).await? {
|
||||
let settings = self.autoplay_settings.lock().unwrap().clone();
|
||||
let settings = self.autoplay_settings.lock_safe().clone();
|
||||
|
||||
let limit_reached = self.increment_autoplay_count();
|
||||
if limit_reached {
|
||||
@@ -961,7 +962,7 @@ impl PlayerController {
|
||||
pub fn start_autoplay_countdown(&self, _next_item: crate::repository::types::MediaItem, countdown_seconds: u32) {
|
||||
// Create cancellation flag
|
||||
let cancel_flag = Arc::new(Mutex::new(false));
|
||||
*self.countdown_cancel.lock().unwrap() = Some(cancel_flag.clone());
|
||||
*self.countdown_cancel.lock_safe() = Some(cancel_flag.clone());
|
||||
|
||||
let event_emitter = self.event_emitter.clone();
|
||||
|
||||
@@ -972,7 +973,7 @@ impl PlayerController {
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
|
||||
// Check cancellation
|
||||
if *cancel_flag.lock().unwrap() {
|
||||
if *cancel_flag.lock_safe() {
|
||||
log::info!("[PlayerController] Autoplay countdown cancelled");
|
||||
return;
|
||||
}
|
||||
@@ -980,7 +981,7 @@ impl PlayerController {
|
||||
remaining -= 1;
|
||||
|
||||
// Emit countdown tick event
|
||||
if let Some(emitter) = event_emitter.lock().unwrap().as_ref() {
|
||||
if let Some(emitter) = event_emitter.lock_safe().as_ref() {
|
||||
emitter.emit(PlayerStatusEvent::CountdownTick {
|
||||
remaining_seconds: remaining,
|
||||
});
|
||||
@@ -1081,7 +1082,7 @@ mod tests {
|
||||
// Verify initial state
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.items().len(), 5, "Queue should have 5 items");
|
||||
assert_eq!(queue_lock.current_index(), Some(0), "Should start at index 0");
|
||||
assert_eq!(queue_lock.current().unwrap().id, "item_0", "Current item should be item_0");
|
||||
@@ -1093,7 +1094,7 @@ mod tests {
|
||||
// Verify queue is intact and index advanced
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.items().len(), 5, "Queue should still have 5 items after skip");
|
||||
assert_eq!(queue_lock.current_index(), Some(1), "Index should advance to 1");
|
||||
assert_eq!(queue_lock.current().unwrap().id, "item_1", "Current item should be item_1");
|
||||
@@ -1112,7 +1113,7 @@ mod tests {
|
||||
// Verify queue still intact and index advanced again
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.items().len(), 5, "Queue should still have 5 items after second skip");
|
||||
assert_eq!(queue_lock.current_index(), Some(2), "Index should advance to 2");
|
||||
assert_eq!(queue_lock.current().unwrap().id, "item_2", "Current item should be item_2");
|
||||
@@ -1125,7 +1126,7 @@ mod tests {
|
||||
// Verify we're at the last item
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.items().len(), 5, "Queue should still have 5 items at end");
|
||||
assert_eq!(queue_lock.current_index(), Some(4), "Index should be at last item (4)");
|
||||
assert_eq!(queue_lock.current().unwrap().id, "item_4", "Current item should be item_4");
|
||||
@@ -1147,7 +1148,7 @@ mod tests {
|
||||
// Verify we're at the last item
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.current_index(), Some(2), "Should be at last item");
|
||||
}
|
||||
|
||||
@@ -1158,7 +1159,7 @@ mod tests {
|
||||
// Verify queue is still intact
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.items().len(), 3, "Queue should still have 3 items after skip at end");
|
||||
// When we skip past the end, the queue index should stay at the last item
|
||||
// or become None (depending on implementation)
|
||||
@@ -1187,7 +1188,7 @@ mod tests {
|
||||
// Verify we wrapped to the first item
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.items().len(), 3, "Queue should still have 3 items");
|
||||
assert_eq!(queue_lock.current_index(), Some(0), "Should wrap to index 0");
|
||||
assert_eq!(queue_lock.current().unwrap().id, "item_0", "Should be back at item_0");
|
||||
@@ -1206,7 +1207,7 @@ mod tests {
|
||||
// Verify starting position
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.current_index(), Some(3), "Should start at index 3");
|
||||
}
|
||||
|
||||
@@ -1216,7 +1217,7 @@ mod tests {
|
||||
// Verify queue is intact and index moved back
|
||||
{
|
||||
let queue = controller.queue();
|
||||
let queue_lock = queue.lock().unwrap();
|
||||
let queue_lock = queue.lock_safe();
|
||||
assert_eq!(queue_lock.items().len(), 5, "Queue should still have 5 items after previous");
|
||||
assert_eq!(queue_lock.current_index(), Some(2), "Index should move to 2");
|
||||
assert_eq!(queue_lock.current().unwrap().id, "item_2", "Current item should be item_2");
|
||||
@@ -1385,7 +1386,7 @@ mod tests {
|
||||
|
||||
// Set sleep timer to end of track
|
||||
{
|
||||
let mut timer = controller.sleep_timer.lock().unwrap();
|
||||
let mut timer = controller.sleep_timer.lock_safe();
|
||||
timer.mode = SleepTimerMode::EndOfTrack;
|
||||
}
|
||||
|
||||
@@ -1400,7 +1401,7 @@ mod tests {
|
||||
|
||||
// Verify timer was cancelled
|
||||
{
|
||||
let timer = controller.sleep_timer.lock().unwrap();
|
||||
let timer = controller.sleep_timer.lock_safe();
|
||||
assert!(
|
||||
matches!(timer.mode, SleepTimerMode::Off),
|
||||
"Sleep timer should be cancelled after EndOfTrack"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::utils::lock::MutexSafe;
|
||||
use log::{debug, error, info, warn};
|
||||
use super::backend::{PlayerBackend, PlayerError};
|
||||
use super::events::{PlayerEventEmitter, PlayerStatusEvent};
|
||||
@@ -190,7 +191,7 @@ impl MpvBackend {
|
||||
libmpv::events::Event::PlaybackRestart => {
|
||||
debug!("[MpvBackend] Playback started/resumed");
|
||||
|
||||
let media_id = state.lock().unwrap().current_media.as_ref().map(|m| m.id.clone());
|
||||
let media_id = state.lock_safe().current_media.as_ref().map(|m| m.id.clone());
|
||||
|
||||
if let Some(emitter) = &event_emitter {
|
||||
emitter.emit(PlayerStatusEvent::StateChanged {
|
||||
@@ -202,7 +203,7 @@ impl MpvBackend {
|
||||
libmpv::events::Event::PropertyChange { name, .. } if name == "pause" => {
|
||||
// Handle pause state changes
|
||||
if let Ok(is_paused) = mpv.get_property::<bool>("pause") {
|
||||
let media_id = state.lock().unwrap().current_media.as_ref().map(|m| m.id.clone());
|
||||
let media_id = state.lock_safe().current_media.as_ref().map(|m| m.id.clone());
|
||||
|
||||
if let Some(emitter) = &event_emitter {
|
||||
emitter.emit(PlayerStatusEvent::StateChanged {
|
||||
@@ -308,7 +309,7 @@ impl MpvBackend {
|
||||
if !is_paused {
|
||||
// Throttled progress reporting (every 30s)
|
||||
let jellyfin_id = {
|
||||
let state = state_for_position.lock().unwrap();
|
||||
let state = state_for_position.lock_safe();
|
||||
state.current_media.as_ref()
|
||||
.and_then(|m| m.jellyfin_id().map(|s| s.to_string()))
|
||||
};
|
||||
@@ -376,7 +377,7 @@ impl PlayerBackend for MpvBackend {
|
||||
|
||||
// Update state
|
||||
{
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let mut state = self.state.lock_safe();
|
||||
state.current_media = Some(media.clone());
|
||||
}
|
||||
|
||||
@@ -422,7 +423,7 @@ impl PlayerBackend for MpvBackend {
|
||||
message: format!("Failed to stop: {:?}", e),
|
||||
})?;
|
||||
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let mut state = self.state.lock_safe();
|
||||
state.current_media = None;
|
||||
|
||||
Ok(())
|
||||
@@ -460,7 +461,7 @@ impl PlayerBackend for MpvBackend {
|
||||
message: format!("Failed to set volume: {:?}", e),
|
||||
})?;
|
||||
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let mut state = self.state.lock_safe();
|
||||
state.volume = clamped;
|
||||
|
||||
Ok(())
|
||||
@@ -480,7 +481,7 @@ impl PlayerBackend for MpvBackend {
|
||||
}
|
||||
|
||||
fn state(&self) -> PlayerState {
|
||||
let state = self.state.lock().unwrap();
|
||||
let state = self.state.lock_safe();
|
||||
|
||||
if let Some(ref media) = state.current_media {
|
||||
let is_paused = self.mpv.get_property::<bool>("pause").unwrap_or(true);
|
||||
@@ -506,7 +507,7 @@ impl PlayerBackend for MpvBackend {
|
||||
}
|
||||
|
||||
fn volume(&self) -> f32 {
|
||||
let state = self.state.lock().unwrap();
|
||||
let state = self.state.lock_safe();
|
||||
state.volume
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user