forked from mia/Aegisub
Merge branch 'vapoursynth' into feature
This commit is contained in:
commit
6f073c6a31
14 changed files with 406 additions and 63 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
|||
/tests/data
|
||||
|
||||
automation/vapoursynth/__pycache__
|
||||
packages/desktop/aegisub.desktop
|
||||
packages/desktop/aegisub.desktop.template
|
||||
src/libresrc/bitmap.cpp
|
||||
|
|
|
@ -41,3 +41,7 @@ install_data(
|
|||
'include/aegisub/unicode.moon',
|
||||
'include/aegisub/util.moon',
|
||||
install_dir: automation_dir / 'include' / 'aegisub')
|
||||
|
||||
install_data(
|
||||
'vapoursynth/aegisub_vs.py',
|
||||
install_dir: automation_dir / 'vapoursynth')
|
||||
|
|
208
automation/vapoursynth/aegisub_vs.py
Normal file
208
automation/vapoursynth/aegisub_vs.py
Normal 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
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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-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
|
||||
; DepCtrl
|
||||
DestDir: {userappdata}\Aegisub\automation\include\l0; Source: {#DEPS_DIR}\DependencyControl\modules\*; Flags: ignoreversion recursesubdirs createallsubdirs; Components: macros\modules\depctrl
|
||||
|
|
|
@ -165,7 +165,6 @@ VapoursynthAudioProvider::~VapoursynthAudioProvider() {
|
|||
}
|
||||
|
||||
std::unique_ptr<agi::AudioProvider> CreateVapoursynthAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *) {
|
||||
agi::acs::CheckFileRead(file);
|
||||
return agi::make_unique<VapoursynthAudioProvider>(file);
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -73,6 +73,7 @@ namespace {
|
|||
class DialogProgressSink final : public agi::ProgressSink {
|
||||
DialogProgress *dialog;
|
||||
std::atomic<bool> cancelled{false};
|
||||
std::atomic<bool> 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<void(agi::ProgressSink*)> 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()) {
|
||||
|
|
|
@ -351,7 +351,7 @@
|
|||
"Aegisub Cache" : true
|
||||
},
|
||||
"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" : {
|
||||
|
@ -391,7 +391,8 @@
|
|||
"Seek Preroll" : 12
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -351,7 +351,7 @@
|
|||
"Aegisub Cache" : true
|
||||
},
|
||||
"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" : {
|
||||
|
@ -391,7 +391,8 @@
|
|||
"Seek Preroll" : 12
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -490,6 +490,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."));
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#include "vapoursynth_wrap.h"
|
||||
#include "options.h"
|
||||
#include "utils.h"
|
||||
#include <libaegisub/background_runner.h>
|
||||
#include <libaegisub/fs.h>
|
||||
#include <libaegisub/path.h>
|
||||
|
||||
|
@ -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<agi::ProgressSink *>(userData)->Log(msg);
|
||||
}
|
||||
|
||||
void VSCleanCache() {
|
||||
CleanCache(config::path->Decode("?local/vscache/"),
|
||||
"",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -17,13 +17,17 @@
|
|||
#ifdef WITH_VAPOURSYNTH
|
||||
#include "include/aegisub/video_provider.h"
|
||||
|
||||
#include "compat.h"
|
||||
#include "options.h"
|
||||
#include "video_frame.h"
|
||||
|
||||
#include <libaegisub/access.h>
|
||||
#include <libaegisub/background_runner.h>
|
||||
#include <libaegisub/format.h>
|
||||
#include <libaegisub/path.h>
|
||||
#include <libaegisub/keyframe.h>
|
||||
#include <libaegisub/log.h>
|
||||
#include <libaegisub/make_unique.h>
|
||||
#include <libaegisub/path.h>
|
||||
|
||||
#include <mutex>
|
||||
|
||||
|
@ -33,6 +37,10 @@
|
|||
#include "VSHelper4.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 {
|
||||
class VapoursynthVideoProvider: public VideoProvider {
|
||||
VapourSynthWrapper vs;
|
||||
|
@ -45,12 +53,13 @@ class VapoursynthVideoProvider: public VideoProvider {
|
|||
std::vector<int> keyframes;
|
||||
std::string colorspace;
|
||||
std::string real_colorspace;
|
||||
bool has_audio = false;
|
||||
|
||||
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:
|
||||
VapoursynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix);
|
||||
VapoursynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix, agi::BackgroundRunner *br);
|
||||
~VapoursynthVideoProvider();
|
||||
|
||||
void GetFrame(int n, VideoFrame &frame) override;
|
||||
|
@ -65,7 +74,7 @@ public:
|
|||
std::vector<int> GetKeyFrames() const override { return keyframes; }
|
||||
std::string GetColorSpace() const override { return GetRealColorSpace(); }
|
||||
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; }
|
||||
std::string GetDecoderName() const override { return "VapourSynth"; }
|
||||
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
|
||||
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 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) {
|
||||
result = deflt;
|
||||
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());
|
||||
|
||||
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) {
|
||||
vs.GetAPI()->freeCore(core);
|
||||
throw VapoursynthError("Error creating script API");
|
||||
}
|
||||
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));
|
||||
vs.GetScriptAPI()->freeScript(script);
|
||||
throw VapoursynthError(msg);
|
||||
}
|
||||
node = vs.GetScriptAPI()->getOutputNode(script, 0);
|
||||
if (node == nullptr) {
|
||||
vs.GetScriptAPI()->freeScript(script);
|
||||
if (node == nullptr)
|
||||
throw VapoursynthError("No output node set");
|
||||
}
|
||||
|
||||
if (vs.GetAPI()->getNodeType(node) != mtVideo) {
|
||||
vs.GetAPI()->freeNode(node);
|
||||
vs.GetScriptAPI()->freeScript(script);
|
||||
throw VapoursynthError("Output node isn't a video node");
|
||||
}
|
||||
vi = vs.GetAPI()->getVideoInfo(node);
|
||||
if (!vsh::isConstantVideoFormat(vi)) {
|
||||
vs.GetAPI()->freeNode(node);
|
||||
vs.GetScriptAPI()->freeScript(script);
|
||||
if (vi == nullptr)
|
||||
throw VapoursynthError("Couldn't get video info");
|
||||
if (!vsh::isConstantVideoFormat(vi))
|
||||
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 fpsDen = vi->fpsDen;
|
||||
if (fpsDen == 0) {
|
||||
|
@ -151,25 +171,89 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename
|
|||
}
|
||||
fps = agi::vfr::Framerate(fpsNum, fpsDen);
|
||||
|
||||
// Find the first frame to get some info
|
||||
const VSFrame *frame;
|
||||
try {
|
||||
frame = GetVSFrame(0);
|
||||
} catch (VapoursynthError const& err) {
|
||||
vs.GetAPI()->freeNode(node);
|
||||
vs.GetScriptAPI()->freeScript(script);
|
||||
throw err;
|
||||
}
|
||||
int err1, err2;
|
||||
const VSMap *props = vs.GetAPI()->getFramePropertiesRO(frame);
|
||||
int sarn = vs.GetAPI()->mapGetInt(props, "_SARNum", 0, &err1);
|
||||
int sard = vs.GetAPI()->mapGetInt(props, "_SARDen", 0, &err2);
|
||||
if (!err1 && !err2) {
|
||||
dar = ((double) vi->width * sarn) / (vi->height * sard);
|
||||
// Get timecodes and/or keyframes if provided
|
||||
VSMap *clipinfo = vs.GetAPI()->createMap();
|
||||
if (clipinfo == nullptr)
|
||||
throw VapoursynthError("Couldn't create map");
|
||||
vs.GetScriptAPI()->getVariable(script, kf_key, clipinfo);
|
||||
vs.GetScriptAPI()->getVariable(script, tc_key, clipinfo);
|
||||
vs.GetScriptAPI()->getVariable(script, audio_key, clipinfo);
|
||||
|
||||
int numkf = vs.GetAPI()->mapNumElements(clipinfo, kf_key);
|
||||
int numtc = vs.GetAPI()->mapNumElements(clipinfo, tc_key);
|
||||
|
||||
int64_t audio = vs.GetAPI()->mapGetInt(clipinfo, audio_key, 0, &err1);
|
||||
if (!err1)
|
||||
has_audio = bool(audio);
|
||||
|
||||
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);
|
||||
int matrix = vs.GetAPI()->mapGetInt(props, "_Matrix", 0, &err2);
|
||||
if (numtc != -1) {
|
||||
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);
|
||||
|
||||
vs.GetAPI()->freeFrame(frame);
|
||||
|
@ -177,13 +261,12 @@ VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename
|
|||
if (vi->format.colorFamily != cfRGB || vi->format.bitsPerSample != 8) {
|
||||
// Convert to RGB24 format
|
||||
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");
|
||||
}
|
||||
|
||||
VSMap *args = vs.GetAPI()->createMap();
|
||||
if (args == nullptr) {
|
||||
if (args == nullptr)
|
||||
throw VapoursynthError("Failed to create argument map");
|
||||
}
|
||||
|
||||
vs.GetAPI()->mapSetNode(args, "clip", node, 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
|
||||
const VSFrame *rgbframe;
|
||||
try {
|
||||
rgbframe = GetVSFrame(0);
|
||||
} catch (VapoursynthError const& err) {
|
||||
vs.GetAPI()->freeNode(node);
|
||||
vs.GetScriptAPI()->freeScript(script);
|
||||
throw err;
|
||||
}
|
||||
const VSFrame *rgbframe = GetVSFrame(0);
|
||||
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()));
|
||||
}
|
||||
|
||||
|
@ -287,8 +370,7 @@ VapoursynthVideoProvider::~VapoursynthVideoProvider() {
|
|||
}
|
||||
|
||||
namespace agi { class BackgroundRunner; }
|
||||
std::unique_ptr<VideoProvider> CreateVapoursynthVideoProvider(agi::fs::path const& path, std::string const& colormatrix, agi::BackgroundRunner *) {
|
||||
agi::acs::CheckFileRead(path);
|
||||
return agi::make_unique<VapoursynthVideoProvider>(path, colormatrix);
|
||||
std::unique_ptr<VideoProvider> CreateVapoursynthVideoProvider(agi::fs::path const& path, std::string const& colormatrix, agi::BackgroundRunner *br) {
|
||||
return agi::make_unique<VapoursynthVideoProvider>(path, colormatrix, br);
|
||||
}
|
||||
#endif // WITH_VAPOURSYNTH
|
||||
|
|
Loading…
Reference in a new issue