Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions crates/rustmotion-components/src/gif.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,9 +455,9 @@ mod tests {
/// own video is, so passing generous `max_w`/`max_h` here isolates the
/// byte-budget check from the video-dimensions check exercised by the
/// next test. 144 MiB is deliberately far short of the 65535×65535
/// (~17 GiB) header the audit's own crafted file could declare — large
/// enough to prove the budget check fires, small enough that running
/// this test never risks the allocation it is asserting never happens.
/// (~17 GiB) header a real crafted file could declare — large enough to
/// prove the budget check fires, small enough that running this test
/// never risks the allocation it is asserting never happens.
#[test]
fn a_canvas_over_the_byte_budget_is_rejected_without_allocating_it() {
let path = std::env::temp_dir().join(format!(
Expand Down
126 changes: 11 additions & 115 deletions crates/rustmotion-components/src/video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use rustmotion_core::css::CssStyle;
use rustmotion_core::engine::animator::AnimatedProperties;
use rustmotion_core::engine::layout_pass::BoxLayout;
use rustmotion_core::engine::renderer::{
extract_video_frame, find_closest_frame, probe_video_metadata, video_frame_cache,
extract_video_frame, find_closest_frame, video_frame_cache,
};
use rustmotion_core::schema::{ImageFit, TimelineStep};
use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig};
Expand Down Expand Up @@ -46,116 +46,6 @@ rustmotion_core::impl_traits!(Video {
Styled => style,
});

/// The rectangle an `img_w`×`img_h` source draws into to honour `fit` inside
/// a `target_w`×`target_h` box — the same three CSS `object-fit` semantics
/// `image.rs`'s painter already implements for the `image` component.
fn fit_rect(fit: &ImageFit, img_w: f32, img_h: f32, target_w: f32, target_h: f32) -> Rect {
match fit {
ImageFit::Fill => Rect::from_xywh(0.0, 0.0, target_w, target_h),
ImageFit::Contain => {
let scale = (target_w / img_w).min(target_h / img_h);
let w = img_w * scale;
let h = img_h * scale;
Rect::from_xywh((target_w - w) / 2.0, (target_h - h) / 2.0, w, h)
}
ImageFit::Cover => {
let scale = (target_w / img_w).max(target_h / img_h);
let w = img_w * scale;
let h = img_h * scale;
Rect::from_xywh((target_w - w) / 2.0, (target_h - h) / 2.0, w, h)
}
}
}

/// Draws `img` into `layout`'s box according to `fit`, clipping to the box
/// for `Cover` (the only mode whose fitted rectangle can extend past it).
fn draw_fitted(canvas: &Canvas, img: skia_safe::Image, fit: &ImageFit, layout: &BoxLayout) {
let dst = fit_rect(
fit,
img.width() as f32,
img.height() as f32,
layout.width,
layout.height,
);
let paint = Paint::default();
if matches!(fit, ImageFit::Cover) {
canvas.save();
canvas.clip_rect(
Rect::from_xywh(0.0, 0.0, layout.width, layout.height),
skia_safe::ClipOp::Intersect,
true,
);
canvas.draw_image_rect(img, None, dst, &paint);
canvas.restore();
} else {
canvas.draw_image_rect(img, None, dst, &paint);
}
}

/// The source clip's own duration, probed via `ffprobe` and memoized per
/// `src` for the life of the process — `effective_source_time` below is
/// called once per painted frame, and re-probing on every one of them would
/// mean one subprocess spawn per frame for any looping video with no
/// explicit `trim_end`. `None` on a probe failure (no ffprobe on `PATH`, or
/// the source can't be read) is memoized too, so a broken source fails fast
/// on every subsequent frame instead of retrying the same failing probe.
fn video_duration_secs(src: &str) -> Option<f64> {
static CACHE: std::sync::OnceLock<
std::sync::Mutex<std::collections::HashMap<String, Option<f64>>>,
> = std::sync::OnceLock::new();
let cache = CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));

if let Some(hit) = cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(src)
{
return *hit;
}
let probed = probe_video_metadata(src).ok().map(|p| p.duration_secs);
cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(src.to_string(), probed);
probed
}

impl Video {
/// The timestamp to sample from the source clip for a given scene time.
///
/// Honours `trim_end` on the picture the same way the audio track
/// already does: past `trim_end`, playback holds on the last in-window
/// frame instead of continuing to draw whatever the source contains
/// beyond the intended trim point. When `loop_video` is set, playback
/// wraps within `[trim_start, trim_end)` instead of clamping — falling
/// back to the source's own probed duration as the loop window only
/// when `trim_end` is absent, since that is the only case where the
/// window cannot otherwise be known at all.
fn effective_source_time(&self, ctx_time: f64) -> f64 {
let rate = self.playback_rate.unwrap_or(1.0);
let trim_start = self.trim_start.unwrap_or(0.0);
let raw = trim_start + ctx_time * rate;

if let Some(end) = self.trim_end {
if end > trim_start {
return if self.loop_video == Some(true) {
trim_start + (raw - trim_start).rem_euclid(end - trim_start)
} else {
raw.min(end)
};
}
} else if self.loop_video == Some(true) {
if let Some(duration) = video_duration_secs(&self.src) {
if duration > trim_start {
return trim_start + (raw - trim_start).rem_euclid(duration - trim_start);
}
}
}

raw
}
}

impl Painter for Video {
fn paint_content(
&self,
Expand All @@ -164,7 +54,9 @@ impl Painter for Video {
_props: &AnimatedProperties,
ctx: &PaintCtx,
) {
let source_time = self.effective_source_time(ctx.time);
let rate = self.playback_rate.unwrap_or(1.0);
let trim_start = self.trim_start.unwrap_or(0.0);
let source_time = trim_start + ctx.time * rate;
let width = layout.width as u32;
let height = layout.height as u32;

Expand All @@ -176,13 +68,15 @@ impl Painter for Video {
let img_info = ImageInfo::new(
(fw as i32, fh as i32),
ColorType::RGBA8888,
skia_safe::AlphaType::Unpremul,
skia_safe::AlphaType::Premul,
None,
);
let row_bytes = fw as usize * 4;
let data = skia_safe::Data::new_copy(rgba);
if let Some(img) = skia_safe::images::raster_from_data(&img_info, data, row_bytes) {
draw_fitted(canvas, img, &self.fit, layout);
let dst = Rect::from_xywh(0.0, 0.0, layout.width, layout.height);
let paint = Paint::default();
canvas.draw_image_rect(img, None, dst, &paint);
}
return;
}
Expand Down Expand Up @@ -211,7 +105,9 @@ impl Painter for Video {
};
let skia_data = skia_safe::Data::new_copy(&frame_data);
if let Some(img) = skia_safe::Image::from_encoded(skia_data) {
draw_fitted(canvas, img, &self.fit, layout);
let dst = Rect::from_xywh(0.0, 0.0, layout.width, layout.height);
let paint = Paint::default();
canvas.draw_image_rect(img, None, dst, &paint);
}
}
}
Expand Down
Loading
Loading