Skip to main content

wisp_storybook/stories/
s_video_frame_handoff.rs

1//! Story: synthetic `decode::VideoFrame` → wisp `VideoTexture` → Sprite
2//! (M-MEDIA.12 / AUT-108).
3//!
4//! Proves the second seam — the video-side counterpart to the audio
5//! `WaveformBarRect` handoff. The frame is constructed by hand
6//! (decode crate, BGRA bytes) and uploaded to wisp's pre-existing
7//! `VideoTexture`. wisp doesn't know where the bytes came from.
8
9use glam::Vec2;
10use media::VideoFrame;
11use wisp::application::Application;
12use wisp::texture::video_texture::VideoTexture;
13use wisp::{Sprite, Stage};
14
15use crate::story::Story;
16
17pub fn story() -> Story {
18    Story {
19        id: "video-frame-handoff",
20        category: "Media",
21        title: "VideoFrame → VideoTexture",
22        milestone: "M-MEDIA.12",
23        writeup: include_str!("writeups/video_frame_handoff.md"),
24        build,
25        tick: None,
26    }
27}
28
29fn build(app: &Application, stage: &mut Stage) {
30    // 128×72 synthetic BGRA frame — a smooth diagonal gradient with
31    // a few colored stripes. Deterministic.
32    let frame = synthetic_frame(128, 72);
33
34    let video_tex = VideoTexture::new(app, frame.width, frame.height);
35    video_tex.upload_bgra(app, &frame.bgra);
36
37    let mut sprite =
38        Sprite::from_texture(video_tex.texture().clone()).with_anchor(Vec2::splat(0.5));
39    // Fill ~75% of the NDC viewport, preserving the 128:72 aspect.
40    sprite.container.transform.scale = Vec2::new(1.5, 1.5 * 72.0 / 128.0);
41    let _ = stage.add_child(stage.root(), sprite);
42}
43
44fn synthetic_frame(width: u32, height: u32) -> VideoFrame {
45    let pixels_w = width as usize;
46    let pixels_h = height as usize;
47    let mut bgra = vec![0u8; pixels_w * pixels_h * 4];
48    for row in 0..pixels_h {
49        for col in 0..pixels_w {
50            let idx = (row * pixels_w + col) * 4;
51            // Diagonal gradient — interesting per-pixel content.
52            // Each numerator stays ≤ 255 by construction.
53            let red = u8::try_from((col * 255) / pixels_w).unwrap_or(255);
54            let green = u8::try_from((row * 255) / pixels_h).unwrap_or(255);
55            let blue = u8::try_from(((col + row) * 255) / (pixels_w + pixels_h)).unwrap_or(255);
56            // Horizontal stripes every 12 rows give the snapshot fingerprint
57            // enough cross-quadrant variance to detect orientation regressions.
58            let stripe = if row % 12 < 3 { 60 } else { 0 };
59            bgra[idx] = blue.saturating_add(stripe); // B
60            bgra[idx + 1] = green; // G
61            bgra[idx + 2] = red.saturating_add(stripe); // R
62            bgra[idx + 3] = 255; // A
63        }
64    }
65    VideoFrame {
66        width,
67        height,
68        bgra,
69        pts_seconds: 0.0,
70        frame_index: 0,
71    }
72}