Wire up playback reporting, fix duration flash, hide video from audio mini player
Playback reporting (position sync / resume-on-another-device): - player_configure_jellyfin now builds a PlaybackReporter sharing the player controller's Arc, so Start/Progress/Stopped actually reach Jellyfin on every auth path (login/restore/reauth); previously they never did. - The PlaybackReporterWrapper now shares the same Arc the controller and MPV progress loop report through, instead of a dead parallel Option. - Android position callbacks now emit throttled progress reports (30s/item), mirroring the MPV backend. Duration flash on pause: - resolveDuration() prefers the live store duration for the already-loaded track over the runTimeTicks estimate, so pausing no longer clobbers the slider's max to 0 when runTimeTicks is missing. Video leaking into audio mini player: - isVideoItem() also checks the backend PlayerMediaItem mediaType discriminator, so a video started via player_play_item (no Jellyfin `type`, mediaType "video") no longer surfaces in the audio mini player. Middle-truncation of long media names: - New truncateMiddle util applied to track/episode/card/mini-player titles so distinguishing tails (episode numbers, suffixes) stay visible. Adds regression tests for the duration and mini-player fixes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1826,9 +1826,10 @@ pub async fn player_get_cache_config(
|
||||
#[specta::specta]
|
||||
pub async fn player_configure_jellyfin(
|
||||
player: State<'_, PlayerStateWrapper>,
|
||||
db: State<'_, DatabaseWrapper>,
|
||||
server_url: String,
|
||||
access_token: String,
|
||||
_user_id: String,
|
||||
user_id: String,
|
||||
device_id: String,
|
||||
) -> Result<(), String> {
|
||||
log::info!("[PlayerCommand] Configuring Jellyfin client for playback reporting");
|
||||
@@ -1839,12 +1840,31 @@ pub async fn player_configure_jellyfin(
|
||||
device_id,
|
||||
};
|
||||
|
||||
let client = JellyfinClient::new(config)?;
|
||||
// Legacy client (used for remote session control / casting).
|
||||
let client = JellyfinClient::new(config.clone())?;
|
||||
|
||||
// Build the PlaybackReporter the player and backends (MPV + ExoPlayer)
|
||||
// actually report through. Without this, Start/Progress/Stopped never reach
|
||||
// Jellyfin, so playback position never syncs and you can't resume on another
|
||||
// device. The reporter shares the player controller's Arc, so populating it
|
||||
// here lights up reporting on both desktop and Android, on every auth path
|
||||
// that configures the player (login / restore / reauth).
|
||||
let db_service = {
|
||||
let database = db.0.lock().map_err(|e| e.to_string())?;
|
||||
Arc::new(database.service())
|
||||
};
|
||||
let reporter_client = JellyfinClient::new(config)?;
|
||||
let reporter = crate::playback_reporting::PlaybackReporter::new(
|
||||
db_service,
|
||||
Arc::new(TokioMutex::new(Some(reporter_client))),
|
||||
user_id,
|
||||
);
|
||||
|
||||
let controller = player.0.lock().await;
|
||||
controller.set_jellyfin_client(Some(client));
|
||||
controller.set_playback_reporter(Some(reporter)).await;
|
||||
|
||||
log::info!("[PlayerCommand] Jellyfin client configured successfully");
|
||||
log::info!("[PlayerCommand] Jellyfin client and playback reporter configured successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1858,8 +1878,9 @@ pub async fn player_disable_jellyfin(
|
||||
|
||||
let controller = player.0.lock().await;
|
||||
controller.set_jellyfin_client(None);
|
||||
controller.set_playback_reporter(None).await;
|
||||
|
||||
log::info!("[PlayerCommand] Jellyfin client disabled");
|
||||
log::info!("[PlayerCommand] Jellyfin client and playback reporter disabled");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1018,9 +1018,13 @@ pub fn run() {
|
||||
let repository_manager_wrapper = RepositoryManagerWrapper(repository_manager);
|
||||
app.manage(repository_manager_wrapper);
|
||||
|
||||
// Initialize playback reporter wrapper (initially empty, set on login)
|
||||
// Initialize playback reporter wrapper. This MUST share the same Arc
|
||||
// the player controller and MPV progress loop report through (created
|
||||
// above at `playback_reporter`), otherwise `playback_reporter_init`
|
||||
// would populate a dead, parallel Option and no Start/Progress/Stopped
|
||||
// would ever reach Jellyfin.
|
||||
info!("[INIT] Initializing playback reporter wrapper...");
|
||||
let playback_reporter_wrapper = PlaybackReporterWrapper(Arc::new(tokio::sync::Mutex::new(None)));
|
||||
let playback_reporter_wrapper = PlaybackReporterWrapper(playback_reporter.clone());
|
||||
app.manage(playback_reporter_wrapper);
|
||||
|
||||
info!("[INIT] Application setup completed successfully");
|
||||
|
||||
@@ -17,7 +17,8 @@ use super::backend::{PlayerBackend, PlayerError};
|
||||
use super::events::{PlayerStatusEvent, SharedEventEmitter};
|
||||
use super::media::{MediaItem, MediaType};
|
||||
use super::state::PlayerState;
|
||||
use crate::playback_reporting::{PlaybackReporter, EventThrottler};
|
||||
use crate::playback_reporting::{PlaybackReporter, EventThrottler, PlaybackOperation};
|
||||
use crate::utils::conversions::seconds_to_ticks;
|
||||
|
||||
/// Global reference to the JavaVM for JNI callbacks
|
||||
static JAVA_VM: OnceLock<JavaVM> = OnceLock::new();
|
||||
@@ -584,6 +585,75 @@ pub extern "system" fn Java_com_dtourolle_jellytau_player_JellyTauPlayer_nativeO
|
||||
} else {
|
||||
log::error!("[Android] WARNING: No event emitter for position update!");
|
||||
}
|
||||
|
||||
// Throttled progress reporting to Jellyfin so playback position syncs and can
|
||||
// be resumed on another device. ExoPlayer only fires position updates while
|
||||
// playing, but guard on the stored state anyway. Mirrors the MPV backend's
|
||||
// progress loop; both share the same EventThrottler (every 30s per item).
|
||||
report_android_progress(position);
|
||||
}
|
||||
|
||||
/// Report throttled playback progress to Jellyfin from the Android position
|
||||
/// callback. No-op until the reporter/throttler are wired (post-login) or when
|
||||
/// not actively playing.
|
||||
fn report_android_progress(position: f64) {
|
||||
let item_id = match SHARED_STATE.get() {
|
||||
Some(state) => {
|
||||
let state = state.lock_safe();
|
||||
if !state.state.is_playing() {
|
||||
return;
|
||||
}
|
||||
match state.current_media.as_ref().and_then(|m| m.jellyfin_id().map(|s| s.to_string())) {
|
||||
Some(id) => id,
|
||||
None => return,
|
||||
}
|
||||
}
|
||||
None => return,
|
||||
};
|
||||
|
||||
let throttler = match POSITION_THROTTLER.get() {
|
||||
Some(t) => t,
|
||||
None => return,
|
||||
};
|
||||
if !throttler.should_report(&item_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let reporter_arc = match PLAYBACK_REPORTER.get() {
|
||||
Some(r) => r.clone(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
let position_ticks = seconds_to_ticks(position);
|
||||
let item_id_for_task = item_id.clone();
|
||||
|
||||
// The reporter is async; the JNI callback is sync. Spawn onto the Tokio
|
||||
// runtime when present, otherwise a throwaway runtime on a new thread.
|
||||
let spawn_report = move || async move {
|
||||
let reporter_guard = reporter_arc.lock().await;
|
||||
if let Some(reporter) = reporter_guard.as_ref() {
|
||||
let operation = PlaybackOperation::Progress {
|
||||
item_id: item_id_for_task.clone(),
|
||||
position_ticks,
|
||||
is_paused: false,
|
||||
};
|
||||
match reporter.report(operation, true).await {
|
||||
Ok(_) => debug!("[Android] Reported progress for {}", item_id_for_task),
|
||||
Err(e) => log::warn!("[Android] Failed to report progress: {}", e),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||
handle.spawn(spawn_report());
|
||||
} else {
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(spawn_report());
|
||||
});
|
||||
}
|
||||
|
||||
throttler.mark_reported(&item_id);
|
||||
}
|
||||
|
||||
/// Called when player state changes.
|
||||
|
||||
@@ -165,9 +165,8 @@ impl PlayerController {
|
||||
self.jellyfin_client.clone()
|
||||
}
|
||||
|
||||
/// Configure the playback reporter for dual sync (local DB + server)
|
||||
/// Will be called from initialization commands after login
|
||||
#[allow(dead_code)]
|
||||
/// 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<PlaybackReporter>) {
|
||||
let mut reporter_guard = self.playback_reporter.lock().await;
|
||||
*reporter_guard = reporter;
|
||||
|
||||
Reference in New Issue
Block a user