Bug 1275360 - add sdp handling for video FEC (red/ulpfec), r=drno draft
authorMichael Froman <mfroman@mozilla.com>
Thu, 30 Jun 2016 00:40:40 -0500
changeset 392975 a9277bd7aafb49949dec9ea22ced3d062c9066db
parent 392445 251fccc1f62bf0eac569ef4f6717fea61ebadb27
child 526444 d78ac326637630423307ed5746f4806aeb418137
push id24160
push userbmo:mfroman@nostrum.com
push dateTue, 26 Jul 2016 16:35:54 +0000
reviewersdrno
bugs1275360
milestone50.0a1
Bug 1275360 - add sdp handling for video FEC (red/ulpfec), r=drno MozReview-Commit-ID: BIt7SkSEquj
media/webrtc/signaling/src/jsep/JsepCodecDescription.h
media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
media/webrtc/signaling/src/jsep/JsepTrack.cpp
media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.cpp
media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
media/webrtc/signaling/src/sdp/SdpAttribute.cpp
media/webrtc/signaling/src/sdp/SdpAttribute.h
media/webrtc/signaling/src/sdp/SipccSdpAttributeList.cpp
media/webrtc/signaling/src/sdp/sipcc/ccsdp.h
media/webrtc/signaling/src/sdp/sipcc/sdp.h
media/webrtc/signaling/src/sdp/sipcc/sdp_access.c
media/webrtc/signaling/src/sdp/sipcc/sdp_attr.c
media/webrtc/signaling/test/jsep_session_unittest.cpp
media/webrtc/signaling/test/jsep_track_unittest.cpp
media/webrtc/signaling/test/sdp_unittests.cpp
modules/libpref/init/all.js
--- a/media/webrtc/signaling/src/jsep/JsepCodecDescription.h
+++ b/media/webrtc/signaling/src/jsep/JsepCodecDescription.h
@@ -45,16 +45,17 @@ class JsepCodecDescription {
   GetPtAsInt(uint16_t* ptOutparam) const
   {
     return SdpHelper::GetPtAsInt(mDefaultPt, ptOutparam);
   }
 
   virtual bool
   Matches(const std::string& fmt, const SdpMediaSection& remoteMsection) const
   {
+    // note: fmt here is remote fmt (to go with remoteMsection)
     if (mType != remoteMsection.GetMediaType()) {
       return false;
     }
 
     const SdpRtpmapAttributeList::Rtpmap* entry(remoteMsection.FindRtpmap(fmt));
 
     if (entry) {
       if (!nsCRT::strcasecmp(mName.c_str(), entry->name.c_str())
@@ -209,16 +210,17 @@ class JsepVideoCodecDescription : public
   JsepVideoCodecDescription(const std::string& defaultPt,
                             const std::string& name,
                             uint32_t clock,
                             bool enabled = true)
       : JsepCodecDescription(mozilla::SdpMediaSection::kVideo, defaultPt, name,
                              clock, 0, enabled),
         mTmmbrEnabled(false),
         mRembEnabled(false),
+        mFECEnabled(false),
         mPacketizationMode(0)
   {
     // Add supported rtcp-fb types
     mNackFbTypes.push_back("");
     mNackFbTypes.push_back(SdpRtcpFbAttributeList::pli);
     mCcmFbTypes.push_back(SdpRtcpFbAttributeList::fir);
   }
 
@@ -237,16 +239,26 @@ class JsepVideoCodecDescription : public
     // EnableRemb can be called multiple times due to multiple calls to
     // PeerConnectionImpl::ConfigureJsepSessionCodecs
     if (!mRembEnabled) {
       mRembEnabled = true;
       mOtherFbTypes.push_back({ "", SdpRtcpFbAttributeList::kRemb, "", ""});
     }
   }
 
+  virtual void
+  EnableFec() {
+    // Enabling FEC for video works a little differently than enabling
+    // REMB or TMMBR.  Support for FEC is indicated by the presence of
+    // particular codes (red and ulpfec) instead of using rtcpfb
+    // attributes on a given codec.  There is no rtcpfb to push for FEC
+    // as can be seen above when REMB or TMMBR are enabled.
+    mFECEnabled = true;
+  }
+
   void
   AddParametersToMSection(SdpMediaSection& msection) const override
   {
     AddFmtpsToMSection(msection);
     AddRtcpFbsToMSection(msection);
   }
 
   void
@@ -277,16 +289,21 @@ class JsepVideoCodecDescription : public
       }
 
       // Parameters that apply to both the send and recv directions
       h264Params.packetization_mode = mPacketizationMode;
       // Hard-coded, may need to change someday?
       h264Params.level_asymmetry_allowed = true;
 
       msection.SetFmtp(SdpFmtpAttributeList::Fmtp(mDefaultPt, h264Params));
+    } else if (mName == "red") {
+      SdpFmtpAttributeList::RedParameters redParams(
+          GetRedParameters(mDefaultPt, msection));
+      redParams.encodings = mRedundantEncodings;
+      msection.SetFmtp(SdpFmtpAttributeList::Fmtp(mDefaultPt, redParams));
     } else if (mName == "VP8" || mName == "VP9") {
       if (mDirection == sdp::kRecv) {
         // VP8 and VP9 share the same SDP parameters thus far
         SdpFmtpAttributeList::VP8Parameters vp8Params(
             GetVP8Parameters(mDefaultPt, msection));
 
         vp8Params.max_fs = mConstraints.maxFs;
         vp8Params.max_fr = mConstraints.maxFps;
@@ -333,16 +350,31 @@ class JsepVideoCodecDescription : public
     if (params && params->codec_type == SdpRtpmapAttributeList::kH264) {
       result =
         static_cast<const SdpFmtpAttributeList::H264Parameters&>(*params);
     }
 
     return result;
   }
 
+  SdpFmtpAttributeList::RedParameters
+  GetRedParameters(const std::string& pt,
+                   const SdpMediaSection& msection) const
+  {
+    SdpFmtpAttributeList::RedParameters result;
+    auto* params = msection.FindFmtp(pt);
+
+    if (params && params->codec_type == SdpRtpmapAttributeList::kRed) {
+      result =
+        static_cast<const SdpFmtpAttributeList::RedParameters&>(*params);
+    }
+
+    return result;
+  }
+
   SdpFmtpAttributeList::VP8Parameters
   GetVP8Parameters(const std::string& pt,
                    const SdpMediaSection& msection) const
   {
     SdpRtpmapAttributeList::CodecType expectedType(
         mName == "VP8" ?
         SdpRtpmapAttributeList::kVP8 :
         SdpRtpmapAttributeList::kVP9);
@@ -422,17 +454,20 @@ class JsepVideoCodecDescription : public
         // Only do this if we didn't symmetrically negotiate above
         if (h264Params.level_asymmetry_allowed) {
           SetSaneH264Level(GetSaneH264Level(h264Params.profile_level_id),
                            &mProfileLevelId);
         }
       } else {
         // TODO(bug 1143709): max-recv-level support
       }
-
+    } else if (mName == "red") {
+      SdpFmtpAttributeList::RedParameters redParams(
+          GetRedParameters(mDefaultPt, remoteMsection));
+      mRedundantEncodings = redParams.encodings;
     } else if (mName == "VP8" || mName == "VP9") {
       if (mDirection == sdp::kSend) {
         SdpFmtpAttributeList::VP8Parameters vp8Params(
             GetVP8Parameters(mDefaultPt, remoteMsection));
 
         mConstraints.maxFs = vp8Params.max_fs;
         mConstraints.maxFps = vp8Params.max_fr;
       }
@@ -644,24 +679,44 @@ class JsepVideoCodecDescription : public
     for (const auto& fb : mOtherFbTypes) {
       if (fb.type == SdpRtcpFbAttributeList::kRemb) {
         return true;
       }
     }
     return false;
   }
 
+  virtual void
+  UpdateRedundantEncodings(std::vector<JsepCodecDescription*> codecs)
+  {
+    for (const auto codec : codecs) {
+      if (codec->mType == SdpMediaSection::kVideo &&
+          codec->mEnabled &&
+          codec->mName != "red") {
+        uint8_t pt = (uint8_t)strtoul(codec->mDefaultPt.c_str(), nullptr, 10);
+        // returns 0 if failed to convert, and since zero could
+        // be valid, check the defaultPt for 0
+        if (pt == 0 && codec->mDefaultPt != "0") {
+          continue;
+        }
+        mRedundantEncodings.push_back(pt);
+      }
+    }
+  }
+
   JSEP_CODEC_CLONE(JsepVideoCodecDescription)
 
   std::vector<std::string> mAckFbTypes;
   std::vector<std::string> mNackFbTypes;
   std::vector<std::string> mCcmFbTypes;
   std::vector<SdpRtcpFbAttributeList::Feedback> mOtherFbTypes;
   bool mTmmbrEnabled;
   bool mRembEnabled;
+  bool mFECEnabled;
+  std::vector<uint8_t> mRedundantEncodings;
 
   // H264-specific stuff
   uint32_t mProfileLevelId;
   uint32_t mPacketizationMode;
   std::string mSpropParameterSets;
 };
 
 class JsepApplicationCodecDescription : public JsepCodecDescription {
--- a/media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
+++ b/media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
@@ -2177,21 +2177,39 @@ JsepSessionImpl::SetupDefaultCodecs()
       "H264",
       90000
       );
   h264_0->mPacketizationMode = 0;
   // Defaults for mandatory params
   h264_0->mProfileLevelId = 0x42E00D;
   mSupportedCodecs.values.push_back(h264_0);
 
