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/aegisub_vs.py b/automation/vapoursynth/aegisub_vs.py index 412ac1e5d..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) @@ -172,7 +201,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. @@ -182,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: @@ -197,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]] @@ -225,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], @@ -245,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): @@ -253,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) @@ -267,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/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 diff --git a/src/audio_provider_vs.cpp b/src/audio_provider_vs.cpp index bdc6bc161..f789607b6 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/dialog_progress.cpp b/src/dialog_progress.cpp index f337aa790..5ed67f84f 100644 --- a/src/dialog_progress.cpp +++ b/src/dialog_progress.cpp @@ -256,6 +256,8 @@ void DialogProgress::OnCancel(wxCommandEvent &) { } void DialogProgress::SetProgress(int target) { + pulse_timer.Stop(); + if (target == progress_target) return; using namespace std::chrono; diff --git a/src/libresrc/default_config.json b/src/libresrc/default_config.json index 4c0710e21..240998fdb 100644 --- a/src/libresrc/default_config.json +++ b/src/libresrc/default_config.json @@ -373,6 +373,7 @@ } }, "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 3d172673a..dcecf7842 100644 --- a/src/libresrc/meson.build +++ b/src/libresrc/meson.build @@ -19,9 +19,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 665ef8065..01acf37da 100644 --- a/src/libresrc/osx/default_config.json +++ b/src/libresrc/osx/default_config.json @@ -373,6 +373,7 @@ } }, "VapourSynth" : { + "Autoload User Plugins": true, "Cache" : { "Files" : 500, "Size" : 1000 diff --git a/src/preferences.cpp b/src/preferences.cpp index b115a08dd..a9b44e4eb 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -502,7 +502,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/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() { diff --git a/src/video_provider_vs.cpp b/src/video_provider_vs.cpp index 57f9e2cb0..3533e221e 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"); }