makepad/platform/video/tests/fragmented_growing.rs
Admin 9b404d6460 platform: GPU passes store top-left rows on every backend, hosted apps get software frames as textures, the Android desk renders at 120 Hz, and the platform builds warning-free on every CI target
The platform part of work ebf442a94..76627b6f1, as one step that builds (makepad-platform checks for macOS and wasm32):

- wm: the Android super-app compiles tiles on the phone against the host engine
- cargo-makepad: the super-app APK is packed by `android dyn-pack`, in Rust
- widgets, storybook: the widget catalogue app, some eighty new widgets, a theme store with mixable style sheets, and the rule that a press belongs to whatever
- platform: the Android desk swipes at 120 Hz — offscreen render passes and framebuffers cached, SVG meshes kept, font families that complete
- ai stems: LaneDemixer, VocalsModel and the platform Stage origin/main needs
- platform, widgets, wm: every Vulkan pass renders through a negative-height viewport again, so a Wayland or X11 window stands upright, and no capture consumer
- platform: every OpenGL texture pass stores top-left rows, custom cameras included, and culls with the winding its inverted projection gives
- platform, wm, director: a hosted app's software frame arrives as a top-left texture and the window manager and the director carry no flip term at all
- The regex engine accepts \b and \B, reports the earliest match so a scan can resume after it, and can decide word boundaries on ASCII so a code search never f
- cef, video, platform: what Stage's browser needs from the shared layers
- platform, wm: work builds again on iOS, tvOS and wasm, and the window manager is warning-free on Windows
- stitch: the crate says what the rest of the repository says about its license
- platform: no warnings on tvOS and linux_direct, and a missing pty helper says so
- tests: the workspace suite, run as a whole for the first time, passes outside the example UI tests
- platform: a hidden window wears no hands-off frame
- libs: no warnings in the workspace check on any row
- tools: makepad-screen is makepad-agents, and its binary is `agents`
- platform: the storage module says how much a volume has free
- git: the memory ledger can take what it is asked for
- ci: a driven app takes every key once, and the storage module builds for the browser

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 20:08:13 +02:00

103 lines
4 KiB
Rust

//! `VideoFileEncoder::new_fragmented` — is the file on disk a movie a
//! reader can open while the encoder is still writing it?
//!
//! Stage's WEB cache records a page's audio and picture into one mp4 that
//! a DJ deck and a VJ slot read from behind the recorder, so the container
//! has to be readable before `finish`. A plain movie is not: its index is
//! written last. A fragmented movie is: the header goes down first and
//! every interval closes a fragment behind the frames.
//!
//! Runs for real on Apple only (the platform that honours the interval);
//! elsewhere the fragmented constructor writes one movie and this test
//! only checks it still finishes.
use makepad_video::{
PcmAudioTrackOptions, VideoFileCodec, VideoFileDecoder, VideoFileEncoder,
VideoFileEncoderOptions,
};
const WIDTH: u32 = 320;
const HEIGHT: u32 = 192;
const FPS: u32 = 30;
const SECONDS: usize = 4;
const RATE: u32 = 48_000;
fn frame(index: usize) -> Vec<u8> {
let (w, h) = (WIDTH as usize, HEIGHT as usize);
let mut nv12 = vec![128u8; w * h * 3 / 2];
let level = 16 + ((index * 7) % 200) as u8;
for y in 0..h {
for x in 0..w {
nv12[y * w + x] = if x < w / 2 { level } else { 235 - level };
}
}
nv12
}
fn options() -> VideoFileEncoderOptions {
VideoFileEncoderOptions {
codec: VideoFileCodec::H264,
width: WIDTH,
height: HEIGHT,
fps_num: FPS,
fps_den: 1,
video_bitrate_bps: 6_000_000,
audio: Some(PcmAudioTrackOptions { sample_rate: RATE, channels: 2, aac_bitrate_bps: 128_000 }),
keyframe_only: true,
}
}
fn frames_readable(path: &str) -> Result<usize, String> {
let mut decoder = VideoFileDecoder::open(path).map_err(|e| e.to_string())?;
let mut count = 0;
while decoder.next_frame().map_err(|e| e.to_string())?.is_some() {
count += 1;
}
Ok(count)
}
#[test]
fn a_fragmented_recording_opens_before_it_finishes() {
let dir = std::env::temp_dir().join(format!("makepad-fragmented-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("growing.mp4");
let text = path.to_string_lossy().to_string();
let mut encoder = match VideoFileEncoder::new_fragmented(&text, options(), 0.5) {
Ok(encoder) => encoder,
Err(e) if e.context.contains("not implemented") => return,
Err(e) => panic!("encoder: {e:?}"),
};
let per_frame_audio: Vec<i16> = (0..(RATE as usize / FPS as usize) * 2)
.map(|i| ((i / 2) % 100) as i16 * 200 - 10_000)
.collect();
let total = SECONDS * FPS as usize;
for index in 0..total {
let pts = (index as i64) * 10_000_000 / FPS as i64;
encoder.push_frame_nv12(&frame(index), Some(pts)).expect("push frame");
encoder.push_audio_i16(&per_frame_audio).expect("push audio");
}
// Still recording: the file must already be a movie with most of the
// frames in it. The writer keeps about a second in its own pipeline
// and the last fragment is still open, so a reader behind the
// recorder by up to two seconds is the expectation, not a failure.
if cfg!(target_vendor = "apple") {
let readable = frames_readable(&text).expect("a fragmented movie opens while it is being written");
let lag_frames = 2 * FPS as usize;
assert!(
readable + lag_frames >= total && readable > 0,
"{readable} of {total} frames readable before finish"
);
// A reader that opens later sees more, never less.
let again = frames_readable(&text).expect("opens again");
assert!(again >= readable, "{again} < {readable}");
}
encoder.finish().expect("finish");
let whole = frames_readable(&text).expect("the finished movie opens");
assert_eq!(whole, total, "every frame once finished");
let info = VideoFileDecoder::open(&text).expect("open").info().clone();
assert!(info.has_audio, "the audio track is in the same file");
let _ = std::fs::remove_dir_all(&dir);
}