+  JsepVideoCodecDescription* red = new JsepVideoCodecDescription(
+      "122", // payload type
+      "red", // codec name
+      90000  // clock rate (match other video codecs)
+      );
+  mSupportedCodecs.values.push_back(red);
+
+  JsepVideoCodecDescription* ulpfec = new JsepVideoCodecDescription(
+      "123",    // payload type
+      "ulpfec", // codec name
+      90000     // clock rate (match other video codecs)
+      );
+  mSupportedCodecs.values.push_back(ulpfec);
+
   mSupportedCodecs.values.push_back(new JsepApplicationCodecDescription(
       "5000",
       "webrtc-datachannel",
       WEBRTC_DATACHANNEL_STREAMS_DEFAULT
       ));
+
+  // Update the redundant encodings for the RED codec with the supported
+  // codecs.  Note: only uses the video codecs.
+  red->UpdateRedundantEncodings(mSupportedCodecs.values);
 }
 
 void
 JsepSessionImpl::SetupDefaultRtpExtensions()
 {
   AddAudioRtpExtension("urn:ietf:params:rtp-hdrext:ssrc-audio-level");
 }
 
--- a/media/webrtc/signaling/src/jsep/JsepTrack.cpp
+++ b/media/webrtc/signaling/src/jsep/JsepTrack.cpp
@@ -339,23 +339,68 @@ JsepTrack::NegotiateCodecs(
         if (formatChanges) {
           (*formatChanges)[originalFormat] = codec->mDefaultPt;
         }
         break;
       }
     }
   }
 
+  // Find the (potential) red codec and ulpfec codec
+  JsepVideoCodecDescription* red = nullptr;
+  JsepVideoCodecDescription* ulpfec = nullptr;
+  for (auto codec : *codecs) {
+    if (codec->mName == "red") {
+      red = static_cast<JsepVideoCodecDescription*>(codec);
+      break;
+    }
+    if (codec->mName == "ulpfec") {
+      ulpfec = static_cast<JsepVideoCodecDescription*>(codec);
+      break;
+    }
+  }
+  // if we have a red codec remove redundant encodings that don't exist
+  if (red) {
+    // Since we could have an externally specified redundant endcodings
+    // list, we shouldn't simply rebuild the redundant encodings list
+    // based on the current list of codecs.
+    std::vector<uint8_t> unnegotiatedEncodings;
+    std::swap(unnegotiatedEncodings, red->mRedundantEncodings);
+    for (auto redundantPt : unnegotiatedEncodings) {
+      std::string pt = std::to_string(redundantPt);
+      for (auto codec : *codecs) {
+        if (pt == codec->mDefaultPt) {
+          red->mRedundantEncodings.push_back(redundantPt);
+          break;
+        }
+      }
+    }
+  }
+  // Video FEC is indicated by the existence of the red and ulpfec
+  // codecs and not an attribute on the particular video codec (like in
+  // a rtcpfb attr). If we see both red and ulpfec codecs, we enable FEC
+  // on all the other codecs.
+  if (red && ulpfec) {
+    for (auto codec : *codecs) {
+      if (codec->mName != "red" && codec->mName != "ulpfec") {
+        JsepVideoCodecDescription* videoCodec =
+            static_cast<JsepVideoCodecDescription*>(codec);
+        videoCodec->EnableFec();
+      }
+    }
+  }
+
   // Make sure strongly preferred codecs are up front, overriding the remote
   // side's preference.
   std::stable_sort(codecs->begin(), codecs->end(), CompareCodec);
 
   // TODO(bug 814227): Remove this once we're ready to put multiple codecs in an
-  // answer
-  if (!codecs->empty()) {
+  // answer.  For now, remove all but the first codec unless the red codec
+  // exists, and then we include the others per RFC 5109, section 14.2.
+  if (!codecs->empty() && !red) {
     for (size_t i = 1; i < codecs->size(); ++i) {
       delete (*codecs)[i];
       (*codecs)[i] = nullptr;
     }
     codecs->resize(1);
   }
 }
 
