Compare commits
6
Commits
19a83f8359
...
2c64317a58
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c64317a58 | ||
|
|
92813707bc | ||
|
|
499246b48e | ||
|
|
56af662b83 | ||
|
|
660a32a1b3 | ||
|
|
c900804941 |
+14
-1
@@ -183,6 +183,7 @@
|
||||
"manager.title": "Projektmanager"
|
||||
"manager.new": "Neues Projekt"
|
||||
"manager.new.default_name": "Unbenanntes Projekt"
|
||||
"manager.new.project_name": "Projektname"
|
||||
"manager.open": "Öffnen"
|
||||
"manager.rename": "Umbenennen…"
|
||||
"manager.rename.title": "Projekt umbenennen"
|
||||
@@ -303,7 +304,7 @@
|
||||
"about.description": "Oak Video Editor ist ein freier, quelloffener nichtlinearer Videoeditor. Diese Software ist unter der GNU GPL Version 3 lizenziert."
|
||||
"about.thanks": "Besonderer Dank gilt Enzo GD, unserem Community Promoter, für seine großzügige Unterstützung bei der Verbreitung dieses Projekts in dessen Anfangszeit."
|
||||
"about.fork_notice": "Dieses Projekt ist ein Fork des Olive Video Editors."
|
||||
"file.open.title": "Projekt öffnen"
|
||||
"file.open.title": "Projekt öffnen (.ove / .otio / .fcpxml)"
|
||||
"file.import_footage.title": "Material importieren"
|
||||
"preferences.title": "Einstellungen"
|
||||
"preferences.section.general": "Allgemein"
|
||||
@@ -374,6 +375,18 @@
|
||||
"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.resolution": "Auflösung (0 = Sequenz)"
|
||||
"export.framerate": "Bildrate (0 = Sequenz)"
|
||||
"export.bitrate": "Bitrate (0 = Codec-Standard)"
|
||||
"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"
|
||||
"export.run": "Exportieren"
|
||||
|
||||
+14
-1
@@ -181,6 +181,7 @@
|
||||
"manager.title": "Project Manager"
|
||||
"manager.new": "New Project"
|
||||
"manager.new.default_name": "Untitled Project"
|
||||
"manager.new.project_name": "Project Name"
|
||||
"manager.open": "Open"
|
||||
"manager.rename": "Rename…"
|
||||
"manager.rename.title": "Rename Project"
|
||||
@@ -301,7 +302,7 @@
|
||||
"about.description": "Oak Video Editor is a free open source non-linear video editor. This software is licensed under the GNU GPL Version 3."
|
||||
"about.thanks": "Special thanks to Enzo GD, our Community Promoter, for his generous support in spreading the word about this project in its early days."
|
||||
"about.fork_notice": "This project is a fork of Olive Video Editor."
|
||||
"file.open.title": "Open Project"
|
||||
"file.open.title": "Open Project (.ove / .otio / .fcpxml)"
|
||||
"file.import_footage.title": "Import Footage"
|
||||
"preferences.title": "Preferences"
|
||||
"preferences.section.general": "General"
|
||||
@@ -372,6 +373,18 @@
|
||||
"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.resolution": "Resolution (0 = sequence)"
|
||||
"export.framerate": "Frame Rate (0 = sequence)"
|
||||
"export.bitrate": "Bit Rate (0 = codec default)"
|
||||
"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"
|
||||
"export.run": "Export"
|
||||
|
||||
+14
-1
@@ -183,6 +183,7 @@
|
||||
"manager.title": "Administrador de proyectos"
|
||||
"manager.new": "Nuevo proyecto"
|
||||
"manager.new.default_name": "Proyecto sin título"
|
||||
"manager.new.project_name": "Nombre del proyecto"
|
||||
"manager.open": "Abrir"
|
||||
"manager.rename": "Cambiar nombre…"
|
||||
"manager.rename.title": "Cambiar nombre del proyecto"
|
||||
@@ -303,7 +304,7 @@
|
||||
"about.description": "Oak Video Editor es un editor de vídeo no lineal libre y de código abierto. Este software está licenciado bajo la GNU GPL versión 3."
|
||||
"about.thanks": "Un agradecimiento especial a Enzo GD, nuestro Community Promoter, por su generoso apoyo al difundir este proyecto en sus primeros días."
|
||||
"about.fork_notice": "Este proyecto es un fork de Olive Video Editor."
|
||||
"file.open.title": "Abrir proyecto"
|
||||
"file.open.title": "Abrir proyecto (.ove / .otio / .fcpxml)"
|
||||
"file.import_footage.title": "Importar material"
|
||||
"preferences.title": "Preferencias"
|
||||
"preferences.section.general": "General"
|
||||
@@ -374,6 +375,18 @@
|
||||
"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.resolution": "Resolución (0 = secuencia)"
|
||||
"export.framerate": "Frecuencia de imagen (0 = secuencia)"
|
||||
"export.bitrate": "Tasa de bits (0 = valor por defecto)"
|
||||
"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"
|
||||
"export.run": "Exportar"
|
||||
|
||||
+14
-1
@@ -183,6 +183,7 @@
|
||||
"manager.title": "Gestionnaire de projets"
|
||||
"manager.new": "Nouveau projet"
|
||||
"manager.new.default_name": "Projet sans titre"
|
||||
"manager.new.project_name": "Nom du projet"
|
||||
"manager.open": "Ouvrir"
|
||||
"manager.rename": "Renommer…"
|
||||
"manager.rename.title": "Renommer le projet"
|
||||
@@ -303,7 +304,7 @@
|
||||
"about.description": "Oak Video Editor est un éditeur vidéo non linéaire libre et open source. Ce logiciel est sous licence GNU GPL version 3."
|
||||
"about.thanks": "Remerciements particuliers à Enzo GD, notre Community Promoter, pour son généreux soutien à la diffusion de ce projet à ses débuts."
|
||||
"about.fork_notice": "Ce projet est un fork d'Olive Video Editor."
|
||||
"file.open.title": "Ouvrir le projet"
|
||||
"file.open.title": "Ouvrir le projet (.ove / .otio / .fcpxml)"
|
||||
"file.import_footage.title": "Importer des médias"
|
||||
"preferences.title": "Préférences"
|
||||
"preferences.section.general": "Général"
|
||||
@@ -374,6 +375,18 @@
|
||||
"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.resolution": "Résolution (0 = séquence)"
|
||||
"export.framerate": "Cadence (0 = séquence)"
|
||||
"export.bitrate": "Débit (0 = défaut codec)"
|
||||
"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"
|
||||
"export.run": "Exporter"
|
||||
|
||||
+14
-1
@@ -183,6 +183,7 @@
|
||||
"manager.title": "プロジェクトマネージャー"
|
||||
"manager.new": "新規プロジェクト"
|
||||
"manager.new.default_name": "無題のプロジェクト"
|
||||
"manager.new.project_name": "プロジェクト名"
|
||||
"manager.open": "開く"
|
||||
"manager.rename": "名前を変更…"
|
||||
"manager.rename.title": "プロジェクトの名前を変更"
|
||||
@@ -303,7 +304,7 @@
|
||||
"about.description": "Oak Video Editor はフリーでオープンソースのノンリニア動画エディタです。本ソフトウェアは GNU GPL バージョン 3 でライセンスされています。"
|
||||
"about.thanks": "プロジェクト初期に広く紹介してくれたコミュニティプロモーター Enzo GD に心より感謝します。"
|
||||
"about.fork_notice": "本プロジェクトは Olive Video Editor のフォークです。"
|
||||
"file.open.title": "プロジェクトを開く"
|
||||
"file.open.title": "プロジェクトを開く(.ove / .otio / .fcpxml)"
|
||||
"file.import_footage.title": "素材を読み込み"
|
||||
"preferences.title": "環境設定"
|
||||
"preferences.section.general": "一般"
|
||||
@@ -374,6 +375,18 @@
|
||||
"actionsearch.no_actions": "利用可能なアクションがありません"
|
||||
"export.title": "シーケンスを書き出し"
|
||||
"export.format": "フォーマット"
|
||||
"export.video_codec": "ビデオコーデック"
|
||||
"export.audio_codec": "オーディオコーデック"
|
||||
"export.color": "色域"
|
||||
"export.color.sdr": "SDR — Rec.709 8bit"
|
||||
"export.resolution": "解像度(0 = シーケンスに従う)"
|
||||
"export.framerate": "フレームレート(0 = シーケンスに従う)"
|
||||
"export.bitrate": "ビットレート(0 = コーデック既定)"
|
||||
"export.color.hdr": "HDR — BT.2020 10bit"
|
||||
"export.range": "書き出し範囲"
|
||||
"export.range.all": "タイムライン全体"
|
||||
"export.range.inout": "イン/アウトポイント間"
|
||||
"project.export.format": "プロジェクト形式"
|
||||
"export.format.placeholder": "フォーマットを選択…"
|
||||
"export.path": "出力先"
|
||||
"export.run": "書き出し"
|
||||
|
||||
+14
-1
@@ -183,6 +183,7 @@
|
||||
"manager.title": "Gerenciador de projetos"
|
||||
"manager.new": "Novo projeto"
|
||||
"manager.new.default_name": "Projeto sem título"
|
||||
"manager.new.project_name": "Nome do projeto"
|
||||
"manager.open": "Abrir"
|
||||
"manager.rename": "Renomear…"
|
||||
"manager.rename.title": "Renomear projeto"
|
||||
@@ -303,7 +304,7 @@
|
||||
"about.description": "O Oak Video Editor é um editor de vídeo não linear livre e de código aberto. Este software é licenciado sob a GNU GPL versão 3."
|
||||
"about.thanks": "Um agradecimento especial a Enzo GD, nosso Community Promoter, por seu generoso apoio na divulgação deste projeto em seus primórdios."
|
||||
"about.fork_notice": "Este projeto é um fork do Olive Video Editor."
|
||||
"file.open.title": "Abrir projeto"
|
||||
"file.open.title": "Abrir projeto (.ove / .otio / .fcpxml)"
|
||||
"file.import_footage.title": "Importar mídia"
|
||||
"preferences.title": "Preferências"
|
||||
"preferences.section.general": "Geral"
|
||||
@@ -374,6 +375,18 @@
|
||||
"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.resolution": "Resolução (0 = sequência)"
|
||||
"export.framerate": "Taxa de quadros (0 = sequência)"
|
||||
"export.bitrate": "Taxa de bits (0 = padrão do codec)"
|
||||
"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"
|
||||
"export.run": "Exportar"
|
||||
|
||||
+14
-1
@@ -183,6 +183,7 @@
|
||||
"manager.title": "Диспетчер проектов"
|
||||
"manager.new": "Новый проект"
|
||||
"manager.new.default_name": "Проект без названия"
|
||||
"manager.new.project_name": "Название проекта"
|
||||
"manager.open": "Открыть"
|
||||
"manager.rename": "Переименовать…"
|
||||
"manager.rename.title": "Переименовать проект"
|
||||
@@ -303,7 +304,7 @@
|
||||
"about.description": "Oak Video Editor — свободный нелинейный видеоредактор с открытым исходным кодом. Программа распространяется по лицензии GNU GPL версии 3."
|
||||
"about.thanks": "Особая благодарность Enzo GD, нашему Community Promoter, за щедрую поддержку в распространении этого проекта в первые дни."
|
||||
"about.fork_notice": "Этот проект является форком Olive Video Editor."
|
||||
"file.open.title": "Открыть проект"
|
||||
"file.open.title": "Открыть проект (.ove / .otio / .fcpxml)"
|
||||
"file.import_footage.title": "Импорт медиафайлов"
|
||||
"preferences.title": "Настройки"
|
||||
"preferences.section.general": "Общие"
|
||||
@@ -374,6 +375,18 @@
|
||||
"actionsearch.no_actions": "Нет доступных действий"
|
||||
"export.title": "Экспорт последовательности"
|
||||
"export.format": "Формат"
|
||||
"export.video_codec": "Видеокодек"
|
||||
"export.audio_codec": "Аудиокодек"
|
||||
"export.color": "Цветовое пространство"
|
||||
"export.color.sdr": "SDR — Rec.709 8 бит"
|
||||
"export.resolution": "Разрешение (0 = как в последовательности)"
|
||||
"export.framerate": "Частота кадров (0 = как в последовательности)"
|
||||
"export.bitrate": "Битрейт (0 = по умолчанию кодекa)"
|
||||
"export.color.hdr": "HDR — BT.2020 10 бит"
|
||||
"export.range": "Диапазон экспорта"
|
||||
"export.range.all": "Вся таймлиния"
|
||||
"export.range.inout": "Точки входа/выхода"
|
||||
"project.export.format": "Формат проекта"
|
||||
"export.format.placeholder": "Выберите формат…"
|
||||
"export.path": "Путь вывода"
|
||||
"export.run": "Экспортировать"
|
||||
|
||||
+14
-1
@@ -181,6 +181,7 @@
|
||||
"manager.title": "项目管理器"
|
||||
"manager.new": "新建项目"
|
||||
"manager.new.default_name": "未命名项目"
|
||||
"manager.new.project_name": "项目名称"
|
||||
"manager.open": "打开"
|
||||
"manager.rename": "重命名…"
|
||||
"manager.rename.title": "重命名工程"
|
||||
@@ -301,7 +302,7 @@
|
||||
"about.description": "Oak 视频编辑器是一款自由开源的非线性视频编辑器。本软件以 GNU GPL 第三版许可证发布。"
|
||||
"about.thanks": "特别感谢我们的社区推广者 Enzo GD,感谢他在项目早期为传播本项目所给予的慷慨支持。"
|
||||
"about.fork_notice": "本项目是 Olive 视频编辑器的 fork。"
|
||||
"file.open.title": "打开项目"
|
||||
"file.open.title": "打开项目(.ove / .otio / .fcpxml)"
|
||||
"file.import_footage.title": "导入素材"
|
||||
"preferences.title": "偏好设置"
|
||||
"preferences.section.general": "常规"
|
||||
@@ -372,6 +373,18 @@
|
||||
"actionsearch.no_actions": "没有可用动作"
|
||||
"export.title": "导出序列"
|
||||
"export.format": "格式"
|
||||
"export.video_codec": "视频编码"
|
||||
"export.audio_codec": "音频编码"
|
||||
"export.color": "色域/色深"
|
||||
"export.color.sdr": "SDR — Rec.709 8 位"
|
||||
"export.resolution": "分辨率(0 = 跟随序列)"
|
||||
"export.framerate": "帧率(0 = 跟随序列)"
|
||||
"export.bitrate": "码率(0 = 编码器默认)"
|
||||
"export.color.hdr": "HDR — BT.2020 10 位"
|
||||
"export.range": "导出范围"
|
||||
"export.range.all": "整条时间轴"
|
||||
"export.range.inout": "入点-出点之间"
|
||||
"project.export.format": "项目格式"
|
||||
"export.format.placeholder": "选择格式…"
|
||||
"export.path": "输出路径"
|
||||
"export.run": "导出"
|
||||
|
||||
+255
-17
@@ -140,6 +140,12 @@ mod modal_ids {
|
||||
pub const MULTICAM_WIZARD: usize = 17;
|
||||
/// The rename dialog (project explorer 重命名).
|
||||
pub const RENAME: usize = 18;
|
||||
/// The project export dialog (文件 → 导出工程文件…).
|
||||
pub const EXPORT_PROJECT_DFD: usize = 19;
|
||||
/// The new-project dialog (文件 → 新建项目).
|
||||
pub const NEW_PROJECT: usize = 20;
|
||||
/// The save-as dialog (文件 → 另存为).
|
||||
pub const SAVE_AS: usize = 21;
|
||||
}
|
||||
|
||||
/// What a picked platform-dialog path should do.
|
||||
@@ -239,6 +245,22 @@ enum ModalState<E: AppEngine> {
|
||||
content: Entity<crate::dialogs::RenameContent>,
|
||||
entry_id: u64,
|
||||
},
|
||||
/// The project export dialog (文件 → 导出工程文件…): choose the
|
||||
/// project format (OTIO / OVE / FCPXML) + the output path.
|
||||
ExportProjectDfd {
|
||||
modal: Entity<Modal>,
|
||||
content: Entity<crate::dialogs::ExportProjectDialogContent>,
|
||||
},
|
||||
/// The new-project dialog (文件 → 新建项目): name + sequence format.
|
||||
NewProject {
|
||||
modal: Entity<Modal>,
|
||||
content: Entity<crate::dialogs::NewProjectContent>,
|
||||
},
|
||||
/// The save-as dialog (文件 → 另存为): the copy's name.
|
||||
SaveAs {
|
||||
modal: Entity<Modal>,
|
||||
content: Entity<crate::dialogs::RenameContent>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A running export: the session the tick loop drains for progress.
|
||||
@@ -266,6 +288,9 @@ impl<E: AppEngine> ModalState<E> {
|
||||
| ModalState::DropSequenceChoice { modal, .. }
|
||||
| ModalState::MulticamWizard { modal, .. }
|
||||
| ModalState::EntryRename { modal, .. } => Some(modal.clone()),
|
||||
| ModalState::ExportProjectDfd { modal, .. } => Some(modal.clone()),
|
||||
| ModalState::NewProject { modal, .. } => Some(modal.clone()),
|
||||
| ModalState::SaveAs { modal, .. } => Some(modal.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1010,13 +1035,13 @@ impl<E: AppEngine> OakApp<E> {
|
||||
use crate::actions::ActionId as A;
|
||||
match action {
|
||||
// --- File ------------------------------------------------------
|
||||
A::NewProject => self.new_project(cx),
|
||||
A::NewProject => self.open_new_project(cx),
|
||||
A::OpenProject => self.open_file_dialog(FileAction::Open, cx),
|
||||
A::OpenFromLibrary | A::ProjectManager => self.show_project_manager(cx),
|
||||
A::Import => self.open_file_dialog(FileAction::ImportFootage, cx),
|
||||
A::SaveProject | A::SaveProjectAs => {
|
||||
self.open_file_dialog(FileAction::ExportProjectFile, cx)
|
||||
}
|
||||
// The 导出工程文件… dialog: choose OTIO / OVE / FCPXML + path.
|
||||
A::SaveProject => self.open_export_project_dfd(cx),
|
||||
A::SaveProjectAs => self.open_save_as(cx),
|
||||
A::CloseProject => self
|
||||
.engine
|
||||
.update(cx, |engine, cx| engine.close_project(cx)),
|
||||
@@ -1249,14 +1274,61 @@ impl<E: AppEngine> OakApp<E> {
|
||||
/// 新建项目: creates a blank project in the library and opens it (the
|
||||
/// write-through persists it from the first edit). Falls back to the
|
||||
/// engine's plain new-project path when the library is unavailable.
|
||||
fn new_project(&mut self, cx: &mut Context<Self>) {
|
||||
let name = crate::i18n::tr("manager.new.default_name").to_string();
|
||||
let result = self
|
||||
.engine
|
||||
.update(cx, |engine, cx| engine.library_create_project(&name, cx));
|
||||
if let Err(err) = result {
|
||||
println!("[file] library create failed ({err}); plain new project");
|
||||
self.engine.update(cx, |engine, cx| engine.new_project(cx));
|
||||
fn open_new_project(&mut self, cx: &mut Context<Self>) {
|
||||
if !matches!(self.modal, ModalState::None) {
|
||||
return;
|
||||
}
|
||||
self.spawn_modal(cx, move |window, app| {
|
||||
let content = app.new(|cx| crate::dialogs::NewProjectContent::new(window, cx));
|
||||
let modal = app.new(|cx| {
|
||||
Modal::new(
|
||||
modal_ids::NEW_PROJECT,
|
||||
ModalOptions::new(
|
||||
crate::i18n::tr("menu.file.new_project"),
|
||||
px(480.0),
|
||||
)
|
||||
.with_button(DialogButton::primary(crate::i18n::tr("dialog.ok")))
|
||||
.with_button(DialogButton::new(
|
||||
crate::i18n::tr("dialog.cancel"),
|
||||
gpui_widgets::dialog::DialogButtonRole::Secondary,
|
||||
)),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.with_content(content.clone())
|
||||
});
|
||||
ModalState::NewProject { modal, content }
|
||||
});
|
||||
}
|
||||
|
||||
/// Creates the project from the dialog's name + format, then seeds its
|
||||
/// first sequence (the old `new_project` path kept for manager fallback).
|
||||
fn commit_new_project(&mut self, content: &Entity<crate::dialogs::NewProjectContent>, cx: &mut Context<Self>) {
|
||||
let name = content.read(cx).name(cx).to_string();
|
||||
let format = content.read(cx).format(cx);
|
||||
let interlaced = content.read(cx).interlaced(cx);
|
||||
let engine = self.engine.clone();
|
||||
let result = engine.update(cx, |engine, cx| {
|
||||
engine.library_create_project(&name, cx)
|
||||
});
|
||||
match result {
|
||||
Ok(()) => {
|
||||
self.close_modal(cx);
|
||||
let engine = self.engine.clone();
|
||||
engine.update(cx, |engine, cx| {
|
||||
let _ = engine.create_sequence_with_params(
|
||||
crate::i18n::tr("manager.new.default_name").to_string(),
|
||||
format,
|
||||
interlaced,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[file] library create failed ({err}); plain new project");
|
||||
self.close_modal(cx);
|
||||
self.engine.update(cx, |engine, cx| engine.new_project(cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2542,6 +2614,113 @@ impl<E: AppEngine> OakApp<E> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Opens the 导出工程文件 dialog: choose the project file format
|
||||
/// (OTIO / OVE / FCPXML) and the output path; the OK button runs the
|
||||
/// engine's export_project_path with the picked extension.
|
||||
fn open_export_project_dfd(&mut self, cx: &mut Context<Self>) {
|
||||
if !matches!(self.modal, ModalState::None) {
|
||||
return;
|
||||
}
|
||||
let current = self
|
||||
.engine
|
||||
.read(cx)
|
||||
.project()
|
||||
.map(|p| p.path.clone())
|
||||
.filter(|p| !p.as_os_str().is_empty());
|
||||
let suggested = current
|
||||
.and_then(|path| {
|
||||
path.file_stem()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
})
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
self.spawn_modal(cx, move |window, app| {
|
||||
let content = app.new(|cx| {
|
||||
crate::dialogs::ExportProjectDialogContent::new(window, cx)
|
||||
});
|
||||
// Default selection is OTIO: pre-fill the pathway with the
|
||||
// correct extension, the OK handler applies the chosen one.
|
||||
content.update(app, |content, cx| {
|
||||
content.set_path(format!("{suggested}.otio"), cx)
|
||||
});
|
||||
let modal = app.new(|cx| {
|
||||
Modal::new(
|
||||
modal_ids::EXPORT_PROJECT_DFD,
|
||||
ModalOptions::new(
|
||||
crate::i18n::tr("menu.file.export_project"),
|
||||
px(480.0),
|
||||
)
|
||||
.with_button(DialogButton::primary(crate::i18n::tr("dialog.ok")))
|
||||
.with_button(DialogButton::new(
|
||||
crate::i18n::tr("dialog.cancel"),
|
||||
gpui_widgets::dialog::DialogButtonRole::Secondary,
|
||||
)),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.with_content(content.clone())
|
||||
});
|
||||
ModalState::ExportProjectDfd { modal, content }
|
||||
});
|
||||
}
|
||||
|
||||
/// Opens the 另存为 dialog: the copy's new name (prefilled with the
|
||||
/// current project name). OK duplicates the project into the library
|
||||
/// under that name and switches to the copy.
|
||||
fn open_save_as(&mut self, cx: &mut Context<Self>) {
|
||||
if !matches!(self.modal, ModalState::None) {
|
||||
return;
|
||||
}
|
||||
let current_name = self
|
||||
.engine
|
||||
.read(cx)
|
||||
.project()
|
||||
.map(|p| p.name.clone())
|
||||
.unwrap_or_else(|| crate::i18n::tr("manager.new.default_name").to_string());
|
||||
let mut name = current_name.clone();
|
||||
if let Some((stem, _)) = name.rsplit_once('.') {
|
||||
name = stem.to_string();
|
||||
}
|
||||
name = format!("{name} Copy");
|
||||
self.spawn_modal(cx, move |window, app| {
|
||||
let content = app.new(|cx| {
|
||||
crate::dialogs::RenameContent::new(name.clone().into(), window, cx)
|
||||
});
|
||||
let modal = app.new(|cx| {
|
||||
Modal::new(
|
||||
modal_ids::SAVE_AS,
|
||||
ModalOptions::new(
|
||||
crate::i18n::tr("menu.file.save_as"),
|
||||
px(420.0),
|
||||
)
|
||||
.with_button(DialogButton::primary(crate::i18n::tr("dialog.ok")))
|
||||
.with_button(DialogButton::new(
|
||||
crate::i18n::tr("dialog.cancel"),
|
||||
gpui_widgets::dialog::DialogButtonRole::Secondary,
|
||||
)),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.with_content(content.clone())
|
||||
});
|
||||
ModalState::SaveAs { modal, content }
|
||||
});
|
||||
}
|
||||
|
||||
/// Confirms the save-as dialog: duplicate + switch to the copy.
|
||||
fn commit_save_as(
|
||||
&mut self,
|
||||
content: &Entity<crate::dialogs::RenameContent>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let name = content.read(cx).value(cx).to_string();
|
||||
let engine = self.engine.clone();
|
||||
let result = engine.update(cx, |engine, cx| engine.save_project_as(&name, cx));
|
||||
if let Err(err) = result {
|
||||
println!("[file] save as failed: {err}");
|
||||
}
|
||||
self.close_modal(cx);
|
||||
}
|
||||
|
||||
/// Opens the multicam wizard (窗口 → 多机位向导): pick the angle
|
||||
/// footage, choose the sync mode, then create the multicam sequence.
|
||||
fn open_multicam_wizard(&mut self, cx: &mut Context<Self>) {
|
||||
@@ -2704,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() {
|
||||
@@ -2720,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) => {
|
||||
@@ -2946,6 +3125,63 @@ impl<E: AppEngine> OakApp<E> {
|
||||
}
|
||||
}
|
||||
}
|
||||
modal_ids::EXPORT_PROJECT_DFD => {
|
||||
if let ModalState::ExportProjectDfd { content, .. } = &self.modal {
|
||||
if *button == 0 {
|
||||
// OK: export with the chosen format — the path
|
||||
// gets the format's extension appended.
|
||||
let format = content.read(cx).format(cx);
|
||||
let ext = match format {
|
||||
crate::dialogs::PROJECT_FORMAT_OVE => "ove",
|
||||
crate::dialogs::PROJECT_FORMAT_FCPXML => "fcpxml",
|
||||
_ => "otio",
|
||||
};
|
||||
let path = content.read(cx).path(cx).to_string();
|
||||
let path = if path.to_ascii_lowercase().ends_with(".otio")
|
||||
|| path.to_ascii_lowercase().ends_with(".ove")
|
||||
|| path.to_ascii_lowercase().ends_with(".fcpxml")
|
||||
{
|
||||
// Stem swap: the entered name already has
|
||||
// an extension — replace it.
|
||||
let stem = path
|
||||
.split('.')
|
||||
.next()
|
||||
.unwrap_or(&path)
|
||||
.to_string();
|
||||
format!("{stem}.{ext}")
|
||||
} else {
|
||||
format!("{path}.{ext}")
|
||||
};
|
||||
let engine = self.engine.clone();
|
||||
engine.update(cx, |engine, cx| {
|
||||
engine.export_project_path(PathBuf::from(path), cx)
|
||||
});
|
||||
self.close_modal(cx);
|
||||
} else {
|
||||
self.close_modal(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
modal_ids::NEW_PROJECT => {
|
||||
if let ModalState::NewProject { content, .. } = &self.modal {
|
||||
if *button == 0 {
|
||||
let content = content.clone();
|
||||
self.commit_new_project(&content, cx);
|
||||
} else {
|
||||
self.close_modal(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
modal_ids::SAVE_AS => {
|
||||
if let ModalState::SaveAs { content, .. } = &self.modal {
|
||||
if *button == 0 {
|
||||
let content = content.clone();
|
||||
self.commit_save_as(&content, cx);
|
||||
} else {
|
||||
self.close_modal(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
modal_ids::SEQUENCE_PROPERTIES => {
|
||||
if let ModalState::SequenceProperties { content, .. } = &self.modal {
|
||||
let content = content.clone();
|
||||
@@ -4751,15 +4987,17 @@ mod tests {
|
||||
);
|
||||
|
||||
// Down selects the first listed action, Enter runs it. The empty query
|
||||
// lists every menu-bar action, so the first one is New Project.
|
||||
// lists every menu-bar action, so the first one is New Project — the
|
||||
// v0.5 flow opens the new-project dialog (name + format), so the
|
||||
// search dialog hands off to it.
|
||||
cx.dispatch_keystroke(window.into(), gpui::Keystroke::parse("down").unwrap());
|
||||
cx.run_until_parked();
|
||||
cx.dispatch_keystroke(window.into(), gpui::Keystroke::parse("enter").unwrap());
|
||||
cx.run_until_parked();
|
||||
|
||||
assert!(
|
||||
cx.read(|app| matches!(root.read(app).modal, ModalState::None)),
|
||||
"executing the selected action closes the dialog"
|
||||
cx.read(|app| matches!(root.read(app).modal, ModalState::NewProject { .. })),
|
||||
"the search dialog hands off to the new-project dialog"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -993,14 +993,27 @@ 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>,
|
||||
resolution_w: Entity<SpinBox>,
|
||||
resolution_h: Entity<SpinBox>,
|
||||
frame_rate: Entity<SpinBox>,
|
||||
bitrate: Entity<SpinBox>,
|
||||
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 +1034,114 @@ 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));
|
||||
|
||||
// Resolution / frame rate / bitrate SPIN boxes: 0 = keep the
|
||||
// sequence's / the codec's default (the engine treats 0 that way),
|
||||
// any typed value is honored (the backend accepts arbitrary
|
||||
// width/height/rate/bitrate).
|
||||
let resolution_w = cx.new(|cx| {
|
||||
SpinBox::new(
|
||||
9,
|
||||
SliderModel::new(ValueKind::Integer, 0.0, 16384.0, 4.0, 0.0),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let resolution_h = cx.new(|cx| {
|
||||
SpinBox::new(
|
||||
12,
|
||||
SliderModel::new(ValueKind::Integer, 0.0, 16384.0, 4.0, 0.0),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let frame_rate = cx.new(|cx| {
|
||||
SpinBox::new(
|
||||
10,
|
||||
SliderModel::new(ValueKind::Float, 0.0, 240.0, 0.1, 0.0),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let bitrate = cx.new(|cx| {
|
||||
SpinBox::new(
|
||||
11,
|
||||
SliderModel::new(ValueKind::Integer, 0.0, 500_000_000.0, 1_000_000.0, 0.0),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let path = cx.new(|cx| {
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
PathField {
|
||||
@@ -1031,11 +1152,61 @@ impl ExportDialogContent {
|
||||
|
||||
Self {
|
||||
format,
|
||||
video_codec,
|
||||
audio_codec,
|
||||
color,
|
||||
range,
|
||||
resolution_w,
|
||||
resolution_h,
|
||||
frame_rate,
|
||||
bitrate,
|
||||
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 +1241,65 @@ impl ExportDialogContent {
|
||||
.update(cx, |content, cx| content.set_path(path, cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// The full export settings (codec/color/bit-depth/range/size/fps/
|
||||
/// bitrate) 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);
|
||||
let w = self.resolution_w.read(cx).value().to_f64().round() as i32;
|
||||
let h = self.resolution_h.read(cx).value().to_f64().round() as i32;
|
||||
let size = if w > 0 && h > 0 { (w, h) } else { (0, 0) };
|
||||
let frame_rate = self.frame_rate.read(cx).value().to_f64();
|
||||
let video_bitrate = self.bitrate.read(cx).value().to_f64().round() as i64;
|
||||
crate::oakui::engine::ExportSettings {
|
||||
format: self.active_format,
|
||||
video_codec,
|
||||
audio_codec,
|
||||
size,
|
||||
frame_rate,
|
||||
video_bitrate,
|
||||
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 +1315,49 @@ 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.resolution").into(),
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(self.resolution_w.clone())
|
||||
.child(div()
|
||||
.text_color(colors.disabled)
|
||||
.text_xs()
|
||||
.child("×"))
|
||||
.child(self.resolution_h.clone()),
|
||||
))
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("export.framerate").into(),
|
||||
self.frame_rate.clone(),
|
||||
))
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("export.bitrate").into(),
|
||||
self.bitrate.clone(),
|
||||
))
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("export.path").into(),
|
||||
@@ -3857,6 +4130,159 @@ impl Render for RenameContent {
|
||||
}
|
||||
}
|
||||
|
||||
/// The project export dialog (文件 → 导出工程文件…): choose the project
|
||||
/// file format (OTIO / OVE / FCPXML) and the output path. The host reads
|
||||
/// [`Self::format`] + [`Self::path`] on OK.
|
||||
pub struct ExportProjectDialogContent {
|
||||
format: Entity<ComboBox>,
|
||||
path: Entity<PathField>,
|
||||
/// (format id, display name) in dropdown order.
|
||||
formats: Vec<(i32, String)>,
|
||||
}
|
||||
|
||||
/// The project-file format ids (mirror the engine's serializer dispatch;
|
||||
/// the extension is derived on the host).
|
||||
pub const PROJECT_FORMAT_OTIO: i32 = 0;
|
||||
pub const PROJECT_FORMAT_OVE: i32 = 1;
|
||||
pub const PROJECT_FORMAT_FCPXML: i32 = 2;
|
||||
|
||||
impl ExportProjectDialogContent {
|
||||
/// Builds the dialog (OTIO default, empty path).
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let formats: Vec<(i32, String)> = vec![
|
||||
(PROJECT_FORMAT_OTIO, "OpenTimelineIO (.otio)".to_string()),
|
||||
(PROJECT_FORMAT_OVE, "Oak (.ove)".to_string()),
|
||||
(PROJECT_FORMAT_FCPXML, "Final Cut Pro XML (.fcpxml)".to_string()),
|
||||
];
|
||||
let options = formats
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (_, name))| ComboBoxOption::new(i, name.clone()))
|
||||
.collect();
|
||||
let format = cx.new(|cx| {
|
||||
ComboBox::new(4, options, window, cx)
|
||||
.with_placeholder(i18n::tr("project.export.format"))
|
||||
});
|
||||
format.update(cx, |combo, cx| combo.set_selected(Some(PROJECT_FORMAT_OTIO as usize), cx));
|
||||
let path = cx.new(|cx| {
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
PathField { editor, enabled: true }
|
||||
});
|
||||
Self { format, path, formats }
|
||||
}
|
||||
|
||||
/// The selected project format id.
|
||||
pub fn format(&self, cx: &App) -> i32 {
|
||||
let Some(selected) = self.format.read(cx).selected() else {
|
||||
return PROJECT_FORMAT_OTIO;
|
||||
};
|
||||
self.formats
|
||||
.get(selected)
|
||||
.map(|(id, _)| *id)
|
||||
.unwrap_or(PROJECT_FORMAT_OTIO)
|
||||
}
|
||||
|
||||
/// The format's file extension (no dot).
|
||||
pub fn extension(&self, cx: &App) -> &'static str {
|
||||
match self.format(cx) {
|
||||
PROJECT_FORMAT_OVE => "ove",
|
||||
PROJECT_FORMAT_FCPXML => "fcpxml",
|
||||
_ => "otio",
|
||||
}
|
||||
}
|
||||
|
||||
/// The output path currently entered.
|
||||
pub fn path(&self, cx: &App) -> SharedString {
|
||||
self.path.read(cx).path(cx)
|
||||
}
|
||||
|
||||
/// Pre-fills the output path (the suggested file name).
|
||||
pub fn set_path(&mut self, path: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||
let path = path.into();
|
||||
self.path.update(cx, |field, cx| field.set_path(path, cx));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ExportProjectDialogContent {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = cx.default_colors().clone();
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("project.export.format").into(),
|
||||
self.format.clone(),
|
||||
))
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("export.path").into(),
|
||||
self.path.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// The new-project dialog (文件 → 新建项目): project name + the first
|
||||
/// sequence's format (preset / custom width-height-rate / interlaced).
|
||||
/// The host reads [`Self::name`] + [`Self::format`]; the engine's
|
||||
/// library_create_project then seeds the first sequence.
|
||||
pub struct NewProjectContent {
|
||||
name: Entity<TextValue>,
|
||||
format: Entity<SequenceFormatFields>,
|
||||
}
|
||||
|
||||
impl NewProjectContent {
|
||||
/// Builds the dialog (default name, HD 1080p25).
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let name = cx.new(|cx| {
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
TextValue { editor }
|
||||
});
|
||||
name.update(cx, |field, cx| {
|
||||
field.set_value(i18n::tr("manager.new.default_name"), cx)
|
||||
});
|
||||
let format = cx.new(|cx| {
|
||||
SequenceFormatFields::build(SequenceFormatSeed::hd_1080p25(), window, cx)
|
||||
});
|
||||
Self { name, format }
|
||||
}
|
||||
|
||||
/// The project name entered.
|
||||
pub fn name(&self, cx: &App) -> SharedString {
|
||||
self.name.read(cx).value(cx)
|
||||
}
|
||||
|
||||
/// The format selected.
|
||||
pub fn format(&self, cx: &App) -> crate::oakui::engine::VideoFormat {
|
||||
self.format.read(cx).format(cx)
|
||||
}
|
||||
|
||||
/// The interlaced flag.
|
||||
pub fn interlaced(&self, cx: &App) -> bool {
|
||||
self.format.read(cx).interlaced(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for NewProjectContent {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = cx.default_colors().clone();
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("manager.new.project_name").into(),
|
||||
self.name.clone(),
|
||||
))
|
||||
.child(self.format.read(cx).rows(&colors))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -726,6 +726,14 @@ pub trait AppEngine:
|
||||
Err("project library not supported".into())
|
||||
}
|
||||
|
||||
/// 另存为: saves the OPEN project into the library as a differently-
|
||||
/// named copy and switches the current project to that copy. Default:
|
||||
/// unsupported.
|
||||
fn save_project_as(&mut self, name: &str, cx: &mut Context<Self>) -> Result<(), String> {
|
||||
let _ = (name, cx);
|
||||
Err("save-as not supported".into())
|
||||
}
|
||||
|
||||
/// Imports a `.ove` / `.otio` / `.fcpxml` project file into the library
|
||||
/// as a new row; returns the new row's uuid.
|
||||
fn library_import_project(&mut self, path: PathBuf) -> Result<String, String> {
|
||||
@@ -802,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
|
||||
@@ -1496,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 || {
|
||||
|
||||
@@ -5387,6 +5387,26 @@ impl AppEngine for RealEngine {
|
||||
.map_err(|e| format!("failed to duplicate the project: {e}"))
|
||||
}
|
||||
|
||||
/// 另存为: duplicate the open project into the library under a new
|
||||
/// (named) row, then open that copy as the current project. The
|
||||
/// duplicator applies the new name to the copy's row and projectname.
|
||||
fn save_project_as(&mut self, name: &str, cx: &mut Context<Self>) -> Result<(), String> {
|
||||
let Some(project) = self.project.clone() else {
|
||||
return Err("no project open".to_string());
|
||||
};
|
||||
let current_uuid = {
|
||||
let guard = graphops::lock(&project);
|
||||
guard.uuid.clone()
|
||||
};
|
||||
let new_uuid = graphops::library_duplicate(¤t_uuid)
|
||||
.map_err(|e| format!("duplicate failed: {e}"))?;
|
||||
graphops::library_rename(&new_uuid, name.trim())
|
||||
.map_err(|e| format!("rename copy failed: {e}"))?;
|
||||
// Open the new row (adopt switches the current project).
|
||||
self.open_library_project(&new_uuid, cx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn library_import_project(&mut self, path: PathBuf) -> Result<String, String> {
|
||||
graphops::library_import(&path)
|
||||
.map_err(|e| format!("failed to import \"{}\": {e}", path.display()))
|
||||
@@ -6176,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