diff --git a/CMakeLists.txt b/CMakeLists.txt index ab6427c..7f64eb7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ target_sources(raven editing.h inspector.h timeline.h + tools.h colors.h widgets.h @@ -21,6 +22,7 @@ target_sources(raven editing.cpp inspector.cpp timeline.cpp + tools.cpp colors.cpp widgets.cpp diff --git a/app.cpp b/app.cpp index c6e4c0d..cf9e4ab 100644 --- a/app.cpp +++ b/app.cpp @@ -12,6 +12,7 @@ #include "imgui_internal.h" #include "widgets.h" +#include "tools.h" #ifndef EMSCRIPTEN #include "nfd.h" @@ -581,6 +582,14 @@ void MainInit(int argc, char** argv, int initial_width, int initial_height) { LoadFonts(); + // Check for otiotool + if (otiotool_found()) { + appState.otiotool_found = true; + Message("otiotool found, relevant tools have been enabled"); + } else { + Message("oitotool not found, relevant tools have been disabled"); + } + if (argc > 1) { LoadFile(argv[1]); } @@ -942,6 +951,24 @@ void MainGui() { if (appState.show_implot_demo_window) { ImPlot::ShowDemoWindow(); } + + // Handle tool popups + // These modal popups are all triggered by the Tools menu and + // therefore can't have their draw code in the menu code. To + // get around that the menu flags if a modal is to be drawn and + // then we call the relevant ImGui::OpenPopup here. We then call + // our all encompassing DrawToolPopups function. + if (appState.draw_stat_popup) { + ImGui::OpenPopup("Statistics"); + appState.draw_stat_popup = false; + } + if (appState.draw_extract_clips) { + ImGui::OpenPopup("Extract Clips"); + appState.draw_extract_clips = false; + } + if (GetActiveRoot()) { + DrawToolPopups(); + } } void SaveTheme() { @@ -1066,6 +1093,72 @@ void DrawMenu() { ImGui::EndMenu(); } + if (ImGui::BeginMenu("Tools", GetActiveRoot() && appState.otiotool_found)) { + std::string current_file = appState.active_tab->file_path; + if (ImGui::MenuItem("Redact OTIO File")) { + if (Redact()) { + Message("Successfully redacted %s\n", current_file.c_str()); + } else { + ErrorMessage("Failed to redact %s\n", current_file.c_str()); + } + } + if (ImGui::BeginMenu("Extract Track Type")) { + if (ImGui::MenuItem("Video Tracks Only")) { + if (VideoOnly()) { + Message("Sucessfully extracted video tracks from %s\n", current_file.c_str()); + } else { + ErrorMessage("Failed to extract video tracks from %s\n", current_file.c_str()); + } + } + if (ImGui::MenuItem("Audio Tracks Only")) { + if (AudioOnly()) { + Message("Sucessfully extracted audio tracks from %s\n", current_file.c_str()); + } else { + ErrorMessage("Failed to extract audio tracks from %s\n", current_file.c_str()); + } + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Flatten Tracks")) { + if (ImGui::MenuItem("All")) { + if (FlattenAllTracks()) { + Message("Sucessfully flattened tracks from %s\n", current_file.c_str()); + } else { + ErrorMessage("Failed to flatten tracks from %s\n", current_file.c_str()); + } + } + if (ImGui::MenuItem("Video")) { + if (FlattenVideoTracks()) { + Message("Sucessfully flattened video tracks from %s\n", current_file.c_str()); + } else { + ErrorMessage("Failed to flatten video tracks from %s\n", current_file.c_str()); + } + } + if (ImGui::MenuItem("Audio")) { + if (FlattenAudioTracks()) { + Message("Sucessfully flattened audio tracks from %s\n", current_file.c_str()); + } else { + ErrorMessage("Failed to flatten audio tracks from %s\n", current_file.c_str()); + } + } + ImGui::EndMenu(); + } + if (ImGui::MenuItem("Extract Clips")) { + // This option requires user input before the otiotool command + // can be called so we simply flag the popup for drawing here. + appState.draw_extract_clips = true; + } + if (ImGui::MenuItem("Statistics")) { + if (Statistics()) { + Message("Sucessfully returned statistics from %s\n", current_file.c_str()); + appState.draw_stat_popup = true; + } else { + ErrorMessage("Failed to return statistics from %s\n", current_file.c_str()); + } + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("View")) { bool showTimecodeOnClips = appState.track_height >= appState.default_track_height * 2; if (ImGui::MenuItem( diff --git a/app.h b/app.h index c4275d8..e56b8c5 100644 --- a/app.h +++ b/app.h @@ -136,6 +136,14 @@ struct AppState { bool show_demo_window = false; bool show_metrics = false; bool show_implot_demo_window = false; + + // Was otiotool found? + bool otiotool_found = false; + + // otiotool data for popup windows + std::string otiotool_return_value; // Value returned by call to otiotool + bool draw_stat_popup = false; // Draw statistics popup + bool draw_extract_clips = false; // Draw clip extraction popup }; extern AppState appState; diff --git a/inspector.cpp b/inspector.cpp index 5b4eadc..ab74850 100644 --- a/inspector.cpp +++ b/inspector.cpp @@ -803,7 +803,7 @@ void DrawInspector() { } // Set the active media ref key based on user selection - if (ImGui::Combo("", &appState.selected_reference_index, reference_names.data(), num_references)) { + if (ImGui::Combo("##", &appState.selected_reference_index, reference_names.data(), num_references)) { if (appState.selected_reference_index >= 0 && appState.selected_reference_index < num_references) { clip->set_active_media_reference_key(reference_names[appState.selected_reference_index]); } diff --git a/tools.cpp b/tools.cpp new file mode 100644 index 0000000..a6da064 --- /dev/null +++ b/tools.cpp @@ -0,0 +1,220 @@ +// Tools +#include "tools.h" + +#include "app.h" + +#include +#include +#include + +std::string run_subprocess(const std::string cmd, int& return_val) +{ + #ifdef _WIN32 + auto pipe = _popen(cmd.c_str(), "r"); + #else + auto pipe = popen(cmd.c_str(), "r"); + #endif + + if (pipe == nullptr) { + std::cout << "Failed to open pipe" << std::endl; + return_val = 1; + return std::string(); + } + + char buffer[8192]; + std::string result; + while (fgets(buffer, sizeof buffer, pipe) != NULL){ + result += buffer; + } + + #ifdef _WIN32 + return_val = _pclose(pipe); + #else + return_val = pclose(pipe); + #endif + + return result; +} + +bool otiotool_found() +{ + int result; + + run_subprocess("otiotool -h", result); + + return !result; +} + +std::string run_otiotool_command(std::string options, bool output = true, bool debug = false) +{ + // Write the current root to a temp json file + std::filesystem::path file = std::filesystem::temp_directory_path(); + file.replace_filename(std::tmpnam(nullptr)); + file.replace_extension("otio"); + if (debug) { + std::cout << file << std::endl; + } + GetActiveRoot()->to_json_file(file.generic_string()); + + // Build command, the file path is wrapped in quotation marks in case of spaces + std::string command = "otiotool --input \"" + file.generic_string() + "\" " + options; + + // Output otio file? + if (output) { + command += " --output -"; + } + + if (debug) { + std::cout << command << std::endl; + } + + // Run subproces + int return_val = 0; + std::string result = run_subprocess(command, return_val); + + // Clean up temp file + std::remove(file.generic_string().c_str()); + + // Load new otio file + if (!result.empty() && return_val == 0) { + return result; + } else { + ErrorMessage("Error trying to run otiotool command, see console for details"); + return ""; + } +} + +bool load_otio_file_from_otiotool_command(std::string options,bool output = true, bool debug = false) +{ + std::string result = run_otiotool_command(options, output, debug); + + // Load new otio file + if (!result.empty()) { + LoadString(result); + return true; + } else { + ErrorMessage("Error trying to run otiotool command, see console for details"); + return false; + } +} + +bool Redact() { + return load_otio_file_from_otiotool_command("--redact"); +} + +bool VideoOnly() { + return load_otio_file_from_otiotool_command("--video-only"); +} + +bool AudioOnly() { + return load_otio_file_from_otiotool_command("--audio-only"); +} + +bool FlattenAllTracks() { + return load_otio_file_from_otiotool_command("--flatten all"); +} + +bool FlattenVideoTracks() { + return load_otio_file_from_otiotool_command("--flatten video"); +} + +bool FlattenAudioTracks() { + return load_otio_file_from_otiotool_command("--flatten audio"); +} + +bool Statistics() { + std::string result = run_otiotool_command("--stats", false, false); + + if (!result.empty()) { + appState.otiotool_return_value = result; + return true; + } else { + ErrorMessage("Error trying to run otiotool command, see console for details"); + return false; + } +} + +void DrawStatisticsPopup() +{ + ImGui::Text("Statistics for %s", appState.active_tab->file_path.c_str()); + ImGui::Separator(); + + ImGui::Text("%s", appState.otiotool_return_value.c_str()); + + if (ImGui::Button("OK", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + appState.otiotool_return_value = ""; + } + + ImGui::EndPopup(); +} + +void DrawExtractClipsPopup() +{ + static bool use_regex = false; + + ImGui::BeginDisabled(use_regex); + static char clip_input[1024]; + ImGui::InputText("Clip Name(s)", clip_input, IM_ARRAYSIZE(clip_input)); + ImGui::EndDisabled(); + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + ImGui::Checkbox("Use regex", &use_regex); + ImGui::PopStyleVar(); + + ImGui::BeginDisabled(!use_regex); + static char regex_input[1024]; + ImGui::InputText("Regex", regex_input, IM_ARRAYSIZE(regex_input)); + ImGui::EndDisabled(); + + if (ImGui::Button("OK", ImVec2(120, 0))) { + std::string current_file = appState.active_tab->file_path; + if (!use_regex) { + std::string command = "--only-clips-with-name " + std::string(clip_input); + if (load_otio_file_from_otiotool_command(command, true, true)) { + Message("Sucessfully extracted clips from %s", current_file.c_str()); + } else { + ErrorMessage("Failed to extract clips from %s", current_file.c_str()); + } + strcpy(clip_input, ""); + } else { + std::string command = "--only-clips-with-name-regex " + std::string(regex_input); + if (load_otio_file_from_otiotool_command(command, true, true)) { + Message("Sucessfully extracted clips from %s", current_file.c_str()); + } else { + ErrorMessage("Failed to extract clips from %s", current_file.c_str()); + } + strcpy(regex_input, ""); + } + ImGui::CloseCurrentPopup(); + } + + ImGui::SetItemDefaultFocus(); + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120, 0))) { + strcpy(clip_input, ""); + strcpy(regex_input, ""); + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); +} + +void DrawToolPopups() +{ + // Always center this window when appearing + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + + bool close; + + // Statistics popup + if (ImGui::BeginPopupModal("Statistics", &close, ImGuiWindowFlags_AlwaysAutoResize)) { + DrawStatisticsPopup(); + } + + // Clip extraction popup + if (ImGui::BeginPopupModal("Extract Clips", &close, ImGuiWindowFlags_AlwaysAutoResize)) { + DrawExtractClipsPopup(); + } +} diff --git a/tools.h b/tools.h new file mode 100644 index 0000000..1cb2c60 --- /dev/null +++ b/tools.h @@ -0,0 +1,28 @@ +// Tools + + +bool otiotool_found(); + +void DrawToolPopups(); + +// Remove all metadata, names, or other identifying information from this +// timeline. Only the structure, schema and timing will remain. +bool Redact(); + +// Output only video tracks +bool VideoOnly(); + +// Output only audio tracks +bool AudioOnly(); + +// Flatten all tracks +bool FlattenAllTracks(); + +// Flatten vieo tracks +bool FlattenVideoTracks(); + +// Flatten audio tracks +bool FlattenAudioTracks(); + +// Display statistics about the current OTIO file +bool Statistics();