Files
plezy/linux/runner/mpv/video_params_test.cc
T

280 lines
10 KiB
C++
Raw Normal View History

#include "video_params.h"
#include <iostream>
#include <vector>
namespace {
int failures = 0;
void Expect(bool condition, const char* expression, int line) {
if (condition) return;
std::cerr << "line " << line << ": check failed: " << expression << '\n';
++failures;
}
#define EXPECT(condition) Expect(static_cast<bool>(condition), #condition, __LINE__)
mpv_node Text(const char* value) {
mpv_node node{};
node.format = MPV_FORMAT_STRING;
node.u.string = const_cast<char*>(value);
return node;
}
mpv_node Number(double value) {
mpv_node node{};
node.format = MPV_FORMAT_DOUBLE;
node.u.double_ = value;
return node;
}
mpv_node Whole(int64_t value) {
mpv_node node{};
node.format = MPV_FORMAT_INT64;
node.u.int64 = value;
return node;
}
// Builds the node shape mpv delivers for an observed `video-params`: a map whose
// absent fields are missing keys rather than zeroed ones. Node() borrows the
// builder's storage, so nothing may be added after it is called.
class Params {
public:
Params& Add(const char* key, mpv_node value) {
keys_.push_back(const_cast<char*>(key));
values_.push_back(value);
return *this;
}
mpv_node Node() {
list_.num = static_cast<int>(values_.size());
list_.keys = keys_.empty() ? nullptr : keys_.data();
list_.values = values_.empty() ? nullptr : values_.data();
mpv_node node{};
node.format = MPV_FORMAT_NODE_MAP;
node.u.list = &list_;
return node;
}
private:
std::vector<char*> keys_;
std::vector<mpv_node> values_;
mpv_node_list list_{};
};
// A fully described HDR10 master: both names and all four luminances, mixed in
// among the fields the HDR decision has no use for, since mpv sends the whole
// map every time and the keys are in mpv's order, not ours.
void TestEveryFieldPresentIsRead() {
Params params;
params.Add("pixelformat", Text("yuv420p10"))
.Add("w", Whole(3840))
.Add("gamma", Text("pq"))
.Add("primaries", Text("bt.2020"))
.Add("min-luma", Number(0.0001))
.Add("max-luma", Number(1000.0))
.Add("max-cll", Number(999.0))
.Add("max-fall", Number(400.0))
.Add("chroma-location", Text("mpeg2/4/h264"));
const mpv_node node = params.Node();
const mpv::SourceHdrMetadata metadata = mpv::ParseSourceHdrMetadata(&node);
EXPECT(metadata.transfer == "pq");
EXPECT(metadata.primaries == "bt.2020");
EXPECT(metadata.max_cll == 999.0);
EXPECT(metadata.max_fall == 400.0);
EXPECT(metadata.max_luminance == 1000.0);
EXPECT(metadata.min_luminance == 0.0001);
}
// The common HDR10 case: a PQ / BT.2020 stream that states no static metadata at
// all. mpv omits the keys entirely, and every luminance has to stay at zero -
// which is how the rest of the pipeline spells "not stated" - while both names
// still come through, because they are what decides whether the plane may be
// described as HDR in the first place.
void TestNamesSurviveWithNoLuminances() {
Params params;
params.Add("gamma", Text("hlg")).Add("primaries", Text("bt.2020"));
const mpv_node node = params.Node();
const mpv::SourceHdrMetadata metadata = mpv::ParseSourceHdrMetadata(&node);
EXPECT(metadata.transfer == "hlg");
EXPECT(metadata.primaries == "bt.2020");
EXPECT(metadata.max_cll == 0.0);
EXPECT(metadata.max_fall == 0.0);
EXPECT(metadata.max_luminance == 0.0);
EXPECT(metadata.min_luminance == 0.0);
}
// Each luminance is independent: a source may state MaxCLL and nothing else, or
// a mastering range and no light levels. An absent field must never pick up its
// neighbour's value or a default.
void TestEachLuminanceIsAbsentOnItsOwn() {
{
Params params;
params.Add("max-cll", Number(1200.0));
const mpv_node node = params.Node();
const mpv::SourceHdrMetadata metadata = mpv::ParseSourceHdrMetadata(&node);
EXPECT(metadata.max_cll == 1200.0);
EXPECT(metadata.max_fall == 0.0);
EXPECT(metadata.max_luminance == 0.0);
EXPECT(metadata.min_luminance == 0.0);
}
{
Params params;
params.Add("max-fall", Number(250.0));
const mpv_node node = params.Node();
const mpv::SourceHdrMetadata metadata = mpv::ParseSourceHdrMetadata(&node);
EXPECT(metadata.max_cll == 0.0);
EXPECT(metadata.max_fall == 250.0);
EXPECT(metadata.max_luminance == 0.0);
}
{
Params params;
params.Add("min-luma", Number(0.005));
const mpv_node node = params.Node();
const mpv::SourceHdrMetadata metadata = mpv::ParseSourceHdrMetadata(&node);
EXPECT(metadata.max_luminance == 0.0);
EXPECT(metadata.min_luminance == 0.005);
}
{
Params params;
params.Add("max-luma", Number(4000.0));
const mpv_node node = params.Node();
const mpv::SourceHdrMetadata metadata = mpv::ParseSourceHdrMetadata(&node);
EXPECT(metadata.max_luminance == 4000.0);
EXPECT(metadata.min_luminance == 0.0);
}
}
// The mastering floor is the one luminance a source may legitimately state as
// zero, so a present zero has to be taken rather than dropped. The others treat
// zero as no statement, which is the same thing they do with an absent key -
// and since the field's absent value *is* zero, only a negative can tell the
// two rules apart from the outside.
void TestMinLumaAcceptsAZeroTheOthersRefuse() {
Params params;
params.Add("gamma", Text("pq"))
.Add("min-luma", Number(0.0))
.Add("max-luma", Number(0.0))
.Add("max-cll", Number(0.0))
.Add("max-fall", Number(0.0));
const mpv_node node = params.Node();
const mpv::SourceHdrMetadata metadata = mpv::ParseSourceHdrMetadata(&node);
EXPECT(metadata.min_luminance == 0.0);
EXPECT(metadata.max_luminance == 0.0);
EXPECT(metadata.max_cll == 0.0);
EXPECT(metadata.max_fall == 0.0);
// No luminance may be negative. The floor would scale into a mastering
// minimum the protocol rejects, and a negative peak would sail through the
// containment checks that only ever compare upwards.
Params negative;
negative.Add("min-luma", Number(-1.0))
.Add("max-luma", Number(-4000.0))
.Add("max-cll", Number(-1000.0))
.Add("max-fall", Number(-400.0));
const mpv_node negative_node = negative.Node();
const mpv::SourceHdrMetadata refused = mpv::ParseSourceHdrMetadata(&negative_node);
EXPECT(refused.min_luminance == 0.0);
EXPECT(refused.max_luminance == 0.0);
EXPECT(refused.max_cll == 0.0);
EXPECT(refused.max_fall == 0.0);
}
// The names are mpv's own and this parse does not judge them: SDR curves, gamuts
// with no protocol counterpart, and anything a future mpv adds all pass through
// verbatim for the caller to classify. Copying them into an enum here would put
// the same table in two places.
void TestUnknownNamesPassThroughVerbatim() {
Params params;
params.Add("gamma", Text("bt.1886")).Add("primaries", Text("display-p3")).Add("max-cll", Number(120.0));
const mpv_node node = params.Node();
const mpv::SourceHdrMetadata metadata = mpv::ParseSourceHdrMetadata(&node);
EXPECT(metadata.transfer == "bt.1886");
EXPECT(metadata.primaries == "display-p3");
EXPECT(metadata.max_cll == 120.0);
}
// Everything that is not the map mpv documents has to read as "no stream": the
// property is unavailable between files and while an audio-only core runs, and
// the event carries MPV_FORMAT_NONE then. Deciding from a half-read map would
// describe the plane in a colour space nothing is emitting.
void TestNonMapNodesYieldNothing() {
EXPECT(mpv::ParseSourceHdrMetadata(nullptr).transfer.empty());
mpv_node none{};
none.format = MPV_FORMAT_NONE;
EXPECT(mpv::ParseSourceHdrMetadata(&none).transfer.empty());
// The shape an observation of the same property in another format delivers.
const mpv_node string_node = Text("pq");
const mpv::SourceHdrMetadata from_string = mpv::ParseSourceHdrMetadata(&string_node);
EXPECT(from_string.transfer.empty());
EXPECT(from_string.primaries.empty());
// An array is a map's near neighbour and has no keys to walk.
mpv_node_list list{};
list.num = 1;
mpv_node array_node{};
array_node.format = MPV_FORMAT_NODE_ARRAY;
array_node.u.list = &list;
EXPECT(mpv::ParseSourceHdrMetadata(&array_node).transfer.empty());
// A map claiming entries it does not carry, which is what a truncated or
// hostile payload looks like.
mpv_node empty_map{};
empty_map.format = MPV_FORMAT_NODE_MAP;
empty_map.u.list = &list;
EXPECT(mpv::ParseSourceHdrMetadata(&empty_map).transfer.empty());
mpv_node null_map{};
null_map.format = MPV_FORMAT_NODE_MAP;
EXPECT(mpv::ParseSourceHdrMetadata(&null_map).transfer.empty());
}
// A key whose value is not the format mpv documents for it is ignored rather
// than reinterpreted, and it must not stop the rest of the map being read. An
// integer luminance is the one exception: the blocking sub-property read this
// replaced asked for a double and libmpv would have converted it.
void TestMistypedValuesAreSkippedButIntegersAreNot() {
Params params;
params.Add("gamma", Number(2.2))
.Add("primaries", Text("bt.2020"))
.Add("max-cll", Text("1000"))
.Add("max-luma", Whole(4000))
.Add("min-luma", Text("0.0001"));
const mpv_node node = params.Node();
const mpv::SourceHdrMetadata metadata = mpv::ParseSourceHdrMetadata(&node);
EXPECT(metadata.transfer.empty());
EXPECT(metadata.primaries == "bt.2020");
EXPECT(metadata.max_cll == 0.0);
EXPECT(metadata.max_luminance == 4000.0);
EXPECT(metadata.min_luminance == 0.0);
// mpv owns the strings, and a null one is not a name.
Params null_name;
null_name.Add("gamma", Text(nullptr)).Add("primaries", Text("bt.2020"));
const mpv_node null_name_node = null_name.Node();
const mpv::SourceHdrMetadata from_null = mpv::ParseSourceHdrMetadata(&null_name_node);
EXPECT(from_null.transfer.empty());
EXPECT(from_null.primaries == "bt.2020");
}
} // namespace
int main() {
TestEveryFieldPresentIsRead();
TestNamesSurviveWithNoLuminances();
TestEachLuminanceIsAbsentOnItsOwn();
TestMinLumaAcceptsAZeroTheOthersRefuse();
TestUnknownNamesPassThroughVerbatim();
TestNonMapNodesYieldNothing();
TestMistypedValuesAreSkippedButIntegersAreNot();
return failures == 0 ? 0 : 1;
}