diff --git a/automation/vapoursynth/aegisub_vs.py b/automation/vapoursynth/aegisub_vs.py index 510f5b2ac..c3cc5876c 100644 --- a/automation/vapoursynth/aegisub_vs.py +++ b/automation/vapoursynth/aegisub_vs.py @@ -150,11 +150,17 @@ def make_keyframes(clip: vs.VideoNode, use_scxvid: bool = False, .format("scxvid" if use_scxvid else "wwxd")) keyframes = {} + done = 0 def _cb(n: int, f: vs.VideoFrame) -> vs.VideoFrame: + nonlocal done keyframes[n] = f.props._SceneChangePrev if use_scxvid else f.props.Scenechange # type: ignore + done += 1 + if done % (clip.num_frames // 25) == 0: + vs.core.log_message(vs.MESSAGE_TYPE_INFORMATION, "Detecting keyframes... {}% done.\n".format(100 * done // clip.num_frames)) return f deque(clip.std.ModifyFrame(clip, _cb).frames(close=True), 0) + vs.core.log_message(vs.MESSAGE_TYPE_INFORMATION, "Done detecting keyframes.\n") return [n for n in range(clip.num_frames) if keyframes[n]] @@ -178,6 +184,7 @@ def get_keyframes(filename: str, clip: vs.VideoNode, **kwargs: Any) -> str: kffilename = make_keyframes_filename(filename) if not os.path.exists(kffilename): + vs.core.log_message(vs.MESSAGE_TYPE_INFORMATION, "No keyframes file found, detecting keyframes...\n") keyframes = make_keyframes(clip, **kwargs) save_keyframes(kffilename, keyframes) diff --git a/libaegisub/include/libaegisub/background_runner.h b/libaegisub/include/libaegisub/background_runner.h index 29bd8efcb..d33f9d447 100644 --- a/libaegisub/include/libaegisub/background_runner.h +++ b/libaegisub/include/libaegisub/background_runner.h @@ -46,10 +46,15 @@ namespace agi { /// @brief Log a message /// - /// If any messages are logged then the dialog will not automatically close - /// when the task finishes so that the user has the chance to read them. + /// If any messages are logged and StayOpen is set (set by default) + /// then the dialog will not automatically close when the task finishes + /// so that the user has the chance to read them. virtual void Log(std::string const& str)=0; + /// Set whether the dialog should stay open after the task finishes. + /// Defaults to true. + virtual void SetStayOpen(bool stayopen)=0; + /// Has the user asked the task to cancel? virtual bool IsCancelled()=0; }; diff --git a/src/auto4_base.h b/src/auto4_base.h index 5f645ccd0..dae5f2fdc 100644 --- a/src/auto4_base.h +++ b/src/auto4_base.h @@ -127,6 +127,7 @@ namespace Automation4 { void SetMessage(std::string const& msg) override { impl->SetMessage(msg); } void SetProgress(int64_t cur, int64_t max) override { impl->SetProgress(cur, max); } void Log(std::string const& str) override { impl->Log(str); } + void SetStayOpen(bool stayopen) override { impl->SetStayOpen(stayopen); } bool IsCancelled() override { return impl->IsCancelled(); } /// Show the passed dialog on the GUI thread, blocking the calling diff --git a/src/dialog_progress.cpp b/src/dialog_progress.cpp index bcf4de96d..9970dd0e6 100644 --- a/src/dialog_progress.cpp +++ b/src/dialog_progress.cpp @@ -73,6 +73,7 @@ namespace { class DialogProgressSink final : public agi::ProgressSink { DialogProgress *dialog; std::atomic cancelled{false}; + std::atomic stayopen{true}; int progress = 0; public: @@ -98,6 +99,14 @@ public: Main().Async([=]{ dialog->pending_log += to_wx(str); }); } + void SetStayOpen(bool b) override { + stayopen = b; + } + + bool GetStayOpen() { + return stayopen; + } + bool IsCancelled() override { return cancelled; } @@ -169,7 +178,7 @@ void DialogProgress::Run(std::function task) { // so the user can read the debug output and switch the cancel button to a // close button bool cancelled = this->ps->IsCancelled(); - if (cancelled || (log_output->IsEmpty() && !pending_log)) + if (cancelled || !this->ps->GetStayOpen() || (log_output->IsEmpty() && !pending_log)) EndModal(!cancelled); else { if (!pending_log.empty()) { diff --git a/src/libresrc/default_config.json b/src/libresrc/default_config.json index 5daae04bd..4710552f9 100644 --- a/src/libresrc/default_config.json +++ b/src/libresrc/default_config.json @@ -373,6 +373,7 @@ "Seek Preroll" : 12 }, "VapourSynth" : { + "Log Level": "Information", "Default Script" : "# This default script will load a video file using LWLibavSource.\n# It requires the `lsmas` plugin.\n# See ?data/automation/vapoursynth/aegisub_vs.py for more information.\n\nimport aegisub_vs as a\nimport vapoursynth as vs\n\nclip, videoinfo = a.wrap_lwlibavsource(filename, __aegi_vscache)\nclip.set_output()\n__aegi_timecodes = videoinfo[\"timecodes\"]\n__aegi_keyframes = videoinfo[\"keyframes\"]\n# Uncomment this to automatically generate keyframes at scene changes.\n#__aegi_keyframes = a.get_keyframes(filename, clip)\n\n# Check if the file has an audio track. This requires the `bas` plugin.\n__aegi_hasaudio = 1 if a.check_audio(filename) else 0" } } diff --git a/src/libresrc/osx/default_config.json b/src/libresrc/osx/default_config.json index c819f2338..0bca6144b 100644 --- a/src/libresrc/osx/default_config.json +++ b/src/libresrc/osx/default_config.json @@ -373,6 +373,7 @@ "Seek Preroll" : 12 }, "VapourSynth" : { + "Log Level": "Information", "Default Script" : "# This default script will load a video file using LWLibavSource.\n# It requires the `lsmas` plugin.\n# See ?data/automation/vapoursynth/aegisub_vs.py for more information.\n\nimport aegisub_vs as a\nimport vapoursynth as vs\n\nclip, videoinfo = a.wrap_lwlibavsource(filename, __aegi_vscache)\nclip.set_output()\n__aegi_timecodes = videoinfo[\"timecodes\"]\n__aegi_keyframes = videoinfo[\"keyframes\"]\n# Uncomment this to automatically generate keyframes at scene changes.\n#__aegi_keyframes = a.get_keyframes(filename, clip)\n\n# Check if the file has an audio track. This requires the `bas` plugin.\n__aegi_hasaudio = 1 if a.check_audio(filename) else 0" } } diff --git a/src/preferences.cpp b/src/preferences.cpp index d97acb88c..b6ef356b6 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -472,6 +472,12 @@ void Advanced_Video(wxTreebook *book, Preferences *parent) { void VapourSynth(wxTreebook *book, Preferences *parent) { #ifdef WITH_VAPOURSYNTH auto p = new OptionPage(book, parent, _("VapourSynth"), OptionPage::PAGE_SUB); + auto general = p->PageSizer(_("General")); + + const wxString log_levels[] = { "Quiet", "Fatal", "Critical", "Warning", "Information", "Debug" }; + wxArrayString log_levels_choice(6, log_levels); + p->OptionChoice(general, _("Log Level"), log_levels_choice, "Provider/Video/VapourSynth/Log Level"); + auto video = p->PageSizer(_("Default Video Script")); auto vhint = new wxStaticText(p, wxID_ANY, _("This script will be executed to load video files that aren't\nVapourSynth scripts (i.e. end in .py or .vpy).\nThe filename variable stores the path to the file.")); diff --git a/src/vapoursynth_common.cpp b/src/vapoursynth_common.cpp index 9a0e3ebe3..b360c6144 100644 --- a/src/vapoursynth_common.cpp +++ b/src/vapoursynth_common.cpp @@ -20,6 +20,7 @@ #include "vapoursynth_wrap.h" #include "options.h" #include "utils.h" +#include #include #include @@ -60,6 +61,28 @@ int OpenScriptOrVideo(const VSAPI *api, const VSSCRIPTAPI *sapi, VSScript *scrip return result; } +void VSLogToProgressSink(int msgType, const char *msg, void *userData) { + int loglevel = 0; + std::string loglevel_str = OPT_GET("Provider/Video/VapourSynth/Log Level")->GetString(); + if (loglevel_str == "Quiet") + loglevel = 5; + else if (loglevel_str == "Fatal") + loglevel = 4; + else if (loglevel_str == "Critical") + loglevel = 3; + else if (loglevel_str == "Warning") + loglevel = 2; + else if (loglevel_str == "Information") + loglevel = 1; + else if (loglevel_str == "Debug") + loglevel = 0; + + if (msgType < loglevel) + return; + + reinterpret_cast(userData)->Log(msg); +} + void VSCleanCache() { CleanCache(config::path->Decode("?local/vscache/"), "", diff --git a/src/vapoursynth_common.h b/src/vapoursynth_common.h index af35da3aa..634c3e94f 100644 --- a/src/vapoursynth_common.h +++ b/src/vapoursynth_common.h @@ -21,5 +21,6 @@ int OpenScriptOrVideo(const VSAPI *api, const VSSCRIPTAPI *sapi, VSScript *script, agi::fs::path const& filename, std::string default_script); void VSCleanCache(); +void VSLogToProgressSink(int msgType, const char *msg, void *userData); #endif // WITH_VAPOURSYNTH diff --git a/src/video_provider_vs.cpp b/src/video_provider_vs.cpp index 4d2277317..b08dd65a6 100644 --- a/src/video_provider_vs.cpp +++ b/src/video_provider_vs.cpp @@ -123,15 +123,28 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename VSCleanCache(); int err1, err2; - script = vs.GetScriptAPI()->createScript(nullptr); + VSCore *core = vs.GetAPI()->createCore(0); + if (core == nullptr) { + throw VapoursynthError("Error creating core"); + } + script = vs.GetScriptAPI()->createScript(core); if (script == nullptr) { + vs.GetAPI()->freeCore(core); throw VapoursynthError("Error creating script API"); } vs.GetScriptAPI()->evalSetWorkingDir(script, 1); br->Run([&](agi::ProgressSink *ps) { ps->SetTitle(from_wx(_("Executing Vapoursynth Script"))); + ps->SetMessage(""); ps->SetIndeterminate(); + + VSLogHandle *logger = vs.GetAPI()->addLogHandler(VSLogToProgressSink, nullptr, ps, core); err1 = OpenScriptOrVideo(vs.GetAPI(), vs.GetScriptAPI(), script, filename, OPT_GET("Provider/Video/VapourSynth/Default Script")->GetString()); + vs.GetAPI()->removeLogHandler(logger, core); + + ps->SetStayOpen(bool(err1)); + if (err1) + ps->SetMessage(from_wx(_("Failed to execute script! Press \"Close\" to continue."))); }); if (err1) { std::string msg = agi::format("Error executing VapourSynth script: %s", vs.GetScriptAPI()->getError(script));