Files
effectcraft/crates/media/src/lib.rs
T
Brandon ThomasandClaude Opus 5.5 6baff9f5fe Integration fixes for the community batch
- Footage sources: the cancellable wrapper (#784) forwards frame_with (#752 float curves)
  and problem (#887); problem's default lives on the trait.
- Media: a called-off decode (#784) is not a footage problem (#887).
- Effect Controls rename (#953): Escape cancels (egui drops focus on Escape the same
  frame), the Rename item is translated; regression test.
- i18n (#873): drop the /out-i18n/ ignore rule and the note about an uncommitted script.
- Tests: text range selector caps (#926) re-borrow the layer; the RAM preview test (#793)
  sets the comp's own Resolution (#462).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 04:27:59 -04:00

102 lines
4.3 KiB
Rust

//! Footage import for EffectCraft (layer **L3**: it implements `effectcraft_render::FootageSource`).
//!
//! - [`probe`] / [`probe_bytes`]: inspect a file and describe it as an [`effectcraft_project::Footage`]
//! (video, still, image sequence or audio; size, rate, duration, streams, codec, alpha).
//! - [`MediaPool`]: decodes footage frames on demand into [`effectcraft_raster::Image`]s
//! (premultiplied f32, sRGB/Rec.709-encoded values, as the compositor expects) with a
//! memory-budgeted LRU frame cache, one decoder per source, and the decoder kept positioned for
//! sequential playback. Also audio ([`MediaPool::audio_samples`]) and thumbnails.
//!
//! Video containers (MP4/MOV, Matroska/WebM, MXF OP1a / OP-Atom) and codecs (H.264, HEVC, VP9,
//! AV1, ProRes, DNxHD, MPEG-2, MJPEG; AAC, Opus, PCM, MP3/FLAC/Vorbis) come from FilmCraft's pure-Rust crates (git dependency,
//! pinned; `plan/adr/0001`). FilmCraft types never leave this crate. Stills use the `image` crate
//! (PNG, JPEG, GIF, WebP, TIFF, BMP, OpenEXR, Targa, Radiance HDR); Photoshop documents (merged image or one layer) come
//! from `effectcraft-psd` and SVG from `effectcraft-svg` (rasterised at any scale).
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
mod convert;
pub mod downmix;
mod dpx;
pub mod exr_channels;
mod gif_anim;
mod layered;
pub use layered::vector_doc;
mod pool;
mod probe;
mod stream;
mod webp_anim;
pub use pool::{DEFAULT_BUDGET, MediaPool, PoolStats};
pub use probe::{DEFAULT_SEQUENCE_RATE, probe, probe_bytes, probe_model};
/// Errors from probing or decoding footage.
#[derive(Debug, thiserror::Error)]
pub enum MediaError {
#[error("I/O error: {0}")]
Io(String),
#[error("unsupported media: {0}")]
Unsupported(String),
#[error("decode error: {0}")]
Decode(String),
/// The read was called off before it finished (see [`effectcraft_render::FootageSource::cancellable`]).
#[error("cancelled")]
Cancelled,
}
pub type Result<T> = std::result::Result<T, MediaError>;
impl From<filmcraft_media::MediaError> for MediaError {
fn from(e: filmcraft_media::MediaError) -> Self {
use filmcraft_media::MediaError as F;
match e {
F::Unsupported(s) => MediaError::Unsupported(s),
F::Io(s) | F::Offline(s) => MediaError::Io(s),
F::Cancelled => MediaError::Cancelled,
other => MediaError::Decode(other.to_string()),
}
}
}
/// File extensions recognised as stills.
pub const STILL_EXTENSIONS: &[&str] =
&["png", "jpg", "jpeg", "gif", "webp", "tif", "tiff", "bmp", "exr", "tga", "hdr", "psd", "psb", "svg", "pdf", "ai", "eps"];
/// File extensions recognised as audio-only files.
pub const AUDIO_EXTENSIONS: &[&str] = filmcraft_media::AUDIO_EXTENSIONS;
/// File extensions recognised as movies.
pub const VIDEO_EXTENSIONS: &[&str] = &["mp4", "m4v", "mov", "mkv", "webm", "mxf"];
/// File extensions recognised as 3D models (Advanced 3D model layers).
pub const MODEL_EXTENSIONS: &[&str] = &["gltf", "glb", "obj", "stl", "fbx"];
/// Whether `path` has an extension we can import.
pub fn is_importable(path: &std::path::Path) -> bool {
let ext = ext_of(path);
MODEL_EXTENSIONS.contains(&ext.as_str())
|| STILL_EXTENSIONS.contains(&ext.as_str())
|| AUDIO_EXTENSIONS.contains(&ext.as_str())
|| VIDEO_EXTENSIONS.contains(&ext.as_str())
|| dpx::EXTENSIONS.contains(&ext.as_str())
}
pub(crate) fn ext_of(path: &std::path::Path) -> String {
path.extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase()
}
/// The `image` crate format of a still: from its signature, else from its extension (Targa has
/// no signature; a `.hdr` without the Radiance one then fails with the decoder's own error).
pub(crate) fn still_format(path: &str, bytes: &[u8]) -> Option<image::ImageFormat> {
if let Ok(fmt) = image::guess_format(bytes) {
return Some(fmt);
}
match ext_of(std::path::Path::new(path)).as_str() {
"tga" => Some(image::ImageFormat::Tga),
"hdr" => Some(image::ImageFormat::Hdr),
_ => None,
}
}
pub(crate) fn rate_from_fc(r: filmcraft_time::FrameRate) -> effectcraft_time::FrameRate {
if r.num > 0 && r.den > 0 { effectcraft_time::FrameRate::new(r.num, r.den) } else { effectcraft_time::FrameRate::FPS_30 }
}