fix: play gif frames sequentially
Build And Push Container / build-and-push (push) Successful in 57s
Build And Push Container / build-and-push (push) Successful in 57s
This commit is contained in:
+185
-106
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user