Bug Fix: iOS H264 Encoder Crash Issue

When using H264 encoder with profile level 3.1, the encoder may crash.
The reason is that we set the expected frame rate using kVTCompressionPropertyKey_ExpectedFrameRate
to the VideoToolBox. However, by iOS implementation, if our setting violates the sample rate limit
[1], the encoder will crash.

This CL fixes the bug by capping the expected frame rate with max allowed frame rate computed from the sample rate limit.

Change-Id: I090d7be8c20713c6a5a4ec80ed243c8fa7b4aa14
Bug: webrtc:10172
Reviewed-on: https://webrtc-review.googlesource.com/c/116056
Commit-Queue: Qiang Chen <qiangchen@chromium.org>
Reviewed-by: Kári Helgason <kthelgason@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#26254}
diff --git a/sdk/objc/components/video_codec/RTCVideoEncoderH264.mm b/sdk/objc/components/video_codec/RTCVideoEncoderH264.mm
index 51fa51e..4028e7a 100644
--- a/sdk/objc/components/video_codec/RTCVideoEncoderH264.mm
+++ b/sdk/objc/components/video_codec/RTCVideoEncoderH264.mm
@@ -177,14 +177,11 @@
 // no specific VideoToolbox profile for the specified level, AutoLevel will be
 // returned. The user must initialize the encoder with a resolution and
 // framerate conforming to the selected H264 level regardless.
