mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-02 22:42:24 -06:00
Merge PR #2804
This commit is contained in:
@@ -123,3 +123,7 @@ sync_github_name = true
|
||||
real_name = "Arshiya Rahgozar"
|
||||
display_name = "Arshiya"
|
||||
sync_github_name = false
|
||||
|
||||
[people.JonRC]
|
||||
real_name = "Jonathan Cardoso"
|
||||
display_name = "Jonathan Cardoso"
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use effectcraft_project::Comp;
|
||||
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::*;
|
||||
@@ -164,6 +164,28 @@ struct ProRes {
|
||||
w: u32,
|
||||
h: u32,
|
||||
alpha: bool,
|
||||
colr: ColorInfo,
|
||||
}
|
||||
|
||||
/// The `colr` atom of a ProRes render: what its frames are, in H.273 code points. MOV uses
|
||||
/// QuickTime's `nclc`, which has no range flag; ProRes frames are limited range. The matrix is the
|
||||
/// one [`rgba_to_yuv_10`] always uses. Primaries and transfer follow the render path
|
||||
/// (`Pipe::output`): only a working space converts to the output space, so unmanaged frames pass
|
||||
/// through. sRGB output keeps transfer 1 rather than 13 (#340), and an alpha matte is not in the
|
||||
/// output space.
|
||||
fn prores_colr(settings: &ProjectSettings, channels: Channels) -> ColorInfo {
|
||||
use effectcraft_project::ColorSpace as Space;
|
||||
let output = settings.working_space.and(settings.output_space).filter(|_| channels != Channels::Alpha);
|
||||
let (primaries, transfer) = match output {
|
||||
None | Some(Space::Srgb | Space::Rec709) => (1, 1),
|
||||
Some(Space::Rec2020) => (9, 1),
|
||||
Some(Space::DisplayP3) => (12, 13),
|
||||
Some(Space::Rec2100Pq) => (9, 16),
|
||||
Some(Space::Rec2100Hlg) => (9, 18),
|
||||
// H.273 has no code for the ACES primaries: unspecified, in linear light.
|
||||
Some(Space::AcesCg | Space::Aces2065) => (2, 8),
|
||||
};
|
||||
ColorInfo::Nclc { primaries, transfer, matrix: 1 }
|
||||
}
|
||||
|
||||
fn prores_profile(p: ProResProfile) -> filmcraft_prores::Profile {
|
||||
@@ -179,14 +201,14 @@ fn prores_profile(p: ProResProfile) -> filmcraft_prores::Profile {
|
||||
}
|
||||
|
||||
impl ProRes {
|
||||
fn new(w: u32, h: u32, profile: ProResProfile, channels: Channels) -> ProRes {
|
||||
fn new(w: u32, h: u32, profile: ProResProfile, channels: Channels, colr: ColorInfo) -> ProRes {
|
||||
// Alpha needs a 4444 flavour.
|
||||
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);
|
||||
cfg.encode_alpha = alpha;
|
||||
ProRes { enc: filmcraft_prores::Encoder::with_config(cfg), profile, w, h, alpha }
|
||||
ProRes { enc: filmcraft_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)
|
||||
@@ -195,7 +217,11 @@ impl ProRes {
|
||||
|
||||
impl VideoEncoder for ProRes {
|
||||
fn sample_entry(&self) -> SampleEntry {
|
||||
SampleEntry::prores(FourCc(self.profile.fourcc()), self.w as u16, self.h as u16)
|
||||
let mut entry = SampleEntry::prores(FourCc(self.profile.fourcc()), self.w as u16, self.h as u16);
|
||||
if let Some(v) = entry.video.as_mut() {
|
||||
v.color = Some(self.colr.clone());
|
||||
}
|
||||
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 };
|
||||
@@ -301,7 +327,10 @@ pub(crate) async fn movie(job: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut St
|
||||
OutputFormat::Hevc => Box::new(crate::hevc_av1::Hevc::new(w, h, rate, job.output)?),
|
||||
OutputFormat::Av1 => Box::new(crate::hevc_av1::Av1::new(w, h, rate, job.output)?),
|
||||
OutputFormat::Uncompressed => Box::new(crate::uncompressed::Raw::new(w, h, channels == Channels::Rgba)?),
|
||||
_ => Box::new(ProRes::new(w, h, job.output.prores_profile, if channels == Channels::Alpha { Channels::Rgb } else { channels })),
|
||||
_ => {
|
||||
let colr = prores_colr(&job.project.settings, channels);
|
||||
Box::new(ProRes::new(w, h, job.output.prores_profile, if channels == Channels::Alpha { Channels::Rgb } else { channels }, colr))
|
||||
}
|
||||
};
|
||||
let brand = if matches!(fmt, OutputFormat::ProRes | OutputFormat::Uncompressed) { Brand::Mov } else { Brand::Mp4 };
|
||||
let batch = batch_size();
|
||||
|
||||
@@ -11,6 +11,7 @@ mod ms_rounded_footage;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod native_audio_sample_positions;
|
||||
mod opus_roundtrip;
|
||||
mod prores_color;
|
||||
mod render_settings;
|
||||
mod roundtrip;
|
||||
mod targa_sequence;
|
||||
|
||||
@@ -102,5 +102,6 @@ fn h264_movie_bytes_are_pinned() {
|
||||
#[test]
|
||||
fn prores_movie_bytes_are_pinned() {
|
||||
let data = export_bytes(OutputFormat::ProRes, "/out/pinned.mov");
|
||||
assert_eq!((data.len(), fnv1a(&data)), (270_094, 8_038_709_743_204_075_800), "ProRes MOV");
|
||||
// The `nclc` colr atom (#2801) adds exactly 18 bytes; the frames don't move.
|
||||
assert_eq!((data.len(), fnv1a(&data)), (270_112, 2_693_123_490_992_200_481), "ProRes MOV");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
//! ProRes `colr` atom (#2801): it describes the frames the render path made, read back with
|
||||
//! FilmCraft's demuxer and, when installed, ffprobe as an external oracle.
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use effectcraft_color::Label;
|
||||
use effectcraft_export::render_queue::{Channels, OutputFormat, OutputModule, RenderSettings, TimeSpan};
|
||||
use effectcraft_export::{Job, export};
|
||||
use effectcraft_project::{ColorSpace, Comp, ItemKind, LayerSource, Project, Solid, build};
|
||||
use effectcraft_render::NoFootage;
|
||||
use effectcraft_time::{FrameRate, Tick};
|
||||
use filmcraft_isobmff::{ColorInfo, TrackKind};
|
||||
|
||||
struct Row {
|
||||
name: &'static str,
|
||||
working: Option<ColorSpace>,
|
||||
output: Option<ColorSpace>,
|
||||
channels: Channels,
|
||||
/// H.273 primaries, transfer and matrix.
|
||||
codes: [u16; 3],
|
||||
/// The same as ffprobe names them.
|
||||
names: [&'static str; 3],
|
||||
}
|
||||
|
||||
const SRGB: Option<ColorSpace> = Some(ColorSpace::Srgb);
|
||||
const PQ: Option<ColorSpace> = Some(ColorSpace::Rec2100Pq);
|
||||
const BT709: [&str; 3] = ["bt709", "bt709", "bt709"];
|
||||
|
||||
const ROWS: &[Row] = &[
|
||||
Row { name: "unmanaged", working: None, output: None, channels: Channels::Rgb, codes: [1, 1, 1], names: BT709 },
|
||||
Row { name: "unmanaged_ignores_output", working: None, output: PQ, channels: Channels::Rgb, codes: [1, 1, 1], names: BT709 },
|
||||
Row { name: "srgb_output_stays_1_1_1", working: SRGB, output: None, channels: Channels::Rgb, codes: [1, 1, 1], names: BT709 },
|
||||
Row { name: "rec709", working: SRGB, output: Some(ColorSpace::Rec709), channels: Channels::Rgb, codes: [1, 1, 1], names: BT709 },
|
||||
Row { name: "rec2020", working: SRGB, output: Some(ColorSpace::Rec2020), channels: Channels::Rgb, codes: [9, 1, 1], names: ["bt2020", "bt709", "bt709"] },
|
||||
Row {
|
||||
name: "p3",
|
||||
working: SRGB,
|
||||
output: Some(ColorSpace::DisplayP3),
|
||||
channels: Channels::Rgb,
|
||||
codes: [12, 13, 1],
|
||||
names: ["smpte432", "iec61966-2-1", "bt709"],
|
||||
},
|
||||
Row { name: "pq", working: SRGB, output: PQ, channels: Channels::Rgb, codes: [9, 16, 1], names: ["bt2020", "smpte2084", "bt709"] },
|
||||
Row {
|
||||
name: "hlg",
|
||||
working: SRGB,
|
||||
output: Some(ColorSpace::Rec2100Hlg),
|
||||
channels: Channels::Rgb,
|
||||
codes: [9, 18, 1],
|
||||
names: ["bt2020", "arib-std-b67", "bt709"],
|
||||
},
|
||||
Row { name: "aces", working: SRGB, output: Some(ColorSpace::AcesCg), channels: Channels::Rgb, codes: [2, 8, 1], names: ["unknown", "linear", "bt709"] },
|
||||
Row { name: "pq_with_alpha", working: SRGB, output: PQ, channels: Channels::Rgba, codes: [9, 16, 1], names: ["bt2020", "smpte2084", "bt709"] },
|
||||
Row { name: "alpha_matte", working: SRGB, output: PQ, channels: Channels::Alpha, codes: [1, 1, 1], names: BT709 },
|
||||
];
|
||||
|
||||
fn render(row: &Row, path: &Path) {
|
||||
let mut p = Project::default();
|
||||
p.settings.working_space = row.working;
|
||||
p.settings.output_space = row.output;
|
||||
let mut comp = Comp::new(64, 48, FrameRate::new(10, 1), Tick::from_seconds_f64(0.2));
|
||||
let sid = p.add_item("Solid", Label::Red, None, ItemKind::Solid(Solid { color: [0.75, 0.25, 0.5], width: 64, height: 48, pixel_aspect: 1.0 }));
|
||||
comp.layers.push(build::layer(&mut p, &comp, "Solid", LayerSource::Solid { item: sid }, (64, 48), None));
|
||||
let comp = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(comp.into()));
|
||||
let settings = RenderSettings { time_span: TimeSpan::LengthOfComp, ..Default::default() };
|
||||
let output = OutputModule { channels: row.channels, ..OutputModule::for_format(OutputFormat::ProRes) };
|
||||
let out = path.to_string_lossy().to_string();
|
||||
let job = Job {
|
||||
project: &p,
|
||||
footage: &NoFootage,
|
||||
expr: None,
|
||||
accel: None,
|
||||
comp,
|
||||
settings: &settings,
|
||||
output: &output,
|
||||
path: &out,
|
||||
sink: None,
|
||||
options: Default::default(),
|
||||
nested_switches: true,
|
||||
};
|
||||
export(&job, &mut |_| true).expect("export");
|
||||
}
|
||||
|
||||
/// ffprobe's primaries, transfer and matrix for the stream, or `None` without ffprobe.
|
||||
fn ffprobe_tags(path: &Path) -> Option<Vec<String>> {
|
||||
let out = Command::new("ffprobe")
|
||||
.args(["-v", "error", "-select_streams", "v:0", "-show_entries", "stream=color_primaries,color_transfer,color_space", "-of", "default=nw=1"])
|
||||
.arg(path)
|
||||
.output()
|
||||
.ok()?;
|
||||
assert!(out.status.success(), "ffprobe rejected {}: {}", path.display(), String::from_utf8_lossy(&out.stderr));
|
||||
let text = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
let tag = |key: &str| text.lines().find_map(|l| l.strip_prefix(key)?.strip_prefix('=')).unwrap_or("").to_string();
|
||||
Some(vec![tag("color_primaries"), tag("color_transfer"), tag("color_space")])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prores_colr_describes_the_frames() {
|
||||
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/test-out/export/prores_color");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).expect("mkdir");
|
||||
for row in ROWS {
|
||||
let path = dir.join(format!("{}.mov", row.name));
|
||||
render(row, &path);
|
||||
|
||||
let bytes = std::fs::read(&path).expect("read movie");
|
||||
let mov = filmcraft_isobmff::open(bytes.as_slice()).expect("demux");
|
||||
let track = mov.tracks.iter().find(|t| t.kind == TrackKind::Video).expect("video track");
|
||||
let [primaries, transfer, matrix] = row.codes;
|
||||
let colr = track.entries[0].video.as_ref().and_then(|v| v.color.clone());
|
||||
assert_eq!(colr, Some(ColorInfo::Nclc { primaries, transfer, matrix }), "{}: colr", row.name);
|
||||
|
||||
if let Some(stream) = ffprobe_tags(&path) {
|
||||
assert_eq!(stream, row.names, "{}: ffprobe stream tags", row.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user