diff --git a/.gitignore b/.gitignore index 76f92ff5c..f0468aa12 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ subprojects/zlib-* subprojects/dirent-* subprojects/hunspell-* subprojects/uchardet-* +subprojects/vapoursynth diff --git a/meson.build b/meson.build index 21293e1b9..ae78bda05 100644 --- a/meson.build +++ b/meson.build @@ -242,6 +242,13 @@ if host_machine.system() == 'windows' and get_option('avisynth').enabled() conf.set('WITH_AVISYNTH', 1) # bundled separately with installer endif +if get_option('vapoursynth').enabled() + conf.set('WITH_VAPOURSYNTH', 1) + vs_sub = subproject('vapoursynth') + deps_inc += vs_sub.get_variable('vs_inc') + dep_avail += 'VapourSynth' +endif + if host_machine.system() == 'windows' and not get_option('directsound').disabled() dsound_dep = cc.find_library('dsound', required: get_option('directsound')) winmm_dep = cc.find_library('winmm', required: get_option('directsound')) diff --git a/meson_options.txt b/meson_options.txt index 19c447af1..8be9ca584 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -8,6 +8,7 @@ option('default_audio_output', type: 'combo', choices: ['auto', 'ALSA', 'OpenAL' option('ffms2', type: 'feature', description: 'FFMS2 video source') option('avisynth', type: 'feature', description: 'AviSynth video source') option('bestsource', type: 'feature', description: 'BestSource video source') +option('vapoursynth', type: 'feature', description: 'VapourSynth video source') option('fftw3', type: 'feature', description: 'FFTW3 support') option('hunspell', type: 'feature', description: 'Hunspell spell checker') diff --git a/packages/win_installer/fragment_associations.iss b/packages/win_installer/fragment_associations.iss index c766011da..ec33599b9 100644 --- a/packages/win_installer/fragment_associations.iss +++ b/packages/win_installer/fragment_associations.iss @@ -29,6 +29,7 @@ Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".wav"; ValueData: ""; Flags: uninsdeletekey Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".ogg"; ValueData: ""; Flags: uninsdeletekey Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".avs"; ValueData: ""; Flags: uninsdeletekey +Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".vpy"; ValueData: ""; Flags: uninsdeletekey Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".opus"; ValueData: ""; Flags: uninsdeletekey Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".h264"; ValueData: ""; Flags: uninsdeletekey Root: HKLM; Subkey: "SOFTWARE\Classes\Applications\aegisub.exe\SupportedTypes"; ValueType: string; ValueName: ".hevc"; ValueData: ""; Flags: uninsdeletekey @@ -165,6 +166,7 @@ Root: HKLM; Subkey: "SOFTWARE\Classes\.m4a\OpenWithProgids"; ValueType: string; Root: HKLM; Subkey: "SOFTWARE\Classes\.wav\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Audio.1"; Flags: uninsdeletevalue Root: HKLM; Subkey: "SOFTWARE\Classes\.ogg\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Media.1"; Flags: uninsdeletevalue Root: HKLM; Subkey: "SOFTWARE\Classes\.avs\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Video.1"; Flags: uninsdeletevalue +Root: HKLM; Subkey: "SOFTWARE\Classes\.vpy\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Media.1"; Flags: uninsdeletevalue Root: HKLM; Subkey: "SOFTWARE\Classes\.opus\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Audio.1"; Flags: uninsdeletevalue Root: HKLM; Subkey: "SOFTWARE\Classes\.h264\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Video.1"; Flags: uninsdeletevalue Root: HKLM; Subkey: "SOFTWARE\Classes\.hevc\OpenWithProgids"; ValueType: string; ValueName: "Aegisub.Video.1"; Flags: uninsdeletevalue diff --git a/src/command/video.cpp b/src/command/video.cpp index d3ffaf7fe..589bae337 100644 --- a/src/command/video.cpp +++ b/src/command/video.cpp @@ -564,7 +564,7 @@ struct video_open final : public Command { STR_HELP("Open a video file") void operator()(agi::Context *c) override { - auto str = from_wx(_("Video Formats") + " (*.asf,*.avi,*.avs,*.d2v,*.h264,*.hevc,*.m2ts,*.m4v,*.mkv,*.mov,*.mp4,*.mpeg,*.mpg,*.ogm,*.webm,*.wmv,*.ts,*.y4m,*.yuv)|*.asf;*.avi;*.avs;*.d2v;*.h264;*.hevc;*.m2ts;*.m4v;*.mkv;*.mov;*.mp4;*.mpeg;*.mpg;*.ogm;*.webm;*.wmv;*.ts;*.y4m;*.yuv|" + auto str = from_wx(_("Video Formats") + " (*.asf,*.avi,*.avs,*.d2v,*.h264,*.hevc,*.m2ts,*.m4v,*.mkv,*.mov,*.mp4,*.mpeg,*.mpg,*.ogm,*.webm,*.wmv,*.ts,*.vpy,*.y4m,*.yuv)|*.asf;*.avi;*.avs;*.d2v;*.h264;*.hevc;*.m2ts;*.m4v;*.mkv;*.mov;*.mp4;*.mpeg;*.mpg;*.ogm;*.webm;*.wmv;*.ts;*.vpy;*.y4m;*.yuv|" + _("All Files") + " (*.*)|*.*"); auto filename = OpenFileSelector(_("Open video file"), "Path/Last/Video", "", "", str, c->parent); if (!filename.empty()) diff --git a/src/meson.build b/src/meson.build index 51a5753cd..a9e2ebcf2 100644 --- a/src/meson.build +++ b/src/meson.build @@ -239,6 +239,8 @@ opt_src = [ ['BestSource', ['audio_provider_bestsource.cpp', 'video_provider_bestsource.cpp', 'bestsource_common.cpp']], + ['VapourSynth', ['vapoursynth_wrap.cpp', + 'video_provider_vs.cpp']], ['Hunspell', 'spellchecker_hunspell.cpp'], ] diff --git a/src/vapoursynth_wrap.cpp b/src/vapoursynth_wrap.cpp new file mode 100644 index 000000000..d0d0fbd8b --- /dev/null +++ b/src/vapoursynth_wrap.cpp @@ -0,0 +1,105 @@ +// Copyright (c) 2022, arch1t3cht +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +/// @file vapoursynth_wrap.cpp +/// @brief Wrapper-layer for Vapoursynth +/// @ingroup video_input audio_input +/// + +#ifdef WITH_VAPOURSYNTH +#include "vapoursynth_wrap.h" + +#include "VSScript4.h" + +#include "options.h" + +#include + +#ifndef _WIN32 +#include +#endif + +#ifdef _WIN32 +#define VSSCRIPT_SO "vsscript.dll" +#else +#define VSSCRIPT_SO "libvapoursynth-script.so" +#endif + +// Allocate storage for and initialise static members +namespace { + bool vs_loaded = false; +#ifdef _WIN32 + HINSTANCE hLib = nullptr; +#else + void* hLib = nullptr; +#endif + const VSAPI *api = nullptr; + VSSCRIPTAPI *scriptapi = nullptr; + std::mutex VapourSynthMutex; +} + +typedef VSSCRIPTAPI* VS_CC FUNC(int); + +VapourSynthWrapper::VapourSynthWrapper() { + // VSScript assumes it's only loaded once, so unlike AVS we can't unload it when the refcount reaches zero + if (!vs_loaded) { + vs_loaded = true; +#ifdef _WIN32 +#define CONCATENATE(x, y) x ## y +#define _Lstr(x) CONCATENATE(L, x) + hLib = LoadLibraryW(_Lstr(VSSCRIPT_SO)); +#undef _Lstr +#undef CONCATENATE +#else + hLib = dlopen(VSSCRIPT_SO, RTLD_LAZY | RTLD_GLOBAL | RTLD_DEEPBIND); +#endif + + if (!hLib) + throw VapoursynthError("Could not load " VSSCRIPT_SO); + +#ifdef _WIN32 + FUNC* getVSScriptAPI = (FUNC*)GetProcAddress(hLib, "getVSScriptAPI"); +#else + FUNC* getVSScriptAPI = (FUNC*)dlsym(hLib, "getVSScriptAPI"); +#endif + if (!getVSScriptAPI) + throw VapoursynthError("Failed to get address of getVSScriptAPI from " VSSCRIPT_SO); + + scriptapi = getVSScriptAPI(VSSCRIPT_API_VERSION); + + if (!scriptapi) + throw VapoursynthError("Failed to get Vapoursynth ScriptAPI"); + + api = scriptapi->getVSAPI(VAPOURSYNTH_API_VERSION); + + if (!api) + throw VapoursynthError("Failed to get Vapoursynth API"); + } +} + +std::mutex& VapourSynthWrapper::GetMutex() const { + return VapourSynthMutex; +} + +const VSAPI *VapourSynthWrapper::GetAPI() const { + return api; +} + +const VSSCRIPTAPI *VapourSynthWrapper::GetScriptAPI() const { + return scriptapi; +} + +#endif diff --git a/src/vapoursynth_wrap.h b/src/vapoursynth_wrap.h new file mode 100644 index 000000000..802cb3d58 --- /dev/null +++ b/src/vapoursynth_wrap.h @@ -0,0 +1,42 @@ +// Copyright (c) 2022, arch1t3cht +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +/// @file vapoursynth_wrap.h +/// @see vapoursynth_wrap.cpp +/// @ingroup video_input audio_input +/// + +#ifdef WITH_VAPOURSYNTH + +#include + +DEFINE_EXCEPTION(VapoursynthError, agi::Exception); + +struct VSAPI; +struct VSSCRIPTAPI; +namespace std { class mutex; } + +class VapourSynthWrapper { + VapourSynthWrapper(VapourSynthWrapper const&); +public: + std::mutex& GetMutex() const; + const VSAPI *GetAPI() const; + const VSSCRIPTAPI *GetScriptAPI() const; + + VapourSynthWrapper(); +}; + +#endif diff --git a/src/video_provider_manager.cpp b/src/video_provider_manager.cpp index 8ec7c18fc..c239163b8 100644 --- a/src/video_provider_manager.cpp +++ b/src/video_provider_manager.cpp @@ -30,6 +30,7 @@ std::unique_ptr CreateYUV4MPEGVideoProvider(agi::fs::path const&, std::unique_ptr CreateFFmpegSourceVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); std::unique_ptr CreateAvisynthVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); std::unique_ptr CreateBSVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); +std::unique_ptr CreateVapoursynthVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); std::unique_ptr CreateCacheVideoProvider(std::unique_ptr); @@ -51,6 +52,9 @@ namespace { #endif #ifdef WITH_BESTSOURCE {"BestSource", CreateBSVideoProvider, false}, +#endif +#ifdef WITH_VAPOURSYNTH + {"Vapoursynth", CreateVapoursynthVideoProvider, false}, #endif }; } diff --git a/src/video_provider_vs.cpp b/src/video_provider_vs.cpp new file mode 100644 index 000000000..3f9839e38 --- /dev/null +++ b/src/video_provider_vs.cpp @@ -0,0 +1,282 @@ +// Copyright (c) 2022, arch1t3cht +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +#ifdef WITH_VAPOURSYNTH +#include "include/aegisub/video_provider.h" + +#include "options.h" +#include "video_frame.h" + +#include +#include +#include +#include +#include + +#include + +#include "vapoursynth_wrap.h" +#include "VSScript4.h" +#include "VSHelper4.h" +#include "VSConstants4.h" + +namespace { +class VapoursynthVideoProvider: public VideoProvider { + VapourSynthWrapper vs; + VSScript *script = nullptr; + VSNode *node = nullptr; + const VSVideoInfo *vi = nullptr; + + double dar = 0; + agi::vfr::Framerate fps; + std::vector keyframes; + std::string colorspace; + std::string real_colorspace; + + 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); + +public: + VapoursynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix); + ~VapoursynthVideoProvider(); + + void GetFrame(int n, VideoFrame &frame) override; + + void SetColorSpace(std::string const& matrix) override { } + + int GetFrameCount() const override { return vi->numFrames; } + agi::vfr::Framerate GetFPS() const override { return fps; } + int GetWidth() const override { return vi->width; } + int GetHeight() const override { return vi->height; } + double GetDAR() const override { return dar; } + std::vector GetKeyFrames() const override { return keyframes; } + std::string GetColorSpace() const override { return colorspace; } + std::string GetRealColorSpace() const override { return colorspace; } + bool HasAudio() const override { return false; } + virtual bool WantsCaching() const override { return true; } + virtual std::string GetDecoderName() const override { return "VapourSynth"; } +}; + +std::string colormatrix_description(int colorFamily, int colorRange, int matrix) { + if (colorFamily != cfYUV) { + return "None"; + } + // Assuming TV for unspecified + std::string str = colorRange == VSC_RANGE_FULL ? "PC" : "TV"; + + switch (matrix) { + case VSC_MATRIX_RGB: + return "None"; + case VSC_MATRIX_BT709: + return str + ".709"; + case VSC_MATRIX_FCC: + return str + ".FCC"; + case VSC_MATRIX_BT470_BG: + case VSC_MATRIX_ST170_M: + return str + ".601"; + case VSC_MATRIX_ST240_M: + return str + ".240M"; + default: + return "None"; + } +} + +// 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) { + int err; + int result = vs.GetAPI()->mapGetInt(props, prop_name, 0, &err); + if (err != 0 || result == unspecified) { + result = deflt; + vs.GetAPI()->mapSetInt(args, arg_name, result, maAppend); + } +} + +VapoursynthVideoProvider::VapoursynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix) try { + agi::acs::CheckFileRead(filename); + std::lock_guard lock(vs.GetMutex()); + + script = vs.GetScriptAPI()->createScript(nullptr); + if (script == nullptr) { + throw VapoursynthError("Error creating script API"); + } + vs.GetScriptAPI()->evalSetWorkingDir(script, 1); + if (vs.GetScriptAPI()->evaluateFile(script, filename.string().c_str())) { + 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); + 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); + 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. + fps = (double) vi->fpsNum / vi->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); + } + + int range = vs.GetAPI()->mapGetInt(props, "_ColorRange", 0, &err1); + int 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); + + 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) { + throw VapoursynthError("Couldn't find resize plugin"); + } + VSMap *args = vs.GetAPI()->createMap(); + if (args == nullptr) { + throw VapoursynthError("Failed to create argument map"); + } + + vs.GetAPI()->mapSetNode(args, "clip", node, maAppend); + vs.GetAPI()->mapSetInt(args, "format", pfRGB24, maAppend); + if (vi->format.colorFamily != cfGray) + SetResizeArg(args, props, "matrix_in", "_Matrix", VSC_MATRIX_BT709, VSC_MATRIX_UNSPECIFIED); + SetResizeArg(args, props, "transfer_in", "_Transfer", VSC_TRANSFER_BT709, VSC_TRANSFER_UNSPECIFIED); + SetResizeArg(args, props, "primaries_in", "_Primaries", VSC_PRIMARIES_BT709, VSC_PRIMARIES_UNSPECIFIED); + SetResizeArg(args, props, "range_in", "_ColorRange", VSC_RANGE_LIMITED); + SetResizeArg(args, props, "chromaloc_in", "_ChromaLocation", VSC_CHROMA_LEFT); + + VSMap *result = vs.GetAPI()->invoke(resize, "Bicubic", args); + vs.GetAPI()->freeMap(args); + const char *error = vs.GetAPI()->mapGetError(result); + if (error) { + vs.GetAPI()->freeMap(result); + vs.GetAPI()->freeNode(node); + vs.GetScriptAPI()->freeScript(script); + throw VideoProviderError(agi::format("Failed to convert to RGB24: %s", error)); + } + int err; + vs.GetAPI()->freeNode(node); + node = vs.GetAPI()->mapGetNode(result, "clip", 0, &err); + vs.GetAPI()->freeMap(result); + if (err) { + vs.GetScriptAPI()->freeScript(script); + throw VideoProviderError("Failed to get resize output node"); + } + + // 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; + } + vs.GetAPI()->freeFrame(rgbframe); + } +} +catch (VapoursynthError const& err) { + throw VideoProviderError(agi::format("Vapoursynth error: %s", err.GetMessage())); +} + +const VSFrame *VapoursynthVideoProvider::GetVSFrame(int n) { + char errorMsg[1024]; + const VSFrame *frame = vs.GetAPI()->getFrame(n, node, errorMsg, sizeof(errorMsg)); + if (frame == nullptr) { + throw VapoursynthError(agi::format("Error getting frame: %s", errorMsg)); + } + return frame; +} + +void VapoursynthVideoProvider::GetFrame(int n, VideoFrame &out) { + std::lock_guard lock(vs.GetMutex()); + + const VSFrame *frame = GetVSFrame(n); + + const VSVideoFormat *format = vs.GetAPI()->getVideoFrameFormat(frame); + if (format->colorFamily != cfRGB || format->numPlanes != 3 || format->bitsPerSample != 8 || format->subSamplingH != 0 || format->subSamplingW != 0) { + throw VapoursynthError("Frame not in RGB24 format"); + } + + out.width = vs.GetAPI()->getFrameWidth(frame, 0); + out.height = vs.GetAPI()->getFrameHeight(frame, 0); + out.pitch = out.width * 4; + out.flipped = false; + + out.data.resize(out.pitch * out.height); + + for (int p = 0; p < format->numPlanes; p++) { + ptrdiff_t stride = vs.GetAPI()->getStride(frame, p); + const uint8_t *readPtr = vs.GetAPI()->getReadPtr(frame, p); + uint8_t *writePtr = &out.data[2 - p]; + int rows = vs.GetAPI()->getFrameHeight(frame, p); + int cols = vs.GetAPI()->getFrameWidth(frame, p); + + for (int row = 0; row < rows; row++) { + const uint8_t *rowPtr = readPtr; + uint8_t *rowWritePtr = writePtr; + for (int col = 0; col < cols; col++) { + *rowWritePtr = *rowPtr++; + rowWritePtr += 4; + } + readPtr += stride; + writePtr += out.pitch; + } + } + + vs.GetAPI()->freeFrame(frame); +} + +VapoursynthVideoProvider::~VapoursynthVideoProvider() { + if (node != nullptr) { + vs.GetAPI()->freeNode(node); + } + if (script != nullptr) { + vs.GetScriptAPI()->freeScript(script); + } +} +} + +namespace agi { class BackgroundRunner; } +std::unique_ptr CreateVapoursynthVideoProvider(agi::fs::path const& path, std::string const& colormatrix, agi::BackgroundRunner *) { + return agi::make_unique(path, colormatrix); +} +#endif // HAVE_VAPOURSYNTH diff --git a/subprojects/packagefiles/vapoursynth/meson.build b/subprojects/packagefiles/vapoursynth/meson.build new file mode 100644 index 000000000..5dfef44f7 --- /dev/null +++ b/subprojects/packagefiles/vapoursynth/meson.build @@ -0,0 +1,3 @@ +project('vapoursynth', 'cpp') + +vs_inc = include_directories('include') diff --git a/subprojects/vapoursynth.wrap b/subprojects/vapoursynth.wrap new file mode 100644 index 000000000..592446ccf --- /dev/null +++ b/subprojects/vapoursynth.wrap @@ -0,0 +1,4 @@ +[wrap-git] +url = https://github.com/Vapoursynth/vapoursynth.git +revision = R59 +patch_directory = vapoursynth