-CFStringRef ExtractProfile(webrtc::SdpVideoFormat videoFormat) {
-  const absl::optional<webrtc::H264::ProfileLevelId> profile_level_id =
-      webrtc::H264::ParseSdpProfileLevelId(videoFormat.parameters);
-  RTC_DCHECK(profile_level_id);
-  switch (profile_level_id->profile) {
+CFStringRef ExtractProfile(const webrtc::H264::ProfileLevelId &profile_level_id) {
+  switch (profile_level_id.profile) {
     case webrtc::H264::kProfileConstrainedBaseline:
     case webrtc::H264::kProfileBaseline:
-      switch (profile_level_id->level) {
+      switch (profile_level_id.level) {
         case webrtc::H264::kLevel3:
           return kVTProfileLevel_H264_Baseline_3_0;
         case webrtc::H264::kLevel3_1:
@@ -215,7 +212,7 @@
       }
 
     case webrtc::H264::kProfileMain:
-      switch (profile_level_id->level) {
+      switch (profile_level_id.level) {
         case webrtc::H264::kLevel3:
           return kVTProfileLevel_H264_Main_3_0;
         case webrtc::H264::kLevel3_1:
@@ -247,7 +244,7 @@
 
     case webrtc::H264::kProfileConstrainedHigh:
     case webrtc::H264::kProfileHigh:
-      switch (profile_level_id->level) {
+      switch (profile_level_id.level) {
         case webrtc::H264::kLevel3:
           return kVTProfileLevel_H264_High_3_0;
         case webrtc::H264::kLevel3_1:
@@ -278,16 +275,53 @@
       }
   }
 }
+
+// The function returns the max allowed sample rate (pixels per second) that
+// can be processed by given encoder with |profile_level_id|.
+// See https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-H.264-201610-S!!PDF-E&type=items
+// for details.
+NSUInteger GetMaxSampleRate(const webrtc::H264::ProfileLevelId &profile_level_id) {
+  switch (profile_level_id.level) {
+    case webrtc::H264::kLevel3:
+      return 10368000;
+    case webrtc::H264::kLevel3_1:
+      return 27648000;
+    case webrtc::H264::kLevel3_2:
+      return 55296000;
+    case webrtc::H264::kLevel4:
+    case webrtc::H264::kLevel4_1:
+      return 62914560;
+    case webrtc::H264::kLevel4_2:
+      return 133693440;
+    case webrtc::H264::kLevel5:
+      return 150994944;
+    case webrtc::H264::kLevel5_1:
+      return 251658240;
+    case webrtc::H264::kLevel5_2:
+      return 530841600;
+    case webrtc::H264::kLevel1:
+    case webrtc::H264::kLevel1_b:
+    case webrtc::H264::kLevel1_1:
+    case webrtc::H264::kLevel1_2:
+    case webrtc::H264::kLevel1_3:
+    case webrtc::H264::kLevel2:
+    case webrtc::H264::kLevel2_1:
+    case webrtc::H264::kLevel2_2:
+      // Zero means auto rate setting.
+      return 0;
+  }
+}
 }  // namespace
 
 @implementation RTCVideoEncoderH264 {
   RTCVideoCodecInfo *_codecInfo;
   std::unique_ptr<webrtc::BitrateAdjuster> _bitrateAdjuster;
   uint32_t _targetBitrateBps;
-  uint32_t _encoderFrameRate;
   uint32_t _encoderBitrateBps;
+  uint32_t _encoderFrameRate;
+  uint32_t _maxAllowedFrameRate;
   RTCH264PacketizationMode _packetizationMode;
-  CFStringRef _profile;
+  absl::optional<webrtc::H264::ProfileLevelId> _profile_level_id;
   RTCVideoEncoderCallback _callback;
   int32_t _width;
   int32_t _height;
@@ -311,8 +345,10 @@
     _codecInfo = codecInfo;
     _bitrateAdjuster.reset(new webrtc::BitrateAdjuster(.5, .95));
     _packetizationMode = RTCH264PacketizationModeNonInterleaved;
-    _profile = ExtractProfile([codecInfo nativeSdpVideoFormat]);
-    RTC_LOG(LS_INFO) << "Using profile " << CFStringToString(_profile);
+    _profile_level_id =
+        webrtc::H264::ParseSdpProfileLevelId([codecInfo nativeSdpVideoFormat].parameters);
+    RTC_DCHECK(_profile_level_id);
+    RTC_LOG(LS_INFO) << "Using profile " << CFStringToString(ExtractProfile(*_profile_level_id));
     RTC_CHECK([codecInfo.name isEqualToString:kRTCVideoCodecH264Name]);
   }
   return self;
@@ -331,10 +367,20 @@
   _height = settings.height;
   _mode = settings.mode;
 
+  uint32_t aligned_width = (((_width + 15) >> 4) << 4);
+  uint32_t aligned_height = (((_height + 15) >> 4) << 4);
+  _maxAllowedFrameRate = static_cast<uint32_t>(GetMaxSampleRate(*_profile_level_id) /
+                                               (aligned_width * aligned_height));
+
   // We can only set average bitrate on the HW encoder.
   _targetBitrateBps = settings.startBitrate * 1000;  // startBitrate is in kbps.
   _bitrateAdjuster->SetTargetBitrateBps(_targetBitrateBps);
-  _encoderFrameRate = settings.maxFramerate;
+  _encoderFrameRate = MIN(settings.maxFramerate, _maxAllowedFrameRate);
+  if (settings.maxFramerate > _maxAllowedFrameRate && _maxAllowedFrameRate > 0) {
+    RTC_LOG(LS_WARNING) << "Initial encoder frame rate setting " << settings.maxFramerate
+                        << " is larger than the "
+                        << "maximal allowed frame rate " << _maxAllowedFrameRate << ".";
+  }
 
   // TODO(tkchin): Try setting payload size via
   // kVTCompressionPropertyKey_MaxH264SliceBytes.
@@ -470,6 +516,11 @@
 - (int)setBitrate:(uint32_t)bitrateKbit framerate:(uint32_t)framerate {
   _targetBitrateBps = 1000 * bitrateKbit;
   _bitrateAdjuster->SetTargetBitrateBps(_targetBitrateBps);
+  if (framerate > _maxAllowedFrameRate && _maxAllowedFrameRate > 0) {
+    RTC_LOG(LS_WARNING) << "Encoder frame rate setting " << framerate << " is larger than the "
+                        << "maximal allowed frame rate " << _maxAllowedFrameRate << ".";
+  }
+  framerate = MIN(framerate, _maxAllowedFrameRate);
   [self setBitrateBps:_bitrateAdjuster->GetAdjustedBitrateBps() frameRate:framerate];
   return WEBRTC_VIDEO_CODEC_OK;
 }
@@ -616,7 +667,9 @@
 - (void)configureCompressionSession {
   RTC_DCHECK(_compressionSession);
   SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_RealTime, true);
-  SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_ProfileLevel, _profile);
+  SetVTSessionProperty(_compressionSession,
+                       kVTCompressionPropertyKey_ProfileLevel,
+                       ExtractProfile(*_profile_level_id));
   SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_AllowFrameReordering, false);
   [self setEncoderBitrateBps:_targetBitrateBps frameRate:_encoderFrameRate];
   // TODO(tkchin): Look at entropy mode and colorspace matrices.
@@ -655,8 +708,12 @@
 - (void)setEncoderBitrateBps:(uint32_t)bitrateBps frameRate:(uint32_t)frameRate {
   if (_compressionSession) {
     SetVTSessionProperty(_compressionSession, kVTCompressionPropertyKey_AverageBitRate, bitrateBps);
-    SetVTSessionProperty(
-        _compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, frameRate);
+
+    // With zero |_maxAllowedFrameRate|, we fall back to automatic frame rate detection.
+    if (_maxAllowedFrameRate > 0) {
+      SetVTSessionProperty(
+          _compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, frameRate);
+    }
 
     // TODO(tkchin): Add a helper method to set array value.
     int64_t dataLimitBytesPerSecondValue =