fix: play gif frames sequentially
Build And Push Container / build-and-push (push) Successful in 57s

This commit is contained in:
2026-06-30 23:13:29 +02:00
parent d299526606
commit f88b23493f
+185 -106
View File
@@ -19,6 +19,14 @@ use crate::{
const SYSTEM_FRAME_NAME: &str = "System Specs"; const SYSTEM_FRAME_NAME: &str = "System Specs";
const IDLE_SLEEP_MS: u64 = 750; const IDLE_SLEEP_MS: u64 = 750;
#[derive(Clone, Debug)]
struct PlaybackCursor {
slot_idx: usize,
frame_idx: usize,
slot_started_at: Instant,
frame_started_at: Instant,
}
pub(crate) fn initial_display_status(config: &DisplayConfig) -> DisplayStatus { pub(crate) fn initial_display_status(config: &DisplayConfig) -> DisplayStatus {
let mut status = DisplayStatus { let mut status = DisplayStatus {
native_enabled: config.native_enabled, native_enabled: config.native_enabled,
@@ -115,9 +123,9 @@ fn open_screen(config: &DisplayConfig) -> Result<AooScreen> {
fn run_display_session(state: &AppState, screen: &mut AooScreen) -> Result<()> { fn run_display_session(state: &AppState, screen: &mut AooScreen) -> Result<()> {
let mut snapshot = RotationSnapshot::default(); let mut snapshot = RotationSnapshot::default();
let mut cycle_started_at = Instant::now();
let mut current_frame_key: Option<String> = None; let mut current_frame_key: Option<String> = None;
let mut slots: Vec<RotationSlot> = Vec::new(); let mut slots: Vec<RotationSlot> = Vec::new();
let mut playback: Option<PlaybackCursor> = None;
loop { loop {
let monitor = match load_monitor_json_sync(&state.monitor_path) { let monitor = match load_monitor_json_sync(&state.monitor_path) {
@@ -136,9 +144,9 @@ fn run_display_session(state: &AppState, screen: &mut AooScreen) -> Result<()> {
let new_snapshot = rotation_snapshot(&monitor); let new_snapshot = rotation_snapshot(&monitor);
if new_snapshot != snapshot { if new_snapshot != snapshot {
snapshot = new_snapshot; snapshot = new_snapshot;
cycle_started_at = Instant::now();
current_frame_key = None; current_frame_key = None;
slots = build_rotation_slots(&snapshot, &state.image_dir); slots = build_rotation_slots(&snapshot, &state.image_dir);
playback = new_playback_cursor(&slots, Instant::now());
} }
update_display_status(&state.display_status, |status| { update_display_status(&state.display_status, |status| {
@@ -162,8 +170,8 @@ fn run_display_session(state: &AppState, screen: &mut AooScreen) -> Result<()> {
} }
let specs_only = snapshot.specs_enabled && slots.is_empty(); let specs_only = snapshot.specs_enabled && slots.is_empty();
if let Some((_slot_idx, active_frame)) = if let Some(active_frame) =
current_slot_frame(&slots, specs_only, snapshot.switch_time, cycle_started_at) current_slot_frame(&slots, specs_only, &mut playback, Instant::now())
{ {
let send_due = current_frame_key.as_deref() != Some(active_frame.key.as_str()); let send_due = current_frame_key.as_deref() != Some(active_frame.key.as_str());
if send_due { if send_due {
@@ -220,7 +228,8 @@ fn run_display_session(state: &AppState, screen: &mut AooScreen) -> Result<()> {
thread::sleep(rotation_sleep( thread::sleep(rotation_sleep(
snapshot.rotation_active(), snapshot.rotation_active(),
&slots, &slots,
cycle_started_at, specs_only,
&playback,
)); ));
} }
} }
@@ -296,83 +305,77 @@ fn build_rotation_slots(
fn current_slot_frame( fn current_slot_frame(
slots: &[RotationSlot], slots: &[RotationSlot],
specs_only: bool, specs_only: bool,
switch_time: u32, playback: &mut Option<PlaybackCursor>,
cycle_started_at: Instant, now: Instant,
) -> Option<(usize, ActiveFrame)> { ) -> Option<ActiveFrame> {
if specs_only { if specs_only {
return Some(( return Some(ActiveFrame {
0,
ActiveFrame {
key: SYSTEM_FRAME_NAME.into(), key: SYSTEM_FRAME_NAME.into(),
image_name: SYSTEM_FRAME_NAME.into(), image_name: SYSTEM_FRAME_NAME.into(),
display_name: SYSTEM_FRAME_NAME.into(), display_name: SYSTEM_FRAME_NAME.into(),
animated: false, animated: false,
}, });
));
} }
if slots.is_empty() { if slots.is_empty() {
return None; return None;
} }
let cycle_ms: u64 = slots if playback.is_none() {
.iter() *playback = new_playback_cursor(slots, now);
.map(|slot| slot.total_duration.as_millis() as u64)
.sum::<u64>()
.max((switch_time as u64).max(1) * 1000);
let elapsed_ms = (cycle_started_at.elapsed().as_millis() as u64) % cycle_ms;
let mut cursor = 0u64;
for (slot_idx, slot) in slots.iter().enumerate() {
let slot_ms = slot.total_duration.as_millis() as u64;
if elapsed_ms < cursor + slot_ms {
let local_ms = elapsed_ms - cursor;
return slot_frame_at(slot, slot_idx, local_ms);
}
cursor += slot_ms;
} }
let cursor = playback.as_mut()?;
advance_playback_cursor(cursor, slots, now);
slots slots
.last() .get(cursor.slot_idx)
.and_then(|slot| slot.frames.last().map(|(frame, _)| frame.clone())) .and_then(|slot| slot.frames.get(cursor.frame_idx))
.map(|frame| (slots.len() - 1, frame)) .map(|(frame, _)| frame.clone())
} }
fn slot_frame_at( fn new_playback_cursor(slots: &[RotationSlot], now: Instant) -> Option<PlaybackCursor> {
slot: &RotationSlot, if slots.is_empty() {
slot_idx: usize, None
local_ms: u64,
) -> Option<(usize, ActiveFrame)> {
if slot.frames.len() == 1 {
return Some((slot_idx, slot.frames[0].0.clone()));
}
let frame_cycle = frame_cycle_ms(slot);
let frame_elapsed_ms = if frame_cycle == 0 {
0
} else { } else {
local_ms % frame_cycle Some(PlaybackCursor {
}; slot_idx: 0,
let mut frame_cursor = 0u64; frame_idx: 0,
slot_started_at: now,
for (frame, delay) in &slot.frames { frame_started_at: now,
let delay_ms = delay.as_millis() as u64; })
if frame_elapsed_ms < frame_cursor + delay_ms {
return Some((slot_idx, frame.clone()));
} }
frame_cursor += delay_ms;
} }
slot.frames fn advance_playback_cursor(cursor: &mut PlaybackCursor, slots: &[RotationSlot], now: Instant) {
.last() if slots.is_empty() {
.map(|(frame, _)| (slot_idx, frame.clone())) return;
} }
fn frame_cycle_ms(slot: &RotationSlot) -> u64 { let mut guard = 0usize;
slot.frames while guard < slots.len().saturating_mul(2).max(1) {
.iter() guard += 1;
.map(|(_, delay)| delay.as_millis() as u64) let slot = &slots[cursor.slot_idx];
.sum::<u64>()
.max(1) if now.duration_since(cursor.slot_started_at) >= slot.total_duration {
cursor.slot_idx = (cursor.slot_idx + 1) % slots.len();
cursor.frame_idx = 0;
cursor.slot_started_at = now;
cursor.frame_started_at = now;
continue;
}
if slot.frames.len() <= 1 {
return;
}
let current_delay = slot.frames[cursor.frame_idx].1;
if now.duration_since(cursor.frame_started_at) >= current_delay {
cursor.frame_idx = (cursor.frame_idx + 1) % slot.frames.len();
cursor.frame_started_at = now;
continue;
}
return;
}
} }
fn prepare_screen_for_frame(screen: &mut AooScreen, frame: &ActiveFrame) { fn prepare_screen_for_frame(screen: &mut AooScreen, frame: &ActiveFrame) {
@@ -385,51 +388,37 @@ fn prepare_screen_for_frame(screen: &mut AooScreen, frame: &ActiveFrame) {
fn rotation_sleep( fn rotation_sleep(
rotation_active: bool, rotation_active: bool,
slots: &[RotationSlot], slots: &[RotationSlot],
cycle_started_at: Instant, specs_only: bool,
playback: &Option<PlaybackCursor>,
) -> Duration { ) -> Duration {
if !rotation_active || slots.is_empty() { if !rotation_active || specs_only || slots.is_empty() {
return Duration::from_millis(IDLE_SLEEP_MS);
}
let total_frames: usize = slots.iter().map(|slot| slot.frames.len()).sum();
if total_frames <= 1 {
return Duration::from_millis(IDLE_SLEEP_MS); return Duration::from_millis(IDLE_SLEEP_MS);
} }
let cycle_ms: u64 = slots let Some(cursor) = playback.as_ref() else {
.iter() return Duration::from_millis(IDLE_SLEEP_MS);
.map(|slot| slot.total_duration.as_millis() as u64) };
.sum::<u64>()
.max(1);
let elapsed_ms = (cycle_started_at.elapsed().as_millis() as u64) % cycle_ms;
let mut cursor = 0u64;
for slot in slots { let slot = &slots[cursor.slot_idx];
let slot_ms = slot.total_duration.as_millis() as u64;
if elapsed_ms < cursor + slot_ms {
if slot.frames.len() <= 1 { if slot.frames.len() <= 1 {
return Duration::from_millis(IDLE_SLEEP_MS); let remaining = slot
} .total_duration
let local_ms = elapsed_ms - cursor; .saturating_sub(cursor.slot_started_at.elapsed());
let frame_elapsed_ms = local_ms % frame_cycle_ms(slot); return remaining.clamp(
let mut frame_cursor = 0u64; Duration::from_millis(MIN_GIF_FRAME_DELAY_MS as u64),
for (_, delay) in &slot.frames { Duration::from_millis(IDLE_SLEEP_MS),
let delay_ms = delay.as_millis() as u64;
if frame_elapsed_ms < frame_cursor + delay_ms {
let frame_remaining = frame_cursor + delay_ms - frame_elapsed_ms;
let slot_remaining = slot_ms.saturating_sub(local_ms);
return Duration::from_millis(
frame_remaining
.min(slot_remaining)
.clamp(MIN_GIF_FRAME_DELAY_MS as u64, IDLE_SLEEP_MS),
); );
} }
frame_cursor += delay_ms;
}
}
cursor += slot_ms;
}
Duration::from_millis(250) let frame_delay = slot.frames[cursor.frame_idx].1;
let frame_remaining = frame_delay.saturating_sub(cursor.frame_started_at.elapsed());
let slot_remaining = slot
.total_duration
.saturating_sub(cursor.slot_started_at.elapsed());
frame_remaining.min(slot_remaining).clamp(
Duration::from_millis(MIN_GIF_FRAME_DELAY_MS as u64),
Duration::from_millis(IDLE_SLEEP_MS),
)
} }
pub(crate) fn read_display_status(status: &Arc<RwLock<DisplayStatus>>) -> DisplayStatus { pub(crate) fn read_display_status(status: &Arc<RwLock<DisplayStatus>>) -> DisplayStatus {
@@ -521,21 +510,110 @@ mod tests {
} }
#[test] #[test]
fn slot_frame_at_loops_animated_frames_for_full_slot() { fn playback_cursor_advances_frames_sequentially() {
let slot = RotationSlot { let slots = vec![RotationSlot {
total_duration: Duration::from_secs(10), total_duration: Duration::from_secs(10),
frames: vec![ frames: vec![
(test_frame("gif#0"), Duration::from_millis(100)), (test_frame("gif#0"), Duration::from_millis(100)),
(test_frame("gif#1"), Duration::from_millis(100)), (test_frame("gif#1"), Duration::from_millis(100)),
(test_frame("gif#2"), Duration::from_millis(100)), (test_frame("gif#2"), Duration::from_millis(100)),
], ],
}; }];
let start = Instant::now();
let mut cursor = new_playback_cursor(&slots, start);
assert_eq!(slot_frame_at(&slot, 0, 50).unwrap().1.key, "gif#0"); assert_eq!(
assert_eq!(slot_frame_at(&slot, 0, 150).unwrap().1.key, "gif#1"); current_slot_frame(
assert_eq!(slot_frame_at(&slot, 0, 250).unwrap().1.key, "gif#2"); &slots,
assert_eq!(slot_frame_at(&slot, 0, 350).unwrap().1.key, "gif#0"); false,
assert_eq!(slot_frame_at(&slot, 0, 9_950).unwrap().1.key, "gif#0"); &mut cursor,
start + Duration::from_millis(50)
)
.unwrap()
.key,
"gif#0"
);
assert_eq!(
current_slot_frame(
&slots,
false,
&mut cursor,
start + Duration::from_millis(150)
)
.unwrap()
.key,
"gif#1"
);
assert_eq!(
current_slot_frame(
&slots,
false,
&mut cursor,
start + Duration::from_millis(250)
)
.unwrap()
.key,
"gif#2"
);
assert_eq!(
current_slot_frame(
&slots,
false,
&mut cursor,
start + Duration::from_millis(350)
)
.unwrap()
.key,
"gif#0"
);
}
#[test]
fn playback_cursor_does_not_skip_overdue_frames() {
let slots = vec![RotationSlot {
total_duration: Duration::from_secs(10),
frames: vec![
(test_frame("gif#0"), Duration::from_millis(50)),
(test_frame("gif#1"), Duration::from_millis(50)),
(test_frame("gif#2"), Duration::from_millis(50)),
],
}];
let start = Instant::now();
let mut cursor = new_playback_cursor(&slots, start);
assert_eq!(
current_slot_frame(
&slots,
false,
&mut cursor,
start + Duration::from_millis(220)
)
.unwrap()
.key,
"gif#1"
);
assert_eq!(
current_slot_frame(
&slots,
false,
&mut cursor,
start + Duration::from_millis(221)
)
.unwrap()
.key,
"gif#1"
);
assert_eq!(
current_slot_frame(
&slots,
false,
&mut cursor,
start + Duration::from_millis(271)
)
.unwrap()
.key,
"gif#2"
);
} }
#[test] #[test]
@@ -547,8 +625,9 @@ mod tests {
(test_frame("gif#1"), Duration::from_millis(50)), (test_frame("gif#1"), Duration::from_millis(50)),
], ],
}]; }];
let playback = new_playback_cursor(&slots, Instant::now());
let sleep = rotation_sleep(true, &slots, Instant::now()); let sleep = rotation_sleep(true, &slots, false, &playback);
assert!(sleep <= Duration::from_millis(50)); assert!(sleep <= Duration::from_millis(50));
assert!(sleep >= Duration::from_millis(MIN_GIF_FRAME_DELAY_MS as u64)); assert!(sleep >= Duration::from_millis(MIN_GIF_FRAME_DELAY_MS as u64));
} }