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 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 {
let mut status = DisplayStatus {
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<()> {
let mut snapshot = RotationSnapshot::default();
let mut cycle_started_at = Instant::now();
let mut current_frame_key: Option<String> = None;
let mut slots: Vec<RotationSlot> = Vec::new();
let mut playback: Option<PlaybackCursor> = None;
loop {
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);
if new_snapshot != snapshot {
snapshot = new_snapshot;
cycle_started_at = Instant::now();
current_frame_key = None;
slots = build_rotation_slots(&snapshot, &state.image_dir);
playback = new_playback_cursor(&slots, Instant::now());
}
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();
if let Some((_slot_idx, active_frame)) =
current_slot_frame(&slots, specs_only, snapshot.switch_time, cycle_started_at)
if let Some(active_frame) =
current_slot_frame(&slots, specs_only, &mut playback, Instant::now())
{
let send_due = current_frame_key.as_deref() != Some(active_frame.key.as_str());
if send_due {
@@ -220,7 +228,8 @@ fn run_display_session(state: &AppState, screen: &mut AooScreen) -> Result<()> {
thread::sleep(rotation_sleep(
snapshot.rotation_active(),
&slots,
cycle_started_at,
specs_only,
&playback,
));
}
}
@@ -296,83 +305,77 @@ fn build_rotation_slots(
fn current_slot_frame(
slots: &[RotationSlot],
specs_only: bool,
switch_time: u32,
cycle_started_at: Instant,
) -> Option<(usize, ActiveFrame)> {
playback: &mut Option<PlaybackCursor>,
now: Instant,
) -> Option<ActiveFrame> {
if specs_only {
return Some((
0,
ActiveFrame {
return Some(ActiveFrame {
key: SYSTEM_FRAME_NAME.into(),
image_name: SYSTEM_FRAME_NAME.into(),
display_name: SYSTEM_FRAME_NAME.into(),
animated: false,
},
));
});
}
if slots.is_empty() {
return None;
}
let cycle_ms: u64 = slots
.iter()
.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;
if playback.is_none() {
*playback = new_playback_cursor(slots, now);
}
let cursor = playback.as_mut()?;
advance_playback_cursor(cursor, slots, now);
slots
.last()
.and_then(|slot| slot.frames.last().map(|(frame, _)| frame.clone()))
.map(|frame| (slots.len() - 1, frame))
.get(cursor.slot_idx)
.and_then(|slot| slot.frames.get(cursor.frame_idx))
.map(|(frame, _)| frame.clone())
}
fn slot_frame_at(
slot: &RotationSlot,
slot_idx: usize,
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
fn new_playback_cursor(slots: &[RotationSlot], now: Instant) -> Option<PlaybackCursor> {
if slots.is_empty() {
None
} else {
local_ms % frame_cycle
};
let mut frame_cursor = 0u64;
for (frame, delay) in &slot.frames {
let delay_ms = delay.as_millis() as u64;
if frame_elapsed_ms < frame_cursor + delay_ms {
return Some((slot_idx, frame.clone()));
Some(PlaybackCursor {
slot_idx: 0,
frame_idx: 0,
slot_started_at: now,
frame_started_at: now,
})
}
frame_cursor += delay_ms;
}
slot.frames
.last()
.map(|(frame, _)| (slot_idx, frame.clone()))
fn advance_playback_cursor(cursor: &mut PlaybackCursor, slots: &[RotationSlot], now: Instant) {
if slots.is_empty() {
return;
}
fn frame_cycle_ms(slot: &RotationSlot) -> u64 {
slot.frames
.iter()
.map(|(_, delay)| delay.as_millis() as u64)
.sum::<u64>()
.max(1)
let mut guard = 0usize;
while guard < slots.len().saturating_mul(2).max(1) {
guard += 1;
let slot = &slots[cursor.slot_idx];
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) {
@@ -385,51 +388,37 @@ fn prepare_screen_for_frame(screen: &mut AooScreen, frame: &ActiveFrame) {
fn rotation_sleep(
rotation_active: bool,
slots: &[RotationSlot],
cycle_started_at: Instant,
specs_only: bool,
playback: &Option<PlaybackCursor>,
) -> Duration {
if !rotation_active || 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 {
if !rotation_active || specs_only || slots.is_empty() {
return Duration::from_millis(IDLE_SLEEP_MS);
}
let cycle_ms: u64 = slots
.iter()
.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;
let Some(cursor) = playback.as_ref() else {
return Duration::from_millis(IDLE_SLEEP_MS);
};
for slot in slots {
let slot_ms = slot.total_duration.as_millis() as u64;
if elapsed_ms < cursor + slot_ms {
let slot = &slots[cursor.slot_idx];
if slot.frames.len() <= 1 {
return Duration::from_millis(IDLE_SLEEP_MS);
}
let local_ms = elapsed_ms - cursor;
let frame_elapsed_ms = local_ms % frame_cycle_ms(slot);
let mut frame_cursor = 0u64;
for (_, delay) in &slot.frames {
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),
let remaining = slot
.total_duration
.saturating_sub(cursor.slot_started_at.elapsed());
return remaining.clamp(
Duration::from_millis(MIN_GIF_FRAME_DELAY_MS as u64),
Duration::from_millis(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 {
@@ -521,21 +510,110 @@ mod tests {
}
#[test]
fn slot_frame_at_loops_animated_frames_for_full_slot() {
let slot = RotationSlot {
fn playback_cursor_advances_frames_sequentially() {
let slots = vec![RotationSlot {
total_duration: Duration::from_secs(10),
frames: vec![
(test_frame("gif#0"), Duration::from_millis(100)),
(test_frame("gif#1"), 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!(slot_frame_at(&slot, 0, 150).unwrap().1.key, "gif#1");
assert_eq!(slot_frame_at(&slot, 0, 250).unwrap().1.key, "gif#2");
assert_eq!(slot_frame_at(&slot, 0, 350).unwrap().1.key, "gif#0");
assert_eq!(slot_frame_at(&slot, 0, 9_950).unwrap().1.key, "gif#0");
assert_eq!(
current_slot_frame(
&slots,
false,
&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]
@@ -547,8 +625,9 @@ mod tests {
(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(MIN_GIF_FRAME_DELAY_MS as u64));
}