mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-25 19:17:06 -06:00
Merge PR #587 from MMexicann: Media: preserve video frames and EOF across seeks
This commit is contained in:
+3
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~20 points and ~120–210 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (hours to ~95% per audience added; ready 56% → 55%; stage stays alpha; previous: major, full re-measure against PowerPoint for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (video-seek race fixed and regression-tested; previous: minor, hours to ~95% per audience added; ready 56% → 55%; stage stays alpha; previous: major, full re-measure against PowerPoint for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
DeckCraft aims at full PowerPoint parity, and to be better: faster, open (a documented zip+JSON
|
||||
format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and available everywhere
|
||||
@@ -131,6 +131,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-10-13 | Eyedropper: Shape Fill, Shape Outline, Font Color and every other colour menu end with an Eyedropper that takes the colour drawn at a point of the slide (hover shows it, a click applies it, Esc cancels); `slide.colorAt` gives the same from scripts and MCP (#367) |
|
||||
| 2026-10-13 | Slide show: Honeycomb's hexagons cover the whole slide again — the bottom quarter stayed black until the endpoint (#420) |
|
||||
| 2026-10-13 | Copy and Cut from the ribbon, the slide's right-click menu and the menus put the text on the system clipboard, as Ctrl+C and Ctrl+X do (#431) |
|
||||
| 2026-10-13 | Video seeks and loops keep newly decoded frames and end-of-stream notifications; stale generations are filtered when consumed (upstream #416). Deterministic queue coverage and repeated seeks across H.264, HEVC, VP9 and AV1 |
|
||||
| 2026-10-13 | Slide transitions like PowerPoint: every transition and option measured frame by frame against PowerPoint's video export and retuned (curves, directions, soft edges, 3-D perspective, contents moving over backgrounds); PowerPoint's own XML elements and defaults read and written (diagonal Wipe, Zoom, Shape In/Out, Glitter Hexagons, Shred In/Out, Page Curl Single); `deckcraft-cli render-transition` renders frames headless |
|
||||
| 2026-10-13 | Wild text cases measured against PowerPoint (102 synthetic slides): bidi by run language, East Asian vertical text upright, mongolianVert/wordArtVert/wordArtVertRtl, underline styles and strikethrough at the font's metrics, centre/right/decimal tabs, East Asian, Thai, Hindi, Arabic and Hebrew numbering, small caps, all-caps "ß", transparent text, soft hyphens, kinsoku, rtlCol and just/dist anchoring, emoji presentation, fallback faces that no longer move the baseline |
|
||||
| 2026-10-13 | Render matches PowerPoint more closely on wild shape, picture and table cases: even-odd custom paths, dashes and caps, arrowheads, radial and path gradients, tile rects, rotWithShape, gradient lines, shadows cast by what is drawn (group and perspective too), group fill, picture borders, tiles, reflection, recolour (biLevel, alphaInv, brightness/contrast), table borders and grid default, 20+ preset shapes by their ECMA formulas, default miter joins, and approximate 3-D bevels, cameras and face lighting |
|
||||
@@ -155,6 +156,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | trivial | Progress log: Eyedropper in the colour menus, slide.colorAt (#367) |
|
||||
| 2026-10-13 | trivial | Progress log: video seek frame-loss race and regression coverage |
|
||||
| 2026-10-13 | trivial | Progress log: wild text cases |
|
||||
| 2026-10-13 | trivial | Progress log: slide show for interactive decks |
|
||||
| 2026-10-12 | trivial | Progress log: File tab (Backstage, #76) |
|
||||
|
||||
@@ -66,8 +66,8 @@ impl VideoFeed {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let _ = self.tx.send(Cmd::Seek(t, self.generation));
|
||||
// Unblock the worker if it's waiting to hand over a stale frame.
|
||||
while self.rx.try_recv().is_ok() {}
|
||||
// pull() discards stale generations and unblocks the worker. Draining here could
|
||||
// also discard the new generation's frames or EOF if the worker handles seek fast.
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
self.dec.seek(t);
|
||||
@@ -178,3 +178,57 @@ fn worker(mut dec: VideoDecoder, cmds: std::sync::mpsc::Receiver<Cmd>, frames: s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn seek_keeps_new_generation_frames_and_end_of_stream() {
|
||||
let (tx, _commands) = std::sync::mpsc::channel();
|
||||
let (frames, rx) = std::sync::mpsc::sync_channel(AHEAD);
|
||||
let frame = |time| Frame { time, width: 1, height: 1, rgba: vec![0, 0, 0, 255] };
|
||||
frames.send((0, Some(frame(0.3)))).expect("stale frame");
|
||||
frames.send((0, None)).expect("stale end of stream");
|
||||
// Model a fast worker's seek response already waiting when seek drains the queue.
|
||||
frames.send((1, Some(frame(0.0)))).expect("new frame");
|
||||
frames.send((1, None)).expect("new end of stream");
|
||||
let mut feed = VideoFeed { size: (1, 1), current: None, next: None, generation: 0, ended: false, tx, rx };
|
||||
|
||||
feed.seek(0.05);
|
||||
let shown = feed.frame_at(0.05).expect("frame from the new generation");
|
||||
assert_eq!(shown.time, 0.0);
|
||||
assert!(feed.ended());
|
||||
|
||||
frames.send((2, Some(frame(0.3)))).expect("superseded seek frame");
|
||||
frames.send((2, None)).expect("superseded seek end of stream");
|
||||
frames.send((3, Some(frame(0.0)))).expect("latest seek frame");
|
||||
frames.send((3, None)).expect("latest seek end of stream");
|
||||
feed.seek(0.35);
|
||||
feed.seek(0.05);
|
||||
assert_eq!(feed.frame_at(0.05).expect("latest seek wins").time, 0.0);
|
||||
assert!(feed.ended());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seek_unblocks_worker_when_stale_queue_is_full() {
|
||||
let (tx, commands) = std::sync::mpsc::channel();
|
||||
let (frames, rx) = std::sync::mpsc::sync_channel(AHEAD);
|
||||
let frame = || Frame { time: 0.0, width: 1, height: 1, rgba: vec![0, 0, 0, 255] };
|
||||
for _ in 0..AHEAD {
|
||||
frames.send((0, Some(frame()))).expect("fill stale queue");
|
||||
}
|
||||
let worker = std::thread::spawn(move || {
|
||||
let Cmd::Seek(_, generation) = commands.recv().expect("seek command");
|
||||
frames.send((generation, Some(frame()))).expect("seek frame");
|
||||
frames.send((generation, None)).expect("seek end of stream");
|
||||
});
|
||||
let mut feed = VideoFeed { size: (1, 1), current: None, next: None, generation: 0, ended: false, tx, rx };
|
||||
|
||||
feed.seek(0.05);
|
||||
let shown = feed.frame_at_blocking(0.05, std::time::Duration::from_secs(5)).expect("worker unblocked");
|
||||
assert_eq!(shown.time, 0.0);
|
||||
assert!(feed.ended());
|
||||
worker.join().expect("worker completed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +312,23 @@ fn video_feed_follows_the_clock() {
|
||||
assert!(f.is_some_and(|v| (v - 0.5).abs() < 0.02), "{f:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_feed_repeated_seeks_keep_the_requested_frame() {
|
||||
let timeout = Duration::from_secs(5);
|
||||
for name in ["clip-h264.mov", "clip-hevc.mp4", "clip-vp9.webm", "clip-av1.mkv"] {
|
||||
let mut feed = deckcraft_media::VideoFeed::new(fixture(name)).expect("feed");
|
||||
for _ in 0..20 {
|
||||
// A decoder can finish this small clip before the consumer next polls it.
|
||||
// Rewinding must keep the new generation's first frame, even at end of stream.
|
||||
feed.seek(0.05);
|
||||
let frame = feed.frame_at_blocking(0.05, timeout).expect("frame after rewind");
|
||||
assert_eq!(frame.time, 0.0, "{name}");
|
||||
let frame = feed.frame_at_blocking(0.45, timeout).expect("frame after advancing");
|
||||
assert!((frame.time - 0.4).abs() < 0.02, "{name}: {}", frame.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_plays_decoded_audio_through_the_mixer() {
|
||||
use deckcraft_media::{ClipParams, Player, VoiceSpec};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Slide show and presenter view: parity with PowerPoint
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (interactive-deck clicks in the slide show; previous: major (created; measured from `crates/ui-egui/src/show.rs` and `crates/anim/src/show.rs` against PowerPoint 16.113's Slide Show tab and menu)) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (video seek frame-loss race fixed, upstream #416; previous: major, created; measured from `crates/ui-egui/src/show.rs` and `crates/anim/src/show.rs` against PowerPoint 16.113's Slide Show tab and menu) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
Presenting is the job PowerPoint is hired for. This doc covers the show window, presenter view,
|
||||
rehearsal and recording. Transition and animation playback are in
|
||||
@@ -24,6 +25,7 @@ ready** (estimated), **10–16 h** to parity.
|
||||
| A slide the show returns to (link, arrow keys) looks as it was left; a triggered animation goes on where it stopped | yes | yes | A main-sequence step still playing when the show leaves is finished |
|
||||
| "Advance on click" off: a click never leaves the slide (animation steps still play, keys still move on) | yes | yes | |
|
||||
| Media playback with controls, play across slides, full-screen video | yes | yes | WMA/WMV not decodable |
|
||||
| Media playback with controls, play across slides, full-screen video | yes | yes | Seeks and loops preserve new-generation video frames and end-of-stream; queue regression and repeated seeks across H.264/HEVC/VP9/AV1 in `crates/media`. WMA/WMV not decodable |
|
||||
| Animated GIFs play; pause from their corner button | yes | yes (#39) | |
|
||||
| See All Slides grid during the show | yes | no | |
|
||||
| Zoom into part of a slide during the show | yes | no | |
|
||||
@@ -77,4 +79,5 @@ Reading View in a window with navigation: present (checklist row done).
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | Click handling for interactive decks: triggers, per-slide state kept across visits, visible-shape hit testing with hand cursor, advance on click |
|
||||
| 2026-10-13 | minor | Video seek handoff no longer discards freshly decoded frames or end-of-stream (upstream #416); deterministic queue and four-codec seek regression tests |
|
||||
| 2026-10-10 | major | Created: show, presenter view, rehearse and record, compared with PowerPoint 16.113's Slide Show tab and menu |
|
||||
|
||||
Reference in New Issue
Block a user