Bug 1091242 - Part 2: New JSEP handling code. See https://github.com/unicorn-wg/gecko-dev/tree/multistream_rebase for more history. r=ehugg, r=jesup
☠☠ backed out by 960303b07c90 ☠ ☠
authorByron Campen [:bwc] <docfaraday@gmail.com>
Wed, 19 Nov 2014 16:12:08 -0800
changeset 218761 c82d484e135a69da07f84e1319a2ea8f9a5ed2ed
parent 218760 3e0c8932f1b17b6e44e8ba4ea889848031041f1b
child 218762 8d0653eb85ab002399a3b288807e901153b75d06
push id27944
push usercbook@mozilla.com
push dateTue, 09 Dec 2014 11:54:28 +0000
treeherderautoland@acf5660d2048 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehugg, jesup
bugs1091242
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1091242 - Part 2: New JSEP handling code. See https://github.com/unicorn-wg/gecko-dev/tree/multistream_rebase for more history. r=ehugg, r=jesup
media/webrtc/signaling/src/jsep/JsepCodecDescription.h
media/webrtc/signaling/src/jsep/JsepSession.h
media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
media/webrtc/signaling/src/jsep/JsepSessionImpl.h
media/webrtc/signaling/src/jsep/JsepTrack.h
media/webrtc/signaling/src/jsep/JsepTrackImpl.h
media/webrtc/signaling/src/jsep/JsepTransport.h
media/webrtc/signaling/test/jsep_session_unittest.cpp
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/jsep/JsepCodecDescription.h
@@ -0,0 +1,620 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _JSEPCODECDESCRIPTION_H_
+#define _JSEPCODECDESCRIPTION_H_
+
+#include <iostream>
+#include <string>
+#include "signaling/src/sdp/SdpMediaSection.h"
+
+namespace mozilla {
+
+#define JSEP_CODEC_CLONE(T)                                                    \
+  virtual JsepCodecDescription* Clone() const MOZ_OVERRIDE                     \
+  {                                                                            \
+    return new T(*this);                                                       \
+  }
+
+// A single entry in our list of known codecs.
+struct JsepCodecDescription {
+  JsepCodecDescription(mozilla::SdpMediaSection::MediaType type,
+                       const std::string& defaultPt,
+                       const std::string& name,
+                       uint32_t clock,
+                       uint32_t channels,
+                       bool enabled)
+      : mType(type),
+        mDefaultPt(defaultPt),
+        mName(name),
+        mClock(clock),
+        mChannels(channels),
+        mEnabled(enabled)
+  {
+  }
+  virtual ~JsepCodecDescription() {}
+
+  virtual JsepCodecDescription* Clone() const = 0;
+  virtual void AddFmtps(SdpFmtpAttributeList& fmtp) const = 0;
+  virtual void AddRtcpFbs(SdpRtcpFbAttributeList& rtcpfb) const = 0;
+  virtual bool LoadFmtps(const SdpFmtpAttributeList::Parameters& params) = 0;
+  virtual bool LoadRtcpFbs(
+      const SdpRtcpFbAttributeList::Feedback& feedback) = 0;
+
+  static bool
+  GetPtAsInt(const std::string& ptString, uint16_t* ptOutparam)
+  {
+    char* end;
+    unsigned long pt = strtoul(ptString.c_str(), &end, 10);
+    size_t length = static_cast<size_t>(end - ptString.c_str());
+    if ((pt > UINT16_MAX) || (length != ptString.size())) {
+      return false;
+    }
+    *ptOutparam = pt;
+    return true;
+  }
+
+  bool
+  GetPtAsInt(uint16_t* ptOutparam) const
+  {
+    return GetPtAsInt(mDefaultPt, ptOutparam);
+  }
+
+  virtual bool
+  Matches(const std::string& fmt, const SdpMediaSection& remoteMsection) const
+  {
+    auto& attrs = remoteMsection.GetAttributeList();
+    if (!attrs.HasAttribute(SdpAttribute::kRtpmapAttribute)) {
+      return false;
+    }
+
+    const SdpRtpmapAttributeList& rtpmap = attrs.GetRtpmap();
+    if (!rtpmap.HasEntry(fmt)) {
+      return false;
+    }
+
+    const SdpRtpmapAttributeList::Rtpmap& entry = rtpmap.GetEntry(fmt);
+
+    if (mType == remoteMsection.GetMediaType()
+        && (mName == entry.name)
+        && (mClock == entry.clock)
+        && (mChannels == entry.channels)) {
+      return ParametersMatch(FindParameters(entry.pt, remoteMsection));
+    }
+    return false;
+  }
+
+  virtual bool
+  ParametersMatch(const SdpFmtpAttributeList::Parameters* fmtp) const
+  {
+    return true;
+  }
+
+  static const SdpFmtpAttributeList::Parameters*
+  FindParameters(const std::string& pt,
+                 const mozilla::SdpMediaSection& remoteMsection)
+  {
+    const SdpAttributeList& attrs = remoteMsection.GetAttributeList();
+
+    if (attrs.HasAttribute(SdpAttribute::kFmtpAttribute)) {
+      const SdpFmtpAttributeList& fmtps = attrs.GetFmtp();
+      for (auto i = fmtps.mFmtps.begin(); i != fmtps.mFmtps.end(); ++i) {
+        if (i->format == pt && i->parameters) {
+          return i->parameters.get();
+        }
+      }
+    }
+    return nullptr;
+  }
+
+  virtual JsepCodecDescription*
+  MakeNegotiatedCodec(const mozilla::SdpMediaSection& remoteMsection,
+                      const std::string& pt,
+                      bool sending) const
+  {
+    UniquePtr<JsepCodecDescription> negotiated(Clone());
+    negotiated->mDefaultPt = pt;
+
+    const SdpAttributeList& attrs = remoteMsection.GetAttributeList();
+
+    if (sending) {
+      auto* parameters = FindParameters(negotiated->mDefaultPt, remoteMsection);
+      if (parameters) {
+        if (!negotiated->LoadFmtps(*parameters)) {
+          // Remote parameters were invalid
+          return nullptr;
+        }
+      }
+    } else {
+      // If a receive track, we need to pay attention to remote end's rtcp-fb
+      if (attrs.HasAttribute(SdpAttribute::kRtcpFbAttribute)) {
+        auto& rtcpfbs = attrs.GetRtcpFb().mFeedbacks;
+        for (auto i = rtcpfbs.begin(); i != rtcpfbs.end(); ++i) {
+          if (i->pt == negotiated->mDefaultPt) {
+            if (!negotiated->LoadRtcpFbs(*i)) {
+              // Remote parameters were invalid
+              return nullptr;
+            }
+          }
+        }
+      }
+    }
+
+    return negotiated.release();
+  }
+
+  virtual void
+  AddToMediaSection(SdpMediaSection& msection) const
+  {
+    if (mEnabled && msection.GetMediaType() == mType) {
+      if (mType == SdpMediaSection::kApplication) {
+        // Hack: using mChannels for number of streams
+        msection.AddDataChannel(mDefaultPt, mName, mChannels);
+      } else {
+        msection.AddCodec(mDefaultPt, mName, mClock, mChannels);
+      }
+      AddFmtpsToMSection(msection);
+      AddRtcpFbsToMSection(msection);
+    }
+  }
+
+  virtual void
+  AddFmtpsToMSection(SdpMediaSection& msection) const
+  {
+    SdpAttributeList& attrs = msection.GetAttributeList();
+
+    UniquePtr<SdpFmtpAttributeList> fmtps;
+
+    if (attrs.HasAttribute(SdpAttribute::kFmtpAttribute)) {
+      fmtps.reset(new SdpFmtpAttributeList(attrs.GetFmtp()));
+    } else {
+      fmtps.reset(new SdpFmtpAttributeList);
+    }
+
+    AddFmtps(*fmtps);
+
+    if (!fmtps->mFmtps.empty()) {
+      attrs.SetAttribute(fmtps.release());
+    }
+  }
+
+  virtual void
+  AddRtcpFbsToMSection(SdpMediaSection& msection) const
+  {
+    SdpAttributeList& attrs = msection.GetAttributeList();
+
+    UniquePtr<SdpRtcpFbAttributeList> rtcpfbs;
+
+    if (attrs.HasAttribute(SdpAttribute::kRtcpFbAttribute)) {
+      rtcpfbs.reset(new SdpRtcpFbAttributeList(attrs.GetRtcpFb()));
+    } else {
+      rtcpfbs.reset(new SdpRtcpFbAttributeList);
+    }
+
+    AddRtcpFbs(*rtcpfbs);
+
+    if (!rtcpfbs->mFeedbacks.empty()) {
+      attrs.SetAttribute(rtcpfbs.release());
+    }
+  }
+
+  mozilla::SdpMediaSection::MediaType mType;
+  std::string mDefaultPt;
+  std::string mName;
+  uint32_t mClock;
+  uint32_t mChannels;
+  bool mEnabled;
+};
+
+struct JsepAudioCodecDescription : public JsepCodecDescription {
+  JsepAudioCodecDescription(const std::string& defaultPt,
+                            const std::string& name,
+                            uint32_t clock,
+                            uint32_t channels,
+                            uint32_t packetSize,
+                            uint32_t bitRate,
+                            bool enabled = true)
+      : JsepCodecDescription(mozilla::SdpMediaSection::kAudio, defaultPt, name,
+                             clock, channels, enabled),
+        mPacketSize(packetSize),
+        mBitrate(bitRate)
+  {
+  }
+
+  virtual void
+  AddFmtps(SdpFmtpAttributeList& fmtp) const MOZ_OVERRIDE
+  {
+    // TODO
+  }
+
+  virtual void
+  AddRtcpFbs(SdpRtcpFbAttributeList& rtcpfb) const MOZ_OVERRIDE
+  {
+    // TODO: Do we want to add anything?
+  }
+
+  virtual bool
+  LoadFmtps(const SdpFmtpAttributeList::Parameters& params) MOZ_OVERRIDE
+  {
+    // TODO
+    return true;
+  }
+
+  virtual bool
+  LoadRtcpFbs(const SdpRtcpFbAttributeList::Feedback& feedback) MOZ_OVERRIDE
+  {
+    // Nothing to do
+    return true;
+  }
+
+  JSEP_CODEC_CLONE(JsepAudioCodecDescription)
+
+  uint32_t mPacketSize;
+  uint32_t mBitrate;
+};
+
+struct JsepVideoCodecDescription : public JsepCodecDescription {
+  JsepVideoCodecDescription(const std::string& defaultPt,
+                            const std::string& name,
+                            uint32_t clock,
+                            bool enabled = true)
+      : JsepCodecDescription(mozilla::SdpMediaSection::kVideo, defaultPt, name,
+                             clock, 0, enabled),
+        mMaxFs(0),
+        mMaxFr(0),
+        mPacketizationMode(0),
+        mMaxMbps(0),
+        mMaxCpb(0),
+        mMaxDpb(0),
+        mMaxBr(0)
+  {
+  }
+
+  virtual void
+  AddFmtps(SdpFmtpAttributeList& fmtp) const MOZ_OVERRIDE
+  {
+    if (mName == "H264") {
+      UniquePtr<SdpFmtpAttributeList::H264Parameters> params =
+          MakeUnique<SdpFmtpAttributeList::H264Parameters>();
+
+      params->packetization_mode = mPacketizationMode;
+      // Hard-coded, may need to change someday?
+      params->level_asymmetry_allowed = true;
+      params->profile_level_id = mProfileLevelId;
+      params->max_mbps = mMaxMbps;
+      params->max_fs = mMaxFs;
+      params->max_cpb = mMaxCpb;
+      params->max_dpb = mMaxDpb;
+      params->max_br = mMaxBr;
+      strncpy(params->sprop_parameter_sets,
+              mSpropParameterSets.c_str(),
+              sizeof(params->sprop_parameter_sets) - 1);
+      fmtp.PushEntry(mDefaultPt, "", mozilla::Move(params));
+    } else if (mName == "VP8") {
+      UniquePtr<SdpFmtpAttributeList::VP8Parameters> params =
+          MakeUnique<SdpFmtpAttributeList::VP8Parameters>();
+
+      params->max_fs = mMaxFs;
+      params->max_fr = mMaxFr;
+      fmtp.PushEntry(mDefaultPt, "", mozilla::Move(params));
+    }
+  }
+
+  virtual void
+  AddRtcpFbs(SdpRtcpFbAttributeList& rtcpfb) const MOZ_OVERRIDE
+  {
+    // Just hard code for now
+    rtcpfb.PushEntry(mDefaultPt, SdpRtcpFbAttributeList::kNack);
+    rtcpfb.PushEntry(
+        mDefaultPt, SdpRtcpFbAttributeList::kNack, SdpRtcpFbAttributeList::pli);
+    rtcpfb.PushEntry(
+        mDefaultPt, SdpRtcpFbAttributeList::kCcm, SdpRtcpFbAttributeList::fir);
+  }
+
+  virtual bool
+  LoadFmtps(const SdpFmtpAttributeList::Parameters& params) MOZ_OVERRIDE
+  {
+    switch (params.codec_type) {
+      case SdpRtpmapAttributeList::kH264:
+        LoadH264Parameters(params);
+        break;
+      case SdpRtpmapAttributeList::kVP8:
+        LoadVP8Parameters(params);
+        break;
+      case SdpRtpmapAttributeList::kVP9:
+      case SdpRtpmapAttributeList::kiLBC:
+      case SdpRtpmapAttributeList::kiSAC:
+      case SdpRtpmapAttributeList::kOpus:
+      case SdpRtpmapAttributeList::kG722:
+      case SdpRtpmapAttributeList::kPCMU:
+      case SdpRtpmapAttributeList::kPCMA:
+      case SdpRtpmapAttributeList::kOtherCodec:
+        MOZ_ASSERT(false, "Invalid codec type for video");
+    }
+    return true;
+  }
+
+  virtual bool
+  LoadRtcpFbs(const SdpRtcpFbAttributeList::Feedback& feedback) MOZ_OVERRIDE
+  {
+    switch (feedback.type) {
+      case SdpRtcpFbAttributeList::kAck:
+        mAckFbTypes.push_back(feedback.parameter);
+        break;
+      case SdpRtcpFbAttributeList::kCcm:
+        mCcmFbTypes.push_back(feedback.parameter);
+        break;
+      case SdpRtcpFbAttributeList::kNack:
+        mNackFbTypes.push_back(feedback.parameter);
+        break;
+      case SdpRtcpFbAttributeList::kApp:
+      case SdpRtcpFbAttributeList::kTrrInt:
+        // We don't support these, ignore.
+        {}
+    }
+    return true;
+  }
+
+  enum Subprofile {
+    kH264ConstrainedBaseline,
+    kH264Baseline,
+    kH264Main,
+    kH264Extended,
+    kH264High,
+    kH264High10,
+    kH264High42,
+    kH264High44,
+    kH264High10I,
+    kH264High42I,
+    kH264High44I,
+    kH264CALVC44,
+    kH264UnknownSubprofile
+  };
+
+  static Subprofile
+  GetSubprofile(uint32_t profileLevelId)
+  {
+    // Based on Table 5 from RFC 6184:
+    //        Profile     profile_idc        profile-iop
+    //                    (hexadecimal)      (binary)
+
+    //        CB          42 (B)             x1xx0000
+    //           same as: 4D (M)             1xxx0000
+    //           same as: 58 (E)             11xx0000
+    //        B           42 (B)             x0xx0000
+    //           same as: 58 (E)             10xx0000
+    //        M           4D (M)             0x0x0000
+    //        E           58                 00xx0000
+    //        H           64                 00000000
+    //        H10         6E                 00000000
+    //        H42         7A                 00000000
+    //        H44         F4                 00000000
+    //        H10I        6E                 00010000
+    //        H42I        7A                 00010000
+    //        H44I        F4                 00010000
+    //        C44I        2C                 00010000
+
+    if ((profileLevelId & 0xFF4F00) == 0x424000) {
+      // 01001111 (mask, 0x4F)
+      // x1xx0000 (from table)
+      // 01000000 (expected value, 0x40)
+      return kH264ConstrainedBaseline;
+    }
+
+    if ((profileLevelId & 0xFF8F00) == 0x4D8000) {
+      // 10001111 (mask, 0x8F)
+      // 1xxx0000 (from table)
+      // 10000000 (expected value, 0x80)
+      return kH264ConstrainedBaseline;
+    }
+
+    if ((profileLevelId & 0xFFCF00) == 0x58C000) {
+      // 11001111 (mask, 0xCF)
+      // 11xx0000 (from table)
+      // 11000000 (expected value, 0xC0)
+      return kH264ConstrainedBaseline;
+    }
+
+    if ((profileLevelId & 0xFF4F00) == 0x420000) {
+      // 01001111 (mask, 0x4F)
+      // x0xx0000 (from table)
+      // 00000000 (expected value)
+      return kH264Baseline;
+    }
+
+    if ((profileLevelId & 0xFFCF00) == 0x588000) {
+      // 11001111 (mask, 0xCF)
+      // 10xx0000 (from table)
+      // 10000000 (expected value, 0x80)
+      return kH264Baseline;
+    }
+
+    if ((profileLevelId & 0xFFAF00) == 0x4D0000) {
+      // 10101111 (mask, 0xAF)
+      // 0x0x0000 (from table)
+      // 00000000 (expected value)
+      return kH264Main;
+    }
+
+    if ((profileLevelId & 0xFF0000) == 0x580000) {
+      // 11001111 (mask, 0xCF)
+      // 00xx0000 (from table)
+      // 00000000 (expected value)
+      return kH264Extended;
+    }
+
+    if ((profileLevelId & 0xFFFF00) == 0x640000) {
+      return kH264High;
+    }
+
+    if ((profileLevelId & 0xFFFF00) == 0x6E0000) {
+      return kH264High10;
+    }
+
+    if ((profileLevelId & 0xFFFF00) == 0x7A0000) {
+      return kH264High42;
+    }
+
+    if ((profileLevelId & 0xFFFF00) == 0xF40000) {
+      return kH264High44;
+    }
+
+    if ((profileLevelId & 0xFFFF00) == 0x6E1000) {
+      return kH264High10I;
+    }
+
+    if ((profileLevelId & 0xFFFF00) == 0x7A1000) {
+      return kH264High42I;
+    }
+
+    if ((profileLevelId & 0xFFFF00) == 0xF41000) {
+      return kH264High44I;
+    }
+
+    if ((profileLevelId & 0xFFFF00) == 0x2C1000) {
+      return kH264CALVC44;
+    }
+
+    return kH264UnknownSubprofile;
+  }
+
+  virtual bool
+  ParametersMatch(const SdpFmtpAttributeList::Parameters* fmtp) const
+      MOZ_OVERRIDE
+  {
+    if (mName == "H264") {
+      if (!fmtp) {
+        // No fmtp means that we cannot assume level asymmetry is allowed,
+        // and since we have no way of knowing the profile-level-id, we can't
+        // say that we match.
+        return false;
+      }
+
+      auto* h264Params =
+          static_cast<const SdpFmtpAttributeList::H264Parameters*>(fmtp);
+
+      if (!h264Params->level_asymmetry_allowed) {
+        if (GetSubprofile(h264Params->profile_level_id) !=
+            GetSubprofile(mProfileLevelId)) {
+          return false;
+        }
+      }
+
+      if (h264Params->packetization_mode != mPacketizationMode) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  void
+  LoadH264Parameters(const SdpFmtpAttributeList::Parameters& params)
+  {
+    const SdpFmtpAttributeList::H264Parameters& h264Params =
+        static_cast<const SdpFmtpAttributeList::H264Parameters&>(params);
+
+    mMaxFs = h264Params.max_fs;
+    mProfileLevelId = h264Params.profile_level_id;
+    mPacketizationMode = h264Params.packetization_mode;
+    mMaxMbps = h264Params.max_mbps;
+    mMaxCpb = h264Params.max_cpb;
+    mMaxDpb = h264Params.max_dpb;
+    mMaxBr = h264Params.max_br;
+    mSpropParameterSets = h264Params.sprop_parameter_sets;
+  }
+
+  void
+  LoadVP8Parameters(const SdpFmtpAttributeList::Parameters& params)
+  {
+    const SdpFmtpAttributeList::VP8Parameters& vp8Params =
+        static_cast<const SdpFmtpAttributeList::VP8Parameters&>(params);
+
+    mMaxFs = vp8Params.max_fs;
+    mMaxFr = vp8Params.max_fr;
+  }
+
+  JSEP_CODEC_CLONE(JsepVideoCodecDescription)
+
+  std::vector<std::string> mAckFbTypes;
+  std::vector<std::string> mNackFbTypes;
+  std::vector<std::string> mCcmFbTypes;
+
+  uint32_t mMaxFs;
+
+  // H264-specific stuff
+  uint32_t mProfileLevelId;
+  uint32_t mMaxFr;
+  uint32_t mPacketizationMode;
+  uint32_t mMaxMbps;
+  uint32_t mMaxCpb;
+  uint32_t mMaxDpb;
+  uint32_t mMaxBr;
+  std::string mSpropParameterSets;
+};
+
+struct JsepApplicationCodecDescription : public JsepCodecDescription {
+  JsepApplicationCodecDescription(const std::string& defaultPt,
+                                  const std::string& name,
+                                  uint16_t channels,
+                                  bool enabled = true)
+      : JsepCodecDescription(mozilla::SdpMediaSection::kApplication, defaultPt,
+                             name, 0, channels, enabled)
+  {
+  }
+
+  virtual void
+  AddFmtps(SdpFmtpAttributeList& fmtp) const MOZ_OVERRIDE
+  {
+    // TODO: Is there anything to do here?
+  }
+
+  virtual void
+  AddRtcpFbs(SdpRtcpFbAttributeList& rtcpfb) const MOZ_OVERRIDE
+  {
+    // Nothing to do here.
+  }
+
+  virtual bool
+  LoadFmtps(const SdpFmtpAttributeList::Parameters& params) MOZ_OVERRIDE
+  {
+    // TODO: Is there anything to do here?
+    return true;
+  }
+
+  virtual bool
+  LoadRtcpFbs(const SdpRtcpFbAttributeList::Feedback& feedback) MOZ_OVERRIDE
+  {
+    // Nothing to do
+    return true;
+  }
+
+  JSEP_CODEC_CLONE(JsepApplicationCodecDescription)
+
+  // Override, uses sctpmap instead of rtpmap
+  virtual bool
+  Matches(const std::string& fmt,
+          const SdpMediaSection& remoteMsection) const MOZ_OVERRIDE
+  {
+    auto& attrs = remoteMsection.GetAttributeList();
+    if (!attrs.HasAttribute(SdpAttribute::kSctpmapAttribute)) {
+      return false;
+    }
+
+    const SdpSctpmapAttributeList& sctpmap = attrs.GetSctpmap();
+    if (!sctpmap.HasEntry(fmt)) {
+      return false;
+    }
+
+    const SdpSctpmapAttributeList::Sctpmap& entry = sctpmap.GetEntry(fmt);
+
+    if (mType == remoteMsection.GetMediaType() && (mName == entry.name)) {
+      return true;
+    }
+    return false;
+  }
+};
+
+} // namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/jsep/JsepSession.h
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _JSEPSESSION_H_
+#define _JSEPSESSION_H_
+
+#include <string>
+#include <vector>
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nsError.h"
+
+#include "signaling/src/jsep/JsepTransport.h"
+#include "signaling/src/sdp/Sdp.h"
+
+
+namespace mozilla {
+
+// Forward declarations
+struct JsepCodecDescription;
+class JsepTrack;
+struct JsepTrackPair;
+
+enum JsepSignalingState {
+  kJsepStateStable,
+  kJsepStateHaveLocalOffer,
+  kJsepStateHaveRemoteOffer,
+  kJsepStateHaveLocalPranswer,
+  kJsepStateHaveRemotePranswer,
+  kJsepStateClosed
+};
+
+enum JsepSdpType {
+  kJsepSdpOffer,
+  kJsepSdpAnswer,
+  kJsepSdpPranswer,
+};
+
+struct JsepOAOptions {};
+struct JsepOfferOptions : public JsepOAOptions {
+  Maybe<size_t> mOfferToReceiveAudio;
+  Maybe<size_t> mOfferToReceiveVideo;
+  Maybe<bool> mDontOfferDataChannel;
+};
+struct JsepAnswerOptions : public JsepOAOptions {};
+
+class JsepSession
+{
+public:
+  explicit JsepSession(const std::string& name)
+      : mName(name), mState(kJsepStateStable)
+  {
+  }
+  virtual ~JsepSession() {}
+
+  virtual nsresult Init() = 0;
+
+  // Accessors for basic properties.
+  virtual const std::string&
+  GetName() const
+  {
+    return mName;
+  }
+  virtual JsepSignalingState
+  GetState() const
+  {
+    return mState;
+  }
+
+  // Set up the ICE And DTLS data.
+  virtual nsresult SetIceCredentials(const std::string& ufrag,
+                                     const std::string& pwd) = 0;
+  virtual bool RemoteIsIceLite() const = 0;
+  virtual std::vector<std::string> GetIceOptions() const = 0;
+
+  virtual nsresult AddDtlsFingerprint(const std::string& algorithm,
+                                      const std::vector<uint8_t>& value) = 0;
+
+  virtual nsresult AddAudioRtpExtension(const std::string& extensionName) = 0;
+  virtual nsresult AddVideoRtpExtension(const std::string& extensionName) = 0;
+
+  // Kinda gross to be locking down the data structure type like this, but
+  // returning by value is problematic due to the lack of stl move semantics in
+  // our build config, since we can't use UniquePtr in the container. The
+  // alternative is writing a raft of accessor functions that allow arbitrary
+  // manipulation (which will be unwieldy), or allowing functors to be injected
+  // that manipulate the data structure (still pretty unwieldy).
+  virtual std::vector<JsepCodecDescription*>& Codecs() = 0;
+
+  // Manage tracks. We take shared ownership of any track.
+  virtual nsresult AddTrack(const RefPtr<JsepTrack>& track) = 0;
+  virtual nsresult RemoveTrack(size_t track_index) = 0;
+  virtual nsresult ReplaceTrack(size_t track_index,
+                                const RefPtr<JsepTrack>& track) = 0;
+
+  virtual size_t GetLocalTrackCount() const = 0;
+  virtual nsresult GetLocalTrack(size_t index,
+                                 RefPtr<JsepTrack>* track) const = 0;
+
+  virtual size_t GetRemoteTrackCount() const = 0;
+  virtual nsresult GetRemoteTrack(size_t index,
+                                  RefPtr<JsepTrack>* track) const = 0;
+
+  // Access the negotiated track pairs.
+  virtual size_t GetNegotiatedTrackPairCount() const = 0;
+  virtual nsresult GetNegotiatedTrackPair(size_t index,
+                                          const JsepTrackPair** pair) const = 0;
+
+  // Access transports.
+  virtual size_t GetTransportCount() const = 0;
+  virtual nsresult GetTransport(size_t index,
+                                RefPtr<JsepTransport>* transport) const = 0;
+
+  // Basic JSEP operations.
+  virtual nsresult CreateOffer(const JsepOfferOptions& options,
+                               std::string* offer) = 0;
+  virtual nsresult CreateAnswer(const JsepAnswerOptions& options,
+                                std::string* answer) = 0;
+  virtual std::string GetLocalDescription() const = 0;
+  virtual std::string GetRemoteDescription() const = 0;
+  virtual nsresult SetLocalDescription(JsepSdpType type,
+                                       const std::string& sdp) = 0;
+  virtual nsresult SetRemoteDescription(JsepSdpType type,
+                                        const std::string& sdp) = 0;
+  virtual nsresult AddRemoteIceCandidate(const std::string& candidate,
+                                         const std::string& mid,
+                                         uint16_t level) = 0;
+  virtual nsresult AddLocalIceCandidate(const std::string& candidate,
+                                        const std::string& mid,
+                                        uint16_t level) = 0;
+  virtual nsresult EndOfLocalCandidates(const std::string& defaultCandidateAddr,
+                                        uint16_t defaultCandidatePort,
+                                        uint16_t level) = 0;
+  virtual nsresult Close() = 0;
+
+  // ICE controlling or controlled
+  virtual bool IsIceControlling() const = 0;
+
+  virtual const std::string
+  GetLastError() const
+  {
+    return "Error";
+  }
+
+  static const char*
+  GetStateStr(JsepSignalingState state)
+  {
+    static const char* states[] = { "stable", "have-local-offer",
+                                    "have-remote-offer", "have-local-pranswer",
+                                    "have-remote-pranswer", "closed" };
+
+    return states[state];
+  }
+
+protected:
+  const std::string mName;
+  JsepSignalingState mState;
+};
+
+} // namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
@@ -0,0 +1,1655 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "logging.h"
+
+#include "signaling/src/jsep/JsepSessionImpl.h"
+#include <string>
+#include <stdlib.h>
+
+#include "nspr.h"
+#include "nss.h"
+#include "pk11pub.h"
+#include "nsDebug.h"
+
+#include <mozilla/Move.h>
+#include <mozilla/UniquePtr.h>
+
+#include "signaling/src/jsep/JsepTrack.h"
+#include "signaling/src/jsep/JsepTrack.h"
+#include "signaling/src/jsep/JsepTrackImpl.h"
+#include "signaling/src/jsep/JsepTransport.h"
+#include "signaling/src/sdp/Sdp.h"
+#include "signaling/src/sdp/SipccSdp.h"
+#include "signaling/src/sdp/SipccSdpParser.h"
+
+namespace mozilla {
+
+MOZ_MTLOG_MODULE("jsep")
+
+#define JSEP_SET_ERROR(error)                                                  \
+  do {                                                                         \
+    std::ostringstream os;                                                     \
+    os << error;                                                               \
+    mLastError = os.str();                                                     \
+    MOZ_MTLOG(ML_ERROR, mLastError);                                           \
+  } while (0);
+
+JsepSessionImpl::~JsepSessionImpl()
+{
+  for (auto i = mCodecs.begin(); i != mCodecs.end(); ++i) {
+    delete *i;
+  }
+}
+
+nsresult
+JsepSessionImpl::Init()
+{
+  mLastError.clear();
+
+  MOZ_ASSERT(!mSessionId, "Init called more than once");
+
+  SECStatus rv = PK11_GenerateRandom(
+      reinterpret_cast<unsigned char*>(&mSessionId), sizeof(mSessionId));
+  // RFC 3264 says that session-ids MUST be representable as a _signed_
+  // 64 bit number, meaning the MSB cannot be set.
+  mSessionId = mSessionId >> 1;
+  if (rv != SECSuccess) {
+    JSEP_SET_ERROR("Failed to generate session id: " << rv);
+    return NS_ERROR_FAILURE;
+  }
+
+  if (!mUuidGen->Generate(&mDefaultRemoteStreamId)) {
+    JSEP_SET_ERROR("Failed to generate default uuid for streams");
+    return NS_ERROR_FAILURE;
+  }
+
+  SetupDefaultCodecs();
+  SetupDefaultRtpExtensions();
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::AddTrack(const RefPtr<JsepTrack>& track)
+{
+  mLastError.clear();
+  MOZ_ASSERT(track->GetDirection() == JsepTrack::kJsepTrackSending);
+
+  JsepSendingTrack strack;
+  strack.mTrack = track;
+
+  mLocalTracks.push_back(strack);
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::SetIceCredentials(const std::string& ufrag,
+                                   const std::string& pwd)
+{
+  mLastError.clear();
+  mIceUfrag = ufrag;
+  mIcePwd = pwd;
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::AddDtlsFingerprint(const std::string& algorithm,
+                                    const std::vector<uint8_t>& value)
+{
+  mLastError.clear();
+  JsepDtlsFingerprint fp;
+
+  fp.mAlgorithm = algorithm;
+  fp.mValue = value;
+
+  mDtlsFingerprints.push_back(fp);
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::AddAudioRtpExtension(const std::string& extensionName)
+{
+  mLastError.clear();
+
+  if (mAudioRtpExtensions.size() + 1 > UINT16_MAX) {
+    JSEP_SET_ERROR("Too many audio rtp extensions have been added");
+    return NS_ERROR_FAILURE;
+  }
+
+  mAudioRtpExtensions.push_back(
+      { static_cast<uint16_t>(mAudioRtpExtensions.size() + 1),
+        SdpDirectionAttribute::kSendrecv,
+        false, // don't actually specify direction
+        extensionName, "" });
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::AddVideoRtpExtension(const std::string& extensionName)
+{
+  mLastError.clear();
+
+  if (mVideoRtpExtensions.size() + 1 > UINT16_MAX) {
+    JSEP_SET_ERROR("Too many video rtp extensions have been added");
+    return NS_ERROR_FAILURE;
+  }
+
+  mVideoRtpExtensions.push_back(
+      { static_cast<uint16_t>(mVideoRtpExtensions.size() + 1),
+        SdpDirectionAttribute::kSendrecv,
+        false, // don't actually specify direction
+        extensionName, "" });
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::GetLocalTrack(size_t index, RefPtr<JsepTrack>* track) const
+{
+  if (index >= mLocalTracks.size()) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  *track = mLocalTracks[index].mTrack;
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::GetRemoteTrack(size_t index, RefPtr<JsepTrack>* track) const
+{
+  if (index >= mRemoteTracks.size()) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  *track = mRemoteTracks[index].mTrack;
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::AddOfferMSectionsByType(SdpMediaSection::MediaType mediatype,
+                                         Maybe<size_t> offerToReceive,
+                                         Sdp* sdp)
+{
+
+  // TODO(bug 1094447): Use kUdpTlsRtpSavpf once it interops well
+  SdpMediaSection::Protocol proto = SdpMediaSection::kRtpSavpf;
+
+  if (mediatype == SdpMediaSection::kApplication) {
+    proto = SdpMediaSection::kDtlsSctp;
+  }
+
+  size_t added = 0;
+
+  for (auto track = mLocalTracks.begin(); track != mLocalTracks.end();
+       ++track) {
+    if (mediatype != track->mTrack->GetMediaType()) {
+      continue;
+    }
+
+    SdpDirectionAttribute::Direction dir = SdpDirectionAttribute::kSendrecv;
+
+    ++added;
+
+    // spec says that if offerToReceive(Audio|Video) is set lower than the
+    // number of tracks, the remaining m-lines are sendonly
+    if (offerToReceive.isSome() && added > *offerToReceive) {
+      dir = SdpDirectionAttribute::kSendonly;
+    }
+
+    nsresult rv = CreateOfferMSection(mediatype, dir, proto, sdp);
+
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    track->mAssignedMLine = Some(sdp->GetMediaSectionCount() - 1);
+  }
+
+  while (offerToReceive.isSome() && added < *offerToReceive) {
+    nsresult rv = CreateOfferMSection(
+        mediatype, SdpDirectionAttribute::kRecvonly, proto, sdp);
+
+    NS_ENSURE_SUCCESS(rv, rv);
+    ++added;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::CreateOffer(const JsepOfferOptions& options,
+                             std::string* offer)
+{
+  mLastError.clear();
+
+  switch (mState) {
+    case kJsepStateStable:
+      break;
+    default:
+      JSEP_SET_ERROR("Cannot create offer in state " << GetStateStr(mState));
+      return NS_ERROR_UNEXPECTED;
+  }
+
+  UniquePtr<Sdp> sdp;
+
+  // Make the basic SDP that is common to offer/answer.
+  nsresult rv = CreateGenericSDP(&sdp);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // TODO(bug 1017888): When doing renegotiation, we need to make sure we
+  // keep the old m-lines in the same order.
+
+  // Now add all the m-lines that we are attempting to negotiate.
+  // First audio, then video, then datachannel, for interop
+  rv = AddOfferMSectionsByType(
+      SdpMediaSection::kAudio, options.mOfferToReceiveAudio, sdp.get());
+
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = AddOfferMSectionsByType(
+      SdpMediaSection::kVideo, options.mOfferToReceiveVideo, sdp.get());
+
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (!(options.mDontOfferDataChannel.isSome() &&
+        *options.mDontOfferDataChannel)) {
+    rv = AddOfferMSectionsByType(
+        SdpMediaSection::kApplication, Maybe<size_t>(), sdp.get());
+
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  if (!sdp->GetMediaSectionCount()) {
+    JSEP_SET_ERROR("Cannot create an offer with no local tracks, "
+                   "no offerToReceiveAudio/Video, and no DataChannel.");
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  *offer = sdp->ToString();
+  mGeneratedLocalDescription = Move(sdp);
+
+  return NS_OK;
+}
+
+std::string
+JsepSessionImpl::GetLocalDescription() const
+{
+  std::ostringstream os;
+  if (mPendingLocalDescription) {
+    mPendingLocalDescription->Serialize(os);
+  } else if (mCurrentLocalDescription) {
+    mCurrentLocalDescription->Serialize(os);
+  }
+  return os.str();
+}
+
+std::string
+JsepSessionImpl::GetRemoteDescription() const
+{
+  std::ostringstream os;
+  if (mPendingRemoteDescription) {
+    mPendingRemoteDescription->Serialize(os);
+  } else if (mCurrentRemoteDescription) {
+    mCurrentRemoteDescription->Serialize(os);
+  }
+  return os.str();
+}
+
+void
+JsepSessionImpl::AddCodecs(SdpMediaSection* msection) const
+{
+  for (auto c = mCodecs.begin(); c != mCodecs.end(); ++c) {
+    (*c)->AddToMediaSection(*msection);
+  }
+}
+
+void
+JsepSessionImpl::AddExtmap(SdpMediaSection* msection) const
+{
+  const auto* extensions = GetRtpExtensions(msection->GetMediaType());
+
+  if (extensions && !extensions->empty()) {
+    SdpExtmapAttributeList* extmap = new SdpExtmapAttributeList;
+    extmap->mExtmaps = *extensions;
+    msection->GetAttributeList().SetAttribute(extmap);
+  }
+}
+
+JsepCodecDescription*
+JsepSessionImpl::FindMatchingCodec(const std::string& fmt,
+                                   const SdpMediaSection& msection) const
+{
+  for (auto c = mCodecs.begin(); c != mCodecs.end(); ++c) {
+    auto codec = *c;
+    if (codec->mEnabled && codec->Matches(fmt, msection)) {
+      return codec;
+    }
+  }
+
+  return nullptr;
+}
+
+const std::vector<SdpExtmapAttributeList::Extmap>*
+JsepSessionImpl::GetRtpExtensions(SdpMediaSection::MediaType type) const
+{
+  switch (type) {
+    case SdpMediaSection::kAudio:
+      return &mAudioRtpExtensions;
+    case SdpMediaSection::kVideo:
+      return &mVideoRtpExtensions;
+    default:
+      return nullptr;
+  }
+}
+
+void
+JsepSessionImpl::AddCommonCodecs(const SdpMediaSection& remoteMsection,
+                                 SdpMediaSection* msection)
+{
+  const std::vector<std::string>& formats = remoteMsection.GetFormats();
+
+  for (auto fmt = formats.begin(); fmt != formats.end(); ++fmt) {
+    JsepCodecDescription* codec = FindMatchingCodec(*fmt, remoteMsection);
+    if (codec) {
+      codec->mDefaultPt = *fmt; // Reflect the other side's PT
+      codec->AddToMediaSection(*msection);
+      // TODO(bug 1099351): Once bug 1073475 is fixed on all supported
+      // versions, we can remove this limitation.
+      break;
+    }
+  }
+}
+
+void
+JsepSessionImpl::AddCommonExtmaps(const SdpMediaSection& remoteMsection,
+                                  SdpMediaSection* msection)
+{
+  if (!remoteMsection.GetAttributeList().HasAttribute(
+        SdpAttribute::kExtmapAttribute)) {
+    return;
+  }
+
+  auto* ourExtensions = GetRtpExtensions(remoteMsection.GetMediaType());
+
+  if (!ourExtensions) {
+    return;
+  }
+
+  UniquePtr<SdpExtmapAttributeList> ourExtmap(new SdpExtmapAttributeList);
+  auto& theirExtmap = remoteMsection.GetAttributeList().GetExtmap().mExtmaps;
+  for (auto i = theirExtmap.begin(); i != theirExtmap.end(); ++i) {
+    for (auto j = ourExtensions->begin(); j != ourExtensions->end(); ++j) {
+      if (i->extensionname == j->extensionname) {
+        ourExtmap->mExtmaps.push_back(*i);
+
+        // RFC 5285 says that ids >= 4096 can be used by the offerer to
+        // force the answerer to pick, otherwise the value in the offer is
+        // used.
+        if (ourExtmap->mExtmaps.back().entry >= 4096) {
+          ourExtmap->mExtmaps.back().entry = j->entry;
+        }
+      } 
+    }
+  }
+
+  if (!ourExtmap->mExtmaps.empty()) {
+    msection->GetAttributeList().SetAttribute(ourExtmap.release());
+  }
+}
+
+nsresult
+JsepSessionImpl::CreateAnswer(const JsepAnswerOptions& options,
+                              std::string* answer)
+{
+  mLastError.clear();
+
+  switch (mState) {
+    case kJsepStateHaveRemoteOffer:
+      break;
+    default:
+      JSEP_SET_ERROR("Cannot create answer in state " << GetStateStr(mState));
+      return NS_ERROR_UNEXPECTED;
+  }
+
+  // This is the heart of the negotiation code. Depressing that it's
+  // so bad.
+  //
+  // Here's the current algorithm:
+  // 1. Walk through all the m-lines on the other side.
+  // 2. For each m-line, walk through all of our local tracks
+  //    in sequence and see if any are unassigned. If so, assign
+  //    them and mark it sendrecv, otherwise it's recvonly.
+  // 3. Just replicate their media attributes.
+  // 4. Profit.
+  UniquePtr<Sdp> sdp;
+
+  // Make the basic SDP that is common to offer/answer.
+  nsresult rv = CreateGenericSDP(&sdp);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  const Sdp& offer = *mPendingRemoteDescription;
+
+  size_t numMsections = offer.GetMediaSectionCount();
+
+  for (size_t i = 0; i < numMsections; ++i) {
+    const SdpMediaSection& remoteMsection = offer.GetMediaSection(i);
+    SdpMediaSection& msection =
+        sdp->AddMediaSection(remoteMsection.GetMediaType(),
+                             SdpDirectionAttribute::kSendrecv,
+                             9,
+                             remoteMsection.GetProtocol(),
+                             sdp::kIPv4,
+                             "0.0.0.0");
+
+    rv = CreateAnswerMSection(options, i, remoteMsection, &msection, sdp.get());
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  *answer = sdp->ToString();
+  mGeneratedLocalDescription = Move(sdp);
+
+  return NS_OK;
+}
+
+static bool
+HasRtcp(SdpMediaSection::Protocol proto)
+{
+  switch (proto) {
+    case SdpMediaSection::kRtpAvpf:
+    case SdpMediaSection::kDccpRtpAvpf:
+    case SdpMediaSection::kDccpRtpSavpf:
+    case SdpMediaSection::kRtpSavpf:
+    case SdpMediaSection::kUdpTlsRtpSavpf:
+    case SdpMediaSection::kTcpTlsRtpSavpf:
+    case SdpMediaSection::kDccpTlsRtpSavpf:
+      return true;
+    case SdpMediaSection::kRtpAvp:
+    case SdpMediaSection::kUdp:
+    case SdpMediaSection::kVat:
+    case SdpMediaSection::kRtp:
+    case SdpMediaSection::kUdptl:
+    case SdpMediaSection::kTcp:
+    case SdpMediaSection::kTcpRtpAvp:
+    case SdpMediaSection::kRtpSavp:
+    case SdpMediaSection::kTcpBfcp:
+    case SdpMediaSection::kTcpTlsBfcp:
+    case SdpMediaSection::kTcpTls:
+    case SdpMediaSection::kFluteUdp:
+    case SdpMediaSection::kTcpMsrp:
+    case SdpMediaSection::kTcpTlsMsrp:
+    case SdpMediaSection::kDccp:
+    case SdpMediaSection::kDccpRtpAvp:
+    case SdpMediaSection::kDccpRtpSavp:
+    case SdpMediaSection::kUdpTlsRtpSavp:
+    case SdpMediaSection::kTcpTlsRtpSavp:
+    case SdpMediaSection::kDccpTlsRtpSavp:
+    case SdpMediaSection::kUdpMbmsFecRtpAvp:
+    case SdpMediaSection::kUdpMbmsFecRtpSavp:
+    case SdpMediaSection::kUdpMbmsRepair:
+    case SdpMediaSection::kFecUdp:
+    case SdpMediaSection::kUdpFec:
+    case SdpMediaSection::kTcpMrcpv2:
+    case SdpMediaSection::kTcpTlsMrcpv2:
+    case SdpMediaSection::kPstn:
+    case SdpMediaSection::kUdpTlsUdptl:
+    case SdpMediaSection::kSctp:
+    case SdpMediaSection::kSctpDtls:
+    case SdpMediaSection::kDtlsSctp:
+      return false;
+  }
+  MOZ_CRASH("Unknown protocol, probably corruption.");
+}
+
+nsresult
+JsepSessionImpl::CreateOfferMSection(SdpMediaSection::MediaType mediatype,
+                                     SdpDirectionAttribute::Direction dir,
+                                     SdpMediaSection::Protocol proto,
+                                     Sdp* sdp)
+{
+
+  nsresult rv;
+
+  SdpMediaSection* msection =
+      &sdp->AddMediaSection(mediatype, dir, 9, proto, sdp::kIPv4, "0.0.0.0");
+
+  // We don't do this in AddTransportAttributes because that is also used for
+  // making answers, and we don't want to unconditionally set rtcp-mux there.
+  if (HasRtcp(proto)) {
+    // Set RTCP-MUX.
+    msection->GetAttributeList().SetAttribute(
+        new SdpFlagAttribute(SdpAttribute::kRtcpMuxAttribute));
+  }
+
+  rv = AddTransportAttributes(msection, SdpSetupAttribute::kActpass);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  AddCodecs(msection);
+
+  AddExtmap(msection);
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::CreateAnswerMSection(const JsepAnswerOptions& options,
+                                      size_t mlineIndex,
+                                      const SdpMediaSection& remoteMsection,
+                                      SdpMediaSection* msection,
+                                      Sdp* sdp)
+{
+  SdpSetupAttribute::Role role;
+  nsresult rv = DetermineAnswererSetupRole(remoteMsection, &role);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = AddTransportAttributes(msection, role);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  SdpDirectionAttribute::Direction remoteDirection =
+      remoteMsection.GetDirectionAttribute().mValue;
+  SdpDirectionAttribute::Direction localDirection =
+      SdpDirectionAttribute::kInactive;
+
+  // Only attempt to match up local tracks if the offerer has elected to
+  // receive traffic.
+  if (remoteDirection & SdpDirectionAttribute::kRecvFlag) {
+    bool found = false;
+    for (auto track = mLocalTracks.begin(); track != mLocalTracks.end();
+         ++track) {
+      if (track->mAssignedMLine.isSome())
+        continue;
+
+      // TODO(bug 1095218): Pay attention to msid
+      if (track->mTrack->GetMediaType() != remoteMsection.GetMediaType())
+        continue;
+
+      localDirection = SdpDirectionAttribute::kSendonly;
+      track->mAssignedMLine = Some(mlineIndex);
+      found = true;
+      break;
+    }
+
+    if (!found &&
+        remoteMsection.GetMediaType() == SdpMediaSection::kApplication) {
+      // If we are offered datachannel, we need to play along even if no track
+      // for it has been added yet.
+      std::string streamId;
+      std::string trackId;
+
+      if (!mUuidGen->Generate(&streamId) || !mUuidGen->Generate(&trackId)) {
+        JSEP_SET_ERROR("Failed to generate UUIDs for datachannel track");
+        return NS_ERROR_FAILURE;
+      }
+
+      AddTrack(RefPtr<JsepTrack>(
+          new JsepTrack(SdpMediaSection::kApplication, streamId, trackId)));
+      localDirection = SdpDirectionAttribute::kSendonly;
+      mLocalTracks.back().mAssignedMLine = Some(mlineIndex);
+      found = true;
+    }
+  }
+
+  if (remoteDirection & SdpDirectionAttribute::kSendFlag) {
+    localDirection = static_cast<SdpDirectionAttribute::Direction>(
+        localDirection | SdpDirectionAttribute::kRecvFlag);
+  }
+
+  msection->GetAttributeList().SetAttribute(
+      new SdpDirectionAttribute(localDirection));
+
+  if (remoteMsection.GetAttributeList().HasAttribute(
+          SdpAttribute::kRtcpMuxAttribute)) {
+    // If we aren't using a protocol with RTCP, just smile and nod.
+    msection->GetAttributeList().SetAttribute(
+        new SdpFlagAttribute(SdpAttribute::kRtcpMuxAttribute));
+  }
+
+  // Now add the codecs.
+  AddCommonCodecs(remoteMsection, msection);
+
+  // Add extmap attributes.
+  AddCommonExtmaps(remoteMsection, msection);
+
+  if (msection->GetFormats().empty()) {
+    // Could not negotiate anything. Disable m-section.
+
+    // Clear out attributes.
+    msection->GetAttributeList().Clear();
+
+    // We need to have something here to fit the grammar
+    // TODO(bcampen@mozilla.com): What's the accepted way of doing this? Just
+    // add the codecs we do support? Does it matter?
+    msection->AddCodec("111", "NULL", 0, 0);
+
+    auto* direction =
+        new SdpDirectionAttribute(SdpDirectionAttribute::kInactive);
+    msection->GetAttributeList().SetAttribute(direction);
+    msection->SetPort(0);
+  }
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::DetermineAnswererSetupRole(
+    const SdpMediaSection& remoteMsection,
+    SdpSetupAttribute::Role* rolep)
+{
+  // Determine the role.
+  // RFC 5763 says:
+  //
+  //   The endpoint MUST use the setup attribute defined in [RFC4145].
+  //   The endpoint that is the offerer MUST use the setup attribute
+  //   value of setup:actpass and be prepared to receive a client_hello
+  //   before it receives the answer.  The answerer MUST use either a
+  //   setup attribute value of setup:active or setup:passive.  Note that
+  //   if the answerer uses setup:passive, then the DTLS handshake will
+  //   not begin until the answerer is received, which adds additional
+  //   latency. setup:active allows the answer and the DTLS handshake to
+  //   occur in parallel.  Thus, setup:active is RECOMMENDED.  Whichever
+  //   party is active MUST initiate a DTLS handshake by sending a
+  //   ClientHello over each flow (host/port quartet).
+  //
+  //   We default to assuming that the offerer is passive and we are active.
+  SdpSetupAttribute::Role role = SdpSetupAttribute::kActive;
+
+  if (remoteMsection.GetAttributeList().HasAttribute(
+          SdpAttribute::kSetupAttribute)) {
+    switch (remoteMsection.GetAttributeList().GetSetup().mRole) {
+      case SdpSetupAttribute::kActive:
+        role = SdpSetupAttribute::kPassive;
+        break;
+      case SdpSetupAttribute::kPassive:
+      case SdpSetupAttribute::kActpass:
+        role = SdpSetupAttribute::kActive;
+        break;
+      case SdpSetupAttribute::kHoldconn:
+        // This should have been caught by ParseSdp
+        MOZ_ASSERT(false);
+        JSEP_SET_ERROR("The other side used an illegal setup attribute"
+                       " (\"holdconn\").");
+        return NS_ERROR_INVALID_ARG;
+    }
+  }
+
+  *rolep = role;
+  return NS_OK;
+}
+
+static void
+appendSdpParseErrors(
+    const std::vector<std::pair<size_t, std::string> >& aErrors,
+    std::string* aErrorString)
+{
+  std::ostringstream os;
+  for (auto i = aErrors.begin(); i != aErrors.end(); ++i) {
+    os << "SDP Parse Error on line " << i->first << ": " + i->second
+       << std::endl;
+  }
+  *aErrorString += os.str();
+}
+
+nsresult
+JsepSessionImpl::SetLocalDescription(JsepSdpType type, const std::string& sdp)
+{
+  mLastError.clear();
+
+  switch (mState) {
+    case kJsepStateStable:
+      if (type != kJsepSdpOffer) {
+        JSEP_SET_ERROR("Cannot set local answer in state "
+                       << GetStateStr(mState));
+        return NS_ERROR_UNEXPECTED;
+      }
+      mIsOfferer = true;
+      break;
+    case kJsepStateHaveRemoteOffer:
+      if (type != kJsepSdpAnswer && type != kJsepSdpPranswer) {
+        JSEP_SET_ERROR("Cannot set local offer in state "
+                       << GetStateStr(mState));
+        return NS_ERROR_UNEXPECTED;
+      }
+      break;
+    default:
+      JSEP_SET_ERROR("Cannot set local offer or answer in state "
+                     << GetStateStr(mState));
+      return NS_ERROR_UNEXPECTED;
+  }
+
+  UniquePtr<Sdp> parsed;
+  nsresult rv = ParseSdp(sdp, &parsed);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Check that content hasn't done anything unsupported with the SDP
+  rv = ValidateLocalDescription(*parsed);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Create transport objects.
+  size_t numMsections = parsed->GetMediaSectionCount();
+  for (size_t t = 0; t < numMsections; ++t) {
+    if (t < mTransports.size())
+      continue; // This transport already exists (assume we are renegotiating).
+
+    // TODO(bug 1016476): Deal with bundle-only and the like.
+    RefPtr<JsepTransport> transport;
+    nsresult rv = CreateTransport(parsed->GetMediaSection(t), &transport);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    mTransports.push_back(transport);
+  }
+
+  switch (type) {
+    case kJsepSdpOffer:
+      rv = SetLocalDescriptionOffer(Move(parsed));
+      break;
+    case kJsepSdpAnswer:
+    case kJsepSdpPranswer:
+      rv = SetLocalDescriptionAnswer(type, Move(parsed));
+      break;
+  }
+
+  return rv;
+}
+
+nsresult
+JsepSessionImpl::SetLocalDescriptionOffer(UniquePtr<Sdp> offer)
+{
+  MOZ_ASSERT(mState == kJsepStateStable);
+  mPendingLocalDescription = Move(offer);
+  SetState(kJsepStateHaveLocalOffer);
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::SetLocalDescriptionAnswer(JsepSdpType type,
+                                           UniquePtr<Sdp> answer)
+{
+  MOZ_ASSERT(mState == kJsepStateHaveRemoteOffer);
+  mPendingLocalDescription = Move(answer);
+
+  nsresult rv = HandleNegotiatedSession(mPendingLocalDescription,
+                                        mPendingRemoteDescription);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mCurrentRemoteDescription = Move(mPendingRemoteDescription);
+  mCurrentLocalDescription = Move(mPendingLocalDescription);
+
+  SetState(kJsepStateStable);
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::SetRemoteDescription(JsepSdpType type, const std::string& sdp)
+{
+  mLastError.clear();
+
+  MOZ_MTLOG(ML_DEBUG, "SetRemoteDescription type=" << type << "\nSDP=\n"
+                                                   << sdp);
+  switch (mState) {
+    case kJsepStateStable:
+      if (type != kJsepSdpOffer) {
+        JSEP_SET_ERROR("Cannot set remote answer in state "
+                       << GetStateStr(mState));
+        return NS_ERROR_UNEXPECTED;
+      }
+      mIsOfferer = false;
+      break;
+    case kJsepStateHaveLocalOffer:
+    case kJsepStateHaveRemotePranswer:
+      if (type != kJsepSdpAnswer && type != kJsepSdpPranswer) {
+        JSEP_SET_ERROR("Cannot set remote offer in state "
+                       << GetStateStr(mState));
+        return NS_ERROR_UNEXPECTED;
+      }
+      break;
+    default:
+      JSEP_SET_ERROR("Cannot set remote offer or answer in current state "
+                     << GetStateStr(mState));
+      return NS_ERROR_UNEXPECTED;
+  }
+
+  // Parse.
+  UniquePtr<Sdp> parsed;
+  nsresult rv = ParseSdp(sdp, &parsed);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  bool iceLite =
+      parsed->GetAttributeList().HasAttribute(SdpAttribute::kIceLiteAttribute);
+
+  std::vector<std::string> iceOptions;
+  if (parsed->GetAttributeList().HasAttribute(
+          SdpAttribute::kIceOptionsAttribute)) {
+    iceOptions = parsed->GetAttributeList().GetIceOptions().mValues;
+  }
+
+  switch (type) {
+    case kJsepSdpOffer:
+      rv = SetRemoteDescriptionOffer(Move(parsed));
+      break;
+    case kJsepSdpAnswer:
+    case kJsepSdpPranswer:
+      rv = SetRemoteDescriptionAnswer(type, Move(parsed));
+      break;
+  }
+
+  if (NS_SUCCEEDED(rv)) {
+    mRemoteIsIceLite = iceLite;
+    mIceOptions = iceOptions;
+  }
+
+  return rv;
+}
+
+// Helper function to find the track for a given m= section.
+template <class T>
+nsresult
+FindTrackForMSection(const SdpMediaSection& msection,
+                     const std::vector<T>& tracks,
+                     size_t mLine,
+                     RefPtr<JsepTrack>* mst)
+{
+  for (auto t = tracks.begin(); t != tracks.end(); ++t) {
+    if (t->mAssignedMLine.isSome() &&
+        (*t->mAssignedMLine == msection.GetLevel())) {
+      *mst = t->mTrack;
+      return NS_OK;
+    }
+  }
+
+  return NS_ERROR_NOT_AVAILABLE;
+}
+
+nsresult
+JsepSessionImpl::HandleNegotiatedSession(const UniquePtr<Sdp>& local,
+                                         const UniquePtr<Sdp>& remote)
+{
+  bool remoteIceLite =
+      remote->GetAttributeList().HasAttribute(SdpAttribute::kIceLiteAttribute);
+
+  mIceControlling = remoteIceLite || mIsOfferer;
+
+  if (local->GetMediaSectionCount() != remote->GetMediaSectionCount()) {
+    JSEP_SET_ERROR("Local and remote SDP have different number of m-lines "
+                   << "(" << local->GetMediaSectionCount() << " vs "
+                   << remote->GetMediaSectionCount() << ")");
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  std::vector<JsepTrackPair> trackPairs;
+
+  // Now walk through the m-sections, make sure they match, and create
+  // track pairs that describe the media to be set up.
+  for (size_t i = 0; i < local->GetMediaSectionCount(); ++i) {
+    const SdpMediaSection& lm = local->GetMediaSection(i);
+    const SdpMediaSection& rm = remote->GetMediaSection(i);
+    const SdpMediaSection& offer = mIsOfferer ? lm : rm;
+    const SdpMediaSection& answer = mIsOfferer ? rm : lm;
+
+    if (lm.GetMediaType() != rm.GetMediaType()) {
+      JSEP_SET_ERROR("Answer and offer have different media types at m-line "
+                     << i);
+      return NS_ERROR_INVALID_ARG;
+    }
+
+    RefPtr<JsepTransport> transport;
+
+    // Transports are created in SetLocal.
+    // TODO(bug 1016476): This will need to be changed for bundle.
+    MOZ_ASSERT(mTransports.size() > i);
+    if (mTransports.size() < i) {
+      JSEP_SET_ERROR("Fewer transports set up than m-lines");
+      return NS_ERROR_FAILURE;
+    }
+    transport = mTransports[i];
+
+    // If the answer says it's inactive we're not doing anything with it.
+    // TODO(bug 1017888): Need to handle renegotiation somehow.
+    // Note: the SDP engine guarantees the presence of an SDP direction
+    // attribute (with sendrecv as the default if one isn't in the SDP).
+    if (answer.GetDirectionAttribute().mValue ==
+            SdpDirectionAttribute::kInactive &&
+        answer.GetPort() == 0) {
+      transport->mState = JsepTransport::kJsepTransportClosed;
+      continue;
+    }
+
+    bool sending;
+    bool receiving;
+
+    nsresult rv = DetermineSendingDirection(
+        offer.GetDirectionAttribute().mValue,
+        answer.GetDirectionAttribute().mValue, &sending, &receiving);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    MOZ_MTLOG(ML_DEBUG, "Negotiated m= line"
+                            << " index=" << i << " type=" << lm.GetMediaType()
+                            << " sending=" << sending
+                            << " receiving=" << receiving);
+
+    JsepTrackPair jpair;
+
+    // TODO(bug 1016476): Set the bundle level.
+    jpair.mLevel = i;
+
+    RefPtr<JsepTrack> track;
+    if (sending) {
+      rv = FindTrackForMSection(lm, mLocalTracks, i, &track);
+      if (NS_FAILED(rv)) {
+        JSEP_SET_ERROR("Failed to find local track for level " << i
+                       << " in local SDP. This should never happen.");
+        NS_ASSERTION(false, "Failed to find local track for level");
+        return NS_ERROR_FAILURE;
+      }
+
+      rv = NegotiateTrack(rm, lm, JsepTrack::kJsepTrackSending, &track);
+      NS_ENSURE_SUCCESS(rv, rv);
+
+      jpair.mSending = track;
+    }
+
+    if (receiving) {
+      rv = FindTrackForMSection(lm, mRemoteTracks, i, &track);
+      if (NS_FAILED(rv)) {
+        JSEP_SET_ERROR("Failed to find remote track for level " << i
+                       << " in remote SDP. This should never happen.");
+        NS_ASSERTION(false, "Failed to find remote track for level");
+        return NS_ERROR_FAILURE;
+      }
+
+      rv = NegotiateTrack(rm, lm, JsepTrack::kJsepTrackReceiving, &track);
+      NS_ENSURE_SUCCESS(rv, rv);
+
+      jpair.mReceiving = track;
+    }
+
+    rv = SetupTransport(
+        rm.GetAttributeList(), answer.GetAttributeList(), transport);
+    NS_ENSURE_SUCCESS(rv, rv);
+    jpair.mRtpTransport = transport;
+
+    if (HasRtcp(lm.GetProtocol())) {
+      // RTCP MUX or not.
+      // TODO(bug 1095743): verify that the PTs are consistent with mux.
+      if (offer.GetAttributeList().HasAttribute(
+              SdpAttribute::kRtcpMuxAttribute) &&
+          answer.GetAttributeList().HasAttribute(
+              SdpAttribute::kRtcpMuxAttribute)) {
+        jpair.mRtcpTransport = nullptr; // We agree on mux.
+        MOZ_MTLOG(ML_DEBUG, "RTCP-MUX is on");
+      } else {
+        MOZ_MTLOG(ML_DEBUG, "RTCP-MUX is off");
+        rv = SetupTransport(
+            rm.GetAttributeList(), answer.GetAttributeList(), transport);
+        NS_ENSURE_SUCCESS(rv, rv);
+
+        jpair.mRtcpTransport = transport;
+      }
+    }
+
+    trackPairs.push_back(jpair);
+  }
+
+  // Ouch, this probably needs some dirty bit instead of just clearing
+  // stuff for renegotiation.
+  mNegotiatedTrackPairs = trackPairs;
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::NegotiateTrack(const SdpMediaSection& remoteMsection,
+                                const SdpMediaSection& localMsection,
+                                JsepTrack::Direction direction,
+                                RefPtr<JsepTrack>* track)
+{
+  UniquePtr<JsepTrackNegotiatedDetailsImpl> negotiatedDetails =
+      MakeUnique<JsepTrackNegotiatedDetailsImpl>();
+  negotiatedDetails->mProtocol = remoteMsection.GetProtocol();
+
+  // Insert all the codecs we jointly support.
+  const std::vector<std::string>& formats = remoteMsection.GetFormats();
+
+  for (auto fmt = formats.begin(); fmt != formats.end(); ++fmt) {
+    JsepCodecDescription* codec = FindMatchingCodec(*fmt, remoteMsection);
+
+    if (!codec) {
+      continue;
+    }
+
+    bool sending = (direction == JsepTrack::kJsepTrackSending);
+
+    // We need to take the remote side's parameters into account so we can
+    // configure our send media.
+    // |codec| is assumed to have the necessary state about our own config
+    // in order to negotiate.
+    JsepCodecDescription* negotiated =
+        codec->MakeNegotiatedCodec(remoteMsection, *fmt, sending);
+
+    if (!negotiated) {
+      continue;
+    }
+
+    negotiatedDetails->mCodecs.push_back(negotiated);
+  }
+
+  if (negotiatedDetails->mCodecs.empty()) {
+    JSEP_SET_ERROR("Failed to negotiate codec details for all codecs");
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  auto& answerMsection = mIsOfferer ? remoteMsection : localMsection;
+
+  if (answerMsection.GetAttributeList().HasAttribute(
+        SdpAttribute::kExtmapAttribute)) {
+    auto& extmap = answerMsection.GetAttributeList().GetExtmap().mExtmaps;
+    for (auto i = extmap.begin(); i != extmap.end(); ++i) {
+      negotiatedDetails->mExtmap[i->extensionname] = *i;
+    }
+  }
+
+  (*track)->SetNegotiatedDetails(Move(negotiatedDetails));
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::CreateTransport(const SdpMediaSection& msection,
+                                 RefPtr<JsepTransport>* transport)
+{
+  size_t components;
+
+  if (HasRtcp(msection.GetProtocol())) {
+    components = 2;
+  } else {
+    components = 1;
+  }
+
+  *transport = new JsepTransport("transport-id", components);
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::SetupTransport(const SdpAttributeList& remote,
+                                const SdpAttributeList& answer,
+                                const RefPtr<JsepTransport>& transport)
+{
+  UniquePtr<JsepIceTransport> ice = MakeUnique<JsepIceTransport>();
+
+  // We do sanity-checking for these in ParseSdp
+  ice->mUfrag = remote.GetIceUfrag();
+  ice->mPwd = remote.GetIcePwd();
+  if (remote.HasAttribute(SdpAttribute::kCandidateAttribute)) {
+    ice->mCandidates = remote.GetCandidate();
+  }
+
+  // RFC 5763 says:
+  //
+  //   The endpoint MUST use the setup attribute defined in [RFC4145].
+  //   The endpoint that is the offerer MUST use the setup attribute
+  //   value of setup:actpass and be prepared to receive a client_hello
+  //   before it receives the answer.  The answerer MUST use either a
+  //   setup attribute value of setup:active or setup:passive.  Note that
+  //   if the answerer uses setup:passive, then the DTLS handshake will
+  //   not begin until the answerer is received, which adds additional
+  //   latency. setup:active allows the answer and the DTLS handshake to
+  //   occur in parallel.  Thus, setup:active is RECOMMENDED.  Whichever
+  //   party is active MUST initiate a DTLS handshake by sending a
+  //   ClientHello over each flow (host/port quartet).
+  UniquePtr<JsepDtlsTransport> dtls = MakeUnique<JsepDtlsTransport>();
+  dtls->mFingerprints = remote.GetFingerprint();
+  if (!answer.HasAttribute(mozilla::SdpAttribute::kSetupAttribute)) {
+    dtls->mRole = mIsOfferer ? JsepDtlsTransport::kJsepDtlsServer
+                             : JsepDtlsTransport::kJsepDtlsClient;
+  } else {
+    if (mIsOfferer) {
+      dtls->mRole = (answer.GetSetup().mRole == SdpSetupAttribute::kActive)
+                        ? JsepDtlsTransport::kJsepDtlsServer
+                        : JsepDtlsTransport::kJsepDtlsClient;
+    } else {
+      dtls->mRole = (answer.GetSetup().mRole == SdpSetupAttribute::kActive)
+                        ? JsepDtlsTransport::kJsepDtlsClient
+                        : JsepDtlsTransport::kJsepDtlsServer;
+    }
+  }
+
+  transport->mIce = Move(ice);
+  transport->mDtls = Move(dtls);
+
+  // TODO(bug 1016476): If we are doing bundle, and this is not the bundle
+  // level, we should be marking this Closed, right?
+
+  if (answer.HasAttribute(SdpAttribute::kRtcpMuxAttribute)) {
+    transport->mComponents = 1;
+  }
+
+  transport->mState = JsepTransport::kJsepTransportAccepted;
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::DetermineSendingDirection(
+    SdpDirectionAttribute::Direction offer,
+    SdpDirectionAttribute::Direction answer, bool* sending, bool* receiving)
+{
+
+  if (!(offer & SdpDirectionAttribute::kSendFlag) &&
+      answer & SdpDirectionAttribute::kRecvFlag) {
+    JSEP_SET_ERROR("Answer tried to set recv when offer did not set send");
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  if (!(offer & SdpDirectionAttribute::kRecvFlag) &&
+      answer & SdpDirectionAttribute::kSendFlag) {
+    JSEP_SET_ERROR("Answer tried to set send when offer did not set recv");
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  if (mIsOfferer) {
+    *receiving = answer & SdpDirectionAttribute::kSendFlag;
+    *sending = answer & SdpDirectionAttribute::kRecvFlag;
+  } else {
+    *sending = answer & SdpDirectionAttribute::kSendFlag;
+    *receiving = answer & SdpDirectionAttribute::kRecvFlag;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::AddTransportAttributes(SdpMediaSection* msection,
+                                        SdpSetupAttribute::Role dtlsRole)
+{
+  if (mIceUfrag.empty() || mIcePwd.empty()) {
+    JSEP_SET_ERROR("Missing ICE ufrag or password");
+    return NS_ERROR_FAILURE;
+  }
+
+  SdpAttributeList& attrList = msection->GetAttributeList();
+  attrList.SetAttribute(
+      new SdpStringAttribute(SdpAttribute::kIceUfragAttribute, mIceUfrag));
+  attrList.SetAttribute(
+      new SdpStringAttribute(SdpAttribute::kIcePwdAttribute, mIcePwd));
+
+  msection->GetAttributeList().SetAttribute(new SdpSetupAttribute(dtlsRole));
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::ParseSdp(const std::string& sdp, UniquePtr<Sdp>* parsedp)
+{
+  UniquePtr<Sdp> parsed = mParser.Parse(sdp);
+  if (!parsed) {
+    std::string error = "Failed to parse SDP: ";
+    appendSdpParseErrors(mParser.GetParseErrors(), &error);
+    JSEP_SET_ERROR(error);
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  // Verify that the JSEP rules for all SDP are followed
+  if (!parsed->GetMediaSectionCount()) {
+    JSEP_SET_ERROR("Description has no media sections");
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  for (size_t i = 0; i < parsed->GetMediaSectionCount(); ++i) {
+    if (parsed->GetMediaSection(i).GetPort() == 0) {
+      // Disabled, let this stuff slide.
+      continue;
+    }
+
+    auto& mediaAttrs = parsed->GetMediaSection(i).GetAttributeList();
+
+    if (mediaAttrs.GetIceUfrag().empty()) {
+      JSEP_SET_ERROR("Invalid description, no ice-ufrag attribute");
+      return NS_ERROR_INVALID_ARG;
+    }
+
+    if (mediaAttrs.GetIcePwd().empty()) {
+      JSEP_SET_ERROR("Invalid description, no ice-pwd attribute");
+      return NS_ERROR_INVALID_ARG;
+    }
+
+    if (!mediaAttrs.HasAttribute(SdpAttribute::kFingerprintAttribute)) {
+      JSEP_SET_ERROR("Invalid description, no fingerprint attribute");
+      return NS_ERROR_INVALID_ARG;
+    }
+
+    if (mediaAttrs.HasAttribute(SdpAttribute::kSetupAttribute) &&
+        mediaAttrs.GetSetup().mRole == SdpSetupAttribute::kHoldconn) {
+      JSEP_SET_ERROR("Description has illegal setup attribute "
+                     "\"holdconn\" at level "
+                     << i);
+      return NS_ERROR_INVALID_ARG;
+    }
+
+    auto& formats = parsed->GetMediaSection(i).GetFormats();
+    for (auto f = formats.begin(); f != formats.end(); ++f) {
+      uint16_t pt;
+      if (!JsepCodecDescription::GetPtAsInt(*f, &pt)) {
+        JSEP_SET_ERROR("Payload type \""
+                       << *f << "\" is not a 16-bit unsigned int at level "
+                       << i);
+        return NS_ERROR_INVALID_ARG;
+      }
+    }
+  }
+
+  *parsedp = Move(parsed);
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::SetRemoteDescriptionOffer(UniquePtr<Sdp> offer)
+{
+  MOZ_ASSERT(mState == kJsepStateStable);
+
+  // TODO(bug 1095780): Note that we create remote tracks even when
+  // They contain only codecs we can't negotiate or other craziness.
+  nsresult rv = SetRemoteTracksFromDescription(*offer);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mPendingRemoteDescription = Move(offer);
+
+  SetState(kJsepStateHaveRemoteOffer);
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::SetRemoteDescriptionAnswer(JsepSdpType type,
+                                            UniquePtr<Sdp> answer)
+{
+  MOZ_ASSERT(mState == kJsepStateHaveLocalOffer ||
+             mState == kJsepStateHaveRemotePranswer);
+  mPendingRemoteDescription = Move(answer);
+
+  // TODO(bug 1095780): Note that this creates remote tracks even if
+  // we offered sendonly and other side offered sendrecv or recvonly.
+  nsresult rv = SetRemoteTracksFromDescription(*mPendingRemoteDescription);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = HandleNegotiatedSession(mPendingLocalDescription,
+                               mPendingRemoteDescription);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  mCurrentRemoteDescription = Move(mPendingRemoteDescription);
+  mCurrentLocalDescription = Move(mPendingLocalDescription);
+
+  SetState(kJsepStateStable);
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::SetRemoteTracksFromDescription(const Sdp& remoteDescription)
+{
+  size_t numMlines = remoteDescription.GetMediaSectionCount();
+
+  for (size_t i = 0; i < numMlines; ++i) {
+    const SdpMediaSection& msection = remoteDescription.GetMediaSection(i);
+    auto direction = msection.GetDirectionAttribute().mValue;
+
+    // TODO(bug 1017888): Suppress new track creation on renegotiation
+    // of existing tracks.
+    if (direction & SdpDirectionAttribute::kSendFlag) {
+      nsresult rv = CreateReceivingTrack(i, msection);
+      NS_ENSURE_SUCCESS(rv, rv);
+    }
+  }
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::ValidateLocalDescription(const Sdp& description)
+{
+  // TODO(bug 1095226): Better checking.
+  // (Also, at what point do we clear out the generated offer?)
+
+  if (description.GetMediaSectionCount() !=
+      mGeneratedLocalDescription->GetMediaSectionCount()) {
+    JSEP_SET_ERROR("Changing the number of m-sections is not allowed");
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  for (size_t i = 0; i < description.GetMediaSectionCount(); ++i) {
+    auto& origMsection = mGeneratedLocalDescription->GetMediaSection(i);
+    auto& finalMsection = description.GetMediaSection(i);
+    if (origMsection.GetMediaType() != finalMsection.GetMediaType()) {
+      JSEP_SET_ERROR("Changing the media-type of m-sections is not allowed");
+      return NS_ERROR_INVALID_ARG;
+    }
+
+    if (finalMsection.GetAttributeList().HasAttribute(
+            SdpAttribute::kCandidateAttribute)) {
+      JSEP_SET_ERROR("Adding your own candidate attributes is not supported");
+      return NS_ERROR_INVALID_ARG;
+    }
+
+    if (finalMsection.GetAttributeList().HasAttribute(
+            SdpAttribute::kEndOfCandidatesAttribute)) {
+      JSEP_SET_ERROR("Why are you trying to set a=end-of-candidates?");
+      return NS_ERROR_INVALID_ARG;
+    }
+
+    // TODO(bug 1095218): Check msid
+    // TODO(bug 1095226): Check ice-ufrag and ice-pwd
+    // TODO(bug 1095226): Check fingerprints
+    // TODO(bug 1095226): Check payload types (at least ensure that payload
+    // types we don't actually support weren't added)
+    // TODO(bug 1095226): Check ice-options?
+  }
+
+  if (description.GetAttributeList().HasAttribute(
+          SdpAttribute::kIceLiteAttribute)) {
+    JSEP_SET_ERROR("Running ICE in lite mode is unsupported");
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::CreateReceivingTrack(size_t mline,
+                                      const SdpMediaSection& msection)
+{
+  std::string streamId;
+  std::string trackId;
+
+  // Generate random track ids.
+  // TODO(bug 1095218): Pull track and stream IDs out of SDP if available.
+  if (!mUuidGen->Generate(&trackId)) {
+    JSEP_SET_ERROR("Failed to generate UUID for JsepTrack");
+    return NS_ERROR_FAILURE;
+  }
+
+  JsepTrack* remote = new JsepTrack(msection.GetMediaType(),
+                                    mDefaultRemoteStreamId,
+                                    trackId,
+                                    JsepTrack::kJsepTrackReceiving);
+  JsepReceivingTrack rtrack;
+  rtrack.mTrack = remote;
+  rtrack.mAssignedMLine = Some(mline);
+  mRemoteTracks.push_back(rtrack);
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::CreateGenericSDP(UniquePtr<Sdp>* sdpp)
+{
+  // draft-ietf-rtcweb-jsep-08 Section 5.2.1:
+  //  o  The second SDP line MUST be an "o=" line, as specified in
+  //     [RFC4566], Section 5.2.  The value of the <username> field SHOULD
+  //     be "-".  The value of the <sess-id> field SHOULD be a
+  //     cryptographically random number.  To ensure uniqueness, this
+  //     number SHOULD be at least 64 bits long.  The value of the <sess-
+  //     version> field SHOULD be zero.  The value of the <nettype>
+  //     <addrtype> <unicast-address> tuple SHOULD be set to a non-
+  //     meaningful address, such as IN IP4 0.0.0.0, to prevent leaking the
+  //     local address in this field.  As mentioned in [RFC4566], the
+  //     entire o= line needs to be unique, but selecting a random number
+  //     for <sess-id> is sufficient to accomplish this.
+
+  auto origin =
+      SdpOrigin("-", mSessionId, mSessionVersion, sdp::kIPv4, "0.0.0.0");
+
+  UniquePtr<Sdp> sdp = MakeUnique<SipccSdp>(origin);
+
+  if (mDtlsFingerprints.empty()) {
+    JSEP_SET_ERROR("Missing DTLS fingerprint");
+    return NS_ERROR_FAILURE;
+  }
+
+  UniquePtr<SdpFingerprintAttributeList> fpl =
+      MakeUnique<SdpFingerprintAttributeList>();
+  for (auto fp = mDtlsFingerprints.begin(); fp != mDtlsFingerprints.end();
+       ++fp) {
+    fpl->PushEntry(fp->mAlgorithm, fp->mValue);
+  }
+  sdp->GetAttributeList().SetAttribute(fpl.release());
+
+  auto* iceOpts = new SdpOptionsAttribute(SdpAttribute::kIceOptionsAttribute);
+  iceOpts->PushEntry("trickle");
+  sdp->GetAttributeList().SetAttribute(iceOpts);
+
+  *sdpp = Move(sdp);
+  return NS_OK;
+}
+
+void
+JsepSessionImpl::SetupDefaultCodecs()
+{
+  // Supported audio codecs.
+  mCodecs.push_back(new JsepAudioCodecDescription(
+      "109",
+      "opus",
+      48000,
+      2,
+      960,
+      16000));
+
+  mCodecs.push_back(new JsepAudioCodecDescription(
+      "9",
+      "G722",
+      8000,
+      1,
+      320,
+      64000));
+
+  // packet size and bitrate values below copied from sipcc.
+  // May need reevaluation from a media expert.
+  mCodecs.push_back(
+      new JsepAudioCodecDescription("0",
+                                    "PCMU",
+                                    8000,
+                                    1,
+                                    8000 / 50,   // frequency / 50
+                                    8 * 8000 * 1 // 8 * frequency * channels
+                                    ));
+
+  mCodecs.push_back(
+      new JsepAudioCodecDescription("8",
+                                    "PCMA",
+                                    8000,
+                                    1,
+                                    8000 / 50,   // frequency / 50
+                                    8 * 8000 * 1 // 8 * frequency * channels
+                                    ));
+
+  // Supported video codecs.
+  JsepVideoCodecDescription* vp8 = new JsepVideoCodecDescription(
+      "120",
+      "VP8",
+      90000
+      );
+  // Defaults for mandatory params
+  vp8->mMaxFs = 12288;
+  vp8->mMaxFr = 60;
+  mCodecs.push_back(vp8);
+
+  JsepVideoCodecDescription* h264_1 = new JsepVideoCodecDescription(
+      "126",
+      "H264",
+      90000
+      );
+  h264_1->mPacketizationMode = 1;
+  // Defaults for mandatory params
+  h264_1->mProfileLevelId = 0x42E00D;
+  mCodecs.push_back(h264_1);
+
+  JsepVideoCodecDescription* h264_0 = new JsepVideoCodecDescription(
+      "97",
+      "H264",
+      90000
+      );
+  h264_0->mPacketizationMode = 0;
+  // Defaults for mandatory params
+  h264_0->mProfileLevelId = 0x42E00D;
+  mCodecs.push_back(h264_0);
+
+  mCodecs.push_back(new JsepApplicationCodecDescription(
+      "5000",
+      "webrtc-datachannel",
+      16
+      ));
+}
+
+void
+JsepSessionImpl::SetupDefaultRtpExtensions()
+{
+  AddAudioRtpExtension("urn:ietf:params:rtp-hdrext:ssrc-audio-level");
+}
+
+void
+JsepSessionImpl::SetState(JsepSignalingState state)
+{
+  if (state == mState)
+    return;
+
+  MOZ_MTLOG(ML_NOTICE, "[" << mName << "]: " <<
+            GetStateStr(mState) << " -> " << GetStateStr(state));
+  mState = state;
+}
+
+nsresult
+JsepSessionImpl::AddCandidateToSdp(Sdp* sdp,
+                                   const std::string& candidateUntrimmed,
+                                   const std::string& mid,
+                                   uint16_t level)
+{
+
+  if (level >= sdp->GetMediaSectionCount()) {
+    // Ignore
+    return NS_OK;
+  }
+
+  // Trim off a=candidate:
+  size_t begin = candidateUntrimmed.find(':');
+  if (begin == std::string::npos) {
+    JSEP_SET_ERROR("Invalid candidate, no ':' (" << candidateUntrimmed << ")");
+    return NS_ERROR_INVALID_ARG;
+  }
+  ++begin;
+
+  std::string candidate = candidateUntrimmed.substr(begin);
+
+  // TODO(bug 1095793): mid
+
+  SdpMediaSection& msection = sdp->GetMediaSection(level);
+  SdpAttributeList& attrList = msection.GetAttributeList();
+
+  UniquePtr<SdpMultiStringAttribute> candidates;
+  if (!attrList.HasAttribute(SdpAttribute::kCandidateAttribute)) {
+    // Create new
+    candidates.reset(
+        new SdpMultiStringAttribute(SdpAttribute::kCandidateAttribute));
+  } else {
+    // Copy existing
+    candidates.reset(new SdpMultiStringAttribute(
+        *static_cast<const SdpMultiStringAttribute*>(
+            attrList.GetAttribute(SdpAttribute::kCandidateAttribute))));
+  }
+  candidates->PushEntry(candidate);
+  attrList.SetAttribute(candidates.release());
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::AddRemoteIceCandidate(const std::string& candidate,
+                                       const std::string& mid,
+                                       uint16_t level)
+{
+  mLastError.clear();
+
+  mozilla::Sdp* sdp = 0;
+
+  if (mPendingRemoteDescription) {
+    sdp = mPendingRemoteDescription.get();
+  } else if (mCurrentRemoteDescription) {
+    sdp = mCurrentRemoteDescription.get();
+  } else {
+    JSEP_SET_ERROR("Cannot add ICE candidate in state " << GetStateStr(mState));
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  return AddCandidateToSdp(sdp, candidate, mid, level);
+}
+
+nsresult
+JsepSessionImpl::AddLocalIceCandidate(const std::string& candidate,
+                                      const std::string& mid,
+                                      uint16_t level)
+{
+  mLastError.clear();
+
+  mozilla::Sdp* sdp = 0;
+
+  if (mPendingLocalDescription) {
+    sdp = mPendingLocalDescription.get();
+  } else if (mCurrentLocalDescription) {
+    sdp = mCurrentLocalDescription.get();
+  } else {
+    JSEP_SET_ERROR("Cannot add ICE candidate in state " << GetStateStr(mState));
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  return AddCandidateToSdp(sdp, candidate, mid, level);
+}
+
+nsresult
+JsepSessionImpl::EndOfLocalCandidates(const std::string& defaultCandidateAddr,
+                                      uint16_t defaultCandidatePort,
+                                      uint16_t level)
+{
+  mLastError.clear();
+
+  mozilla::Sdp* sdp = 0;
+
+  if (mPendingLocalDescription) {
+    sdp = mPendingLocalDescription.get();
+  } else if (mCurrentLocalDescription) {
+    sdp = mCurrentLocalDescription.get();
+  } else {
+    JSEP_SET_ERROR("Cannot add ICE candidate in state " << GetStateStr(mState));
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  if (level < sdp->GetMediaSectionCount()) {
+    SdpMediaSection& msection = sdp->GetMediaSection(level);
+    msection.GetConnection().SetAddress(defaultCandidateAddr);
+    msection.SetPort(defaultCandidatePort);
+
+    // TODO(bug 1095793): Will this have an mid someday?
+
+    SdpAttributeList& attrs = msection.GetAttributeList();
+    // We verify that content has not set this in VerifyLocalDescription
+    MOZ_ASSERT(!attrs.HasAttribute(SdpAttribute::kEndOfCandidatesAttribute));
+    attrs.SetAttribute(
+        new SdpFlagAttribute(SdpAttribute::kEndOfCandidatesAttribute));
+    if (!mIsOfferer) {
+      attrs.RemoveAttribute(SdpAttribute::kIceOptionsAttribute);
+    }
+  }
+
+  return NS_OK;
+}
+
+nsresult
+JsepSessionImpl::Close()
+{
+  mLastError.clear();
+  SetState(kJsepStateClosed);
+  return NS_OK;
+}
+
+const std::string
+JsepSessionImpl::GetLastError() const
+{
+  return mLastError;
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/jsep/JsepSessionImpl.h
@@ -0,0 +1,302 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _JSEPSESSIONIMPL_H_
+#define _JSEPSESSIONIMPL_H_
+
+#include <string>
+#include <vector>
+
+#include "signaling/src/jsep/JsepCodecDescription.h"
+#include "signaling/src/jsep/JsepTrack.h"
+#include "signaling/src/jsep/JsepSession.h"
+#include "signaling/src/jsep/JsepTrack.h"
+#include "signaling/src/jsep/JsepTrackImpl.h"
+#include "signaling/src/sdp/SipccSdpParser.h"
+
+namespace mozilla {
+
+class JsepUuidGenerator
+{
+public:
+  virtual ~JsepUuidGenerator() {}
+  virtual bool Generate(std::string* id) = 0;
+};
+
+class JsepSessionImpl : public JsepSession
+{
+public:
+  JsepSessionImpl(const std::string& name, UniquePtr<JsepUuidGenerator> uuidgen)
+      : JsepSession(name),
+        mIsOfferer(false),
+        mIceControlling(false),
+        mRemoteIsIceLite(false),
+        mSessionId(0),
+        mSessionVersion(0),
+        mUuidGen(Move(uuidgen))
+  {
+  }
+
+  virtual ~JsepSessionImpl();
+
+  // Implement JsepSession methods.
+  virtual nsresult Init() MOZ_OVERRIDE;
+
+  virtual nsresult AddTrack(const RefPtr<JsepTrack>& track) MOZ_OVERRIDE;
+
+  virtual nsresult
+  RemoveTrack(size_t trackIndex) MOZ_OVERRIDE
+  {
+    mLastError.clear();
+    MOZ_CRASH(); // Stub
+  }
+
+  virtual nsresult SetIceCredentials(const std::string& ufrag,
+                                     const std::string& pwd) MOZ_OVERRIDE;
+
+  virtual bool
+  RemoteIsIceLite() const MOZ_OVERRIDE
+  {
+    return mRemoteIsIceLite;
+  }
+
+  virtual std::vector<std::string>
+  GetIceOptions() const MOZ_OVERRIDE
+  {
+    return mIceOptions;
+  }
+
+  virtual nsresult AddDtlsFingerprint(const std::string& algorithm,
+                                      const std::vector<uint8_t>& value) MOZ_OVERRIDE;
+
+  virtual nsresult AddAudioRtpExtension(
+      const std::string& extensionName) MOZ_OVERRIDE;
+
+  virtual nsresult AddVideoRtpExtension(
+      const std::string& extensionName) MOZ_OVERRIDE;
+
+  virtual std::vector<JsepCodecDescription*>&
+  Codecs() MOZ_OVERRIDE
+  {
+    return mCodecs;
+  }
+
+  virtual nsresult
+  ReplaceTrack(size_t trackIndex, const RefPtr<JsepTrack>& track) MOZ_OVERRIDE
+  {
+    mLastError.clear();
+    MOZ_CRASH(); // Stub
+  }
+
+  virtual size_t
+  GetLocalTrackCount() const MOZ_OVERRIDE
+  {
+    return mLocalTracks.size();
+  }
+
+  virtual nsresult GetLocalTrack(size_t index,
+                                 RefPtr<JsepTrack>* track) const MOZ_OVERRIDE;
+
+  virtual size_t
+  GetRemoteTrackCount() const MOZ_OVERRIDE
+  {
+    return mRemoteTracks.size();
+  }
+
+  virtual nsresult GetRemoteTrack(size_t index,
+                                  RefPtr<JsepTrack>* track) const MOZ_OVERRIDE;
+
+  virtual nsresult CreateOffer(const JsepOfferOptions& options,
+                               std::string* offer) MOZ_OVERRIDE;
+
+  virtual nsresult CreateAnswer(const JsepAnswerOptions& options,
+                                std::string* answer) MOZ_OVERRIDE;
+
+  virtual std::string GetLocalDescription() const MOZ_OVERRIDE;
+
+  virtual std::string GetRemoteDescription() const MOZ_OVERRIDE;
+
+  virtual nsresult SetLocalDescription(JsepSdpType type,
+                                       const std::string& sdp) MOZ_OVERRIDE;
+
+  virtual nsresult SetRemoteDescription(JsepSdpType type,
+                                        const std::string& sdp) MOZ_OVERRIDE;
+
+  virtual nsresult AddRemoteIceCandidate(const std::string& candidate,
+                                         const std::string& mid,
+                                         uint16_t level) MOZ_OVERRIDE;
+
+  virtual nsresult AddLocalIceCandidate(const std::string& candidate,
+                                        const std::string& mid,
+                                        uint16_t level) MOZ_OVERRIDE;
+
+  virtual nsresult EndOfLocalCandidates(const std::string& defaultCandidateAddr,
+                                        uint16_t defaultCandidatePort,
+                                        uint16_t level) MOZ_OVERRIDE;
+
+  virtual nsresult Close() MOZ_OVERRIDE;
+
+  virtual const std::string GetLastError() const MOZ_OVERRIDE;
+
+  virtual bool
+  IsIceControlling() const
+  {
+    return mIceControlling;
+  }
+
+  virtual bool
+  IsOfferer() const
+  {
+    return mIsOfferer;
+  }
+
+  // Access transports.
+  virtual size_t
+  GetTransportCount() const MOZ_OVERRIDE
+  {
+    return mTransports.size();
+  }
+
+  virtual nsresult
+  GetTransport(size_t index,
+               RefPtr<JsepTransport>* transport) const MOZ_OVERRIDE
+  {
+    if (index >= mTransports.size())
+      return NS_ERROR_INVALID_ARG;
+
+    *transport = mTransports[index];
+
+    return NS_OK;
+  }
+
+  // Access the negotiated track pairs.
+  virtual size_t
+  GetNegotiatedTrackPairCount() const MOZ_OVERRIDE
+  {
+    return mNegotiatedTrackPairs.size();
+  }
+
+  virtual nsresult
+  GetNegotiatedTrackPair(size_t index, const JsepTrackPair** pair) const
+  {
+    if (index >= mNegotiatedTrackPairs.size())
+      return NS_ERROR_INVALID_ARG;
+
+    *pair = &mNegotiatedTrackPairs[index];
+
+    return NS_OK;
+  }
+
+private:
+  struct JsepDtlsFingerprint {
+    std::string mAlgorithm;
+    std::vector<uint8_t> mValue;
+  };
+
+  struct JsepSendingTrack {
+    RefPtr<JsepTrack> mTrack;
+    Maybe<size_t> mAssignedMLine;
+  };
+
+  struct JsepReceivingTrack {
+    RefPtr<JsepTrack> mTrack;
+    Maybe<size_t> mAssignedMLine;
+  };
+
+  // Non-const so it can set mLastError
+  nsresult CreateGenericSDP(UniquePtr<Sdp>* sdp);
+  void AddCodecs(SdpMediaSection* msection) const;
+  void AddExtmap(SdpMediaSection* msection) const;
+  JsepCodecDescription* FindMatchingCodec(
+      const std::string& pt,
+      const SdpMediaSection& msection) const;
+  const std::vector<SdpExtmapAttributeList::Extmap>* GetRtpExtensions(
+      SdpMediaSection::MediaType type) const;
+  void AddCommonCodecs(const SdpMediaSection& remoteMsection,
+                       SdpMediaSection* msection);
+  void AddCommonExtmaps(const SdpMediaSection& remoteMsection,
+                        SdpMediaSection* msection);
+  void SetupDefaultCodecs();
+  void SetupDefaultRtpExtensions();
+  void SetState(JsepSignalingState state);
+  // Non-const so it can set mLastError
+  nsresult ParseSdp(const std::string& sdp, UniquePtr<Sdp>* parsedp);
+  nsresult SetLocalDescriptionOffer(UniquePtr<Sdp> offer);
+  nsresult SetLocalDescriptionAnswer(JsepSdpType type, UniquePtr<Sdp> answer);
+  nsresult SetRemoteDescriptionOffer(UniquePtr<Sdp> offer);
+  nsresult SetRemoteDescriptionAnswer(JsepSdpType type, UniquePtr<Sdp> answer);
+  nsresult ValidateLocalDescription(const Sdp& description);
+  nsresult SetRemoteTracksFromDescription(const Sdp& remoteDescription);
+  // Non-const because we use our Uuid generator
+  nsresult CreateReceivingTrack(size_t mline, const SdpMediaSection& msection);
+  nsresult HandleNegotiatedSession(const UniquePtr<Sdp>& local,
+                                   const UniquePtr<Sdp>& remote);
+  nsresult DetermineSendingDirection(SdpDirectionAttribute::Direction offer,
+                                     SdpDirectionAttribute::Direction answer,
+                                     bool* sending, bool* receiving);
+  nsresult AddTransportAttributes(SdpMediaSection* msection,
+                                  SdpSetupAttribute::Role dtlsRole);
+  // Non-const so it can assign m-line index to tracks
+  nsresult AddOfferMSectionsByType(SdpMediaSection::MediaType type,
+                                   Maybe<size_t> offerToReceive,
+                                   Sdp* sdp);
+  nsresult CreateOfferMSection(SdpMediaSection::MediaType type,
+                               SdpDirectionAttribute::Direction direction,
+                               SdpMediaSection::Protocol proto,
+                               Sdp* sdp);
+  nsresult CreateAnswerMSection(const JsepAnswerOptions& options,
+                                size_t mlineIndex,
+                                const SdpMediaSection& remoteMsection,
+                                SdpMediaSection* msection,
+                                Sdp* sdp);
+  nsresult DetermineAnswererSetupRole(const SdpMediaSection& remoteMsection,
+                                      SdpSetupAttribute::Role* rolep);
+  nsresult NegotiateTrack(const SdpMediaSection& remoteMsection,
+                          const SdpMediaSection& localMsection,
+                          JsepTrack::Direction,
+                          RefPtr<JsepTrack>* track);
+
+  nsresult CreateTransport(const SdpMediaSection& msection,
+                           RefPtr<JsepTransport>* transport);
+
+  nsresult SetupTransport(const SdpAttributeList& remote,
+                          const SdpAttributeList& answer,
+                          const RefPtr<JsepTransport>& transport);
+
+  nsresult AddCandidateToSdp(Sdp* sdp,
+                             const std::string& candidate,
+                             const std::string& mid,
+                             uint16_t level);
+
+  std::vector<JsepSendingTrack> mLocalTracks;
+  std::vector<JsepReceivingTrack> mRemoteTracks;
+  std::vector<RefPtr<JsepTransport> > mTransports;
+  std::vector<JsepTrackPair> mNegotiatedTrackPairs;
+
+  bool mIsOfferer;
+  bool mIceControlling;
+  std::string mIceUfrag;
+  std::string mIcePwd;
+  bool mRemoteIsIceLite;
+  std::vector<std::string> mIceOptions;
+  std::vector<JsepDtlsFingerprint> mDtlsFingerprints;
+  uint64_t mSessionId;
+  uint64_t mSessionVersion;
+  std::vector<SdpExtmapAttributeList::Extmap> mAudioRtpExtensions;
+  std::vector<SdpExtmapAttributeList::Extmap> mVideoRtpExtensions;
+  UniquePtr<JsepUuidGenerator> mUuidGen;
+  std::string mDefaultRemoteStreamId;
+  UniquePtr<Sdp> mGeneratedLocalDescription; // Created but not set.
+  UniquePtr<Sdp> mCurrentLocalDescription;
+  UniquePtr<Sdp> mCurrentRemoteDescription;
+  UniquePtr<Sdp> mPendingLocalDescription;
+  UniquePtr<Sdp> mPendingRemoteDescription;
+  std::vector<JsepCodecDescription*> mCodecs;
+  std::string mLastError;
+  SipccSdpParser mParser;
+};
+
+} // namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/jsep/JsepTrack.h
@@ -0,0 +1,120 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _JSEPTRACK_H_
+#define _JSEPTRACK_H_
+
+#include <string>
+
+#include <mozilla/RefPtr.h>
+#include <mozilla/UniquePtr.h>
+#include <mozilla/Maybe.h>
+#include "nsISupportsImpl.h"
+#include "nsError.h"
+
+#include "signaling/src/jsep/JsepTransport.h"
+#include "signaling/src/sdp/Sdp.h"
+#include "signaling/src/sdp/SdpMediaSection.h"
+
+namespace mozilla {
+
+// Forward reference.
+struct JsepCodecDescription;
+
+class JsepTrackNegotiatedDetails
+{
+public:
+  virtual ~JsepTrackNegotiatedDetails() {}
+
+  virtual mozilla::SdpMediaSection::Protocol GetProtocol() const = 0;
+  virtual Maybe<std::string> GetBandwidth(const std::string& type) const = 0;
+  virtual size_t GetCodecCount() const = 0;
+  virtual nsresult GetCodec(size_t index,
+                            const JsepCodecDescription** config) const = 0;
+  virtual const SdpExtmapAttributeList::Extmap* GetExt(
+      const std::string& ext_name) const = 0;
+};
+
+class JsepTrack
+{
+public:
+  enum Direction { kJsepTrackSending, kJsepTrackReceiving };
+
+  JsepTrack(mozilla::SdpMediaSection::MediaType type,
+            const std::string& streamid,
+            const std::string& trackid,
+            Direction direction = kJsepTrackSending)
+      : mType(type),
+        mStreamId(streamid),
+        mTrackId(trackid),
+        mDirection(direction)
+  {
+  }
+
+  virtual mozilla::SdpMediaSection::MediaType
+  GetMediaType() const
+  {
+    return mType;
+  }
+
+  virtual const std::string&
+  GetStreamId() const
+  {
+    return mStreamId;
+  }
+
+  virtual const std::string&
+  GetTrackId() const
+  {
+    return mTrackId;
+  }
+
+  virtual Direction
+  GetDirection() const
+  {
+    return mDirection;
+  }
+
+  // This will be set when negotiation is carried out.
+  virtual const JsepTrackNegotiatedDetails*
+  GetNegotiatedDetails() const
+  {
+    if (mNegotiatedDetails) {
+      return mNegotiatedDetails.get();
+    }
+    return nullptr;
+  }
+
+  // This is for JsepSession's use.
+  virtual void
+  SetNegotiatedDetails(UniquePtr<JsepTrackNegotiatedDetails> details)
+  {
+    mNegotiatedDetails = Move(details);
+  }
+
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(JsepTrack);
+
+protected:
+  virtual ~JsepTrack() {}
+
+private:
+  const mozilla::SdpMediaSection::MediaType mType;
+  const std::string mStreamId;
+  const std::string mTrackId;
+  const Direction mDirection;
+  UniquePtr<JsepTrackNegotiatedDetails> mNegotiatedDetails;
+};
+
+// Need a better name for this.
+struct JsepTrackPair {
+  size_t mLevel;
+  RefPtr<JsepTrack> mSending;
+  RefPtr<JsepTrack> mReceiving;
+  RefPtr<JsepTransport> mRtpTransport;
+  RefPtr<JsepTransport> mRtcpTransport;
+};
+
+} // namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/jsep/JsepTrackImpl.h
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _JSEPTRACKIMPL_H_
+#define _JSEPTRACKIMPL_H_
+
+#include <map>
+
+#include <mozilla/RefPtr.h>
+#include <mozilla/UniquePtr.h>
+
+#include "signaling/src/jsep/JsepCodecDescription.h"
+#include "signaling/src/jsep/JsepTrack.h"
+#include "signaling/src/sdp/Sdp.h"
+#include "signaling/src/sdp/SdpMediaSection.h"
+
+namespace mozilla {
+
+class JsepTrackNegotiatedDetailsImpl : public JsepTrackNegotiatedDetails
+{
+public:
+  virtual ~JsepTrackNegotiatedDetailsImpl()
+  {
+    for (auto c = mCodecs.begin(); c != mCodecs.end(); ++c) {
+      delete *c;
+    }
+  }
+
+  // Implement JsepTrackNegotiatedDetails.
+  virtual mozilla::SdpMediaSection::Protocol
+  GetProtocol() const MOZ_OVERRIDE
+  {
+    return mProtocol;
+  }
+  virtual Maybe<std::string>
+  GetBandwidth(const std::string& type) const MOZ_OVERRIDE
+  {
+    return mBandwidth;
+  }
+  virtual size_t
+  GetCodecCount() const MOZ_OVERRIDE
+  {
+    return mCodecs.size();
+  }
+  virtual nsresult
+  GetCodec(size_t index, const JsepCodecDescription** config) const MOZ_OVERRIDE
+  {
+    if (index >= mCodecs.size()) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    *config = mCodecs[index];
+    return NS_OK;
+  }
+
+  virtual const SdpExtmapAttributeList::Extmap*
+  GetExt(const std::string& ext_name) const MOZ_OVERRIDE
+  {
+    auto it = mExtmap.find(ext_name);
+    if (it != mExtmap.end()) {
+      return &it->second;
+    }
+    return nullptr;
+  }
+
+private:
+  // Make these friends to JsepSessionImpl to avoid having to
+  // write setters.
+  friend class JsepSessionImpl;
+
+  mozilla::SdpMediaSection::Protocol mProtocol;
+  Maybe<std::string> mBandwidth;
+  std::vector<JsepCodecDescription*> mCodecs;
+  std::map<std::string, SdpExtmapAttributeList::Extmap> mExtmap;
+};
+
+} // namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/jsep/JsepTransport.h
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _JSEPTRANSPORT_H_
+#define _JSEPTRANSPORT_H_
+
+#include <string>
+#include <vector>
+
+#include <mozilla/RefPtr.h>
+#include <mozilla/UniquePtr.h>
+#include "nsISupportsImpl.h"
+
+#include "signaling/src/sdp/SdpAttribute.h"
+
+namespace mozilla {
+
+class JsepDtlsTransport
+{
+public:
+  JsepDtlsTransport() : mRole(kJsepDtlsInvalidRole) {}
+
+  virtual ~JsepDtlsTransport() {}
+
+  enum Role {
+    kJsepDtlsClient,
+    kJsepDtlsServer,
+    kJsepDtlsInvalidRole
+  };
+
+  virtual const SdpFingerprintAttributeList&
+  GetFingerprints() const
+  {
+    return mFingerprints;
+  }
+
+  virtual Role
+  GetRole() const
+  {
+    return mRole;
+  }
+
+private:
+  friend class JsepSessionImpl;
+
+  SdpFingerprintAttributeList mFingerprints;
+  Role mRole;
+};
+
+class JsepIceTransport
+{
+public:
+  JsepIceTransport() {}
+
+  virtual ~JsepIceTransport() {}
+
+  const std::string&
+  GetUfrag() const
+  {
+    return mUfrag;
+  }
+  const std::string&
+  GetPassword() const
+  {
+    return mPwd;
+  }
+  const std::vector<std::string>&
+  GetCandidates() const
+  {
+    return mCandidates;
+  }
+
+private:
+  friend class JsepSessionImpl;
+
+  std::string mUfrag;
+  std::string mPwd;
+  std::vector<std::string> mCandidates;
+};
+
+class JsepTransport
+{
+public:
+  JsepTransport(const std::string& id, size_t components)
+      : mTransportId(id), mState(kJsepTransportOffered), mComponents(components)
+  {
+  }
+
+  enum State {
+    kJsepTransportOffered,
+    kJsepTransportAccepted,
+    kJsepTransportClosed
+  };
+
+  // Unique identifier for this transport within this call. Group?
+  std::string mTransportId;
+
+  // State.
+  State mState;
+
+  // ICE stuff.
+  UniquePtr<JsepIceTransport> mIce;
+  UniquePtr<JsepDtlsTransport> mDtls;
+
+  // Number of required components.
+  size_t mComponents;
+
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(JsepTransport);
+
+protected:
+  ~JsepTransport() {}
+};
+
+} // namespace mozilla
+
+#endif
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/test/jsep_session_unittest.cpp
@@ -0,0 +1,1130 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <iostream>
+#include <map>
+
+#include "nspr.h"
+#include "nss.h"
+#include "ssl.h"
+
+#include "mozilla/RefPtr.h"
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+#include "gtest_utils.h"
+
+#include "FakeMediaStreams.h"
+#include "FakeMediaStreamsImpl.h"
+
+#include "signaling/src/sdp/SdpMediaSection.h"
+#include "signaling/src/sdp/SipccSdpParser.h"
+#include "signaling/src/jsep/JsepCodecDescription.h"
+#include "signaling/src/jsep/JsepTrack.h"
+#include "signaling/src/jsep/JsepSession.h"
+#include "signaling/src/jsep/JsepSessionImpl.h"
+#include "signaling/src/jsep/JsepTrack.h"
+
+namespace mozilla {
+static const char* kCandidates[] = {
+  "0 1 UDP 9999 192.168.0.1 2000 typ host",
+  "0 1 UDP 9999 192.168.0.1 2001 typ host",
+  "0 1 UDP 9999 192.168.0.2 2002 typ srflx raddr 10.252.34.97 rport 53594",
+  // Mix up order
+  "0 1 UDP 9999 192.168.1.2 2012 typ srflx raddr 10.252.34.97 rport 53594",
+  "0 1 UDP 9999 192.168.1.1 2010 typ host",
+  "0 1 UDP 9999 192.168.1.1 2011 typ host"
+};
+
+static std::string kAEqualsCandidate("a=candidate:");
+
+class JsepSessionTestBase : public ::testing::Test
+{
+};
+
+class FakeUuidGenerator : public mozilla::JsepUuidGenerator
+{
+public:
+  bool
+  Generate(std::string* str)
+  {
+    std::ostringstream os;
+    os << "FAKE_UUID_" << ++ctr;
+    *str = os.str();
+
+    return true;
+  }
+
+private:
+  static uint64_t ctr;
+};
+
+uint64_t FakeUuidGenerator::ctr = 1000;
+
+class JsepSessionTest : public JsepSessionTestBase,
+                        public ::testing::WithParamInterface<std::string>
+{
+public:
+  JsepSessionTest()
+      : mSessionOff("Offerer", MakeUnique<FakeUuidGenerator>()),
+        mSessionAns("Answerer", MakeUnique<FakeUuidGenerator>())
+  {
+    EXPECT_EQ(NS_OK, mSessionOff.Init());
+    EXPECT_EQ(NS_OK, mSessionAns.Init());
+
+    AddTransportData(&mSessionOff, &mOffererTransport);
+    AddTransportData(&mSessionAns, &mAnswererTransport);
+  }
+
+protected:
+  struct TransportData {
+    std::string mIceUfrag;
+    std::string mIcePwd;
+    std::map<std::string, std::vector<uint8_t> > mFingerprints;
+  };
+
+  void
+  AddDtlsFingerprint(const std::string& alg, JsepSessionImpl* session,
+                     TransportData* tdata)
+  {
+    std::vector<uint8_t> fp;
+    fp.assign((alg == "sha-1") ? 20 : 32,
+              (session->GetName() == "Offerer") ? 0x4f : 0x41);
+    session->AddDtlsFingerprint(alg, fp);
+    tdata->mFingerprints[alg] = fp;
+  }
+
+  void
+  AddTransportData(JsepSessionImpl* session, TransportData* tdata)
+  {
+    // Values here semi-borrowed from JSEP draft.
+    tdata->mIceUfrag = session->GetName() + "-ufrag";
+    tdata->mIcePwd = session->GetName() + "-1234567890";
+    session->SetIceCredentials(tdata->mIceUfrag, tdata->mIcePwd);
+    AddDtlsFingerprint("sha-1", session, tdata);
+    AddDtlsFingerprint("sha-256", session, tdata);
+  }
+
+  std::string
+  CreateOffer(const Maybe<JsepOfferOptions> options = Nothing())
+  {
+    JsepOfferOptions defaultOptions;
+    const JsepOfferOptions& optionsRef = options ? *options : defaultOptions;
+    std::string offer;
+    nsresult rv = mSessionOff.CreateOffer(optionsRef, &offer);
+    EXPECT_EQ(NS_OK, rv) << mSessionOff.GetLastError();
+
+    std::cerr << "OFFER: " << offer << std::endl;
+
+    ValidateTransport(mOffererTransport, offer);
+
+    return offer;
+  }
+
+  void
+  AddTracks(JsepSessionImpl* side)
+  {
+    // Add tracks.
+    if (types.empty()) {
+      types = BuildTypes(GetParam());
+    }
+    AddTracks(side, types);
+
+    // Now that we have added streams, we expect audio, then video, then
+    // application in the SDP, regardless of the order in which the streams were
+    // added.
+    std::sort(types.begin(), types.end());
+  }
+
+  void
+  AddTracks(JsepSessionImpl* side, const std::string& mediatypes)
+  {
+    AddTracks(side, BuildTypes(mediatypes));
+  }
+
+  std::vector<SdpMediaSection::MediaType>
+  BuildTypes(const std::string& mediatypes)
+  {
+    std::vector<SdpMediaSection::MediaType> result;
+    size_t ptr = 0;
+
+    for (;;) {
+      size_t comma = mediatypes.find(',', ptr);
+      std::string chunk = mediatypes.substr(ptr, comma - ptr);
+
+      SdpMediaSection::MediaType type;
+      if (chunk == "audio") {
+        type = SdpMediaSection::kAudio;
+      } else if (chunk == "video") {
+        type = SdpMediaSection::kVideo;
+      } else if (chunk == "datachannel") {
+        type = SdpMediaSection::kApplication;
+      } else {
+        MOZ_CRASH();
+      }
+      result.push_back(type);
+
+      if (comma == std::string::npos)
+        break;
+      ptr = comma + 1;
+    }
+
+    return result;
+  }
+
+  void
+  AddTracks(JsepSessionImpl* side,
+            const std::vector<SdpMediaSection::MediaType>& mediatypes)
+  {
+    FakeUuidGenerator uuid_gen;
+    std::string stream_id;
+    std::string track_id;
+
+    ASSERT_TRUE(uuid_gen.Generate(&stream_id));
+
+    for (auto track = mediatypes.begin(); track != mediatypes.end(); ++track) {
+      ASSERT_TRUE(uuid_gen.Generate(&track_id));
+
+      RefPtr<JsepTrack> mst(new JsepTrack(*track, stream_id, track_id));
+      side->AddTrack(mst);
+    }
+  }
+
+  void
+  EnsureNegotiationFailure(SdpMediaSection::MediaType type,
+                           const std::string& codecName)
+  {
+    for (auto i = mSessionOff.Codecs().begin(); i != mSessionOff.Codecs().end();
+         ++i) {
+      auto* codec = *i;
+      if (codec->mType == type && codec->mName != codecName) {
+        codec->mEnabled = false;
+      }
+    }
+
+    for (auto i = mSessionAns.Codecs().begin(); i != mSessionAns.Codecs().end();
+         ++i) {
+      auto* codec = *i;
+      if (codec->mType == type && codec->mName == codecName) {
+        codec->mEnabled = false;
+      }
+    }
+  }
+
+  std::string
+  CreateAnswer()
+  {
+    JsepAnswerOptions options;
+    std::string answer;
+    nsresult rv = mSessionAns.CreateAnswer(options, &answer);
+    EXPECT_EQ(NS_OK, rv);
+
+    std::cerr << "ANSWER: " << answer << std::endl;
+
+    ValidateTransport(mAnswererTransport, answer);
+
+    return answer;
+  }
+
+  static const uint32_t NO_CHECKS = 0;
+  static const uint32_t CHECK_SUCCESS = 1;
+  static const uint32_t CHECK_TRACKS = 1 << 2;
+  static const uint32_t ALL_CHECKS = CHECK_SUCCESS | CHECK_TRACKS;
+
+  void
+  SetLocalOffer(const std::string& offer, uint32_t checkFlags = ALL_CHECKS)
+  {
+    nsresult rv = mSessionOff.SetLocalDescription(kJsepSdpOffer, offer);
+
+    if (checkFlags & CHECK_SUCCESS) {
+      ASSERT_EQ(NS_OK, rv);
+    }
+
+    if (checkFlags & CHECK_TRACKS) {
+      // Check that the transports exist.
+      ASSERT_EQ(types.size(), mSessionOff.GetTransportCount());
+    }
+  }
+
+  void
+  SetRemoteOffer(const std::string& offer, uint32_t checkFlags = ALL_CHECKS)
+  {
+    nsresult rv = mSessionAns.SetRemoteDescription(kJsepSdpOffer, offer);
+
+    if (checkFlags & CHECK_SUCCESS) {
+      ASSERT_EQ(NS_OK, rv);
+    }
+
+    if (checkFlags & CHECK_TRACKS) {
+      // Now verify that the right stuff is in the tracks.
+      ASSERT_EQ(types.size(), mSessionAns.GetRemoteTrackCount());
+      for (size_t i = 0; i < types.size(); ++i) {
+        RefPtr<JsepTrack> rtrack;
+        ASSERT_EQ(NS_OK, mSessionAns.GetRemoteTrack(i, &rtrack));
+        ASSERT_EQ(types[i], rtrack->GetMediaType());
+      }
+    }
+  }
+
+  void
+  SetLocalAnswer(const std::string& answer, uint32_t checkFlags = ALL_CHECKS)
+  {
+    nsresult rv = mSessionAns.SetLocalDescription(kJsepSdpAnswer, answer);
+    if (checkFlags & CHECK_SUCCESS) {
+      ASSERT_EQ(NS_OK, rv);
+    }
+
+    if (checkFlags & CHECK_TRACKS) {
+      // Verify that the right stuff is in the tracks.
+      ASSERT_EQ(types.size(), mSessionAns.GetNegotiatedTrackPairCount());
+      for (size_t i = 0; i < types.size(); ++i) {
+        const JsepTrackPair* pair;
+        ASSERT_EQ(NS_OK, mSessionAns.GetNegotiatedTrackPair(i, &pair));
+        ASSERT_TRUE(pair->mSending);
+        ASSERT_EQ(types[i], pair->mSending->GetMediaType());
+        ASSERT_TRUE(pair->mReceiving);
+        ASSERT_EQ(types[i], pair->mReceiving->GetMediaType());
+      }
+    }
+    DumpTrackPairs(mSessionOff);
+  }
+
+  void
+  SetRemoteAnswer(const std::string& answer, uint32_t checkFlags = ALL_CHECKS)
+  {
+    nsresult rv = mSessionOff.SetRemoteDescription(kJsepSdpAnswer, answer);
+    if (checkFlags & CHECK_SUCCESS) {
+      ASSERT_EQ(NS_OK, rv);
+    }
+
+    if (checkFlags & CHECK_TRACKS) {
+      // Verify that the right stuff is in the tracks.
+      ASSERT_EQ(types.size(), mSessionAns.GetNegotiatedTrackPairCount());
+      for (size_t i = 0; i < types.size(); ++i) {
+        const JsepTrackPair* pair;
+        ASSERT_EQ(NS_OK, mSessionAns.GetNegotiatedTrackPair(i, &pair));
+        ASSERT_TRUE(pair->mSending);
+        ASSERT_EQ(types[i], pair->mSending->GetMediaType());
+        ASSERT_TRUE(pair->mReceiving);
+        ASSERT_EQ(types[i], pair->mReceiving->GetMediaType());
+      }
+    }
+    DumpTrackPairs(mSessionAns);
+  }
+
+  void
+  GatherCandidates(JsepSession& session)
+  {
+    session.AddLocalIceCandidate(kAEqualsCandidate + kCandidates[0], "", 0);
+    session.AddLocalIceCandidate(kAEqualsCandidate + kCandidates[1], "", 0);
+    session.AddLocalIceCandidate(kAEqualsCandidate + kCandidates[2], "", 0);
+    session.EndOfLocalCandidates("192.168.0.2", 2002, 0);
+
+    session.AddLocalIceCandidate(kAEqualsCandidate + kCandidates[3], "", 1);
+    session.AddLocalIceCandidate(kAEqualsCandidate + kCandidates[4], "", 1);
+    session.AddLocalIceCandidate(kAEqualsCandidate + kCandidates[5], "", 1);
+    session.EndOfLocalCandidates("192.168.1.2", 2012, 1);
+
+    std::cerr << "local SDP after candidates: "
+              << session.GetLocalDescription();
+  }
+
+  void
+  TrickleCandidates(JsepSession& session)
+  {
+    session.AddRemoteIceCandidate(kAEqualsCandidate + kCandidates[0], "", 0);
+    session.AddRemoteIceCandidate(kAEqualsCandidate + kCandidates[1], "", 0);
+    session.AddRemoteIceCandidate(kAEqualsCandidate + kCandidates[2], "", 0);
+
+    session.AddRemoteIceCandidate(kAEqualsCandidate + kCandidates[3], "", 1);
+    session.AddRemoteIceCandidate(kAEqualsCandidate + kCandidates[4], "", 1);
+    session.AddRemoteIceCandidate(kAEqualsCandidate + kCandidates[5], "", 1);
+
+    std::cerr << "remote SDP after candidates: "
+              << session.GetRemoteDescription();
+  }
+
+  void
+  GatherOffererCandidates()
+  {
+    GatherCandidates(mSessionOff);
+  }
+
+  void
+  TrickleOffererCandidates()
+  {
+    TrickleCandidates(mSessionAns);
+  }
+
+  // For streaming parse errors
+  std::string
+  GetParseErrors(const SipccSdpParser& parser) const
+  {
+    std::stringstream output;
+    for (auto e = parser.GetParseErrors().begin();
+         e != parser.GetParseErrors().end();
+         ++e) {
+      output << e->first << ": " << e->second << std::endl;
+    }
+    return output.str();
+  }
+
+  void
+  ValidateCandidates(JsepSession& session, bool local)
+  {
+    std::string sdp =
+        local ? session.GetLocalDescription() : session.GetRemoteDescription();
+    SipccSdpParser parser;
+    UniquePtr<Sdp> parsed = parser.Parse(sdp);
+    ASSERT_TRUE(parsed) << "Parse failed on " << std::endl << sdp << std::endl
+                        << "Errors were: " << GetParseErrors(parser);
+    ASSERT_LT(0U, parsed->GetMediaSectionCount());
+
+    auto& msection_0 = parsed->GetMediaSection(0);
+
+    // We should not be doing things like setting the c-line on remote SDP
+    if (local) {
+      ASSERT_EQ("192.168.0.2", msection_0.GetConnection().GetAddress());
+      ASSERT_EQ(2002U, msection_0.GetPort());
+      // TODO: Check end-of-candidates. Issue 200
+    }
+
+    auto& attrs_0 = msection_0.GetAttributeList();
+    ASSERT_TRUE(attrs_0.HasAttribute(SdpAttribute::kCandidateAttribute));
+
+    auto& candidates_0 = attrs_0.GetCandidate();
+    ASSERT_EQ(3U, candidates_0.size());
+    ASSERT_EQ(kCandidates[0], candidates_0[0]);
+    ASSERT_EQ(kCandidates[1], candidates_0[1]);
+    ASSERT_EQ(kCandidates[2], candidates_0[2]);
+
+    if (parsed->GetMediaSectionCount() > 1) {
+      auto& msection_1 = parsed->GetMediaSection(1);
+
+      if (local) {
+        ASSERT_EQ("192.168.1.2", msection_1.GetConnection().GetAddress());
+        ASSERT_EQ(2012U, msection_1.GetPort());
+        // TODO: Check end-of-candidates. Issue 200
+      }
+
+      auto& attrs_1 = msection_1.GetAttributeList();
+      ASSERT_TRUE(attrs_1.HasAttribute(SdpAttribute::kCandidateAttribute));
+
+      auto& candidates_1 = attrs_1.GetCandidate();
+      ASSERT_EQ(3U, candidates_1.size());
+      ASSERT_EQ(kCandidates[3], candidates_1[0]);
+      ASSERT_EQ(kCandidates[4], candidates_1[1]);
+      ASSERT_EQ(kCandidates[5], candidates_1[2]);
+    }
+  }
+
+  void
+  ValidateOffererCandidates()
+  {
+    ValidateCandidates(mSessionOff, true);
+  }
+
+  void
+  ValidateAnswererCandidates()
+  {
+    ValidateCandidates(mSessionAns, false);
+  }
+
+  void
+  DumpTrack(const JsepTrack& track)
+  {
+    std::cerr << "  type=" << track.GetMediaType() << std::endl;
+    std::cerr << "  protocol=" << track.GetNegotiatedDetails()->GetProtocol()
+              << std::endl;
+    std::cerr << "  codecs=" << std::endl;
+    size_t num_codecs = track.GetNegotiatedDetails()->GetCodecCount();
+    for (size_t i = 0; i < num_codecs; ++i) {
+      const JsepCodecDescription* codec;
+      ASSERT_EQ(NS_OK, track.GetNegotiatedDetails()->GetCodec(i, &codec));
+      std::cerr << "    " << codec->mName << std::endl;
+    }
+  }
+
+  void
+  DumpTrackPairs(const JsepSessionImpl& session)
+  {
+    size_t count = mSessionAns.GetNegotiatedTrackPairCount();
+    for (size_t i = 0; i < count; ++i) {
+      std::cerr << "Track pair " << i << std::endl;
+      const JsepTrackPair* pair;
+      ASSERT_EQ(NS_OK, mSessionAns.GetNegotiatedTrackPair(i, &pair));
+      if (pair->mSending) {
+        std::cerr << "Sending-->" << std::endl;
+        DumpTrack(*pair->mSending);
+      }
+      if (pair->mReceiving) {
+        std::cerr << "Receiving-->" << std::endl;
+        DumpTrack(*pair->mReceiving);
+      }
+    }
+  }
+
+  JsepSessionImpl mSessionOff;
+  JsepSessionImpl mSessionAns;
+  std::vector<SdpMediaSection::MediaType> types;
+
+private:
+  void
+  ValidateTransport(TransportData& source, const std::string& sdp_str)
+  {
+    SipccSdpParser parser;
+    auto sdp = mozilla::Move(parser.Parse(sdp_str));
+    ASSERT_TRUE(sdp) << "Should have valid SDP" << std::endl
+                     << "Errors were: " << GetParseErrors(parser);
+    size_t num_m_sections = sdp->GetMediaSectionCount();
+    for (size_t i = 0; i < num_m_sections; ++i) {
+      auto& msection = sdp->GetMediaSection(i);
+
+      if (msection.GetMediaType() == SdpMediaSection::kApplication) {
+        ASSERT_EQ(SdpMediaSection::kDtlsSctp, msection.GetProtocol());
+      } else {
+        ASSERT_EQ(SdpMediaSection::kRtpSavpf, msection.GetProtocol());
+      }
+
+      if (msection.GetPort() == 0) {
+        ASSERT_EQ(SdpDirectionAttribute::kInactive,
+                  msection.GetDirectionAttribute().mValue);
+        // Maybe validate that no attributes are present except rtpmap and
+        // inactive?
+        continue;
+      }
+      const SdpAttributeList& attrs = msection.GetAttributeList();
+      ASSERT_EQ(source.mIceUfrag, attrs.GetIceUfrag());
+      ASSERT_EQ(source.mIcePwd, attrs.GetIcePwd());
+      const SdpFingerprintAttributeList& fps = attrs.GetFingerprint();
+      for (auto fp = fps.mFingerprints.begin(); fp != fps.mFingerprints.end();
+           ++fp) {
+        std::string alg_str = "None";
+
+        if (fp->hashFunc == SdpFingerprintAttributeList::kSha1) {
+          alg_str = "sha-1";
+        } else if (fp->hashFunc == SdpFingerprintAttributeList::kSha256) {
+          alg_str = "sha-256";
+        }
+
+        ASSERT_EQ(source.mFingerprints[alg_str], fp->fingerprint);
+      }
+      ASSERT_EQ(source.mFingerprints.size(), fps.mFingerprints.size());
+    }
+  }
+
+  TransportData mOffererTransport;
+  TransportData mAnswererTransport;
+};
+
+TEST_F(JsepSessionTestBase, CreateDestroy) {}
+
+TEST_P(JsepSessionTest, CreateOffer)
+{
+  AddTracks(&mSessionOff);
+  CreateOffer();
+}
+
+TEST_P(JsepSessionTest, CreateOfferSetLocal)
+{
+  AddTracks(&mSessionOff);
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer);
+}
+
+TEST_P(JsepSessionTest, CreateOfferSetLocalSetRemote)
+{
+  AddTracks(&mSessionOff);
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer);
+  SetRemoteOffer(offer);
+}
+
+TEST_P(JsepSessionTest, CreateOfferSetLocalSetRemoteCreateAnswer)
+{
+  AddTracks(&mSessionOff);
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer);
+  SetRemoteOffer(offer);
+  AddTracks(&mSessionAns);
+  std::string answer = CreateAnswer();
+}
+
+TEST_P(JsepSessionTest, CreateOfferSetLocalSetRemoteCreateAnswerSetLocal)
+{
+  AddTracks(&mSessionOff);
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer);
+  SetRemoteOffer(offer);
+  AddTracks(&mSessionAns);
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer);
+}
+
+TEST_P(JsepSessionTest, FullCall)
+{
+  AddTracks(&mSessionOff);
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer);
+  SetRemoteOffer(offer);
+  AddTracks(&mSessionAns);
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer);
+  SetRemoteAnswer(answer);
+}
+
+TEST_P(JsepSessionTest, FullCallWithCandidates)
+{
+  AddTracks(&mSessionOff);
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer);
+  GatherOffererCandidates();
+  ValidateOffererCandidates();
+  SetRemoteOffer(offer);
+  TrickleOffererCandidates();
+  ValidateAnswererCandidates();
+  AddTracks(&mSessionAns);
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer);
+  SetRemoteAnswer(answer);
+}
+
+INSTANTIATE_TEST_CASE_P(Variants, JsepSessionTest,
+                        ::testing::Values("audio",
+                                          "video",
+                                          "datachannel",
+                                          "audio,video",
+                                          "video,audio",
+                                          "audio,datachannel",
+                                          "video,datachannel",
+                                          "video,audio,datachannel",
+                                          "audio,video,datachannel",
+                                          "datachannel,audio",
+                                          "datachannel,video",
+                                          "datachannel,audio,video",
+                                          "datachannel,video,audio"));
+
+// offerToReceiveXxx variants
+
+TEST_F(JsepSessionTest, OfferAnswerRecvOnlyLines)
+{
+  JsepOfferOptions options;
+  options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U));
+  options.mOfferToReceiveVideo = Some(static_cast<size_t>(2U));
+  options.mDontOfferDataChannel = Some(true);
+  std::string offer = CreateOffer(Some(options));
+
+  SipccSdpParser parser;
+  auto outputSdp = mozilla::Move(parser.Parse(offer));
+  ASSERT_TRUE(outputSdp) << "Should have valid SDP" << std::endl
+                         << "Errors were: " << GetParseErrors(parser);
+
+  ASSERT_EQ(3U, outputSdp->GetMediaSectionCount());
+  ASSERT_EQ(SdpMediaSection::kAudio,
+            outputSdp->GetMediaSection(0).GetMediaType());
+  ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
+            outputSdp->GetMediaSection(0).GetAttributeList().GetDirection());
+  ASSERT_EQ(SdpMediaSection::kVideo,
+            outputSdp->GetMediaSection(1).GetMediaType());
+  ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
+            outputSdp->GetMediaSection(1).GetAttributeList().GetDirection());
+  ASSERT_EQ(SdpMediaSection::kVideo,
+            outputSdp->GetMediaSection(2).GetMediaType());
+  ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
+            outputSdp->GetMediaSection(2).GetAttributeList().GetDirection());
+
+  ASSERT_TRUE(outputSdp->GetMediaSection(0).GetAttributeList().HasAttribute(
+      SdpAttribute::kRtcpMuxAttribute));
+  ASSERT_TRUE(outputSdp->GetMediaSection(1).GetAttributeList().HasAttribute(
+      SdpAttribute::kRtcpMuxAttribute));
+  ASSERT_TRUE(outputSdp->GetMediaSection(2).GetAttributeList().HasAttribute(
+      SdpAttribute::kRtcpMuxAttribute));
+
+  SetLocalOffer(offer, CHECK_SUCCESS);
+
+  AddTracks(&mSessionAns, "audio,video");
+  SetRemoteOffer(offer, CHECK_SUCCESS);
+
+  std::string answer = CreateAnswer();
+  outputSdp = mozilla::Move(parser.Parse(answer));
+
+  ASSERT_EQ(3U, outputSdp->GetMediaSectionCount());
+  ASSERT_EQ(SdpMediaSection::kAudio,
+            outputSdp->GetMediaSection(0).GetMediaType());
+  ASSERT_EQ(SdpDirectionAttribute::kSendonly,
+            outputSdp->GetMediaSection(0).GetAttributeList().GetDirection());
+  ASSERT_EQ(SdpMediaSection::kVideo,
+            outputSdp->GetMediaSection(1).GetMediaType());
+  ASSERT_EQ(SdpDirectionAttribute::kSendonly,
+            outputSdp->GetMediaSection(1).GetAttributeList().GetDirection());
+  ASSERT_EQ(SdpMediaSection::kVideo,
+            outputSdp->GetMediaSection(2).GetMediaType());
+  ASSERT_EQ(SdpDirectionAttribute::kInactive,
+            outputSdp->GetMediaSection(2).GetAttributeList().GetDirection());
+}
+
+TEST_F(JsepSessionTest, OfferAnswerSendOnlyLines)
+{
+  AddTracks(&mSessionOff, "audio,video,video");
+
+  JsepOfferOptions options;
+  options.mOfferToReceiveAudio = Some(static_cast<size_t>(0U));
+  options.mOfferToReceiveVideo = Some(static_cast<size_t>(1U));
+  options.mDontOfferDataChannel = Some(true);
+  std::string offer = CreateOffer(Some(options));
+
+  SipccSdpParser parser;
+  auto outputSdp = mozilla::Move(parser.Parse(offer));
+  ASSERT_TRUE(outputSdp) << "Should have valid SDP" << std::endl
+                         << "Errors were: " << GetParseErrors(parser);
+
+  ASSERT_EQ(3U, outputSdp->GetMediaSectionCount());
+  ASSERT_EQ(SdpMediaSection::kAudio,
+            outputSdp->GetMediaSection(0).GetMediaType());
+  ASSERT_EQ(SdpDirectionAttribute::kSendonly,
+            outputSdp->GetMediaSection(0).GetAttributeList().GetDirection());
+  ASSERT_EQ(SdpMediaSection::kVideo,
+            outputSdp->GetMediaSection(1).GetMediaType());
+  ASSERT_EQ(SdpDirectionAttribute::kSendrecv,
+            outputSdp->GetMediaSection(1).GetAttributeList().GetDirection());
+  ASSERT_EQ(SdpMediaSection::kVideo,
+            outputSdp->GetMediaSection(2).GetMediaType());
+  ASSERT_EQ(SdpDirectionAttribute::kSendonly,
+            outputSdp->GetMediaSection(2).GetAttributeList().GetDirection());
+
+  ASSERT_TRUE(outputSdp->GetMediaSection(0).GetAttributeList().HasAttribute(
+      SdpAttribute::kRtcpMuxAttribute));
+  ASSERT_TRUE(outputSdp->GetMediaSection(1).GetAttributeList().HasAttribute(
+      SdpAttribute::kRtcpMuxAttribute));
+  ASSERT_TRUE(outputSdp->GetMediaSection(2).GetAttributeList().HasAttribute(
+      SdpAttribute::kRtcpMuxAttribute));
+
+  SetLocalOffer(offer, CHECK_SUCCESS);
+
+  AddTracks(&mSessionAns, "audio,video");
+  SetRemoteOffer(offer, CHECK_SUCCESS);
+
+  std::string answer = CreateAnswer();
+  outputSdp = mozilla::Move(parser.Parse(answer));
+
+  ASSERT_EQ(3U, outputSdp->GetMediaSectionCount());
+  ASSERT_EQ(SdpMediaSection::kAudio,
+            outputSdp->GetMediaSection(0).GetMediaType());
+  ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
+            outputSdp->GetMediaSection(0).GetAttributeList().GetDirection());
+  ASSERT_EQ(SdpMediaSection::kVideo,
+            outputSdp->GetMediaSection(1).GetMediaType());
+  ASSERT_EQ(SdpDirectionAttribute::kSendrecv,
+            outputSdp->GetMediaSection(1).GetAttributeList().GetDirection());
+  ASSERT_EQ(SdpMediaSection::kVideo,
+            outputSdp->GetMediaSection(2).GetMediaType());
+  ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
+            outputSdp->GetMediaSection(2).GetAttributeList().GetDirection());
+}
+
+TEST_F(JsepSessionTest, CreateOfferNoDatachannelDefault)
+{
+  RefPtr<JsepTrack> msta(
+      new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1"));
+  mSessionOff.AddTrack(msta);
+
+  RefPtr<JsepTrack> mstv1(
+      new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v1"));
+  mSessionOff.AddTrack(mstv1);
+
+  std::string offer = CreateOffer();
+
+  SipccSdpParser parser;
+  auto outputSdp = mozilla::Move(parser.Parse(offer));
+  ASSERT_TRUE(outputSdp) << "Should have valid SDP" << std::endl
+                         << "Errors were: " << GetParseErrors(parser);
+
+  ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
+  ASSERT_EQ(SdpMediaSection::kAudio,
+            outputSdp->GetMediaSection(0).GetMediaType());
+  ASSERT_EQ(SdpMediaSection::kVideo,
+            outputSdp->GetMediaSection(1).GetMediaType());
+}
+
+TEST_F(JsepSessionTest, ValidateOfferedCodecParams)
+{
+  types.push_back(SdpMediaSection::kAudio);
+  types.push_back(SdpMediaSection::kVideo);
+
+  RefPtr<JsepTrack> msta(
+      new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1"));
+  mSessionOff.AddTrack(msta);
+  RefPtr<JsepTrack> mstv1(
+      new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v2"));
+  mSessionOff.AddTrack(mstv1);
+
+  std::string offer = CreateOffer();
+
+  SipccSdpParser parser;
+  auto outputSdp = mozilla::Move(parser.Parse(offer));
+  ASSERT_TRUE(outputSdp) << "Should have valid SDP" << std::endl
+                         << "Errors were: " << GetParseErrors(parser);
+
+  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(3U, video_section.GetFormats().size());
+  ASSERT_EQ("120", video_section.GetFormats()[0]);
+  ASSERT_EQ("126", video_section.GetFormats()[1]);
+  ASSERT_EQ("97", video_section.GetFormats()[2]);
+
+  // Validate rtpmap
+  ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kRtpmapAttribute));
+  auto& rtpmaps = video_attrs.GetRtpmap();
+  ASSERT_TRUE(rtpmaps.HasEntry("120"));
+  ASSERT_TRUE(rtpmaps.HasEntry("126"));
+  ASSERT_TRUE(rtpmaps.HasEntry("97"));
+
+  auto& vp8_entry = rtpmaps.GetEntry("120");
+  auto& h264_1_entry = rtpmaps.GetEntry("126");
+  auto& h264_0_entry = rtpmaps.GetEntry("97");
+
+  ASSERT_EQ("VP8", vp8_entry.name);
+  ASSERT_EQ("H264", h264_1_entry.name);
+  ASSERT_EQ("H264", h264_0_entry.name);
+
+  // Validate fmtps
+  ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kFmtpAttribute));
+  auto& fmtps = video_attrs.GetFmtp().mFmtps;
+
+  ASSERT_EQ(3U, fmtps.size());
+
+  // VP8
+  ASSERT_EQ("120", fmtps[0].format);
+  ASSERT_TRUE(fmtps[0].parameters);
+  ASSERT_EQ(SdpRtpmapAttributeList::kVP8, fmtps[0].parameters->codec_type);
+
+  auto& parsed_vp8_params =
+      *static_cast<const SdpFmtpAttributeList::VP8Parameters*>(
+          fmtps[0].parameters.get());
+
+  ASSERT_EQ((uint32_t)12288, parsed_vp8_params.max_fs);
+  ASSERT_EQ((uint32_t)60, parsed_vp8_params.max_fr);
+
+  // H264 packetization mode 1
+  ASSERT_EQ("126", fmtps[1].format);
+  ASSERT_TRUE(fmtps[1].parameters);
+  ASSERT_EQ(SdpRtpmapAttributeList::kH264, fmtps[1].parameters->codec_type);
+
+  auto& parsed_h264_1_params =
+      *static_cast<const SdpFmtpAttributeList::H264Parameters*>(
+          fmtps[1].parameters.get());
+
+  ASSERT_EQ((uint32_t)0x42e00d, parsed_h264_1_params.profile_level_id);
+  ASSERT_TRUE(parsed_h264_1_params.level_asymmetry_allowed);
+  ASSERT_EQ(1U, parsed_h264_1_params.packetization_mode);
+
+  // H264 packetization mode 0
+  ASSERT_EQ("97", fmtps[2].format);
+  ASSERT_TRUE(fmtps[2].parameters);
+  ASSERT_EQ(SdpRtpmapAttributeList::kH264, fmtps[2].parameters->codec_type);
+
+  auto& parsed_h264_0_params =
+      *static_cast<const SdpFmtpAttributeList::H264Parameters*>(
+          fmtps[2].parameters.get());
+
+  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);
+}
+
+TEST_F(JsepSessionTest, ValidateAnsweredCodecParams)
+{
+
+  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
+      if (h264->mDefaultPt == "126") {
+        h264->mDefaultPt = "97";
+      } else {
+        h264->mDefaultPt = "126";
+      }
+    }
+  }
+
+  types.push_back(SdpMediaSection::kAudio);
+  types.push_back(SdpMediaSection::kVideo);
+
+  RefPtr<JsepTrack> msta(
+      new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1"));
+  mSessionOff.AddTrack(msta);
+  RefPtr<JsepTrack> mstv1(
+      new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v1"));
+  mSessionOff.AddTrack(mstv1);
+
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer);
+  SetRemoteOffer(offer);
+
+  RefPtr<JsepTrack> msta_ans(
+      new JsepTrack(SdpMediaSection::kAudio, "answerer_stream", "a1"));
+  mSessionAns.AddTrack(msta);
+  RefPtr<JsepTrack> mstv1_ans(
+      new JsepTrack(SdpMediaSection::kVideo, "answerer_stream", "v1"));
+  mSessionAns.AddTrack(mstv1);
+
+  std::string answer = CreateAnswer();
+
+  SipccSdpParser parser;
+  auto outputSdp = mozilla::Move(parser.Parse(answer));
+  ASSERT_TRUE(outputSdp) << "Should have valid SDP" << std::endl
+                         << "Errors were: " << GetParseErrors(parser);
+
+  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());
+
+  // TODO(bug 1099351): Once fixed, this stuff will need to be updated.
+  ASSERT_EQ(1U, video_section.GetFormats().size());
+  // ASSERT_EQ(3U, video_section.GetFormats().size());
+  ASSERT_EQ("120", video_section.GetFormats()[0]);
+  // ASSERT_EQ("126", video_section.GetFormats()[1]);
+  // ASSERT_EQ("97", video_section.GetFormats()[2]);
+
+  // Validate rtpmap
+  ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kRtpmapAttribute));
+  auto& rtpmaps = video_attrs.GetRtpmap();
+  ASSERT_TRUE(rtpmaps.HasEntry("120"));
+  // ASSERT_TRUE(rtpmaps.HasEntry("126"));
+  // ASSERT_TRUE(rtpmaps.HasEntry("97"));
+
+  auto& vp8_entry = rtpmaps.GetEntry("120");
+  // auto& h264_1_entry = rtpmaps.GetEntry("126");
+  // auto& h264_0_entry = rtpmaps.GetEntry("97");
+
+  ASSERT_EQ("VP8", vp8_entry.name);
+  // ASSERT_EQ("H264", h264_1_entry.name);
+  // ASSERT_EQ("H264", h264_0_entry.name);
+
+  // Validate fmtps
+  ASSERT_TRUE(video_attrs.HasAttribute(SdpAttribute::kFmtpAttribute));
+  auto& fmtps = video_attrs.GetFmtp().mFmtps;
+
+  ASSERT_EQ(1U, fmtps.size());
+  // ASSERT_EQ(3U, fmtps.size());
+
+  // VP8
+  ASSERT_EQ("120", fmtps[0].format);
+  ASSERT_TRUE(fmtps[0].parameters);
+  ASSERT_EQ(SdpRtpmapAttributeList::kVP8, fmtps[0].parameters->codec_type);
+
+  auto& parsed_vp8_params =
+      *static_cast<const SdpFmtpAttributeList::VP8Parameters*>(
+          fmtps[0].parameters.get());
+
+  ASSERT_EQ((uint32_t)12288, parsed_vp8_params.max_fs);
+  ASSERT_EQ((uint32_t)60, parsed_vp8_params.max_fr);
+
+#if 0
+  // H264 packetization mode 1
+  ASSERT_EQ("126", fmtps[1].format);
+  ASSERT_TRUE(fmtps[1].parameters);
+  ASSERT_EQ(SdpRtpmapAttributeList::kH264, fmtps[1].parameters->codec_type);
+
+  auto& parsed_h264_1_params =
+    *static_cast<const SdpFmtpAttributeList::H264Parameters*>(
+        fmtps[1].parameters.get());
+
+  ASSERT_EQ((uint32_t)0x42a00d, parsed_h264_1_params.profile_level_id);
+  ASSERT_TRUE(parsed_h264_1_params.level_asymmetry_allowed);
+  ASSERT_EQ(1U, parsed_h264_1_params.packetization_mode);
+
+  // H264 packetization mode 0
+  ASSERT_EQ("97", fmtps[2].format);
+  ASSERT_TRUE(fmtps[2].parameters);
+  ASSERT_EQ(SdpRtpmapAttributeList::kH264, fmtps[2].parameters->codec_type);
+
+  auto& parsed_h264_0_params =
+    *static_cast<const SdpFmtpAttributeList::H264Parameters*>(
+        fmtps[2].parameters.get());
+
+  ASSERT_EQ((uint32_t)0x42a00d, 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);
+#endif
+}
+
+TEST_P(JsepSessionTest, TestRejectMline)
+{
+  AddTracks(&mSessionOff);
+  AddTracks(&mSessionAns);
+
+  switch (types.front()) {
+    case SdpMediaSection::kAudio:
+      // Sabotage audio
+      EnsureNegotiationFailure(types.front(), "opus");
+      break;
+    case SdpMediaSection::kVideo:
+      // Sabotage video
+      EnsureNegotiationFailure(types.front(), "H264");
+      break;
+    case SdpMediaSection::kApplication:
+      // Sabotage datachannel
+      EnsureNegotiationFailure(types.front(), "webrtc-datachannel");
+      break;
+    default:
+      ASSERT_TRUE(false) << "Unknown media type";
+  }
+
+  std::string offer = CreateOffer();
+  mSessionOff.SetLocalDescription(kJsepSdpOffer, offer);
+  mSessionAns.SetRemoteDescription(kJsepSdpOffer, offer);
+
+  std::string answer = CreateAnswer();
+
+  SipccSdpParser parser;
+  auto outputSdp = mozilla::Move(parser.Parse(answer));
+  ASSERT_TRUE(outputSdp) << "Should have valid SDP" << std::endl
+                         << "Errors were: " << GetParseErrors(parser);
+
+  ASSERT_NE(0U, outputSdp->GetMediaSectionCount());
+  SdpMediaSection* failed_section = nullptr;
+
+  for (size_t i = 0; i < outputSdp->GetMediaSectionCount(); ++i) {
+    if (outputSdp->GetMediaSection(i).GetMediaType() == types.front()) {
+      failed_section = &outputSdp->GetMediaSection(i);
+    }
+  }
+
+  ASSERT_TRUE(failed_section) << "Failed type was entirely absent from SDP";
+  auto& failed_attrs = failed_section->GetAttributeList();
+  ASSERT_EQ(SdpDirectionAttribute::kInactive, failed_attrs.GetDirection());
+  ASSERT_EQ(0U, failed_section->GetPort());
+
+  mSessionAns.SetLocalDescription(kJsepSdpAnswer, answer);
+  mSessionOff.SetRemoteDescription(kJsepSdpAnswer, answer);
+
+  ASSERT_EQ(types.size() - 1, mSessionOff.GetNegotiatedTrackPairCount());
+  ASSERT_EQ(types.size() - 1, mSessionAns.GetNegotiatedTrackPairCount());
+
+  ASSERT_EQ(types.size(), mSessionOff.GetTransportCount());
+  ASSERT_EQ(types.size(), mSessionOff.GetLocalTrackCount());
+  ASSERT_EQ(types.size() - 1, mSessionOff.GetRemoteTrackCount());
+
+  ASSERT_EQ(types.size(), mSessionAns.GetTransportCount());
+  ASSERT_EQ(types.size(), mSessionAns.GetLocalTrackCount());
+  ASSERT_EQ(types.size(), mSessionAns.GetRemoteTrackCount());
+}
+
+TEST_F(JsepSessionTest, CreateOfferNoMlines)
+{
+  JsepOfferOptions options;
+  std::string offer;
+  nsresult rv = mSessionOff.CreateOffer(options, &offer);
+  ASSERT_NE(NS_OK, rv);
+  ASSERT_NE("", mSessionOff.GetLastError());
+}
+
+TEST_F(JsepSessionTest, TestIceLite)
+{
+  AddTracks(&mSessionOff, "audio");
+  AddTracks(&mSessionAns, "audio");
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer, CHECK_SUCCESS);
+
+  SipccSdpParser parser;
+  UniquePtr<Sdp> parsedOffer = parser.Parse(offer);
+  parsedOffer->GetAttributeList().SetAttribute(
+      new SdpFlagAttribute(SdpAttribute::kIceLiteAttribute));
+
+  std::ostringstream os;
+  parsedOffer->Serialize(os);
+  SetRemoteOffer(os.str(), CHECK_SUCCESS);
+
+  ASSERT_TRUE(mSessionAns.RemoteIsIceLite());
+  ASSERT_FALSE(mSessionOff.RemoteIsIceLite());
+}
+
+TEST_F(JsepSessionTest, TestIceOptions)
+{
+  AddTracks(&mSessionOff, "audio");
+  AddTracks(&mSessionAns, "audio");
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer, CHECK_SUCCESS);
+  SetRemoteOffer(offer, CHECK_SUCCESS);
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer, CHECK_SUCCESS);
+  SetRemoteAnswer(answer, CHECK_SUCCESS);
+
+  ASSERT_EQ(1U, mSessionOff.GetIceOptions().size());
+  ASSERT_EQ("trickle", mSessionOff.GetIceOptions()[0]);
+
+  ASSERT_EQ(1U, mSessionAns.GetIceOptions().size());
+  ASSERT_EQ("trickle", mSessionAns.GetIceOptions()[0]);
+}
+
+TEST_F(JsepSessionTest, TestExtmap)
+{
+  AddTracks(&mSessionOff, "audio");
+  AddTracks(&mSessionAns, "audio");
+  // ssrc-audio-level will be extmap 1 for both
+  mSessionOff.AddAudioRtpExtension("foo"); // Default mapping of 2
+  mSessionOff.AddAudioRtpExtension("bar"); // Default mapping of 3
+  mSessionAns.AddAudioRtpExtension("bar"); // Default mapping of 2
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer, CHECK_SUCCESS);
+  SetRemoteOffer(offer, CHECK_SUCCESS);
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer, CHECK_SUCCESS);
+  SetRemoteAnswer(answer, CHECK_SUCCESS);
+
+  SipccSdpParser parser;
+  UniquePtr<Sdp> parsedOffer = parser.Parse(offer);
+  ASSERT_EQ(1U, parsedOffer->GetMediaSectionCount());
+
+  auto& offerMediaAttrs = parsedOffer->GetMediaSection(0).GetAttributeList();
+  ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute));
+  auto& offerExtmap = offerMediaAttrs.GetExtmap().mExtmaps;
+  ASSERT_EQ(3U, offerExtmap.size());
+  ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level",
+      offerExtmap[0].extensionname);
+  ASSERT_EQ(1U, offerExtmap[0].entry);
+  ASSERT_EQ("foo", offerExtmap[1].extensionname);
+  ASSERT_EQ(2U, offerExtmap[1].entry);
+  ASSERT_EQ("bar", offerExtmap[2].extensionname);
+  ASSERT_EQ(3U, offerExtmap[2].entry);
+
+  UniquePtr<Sdp> parsedAnswer = parser.Parse(answer);
+  ASSERT_EQ(1U, parsedAnswer->GetMediaSectionCount());
+
+  auto& answerMediaAttrs = parsedAnswer->GetMediaSection(0).GetAttributeList();
+  ASSERT_TRUE(answerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute));
+  auto& answerExtmap = answerMediaAttrs.GetExtmap().mExtmaps;
+  ASSERT_EQ(2U, answerExtmap.size());
+  ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level",
+      answerExtmap[0].extensionname);
+  ASSERT_EQ(1U, answerExtmap[0].entry);
+  // We ensure that the entry for "bar" matches what was in the offer
+  ASSERT_EQ("bar", answerExtmap[1].extensionname);
+  ASSERT_EQ(3U, answerExtmap[1].entry);
+}
+
+} // namespace mozilla
+
+int
+main(int argc, char** argv)
+{
+  NSS_NoDB_Init(nullptr);
+  NSS_SetDomesticPolicy();
+
+  ::testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}