From 41ef3fa56acf0eb67f79f227b8b77c8606698de3 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Tue, 5 Sep 2023 00:21:11 +0200 Subject: [PATCH 1/5] vapoursynth: Remove old and incorrect docstring sentence --- automation/vapoursynth/aegisub_vs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/automation/vapoursynth/aegisub_vs.py b/automation/vapoursynth/aegisub_vs.py index 412ac1e5d..b7bd160ea 100644 --- a/automation/vapoursynth/aegisub_vs.py +++ b/automation/vapoursynth/aegisub_vs.py @@ -172,7 +172,6 @@ def make_keyframes(clip: vs.VideoNode, use_scxvid: bool = False, **kwargs: Any) -> List[int]: """ Generates a list of keyframes from a clip, using either WWXD or Scxvid. - Will be slightly more efficient with the `akarin` plugin installed. :param clip: Clip to process. :param use_scxvid: Whether to use Scxvid. If False, the function uses WWXD. From 79b3f4ccb053551afbea59d3d8999a6fef158202 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Wed, 18 Oct 2023 13:31:47 +0200 Subject: [PATCH 2/5] vapoursynth: Add option to enable/disable user plugin loading On Windows, plugins are shipped with Aegisub now and loaded from aegisub_vs.py, so user plugins installed out of Aegisub are more likely to just interfere with that (like when the user has an old version of libvslsmashsource installed). So this option defaults to off on Windows, but to on everywhere else since VS plugins aren't shipped with Aegisub there. Note that this option only disables autoloading of user plugins. VapourSynth loads system plugins no matter what. --- src/audio_provider_vs.cpp | 6 +++++- src/libresrc/default_config.json | 1 + src/libresrc/default_config_win.json | 6 ++++-- src/libresrc/meson.build | 12 +++++++++--- src/libresrc/osx/default_config.json | 1 + src/preferences.cpp | 4 +++- src/video_provider_vs.cpp | 2 +- 7 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/audio_provider_vs.cpp b/src/audio_provider_vs.cpp index 2d81f06bb..db9d6a871 100644 --- a/src/audio_provider_vs.cpp +++ b/src/audio_provider_vs.cpp @@ -58,7 +58,11 @@ VapourSynthAudioProvider::VapourSynthAudioProvider(agi::fs::path const& filename VSCleanCache(); - script = vs.GetScriptAPI()->createScript(nullptr); + VSCore *core = vs.GetAPI()->createCore(OPT_GET("Provider/VapourSynth/Autoload User Plugins")->GetBool() ? 0 : VSCoreCreationFlags::ccfDisableAutoLoading); + if (core == nullptr) { + throw VapourSynthError("Error creating core"); + } + script = vs.GetScriptAPI()->createScript(core); if (script == nullptr) { throw VapourSynthError("Error creating script API"); } diff --git a/src/libresrc/default_config.json b/src/libresrc/default_config.json index a93416e41..f2451d7ea 100644 --- a/src/libresrc/default_config.json +++ b/src/libresrc/default_config.json @@ -354,6 +354,7 @@ "Log Level" : "quiet" }, "VapourSynth" : { + "Autoload User Plugins": true, "Cache" : { "Files" : 500, "Size" : 1000 diff --git a/src/libresrc/default_config_win.json b/src/libresrc/default_config_win.json index 44b291343..5e3b68269 100644 --- a/src/libresrc/default_config_win.json +++ b/src/libresrc/default_config_win.json @@ -2,7 +2,9 @@ "Audio" : { "Player" : "DirectSound" }, - "Subtitle" : { - "Provider" : "CSRI/xy-vsfilter_aegisub" + "Provider" : { + "VapourSynth" : { + "Autoload User Plugins": false + } } } diff --git a/src/libresrc/meson.build b/src/libresrc/meson.build index 0b9414736..8d727b4c0 100644 --- a/src/libresrc/meson.build +++ b/src/libresrc/meson.build @@ -9,9 +9,15 @@ resrc = [ output: ['bitmap.cpp', 'bitmap.h']) ] -conf_platform_json = configure_file(input: 'default_config_platform.json.in', - output: '@BASENAME@', - configuration: conf_platform) +if host_machine.system() == 'windows' + conf_platform_json = configure_file(input: 'default_config_win.json', + output: 'default_config_platform.json', + copy: true) +else + conf_platform_json = configure_file(input: 'default_config_platform.json.in', + output: '@BASENAME@', + configuration: conf_platform) +endif if host_machine.system() == 'darwin' resmanifest = 'manifest_osx.respack' diff --git a/src/libresrc/osx/default_config.json b/src/libresrc/osx/default_config.json index 80b54b52e..8a43c5423 100644 --- a/src/libresrc/osx/default_config.json +++ b/src/libresrc/osx/default_config.json @@ -354,6 +354,7 @@ "Log Level" : "quiet" }, "VapourSynth" : { + "Autoload User Plugins": true, "Cache" : { "Files" : 500, "Size" : 1000 diff --git a/src/preferences.cpp b/src/preferences.cpp index ff468f000..ac3aad97c 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -476,7 +476,9 @@ void VapourSynth(wxTreebook *book, Preferences *parent) { 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"); + p->OptionChoice(general, _("Log level"), log_levels_choice, "Provider/Video/VapourSynth/Log Level"); + p->CellSkip(general); + p->OptionAdd(general, _("Load user plugins"), "Provider/VapourSynth/Autoload User Plugins"); auto video = p->PageSizer(_("Default Video Script")); diff --git a/src/video_provider_vs.cpp b/src/video_provider_vs.cpp index da4a3d1a5..a59c583e1 100644 --- a/src/video_provider_vs.cpp +++ b/src/video_provider_vs.cpp @@ -123,7 +123,7 @@ VapourSynthVideoProvider::VapourSynthVideoProvider(agi::fs::path const& filename VSCleanCache(); int err1, err2; - VSCore *core = vs.GetAPI()->createCore(0); + VSCore *core = vs.GetAPI()->createCore(OPT_GET("Provider/VapourSynth/Autoload User Plugins")->GetBool() ? 0 : VSCoreCreationFlags::ccfDisableAutoLoading); if (core == nullptr) { throw VapourSynthError("Error creating core"); } From a1b3e0d9f169bac2e4469a36d1730101f190215c Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Wed, 18 Oct 2023 14:20:37 +0200 Subject: [PATCH 3/5] vapoursynth: Allow scripts to control the progress dialog This happens via VapourSynth's message logger. Also add wrapper functions for this to aegisub_vs.py and add progress updates everywhere - both to improve UX and to help with debugging for when scripts get stuck somewhere. --- automation/vapoursynth/aegisub_vs.py | 50 +++++++++++++++++++++++++--- src/vapoursynth_common.cpp | 28 +++++++++++++++- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/automation/vapoursynth/aegisub_vs.py b/automation/vapoursynth/aegisub_vs.py index b7bd160ea..8e3ed93e8 100644 --- a/automation/vapoursynth/aegisub_vs.py +++ b/automation/vapoursynth/aegisub_vs.py @@ -22,6 +22,9 @@ Aegisub using the following variables: - __aegi_hasaudio: int: If nonzero, Aegisub will try to load an audio track from the same file. +The script can control the progress dialog shown by Aegisub with certain log +messages. Check the functions defined below for more information. + This module provides some utility functions to obtain timecodes, keyframes, and other data. """ @@ -40,6 +43,28 @@ aegi_vsplugins: str = "" plugin_extension = ".dll" if os.name == "nt" else ".so" +def progress_set_message(message: str): + """ + Sets the message of Aegisub's progress dialog. + """ + vs.core.log_message(vs.MESSAGE_TYPE_DEBUG, f"__aegi_set_message,{message}") + + +def progress_set_progress(percent: float): + """ + Sets the progress shown in Aegisub's progress dialog to + the given percentage. + """ + vs.core.log_message(vs.MESSAGE_TYPE_DEBUG, f"__aegi_set_progress,{percent}") + + +def progress_set_indeterminate(): + """ + Sets Aegisub's progress dialog to show indeterminate progress. + """ + vs.core.log_message(vs.MESSAGE_TYPE_DEBUG, f"__aegi_set_indeterminate,") + + def set_paths(vars: dict): """ Initialize the wrapper library with the given configuration directories. @@ -157,6 +182,9 @@ def wrap_lwlibavsource(filename: str, cachedir: str | None = None, **kwargs: Any pass cachefile = os.path.join(cachedir, make_lwi_cache_filename(filename)) + progress_set_message("Loading video file") + progress_set_indeterminate() + ensure_plugin("lsmas", "libvslsmashsource", "To use Aegisub's LWLibavSource wrapper, the `lsmas` plugin for VapourSynth must be installed") if b"-Dcachedir" not in core.lsmas.Version()["config"]: # type: ignore @@ -164,6 +192,7 @@ def wrap_lwlibavsource(filename: str, cachedir: str | None = None, **kwargs: Any clip = core.lsmas.LWLibavSource(source=filename, cachefile=cachefile, **kwargs) + progress_set_message("Getting timecodes and keyframes from the index file") return clip, info_from_lwindex(cachefile) @@ -181,6 +210,9 @@ def make_keyframes(clip: vs.VideoNode, use_scxvid: bool = False, The remaining keyword arguments are passed on to the respective filter. """ + progress_set_message("Generating keyframes") + progress_set_progress(1) + clip = core.resize.Bilinear(clip, width=resize_h * clip.width // clip.height, height=resize_h, format=resize_format) if use_scxvid: @@ -196,12 +228,12 @@ def make_keyframes(clip: vs.VideoNode, use_scxvid: bool = False, 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)) + if done % (clip.num_frames // 200) == 0: + progress_set_progress(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") + progress_set_progress(100) return [n for n in range(clip.num_frames) if keyframes[n]] @@ -224,8 +256,12 @@ class GenKeyframesMode(Enum): def ask_gen_keyframes(_: str) -> bool: from tkinter.messagebox import askyesno - return askyesno("Generate Keyframes", \ + progress_set_message("Asking whether to generate keyframes") + progress_set_indeterminate() + result = askyesno("Generate Keyframes", \ "No keyframes file was found for this video file.\nShould Aegisub detect keyframes from the video?\nThis will take a while.", default="no") + progress_set_message("") + return result def get_keyframes(filename: str, clip: vs.VideoNode, fallback: str | List[int], @@ -244,6 +280,9 @@ def get_keyframes(filename: str, clip: vs.VideoNode, fallback: str | List[int], generated or not Additional keyword arguments are passed on to make_keyframes. """ + progress_set_message("Looking for keyframes") + progress_set_indeterminate() + kffilename = make_keyframes_filename(filename) if not os.path.exists(kffilename): @@ -252,7 +291,6 @@ def get_keyframes(filename: str, clip: vs.VideoNode, fallback: str | List[int], if generate == GenKeyframesMode.ASK and not ask_callback(filename): return fallback - vs.core.log_message(vs.MESSAGE_TYPE_INFORMATION, "No keyframes file found, detecting keyframes...\n") keyframes = make_keyframes(clip, **kwargs) save_keyframes(kffilename, keyframes) @@ -266,6 +304,8 @@ def check_audio(filename: str, **kwargs: Any) -> bool: won't crash if it's not installed. Additional keyword arguments are passed on to BestAudioSource. """ + progress_set_message("Checking if the file has an audio track") + progress_set_indeterminate() try: ensure_plugin("bas", "BestAudioSource", "") vs.core.bas.Source(source=filename, **kwargs) diff --git a/src/vapoursynth_common.cpp b/src/vapoursynth_common.cpp index e5346f6bc..24df7a571 100644 --- a/src/vapoursynth_common.cpp +++ b/src/vapoursynth_common.cpp @@ -21,8 +21,10 @@ #include "options.h" #include "utils.h" #include +#include #include #include +#include #include @@ -67,6 +69,30 @@ int OpenScriptOrVideo(const VSAPI *api, const VSSCRIPTAPI *sapi, VSScript *scrip } void VSLogToProgressSink(int msgType, const char *msg, void *userData) { + auto sink = reinterpret_cast(userData); + + std::string msgStr(msg); + int commaPos = msgStr.find(','); + if (commaPos) { + std::string command = msgStr.substr(0, commaPos); + std::string tail = msgStr.substr(commaPos + 1, msgStr.length()); + + // We don't allow setting the title since that should stay as "Executing VapourSynth Script". + if (command == "__aegi_set_message") { + sink->SetMessage(tail); + } else if (command == "__aegi_set_progress") { + double percent; + if (!agi::util::try_parse(tail, &percent)) { + msgType = 2; + msgStr = agi::format("Warning: Invalid argument to __aegi_set_progress: %s\n", tail); + } else { + sink->SetProgress(percent, 100); + } + } else if (command == "__aegi_set_indeterminate") { + sink->SetIndeterminate(); + } + } + int loglevel = 0; std::string loglevel_str = OPT_GET("Provider/Video/VapourSynth/Log Level")->GetString(); if (loglevel_str == "Quiet") @@ -85,7 +111,7 @@ void VSLogToProgressSink(int msgType, const char *msg, void *userData) { if (msgType < loglevel) return; - reinterpret_cast(userData)->Log(msg); + sink->Log(msgStr); } void VSCleanCache() { From 2dbee37ad82897e8932a543251a29c7fd80456c6 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Wed, 18 Oct 2023 14:33:26 +0200 Subject: [PATCH 4/5] vapoursynth: Copy aegisub_vs.py to build folder for testing --- automation/meson.build | 1 + automation/vapoursynth/meson.build | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 automation/vapoursynth/meson.build diff --git a/automation/meson.build b/automation/meson.build index dc3319658..2d4fa386e 100644 --- a/automation/meson.build +++ b/automation/meson.build @@ -1,4 +1,5 @@ subdir('include') +subdir('vapoursynth') automation_dir = dataroot / 'automation' diff --git a/automation/vapoursynth/meson.build b/automation/vapoursynth/meson.build new file mode 100644 index 000000000..a3af6a17b --- /dev/null +++ b/automation/vapoursynth/meson.build @@ -0,0 +1,8 @@ +# Copy files to build directory for testing purposes +vs_files = files( + 'aegisub_vs.py', +) + +foreach f: vs_files + configure_file(input: f, output: '@PLAINNAME@', copy: true) +endforeach From 47b10e5ffc20de287f7812c3aabbbfc6a8c5e424 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Wed, 18 Oct 2023 21:21:37 +0200 Subject: [PATCH 5/5] Fix progress dialog when switching from indeterminate to setting progress --- src/dialog_progress.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dialog_progress.cpp b/src/dialog_progress.cpp index bcf4de96d..4568307d1 100644 --- a/src/dialog_progress.cpp +++ b/src/dialog_progress.cpp @@ -246,6 +246,8 @@ void DialogProgress::OnCancel(wxCommandEvent &) { } void DialogProgress::SetProgress(int target) { + pulse_timer.Stop(); + if (target == progress_target) return; using namespace std::chrono;