Merge branch 'vapoursynth' into feature

This commit is contained in:
arch1t3cht 2023-02-24 01:42:58 +01:00
commit 6f073c6a31
14 changed files with 406 additions and 63 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
/tests/data /tests/data
automation/vapoursynth/__pycache__
packages/desktop/aegisub.desktop packages/desktop/aegisub.desktop
packages/desktop/aegisub.desktop.template packages/desktop/aegisub.desktop.template
src/libresrc/bitmap.cpp src/libresrc/bitmap.cpp

View file

@ -41,3 +41,7 @@ install_data(
'include/aegisub/unicode.moon', 'include/aegisub/unicode.moon',
'include/aegisub/util.moon', 'include/aegisub/util.moon',
install_dir: automation_dir / 'include' / 'aegisub') install_dir: automation_dir / 'include' / 'aegisub')
install_data(
'vapoursynth/aegisub_vs.py',
install_dir: automation_dir / 'vapoursynth')

View file

@ -0,0 +1,208 @@
"""
Utility functions for loading video files into Aegisub using the Vapoursynth
video provider.
When encountering a file whose file extension is not .py or .vpy, the
Vapoursynth audio and video providers will execute the respective default
script set in Aegisub's configuration, with the following string variables set:
- filename: The path to the file that's being opened.
- __aegi_data, __aegi_dictionary, __aegi_local, __aegi_script, __aegi_temp, __aegi_user:
The values of ?data, ?dictionary, etc. respectively.
- __aegi_vscache: The path to a directory where the Vapoursynth script can
store cache files. This directory is cleaned by Aegisub when it gets too
large (as defined by Aegisub's configuration).
The provider reads the video from the script's 0-th output node. By default,
the video is assumed to be CFR. The script can pass further information to
Aegisub using the following variables:
- __aegi_timecodes: List[int] | str: The timecodes for the video, or the
path to a timecodes file.
- __aegi_keyframes: List[int] | str: List of frame numbers to load as
keyframes, or the path to a keyframes file.
- __aegi_hasaudio: int: If nonzero, Aegisub will try to load an audio track
from the same file.
This module provides some utility functions to obtain timecodes, keyframes, and
other data.
"""
import os
import os.path
import re
from collections import deque
from typing import Any, Dict, List, Tuple
import vapoursynth as vs
core = vs.core
def make_lwi_cache_filename(filename: str) -> str:
"""
Given a path to a video, will return a file name like the one LWLibavSource
would use for a .lwi file.
"""
max_len = 254
extension = ".lwi"
if len(filename) + len(extension) > max_len:
filename = filename[-(max_len + len(extension)):]
return "".join(("_" if c in "/\\:" else c) for c in filename) + extension
def make_keyframes_filename(filename: str) -> str:
"""
Given a path `path/to/file.mkv`, will return the path
`path/to/file_keyframes.txt`.
"""
extlen = filename[::-1].find(".") + 1
return filename[:len(filename) - extlen] + "_keyframes.txt"
lwindex_re1 = re.compile(r"Index=(?P<Index>-?[0-9]+),POS=(?P<POS>-?[0-9]+),PTS=(?P<PTS>-?[0-9]+),DTS=(?P<DTS>-?[0-9]+),EDI=(?P<EDI>-?[0-9]+)")
lwindex_re2 = re.compile(r"Key=(?P<Key>-?[0-9]+),Pic=(?P<Pic>-?[0-9]+),POC=(?P<POC>-?[0-9]+),Repeat=(?P<Repeat>-?[0-9]+),Field=(?P<Field>-?[0-9]+)")
streaminfo_re = re.compile(r"Codec=(?P<Codec>[0-9]+),TimeBase=(?P<TimeBase>[0-9\/]+),Width=(?P<Width>[0-9]+),Height=(?P<Height>[0-9]+),Format=(?P<Format>[0-9a-zA-Z]+),ColorSpace=(?P<ColorSpace>[0-9]+)")
class LWIndexFrame:
pts: int
key: int
def __init__(self, raw: list[str]):
match1 = lwindex_re1.match(raw[0])
match2 = lwindex_re2.match(raw[1])
if not match1 or not match2:
raise ValueError("Invalid lwindex format")
self.pts = int(match1.group("PTS"))
self.key = int(match2.group("Key"))
def __int__(self) -> int:
return self.pts
def info_from_lwindex(indexfile: str) -> Dict[str, List[int]]:
"""
Given a path to an .lwi file, will return a dictionary containing
information about the video, with the keys
- timcodes: The timecodes.
- keyframes: Array of frame numbers of keyframes.
"""
with open(indexfile, encoding="latin1") as f:
index = f.read().splitlines()
indexstart, indexend = index.index("</StreamInfo>") + 1, index.index("</LibavReaderIndex>")
frames = [LWIndexFrame(index[i:i+2]) for i in range(indexstart, indexend, 2)]
frames.sort(key=int)
streaminfo = streaminfo_re.match(index[indexstart - 2])
if not streaminfo:
raise ValueError("Invalid lwindex format")
timebase_num, timebase_den = [int(i) for i in streaminfo.group("TimeBase").split("/")]
return {
"timecodes": [(f.pts * 1000 * timebase_num) // timebase_den for f in frames],
"keyframes": [i for i, f in enumerate(frames) if f.key],
}
def wrap_lwlibavsource(filename: str, cachedir: str, **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.
"""
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")
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.")
clip = core.lsmas.LWLibavSource(source=filename, cachefile=cachefile, **kwargs)
return clip, info_from_lwindex(cachefile)
def make_keyframes(clip: vs.VideoNode, use_scxvid: bool = False,
resize_h: int = 360, resize_format: int = vs.YUV420P8,
**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.
:param resize_h: Height to resize the clip to before processing.
:param resize_format: Format to convert the clip to before processing.
The remaining keyword arguments are passed on to the respective filter.
"""
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"))
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]]
def save_keyframes(filename: str, keyframes: List[int]):
"""
Saves a list of keyframes in Aegisub's keyframe format v1 to a file with
the given filename.
"""
with open(filename, "w") as f:
f.write("# keyframe format v1\n")
f.write("fps 0\n")
f.write("".join(f"{n}\n" for n in keyframes))
def get_keyframes(filename: str, clip: vs.VideoNode, **kwargs: Any) -> str:
"""
When not already present, creates a keyframe file for the given clip next
to the given filename using WWXD or Scxvid (see the make_keyframes docstring).
Additional keyword arguments are passed on to make_keyframes.
"""
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)
return kffilename
def check_audio(filename: str, **kwargs: Any) -> bool:
"""
Checks whether the given file has an audio track by trying to open it with
BestAudioSource. Requires the `bas` plugin to return correct results, but
won't crash if it's not installed.
Additional keyword arguments are passed on to BestAudioSource.
"""
try:
vs.core.bas.Source(source=filename, **kwargs)
return True
except AttributeError:
pass
except vs.Error:
pass
return False

