[PCLF] Improve error handling and test coverage for AnalyzingVideoSink
Bug: b/240540204 Change-Id: If60ade3dce760e8e730cbde2b199d407461b16ce Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/281080 Reviewed-by: Andrey Logvin <landrey@google.com> Commit-Queue: Artem Titov <titovartem@webrtc.org> Cr-Commit-Position: refs/heads/main@{#38506}
This commit is contained in:
parent
48f05cd0e8
commit
3ea1608816
@ -30,7 +30,6 @@
|
||||
|
||||
namespace webrtc {
|
||||
namespace webrtc_pc_e2e {
|
||||
namespace {
|
||||
|
||||
using VideoSubscription = ::webrtc::webrtc_pc_e2e::
|
||||
PeerConnectionE2EQualityTestFixture::VideoSubscription;
|
||||
@ -39,35 +38,6 @@ using VideoResolution = ::webrtc::webrtc_pc_e2e::
|
||||
using VideoConfig =
|
||||
::webrtc::webrtc_pc_e2e::PeerConnectionE2EQualityTestFixture::VideoConfig;
|
||||
|
||||
// Scales video frame to `required_resolution` if necessary. Crashes if video
|
||||
// frame and `required_resolution` have different aspect ratio.
|
||||
VideoFrame ScaleVideoFrame(const VideoFrame& frame,
|
||||
VideoResolution required_resolution) {
|
||||
if (required_resolution.width() == static_cast<size_t>(frame.width()) &&
|
||||
required_resolution.height() == static_cast<size_t>(frame.height())) {
|
||||
return frame;
|
||||
}
|
||||
|
||||
RTC_CHECK_LE(std::abs(static_cast<double>(required_resolution.width()) /
|
||||
required_resolution.height() -
|
||||
static_cast<double>(frame.width()) / frame.height()),
|
||||
2 * std::numeric_limits<double>::epsilon())
|
||||
<< "Received frame has different aspect ratio compared to requested "
|
||||
<< "video resolution: required resolution="
|
||||
<< required_resolution.ToString()
|
||||
<< "; actual resolution=" << frame.width() << "x" << frame.height();
|
||||
|
||||
rtc::scoped_refptr<I420Buffer> scaled_buffer(I420Buffer::Create(
|
||||
required_resolution.width(), required_resolution.height()));
|
||||
scaled_buffer->ScaleFrom(*frame.video_frame_buffer()->ToI420());
|
||||
|
||||
VideoFrame scaled_frame = frame;
|
||||
scaled_frame.set_video_frame_buffer(scaled_buffer);
|
||||
return scaled_frame;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AnalyzingVideoSink::AnalyzingVideoSink(absl::string_view peer_name,
|
||||
Clock* clock,
|
||||
VideoQualityAnalyzerInterface& analyzer,
|
||||
@ -93,6 +63,13 @@ void AnalyzingVideoSink::UpdateSubscription(
|
||||
subscription_.GetResolutionForPeer(it->second.sender_peer_name);
|
||||
if (!new_requested_resolution.has_value() ||
|
||||
(*new_requested_resolution != it->second.resolution)) {
|
||||
RTC_LOG(LS_INFO) << peer_name_ << ": Subscribed resolution for stream "
|
||||
<< it->first << " from " << it->second.sender_peer_name
|
||||
<< " was updated from "
|
||||
<< it->second.resolution.ToString() << " to "
|
||||
<< new_requested_resolution->ToString()
|
||||
<< ". Repopulating all video sinks and recreating "
|
||||
<< "requested video writers";
|
||||
writers_to_close.insert(it->second.video_frame_writer);
|
||||
it = stream_sinks_.erase(it);
|
||||
} else {
|
||||
@ -127,6 +104,36 @@ void AnalyzingVideoSink::OnFrame(const VideoFrame& frame) {
|
||||
}
|
||||
}
|
||||
|
||||
VideoFrame AnalyzingVideoSink::ScaleVideoFrame(
|
||||
const VideoFrame& frame,
|
||||
const VideoResolution& required_resolution) {
|
||||
if (required_resolution.width() == static_cast<size_t>(frame.width()) &&
|
||||
required_resolution.height() == static_cast<size_t>(frame.height())) {
|
||||
return frame;
|
||||
}
|
||||
|
||||
// We allow some difference in the aspect ration because when decoder
|
||||
// downscales video stream it may round up some dimensions to make them even,
|
||||
// ex: 960x540 -> 480x270 -> 240x136 instead of 240x135.
|
||||
RTC_CHECK_LE(std::abs(static_cast<double>(required_resolution.width()) /
|
||||
required_resolution.height() -
|
||||
static_cast<double>(frame.width()) / frame.height()),
|
||||
0.1)
|
||||
<< peer_name_
|
||||
<< ": Received frame has too different aspect ratio compared to "
|
||||
<< "requested video resolution: required resolution="
|
||||
<< required_resolution.ToString()
|
||||
<< "; actual resolution=" << frame.width() << "x" << frame.height();
|
||||
|
||||
rtc::scoped_refptr<I420Buffer> scaled_buffer(I420Buffer::Create(
|
||||
required_resolution.width(), required_resolution.height()));
|
||||
scaled_buffer->ScaleFrom(*frame.video_frame_buffer()->ToI420());
|
||||
|
||||
VideoFrame scaled_frame = frame;
|
||||
scaled_frame.set_video_frame_buffer(scaled_buffer);
|
||||
return scaled_frame;
|
||||
}
|
||||
|
||||
void AnalyzingVideoSink::AnalyzeFrame(const VideoFrame& frame) {
|
||||
VideoFrame frame_copy = frame;
|
||||
frame_copy.set_video_frame_buffer(
|
||||
@ -159,6 +166,12 @@ AnalyzingVideoSink::SinksDescriptor* AnalyzingVideoSink::PopulateSinks(
|
||||
<< " for which they were not subscribed";
|
||||
resolution = config.GetResolution();
|
||||
}
|
||||
if (!resolution->IsRegular()) {
|
||||
RTC_LOG(LS_ERROR) << peer_name_ << " received stream " << stream_label
|
||||
<< " from " << sender_peer_name
|
||||
<< " for which resolution wasn't resolved";
|
||||
resolution = config.GetResolution();
|
||||
}
|
||||
|
||||
RTC_CHECK(resolution.has_value());
|
||||
|
||||
|
||||
@ -66,9 +66,15 @@ class AnalyzingVideoSink : public rtc::VideoSinkInterface<VideoFrame> {
|
||||
std::vector<std::unique_ptr<rtc::VideoSinkInterface<VideoFrame>>> sinks;
|
||||
};
|
||||
|
||||
// Creates full copy of the frame to free any frame owned internal buffers and
|
||||
// passes created copy to analyzer. Uses `I420Buffer` to represent frame
|
||||
// content.
|
||||
// Scales video frame to `required_resolution` if necessary. Crashes if video
|
||||
// frame and `required_resolution` have different aspect ratio.
|
||||
VideoFrame ScaleVideoFrame(
|
||||
const VideoFrame& frame,
|
||||
const PeerConnectionE2EQualityTestFixture::VideoResolution&
|
||||
required_resolution);
|
||||
// Creates full copy of the frame to free any frame owned internal buffers
|
||||
// and passes created copy to analyzer. Uses `I420Buffer` to represent
|
||||
// frame content.
|
||||
void AnalyzeFrame(const VideoFrame& frame);
|
||||
// Populates sink for specified stream and caches them in `stream_sinks_`.
|
||||
SinksDescriptor* PopulateSinks(absl::string_view stream_label);
|
||||
|
||||
@ -183,7 +183,7 @@ TEST_F(AnalyzingVideoSinkTest, VideoFramesAreDumpedCorrectly) {
|
||||
}
|
||||
|
||||
TEST_F(AnalyzingVideoSinkTest,
|
||||
FallbackOnConfigResolutionIfNoSucscriptionProvided) {
|
||||
FallbackOnConfigResolutionIfNoSubscriptionProvided) {
|
||||
VideoSubscription subscription;
|
||||
VideoConfig video_config("alice_video", /*width=*/320, /*height=*/240,
|
||||
/*fps=*/30);
|
||||
@ -225,6 +225,51 @@ TEST_F(AnalyzingVideoSinkTest,
|
||||
ExpectOutputFilesCount(1);
|
||||
}
|
||||
|
||||
TEST_F(AnalyzingVideoSinkTest,
|
||||
FallbackOnConfigResolutionIfNoSubscriptionIsNotResolved) {
|
||||
VideoSubscription subscription;
|
||||
subscription.SubscribeToAllPeers(
|
||||
VideoResolution(VideoResolution::Spec::kMaxFromSender));
|
||||
VideoConfig video_config("alice_video", /*width=*/320, /*height=*/240,
|
||||
/*fps=*/30);
|
||||
video_config.output_dump_options = VideoDumpOptions(test_directory_);
|
||||
|
||||
ExampleVideoQualityAnalyzer analyzer;
|
||||
std::unique_ptr<test::FrameGeneratorInterface> frame_generator =
|
||||
CreateFrameGenerator(/*width=*/320, /*height=*/240);
|
||||
VideoFrame frame = CreateFrame(*frame_generator);
|
||||
frame.set_id(analyzer.OnFrameCaptured("alice", "alice_video", frame));
|
||||
|
||||
{
|
||||
// `helper` and `sink` has to be destroyed so all frames will be written
|
||||
// to the disk.
|
||||
AnalyzingVideoSinksHelper helper;
|
||||
helper.AddConfig("alice", video_config);
|
||||
AnalyzingVideoSink sink("bob", Clock::GetRealTimeClock(), analyzer, helper,
|
||||
subscription);
|
||||
sink.OnFrame(frame);
|
||||
}
|
||||
|
||||
EXPECT_THAT(analyzer.frames_rendered(), Eq(static_cast<uint64_t>(1)));
|
||||
|
||||
test::Y4mFrameReaderImpl frame_reader(
|
||||
test::JoinFilename(test_directory_, "alice_video_bob_320x240_30.y4m"),
|
||||
/*width=*/320,
|
||||
/*height=*/240);
|
||||
ASSERT_TRUE(frame_reader.Init());
|
||||
EXPECT_THAT(frame_reader.NumberOfFrames(), Eq(1));
|
||||
rtc::scoped_refptr<I420Buffer> actual_frame = frame_reader.ReadFrame();
|
||||
rtc::scoped_refptr<I420BufferInterface> expected_frame =
|
||||
frame.video_frame_buffer()->ToI420();
|
||||
double psnr = I420PSNR(*expected_frame, *actual_frame);
|
||||
double ssim = I420SSIM(*expected_frame, *actual_frame);
|
||||
// Frames should be equal.
|
||||
EXPECT_DOUBLE_EQ(ssim, 1.00);
|
||||
EXPECT_DOUBLE_EQ(psnr, 48);
|
||||
|
||||
ExpectOutputFilesCount(1);
|
||||
}
|
||||
|
||||
TEST_F(AnalyzingVideoSinkTest,
|
||||
VideoFramesAreDumpedCorrectlyWhenSubscriptionChanged) {
|
||||
VideoSubscription subscription_before;
|
||||
@ -298,6 +343,116 @@ TEST_F(AnalyzingVideoSinkTest,
|
||||
ExpectOutputFilesCount(2);
|
||||
}
|
||||
|
||||
TEST_F(AnalyzingVideoSinkTest,
|
||||
VideoFramesAreDumpedCorrectlyWhenSubscriptionChangedOnTheSameOne) {
|
||||
VideoSubscription subscription_before;
|
||||
subscription_before.SubscribeToPeer(
|
||||
"alice", VideoResolution(/*width=*/640, /*height=*/360, /*fps=*/30));
|
||||
VideoSubscription subscription_after;
|
||||
subscription_after.SubscribeToPeer(
|
||||
"alice", VideoResolution(/*width=*/640, /*height=*/360, /*fps=*/30));
|
||||
VideoConfig video_config("alice_video", /*width=*/640, /*height=*/360,
|
||||
/*fps=*/30);
|
||||
video_config.output_dump_options = VideoDumpOptions(test_directory_);
|
||||
|
||||
ExampleVideoQualityAnalyzer analyzer;
|
||||
std::unique_ptr<test::FrameGeneratorInterface> frame_generator =
|
||||
CreateFrameGenerator(/*width=*/640, /*height=*/360);
|
||||
VideoFrame frame_before = CreateFrame(*frame_generator);
|
||||
frame_before.set_id(
|
||||
analyzer.OnFrameCaptured("alice", "alice_video", frame_before));
|
||||
VideoFrame frame_after = CreateFrame(*frame_generator);
|
||||
frame_after.set_id(
|
||||
analyzer.OnFrameCaptured("alice", "alice_video", frame_after));
|
||||
|
||||
{
|
||||
// `helper` and `sink` has to be destroyed so all frames will be written
|
||||
// to the disk.
|
||||
AnalyzingVideoSinksHelper helper;
|
||||
helper.AddConfig("alice", video_config);
|
||||
AnalyzingVideoSink sink("bob", Clock::GetRealTimeClock(), analyzer, helper,
|
||||
subscription_before);
|
||||
sink.OnFrame(frame_before);
|
||||
|
||||
sink.UpdateSubscription(subscription_after);
|
||||
sink.OnFrame(frame_after);
|
||||
}
|
||||
|
||||
EXPECT_THAT(analyzer.frames_rendered(), Eq(static_cast<uint64_t>(2)));
|
||||
|
||||
{
|
||||
test::Y4mFrameReaderImpl frame_reader(
|
||||
test::JoinFilename(test_directory_, "alice_video_bob_640x360_30.y4m"),
|
||||
/*width=*/640,
|
||||
/*height=*/360);
|
||||
ASSERT_TRUE(frame_reader.Init());
|
||||
EXPECT_THAT(frame_reader.NumberOfFrames(), Eq(2));
|
||||
// Read the first frame.
|
||||
rtc::scoped_refptr<I420Buffer> actual_frame = frame_reader.ReadFrame();
|
||||
rtc::scoped_refptr<I420BufferInterface> expected_frame =
|
||||
frame_before.video_frame_buffer()->ToI420();
|
||||
// Frames should be equal.
|
||||
EXPECT_DOUBLE_EQ(I420SSIM(*expected_frame, *actual_frame), 1.00);
|
||||
EXPECT_DOUBLE_EQ(I420PSNR(*expected_frame, *actual_frame), 48);
|
||||
// Read the second frame.
|
||||
actual_frame = frame_reader.ReadFrame();
|
||||
expected_frame = frame_after.video_frame_buffer()->ToI420();
|
||||
// Frames should be equal.
|
||||
EXPECT_DOUBLE_EQ(I420SSIM(*expected_frame, *actual_frame), 1.00);
|
||||
EXPECT_DOUBLE_EQ(I420PSNR(*expected_frame, *actual_frame), 48);
|
||||
}
|
||||
|
||||
ExpectOutputFilesCount(1);
|
||||
}
|
||||
|
||||
TEST_F(AnalyzingVideoSinkTest, SmallDiviationsInAspectRationAreAllowed) {
|
||||
VideoSubscription subscription;
|
||||
subscription.SubscribeToPeer(
|
||||
"alice", VideoResolution(/*width=*/480, /*height=*/270, /*fps=*/30));
|
||||
VideoConfig video_config("alice_video", /*width=*/480, /*height=*/270,
|
||||
/*fps=*/30);
|
||||
video_config.output_dump_options = VideoDumpOptions(test_directory_);
|
||||
|
||||
ExampleVideoQualityAnalyzer analyzer;
|
||||
// Generator produces downscaled frames with a bit different aspect ration.
|
||||
std::unique_ptr<test::FrameGeneratorInterface> frame_generator =
|
||||
CreateFrameGenerator(/*width=*/240, /*height=*/136);
|
||||
VideoFrame frame = CreateFrame(*frame_generator);
|
||||
frame.set_id(analyzer.OnFrameCaptured("alice", "alice_video", frame));
|
||||
|
||||
{
|
||||
// `helper` and `sink` has to be destroyed so all frames will be written
|
||||
// to the disk.
|
||||
AnalyzingVideoSinksHelper helper;
|
||||
helper.AddConfig("alice", video_config);
|
||||
AnalyzingVideoSink sink("bob", Clock::GetRealTimeClock(), analyzer, helper,
|
||||
subscription);
|
||||
sink.OnFrame(frame);
|
||||
}
|
||||
|
||||
EXPECT_THAT(analyzer.frames_rendered(), Eq(static_cast<uint64_t>(1)));
|
||||
|
||||
{
|
||||
test::Y4mFrameReaderImpl frame_reader(
|
||||
test::JoinFilename(test_directory_, "alice_video_bob_480x270_30.y4m"),
|
||||
/*width=*/480,
|
||||
/*height=*/270);
|
||||
ASSERT_TRUE(frame_reader.Init());
|
||||
EXPECT_THAT(frame_reader.NumberOfFrames(), Eq(1));
|
||||
// Read the first frame.
|
||||
rtc::scoped_refptr<I420Buffer> actual_frame = frame_reader.ReadFrame();
|
||||
rtc::scoped_refptr<I420BufferInterface> expected_frame =
|
||||
frame.video_frame_buffer()->ToI420();
|
||||
// Actual frame is upscaled version of the expected. But because rendered
|
||||
// resolution is equal to the actual frame size we need to upscale expected
|
||||
// during comparison and then they have to be the same.
|
||||
EXPECT_DOUBLE_EQ(I420SSIM(*actual_frame, *expected_frame), 1);
|
||||
EXPECT_DOUBLE_EQ(I420PSNR(*actual_frame, *expected_frame), 48);
|
||||
}
|
||||
|
||||
ExpectOutputFilesCount(1);
|
||||
}
|
||||
|
||||
TEST_F(AnalyzingVideoSinkTest, VideoFramesIdsAreDumpedWhenRequested) {
|
||||
VideoSubscription subscription;
|
||||
subscription.SubscribeToPeer(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user