diff --git a/api/video/video_frame.cc b/api/video/video_frame.cc index 63902af3d5..4f6bd86530 100644 --- a/api/video/video_frame.cc +++ b/api/video/video_frame.cc @@ -60,6 +60,103 @@ bool VideoFrame::UpdateRect::IsEmpty() const { return width == 0 && height == 0; } +VideoFrame::UpdateRect VideoFrame::UpdateRect::ScaleWithFrame( + int frame_width, + int frame_height, + int crop_x, + int crop_y, + int crop_width, + int crop_height, + int scaled_width, + int scaled_height) const { + RTC_DCHECK_GT(frame_width, 0); + RTC_DCHECK_GT(frame_height, 0); + + RTC_DCHECK_GT(crop_width, 0); + RTC_DCHECK_GT(crop_height, 0); + + RTC_DCHECK_LE(crop_width + crop_x, frame_width); + RTC_DCHECK_LE(crop_height + crop_y, frame_height); + + RTC_DCHECK_GT(scaled_width, 0); + RTC_DCHECK_GT(scaled_height, 0); + + // Check if update rect is out of the cropped area. + if (offset_x + width < crop_x || offset_x > crop_x + crop_width || + offset_y + height < crop_y || offset_y > crop_y + crop_width) { + return {0, 0, 0, 0}; + } + + int x = offset_x - crop_x; + int w = width; + if (x < 0) { + w += x; + x = 0; + } + int y = offset_y - crop_y; + int h = height; + if (y < 0) { + h += y; + y = 0; + } + + // Lower corner is rounded down. + x = x * scaled_width / crop_width; + y = y * scaled_height / crop_height; + // Upper corner is rounded up. + w = (w * scaled_width + crop_width - 1) / crop_width; + h = (h * scaled_height + crop_height - 1) / crop_height; + + // Round to full 2x2 blocks due to possible subsampling in the pixel data. + if (x % 2) { + --x; + ++w; + } + if (y % 2) { + --y; + ++h; + } + if (w % 2) { + ++w; + } + if (h % 2) { + ++h; + } + + // Expand the update rect by 2 pixels in each direction to include any + // possible scaling artifacts. + if (scaled_width != crop_width || scaled_height != crop_height) { + if (x > 0) { + x -= 2; + w += 2; + } + if (y > 0) { + y -= 2; + h += 2; + } + w += 2; + h += 2; + } + + // Ensure update rect is inside frame dimensions. + if (x + w > scaled_width) { + w = scaled_width - x; + } + if (y + h > scaled_height) { + h = scaled_height - y; + } + RTC_DCHECK_GE(w, 0); + RTC_DCHECK_GE(h, 0); + if (w == 0 || h == 0) { + w = 0; + h = 0; + x = 0; + y = 0; + } + + return {x, y, w, h}; +} + VideoFrame::Builder::Builder() = default; VideoFrame::Builder::~Builder() = default; diff --git a/api/video/video_frame.h b/api/video/video_frame.h index 338e2fdbd1..d16ef8ce24 100644 --- a/api/video/video_frame.h +++ b/api/video/video_frame.h @@ -49,6 +49,31 @@ class RTC_EXPORT VideoFrame { void MakeEmptyUpdate(); bool IsEmpty() const; + + // Per-member equality check. Empty rectangles with different offsets would + // be considered different. + bool operator==(const UpdateRect& other) const { + return other.offset_x == offset_x && other.offset_y == offset_y && + other.width == width && other.height == height; + } + + bool operator!=(const UpdateRect& other) const { return !(*this == other); } + + // Scales update_rect given original frame dimensions. + // Cropping is applied first, then rect is scaled down. + // Update rect is snapped to 2x2 grid due to possible UV subsampling and + // then expanded by additional 2 pixels in each direction to accommodate any + // possible scaling artifacts. + // Note, close but not equal update_rects on original frame may result in + // the same scaled update rects. + UpdateRect ScaleWithFrame(int frame_width, + int frame_height, + int crop_x, + int crop_y, + int crop_width, + int crop_height, + int scaled_width, + int scaled_height) const; }; // Interface for accessing elements of the encoded frame that was the base for diff --git a/common_video/video_frame_unittest.cc b/common_video/video_frame_unittest.cc index f7a27be747..6b2c97b1df 100644 --- a/common_video/video_frame_unittest.cc +++ b/common_video/video_frame_unittest.cc @@ -551,4 +551,146 @@ INSTANTIATE_TEST_SUITE_P( ::testing::Values(VideoFrameBuffer::Type::kI420, VideoFrameBuffer::Type::kI010))); +TEST(TestUpdateRect, CanCompare) { + VideoFrame::UpdateRect a = {0, 0, 100, 200}; + VideoFrame::UpdateRect b = {0, 0, 100, 200}; + VideoFrame::UpdateRect c = {1, 0, 100, 200}; + VideoFrame::UpdateRect d = {0, 1, 100, 200}; + EXPECT_TRUE(a == b); + EXPECT_FALSE(a == c); + EXPECT_FALSE(a == d); +} + +TEST(TestUpdateRect, ComputesIsEmpty) { + VideoFrame::UpdateRect a = {0, 0, 0, 0}; + VideoFrame::UpdateRect b = {0, 0, 100, 200}; + VideoFrame::UpdateRect c = {1, 100, 0, 0}; + VideoFrame::UpdateRect d = {1, 100, 100, 200}; + EXPECT_TRUE(a.IsEmpty()); + EXPECT_FALSE(b.IsEmpty()); + EXPECT_TRUE(c.IsEmpty()); + EXPECT_FALSE(d.IsEmpty()); +} + +TEST(TestUpdateRectUnion, NonIntersecting) { + VideoFrame::UpdateRect a = {0, 0, 10, 20}; + VideoFrame::UpdateRect b = {100, 200, 10, 20}; + a.Union(b); + EXPECT_EQ(a, VideoFrame::UpdateRect({0, 0, 110, 220})); +} + +TEST(TestUpdateRectUnion, Intersecting) { + VideoFrame::UpdateRect a = {0, 0, 10, 10}; + VideoFrame::UpdateRect b = {5, 5, 30, 20}; + a.Union(b); + EXPECT_EQ(a, VideoFrame::UpdateRect({0, 0, 35, 25})); +} + +TEST(TestUpdateRectUnion, OneInsideAnother) { + VideoFrame::UpdateRect a = {0, 0, 100, 100}; + VideoFrame::UpdateRect b = {5, 5, 30, 20}; + a.Union(b); + EXPECT_EQ(a, VideoFrame::UpdateRect({0, 0, 100, 100})); +} + +TEST(TestUpdateRectIntersect, NonIntersecting) { + VideoFrame::UpdateRect a = {0, 0, 10, 20}; + VideoFrame::UpdateRect b = {100, 200, 10, 20}; + a.Intersect(b); + EXPECT_EQ(a, VideoFrame::UpdateRect({0, 0, 0, 0})); +} + +TEST(TestUpdateRectIntersect, Intersecting) { + VideoFrame::UpdateRect a = {0, 0, 10, 10}; + VideoFrame::UpdateRect b = {5, 5, 30, 20}; + a.Intersect(b); + EXPECT_EQ(a, VideoFrame::UpdateRect({5, 5, 5, 5})); +} + +TEST(TestUpdateRectIntersect, OneInsideAnother) { + VideoFrame::UpdateRect a = {0, 0, 100, 100}; + VideoFrame::UpdateRect b = {5, 5, 30, 20}; + a.Intersect(b); + EXPECT_EQ(a, VideoFrame::UpdateRect({5, 5, 30, 20})); +} + +TEST(TestUpdateRectScale, NoScale) { + const int width = 640; + const int height = 480; + VideoFrame::UpdateRect a = {100, 50, 100, 200}; + VideoFrame::UpdateRect scaled = + a.ScaleWithFrame(width, height, 0, 0, width, height, width, height); + EXPECT_EQ(scaled, VideoFrame::UpdateRect({100, 50, 100, 200})); +} + +TEST(TestUpdateRectScale, CropOnly) { + const int width = 640; + const int height = 480; + VideoFrame::UpdateRect a = {100, 50, 100, 200}; + VideoFrame::UpdateRect scaled = a.ScaleWithFrame( + width, height, 10, 10, width - 20, height - 20, width - 20, height - 20); + EXPECT_EQ(scaled, VideoFrame::UpdateRect({90, 40, 100, 200})); +} + +TEST(TestUpdateRectScale, CropOnlyToOddOffset) { + const int width = 640; + const int height = 480; + VideoFrame::UpdateRect a = {100, 50, 100, 200}; + VideoFrame::UpdateRect scaled = a.ScaleWithFrame( + width, height, 5, 5, width - 10, height - 10, width - 10, height - 10); + EXPECT_EQ(scaled, VideoFrame::UpdateRect({94, 44, 102, 202})); +} + +TEST(TestUpdateRectScale, ScaleByHalf) { + const int width = 640; + const int height = 480; + VideoFrame::UpdateRect a = {100, 60, 100, 200}; + VideoFrame::UpdateRect scaled = a.ScaleWithFrame( + width, height, 0, 0, width, height, width / 2, height / 2); + // Scaled by half and +2 pixels in all directions. + EXPECT_EQ(scaled, VideoFrame::UpdateRect({48, 28, 54, 104})); +} + +TEST(TestUpdateRectScale, CropToUnchangedRegionBelowUpdateRect) { + const int width = 640; + const int height = 480; + VideoFrame::UpdateRect a = {100, 60, 100, 200}; + VideoFrame::UpdateRect scaled = a.ScaleWithFrame( + width, height, (width - 10) / 2, (height - 10) / 2, 10, 10, 10, 10); + // Update is out of the cropped frame. + EXPECT_EQ(scaled, VideoFrame::UpdateRect({0, 0, 0, 0})); +} + +TEST(TestUpdateRectScale, CropToUnchangedRegionAboveUpdateRect) { + const int width = 640; + const int height = 480; + VideoFrame::UpdateRect a = {600, 400, 10, 10}; + VideoFrame::UpdateRect scaled = a.ScaleWithFrame( + width, height, (width - 10) / 2, (height - 10) / 2, 10, 10, 10, 10); + // Update is out of the cropped frame. + EXPECT_EQ(scaled, VideoFrame::UpdateRect({0, 0, 0, 0})); +} + +TEST(TestUpdateRectScale, CropInsideUpdate) { + const int width = 640; + const int height = 480; + VideoFrame::UpdateRect a = {300, 200, 100, 100}; + VideoFrame::UpdateRect scaled = a.ScaleWithFrame( + width, height, (width - 10) / 2, (height - 10) / 2, 10, 10, 10, 10); + // Cropped frame is inside the update rect. + EXPECT_EQ(scaled, VideoFrame::UpdateRect({0, 0, 10, 10})); +} + +TEST(TestUpdateRectScale, CropAndScaleByHalf) { + const int width = 640; + const int height = 480; + VideoFrame::UpdateRect a = {100, 60, 100, 200}; + VideoFrame::UpdateRect scaled = + a.ScaleWithFrame(width, height, 10, 10, width - 20, height - 20, + (width - 20) / 2, (height - 20) / 2); + // Scaled by half and +3 pixels in all directions, because of odd offset after + // crop and scale. + EXPECT_EQ(scaled, VideoFrame::UpdateRect({42, 22, 56, 106})); +} + } // namespace webrtc