From 256ddab369a7c25963da60cf28ef9c6934a8185a Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Thu, 16 Mar 2023 02:01:55 +0100 Subject: [PATCH] vapoursynth: Ship plugins on windows and add ensure_plugin function --- automation/vapoursynth/aegisub_vs.py | 58 ++++++++++++++++--- packages/win_installer/fragment_codecs.iss | 5 ++ .../win_installer/fragment_mainprogram.iss | 1 + .../portable/create-portable.ps1 | 5 ++ src/libresrc/default_config.json | 4 +- src/libresrc/osx/default_config.json | 4 +- src/vapoursynth_common.cpp | 5 ++ tools/win-installer-setup.ps1 | 45 ++++++++++++++ 8 files changed, 115 insertions(+), 12 deletions(-) diff --git a/automation/vapoursynth/aegisub_vs.py b/automation/vapoursynth/aegisub_vs.py index c3cc5876c..166802adf 100644 --- a/automation/vapoursynth/aegisub_vs.py +++ b/automation/vapoursynth/aegisub_vs.py @@ -34,6 +34,43 @@ from typing import Any, Dict, List, Tuple import vapoursynth as vs core = vs.core +aegi_vscache: str = "" +aegi_vsplugins: str = "" + +plugin_extension = ".dll" if os.name == "nt" else ".so" + +def set_paths(vars: dict): + """ + Initialize the wrapper library with the given configuration directories. + Should usually be called at the start of the default script as + set_paths(globals()) + """ + global aegi_vscache + global aegi_vsplugins + aegi_vscache = vars["__aegi_vscache"] + aegi_vsplugins = vars["__aegi_vsplugins"] + + +def ensure_plugin(name: str, loadname: str, errormsg: str): + """ + Ensures that the Vapoursynth plugin with the given name exists. + If it doesn't, it tries to load it from `loadname`. + If that fails, it raises an error with the given error message. + """ + if hasattr(core, name): + return + + if aegi_vsplugins and loadname: + try: + core.std.LoadPlugin(os.path.join(aegi_vsplugins, loadname + plugin_extension)) + if hasattr(core, name): + return + except vs.Error: + pass + + raise vs.Error(errormsg) + + def make_lwi_cache_filename(filename: str) -> str: """ Given a path to a video, will return a file name like the one LWLibavSource @@ -103,21 +140,23 @@ def info_from_lwindex(indexfile: str) -> Dict[str, List[int]]: } -def wrap_lwlibavsource(filename: str, cachedir: str, **kwargs: Any) -> Tuple[vs.VideoNode, Dict[str, List[int]]]: +def wrap_lwlibavsource(filename: str, cachedir: str | None = None, **kwargs: Any) -> Tuple[vs.VideoNode, Dict[str, List[int]]]: """ Given a path to a video file and a directory to store index files in (usually __aegi_vscache), will open the video with LWLibavSource and read the generated .lwi file to obtain the timecodes and keyframes. Additional keyword arguments are passed on to LWLibavSource. """ + if cachedir is None: + cachedir = aegi_vscache + try: os.mkdir(cachedir) except FileExistsError: pass cachefile = os.path.join(cachedir, make_lwi_cache_filename(filename)) - if not hasattr(core, "lsmas"): - raise vs.Error("To use Aegisub's LWLibavSource wrapper, the `lsmas` plugin for VapourSynth must be installed") + 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 raise vs.Error("To use Aegisub's LWLibavSource wrapper, the `lsmas` plugin must support the `cachedir` option for LWLibavSource.") @@ -143,11 +182,13 @@ def make_keyframes(clip: vs.VideoNode, use_scxvid: bool = False, """ clip = core.resize.Bilinear(clip, width=resize_h * clip.width // clip.height, height=resize_h, format=resize_format); - try: - clip = core.scxvid.Scxvid(clip, **kwargs) if use_scxvid else core.wwxd.WWXD(clip, **kwargs) - except AttributeError: - raise vs.Error("To use the keyframe generation, the `{}` plugin for VapourSynth must be installed" - .format("scxvid" if use_scxvid else "wwxd")) + + if use_scxvid: + ensure_plugin("scxvid", "libscxvid", "To use the keyframe generation, the scxvid plugin for VapourSynth must be installed") + clip = core.scxvid.Scxvid(clip, **kwargs) + else: + ensure_plugin("wwxd", "libwwxd64", "To use the keyframe generation, the wwxdplugin for VapourSynth must be installed") + clip = core.wwxd.WWXD(clip, **kwargs) keyframes = {} done = 0 @@ -199,6 +240,7 @@ def check_audio(filename: str, **kwargs: Any) -> bool: Additional keyword arguments are passed on to BestAudioSource. """ try: + ensure_plugin("bas", "BestAudioSource", "") vs.core.bas.Source(source=filename, **kwargs) return True except AttributeError: diff --git a/packages/win_installer/fragment_codecs.iss b/packages/win_installer/fragment_codecs.iss index 06e73b037..f71f74b31 100644 --- a/packages/win_installer/fragment_codecs.iss +++ b/packages/win_installer/fragment_codecs.iss @@ -5,3 +5,8 @@ DestDir: {app}; Source: {#DEPS_DIR}\AvisynthPlus64\x64\AviSynth.dll; Flags: igno DestDir: {app}; Source: {#DEPS_DIR}\AvisynthPlus64\x64\plugins\DirectShowSource.dll; Flags: ignoreversion; Components: main ; VSFilter DestDir: {app}\csri; Source: {#DEPS_DIR}\VSFilter\x64\VSFilter.dll; Flags: ignoreversion; Components: main +; VapourSynth +DestDir: {app}\vapoursynth; Source: {#DEPS_DIR}\L-SMASH-Works\libvslsmashsource.dll; Flags: ignoreversion; Components: vapoursynth +DestDir: {app}\vapoursynth; Source: {#DEPS_DIR}\bestaudiosource\win64\BestAudioSource.dll; Flags: ignoreversion; Components: vapoursynth +DestDir: {app}\vapoursynth; Source: {#DEPS_DIR}\SCXVid\libscxvid.dll; Flags: ignoreversion; Components: vapoursynth +DestDir: {app}\vapoursynth; Source: {#DEPS_DIR}\WWXD\libwwxd64.dll; Flags: ignoreversion; Components: vapoursynth diff --git a/packages/win_installer/fragment_mainprogram.iss b/packages/win_installer/fragment_mainprogram.iss index 7feeaf740..4337261b7 100644 --- a/packages/win_installer/fragment_mainprogram.iss +++ b/packages/win_installer/fragment_mainprogram.iss @@ -1,5 +1,6 @@ [Components] Name: "main"; Description: "Main Files"; Types: full compact custom; Flags: fixed +Name: "vapoursynth"; Description: "Bundled VapourSynth Plugins"; Types: full Name: "macros"; Description: "Automation Scripts"; Types: full Name: "macros\bundled"; Description: "Bundled macros"; Types: full Name: "macros\demos"; Description: "Example macros/Demos"; Types: full diff --git a/packages/win_installer/portable/create-portable.ps1 b/packages/win_installer/portable/create-portable.ps1 index 0b121254f..e14c6fd73 100644 --- a/packages/win_installer/portable/create-portable.ps1 +++ b/packages/win_installer/portable/create-portable.ps1 @@ -56,6 +56,11 @@ Write-Output 'Copying - codecs\Avisynth' Copy-New-Item $InstallerDepsDir\AvisynthPlus64\x86-64\DevIL.dll $PortableOutputDir Copy-New-Item $InstallerDepsDir\AvisynthPlus64\x86-64\AviSynth.dll $PortableOutputDir Copy-New-Item $InstallerDepsDir\AvisynthPlus64\x86-64\plugins\DirectShowSource.dll $PortableOutputDir +Write-Output 'Copying - codecs\VapourSynth' +Copy-New-Item $InstallerDepsDir\L-SMASH-Works\libvslsmashsource.dll $PortableOutputDir\vapoursynth +Copy-New-Item $InstallerDepsDir\bestaudiosource\win64\BestAudioSource.dll $PortableOutputDir\vapoursynth +Copy-New-Item $InstallerDepsDir\SCXVid\libscxvid.dll $PortableOutputDir\vapoursynth +Copy-New-Item $InstallerDepsDir\WWXD\libwwxd64.dll $PortableOutputDir\vapoursynth Write-Output 'Copying - codecs\VSFilter' Copy-New-Item $InstallerDepsDir\VSFilter\x64\VSFilter.dll $PortableOutputDir\csri Write-Output 'Copying - runtimes\MS-CRT' diff --git a/src/libresrc/default_config.json b/src/libresrc/default_config.json index 4710552f9..1fab96699 100644 --- a/src/libresrc/default_config.json +++ b/src/libresrc/default_config.json @@ -338,7 +338,7 @@ "Aegisub Cache" : true }, "VapourSynth" : { - "Default Script" : "# This default script will load an audio file using BestAudioSource.\n# It requires the `bas` plugin.\n\nimport vapoursynth as vs\ntry:\n vs.core.bas.Source(source=filename).set_output()\nexcept AttributeError:\n raise vs.Error(\"To use Aegisub's default audio loader, the `bas` plugin for VapourSynth must be installed\")" + "Default Script" : "# This default script will load an audio file using BestAudioSource.\n# It requires the `bas` plugin.\n\nimport vapoursynth as vs\nimport aegisub_vs as a\na.set_paths(locals())\n\na.ensure_plugin(\"bas\", \"BestAudioSource\", \"To use Aegisub's default audio loader, the `bas` plugin for VapourSynth must be installed\")\nvs.core.bas.Source(source=filename).set_output()" } }, "Avisynth" : { @@ -374,7 +374,7 @@ }, "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" + "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 vapoursynth as vs\nimport time\nimport aegisub_vs as a\na.set_paths(locals())\n\nclip, videoinfo = a.wrap_lwlibavsource(filename)\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 0bca6144b..40db6b02e 100644 --- a/src/libresrc/osx/default_config.json +++ b/src/libresrc/osx/default_config.json @@ -338,7 +338,7 @@ "Aegisub Cache" : true }, "VapourSynth" : { - "Default Script" : "# This default script will load an audio file using BestAudioSource.\n# It requires the `bas` plugin.\n\nimport vapoursynth as vs\ntry:\n vs.core.bas.Source(source=filename).set_output()\nexcept AttributeError:\n raise vs.Error(\"To use Aegisub's default audio loader, the `bas` plugin for VapourSynth must be installed\")" + "Default Script" : "# This default script will load an audio file using BestAudioSource.\n# It requires the `bas` plugin.\n\nimport vapoursynth as vs\nimport aegisub_vs as a\na.set_paths(globals())\n\na.ensure_plugin(\"bas\", \"BestAudioSource\", \"To use Aegisub's default audio loader, the `bas` plugin for VapourSynth must be installed\")\nvs.core.bas.Source(source=filename).set_output()" } }, "Avisynth" : { @@ -374,7 +374,7 @@ }, "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" + "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 vapoursynth as vs\nimport time\nimport aegisub_vs as a\na.set_paths(locals())\n\nclip, videoinfo = a.wrap_lwlibavsource(filename)\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/vapoursynth_common.cpp b/src/vapoursynth_common.cpp index b360c6144..88a8446d2 100644 --- a/src/vapoursynth_common.cpp +++ b/src/vapoursynth_common.cpp @@ -42,6 +42,11 @@ int OpenScriptOrVideo(const VSAPI *api, const VSSCRIPTAPI *sapi, VSScript *scrip SetStringVar(api, map, "filename", filename.string()); SetStringVar(api, map, "__aegi_vscache", config::path->Decode("?local/vscache").string()); +#ifdef WIN32 + SetStringVar(api, map, "__aegi_vsplugins", config::path->Decode("?data/vapoursynth").string()); +#else + SetStringVar(api, map, "__aegi_vsplugins", ""); +#endif for (std::string dir : { "data", "dictionary", "local", "script", "temp", "user", }) // Don't include ?audio and ?video in here since these only hold the paths to the previous audio/video files. SetStringVar(api, map, "__aegi_" + dir, config::path->Decode("?" + dir).string()); diff --git a/tools/win-installer-setup.ps1 b/tools/win-installer-setup.ps1 index 312bf292c..20684511e 100644 --- a/tools/win-installer-setup.ps1 +++ b/tools/win-installer-setup.ps1 @@ -58,6 +58,51 @@ if (!(Test-Path VSFilter)) { Set-Location $DepsDir } +### Vapoursynth plugins + +# L-SMASH-Works +if (!(Test-Path L-SMASH-Works)) { + New-Item -ItemType Directory L-SMASH-Works + $lsmasReleases = Invoke-WebRequest "https://api.github.com/repos/AkarinVS/L-SMASH-Works/releases/latest" -Headers $GitHeaders -UseBasicParsing | ConvertFrom-Json + $lsmasUrl = "https://github.com/AkarinVS/L-SMASH-Works/releases/download/" + $lsmasReleases.tag_name + "/release-x86_64-cachedir-cwd.zip" + Invoke-WebRequest $lsmasUrl -OutFile release-x86_64-cachedir-cwd.zip -UseBasicParsing + Expand-Archive -LiteralPath release-x86_64-cachedir-cwd.zip -DestinationPath L-SMASH-Works + Remove-Item release-x86_64-cachedir-cwd.zip +} + +# bestaudiosource +if (!(Test-Path bestaudiosource)) { + $basDir = New-Item -ItemType Directory bestaudiosource + Set-Location $basDir + $basReleases = Invoke-WebRequest "https://api.github.com/repos/vapoursynth/bestaudiosource/releases/latest" -Headers $GitHeaders -UseBasicParsing | ConvertFrom-Json + $basUrl = $basReleases.assets[0].browser_download_url + Invoke-WebRequest $basUrl -OutFile bas-r1.7z -UseBasicParsing + 7z x bas-r1.7z + Remove-Item bas-r1.7z + Set-Location $DepsDir +} + +# SCXVid +if (!(Test-Path SCXVid)) { + $scxDir = New-Item -ItemType Directory SCXVid + Set-Location $scxDir + $scxReleases = Invoke-WebRequest "https://api.github.com/repos/dubhater/vapoursynth-scxvid/releases/latest" -Headers $GitHeaders -UseBasicParsing | ConvertFrom-Json + $scxUrl = "https://github.com/dubhater/vapoursynth-scxvid/releases/download/" + $scxReleases.tag_name + "/vapoursynth-scxvid-v1-win64.7z" + Invoke-WebRequest $scxUrl -OutFile vapoursynth-scxvid-v1-win64.7z -UseBasicParsing + 7z x vapoursynth-scxvid-v1-win64.7z + Remove-Item vapoursynth-scxvid-v1-win64.7z + Set-Location $DepsDir +} + +# WWXD +if (!(Test-Path WWXD)) { + New-Item -ItemType Directory WWXD + $wwxdReleases = Invoke-WebRequest "https://api.github.com/repos/dubhater/vapoursynth-wwxd/releases/latest" -Headers $GitHeaders -UseBasicParsing | ConvertFrom-Json + $wwxdUrl = "https://github.com/dubhater/vapoursynth-wwxd/releases/download/" + $wwxdReleases.tag_name + "/libwwxd64.dll" + Invoke-WebRequest $wwxdUrl -OutFile WWXD/libwwxd64.dll -UseBasicParsing +} + + # ffi-experiments if (!(Test-Path ffi-experiments)) { Get-Command "moonc" # check to ensure Moonscript is present