export media: settings protocol (codec/color/bit-depth/bitrate/range) end to end
- ExportSettings: container format, video/audio codecs, size/fps/ bitrate, bit depth (8/10 → U8/U10), SDR 709 + HDR 2020 code points, export range - AppEngine::start_export_with(settings, path); real builds the task params via encoding_params_with_settings (codec tables gate each container — a non-supporting combo errors, never silently defaults; the one menu only offers compatible codecs) - oak-task EncodingParams: video_bit_rate / audio_bit_rate + color_override_enabled + primaries/trc/space (override wins over the project's delivery colorspace) - mock start_export_with (no-op fake progress) The dialog UI rebuild (fields + container→codec→color enability linking) follows in the next unit.
This commit is contained in:
@@ -810,6 +810,15 @@ pub trait AppEngine:
|
||||
/// [`ExportSession`] carries the event channel and the cancel handle.
|
||||
fn start_export(&mut self, format: i32, path: PathBuf) -> Result<ExportSession, String>;
|
||||
|
||||
/// Like [`Self::start_export`] but with the full dialog settings
|
||||
/// (codecs, color space/bit depth, size/rate/bitrate, range). The
|
||||
/// default start_export stays for the legacy callers.
|
||||
fn start_export_with(
|
||||
&mut self,
|
||||
settings: &ExportSettings,
|
||||
path: PathBuf,
|
||||
) -> Result<ExportSession, String>;
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Proxy media (the C++ Tools > proxy pipeline): global switch, per
|
||||
// footage state and the generate / delete / reveal entries. Defaults
|
||||
@@ -1504,8 +1513,59 @@ pub enum ExportEvent {
|
||||
/// A running export: the event channel the host drains plus the cancel
|
||||
/// handle. Dropping the session does not abort the export thread; the
|
||||
/// thread owns the task and frees it when it finishes.
|
||||
pub struct ExportSession {
|
||||
/// The event receiver (the background thread's sender lives as long as
|
||||
/// The export dialog's settings (文件 → 导出媒体…): container format,
|
||||
/// video/audio codecs, color space + bit depth, output size / rate /
|
||||
/// bitrate and the export range. The engine's `start_export` consumes
|
||||
/// these to build the oak-task `EncodingParams` (the codec tables gate
|
||||
/// which combos are selectable — an incompatible codec is never offered).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExportSettings {
|
||||
/// The container format id ([`oak_codec::exportformat::Format`]).
|
||||
pub format: i32,
|
||||
/// The video codec ([`oak_codec::exportcodec::Codec`]).
|
||||
pub video_codec: i32,
|
||||
/// The audio codec ([`oak_codec::exportcodec::Codec`]).
|
||||
pub audio_codec: i32,
|
||||
/// Output size (`(width, height)`; `(0,0)` keeps the sequence size).
|
||||
pub size: (i32, i32),
|
||||
/// Output frame rate (fps; `0` = the sequence rate).
|
||||
pub frame_rate: f64,
|
||||
/// Video bitrate in bit/s (`0` = codec default).
|
||||
pub video_bitrate: i64,
|
||||
/// Audio bitrate in bit/s (`0` = codec default).
|
||||
pub audio_bitrate: i64,
|
||||
/// Bit depth: 8 → U8, 10 → U10 (SDR / HDR respectively).
|
||||
pub bit_depth: i32,
|
||||
/// AVColorPrimaries (SDR 709 = 1, HDR 2020 = 9).
|
||||
pub color_primaries: i32,
|
||||
/// AVColorTransferCharacteristic (SDR 709 = 1, HDR PQ = 16).
|
||||
pub color_transfer: i32,
|
||||
/// AVColorSpace (SDR 709 = 1, HDR 2020 = 9).
|
||||
pub color_space: i32,
|
||||
/// Export range: `None` = whole sequence, `Some((in_s, out_s))`.
|
||||
pub range: Option<(f64, f64)>,
|
||||
}
|
||||
|
||||
impl Default for ExportSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
format: 2, // MPEG4Video (.mp4)
|
||||
video_codec: 1, // H.264
|
||||
audio_codec: 12, // AAC
|
||||
size: (0, 0),
|
||||
frame_rate: 0.0,
|
||||
video_bitrate: 0,
|
||||
audio_bitrate: 0,
|
||||
bit_depth: 8,
|
||||
color_primaries: 1,
|
||||
color_transfer: 1,
|
||||
color_space: 1,
|
||||
range: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ExportSession { /// The event receiver (the background thread's sender lives as long as
|
||||
/// the session's `cancel` side, so a dropped receiver just stops
|
||||
/// delivering).
|
||||
pub events: std::sync::mpsc::Receiver<ExportEvent>,
|
||||
|
||||
@@ -2012,6 +2012,14 @@ impl AppEngine for MockEngine {
|
||||
}
|
||||
|
||||
fn start_export(&mut self, _format: i32, _path: PathBuf) -> Result<ExportSession, String> {
|
||||
self.start_export_with(&crate::oakui::engine::ExportSettings::default(), _path)
|
||||
}
|
||||
|
||||
fn start_export_with(
|
||||
&mut self,
|
||||
_settings: &crate::oakui::engine::ExportSettings,
|
||||
_path: PathBuf,
|
||||
) -> Result<ExportSession, String> {
|
||||
// Mock export: fake progress on a background thread, no file.
|
||||
let (tx, rx) = mpsc::channel::<ExportEvent>();
|
||||
std::thread::spawn(move || {
|
||||
|
||||
@@ -6196,14 +6196,26 @@ impl AppEngine for RealEngine {
|
||||
}
|
||||
|
||||
fn start_export(&mut self, format: i32, path: PathBuf) -> Result<ExportSession, String> {
|
||||
let settings = super::engine::ExportSettings {
|
||||
format,
|
||||
..Default::default()
|
||||
};
|
||||
self.start_export_with(&settings, path)
|
||||
}
|
||||
|
||||
fn start_export_with(
|
||||
&mut self,
|
||||
settings: &super::engine::ExportSettings,
|
||||
path: PathBuf,
|
||||
) -> Result<ExportSession, String> {
|
||||
let (Some(project), Some(seq)) = (self.project.clone(), self.sequence) else {
|
||||
return Err("no sequence open".into());
|
||||
};
|
||||
let workarea = self.workarea().map(|(s, e)| (s.0, e.0));
|
||||
let params = super::renderops::encoding_params(
|
||||
let params = super::renderops::encoding_params_with_settings(
|
||||
&project,
|
||||
seq,
|
||||
format,
|
||||
settings,
|
||||
&path,
|
||||
workarea,
|
||||
self.sequence_length().0,
|
||||
|
||||
@@ -1096,6 +1096,119 @@ pub fn encoding_params(
|
||||
custom_range_in_den: range_in.denominator() as i32,
|
||||
custom_range_out_num: range_out.numerator() as i32,
|
||||
custom_range_out_den: range_out.denominator() as i32,
|
||||
video_bit_rate: 0,
|
||||
audio_bit_rate: 0,
|
||||
color_override_enabled: false,
|
||||
color_primaries: 0,
|
||||
color_trc: 0,
|
||||
color_space: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Like [`encoding_params`] but with the dialog's full settings: chosen
|
||||
/// video/audio codecs, color space + bit depth, output size / rate /
|
||||
/// bitrate and the export range. The sequence's parameters fill anything
|
||||
/// the settings left at zero.
|
||||
pub fn encoding_params_with_settings(
|
||||
p: &ProjectRef,
|
||||
seq: NodeId,
|
||||
settings: &crate::oakui::engine::ExportSettings,
|
||||
path: &std::path::Path,
|
||||
workarea: Option<(i64, i64)>,
|
||||
length_frames: i64,
|
||||
) -> Result<oak_task::export::EncodingParams, String> {
|
||||
let container = oak_codec::exportformat::Format::from_i32(settings.format)
|
||||
.ok_or_else(|| format!("unknown export format {}", settings.format))?;
|
||||
// ONLY the codecs the container supports are selectable upstream (the
|
||||
// dialog rebuilds its lists from these tables); the chosen codec must
|
||||
// be in the table — a mismatch is a hard error, not a silent default.
|
||||
let video_codec = oak_codec::exportformat::Format::get_video_codecs(container)
|
||||
.iter()
|
||||
.find(|c| **c as i32 == settings.video_codec)
|
||||
.copied()
|
||||
.ok_or_else(|| format!("codec {} not supported by {container:?}", settings.video_codec))?;
|
||||
let audio_codec = oak_codec::exportformat::Format::get_audio_codecs(container)
|
||||
.iter()
|
||||
.find(|c| **c as i32 == settings.audio_codec)
|
||||
.copied()
|
||||
.ok_or_else(|| format!("audio codec {} not supported by {container:?}", settings.audio_codec))?;
|
||||
let (mut width, mut height, rate) = {
|
||||
let g = lock(p);
|
||||
super::graphops::sequence_video_params(&g.graph, seq)
|
||||
.ok_or_else(|| "the sequence has no video parameters".to_string())?
|
||||
};
|
||||
if settings.size.0 > 0 && settings.size.1 > 0 {
|
||||
width = settings.size.0;
|
||||
height = settings.size.1;
|
||||
}
|
||||
let mut rate_num = rate.numerator().max(1) as i32;
|
||||
let mut rate_den = rate.denominator().max(1) as i32;
|
||||
if settings.frame_rate > 0.0 {
|
||||
// Round to the nearest 1/1 frame; 29.97 etc. stay as-is.
|
||||
rate_num = settings.frame_rate.round().max(1.0) as i32;
|
||||
rate_den = 1;
|
||||
}
|
||||
|
||||
let (has_custom_range, range_in, range_out, length) = match settings.range {
|
||||
Some((in_s, out_s)) if out_s > in_s && in_s >= 0.0 => {
|
||||
let fps = i64::from(rate_num.max(1));
|
||||
(
|
||||
true,
|
||||
Rational::new((in_s * fps as f64).round().max(0.0) as i64, fps),
|
||||
Rational::new((out_s * fps as f64).round().max(0.0) as i64, fps),
|
||||
Rational::new(((out_s - in_s) * fps as f64).round().max(0.0) as i64, fps),
|
||||
)
|
||||
}
|
||||
_ => match workarea.filter(|(s, e)| e > s) {
|
||||
Some((in_ts, out_ts)) => (
|
||||
true,
|
||||
Rational::new(in_ts * i64::from(rate_den.max(1)), i64::from(rate_num.max(1))),
|
||||
Rational::new(out_ts * i64::from(rate_den.max(1)), i64::from(rate_num.max(1))),
|
||||
Rational::new((out_ts - in_ts) * i64::from(rate_den.max(1)), i64::from(rate_num.max(1))),
|
||||
),
|
||||
None => (
|
||||
false,
|
||||
Rational::new(0, 1),
|
||||
Rational::new(0, 1),
|
||||
Rational::new(length_frames.max(0) * i64::from(rate_den.max(1)), i64::from(rate_num.max(1))),
|
||||
),
|
||||
},
|
||||
};
|
||||
let bit_depth = settings.bit_depth;
|
||||
let pixel_format = match bit_depth {
|
||||
10 => 1, // PixelFormat::U10 (only when the codec supports it; the
|
||||
// dialog gates 10-bit behind HDR — codecs without 10-bit
|
||||
// stay at the 8-bit default and the dialog disables HDR).
|
||||
_ => 0, // PixelFormat::U8
|
||||
};
|
||||
Ok(oak_task::export::EncodingParams {
|
||||
filename: path.to_string_lossy().into_owned(),
|
||||
format: settings.format,
|
||||
video_enabled: true,
|
||||
video_codec: video_codec as i32,
|
||||
video_width: width.max(1),
|
||||
video_height: height.max(1),
|
||||
video_time_base_num: rate_den.max(1),
|
||||
video_time_base_den: rate_num.max(1),
|
||||
video_pixel_format: pixel_format,
|
||||
audio_enabled: true,
|
||||
audio_codec: audio_codec as i32,
|
||||
audio_sample_rate: EXPORT_SAMPLE_RATE,
|
||||
audio_channel_layout: EXPORT_CHANNEL_LAYOUT,
|
||||
subtitles_enabled: false,
|
||||
export_length_num: length.numerator() as i32,
|
||||
export_length_den: length.denominator() as i32,
|
||||
has_custom_range,
|
||||
custom_range_in_num: range_in.numerator() as i32,
|
||||
custom_range_in_den: range_in.denominator() as i32,
|
||||
custom_range_out_num: range_out.numerator() as i32,
|
||||
custom_range_out_den: range_out.denominator() as i32,
|
||||
video_bit_rate: settings.video_bitrate,
|
||||
audio_bit_rate: settings.audio_bitrate,
|
||||
color_override_enabled: true,
|
||||
color_primaries: settings.color_primaries,
|
||||
color_trc: settings.color_transfer,
|
||||
color_space: settings.color_space,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -105,14 +105,29 @@ pub struct EncodingParams {
|
||||
/// set, [`ExportTask::export_range`] renders exactly `[in, out)` instead
|
||||
/// of the whole viewer length.
|
||||
pub has_custom_range: bool,
|
||||
/// Custom range in point numerator (seconds rational).
|
||||
/// Custom range in, rational seconds.
|
||||
pub custom_range_in_num: i32,
|
||||
/// Custom range in point denominator.
|
||||
/// Custom range in denominator.
|
||||
pub custom_range_in_den: i32,
|
||||
/// Custom range out point numerator (seconds rational).
|
||||
/// Custom range out, rational seconds.
|
||||
pub custom_range_out_num: i32,
|
||||
/// Custom range out point denominator.
|
||||
/// Custom range out denominator.
|
||||
pub custom_range_out_den: i32,
|
||||
/// Video bit rate (bit/s; 0 = codec default).
|
||||
pub video_bit_rate: i64,
|
||||
/// Audio bit rate (bit/s; 0 = codec default).
|
||||
pub audio_bit_rate: i64,
|
||||
/// Delivery colorimetry override flags: `0` = use the project's output
|
||||
/// spec (legacy), `1` = apply the `color_*` code points from the export
|
||||
/// dialog (SDR 709 / HDR 2020 selection).
|
||||
pub color_override_enabled: bool,
|
||||
/// AVColorPrimaries (SDR 709 = 1, HDR BT.2020 = 9).
|
||||
pub color_primaries: i32,
|
||||
/// AVColorTransferCharacteristic (SDR 709 = 1, HDR PQ = 16).
|
||||
pub color_trc: i32,
|
||||
/// AVColorSpace (SDR 709 = 1, HDR BT.2020 = 9).
|
||||
pub color_space: i32,
|
||||
|
||||
}
|
||||
|
||||
impl ExportTask {
|
||||
@@ -164,15 +179,25 @@ impl ExportTask {
|
||||
params.subtitles_enabled = self.encoding_params.subtitles_enabled as i32;
|
||||
params.export_length_num = self.encoding_params.export_length_num;
|
||||
params.export_length_den = self.encoding_params.export_length_den;
|
||||
// Delivery colorimetry: tag the output container with the project's
|
||||
params.video_bit_rate = self.encoding_params.video_bit_rate;
|
||||
params.audio_bit_rate = self.encoding_params.audio_bit_rate;
|
||||
// Delivery colorimetry: the EXPORT DIALOG's choice (SDR 709 8-bit /
|
||||
// HDR 2020 10-bit) wins when enabled; otherwise the project's
|
||||
// output colorspace (H.273 code points → mov `colr` atom / VUI).
|
||||
// Limited range is the video-delivery convention; the encoder's
|
||||
// RGB→YCbCr runs limited.
|
||||
let (_working, spec) = self.delivery_color();
|
||||
params.color_primaries = spec.gamut.av_color_primaries();
|
||||
params.color_trc = spec.transfer.av_color_trc();
|
||||
params.color_space = spec.gamut.av_color_space();
|
||||
params.color_range = 1; // AVCOL_RANGE_MPEG (limited)
|
||||
if self.encoding_params.color_override_enabled {
|
||||
params.color_primaries = self.encoding_params.color_primaries;
|
||||
params.color_trc = self.encoding_params.color_trc;
|
||||
params.color_space = self.encoding_params.color_space;
|
||||
params.color_range = 1; // AVCOL_RANGE_MPEG (limited)
|
||||
} else {
|
||||
let (_working, spec) = self.delivery_color();
|
||||
params.color_primaries = spec.gamut.av_color_primaries();
|
||||
params.color_trc = spec.transfer.av_color_trc();
|
||||
params.color_space = spec.gamut.av_color_space();
|
||||
params.color_range = 1; // AVCOL_RANGE_MPEG (limited)
|
||||
}
|
||||
params
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user