Wire up playback reporting, fix duration flash, hide video from audio mini player
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m14s
Traceability Validation / Check Requirement Traces (push) Successful in 21s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 19m3s

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:
2026-07-01 21:52:27 +02:00
co-authored by Claude Opus 4.8
parent dcee342c47
commit 342f95cac1
21 changed files with 420 additions and 28 deletions
+71 -1
View File
@@ -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.
+2 -3
View File
@@ -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;