View file

@ -46,10 +46,15 @@ namespace agi {
/// @brief Log a message /// @brief Log a message
/// ///
/// If any messages are logged then the dialog will not automatically close /// If any messages are logged and StayOpen is set (set by default)
/// when the task finishes so that the user has the chance to read them. /// 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; 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? /// Has the user asked the task to cancel?
virtual bool IsCancelled()=0; virtual bool IsCancelled()=0;
}; };

View file

@ -30,6 +30,8 @@ DestDir: {app}\automation\include; Source: {#SOURCE_ROOT}\automation\include\uni
DestDir: {app}\automation\include; Source: {#SOURCE_ROOT}\automation\include\utils.lua; Flags: ignoreversion overwritereadonly uninsremovereadonly; Attribs: readonly; Components: main DestDir: {app}\automation\include; Source: {#SOURCE_ROOT}\automation\include\utils.lua; Flags: ignoreversion overwritereadonly uninsremovereadonly; Attribs: readonly; Components: main
DestDir: {app}\automation\include; Source: {#SOURCE_ROOT}\automation\include\utils-auto4.lua; Flags: ignoreversion overwritereadonly uninsremovereadonly; Attribs: readonly; Components: main DestDir: {app}\automation\include; Source: {#SOURCE_ROOT}\automation\include\utils-auto4.lua; Flags: ignoreversion overwritereadonly uninsremovereadonly; Attribs: readonly; Components: main
DestDir: {app}\automation\vapoursynth; Source: {#SOURCE_ROOT}\automation\vapoursynth\aegisub_vs.py; Flags: ignoreversion overwritereadonly uninsremovereadonly; Attribs: readonly; Components: main
#ifdef DEPCTRL #ifdef DEPCTRL
; DepCtrl ; DepCtrl
DestDir: {userappdata}\Aegisub\automation\include\l0; Source: {#DEPS_DIR}\DependencyControl\modules\*; Flags: ignoreversion recursesubdirs createallsubdirs; Components: macros\modules\depctrl DestDir: {userappdata}\Aegisub\automation\include\l0; Source: {#DEPS_DIR}\DependencyControl\modules\*; Flags: ignoreversion recursesubdirs createallsubdirs; Components: macros\modules\depctrl

View file

@ -165,7 +165,6 @@ VapoursynthAudioProvider::~VapoursynthAudioProvider() {
} }
std::unique_ptr<agi::AudioProvider> CreateVapoursynthAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *) { std::unique_ptr<agi::AudioProvider> CreateVapoursynthAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *) {
agi::acs::CheckFileRead(file);
return agi::make_unique<VapoursynthAudioProvider>(file); return agi::make_unique<VapoursynthAudioProvider>(file);
} }
#endif #endif

View file

@ -127,6 +127,7 @@ namespace Automation4 {
void SetMessage(std::string const& msg) override { impl->SetMessage(msg); } void SetMessage(std::string const& msg) override { impl->SetMessage(msg); }
void SetProgress(int64_t cur, int64_t max) override { impl->SetProgress(cur, max); } void SetProgress(int64_t cur, int64_t max) override { impl->SetProgress(cur, max); }
void Log(std::string const& str) override { impl->Log(str); } void Log(std::string const& str) override { impl->Log(str); }
void SetStayOpen(bool stayopen) override { impl->SetStayOpen(stayopen); }
bool IsCancelled() override { return impl->IsCancelled(); } bool IsCancelled() override { return impl->IsCancelled(); }
/// Show the passed dialog on the GUI thread, blocking the calling /// Show the passed dialog on the GUI thread, blocking the calling

View file

@ -73,6 +73,7 @@ namespace {
class DialogProgressSink final : public agi::ProgressSink { class DialogProgressSink final : public agi::ProgressSink {
DialogProgress *dialog; DialogProgress *dialog;
std::atomic<bool> cancelled{false}; std::atomic<bool> cancelled{false};
std::atomic<bool> stayopen{true};
int progress = 0; int progress = 0;
public: public:
@ -98,6 +99,14 @@ public:
Main().Async([=]{ dialog->pending_log += to_wx(str); }); Main().Async([=]{ dialog->pending_log += to_wx(str); });
} }
void SetStayOpen(bool b) override {
stayopen = b;
}
bool GetStayOpen() {
return stayopen;
}
bool IsCancelled() override { bool IsCancelled() override {
return cancelled; return cancelled;
} }
@ -169,7 +178,7 @@ void DialogProgress::Run(std::function<void(agi::ProgressSink*)> task) {
// so the user can read the debug output and switch the cancel button to a // so the user can read the debug output and switch the cancel button to a
// close button // close button
bool cancelled = this->ps->IsCancelled(); bool cancelled = this->ps->IsCancelled();
if (cancelled || (log_output->IsEmpty() && !pending_log)) if (cancelled || !this->ps->GetStayOpen() || (log_output->IsEmpty() && !pending_log))
EndModal(!cancelled); EndModal(!cancelled);
else { else {
if (!pending_log.empty()) { if (!pending_log.empty()) {

View file

@ -351,7 +351,7 @@
"Aegisub Cache" : true "Aegisub Cache" : true
}, },
"VapourSynth" : { "VapourSynth" : {
"Default Script" : "import vapoursynth as vs\nvs.core.bas.Source(source=filename).set_output()" "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\")"
} }
}, },
"Avisynth" : { "Avisynth" : {
@ -391,7 +391,8 @@
"Seek Preroll" : 12 "Seek Preroll" : 12
}, },
"VapourSynth" : { "VapourSynth" : {
"Default Script" : "import vapoursynth as vs\nvs.core.lsmas.LWLibavSource(source=filename).set_output()" "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"
} }
} }
}, },

View file

@ -351,7 +351,7 @@
"Aegisub Cache" : true "Aegisub Cache" : true
}, },
"VapourSynth" : { "VapourSynth" : {
"Default Script" : "import vapoursynth as vs\nvs.core.bas.Source(source=filename).set_output()" "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\")"
} }
}, },
"Avisynth" : { "Avisynth" : {
@ -391,7 +391,8 @@
"Seek Preroll" : 12 "Seek Preroll" : 12
}, },
"VapourSynth" : { "VapourSynth" : {
"Default Script" : "import vapoursynth as vs\nvs.core.lsmas.LWLibavSource(source=filename).set_output()" "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"
} }
} }
}, },

