Helper API for codec factories to calculate supported H.265 levels.

This expose a new GetSupportedH265Level API for WebRTC external
factories to calculate H.265 levels to be use for SDP negotation.

Bug: webrtc:13485
Change-Id: Ib420da2b9b1b7af00129294be5b3efec172e8faf
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/345544
Commit-Queue: Sergey Silkin <ssilkin@webrtc.org>
Reviewed-by: Philip Eliasson <philipel@webrtc.org>
Reviewed-by: Sergey Silkin <ssilkin@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#42079}
diff --git a/api/video/BUILD.gn b/api/video/BUILD.gn
index 5ec689c..72befb5 100644
--- a/api/video/BUILD.gn
+++ b/api/video/BUILD.gn
@@ -139,6 +139,7 @@
 rtc_source_set("resolution") {
   visibility = [ "*" ]
   public = [ "resolution.h" ]
+  deps = [ "../../rtc_base/system:rtc_export" ]
 }
 
 rtc_library("encoded_image") {
diff --git a/api/video/resolution.h b/api/video/resolution.h
index 11ffef0..1a3c97c 100644
--- a/api/video/resolution.h
+++ b/api/video/resolution.h
@@ -13,10 +13,12 @@
 
 #include <utility>
 
+#include "rtc_base/system/rtc_export.h"
+
 namespace webrtc {
 
 // A struct representing a video resolution in pixels.
-struct Resolution {
+struct RTC_EXPORT Resolution {
   int width = 0;
   int height = 0;
 
diff --git a/api/video_codecs/h265_profile_tier_level.cc b/api/video_codecs/h265_profile_tier_level.cc
index f4dcebb..9705f46 100644
--- a/api/video_codecs/h265_profile_tier_level.cc
+++ b/api/video_codecs/h265_profile_tier_level.cc
@@ -12,6 +12,7 @@
 
 #include <string>
 
+#include "rtc_base/arraysize.h"
 #include "rtc_base/string_to_number.h"
 
 namespace webrtc {
@@ -22,6 +23,42 @@
 const char kH265FmtpTier[] = "tier-flag";
 const char kH265FmtpLevel[] = "level-id";
 
+// Used to align frame width and height for luma picture size calculation.
+// Use the maximum value allowed by spec to get upper bound of luma picture
+// size for given resolution.
+static constexpr int kMinCbSizeYMax = 64;
+
+struct LevelConstraint {
+  const int max_luma_picture_size;
+  const double max_luma_sample_rate;
+  const int max_pic_width_or_height_in_pixels;
+  const H265Level level;
+};
+
+// This is from ITU-T H.265 (09/2023) Table A.8, A.9 & A.11 – Level limits.
+// The max_pic_width_or_height_in_luma_samples is pre-calculated following
+// ITU-T H.265 section A.4.1, that is, find the largest integer value that
+// is multiple of minimal MinCbSizeY(8 according to equation 7-10 and 7-12), is
+// less than sqrt(max_luma_picture_size * 8). For example, at level 1,
+// max_luma_picture_size is 36864, so pic_width_in_luma_samples <= sqrt(36864 *
+// 8) = 543.06. The largest integer that is multiple of 8 and less than 543.06
+// is 536.
+static constexpr LevelConstraint kLevelConstraints[] = {
+    {36864, 552960, 536, H265Level::kLevel1},
+    {122880, 3686400, 984, H265Level::kLevel2},
+    {245760, 7372800, 1400, H265Level::kLevel2_1},
+    {552960, 16588800, 2096, H265Level::kLevel3},
+    {983040, 33177600, 2800, H265Level::kLevel3_1},
+    {2228224, 66846720, 4216, H265Level::kLevel4},
+    {2228224, 133693400, 4216, H265Level::kLevel4_1},
+    {8912896, 267386880, 8440, H265Level::kLevel5},
+    {8912896, 534773760, 8440, H265Level::kLevel5_1},
+    {8912896, 1069547520, 8440, H265Level::kLevel5_2},
+    {35651584, 1069547520, 16888, H265Level::kLevel6},
+    {35651584, 2139095040, 16888, H265Level::kLevel6_1},
+    {35651584, 4278190080, 16888, H265Level::kLevel6_2},
+};
+
 }  // anonymous namespace
 
 // Annex A of https://www.itu.int/rec/T-REC-H.265 (08/21), section A.3.
@@ -245,4 +282,25 @@
          ptl1->tier == ptl2->tier && ptl1->level == ptl2->level;
 }
 
+absl::optional<H265Level> GetSupportedH265Level(const Resolution& resolution,
+                                                float max_fps) {
+  int aligned_width =
+      (resolution.width + kMinCbSizeYMax - 1) & ~(kMinCbSizeYMax - 1);
+  int aligned_height =
+      (resolution.height + kMinCbSizeYMax - 1) & ~(kMinCbSizeYMax - 1);
+
+  for (int i = arraysize(kLevelConstraints) - 1; i >= 0; --i) {
+    const LevelConstraint& level_constraint = kLevelConstraints[i];
+    if (level_constraint.max_luma_picture_size <=
+            aligned_width * aligned_height &&
+        level_constraint.max_luma_sample_rate <=
+            aligned_width * aligned_height * max_fps &&
+        level_constraint.max_pic_width_or_height_in_pixels >= aligned_width &&
+        level_constraint.max_pic_width_or_height_in_pixels >= aligned_height) {
+      return level_constraint.level;
+    }
+  }
+  return absl::nullopt;
+}
+
 }  // namespace webrtc
diff --git a/api/video_codecs/h265_profile_tier_level.h b/api/video_codecs/h265_profile_tier_level.h
index efbdf28..28a35ae 100644
--- a/api/video_codecs/h265_profile_tier_level.h
+++ b/api/video_codecs/h265_profile_tier_level.h
@@ -14,6 +14,7 @@
 #include <string>
 
 #include "absl/types/optional.h"
+#include "api/video/resolution.h"
 #include "api/video_codecs/sdp_video_format.h"
 #include "rtc_base/system/rtc_export.h"
 
@@ -89,6 +90,14 @@
 RTC_EXPORT absl::optional<H265Level> StringToH265Level(
     const std::string& level);
 
+// Given that a decoder supports up to a give frame size(in pixels) at up to a
+// given number of frames per second, return the highest H.265 level where it
+// can guranatee that it will be able to support all valid encoded streams that
+// are within that level.
+RTC_EXPORT absl::optional<H265Level> GetSupportedH265Level(
+    const Resolution& resolution,
+    float max_fps);
+
 // Parses an SDP key-value map of format parameters to retrive an H265
 // profile/tier/level. Returns an H265ProfileTierlevel by setting its
 // members. profile defaults to `kProfileMain` if no profile-id is specified.
diff --git a/api/video_codecs/test/h265_profile_tier_level_unittest.cc b/api/video_codecs/test/h265_profile_tier_level_unittest.cc
index 85c0f09..aacfee9 100644
--- a/api/video_codecs/test/h265_profile_tier_level_unittest.cc
+++ b/api/video_codecs/test/h265_profile_tier_level_unittest.cc
@@ -245,4 +245,53 @@
   EXPECT_FALSE(H265IsSameProfileTierLevel(params1, params2));
 }
 
+TEST(H265ProfileTierLevel, TestGetSupportedH265Level) {
+  // Test with 720p at 30fps
+  Resolution r{.width = 1280, .height = 720};
+  EXPECT_EQ(GetSupportedH265Level(r, 30).value_or(H265Level::kLevel1),
+            H265Level::kLevel3);
+
+  // Test with QCIF at 15fps
+  r.width = 176;
+  r.height = 144;
+  EXPECT_EQ(GetSupportedH265Level(r, 15).value_or(H265Level::kLevel2),
+            H265Level::kLevel1);
+
+  // Test with 1080p at 30fps
+  r.width = 1920;
+  r.height = 1080;
+  EXPECT_EQ(GetSupportedH265Level(r, 30).value_or(H265Level::kLevel1),
+            H265Level::kLevel3_1);
+
+  // Test with 1080p at 60fps
+  EXPECT_EQ(GetSupportedH265Level(r, 60).value_or(H265Level::kLevel1),
+            H265Level::kLevel3_1);
+
+  // Test with 4K at 30fps
+  r.width = 3840;
+  r.height = 2160;
+  EXPECT_EQ(GetSupportedH265Level(r, 30).value_or(H265Level::kLevel1),
+            H265Level::kLevel4_1);
+
+  // Test with 4K at 60fps
+  EXPECT_EQ(GetSupportedH265Level(r, 60).value_or(H265Level::kLevel1),
+            H265Level::kLevel4_1);
+
+  // Test with 8K at 30fps
+  r.width = 8192;
+  r.height = 4320;
+  EXPECT_EQ(GetSupportedH265Level(r, 30).value_or(H265Level::kLevel1),
+            H265Level::kLevel6);
+
+  // Test with 64x64 at 30fps
+  r.width = 64;
+  r.height = 64;
+  EXPECT_EQ(GetSupportedH265Level(r, 30), absl::nullopt);
+
+  // Test with extremly large width or height at 15fps
+  r.width = 16928;
+  r.height = 64;
+  EXPECT_EQ(GetSupportedH265Level(r, 15), absl::nullopt);
+}
+
 }  // namespace webrtc