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 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));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user