/** * @file tests/unit/test_video.cpp * @brief Test src/video.*. */ // test includes #include "../tests_common.h" // standard includes #include #include #include #include #include #include #include #include // ffmpeg includes extern "C" { #include #include } // local includes #include #include using namespace std::literals; /** * @brief Packet replacement rules retain the original non-owning byte views. */ TEST(VideoPacketReplacementTest, CopiesViewsWithoutCopyingTheirBackingBytes) { std::string original = "__old__"; std::string replacement = "__new__"; const video::packet_raw_t::replace_t rule {std::string_view(original).substr(2, 3), std::string_view(replacement).substr(2, 3)}; EXPECT_EQ(rule.old, "old"); EXPECT_EQ(rule._new, "new"); EXPECT_EQ(rule.old.data(), original.data() + 2); EXPECT_EQ(rule._new.data(), replacement.data() + 2); original[2] = 'O'; replacement[2] = 'N'; EXPECT_EQ(rule.old, "Old"); EXPECT_EQ(rule._new, "New"); } TEST(VideoFormatMappingTest, MapsSupportedHardwareDevicesAndRejectsUnsupportedOnes) { using enum platf::mem_type_e; const std::array cases { std::pair {AV_HWDEVICE_TYPE_D3D11VA, dxgi}, std::pair {AV_HWDEVICE_TYPE_VAAPI, vaapi}, std::pair {AV_HWDEVICE_TYPE_CUDA, cuda}, std::pair {AV_HWDEVICE_TYPE_NONE, system}, std::pair {AV_HWDEVICE_TYPE_VIDEOTOOLBOX, videotoolbox}, #ifdef SUNSHINE_BUILD_VULKAN std::pair {AV_HWDEVICE_TYPE_VULKAN, vulkan}, #else std::pair {AV_HWDEVICE_TYPE_VULKAN, unknown}, #endif std::pair {AV_HWDEVICE_TYPE_DXVA2, unknown}, }; for (const auto &[format, expected] : cases) { EXPECT_EQ(video::map_base_dev_type(format), expected) << format; } } TEST(VideoFormatMappingTest, MapsSupportedPixelFormatsAndRejectsUnsupportedOnes) { using enum platf::pix_fmt_e; const std::array cases { std::pair {AV_PIX_FMT_VUYX, ayuv}, std::pair {AV_PIX_FMT_XV30, y410}, std::pair {AV_PIX_FMT_YUV420P10, yuv420p10}, std::pair {AV_PIX_FMT_YUV420P, yuv420p}, std::pair {AV_PIX_FMT_NV12, nv12}, std::pair {AV_PIX_FMT_P010, p010}, std::pair {AV_PIX_FMT_YUV444P, yuv444p}, std::pair {AV_PIX_FMT_YUV444P16, yuv444p16}, std::pair {AV_PIX_FMT_NONE, unknown}, std::pair {AV_PIX_FMT_RGB24, unknown}, }; for (const auto &[format, expected] : cases) { EXPECT_EQ(video::map_pix_fmt(format), expected) << format; } } struct EncoderTest: PlatformTestSuite, testing::WithParamInterface { void SetUp() override { BaseTest::SetUp(); auto &encoder = *GetParam(); if (!video::validate_encoder(encoder, false)) { // Encoder failed validation, // if it's software - fail, otherwise skip if (encoder.name == "software") { FAIL() << "Software encoder not available"; } else { GTEST_SKIP() << "Encoder not available"; } } } }; /** * @brief Available encoder backends for this platform. */ const auto encoder_variants = std::to_array({ #if !defined(__APPLE__) &video::nvenc, #endif #ifdef _WIN32 &video::amdvce, &video::quicksync, #endif #if defined(__linux__) || defined(__FreeBSD__) &video::vaapi, #endif #ifdef __APPLE__ &video::videotoolbox, #endif &video::software }); INSTANTIATE_TEST_SUITE_P( EncoderVariants, EncoderTest, testing::ValuesIn(encoder_variants), [](const auto &info) { return std::string(info.param->name); } ); #if !defined(_WIN32) && !defined(__APPLE__) TEST(NvencAvcodecOptionsTest, UsesVersionStablePresetAndSpatialAqNames) { const std::array codecs { &video::nvenc.av1, &video::nvenc.hevc, &video::nvenc.h264, }; for (const auto *codec : codecs) { const auto preset = std::ranges::find(codec->common_options, "preset"sv, &video::encoder_t::option_t::name); ASSERT_NE(codec->common_options.end(), preset); ASSERT_TRUE(std::holds_alternative(preset->value)); EXPECT_EQ(&config::video.nv_legacy.preset, std::get(preset->value)); const auto spatial_aq = std::ranges::find(codec->common_options, "spatial-aq"sv, &video::encoder_t::option_t::name); ASSERT_NE(codec->common_options.end(), spatial_aq); ASSERT_TRUE(std::holds_alternative(spatial_aq->value)); EXPECT_EQ(&config::video.nv_legacy.spatial_aq, std::get(spatial_aq->value)); EXPECT_EQ( codec->common_options.end(), std::ranges::find(codec->common_options, "aq"sv, &video::encoder_t::option_t::name) ); } } #endif TEST_P(EncoderTest, ValidateEncoder) { // todo:: test something besides fixture setup } /** * @brief Parameterized coverage for effective H.264 profile selection. */ struct H264ProfileTest: testing::TestWithParam> {}; TEST_P(H264ProfileTest, SelectProfile) { const auto &[encoder_name, coder, chroma_sampling_type, expected_profile] = GetParam(); video::config_t config {}; config.chromaSamplingType = chroma_sampling_type; EXPECT_EQ(expected_profile, video::select_h264_profile(encoder_name, config, std::to_underlying(coder))); } INSTANTIATE_TEST_SUITE_P( H264ProfileTests, H264ProfileTest, testing::Values( std::make_tuple("h264_amf"sv, video::amf::coder_e::auto_, 0, AV_PROFILE_H264_HIGH), std::make_tuple("h264_amf"sv, video::amf::coder_e::cabac, 0, AV_PROFILE_H264_HIGH), std::make_tuple("h264_amf"sv, video::amf::coder_e::cavlc, 0, AV_PROFILE_H264_CONSTRAINED_BASELINE), std::make_tuple("h264_amf"sv, video::amf::coder_e::cavlc, 1, AV_PROFILE_H264_HIGH_444_PREDICTIVE), std::make_tuple("h264_nvenc"sv, video::amf::coder_e::cavlc, 0, AV_PROFILE_H264_HIGH) ) ); /** * @brief Parameterized coverage for resolving requested dynamic range against encoder capabilities. */ struct DynamicRangeTest: testing::TestWithParam> {}; TEST_P(DynamicRangeTest, Resolve) { const auto &[video_format, chroma_sampling_type, requested_dynamic_range, supports_hdr, supports_hdr_yuv444, expected_dynamic_range] = GetParam(); video::encoder_t encoder { "test"sv, {}, {}, {}, {}, 0, }; encoder.h264.name = "h264_test"; encoder.hevc.name = "hevc_test"; encoder.av1.name = "av1_test"; video::config_t config {}; config.videoFormat = video_format; config.dynamicRange = requested_dynamic_range; config.chromaSamplingType = chroma_sampling_type; auto *codec = &encoder.h264; if (video_format == 1) { codec = &encoder.hevc; } else if (video_format == 2) { codec = &encoder.av1; } (*codec)[video::encoder_t::DYNAMIC_RANGE] = supports_hdr; (*codec)[video::encoder_t::DYNAMIC_RANGE_YUV444] = supports_hdr_yuv444; const auto effective_config = video::resolve_dynamic_range(encoder, config); EXPECT_EQ(expected_dynamic_range, effective_config.dynamicRange); EXPECT_EQ(requested_dynamic_range, config.dynamicRange); EXPECT_EQ(video_format, effective_config.videoFormat); EXPECT_EQ(chroma_sampling_type, effective_config.chromaSamplingType); } INSTANTIATE_TEST_SUITE_P( DynamicRangeTests, DynamicRangeTest, testing::Values( std::make_tuple(0, 0, 1, false, true, 0), std::make_tuple(1, 0, 1, false, true, 0), std::make_tuple(1, 0, 1, true, false, 1), std::make_tuple(2, 1, 1, true, false, 0), std::make_tuple(2, 1, 1, false, true, 1), std::make_tuple(1, 0, 0, false, false, 0) ) ); /** * @brief Reset the published probe results before and after each test. */ struct EncoderProbeResultsTest: testing::Test { /** * @brief Start from an empty published state. */ void SetUp() override { video::reset_encoder_probe_results(); } /** * @brief Leave no published state behind for other tests. */ void TearDown() override { video::reset_encoder_probe_results(); } }; TEST_F(EncoderProbeResultsTest, FailedCodecProbeDoesNotPublishCapabilities) { video::encoder_t encoder {"test"sv, {}, {}, {}, {}, 0}; for (auto *codec : {&encoder.h264, &encoder.hevc, &encoder.av1}) { (*codec)[video::encoder_t::PASSED] = false; (*codec)[video::encoder_t::YUV444] = true; (*codec)[video::encoder_t::DYNAMIC_RANGE] = true; (*codec)[video::encoder_t::DYNAMIC_RANGE_YUV444] = true; } video::record_encoder_probe_results(encoder); for (std::size_t i = 0; i < 3; ++i) { EXPECT_FALSE(video::last_encoder_probe_supported_yuv444_for_codec[i]) << i; EXPECT_FALSE(video::last_encoder_probe_supported_dynamic_range_for_codec[i]) << i; EXPECT_FALSE(video::last_encoder_probe_supported_dynamic_range_yuv444_for_codec[i]) << i; } } TEST_F(EncoderProbeResultsTest, SubsequentProbeReplacesPriorResults) { constexpr uint32_t ref_frames_invalidation = 1 << 8; // Mirrors REF_FRAMES_INVALIDATION in video.cpp. video::encoder_t first {"first"sv, {}, {}, {}, {}, ref_frames_invalidation}; for (auto *codec : {&first.h264, &first.hevc, &first.av1}) { (*codec)[video::encoder_t::PASSED] = true; (*codec)[video::encoder_t::YUV444] = true; (*codec)[video::encoder_t::DYNAMIC_RANGE] = true; (*codec)[video::encoder_t::DYNAMIC_RANGE_YUV444] = true; } // The second encoder only supports plain 8-bit 4:2:0 H.264. video::encoder_t second {"second"sv, {}, {}, {}, {}, 0}; second.h264[video::encoder_t::PASSED] = true; video::record_encoder_probe_results(first); ASSERT_TRUE(video::last_encoder_probe_supported_ref_frames_invalidation); ASSERT_TRUE(video::last_encoder_probe_supported_yuv444_for_codec[1]); ASSERT_TRUE(video::last_encoder_probe_supported_dynamic_range_for_codec[2]); ASSERT_TRUE(video::last_encoder_probe_supported_dynamic_range_yuv444_for_codec[2]); video::record_encoder_probe_results(second); EXPECT_FALSE(video::last_encoder_probe_supported_ref_frames_invalidation); for (std::size_t i = 0; i < 3; ++i) { EXPECT_FALSE(video::last_encoder_probe_supported_yuv444_for_codec[i]) << i; EXPECT_FALSE(video::last_encoder_probe_supported_dynamic_range_for_codec[i]) << i; EXPECT_FALSE(video::last_encoder_probe_supported_dynamic_range_yuv444_for_codec[i]) << i; } } #ifdef _WIN32 TEST(AmfH264OptionsTest, CoderUsesConfiguredValue) { const auto coder_option = std::ranges::find(video::amdvce.h264.common_options, "coder"sv, &video::encoder_t::option_t::name); ASSERT_NE(video::amdvce.h264.common_options.end(), coder_option); ASSERT_TRUE(std::holds_alternative(coder_option->value)); EXPECT_EQ(&config::video.amd.amd_coder, std::get(coder_option->value)); } /** * @brief Parameterized coverage for the AMF maximum access-unit-size option mappings. */ struct AmfMaxAuSizeOptionsTest: testing::TestWithParam> {}; TEST_P(AmfMaxAuSizeOptionsTest, UsesConfiguredValueForSupportedCodecsOnly) { const auto &[codec, supported] = GetParam(); const auto option = std::ranges::find(codec->common_options, "max_au_size"sv, &video::encoder_t::option_t::name); if (!supported) { EXPECT_EQ(codec->common_options.end(), option); return; } ASSERT_NE(codec->common_options.end(), option); ASSERT_TRUE(std::holds_alternative *>(option->value)); EXPECT_EQ(&config::video.amd.amd_max_au_size, std::get *>(option->value)); } INSTANTIATE_TEST_SUITE_P( AmfCodecOptions, AmfMaxAuSizeOptionsTest, testing::Values( std::make_tuple(&video::amdvce.h264, true), std::make_tuple(&video::amdvce.hevc, true), std::make_tuple(&video::amdvce.av1, false) ) ); #endif using AmfMaxAuSizeConfigParam = std::tuple>; /** * @brief Parameterized coverage for parsing and validating the AMF maximum access-unit size. */ struct AmfMaxAuSizeConfigTest: BaseTest, testing::WithParamInterface { void SetUp() override { BaseTest::SetUp(); config::video.amd.amd_max_au_size.reset(); config::stream.file_apps = SUNSHINE_SOURCE_DIR "/tests/unit/test_video.cpp"; } void TearDown() override { config::video = original_video; config::audio = original_audio; config::stream = original_stream; config::nvhttp = original_nvhttp; config::input = original_input; config::sunshine = original_sunshine; config::modified_config_settings = original_modified_config_settings; BaseTest::TearDown(); } config::video_t original_video {config::video}; ///< Video configuration restored after each parameterized test. config::audio_t original_audio {config::audio}; ///< Audio configuration restored after each parameterized test. config::stream_t original_stream {config::stream}; ///< Stream configuration restored after each parameterized test. config::nvhttp_t original_nvhttp {config::nvhttp}; ///< HTTP configuration restored after each parameterized test. config::input_t original_input {config::input}; ///< Input configuration restored after each parameterized test. config::sunshine_t original_sunshine {config::sunshine}; ///< Core configuration restored after each parameterized test. decltype(config::modified_config_settings) original_modified_config_settings {config::modified_config_settings}; ///< Modified settings restored after each parameterized test. }; TEST_P(AmfMaxAuSizeConfigTest, AcceptsOnlyFfmpegSupportedRange) { const auto &[setting, expected] = GetParam(); config::apply_config_for_test(setting); EXPECT_EQ(expected, config::video.amd.amd_max_au_size); } INSTANTIATE_TEST_SUITE_P( AmfMaxAuSizeValues, AmfMaxAuSizeConfigTest, testing::Values( AmfMaxAuSizeConfigParam {""sv, std::nullopt}, AmfMaxAuSizeConfigParam {"amd_max_au_size = -2\n"sv, std::nullopt}, AmfMaxAuSizeConfigParam {"amd_max_au_size = -1\n"sv, -1}, AmfMaxAuSizeConfigParam {"amd_max_au_size = 0\n"sv, 0}, AmfMaxAuSizeConfigParam {"amd_max_au_size = 800000\n"sv, 800000}, AmfMaxAuSizeConfigParam {"amd_max_au_size = 2147483647\n"sv, std::numeric_limits::max()} ) ); struct FramerateX100Test: BaseTest, testing::WithParamInterface> {}; TEST_P(FramerateX100Test, Run) { const auto &[x100, expected] = GetParam(); auto res = video::framerateX100_to_rational(x100); ASSERT_EQ(0, av_cmp_q(res, expected)) << "expected " << expected.num << "/" << expected.den << ", got " << res.num << "/" << res.den; } INSTANTIATE_TEST_SUITE_P( FramerateX100Tests, FramerateX100Test, testing::Values( std::make_tuple(2397, AVRational {24000, 1001}), std::make_tuple(2398, AVRational {24000, 1001}), std::make_tuple(2500, AVRational {25, 1}), std::make_tuple(2997, AVRational {30000, 1001}), std::make_tuple(3000, AVRational {30, 1}), std::make_tuple(5994, AVRational {60000, 1001}), std::make_tuple(6000, AVRational {60, 1}), std::make_tuple(11988, AVRational {120000, 1001}), std::make_tuple(23976, AVRational {240000, 1001}), // future NTSC 240hz? std::make_tuple(9498, AVRational {4749, 50}) // from my LG 27GN950 ) ); struct FramerateToRationalTest: testing::TestWithParam> {}; TEST_P(FramerateToRationalTest, Run) { const auto &[framerate, framerateX100, expected] = GetParam(); video::config_t config {}; config.framerate = framerate; config.framerateX100 = framerateX100; auto res = video::framerate_to_rational(config); ASSERT_EQ(0, av_cmp_q(res, expected)) << "expected " << expected.num << "/" << expected.den << ", got " << res.num << "/" << res.den; } INSTANTIATE_TEST_SUITE_P( FramerateToRationalTests, FramerateToRationalTest, testing::Values( std::make_tuple(60, 0, AVRational {60, 1}), // no X100 value, fall back to integer framerate std::make_tuple(60, 5994, AVRational {60000, 1001}), std::make_tuple(120, 11988, AVRational {120000, 1001}), std::make_tuple(24, 2398, AVRational {24000, 1001}) ) ); struct CaptureFrameIntervalTest: testing::TestWithParam> {}; TEST_P(CaptureFrameIntervalTest, Run) { const auto &[framerate, framerateX100, expected] = GetParam(); video::config_t config {}; config.framerate = framerate; config.framerateX100 = framerateX100; ASSERT_EQ(expected, video::capture_frame_interval(config)); } INSTANTIATE_TEST_SUITE_P( CaptureFrameIntervalTests, CaptureFrameIntervalTest, testing::Values( std::make_tuple(60, 0, std::chrono::nanoseconds {16666666}), std::make_tuple(60, 5994, std::chrono::nanoseconds {16683333}), // 1e9 * 1001 / 60000 std::make_tuple(120, 11988, std::chrono::nanoseconds {8341666}) // 1e9 * 1001 / 120000 ) ); /** * @brief Software encoder converts BGR0 and NV12 frames, including padded strides and * backends that don't report the pixel pitch. */ TEST(SoftwareEncoderConversion, Bgr0AndNv12) { constexpr int w = 320; constexpr int h = 240; AVFrame *frame = av_frame_alloc(); ASSERT_NE(frame, nullptr); frame->width = w; frame->height = h; frame->format = AV_PIX_FMT_YUV420P; video::avcodec_software_encode_device_t device; ASSERT_EQ(device.init(w, h, frame, AV_PIX_FMT_YUV420P, false), 0); // set_frame() takes ownership of the frame; the device frees it on destruction. ASSERT_EQ(device.set_frame(frame, nullptr), 0); // BGR0 frame (4 bytes per pixel) -- the classic KMS/DMABUF capture layout. std::vector bgr0_buffer(static_cast(w) * h * 4); platf::img_t bgr0_img {}; bgr0_img.data = bgr0_buffer.data(); bgr0_img.width = w; bgr0_img.height = h; bgr0_img.row_pitch = w * 4; bgr0_img.pixel_pitch = 4; EXPECT_EQ(device.convert(bgr0_img), 0); // NV12 frame (1 byte per pixel row pitch, Y plane + interleaved UV) -- the // layout delivered by PipeWire-based captures (KWin screencast / portal). std::vector nv12_buffer(static_cast(w) * h + static_cast(w) * h / 2); platf::img_t nv12_img {}; nv12_img.data = nv12_buffer.data(); nv12_img.width = w; nv12_img.height = h; nv12_img.row_pitch = w; nv12_img.pixel_pitch = 1; EXPECT_EQ(device.convert(nv12_img), 0); // Padded-stride NV12 (alignment padding) -- the case the old row_pitch == // width heuristic misdetected as BGR0, causing out-of-bounds reads. constexpr int padded_stride = w + 32; std::vector padded_nv12_buffer(static_cast(padded_stride) * h + static_cast(padded_stride) * h / 2); platf::img_t padded_nv12_img {}; padded_nv12_img.data = padded_nv12_buffer.data(); padded_nv12_img.width = w; padded_nv12_img.height = h; padded_nv12_img.row_pitch = padded_stride; padded_nv12_img.pixel_pitch = 1; EXPECT_EQ(device.convert(padded_nv12_img), 0); // Capture backends that don't report pixel_pitch fall back to deriving it // from the row pitch (1 byte per pixel = NV12, 4 = BGR0). platf::img_t fallback_bgr0_img {}; fallback_bgr0_img.data = bgr0_buffer.data(); fallback_bgr0_img.width = w; fallback_bgr0_img.height = h; fallback_bgr0_img.row_pitch = w * 4; EXPECT_EQ(device.convert(fallback_bgr0_img), 0); platf::img_t fallback_nv12_img {}; fallback_nv12_img.data = nv12_buffer.data(); fallback_nv12_img.width = w; fallback_nv12_img.height = h; fallback_nv12_img.row_pitch = w; EXPECT_EQ(device.convert(fallback_nv12_img), 0); }