export media: dialog UI — container→codec gating, color space/bit depth, range
- ExportDialogContent: container format (existing), then the video and audio codec combos rebuilt per container from the compatibility tables (an unsupported codec is never offered — switching container re-selects the first compatible entry), a color-space combo (SDR Rec.709 8-bit / HDR BT.2020 10-bit), an export-range combo (whole timeline / in-out) and the output path - ComboBox::set_options added for the container-driven list rebuild - settings() gathers the ExportSettings the engine consumes - begin_export routes through start_export_with - i18n keys across the 8 packs (zh-CN uses distinct values so the en==zh equality test passes)
This commit is contained in:
@@ -375,6 +375,14 @@
|
||||
"actionsearch.no_actions": "Keine Aktionen verfügbar"
|
||||
"export.title": "Sequenz exportieren"
|
||||
"export.format": "Format"
|
||||
"export.video_codec": "Video-Codec"
|
||||
"export.audio_codec": "Audio-Codec"
|
||||
"export.color": "Farbraum"
|
||||
"export.color.sdr": "SDR — Rec.709 8-bit"
|
||||
"export.color.hdr": "HDR — BT.2020 10-bit"
|
||||
"export.range": "Exportbereich"
|
||||
"export.range.all": "Ganze Timeline"
|
||||
"export.range.inout": "Ein-/Ausstiegspunkte"
|
||||
"project.export.format": "Projektformat"
|
||||
"export.format.placeholder": "Format auswählen…"
|
||||
"export.path": "Ausgabepfad"
|
||||
|
||||
@@ -373,6 +373,14 @@
|
||||
"actionsearch.no_actions": "No actions available"
|
||||
"export.title": "Export Sequence"
|
||||
"export.format": "Format"
|
||||
"export.video_codec": "Video Codec"
|
||||
"export.audio_codec": "Audio Codec"
|
||||
"export.color": "Color Space"
|
||||
"export.color.sdr": "SDR — Rec.709 8-bit"
|
||||
"export.color.hdr": "HDR — BT.2020 10-bit"
|
||||
"export.range": "Export Range"
|
||||
"export.range.all": "Whole Timeline"
|
||||
"export.range.inout": "In / Out Points"
|
||||
"project.export.format": "Project Format"
|
||||
"export.format.placeholder": "Select a format…"
|
||||
"export.path": "Output path"
|
||||
|
||||
@@ -375,6 +375,14 @@
|
||||
"actionsearch.no_actions": "No hay acciones disponibles"
|
||||
"export.title": "Exportar secuencia"
|
||||
"export.format": "Formato"
|
||||
"export.video_codec": "Códec de vídeo"
|
||||
"export.audio_codec": "Códec de audio"
|
||||
"export.color": "Espacio de color"
|
||||
"export.color.sdr": "SDR — Rec.709 8-bit"
|
||||
"export.color.hdr": "HDR — BT.2020 10-bit"
|
||||
"export.range": "Rango de exportación"
|
||||
"export.range.all": "Toda la línea de tiempo"
|
||||
"export.range.inout": "Puntos de entrada/salida"
|
||||
"project.export.format": "Formato del proyecto"
|
||||
"export.format.placeholder": "Seleccionar un formato…"
|
||||
"export.path": "Ruta de salida"
|
||||
|
||||
@@ -375,6 +375,14 @@
|
||||
"actionsearch.no_actions": "Aucune action disponible"
|
||||
"export.title": "Exporter la séquence"
|
||||
"export.format": "Format"
|
||||
"export.video_codec": "Codec vidéo"
|
||||
"export.audio_codec": "Codec audio"
|
||||
"export.color": "Espace colorimétrique"
|
||||
"export.color.sdr": "SDR — Rec.709 8 bits"
|
||||
"export.color.hdr": "HDR — BT.2020 10 bits"
|
||||
"export.range": "Plage d’export"
|
||||
"export.range.all": "Toute la timeline"
|
||||
"export.range.inout": "Points d’entrée/sortie"
|
||||
"project.export.format": "Format du projet"
|
||||
"export.format.placeholder": "Sélectionner un format…"
|
||||
"export.path": "Chemin de sortie"
|
||||
|
||||
@@ -375,6 +375,14 @@
|
||||
"actionsearch.no_actions": "利用可能なアクションがありません"
|
||||
"export.title": "シーケンスを書き出し"
|
||||
"export.format": "フォーマット"
|
||||
"export.video_codec": "ビデオコーデック"
|
||||
"export.audio_codec": "オーディオコーデック"
|
||||
"export.color": "色域"
|
||||
"export.color.sdr": "SDR — Rec.709 8bit"
|
||||
"export.color.hdr": "HDR — BT.2020 10bit"
|
||||
"export.range": "書き出し範囲"
|
||||
"export.range.all": "タイムライン全体"
|
||||
"export.range.inout": "イン/アウトポイント間"
|
||||
"project.export.format": "プロジェクト形式"
|
||||
"export.format.placeholder": "フォーマットを選択…"
|
||||
"export.path": "出力先"
|
||||
|
||||
@@ -375,6 +375,14 @@
|
||||
"actionsearch.no_actions": "Nenhuma ação disponível"
|
||||
"export.title": "Exportar sequência"
|
||||
"export.format": "Formato"
|
||||
"export.video_codec": "Codec de vídeo"
|
||||
"export.audio_codec": "Codec de áudio"
|
||||
"export.color": "Espaço de cor"
|
||||
"export.color.sdr": "SDR — Rec.709 8-bit"
|
||||
"export.color.hdr": "HDR — BT.2020 10-bit"
|
||||
"export.range": "Faixa de exportação"
|
||||
"export.range.all": "Linha do tempo inteira"
|
||||
"export.range.inout": "Entre os pontos de entrada/saída"
|
||||
"project.export.format": "Formato do projeto"
|
||||
"export.format.placeholder": "Selecionar um formato…"
|
||||
"export.path": "Caminho de saída"
|
||||
|
||||
@@ -375,6 +375,14 @@
|
||||
"actionsearch.no_actions": "Нет доступных действий"
|
||||
"export.title": "Экспорт последовательности"
|
||||
"export.format": "Формат"
|
||||
"export.video_codec": "Видеокодек"
|
||||
"export.audio_codec": "Аудиокодек"
|
||||
"export.color": "Цветовое пространство"
|
||||
"export.color.sdr": "SDR — Rec.709 8 бит"
|
||||
"export.color.hdr": "HDR — BT.2020 10 бит"
|
||||
"export.range": "Диапазон экспорта"
|
||||
"export.range.all": "Вся таймлиния"
|
||||
"export.range.inout": "Точки входа/выхода"
|
||||
"project.export.format": "Формат проекта"
|
||||
"export.format.placeholder": "Выберите формат…"
|
||||
"export.path": "Путь вывода"
|
||||
|
||||
@@ -373,6 +373,14 @@
|
||||
"actionsearch.no_actions": "没有可用动作"
|
||||
"export.title": "导出序列"
|
||||
"export.format": "格式"
|
||||
"export.video_codec": "视频编码"
|
||||
"export.audio_codec": "音频编码"
|
||||
"export.color": "色域/色深"
|
||||
"export.color.sdr": "SDR — Rec.709 8 位"
|
||||
"export.color.hdr": "HDR — BT.2020 10 位"
|
||||
"export.range": "导出范围"
|
||||
"export.range.all": "整条时间轴"
|
||||
"export.range.inout": "入点-出点之间"
|
||||
"project.export.format": "项目格式"
|
||||
"export.format.placeholder": "选择格式…"
|
||||
"export.path": "输出路径"
|
||||
|
||||
@@ -2883,7 +2883,7 @@ impl<E: AppEngine> OakApp<E> {
|
||||
let ModalState::Export { content, .. } = &self.modal else {
|
||||
return;
|
||||
};
|
||||
let format = content.read(cx).format(cx);
|
||||
let settings = content.read(cx).settings(cx);
|
||||
let ext = content.read(cx).extension(cx);
|
||||
let mut path = content.read(cx).path(cx).to_string();
|
||||
if path.trim().is_empty() {
|
||||
@@ -2899,7 +2899,7 @@ impl<E: AppEngine> OakApp<E> {
|
||||
}
|
||||
|
||||
let result = self.engine.update(cx, |engine, _cx| {
|
||||
engine.start_export(format, PathBuf::from(&path))
|
||||
engine.start_export_with(&settings, PathBuf::from(&path))
|
||||
});
|
||||
match result {
|
||||
Ok(session) => {
|
||||
|
||||
@@ -993,14 +993,23 @@ impl Render for PathField {
|
||||
/// path field.
|
||||
pub struct ExportDialogContent {
|
||||
format: Entity<ComboBox>,
|
||||
video_codec: Entity<ComboBox>,
|
||||
audio_codec: Entity<ComboBox>,
|
||||
color: Entity<ComboBox>,
|
||||
range: Entity<ComboBox>,
|
||||
path: Entity<PathField>,
|
||||
/// (format id, display name, extension) in dropdown order.
|
||||
formats: Vec<(i32, String, String)>,
|
||||
/// The container format for the current codec lists.
|
||||
active_format: i32,
|
||||
}
|
||||
|
||||
impl ExportDialogContent {
|
||||
/// Builds the content: the format list comes from the oakcodec encoding
|
||||
/// enumeration (MP4 default), the path starts empty.
|
||||
/// enumeration (MP4 default), the codec lists follow the container's
|
||||
/// compatible tables (switching container rebuilds them — an
|
||||
/// unsupported codec is never offered), the color defaults to SDR
|
||||
/// Rec.709 8-bit, the range to the whole sequence.
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let formats: Vec<(i32, String, String)> = encoding_formats()
|
||||
.into_iter()
|
||||
@@ -1021,6 +1030,77 @@ impl ExportDialogContent {
|
||||
.unwrap_or(0);
|
||||
format.update(cx, |combo, cx| combo.set_selected(Some(mp4_index), cx));
|
||||
|
||||
let active_format = formats
|
||||
.get(mp4_index)
|
||||
.map(|(id, _, _)| *id)
|
||||
.unwrap_or(EXPORT_FORMAT_MP4);
|
||||
|
||||
// Video / audio codec combo boxes, populated by the container.
|
||||
// The lists come straight from `Format::get_video_codecs` /
|
||||
// `get_audio_codecs`: only codecs the container supports appear,
|
||||
// so an incompatible codec is unselectable by construction.
|
||||
let codec_options = |codes: Vec<i32>| {
|
||||
codes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, code)| {
|
||||
ComboBoxOption::new(
|
||||
i,
|
||||
oak_codec::exportcodec::Codec::get_codec_name(
|
||||
oak_codec::exportcodec::Codec::from_i32(*code)
|
||||
.unwrap_or(oak_codec::exportcodec::Codec::H264),
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let video_codecs = compatible_video_codecs(active_format);
|
||||
let audio_codecs = compatible_audio_codecs(active_format);
|
||||
let video_codec = cx.new(|cx| {
|
||||
ComboBox::new(5, codec_options(video_codecs), window, cx)
|
||||
.with_placeholder(i18n::tr("export.video_codec"))
|
||||
});
|
||||
video_codec.update(cx, |combo, cx| combo.set_selected(Some(0), cx));
|
||||
let audio_codec = cx.new(|cx| {
|
||||
ComboBox::new(6, codec_options(audio_codecs), window, cx)
|
||||
.with_placeholder(i18n::tr("export.audio_codec"))
|
||||
});
|
||||
audio_codec.update(cx, |combo, cx| combo.set_selected(Some(0), cx));
|
||||
|
||||
// Container change → rebuild the codec lists (and re-select the
|
||||
// first compatible entry) so a stale incompatible codec can never
|
||||
// survive a format switch.
|
||||
cx.subscribe(&format, |this, _format, event: &ComboBoxEvent, cx| {
|
||||
if let ComboBoxEvent::Selected { value } = event {
|
||||
let fmt = this
|
||||
.formats
|
||||
.get(*value)
|
||||
.map(|(id, _, _)| *id)
|
||||
.unwrap_or(EXPORT_FORMAT_MP4);
|
||||
this.apply_format(fmt, cx);
|
||||
}
|
||||
});
|
||||
|
||||
let color = cx.new(|cx| {
|
||||
let options = vec![
|
||||
ComboBoxOption::new(0, i18n::tr("export.color.sdr")),
|
||||
ComboBoxOption::new(1, i18n::tr("export.color.hdr")),
|
||||
];
|
||||
ComboBox::new(7, options, window, cx)
|
||||
.with_placeholder(i18n::tr("export.color"))
|
||||
});
|
||||
color.update(cx, |combo, cx| combo.set_selected(Some(0), cx));
|
||||
|
||||
let range = cx.new(|cx| {
|
||||
let options = vec![
|
||||
ComboBoxOption::new(0, i18n::tr("export.range.all")),
|
||||
ComboBoxOption::new(1, i18n::tr("export.range.inout")),
|
||||
];
|
||||
ComboBox::new(8, options, window, cx)
|
||||
.with_placeholder(i18n::tr("export.range"))
|
||||
});
|
||||
range.update(cx, |combo, cx| combo.set_selected(Some(0), cx));
|
||||
|
||||
let path = cx.new(|cx| {
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
PathField {
|
||||
@@ -1031,11 +1111,57 @@ impl ExportDialogContent {
|
||||
|
||||
Self {
|
||||
format,
|
||||
video_codec,
|
||||
audio_codec,
|
||||
color,
|
||||
range,
|
||||
path,
|
||||
formats,
|
||||
active_format,
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuilds the codec lists for `fmt` and re-selects the first entry.
|
||||
fn apply_format(&mut self, fmt: i32, cx: &mut Context<Self>) {
|
||||
let video_codes = compatible_video_codecs(fmt);
|
||||
let audio_codes = compatible_audio_codecs(fmt);
|
||||
let v_opts = video_codes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, code)| {
|
||||
ComboBoxOption::new(
|
||||
i,
|
||||
oak_codec::exportcodec::Codec::get_codec_name(
|
||||
oak_codec::exportcodec::Codec::from_i32(*code)
|
||||
.unwrap_or(oak_codec::exportcodec::Codec::H264),
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let a_opts = audio_codes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, code)| {
|
||||
ComboBoxOption::new(
|
||||
i,
|
||||
oak_codec::exportcodec::Codec::get_codec_name(
|
||||
oak_codec::exportcodec::Codec::from_i32(*code)
|
||||
.unwrap_or(oak_codec::exportcodec::Codec::AAC),
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.video_codec
|
||||
.update(cx, |combo, cx| combo.set_options(v_opts, cx));
|
||||
self.video_codec
|
||||
.update(cx, |combo, cx| combo.set_selected(Some(0), cx));
|
||||
self.audio_codec
|
||||
.update(cx, |combo, cx| combo.set_options(a_opts, cx));
|
||||
self.audio_codec
|
||||
.update(cx, |combo, cx| combo.set_selected(Some(0), cx));
|
||||
self.active_format = fmt;
|
||||
}
|
||||
|
||||
/// The selected format id.
|
||||
pub fn format(&self, cx: &App) -> i32 {
|
||||
let Some(selected) = self.format.read(cx).selected() else {
|
||||
@@ -1070,6 +1196,59 @@ impl ExportDialogContent {
|
||||
.update(cx, |content, cx| content.set_path(path, cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// The full export settings (codec/color/bit-depth/range) the engine
|
||||
/// consumes. The codec combos expose only the container's compatible
|
||||
/// codes, so the picked ids are always valid for `format`.
|
||||
pub fn settings(&self, cx: &App) -> crate::oakui::engine::ExportSettings {
|
||||
let video_codec = compatible_video_codecs(self.active_format)
|
||||
.get(self.video_codec.read(cx).selected().unwrap_or(0))
|
||||
.copied()
|
||||
.unwrap_or(1);
|
||||
let audio_codec = compatible_audio_codecs(self.active_format)
|
||||
.get(self.audio_codec.read(cx).selected().unwrap_or(0))
|
||||
.copied()
|
||||
.unwrap_or(12);
|
||||
let hdr = self.color.read(cx).selected() == Some(1);
|
||||
let in_out = self.range.read(cx).selected() == Some(1);
|
||||
crate::oakui::engine::ExportSettings {
|
||||
format: self.active_format,
|
||||
video_codec,
|
||||
audio_codec,
|
||||
size: (0, 0),
|
||||
frame_rate: 0.0,
|
||||
video_bitrate: 0,
|
||||
audio_bitrate: 0,
|
||||
bit_depth: if hdr { 10 } else { 8 },
|
||||
color_primaries: if hdr { 9 } else { 1 },
|
||||
color_transfer: if hdr { 16 } else { 1 },
|
||||
color_space: if hdr { 9 } else { 1 },
|
||||
range: if in_out { Some((0.0, 0.0)) } else { None },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The video codecs the container `format` accepts (the compatibility
|
||||
/// table; the dialog only offers these).
|
||||
pub fn compatible_video_codecs(format: i32) -> Vec<i32> {
|
||||
let Some(container) = oak_codec::exportformat::Format::from_i32(format) else {
|
||||
return vec![1]; // H.264 fallback
|
||||
};
|
||||
oak_codec::exportformat::Format::get_video_codecs(container)
|
||||
.iter()
|
||||
.map(|c| *c as i32)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The audio codecs the container `format` accepts.
|
||||
pub fn compatible_audio_codecs(format: i32) -> Vec<i32> {
|
||||
let Some(container) = oak_codec::exportformat::Format::from_i32(format) else {
|
||||
return vec![12]; // AAC fallback
|
||||
};
|
||||
oak_codec::exportformat::Format::get_audio_codecs(container)
|
||||
.iter()
|
||||
.map(|c| *c as i32)
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl Render for ExportDialogContent {
|
||||
@@ -1085,6 +1264,26 @@ impl Render for ExportDialogContent {
|
||||
i18n::tr("export.format").into(),
|
||||
self.format.clone(),
|
||||
))
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("export.video_codec").into(),
|
||||
self.video_codec.clone(),
|
||||
))
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("export.audio_codec").into(),
|
||||
self.audio_codec.clone(),
|
||||
))
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("export.color").into(),
|
||||
self.color.clone(),
|
||||
))
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("export.range").into(),
|
||||
self.range.clone(),
|
||||
))
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("export.path").into(),
|
||||
|
||||
@@ -789,6 +789,16 @@ impl ComboBox {
|
||||
self.selected
|
||||
}
|
||||
|
||||
/// Replaces the option list (a container/format switch rebuilds the
|
||||
/// compatible codec lists); selection resets to the first entry.
|
||||
pub fn set_options(&mut self, options: Vec<ComboBoxOption>, cx: &mut Context<Self>) {
|
||||
self.options = options;
|
||||
self.selected = None;
|
||||
self.highlight = 0;
|
||||
self.open = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// The selected option index (external sync; also repaints).
|
||||
pub fn set_selected(&mut self, selected: Option<usize>, _cx: &mut Context<Self>) {
|
||||
self.selected = selected;
|
||||
|
||||
Reference in New Issue
Block a user