View file

@ -490,6 +490,12 @@ void Advanced_Video(wxTreebook *book, Preferences *parent) {
void VapourSynth(wxTreebook *book, Preferences *parent) { void VapourSynth(wxTreebook *book, Preferences *parent) {
#ifdef WITH_VAPOURSYNTH #ifdef WITH_VAPOURSYNTH
auto p = new OptionPage(book, parent, _("VapourSynth"), OptionPage::PAGE_SUB); 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 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.")); 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."));

View file

@ -20,6 +20,7 @@
#include "vapoursynth_wrap.h" #include "vapoursynth_wrap.h"
#include "options.h" #include "options.h"
#include "utils.h" #include "utils.h"
#include <libaegisub/background_runner.h>
#include <libaegisub/fs.h> #include <libaegisub/fs.h>
#include <libaegisub/path.h> #include <libaegisub/path.h>
@ -60,6 +61,28 @@ int OpenScriptOrVideo(const VSAPI *api, const VSSCRIPTAPI *sapi, VSScript *scrip
return result; 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<agi::ProgressSink *>(userData)->Log(msg);
}
void VSCleanCache() { void VSCleanCache() {
CleanCache(config::path->Decode("?local/vscache/"), CleanCache(config::path->Decode("?local/vscache/"),
"", "",

View file

@ -21,5 +21,6 @@
int OpenScriptOrVideo(const VSAPI *api, const VSSCRIPTAPI *sapi, VSScript *script, agi::fs::path const& filename, std::string default_script); int OpenScriptOrVideo(const VSAPI *api, const VSSCRIPTAPI *sapi, VSScript *script, agi::fs::path const& filename, std::string default_script);
void VSCleanCache(); void VSCleanCache();
void VSLogToProgressSink(int msgType, const char *msg, void *userData);
#endif // WITH_VAPOURSYNTH #endif // WITH_VAPOURSYNTH

View file

@ -17,13 +17,17 @@
#ifdef WITH_VAPOURSYNTH #ifdef WITH_VAPOURSYNTH
#include "include/aegisub/video_provider.h" #include "include/aegisub/video_provider.h"
#include "compat.h"
#include "options.h" #include "options.h"
#include "video_frame.h" #include "video_frame.h"
#include <libaegisub/access.h> #include <libaegisub/access.h>
#include <libaegisub/background_runner.h>
#include <libaegisub/format.h> #include <libaegisub/format.h>
#include <libaegisub/path.h> #include <libaegisub/keyframe.h>
#include <libaegisub/log.h>
#include <libaegisub/make_unique.h> #include <libaegisub/make_unique.h>
#include <libaegisub/path.h>
#include <mutex> #include <mutex>
@ -33,6 +37,10 @@
#include "VSHelper4.h" #include "VSHelper4.h"
#include "VSConstants4.h" #include "VSConstants4.h"
static const char *kf_key = "__aegi_keyframes";
static const char *tc_key = "__aegi_timecodes";
static const char *audio_key = "__aegi_hasaudio";
namespace { namespace {
class VapoursynthVideoProvider: public VideoProvider { class VapoursynthVideoProvider: public VideoProvider {
VapourSynthWrapper vs; VapourSynthWrapper vs;
@ -45,12 +53,13 @@ class VapoursynthVideoProvider: public VideoProvider {
std::vector<int> keyframes; std::vector<int> keyframes;
std::string colorspace; std::string colorspace;
std::string real_colorspace; std::string real_colorspace;
bool has_audio = false;
const VSFrame *GetVSFrame(int n); const VSFrame *GetVSFrame(int n);
void SetResizeArg(VSMap *args, const VSMap *props, const char *arg_name, const char *prop_name, int deflt, int unspecified = -1); void SetResizeArg(VSMap *args, const VSMap *props, const char *arg_name, const char *prop_name, int64_t deflt, int64_t unspecified = -1);
public: public:
VapoursynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix); VapoursynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix, agi::BackgroundRunner *br);
~VapoursynthVideoProvider(); ~VapoursynthVideoProvider();
void GetFrame(int n, VideoFrame &frame) override; void GetFrame(int n, VideoFrame &frame) override;
@ -65,7 +74,7 @@ public:
std::vector<int> GetKeyFrames() const override { return keyframes; } std::vector<int> GetKeyFrames() const override { return keyframes; }
std::string GetColorSpace() const override { return GetRealColorSpace(); } std::string GetColorSpace() const override { return GetRealColorSpace(); }
std::string GetRealColorSpace() const override { return colorspace == "Unknown" ? "None" : colorspace; } std::string GetRealColorSpace() const override { return colorspace == "Unknown" ? "None" : colorspace; }
bool HasAudio() const override { return false; } bool HasAudio() const override { return has_audio; }
bool WantsCaching() const override { return true; } bool WantsCaching() const override { return true; }
std::string GetDecoderName() const override { return "VapourSynth"; } std::string GetDecoderName() const override { return "VapourSynth"; }
bool ShouldSetVideoProperties() const override { return colorspace != "Unknown"; } bool ShouldSetVideoProperties() const override { return colorspace != "Unknown"; }
@ -96,9 +105,9 @@ std::string colormatrix_description(int colorFamily, int colorRange, int matrix)
} }
// Adds an argument to the rescaler if the corresponding frameprop does not exist or is set as unspecified // Adds an argument to the rescaler if the corresponding frameprop does not exist or is set as unspecified
void VapoursynthVideoProvider::SetResizeArg(VSMap *args, const VSMap *props, const char *arg_name, const char *prop_name, int deflt, int unspecified) { void VapoursynthVideoProvider::SetResizeArg(VSMap *args, const VSMap *props, const char *arg_name, const char *prop_name, int64_t deflt, int64_t unspecified) {
int err; int err;
int result = vs.GetAPI()->mapGetInt(props, prop_name, 0, &err); int64_t result = vs.GetAPI()->mapGetInt(props, prop_name, 0, &err);
if (err != 0 || result == unspecified) { if (err != 0 || result == unspecified) {
result = deflt; result = deflt;
if (!strcmp(arg_name, "range_in")) { if (!strcmp(arg_name, "range_in")) {
@ -108,41 +117,52 @@ void VapoursynthVideoProvider::SetResizeArg(VSMap *args, const VSMap *props, con
} }
} }
VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix) try { VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix, agi::BackgroundRunner *br) try { try {
std::lock_guard<std::mutex> lock(vs.GetMutex()); std::lock_guard<std::mutex> lock(vs.GetMutex());
VSCleanCache(); VSCleanCache();
script = vs.GetScriptAPI()->createScript(nullptr); int err1, err2;
VSCore *core = vs.GetAPI()->createCore(0);
if (core == nullptr) {
throw VapoursynthError("Error creating core");
}
script = vs.GetScriptAPI()->createScript(core);
if (script == nullptr) { if (script == nullptr) {
vs.GetAPI()->freeCore(core);
throw VapoursynthError("Error creating script API"); throw VapoursynthError("Error creating script API");
} }
vs.GetScriptAPI()->evalSetWorkingDir(script, 1); vs.GetScriptAPI()->evalSetWorkingDir(script, 1);
if (OpenScriptOrVideo(vs.GetAPI(), vs.GetScriptAPI(), script, filename, OPT_GET("Provider/Video/VapourSynth/Default Script")->GetString())) { 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)); std::string msg = agi::format("Error executing VapourSynth script: %s", vs.GetScriptAPI()->getError(script));
vs.GetScriptAPI()->freeScript(script);
throw VapoursynthError(msg); throw VapoursynthError(msg);
} }
node = vs.GetScriptAPI()->getOutputNode(script, 0); node = vs.GetScriptAPI()->getOutputNode(script, 0);
if (node == nullptr) { if (node == nullptr)
vs.GetScriptAPI()->freeScript(script);
throw VapoursynthError("No output node set"); throw VapoursynthError("No output node set");
}
if (vs.GetAPI()->getNodeType(node) != mtVideo) { if (vs.GetAPI()->getNodeType(node) != mtVideo) {
vs.GetAPI()->freeNode(node);
vs.GetScriptAPI()->freeScript(script);
throw VapoursynthError("Output node isn't a video node"); throw VapoursynthError("Output node isn't a video node");
} }
vi = vs.GetAPI()->getVideoInfo(node); vi = vs.GetAPI()->getVideoInfo(node);
if (!vsh::isConstantVideoFormat(vi)) { if (vi == nullptr)
vs.GetAPI()->freeNode(node); throw VapoursynthError("Couldn't get video info");
vs.GetScriptAPI()->freeScript(script); if (!vsh::isConstantVideoFormat(vi))
throw VapoursynthError("Video doesn't have constant format"); throw VapoursynthError("Video doesn't have constant format");
}
// Assume constant frame rate, since handling VFR would require going through all frames when loading.
// Users can load custom timecodes files to deal with VFR.
// Alternatively (TODO) the provider could read timecodes and keyframes from a second output node.
int fpsNum = vi->fpsNum; int fpsNum = vi->fpsNum;
int fpsDen = vi->fpsDen; int fpsDen = vi->fpsDen;
if (fpsDen == 0) { if (fpsDen == 0) {
@ -151,25 +171,89 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename
} }
fps = agi::vfr::Framerate(fpsNum, fpsDen); fps = agi::vfr::Framerate(fpsNum, fpsDen);
// Find the first frame to get some info // Get timecodes and/or keyframes if provided
const VSFrame *frame; VSMap *clipinfo = vs.GetAPI()->createMap();
try { if (clipinfo == nullptr)
frame = GetVSFrame(0); throw VapoursynthError("Couldn't create map");
} catch (VapoursynthError const& err) { vs.GetScriptAPI()->getVariable(script, kf_key, clipinfo);
vs.GetAPI()->freeNode(node); vs.GetScriptAPI()->getVariable(script, tc_key, clipinfo);
vs.GetScriptAPI()->freeScript(script); vs.GetScriptAPI()->getVariable(script, audio_key, clipinfo);
throw err;
} int numkf = vs.GetAPI()->mapNumElements(clipinfo, kf_key);
int err1, err2; int numtc = vs.GetAPI()->mapNumElements(clipinfo, tc_key);
const VSMap *props = vs.GetAPI()->getFramePropertiesRO(frame);
int sarn = vs.GetAPI()->mapGetInt(props, "_SARNum", 0, &err1); int64_t audio = vs.GetAPI()->mapGetInt(clipinfo, audio_key, 0, &err1);
int sard = vs.GetAPI()->mapGetInt(props, "_SARDen", 0, &err2); if (!err1)
if (!err1 && !err2) { has_audio = bool(audio);
dar = ((double) vi->width * sarn) / (vi->height * sard);
if (numkf > 0) {
const int64_t *kfs = vs.GetAPI()->mapGetIntArray(clipinfo, kf_key, &err1);
const char *kfs_path = vs.GetAPI()->mapGetData(clipinfo, kf_key, 0, &err2);
if (err1 && err2)
throw VapoursynthError("Error getting keyframes from returned VSMap");
if (!err1) {
keyframes.reserve(numkf);
for (int i = 0; i < numkf; i++)
keyframes.push_back(int(kfs[i]));
} else {
int kfs_path_size = vs.GetAPI()->mapGetDataSize(clipinfo, kf_key, 0, &err1);
if (err1)
throw VapoursynthError("Error getting size of keyframes path");
try {
keyframes = agi::keyframe::Load(config::path->Decode(std::string(kfs_path, size_t(kfs_path_size))));
} catch (agi::Exception const& e) {
LOG_E("vapoursynth/video/keyframes") << "Failed to open keyframes file specified by script: " << e.GetMessage();
}
}
} }
int range = vs.GetAPI()->mapGetInt(props, "_ColorRange", 0, &err1); if (numtc != -1) {
int matrix = vs.GetAPI()->mapGetInt(props, "_Matrix", 0, &err2); const int64_t *tcs = vs.GetAPI()->mapGetIntArray(clipinfo, tc_key, &err1);
const char *tcs_path = vs.GetAPI()->mapGetData(clipinfo, tc_key, 0, &err2);
if (err1 && err2)
throw VapoursynthError("Error getting timecodes from returned map");
if (!err1) {
if (numtc != vi->numFrames)
throw VapoursynthError("Number of returned timecodes does not match number of frames");
std::vector<int> timecodes;
timecodes.reserve(numtc);
for (int i = 0; i < numtc; i++)
timecodes.push_back(int(tcs[i]));
fps = agi::vfr::Framerate(timecodes);
} else {
int tcs_path_size = vs.GetAPI()->mapGetDataSize(clipinfo, tc_key, 0, &err1);
if (err1)
throw VapoursynthError("Error getting size of keyframes path");
try {
fps = agi::vfr::Framerate(config::path->Decode(std::string(tcs_path, size_t(tcs_path_size))));
} catch (agi::Exception const& e) {
// Throw an error here unlike with keyframes since the timecodes not being loaded might not be immediately noticeable
throw VapoursynthError("Failed to open timecodes file specified by script: " + e.GetMessage());
}
}
}
vs.GetAPI()->freeMap(clipinfo);
// Find the first frame Of the video to get some info
const VSFrame *frame = GetVSFrame(0);
const VSMap *props = vs.GetAPI()->getFramePropertiesRO(frame);
if (props == nullptr)
throw VapoursynthError("Couldn't get frame properties");
int64_t sarn = vs.GetAPI()->mapGetInt(props, "_SARNum", 0, &err1);
int64_t sard = vs.GetAPI()->mapGetInt(props, "_SARDen", 0, &err2);
if (!err1 && !err2) {
dar = double(vi->width * sarn) / (vi->height * sard);
}
int64_t range = vs.GetAPI()->mapGetInt(props, "_ColorRange", 0, &err1);
int64_t matrix = vs.GetAPI()->mapGetInt(props, "_Matrix", 0, &err2);
colorspace = colormatrix_description(vi->format.colorFamily, err1 == 0 ? range : -1, err2 == 0 ? matrix : -1); colorspace = colormatrix_description(vi->format.colorFamily, err1 == 0 ? range : -1, err2 == 0 ? matrix : -1);
vs.GetAPI()->freeFrame(frame); vs.GetAPI()->freeFrame(frame);
@ -177,13 +261,12 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename
if (vi->format.colorFamily != cfRGB || vi->format.bitsPerSample != 8) { if (vi->format.colorFamily != cfRGB || vi->format.bitsPerSample != 8) {
// Convert to RGB24 format // Convert to RGB24 format
VSPlugin *resize = vs.GetAPI()->getPluginByID(VSH_RESIZE_PLUGIN_ID, vs.GetScriptAPI()->getCore(script)); VSPlugin *resize = vs.GetAPI()->getPluginByID(VSH_RESIZE_PLUGIN_ID, vs.GetScriptAPI()->getCore(script));
if (resize == nullptr) { if (resize == nullptr)
throw VapoursynthError("Couldn't find resize plugin"); throw VapoursynthError("Couldn't find resize plugin");
}
VSMap *args = vs.GetAPI()->createMap(); VSMap *args = vs.GetAPI()->createMap();
if (args == nullptr) { if (args == nullptr)
throw VapoursynthError("Failed to create argument map"); throw VapoursynthError("Failed to create argument map");
}
vs.GetAPI()->mapSetNode(args, "clip", node, maAppend); vs.GetAPI()->mapSetNode(args, "clip", node, maAppend);
vs.GetAPI()->mapSetInt(args, "format", pfRGB24, maAppend); vs.GetAPI()->mapSetInt(args, "format", pfRGB24, maAppend);
@ -213,18 +296,18 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename
} }
// Finally, try to get the first frame again, so if the filter does crash, it happens before loading finishes // Finally, try to get the first frame again, so if the filter does crash, it happens before loading finishes
const VSFrame *rgbframe; const VSFrame *rgbframe = GetVSFrame(0);
try {
rgbframe = GetVSFrame(0);
} catch (VapoursynthError const& err) {
vs.GetAPI()->freeNode(node);
vs.GetScriptAPI()->freeScript(script);
throw err;
}
vs.GetAPI()->freeFrame(rgbframe); vs.GetAPI()->freeFrame(rgbframe);
} }
} catch (VapoursynthError const& err) { // for try inside of function. We need both here since we need to catch errors from the VapoursynthWrap constructor.
if (node != nullptr)
vs.GetAPI()->freeNode(node);
if (script != nullptr)
vs.GetScriptAPI()->freeScript(script);
throw err;
} }
catch (VapoursynthError const& err) { }
catch (VapoursynthError const& err) { // for the entire constructor
throw VideoProviderError(agi::format("Vapoursynth error: %s", err.GetMessage())); throw VideoProviderError(agi::format("Vapoursynth error: %s", err.GetMessage()));
} }
@ -287,8 +370,7 @@ VapoursynthVideoProvider::~VapoursynthVideoProvider() {
} }
namespace agi { class BackgroundRunner; } namespace agi { class BackgroundRunner; }
std::unique_ptr<VideoProvider> CreateVapoursynthVideoProvider(agi::fs::path const& path, std::string const& colormatrix, agi::BackgroundRunner *) { std::unique_ptr<VideoProvider> CreateVapoursynthVideoProvider(agi::fs::path const& path, std::string const& colormatrix, agi::BackgroundRunner *br) {
agi::acs::CheckFileRead(path); return agi::make_unique<VapoursynthVideoProvider>(path, colormatrix, br);
return agi::make_unique<VapoursynthVideoProvider>(path, colormatrix);
} }
#endif // WITH_VAPOURSYNTH #endif // WITH_VAPOURSYNTH