vapoursynth: Respect YCbCr Matrix header

This probably leaks some memory in some of the error cases, but the next
commit will replace everything with proper RAII wrappers.
This commit is contained in:
arch1t3cht 2024-10-25 17:16:02 +02:00
parent be2e6cb2d8
commit 2b855a327d
1 changed files with 132 additions and 86 deletions

View File

@ -45,17 +45,19 @@ namespace {
class VapourSynthVideoProvider: public VideoProvider { class VapourSynthVideoProvider: public VideoProvider {
VapourSynthWrapper vs; VapourSynthWrapper vs;
VSScript *script = nullptr; VSScript *script = nullptr;
VSNode *node = nullptr; VSNode *source_node = nullptr;
VSNode *prepared_node = nullptr;
const VSVideoInfo *vi = nullptr; const VSVideoInfo *vi = nullptr;
double dar = 0; double dar = 0;
agi::vfr::Framerate fps; agi::vfr::Framerate fps;
std::vector<int> keyframes; std::vector<int> keyframes;
std::string colorspace; std::string colorspace;
std::string real_colorspace; int video_cs = -1; // Reported or guessed color matrix of first frame
int video_cr = -1; // Reported or guessed color range of first frame
bool has_audio = false; bool has_audio = false;
const VSFrame *GetVSFrame(int n); const VSFrame *GetVSFrame(VSNode *node, int n);
void SetResizeArg(VSMap *args, const VSMap *props, const char *arg_name, const char *prop_name, int64_t deflt, int64_t 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:
@ -64,7 +66,7 @@ public:
void GetFrame(int n, VideoFrame &frame) override; void GetFrame(int n, VideoFrame &frame) override;
void SetColorSpace(std::string const& matrix) override { } void SetColorSpace(std::string const& matrix) override;
int GetFrameCount() const override { return vi->numFrames; } int GetFrameCount() const override { return vi->numFrames; }
agi::vfr::Framerate GetFPS() const override { return fps; } agi::vfr::Framerate GetFPS() const override { return fps; }
@ -72,38 +74,20 @@ public:
int GetHeight() const override { return vi->height; } int GetHeight() const override { return vi->height; }
double GetDAR() const override { return dar; } double GetDAR() const override { return dar; }
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 colorspace; }
std::string GetRealColorSpace() const override { return colorspace == "Unknown" ? "None" : colorspace; } std::string GetRealColorSpace() const override {
std::string result = ColorMatrix::colormatrix_description(video_cs, video_cr);
if (result == "") {
return "None";
}
return result;
}
bool HasAudio() const override { return has_audio; } 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"; }
}; };
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 "Unknown"; // Will return "None" in GetColorSpace
}
}
VapourSynthVideoProvider::VapourSynthVideoProvider(agi::fs::path const& filename, std::string const& colormatrix, agi::BackgroundRunner *br) try { 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());
@ -137,14 +121,14 @@ VapourSynthVideoProvider::VapourSynthVideoProvider(agi::fs::path const& filename
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));
throw VapourSynthError(msg); throw VapourSynthError(msg);
} }
node = vs.GetScriptAPI()->getOutputNode(script, 0); source_node = vs.GetScriptAPI()->getOutputNode(script, 0);
if (node == nullptr) if (source_node == nullptr)
throw VapourSynthError("No output node set"); throw VapourSynthError("No output node set");
if (vs.GetAPI()->getNodeType(node) != mtVideo) { if (vs.GetAPI()->getNodeType(source_node) != mtVideo) {
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(source_node);
if (vi == nullptr) if (vi == nullptr)
throw VapourSynthError("Couldn't get video info"); throw VapourSynthError("Couldn't get video info");
if (!vsh::isConstantVideoFormat(vi)) if (!vsh::isConstantVideoFormat(vi))
@ -228,7 +212,7 @@ VapourSynthVideoProvider::VapourSynthVideoProvider(agi::fs::path const& filename
vs.GetAPI()->freeMap(clipinfo); vs.GetAPI()->freeMap(clipinfo);
// Find the first frame Of the video to get some info // Find the first frame Of the video to get some info
const VSFrame *frame = GetVSFrame(0); const VSFrame *frame = GetVSFrame(source_node, 0);
const VSMap *props = vs.GetAPI()->getFramePropertiesRO(frame); const VSMap *props = vs.GetAPI()->getFramePropertiesRO(frame);
if (props == nullptr) if (props == nullptr)
@ -239,57 +223,26 @@ VapourSynthVideoProvider::VapourSynthVideoProvider(agi::fs::path const& filename
dar = double(vi->width * sarn) / (vi->height * sard); dar = double(vi->width * sarn) / (vi->height * sard);
} }
int64_t range = vs.GetAPI()->mapGetInt(props, "_ColorRange", 0, &err1); int video_cr_vs = vs.GetAPI()->mapGetInt(props, "_ColorRange", 0, &err1);
int64_t matrix = vs.GetAPI()->mapGetInt(props, "_Matrix", 0, &err2); switch (video_cr_vs) {
colorspace = colormatrix_description(vi->format.colorFamily, err1 == 0 ? range : -1, err2 == 0 ? matrix : -1); case VSC_RANGE_FULL:
video_cr = AGI_CR_JPEG;
case VSC_RANGE_LIMITED:
video_cr = AGI_CR_MPEG;
default:
video_cr = AGI_CR_UNSPECIFIED;
}
video_cs = vs.GetAPI()->mapGetInt(props, "_Matrix", 0, &err2);
ColorMatrix::guess_colorspace(video_cs, video_cr, vi->width, vi->height);
vs.GetAPI()->freeFrame(frame); vs.GetAPI()->freeFrame(frame);
if (vi->format.colorFamily != cfRGB || vi->format.bitsPerSample != 8) { SetColorSpace(colormatrix);
// 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);
// Set defaults for the colorspace parameters.
// If the video node has frame props (like if the video is tagged with
// some color space), these will override these arguments.
vs.GetAPI()->mapSetInt(args, "matrix_in", VSC_MATRIX_BT709, maAppend);
vs.GetAPI()->mapSetInt(args, "range_in", 0, maAppend);
vs.GetAPI()->mapSetInt(args, "chromaloc_in", VSC_CHROMA_LEFT, maAppend);
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 = 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. } 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) if (prepared_node != nullptr)
vs.GetAPI()->freeNode(node); vs.GetAPI()->freeNode(prepared_node);
if (source_node != nullptr)
vs.GetAPI()->freeNode(source_node);
if (script != nullptr) if (script != nullptr)
vs.GetScriptAPI()->freeScript(script); vs.GetScriptAPI()->freeScript(script);
throw err; throw err;
@ -299,7 +252,97 @@ 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()));
} }
const VSFrame *VapourSynthVideoProvider::GetVSFrame(int n) { void VapourSynthVideoProvider::SetColorSpace(std::string const& matrix) {
if (vi->format.colorFamily != cfRGB || vi->format.bitsPerSample != 8) {
if (matrix == colorspace && prepared_node != nullptr) {
return;
}
if (prepared_node != nullptr) {
vs.GetAPI()->freeNode(prepared_node);
}
VSNode *intermediary = nullptr;
auto [force_cs, force_cr] = ColorMatrix::parse_colormatrix(matrix);
if (force_cs != AGI_CS_UNSPECIFIED && force_cr != AGI_CR_UNSPECIFIED) {
// Override the _Matrix and _Range frame props to force the color space
VSPlugin *std = vs.GetAPI()->getPluginByID(VSH_STD_PLUGIN_ID, vs.GetScriptAPI()->getCore(script));
if (std == nullptr)
throw VapourSynthError("Couldn't find std plugin");
VSMap *args = vs.GetAPI()->createMap();
if (args == nullptr)
throw VapourSynthError("Failed to create argument map");
vs.GetAPI()->mapSetNode(args, "clip", source_node, maAppend);
vs.GetAPI()->mapSetInt(args, "_Matrix", force_cs, maAppend);
vs.GetAPI()->mapSetInt(args, "_ColorRange", force_cr == AGI_CR_JPEG ? VSC_RANGE_FULL : VSC_RANGE_LIMITED, maAppend);
VSMap *result = vs.GetAPI()->invoke(std, "SetFrameProps", args);
vs.GetAPI()->freeMap(args);
const char *error = vs.GetAPI()->mapGetError(result);
if (error) {
vs.GetAPI()->freeMap(result);
throw VideoOpenError(agi::format("Failed set color space frame props: %s", error));
}
int err;
intermediary = vs.GetAPI()->mapGetNode(result, "clip", 0, &err);
vs.GetAPI()->freeMap(result);
if (err) {
throw VideoOpenError("Failed to get SetFrameProps output node");
}
}
// 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", intermediary == nullptr ? source_node : intermediary, maAppend);
vs.GetAPI()->mapSetInt(args, "format", pfRGB24, maAppend);
// Set defaults for the colorspace parameters.
// If the video node has frame props (like if the video is tagged with
// some color space), these will override these arguments.
vs.GetAPI()->mapSetInt(args, "matrix_in", video_cs, maAppend);
vs.GetAPI()->mapSetInt(args, "range_in", video_cr == AGI_CR_JPEG, maAppend);
vs.GetAPI()->mapSetInt(args, "chromaloc_in", VSC_CHROMA_LEFT, maAppend);
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.GetScriptAPI()->freeScript(script);
throw VideoOpenError(agi::format("Failed to convert to RGB24: %s", error));
}
int err;
prepared_node = vs.GetAPI()->mapGetNode(result, "clip", 0, &err);
vs.GetAPI()->freeMap(result);
if (err) {
vs.GetScriptAPI()->freeScript(script);
throw VideoOpenError("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 = GetVSFrame(prepared_node, 0);
vs.GetAPI()->freeFrame(rgbframe);
if (intermediary != nullptr) {
vs.GetAPI()->freeNode(intermediary);
}
} else {
vs.GetAPI()->freeNode(prepared_node);
prepared_node = vs.GetAPI()->addNodeRef(source_node);
}
colorspace = matrix;
}
const VSFrame *VapourSynthVideoProvider::GetVSFrame(VSNode *node, int n) {
char errorMsg[1024]; char errorMsg[1024];
const VSFrame *frame = vs.GetAPI()->getFrame(n, node, errorMsg, sizeof(errorMsg)); const VSFrame *frame = vs.GetAPI()->getFrame(n, node, errorMsg, sizeof(errorMsg));
if (frame == nullptr) { if (frame == nullptr) {
@ -311,7 +354,7 @@ const VSFrame *VapourSynthVideoProvider::GetVSFrame(int n) {
void VapourSynthVideoProvider::GetFrame(int n, VideoFrame &out) { void VapourSynthVideoProvider::GetFrame(int n, VideoFrame &out) {
std::lock_guard<std::mutex> lock(vs.GetMutex()); std::lock_guard<std::mutex> lock(vs.GetMutex());
const VSFrame *frame = GetVSFrame(n); const VSFrame *frame = GetVSFrame(prepared_node, n);
const VSVideoFormat *format = vs.GetAPI()->getVideoFrameFormat(frame); const VSVideoFormat *format = vs.GetAPI()->getVideoFrameFormat(frame);
if (format->colorFamily != cfRGB || format->numPlanes != 3 || format->bitsPerSample != 8 || format->subSamplingH != 0 || format->subSamplingW != 0) { if (format->colorFamily != cfRGB || format->numPlanes != 3 || format->bitsPerSample != 8 || format->subSamplingH != 0 || format->subSamplingW != 0) {
@ -348,8 +391,11 @@ void VapourSynthVideoProvider::GetFrame(int n, VideoFrame &out) {
} }
VapourSynthVideoProvider::~VapourSynthVideoProvider() { VapourSynthVideoProvider::~VapourSynthVideoProvider() {
if (node != nullptr) { if (prepared_node != nullptr) {
vs.GetAPI()->freeNode(node); vs.GetAPI()->freeNode(prepared_node);
}
if (source_node != nullptr) {
vs.GetAPI()->freeNode(source_node);
} }
if (script != nullptr) { if (script != nullptr) {
vs.GetScriptAPI()->freeScript(script); vs.GetScriptAPI()->freeScript(script);