--- a/media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.cpp
+++ b/media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.cpp
@@ -144,16 +144,17 @@ JsepCodecDescToCodecConfig(const JsepCod
   VideoCodecConfig* configRaw;
   configRaw = new VideoCodecConfig(
       pt, desc.mName, desc.mConstraints, h264Config.get());
 
   configRaw->mAckFbTypes = desc.mAckFbTypes;
   configRaw->mNackFbTypes = desc.mNackFbTypes;
   configRaw->mCcmFbTypes = desc.mCcmFbTypes;
   configRaw->mRembFbSet = desc.RtcpFbRembIsSet();
+  configRaw->mFECFbSet = desc.mFECEnabled;
 
   *aConfig = configRaw;
   return NS_OK;
 }
 
 static nsresult
 NegotiatedDetailsToVideoCodecConfigs(const JsepTrackNegotiatedDetails& aDetails,
                                      PtrVector<VideoCodecConfig>* aConfigs)
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
@@ -881,17 +881,18 @@ class ConfigureCodec {
       mVP9Enabled(false),
       mH264Level(13), // minimum suggested for WebRTC spec
       mH264MaxBr(0), // Unlimited
       mH264MaxMbps(0), // Unlimited
       mVP8MaxFs(0),
       mVP8MaxFr(0),
       mUseTmmbr(false),
       mUseRemb(false),
-      mUseAudioFec(false)
+      mUseAudioFec(false),
+      mRedUlpfecEnabled(false)
     {
 #ifdef MOZ_WEBRTC_OMX
       // Check to see if what HW codecs are available (not in use) at this moment.
       // Note that streaming video decode can reserve a decoder
 
       // XXX See bug 1018791 Implement W3 codec reservation policy
       // Note that currently, OMXCodecReservation needs to be held by an sp<> because it puts
       // 'this' into an sp<EventListener> to talk to the resource reservation code
@@ -952,16 +953,19 @@ class ConfigureCodec {
 
       // TMMBR is enabled from a pref in about:config
       branch->GetBoolPref("media.navigator.video.use_tmmbr", &mUseTmmbr);
 
       // REMB is enabled by default, but can be disabled from about:config
       branch->GetBoolPref("media.navigator.video.use_remb", &mUseRemb);
 
       branch->GetBoolPref("media.navigator.audio.use_fec", &mUseAudioFec);
+
+      branch->GetBoolPref("media.navigator.video.red_ulpfec_enabled",
+                          &mRedUlpfecEnabled);
     }
 
     void operator()(JsepCodecDescription* codec) const
     {
       switch (codec->mType) {
         case SdpMediaSection::kAudio:
           {
             JsepAudioCodecDescription& audioCodec =
@@ -992,16 +996,20 @@ class ConfigureCodec {
                 // We're assuming packetization mode 0 is unsupported by
                 // hardware.
                 videoCodec.mEnabled = false;
               }
 
               if (mHardwareH264Supported) {
                 videoCodec.mStronglyPreferred = true;
               }
+            } else if (videoCodec.mName == "red") {
+              videoCodec.mEnabled = mRedUlpfecEnabled;
+            } else if (videoCodec.mName == "ulpfec") {
+              videoCodec.mEnabled = mRedUlpfecEnabled;
             } else if (videoCodec.mName == "VP8" || videoCodec.mName == "VP9") {
               if (videoCodec.mName == "VP9" && !mVP9Enabled) {
                 videoCodec.mEnabled = false;
                 break;
               }
               videoCodec.mConstraints.maxFs = mVP8MaxFs;
               videoCodec.mConstraints.maxFps = mVP8MaxFr;
             }
@@ -1030,16 +1038,51 @@ class ConfigureCodec {
     int32_t mH264Level;
     int32_t mH264MaxBr;
     int32_t mH264MaxMbps;
     int32_t mVP8MaxFs;
     int32_t mVP8MaxFr;
     bool mUseTmmbr;
     bool mUseRemb;
     bool mUseAudioFec;
+    bool mRedUlpfecEnabled;
+};
+
+class ConfigureRedCodec {
+  public:
+    explicit ConfigureRedCodec(nsCOMPtr<nsIPrefBranch>& branch,
+                               std::vector<uint8_t>* redundantEncodings) :
+      mRedundantEncodings(redundantEncodings)
+    {
+      // if we wanted to override or modify which encodings are considered
+      // for redundant encodings, we'd probably want to handle it here by
+      // checking prefs modifying the operator() code below
+    }
+
+    void operator()(JsepCodecDescription* codec) const
+    {
+      if (codec->mType == SdpMediaSection::kVideo &&
+          codec->mEnabled == false) {
+        uint8_t pt = (uint8_t)strtoul(codec->mDefaultPt.c_str(), nullptr, 10);
+        // don't search for the codec payload type unless we have a valid
+        // conversion (non-zero)
+        if (pt != 0) {
+          std::vector<uint8_t>::iterator it =
+            std::find(mRedundantEncodings->begin(),
+                      mRedundantEncodings->end(),
+                      pt);
+          if (it != mRedundantEncodings->end()) {
+            mRedundantEncodings->erase(it);
+          }
+        }
+      }
+    }
+
+  private:
+    std::vector<uint8_t>* mRedundantEncodings;
 };
 
 nsresult
 PeerConnectionImpl::ConfigureJsepSessionCodecs() {
   nsresult res;
   nsCOMPtr<nsIPrefService> prefs =
     do_GetService("@mozilla.org/preferences-service;1", &res);
 
@@ -1054,16 +1097,33 @@ PeerConnectionImpl::ConfigureJsepSession
   if (!branch) {
     CSFLogError(logTag, "%s: Couldn't get prefs branch", __FUNCTION__);
     return NS_ERROR_FAILURE;
   }
 
   ConfigureCodec configurer(branch);
   mJsepSession->ForEachCodec(configurer);
 
+  // first find the red codec description
+  std::vector<JsepCodecDescription*>& codecs = mJsepSession->Codecs();
+  JsepVideoCodecDescription* redCodec = nullptr;
+  for (auto codec : codecs) {
+    // we only really care about finding the RED codec if it is
+    // enabled
+    if (codec->mName == "red" && codec->mEnabled) {
+      redCodec = static_cast<JsepVideoCodecDescription*>(codec);
+      break;
+    }
+  }
+  // if red codec was found, configure it for the other enabled codecs
+  if (redCodec) {
+    ConfigureRedCodec configureRed(branch, &(redCodec->mRedundantEncodings));
+    mJsepSession->ForEachCodec(configureRed);
+  }
+
   // We use this to sort the list of codecs once everything is configured
   CompareCodecPriority comparator;
 
   // Sort by priority
   int32_t preferredCodec = 0;
   branch->GetIntPref("media.navigator.video.preferred_codec",
                      &preferredCodec);
 
--- a/media/webrtc/signaling/src/sdp/SdpAttribute.cpp
+++ b/media/webrtc/signaling/src/sdp/SdpAttribute.cpp
@@ -1097,16 +1097,18 @@ ShouldSerializeChannels(SdpRtpmapAttribu
       return true;
     case SdpRtpmapAttributeList::kPCMU:
     case SdpRtpmapAttributeList::kPCMA:
     case SdpRtpmapAttributeList::kVP8:
     case SdpRtpmapAttributeList::kVP9:
     case SdpRtpmapAttributeList::kiLBC:
     case SdpRtpmapAttributeList::kiSAC:
     case SdpRtpmapAttributeList::kH264:
+    case SdpRtpmapAttributeList::kRed:
+    case SdpRtpmapAttributeList::kUlpfec:
       return false;
     case SdpRtpmapAttributeList::kOtherCodec:
       return true;
   }
   MOZ_CRASH();
 }
 
 void
--- a/media/webrtc/signaling/src/sdp/SdpAttribute.h
+++ b/media/webrtc/signaling/src/sdp/SdpAttribute.h
@@ -1018,16 +1018,18 @@ public:
     kG722,
     kPCMU,
     kPCMA,
     kVP8,
     kVP9,
     kiLBC,
     kiSAC,
     kH264,
+    kRed,
+    kUlpfec,
     kOtherCodec
   };
 
   struct Rtpmap {
     std::string pt;
     CodecType codec;
     std::string name;
     uint32_t clock;
@@ -1097,16 +1099,22 @@ inline std::ostream& operator<<(std::ost
       os << "iLBC";
       break;
     case SdpRtpmapAttributeList::kiSAC:
       os << "iSAC";
       break;
     case SdpRtpmapAttributeList::kH264:
       os << "H264";
       break;
+    case SdpRtpmapAttributeList::kRed:
+      os << "red";
+      break;
+    case SdpRtpmapAttributeList::kUlpfec:
+      os << "ulpfec";
+      break;
     default:
       MOZ_ASSERT(false);
       os << "?";
   }
   return os;
 }
 
 ///////////////////////////////////////////////////////////////////////////
@@ -1130,16 +1138,42 @@ public:
 
     virtual ~Parameters() {}
     virtual Parameters* Clone() const = 0;
     virtual void Serialize(std::ostream& os) const = 0;
 
     SdpRtpmapAttributeList::CodecType codec_type;
   };
 
+  class RedParameters : public Parameters
+  {
+  public:
+    RedParameters()
+        : Parameters(SdpRtpmapAttributeList::kRed)
+    {
+    }
+
+    virtual Parameters*
+    Clone() const override
+    {
+      return new RedParameters(*this);
+    }
+
+    virtual void
+    Serialize(std::ostream& os) const override
+    {
+      for(size_t i = 0; i < encodings.size(); ++i) {
+        os << (i != 0 ? "/" : "")
+           << std::to_string(encodings[i]);
+      }
+    }
+
+    std::vector<uint8_t> encodings;
+  };
+
   class H264Parameters : public Parameters
   {
   public:
     static const uint32_t kDefaultProfileLevelId = 0x420010;
 
     H264Parameters()
         : Parameters(SdpRtpmapAttributeList::kH264),
           packetization_mode(0),
--- a/media/webrtc/signaling/src/sdp/SipccSdpAttributeList.cpp
+++ b/media/webrtc/signaling/src/sdp/SipccSdpAttributeList.cpp
@@ -354,16 +354,20 @@ SipccSdpAttributeList::GetCodecType(rtp_
     case RTP_H264_P1:
       return SdpRtpmapAttributeList::kH264;
     case RTP_OPUS:
       return SdpRtpmapAttributeList::kOpus;
     case RTP_VP8:
       return SdpRtpmapAttributeList::kVP8;
     case RTP_VP9:
       return SdpRtpmapAttributeList::kVP9;
+    case RTP_RED:
+      return SdpRtpmapAttributeList::kRed;
+    case RTP_ULPFEC:
+      return SdpRtpmapAttributeList::kUlpfec;
     case RTP_NONE:
     // Happens when sipcc doesn't know how to translate to the enum
     case RTP_CELP:
     case RTP_G726:
     case RTP_GSM:
     case RTP_G723:
     case RTP_DVI4:
     case RTP_DVI4_II:
@@ -716,16 +720,27 @@ SipccSdpAttributeList::LoadFmtp(sdp_t* s
             new SdpFmtpAttributeList::VP8Parameters(
               SdpRtpmapAttributeList::kVP8));
 
         vp8Parameters->max_fs = fmtp->max_fs;
         vp8Parameters->max_fr = fmtp->max_fr;
 
         parameters.reset(vp8Parameters);
       } break;
+      case RTP_RED: {
+        SdpFmtpAttributeList::RedParameters* redParameters(
+            new SdpFmtpAttributeList::RedParameters);
+        for (int i = 0;
+             i < SDP_FMTP_MAX_REDUNDANT_ENCODINGS && fmtp->redundant_encodings[i];
+             ++i) {
+          redParameters->encodings.push_back(fmtp->redundant_encodings[i]);
+        }
+
+        parameters.reset(redParameters);
+      } break;
       case RTP_OPUS: {
         SdpFmtpAttributeList::OpusParameters* opusParameters(
             new SdpFmtpAttributeList::OpusParameters);
         opusParameters->maxplaybackrate = fmtp->maxplaybackrate;
         opusParameters->stereo = fmtp->stereo;
         opusParameters->useInBandFec = fmtp->useinbandfec;
         parameters.reset(opusParameters);
       } break;
--- a/media/webrtc/signaling/src/sdp/sipcc/ccsdp.h
+++ b/media/webrtc/signaling/src/sdp/sipcc/ccsdp.h
@@ -35,16 +35,18 @@ typedef enum rtp_ptype_
     RTP_H264_P1      = 126,
     RTP_AVT          = 101,
     RTP_L16          = 102,
     RTP_H263         = 103,
     RTP_ILBC         = 116, /* used only to make an offer */
     RTP_OPUS         = 109,
     RTP_VP8          = 120,
     RTP_VP9          = 121,
+    RTP_RED          = 122,
+    RTP_ULPFEC       = 123,
     RTP_I420         = 124,
     RTP_ISAC         = 124
 } rtp_ptype;
 
 typedef struct {
     const char *name;
     int         value;
 } ccsdp_key_table_entry_t;
--- a/media/webrtc/signaling/src/sdp/sipcc/sdp.h
+++ b/media/webrtc/signaling/src/sdp/sipcc/sdp.h
@@ -44,16 +44,19 @@
 #define SDP_SRTP_MAX_MKI_SIZE_BYTES   4
 
 /* Max number of characters for Lifetime */
 #define SDP_SRTP_MAX_LIFETIME_BYTES 16
 
 #define SDP_SDESCRIPTIONS_KEY_SIZE_UNKNOWN      0
 #define SDP_SRTP_CRYPTO_SELECTION_FLAGS_UNKNOWN 0
 
+/* Max number of fmtp redundant encodings */
+#define SDP_FMTP_MAX_REDUNDANT_ENCODINGS 128
+
 /*
  * SRTP_CONTEXT_SET_*
  *  Set a SRTP Context field flag
  */
 #define SDP_SRTP_ENCRYPT_MASK           0x00000001
 #define SDP_SRTP_AUTHENTICATE_MASK      0x00000002
 #define SDP_SRTCP_ENCRYPT_MASK          0x00000004
 #define SDP_SRTCP_SSRC_MASK             0x20000000
@@ -705,16 +708,19 @@ typedef struct sdp_fmtp {
     tinybool                  annex_i;
     tinybool                  annex_j;
     tinybool                  annex_t;
 
     /* H.263 codec requires annex K,N and P to have values */
     uint16_t                       annex_k_val;
     uint16_t                       annex_n_val;
 
+    /* RFC 5109 Section 4.2 for specifying redundant encodings */
+    uint8_t              redundant_encodings[SDP_FMTP_MAX_REDUNDANT_ENCODINGS];
+
     /* Annex P can take one or more values in the range 1-4 . e.g P=1,3 */
     uint16_t                       annex_p_val_picture_resize; /* 1 = four; 2 = sixteenth */
     uint16_t                       annex_p_val_warp; /* 3 = half; 4=sixteenth */
 
     uint8_t                        flag;
 
   /* END - All Video related FMTP parameters */
 
--- a/media/webrtc/signaling/src/sdp/sipcc/sdp_access.c
+++ b/media/webrtc/signaling/src/sdp/sipcc/sdp_access.c
@@ -24,16 +24,18 @@ static const char* logTag = "sdp_access"
 #define SIPSDP_ATTR_ENCNAME_ILBC      "iLBC"
 #define SIPSDP_ATTR_ENCNAME_H263v2    "H263-1998"
 #define SIPSDP_ATTR_ENCNAME_H264      "H264"
 #define SIPSDP_ATTR_ENCNAME_VP8       "VP8"
 #define SIPSDP_ATTR_ENCNAME_VP9       "VP9"
 #define SIPSDP_ATTR_ENCNAME_L16_256K  "L16"
 #define SIPSDP_ATTR_ENCNAME_ISAC      "ISAC"
 #define SIPSDP_ATTR_ENCNAME_OPUS      "opus"
+#define SIPSDP_ATTR_ENCNAME_RED       "red"
+#define SIPSDP_ATTR_ENCNAME_ULPFEC    "ulpfec"
 
 /* Function:    sdp_find_media_level
  * Description: Find and return a pointer to the specified media level,
  *              if it exists.
  *              Note: This is not an API for the application but an internal
  *              routine used by the SDP library.
  * Parameters:  sdp_p     The SDP handle returned by sdp_init_description.
  *              level       The media level to find.
@@ -1372,16 +1374,22 @@ rtp_ptype sdp_get_known_payload_type(sdp
           }
         }
         if (cpr_strcasecmp(encname, SIPSDP_ATTR_ENCNAME_VP8) == 0) {
           return (RTP_VP8);
         }
         if (cpr_strcasecmp(encname, SIPSDP_ATTR_ENCNAME_VP9) == 0) {
           return (RTP_VP9);
         }
+        if (cpr_strcasecmp(encname, SIPSDP_ATTR_ENCNAME_RED) == 0) {
+          return (RTP_RED);
+        }
+        if (cpr_strcasecmp(encname, SIPSDP_ATTR_ENCNAME_ULPFEC) == 0) {
+          return (RTP_ULPFEC);
+        }
       }
     }
   }
 
   return (RTP_NONE);
 }
 
 /* Function:    sdp_get_media_payload_type
--- a/media/webrtc/signaling/src/sdp/sipcc/sdp_attr.c
+++ b/media/webrtc/signaling/src/sdp/sipcc/sdp_attr.c
@@ -1504,33 +1504,33 @@ sdp_result_e sdp_parse_attr_fmtp (sdp_t 
                     sdp_attr_fmtp_no_value(sdp_p, "annex_p");
                     SDP_FREE(temp_ptr);
                     return SDP_INVALID_PARAMETER;
                 }
             }
             fmtp_p->annex_p_val_picture_resize = 0;
             fmtp_p->annex_p_val_warp = 0;
             tok = tmp;
-            tok++; temp=PL_strtok_r(tok, ",", &strtok_state);
+            tok++; temp = PL_strtok_r(tok, ",", &strtok_state);
             if (temp) {
                 iter=1;
                 while (temp != NULL) {
                     errno = 0;
                     strtoul_result = strtoul(temp, &strtoul_end, 10);
 
                     if (errno || temp == strtoul_end || strtoul_result > USHRT_MAX) {
                         break;
                     }
 
                     if (iter == 1)
                         fmtp_p->annex_p_val_picture_resize = (uint16_t) strtoul_result;
                     else if (iter == 2)
                         fmtp_p->annex_p_val_warp = (uint16_t) strtoul_result;
 
-                    temp=PL_strtok_r(NULL, ",", &strtok_state);
+                    temp = PL_strtok_r(NULL, ",", &strtok_state);
                     iter++;
                 }
             }
 
             fmtp_p->fmtp_format = SDP_FMTP_CODEC_INFO;
             codec_info_found = TRUE;
         } else if (cpr_strncasecmp(tmp,sdp_fmtp_codec_param[42].name,
                                sdp_fmtp_codec_param[42].strlen) == 0) {
@@ -1775,16 +1775,37 @@ sdp_result_e sdp_parse_attr_fmtp (sdp_t 
                     }
                     if (strchr(temp, 'T') !=NULL) {
                         attr_p->attr.fmtp.annex_t = TRUE;
                     }
                     temp=PL_strtok_r(NULL, ";", &strtok_state);
                 }
             } /* if (temp) */
             done = TRUE;
+        } else if (strchr(tmp, '/')) {
+            // XXX Note that because RFC 5109 so conveniently specified
+            // this fmtp with no param names, we hope that nothing else
+            // has a slash in the string because otherwise we won't know
+            // how to differentiate.
+            temp=PL_strtok_r(tmp, "/", &strtok_state);
+            if (temp) {
+                iter = 0;
+                while (temp != NULL) {
+                    errno = 0;
+                    strtoul_result = strtoul(temp, &strtoul_end, 10);
+
+                    if (errno ||
+                        temp == strtoul_end || strtoul_result > USHRT_MAX) {
+                        continue;
+                    }
+                    fmtp_p->redundant_encodings[iter++] =
+                        (uint8_t)strtoul_result;
+                    temp=PL_strtok_r(NULL, "/", &strtok_state);
+                }
+            } /* if (temp) */
         } else {
           // XXX Note that DTMF fmtp will fall into here:
           // a=fmtp:101 0-15 (or 0-15,NN,NN etc)
 
           // unknown parameter - eat chars until ';'
           CSFLogDebug(logTag, "%s Unknown fmtp type (%s) - ignoring", __FUNCTION__,
                       tmp);
           fmtp_ptr = sdp_getnextstrtok(fmtp_ptr, tmp, sizeof(tmp), "; \t",
--- a/media/webrtc/signaling/test/jsep_session_unittest.cpp
+++ b/media/webrtc/signaling/test/jsep_session_unittest.cpp
@@ -2737,45 +2737,53 @@ TEST_F(JsepSessionTest, ValidateOfferedC
   ASSERT_TRUE(!!outputSdp);
 
   ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
   auto& video_section = outputSdp->GetMediaSection(1);
   ASSERT_EQ(SdpMediaSection::kVideo, video_section.GetMediaType());
   auto& video_attrs = video_section.GetAttributeList();
   ASSERT_EQ(SdpDirectionAttribute::kSendrecv, video_attrs.GetDirection());
 
-  ASSERT_EQ(4U, video_section.GetFormats().size());
+  ASSERT_EQ(6U, video_section.GetFormats().size());
   ASSERT_EQ("121", video_section.GetFormats()[0]);
   ASSERT_EQ("120", video_section.GetFormats()[1]);
   ASSERT_EQ("126", video_section.GetFormats()[2]);
   ASSERT_EQ("97", video_section.GetFormats()[3]);
+  ASSERT_EQ("122", video_section.GetFormats()[4]);
+  ASSERT_EQ("123", video_section.GetFormats()[5]);
 
   // Validate rtpmap
   ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kRtpmapAttribute));
   auto& rtpmaps = video_attrs.GetRtpmap();
   ASSERT_TRUE(rtpmaps.HasEntry("120"));
   ASSERT_TRUE(rtpmaps.HasEntry("121"));
   ASSERT_TRUE(rtpmaps.HasEntry("126"));
   ASSERT_TRUE(rtpmaps.HasEntry("97"));
+  ASSERT_TRUE(rtpmaps.HasEntry("122"));
+  ASSERT_TRUE(rtpmaps.HasEntry("123"));
 
   auto& vp8_entry = rtpmaps.GetEntry("120");
   auto& vp9_entry = rtpmaps.GetEntry("121");
   auto& h264_1_entry = rtpmaps.GetEntry("126");
   auto& h264_0_entry = rtpmaps.GetEntry("97");
+  auto& red_0_entry = rtpmaps.GetEntry("122");
+  auto& ulpfec_0_entry = rtpmaps.GetEntry("123");
 
   ASSERT_EQ("VP8", vp8_entry.name);
   ASSERT_EQ("VP9", vp9_entry.name);
   ASSERT_EQ("H264", h264_1_entry.name);
   ASSERT_EQ("H264", h264_0_entry.name);
+  ASSERT_EQ("red", red_0_entry.name);
+  ASSERT_EQ("ulpfec", ulpfec_0_entry.name);
 
   // Validate fmtps
   ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kFmtpAttribute));
   auto& fmtps = video_attrs.GetFmtp().mFmtps;
 
-  ASSERT_EQ(4U, fmtps.size());
+  ASSERT_EQ(5U, fmtps.size());
 
   // VP8
   const SdpFmtpAttributeList::Parameters* vp8_params =
     video_section.FindFmtp("120");
   ASSERT_TRUE(vp8_params);
   ASSERT_EQ(SdpRtpmapAttributeList::kVP8, vp8_params->codec_type);
 
   auto& parsed_vp8_params =
@@ -2816,21 +2824,40 @@ TEST_F(JsepSessionTest, ValidateOfferedC
   ASSERT_EQ(SdpRtpmapAttributeList::kH264, h264_0_params->codec_type);
 
   auto& parsed_h264_0_params =
       *static_cast<const SdpFmtpAttributeList::H264Parameters*>(h264_0_params);
 
   ASSERT_EQ((uint32_t)0x42e00d, parsed_h264_0_params.profile_level_id);
   ASSERT_TRUE(parsed_h264_0_params.level_asymmetry_allowed);
   ASSERT_EQ(0U, parsed_h264_0_params.packetization_mode);
+
+  // red
+  const SdpFmtpAttributeList::Parameters* red_params =
+    video_section.FindFmtp("122");
+  ASSERT_TRUE(red_params);
+  ASSERT_EQ(SdpRtpmapAttributeList::kRed, red_params->codec_type);
+
+  auto& parsed_red_params =
+      *static_cast<const SdpFmtpAttributeList::RedParameters*>(red_params);
+  ASSERT_EQ(5U, parsed_red_params.encodings.size());
+  ASSERT_EQ(121, parsed_red_params.encodings[0]);
+  ASSERT_EQ(120, parsed_red_params.encodings[1]);
+  ASSERT_EQ(126, parsed_red_params.encodings[2]);
+  ASSERT_EQ(97, parsed_red_params.encodings[3]);
+  ASSERT_EQ(123, parsed_red_params.encodings[4]);
 }
 
 TEST_F(JsepSessionTest, ValidateAnsweredCodecParams)
 {
-
+  // TODO(bug 1099351): Once fixed, we can allow red in this offer,
+  // which will also cause multiple codecs in answer.  For now,
+  // red/ulpfec for video are behind a pref to mitigate potential for
+  // errors.
+  SetCodecEnabled(mSessionOff, "red", false);
   for (auto i = mSessionAns.Codecs().begin(); i != mSessionAns.Codecs().end();
        ++i) {
     auto* codec = *i;
     if (codec->mName == "H264") {
       JsepVideoCodecDescription* h264 =
           static_cast<JsepVideoCodecDescription*>(codec);
       h264->mProfileLevelId = 0x42a00d;
       // Switch up the pts
--- a/media/webrtc/signaling/test/jsep_track_unittest.cpp
+++ b/media/webrtc/signaling/test/jsep_track_unittest.cpp
@@ -27,43 +27,76 @@
 namespace mozilla {
 
 class JsepTrackTest : public ::testing::Test
 {
   public:
     JsepTrackTest() {}
 
     std::vector<JsepCodecDescription*>
-    MakeCodecs() const
+    MakeCodecs(bool addFecCodecs = false, bool preferRed = false) const
     {
       std::vector<JsepCodecDescription*> results;
       results.push_back(
           new JsepAudioCodecDescription("1", "opus", 48000, 2, 960, 40000));
       results.push_back(
           new JsepAudioCodecDescription("9", "G722", 8000, 1, 320, 64000));
 
+      JsepVideoCodecDescription* red = nullptr;
+      if (addFecCodecs && preferRed) {
+        red = new JsepVideoCodecDescription(
+            "122",
+            "red",
+            90000
+            );
+        results.push_back(red);
+      }
+
       JsepVideoCodecDescription* vp8 =
           new JsepVideoCodecDescription("120", "VP8", 90000);
       vp8->mConstraints.maxFs = 12288;
       vp8->mConstraints.maxFps = 60;
       results.push_back(vp8);
 
       JsepVideoCodecDescription* h264 =
           new JsepVideoCodecDescription("126", "H264", 90000);
       h264->mPacketizationMode = 1;
       h264->mProfileLevelId = 0x42E00D;
       results.push_back(h264);
 
+      if (addFecCodecs) {
+        if (!preferRed) {
+          red = new JsepVideoCodecDescription(
+              "122",
+              "red",
+              90000
+              );
+          results.push_back(red);
+        }
+        JsepVideoCodecDescription* ulpfec = new JsepVideoCodecDescription(
+            "123",
+            "ulpfec",
+            90000
+            );
+        results.push_back(ulpfec);
+      }
+
       results.push_back(
           new JsepApplicationCodecDescription(
             "5000",
             "webrtc-datachannel",
             16
             ));
 
+      // if we're doing something with red, it needs
+      // to update the redundant encodings list
+      if (red) {
+        red->UpdateRedundantEncodings(results);
+      }
+
       return results;
     }
 
     void Init(SdpMediaSection::MediaType type) {
       InitCodecs();
       InitTracks(type);
       InitSdp(type);
     }
@@ -206,26 +239,28 @@ class JsepTrackTest : public ::testing::
     }
 
     void CheckAnsEncodingCount(size_t expected) const
     {
       CheckEncodingCount(expected, mSendAns, mRecvOff);
     }
 
     const JsepVideoCodecDescription*
-    GetVideoCodec(const JsepTrack& track) const
+    GetVideoCodec(const JsepTrack& track,
+                  size_t expectedSize = 1,
+                  size_t codecIndex = 0) const
     {
       if (!track.GetNegotiatedDetails() ||
           track.GetNegotiatedDetails()->GetEncodingCount() != 1U) {
         return nullptr;
       }
       const std::vector<JsepCodecDescription*>& codecs =
         track.GetNegotiatedDetails()->GetEncoding(0).GetCodecs();
-      if (codecs.size() != 1U ||
-          codecs[0]->mType != SdpMediaSection::kVideo) {
+      if (codecs.size() != expectedSize ||
+          codecs[codecIndex]->mType != SdpMediaSection::kVideo) {
         return nullptr;
       }
       return static_cast<const JsepVideoCodecDescription*>(codecs[0]);
     }
 
     void CheckOtherFbsSize(const JsepTrack& track, size_t expected) const
     {
       const JsepVideoCodecDescription* videoCodec = GetVideoCodec(track);
@@ -361,16 +396,210 @@ TEST_F(JsepTrackTest, AudioNegotiation)
 TEST_F(JsepTrackTest, VideoNegotiation)
 {
   Init(SdpMediaSection::kVideo);
   OfferAnswer();
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(1);
 }
 
+TEST_F(JsepTrackTest, VideoNegotationOffererFEC)
+{
+  mOffCodecs.values = MakeCodecs(true);
+  mAnsCodecs.values = MakeCodecs(false);
+
+  InitTracks(SdpMediaSection::kVideo);
+  InitSdp(SdpMediaSection::kVideo);
+  OfferAnswer();
+
+  CheckOffEncodingCount(1);
+  CheckAnsEncodingCount(1);
+
+  ASSERT_NE(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos);
+  ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
+  ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
+  ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
+
+  ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
+  ASSERT_EQ(mAnswer->ToString().find("a=fmtp:122"), std::string::npos);
+
+  const JsepVideoCodecDescription* track = nullptr;
+  ASSERT_TRUE((track = GetVideoCodec(*mSendOff)));
+  ASSERT_EQ("120", track->mDefaultPt);
+  ASSERT_TRUE((track = GetVideoCodec(*mRecvOff)));
+  ASSERT_EQ("120", track->mDefaultPt);
+  ASSERT_TRUE((track = GetVideoCodec(*mSendAns)));
+  ASSERT_EQ("120", track->mDefaultPt);
+  ASSERT_TRUE((track = GetVideoCodec(*mRecvAns)));
+  ASSERT_EQ("120", track->mDefaultPt);
+}
+
+TEST_F(JsepTrackTest, VideoNegotationAnswererFEC)
+{
+  mOffCodecs.values = MakeCodecs(false);
+  mAnsCodecs.values = MakeCodecs(true);
+
+  InitTracks(SdpMediaSection::kVideo);
+  InitSdp(SdpMediaSection::kVideo);
+  OfferAnswer();
+
+  CheckOffEncodingCount(1);
+  CheckAnsEncodingCount(1);
+
+  ASSERT_EQ(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos);
+  ASSERT_EQ(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
+  ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
+  ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
+
+  ASSERT_EQ(mOffer->ToString().find("a=fmtp:122"), std::string::npos);
+  ASSERT_EQ(mAnswer->ToString().find("a=fmtp:122"), std::string::npos);
+
+  const JsepVideoCodecDescription* track = nullptr;
+  ASSERT_TRUE((track = GetVideoCodec(*mSendOff)));
+  ASSERT_EQ("120", track->mDefaultPt);
+  ASSERT_TRUE((track = GetVideoCodec(*mRecvOff)));
+  ASSERT_EQ("120", track->mDefaultPt);
+  ASSERT_TRUE((track = GetVideoCodec(*mSendAns)));
+  ASSERT_EQ("120", track->mDefaultPt);
+  ASSERT_TRUE((track = GetVideoCodec(*mRecvAns)));
+  ASSERT_EQ("120", track->mDefaultPt);
+}
+
+TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFEC)
+{
+  mOffCodecs.values = MakeCodecs(true);
+  mAnsCodecs.values = MakeCodecs(true);
+
+  InitTracks(SdpMediaSection::kVideo);
+  InitSdp(SdpMediaSection::kVideo);
+  OfferAnswer();
+
+  CheckOffEncodingCount(1);
+  CheckAnsEncodingCount(1);
+
+  ASSERT_NE(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos);
+  ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
+  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
+  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
+
+  ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
+  ASSERT_NE(mAnswer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
+
+  const JsepVideoCodecDescription* track = nullptr;
+  ASSERT_TRUE((track = GetVideoCodec(*mSendOff, 4)));
+  ASSERT_EQ("120", track->mDefaultPt);
+  ASSERT_TRUE((track = GetVideoCodec(*mRecvOff, 4)));
+  ASSERT_EQ("120", track->mDefaultPt);
+  ASSERT_TRUE((track = GetVideoCodec(*mSendAns, 4)));
+  ASSERT_EQ("120", track->mDefaultPt);
+  ASSERT_TRUE((track = GetVideoCodec(*mRecvAns, 4)));
+  ASSERT_EQ("120", track->mDefaultPt);
+}
+
+TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECPreferred)
+{
+  mOffCodecs.values = MakeCodecs(true, true);
+  mAnsCodecs.values = MakeCodecs(true);
+
+  InitTracks(SdpMediaSection::kVideo);
+  InitSdp(SdpMediaSection::kVideo);
+  OfferAnswer();
+
+  CheckOffEncodingCount(1);
+  CheckAnsEncodingCount(1);
+
+  ASSERT_NE(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos);
+  ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
+  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
+  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
+
+  ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
+  ASSERT_NE(mAnswer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
+
+  const JsepVideoCodecDescription* track = nullptr;
+  ASSERT_TRUE((track = GetVideoCodec(*mSendOff, 4)));
+  ASSERT_EQ("122", track->mDefaultPt);
+  ASSERT_TRUE((track = GetVideoCodec(*mRecvOff, 4)));
+  ASSERT_EQ("122", track->mDefaultPt);
+  ASSERT_TRUE((track = GetVideoCodec(*mSendAns, 4)));
+  ASSERT_EQ("122", track->mDefaultPt);
+  ASSERT_TRUE((track = GetVideoCodec(*mRecvAns, 4)));
+  ASSERT_EQ("122", track->mDefaultPt);
+}
+
+// Make sure we only put the right things in the fmtp:122 120/.... line
+TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECMismatch)
+{
+  mOffCodecs.values = MakeCodecs(true, true);
+  mAnsCodecs.values = MakeCodecs(true);
+  // remove h264 from answer codecs
+  ASSERT_EQ("H264", mAnsCodecs.values[3]->mName);
+  mAnsCodecs.values.erase(mAnsCodecs.values.begin()+3);
+
+  InitTracks(SdpMediaSection::kVideo);
+  InitSdp(SdpMediaSection::kVideo);
+  OfferAnswer();
+
+  CheckOffEncodingCount(1);
+  CheckAnsEncodingCount(1);
+
+  ASSERT_NE(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos);
+  ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
+  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
+  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
+
+  ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
+  ASSERT_NE(mAnswer->ToString().find("a=fmtp:122 120/123"), std::string::npos);
+
+  const JsepVideoCodecDescription* track = nullptr;
+  ASSERT_TRUE((track = GetVideoCodec(*mSendOff, 3)));
+  ASSERT_EQ("122", track->mDefaultPt);
+  ASSERT_TRUE((track = GetVideoCodec(*mRecvOff, 3)));
+  ASSERT_EQ("122", track->mDefaultPt);
+  ASSERT_TRUE((track = GetVideoCodec(*mSendAns, 3)));
+  ASSERT_EQ("122", track->mDefaultPt);
+  ASSERT_TRUE((track = GetVideoCodec(*mRecvAns, 3)));
+  ASSERT_EQ("122", track->mDefaultPt);
+}
+
+TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECZeroVP9Codec)
+{
+  mOffCodecs.values = MakeCodecs(true);
+  JsepVideoCodecDescription* vp9 =
+    new JsepVideoCodecDescription("0", "VP9", 90000);
+  vp9->mConstraints.maxFs = 12288;
+  vp9->mConstraints.maxFps = 60;
+  mOffCodecs.values.push_back(vp9);
+
+  ASSERT_EQ(8U, mOffCodecs.values.size());
+  JsepVideoCodecDescription* red =
+      static_cast<JsepVideoCodecDescription*>(mOffCodecs.values[4]);
+  ASSERT_EQ("red", red->mName);
+  // rebuild the redundant encodings with our newly added "wacky" VP9
+  red->mRedundantEncodings.clear();
+  red->UpdateRedundantEncodings(mOffCodecs.values);
+
+  mAnsCodecs.values = MakeCodecs(true);
+
+  InitTracks(SdpMediaSection::kVideo);
+  InitSdp(SdpMediaSection::kVideo);
+  OfferAnswer();
+
+  CheckOffEncodingCount(1);
+  CheckAnsEncodingCount(1);
+
+  ASSERT_NE(mOffer->ToString().find("a=rtpmap:122 red"), std::string::npos);
+  ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
+  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
+  ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
+
+  ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123/0"), std::string::npos);
+  ASSERT_NE(mAnswer->ToString().find("a=fmtp:122 120/126/123\r\n"), std::string::npos);
+}
+
 TEST_F(JsepTrackTest, VideoNegotiationOfferRemb)
 {
   InitCodecs();
   // enable remb on the offer codecs
   ((JsepVideoCodecDescription*)mOffCodecs.values[2])->EnableRemb();
   InitTracks(SdpMediaSection::kVideo);
   InitSdp(SdpMediaSection::kVideo);
   OfferAnswer();
--- a/media/webrtc/signaling/test/sdp_unittests.cpp
+++ b/media/webrtc/signaling/test/sdp_unittests.cpp
@@ -1220,24 +1220,26 @@ const std::string kBasicAudioVideoOffer 
 "a=candidate:6 1 UDP 16515071 162.222.183.171 51858 typ relay raddr 162.222.183.171 rport 51858" CRLF
 "a=candidate:3 2 UDP 100401150 162.222.183.171 62454 typ relay raddr 162.222.183.171 rport 62454" CRLF
 "a=candidate:2 2 UDP 1694236670 24.6.134.204 55428 typ srflx raddr 10.0.0.36 rport 55428" CRLF
 "a=candidate:6 2 UDP 16515070 162.222.183.171 50340 typ relay raddr 162.222.183.171 rport 50340" CRLF
 "a=candidate:0 2 UDP 2130379006 10.0.0.36 55428 typ host" CRLF
 "a=rtcp:62454 IN IP4 162.222.183.171" CRLF
 "a=end-of-candidates" CRLF
 "a=ssrc:5150" CRLF
-"m=video 9 RTP/SAVPF 120 121" CRLF
+"m=video 9 RTP/SAVPF 120 121 122 123" CRLF
 "c=IN IP6 ::1" CRLF
 "a=fingerprint:sha-1 DF:FA:FB:08:3B:3C:54:1D:D7:D4:05:77:A0:72:9B:14:08:6D:0F:4C:2E:AC:8A:FD:0A:8E:99:BF:5D:E8:3C:E7" CRLF
 "a=mid:second" CRLF
 "a=rtpmap:120 VP8/90000" CRLF
 "a=fmtp:120 max-fs=3600;max-fr=30" CRLF
 "a=rtpmap:121 VP9/90000" CRLF
 "a=fmtp:121 max-fs=3600;max-fr=30" CRLF
+"a=rtpmap:122 red/90000" CRLF
+"a=rtpmap:123 ulpfec/90000" CRLF
 "a=recvonly" CRLF
 "a=rtcp-fb:120 nack" CRLF
 "a=rtcp-fb:120 nack pli" CRLF
 "a=rtcp-fb:120 ccm fir" CRLF
 "a=rtcp-fb:121 nack" CRLF
 "a=rtcp-fb:121 nack pli" CRLF
 "a=rtcp-fb:121 ccm fir" CRLF
 "a=setup:active" CRLF
@@ -1413,19 +1415,21 @@ TEST_P(NewSdpTest, CheckMlines) {
   ASSERT_EQ("101", audio_formats[4]);
 
   ASSERT_EQ(SdpMediaSection::kVideo, mSdp->GetMediaSection(1).GetMediaType())
     << "Wrong type for second media section";
   ASSERT_EQ(SdpMediaSection::kRtpSavpf,
             mSdp->GetMediaSection(1).GetProtocol())
     << "Wrong protocol for video";
   auto video_formats = mSdp->GetMediaSection(1).GetFormats();
-  ASSERT_EQ(2U, video_formats.size()) << "Wrong number of formats for video";
+  ASSERT_EQ(4U, video_formats.size()) << "Wrong number of formats for video";
   ASSERT_EQ("120", video_formats[0]);
   ASSERT_EQ("121", video_formats[1]);
+  ASSERT_EQ("122", video_formats[2]);
+  ASSERT_EQ("123", video_formats[3]);
 
   ASSERT_EQ(SdpMediaSection::kAudio, mSdp->GetMediaSection(2).GetMediaType())
     << "Wrong type for third media section";
 }
 
 TEST_P(NewSdpTest, CheckSetup) {
   ParseSdp(kBasicAudioVideoOffer);
   ASSERT_TRUE(!!mSdp) << "Parse failed: " << GetParseErrors();
@@ -1513,33 +1517,138 @@ TEST_P(NewSdpTest, CheckRtpmap) {
   CheckRtpmap("101",
               SdpRtpmapAttributeList::kOtherCodec,
               "telephone-event",
               8000,
               1,
               audiosec.GetFormats()[4],
               rtpmap);
 
-  const SdpMediaSection& videosec1 = mSdp->GetMediaSection(1);
+  const SdpMediaSection& videosec = mSdp->GetMediaSection(1);
+  const SdpRtpmapAttributeList videoRtpmap =
+    videosec.GetAttributeList().GetRtpmap();
+  ASSERT_EQ(4U, videoRtpmap.mRtpmaps.size())
+    << "Wrong number of rtpmap attributes for video";
+
   CheckRtpmap("120",
               SdpRtpmapAttributeList::kVP8,
               "VP8",
               90000,
               0,
-              videosec1.GetFormats()[0],
-              videosec1.GetAttributeList().GetRtpmap());
-
-  const SdpMediaSection& videosec2 = mSdp->GetMediaSection(1);
+              videosec.GetFormats()[0],
+              videoRtpmap);
+
   CheckRtpmap("121",
               SdpRtpmapAttributeList::kVP9,
               "VP9",
               90000,
               0,
-              videosec2.GetFormats()[1],
-              videosec2.GetAttributeList().GetRtpmap());
+              videosec.GetFormats()[1],
+              videoRtpmap);
+
+  CheckRtpmap("122",
+              SdpRtpmapAttributeList::kRed,
+              "red",
+              90000,
+              0,
+              videosec.GetFormats()[2],
+              videoRtpmap);
+
+  CheckRtpmap("123",
+              SdpRtpmapAttributeList::kUlpfec,
+              "ulpfec",
+              90000,
+              0,
+              videosec.GetFormats()[3],
+              videoRtpmap);
+}
+
+static const std::string kVideoWithRedAndUlpfecSdp =
+  "v=0" CRLF
+  "o=- 4294967296 2 IN IP4 127.0.0.1" CRLF
+  "s=SIP Call" CRLF
+  "c=IN IP4 198.51.100.7" CRLF
+  "t=0 0" CRLF
+  "m=video 9 RTP/SAVPF 97 120 121 122 123" CRLF
+  "c=IN IP6 ::1" CRLF
+  "a=fingerprint:sha-1 DF:FA:FB:08:3B:3C:54:1D:D7:D4:05:77:A0:72:9B:14:08:6D:0F:4C:2E:AC:8A:FD:0A:8E:99:BF:5D:E8:3C:E7" CRLF
+  "a=rtpmap:97 H264/90000" CRLF
+  "a=fmtp:97 profile-level-id=42a01e" CRLF
+  "a=rtpmap:120 VP8/90000" CRLF
+  "a=fmtp:120 max-fs=3600;max-fr=30" CRLF
+  "a=rtpmap:121 VP9/90000" CRLF
+  "a=fmtp:121 max-fs=3600;max-fr=30" CRLF
+  "a=rtpmap:122 red/90000" CRLF
+  "a=rtpmap:123 ulpfec/90000" CRLF;
+
+TEST_P(NewSdpTest, CheckRedNoFmtp) {
+  ParseSdp(kVideoWithRedAndUlpfecSdp);
+  ASSERT_TRUE(!!mSdp) << "Parse failed: " << GetParseErrors();
+  ASSERT_EQ(1U, mSdp->GetMediaSectionCount())
+    << "Wrong number of media sections";
+
+  ASSERT_TRUE(mSdp->GetMediaSection(0).GetAttributeList().HasAttribute(
+              SdpAttribute::kFmtpAttribute));
+  auto video_format_params =
+      mSdp->GetMediaSection(0).GetAttributeList().GetFmtp().mFmtps;
+  ASSERT_EQ(3U, video_format_params.size());
+
+  // make sure we don't get a fmtp for codec 122
+  for (size_t i = 0; i < video_format_params.size(); ++i) {
+    ASSERT_NE("122", video_format_params[i].format);
+  }
+}
+
+TEST_P(NewSdpTest, CheckRedFmtpWith2Codecs) {
+  ParseSdp(kVideoWithRedAndUlpfecSdp + "a=fmtp:122 120/121" CRLF);
+  ASSERT_TRUE(!!mSdp) << "Parse failed: " << GetParseErrors();
+  ASSERT_EQ(1U, mSdp->GetMediaSectionCount())
+    << "Wrong number of media sections";
+
+  ASSERT_TRUE(mSdp->GetMediaSection(0).GetAttributeList().HasAttribute(
+              SdpAttribute::kFmtpAttribute));
+  auto video_format_params =
+      mSdp->GetMediaSection(0).GetAttributeList().GetFmtp().mFmtps;
+  ASSERT_EQ(4U, video_format_params.size());
+
+  ASSERT_EQ("122", video_format_params[3].format);
+  ASSERT_TRUE(!!video_format_params[3].parameters);
+  ASSERT_EQ(SdpRtpmapAttributeList::kRed,
+            video_format_params[3].parameters->codec_type);
+  const SdpFmtpAttributeList::RedParameters* red_parameters(
+      static_cast<SdpFmtpAttributeList::RedParameters*>(
+        video_format_params[3].parameters.get()));
+  ASSERT_EQ(2U, red_parameters->encodings.size());
+  ASSERT_EQ(120U, red_parameters->encodings[0]);
+  ASSERT_EQ(121U, red_parameters->encodings[1]);
+}
+
+TEST_P(NewSdpTest, CheckRedFmtpWith3Codecs) {
+  ParseSdp(kVideoWithRedAndUlpfecSdp + "a=fmtp:122 120/121/123" CRLF);
+  ASSERT_TRUE(!!mSdp) << "Parse failed: " << GetParseErrors();
+  ASSERT_EQ(1U, mSdp->GetMediaSectionCount())
+    << "Wrong number of media sections";
+
+  ASSERT_TRUE(mSdp->GetMediaSection(0).GetAttributeList().HasAttribute(
+              SdpAttribute::kFmtpAttribute));
+  auto video_format_params =
+      mSdp->GetMediaSection(0).GetAttributeList().GetFmtp().mFmtps;
+  ASSERT_EQ(4U, video_format_params.size());
+
+  ASSERT_EQ("122", video_format_params[3].format);
+  ASSERT_TRUE(!!video_format_params[3].parameters);
+  ASSERT_EQ(SdpRtpmapAttributeList::kRed,
+            video_format_params[3].parameters->codec_type);
+  const SdpFmtpAttributeList::RedParameters* red_parameters(
+      static_cast<SdpFmtpAttributeList::RedParameters*>(
+        video_format_params[3].parameters.get()));
+  ASSERT_EQ(3U, red_parameters->encodings.size());
+  ASSERT_EQ(120U, red_parameters->encodings[0]);
+  ASSERT_EQ(121U, red_parameters->encodings[1]);
+  ASSERT_EQ(123U, red_parameters->encodings[2]);
 }
 
 const std::string kH264AudioVideoOffer =
 "v=0" CRLF
 "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF
 "s=SIP Call" CRLF
 "c=IN IP4 224.0.0.1/100/12" CRLF
 "t=0 0" CRLF
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -398,16 +398,17 @@ pref("media.navigator.load_adapt.measure
 pref("media.navigator.load_adapt.avg_seconds",3);
 pref("media.navigator.load_adapt.high_load","0.90");
 pref("media.navigator.load_adapt.low_load","0.40");
 pref("media.navigator.video.default_fps",30);
 pref("media.navigator.video.default_minfps",10);
 pref("media.navigator.video.use_remb", true);
 pref("media.navigator.video.use_tmmbr", false);
 pref("media.navigator.audio.use_fec", true);
+pref("media.navigator.video.red_ulpfec_enabled", false);
 
 pref("media.webrtc.debug.trace_mask", 0);
 pref("media.webrtc.debug.multi_log", false);
 pref("media.webrtc.debug.aec_log_dir", "");
 pref("media.webrtc.debug.log_file", "");
 pref("media.webrtc.debug.aec_dump_max_size", 4194304); // 4MB
 
 #ifdef MOZ_WIDGET_GONK