Export, Media: codecs and containers from craft-libs; remove hevcenc, av1enc, vp9enc (#3522)

* Export, Media: H.264, ProRes, HEVC, VP9, AV1, AAC, MP4 and Matroska come from craft-libs

crates/export and crates/media use craft-video, craft-audio and craft-media
(one craft-libs rev) instead of filmcraft-h264enc, -prores, -aac, -isobmff,
-matroska and -opus. The av1enc, hevcenc and vp9enc crates are removed:
craft-video carries the same encoders, with the later changes merged in
storytold/craft-libs#829. Their round-trip tests move to crates/export/tests
and run against craft-video.

opusenc stays (craft-libs has no Opus encoder yet), as do filmcraft-mpegts
and the filmcraft-vp9, -hevc and -av1 decoders the tests use as oracles.

* Pin craft-libs at the main commit that has the encoder colour description (e7c3dda7)
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-11 18:46:50 -05:00
1 parent f334bcf3ef
commit 79c42749a8
75 files changed
+228 -16997

No files matched your search

+26 -26
View File
@@ -1,13 +1,13 @@
//! Movie export: FilmCraft H.264, HEVC (`effectcraft-hevcenc`) and AV1 (`effectcraft-av1enc`)
//! Movie export: H.264 (`craft_video::h264`), HEVC (`craft_video::hevc`) and AV1 (`craft_video::av1`)
//! → MP4 (+ AAC), ProRes and uncompressed `raw ` → MOV (+ PCM), muxed by
//! FilmCraft's ISO BMFF / QuickTime writer.
//! craft-media's ISO BMFF / QuickTime writer.
use std::io::Write;
use craft_media::isobmff::{Brand, ColorInfo, FourCc, Mp4Writer, PcmConfig, SampleEntry, TrackConfig, WriteSample, WriterOptions};
use effectcraft_project::render_queue::{AudioFormat, Channels, OutputFormat, ProResProfile};
use effectcraft_project::{Comp, ProjectSettings};
use effectcraft_time::{FrameRate, Tick};
use filmcraft_isobmff::{Brand, ColorInfo, FourCc, Mp4Writer, PcmConfig, SampleEntry, TrackConfig, WriteSample, WriterOptions};
use rayon::prelude::*;
use crate::{Cx, ExportError, Report, Result, State, batch_size, wants_audio};
@@ -117,7 +117,7 @@ pub(crate) trait VideoEncoder: Send {
// ---------------------------------------------------------------- H.264
struct H264 {
enc: filmcraft_h264enc::Encoder,
enc: craft_video::h264::Encoder,
w: u32,
h: u32,
rate: FrameRate,
@@ -129,26 +129,26 @@ struct H264 {
impl H264 {
fn new(w: u32, h: u32, rate: FrameRate, kbps: u32, codes: Codes) -> Result<H264> {
let mut cfg = filmcraft_h264enc::EncoderConfig::new(w, h, rate.num as u32, rate.den as u32);
cfg.format = filmcraft_h264enc::PacketFormat::LengthPrefixed;
let mut cfg = craft_video::h264::EncoderConfig::new(w, h, rate.num as u32, rate.den as u32);
cfg.format = craft_video::h264::PacketFormat::LengthPrefixed;
cfg.aud = false;
cfg.keyint = (rate.as_f64() * 2.0).round().max(1.0) as u32;
let kbps = kbps.max(100);
cfg.rate = filmcraft_h264enc::RateControl::Vbr { target_kbps: kbps, max_kbps: kbps * 3 / 2 };
cfg.color = filmcraft_h264enc::ColorConfig { primaries: codes.primaries, transfer: codes.transfer, matrix: codes.matrix, full_range: false };
let enc = filmcraft_h264enc::Encoder::new(cfg).map_err(enc)?;
cfg.rate = craft_video::h264::RateControl::Vbr { target_kbps: kbps, max_kbps: kbps * 3 / 2 };
cfg.color = craft_video::h264::ColorConfig { primaries: codes.primaries, transfer: codes.transfer, matrix: codes.matrix, full_range: false };
let enc = craft_video::h264::Encoder::new(cfg).map_err(enc)?;
Ok(H264 { enc, w, h, rate, codes, y: vec![], u: vec![], v: vec![] })
}
fn packets(&self, ps: Vec<filmcraft_h264enc::Packet>) -> Vec<Packet> {
fn packets(&self, ps: Vec<craft_video::h264::Packet>) -> Vec<Packet> {
ps.into_iter().map(|p| Packet { data: p.data, key: p.keyframe, cto: (p.pts - p.dts) as i32 }).collect()
}
}
impl VideoEncoder for H264 {
fn sample_entry(&self) -> SampleEntry {
let cfg = filmcraft_isobmff::AvcConfig::parse(&self.enc.avcc()).unwrap_or_else(|_| {
let cfg = craft_media::isobmff::AvcConfig::parse(&self.enc.avcc()).unwrap_or_else(|_| {
let (sps, pps) = self.enc.sps_pps();
filmcraft_isobmff::AvcConfig::new(vec![sps], vec![pps], 4)
craft_media::isobmff::AvcConfig::new(vec![sps], vec![pps], 4)
});
let mut entry = SampleEntry::avc(cfg, self.w as u16, self.h as u16);
tag_nclx_limited(&mut entry, self.codes);
@@ -170,7 +170,7 @@ impl VideoEncoder for H264 {
fn encode_yuv420(&mut self, p: &Yuv420, index: u64) -> Result<Vec<Packet>> {
let (w, cw) = (self.w as usize, (self.w as usize).div_ceil(2));
let (y, u, v) = p.slices(w, self.h as usize);
let frame = filmcraft_h264enc::YuvFrame { y, u, v, y_stride: w, uv_stride: cw };
let frame = craft_video::h264::YuvFrame { y, u, v, y_stride: w, uv_stride: cw };
let ps = self.enc.try_encode(&frame, index as i64 * self.rate.den).map_err(enc)?;
Ok(self.packets(ps))
}
@@ -188,8 +188,8 @@ pub(crate) use effectcraft_raster::yuv::rgba_to_yuv420;
// ---------------------------------------------------------------- ProRes
struct ProRes {
enc: filmcraft_prores::Encoder,
profile: filmcraft_prores::Profile,
enc: craft_video::prores::Encoder,
profile: craft_video::prores::Profile,
w: u32,
h: u32,
alpha: bool,
@@ -202,8 +202,8 @@ fn prores_colr(c: Codes) -> ColorInfo {
ColorInfo::Nclc { primaries: c.primaries.into(), transfer: c.transfer.into(), matrix: c.matrix.into() }
}
fn prores_profile(p: ProResProfile) -> filmcraft_prores::Profile {
use filmcraft_prores::Profile;
fn prores_profile(p: ProResProfile) -> craft_video::prores::Profile {
use craft_video::prores::Profile;
match p {
ProResProfile::Proxy => Profile::Proxy,
ProResProfile::Lt => Profile::Lt,
@@ -220,18 +220,18 @@ impl ProRes {
let profile = if channels == Channels::Rgba && !profile.is_4444() { ProResProfile::P4444 } else { profile };
let profile = prores_profile(profile);
let alpha = channels == Channels::Rgba;
let mut cfg = filmcraft_prores::EncoderConfig::new(profile, w, h);
let mut cfg = craft_video::prores::EncoderConfig::new(profile, w, h);
cfg.encode_alpha = alpha;
// The frame header carries the same H.273 codes as the `colr` atom, so readers that trust
// either one agree.
if let ColorInfo::Nclc { primaries, transfer, matrix } = colr {
let code = |c| u8::try_from(c).unwrap_or(2);
cfg.color = filmcraft_prores::ColorInfo { primaries: code(primaries), transfer: code(transfer), matrix: code(matrix) };
cfg.color = craft_video::prores::ColorInfo { primaries: code(primaries), transfer: code(transfer), matrix: code(matrix) };
}
ProRes { enc: filmcraft_prores::Encoder::with_config(cfg), profile, w, h, alpha, colr }
ProRes { enc: craft_video::prores::Encoder::with_config(cfg), profile, w, h, alpha, colr }
}
fn is_444(&self) -> bool {
matches!(self.profile, filmcraft_prores::Profile::P4444 | filmcraft_prores::Profile::P4444Xq)
matches!(self.profile, craft_video::prores::Profile::P4444 | craft_video::prores::Profile::P4444Xq)
}
}
@@ -244,8 +244,8 @@ impl VideoEncoder for ProRes {
entry
}
fn encode(&mut self, rgba: &[u8], _index: u64) -> Result<Vec<Packet>> {
let chroma = if self.is_444() { filmcraft_prores::ChromaFormat::Yuv444 } else { filmcraft_prores::ChromaFormat::Yuv422 };
let mut fr = filmcraft_prores::Frame::new(self.w, self.h, chroma, 10, self.alpha);
let chroma = if self.is_444() { craft_video::prores::ChromaFormat::Yuv444 } else { craft_video::prores::ChromaFormat::Yuv422 };
let mut fr = craft_video::prores::Frame::new(self.w, self.h, chroma, 10, self.alpha).map_err(enc)?;
rgba_to_yuv_10(rgba, self.w as usize, self.h as usize, self.is_444(), &mut fr.y, &mut fr.cb, &mut fr.cr);
if let Some(a) = fr.alpha.as_mut() {
for (d, s) in a.iter_mut().zip(rgba.as_chunks::<4>().0.iter()) {
@@ -291,7 +291,7 @@ fn rgba_to_yuv_10(rgba: &[u8], w: usize, h: usize, full_chroma: bool, y: &mut [u
// ---------------------------------------------------------------- audio
enum Audio {
Aac(Box<filmcraft_aac::Encoder>),
Aac(Box<craft_audio::aac::Encoder>),
Pcm,
}
@@ -401,7 +401,7 @@ pub(crate) async fn movie(job: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut St
// from the media duration and drop the last frame.
let frames_ts = st.total as i128 * rate.den as i128;
let seg = (frames_ts * movie_ts + rate.num as i128 / 2) / rate.num as i128;
vcfg.edits = vec![filmcraft_isobmff::Edit { segment_duration: seg as u64, media_time: start, media_rate: 0x10000 }];
vcfg.edits = vec![craft_media::isobmff::Edit { segment_duration: seg as u64, media_time: start, media_rate: 0x10000 }];
}
let vt = mux.add_track(vcfg).map_err(mux_err)?;
let sr = job.output.audio_sample_rate.clamp(8_000, 192_000);
@@ -411,7 +411,7 @@ pub(crate) async fn movie(job: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut St
let mut audio = None;
if with_audio {
let (a, entry, start) = if brand == Brand::Mp4 {
let e = filmcraft_aac::Encoder::new(filmcraft_aac::EncoderConfig::cbr(sr, chans as usize, 160_000 * chans)).map_err(enc)?;
let e = craft_audio::aac::Encoder::new(craft_audio::aac::EncoderConfig::cbr(sr, chans as usize, 160_000 * chans)).map_err(enc)?;
let entry = SampleEntry::aac(e.audio_specific_config(), chans, sr);
let priming = e.priming_samples() as i64;
(Audio::Aac(Box::new(e)), entry, Some(priming))