Bug 1290948 - Part 4: Transceivers JSEP/SDP work. r+drno r=drno
☠☠ backed out by e077a6e6e842 ☠ ☠
authorByron Campen [:bwc] <docfaraday@gmail.com>
Wed, 23 Aug 2017 15:55:05 -0500
changeset 436337 ffb6e6da955fa81fc4faca06210aa2e5764ed205
parent 436336 56c169018cebfab1e4bb3a34b1aeddba2fec3d66
child 436338 1a5f090502b0a27641a0866513a82aa8c545fc14
push id117
push userfmarier@mozilla.com
push dateTue, 28 Nov 2017 20:17:16 +0000
reviewersdrno
bugs1290948
milestone59.0a1
Bug 1290948 - Part 4: Transceivers JSEP/SDP work. r+drno r=drno MozReview-Commit-ID: JwK3It3UA5M
media/webrtc/signaling/gtest/jsep_session_unittest.cpp
media/webrtc/signaling/gtest/jsep_track_unittest.cpp
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.cpp
media/webrtc/signaling/src/jsep/JsepTrack.h
media/webrtc/signaling/src/jsep/JsepTrackEncoding.h
media/webrtc/signaling/src/jsep/JsepTransceiver.h
media/webrtc/signaling/src/jsep/JsepTransport.h
media/webrtc/signaling/src/jsep/SsrcGenerator.cpp
media/webrtc/signaling/src/jsep/SsrcGenerator.h
media/webrtc/signaling/src/jsep/moz.build
media/webrtc/signaling/src/sdp/SdpAttribute.h
media/webrtc/signaling/src/sdp/SdpHelper.cpp
media/webrtc/signaling/src/sdp/SdpHelper.h
media/webrtc/signaling/src/sdp/SdpMediaSection.h
--- a/media/webrtc/signaling/gtest/jsep_session_unittest.cpp
+++ b/media/webrtc/signaling/gtest/jsep_session_unittest.cpp
@@ -123,52 +123,158 @@ protected:
   {
     tdata.iceCredentialSerial = 0;
     GenerateNewIceCredentials(session, tdata);
     session.SetIceCredentials(tdata.mIceUfrag, tdata.mIcePwd);
     AddDtlsFingerprint("sha-1", session, tdata);
     AddDtlsFingerprint("sha-256", session, tdata);
   }
 
+  void
+  CheckTransceiverInvariants(
+      const std::vector<RefPtr<JsepTransceiver>>& oldTransceivers,
+      const std::vector<RefPtr<JsepTransceiver>>& newTransceivers)
+  {
+    ASSERT_LE(oldTransceivers.size(), newTransceivers.size());
+    std::set<size_t> levels;
+
+    for (const RefPtr<JsepTransceiver>& newTransceiver : newTransceivers) {
+      if (newTransceiver->HasLevel()) {
+        ASSERT_FALSE(levels.count(newTransceiver->GetLevel()))
+                     << "Two new transceivers are mapped to level "
+                     << newTransceiver->GetLevel();
+        levels.insert(newTransceiver->GetLevel());
+      }
+    }
+
+    auto last = levels.rbegin();
+    if (last != levels.rend()) {
+      ASSERT_LE(*last, levels.size())
+          << "Max level observed in transceivers was " << *last
+          << ", but there are only " << levels.size() << " levels in the "
+          "transceivers.";
+    }
+
+    for (const RefPtr<JsepTransceiver>& oldTransceiver : oldTransceivers) {
+      if (oldTransceiver->HasLevel()) {
+        ASSERT_TRUE(levels.count(oldTransceiver->GetLevel()))
+                    << "Level " << oldTransceiver->GetLevel()
+                    << " had a transceiver in the old, but not the new (or, "
+                    "perhaps this level had more than one transceiver in the "
+                    "old)";
+        levels.erase(oldTransceiver->GetLevel());
+      }
+    }
+  }
+
+  std::vector<RefPtr<JsepTransceiver>>
+  DeepCopy(const std::vector<RefPtr<JsepTransceiver>>& transceivers)
+  {
+    std::vector<RefPtr<JsepTransceiver>> copy;
+    for (const RefPtr<JsepTransceiver>& transceiver : transceivers) {
+      copy.push_back(new JsepTransceiver(*transceiver));
+    }
+    return copy;
+  }
+
   std::string
   CreateOffer(const Maybe<JsepOfferOptions>& options = Nothing())
   {
+    std::vector<RefPtr<JsepTransceiver>> transceiversBefore =
+      DeepCopy(mSessionOff->GetTransceivers());
     JsepOfferOptions defaultOptions;
     const JsepOfferOptions& optionsRef = options ? *options : defaultOptions;
     std::string offer;
     nsresult rv;
     rv = mSessionOff->CreateOffer(optionsRef, &offer);
     EXPECT_EQ(NS_OK, rv) << mSessionOff->GetLastError();
 
     std::cerr << "OFFER: " << offer << std::endl;
 
     ValidateTransport(*mOffererTransport, offer);
 
+    if (transceiversBefore.size() != mSessionOff->GetTransceivers().size()) {
+      EXPECT_TRUE(false) << "CreateOffer changed number of transceivers!";
+      return offer;
+    }
+
+    CheckTransceiverInvariants(transceiversBefore,
+                               mSessionOff->GetTransceivers());
+
+    for (size_t i = 0; i < transceiversBefore.size(); ++i) {
+      RefPtr<JsepTransceiver>& oldTransceiver = transceiversBefore[i];
+      RefPtr<JsepTransceiver>& newTransceiver = mSessionOff->GetTransceivers()[i];
+      EXPECT_EQ(oldTransceiver->IsStopped(), newTransceiver->IsStopped());
+
+      if (oldTransceiver->IsStopped()) {
+        if (!newTransceiver->HasLevel()) {
+          // Tolerate unmapping of stopped transceivers by removing this
+          // difference.
+          oldTransceiver->ClearLevel();
+        }
+      } else if (!oldTransceiver->HasLevel()) {
+        EXPECT_TRUE(newTransceiver->HasLevel());
+        // Tolerate new mappings.
+        oldTransceiver->SetLevel(newTransceiver->GetLevel());
+      }
+
+      EXPECT_TRUE(Equals(*oldTransceiver, *newTransceiver));
+    }
+
     return offer;
   }
 
+  typedef enum {
+    NO_ADDTRACK_MAGIC,
+    ADDTRACK_MAGIC
+  } AddTrackMagic;
+
   void
-  AddTracks(JsepSessionImpl& side)
+  AddTracks(JsepSessionImpl& side, AddTrackMagic magic = ADDTRACK_MAGIC)
   {
     // 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());
+    AddTracks(side, types, magic);
   }
 
   void
-  AddTracks(JsepSessionImpl& side, const std::string& mediatypes)
+  AddTracks(JsepSessionImpl& side,
+            const std::string& mediatypes,
+            AddTrackMagic magic = ADDTRACK_MAGIC)
   {
-    AddTracks(side, BuildTypes(mediatypes));
+    AddTracks(side, BuildTypes(mediatypes), magic);
+  }
+
+  JsepTrack
+  RemoveTrack(JsepSession& side, size_t index) {
+    if (side.GetTransceivers().size() <= index) {
+      EXPECT_TRUE(false) << "Index " << index << " out of bounds!";
+      return JsepTrack(SdpMediaSection::kAudio, sdp::kSend);
+    }
+
+    RefPtr<JsepTransceiver>& transceiver(side.GetTransceivers()[index]);
+    JsepTrack& track = transceiver->mSendTrack;
+    EXPECT_FALSE(track.GetTrackId().empty()) << "No track at index " << index;
+
+    JsepTrack original(track);
+    track.ClearTrackIds();
+    transceiver->mJsDirection &= SdpDirectionAttribute::Direction::kRecvonly;
+    return original;
+  }
+
+  void
+  SetDirection(JsepSession& side,
+               size_t index,
+               SdpDirectionAttribute::Direction direction) {
+    ASSERT_LT(index, side.GetTransceivers().size())
+      << "Index " << index << " out of bounds!";
+
+    side.GetTransceivers()[index]->mJsDirection = direction;
   }
 
   std::vector<SdpMediaSection::MediaType>
   BuildTypes(const std::string& mediatypes)
   {
     std::vector<SdpMediaSection::MediaType> result;
     size_t ptr = 0;
 
@@ -193,87 +299,177 @@ protected:
       ptr = comma + 1;
     }
 
     return result;
   }
 
   void
   AddTracks(JsepSessionImpl& side,
-            const std::vector<SdpMediaSection::MediaType>& mediatypes)
+            const std::vector<SdpMediaSection::MediaType>& mediatypes,
+            AddTrackMagic magic = ADDTRACK_MAGIC)
   {
     FakeUuidGenerator uuid_gen;
     std::string stream_id;
     std::string track_id;
 
     ASSERT_TRUE(uuid_gen.Generate(&stream_id));
 
-    AddTracksToStream(side, stream_id, mediatypes);
+    AddTracksToStream(side, stream_id, mediatypes, magic);
   }
 
   void
   AddTracksToStream(JsepSessionImpl& side,
                     const std::string stream_id,
-                    const std::string& mediatypes)
+                    const std::string& mediatypes,
+                    AddTrackMagic magic = ADDTRACK_MAGIC)
   {
-    AddTracksToStream(side, stream_id, BuildTypes(mediatypes));
+    AddTracksToStream(side, stream_id, BuildTypes(mediatypes), magic);
+  }
+
+  // A bit of a hack. JsepSessionImpl populates the track-id automatically, just
+  // in case, because the w3c spec requires msid to be set even when there's no
+  // send track.
+  bool IsNull(const JsepTrack& track) const {
+    return track.GetStreamIds().empty() &&
+           (track.GetMediaType() != SdpMediaSection::MediaType::kApplication);
   }
 
   void
   AddTracksToStream(JsepSessionImpl& side,
                     const std::string stream_id,
-                    const std::vector<SdpMediaSection::MediaType>& mediatypes)
+                    const std::vector<SdpMediaSection::MediaType>& mediatypes,
+                    AddTrackMagic magic = ADDTRACK_MAGIC)
 
   {
     FakeUuidGenerator uuid_gen;
     std::string track_id;
 
-    for (auto track = mediatypes.begin(); track != mediatypes.end(); ++track) {
+    for (auto type : mediatypes) {
       ASSERT_TRUE(uuid_gen.Generate(&track_id));
 
-      RefPtr<JsepTrack> mst(new JsepTrack(*track, stream_id, track_id));
-      side.AddTrack(mst);
+      std::vector<RefPtr<JsepTransceiver>>& transceivers(side.GetTransceivers());
+      size_t i = transceivers.size();
+      if (magic == ADDTRACK_MAGIC) {
+        for (i = 0; i < transceivers.size(); ++i) {
+          if (transceivers[i]->mSendTrack.GetMediaType() != type) {
+            continue;
+          }
+
+          if (IsNull(transceivers[i]->mSendTrack) ||
+              type == SdpMediaSection::MediaType::kApplication) {
+            break;
+          }
+        }
+      }
+
+      if (i == transceivers.size()) {
+        side.AddTransceiver(new JsepTransceiver(type));
+        MOZ_ASSERT(i < transceivers.size());
+      }
+
+      std::cerr << "Updating send track for transceiver " << i << std::endl;
+      if (magic == ADDTRACK_MAGIC) {
+        transceivers[i]->SetAddTrackMagic();
+      }
+      transceivers[i]->mJsDirection |=
+        SdpDirectionAttribute::Direction::kSendonly;
+      transceivers[i]->mSendTrack.UpdateTrackIds(
+          std::vector<std::string>(1, stream_id), track_id);
     }
   }
 
-  bool HasMediaStream(std::vector<RefPtr<JsepTrack>> tracks) const {
-    for (auto i = tracks.begin(); i != tracks.end(); ++i) {
-      if ((*i)->GetMediaType() != SdpMediaSection::kApplication) {
-        return 1;
+  bool HasMediaStream(const std::vector<JsepTrack>& tracks) const {
+    for (const auto& track : tracks) {
+      if (track.GetMediaType() != SdpMediaSection::kApplication) {
+        return true;
       }
     }
-    return 0;
+    return false;
   }
 
   const std::string GetFirstLocalStreamId(JsepSessionImpl& side) const {
-    auto tracks = side.GetLocalTracks();
-    return (*tracks.begin())->GetStreamId();
+    auto tracks = GetLocalTracks(side);
+    return tracks.begin()->GetStreamIds()[0];
+  }
+
+  std::vector<JsepTrack>
+  GetLocalTracks(const JsepSession& session) const {
+    std::vector<JsepTrack> result;
+    for (const auto& transceiver : session.GetTransceivers()) {
+      if (!IsNull(transceiver->mSendTrack)) {
+        result.push_back(transceiver->mSendTrack);
+      }
+    }
+    return result;
+  }
+
+  std::vector<JsepTrack>
+  GetRemoteTracks(const JsepSession& session) const {
+    std::vector<JsepTrack> result;
+    for (const auto& transceiver : session.GetTransceivers()) {
+      if (!IsNull(transceiver->mRecvTrack)) {
+        result.push_back(transceiver->mRecvTrack);
+      }
+    }
+    return result;
+  }
+
+  JsepTransceiver*
+  GetDatachannelTransceiver(JsepSession& side) {
+    for (const auto& transceiver : side.GetTransceivers()) {
+      if (transceiver->mSendTrack.GetMediaType() ==
+            SdpMediaSection::MediaType::kApplication) {
+        return transceiver.get();
+      }
+    }
+
+    return nullptr;
+  }
+
+  JsepTransceiver*
+  GetNegotiatedTransceiver(JsepSession& side, size_t index) {
+    for (RefPtr<JsepTransceiver>& transceiver : side.GetTransceivers()) {
+      if (transceiver->mSendTrack.GetNegotiatedDetails() ||
+          transceiver->mRecvTrack.GetNegotiatedDetails()) {
+        if (index) {
+          --index;
+          continue;
+        }
+
+        return transceiver.get();
+      }
+    }
+
+    return nullptr;
   }
 
   std::vector<std::string>
-  GetMediaStreamIds(std::vector<RefPtr<JsepTrack>> tracks) const {
+  GetMediaStreamIds(const std::vector<JsepTrack>& tracks) const {
     std::vector<std::string> ids;
-    for (auto i = tracks.begin(); i != tracks.end(); ++i) {
+    for (const auto& track : tracks) {
       // data channels don't have msid's
-      if ((*i)->GetMediaType() == SdpMediaSection::kApplication) {
+      if (track.GetMediaType() == SdpMediaSection::kApplication) {
         continue;
       }
-      ids.push_back((*i)->GetStreamId());
+      ids.insert(ids.end(),
+                 track.GetStreamIds().begin(),
+                 track.GetStreamIds().end());
     }
     return ids;
   }
 
   std::vector<std::string>
   GetLocalMediaStreamIds(JsepSessionImpl& side) const {
-    return GetMediaStreamIds(side.GetLocalTracks());
+    return GetMediaStreamIds(GetLocalTracks(side));
   }
 
   std::vector<std::string>
   GetRemoteMediaStreamIds(JsepSessionImpl& side) const {
-    return GetMediaStreamIds(side.GetRemoteTracks());
+    return GetMediaStreamIds(GetRemoteTracks(side));
   }
 
   std::vector<std::string>
   sortUniqueStrVector(std::vector<std::string> in) const {
     std::sort(in.begin(), in.end());
     auto it = std::unique(in.begin(), in.end());
     in.resize( std::distance(in.begin(), it));
     return in;
@@ -284,59 +480,49 @@ protected:
     return sortUniqueStrVector(GetLocalMediaStreamIds(side));
   }
 
   std::vector<std::string>
   GetRemoteUniqueStreamIds(JsepSessionImpl& side) const {
     return sortUniqueStrVector(GetRemoteMediaStreamIds(side));
   }
 
-  RefPtr<JsepTrack> GetTrack(JsepSessionImpl& side,
-                             SdpMediaSection::MediaType type,
-                             size_t index) const {
-    auto tracks = side.GetLocalTracks();
-
-    for (auto i = tracks.begin(); i != tracks.end(); ++i) {
-      if ((*i)->GetMediaType() != type) {
+  JsepTrack GetTrack(JsepSessionImpl& side,
+                     SdpMediaSection::MediaType type,
+                     size_t index) const {
+    for (const auto& transceiver : side.GetTransceivers()) {
+      if (IsNull(transceiver->mSendTrack) ||
+          transceiver->mSendTrack.GetMediaType() != type) {
         continue;
       }
 
       if (index != 0) {
         --index;
         continue;
       }
 
-      return *i;
+      return transceiver->mSendTrack;
     }
 
-    return RefPtr<JsepTrack>(nullptr);
+    return JsepTrack(type, sdp::kSend);
   }
 
-  RefPtr<JsepTrack> GetTrackOff(size_t index,
-                                SdpMediaSection::MediaType type) {
+  JsepTrack GetTrackOff(size_t index, SdpMediaSection::MediaType type) {
     return GetTrack(*mSessionOff, type, index);
   }
 
-  RefPtr<JsepTrack> GetTrackAns(size_t index,
-                                SdpMediaSection::MediaType type) {
+  JsepTrack GetTrackAns(size_t index, SdpMediaSection::MediaType type) {
     return GetTrack(*mSessionAns, type, index);
   }
 
-  class ComparePairsByLevel {
-    public:
-      bool operator()(const JsepTrackPair& lhs,
-                      const JsepTrackPair& rhs) const {
-        return lhs.mLevel < rhs.mLevel;
-      }
-  };
-
-  std::vector<JsepTrackPair> GetTrackPairsByLevel(JsepSessionImpl& side) const {
-    auto pairs = side.GetNegotiatedTrackPairs();
-    std::sort(pairs.begin(), pairs.end(), ComparePairsByLevel());
-    return pairs;
+  size_t CountRtpTypes() const {
+    return std::count_if(
+        types.begin(), types.end(),
+        [](SdpMediaSection::MediaType type)
+          {return type != SdpMediaSection::MediaType::kApplication;});
   }
 
   bool Equals(const SdpFingerprintAttributeList::Fingerprint& f1,
               const SdpFingerprintAttributeList::Fingerprint& f2) const {
     if (f1.hashFunc != f2.hashFunc) {
       return false;
     }
 
@@ -400,75 +586,143 @@ protected:
 
     if (t1->GetPassword() != t2->GetPassword()) {
       return false;
     }
 
     return true;
   }
 
-  bool Equals(const RefPtr<JsepTransport>& t1,
-              const RefPtr<JsepTransport>& t2) const {
-    if (!t1 && !t2) {
-      return true;
+  bool Equals(const JsepTransport& t1,
+              const JsepTransport& t2) const {
+    if (t1.mTransportId != t2.mTransportId) {
+      std::cerr << "Transport id differs: " << t1.mTransportId << " vs "
+                << t2.mTransportId << std::endl;
+      return false;
     }
 
-    if (!t1 || !t2) {
+    if (t1.mComponents != t2.mComponents) {
+      std::cerr << "Component count differs" << std::endl;
+      return false;
+    }
+
+    if (!Equals(t1.mIce, t2.mIce)) {
+      std::cerr << "ICE differs" << std::endl;
       return false;
     }
 
-    if (t1->mTransportId != t2->mTransportId) {
+    return true;
+  }
+
+  bool Equals(const JsepTrack& t1, const JsepTrack& t2) const {
+    if (t1.GetMediaType() != t2.GetMediaType()) {
+      return false;
+    }
+
+    if (t1.GetDirection() != t2.GetDirection()) {
+      return false;
+    }
+
+    if (t1.GetStreamIds() != t2.GetStreamIds()) {
       return false;
     }
 
-    if (t1->mComponents != t2->mComponents) {
+    if (t1.GetTrackId() != t2.GetTrackId()) {
+      return false;
+    }
+
+    if (t1.GetActive() != t2.GetActive()) {
       return false;
     }
 
-    if (!Equals(t1->mIce, t2->mIce)) {
+    if (t1.GetCNAME() != t2.GetCNAME()) {
+      return false;
+    }
+
+    if (t1.GetSsrcs() != t2.GetSsrcs()) {
       return false;
     }
 
     return true;
   }
 
-  bool Equals(const JsepTrackPair& p1,
-              const JsepTrackPair& p2) const {
-    if (p1.mLevel != p2.mLevel) {
+  bool Equals(const JsepTransceiver& p1,
+              const JsepTransceiver& p2) const {
+    if (p1.HasLevel() != p2.HasLevel()) {
+      std::cerr << "One transceiver has a level, the other doesn't"
+                << std::endl;
+      return false;
+    }
+
+    if (p1.HasLevel() && (p1.GetLevel() != p2.GetLevel())) {
+      std::cerr << "Level differs: " << p1.GetLevel() << " vs " << p2.GetLevel()
+                << std::endl;
       return false;
     }
 
     // We don't check things like BundleLevel(), since that can change without
     // any changes to the transport, which is what we're really interested in.
 
-    if (p1.mSending.get() != p2.mSending.get()) {
+    if (p1.IsStopped() != p2.IsStopped()) {
+      std::cerr << "One transceiver is stopped, the other is not" << std::endl;
+      return false;
+    }
+
+    if (p1.IsAssociated() != p2.IsAssociated()) {
+      std::cerr << "One transceiver has a mid, the other doesn't"
+                << std::endl;
       return false;
     }
 
-    if (p1.mReceiving.get() != p2.mReceiving.get()) {
+    if (p1.IsAssociated() && (p1.GetMid() != p2.GetMid())) {
+      std::cerr << "mid differs: " << p1.GetMid() << " vs " << p2.GetMid()
+                << std::endl;
+      return false;
+    }
+
+    if (!Equals(p1.mSendTrack, p2.mSendTrack)) {
+      std::cerr << "Send track differs" << std::endl;
       return false;
     }
 
-    if (!Equals(p1.mRtpTransport, p2.mRtpTransport)) {
+    if (!Equals(p1.mRecvTrack, p2.mRecvTrack)) {
+      std::cerr << "Receive track differs" << std::endl;
+      return false;
+    }
+
+    if (!Equals(p1.mTransport, p2.mTransport)) {
+      std::cerr << "Transport differs" << std::endl;
       return false;
     }
 
-    if (!Equals(p1.mRtcpTransport, p2.mRtcpTransport)) {
+    return true;
+  }
+
+  bool Equals(const std::vector<RefPtr<JsepTransceiver>>& t1,
+              const std::vector<RefPtr<JsepTransceiver>>& t2) const {
+    if (t1.size() != t2.size()) {
+      std::cerr << "Size differs: t1.size = " << t1.size() << ", t2.size = "
+                << t2.size() << std::endl;
       return false;
     }
 
+    for (size_t i = 0; i < t1.size(); ++i) {
+      if (!Equals(*t1[i], *t2[i])) {
+        return false;
+      }
+    }
+
     return true;
   }
 
   size_t GetTrackCount(JsepSessionImpl& side,
                        SdpMediaSection::MediaType type) const {
-    auto tracks = side.GetLocalTracks();
     size_t result = 0;
-    for (auto i = tracks.begin(); i != tracks.end(); ++i) {
-      if ((*i)->GetMediaType() == type) {
+    for (const auto& track : GetLocalTracks(side)) {
+      if (track.GetMediaType() == type) {
         ++result;
       }
     }
     return result;
   }
 
   UniquePtr<Sdp> GetParsedLocalDescription(const JsepSessionImpl& side) const {
     return Parse(side.GetLocalDescription(kJsepDescriptionCurrent));
@@ -517,52 +771,53 @@ protected:
       }
     }
   }
 
   void
   EnsureNegotiationFailure(SdpMediaSection::MediaType type,
                            const std::string& codecName)
   {
-    for (auto i = mSessionOff->Codecs().begin(); i != mSessionOff->Codecs().end();
-         ++i) {
-      auto* codec = *i;
+    for (auto* codec : mSessionOff->Codecs()) {
       if (codec->mType == type && codec->mName != codecName) {
         codec->mEnabled = false;
       }
     }
 
-    for (auto i = mSessionAns->Codecs().begin(); i != mSessionAns->Codecs().end();
-         ++i) {
-      auto* codec = *i;
+    for (auto* codec : mSessionAns->Codecs()) {
       if (codec->mType == type && codec->mName == codecName) {
         codec->mEnabled = false;
       }
     }
   }
 
   std::string
   CreateAnswer()
   {
+    std::vector<RefPtr<JsepTransceiver>> transceiversBefore =
+      DeepCopy(mSessionAns->GetTransceivers());
+
     JsepAnswerOptions options;
     std::string answer;
 
     // detect ice restart and generate new ice credentials (like
     // PeerConnectionImpl does).
     if (mSessionAns->RemoteIceIsRestarting()) {
       GenerateNewIceCredentials(*mSessionAns, *mAnswererTransport);
       mSessionAns->SetIceCredentials(mAnswererTransport->mIceUfrag,
                                      mAnswererTransport->mIcePwd);
     }
     nsresult rv = mSessionAns->CreateAnswer(options, &answer);
     EXPECT_EQ(NS_OK, rv);
 
     std::cerr << "ANSWER: " << answer << std::endl;
 
     ValidateTransport(*mAnswererTransport, answer);
+    CheckTransceiverInvariants(transceiversBefore,
+                               mSessionAns->GetTransceivers());
 
     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;
@@ -576,154 +831,187 @@ protected:
     std::string answer = CreateAnswer();
     SetLocalAnswer(answer, checkFlags);
     SetRemoteAnswer(answer, checkFlags);
   }
 
   void
   SetLocalOffer(const std::string& offer, uint32_t checkFlags = ALL_CHECKS)
   {
+    std::vector<RefPtr<JsepTransceiver>> transceiversBefore =
+      DeepCopy(mSessionOff->GetTransceivers());
+
     nsresult rv = mSessionOff->SetLocalDescription(kJsepSdpOffer, offer);
 
+    CheckTransceiverInvariants(transceiversBefore,
+                               mSessionOff->GetTransceivers());
+
     if (checkFlags & CHECK_SUCCESS) {
       ASSERT_EQ(NS_OK, rv);
     }
 
     if (checkFlags & CHECK_TRACKS) {
-      // Check that the transports exist.
-      ASSERT_EQ(types.size(), mSessionOff->GetTransports().size());
-      auto tracks = mSessionOff->GetLocalTracks();
-      for (size_t i = 0; i < types.size(); ++i) {
-        ASSERT_NE("", tracks[i]->GetStreamId());
-        ASSERT_NE("", tracks[i]->GetTrackId());
-        if (tracks[i]->GetMediaType() != SdpMediaSection::kApplication) {
+      // This assumes no recvonly or inactive transceivers.
+      ASSERT_EQ(types.size(), mSessionOff->GetTransceivers().size());
+      for (const auto& transceiver : mSessionOff->GetTransceivers()) {
+        if (!transceiver->HasLevel()) {
+          continue;
+        }
+        const auto& track(transceiver->mSendTrack);
+        size_t level = transceiver->GetLevel();
+        ASSERT_FALSE(IsNull(track));
+        ASSERT_EQ(types[level], track.GetMediaType());
+        if (track.GetMediaType() != SdpMediaSection::kApplication) {
           std::string msidAttr("a=msid:");
-          msidAttr += tracks[i]->GetStreamId();
+          msidAttr += track.GetStreamIds()[0];
           msidAttr += " ";
-          msidAttr += tracks[i]->GetTrackId();
+          msidAttr += track.GetTrackId();
           ASSERT_NE(std::string::npos, offer.find(msidAttr))
             << "Did not find " << msidAttr << " in offer";
         }
       }
       if (types.size() == 1 &&
-          tracks[0]->GetMediaType() == SdpMediaSection::kApplication) {
+          types[0] == SdpMediaSection::kApplication) {
         ASSERT_EQ(std::string::npos, offer.find("a=ssrc"))
           << "Data channel should not contain SSRC";
       }
     }
   }
 
   void
   SetRemoteOffer(const std::string& offer, uint32_t checkFlags = ALL_CHECKS)
   {
+    std::vector<RefPtr<JsepTransceiver>> transceiversBefore =
+      DeepCopy(mSessionAns->GetTransceivers());
+
     nsresult rv = mSessionAns->SetRemoteDescription(kJsepSdpOffer, offer);
 
+    CheckTransceiverInvariants(transceiversBefore,
+                               mSessionAns->GetTransceivers());
+
     if (checkFlags & CHECK_SUCCESS) {
       ASSERT_EQ(NS_OK, rv);
     }
 
     if (checkFlags & CHECK_TRACKS) {
-      auto tracks = mSessionAns->GetRemoteTracks();
-      // Now verify that the right stuff is in the tracks.
-      ASSERT_EQ(types.size(), tracks.size());
-      for (size_t i = 0; i < tracks.size(); ++i) {
-        ASSERT_EQ(types[i], tracks[i]->GetMediaType());
-        ASSERT_NE("", tracks[i]->GetStreamId());
-        ASSERT_NE("", tracks[i]->GetTrackId());
-        if (tracks[i]->GetMediaType() != SdpMediaSection::kApplication) {
+      // This assumes no recvonly or inactive transceivers.
+      ASSERT_EQ(types.size(), mSessionAns->GetTransceivers().size());
+      for (const auto& transceiver : mSessionAns->GetTransceivers()) {
+        if (!transceiver->HasLevel()) {
+          continue;
+        }
+        const auto& track(transceiver->mRecvTrack);
+        size_t level = transceiver->GetLevel();
+        ASSERT_FALSE(IsNull(track));
+        ASSERT_EQ(types[level], track.GetMediaType());
+        if (track.GetMediaType() != SdpMediaSection::kApplication) {
           std::string msidAttr("a=msid:");
-          msidAttr += tracks[i]->GetStreamId();
+          msidAttr += track.GetStreamIds()[0];
           msidAttr += " ";
-          msidAttr += tracks[i]->GetTrackId();
+          msidAttr += track.GetTrackId();
           ASSERT_NE(std::string::npos, offer.find(msidAttr))
             << "Did not find " << msidAttr << " in offer";
         }
       }
     }
   }
 
   void
   SetLocalAnswer(const std::string& answer, uint32_t checkFlags = ALL_CHECKS)
   {
+    std::vector<RefPtr<JsepTransceiver>> transceiversBefore =
+      DeepCopy(mSessionAns->GetTransceivers());
+
     nsresult rv = mSessionAns->SetLocalDescription(kJsepSdpAnswer, answer);
     if (checkFlags & CHECK_SUCCESS) {
       ASSERT_EQ(NS_OK, rv);
     }
 
+    CheckTransceiverInvariants(transceiversBefore,
+                               mSessionAns->GetTransceivers());
+
     if (checkFlags & CHECK_TRACKS) {
       // Verify that the right stuff is in the tracks.
-      auto pairs = mSessionAns->GetNegotiatedTrackPairs();
-      ASSERT_EQ(types.size(), pairs.size());
-      for (size_t i = 0; i < types.size(); ++i) {
-        ASSERT_TRUE(pairs[i].mSending);
-        ASSERT_EQ(types[i], pairs[i].mSending->GetMediaType());
-        ASSERT_TRUE(pairs[i].mReceiving);
-        ASSERT_EQ(types[i], pairs[i].mReceiving->GetMediaType());
-        ASSERT_NE("", pairs[i].mSending->GetStreamId());
-        ASSERT_NE("", pairs[i].mSending->GetTrackId());
+      ASSERT_EQ(types.size(), mSessionAns->GetTransceivers().size());
+      for (const auto& transceiver : mSessionAns->GetTransceivers()) {
+        if (!transceiver->HasLevel()) {
+          continue;
+        }
+        const auto& sendTrack(transceiver->mSendTrack);
+        const auto& recvTrack(transceiver->mRecvTrack);
+        size_t level = transceiver->GetLevel();
+        ASSERT_FALSE(IsNull(sendTrack));
+        ASSERT_EQ(types[level], sendTrack.GetMediaType());
         // These might have been in the SDP, or might have been randomly
         // chosen by JsepSessionImpl
-        ASSERT_NE("", pairs[i].mReceiving->GetStreamId());
-        ASSERT_NE("", pairs[i].mReceiving->GetTrackId());
-
-        if (pairs[i].mReceiving->GetMediaType() != SdpMediaSection::kApplication) {
+        ASSERT_FALSE(IsNull(recvTrack));
+        ASSERT_EQ(types[level], recvTrack.GetMediaType());
+
+        if (recvTrack.GetMediaType() != SdpMediaSection::kApplication) {
           std::string msidAttr("a=msid:");
-          msidAttr += pairs[i].mSending->GetStreamId();
+          msidAttr += sendTrack.GetStreamIds()[0];
           msidAttr += " ";
-          msidAttr += pairs[i].mSending->GetTrackId();
+          msidAttr += sendTrack.GetTrackId();
           ASSERT_NE(std::string::npos, answer.find(msidAttr))
-            << "Did not find " << msidAttr << " in offer";
+            << "Did not find " << msidAttr << " in answer";
         }
       }
       if (types.size() == 1 &&
-          pairs[0].mReceiving->GetMediaType() == SdpMediaSection::kApplication) {
+          types[0] == SdpMediaSection::kApplication) {
         ASSERT_EQ(std::string::npos, answer.find("a=ssrc"))
           << "Data channel should not contain SSRC";
       }
     }
-    std::cerr << "OFFER pairs:" << std::endl;
-    DumpTrackPairs(*mSessionOff);
+    std::cerr << "Answerer transceivers:" << std::endl;
+    DumpTransceivers(*mSessionAns);
   }
 
   void
   SetRemoteAnswer(const std::string& answer, uint32_t checkFlags = ALL_CHECKS)
   {
+    std::vector<RefPtr<JsepTransceiver>> transceiversBefore =
+      DeepCopy(mSessionOff->GetTransceivers());
+
     nsresult rv = mSessionOff->SetRemoteDescription(kJsepSdpAnswer, answer);
     if (checkFlags & CHECK_SUCCESS) {
       ASSERT_EQ(NS_OK, rv);
     }
 
+    CheckTransceiverInvariants(transceiversBefore,
+                               mSessionOff->GetTransceivers());
+
     if (checkFlags & CHECK_TRACKS) {
       // Verify that the right stuff is in the tracks.
-      auto pairs = mSessionOff->GetNegotiatedTrackPairs();
-      ASSERT_EQ(types.size(), pairs.size());
-      for (size_t i = 0; i < types.size(); ++i) {
-        ASSERT_TRUE(pairs[i].mSending);
-        ASSERT_EQ(types[i], pairs[i].mSending->GetMediaType());
-        ASSERT_TRUE(pairs[i].mReceiving);
-        ASSERT_EQ(types[i], pairs[i].mReceiving->GetMediaType());
-        ASSERT_NE("", pairs[i].mSending->GetStreamId());
-        ASSERT_NE("", pairs[i].mSending->GetTrackId());
+      ASSERT_EQ(types.size(), mSessionOff->GetTransceivers().size());
+      for (const auto& transceiver : mSessionOff->GetTransceivers()) {
+        if (!transceiver->HasLevel()) {
+          continue;
+        }
+        const auto& sendTrack(transceiver->mSendTrack);
+        const auto& recvTrack(transceiver->mRecvTrack);
+        size_t level = transceiver->GetLevel();
+        ASSERT_FALSE(IsNull(sendTrack));
+        ASSERT_EQ(types[level], sendTrack.GetMediaType());
         // These might have been in the SDP, or might have been randomly
         // chosen by JsepSessionImpl
-        ASSERT_NE("", pairs[i].mReceiving->GetStreamId());
-        ASSERT_NE("", pairs[i].mReceiving->GetTrackId());
-
-        if (pairs[i].mReceiving->GetMediaType() != SdpMediaSection::kApplication) {
+        ASSERT_FALSE(IsNull(recvTrack));
+        ASSERT_EQ(types[level], recvTrack.GetMediaType());
+
+        if (recvTrack.GetMediaType() != SdpMediaSection::kApplication) {
           std::string msidAttr("a=msid:");
-          msidAttr += pairs[i].mReceiving->GetStreamId();
+          msidAttr += recvTrack.GetStreamIds()[0];
           msidAttr += " ";
-          msidAttr += pairs[i].mReceiving->GetTrackId();
+          msidAttr += recvTrack.GetTrackId();
           ASSERT_NE(std::string::npos, answer.find(msidAttr))
             << "Did not find " << msidAttr << " in answer";
         }
       }
     }
-    std::cerr << "ANSWER pairs:" << std::endl;
-    DumpTrackPairs(*mSessionAns);
+    std::cerr << "Offerer transceivers:" << std::endl;
+    DumpTransceivers(*mSessionOff);
   }
 
   typedef enum {
     RTP = 1,
     RTCP = 2
   } ComponentType;
 
   class CandidateSet {
@@ -963,23 +1251,22 @@ protected:
         << context << " (level " << msection.GetLevel() << ")";
     } else {
       ASSERT_FALSE(msection.GetAttributeList().HasAttribute(
             SdpAttribute::kEndOfCandidatesAttribute))
         << context << " (level " << msection.GetLevel() << ")";
     }
   }
 
-  void CheckPairs(const JsepSession& session, const std::string& context)
+  void CheckTransceiversAreBundled(const JsepSession& session,
+                                   const std::string& context)
   {
-    auto pairs = session.GetNegotiatedTrackPairs();
-
-    for (JsepTrackPair& pair : pairs) {
-      ASSERT_TRUE(pair.HasBundleLevel()) << context;
-      ASSERT_EQ(0U, pair.BundleLevel()) << context;
+    for (const auto& transceiver : session.GetTransceivers()) {
+      ASSERT_TRUE(transceiver->HasBundleLevel()) << context;
+      ASSERT_EQ(0U, transceiver->BundleLevel()) << context;
     }
   }
 
   void
   DisableMsid(std::string* sdp) const {
     size_t pos = sdp->find("a=msid-semantic");
     ASSERT_NE(std::string::npos, pos);
     (*sdp)[pos + 2] = 'X'; // garble, a=Xsid-semantic
@@ -1061,16 +1348,19 @@ protected:
     } else {
       // Not that we would have any test which tests this...
       ASSERT_EQ("19", msection->GetFormats()[0]);
       const SdpRtpmapAttributeList::Rtpmap* rtpmap(msection->FindRtpmap("19"));
       ASSERT_TRUE(rtpmap);
       ASSERT_EQ("19", rtpmap->pt);
       ASSERT_EQ("reserved", rtpmap->name);
     }
+
+    ASSERT_FALSE(msection->GetAttributeList().HasAttribute(
+          SdpAttribute::kMsidAttribute));
   }
 
   void
   ValidateSetupAttribute(const JsepSessionImpl& side,
                          const SdpSetupAttribute::Role expectedRole)
   {
     auto sdp = GetParsedLocalDescription(side);
     for (size_t i = 0; sdp && i < sdp->GetMediaSectionCount(); ++i) {
@@ -1081,17 +1371,22 @@ protected:
       }
     }
   }
 
   void
   DumpTrack(const JsepTrack& track)
   {
     const JsepTrackNegotiatedDetails* details = track.GetNegotiatedDetails();
-    std::cerr << "  type=" << track.GetMediaType() << std::endl;
+    std::cerr << "  type=" << track.GetMediaType() << " track-id="
+              << track.GetTrackId() << std::endl;
+    if (!details) {
+      std::cerr << "  not negotiated" << std::endl;
+      return;
+    }
     std::cerr << "  encodings=" << std::endl;
     for (size_t i = 0; i < details->GetEncodingCount(); ++i) {
       const JsepTrackEncoding& encoding = details->GetEncoding(i);
       std::cerr << "    id=" << encoding.mRid << std::endl;
       for (const JsepCodecDescription* codec : encoding.GetCodecs()) {
         std::cerr << "      " << codec->mName
                   << " enabled(" << (codec->mEnabled?"yes":"no") << ")";
         if (track.GetMediaType() == SdpMediaSection::kAudio) {
@@ -1100,28 +1395,36 @@ protected:
           std::cerr << " dtmf(" << (audioCodec->mDtmfEnabled?"yes":"no") << ")";
         }
         std::cerr << std::endl;
       }
     }
   }
 
   void
-  DumpTrackPairs(const JsepSessionImpl& session)
+  DumpTransceivers(const JsepSessionImpl& session)
   {
-    auto pairs = mSessionAns->GetNegotiatedTrackPairs();
-    for (auto i = pairs.begin(); i != pairs.end(); ++i) {
-      std::cerr << "Track pair " << i->mLevel << std::endl;
-      if (i->mSending) {
+    for (const auto& transceiver : mSessionAns->GetTransceivers()) {
+      std::cerr << "Transceiver ";
+      if (transceiver->HasLevel()) {
+        std::cerr << transceiver->GetLevel() << std::endl;
+      } else {
+        std::cerr << "<NO LEVEL>" << std::endl;
+      }
+      if (transceiver->HasBundleLevel()) {
+        std::cerr << "(bundle level is " << transceiver->BundleLevel() << ")"
+                  << std::endl;
+      }
+      if (!IsNull(transceiver->mSendTrack)) {
         std::cerr << "Sending-->" << std::endl;
-        DumpTrack(*i->mSending);
+        DumpTrack(transceiver->mSendTrack);
       }
-      if (i->mReceiving) {
+      if (!IsNull(transceiver->mRecvTrack)) {
         std::cerr << "Receiving-->" << std::endl;
-        DumpTrack(*i->mReceiving);
+        DumpTrack(transceiver->mRecvTrack);
       }
     }
   }
 
   UniquePtr<Sdp>
   Parse(const std::string& sdp) const
   {
     SipccSdpParser parser;
@@ -1333,34 +1636,36 @@ TEST_P(JsepSessionTest, RenegotiationNoC
 {
   AddTracks(*mSessionOff);
   std::string offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
 
   auto added = mSessionAns->GetRemoteTracksAdded();
   auto removed = mSessionAns->GetRemoteTracksRemoved();
-  ASSERT_EQ(types.size(), added.size());
+  ASSERT_EQ(CountRtpTypes(), added.size());
   ASSERT_EQ(0U, removed.size());
 
   AddTracks(*mSessionAns);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
-  ASSERT_EQ(types.size(), added.size());
+  ASSERT_EQ(CountRtpTypes(), added.size());
   ASSERT_EQ(0U, removed.size());
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
 
   std::string reoffer = CreateOffer();
   SetLocalOffer(reoffer);
   SetRemoteOffer(reoffer);
 
   added = mSessionAns->GetRemoteTracksAdded();
   removed = mSessionAns->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
@@ -1373,28 +1678,21 @@ TEST_P(JsepSessionTest, RenegotiationNoC
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(0U, removed.size());
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
-  }
-
-  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());
-  for (size_t i = 0; i < answererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
-  }
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
+  ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers));
 }
 
 // Disabled: See Bug 1329028
 TEST_P(JsepSessionTest, DISABLED_RenegotiationSwappedRolesNoChange)
 {
   AddTracks(*mSessionOff);
   std::string offer = CreateOffer();
   SetLocalOffer(offer);
@@ -1413,18 +1711,18 @@ TEST_P(JsepSessionTest, DISABLED_Renegot
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(types.size(), added.size());
   ASSERT_EQ(0U, removed.size());
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  auto offererTransceivers = DeepCopy(mSessionOff->GetTransceivers());
+  auto answererTransceivers = DeepCopy(mSessionAns->GetTransceivers());
 
   SwapOfferAnswerRoles();
 
   std::string reoffer = CreateOffer();
   SetLocalOffer(reoffer);
   SetRemoteOffer(reoffer);
 
   added = mSessionAns->GetRemoteTracksAdded();
@@ -1439,108 +1737,102 @@ TEST_P(JsepSessionTest, DISABLED_Renegot
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(0U, removed.size());
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kPassive);
 
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(offererPairs.size(), newAnswererPairs.size());
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(offererPairs[i], newAnswererPairs[i]));
-  }
-
-  ASSERT_EQ(answererPairs.size(), newOffererPairs.size());
-  for (size_t i = 0; i < answererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(answererPairs[i], newOffererPairs[i]));
-  }
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_TRUE(Equals(offererTransceivers, newAnswererTransceivers));
+  ASSERT_TRUE(Equals(answererTransceivers, newOffererTransceivers));
 }
 
 
 TEST_P(JsepSessionTest, RenegotiationOffererAddsTrack)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
 
   OfferAnswer();
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
 
   std::vector<SdpMediaSection::MediaType> extraTypes;
   extraTypes.push_back(SdpMediaSection::kAudio);
   extraTypes.push_back(SdpMediaSection::kVideo);
   AddTracks(*mSessionOff, extraTypes);
   types.insert(types.end(), extraTypes.begin(), extraTypes.end());
 
   OfferAnswer(CHECK_SUCCESS);
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
   auto added = mSessionAns->GetRemoteTracksAdded();
   auto removed = mSessionAns->GetRemoteTracksRemoved();
   ASSERT_EQ(2U, added.size());
   ASSERT_EQ(0U, removed.size());
-  ASSERT_EQ(SdpMediaSection::kAudio, added[0]->GetMediaType());
-  ASSERT_EQ(SdpMediaSection::kVideo, added[1]->GetMediaType());
+  ASSERT_EQ(SdpMediaSection::kAudio, added[0].GetMediaType());
+  ASSERT_EQ(SdpMediaSection::kVideo, added[1].GetMediaType());
 
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(0U, removed.size());
 
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(offererPairs.size() + 2, newOffererPairs.size());
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
-  }
-
-  ASSERT_EQ(answererPairs.size() + 2, newAnswererPairs.size());
-  for (size_t i = 0; i < answererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
-  }
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_LE(2U, newOffererTransceivers.size());
+  newOffererTransceivers.resize(newOffererTransceivers.size() - 2);
+  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
+
+  ASSERT_LE(2U, newAnswererTransceivers.size());
+  newAnswererTransceivers.resize(newAnswererTransceivers.size() - 2);
+  ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers));
 }
 
 TEST_P(JsepSessionTest, RenegotiationAnswererAddsTrack)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
 
   OfferAnswer();
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
 
   std::vector<SdpMediaSection::MediaType> extraTypes;
   extraTypes.push_back(SdpMediaSection::kAudio);
   extraTypes.push_back(SdpMediaSection::kVideo);
   AddTracks(*mSessionAns, extraTypes);
   types.insert(types.end(), extraTypes.begin(), extraTypes.end());
 
   // We need to add a recvonly m-section to the offer for this to work
-  JsepOfferOptions options;
-  options.mOfferToReceiveAudio =
-    Some(GetTrackCount(*mSessionOff, SdpMediaSection::kAudio) + 1);
-  options.mOfferToReceiveVideo =
-    Some(GetTrackCount(*mSessionOff, SdpMediaSection::kVideo) + 1);
-
-  std::string offer = CreateOffer(Some(options));
+  mSessionOff->AddTransceiver(new JsepTransceiver(
+        SdpMediaSection::kAudio, SdpDirectionAttribute::Direction::kRecvonly));
+  mSessionOff->AddTransceiver(new JsepTransceiver(
+        SdpMediaSection::kVideo, SdpDirectionAttribute::Direction::kRecvonly));
+
+  std::string offer = CreateOffer();
   SetLocalOffer(offer, CHECK_SUCCESS);
   SetRemoteOffer(offer, CHECK_SUCCESS);
 
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer, CHECK_SUCCESS);
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
@@ -1550,45 +1842,45 @@ TEST_P(JsepSessionTest, RenegotiationAns
   auto removed = mSessionAns->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(0U, removed.size());
 
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(2U, added.size());
   ASSERT_EQ(0U, removed.size());
-  ASSERT_EQ(SdpMediaSection::kAudio, added[0]->GetMediaType());
-  ASSERT_EQ(SdpMediaSection::kVideo, added[1]->GetMediaType());
-
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(offererPairs.size() + 2, newOffererPairs.size());
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
-  }
-
-  ASSERT_EQ(answererPairs.size() + 2, newAnswererPairs.size());
-  for (size_t i = 0; i < answererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
-  }
+  ASSERT_EQ(SdpMediaSection::kAudio, added[0].GetMediaType());
+  ASSERT_EQ(SdpMediaSection::kVideo, added[1].GetMediaType());
+
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_LE(2U, newOffererTransceivers.size());
+  newOffererTransceivers.resize(newOffererTransceivers.size() - 2);
+  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
+
+  ASSERT_LE(2U, newAnswererTransceivers.size());
+  newAnswererTransceivers.resize(newAnswererTransceivers.size() - 2);
+  ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers));
 }
 
 TEST_P(JsepSessionTest, RenegotiationBothAddTrack)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
 
   OfferAnswer();
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
 
   std::vector<SdpMediaSection::MediaType> extraTypes;
   extraTypes.push_back(SdpMediaSection::kAudio);
   extraTypes.push_back(SdpMediaSection::kVideo);
   AddTracks(*mSessionAns, extraTypes);
   AddTracks(*mSessionOff, extraTypes);
   types.insert(types.end(), extraTypes.begin(), extraTypes.end());
 
@@ -1596,591 +1888,533 @@ TEST_P(JsepSessionTest, RenegotiationBot
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
   auto added = mSessionAns->GetRemoteTracksAdded();
   auto removed = mSessionAns->GetRemoteTracksRemoved();
   ASSERT_EQ(2U, added.size());
   ASSERT_EQ(0U, removed.size());
-  ASSERT_EQ(SdpMediaSection::kAudio, added[0]->GetMediaType());
-  ASSERT_EQ(SdpMediaSection::kVideo, added[1]->GetMediaType());
+  ASSERT_EQ(SdpMediaSection::kAudio, added[0].GetMediaType());
+  ASSERT_EQ(SdpMediaSection::kVideo, added[1].GetMediaType());
 
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(2U, added.size());
   ASSERT_EQ(0U, removed.size());
-  ASSERT_EQ(SdpMediaSection::kAudio, added[0]->GetMediaType());
-  ASSERT_EQ(SdpMediaSection::kVideo, added[1]->GetMediaType());
-
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(offererPairs.size() + 2, newOffererPairs.size());
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
-  }
-
-  ASSERT_EQ(answererPairs.size() + 2, newAnswererPairs.size());
-  for (size_t i = 0; i < answererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
-  }
+  ASSERT_EQ(SdpMediaSection::kAudio, added[0].GetMediaType());
+  ASSERT_EQ(SdpMediaSection::kVideo, added[1].GetMediaType());
+
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_LE(2U, newOffererTransceivers.size());
+  newOffererTransceivers.resize(newOffererTransceivers.size() - 2);
+  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
+
+  ASSERT_LE(2U, newAnswererTransceivers.size());
+  newAnswererTransceivers.resize(newAnswererTransceivers.size() - 2);
+  ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers));
 }
 
 TEST_P(JsepSessionTest, RenegotiationBothAddTracksToExistingStream)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
   if (GetParam() == "datachannel") {
     return;
   }
 
   OfferAnswer();
 
-  auto oHasStream = HasMediaStream(mSessionOff->GetLocalTracks());
-  auto aHasStream = HasMediaStream(mSessionAns->GetLocalTracks());
+  auto oHasStream = HasMediaStream(GetLocalTracks(*mSessionOff));
+  auto aHasStream = HasMediaStream(GetLocalTracks(*mSessionAns));
   ASSERT_EQ(oHasStream, !GetLocalUniqueStreamIds(*mSessionOff).empty());
   ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty());
   ASSERT_EQ(aHasStream, !GetRemoteUniqueStreamIds(*mSessionOff).empty());
   ASSERT_EQ(oHasStream, !GetRemoteUniqueStreamIds(*mSessionAns).empty());
 
   auto firstOffId = GetFirstLocalStreamId(*mSessionOff);
   auto firstAnsId = GetFirstLocalStreamId(*mSessionAns);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  auto offererTransceivers = DeepCopy(mSessionOff->GetTransceivers());
+  auto answererTransceivers = DeepCopy(mSessionAns->GetTransceivers());
 
   std::vector<SdpMediaSection::MediaType> extraTypes;
   extraTypes.push_back(SdpMediaSection::kAudio);
   extraTypes.push_back(SdpMediaSection::kVideo);
   AddTracksToStream(*mSessionOff, firstOffId, extraTypes);
   AddTracksToStream(*mSessionAns, firstAnsId, extraTypes);
   types.insert(types.end(), extraTypes.begin(), extraTypes.end());
 
   OfferAnswer(CHECK_SUCCESS);
 
-  oHasStream = HasMediaStream(mSessionOff->GetLocalTracks());
-  aHasStream = HasMediaStream(mSessionAns->GetLocalTracks());
+  oHasStream = HasMediaStream(GetLocalTracks(*mSessionOff));
+  aHasStream = HasMediaStream(GetLocalTracks(*mSessionAns));
 
   ASSERT_EQ(oHasStream, !GetLocalUniqueStreamIds(*mSessionOff).empty());
   ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty());
   ASSERT_EQ(aHasStream, !GetRemoteUniqueStreamIds(*mSessionOff).empty());
   ASSERT_EQ(oHasStream, !GetRemoteUniqueStreamIds(*mSessionAns).empty());
   if (oHasStream) {
     ASSERT_STREQ(firstOffId.c_str(),
                  GetFirstLocalStreamId(*mSessionOff).c_str());
   }
   if (aHasStream) {
     ASSERT_STREQ(firstAnsId.c_str(),
                  GetFirstLocalStreamId(*mSessionAns).c_str());
 
-  auto oHasStream = HasMediaStream(mSessionOff->GetLocalTracks());
-  auto aHasStream = HasMediaStream(mSessionAns->GetLocalTracks());
-  ASSERT_EQ(oHasStream, !GetLocalUniqueStreamIds(*mSessionOff).empty());
-  ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty());
+    auto oHasStream = HasMediaStream(GetLocalTracks(*mSessionOff));
+    auto aHasStream = HasMediaStream(GetLocalTracks(*mSessionAns));
+    ASSERT_EQ(oHasStream, !GetLocalUniqueStreamIds(*mSessionOff).empty());
+    ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty());
   }
 }
 
-TEST_P(JsepSessionTest, RenegotiationOffererRemovesTrack)
+// The JSEP draft explicitly forbids changing the msid on an m-section, but
+// that is a new restriction that older versions of Firefox do not follow.
+// JS will not see the msid change, since that is filtered out (except for
+// RTCRtpTransceiver.remoteTrackId)
+TEST_P(JsepSessionTest, RenegotiationOffererChangesMsid)
+{
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns);
+  if (types.front() == SdpMediaSection::kApplication) {
+    return;
+  }
+
+  OfferAnswer();
+
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer);
+
+  JsepTransceiver* transceiver = GetNegotiatedTransceiver(*mSessionOff, 0);
+  ASSERT_TRUE(transceiver);
+  std::string streamId = transceiver->mSendTrack.GetStreamIds()[0];
+  std::string trackId = transceiver->mSendTrack.GetTrackId();
+  std::string msidToReplace("a=msid:");
+  msidToReplace += streamId;
+  msidToReplace += " ";
+  msidToReplace += trackId;
+  size_t msidOffset = offer.find(msidToReplace);
+  ASSERT_NE(std::string::npos, msidOffset);
+  offer.replace(msidOffset, msidToReplace.size(), "a=msid:foo bar");
+
+  SetRemoteOffer(offer);
+
+  std::vector<JsepTrack> removedTracks = mSessionAns->GetRemoteTracksRemoved();
+  std::vector<JsepTrack> addedTracks = mSessionAns->GetRemoteTracksAdded();
+
+  ASSERT_EQ(1U, removedTracks.size());
+  ASSERT_FALSE(IsNull(removedTracks[0]));
+  ASSERT_EQ(streamId, removedTracks[0].GetStreamIds()[0]);
+  ASSERT_EQ(trackId, removedTracks[0].GetTrackId());
+
+  ASSERT_EQ(1U, addedTracks.size());
+  ASSERT_FALSE(IsNull(addedTracks[0]));
+  ASSERT_EQ("foo", addedTracks[0].GetStreamIds()[0]);
+  ASSERT_EQ("bar", addedTracks[0].GetTrackId());
+
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer);
+  SetRemoteAnswer(answer);
+}
+
+// The JSEP draft explicitly forbids changing the msid on an m-section, but
+// that is a new restriction that older versions of Firefox do not follow.
+TEST_P(JsepSessionTest, RenegotiationAnswererChangesMsid)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
   if (types.front() == SdpMediaSection::kApplication) {
     return;
   }
 
   OfferAnswer();
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  RefPtr<JsepTrack> removedTrack = GetTrackOff(0, types.front());
-  ASSERT_TRUE(removedTrack);
-  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrack->GetStreamId(),
-                                           removedTrack->GetTrackId()));
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer);
+  SetRemoteOffer(offer);
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer);
+
+  JsepTransceiver* transceiver = GetNegotiatedTransceiver(*mSessionAns, 0);
+  ASSERT_TRUE(transceiver);
+  std::string streamId = transceiver->mSendTrack.GetStreamIds()[0];
+  std::string trackId = transceiver->mSendTrack.GetTrackId();
+  std::string msidToReplace("a=msid:");
+  msidToReplace += streamId;
+  msidToReplace += " ";
+  msidToReplace += trackId;
+  size_t msidOffset = answer.find(msidToReplace);
+  ASSERT_NE(std::string::npos, msidOffset);
+  answer.replace(msidOffset, msidToReplace.size(), "a=msid:foo bar");
+
+  SetRemoteAnswer(answer);
+
+  std::vector<JsepTrack> removedTracks = mSessionOff->GetRemoteTracksRemoved();
+  std::vector<JsepTrack> addedTracks = mSessionOff->GetRemoteTracksAdded();
+
+  ASSERT_EQ(1U, removedTracks.size());
+  ASSERT_FALSE(IsNull(removedTracks[0]));
+  ASSERT_EQ(streamId, removedTracks[0].GetStreamIds()[0]);
+  ASSERT_EQ(trackId, removedTracks[0].GetTrackId());
+
+  ASSERT_EQ(1U, addedTracks.size());
+  ASSERT_FALSE(IsNull(addedTracks[0]));
+  ASSERT_EQ("foo", addedTracks[0].GetStreamIds()[0]);
+  ASSERT_EQ("bar", addedTracks[0].GetTrackId());
+}
+
+TEST_P(JsepSessionTest, RenegotiationOffererStopsTransceiver)
+{
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns);
+  if (types.back() == SdpMediaSection::kApplication) {
+    return;
+  }
+
+  OfferAnswer();
+
+  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers =
+    DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers =
+    DeepCopy(mSessionAns->GetTransceivers());
+
+  // Avoid bundle transport side effects; don't stop the BUNDLE-tag!
+  mSessionOff->GetTransceivers().back()->Stop();
+  JsepTrack removedTrack(mSessionOff->GetTransceivers().back()->mSendTrack);
 
   OfferAnswer(CHECK_SUCCESS);
 
   auto added = mSessionAns->GetRemoteTracksAdded();
   auto removed = mSessionAns->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(1U, removed.size());
 
-  ASSERT_EQ(removedTrack->GetMediaType(), removed[0]->GetMediaType());
-  ASSERT_EQ(removedTrack->GetStreamId(), removed[0]->GetStreamId());
-  ASSERT_EQ(removedTrack->GetTrackId(), removed[0]->GetTrackId());
+  ASSERT_EQ(removedTrack.GetMediaType(), removed[0].GetMediaType());
+  ASSERT_EQ(removedTrack.GetStreamIds(), removed[0].GetStreamIds());
+  ASSERT_EQ(removedTrack.GetTrackId(), removed[0].GetTrackId());
 
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
-  ASSERT_EQ(0U, removed.size());
-
-  // First m-section should be recvonly
+  ASSERT_EQ(1U, removed.size());
+
+  // Last m-section should be disabled
   auto offer = GetParsedLocalDescription(*mSessionOff);
-  auto* msection = GetMsection(*offer, types.front(), 0);
+  const SdpMediaSection* msection =
+    &offer->GetMediaSection(offer->GetMediaSectionCount() - 1);
   ASSERT_TRUE(msection);
-  ASSERT_TRUE(msection->IsReceiving());
-  ASSERT_FALSE(msection->IsSending());
-
-  // First audio m-section should be sendonly
+  ValidateDisabledMSection(msection);
+
+  // Last m-section should be disabled
   auto answer = GetParsedLocalDescription(*mSessionAns);
-  msection = GetMsection(*answer, types.front(), 0);
+  msection = &answer->GetMediaSection(answer->GetMediaSectionCount() - 1);
   ASSERT_TRUE(msection);
-  ASSERT_FALSE(msection->IsReceiving());
-  ASSERT_TRUE(msection->IsSending());
-
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  // Will be the same size since we still have a track on one side.
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-
-  // This should be the only difference.
-  ASSERT_TRUE(offererPairs[0].mSending);
-  ASSERT_FALSE(newOffererPairs[0].mSending);
-
-  // Remove this difference, let loop below take care of the rest
-  offererPairs[0].mSending = nullptr;
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
-  }
-
-  // Will be the same size since we still have a track on one side.
-  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());
-
-  // This should be the only difference.
-  ASSERT_TRUE(answererPairs[0].mReceiving);
-  ASSERT_FALSE(newAnswererPairs[0].mReceiving);
-
-  // Remove this difference, let loop below take care of the rest
-  answererPairs[0].mReceiving = nullptr;
-  for (size_t i = 0; i < answererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
-  }
+  ValidateDisabledMSection(msection);
+
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
+
+  ASSERT_FALSE(origOffererTransceivers.back()->IsStopped());
+  ASSERT_TRUE(newOffererTransceivers.back()->IsStopped());
+
+  ASSERT_FALSE(origOffererTransceivers.back()->IsStopped());
+  ASSERT_TRUE(newOffererTransceivers.back()->IsStopped());
+  ASSERT_FALSE(origAnswererTransceivers.back()->IsStopped());
+  ASSERT_TRUE(newAnswererTransceivers.back()->IsStopped());
+  origOffererTransceivers.pop_back(); // Ignore this one
+  newOffererTransceivers.pop_back(); // Ignore this one
+  origAnswererTransceivers.pop_back(); // Ignore this one
+  newAnswererTransceivers.pop_back(); // Ignore this one
+
+  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
+  ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers));
 }
 
-TEST_P(JsepSessionTest, RenegotiationAnswererRemovesTrack)
+TEST_P(JsepSessionTest, RenegotiationAnswererStopsTransceiver)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
-  if (types.front() == SdpMediaSection::kApplication) {
+  if (types.back() == SdpMediaSection::kApplication) {
     return;
   }
 
   OfferAnswer();
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  RefPtr<JsepTrack> removedTrack = GetTrackAns(0, types.front());
-  ASSERT_TRUE(removedTrack);
-  ASSERT_EQ(NS_OK, mSessionAns->RemoveTrack(removedTrack->GetStreamId(),
-                                           removedTrack->GetTrackId()));
+  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
+
+  // Avoid bundle transport side effects; don't stop the BUNDLE-tag!
+  mSessionAns->GetTransceivers().back()->Stop();
+  JsepTrack removedTrack(mSessionAns->GetTransceivers().back()->mSendTrack);
 
   OfferAnswer(CHECK_SUCCESS);
 
   auto added = mSessionAns->GetRemoteTracksAdded();
   auto removed = mSessionAns->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(0U, removed.size());
 
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(1U, removed.size());
 
-  ASSERT_EQ(removedTrack->GetMediaType(), removed[0]->GetMediaType());
-  ASSERT_EQ(removedTrack->GetStreamId(), removed[0]->GetStreamId());
-  ASSERT_EQ(removedTrack->GetTrackId(), removed[0]->GetTrackId());
-
-  // First m-section should be sendrecv
+  ASSERT_EQ(removedTrack.GetMediaType(), removed[0].GetMediaType());
+  ASSERT_EQ(removedTrack.GetStreamIds(), removed[0].GetStreamIds());
+  ASSERT_EQ(removedTrack.GetTrackId(), removed[0].GetTrackId());
+
+  // Last m-section should be sendrecv
   auto offer = GetParsedLocalDescription(*mSessionOff);
-  auto* msection = GetMsection(*offer, types.front(), 0);
+  const SdpMediaSection* msection =
+    &offer->GetMediaSection(offer->GetMediaSectionCount() - 1);
   ASSERT_TRUE(msection);
   ASSERT_TRUE(msection->IsReceiving());
   ASSERT_TRUE(msection->IsSending());
 
-  // First audio m-section should be recvonly
+  // Last m-section should be disabled
   auto answer = GetParsedLocalDescription(*mSessionAns);
-  msection = GetMsection(*answer, types.front(), 0);
+  msection = &answer->GetMediaSection(answer->GetMediaSectionCount() - 1);
   ASSERT_TRUE(msection);
-  ASSERT_TRUE(msection->IsReceiving());
-  ASSERT_FALSE(msection->IsSending());
-
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  // Will be the same size since we still have a track on one side.
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-
-  // This should be the only difference.
-  ASSERT_TRUE(offererPairs[0].mReceiving);
-  ASSERT_FALSE(newOffererPairs[0].mReceiving);
-
-  // Remove this difference, let loop below take care of the rest
-  offererPairs[0].mReceiving = nullptr;
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
-  }
-
-  // Will be the same size since we still have a track on one side.
-  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());
-
-  // This should be the only difference.
-  ASSERT_TRUE(answererPairs[0].mSending);
-  ASSERT_FALSE(newAnswererPairs[0].mSending);
-
-  // Remove this difference, let loop below take care of the rest
-  answererPairs[0].mSending = nullptr;
-  for (size_t i = 0; i < answererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
-  }
+  ValidateDisabledMSection(msection);
+
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
+
+  ASSERT_FALSE(origOffererTransceivers.back()->IsStopped());
+  ASSERT_TRUE(newOffererTransceivers.back()->IsStopped());
+  ASSERT_FALSE(origAnswererTransceivers.back()->IsStopped());
+  ASSERT_TRUE(newAnswererTransceivers.back()->IsStopped());
+  origOffererTransceivers.pop_back(); // Ignore this one
+  newOffererTransceivers.pop_back(); // Ignore this one
+  origAnswererTransceivers.pop_back(); // Ignore this one
+  newAnswererTransceivers.pop_back(); // Ignore this one
+
+  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
+  ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers));
 }
 
-TEST_P(JsepSessionTest, RenegotiationBothRemoveTrack)
+TEST_P(JsepSessionTest, RenegotiationBothStopSameTransceiver)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
-  if (types.front() == SdpMediaSection::kApplication) {
+  if (types.back() == SdpMediaSection::kApplication) {
     return;
   }
 
   OfferAnswer();
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  RefPtr<JsepTrack> removedTrackAnswer = GetTrackAns(0, types.front());
-  ASSERT_TRUE(removedTrackAnswer);
-  ASSERT_EQ(NS_OK, mSessionAns->RemoveTrack(removedTrackAnswer->GetStreamId(),
-                                           removedTrackAnswer->GetTrackId()));
-
-  RefPtr<JsepTrack> removedTrackOffer = GetTrackOff(0, types.front());
-  ASSERT_TRUE(removedTrackOffer);
-  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrackOffer->GetStreamId(),
-                                           removedTrackOffer->GetTrackId()));
+  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
+
+  // Avoid bundle transport side effects; don't stop the BUNDLE-tag!
+  mSessionOff->GetTransceivers().back()->Stop();
+  JsepTrack removedTrackOffer(mSessionOff->GetTransceivers().back()->mSendTrack);
+  mSessionAns->GetTransceivers().back()->Stop();
+  JsepTrack removedTrackAnswer(mSessionAns->GetTransceivers().back()->mSendTrack);
 
   OfferAnswer(CHECK_SUCCESS);
 
   auto added = mSessionAns->GetRemoteTracksAdded();
   auto removed = mSessionAns->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(1U, removed.size());
 
-  ASSERT_EQ(removedTrackOffer->GetMediaType(), removed[0]->GetMediaType());
-  ASSERT_EQ(removedTrackOffer->GetStreamId(), removed[0]->GetStreamId());
-  ASSERT_EQ(removedTrackOffer->GetTrackId(), removed[0]->GetTrackId());
+  ASSERT_EQ(removedTrackOffer.GetMediaType(), removed[0].GetMediaType());
+  ASSERT_EQ(removedTrackOffer.GetStreamIds(), removed[0].GetStreamIds());
+  ASSERT_EQ(removedTrackOffer.GetTrackId(), removed[0].GetTrackId());
 
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(1U, removed.size());
 
-  ASSERT_EQ(removedTrackAnswer->GetMediaType(), removed[0]->GetMediaType());
-  ASSERT_EQ(removedTrackAnswer->GetStreamId(), removed[0]->GetStreamId());
-  ASSERT_EQ(removedTrackAnswer->GetTrackId(), removed[0]->GetTrackId());
-
-  // First m-section should be recvonly
+  ASSERT_EQ(removedTrackAnswer.GetMediaType(), removed[0].GetMediaType());
+  ASSERT_EQ(removedTrackAnswer.GetStreamIds(), removed[0].GetStreamIds());
+  ASSERT_EQ(removedTrackAnswer.GetTrackId(), removed[0].GetTrackId());
+
+  // Last m-section should be disabled
   auto offer = GetParsedLocalDescription(*mSessionOff);
-  auto* msection = GetMsection(*offer, types.front(), 0);
+  const SdpMediaSection* msection =
+    &offer->GetMediaSection(offer->GetMediaSectionCount() - 1);
   ASSERT_TRUE(msection);
-  ASSERT_TRUE(msection->IsReceiving());
-  ASSERT_FALSE(msection->IsSending());
-
-  // First m-section should be inactive, and rejected
+  ValidateDisabledMSection(msection);
+
+  // Last m-section should be disabled
   auto answer = GetParsedLocalDescription(*mSessionAns);
-  msection = GetMsection(*answer, types.front(), 0);
+  msection = &answer->GetMediaSection(answer->GetMediaSectionCount() - 1);
   ASSERT_TRUE(msection);
-  ASSERT_FALSE(msection->IsReceiving());
-  ASSERT_FALSE(msection->IsSending());
-  ASSERT_FALSE(msection->GetPort());
-
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size() + 1);
-
-  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
-    JsepTrackPair oldPair(offererPairs[i + 1]);
-    JsepTrackPair newPair(newOffererPairs[i]);
-    ASSERT_EQ(oldPair.mLevel, newPair.mLevel);
-    ASSERT_EQ(oldPair.mSending.get(), newPair.mSending.get());
-    ASSERT_EQ(oldPair.mReceiving.get(), newPair.mReceiving.get());
-    ASSERT_TRUE(oldPair.HasBundleLevel());
-    ASSERT_TRUE(newPair.HasBundleLevel());
-    ASSERT_EQ(0U, oldPair.BundleLevel());
-    ASSERT_EQ(1U, newPair.BundleLevel());
-  }
-
-  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size() + 1);
-
-  for (size_t i = 0; i < newAnswererPairs.size(); ++i) {
-    JsepTrackPair oldPair(answererPairs[i + 1]);
-    JsepTrackPair newPair(newAnswererPairs[i]);
-    ASSERT_EQ(oldPair.mLevel, newPair.mLevel);
-    ASSERT_EQ(oldPair.mSending.get(), newPair.mSending.get());
-    ASSERT_EQ(oldPair.mReceiving.get(), newPair.mReceiving.get());
-    ASSERT_TRUE(oldPair.HasBundleLevel());
-    ASSERT_TRUE(newPair.BundleLevel());
-    ASSERT_EQ(0U, oldPair.BundleLevel());
-    ASSERT_EQ(1U, newPair.BundleLevel());
-  }
+  ValidateDisabledMSection(msection);
+
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
+
+  ASSERT_FALSE(origOffererTransceivers.back()->IsStopped());
+  ASSERT_TRUE(newOffererTransceivers.back()->IsStopped());
+  ASSERT_FALSE(origAnswererTransceivers.back()->IsStopped());
+  ASSERT_TRUE(newAnswererTransceivers.back()->IsStopped());
+  origOffererTransceivers.pop_back(); // Ignore this one
+  newOffererTransceivers.pop_back(); // Ignore this one
+  origAnswererTransceivers.pop_back(); // Ignore this one
+  newAnswererTransceivers.pop_back(); // Ignore this one
+
+  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
+  ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers));
 }
 
-TEST_P(JsepSessionTest, RenegotiationBothRemoveThenAddTrack)
+TEST_P(JsepSessionTest, RenegotiationBothStopTransceiverThenAddTrack)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
-  if (types.front() == SdpMediaSection::kApplication) {
+  if (types.back() == SdpMediaSection::kApplication) {
     return;
   }
 
-  SdpMediaSection::MediaType removedType = types.front();
+  SdpMediaSection::MediaType removedType = types.back();
 
   OfferAnswer();
 
-  RefPtr<JsepTrack> removedTrackAnswer = GetTrackAns(0, removedType);
-  ASSERT_TRUE(removedTrackAnswer);
-  ASSERT_EQ(NS_OK, mSessionAns->RemoveTrack(removedTrackAnswer->GetStreamId(),
-                                           removedTrackAnswer->GetTrackId()));
-
-  RefPtr<JsepTrack> removedTrackOffer = GetTrackOff(0, removedType);
-  ASSERT_TRUE(removedTrackOffer);
-  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrackOffer->GetStreamId(),
-                                           removedTrackOffer->GetTrackId()));
+  // Avoid bundle transport side effects; don't stop the BUNDLE-tag!
+  mSessionOff->GetTransceivers().back()->Stop();
+  JsepTrack removedTrackOffer(mSessionOff->GetTransceivers().back()->mSendTrack);
+  mSessionOff->GetTransceivers().back()->Stop();
+  JsepTrack removedTrackAnswer(mSessionOff->GetTransceivers().back()->mSendTrack);
 
   OfferAnswer(CHECK_SUCCESS);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
 
   std::vector<SdpMediaSection::MediaType> extraTypes;
   extraTypes.push_back(removedType);
   AddTracks(*mSessionAns, extraTypes);
   AddTracks(*mSessionOff, extraTypes);
   types.insert(types.end(), extraTypes.begin(), extraTypes.end());
 
   OfferAnswer(CHECK_SUCCESS);
 
   auto added = mSessionAns->GetRemoteTracksAdded();
   auto removed = mSessionAns->GetRemoteTracksRemoved();
   ASSERT_EQ(1U, added.size());
   ASSERT_EQ(0U, removed.size());
-  ASSERT_EQ(removedType, added[0]->GetMediaType());
+  ASSERT_EQ(removedType, added[0].GetMediaType());
 
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(1U, added.size());
   ASSERT_EQ(0U, removed.size());
-  ASSERT_EQ(removedType, added[0]->GetMediaType());
-
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(offererPairs.size() + 1, newOffererPairs.size());
-  ASSERT_EQ(answererPairs.size() + 1, newAnswererPairs.size());
+  ASSERT_EQ(removedType, added[0].GetMediaType());
+
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_EQ(origOffererTransceivers.size() + 1, newOffererTransceivers.size());
+  ASSERT_EQ(origAnswererTransceivers.size() + 1,
+            newAnswererTransceivers.size());
 
   // Ensure that the m-section was re-used; no gaps
-  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
-    ASSERT_EQ(i, newOffererPairs[i].mLevel);
-  }
-  for (size_t i = 0; i < newAnswererPairs.size(); ++i) {
-    ASSERT_EQ(i, newAnswererPairs[i].mLevel);
-  }
+  ASSERT_EQ(origOffererTransceivers.back()->GetLevel(),
+            newOffererTransceivers.back()->GetLevel());
+
+  ASSERT_EQ(origAnswererTransceivers.back()->GetLevel(),
+            newAnswererTransceivers.back()->GetLevel());
 }
 
-TEST_P(JsepSessionTest, RenegotiationBothRemoveTrackDifferentMsection)
+TEST_P(JsepSessionTest, RenegotiationBothStopTransceiverDifferentMsection)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
-  if (types.front() == SdpMediaSection::kApplication) {
+
+  if (types.size() < 2) {
     return;
   }
 
-  if (types.size() < 2 || types[0] != types[1]) {
-    // For simplicity, just run in cases where we have two of the same type
+  if (types[0] == SdpMediaSection::kApplication ||
+      types[1] == SdpMediaSection::kApplication) {
     return;
   }
 
   OfferAnswer();
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  RefPtr<JsepTrack> removedTrackAnswer = GetTrackAns(0, types.front());
-  ASSERT_TRUE(removedTrackAnswer);
-  ASSERT_EQ(NS_OK, mSessionAns->RemoveTrack(removedTrackAnswer->GetStreamId(),
-                                           removedTrackAnswer->GetTrackId()));
-
-  // Second instance of the same type
-  RefPtr<JsepTrack> removedTrackOffer = GetTrackOff(1, types.front());
-  ASSERT_TRUE(removedTrackOffer);
-  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrackOffer->GetStreamId(),
-                                           removedTrackOffer->GetTrackId()));
+  mSessionOff->GetTransceivers()[0]->Stop();
+  mSessionOff->GetTransceivers()[1]->Stop();
 
   OfferAnswer(CHECK_SUCCESS);
 
   auto added = mSessionAns->GetRemoteTracksAdded();
   auto removed = mSessionAns->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
-  ASSERT_EQ(1U, removed.size());
-
-  ASSERT_EQ(removedTrackOffer->GetMediaType(), removed[0]->GetMediaType());
-  ASSERT_EQ(removedTrackOffer->GetStreamId(), removed[0]->GetStreamId());
-  ASSERT_EQ(removedTrackOffer->GetTrackId(), removed[0]->GetTrackId());
+  ASSERT_EQ(2U, removed.size());
 
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
-  ASSERT_EQ(1U, removed.size());
-
-  ASSERT_EQ(removedTrackAnswer->GetMediaType(), removed[0]->GetMediaType());
-  ASSERT_EQ(removedTrackAnswer->GetStreamId(), removed[0]->GetStreamId());
-  ASSERT_EQ(removedTrackAnswer->GetTrackId(), removed[0]->GetTrackId());
-
-  // Second m-section should be recvonly
-  auto offer = GetParsedLocalDescription(*mSessionOff);
-  auto* msection = GetMsection(*offer, types.front(), 1);
-  ASSERT_TRUE(msection);
-  ASSERT_TRUE(msection->IsReceiving());
-  ASSERT_FALSE(msection->IsSending());
-
-  // First m-section should be recvonly
-  auto answer = GetParsedLocalDescription(*mSessionAns);
-  msection = GetMsection(*answer, types.front(), 0);
-  ASSERT_TRUE(msection);
-  ASSERT_TRUE(msection->IsReceiving());
-  ASSERT_FALSE(msection->IsSending());
-
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-
-  // This should be the only difference.
-  ASSERT_TRUE(offererPairs[0].mReceiving);
-  ASSERT_FALSE(newOffererPairs[0].mReceiving);
-
-  // Remove this difference, let loop below take care of the rest
-  offererPairs[0].mReceiving = nullptr;
-
-  // This should be the only difference.
-  ASSERT_TRUE(offererPairs[1].mSending);
-  ASSERT_FALSE(newOffererPairs[1].mSending);
-
-  // Remove this difference, let loop below take care of the rest
-  offererPairs[1].mSending = nullptr;
-
-  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
-  }
-
-  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());
-
-  // This should be the only difference.
-  ASSERT_TRUE(answererPairs[0].mSending);
-  ASSERT_FALSE(newAnswererPairs[0].mSending);
-
-  // Remove this difference, let loop below take care of the rest
-  answererPairs[0].mSending = nullptr;
-
-  // This should be the only difference.
-  ASSERT_TRUE(answererPairs[1].mReceiving);
-  ASSERT_FALSE(newAnswererPairs[1].mReceiving);
-
-  // Remove this difference, let loop below take care of the rest
-  answererPairs[1].mReceiving = nullptr;
-
-  for (size_t i = 0; i < newAnswererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
-  }
+  ASSERT_EQ(2U, removed.size());
 }
 
 TEST_P(JsepSessionTest, RenegotiationOffererReplacesTrack)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
 
   if (types.front() == SdpMediaSection::kApplication) {
     return;
   }
 
   OfferAnswer();
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  RefPtr<JsepTrack> removedTrack = GetTrackOff(0, types.front());
-  ASSERT_TRUE(removedTrack);
-  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrack->GetStreamId(),
-                                           removedTrack->GetTrackId()));
-  RefPtr<JsepTrack> addedTrack(
-      new JsepTrack(types.front(), "newstream", "newtrack"));
-  ASSERT_EQ(NS_OK, mSessionOff->AddTrack(addedTrack));
+  mSessionOff->GetTransceivers()[0]->mSendTrack.UpdateTrackIds(
+      std::vector<std::string>(1, "newstream"), "newtrack");
 
   OfferAnswer(CHECK_SUCCESS);
 
+  // Latest JSEP spec says the msid never changes, so the other side will not
+  // notice track replacement.
   auto added = mSessionAns->GetRemoteTracksAdded();
   auto removed = mSessionAns->GetRemoteTracksRemoved();
-  ASSERT_EQ(1U, added.size());
-  ASSERT_EQ(1U, removed.size());
-
-  ASSERT_EQ(removedTrack->GetMediaType(), removed[0]->GetMediaType());
-  ASSERT_EQ(removedTrack->GetStreamId(), removed[0]->GetStreamId());
-  ASSERT_EQ(removedTrack->GetTrackId(), removed[0]->GetTrackId());
-
-  ASSERT_EQ(addedTrack->GetMediaType(), added[0]->GetMediaType());
-  ASSERT_EQ(addedTrack->GetStreamId(), added[0]->GetStreamId());
-  ASSERT_EQ(addedTrack->GetTrackId(), added[0]->GetTrackId());
-
-  added = mSessionOff->GetRemoteTracksAdded();
-  removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(0U, removed.size());
-
-  // First audio m-section should be sendrecv
-  auto offer = GetParsedLocalDescription(*mSessionOff);
-  auto* msection = GetMsection(*offer, types.front(), 0);
-  ASSERT_TRUE(msection);
-  ASSERT_TRUE(msection->IsReceiving());
-  ASSERT_TRUE(msection->IsSending());
-
-  // First audio m-section should be sendrecv
-  auto answer = GetParsedLocalDescription(*mSessionAns);
-  msection = GetMsection(*answer, types.front(), 0);
-  ASSERT_TRUE(msection);
-  ASSERT_TRUE(msection->IsReceiving());
-  ASSERT_TRUE(msection->IsSending());
-
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-
-  ASSERT_NE(offererPairs[0].mSending->GetStreamId(),
-            newOffererPairs[0].mSending->GetStreamId());
-  ASSERT_NE(offererPairs[0].mSending->GetTrackId(),
-            newOffererPairs[0].mSending->GetTrackId());
-
-  // Skip first pair
-  for (size_t i = 1; i < offererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
+}
+
+TEST_P(JsepSessionTest, RenegotiationAnswererReplacesTrack)
+{
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns);
+
+  if (types.front() == SdpMediaSection::kApplication) {
+    return;
   }
 
-  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());
-
-  ASSERT_NE(answererPairs[0].mReceiving->GetStreamId(),
-            newAnswererPairs[0].mReceiving->GetStreamId());
-  ASSERT_NE(answererPairs[0].mReceiving->GetTrackId(),
-            newAnswererPairs[0].mReceiving->GetTrackId());
-
-  // Skip first pair
-  for (size_t i = 1; i < newAnswererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
-  }
+  OfferAnswer();
+
+  mSessionAns->GetTransceivers()[0]->mSendTrack.UpdateTrackIds(
+      std::vector<std::string>(1, "newstream"), "newtrack");
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  // Latest JSEP spec says the msid never changes, so the other side will not
+  // notice track replacement.
+  auto added = mSessionOff->GetRemoteTracksAdded();
+  auto removed = mSessionOff->GetRemoteTracksRemoved();
+  ASSERT_EQ(0U, added.size());
+  ASSERT_EQ(0U, removed.size());
 }
 
 // Tests whether auto-assigned remote msids (ie; what happens when the other
 // side doesn't use msid attributes) are stable across renegotiation.
 TEST_P(JsepSessionTest, RenegotiationAutoAssignedMsidIsStable)
 {
   AddTracks(*mSessionOff);
   std::string offer = CreateOffer();
@@ -2189,120 +2423,114 @@ TEST_P(JsepSessionTest, RenegotiationAut
   AddTracks(*mSessionAns);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
 
   DisableMsid(&answer);
 
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-
-  // Make sure that DisableMsid actually worked, since it is kinda hacky
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
-  ASSERT_EQ(offererPairs.size(), answererPairs.size());
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_TRUE(offererPairs[i].mReceiving);
-    ASSERT_TRUE(answererPairs[i].mSending);
+  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
+
+  ASSERT_EQ(origOffererTransceivers.size(), origAnswererTransceivers.size());
+  for (size_t i = 0; i < origOffererTransceivers.size(); ++i) {
+    ASSERT_FALSE(IsNull(origOffererTransceivers[i]->mRecvTrack));
+    ASSERT_FALSE(IsNull(origAnswererTransceivers[i]->mSendTrack));
     // These should not match since we've monkeyed with the msid
-    ASSERT_NE(offererPairs[i].mReceiving->GetStreamId(),
-              answererPairs[i].mSending->GetStreamId());
-    ASSERT_NE(offererPairs[i].mReceiving->GetTrackId(),
-              answererPairs[i].mSending->GetTrackId());
+    ASSERT_NE(origOffererTransceivers[i]->mRecvTrack.GetStreamIds(),
+              origAnswererTransceivers[i]->mSendTrack.GetStreamIds());
+    ASSERT_NE(origOffererTransceivers[i]->mRecvTrack.GetTrackId(),
+              origAnswererTransceivers[i]->mSendTrack.GetTrackId());
   }
 
   offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
-  AddTracks(*mSessionAns);
   answer = CreateAnswer();
   SetLocalAnswer(answer);
 
   DisableMsid(&answer);
 
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  auto newOffererPairs = mSessionOff->GetNegotiatedTrackPairs();
-
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
-  }
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+
+  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
 }
 
 TEST_P(JsepSessionTest, RenegotiationOffererDisablesTelephoneEvent)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
   OfferAnswer();
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-
   // check all the audio tracks to make sure they have 2 codecs (109 and 101),
   // and dtmf is enabled on all audio tracks
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    std::vector<JsepTrack*> tracks;
-    tracks.push_back(offererPairs[i].mSending.get());
-    tracks.push_back(offererPairs[i].mReceiving.get());
-    for (JsepTrack *track : tracks) {
-      if (track->GetMediaType() != SdpMediaSection::kAudio) {
-        continue;
-      }
-      const JsepTrackNegotiatedDetails* details = track->GetNegotiatedDetails();
-      ASSERT_EQ(1U, details->GetEncodingCount());
-      const JsepTrackEncoding& encoding = details->GetEncoding(0);
-      ASSERT_EQ(2U, encoding.GetCodecs().size());
-      ASSERT_TRUE(encoding.HasFormat("109"));
-      ASSERT_TRUE(encoding.HasFormat("101"));
-      for (JsepCodecDescription* codec: encoding.GetCodecs()) {
-        ASSERT_TRUE(codec);
-        // we can cast here because we've already checked for audio track
-        JsepAudioCodecDescription *audioCodec =
-            static_cast<JsepAudioCodecDescription*>(codec);
-        ASSERT_TRUE(audioCodec->mDtmfEnabled);
-      }
+  std::vector<JsepTrack> tracks;
+  for (const auto& transceiver : mSessionOff->GetTransceivers()) {
+    tracks.push_back(transceiver->mSendTrack);
+    tracks.push_back(transceiver->mRecvTrack);
+  }
+
+  for (const JsepTrack& track : tracks) {
+    if (track.GetMediaType() != SdpMediaSection::kAudio) {
+      continue;
+    }
+    const JsepTrackNegotiatedDetails* details = track.GetNegotiatedDetails();
+    ASSERT_EQ(1U, details->GetEncodingCount());
+    const JsepTrackEncoding& encoding = details->GetEncoding(0);
+    ASSERT_EQ(2U, encoding.GetCodecs().size());
+    ASSERT_TRUE(encoding.HasFormat("109"));
+    ASSERT_TRUE(encoding.HasFormat("101"));
+    for (JsepCodecDescription* codec: encoding.GetCodecs()) {
+      ASSERT_TRUE(codec);
+      // we can cast here because we've already checked for audio track
+      JsepAudioCodecDescription *audioCodec =
+          static_cast<JsepAudioCodecDescription*>(codec);
+      ASSERT_TRUE(audioCodec->mDtmfEnabled);
     }
   }
 
   std::string offer = CreateOffer();
   ReplaceInSdp(&offer, " 109 101 ", " 109 ");
   ReplaceInSdp(&offer, "a=fmtp:101 0-15\r\n", "");
   ReplaceInSdp(&offer, "a=rtpmap:101 telephone-event/8000/1\r\n", "");
   std::cerr << "modified OFFER: " << offer << std::endl;
 
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
-  AddTracks(*mSessionAns);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-
   // check all the audio tracks to make sure they have 1 codec (109),
   // and dtmf is disabled on all audio tracks
-  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
-    std::vector<JsepTrack*> tracks;
-    tracks.push_back(newOffererPairs[i].mSending.get());
-    tracks.push_back(newOffererPairs[i].mReceiving.get());
-    for (JsepTrack* track : tracks) {
-      if (track->GetMediaType() != SdpMediaSection::kAudio) {
-        continue;
-      }
-      const JsepTrackNegotiatedDetails* details = track->GetNegotiatedDetails();
-      ASSERT_EQ(1U, details->GetEncodingCount());
-      const JsepTrackEncoding& encoding = details->GetEncoding(0);
-      ASSERT_EQ(1U, encoding.GetCodecs().size());
-      ASSERT_TRUE(encoding.HasFormat("109"));
-      // we can cast here because we've already checked for audio track
-      JsepAudioCodecDescription *audioCodec =
-          static_cast<JsepAudioCodecDescription*>(encoding.GetCodecs()[0]);
-      ASSERT_TRUE(audioCodec);
-      ASSERT_FALSE(audioCodec->mDtmfEnabled);
+  tracks.clear();
+  for (const auto& transceiver : mSessionOff->GetTransceivers()) {
+    tracks.push_back(transceiver->mSendTrack);
+    tracks.push_back(transceiver->mRecvTrack);
+  }
+
+  for (const JsepTrack& track : tracks) {
+    if (track.GetMediaType() != SdpMediaSection::kAudio) {
+      continue;
     }
+    const JsepTrackNegotiatedDetails* details = track.GetNegotiatedDetails();
+    ASSERT_EQ(1U, details->GetEncodingCount());
+    const JsepTrackEncoding& encoding = details->GetEncoding(0);
+    ASSERT_EQ(1U, encoding.GetCodecs().size());
+    ASSERT_TRUE(encoding.HasFormat("109"));
+    // we can cast here because we've already checked for audio track
+    JsepAudioCodecDescription *audioCodec =
+        static_cast<JsepAudioCodecDescription*>(encoding.GetCodecs()[0]);
+    ASSERT_TRUE(audioCodec);
+    ASSERT_FALSE(audioCodec->mDtmfEnabled);
   }
 }
 
 // Tests behavior when the answerer does not use msid in the initial exchange,
 // but does on renegotiation.
 TEST_P(JsepSessionTest, RenegotiationAnswererEnablesMsid)
 {
   AddTracks(*mSessionOff);
@@ -2312,94 +2540,81 @@ TEST_P(JsepSessionTest, RenegotiationAns
   AddTracks(*mSessionAns);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
 
   DisableMsid(&answer);
 
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
+  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
 
   offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
-  AddTracks(*mSessionAns);
   answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  auto newOffererPairs = mSessionOff->GetNegotiatedTrackPairs();
-
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_EQ(offererPairs[i].mReceiving->GetMediaType(),
-              newOffererPairs[i].mReceiving->GetMediaType());
-
-    ASSERT_EQ(offererPairs[i].mSending, newOffererPairs[i].mSending);
-    ASSERT_TRUE(Equals(offererPairs[i].mRtpTransport,
-                       newOffererPairs[i].mRtpTransport));
-    ASSERT_TRUE(Equals(offererPairs[i].mRtcpTransport,
-                       newOffererPairs[i].mRtcpTransport));
-
-    if (offererPairs[i].mReceiving->GetMediaType() ==
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+
+  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
+  for (size_t i = 0; i < origOffererTransceivers.size(); ++i) {
+    ASSERT_EQ(origOffererTransceivers[i]->mRecvTrack.GetMediaType(),
+              newOffererTransceivers[i]->mRecvTrack.GetMediaType());
+
+    ASSERT_TRUE(Equals(origOffererTransceivers[i]->mSendTrack,
+                       newOffererTransceivers[i]->mSendTrack));
+    ASSERT_TRUE(Equals(origOffererTransceivers[i]->mTransport,
+                       newOffererTransceivers[i]->mTransport));
+
+    if (origOffererTransceivers[i]->mRecvTrack.GetMediaType() ==
         SdpMediaSection::kApplication) {
-      ASSERT_EQ(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving);
+      ASSERT_TRUE(Equals(origOffererTransceivers[i]->mRecvTrack,
+                         newOffererTransceivers[i]->mRecvTrack));
     } else {
       // This should be the only difference
-      ASSERT_NE(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving);
+      ASSERT_FALSE(Equals(origOffererTransceivers[i]->mRecvTrack,
+                          newOffererTransceivers[i]->mRecvTrack));
     }
   }
 }
 
 TEST_P(JsepSessionTest, RenegotiationAnswererDisablesMsid)
 {
   AddTracks(*mSessionOff);
   std::string offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
   AddTracks(*mSessionAns);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
+  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
 
   offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
-  AddTracks(*mSessionAns);
   answer = CreateAnswer();
   SetLocalAnswer(answer);
 
   DisableMsid(&answer);
 
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  auto newOffererPairs = mSessionOff->GetNegotiatedTrackPairs();
-
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_EQ(offererPairs[i].mReceiving->GetMediaType(),
-              newOffererPairs[i].mReceiving->GetMediaType());
-
-    ASSERT_EQ(offererPairs[i].mSending, newOffererPairs[i].mSending);
-    ASSERT_TRUE(Equals(offererPairs[i].mRtpTransport,
-                       newOffererPairs[i].mRtpTransport));
-    ASSERT_TRUE(Equals(offererPairs[i].mRtcpTransport,
-                       newOffererPairs[i].mRtcpTransport));
-
-    if (offererPairs[i].mReceiving->GetMediaType() ==
-        SdpMediaSection::kApplication) {
-      ASSERT_EQ(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving);
-    } else {
-      // This should be the only difference
-      ASSERT_NE(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving);
-    }
-  }
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+
+  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
 }
 
 // Tests behavior when offerer does not use bundle on the initial offer/answer,
 // but does on renegotiation.
 TEST_P(JsepSessionTest, RenegotiationOffererEnablesBundle)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
@@ -2414,178 +2629,157 @@ TEST_P(JsepSessionTest, RenegotiationOff
   DisableBundle(&offer);
 
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
 
   OfferAnswer();
 
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(newOffererPairs.size(), newAnswererPairs.size());
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());
-
-  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_EQ(newOffererTransceivers.size(), newAnswererTransceivers.size());
+  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
+  ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size());
+
+  for (size_t i = 0; i < newOffererTransceivers.size(); ++i) {
     // No bundle initially
-    ASSERT_FALSE(offererPairs[i].HasBundleLevel());
-    ASSERT_FALSE(answererPairs[i].HasBundleLevel());
+    ASSERT_FALSE(origOffererTransceivers[i]->HasBundleLevel());
+    ASSERT_FALSE(origAnswererTransceivers[i]->HasBundleLevel());
     if (i != 0) {
-      ASSERT_NE(offererPairs[0].mRtpTransport.get(),
-                offererPairs[i].mRtpTransport.get());
-      if (offererPairs[0].mRtcpTransport) {
-        ASSERT_NE(offererPairs[0].mRtcpTransport.get(),
-                  offererPairs[i].mRtcpTransport.get());
-      }
-      ASSERT_NE(answererPairs[0].mRtpTransport.get(),
-                answererPairs[i].mRtpTransport.get());
-      if (answererPairs[0].mRtcpTransport) {
-        ASSERT_NE(answererPairs[0].mRtcpTransport.get(),
-                  answererPairs[i].mRtcpTransport.get());
-      }
+      ASSERT_NE(origOffererTransceivers[0]->mTransport.get(),
+                origOffererTransceivers[i]->mTransport.get());
+      ASSERT_NE(origAnswererTransceivers[0]->mTransport.get(),
+                origAnswererTransceivers[i]->mTransport.get());
     }
 
     // Verify that bundle worked after renegotiation
-    ASSERT_TRUE(newOffererPairs[i].HasBundleLevel());
-    ASSERT_TRUE(newAnswererPairs[i].HasBundleLevel());
-    ASSERT_EQ(newOffererPairs[0].mRtpTransport.get(),
-              newOffererPairs[i].mRtpTransport.get());
-    ASSERT_EQ(newOffererPairs[0].mRtcpTransport.get(),
-              newOffererPairs[i].mRtcpTransport.get());
-    ASSERT_EQ(newAnswererPairs[0].mRtpTransport.get(),
-              newAnswererPairs[i].mRtpTransport.get());
-    ASSERT_EQ(newAnswererPairs[0].mRtcpTransport.get(),
-              newAnswererPairs[i].mRtcpTransport.get());
+    ASSERT_TRUE(newOffererTransceivers[i]->HasBundleLevel());
+    ASSERT_TRUE(newAnswererTransceivers[i]->HasBundleLevel());
+    ASSERT_EQ(newOffererTransceivers[0]->mTransport.get(),
+              newOffererTransceivers[i]->mTransport.get());
+    ASSERT_EQ(newAnswererTransceivers[0]->mTransport.get(),
+              newAnswererTransceivers[i]->mTransport.get());
   }
 }
 
 TEST_P(JsepSessionTest, RenegotiationOffererDisablesBundleTransport)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
 
   if (types.size() < 2) {
     return;
   }
 
   OfferAnswer();
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  std::string reoffer = CreateOffer();
-
-  DisableMsection(&reoffer, 0);
-
-  SetLocalOffer(reoffer, CHECK_SUCCESS);
-  SetRemoteOffer(reoffer, CHECK_SUCCESS);
-  std::string reanswer = CreateAnswer();
-  SetLocalAnswer(reanswer, CHECK_SUCCESS);
-  SetRemoteAnswer(reanswer, CHECK_SUCCESS);
-
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(newOffererPairs.size(), newAnswererPairs.size());
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size() + 1);
-  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size() + 1);
-
-  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
-    ASSERT_TRUE(newOffererPairs[i].HasBundleLevel());
-    ASSERT_TRUE(newAnswererPairs[i].HasBundleLevel());
-    ASSERT_EQ(1U, newOffererPairs[i].BundleLevel());
-    ASSERT_EQ(1U, newAnswererPairs[i].BundleLevel());
-    ASSERT_EQ(newOffererPairs[0].mRtpTransport.get(),
-              newOffererPairs[i].mRtpTransport.get());
-    ASSERT_EQ(newOffererPairs[0].mRtcpTransport.get(),
-              newOffererPairs[i].mRtcpTransport.get());
-    ASSERT_EQ(newAnswererPairs[0].mRtpTransport.get(),
-              newAnswererPairs[i].mRtpTransport.get());
-    ASSERT_EQ(newAnswererPairs[0].mRtcpTransport.get(),
-              newAnswererPairs[i].mRtcpTransport.get());
+  mSessionOff->GetTransceivers()[0]->Stop();
+
+  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_EQ(newOffererTransceivers.size(), newAnswererTransceivers.size());
+  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
+  ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size());
+
+  ASSERT_FALSE(newOffererTransceivers[0]->HasBundleLevel());
+  ASSERT_FALSE(newAnswererTransceivers[0]->HasBundleLevel());
+
+  ASSERT_NE(newOffererTransceivers[0]->mTransport.get(),
+            origOffererTransceivers[0]->mTransport.get());
+  ASSERT_NE(newAnswererTransceivers[0]->mTransport.get(),
+            origAnswererTransceivers[0]->mTransport.get());
+
+  ASSERT_EQ(0U, newOffererTransceivers[0]->mTransport->mComponents);
+  ASSERT_EQ(0U, newAnswererTransceivers[0]->mTransport->mComponents);
+
+  for (size_t i = 1; i < newOffererTransceivers.size(); ++i) {
+    ASSERT_TRUE(newOffererTransceivers[i]->HasBundleLevel());
+    ASSERT_TRUE(newAnswererTransceivers[i]->HasBundleLevel());
+    ASSERT_EQ(1U, newOffererTransceivers[i]->BundleLevel());
+    ASSERT_EQ(1U, newAnswererTransceivers[i]->BundleLevel());
+    ASSERT_NE(newOffererTransceivers[0]->mTransport.get(),
+              newOffererTransceivers[i]->mTransport.get());
+    ASSERT_NE(newAnswererTransceivers[0]->mTransport.get(),
+              newAnswererTransceivers[i]->mTransport.get());
   }
-
-  ASSERT_NE(newOffererPairs[0].mRtpTransport.get(),
-            offererPairs[0].mRtpTransport.get());
-  ASSERT_NE(newAnswererPairs[0].mRtpTransport.get(),
-            answererPairs[0].mRtpTransport.get());
-
-  ASSERT_LE(1U, mSessionOff->GetTransports().size());
-  ASSERT_LE(1U, mSessionAns->GetTransports().size());
-
-  ASSERT_EQ(0U, mSessionOff->GetTransports()[0]->mComponents);
-  ASSERT_EQ(0U, mSessionAns->GetTransports()[0]->mComponents);
 }
 
 TEST_P(JsepSessionTest, RenegotiationAnswererDisablesBundleTransport)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
 
   if (types.size() < 2) {
     return;
   }
 
   OfferAnswer();
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  std::string reoffer = CreateOffer();
-  SetLocalOffer(reoffer, CHECK_SUCCESS);
-  SetRemoteOffer(reoffer, CHECK_SUCCESS);
-  std::string reanswer = CreateAnswer();
-
-  CopyTransportAttributes(&reanswer, 0, 1);
-  DisableMsection(&reanswer, 0);
-
-  SetLocalAnswer(reanswer, CHECK_SUCCESS);
-  SetRemoteAnswer(reanswer, CHECK_SUCCESS);
-
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(newOffererPairs.size(), newAnswererPairs.size());
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size() + 1);
-  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size() + 1);
-
-  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
-    ASSERT_TRUE(newOffererPairs[i].HasBundleLevel());
-    ASSERT_TRUE(newAnswererPairs[i].HasBundleLevel());
-    ASSERT_EQ(1U, newOffererPairs[i].BundleLevel());
-    ASSERT_EQ(1U, newAnswererPairs[i].BundleLevel());
-    ASSERT_EQ(newOffererPairs[0].mRtpTransport.get(),
-              newOffererPairs[i].mRtpTransport.get());
-    ASSERT_EQ(newOffererPairs[0].mRtcpTransport.get(),
-              newOffererPairs[i].mRtcpTransport.get());
-    ASSERT_EQ(newAnswererPairs[0].mRtpTransport.get(),
-              newAnswererPairs[i].mRtpTransport.get());
-    ASSERT_EQ(newAnswererPairs[0].mRtcpTransport.get(),
-              newAnswererPairs[i].mRtcpTransport.get());
+  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
+
+  mSessionAns->GetTransceivers()[0]->Stop();
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_EQ(newOffererTransceivers.size(), newAnswererTransceivers.size());
+  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
+  ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size());
+
+  ASSERT_FALSE(newOffererTransceivers[0]->HasBundleLevel());
+  ASSERT_FALSE(newAnswererTransceivers[0]->HasBundleLevel());
+
+  ASSERT_NE(newOffererTransceivers[0]->mTransport.get(),
+            origOffererTransceivers[0]->mTransport.get());
+  ASSERT_NE(newAnswererTransceivers[0]->mTransport.get(),
+            origAnswererTransceivers[0]->mTransport.get());
+
+  ASSERT_EQ(0U, newOffererTransceivers[0]->mTransport->mComponents);
+  ASSERT_EQ(0U, newAnswererTransceivers[0]->mTransport->mComponents);
+
+  for (size_t i = 1; i < newOffererTransceivers.size(); ++i) {
+    ASSERT_TRUE(newOffererTransceivers[i]->HasBundleLevel());
+    ASSERT_TRUE(newAnswererTransceivers[i]->HasBundleLevel());
+    ASSERT_EQ(1U, newOffererTransceivers[i]->BundleLevel());
+    ASSERT_EQ(1U, newAnswererTransceivers[i]->BundleLevel());
+    ASSERT_NE(newOffererTransceivers[0]->mTransport.get(),
+              newOffererTransceivers[i]->mTransport.get());
+    ASSERT_NE(newAnswererTransceivers[0]->mTransport.get(),
+              newAnswererTransceivers[i]->mTransport.get());
   }
-
-  ASSERT_NE(newOffererPairs[0].mRtpTransport.get(),
-            offererPairs[0].mRtpTransport.get());
-  ASSERT_NE(newAnswererPairs[0].mRtpTransport.get(),
-            answererPairs[0].mRtpTransport.get());
 }
 
 TEST_P(JsepSessionTest, ParseRejectsBadMediaFormat)
 {
-  if (GetParam() == "datachannel") {
+  AddTracks(*mSessionOff);
+  if (types.front() == SdpMediaSection::MediaType::kApplication) {
     return;
   }
-  AddTracks(*mSessionOff);
   std::string offer = CreateOffer();
   UniquePtr<Sdp> munge(Parse(offer));
   SdpMediaSection& mediaSection = munge->GetMediaSection(0);
   mediaSection.AddCodec("75", "DummyFormatVal", 8000, 1);
   std::string sdpString = munge->ToString();
   nsresult rv = mSessionOff->SetLocalDescription(kJsepSdpOffer, sdpString);
   ASSERT_EQ(NS_ERROR_INVALID_ARG, rv);
 }
@@ -2904,23 +3098,23 @@ TEST_P(JsepSessionTest, RenegotiationAns
       msection.SetReceiving(false);
     }
   }
 
   answer = parsedAnswer->ToString();
 
   SetRemoteAnswer(answer);
 
-  for (const RefPtr<JsepTrack>& track : mSessionOff->GetLocalTracks()) {
-    if (track->GetMediaType() != SdpMediaSection::kApplication) {
-      ASSERT_FALSE(track->GetActive());
+  for (const JsepTrack& track : GetLocalTracks(*mSessionOff)) {
+    if (track.GetMediaType() != SdpMediaSection::kApplication) {
+      ASSERT_FALSE(track.GetActive());
     }
   }
 
-  ASSERT_EQ(types.size(), mSessionOff->GetNegotiatedTrackPairs().size());
+  ASSERT_EQ(types.size(), mSessionOff->GetTransceivers().size());
 }
 
 TEST_P(JsepSessionTest, RenegotiationAnswererInactive)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
   OfferAnswer();
 
@@ -2938,23 +3132,23 @@ TEST_P(JsepSessionTest, RenegotiationAns
       msection.SetSending(false);
     }
   }
 
   answer = parsedAnswer->ToString();
 
   SetRemoteAnswer(answer, CHECK_SUCCESS); // Won't have answerer tracks
 
-  for (const RefPtr<JsepTrack>& track : mSessionOff->GetLocalTracks()) {
-    if (track->GetMediaType() != SdpMediaSection::kApplication) {
-      ASSERT_FALSE(track->GetActive());
+  for (const JsepTrack& track : GetLocalTracks(*mSessionOff)) {
+    if (track.GetMediaType() != SdpMediaSection::kApplication) {
+      ASSERT_FALSE(track.GetActive());
     }
   }
 
-  ASSERT_EQ(types.size(), mSessionOff->GetNegotiatedTrackPairs().size());
+  ASSERT_EQ(types.size(), mSessionOff->GetTransceivers().size());
 }
 
 
 INSTANTIATE_TEST_CASE_P(
     Variants,
     JsepSessionTest,
     ::testing::Values("audio",
                       "video",
@@ -2977,20 +3171,23 @@ INSTANTIATE_TEST_CASE_P(
                       "audio,video,video",
                       "audio,audio,video,video",
                       "audio,audio,video,video,datachannel"));
 
 // offerToReceiveXxx variants
 
 TEST_F(JsepSessionTest, OfferAnswerRecvOnlyLines)
 {
-  JsepOfferOptions options;
-  options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U));
-  options.mOfferToReceiveVideo = Some(static_cast<size_t>(2U));
-  std::string offer = CreateOffer(Some(options));
+  mSessionOff->AddTransceiver(new JsepTransceiver(
+        SdpMediaSection::kAudio, SdpDirectionAttribute::kRecvonly));
+  mSessionOff->AddTransceiver(new JsepTransceiver(
+        SdpMediaSection::kVideo, SdpDirectionAttribute::kRecvonly));
+  mSessionOff->AddTransceiver(new JsepTransceiver(
+        SdpMediaSection::kVideo, SdpDirectionAttribute::kRecvonly));
+  std::string offer = CreateOffer();
 
   UniquePtr<Sdp> parsedOffer(Parse(offer));
   ASSERT_TRUE(!!parsedOffer);
 
   ASSERT_EQ(3U, parsedOffer->GetMediaSectionCount());
   ASSERT_EQ(SdpMediaSection::kAudio,
             parsedOffer->GetMediaSection(0).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
@@ -3039,34 +3236,32 @@ TEST_F(JsepSessionTest, OfferAnswerRecvO
   ASSERT_EQ(SdpMediaSection::kVideo,
             parsedAnswer->GetMediaSection(2).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kInactive,
             parsedAnswer->GetMediaSection(2).GetAttributeList().GetDirection());
 
   SetLocalAnswer(answer, CHECK_SUCCESS);
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  std::vector<JsepTrackPair> trackPairs(mSessionOff->GetNegotiatedTrackPairs());
-  ASSERT_EQ(2U, trackPairs.size());
-  for (auto pair : trackPairs) {
-    auto ssrcs = parsedOffer->GetMediaSection(pair.mLevel).GetAttributeList()
-                 .GetSsrc().mSsrcs;
+  std::vector<RefPtr<JsepTransceiver>> transceivers(mSessionOff->GetTransceivers());
+  ASSERT_EQ(3U, transceivers.size());
+  for (const auto& transceiver : transceivers) {
+    auto ssrcs = parsedOffer->GetMediaSection(transceiver->GetLevel())
+                 .GetAttributeList().GetSsrc().mSsrcs;
     ASSERT_EQ(1U, ssrcs.size());
-    ASSERT_EQ(pair.mRecvonlySsrc, ssrcs.front().ssrc);
   }
 }
 
 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));
-  std::string offer = CreateOffer(Some(options));
+  SetDirection(*mSessionOff, 0, SdpDirectionAttribute::kSendonly);
+  SetDirection(*mSessionOff, 2, SdpDirectionAttribute::kSendonly);
+  std::string offer = CreateOffer();
 
   UniquePtr<Sdp> outputSdp(Parse(offer));
   ASSERT_TRUE(!!outputSdp);
 
   ASSERT_EQ(3U, outputSdp->GetMediaSectionCount());
   ASSERT_EQ(SdpMediaSection::kAudio,
             outputSdp->GetMediaSection(0).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kSendonly,
@@ -3107,20 +3302,20 @@ TEST_F(JsepSessionTest, OfferAnswerSendO
   ASSERT_EQ(SdpMediaSection::kVideo,
             outputSdp->GetMediaSection(2).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
             outputSdp->GetMediaSection(2).GetAttributeList().GetDirection());
 }
 
 TEST_F(JsepSessionTest, OfferToReceiveAudioNotUsed)
 {
-  JsepOfferOptions options;
-  options.mOfferToReceiveAudio = Some<size_t>(1);
-
-  OfferAnswer(CHECK_SUCCESS, Some(options));
+  mSessionOff->AddTransceiver(new JsepTransceiver(
+        SdpMediaSection::kAudio, SdpDirectionAttribute::kRecvonly));
+
+  OfferAnswer(CHECK_SUCCESS);
 
   UniquePtr<Sdp> offer(Parse(
         mSessionOff->GetLocalDescription(kJsepDescriptionCurrent)));
   ASSERT_TRUE(offer.get());
   ASSERT_EQ(1U, offer->GetMediaSectionCount());
   ASSERT_EQ(SdpMediaSection::kAudio,
             offer->GetMediaSection(0).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
@@ -3133,20 +3328,20 @@ TEST_F(JsepSessionTest, OfferToReceiveAu
   ASSERT_EQ(SdpMediaSection::kAudio,
             answer->GetMediaSection(0).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kInactive,
             answer->GetMediaSection(0).GetAttributeList().GetDirection());
 }
 
 TEST_F(JsepSessionTest, OfferToReceiveVideoNotUsed)
 {
-  JsepOfferOptions options;
-  options.mOfferToReceiveVideo = Some<size_t>(1);
-
-  OfferAnswer(CHECK_SUCCESS, Some(options));
+  mSessionOff->AddTransceiver(new JsepTransceiver(
+        SdpMediaSection::kVideo, SdpDirectionAttribute::kRecvonly));
+
+  OfferAnswer(CHECK_SUCCESS);
 
   UniquePtr<Sdp> offer(Parse(
         mSessionOff->GetLocalDescription(kJsepDescriptionCurrent)));
   ASSERT_TRUE(offer.get());
   ASSERT_EQ(1U, offer->GetMediaSectionCount());
   ASSERT_EQ(SdpMediaSection::kVideo,
             offer->GetMediaSection(0).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
@@ -3159,23 +3354,25 @@ TEST_F(JsepSessionTest, OfferToReceiveVi
   ASSERT_EQ(SdpMediaSection::kVideo,
             answer->GetMediaSection(0).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kInactive,
             answer->GetMediaSection(0).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);
+  RefPtr<JsepTransceiver> audio(new JsepTransceiver(SdpMediaSection::kAudio));
+  audio->mSendTrack.UpdateTrackIds(
+      std::vector<std::string>(1, "offerer_stream"), "a1");
+  mSessionOff->AddTransceiver(audio);
+
+  RefPtr<JsepTransceiver> video(new JsepTransceiver(SdpMediaSection::kVideo));
+  video->mSendTrack.UpdateTrackIds(
+      std::vector<std::string>(1, "offerer_stream"), "v1");
+  mSessionOff->AddTransceiver(video);
 
   std::string offer = CreateOffer();
 
   UniquePtr<Sdp> outputSdp(Parse(offer));
   ASSERT_TRUE(!!outputSdp);
 
   ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
   ASSERT_EQ(SdpMediaSection::kAudio,
@@ -3184,22 +3381,23 @@ TEST_F(JsepSessionTest, CreateOfferNoDat
             outputSdp->GetMediaSection(1).GetMediaType());
 }
 
 TEST_F(JsepSessionTest, ValidateOfferedVideoCodecParams)
 {
   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);
+  RefPtr<JsepTransceiver> audio(new JsepTransceiver(SdpMediaSection::kAudio));
+  audio->mSendTrack.UpdateTrackIds(std::vector<std::string>(1, "offerer_stream"), "a1");
+  mSessionOff->AddTransceiver(audio);
+
+  RefPtr<JsepTransceiver> video(new JsepTransceiver(SdpMediaSection::kVideo));
+  video->mSendTrack.UpdateTrackIds(std::vector<std::string>(1, "offerer_stream"), "v1");
+  mSessionOff->AddTransceiver(video);
 
   std::string offer = CreateOffer();
 
   UniquePtr<Sdp> outputSdp(Parse(offer));
   ASSERT_TRUE(!!outputSdp);
 
   ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
   auto& video_section = outputSdp->GetMediaSection(1);
@@ -3311,22 +3509,23 @@ TEST_F(JsepSessionTest, ValidateOfferedV
   ASSERT_EQ(123, parsed_red_params.encodings[4]);
 }
 
 TEST_F(JsepSessionTest, ValidateOfferedAudioCodecParams)
 {
   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);
+  RefPtr<JsepTransceiver> audio(new JsepTransceiver(SdpMediaSection::kAudio));
+  audio->mSendTrack.UpdateTrackIds(std::vector<std::string>(1, "offerer_stream"), "a1");
+  mSessionOff->AddTransceiver(audio);
+
+  RefPtr<JsepTransceiver> video(new JsepTransceiver(SdpMediaSection::kVideo));
+  video->mSendTrack.UpdateTrackIds(std::vector<std::string>(1, "offerer_stream"), "v1");
+  mSessionOff->AddTransceiver(video);
 
   std::string offer = CreateOffer();
 
   UniquePtr<Sdp> outputSdp(Parse(offer));
   ASSERT_TRUE(!!outputSdp);
 
   ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
   auto& audio_section = outputSdp->GetMediaSection(0);
@@ -3393,39 +3592,29 @@ TEST_F(JsepSessionTest, ValidateOfferedA
   ASSERT_EQ("0-15", parsed_dtmf_params.dtmfTones);
 }
 
 TEST_F(JsepSessionTest, ValidateNoFmtpLineForRedInOfferAndAnswer)
 {
   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);
+  AddTracksToStream(*mSessionOff, "offerer_stream", "audio,video");
 
   std::string offer = CreateOffer();
 
   // look for line with fmtp:122 and remove it
   size_t start = offer.find("a=fmtp:122");
   size_t end = offer.find("\r\n", start);
   offer.replace(start, end+2-start, "");
 
   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);
+  AddTracksToStream(*mSessionAns, "answerer_stream", "audio,video");
 
   std::string answer = CreateAnswer();
   // because parsing will throw out the malformed fmtp, make sure it is not
   // in the answer sdp string
   ASSERT_EQ(std::string::npos, answer.find("a=fmtp:122"));
 
   UniquePtr<Sdp> outputSdp(Parse(answer));
   ASSERT_TRUE(!!outputSdp);
@@ -3462,40 +3651,40 @@ TEST_F(JsepSessionTest, ValidateNoFmtpLi
   ASSERT_EQ("126", fmtps[0].format);
   ASSERT_EQ("97", fmtps[1].format);
   ASSERT_EQ("120", fmtps[2].format);
   ASSERT_EQ("121", fmtps[3].format);
 
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 
-  auto offerPairs = mSessionOff->GetNegotiatedTrackPairs();
-  ASSERT_EQ(2U, offerPairs.size());
-  ASSERT_TRUE(offerPairs[1].mSending);
-  ASSERT_TRUE(offerPairs[1].mReceiving);
-  ASSERT_TRUE(offerPairs[1].mSending->GetNegotiatedDetails());
-  ASSERT_TRUE(offerPairs[1].mReceiving->GetNegotiatedDetails());
+  auto offerTransceivers = mSessionOff->GetTransceivers();
+  ASSERT_EQ(2U, offerTransceivers.size());
+  ASSERT_FALSE(IsNull(offerTransceivers[1]->mSendTrack));
+  ASSERT_FALSE(IsNull(offerTransceivers[1]->mRecvTrack));
+  ASSERT_TRUE(offerTransceivers[1]->mSendTrack.GetNegotiatedDetails());
+  ASSERT_TRUE(offerTransceivers[1]->mRecvTrack.GetNegotiatedDetails());
   ASSERT_EQ(6U,
-      offerPairs[1].mSending->GetNegotiatedDetails()->GetEncoding(0)
+      offerTransceivers[1]->mSendTrack.GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
   ASSERT_EQ(6U,
-      offerPairs[1].mReceiving->GetNegotiatedDetails()->GetEncoding(0)
+      offerTransceivers[1]->mRecvTrack.GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
 
-  auto answerPairs = mSessionAns->GetNegotiatedTrackPairs();
-  ASSERT_EQ(2U, answerPairs.size());
-  ASSERT_TRUE(answerPairs[1].mSending);
-  ASSERT_TRUE(answerPairs[1].mReceiving);
-  ASSERT_TRUE(answerPairs[1].mSending->GetNegotiatedDetails());
-  ASSERT_TRUE(answerPairs[1].mReceiving->GetNegotiatedDetails());
+  auto answerTransceivers = mSessionAns->GetTransceivers();
+  ASSERT_EQ(2U, answerTransceivers.size());
+  ASSERT_FALSE(IsNull(answerTransceivers[1]->mSendTrack));
+  ASSERT_FALSE(IsNull(answerTransceivers[1]->mRecvTrack));
+  ASSERT_TRUE(answerTransceivers[1]->mSendTrack.GetNegotiatedDetails());
+  ASSERT_TRUE(answerTransceivers[1]->mRecvTrack.GetNegotiatedDetails());
   ASSERT_EQ(6U,
-      answerPairs[1].mSending->GetNegotiatedDetails()->GetEncoding(0)
+      answerTransceivers[1]->mSendTrack.GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
   ASSERT_EQ(6U,
-      answerPairs[1].mReceiving->GetNegotiatedDetails()->GetEncoding(0)
+      answerTransceivers[1]->mRecvTrack.GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
 }
 
 TEST_F(JsepSessionTest, ValidateAnsweredCodecParams)
 {
   // TODO(bug 1099351): Once fixed, we can allow red in this offer,
   // which will also cause multiple codecs in answer.  For now,
   // red/ulpfec for video are behind a pref to mitigate potential for
@@ -3515,33 +3704,23 @@ TEST_F(JsepSessionTest, ValidateAnswered
         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);
+  AddTracksToStream(*mSessionOff, "offerer_stream", "audio,video");
 
   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);
+  AddTracksToStream(*mSessionAns, "answerer_stream", "audio,video");
 
   std::string answer = CreateAnswer();
 
   UniquePtr<Sdp> outputSdp(Parse(answer));
   ASSERT_TRUE(!!outputSdp);
 
   ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
   auto& video_section = outputSdp->GetMediaSection(1);
@@ -3593,40 +3772,40 @@ TEST_F(JsepSessionTest, ValidateAnswered
 
   ASSERT_EQ((uint32_t)12288, parsed_vp8_params.max_fs);
   ASSERT_EQ((uint32_t)60, parsed_vp8_params.max_fr);
 
 
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 
-  auto offerPairs = mSessionOff->GetNegotiatedTrackPairs();
-  ASSERT_EQ(2U, offerPairs.size());
-  ASSERT_TRUE(offerPairs[1].mSending);
-  ASSERT_TRUE(offerPairs[1].mReceiving);
-  ASSERT_TRUE(offerPairs[1].mSending->GetNegotiatedDetails());
-  ASSERT_TRUE(offerPairs[1].mReceiving->GetNegotiatedDetails());
+  auto offerTransceivers = mSessionOff->GetTransceivers();
+  ASSERT_EQ(2U, offerTransceivers.size());
+  ASSERT_FALSE(IsNull(offerTransceivers[1]->mSendTrack));
+  ASSERT_FALSE(IsNull(offerTransceivers[1]->mRecvTrack));
+  ASSERT_TRUE(offerTransceivers[1]->mSendTrack.GetNegotiatedDetails());
+  ASSERT_TRUE(offerTransceivers[1]->mRecvTrack.GetNegotiatedDetails());
   ASSERT_EQ(1U,
-      offerPairs[1].mSending->GetNegotiatedDetails()->GetEncoding(0)
+      offerTransceivers[1]->mSendTrack.GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
   ASSERT_EQ(1U,
-      offerPairs[1].mReceiving->GetNegotiatedDetails()->GetEncoding(0)
+      offerTransceivers[1]->mRecvTrack.GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
 
-  auto answerPairs = mSessionAns->GetNegotiatedTrackPairs();
-  ASSERT_EQ(2U, answerPairs.size());
-  ASSERT_TRUE(answerPairs[1].mSending);
-  ASSERT_TRUE(answerPairs[1].mReceiving);
-  ASSERT_TRUE(answerPairs[1].mSending->GetNegotiatedDetails());
-  ASSERT_TRUE(answerPairs[1].mReceiving->GetNegotiatedDetails());
+  auto answerTransceivers = mSessionAns->GetTransceivers();
+  ASSERT_EQ(2U, answerTransceivers.size());
+  ASSERT_FALSE(IsNull(answerTransceivers[1]->mSendTrack));
+  ASSERT_FALSE(IsNull(answerTransceivers[1]->mRecvTrack));
+  ASSERT_TRUE(answerTransceivers[1]->mSendTrack.GetNegotiatedDetails());
+  ASSERT_TRUE(answerTransceivers[1]->mRecvTrack.GetNegotiatedDetails());
   ASSERT_EQ(1U,
-      answerPairs[1].mSending->GetNegotiatedDetails()->GetEncoding(0)
+      answerTransceivers[1]->mSendTrack.GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
   ASSERT_EQ(1U,
-      answerPairs[1].mReceiving->GetNegotiatedDetails()->GetEncoding(0)
+      answerTransceivers[1]->mRecvTrack.GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
 
 #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);
 
@@ -3683,35 +3862,34 @@ static void ReplaceAll(const std::string
 {
   while (in->find(toReplace) != std::string::npos) {
     Replace(toReplace, with, in);
   }
 }
 
 static void
 GetCodec(JsepSession& session,
-         size_t pairIndex,
+         size_t transceiverIndex,
          sdp::Direction direction,
          size_t encodingIndex,
          size_t codecIndex,
          const JsepCodecDescription** codecOut)
 {
   *codecOut = nullptr;
-  ASSERT_LT(pairIndex, session.GetNegotiatedTrackPairs().size());
-  JsepTrackPair pair(session.GetNegotiatedTrackPairs().front());
-  RefPtr<JsepTrack> track(
-      (direction == sdp::kSend) ? pair.mSending : pair.mReceiving);
-  ASSERT_TRUE(track);
-  ASSERT_TRUE(track->GetNegotiatedDetails());
-  ASSERT_LT(encodingIndex, track->GetNegotiatedDetails()->GetEncodingCount());
+  ASSERT_LT(transceiverIndex, session.GetTransceivers().size());
+  RefPtr<JsepTransceiver> transceiver(session.GetTransceivers()[transceiverIndex]);
+  JsepTrack& track =
+      (direction == sdp::kSend) ? transceiver->mSendTrack : transceiver->mRecvTrack;
+  ASSERT_TRUE(track.GetNegotiatedDetails());
+  ASSERT_LT(encodingIndex, track.GetNegotiatedDetails()->GetEncodingCount());
   ASSERT_LT(codecIndex,
-      track->GetNegotiatedDetails()->GetEncoding(encodingIndex)
+      track.GetNegotiatedDetails()->GetEncoding(encodingIndex)
       .GetCodecs().size());
   *codecOut =
-      track->GetNegotiatedDetails()->GetEncoding(encodingIndex)
+      track.GetNegotiatedDetails()->GetEncoding(encodingIndex)
       .GetCodecs()[codecIndex];
 }
 
 static void
 ForceH264(JsepSession& session, uint32_t profileLevelId)
 {
   for (JsepCodecDescription* codec : session.Codecs()) {
     if (codec->mName == "H264") {
@@ -3784,18 +3962,18 @@ TEST_F(JsepSessionTest, TestH264Negotiat
   SetLocalOffer(offer, CHECK_SUCCESS);
 
   SetRemoteOffer(offer, CHECK_SUCCESS);
   std::string answer(CreateAnswer());
 
   SetRemoteAnswer(answer, CHECK_SUCCESS);
   SetLocalAnswer(answer, CHECK_SUCCESS);
 
-  ASSERT_EQ(0U, mSessionOff->GetNegotiatedTrackPairs().size());
-  ASSERT_EQ(0U, mSessionAns->GetNegotiatedTrackPairs().size());
+  ASSERT_EQ(nullptr, GetNegotiatedTransceiver(*mSessionOff, 0));
+  ASSERT_EQ(nullptr, GetNegotiatedTransceiver(*mSessionAns, 0));
 }
 
 TEST_F(JsepSessionTest, TestH264NegotiationOffererDefault)
 {
   ForceH264(*mSessionOff, 0x42000d);
   ForceH264(*mSessionAns, 0x42000d);
 
   AddTracks(*mSessionOff, "video");
@@ -4017,17 +4195,16 @@ TEST_F(JsepSessionTest, TestH264LevelAsy
   // it did not set level-asymmetry-required, and we already check that
   // elsewhere
 }
 
 TEST_P(JsepSessionTest, TestRejectMline)
 {
   // We need to do this before adding tracks
   types = BuildTypes(GetParam());
-  std::sort(types.begin(), types.end());
 
   switch (types.front()) {
     case SdpMediaSection::kAudio:
       // Sabotage audio
       EnsureNegotiationFailure(types.front(), "opus");
       break;
     case SdpMediaSection::kVideo:
       // Sabotage video
@@ -4068,26 +4245,30 @@ TEST_P(JsepSessionTest, TestRejectMline)
   ASSERT_EQ(0U, failed_section->GetPort());
 
   mSessionAns->SetLocalDescription(kJsepSdpAnswer, answer);
   mSessionOff->SetRemoteDescription(kJsepSdpAnswer, answer);
 
   size_t numRejected = std::count(types.begin(), types.end(), types.front());
   size_t numAccepted = types.size() - numRejected;
 
-  ASSERT_EQ(numAccepted, mSessionOff->GetNegotiatedTrackPairs().size());
-  ASSERT_EQ(numAccepted, mSessionAns->GetNegotiatedTrackPairs().size());
-
-  ASSERT_EQ(types.size(), mSessionOff->GetTransports().size());
-  ASSERT_EQ(types.size(), mSessionOff->GetLocalTracks().size());
-  ASSERT_EQ(numAccepted, mSessionOff->GetRemoteTracks().size());
-
-  ASSERT_EQ(types.size(), mSessionAns->GetTransports().size());
-  ASSERT_EQ(types.size(), mSessionAns->GetLocalTracks().size());
-  ASSERT_EQ(types.size(), mSessionAns->GetRemoteTracks().size());
+  if (types.front() == SdpMediaSection::MediaType::kApplication) {
+    ASSERT_TRUE(GetDatachannelTransceiver(*mSessionOff));
+    ASSERT_FALSE(
+        GetDatachannelTransceiver(*mSessionOff)->mRecvTrack.GetActive());
+    ASSERT_TRUE(GetDatachannelTransceiver(*mSessionAns));
+    ASSERT_FALSE(
+        GetDatachannelTransceiver(*mSessionAns)->mRecvTrack.GetActive());
+  } else {
+    ASSERT_EQ(types.size(), GetLocalTracks(*mSessionOff).size());
+    ASSERT_EQ(numAccepted, GetRemoteTracks(*mSessionOff).size());
+
+    ASSERT_EQ(types.size(), GetLocalTracks(*mSessionAns).size());
+    ASSERT_EQ(types.size(), GetRemoteTracks(*mSessionAns).size());
+  }
 }
 
 TEST_F(JsepSessionTest, CreateOfferNoMlines)
 {
   JsepOfferOptions options;
   std::string offer;
   nsresult rv = mSessionOff->CreateOffer(options, &offer);
   ASSERT_NE(NS_OK, rv);
@@ -4315,20 +4496,20 @@ TEST_F(JsepSessionTest, TestRtcpFbStar)
   offer = parsedOffer->ToString();
 
   SetLocalOffer(offer, CHECK_SUCCESS);
   SetRemoteOffer(offer, CHECK_SUCCESS);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer, CHECK_SUCCESS);
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  ASSERT_EQ(1U, mSessionAns->GetRemoteTracks().size());
-  RefPtr<JsepTrack> track = mSessionAns->GetRemoteTracks()[0];
-  ASSERT_TRUE(track->GetNegotiatedDetails());
-  auto* details = track->GetNegotiatedDetails();
+  ASSERT_EQ(1U, GetRemoteTracks(*mSessionAns).size());
+  JsepTrack track = GetRemoteTracks(*mSessionAns)[0];
+  ASSERT_TRUE(track.GetNegotiatedDetails());
+  auto* details = track.GetNegotiatedDetails();
   for (const JsepCodecDescription* codec :
        details->GetEncoding(0).GetCodecs()) {
     const JsepVideoCodecDescription* videoCodec =
       static_cast<const JsepVideoCodecDescription*>(codec);
     ASSERT_EQ(1U, videoCodec->mNackFbTypes.size());
     ASSERT_EQ("", videoCodec->mNackFbTypes[0]);
   }
 }
@@ -4342,55 +4523,55 @@ TEST_F(JsepSessionTest, TestUniquePayloa
 
   std::string offer = CreateOffer();
   SetLocalOffer(offer, CHECK_SUCCESS);
   SetRemoteOffer(offer, CHECK_SUCCESS);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer, CHECK_SUCCESS);
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  auto offerPairs = mSessionOff->GetNegotiatedTrackPairs();
-  auto answerPairs = mSessionAns->GetNegotiatedTrackPairs();
-  ASSERT_EQ(3U, offerPairs.size());
-  ASSERT_EQ(3U, answerPairs.size());
-
-  ASSERT_TRUE(offerPairs[0].mReceiving);
-  ASSERT_TRUE(offerPairs[0].mReceiving->GetNegotiatedDetails());
+  auto offerTransceivers = mSessionOff->GetTransceivers();
+  auto answerTransceivers = mSessionAns->GetTransceivers();
+  ASSERT_EQ(3U, offerTransceivers.size());
+  ASSERT_EQ(3U, answerTransceivers.size());
+
+  ASSERT_FALSE(IsNull(offerTransceivers[0]->mRecvTrack));
+  ASSERT_TRUE(offerTransceivers[0]->mRecvTrack.GetNegotiatedDetails());
   ASSERT_EQ(0U,
-      offerPairs[0].mReceiving->GetNegotiatedDetails()->
+      offerTransceivers[0]->mRecvTrack.GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 
-  ASSERT_TRUE(offerPairs[1].mReceiving);
-  ASSERT_TRUE(offerPairs[1].mReceiving->GetNegotiatedDetails());
+  ASSERT_FALSE(IsNull(offerTransceivers[1]->mRecvTrack));
+  ASSERT_TRUE(offerTransceivers[1]->mRecvTrack.GetNegotiatedDetails());
   ASSERT_EQ(0U,
-      offerPairs[1].mReceiving->GetNegotiatedDetails()->
+      offerTransceivers[1]->mRecvTrack.GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 
-  ASSERT_TRUE(offerPairs[2].mReceiving);
-  ASSERT_TRUE(offerPairs[2].mReceiving->GetNegotiatedDetails());
+  ASSERT_FALSE(IsNull(offerTransceivers[2]->mRecvTrack));
+  ASSERT_TRUE(offerTransceivers[2]->mRecvTrack.GetNegotiatedDetails());
   ASSERT_NE(0U,
-      offerPairs[2].mReceiving->GetNegotiatedDetails()->
+      offerTransceivers[2]->mRecvTrack.GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 
-  ASSERT_TRUE(answerPairs[0].mReceiving);
-  ASSERT_TRUE(answerPairs[0].mReceiving->GetNegotiatedDetails());
+  ASSERT_FALSE(IsNull(answerTransceivers[0]->mRecvTrack));
+  ASSERT_TRUE(answerTransceivers[0]->mRecvTrack.GetNegotiatedDetails());
   ASSERT_EQ(0U,
-      answerPairs[0].mReceiving->GetNegotiatedDetails()->
+      answerTransceivers[0]->mRecvTrack.GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 
-  ASSERT_TRUE(answerPairs[1].mReceiving);
-  ASSERT_TRUE(answerPairs[1].mReceiving->GetNegotiatedDetails());
+  ASSERT_FALSE(IsNull(answerTransceivers[1]->mRecvTrack));
+  ASSERT_TRUE(answerTransceivers[1]->mRecvTrack.GetNegotiatedDetails());
   ASSERT_EQ(0U,
-      answerPairs[1].mReceiving->GetNegotiatedDetails()->
+      answerTransceivers[1]->mRecvTrack.GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 
-  ASSERT_TRUE(answerPairs[2].mReceiving);
-  ASSERT_TRUE(answerPairs[2].mReceiving->GetNegotiatedDetails());
+  ASSERT_FALSE(IsNull(answerTransceivers[2]->mRecvTrack));
+  ASSERT_TRUE(answerTransceivers[2]->mRecvTrack.GetNegotiatedDetails());
   ASSERT_NE(0U,
-      answerPairs[2].mReceiving->GetNegotiatedDetails()->
+      answerTransceivers[2]->mRecvTrack.GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 }
 
 TEST_F(JsepSessionTest, UnknownFingerprintAlgorithm)
 {
   types.push_back(SdpMediaSection::kAudio);
   AddTracks(*mSessionOff, "audio");
   AddTracks(*mSessionAns, "audio");
@@ -4555,17 +4736,17 @@ TEST_P(JsepSessionTest, TestRejectOfferR
 
   std::string offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
 
   ASSERT_EQ(NS_OK,
             mSessionAns->SetRemoteDescription(kJsepSdpRollback, ""));
   ASSERT_EQ(kJsepStateStable, mSessionAns->GetState());
-  ASSERT_EQ(types.size(), mSessionAns->GetRemoteTracksRemoved().size());
+  ASSERT_EQ(CountRtpTypes(), mSessionAns->GetRemoteTracksRemoved().size());
 
   ASSERT_EQ(NS_OK,
             mSessionOff->SetLocalDescription(kJsepSdpRollback, ""));
   ASSERT_EQ(kJsepStateStable, mSessionOff->GetState());
 
   OfferAnswer();
 }
 
@@ -4607,20 +4788,22 @@ TEST_P(JsepSessionTest, TestInvalidRollb
   ASSERT_EQ(NS_ERROR_UNEXPECTED,
             mSessionOff->SetLocalDescription(kJsepSdpRollback, ""));
   ASSERT_EQ(NS_ERROR_UNEXPECTED,
             mSessionOff->SetRemoteDescription(kJsepSdpRollback, ""));
 }
 
 size_t GetActiveTransportCount(const JsepSession& session)
 {
-  auto transports = session.GetTransports();
   size_t activeTransportCount = 0;
-  for (RefPtr<JsepTransport>& transport : transports) {
-    activeTransportCount += transport->mComponents;
+  for (const auto& transceiver : session.GetTransceivers()) {
+    if (!transceiver->HasBundleLevel() ||
+        (transceiver->BundleLevel() == transceiver->GetLevel())) {
+      activeTransportCount += transceiver->mTransport->mComponents;
+    }
   }
   return activeTransportCount;
 }
 
 TEST_P(JsepSessionTest, TestBalancedBundle)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
@@ -4646,18 +4829,18 @@ TEST_P(JsepSessionTest, TestBalancedBund
   }
 
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 
-  CheckPairs(*mSessionOff, "Offerer pairs");
-  CheckPairs(*mSessionAns, "Answerer pairs");
+  CheckTransceiversAreBundled(*mSessionOff, "Offerer transceivers");
+  CheckTransceiversAreBundled(*mSessionAns, "Answerer transceivers");
   EXPECT_EQ(1U, GetActiveTransportCount(*mSessionOff));
   EXPECT_EQ(1U, GetActiveTransportCount(*mSessionAns));
 }
 
 TEST_P(JsepSessionTest, TestMaxBundle)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
@@ -4677,18 +4860,18 @@ TEST_P(JsepSessionTest, TestMaxBundle)
   for (size_t i = 1; i < parsedOffer->GetMediaSectionCount(); ++i) {
     ASSERT_TRUE(
         parsedOffer->GetMediaSection(i).GetAttributeList().HasAttribute(
           SdpAttribute::kBundleOnlyAttribute));
     ASSERT_EQ(0U, parsedOffer->GetMediaSection(i).GetPort());
   }
 
 
-  CheckPairs(*mSessionOff, "Offerer pairs");
-  CheckPairs(*mSessionAns, "Answerer pairs");
+  CheckTransceiversAreBundled(*mSessionOff, "Offerer transceivers");
+  CheckTransceiversAreBundled(*mSessionAns, "Answerer transceivers");
   EXPECT_EQ(1U, GetActiveTransportCount(*mSessionOff));
   EXPECT_EQ(1U, GetActiveTransportCount(*mSessionAns));
 }
 
 TEST_F(JsepSessionTest, TestNonDefaultProtocol)
 {
   AddTracks(*mSessionOff, "audio,video,datachannel");
   AddTracks(*mSessionAns, "audio,video,datachannel");
@@ -4810,60 +4993,48 @@ TEST_F(JsepSessionTest, CreateOfferDontR
 }
 
 TEST_F(JsepSessionTest, CreateOfferRemoveAudioTrack)
 {
   types.push_back(SdpMediaSection::kAudio);
   types.push_back(SdpMediaSection::kVideo);
   AddTracks(*mSessionOff, "audio,video");
 
-  JsepOfferOptions options;
-  options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U));
-  options.mOfferToReceiveVideo = Some(static_cast<size_t>(0U));
-
-  RefPtr<JsepTrack> removedTrack = GetTrackOff(0, types.front());
-  ASSERT_TRUE(removedTrack);
-  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrack->GetStreamId(),
-                                           removedTrack->GetTrackId()));
-
-  CreateOffer(Some(options));
+  SetDirection(*mSessionOff, 1, SdpDirectionAttribute::kSendonly);
+  JsepTrack removedTrack = RemoveTrack(*mSessionOff, 0);
+  ASSERT_FALSE(IsNull(removedTrack));
+
+  CreateOffer();
 }
 
 TEST_F(JsepSessionTest, CreateOfferDontReceiveAudioRemoveAudioTrack)
 {
   types.push_back(SdpMediaSection::kAudio);
   types.push_back(SdpMediaSection::kVideo);
   AddTracks(*mSessionOff, "audio,video");
 
-  JsepOfferOptions options;
-  options.mOfferToReceiveAudio = Some(static_cast<size_t>(0U));
-  options.mOfferToReceiveVideo = Some(static_cast<size_t>(1U));
-
-  RefPtr<JsepTrack> removedTrack = GetTrackOff(0, types.front());
-  ASSERT_TRUE(removedTrack);
-  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrack->GetStreamId(),
-                                           removedTrack->GetTrackId()));
-
-  CreateOffer(Some(options));
+  SetDirection(*mSessionOff, 0, SdpDirectionAttribute::kSendonly);
+  JsepTrack removedTrack = RemoveTrack(*mSessionOff, 0);
+  ASSERT_FALSE(IsNull(removedTrack));
+
+  CreateOffer();
 }
 
 TEST_F(JsepSessionTest, CreateOfferDontReceiveVideoRemoveVideoTrack)
 {
   types.push_back(SdpMediaSection::kAudio);
   types.push_back(SdpMediaSection::kVideo);
   AddTracks(*mSessionOff, "audio,video");
 
   JsepOfferOptions options;
   options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U));
   options.mOfferToReceiveVideo = Some(static_cast<size_t>(0U));
 
-  RefPtr<JsepTrack> removedTrack = GetTrackOff(0, types.back());
-  ASSERT_TRUE(removedTrack);
-  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrack->GetStreamId(),
-                                           removedTrack->GetTrackId()));
+  JsepTrack removedTrack = RemoveTrack(*mSessionOff, 0);
+  ASSERT_FALSE(IsNull(removedTrack));
 
   CreateOffer(Some(options));
 }
 
 static const std::string strSampleCandidate =
   "a=candidate:1 1 UDP 2130706431 192.168.2.1 50005 typ host\r\n";
 
 static const unsigned short nSamplelevel = 2;
@@ -5510,19 +5681,19 @@ TEST_F(JsepSessionTest, AudioCallMismatc
   std::string active = "\r\na=setup:active";
   match = answer.find(active);
   ASSERT_NE(match, std::string::npos);
   answer.replace(match, active.length(), "\r\na=setup:passive");
   SetRemoteAnswer(answer);
 
   // This is as good as it gets in a JSEP test (w/o starting DTLS)
   ASSERT_EQ(JsepDtlsTransport::kJsepDtlsClient,
-      mSessionOff->GetTransports()[0]->mDtls->GetRole());
+      mSessionOff->GetTransceivers()[0]->mTransport->mDtls->GetRole());
   ASSERT_EQ(JsepDtlsTransport::kJsepDtlsClient,
-      mSessionAns->GetTransports()[0]->mDtls->GetRole());
+      mSessionAns->GetTransceivers()[0]->mTransport->mDtls->GetRole());
 }
 
 // Verify that missing a=setup in offer gets rejected
 TEST_F(JsepSessionTest, AudioCallOffererNoSetup)
 {
   types.push_back(SdpMediaSection::kAudio);
   AddTracks(*mSessionOff, "audio");
   AddTracks(*mSessionAns, "audio");
@@ -5562,19 +5733,19 @@ TEST_F(JsepSessionTest, AudioCallAnswerN
   match = answer.find(active);
   ASSERT_NE(match, std::string::npos);
   answer.replace(match, active.length(), "");
   SetRemoteAnswer(answer);
   ASSERT_EQ(kJsepStateStable, mSessionAns->GetState());
 
   // This is as good as it gets in a JSEP test (w/o starting DTLS)
   ASSERT_EQ(JsepDtlsTransport::kJsepDtlsServer,
-      mSessionOff->GetTransports()[0]->mDtls->GetRole());
+      mSessionOff->GetTransceivers()[0]->mTransport->mDtls->GetRole());
   ASSERT_EQ(JsepDtlsTransport::kJsepDtlsClient,
-      mSessionAns->GetTransports()[0]->mDtls->GetRole());
+      mSessionAns->GetTransceivers()[0]->mTransport->mDtls->GetRole());
 }
 
 // Verify that 'holdconn' gets rejected
 TEST_F(JsepSessionTest, AudioCallDtlsRoleHoldconn)
 {
   types.push_back(SdpMediaSection::kAudio);
   AddTracks(*mSessionOff, "audio");
   AddTracks(*mSessionAns, "audio");
@@ -5728,9 +5899,659 @@ TEST_F(JsepSessionTest, AnswerWithoutVP8
   }
 
   std::string answer = CreateAnswer();
 
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 }
 
+// Ok. Hear me out.
+// The JSEP spec specifies very different behavior for the following two cases:
+// 1. AddTrack either caused a transceiver to be created, or set the send
+// track on a preexisting transceiver.
+// 2. The transceiver was not created as a side-effect of AddTrack, and the
+// send track was put in place by some other means than AddTrack.
+//
+// All together now...
+//
+// SADFACE :(
+//
+// Ok, enough of that. The upshot is we need to test two different codepaths for
+// the same thing here. Most of this unit-test suite tests the "magic" case
+// (case 1 above). Case 2 (the non-magic case) is simpler, so we have just a
+// handful of tests.
+TEST_F(JsepSessionTest, OffererNoAddTrackMagic)
+{
+  types = BuildTypes("audio,video");
+  AddTracks(*mSessionOff, NO_ADDTRACK_MAGIC);
+  AddTracks(*mSessionAns);
+
+  // Offerer's transceivers aren't "magic"; they will not associate with the
+  // remote side's m-sections automatically. But, since they went into the
+  // offer, everything works normally.
+  OfferAnswer();
+
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
+}
+
+TEST_F(JsepSessionTest, AnswererNoAddTrackMagic)
+{
+  types = BuildTypes("audio,video");
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns, NO_ADDTRACK_MAGIC);
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
+  // Since answerer's transceivers aren't "magic", they cannot automatically be
+  // attached to the offerer's m-sections.
+  ASSERT_EQ(4U, mSessionAns->GetTransceivers().size());
+
+  SwapOfferAnswerRoles();
+
+  OfferAnswer(CHECK_SUCCESS);
+  ASSERT_EQ(4U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(4U, mSessionAns->GetTransceivers().size());
+}
+
+// JSEP has rules about when a disabled m-section can be reused; the gist is
+// that the m-section has to be negotiated disabled, then it becomes a candidate
+// for reuse on the next renegotiation. Stopping a transceiver does not allow
+// you to reuse on the next negotiation.
+TEST_F(JsepSessionTest, OffererRecycle)
+{
+  types = BuildTypes("audio,video");
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns);
+
+  OfferAnswer();
+
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
+  mSessionOff->GetTransceivers()[0]->Stop();
+  AddTracks(*mSessionOff, "audio");
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  // It is too soon to recycle msection 0, so the new track should have been
+  // given a new msection.
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[0]->GetLevel());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsStopped());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1]->IsStopped());
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->IsStopped());
+
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0]->GetLevel());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsStopped());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
+
+  UniquePtr<Sdp> offer = GetParsedLocalDescription(*mSessionOff);
+  ASSERT_EQ(3U, offer->GetMediaSectionCount());
+  ValidateDisabledMSection(&offer->GetMediaSection(0));
+
+  UniquePtr<Sdp> answer = GetParsedLocalDescription(*mSessionAns);
+  ASSERT_EQ(3U, answer->GetMediaSectionCount());
+  ValidateDisabledMSection(&answer->GetMediaSection(0));
+
+  // Ok. Now renegotiating should recycle m-section 0.
+  AddTracks(*mSessionOff, "audio");
+  ASSERT_EQ(4U, mSessionOff->GetTransceivers().size());
+  OfferAnswer(CHECK_SUCCESS);
+
+  // Transceiver 3 should now be attached to m-section 0
+  ASSERT_EQ(4U, mSessionOff->GetTransceivers().size());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->HasLevel());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsStopped());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1]->IsStopped());
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->IsStopped());
+  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[3]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[3]->IsStopped());
+
+  ASSERT_EQ(4U, mSessionAns->GetTransceivers().size());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->HasLevel());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsStopped());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[3]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[3]->IsStopped());
+}
+
+TEST_F(JsepSessionTest, RecycleAnswererStopsTransceiver)
+{
+  types = BuildTypes("audio,video");
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns);
+
+  OfferAnswer();
+
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
+  mSessionAns->GetTransceivers()[0]->Stop();
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[0]->GetLevel());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsStopped());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1]->IsStopped());
+
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0]->GetLevel());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsStopped());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
+
+  UniquePtr<Sdp> offer = GetParsedLocalDescription(*mSessionOff);
+  ASSERT_EQ(2U, offer->GetMediaSectionCount());
+
+  UniquePtr<Sdp> answer = GetParsedLocalDescription(*mSessionAns);
+  ASSERT_EQ(2U, answer->GetMediaSectionCount());
+  ValidateDisabledMSection(&answer->GetMediaSection(0));
+
+  // Renegotiating should recycle m-section 0.
+  AddTracks(*mSessionOff, "audio");
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  OfferAnswer(CHECK_SUCCESS);
+
+  // Transceiver 3 should now be attached to m-section 0
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->HasLevel());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsStopped());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1]->IsStopped());
+  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->IsStopped());
+
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->HasLevel());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsStopped());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
+}
+
+// TODO: Have a test where offerer stops, and answerer adds a track and reoffers
+// once Nils' role swap code lands.
+
+// TODO: Have a test where answerer stops and adds a track.
+
+TEST_F(JsepSessionTest, OffererRecycleNoMagic)
+{
+  types = BuildTypes("audio,video");
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns);
+
+  OfferAnswer();
+
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
+  mSessionOff->GetTransceivers()[0]->Stop();
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  // Ok. Now renegotiating should recycle m-section 0.
+  AddTracks(*mSessionOff, "audio", NO_ADDTRACK_MAGIC);
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  OfferAnswer(CHECK_SUCCESS);
+
+  // Transceiver 2 should now be attached to m-section 0
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->HasLevel());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsStopped());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1]->IsStopped());
+  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->IsStopped());
+
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->HasLevel());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsStopped());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
+}
+
+TEST_F(JsepSessionTest, OffererRecycleNoMagicAnswererStopsTransceiver)
+{
+  types = BuildTypes("audio,video");
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns);
+
+  OfferAnswer();
+
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
+  mSessionAns->GetTransceivers()[0]->Stop();
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  // Ok. Now renegotiating should recycle m-section 0.
+  AddTracks(*mSessionOff, "audio", NO_ADDTRACK_MAGIC);
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  OfferAnswer(CHECK_SUCCESS);
+
+  // Transceiver 2 should now be attached to m-section 0
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->HasLevel());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsStopped());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1]->IsStopped());
+  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->IsStopped());
+
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->HasLevel());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsStopped());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
+}
+
+TEST_F(JsepSessionTest, RecycleRollback)
+{
+  types = BuildTypes("audio,video");
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns);
+
+  OfferAnswer();
+
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
+  mSessionOff->GetTransceivers()[0]->Stop();
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  AddTracks(*mSessionOff, "audio");
+
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[0]->GetLevel());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsStopped());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->IsAssociated());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1]->IsStopped());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[1]->IsAssociated());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->HasLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->IsStopped());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->IsAssociated());
+
+  std::string offer = CreateOffer();
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->HasLevel());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsStopped());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->IsAssociated());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1]->IsStopped());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[1]->IsAssociated());
+  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->IsStopped());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->IsAssociated());
+
+  SetLocalOffer(offer, CHECK_SUCCESS);
+
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->HasLevel());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsStopped());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->IsAssociated());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1]->IsStopped());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[1]->IsAssociated());
+  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->IsStopped());
+  // This should now be associated
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[2]->IsAssociated());
+
+  ASSERT_EQ(NS_OK,
+            mSessionOff->SetLocalDescription(kJsepSdpRollback, ""));
+
+  // Rollback should not change the levels of any of these, since those are set
+  // in CreateOffer.
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->HasLevel());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsStopped());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->IsAssociated());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1]->IsStopped());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[1]->IsAssociated());
+  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->IsStopped());
+  // This should no longer be associated
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->IsAssociated());
+}
+
+TEST_F(JsepSessionTest, AddTrackMagicWithNullReplaceTrack)
+{
+  types = BuildTypes("audio,video");
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns);
+
+  OfferAnswer();
+
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
+
+  AddTracks(*mSessionAns, "audio");
+  AddTracks(*mSessionOff, "audio");
+
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsAssociated());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1]->IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2]->HasAddTrackMagic());
+
+  // Ok, transceiver 2 is "magical". Ensure it still has this "magical"
+  // auto-matching property even if we null it out with replaceTrack.
+  mSessionAns->GetTransceivers()[2]->mSendTrack.ClearTrackIds();
+  mSessionAns->GetTransceivers()[2]->mJsDirection =
+    SdpDirectionAttribute::Direction::kRecvonly;
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsAssociated());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1]->IsAssociated());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2]->IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2]->HasAddTrackMagic());
+
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[0]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->IsStopped());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsAssociated());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1]->IsStopped());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[1]->IsAssociated());
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->IsStopped());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[2]->IsAssociated());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[2]->HasAddTrackMagic());
+}
+
+// Flipside of AddTrackMagicWithNullReplaceTrack; we want to check that
+// auto-matching does not work for transceivers that were created without a
+// track, but were later given a track with replaceTrack.
+TEST_F(JsepSessionTest, NoAddTrackMagicReplaceTrack)
+{
+  types = BuildTypes("audio,video");
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns);
+
+  OfferAnswer();
+
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
+  AddTracks(*mSessionOff, "audio");
+  mSessionAns->AddTransceiver(
+      new JsepTransceiver(SdpMediaSection::MediaType::kAudio));
+
+  mSessionAns->GetTransceivers()[2]->mSendTrack.UpdateTrackIds(
+      {"newstream"}, "newtrack");
+
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsAssociated());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1]->IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->HasAddTrackMagic());
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  ASSERT_EQ(4U, mSessionAns->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsAssociated());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1]->IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->HasAddTrackMagic());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers()[3]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[3]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[3]->IsAssociated());
+}
+
+// Check that transceivers that were created without a send track, but that
+// were subsequently given a send track with addTrack, are now "magical".
+TEST_F(JsepSessionTest, AddTrackMakesTransceiverMagical)
+{
+  types = BuildTypes("audio,video");
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns);
+
+  OfferAnswer();
+
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
+  AddTracks(*mSessionOff, "audio");
+  mSessionAns->AddTransceiver(
+      new JsepTransceiver(SdpMediaSection::MediaType::kAudio));
+
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsAssociated());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1]->IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->HasAddTrackMagic());
+
+  // :D MAGIC! D:
+  AddTracks(*mSessionAns, "audio");
+
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsAssociated());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1]->IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2]->HasAddTrackMagic());
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsAssociated());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1]->IsAssociated());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2]->IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2]->HasAddTrackMagic());
+}
+
+TEST_F(JsepSessionTest, ComplicatedRemoteRollback)
+{
+  AddTracks(*mSessionOff, "audio,audio,audio,video");
+  AddTracks(*mSessionAns, "video,video");
+
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer, CHECK_SUCCESS);
+  SetRemoteOffer(offer, CHECK_SUCCESS);
+
+  // Three recvonly for audio, one sendrecv for video, and one (unmapped) for
+  // the second video track.
+  ASSERT_EQ(5U, mSessionAns->GetTransceivers().size());
+  // First video transceiver; auto matched with offer
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers()[0]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->HasAddTrackMagic());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->WasCreatedBySetRemote());
+
+  // Second video transceiver, not matched with offer
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1]->HasAddTrackMagic());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->WasCreatedBySetRemote());
+
+  // Audio transceiver, created due to application of SetRemote
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[2]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2]->IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->HasAddTrackMagic());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2]->WasCreatedBySetRemote());
+
+  // Audio transceiver, created due to application of SetRemote
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[3]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[3]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[3]->IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[3]->HasAddTrackMagic());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[3]->WasCreatedBySetRemote());
+
+  // Audio transceiver, created due to application of SetRemote
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers()[4]->GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[4]->IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[4]->IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[4]->HasAddTrackMagic());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[4]->WasCreatedBySetRemote());
+
+  // This will cause the first audio transceiver to become "magical", and
+  // thereby it will stick around after rollback, even though we clear it out
+  // with replaceTrack.
+  AddTracks(*mSessionAns, "audio");
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2]->HasAddTrackMagic());
+  mSessionAns->GetTransceivers()[2]->mSendTrack.ClearTrackIds();
+  mSessionAns->GetTransceivers()[2]->mJsDirection =
+    SdpDirectionAttribute::Direction::kRecvonly;
+
+  // We do nothing with the second audio transceiver; when we rollback, it will
+  // disappear entirely.
+
+  // This will not cause the third audio transceiver to stick around; having a
+  // track is _not_ enough to preserve it. It must have addTrack "magic"!
+  mSessionAns->GetTransceivers()[4]->mSendTrack.UpdateTrackIds(
+      {"newstream"}, "newtrack");
+
+  // Create a fourth audio transceiver. Rollback will leave it alone, since we
+  // created it.
+  mSessionAns->AddTransceiver(new JsepTransceiver(
+        SdpMediaSection::MediaType::kAudio,
+        SdpDirectionAttribute::Direction::kRecvonly));
+
+  ASSERT_EQ(NS_OK,
+            mSessionAns->SetRemoteDescription(kJsepSdpRollback, ""));
+
+  // Three recvonly for audio, one sendrecv for video, and one (unmapped) for
+  // the second video track.
+  ASSERT_EQ(4U, mSessionAns->GetTransceivers().size());
+
+  // First video transceiver
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->HasAddTrackMagic());
+  ASSERT_FALSE(IsNull(mSessionAns->GetTransceivers()[0]->mSendTrack));
+
+  // Second video transceiver
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1]->HasAddTrackMagic());
+  ASSERT_FALSE(IsNull(mSessionAns->GetTransceivers()[1]->mSendTrack));
+
+  // First audio transceiver, kept because AddTrack touched it, even though we
+  // removed the send track after.
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2]->HasAddTrackMagic());
+  ASSERT_TRUE(IsNull(mSessionAns->GetTransceivers()[2]->mSendTrack));
+
+  // Second audio transceiver should be gone.
+
+  // Third audio transceiver should also be gone.
+
+  // Fourth audio transceiver, created after SetRemote
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[3]->HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[3]->IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[3]->IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[3]->HasAddTrackMagic());
+  ASSERT_TRUE(
+      mSessionAns->GetTransceivers()[3]->mSendTrack.GetStreamIds().empty());
+}
+
+TEST_F(JsepSessionTest, LocalRollback)
+{
+  AddTracks(*mSessionOff, "audio,video");
+  AddTracks(*mSessionAns, "audio,video");
+
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer, CHECK_SUCCESS);
+
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsAssociated());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[1]->IsAssociated());
+  ASSERT_EQ(NS_OK,
+            mSessionOff->SetLocalDescription(kJsepSdpRollback, ""));
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->IsAssociated());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1]->IsAssociated());
+}
+
+TEST_F(JsepSessionTest, JsStopsTransceiverBeforeAnswer)
+{
+  AddTracks(*mSessionOff, "audio,video");
+  AddTracks(*mSessionAns, "audio,video");
+
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer, CHECK_SUCCESS);
+  SetRemoteOffer(offer, CHECK_SUCCESS);
+
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer, CHECK_SUCCESS);
+
+  // Now JS decides to stop a transceiver. Make sure transport stuff is still
+  // ready to go when the answer is set. This should only prevent the flow of
+  // media for that transceiver.
+
+  mSessionOff->GetTransceivers()[0]->Stop();
+  SetRemoteAnswer(answer, CHECK_SUCCESS);
+
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsStopped());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[0]->mTransport->mComponents);
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->mSendTrack.GetActive());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->mRecvTrack.GetActive());
+}
+
 } // namespace mozilla
+
--- a/media/webrtc/signaling/gtest/jsep_track_unittest.cpp
+++ b/media/webrtc/signaling/gtest/jsep_track_unittest.cpp
@@ -11,17 +11,22 @@
 #include "signaling/src/sdp/SipccSdp.h"
 #include "signaling/src/sdp/SdpHelper.h"
 
 namespace mozilla {
 
 class JsepTrackTest : public ::testing::Test
 {
   public:
-    JsepTrackTest() {}
+    JsepTrackTest() :
+      mSendOff(SdpMediaSection::kAudio, sdp::kSend),
+      mRecvOff(SdpMediaSection::kAudio, sdp::kRecv),
+      mSendAns(SdpMediaSection::kAudio, sdp::kSend),
+      mRecvAns(SdpMediaSection::kAudio, sdp::kRecv)
+    {}
 
     std::vector<JsepCodecDescription*>
     MakeCodecs(bool addFecCodecs = false,
                bool preferRed = false,
                bool addDtmfCodec = false) const
     {
       std::vector<JsepCodecDescription*> results;
       results.push_back(
@@ -98,138 +103,140 @@ class JsepTrackTest : public ::testing::
 
     void InitCodecs() {
       mOffCodecs.values = MakeCodecs();
       mAnsCodecs.values = MakeCodecs();
     }
 
     void InitTracks(SdpMediaSection::MediaType type)
     {
-      mSendOff = new JsepTrack(type, "stream_id", "track_id", sdp::kSend);
-      mRecvOff = new JsepTrack(type, "stream_id", "track_id", sdp::kRecv);
-      mSendOff->PopulateCodecs(mOffCodecs.values);
-      mRecvOff->PopulateCodecs(mOffCodecs.values);
+      mSendOff = JsepTrack(type, sdp::kSend);
+      if (type != SdpMediaSection::MediaType::kApplication) {
+        mSendOff.UpdateTrackIds(
+            std::vector<std::string>(1, "stream_id"), "track_id");
+      }
+      mRecvOff = JsepTrack(type, sdp::kRecv);
+      mSendOff.PopulateCodecs(mOffCodecs.values);
+      mRecvOff.PopulateCodecs(mOffCodecs.values);
 
-      mSendAns = new JsepTrack(type, "stream_id", "track_id", sdp::kSend);
-      mRecvAns = new JsepTrack(type, "stream_id", "track_id", sdp::kRecv);
-      mSendAns->PopulateCodecs(mAnsCodecs.values);
-      mRecvAns->PopulateCodecs(mAnsCodecs.values);
+      mSendAns = JsepTrack(type, sdp::kSend);
+      if (type != SdpMediaSection::MediaType::kApplication) {
+        mSendAns.UpdateTrackIds(
+            std::vector<std::string>(1, "stream_id"), "track_id");
+      }
+      mRecvAns = JsepTrack(type, sdp::kRecv);
+      mSendAns.PopulateCodecs(mAnsCodecs.values);
+      mRecvAns.PopulateCodecs(mAnsCodecs.values);
     }
 
     void InitSdp(SdpMediaSection::MediaType type)
     {
+      std::vector<std::string> msids(1, "*");
+      std::string error;
+      SdpHelper helper(&error);
+
       mOffer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, "")));
       mOffer->AddMediaSection(
           type,
-          SdpDirectionAttribute::kInactive,
+          SdpDirectionAttribute::kSendrecv,
           0,
           SdpHelper::GetProtocolForMediaType(type),
           sdp::kIPv4,
           "0.0.0.0");
+      // JsepTrack doesn't set msid-semantic
+      helper.SetupMsidSemantic(msids, mOffer.get());
+
       mAnswer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, "")));
       mAnswer->AddMediaSection(
           type,
-          SdpDirectionAttribute::kInactive,
+          SdpDirectionAttribute::kSendrecv,
           0,
           SdpHelper::GetProtocolForMediaType(type),
           sdp::kIPv4,
           "0.0.0.0");
+      // JsepTrack doesn't set msid-semantic
+      helper.SetupMsidSemantic(msids, mAnswer.get());
     }
 
     SdpMediaSection& GetOffer()
     {
       return mOffer->GetMediaSection(0);
     }
 
     SdpMediaSection& GetAnswer()
     {
       return mAnswer->GetMediaSection(0);
     }
 
     void CreateOffer()
     {
-      if (mSendOff) {
-        mSendOff->AddToOffer(&GetOffer());
-      }
-
-      if (mRecvOff) {
-        mRecvOff->AddToOffer(&GetOffer());
-      }
+      mSendOff.AddToOffer(mSsrcGenerator, &GetOffer());
+      mRecvOff.AddToOffer(mSsrcGenerator, &GetOffer());
     }
 
     void CreateAnswer()
     {
-      if (mSendAns && GetOffer().IsReceiving()) {
-        mSendAns->AddToAnswer(GetOffer(), &GetAnswer());
+      if (mRecvAns.GetMediaType() != SdpMediaSection::MediaType::kApplication) {
+        mRecvAns.UpdateRecvTrack(*mOffer, GetOffer());
       }
 
-      if (mRecvAns && GetOffer().IsSending()) {
-        mRecvAns->AddToAnswer(GetOffer(), &GetAnswer());
-      }
+      mSendAns.AddToAnswer(GetOffer(), mSsrcGenerator, &GetAnswer());
+      mRecvAns.AddToAnswer(GetOffer(), mSsrcGenerator, &GetAnswer());
     }
 
     void Negotiate()
     {
       std::cerr << "Offer SDP: " << std::endl;
       mOffer->Serialize(std::cerr);
 
       std::cerr << "Answer SDP: " << std::endl;
       mAnswer->Serialize(std::cerr);
 
-      if (mSendAns && GetAnswer().IsSending()) {
-        mSendAns->Negotiate(GetAnswer(), GetOffer());
+      if (mRecvOff.GetMediaType() != SdpMediaSection::MediaType::kApplication) {
+        mRecvOff.UpdateRecvTrack(*mAnswer, GetAnswer());
       }
 
-      if (mRecvAns && GetAnswer().IsReceiving()) {
-        mRecvAns->Negotiate(GetAnswer(), GetOffer());
+      if (GetAnswer().IsSending()) {
+        mSendAns.Negotiate(GetAnswer(), GetOffer());
+        mRecvOff.Negotiate(GetAnswer(), GetAnswer());
       }
 
-      if (mSendOff && GetAnswer().IsReceiving()) {
-        mSendOff->Negotiate(GetAnswer(), GetAnswer());
-      }
-
-      if (mRecvOff && GetAnswer().IsSending()) {
-        mRecvOff->Negotiate(GetAnswer(), GetAnswer());
+      if (GetAnswer().IsReceiving()) {
+        mRecvAns.Negotiate(GetAnswer(), GetOffer());
+        mSendOff.Negotiate(GetAnswer(), GetAnswer());
       }
     }
 
     void OfferAnswer()
     {
       CreateOffer();
       CreateAnswer();
       Negotiate();
       SanityCheck();
     }
 
-    static size_t EncodingCount(const RefPtr<JsepTrack>& track)
-    {
-      return track->GetNegotiatedDetails()->GetEncodingCount();
-    }
-
     // TODO: Look into writing a macro that wraps an ASSERT_ and returns false
     // if it fails (probably requires writing a bool-returning function that
     // takes a void-returning lambda with a bool outparam, which will in turn
     // invokes the ASSERT_)
     static void CheckEncodingCount(size_t expected,
-                                   const RefPtr<JsepTrack>& send,
-                                   const RefPtr<JsepTrack>& recv)
+                                   const JsepTrack& send,
+                                   const JsepTrack& recv)
     {
       if (expected) {
-        ASSERT_TRUE(!!send);
-        ASSERT_TRUE(send->GetNegotiatedDetails());
-        ASSERT_TRUE(!!recv);
-        ASSERT_TRUE(recv->GetNegotiatedDetails());
+        ASSERT_TRUE(send.GetNegotiatedDetails());
+        ASSERT_TRUE(recv.GetNegotiatedDetails());
       }
 
-      if (send && send->GetNegotiatedDetails()) {
-        ASSERT_EQ(expected, send->GetNegotiatedDetails()->GetEncodingCount());
+      if (!send.GetTrackId().empty() && send.GetNegotiatedDetails()) {
+        ASSERT_EQ(expected, send.GetNegotiatedDetails()->GetEncodingCount());
       }
 
-      if (recv && recv->GetNegotiatedDetails()) {
-        ASSERT_EQ(expected, recv->GetNegotiatedDetails()->GetEncodingCount());
+      if (!recv.GetTrackId().empty() && recv.GetNegotiatedDetails()) {
+        ASSERT_EQ(expected, recv.GetNegotiatedDetails()->GetEncodingCount());
       }
     }
 
     void CheckOffEncodingCount(size_t expected) const
     {
       CheckEncodingCount(expected, mSendOff, mRecvAns);
     }
 
@@ -309,16 +316,17 @@ class JsepTrackTest : public ::testing::
 
     void SanityCheckCodecs(const JsepCodecDescription& a,
                            const JsepCodecDescription& b) const
     {
       ASSERT_EQ(a.mType, b.mType);
       if (a.mType != SdpMediaSection::kApplication) {
         ASSERT_EQ(a.mDefaultPt, b.mDefaultPt);
       }
+      std::cerr << a.mName << " vs " << b.mName << std::endl;
       ASSERT_EQ(a.mName, b.mName);
       ASSERT_EQ(a.mClock, b.mClock);
       ASSERT_EQ(a.mChannels, b.mChannels);
       ASSERT_NE(a.mDirection, b.mDirection);
       // These constraints are for fmtp and rid, which _are_ signaled
       ASSERT_EQ(a.mConstraints, b.mConstraints);
 
       if (a.mType == SdpMediaSection::kVideo) {
@@ -360,48 +368,45 @@ class JsepTrackTest : public ::testing::
       if (!a.GetNegotiatedDetails()) {
         ASSERT_FALSE(!!b.GetNegotiatedDetails());
         return;
       }
 
       ASSERT_TRUE(!!a.GetNegotiatedDetails());
       ASSERT_TRUE(!!b.GetNegotiatedDetails());
       ASSERT_EQ(a.GetMediaType(), b.GetMediaType());
-      ASSERT_EQ(a.GetStreamId(), b.GetStreamId());
+      ASSERT_EQ(a.GetStreamIds(), b.GetStreamIds());
       ASSERT_EQ(a.GetTrackId(), b.GetTrackId());
       ASSERT_EQ(a.GetCNAME(), b.GetCNAME());
       ASSERT_NE(a.GetDirection(), b.GetDirection());
       ASSERT_EQ(a.GetSsrcs().size(), b.GetSsrcs().size());
       for (size_t i = 0; i < a.GetSsrcs().size(); ++i) {
         ASSERT_EQ(a.GetSsrcs()[i], b.GetSsrcs()[i]);
       }
 
       SanityCheckNegotiatedDetails(*a.GetNegotiatedDetails(),
                                    *b.GetNegotiatedDetails());
     }
 
     void SanityCheck() const
     {
-      if (mSendOff && mRecvAns) {
-        SanityCheckTracks(*mSendOff, *mRecvAns);
-      }
-      if (mRecvOff && mSendAns) {
-        SanityCheckTracks(*mRecvOff, *mSendAns);
-      }
+      SanityCheckTracks(mSendOff, mRecvAns);
+      SanityCheckTracks(mRecvOff, mSendAns);
     }
 
   protected:
-    RefPtr<JsepTrack> mSendOff;
-    RefPtr<JsepTrack> mRecvOff;
-    RefPtr<JsepTrack> mSendAns;
-    RefPtr<JsepTrack> mRecvAns;
+    JsepTrack mSendOff;
+    JsepTrack mRecvOff;
+    JsepTrack mSendAns;
+    JsepTrack mRecvAns;
     PtrVector<JsepCodecDescription> mOffCodecs;
     PtrVector<JsepCodecDescription> mAnsCodecs;
     UniquePtr<Sdp> mOffer;
     UniquePtr<Sdp> mAnswer;
+    SsrcGenerator mSsrcGenerator;
 };
 
 TEST_F(JsepTrackTest, CreateDestroy)
 {
   Init(SdpMediaSection::kAudio);
 }
 
 TEST_F(JsepTrackTest, AudioNegotiation)
@@ -440,30 +445,28 @@ private:
 };
 
 TEST_F(JsepTrackTest, CheckForMismatchedAudioCodecAndVideoTrack)
 {
   PtrVector<JsepCodecDescription> offerCodecs;
 
   // make codecs including telephone-event (an audio codec)
   offerCodecs.values = MakeCodecs(false, false, true);
-  RefPtr<JsepTrack> videoTrack = new JsepTrack(SdpMediaSection::kVideo,
-                                               "stream_id",
-                                               "track_id",
-                                               sdp::kSend);
+  JsepTrack videoTrack(SdpMediaSection::kVideo, sdp::kSend);
+  videoTrack.UpdateTrackIds(std::vector<std::string>(1, "stream_id"), "track_id");
   // populate codecs and then make sure we don't have any audio codecs
   // in the video track
-  videoTrack->PopulateCodecs(offerCodecs.values);
+  videoTrack.PopulateCodecs(offerCodecs.values);
 
   bool found = false;
-  videoTrack->ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
+  videoTrack.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
   ASSERT_FALSE(found);
 
   found = false;
-  videoTrack->ForEachCodec(CheckForCodecType(SdpMediaSection::kVideo, &found));
+  videoTrack.ForEachCodec(CheckForCodecType(SdpMediaSection::kVideo, &found));
   ASSERT_TRUE(found); // for sanity, make sure we did find video codecs
 }
 
 TEST_F(JsepTrackTest, CheckVideoTrackWithHackedDtmfSdp)
 {
   Init(SdpMediaSection::kVideo);
   CreateOffer();
   // make sure we don't find sdp containing telephone-event in video track
@@ -486,31 +489,26 @@ TEST_F(JsepTrackTest, CheckVideoTrackWit
             std::string::npos);
 
   Negotiate();
   SanityCheck();
 
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(1);
 
-  ASSERT_TRUE(mSendOff.get());
-  ASSERT_TRUE(mRecvOff.get());
-  ASSERT_TRUE(mSendAns.get());
-  ASSERT_TRUE(mRecvAns.get());
-
   // make sure we still don't find any audio codecs in the video track after
   // hacking the sdp
   bool found = false;
-  mSendOff->ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
+  mSendOff.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
   ASSERT_FALSE(found);
-  mRecvOff->ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
+  mRecvOff.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
   ASSERT_FALSE(found);
-  mSendAns->ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
+  mSendAns.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
   ASSERT_FALSE(found);
-  mRecvAns->ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
+  mRecvAns.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
   ASSERT_FALSE(found);
 }
 
 TEST_F(JsepTrackTest, AudioNegotiationOffererDtmf)
 {
   mOffCodecs.values = MakeCodecs(false, false, true);
   mAnsCodecs.values = MakeCodecs(false, false, false);
 
@@ -525,23 +523,23 @@ TEST_F(JsepTrackTest, AudioNegotiationOf
             std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
             std::string::npos);
 
   ASSERT_NE(mOffer->ToString().find("a=fmtp:101 0-15"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=fmtp:101"), std::string::npos);
 
   const JsepAudioCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns)));
   ASSERT_EQ("1", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, AudioNegotiationAnswererDtmf)
 {
   mOffCodecs.values = MakeCodecs(false, false, false);
   mAnsCodecs.values = MakeCodecs(false, false, true);
 
@@ -556,23 +554,23 @@ TEST_F(JsepTrackTest, AudioNegotiationAn
             std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
             std::string::npos);
 
   ASSERT_EQ(mOffer->ToString().find("a=fmtp:101 0-15"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=fmtp:101"), std::string::npos);
 
   const JsepAudioCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns)));
   ASSERT_EQ("1", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, AudioNegotiationOffererAnswererDtmf)
 {
   mOffCodecs.values = MakeCodecs(false, false, true);
   mAnsCodecs.values = MakeCodecs(false, false, true);
 
@@ -587,32 +585,32 @@ TEST_F(JsepTrackTest, AudioNegotiationOf
             std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
             std::string::npos);
 
   ASSERT_NE(mOffer->ToString().find("a=fmtp:101 0-15"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=fmtp:101 0-15"), std::string::npos);
 
   const JsepAudioCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
 
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererNoFmtpAnswererFmtp)
 {
   mOffCodecs.values = MakeCodecs(false, false, true);
   mAnsCodecs.values = MakeCodecs(false, false, true);
 
@@ -634,32 +632,32 @@ TEST_F(JsepTrackTest, AudioNegotiationDt
             std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
             std::string::npos);
 
   ASSERT_EQ(mOffer->ToString().find("a=fmtp:101"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=fmtp:101 0-15"), std::string::npos);
 
   const JsepAudioCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
 
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererFmtpAnswererNoFmtp)
 {
   mOffCodecs.values = MakeCodecs(false, false, true);
   mAnsCodecs.values = MakeCodecs(false, false, true);
 
@@ -681,32 +679,32 @@ TEST_F(JsepTrackTest, AudioNegotiationDt
             std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
             std::string::npos);
 
   ASSERT_NE(mOffer->ToString().find("a=fmtp:101 0-15"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=fmtp:101"), std::string::npos);
 
   const JsepAudioCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
 
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererNoFmtpAnswererNoFmtp)
 {
   mOffCodecs.values = MakeCodecs(false, false, true);
   mAnsCodecs.values = MakeCodecs(false, false, true);
 
@@ -729,32 +727,32 @@ TEST_F(JsepTrackTest, AudioNegotiationDt
             std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
             std::string::npos);
 
   ASSERT_EQ(mOffer->ToString().find("a=fmtp:101"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=fmtp:101"), std::string::npos);
 
   const JsepAudioCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
 
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, VideoNegotationOffererFEC)
 {
   mOffCodecs.values = MakeCodecs(true);
   mAnsCodecs.values = MakeCodecs(false);
 
@@ -769,23 +767,23 @@ TEST_F(JsepTrackTest, VideoNegotationOff
   ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
 
   ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=fmtp:122"), std::string::npos);
 
   const JsepVideoCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetVideoCodec(*mSendOff)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendOff)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvOff)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvOff)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mSendAns)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendAns)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvAns)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvAns)));
   ASSERT_EQ("120", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, VideoNegotationAnswererFEC)
 {
   mOffCodecs.values = MakeCodecs(false);
   mAnsCodecs.values = MakeCodecs(true);
 
@@ -800,23 +798,23 @@ TEST_F(JsepTrackTest, VideoNegotationAns
   ASSERT_EQ(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
 
   ASSERT_EQ(mOffer->ToString().find("a=fmtp:122"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=fmtp:122"), std::string::npos);
 
   const JsepVideoCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetVideoCodec(*mSendOff)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendOff)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvOff)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvOff)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mSendAns)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendAns)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvAns)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvAns)));
   ASSERT_EQ("120", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFEC)
 {
   mOffCodecs.values = MakeCodecs(true);
   mAnsCodecs.values = MakeCodecs(true);
 
@@ -831,23 +829,23 @@ TEST_F(JsepTrackTest, VideoNegotationOff
   ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
 
   ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
 
   const JsepVideoCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetVideoCodec(*mSendOff, 4)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendOff, 4)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvOff, 4)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 4)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mSendAns, 4)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendAns, 4)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvAns, 4)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 4)));
   ASSERT_EQ("120", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECPreferred)
 {
   mOffCodecs.values = MakeCodecs(true, true);
   mAnsCodecs.values = MakeCodecs(true);
 
@@ -862,23 +860,23 @@ TEST_F(JsepTrackTest, VideoNegotationOff
   ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
 
   ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
 
   const JsepVideoCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetVideoCodec(*mSendOff, 4)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendOff, 4)));
   ASSERT_EQ("122", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvOff, 4)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 4)));
   ASSERT_EQ("122", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mSendAns, 4)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendAns, 4)));
   ASSERT_EQ("122", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvAns, 4)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 4)));
   ASSERT_EQ("122", track->mDefaultPt);
 }
 
 // Make sure we only put the right things in the fmtp:122 120/.... line
 TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECMismatch)
 {
   mOffCodecs.values = MakeCodecs(true, true);
   mAnsCodecs.values = MakeCodecs(true);
@@ -897,23 +895,23 @@ TEST_F(JsepTrackTest, VideoNegotationOff
   ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
 
   ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=fmtp:122 120/123"), std::string::npos);
 
   const JsepVideoCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetVideoCodec(*mSendOff, 3)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendOff, 3)));
   ASSERT_EQ("122", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvOff, 3)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 3)));
   ASSERT_EQ("122", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mSendAns, 3)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendAns, 3)));
   ASSERT_EQ("122", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvAns, 3)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 3)));
   ASSERT_EQ("122", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECZeroVP9Codec)
 {
   mOffCodecs.values = MakeCodecs(true);
   JsepVideoCodecDescription* vp9 =
     new JsepVideoCodecDescription("0", "VP9", 90000);
@@ -959,21 +957,21 @@ TEST_F(JsepTrackTest, VideoNegotiationOf
   // make sure REMB is on offer and not on answer
   ASSERT_NE(mOffer->ToString().find("a=rtcp-fb:120 goog-remb"),
             std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=rtcp-fb:120 goog-remb"),
             std::string::npos);
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(1);
 
-  CheckOtherFbsSize(*mSendOff, 0);
-  CheckOtherFbsSize(*mRecvAns, 0);
+  CheckOtherFbsSize(mSendOff, 0);
+  CheckOtherFbsSize(mRecvAns, 0);
 
-  CheckOtherFbsSize(*mSendAns, 0);
-  CheckOtherFbsSize(*mRecvOff, 0);
+  CheckOtherFbsSize(mSendAns, 0);
+  CheckOtherFbsSize(mRecvOff, 0);
 }
 
 TEST_F(JsepTrackTest, VideoNegotiationAnswerRemb)
 {
   InitCodecs();
   // enable remb on the answer codecs
   ((JsepVideoCodecDescription*)mAnsCodecs.values[2])->EnableRemb();
   InitTracks(SdpMediaSection::kVideo);
@@ -983,21 +981,21 @@ TEST_F(JsepTrackTest, VideoNegotiationAn
   // make sure REMB is not on offer and not on answer
   ASSERT_EQ(mOffer->ToString().find("a=rtcp-fb:120 goog-remb"),
             std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=rtcp-fb:120 goog-remb"),
             std::string::npos);
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(1);
 
-  CheckOtherFbsSize(*mSendOff, 0);
-  CheckOtherFbsSize(*mRecvAns, 0);
+  CheckOtherFbsSize(mSendOff, 0);
+  CheckOtherFbsSize(mRecvAns, 0);
 
-  CheckOtherFbsSize(*mSendAns, 0);
-  CheckOtherFbsSize(*mRecvOff, 0);
+  CheckOtherFbsSize(mSendAns, 0);
+  CheckOtherFbsSize(mRecvOff, 0);
 }
 
 TEST_F(JsepTrackTest, VideoNegotiationOfferAnswerRemb)
 {
   InitCodecs();
   // enable remb on the offer and answer codecs
   ((JsepVideoCodecDescription*)mOffCodecs.values[2])->EnableRemb();
   ((JsepVideoCodecDescription*)mAnsCodecs.values[2])->EnableRemb();
@@ -1008,96 +1006,98 @@ TEST_F(JsepTrackTest, VideoNegotiationOf
   // make sure REMB is on offer and on answer
   ASSERT_NE(mOffer->ToString().find("a=rtcp-fb:120 goog-remb"),
             std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtcp-fb:120 goog-remb"),
             std::string::npos);
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(1);
 
-  CheckOtherFbsSize(*mSendOff, 1);
-  CheckOtherFbsSize(*mRecvAns, 1);
-  CheckOtherFbExists(*mSendOff, SdpRtcpFbAttributeList::kRemb);
-  CheckOtherFbExists(*mRecvAns, SdpRtcpFbAttributeList::kRemb);
+  CheckOtherFbsSize(mSendOff, 1);
+  CheckOtherFbsSize(mRecvAns, 1);
+  CheckOtherFbExists(mSendOff, SdpRtcpFbAttributeList::kRemb);
+  CheckOtherFbExists(mRecvAns, SdpRtcpFbAttributeList::kRemb);
 
-  CheckOtherFbsSize(*mSendAns, 1);
-  CheckOtherFbsSize(*mRecvOff, 1);
-  CheckOtherFbExists(*mSendAns, SdpRtcpFbAttributeList::kRemb);
-  CheckOtherFbExists(*mRecvOff, SdpRtcpFbAttributeList::kRemb);
+  CheckOtherFbsSize(mSendAns, 1);
+  CheckOtherFbsSize(mRecvOff, 1);
+  CheckOtherFbExists(mSendAns, SdpRtcpFbAttributeList::kRemb);
+  CheckOtherFbExists(mRecvOff, SdpRtcpFbAttributeList::kRemb);
 }
 
 TEST_F(JsepTrackTest, AudioOffSendonlyAnsRecvonly)
 {
   Init(SdpMediaSection::kAudio);
-  mRecvOff = nullptr;
-  mSendAns = nullptr;
+  GetOffer().SetDirection(SdpDirectionAttribute::kSendonly);
+  GetAnswer().SetDirection(SdpDirectionAttribute::kRecvonly);
   OfferAnswer();
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(0);
 }
 
 TEST_F(JsepTrackTest, VideoOffSendonlyAnsRecvonly)
 {
   Init(SdpMediaSection::kVideo);
-  mRecvOff = nullptr;
-  mSendAns = nullptr;
+  GetOffer().SetDirection(SdpDirectionAttribute::kSendonly);
+  GetAnswer().SetDirection(SdpDirectionAttribute::kRecvonly);
   OfferAnswer();
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(0);
 }
 
 TEST_F(JsepTrackTest, AudioOffSendrecvAnsRecvonly)
 {
   Init(SdpMediaSection::kAudio);
-  mSendAns = nullptr;
+  GetAnswer().SetDirection(SdpDirectionAttribute::kRecvonly);
   OfferAnswer();
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(0);
 }
 
 TEST_F(JsepTrackTest, VideoOffSendrecvAnsRecvonly)
 {
   Init(SdpMediaSection::kVideo);
-  mSendAns = nullptr;
+  GetAnswer().SetDirection(SdpDirectionAttribute::kRecvonly);
   OfferAnswer();
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(0);
 }
 
-TEST_F(JsepTrackTest, AudioOffRecvonlyAnsSendrecv)
+TEST_F(JsepTrackTest, AudioOffRecvonlyAnsSendonly)
 {
   Init(SdpMediaSection::kAudio);
-  mSendOff = nullptr;
+  GetOffer().SetDirection(SdpDirectionAttribute::kRecvonly);
+  GetAnswer().SetDirection(SdpDirectionAttribute::kSendonly);
   OfferAnswer();
   CheckOffEncodingCount(0);
   CheckAnsEncodingCount(1);
 }
 
-TEST_F(JsepTrackTest, VideoOffRecvonlyAnsSendrecv)
+TEST_F(JsepTrackTest, VideoOffRecvonlyAnsSendonly)
 {
   Init(SdpMediaSection::kVideo);
-  mSendOff = nullptr;
+  GetOffer().SetDirection(SdpDirectionAttribute::kRecvonly);
+  GetAnswer().SetDirection(SdpDirectionAttribute::kSendonly);
   OfferAnswer();
   CheckOffEncodingCount(0);
   CheckAnsEncodingCount(1);
 }
 
 TEST_F(JsepTrackTest, AudioOffSendrecvAnsSendonly)
 {
   Init(SdpMediaSection::kAudio);
-  mRecvAns = nullptr;
+  GetAnswer().SetDirection(SdpDirectionAttribute::kSendonly);
   OfferAnswer();
   CheckOffEncodingCount(0);
   CheckAnsEncodingCount(1);
 }
 
 TEST_F(JsepTrackTest, VideoOffSendrecvAnsSendonly)
 {
   Init(SdpMediaSection::kVideo);
-  mRecvAns = nullptr;
+  GetAnswer().SetDirection(SdpDirectionAttribute::kSendonly);
   OfferAnswer();
   CheckOffEncodingCount(0);
   CheckAnsEncodingCount(1);
 }
 
 TEST_F(JsepTrackTest, DataChannelDraft05)
 {
   Init(SdpMediaSection::kApplication);
@@ -1156,25 +1156,25 @@ TEST_F(JsepTrackTest, DataChannelDraft21
 {
   mOffCodecs.values = MakeCodecs(false, false, false);
   mAnsCodecs.values = MakeCodecs(false, false, false);
   InitTracks(SdpMediaSection::kApplication);
 
   mOffer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, "")));
   mOffer->AddMediaSection(
       SdpMediaSection::kApplication,
-      SdpDirectionAttribute::kInactive,
+      SdpDirectionAttribute::kSendrecv,
       0,
       SdpMediaSection::kUdpDtlsSctp,
       sdp::kIPv4,
       "0.0.0.0");
   mAnswer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, "")));
   mAnswer->AddMediaSection(
       SdpMediaSection::kApplication,
-      SdpDirectionAttribute::kInactive,
+      SdpDirectionAttribute::kSendrecv,
       0,
       SdpMediaSection::kUdpDtlsSctp,
       sdp::kIPv4,
       "0.0.0.0");
 
   OfferAnswer();
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(1);
@@ -1197,84 +1197,86 @@ MakeConstraints(const std::string& rid, 
 }
 
 TEST_F(JsepTrackTest, SimulcastRejected)
 {
   Init(SdpMediaSection::kVideo);
   std::vector<JsepTrack::JsConstraints> constraints;
   constraints.push_back(MakeConstraints("foo", 40000));
   constraints.push_back(MakeConstraints("bar", 10000));
-  mSendOff->SetJsConstraints(constraints);
+  mSendOff.SetJsConstraints(constraints);
   OfferAnswer();
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(1);
 }
 
 TEST_F(JsepTrackTest, SimulcastPrevented)
 {
   Init(SdpMediaSection::kVideo);
   std::vector<JsepTrack::JsConstraints> constraints;
   constraints.push_back(MakeConstraints("foo", 40000));
   constraints.push_back(MakeConstraints("bar", 10000));
-  mSendAns->SetJsConstraints(constraints);
+  mSendAns.SetJsConstraints(constraints);
   OfferAnswer();
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(1);
 }
 
 TEST_F(JsepTrackTest, SimulcastOfferer)
 {
   Init(SdpMediaSection::kVideo);
   std::vector<JsepTrack::JsConstraints> constraints;
   constraints.push_back(MakeConstraints("foo", 40000));
   constraints.push_back(MakeConstraints("bar", 10000));
-  mSendOff->SetJsConstraints(constraints);
+  mSendOff.SetJsConstraints(constraints);
   CreateOffer();
   CreateAnswer();
   // Add simulcast/rid to answer
-  JsepTrack::AddToMsection(constraints, sdp::kRecv, &GetAnswer());
+  mRecvAns.AddToMsection(
+      constraints, sdp::kRecv, mSsrcGenerator, &GetAnswer());
   Negotiate();
-  ASSERT_TRUE(mSendOff->GetNegotiatedDetails());
-  ASSERT_EQ(2U, mSendOff->GetNegotiatedDetails()->GetEncodingCount());
-  ASSERT_EQ("foo", mSendOff->GetNegotiatedDetails()->GetEncoding(0).mRid);
+  ASSERT_TRUE(mSendOff.GetNegotiatedDetails());
+  ASSERT_EQ(2U, mSendOff.GetNegotiatedDetails()->GetEncodingCount());
+  ASSERT_EQ("foo", mSendOff.GetNegotiatedDetails()->GetEncoding(0).mRid);
   ASSERT_EQ(40000U,
-      mSendOff->GetNegotiatedDetails()->GetEncoding(0).mConstraints.maxBr);
-  ASSERT_EQ("bar", mSendOff->GetNegotiatedDetails()->GetEncoding(1).mRid);
+      mSendOff.GetNegotiatedDetails()->GetEncoding(0).mConstraints.maxBr);
+  ASSERT_EQ("bar", mSendOff.GetNegotiatedDetails()->GetEncoding(1).mRid);
   ASSERT_EQ(10000U,
-      mSendOff->GetNegotiatedDetails()->GetEncoding(1).mConstraints.maxBr);
+      mSendOff.GetNegotiatedDetails()->GetEncoding(1).mConstraints.maxBr);
   ASSERT_NE(std::string::npos,
             mOffer->ToString().find("a=simulcast: send rid=foo;bar"));
   ASSERT_NE(std::string::npos,
             mAnswer->ToString().find("a=simulcast: recv rid=foo;bar"));
   ASSERT_NE(std::string::npos, mOffer->ToString().find("a=rid:foo send"));
   ASSERT_NE(std::string::npos, mOffer->ToString().find("a=rid:bar send"));
   ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=rid:foo recv"));
   ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=rid:bar recv"));
 }
 
 TEST_F(JsepTrackTest, SimulcastAnswerer)
 {
   Init(SdpMediaSection::kVideo);
   std::vector<JsepTrack::JsConstraints> constraints;
   constraints.push_back(MakeConstraints("foo", 40000));
   constraints.push_back(MakeConstraints("bar", 10000));
-  mSendAns->SetJsConstraints(constraints);
+  mSendAns.SetJsConstraints(constraints);
   CreateOffer();
   // Add simulcast/rid to offer
-  JsepTrack::AddToMsection(constraints, sdp::kRecv, &GetOffer());
+  mRecvOff.AddToMsection(
+      constraints, sdp::kRecv, mSsrcGenerator, &GetOffer());
   CreateAnswer();
   Negotiate();
-  ASSERT_TRUE(mSendAns->GetNegotiatedDetails());
-  ASSERT_EQ(2U, mSendAns->GetNegotiatedDetails()->GetEncodingCount());
-  ASSERT_EQ("foo", mSendAns->GetNegotiatedDetails()->GetEncoding(0).mRid);
+  ASSERT_TRUE(mSendAns.GetNegotiatedDetails());
+  ASSERT_EQ(2U, mSendAns.GetNegotiatedDetails()->GetEncodingCount());
+  ASSERT_EQ("foo", mSendAns.GetNegotiatedDetails()->GetEncoding(0).mRid);
   ASSERT_EQ(40000U,
-      mSendAns->GetNegotiatedDetails()->GetEncoding(0).mConstraints.maxBr);
-  ASSERT_EQ("bar", mSendAns->GetNegotiatedDetails()->GetEncoding(1).mRid);
+      mSendAns.GetNegotiatedDetails()->GetEncoding(0).mConstraints.maxBr);
+  ASSERT_EQ("bar", mSendAns.GetNegotiatedDetails()->GetEncoding(1).mRid);
   ASSERT_EQ(10000U,
-      mSendAns->GetNegotiatedDetails()->GetEncoding(1).mConstraints.maxBr);
+      mSendAns.GetNegotiatedDetails()->GetEncoding(1).mConstraints.maxBr);
   ASSERT_NE(std::string::npos,
             mOffer->ToString().find("a=simulcast: recv rid=foo;bar"));
   ASSERT_NE(std::string::npos,
             mAnswer->ToString().find("a=simulcast: send rid=foo;bar"));
   ASSERT_NE(std::string::npos, mOffer->ToString().find("a=rid:foo recv"));
   ASSERT_NE(std::string::npos, mOffer->ToString().find("a=rid:bar recv"));
   ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=rid:foo send"));
   ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=rid:bar send"));
@@ -1309,24 +1311,24 @@ TEST_F(JsepTrackTest, SimulcastAnswerer)
   };  \
 }
 
 TEST_F(JsepTrackTest, DefaultOpusParameters)
 {
   Init(SdpMediaSection::kAudio);
   OfferAnswer();
 
-  VERIFY_OPUS_MAX_PLAYBACK_RATE(*mSendOff,
+  VERIFY_OPUS_MAX_PLAYBACK_RATE(mSendOff,
       SdpFmtpAttributeList::OpusParameters::kDefaultMaxPlaybackRate);
-  VERIFY_OPUS_MAX_PLAYBACK_RATE(*mSendAns,
+  VERIFY_OPUS_MAX_PLAYBACK_RATE(mSendAns,
       SdpFmtpAttributeList::OpusParameters::kDefaultMaxPlaybackRate);
-  VERIFY_OPUS_MAX_PLAYBACK_RATE(*mRecvOff, 0U);
-  VERIFY_OPUS_FORCE_MONO(*mRecvOff, false);
-  VERIFY_OPUS_MAX_PLAYBACK_RATE(*mRecvAns, 0U);
-  VERIFY_OPUS_FORCE_MONO(*mRecvAns, false);
+  VERIFY_OPUS_MAX_PLAYBACK_RATE(mRecvOff, 0U);
+  VERIFY_OPUS_FORCE_MONO(mRecvOff, false);
+  VERIFY_OPUS_MAX_PLAYBACK_RATE(mRecvAns, 0U);
+  VERIFY_OPUS_FORCE_MONO(mRecvAns, false);
 }
 
 TEST_F(JsepTrackTest, NonDefaultOpusParameters)
 {
   InitCodecs();
   for (auto& codec : mAnsCodecs.values) {
     if (codec->mName == "opus") {
       JsepAudioCodecDescription* audioCodec =
@@ -1334,20 +1336,20 @@ TEST_F(JsepTrackTest, NonDefaultOpusPara
       audioCodec->mMaxPlaybackRate = 16000;
       audioCodec->mForceMono = true;
     }
   }
   InitTracks(SdpMediaSection::kAudio);
   InitSdp(SdpMediaSection::kAudio);
   OfferAnswer();
 
-  VERIFY_OPUS_MAX_PLAYBACK_RATE(*mSendOff, 16000U);
-  VERIFY_OPUS_FORCE_MONO(*mSendOff, true);
-  VERIFY_OPUS_MAX_PLAYBACK_RATE(*mSendAns,
+  VERIFY_OPUS_MAX_PLAYBACK_RATE(mSendOff, 16000U);
+  VERIFY_OPUS_FORCE_MONO(mSendOff, true);
+  VERIFY_OPUS_MAX_PLAYBACK_RATE(mSendAns,
       SdpFmtpAttributeList::OpusParameters::kDefaultMaxPlaybackRate);
-  VERIFY_OPUS_FORCE_MONO(*mSendAns, false);
-  VERIFY_OPUS_MAX_PLAYBACK_RATE(*mRecvOff, 0U);
-  VERIFY_OPUS_FORCE_MONO(*mRecvOff, false);
-  VERIFY_OPUS_MAX_PLAYBACK_RATE(*mRecvAns, 16000U);
-  VERIFY_OPUS_FORCE_MONO(*mRecvAns, true);
+  VERIFY_OPUS_FORCE_MONO(mSendAns, false);
+  VERIFY_OPUS_MAX_PLAYBACK_RATE(mRecvOff, 0U);
+  VERIFY_OPUS_FORCE_MONO(mRecvOff, false);
+  VERIFY_OPUS_MAX_PLAYBACK_RATE(mRecvAns, 16000U);
+  VERIFY_OPUS_FORCE_MONO(mRecvAns, true);
 }
 
 } // namespace mozilla
--- a/media/webrtc/signaling/src/jsep/JsepSession.h
+++ b/media/webrtc/signaling/src/jsep/JsepSession.h
@@ -10,23 +10,22 @@
 #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"
 
-#include "JsepTrack.h"
+#include "signaling/src/jsep/JsepTransceiver.h"
 
 namespace mozilla {
 
 // Forward declarations
 class JsepCodecDescription;
-class JsepTrack;
 
 enum JsepSignalingState {
   kJsepStateStable,
   kJsepStateHaveLocalOffer,
   kJsepStateHaveRemoteOffer,
   kJsepStateHaveLocalPranswer,
   kJsepStateHaveRemotePranswer,
   kJsepStateClosed
@@ -112,67 +111,40 @@ public:
   // 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;
 
   template <class UnaryFunction>
   void ForEachCodec(UnaryFunction& function)
   {
     std::for_each(Codecs().begin(), Codecs().end(), function);
-    for (RefPtr<JsepTrack>& track : GetLocalTracks()) {
-      track->ForEachCodec(function);
-    }
-    for (RefPtr<JsepTrack>& track : GetRemoteTracks()) {
-      track->ForEachCodec(function);
+    for (auto& transceiver : GetTransceivers()) {
+      transceiver->mSendTrack.ForEachCodec(function);
+      transceiver->mRecvTrack.ForEachCodec(function);
     }
   }
 
   template <class BinaryPredicate>
   void SortCodecs(BinaryPredicate& sorter)
   {
     std::stable_sort(Codecs().begin(), Codecs().end(), sorter);
-    for (RefPtr<JsepTrack>& track : GetLocalTracks()) {
-      track->SortCodecs(sorter);
-    }
-    for (RefPtr<JsepTrack>& track : GetRemoteTracks()) {
-      track->SortCodecs(sorter);
+    for (auto& transceiver : GetTransceivers()) {
+      transceiver->mSendTrack.SortCodecs(sorter);
+      transceiver->mRecvTrack.SortCodecs(sorter);
     }
   }
 
-  // Manage tracks. We take shared ownership of any track.
-  virtual nsresult AddTrack(const RefPtr<JsepTrack>& track) = 0;
-  virtual nsresult RemoveTrack(const std::string& streamId,
-                               const std::string& trackId) = 0;
-  virtual nsresult ReplaceTrack(const std::string& oldStreamId,
-                                const std::string& oldTrackId,
-                                const std::string& newStreamId,
-                                const std::string& newTrackId) = 0;
-  virtual nsresult SetParameters(
-      const std::string& streamId,
-      const std::string& trackId,
-      const std::vector<JsepTrack::JsConstraints>& constraints) = 0;
+  // Helpful for firing events.
+  virtual std::vector<JsepTrack> GetRemoteTracksAdded() const = 0;
+  virtual std::vector<JsepTrack> GetRemoteTracksRemoved() const = 0;
 
-  virtual nsresult GetParameters(
-      const std::string& streamId,
-      const std::string& trackId,
-      std::vector<JsepTrack::JsConstraints>* outConstraints) = 0;
-
-  virtual std::vector<RefPtr<JsepTrack>> GetLocalTracks() const = 0;
-
-  virtual std::vector<RefPtr<JsepTrack>> GetRemoteTracks() const = 0;
-
-  virtual std::vector<RefPtr<JsepTrack>> GetRemoteTracksAdded() const = 0;
-
-  virtual std::vector<RefPtr<JsepTrack>> GetRemoteTracksRemoved() const = 0;
-
-  // Access the negotiated track pairs.
-  virtual std::vector<JsepTrackPair> GetNegotiatedTrackPairs() const = 0;
-
-  // Access transports.
-  virtual std::vector<RefPtr<JsepTransport>> GetTransports() const = 0;
+  virtual const std::vector<RefPtr<JsepTransceiver>>&
+    GetTransceivers() const = 0;
+  virtual std::vector<RefPtr<JsepTransceiver>>& GetTransceivers() = 0;
+  virtual nsresult AddTransceiver(RefPtr<JsepTransceiver> transceiver) = 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(JsepDescriptionPendingOrCurrent type)
                                           const = 0;
@@ -213,39 +185,38 @@ public:
   {
     static const char* states[] = { "stable", "have-local-offer",
                                     "have-remote-offer", "have-local-pranswer",
                                     "have-remote-pranswer", "closed" };
 
     return states[state];
   }
 
-  virtual bool AllLocalTracksAreAssigned() const = 0;
+  virtual bool CheckNegotiationNeeded() const = 0;
 
   void
   CountTracks(uint16_t (&receiving)[SdpMediaSection::kMediaTypes],
               uint16_t (&sending)[SdpMediaSection::kMediaTypes]) const
   {
-    auto trackPairs = GetNegotiatedTrackPairs();
-
     memset(receiving, 0, sizeof(receiving));
     memset(sending, 0, sizeof(sending));
 
-    for (auto& pair : trackPairs) {
-      if (pair.mReceiving) {
-        receiving[pair.mReceiving->GetMediaType()]++;
+    for (const auto& transceiver : GetTransceivers()) {
+      if (!transceiver->mRecvTrack.GetTrackId().empty()) {
+        receiving[transceiver->mRecvTrack.GetMediaType()]++;
       }
 
-      if (pair.mSending) {
-        sending[pair.mSending->GetMediaType()]++;
+      if (!transceiver->mSendTrack.GetTrackId().empty()) {
+        sending[transceiver->mSendTrack.GetMediaType()]++;
       }
     }
   }
 
 protected:
+
   const std::string mName;
   JsepSignalingState mState;
   uint32_t mNegotiations;
 };
 
 } // namespace mozilla
 
 #endif
--- a/media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
+++ b/media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
@@ -1,14 +1,15 @@
 /* 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 "signaling/src/jsep/JsepSessionImpl.h"
 
+#include <iterator>
 #include <string>
 #include <set>
 #include <bitset>
 #include <stdlib.h>
 
 #include "nspr.h"
 #include "nss.h"
 #include "pk11pub.h"
@@ -62,125 +63,49 @@ JsepSessionImpl::Init()
   NS_ENSURE_SUCCESS(rv, rv);
 
   SetupDefaultCodecs();
   SetupDefaultRtpExtensions();
 
   return NS_OK;
 }
 
-// Helper function to find the track for a given m= section.
-template <class T>
-typename std::vector<T>::iterator
-FindTrackByLevel(std::vector<T>& tracks, size_t level)
-{
-  for (auto t = tracks.begin(); t != tracks.end(); ++t) {
-    if (t->mAssignedMLine.isSome() &&
-        (*t->mAssignedMLine == level)) {
-      return t;
-    }
-  }
-
-  return tracks.end();
-}
-
-template <class T>
-typename std::vector<T>::iterator
-FindTrackByIds(std::vector<T>& tracks,
-               const std::string& streamId,
-               const std::string& trackId)
-{
-  for (auto t = tracks.begin(); t != tracks.end(); ++t) {
-    if (t->mTrack->GetStreamId() == streamId &&
-        (t->mTrack->GetTrackId() == trackId)) {
-      return t;
-    }
-  }
-
-  return tracks.end();
-}
-
-template <class T>
-typename std::vector<T>::iterator
-FindUnassignedTrackByType(std::vector<T>& tracks,
-                          SdpMediaSection::MediaType type)
-{
-  for (auto t = tracks.begin(); t != tracks.end(); ++t) {
-    if (!t->mAssignedMLine.isSome() &&
-        (t->mTrack->GetMediaType() == type)) {
-      return t;
-    }
-  }
-
-  return tracks.end();
-}
-
 nsresult
-JsepSessionImpl::AddTrack(const RefPtr<JsepTrack>& track)
+JsepSessionImpl::AddTransceiver(RefPtr<JsepTransceiver> transceiver)
 {
   mLastError.clear();
-  MOZ_ASSERT(track->GetDirection() == sdp::kSend);
-  MOZ_MTLOG(ML_DEBUG, "Adding track.");
-  if (track->GetMediaType() != SdpMediaSection::kApplication) {
-    track->SetCNAME(mCNAME);
-    // Establish minimum number of required SSRCs
-    // Note that AddTrack is only for send direction
-    size_t minimumSsrcCount = 0;
-    std::vector<JsepTrack::JsConstraints> constraints;
-    track->GetJsConstraints(&constraints);
-    for (auto constraint : constraints) {
-      if (!constraint.rid.empty()) {
-        minimumSsrcCount++;
+  MOZ_MTLOG(ML_DEBUG, "Adding transceiver.");
+
+  if (transceiver->GetMediaType() != SdpMediaSection::kApplication) {
+    // Make sure we have an ssrc. Might already be set.
+    transceiver->mSendTrack.EnsureSsrcs(mSsrcGenerator);
+    transceiver->mSendTrack.SetCNAME(mCNAME);
+
+    // Make sure we have identifiers for send track, just in case.
+    // (man I hate this)
+    if (transceiver->mSendTrack.GetTrackId().empty()) {
+      std::string trackId;
+      if (!mUuidGen->Generate(&trackId)) {
+        JSEP_SET_ERROR("Failed to generate UUID for JsepTrack");
+        return NS_ERROR_FAILURE;
       }
+
+      transceiver->mSendTrack.UpdateTrackIds(std::vector<std::string>(), trackId);
     }
-    // We need at least 1 SSRC
-    minimumSsrcCount = std::max<size_t>(1, minimumSsrcCount);
-    size_t currSsrcCount = track->GetSsrcs().size();
-    if (currSsrcCount < minimumSsrcCount ) {
-      MOZ_MTLOG(ML_DEBUG,
-                "Adding " << (minimumSsrcCount - currSsrcCount) << " SSRCs.");
-    }
-    while (track->GetSsrcs().size() < minimumSsrcCount) {
-      uint32_t ssrc=0;
-      nsresult rv = CreateSsrc(&ssrc);
-      NS_ENSURE_SUCCESS(rv, rv);
-      // Don't add duplicate ssrcs
-      std::vector<uint32_t> ssrcs = track->GetSsrcs();
-      if (std::find(ssrcs.begin(), ssrcs.end(), ssrc) == ssrcs.end()) {
-        track->AddSsrc(ssrc);
-      }
-    }
+  } else {
+    // Datachannel transceivers should always be sendrecv. Just set it instead
+    // of asserting.
+    transceiver->mJsDirection = SdpDirectionAttribute::kSendrecv;
   }
 
-  track->PopulateCodecs(mSupportedCodecs.values);
-
-  JsepSendingTrack strack;
-  strack.mTrack = track;
-
-  mLocalTracks.push_back(strack);
-
-  return NS_OK;
-}
+  transceiver->mSendTrack.PopulateCodecs(mSupportedCodecs.values);
+  transceiver->mRecvTrack.PopulateCodecs(mSupportedCodecs.values);
+  // We do not set mLevel yet, we do that either on createOffer, or setRemote
 
-nsresult
-JsepSessionImpl::RemoveTrack(const std::string& streamId,
-                             const std::string& trackId)
-{
-  if (mState != kJsepStateStable) {
-    JSEP_SET_ERROR("Removing tracks outside of stable is unsupported.");
-    return NS_ERROR_UNEXPECTED;
-  }
-
-  auto track = FindTrackByIds(mLocalTracks, streamId, trackId);
-
-  if (track == mLocalTracks.end()) {
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  mLocalTracks.erase(track);
+  mTransceivers.push_back(transceiver);
   return NS_OK;
 }
 
 nsresult
 JsepSessionImpl::SetIceCredentials(const std::string& ufrag,
                                    const std::string& pwd)
 {
   mLastError.clear();
@@ -258,385 +183,106 @@ JsepSessionImpl::AddAudioRtpExtension(co
 
 nsresult
 JsepSessionImpl::AddVideoRtpExtension(const std::string& extensionName,
                                       SdpDirectionAttribute::Direction direction)
 {
   return AddRtpExtension(mVideoRtpExtensions, extensionName, direction);
 }
 
-template<class T>
-std::vector<RefPtr<JsepTrack>>
-GetTracks(const std::vector<T>& wrappedTracks)
-{
-  std::vector<RefPtr<JsepTrack>> result;
-  for (auto i = wrappedTracks.begin(); i != wrappedTracks.end(); ++i) {
-    result.push_back(i->mTrack);
-  }
-  return result;
-}
-
-nsresult
-JsepSessionImpl::ReplaceTrack(const std::string& oldStreamId,
-                              const std::string& oldTrackId,
-                              const std::string& newStreamId,
-                              const std::string& newTrackId)
+std::vector<JsepTrack>
+JsepSessionImpl::GetRemoteTracksAdded() const
 {
-  auto it = FindTrackByIds(mLocalTracks, oldStreamId, oldTrackId);
-
-  if (it == mLocalTracks.end()) {
-    JSEP_SET_ERROR("Track " << oldStreamId << "/" << oldTrackId
-                   << " was never added.");
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  if (FindTrackByIds(mLocalTracks, newStreamId, newTrackId) !=
-      mLocalTracks.end()) {
-    JSEP_SET_ERROR("Track " << newStreamId << "/" << newTrackId
-                   << " was already added.");
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  it->mTrack->SetStreamId(newStreamId);
-  it->mTrack->SetTrackId(newTrackId);
-
-  return NS_OK;
+  return mRemoteTracksAdded;
 }
 
-nsresult
-JsepSessionImpl::SetParameters(const std::string& streamId,
-                               const std::string& trackId,
-                               const std::vector<JsepTrack::JsConstraints>& constraints)
-{
-  auto it = FindTrackByIds(mLocalTracks, streamId, trackId);
-
-  if (it == mLocalTracks.end()) {
-    JSEP_SET_ERROR("Track " << streamId << "/" << trackId << " was never added.");
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  // Add RtpStreamId Extmap
-  // SdpDirectionAttribute::Direction is a bitmask
-  SdpDirectionAttribute::Direction addVideoExt = SdpDirectionAttribute::kInactive;
-  SdpDirectionAttribute::Direction addAudioExt = SdpDirectionAttribute::kInactive;
-  for (auto constraintEntry: constraints) {
-    if (!constraintEntry.rid.empty()) {
-      switch (it->mTrack->GetMediaType()) {
-        case SdpMediaSection::kVideo: {
-          addVideoExt = static_cast<SdpDirectionAttribute::Direction>(addVideoExt
-                                                                      | it->mTrack->GetDirection());
-          break;
-        }
-        case SdpMediaSection::kAudio: {
-          addAudioExt = static_cast<SdpDirectionAttribute::Direction>(addAudioExt
-                                                                      | it->mTrack->GetDirection());
-          break;
-        }
-        default: {
-          MOZ_ASSERT(false);
-          return NS_ERROR_INVALID_ARG;
-        }
-      }
-    }
-  }
-  if (addVideoExt != SdpDirectionAttribute::kInactive) {
-    AddVideoRtpExtension(webrtc::RtpExtension::kRtpStreamIdUri, addVideoExt);
-  }
-
-  it->mTrack->SetJsConstraints(constraints);
-
-  auto track = it->mTrack;
-  if (track->GetDirection() == sdp::kSend) {
-    // Establish minimum number of required SSRCs
-    // Note that AddTrack is only for send direction
-    size_t minimumSsrcCount = 0;
-    std::vector<JsepTrack::JsConstraints> constraints;
-    track->GetJsConstraints(&constraints);
-    for (auto constraint : constraints) {
-      if (!constraint.rid.empty()) {
-        minimumSsrcCount++;
-      }
-    }
-    // We need at least 1 SSRC
-    minimumSsrcCount = std::max<size_t>(1, minimumSsrcCount);
-    size_t currSsrcCount = track->GetSsrcs().size();
-    if (currSsrcCount < minimumSsrcCount ) {
-      MOZ_MTLOG(ML_DEBUG,
-                "Adding " << (minimumSsrcCount - currSsrcCount) << " SSRCs.");
-    }
-    while (track->GetSsrcs().size() < minimumSsrcCount) {
-      uint32_t ssrc=0;
-      nsresult rv = CreateSsrc(&ssrc);
-      NS_ENSURE_SUCCESS(rv, rv);
-      // Don't add duplicate ssrcs
-      std::vector<uint32_t> ssrcs = track->GetSsrcs();
-      if (std::find(ssrcs.begin(), ssrcs.end(), ssrc) == ssrcs.end()) {
-        track->AddSsrc(ssrc);
-      }
-    }
-  }
-  return NS_OK;
-}
-
-nsresult
-JsepSessionImpl::GetParameters(const std::string& streamId,
-                               const std::string& trackId,
-                               std::vector<JsepTrack::JsConstraints>* outConstraints)
-{
-  auto it = FindTrackByIds(mLocalTracks, streamId, trackId);
-
-  if (it == mLocalTracks.end()) {
-    JSEP_SET_ERROR("Track " << streamId << "/" << trackId << " was never added.");
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  it->mTrack->GetJsConstraints(outConstraints);
-  return NS_OK;
-}
-
-std::vector<RefPtr<JsepTrack>>
-JsepSessionImpl::GetLocalTracks() const
-{
-  return GetTracks(mLocalTracks);
-}
-
-std::vector<RefPtr<JsepTrack>>
-JsepSessionImpl::GetRemoteTracks() const
-{
-  return GetTracks(mRemoteTracks);
-}
-
-std::vector<RefPtr<JsepTrack>>
-JsepSessionImpl::GetRemoteTracksAdded() const
-{
-  return GetTracks(mRemoteTracksAdded);
-}
-
-std::vector<RefPtr<JsepTrack>>
+std::vector<JsepTrack>
 JsepSessionImpl::GetRemoteTracksRemoved() const
 {
-  return GetTracks(mRemoteTracksRemoved);
+  return mRemoteTracksRemoved;
 }
 
 nsresult
-JsepSessionImpl::SetupOfferMSections(const JsepOfferOptions& options, Sdp* sdp)
+JsepSessionImpl::CreateOfferMsection(const JsepOfferOptions& options,
+                                     JsepTransceiver& transceiver,
+                                     Sdp* local)
 {
-  // First audio, then video, then datachannel, for interop
-  // TODO(bug 1121756): We need to group these by stream-id, _then_ by media
-  // type, according to the spec. However, this is not going to interop with
-  // older versions of Firefox if a video-only stream is added before an
-  // audio-only stream.
-  // We should probably wait until 38 is ESR before trying to do this.
-  nsresult rv = SetupOfferMSectionsByType(
-      SdpMediaSection::kAudio, options.mOfferToReceiveAudio, sdp);
+  JsepTrack& sendTrack(transceiver.mSendTrack);
+  JsepTrack& recvTrack(transceiver.mRecvTrack);
 
-  NS_ENSURE_SUCCESS(rv, rv);
+  SdpMediaSection::Protocol protocol(
+      mSdpHelper.GetProtocolForMediaType(sendTrack.GetMediaType()));
 
-  rv = SetupOfferMSectionsByType(
-      SdpMediaSection::kVideo, options.mOfferToReceiveVideo, sdp);
-
-  NS_ENSURE_SUCCESS(rv, rv);
+  const Sdp* answer(GetAnswer());
+  const SdpMediaSection* lastAnswerMsection = nullptr;
 
-  rv = SetupOfferMSectionsByType(
-      SdpMediaSection::kApplication, Maybe<size_t>(), sdp);
-
-  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;
+  if (answer &&
+      (local->GetMediaSectionCount() < answer->GetMediaSectionCount())) {
+    lastAnswerMsection =
+      &answer->GetMediaSection(local->GetMediaSectionCount());
+    // Use the protocol the answer used, even if it is not what we would have
+    // used.
+    protocol = lastAnswerMsection->GetProtocol();
   }
 
-  return NS_OK;
-}
-
-nsresult
-JsepSessionImpl::SetupOfferMSectionsByType(SdpMediaSection::MediaType mediatype,
-                                           const Maybe<size_t>& offerToReceiveMaybe,
-                                           Sdp* sdp)
-{
-  // Convert the Maybe into a size_t*, since that is more readable, especially
-  // when using it as an in/out param.
-  size_t offerToReceiveCount;
-  size_t* offerToReceiveCountPtr = nullptr;
-
-  if (offerToReceiveMaybe) {
-    offerToReceiveCount = *offerToReceiveMaybe;
-    offerToReceiveCountPtr = &offerToReceiveCount;
-  }
+  SdpMediaSection* msection = &local->AddMediaSection(
+      sendTrack.GetMediaType(),
+      transceiver.mJsDirection,
+      0,
+      protocol,
+      sdp::kIPv4,
+      "0.0.0.0");
 
-  // Make sure every local track has an m-section
-  nsresult rv = BindLocalTracks(mediatype, sdp);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  // Make sure that m-sections that previously had a remote track have the
-  // recv bit set. Only matters for renegotiation.
-  rv = BindRemoteTracks(mediatype, sdp, offerToReceiveCountPtr);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  // If we need more recv sections, start setting the recv bit on other
-  // msections. If not, disable msections that have no tracks.
-  rv = SetRecvAsNeededOrDisable(mediatype,
-                                sdp,
-                                offerToReceiveCountPtr);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  // If we still don't have enough recv m-sections, add some.
-  if (offerToReceiveCountPtr && *offerToReceiveCountPtr) {
-    rv = AddRecvonlyMsections(mediatype, *offerToReceiveCountPtr, sdp);
+  // Some of this stuff (eg; mid) sticks around even if disabled
+  if (lastAnswerMsection) {
+    nsresult rv = mSdpHelper.CopyStickyParams(*lastAnswerMsection, msection);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  return NS_OK;
-}
-
-nsresult
-JsepSessionImpl::BindLocalTracks(SdpMediaSection::MediaType mediatype, Sdp* sdp)
-{
-  for (JsepSendingTrack& track : mLocalTracks) {
-    if (mediatype != track.mTrack->GetMediaType()) {
-      continue;
-    }
+  if (transceiver.IsStopped()) {
+    mSdpHelper.DisableMsection(local, msection);
+    return NS_OK;
+  }
 
-    SdpMediaSection* msection;
-    if (track.mAssignedMLine.isSome()) {
-      msection = &sdp->GetMediaSection(*track.mAssignedMLine);
-    } else {
-      nsresult rv = GetFreeMsectionForSend(track.mTrack->GetMediaType(),
-                                           sdp,
-                                           &msection);
-      NS_ENSURE_SUCCESS(rv, rv);
-      track.mAssignedMLine = Some(msection->GetLevel());
-    }
-
-    track.mTrack->AddToOffer(msection);
-  }
-  return NS_OK;
-}
+  msection->SetPort(9);
 
-nsresult
-JsepSessionImpl::BindRemoteTracks(SdpMediaSection::MediaType mediatype,
-                                  Sdp* sdp,
-                                  size_t* offerToReceive)
-{
-  for (JsepReceivingTrack& track : mRemoteTracks) {
-    if (mediatype != track.mTrack->GetMediaType()) {
-      continue;
-    }
-
-    if (!track.mAssignedMLine.isSome()) {
-      MOZ_ASSERT(false);
-      continue;
-    }
-
-    auto& msection = sdp->GetMediaSection(*track.mAssignedMLine);
-
-    if (mSdpHelper.MsectionIsDisabled(msection)) {
-      // TODO(bug 1095226) Content probably disabled this? Should we allow
-      // content to do this?
-      continue;
-    }
-
-    track.mTrack->AddToOffer(&msection);
-
-    if (offerToReceive && *offerToReceive) {
-      --(*offerToReceive);
-    }
+  // 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 (mSdpHelper.HasRtcp(msection->GetProtocol())) {
+    // Set RTCP-MUX.
+    msection->GetAttributeList().SetAttribute(
+        new SdpFlagAttribute(SdpAttribute::kRtcpMuxAttribute));
   }
 
-  return NS_OK;
-}
+  nsresult rv = AddTransportAttributes(msection, SdpSetupAttribute::kActpass);
+  NS_ENSURE_SUCCESS(rv, rv);
 
-nsresult
-JsepSessionImpl::SetRecvAsNeededOrDisable(SdpMediaSection::MediaType mediatype,
-                                          Sdp* sdp,
-                                          size_t* offerToRecv)
-{
-  for (size_t i = 0; i < sdp->GetMediaSectionCount(); ++i) {
-    auto& msection = sdp->GetMediaSection(i);
+  sendTrack.AddToOffer(mSsrcGenerator, msection);
+  recvTrack.AddToOffer(mSsrcGenerator, msection);
+
+  AddExtmap(msection);
 
-    if (mSdpHelper.MsectionIsDisabled(msection) ||
-        msection.GetMediaType() != mediatype ||
-        msection.IsReceiving()) {
-      continue;
-    }
-
-    if (offerToRecv) {
-      if (*offerToRecv) {
-        SetupOfferToReceiveMsection(&msection);
-        --(*offerToRecv);
-        continue;
-      }
-    } else if (msection.IsSending()) {
-      SetupOfferToReceiveMsection(&msection);
-      continue;
+  if (lastAnswerMsection && lastAnswerMsection->GetPort()) {
+    MOZ_ASSERT(transceiver.IsAssociated());
+    MOZ_ASSERT(transceiver.GetMid() ==
+               lastAnswerMsection->GetAttributeList().GetMid());
+  } else {
+    std::string mid;
+    // We do not set the mid on the transceiver, that happens when a description
+    // is set.
+    if (transceiver.IsAssociated()) {
+      mid = transceiver.GetMid();
+    } else {
+      std::ostringstream osMid;
+      osMid << "sdparta_" << msection->GetLevel();
+      mid = osMid.str();
     }
 
-    if (!msection.IsSending()) {
-      // Unused m-section, and no reason to offer to recv on it
-      mSdpHelper.DisableMsection(sdp, &msection);
-    }
-  }
-
-  return NS_OK;
-}
-
-void
-JsepSessionImpl::SetupOfferToReceiveMsection(SdpMediaSection* offer)
-{
-  // Create a dummy recv track, and have it add codecs, set direction, etc.
-  RefPtr<JsepTrack> dummy = new JsepTrack(offer->GetMediaType(),
-                                          "",
-                                          "",
-                                          sdp::kRecv);
-  dummy->PopulateCodecs(mSupportedCodecs.values);
-  dummy->AddToOffer(offer);
-}
-
-nsresult
-JsepSessionImpl::AddRecvonlyMsections(SdpMediaSection::MediaType mediatype,
-                                      size_t count,
-                                      Sdp* sdp)
-{
-  while (count--) {
-    nsresult rv = CreateOfferMSection(
-        mediatype,
-        mSdpHelper.GetProtocolForMediaType(mediatype),
-        SdpDirectionAttribute::kRecvonly,
-        sdp);
-
-    NS_ENSURE_SUCCESS(rv, rv);
-    SetupOfferToReceiveMsection(
-        &sdp->GetMediaSection(sdp->GetMediaSectionCount() - 1));
-  }
-  return NS_OK;
-}
-
-// This function creates a skeleton SDP based on the old descriptions
-// (ie; all m-sections are inactive).
-nsresult
-JsepSessionImpl::AddReofferMsections(const Sdp& oldLocalSdp,
-                                     const Sdp& oldAnswer,
-                                     Sdp* newSdp)
-{
-  nsresult rv;
-
-  for (size_t i = 0; i < oldLocalSdp.GetMediaSectionCount(); ++i) {
-    // We do not set the direction in this function (or disable when previously
-    // disabled), that happens in |SetupOfferMSectionsByType|
-    rv = CreateOfferMSection(oldLocalSdp.GetMediaSection(i).GetMediaType(),
-                             oldLocalSdp.GetMediaSection(i).GetProtocol(),
-                             SdpDirectionAttribute::kInactive,
-                             newSdp);
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    rv = mSdpHelper.CopyStickyParams(oldAnswer.GetMediaSection(i),
-                                     &newSdp->GetMediaSection(i));
-    NS_ENSURE_SUCCESS(rv, rv);
+    msection->GetAttributeList().SetAttribute(
+        new SdpStringAttribute(SdpAttribute::kMidAttribute, mid));
   }
 
   return NS_OK;
 }
 
 void
 JsepSessionImpl::SetupBundle(Sdp* sdp) const
 {
@@ -686,93 +332,74 @@ JsepSessionImpl::SetupBundle(Sdp* sdp) c
     groupAttr->PushEntry(SdpGroupAttributeList::kBundle, mids);
     sdp->GetAttributeList().SetAttribute(groupAttr.release());
   }
 }
 
 nsresult
 JsepSessionImpl::GetRemoteIds(const Sdp& sdp,
                               const SdpMediaSection& msection,
-                              std::string* streamId,
+                              std::vector<std::string>* streamIds,
                               std::string* trackId)
 {
-  nsresult rv = mSdpHelper.GetIdsFromMsid(sdp, msection, streamId, trackId);
+  nsresult rv = mSdpHelper.GetIdsFromMsid(sdp, msection, streamIds, trackId);
   if (rv == NS_ERROR_NOT_AVAILABLE) {
-    *streamId = mDefaultRemoteStreamId;
-
-    if (!mDefaultRemoteTrackIdsByLevel.count(msection.GetLevel())) {
-      // Generate random track ids.
-      if (!mUuidGen->Generate(trackId)) {
-        JSEP_SET_ERROR("Failed to generate UUID for JsepTrack");
-        return NS_ERROR_FAILURE;
-      }
+    streamIds->push_back(mDefaultRemoteStreamId);
 
-      mDefaultRemoteTrackIdsByLevel[msection.GetLevel()] = *trackId;
-    } else {
-      *trackId = mDefaultRemoteTrackIdsByLevel[msection.GetLevel()];
+    // Generate random track ids.
+    if (!mUuidGen->Generate(trackId)) {
+      JSEP_SET_ERROR("Failed to generate UUID for JsepTrack");
+      return NS_ERROR_FAILURE;
     }
+
     return NS_OK;
   }
 
-  if (NS_SUCCEEDED(rv)) {
-    // If, for whatever reason, the other end renegotiates with an msid where
-    // there wasn't one before, don't allow the old default to pop up again
-    // later.
-    mDefaultRemoteTrackIdsByLevel.erase(msection.GetLevel());
-  }
-
   return rv;
 }
 
 nsresult
 JsepSessionImpl::CreateOffer(const JsepOfferOptions& options,
                              std::string* offer)
 {
   mLastError.clear();
   mLocalIceIsRestarting = options.mIceRestart.isSome() && *(options.mIceRestart);
 
   if (mState != kJsepStateStable) {
     JSEP_SET_ERROR("Cannot create offer in state " << GetStateStr(mState));
     return NS_ERROR_UNEXPECTED;
   }
 
-  // Undo track assignments from a previous call to CreateOffer
-  // (ie; if the track has not been negotiated yet, it doesn't necessarily need
-  // to stay in the same m-section that it was in)
-  for (JsepSendingTrack& trackWrapper : mLocalTracks) {
-    if (!trackWrapper.mTrack->GetNegotiatedDetails()) {
-      trackWrapper.mAssignedMLine.reset();
-    }
-  }
-
   UniquePtr<Sdp> sdp;
 
   // Make the basic SDP that is common to offer/answer.
   nsresult rv = CreateGenericSDP(&sdp);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  if (mCurrentLocalDescription) {
-    rv = AddReofferMsections(*mCurrentLocalDescription,
-                             *GetAnswer(),
-                             sdp.get());
+  for (size_t level = 0;
+       JsepTransceiver* transceiver = GetTransceiverForLocal(level);
+       ++level) {
+    rv = CreateOfferMsection(options, *transceiver, sdp.get());
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  // Ensure that we have all the m-sections we need, and disable extras
-  rv = SetupOfferMSections(options, sdp.get());
-  NS_ENSURE_SUCCESS(rv, rv);
+  if (!sdp->GetMediaSectionCount()) {
+    JSEP_SET_ERROR("Cannot create offer when there are no valid transceivers.");
+    return NS_ERROR_UNEXPECTED;
+  }
 
   SetupBundle(sdp.get());
 
   if (mCurrentLocalDescription) {
     rv = CopyPreviousTransportParams(*GetAnswer(),
                                      *mCurrentLocalDescription,
                                      *sdp,
                                      sdp.get());
     NS_ENSURE_SUCCESS(rv,rv);
+    CopyPreviousMsid(*mCurrentLocalDescription, sdp.get());
   }
 
   *offer = sdp->ToString();
   mGeneratedLocalDescription = Move(sdp);
   ++mSessionVersion;
 
   return NS_OK;
 }
@@ -795,317 +422,175 @@ JsepSessionImpl::GetRemoteDescription(Js
   mozilla::Sdp* sdp =  GetParsedRemoteDescription(type);
   if (sdp) {
     sdp->Serialize(os);
   }
   return os.str();
 }
 
 void
-JsepSessionImpl::AddExtmap(SdpMediaSection* msection) const
+JsepSessionImpl::AddExtmap(SdpMediaSection* msection)
 {
-  const auto* extensions = GetRtpExtensions(msection->GetMediaType());
+  auto extensions = GetRtpExtensions(*msection);
 
-  if (extensions && !extensions->empty()) {
+  if (!extensions.empty()) {
     SdpExtmapAttributeList* extmap = new SdpExtmapAttributeList;
-    extmap->mExtmaps = *extensions;
+    extmap->mExtmaps = extensions;
     msection->GetAttributeList().SetAttribute(extmap);
   }
 }
 
 void
 JsepSessionImpl::AddMid(const std::string& mid,
                         SdpMediaSection* msection) const
 {
   msection->GetAttributeList().SetAttribute(new SdpStringAttribute(
         SdpAttribute::kMidAttribute, mid));
 }
 
-const std::vector<SdpExtmapAttributeList::Extmap>*
-JsepSessionImpl::GetRtpExtensions(SdpMediaSection::MediaType type) const
+std::vector<SdpExtmapAttributeList::Extmap>
+JsepSessionImpl::GetRtpExtensions(const SdpMediaSection& msection)
 {
-  switch (type) {
+  std::vector<SdpExtmapAttributeList::Extmap> result;
+  switch (msection.GetMediaType()) {
     case SdpMediaSection::kAudio:
-      return &mAudioRtpExtensions;
+      result = mAudioRtpExtensions;
+      break;
     case SdpMediaSection::kVideo:
-      return &mVideoRtpExtensions;
+      result = mVideoRtpExtensions;
+      if (msection.GetAttributeList().HasAttribute(
+            SdpAttribute::kRidAttribute)) {
+        // We need RID support
+        // TODO: Would it be worth checking that the direction is sane?
+        AddRtpExtension(result,
+                        webrtc::RtpExtension::kRtpStreamIdUri,
+                        SdpDirectionAttribute::kSendonly);
+      }
+      break;
     default:
-      return nullptr;
+      ;
   }
+  return result;
 }
 
 void
 JsepSessionImpl::AddCommonExtmaps(const SdpMediaSection& remoteMsection,
                                   SdpMediaSection* msection)
 {
-  auto* ourExtensions = GetRtpExtensions(remoteMsection.GetMediaType());
-
-  if (ourExtensions) {
-    mSdpHelper.AddCommonExtmaps(remoteMsection, *ourExtensions, msection);
-  }
+  mSdpHelper.AddCommonExtmaps(
+      remoteMsection, GetRtpExtensions(*msection), msection);
 }
 
 nsresult
 JsepSessionImpl::CreateAnswer(const JsepAnswerOptions& options,
                               std::string* answer)
 {
   mLastError.clear();
 
   if (mState != kJsepStateHaveRemoteOffer) {
     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;
 
   // Copy the bundle groups into our answer
   UniquePtr<SdpGroupAttributeList> groupAttr(new SdpGroupAttributeList);
   mSdpHelper.GetBundleGroups(offer, &groupAttr->mGroups);
   sdp->GetAttributeList().SetAttribute(groupAttr.release());
 
-  // Disable send for local tracks if the offer no longer allows it
-  // (i.e., the m-section is recvonly, inactive or disabled)
-  for (JsepSendingTrack& trackWrapper : mLocalTracks) {
-    if (!trackWrapper.mAssignedMLine.isSome()) {
-      continue;
-    }
-
-    // Get rid of all m-line assignments that have not been negotiated
-    if (!trackWrapper.mTrack->GetNegotiatedDetails()) {
-      trackWrapper.mAssignedMLine.reset();
-      continue;
+  for (size_t i = 0; i < offer.GetMediaSectionCount(); ++i) {
+    // The transceivers are already in place, due to setRemote
+    JsepTransceiver* transceiver(GetTransceiverForLevel(i));
+    if (!transceiver) {
+      JSEP_SET_ERROR("No transceiver for level " << i);
+      MOZ_ASSERT(false);
+      return NS_ERROR_FAILURE;
     }
-
-    if (!offer.GetMediaSection(*trackWrapper.mAssignedMLine).IsReceiving()) {
-      trackWrapper.mAssignedMLine.reset();
-    }
-  }
-
-  size_t numMsections = offer.GetMediaSectionCount();
-
-  for (size_t i = 0; i < numMsections; ++i) {
-    const SdpMediaSection& remoteMsection = offer.GetMediaSection(i);
-    rv = CreateAnswerMSection(options, i, remoteMsection, sdp.get());
+    rv = CreateAnswerMsection(options,
+                              *transceiver,
+                              offer.GetMediaSection(i),
+                              sdp.get());
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   if (mCurrentLocalDescription) {
     // per discussion with bwc, 3rd parm here should be offer, not *sdp. (mjf)
     rv = CopyPreviousTransportParams(*GetAnswer(),
                                      *mCurrentRemoteDescription,
                                      offer,
                                      sdp.get());
     NS_ENSURE_SUCCESS(rv,rv);
+    CopyPreviousMsid(*mCurrentLocalDescription, sdp.get());
   }
 
   *answer = sdp->ToString();
   mGeneratedLocalDescription = Move(sdp);
   ++mSessionVersion;
 
   return NS_OK;
 }
 
 nsresult
-JsepSessionImpl::CreateOfferMSection(SdpMediaSection::MediaType mediatype,
-                                     SdpMediaSection::Protocol proto,
-                                     SdpDirectionAttribute::Direction dir,
-                                     Sdp* sdp)
-{
-  SdpMediaSection* msection =
-      &sdp->AddMediaSection(mediatype, dir, 0, proto, sdp::kIPv4, "0.0.0.0");
-
-  return EnableOfferMsection(msection);
-}
-
-nsresult
-JsepSessionImpl::GetFreeMsectionForSend(
-    SdpMediaSection::MediaType type,
-    Sdp* sdp,
-    SdpMediaSection** msectionOut)
-{
-  for (size_t i = 0; i < sdp->GetMediaSectionCount(); ++i) {
-    SdpMediaSection& msection = sdp->GetMediaSection(i);
-    // draft-ietf-rtcweb-jsep-08 says we should reclaim disabled m-sections
-    // regardless of media type. This breaks some pretty fundamental rules of
-    // SDP offer/answer, so we probably should not do it.
-    if (msection.GetMediaType() != type) {
-      continue;
-    }
-
-    if (FindTrackByLevel(mLocalTracks, i) != mLocalTracks.end()) {
-      // Not free
-      continue;
-    }
-
-    if (mSdpHelper.MsectionIsDisabled(msection)) {
-      // Was disabled; revive
-      nsresult rv = EnableOfferMsection(&msection);
-      NS_ENSURE_SUCCESS(rv, rv);
-    }
-
-    *msectionOut = &msection;
-    return NS_OK;
-  }
-
-  // Ok, no pre-existing m-section. Make a new one.
-  nsresult rv = CreateOfferMSection(type,
-                                    mSdpHelper.GetProtocolForMediaType(type),
-                                    SdpDirectionAttribute::kInactive,
-                                    sdp);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  *msectionOut = &sdp->GetMediaSection(sdp->GetMediaSectionCount() - 1);
-  return NS_OK;
-}
-
-nsresult
-JsepSessionImpl::CreateAnswerMSection(const JsepAnswerOptions& options,
-                                      size_t mlineIndex,
+JsepSessionImpl::CreateAnswerMsection(const JsepAnswerOptions& options,
+                                      JsepTransceiver& transceiver,
                                       const SdpMediaSection& remoteMsection,
                                       Sdp* sdp)
 {
+  SdpDirectionAttribute::Direction direction =
+    reverse(remoteMsection.GetDirection()) & transceiver.mJsDirection;
   SdpMediaSection& msection =
       sdp->AddMediaSection(remoteMsection.GetMediaType(),
-                           SdpDirectionAttribute::kInactive,
+                           direction,
                            9,
                            remoteMsection.GetProtocol(),
                            sdp::kIPv4,
                            "0.0.0.0");
 
   nsresult rv = mSdpHelper.CopyStickyParams(remoteMsection, &msection);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  if (mSdpHelper.MsectionIsDisabled(remoteMsection)) {
+  if (mSdpHelper.MsectionIsDisabled(remoteMsection) ||
+      // JS might have stopped this
+      transceiver.IsStopped()) {
     mSdpHelper.DisableMsection(sdp, &msection);
     return NS_OK;
   }
 
   SdpSetupAttribute::Role role;
   rv = DetermineAnswererSetupRole(remoteMsection, &role);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = AddTransportAttributes(&msection, role);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  rv = SetRecvonlySsrc(&msection);
-  NS_ENSURE_SUCCESS(rv, rv);
+  transceiver.mSendTrack.AddToAnswer(remoteMsection, mSsrcGenerator, &msection);
+  transceiver.mRecvTrack.AddToAnswer(remoteMsection, mSsrcGenerator, &msection);
 
-  // Only attempt to match up local tracks if the offerer has elected to
-  // receive traffic.
-  if (remoteMsection.IsReceiving()) {
-    rv = BindMatchingLocalTrackToAnswer(&msection);
-    NS_ENSURE_SUCCESS(rv, rv);
-  }
-
-  if (remoteMsection.IsSending()) {
-    BindMatchingRemoteTrackToAnswer(&msection);
-  }
-
-  // Add extmap attributes.
+  // Add extmap attributes. This logic will probably be moved to the track,
+  // since it can be specified on a per-sender basis in JS.
+  // We will need some validation to ensure that the ids are identical for
+  // RTP streams that are bundled together, though (bug 1406529).
   AddCommonExtmaps(remoteMsection, &msection);
 
   if (msection.GetFormats().empty()) {
     // Could not negotiate anything. Disable m-section.
     mSdpHelper.DisableMsection(sdp, &msection);
   }
 
   return NS_OK;
 }
 
 nsresult
-JsepSessionImpl::SetRecvonlySsrc(SdpMediaSection* msection)
-{
-  if (msection->GetMediaType() == SdpMediaSection::kApplication) {
-    return NS_OK;
-  }
-
-  // If previous m-sections are disabled, we do not call this function for them
-  while (mRecvonlySsrcs.size() <= msection->GetLevel()) {
-    uint32_t ssrc;
-    nsresult rv = CreateSsrc(&ssrc);
-    NS_ENSURE_SUCCESS(rv, rv);
-    mRecvonlySsrcs.push_back(ssrc);
-  }
-
-  std::vector<uint32_t> ssrcs;
-  ssrcs.push_back(mRecvonlySsrcs[msection->GetLevel()]);
-  msection->SetSsrcs(ssrcs, mCNAME);
-  return NS_OK;
-}
-
-nsresult
-JsepSessionImpl::BindMatchingLocalTrackToAnswer(SdpMediaSection* msection)
-{
-  auto track = FindTrackByLevel(mLocalTracks, msection->GetLevel());
-
-  if (track == mLocalTracks.end()) {
-    track = FindUnassignedTrackByType(mLocalTracks, msection->GetMediaType());
-  }
-
-  if (track == mLocalTracks.end() &&
-      msection->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)));
-    track = FindUnassignedTrackByType(mLocalTracks, msection->GetMediaType());
-    MOZ_ASSERT(track != mLocalTracks.end());
-  }
-
-  if (track != mLocalTracks.end()) {
-    track->mAssignedMLine = Some(msection->GetLevel());
-    track->mTrack->AddToAnswer(
-        mPendingRemoteDescription->GetMediaSection(msection->GetLevel()),
-        msection);
-  }
-
-  return NS_OK;
-}
-
-nsresult
-JsepSessionImpl::BindMatchingRemoteTrackToAnswer(SdpMediaSection* msection)
-{
-  auto it = FindTrackByLevel(mRemoteTracks, msection->GetLevel());
-  if (it == mRemoteTracks.end()) {
-    MOZ_ASSERT(false);
-    JSEP_SET_ERROR("Failed to find remote track for local answer m-section");
-    return NS_ERROR_FAILURE;
-  }
-
-  it->mTrack->AddToAnswer(
-      mPendingRemoteDescription->GetMediaSection(msection->GetLevel()),
-      msection);
-  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].
@@ -1158,18 +643,17 @@ JsepSessionImpl::SetLocalDescription(Jse
     if (mState != kJsepStateHaveLocalOffer) {
       JSEP_SET_ERROR("Cannot rollback local description in "
                      << GetStateStr(mState));
       return NS_ERROR_UNEXPECTED;
     }
 
     mPendingLocalDescription.reset();
     SetState(kJsepStateStable);
-    mTransports = mOldTransports;
-    mOldTransports.clear();
+    RollbackLocalOffer();
     return NS_OK;
   }
 
   switch (mState) {
     case kJsepStateStable:
       if (type != kJsepSdpOffer) {
         JSEP_SET_ERROR("Cannot set local answer in state "
                        << GetStateStr(mState));
@@ -1193,22 +677,35 @@ JsepSessionImpl::SetLocalDescription(Jse
   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.
-  mOldTransports = mTransports; // Save in case we need to rollback
-  mTransports.clear();
-  for (size_t t = 0; t < parsed->GetMediaSectionCount(); ++t) {
-    mTransports.push_back(RefPtr<JsepTransport>(new JsepTransport));
-    InitTransport(parsed->GetMediaSection(t), mTransports[t].get());
+  if (type == kJsepSdpOffer) {
+    // Save in case we need to rollback
+    mOldTransceivers.clear();
+    for (const auto& transceiver : mTransceivers) {
+      mOldTransceivers.push_back(new JsepTransceiver(*transceiver));
+    }
+  }
+
+  for (size_t i = 0; i < parsed->GetMediaSectionCount(); ++i) {
+    JsepTransceiver* transceiver(GetTransceiverForLevel(i));
+    if (!transceiver) {
+      MOZ_ASSERT(false);
+      JSEP_SET_ERROR("No transceiver for level " << i);
+      return NS_ERROR_FAILURE;
+    }
+    transceiver->Associate(
+        parsed->GetMediaSection(i).GetAttributeList().GetMid());
+    transceiver->mTransport = new JsepTransport;
+    InitTransport(parsed->GetMediaSection(i), transceiver->mTransport.get());
   }
 
   switch (type) {
     case kJsepSdpOffer:
       rv = SetLocalDescriptionOffer(Move(parsed));
       break;
     case kJsepSdpAnswer:
     case kJsepSdpPranswer:
@@ -1242,44 +739,43 @@ JsepSessionImpl::SetLocalDescriptionAnsw
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = HandleNegotiatedSession(mPendingLocalDescription,
                                mPendingRemoteDescription);
   NS_ENSURE_SUCCESS(rv, rv);
 
   mCurrentRemoteDescription = Move(mPendingRemoteDescription);
   mCurrentLocalDescription = Move(mPendingLocalDescription);
-  mWasOffererLastTime = mIsOfferer;
+  MOZ_ASSERT(!mIsOfferer);
+  mWasOffererLastTime = false;
 
   SetState(kJsepStateStable);
   return NS_OK;
 }
 
 nsresult
 JsepSessionImpl::SetRemoteDescription(JsepSdpType type, const std::string& sdp)
 {
   mLastError.clear();
-  mRemoteTracksAdded.clear();
-  mRemoteTracksRemoved.clear();
 
   MOZ_MTLOG(ML_DEBUG, "SetRemoteDescription type=" << type << "\nSDP=\n"
                                                    << sdp);
 
   if (type == kJsepSdpRollback) {
     if (mState != kJsepStateHaveRemoteOffer) {
       JSEP_SET_ERROR("Cannot rollback remote description in "
                      << GetStateStr(mState));
       return NS_ERROR_UNEXPECTED;
     }
 
     mPendingRemoteDescription.reset();
     SetState(kJsepStateStable);
+    RollbackRemoteOffer();
 
-    // Update the remote tracks to what they were before the SetRemote
-    return SetRemoteTracksFromDescription(mCurrentRemoteDescription.get());
+    return NS_OK;
   }
 
   switch (mState) {
     case kJsepStateStable:
       if (type != kJsepSdpOffer) {
         JSEP_SET_ERROR("Cannot set remote answer in state "
                        << GetStateStr(mState));
         return NS_ERROR_UNEXPECTED;
@@ -1334,16 +830,33 @@ JsepSessionImpl::SetRemoteDescription(Js
   }
 
   std::vector<std::string> iceOptions;
   if (parsed->GetAttributeList().HasAttribute(
           SdpAttribute::kIceOptionsAttribute)) {
     iceOptions = parsed->GetAttributeList().GetIceOptions().mValues;
   }
 
+  // Save in case we need to rollback.
+  if (type == kJsepSdpOffer) {
+    mOldTransceivers.clear();
+    for (const auto& transceiver : mTransceivers) {
+      mOldTransceivers.push_back(new JsepTransceiver(*transceiver));
+    }
+  }
+
+  // TODO(bug 1095780): Note that we create remote tracks even when
+  // They contain only codecs we can't negotiate or other craziness.
+  rv = UpdateTransceiversFromRemoteDescription(*parsed);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  for (size_t i = 0; i < parsed->GetMediaSectionCount(); ++i) {
+    MOZ_ASSERT(GetTransceiverForLevel(i));
+  }
+
   switch (type) {
     case kJsepSdpOffer:
       rv = SetRemoteDescriptionOffer(Move(parsed));
       break;
     case kJsepSdpAnswer:
     case kJsepSdpPranswer:
       rv = SetRemoteDescriptionAnswer(type, Move(parsed));
       break;
@@ -1370,179 +883,159 @@ JsepSessionImpl::HandleNegotiatedSession
   mIceControlling = remoteIceLite || mIsOfferer;
 
   const Sdp& answer = mIsOfferer ? *remote : *local;
 
   SdpHelper::BundledMids bundledMids;
   nsresult rv = mSdpHelper.GetBundledMids(answer, &bundledMids);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  if (mTransports.size() < local->GetMediaSectionCount()) {
-    JSEP_SET_ERROR("Fewer transports set up than m-lines");
-    MOZ_ASSERT(false);
-    return NS_ERROR_FAILURE;
-  }
-
-  for (JsepSendingTrack& trackWrapper : mLocalTracks) {
-    trackWrapper.mTrack->ClearNegotiatedDetails();
-  }
+  // Now walk through the m-sections, perform negotiation, and update the
+  // transceivers.
+  for (size_t i = 0; i < local->GetMediaSectionCount(); ++i) {
+    JsepTransceiver* transceiver(GetTransceiverForLevel(i));
+    if (!transceiver) {
+      MOZ_ASSERT(false);
+      JSEP_SET_ERROR("No transceiver for level " << i);
+      return NS_ERROR_FAILURE;
+    }
 
-  for (JsepReceivingTrack& trackWrapper : mRemoteTracks) {
-    trackWrapper.mTrack->ClearNegotiatedDetails();
-  }
-
-  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) {
     // Skip disabled m-sections.
     if (answer.GetMediaSection(i).GetPort() == 0) {
-      mTransports[i]->Close();
+      transceiver->mTransport->Close();
+      transceiver->Stop();
+      transceiver->Disassociate();
+      transceiver->ClearBundleLevel();
+      transceiver->mSendTrack.SetActive(false);
+      transceiver->mRecvTrack.SetActive(false);
+      // Do not clear mLevel yet! That will happen on the next negotiation.
       continue;
     }
 
     // The transport details are not necessarily on the m-section we're
     // currently processing.
     size_t transportLevel = i;
     bool usingBundle = false;
     {
       const SdpMediaSection& answerMsection(answer.GetMediaSection(i));
       if (answerMsection.GetAttributeList().HasAttribute(
             SdpAttribute::kMidAttribute)) {
         if (bundledMids.count(answerMsection.GetAttributeList().GetMid())) {
           const SdpMediaSection* masterBundleMsection =
             bundledMids[answerMsection.GetAttributeList().GetMid()];
           transportLevel = masterBundleMsection->GetLevel();
           usingBundle = true;
-          if (i != transportLevel) {
-            mTransports[i]->Close();
-          }
         }
       }
     }
 
-    RefPtr<JsepTransport> transport = mTransports[transportLevel];
-
-    rv = FinalizeTransport(
-        remote->GetMediaSection(transportLevel).GetAttributeList(),
-        answer.GetMediaSection(transportLevel).GetAttributeList(),
-        transport);
+    rv = MakeNegotiatedTransceiver(remote->GetMediaSection(i),
+                                   local->GetMediaSection(i),
+                                   usingBundle,
+                                   transportLevel,
+                                   transceiver);
     NS_ENSURE_SUCCESS(rv, rv);
-
-    JsepTrackPair trackPair;
-    rv = MakeNegotiatedTrackPair(remote->GetMediaSection(i),
-                                 local->GetMediaSection(i),
-                                 transport,
-                                 usingBundle,
-                                 transportLevel,
-                                 &trackPair);
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    trackPairs.push_back(trackPair);
   }
 
-  JsepTrack::SetUniquePayloadTypes(GetTracks(mRemoteTracks));
-
-  // Ouch, this probably needs some dirty bit instead of just clearing
-  // stuff for renegotiation.
-  mNegotiatedTrackPairs = trackPairs;
+  std::vector<JsepTrack*> remoteTracks;
+  for (const RefPtr<JsepTransceiver>& transceiver : mTransceivers) {
+    remoteTracks.push_back(&transceiver->mRecvTrack);
+  }
+  JsepTrack::SetUniquePayloadTypes(remoteTracks);
 
   mGeneratedLocalDescription.reset();
 
   mNegotiations++;
   return NS_OK;
 }
 
 nsresult
-JsepSessionImpl::MakeNegotiatedTrackPair(const SdpMediaSection& remote,
-                                         const SdpMediaSection& local,
-                                         const RefPtr<JsepTransport>& transport,
-                                         bool usingBundle,
-                                         size_t transportLevel,
-                                         JsepTrackPair* trackPairOut)
+JsepSessionImpl::MakeNegotiatedTransceiver(const SdpMediaSection& remote,
+                                           const SdpMediaSection& local,
+                                           bool usingBundle,
+                                           size_t transportLevel,
+                                           JsepTransceiver* transceiver)
 {
-  MOZ_ASSERT(transport->mComponents);
   const SdpMediaSection& answer = mIsOfferer ? remote : local;
 
-  bool sending;
-  bool receiving;
+  bool sending = false;
+  bool receiving = false;
 
-  if (mIsOfferer) {
-    receiving = answer.IsSending();
-    sending = answer.IsReceiving();
-  } else {
-    sending = answer.IsSending();
-    receiving = answer.IsReceiving();
+  // JS could stop the transceiver after the answer was created.
+  if (!transceiver->IsStopped()) {
+    if (mIsOfferer) {
+      receiving = answer.IsSending();
+      sending = answer.IsReceiving();
+    } else {
+      sending = answer.IsSending();
+      receiving = answer.IsReceiving();
+    }
   }
 
   MOZ_MTLOG(ML_DEBUG, "Negotiated m= line"
                           << " index=" << local.GetLevel()
                           << " type=" << local.GetMediaType()
                           << " sending=" << sending
                           << " receiving=" << receiving);
 
-  trackPairOut->mLevel = local.GetLevel();
-
-  if (local.GetMediaType() != SdpMediaSection::kApplication) {
-    MOZ_ASSERT(mRecvonlySsrcs.size() > local.GetLevel(),
-               "Failed to set the default ssrc for an active m-section");
-    trackPairOut->mRecvonlySsrc = mRecvonlySsrcs[local.GetLevel()];
-  }
+  transceiver->SetNegotiated();
 
   if (usingBundle) {
-    trackPairOut->SetBundleLevel(transportLevel);
+    transceiver->SetBundleLevel(transportLevel);
+  } else {
+    transceiver->ClearBundleLevel();
   }
 
-  auto sendTrack = FindTrackByLevel(mLocalTracks, local.GetLevel());
-  if (sendTrack != mLocalTracks.end()) {
-    sendTrack->mTrack->Negotiate(answer, remote);
-    sendTrack->mTrack->SetActive(sending);
-    trackPairOut->mSending = sendTrack->mTrack;
-  } else if (sending) {
-    JSEP_SET_ERROR("Failed to find local track for level " <<
-                   local.GetLevel()
-                   << " in local SDP. This should never happen.");
-    NS_ASSERTION(false, "Failed to find local track for level");
-    return NS_ERROR_FAILURE;
+  if (transportLevel != remote.GetLevel()) {
+    JsepTransceiver* bundleTransceiver(GetTransceiverForLevel(transportLevel));
+    if (!bundleTransceiver) {
+      MOZ_ASSERT(false);
+      JSEP_SET_ERROR("No transceiver for level " << transportLevel);
+      return NS_ERROR_FAILURE;
+    }
+    transceiver->mTransport = bundleTransceiver->mTransport;
+  } else {
+    // Ensures we only finalize once, when we process the master level
+    nsresult rv = FinalizeTransport(
+        remote.GetAttributeList(),
+        answer.GetAttributeList(),
+        transceiver->mTransport);
+    NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  auto recvTrack = FindTrackByLevel(mRemoteTracks, local.GetLevel());
-  if (recvTrack != mRemoteTracks.end()) {
-    recvTrack->mTrack->Negotiate(answer, remote);
-    recvTrack->mTrack->SetActive(receiving);
-    trackPairOut->mReceiving = recvTrack->mTrack;
+  transceiver->mSendTrack.SetActive(sending);
+  if (sending) {
+    transceiver->mSendTrack.Negotiate(answer, remote);
+  }
 
-    if (receiving &&
-        trackPairOut->HasBundleLevel() &&
-        recvTrack->mTrack->GetSsrcs().empty() &&
-        recvTrack->mTrack->GetMediaType() != SdpMediaSection::kApplication) {
+  JsepTrack& recvTrack = transceiver->mRecvTrack;
+  recvTrack.SetActive(receiving);
+  if (receiving) {
+    recvTrack.Negotiate(answer, remote);
+
+    if (transceiver->HasBundleLevel() &&
+        recvTrack.GetSsrcs().empty() &&
+        recvTrack.GetMediaType() != SdpMediaSection::kApplication) {
+      // TODO(bug 1105005): Once we have urn:ietf:params:rtp-hdrext:sdes:mid
+      // support, we should only fire this warning if that extension was not
+      // negotiated.
       MOZ_MTLOG(ML_ERROR, "Bundled m-section has no ssrc attributes. "
                           "This may cause media packets to be dropped.");
     }
-  } else if (receiving) {
-    JSEP_SET_ERROR("Failed to find remote track for level "
-                   << local.GetLevel()
-                   << " in remote SDP. This should never happen.");
-    NS_ASSERTION(false, "Failed to find remote track for level");
-    return NS_ERROR_FAILURE;
   }
 
-  trackPairOut->mRtpTransport = transport;
-
-  if (transport->mComponents == 2) {
+  if (transceiver->mTransport->mComponents == 2) {
     // RTCP MUX or not.
     // TODO(bug 1095743): verify that the PTs are consistent with mux.
     MOZ_MTLOG(ML_DEBUG, "RTCP-MUX is off");
-    trackPairOut->mRtcpTransport = transport;
   }
 
   if (local.GetMediaType() != SdpMediaSection::kApplication) {
     Telemetry::Accumulate(Telemetry::WEBRTC_RTCP_MUX,
-        transport->mComponents == 1);
+        transceiver->mTransport->mComponents == 1);
   }
 
   return NS_OK;
 }
 
 void
 JsepSessionImpl::InitTransport(const SdpMediaSection& msection,
                                JsepTransport* transport)
@@ -1653,28 +1146,52 @@ JsepSessionImpl::CopyPreviousTransportPa
                                               offerersPreviousSdp,
                                               newOffer,
                                               i) &&
         !mRemoteIceIsRestarting
        ) {
       // If newLocal is an offer, this will be the number of components we used
       // last time, and if it is an answer, this will be the number of
       // components we've decided we're using now.
-      size_t numComponents = mTransports[i]->mComponents;
+      JsepTransceiver* transceiver(GetTransceiverForLevel(i));
+      if (!transceiver) {
+        MOZ_ASSERT(false);
+        JSEP_SET_ERROR("No transceiver for level " << i);
+        return NS_ERROR_FAILURE;
+      }
+      size_t numComponents = transceiver->mTransport->mComponents;
       nsresult rv = mSdpHelper.CopyTransportParams(
           numComponents,
           mCurrentLocalDescription->GetMediaSection(i),
           &newLocal->GetMediaSection(i));
       NS_ENSURE_SUCCESS(rv, rv);
     }
   }
 
   return NS_OK;
 }
 
+void
+JsepSessionImpl::CopyPreviousMsid(const Sdp& oldLocal, Sdp* newLocal)
+{
+  for (size_t i = 0; i < oldLocal.GetMediaSectionCount(); ++i) {
+    const SdpMediaSection& oldMsection(oldLocal.GetMediaSection(i));
+    SdpMediaSection& newMsection(newLocal->GetMediaSection(i));
+    if (oldMsection.GetAttributeList().HasAttribute(
+          SdpAttribute::kMsidAttribute) &&
+        !mSdpHelper.MsectionIsDisabled(newMsection)) {
+      // JSEP says this cannot change, no matter what is happening in JS land.
+      // It can only be updated if there is an intermediate SDP that clears the
+      // msid.
+      newMsection.GetAttributeList().SetAttribute(new SdpMsidAttributeList(
+            oldMsection.GetAttributeList().GetMsid()));
+    }
+  }
+}
+
 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: ";
     mSdpHelper.appendSdpParseErrors(mParser.GetParseErrors(), &error);
     JSEP_SET_ERROR(error);
@@ -1724,21 +1241,21 @@ JsepSessionImpl::ParseSdp(const std::str
     if (mediaAttrs.HasAttribute(SdpAttribute::kSetupAttribute, true) &&
         mediaAttrs.GetSetup().mRole == SdpSetupAttribute::kHoldconn) {
       JSEP_SET_ERROR("Description has illegal setup attribute "
                      "\"holdconn\" in m-section at level "
                      << i);
       return NS_ERROR_INVALID_ARG;
     }
 
-    std::string streamId;
+    std::vector<std::string> streamIds;
     std::string trackId;
     nsresult rv = mSdpHelper.GetIdsFromMsid(*parsed,
                                             parsed->GetMediaSection(i),
-                                            &streamId,
+                                            &streamIds,
                                             &trackId);
 
     if (NS_SUCCEEDED(rv)) {
       if (trackIds.count(trackId)) {
         JSEP_SET_ERROR("track id:" << trackId
                        << " appears in more than one m-section at level " << i);
         return NS_ERROR_INVALID_ARG;
       }
@@ -1781,21 +1298,16 @@ JsepSessionImpl::ParseSdp(const std::str
 nsresult
 JsepSessionImpl::SetRemoteDescriptionOffer(UniquePtr<Sdp> offer)
 {
   MOZ_ASSERT(mState == kJsepStateStable);
 
   nsresult rv = ValidateOffer(*offer);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // TODO(bug 1095780): Note that we create remote tracks even when
-  // They contain only codecs we can't negotiate or other craziness.
-  rv = SetRemoteTracksFromDescription(offer.get());
-  NS_ENSURE_SUCCESS(rv, rv);
-
   mPendingRemoteDescription = Move(offer);
 
   SetState(kJsepStateHaveRemoteOffer);
   return NS_OK;
 }
 
 nsresult
 JsepSessionImpl::SetRemoteDescriptionAnswer(JsepSdpType type,
@@ -1805,98 +1317,270 @@ JsepSessionImpl::SetRemoteDescriptionAns
              mState == kJsepStateHaveRemotePranswer);
 
   mPendingRemoteDescription = Move(answer);
 
   nsresult rv = ValidateAnswer(*mPendingLocalDescription,
                                *mPendingRemoteDescription);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // TODO(bug 1095780): Note that this creates remote tracks even if
-  // we offered sendonly and other side offered sendrecv or recvonly.
-  rv = SetRemoteTracksFromDescription(mPendingRemoteDescription.get());
-  NS_ENSURE_SUCCESS(rv, rv);
-
   rv = HandleNegotiatedSession(mPendingLocalDescription,
                                mPendingRemoteDescription);
   NS_ENSURE_SUCCESS(rv, rv);
 
   mCurrentRemoteDescription = Move(mPendingRemoteDescription);
   mCurrentLocalDescription = Move(mPendingLocalDescription);
-  mWasOffererLastTime = mIsOfferer;
+  MOZ_ASSERT(mIsOfferer);
+  mWasOffererLastTime = true;
 
   SetState(kJsepStateStable);
   return NS_OK;
 }
 
-nsresult
-JsepSessionImpl::SetRemoteTracksFromDescription(const Sdp* remoteDescription)
+static bool
+TrackIdCompare(const JsepTrack& t1, const JsepTrack& t2)
 {
-  // Unassign all remote tracks
-  for (auto& remoteTrack : mRemoteTracks) {
-    remoteTrack.mAssignedMLine.reset();
+  return t1.GetTrackId() < t2.GetTrackId();
+}
+
+JsepTransceiver*
+JsepSessionImpl::GetTransceiverForLevel(size_t level)
+{
+  for (RefPtr<JsepTransceiver>& transceiver : mTransceivers) {
+    if (transceiver->HasLevel() && (transceiver->GetLevel() == level)) {
+      return transceiver.get();
+    }
   }
 
-  // This will not exist if we're rolling back the first remote description
-  if (remoteDescription) {
-    size_t numMlines = remoteDescription->GetMediaSectionCount();
-    nsresult rv;
-
-    // Iterate over the sdp, re-assigning or creating remote tracks as we go
-    for (size_t i = 0; i < numMlines; ++i) {
-      const SdpMediaSection& msection = remoteDescription->GetMediaSection(i);
-
-      if (mSdpHelper.MsectionIsDisabled(msection) || !msection.IsSending()) {
-        continue;
-      }
-
-      std::vector<JsepReceivingTrack>::iterator track;
+  return nullptr;
+}
 
-      if (msection.GetMediaType() == SdpMediaSection::kApplication) {
-        // Datachannel doesn't have msid, just search by type
-        track = FindUnassignedTrackByType(mRemoteTracks,
-                                          msection.GetMediaType());
-      } else {
-        std::string streamId;
-        std::string trackId;
-        rv = GetRemoteIds(*remoteDescription, msection, &streamId, &trackId);
-        NS_ENSURE_SUCCESS(rv, rv);
-
-        track = FindTrackByIds(mRemoteTracks, streamId, trackId);
+JsepTransceiver*
+JsepSessionImpl::GetTransceiverForLocal(size_t level)
+{
+  if (JsepTransceiver* transceiver = GetTransceiverForLevel(level)) {
+    if (WasMsectionDisabledLastNegotiation(level) && transceiver->IsStopped()) {
+      // Attempt to recycle. If this fails, the old transceiver stays put.
+      transceiver->Disassociate();
+      JsepTransceiver* newTransceiver = FindUnassociatedTransceiver(
+          transceiver->GetMediaType(), false);
+      if (newTransceiver) {
+        newTransceiver->SetLevel(level);
+        transceiver->ClearLevel();
+        return newTransceiver;
       }
+    }
 
-      if (track == mRemoteTracks.end()) {
-        RefPtr<JsepTrack> track;
-        rv = CreateReceivingTrack(i, *remoteDescription, msection, &track);
-        NS_ENSURE_SUCCESS(rv, rv);
+    return transceiver;
+  }
+
+  // There is no transceiver for |level| right now.
 
-        JsepReceivingTrack rtrack;
-        rtrack.mTrack = track;
-        rtrack.mAssignedMLine = Some(i);
-        mRemoteTracks.push_back(rtrack);
-        mRemoteTracksAdded.push_back(rtrack);
-      } else {
-        track->mAssignedMLine = Some(i);
-      }
+  for (RefPtr<JsepTransceiver>& transceiver : mTransceivers) {
+    if (!transceiver->IsStopped() && !transceiver->HasLevel()) {
+      transceiver->SetLevel(level);
+      return transceiver.get();
     }
   }
 
-  // Remove any unassigned remote track ids
-  for (size_t i = 0; i < mRemoteTracks.size();) {
-    if (!mRemoteTracks[i].mAssignedMLine.isSome()) {
-      mRemoteTracksRemoved.push_back(mRemoteTracks[i]);
-      mRemoteTracks.erase(mRemoteTracks.begin() + i);
+  return nullptr;
+}
+
+JsepTransceiver*
+JsepSessionImpl::GetTransceiverForRemote(const SdpMediaSection& msection)
+{
+  size_t level = msection.GetLevel();
+  if (JsepTransceiver* transceiver = GetTransceiverForLevel(level)) {
+    if (!WasMsectionDisabledLastNegotiation(level) ||
+        !transceiver->IsStopped()) {
+      return transceiver;
+    }
+    transceiver->Disassociate();
+    transceiver->ClearLevel();
+  }
+
+  // No transceiver for |level|
+
+  JsepTransceiver* transceiver = FindUnassociatedTransceiver(
+      msection.GetMediaType(), true /*magic!*/);
+  if (transceiver) {
+    transceiver->SetLevel(level);
+    return transceiver;
+  }
+
+  // Make a new transceiver
+  RefPtr<JsepTransceiver> newTransceiver(
+      new JsepTransceiver(msection.GetMediaType(),
+                          SdpDirectionAttribute::kRecvonly));
+  newTransceiver->SetLevel(level);
+  newTransceiver->SetCreatedBySetRemote();
+  nsresult rv = AddTransceiver(newTransceiver);
+  NS_ENSURE_SUCCESS(rv, nullptr);
+  return mTransceivers.back().get();
+}
+
+nsresult
+JsepSessionImpl::UpdateTransceiversFromRemoteDescription(const Sdp& remote)
+{
+  std::vector<JsepTrack> oldRemoteTracks;
+  std::vector<JsepTrack> newRemoteTracks;
+
+  // Iterate over the sdp, updating remote tracks as we go
+  for (size_t i = 0; i < remote.GetMediaSectionCount(); ++i) {
+    const SdpMediaSection& msection = remote.GetMediaSection(i);
+
+    JsepTransceiver* transceiver(GetTransceiverForRemote(msection));
+    if (!transceiver) {
+      return NS_ERROR_FAILURE;
+    }
+
+    bool isRtp =
+      msection.GetMediaType() != SdpMediaSection::MediaType::kApplication;
+
+    if (isRtp && transceiver->mRecvTrack.GetActive()) {
+      oldRemoteTracks.push_back(transceiver->mRecvTrack);
+    }
+
+    if (!mSdpHelper.MsectionIsDisabled(msection)) {
+      transceiver->Associate(msection.GetAttributeList().GetMid());
     } else {
-      ++i;
+      transceiver->Disassociate();
+      // This cannot be rolled back.
+      transceiver->Stop();
+      continue;
+    }
+
+    if (!isRtp) {
+      continue;
+    }
+
+    // Interop workaround for endpoints that don't support msid.
+    // If the receiver has no ids, set some initial values, one way or another.
+    if (msection.IsSending() && transceiver->mRecvTrack.GetTrackId().empty()) {
+      std::vector<std::string> streamIds;
+      std::string trackId;
+
+      nsresult rv = GetRemoteIds(remote, msection, &streamIds, &trackId);
+      NS_ENSURE_SUCCESS(rv, rv);
+      transceiver->mRecvTrack.UpdateTrackIds(streamIds, trackId);
+    }
+
+    transceiver->mRecvTrack.UpdateRecvTrack(remote, msection);
+
+    if (msection.IsSending()) {
+      newRemoteTracks.push_back(transceiver->mRecvTrack);
     }
   }
 
+  std::sort(oldRemoteTracks.begin(), oldRemoteTracks.end(), TrackIdCompare);
+  std::sort(newRemoteTracks.begin(), newRemoteTracks.end(), TrackIdCompare);
+
+  mRemoteTracksAdded.clear();
+  mRemoteTracksRemoved.clear();
+
+  std::set_difference(
+      oldRemoteTracks.begin(),
+      oldRemoteTracks.end(),
+      newRemoteTracks.begin(),
+      newRemoteTracks.end(),
+      std::inserter(mRemoteTracksRemoved, mRemoteTracksRemoved.begin()),
+      TrackIdCompare);
+
+  std::set_difference(
+      newRemoteTracks.begin(),
+      newRemoteTracks.end(),
+      oldRemoteTracks.begin(),
+      oldRemoteTracks.end(),
+      std::inserter(mRemoteTracksAdded, mRemoteTracksAdded.begin()),
+      TrackIdCompare);
+
   return NS_OK;
 }
 
+
+bool
+JsepSessionImpl::WasMsectionDisabledLastNegotiation(size_t level) const
+{
+  const Sdp* answer(GetAnswer());
+
+  if (answer && (level < answer->GetMediaSectionCount())) {
+    return mSdpHelper.MsectionIsDisabled(answer->GetMediaSection(level));
+  }
+
+  return false;
+}
+
+JsepTransceiver*
+JsepSessionImpl::FindUnassociatedTransceiver(
+    SdpMediaSection::MediaType type, bool magic)
+{
+  // Look through transceivers that are not mapped to an m-section
+  for (RefPtr<JsepTransceiver>& transceiver : mTransceivers) {
+    if (!transceiver->IsStopped() &&
+        !transceiver->HasLevel() &&
+        (!magic || transceiver->HasAddTrackMagic()) &&
+        (transceiver->GetMediaType() == type)) {
+      return transceiver.get();
+    }
+  }
+
+  return nullptr;
+}
+
+void
+JsepSessionImpl::RollbackLocalOffer()
+{
+  for (size_t i = 0; i < mTransceivers.size(); ++i) {
+    RefPtr<JsepTransceiver>& transceiver(mTransceivers[i]);
+    if (i < mOldTransceivers.size()) {
+      transceiver->Rollback(*mOldTransceivers[i]);
+      continue;
+    }
+
+    RefPtr<JsepTransceiver> temp(
+        new JsepTransceiver(transceiver->GetMediaType()));
+    transceiver->Rollback(*temp);
+  }
+
+  mOldTransceivers.clear();
+}
+
+void
+JsepSessionImpl::RollbackRemoteOffer()
+{
+  for (size_t i = 0; i < mTransceivers.size(); ++i) {
+    RefPtr<JsepTransceiver>& transceiver(mTransceivers[i]);
+    if (i < mOldTransceivers.size()) {
+      transceiver->Rollback(*mOldTransceivers[i]);
+      continue;
+    }
+
+    // New transceiver!
+    if (!transceiver->HasAddTrackMagic() &&
+        transceiver->WasCreatedBySetRemote()) {
+      transceiver->Stop();
+      transceiver->Disassociate();
+      transceiver->ClearLevel();
+      transceiver->SetRemoved();
+      mTransceivers.erase(mTransceivers.begin() + i);
+      --i;
+      continue;
+    }
+
+    // Transceiver has been "touched" by addTrack; let it live, but unhook it
+    // from everything.
+    RefPtr<JsepTransceiver> temp(
+        new JsepTransceiver(transceiver->GetMediaType()));
+    transceiver->Rollback(*temp);
+  }
+
+  mOldTransceivers.clear();
+  std::swap(mRemoteTracksAdded, mRemoteTracksRemoved);
+}
+
 nsresult
 JsepSessionImpl::ValidateLocalDescription(const Sdp& description)
 {
   // TODO(bug 1095226): Better checking.
   if (!mGeneratedLocalDescription) {
     JSEP_SET_ERROR("Calling SetLocal without first calling CreateOffer/Answer"
                    " is not supported.");
     return NS_ERROR_UNEXPECTED;
@@ -2144,39 +1828,16 @@ JsepSessionImpl::ValidateAnswer(const Sd
       }
     }
   }
 
   return NS_OK;
 }
 
 nsresult
-JsepSessionImpl::CreateReceivingTrack(size_t mline,
-                                      const Sdp& sdp,
-                                      const SdpMediaSection& msection,
-                                      RefPtr<JsepTrack>* track)
-{
-  std::string streamId;
-  std::string trackId;
-
-  nsresult rv = GetRemoteIds(sdp, msection, &streamId, &trackId);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  *track = new JsepTrack(msection.GetMediaType(),
-                         streamId,
-                         trackId,
-                         sdp::kRecv);
-
-  (*track)->SetCNAME(mSdpHelper.GetCNAME(msection));
-  (*track)->PopulateCodecs(mSupportedCodecs.values);
-
-  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-
@@ -2243,32 +1904,16 @@ JsepSessionImpl::SetupIds()
   if (!mUuidGen->Generate(&mCNAME)) {
     JSEP_SET_ERROR("Failed to generate CNAME");
     return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 }
 
-nsresult
-JsepSessionImpl::CreateSsrc(uint32_t* ssrc)
-{
-  do {
-    SECStatus rv = PK11_GenerateRandom(
-        reinterpret_cast<unsigned char*>(ssrc), sizeof(uint32_t));
-    if (rv != SECSuccess) {
-      JSEP_SET_ERROR("Failed to generate SSRC, error=" << rv);
-      return NS_ERROR_FAILURE;
-    }
-  } while (mSsrcs.count(*ssrc));
-  mSsrcs.insert(*ssrc);
-
-  return NS_OK;
-}
-
 void
 JsepSessionImpl::SetupDefaultCodecs()
 {
   // Supported audio codecs.
   // Per jmspeex on IRC:
   // For 32KHz sampling, 28 is ok, 32 is good, 40 should be really good
   // quality.  Note that 1-2Kbps will be wasted on a stereo Opus channel
   // with mono input compared to configuring it for mono.
@@ -2453,16 +2098,23 @@ JsepSessionImpl::AddLocalIceCandidate(co
   }
 
   if (sdp->GetMediaSectionCount() <= level) {
     // mainly here to make some testing less complicated, but also just in case
     *skipped = true;
     return NS_OK;
   }
 
+  if (mSdpHelper.MsectionIsDisabled(sdp->GetMediaSection(level))) {
+    // If m-section has port 0, don't update
+    // (either it is disabled, or bundle-only)
+    *skipped = true;
+    return NS_OK;
+  }
+
   if (mState == kJsepStateStable) {
     const Sdp* answer(GetAnswer());
     if (mSdpHelper.IsBundleSlave(*answer, level)) {
       // We do not add candidate attributes to bundled m-sections unless they
       // are the "master" bundle m-section.
       *skipped = true;
       return NS_OK;
     }
@@ -2494,21 +2146,36 @@ JsepSessionImpl::UpdateDefaultCandidate(
     JSEP_SET_ERROR("Cannot add ICE candidate in state " << GetStateStr(mState));
     return NS_ERROR_UNEXPECTED;
   }
 
   if (level >= sdp->GetMediaSectionCount()) {
     return NS_OK;
   }
 
+  if (mSdpHelper.MsectionIsDisabled(sdp->GetMediaSection(level))) {
+    // If m-section has port 0, don't update
+    // (either it is disabled, or bundle-only)
+    return NS_OK;
+  }
+
   std::string defaultRtcpCandidateAddrCopy(defaultRtcpCandidateAddr);
-  if (mState == kJsepStateStable && mTransports[level]->mComponents == 1) {
-    // We know we're doing rtcp-mux by now. Don't create an rtcp attr.
-    defaultRtcpCandidateAddrCopy = "";
-    defaultRtcpCandidatePort = 0;
+  if (mState == kJsepStateStable) {
+    JsepTransceiver* transceiver(GetTransceiverForLevel(level));
+    if (!transceiver) {
+      MOZ_ASSERT(false);
+      JSEP_SET_ERROR("No transceiver for level " << level);
+      return NS_ERROR_FAILURE;
+    }
+
+    if (transceiver->mTransport->mComponents == 1) {
+      // We know we're doing rtcp-mux by now. Don't create an rtcp attr.
+      defaultRtcpCandidateAddrCopy = "";
+      defaultRtcpCandidatePort = 0;
+    }
   }
 
   // If offer/answer isn't done, it is too early to tell whether these defaults
   // need to be applied to other m-sections.
   SdpHelper::BundledMids bundledMids;
   if (mState == kJsepStateStable) {
     nsresult rv = GetNegotiatedBundledMids(&bundledMids);
     if (NS_FAILED(rv)) {
@@ -2573,48 +2240,16 @@ JsepSessionImpl::GetNegotiatedBundledMid
 
   if (!answerSdp) {
     return NS_OK;
   }
 
   return mSdpHelper.GetBundledMids(*answerSdp, bundledMids);
 }
 
-nsresult
-JsepSessionImpl::EnableOfferMsection(SdpMediaSection* msection)
-{
-  // We assert here because adding rtcp-mux to a non-disabled m-section that
-  // did not already have rtcp-mux can cause problems.
-  MOZ_ASSERT(mSdpHelper.MsectionIsDisabled(*msection));
-
-  msection->SetPort(9);
-
-  // 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 (mSdpHelper.HasRtcp(msection->GetProtocol())) {
-    // Set RTCP-MUX.
-    msection->GetAttributeList().SetAttribute(
-        new SdpFlagAttribute(SdpAttribute::kRtcpMuxAttribute));
-  }
-
-  nsresult rv = AddTransportAttributes(msection, SdpSetupAttribute::kActpass);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  rv = SetRecvonlySsrc(msection);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  AddExtmap(msection);
-
-  std::ostringstream osMid;
-  osMid << "sdparta_" << msection->GetLevel();
-  AddMid(osMid.str(), msection);
-
-  return NS_OK;
-}
-
 mozilla::Sdp*
 JsepSessionImpl::GetParsedLocalDescription(JsepDescriptionPendingOrCurrent type) const
 {
   if (type == kJsepDescriptionPending) {
     return mPendingLocalDescription.get();
   } else if (mPendingLocalDescription &&
              type == kJsepDescriptionPendingOrCurrent) {
     return mPendingLocalDescription.get();
@@ -2651,20 +2286,78 @@ JsepSessionImpl::Close()
 
 const std::string
 JsepSessionImpl::GetLastError() const
 {
   return mLastError;
 }
 
 bool
-JsepSessionImpl::AllLocalTracksAreAssigned() const
+JsepSessionImpl::CheckNegotiationNeeded() const
 {
-  for (const auto& localTrack : mLocalTracks) {
-    if (!localTrack.mAssignedMLine.isSome()) {
-      return false;
+  MOZ_ASSERT(mState == kJsepStateStable);
+
+  for (const auto& transceiver : mTransceivers) {
+    if (transceiver->IsStopped()) {
+      if (transceiver->IsAssociated()) {
+        MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: Negotiation needed because of "
+                  "stopped transceiver that still has a mid.");
+        return true;
+      }
+      continue;
+    }
+
+    if (!transceiver->IsAssociated()) {
+      MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: Negotiation needed because of "
+                "unassociated (but not stopped) transceiver.");
+      return true;
+    }
+
+    if (!mCurrentLocalDescription || !mCurrentRemoteDescription) {
+      MOZ_CRASH("Transceivers should not be associated if we're in stable "
+                "before the first negotiation.");
+      continue;
+    }
+
+    if (!transceiver->HasLevel()) {
+      MOZ_CRASH("Associated transceivers should always have a level.");
+      continue;
+    }
+
+    if (transceiver->GetMediaType() == SdpMediaSection::kApplication) {
+      continue;
+    }
+
+    size_t level = transceiver->GetLevel();
+    const SdpMediaSection& local =
+      mCurrentLocalDescription->GetMediaSection(level);
+    const SdpMediaSection& remote =
+      mCurrentRemoteDescription->GetMediaSection(level);
+
+    if (!local.GetAttributeList().HasAttribute(SdpAttribute::kMsidAttribute) &&
+        (transceiver->mJsDirection & sdp::kSend)) {
+      MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: Negotiation needed because of "
+                "lack of a=msid, and transceiver is sending.");
+      return true;
+    }
+
+    if (IsOfferer()) {
+      if ((local.GetDirection() != transceiver->mJsDirection) &&
+          reverse(remote.GetDirection()) != transceiver->mJsDirection) {
+        MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: Negotiation needed because "
+                  "the direction on our offer, and the remote answer, does not "
+                  "match the direction on a transceiver.");
+        return true;
+      }
+    } else if (local.GetDirection() !=
+          (transceiver->mJsDirection & reverse(remote.GetDirection()))) {
+      MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: Negotiation needed because "
+                "the direction on our answer doesn't match the direction on a "
+                "transceiver, even though the remote offer would have allowed "
+                "it.");
+      return true;
     }
   }
 
-  return true;
+  return false;
 }
 
 } // namespace mozilla
--- a/media/webrtc/signaling/src/jsep/JsepSessionImpl.h
+++ b/media/webrtc/signaling/src/jsep/JsepSessionImpl.h
@@ -5,19 +5,20 @@
 #ifndef _JSEPSESSIONIMPL_H_
 #define _JSEPSESSIONIMPL_H_
 
 #include <set>
 #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/JsepTransceiver.h"
+#include "signaling/src/jsep/SsrcGenerator.h"
 #include "signaling/src/sdp/SipccSdpParser.h"
 #include "signaling/src/sdp/SdpHelper.h"
 #include "signaling/src/common/PtrVector.h"
 
 namespace mozilla {
 
 class JsepUuidGenerator
 {
@@ -43,21 +44,16 @@ public:
         mUuidGen(Move(uuidgen)),
         mSdpHelper(&mLastError)
   {
   }
 
   // Implement JsepSession methods.
   virtual nsresult Init() override;
 
-  virtual nsresult AddTrack(const RefPtr<JsepTrack>& track) override;
-
-  virtual nsresult RemoveTrack(const std::string& streamId,
-                               const std::string& trackId) override;
-
   virtual nsresult SetIceCredentials(const std::string& ufrag,
                                      const std::string& pwd) override;
   virtual const std::string& GetUfrag() const override { return mIceUfrag; }
   virtual const std::string& GetPwd() const override { return mIcePwd; }
   nsresult SetBundlePolicy(JsepBundlePolicy policy) override;
 
   virtual bool
   RemoteIsIceLite() const override
@@ -94,40 +90,19 @@ public:
       SdpDirectionAttribute::Direction::kSendrecv) override;
 
   virtual std::vector<JsepCodecDescription*>&
   Codecs() override
   {
     return mSupportedCodecs.values;
   }
 
-  virtual nsresult ReplaceTrack(const std::string& oldStreamId,
-                                const std::string& oldTrackId,
-                                const std::string& newStreamId,
-                                const std::string& newTrackId) override;
-
-  virtual nsresult SetParameters(
-      const std::string& streamId,
-      const std::string& trackId,
-      const std::vector<JsepTrack::JsConstraints>& constraints) override;
+  virtual std::vector<JsepTrack> GetRemoteTracksAdded() const override;
 
-  virtual nsresult GetParameters(
-      const std::string& streamId,
-      const std::string& trackId,
-      std::vector<JsepTrack::JsConstraints>* outConstraints) override;
-
-  virtual std::vector<RefPtr<JsepTrack>> GetLocalTracks() const override;
-
-  virtual std::vector<RefPtr<JsepTrack>> GetRemoteTracks() const override;
-
-  virtual std::vector<RefPtr<JsepTrack>>
-    GetRemoteTracksAdded() const override;
-
-  virtual std::vector<RefPtr<JsepTrack>>
-    GetRemoteTracksRemoved() const override;
+  virtual std::vector<JsepTrack> GetRemoteTracksRemoved() const override;
 
   virtual nsresult CreateOffer(const JsepOfferOptions& options,
                                std::string* offer) override;
 
   virtual nsresult CreateAnswer(const JsepAnswerOptions& options,
                                 std::string* answer) override;
 
   virtual std::string GetLocalDescription(JsepDescriptionPendingOrCurrent type)
@@ -171,157 +146,125 @@ public:
   }
 
   virtual bool
   IsOfferer() const override
   {
     return mIsOfferer;
   }
 
-  // Access transports.
-  virtual std::vector<RefPtr<JsepTransport>>
-  GetTransports() const override
-  {
-    return mTransports;
+  virtual const std::vector<RefPtr<JsepTransceiver>>&
+    GetTransceivers() const override {
+    return mTransceivers;
   }
 
-  virtual std::vector<JsepTrackPair>
-  GetNegotiatedTrackPairs() const override
-  {
-    return mNegotiatedTrackPairs;
+  virtual std::vector<RefPtr<JsepTransceiver>>&
+    GetTransceivers() override {
+    return mTransceivers;
   }
 
-  virtual bool AllLocalTracksAreAssigned() const override;
+  virtual nsresult AddTransceiver(RefPtr<JsepTransceiver> transceiver) override;
+
+  virtual bool CheckNegotiationNeeded() const override;
 
 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 AddExtmap(SdpMediaSection* msection) const;
+  void AddExtmap(SdpMediaSection* msection);
   void AddMid(const std::string& mid, SdpMediaSection* msection) const;
-  const std::vector<SdpExtmapAttributeList::Extmap>* GetRtpExtensions(
-      SdpMediaSection::MediaType type) const;
+  std::vector<SdpExtmapAttributeList::Extmap> GetRtpExtensions(
+      const SdpMediaSection& msection);
 
   void AddCommonExtmaps(const SdpMediaSection& remoteMsection,
                         SdpMediaSection* msection);
   nsresult SetupIds();
-  nsresult CreateSsrc(uint32_t* ssrc);
   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 ValidateRemoteDescription(const Sdp& description);
   nsresult ValidateOffer(const Sdp& offer);
   nsresult ValidateAnswer(const Sdp& offer, const Sdp& answer);
-  nsresult SetRemoteTracksFromDescription(const Sdp* remoteDescription);
-  // Non-const because we use our Uuid generator
-  nsresult CreateReceivingTrack(size_t mline,
-                                const Sdp& sdp,
-                                const SdpMediaSection& msection,
-                                RefPtr<JsepTrack>* track);
+  nsresult UpdateTransceiversFromRemoteDescription(const Sdp& remote);
+  bool WasMsectionDisabledLastNegotiation(size_t level) const;
+  JsepTransceiver* GetTransceiverForLevel(size_t level);
+  JsepTransceiver* GetTransceiverForLocal(size_t level);
+  JsepTransceiver* GetTransceiverForRemote(const SdpMediaSection& msection);
+  // The w3c and IETF specs have a lot of "magical" behavior that happens when
+  // addTrack is used. This was a deliberate design choice. Sadface.
+  JsepTransceiver* FindUnassociatedTransceiver(
+      SdpMediaSection::MediaType type, bool magic);
+  // Called for rollback of local description
+  void RollbackLocalOffer();
+  // Called for rollback of remote description
+  void RollbackRemoteOffer();
   nsresult HandleNegotiatedSession(const UniquePtr<Sdp>& local,
                                    const UniquePtr<Sdp>& remote);
   nsresult AddTransportAttributes(SdpMediaSection* msection,
                                   SdpSetupAttribute::Role dtlsRole);
   nsresult CopyPreviousTransportParams(const Sdp& oldAnswer,
                                        const Sdp& offerersPreviousSdp,
                                        const Sdp& newOffer,
                                        Sdp* newLocal);
-  nsresult SetupOfferMSections(const JsepOfferOptions& options, Sdp* sdp);
-  // Non-const so it can assign m-line index to tracks
-  nsresult SetupOfferMSectionsByType(SdpMediaSection::MediaType type,
-                                     const Maybe<size_t>& offerToReceive,
-                                     Sdp* sdp);
-  nsresult BindLocalTracks(SdpMediaSection::MediaType mediatype,
-                           Sdp* sdp);
-  nsresult BindRemoteTracks(SdpMediaSection::MediaType mediatype,
-                            Sdp* sdp,
-                            size_t* offerToReceive);
-  nsresult SetRecvAsNeededOrDisable(SdpMediaSection::MediaType mediatype,
-                                    Sdp* sdp,
-                                    size_t* offerToRecv);
-  void SetupOfferToReceiveMsection(SdpMediaSection* offer);
-  nsresult AddRecvonlyMsections(SdpMediaSection::MediaType mediatype,
-                                size_t count,
-                                Sdp* sdp);
-  nsresult AddReofferMsections(const Sdp& oldLocalSdp,
-                               const Sdp& oldAnswer,
-                               Sdp* newSdp);
+  void CopyPreviousMsid(const Sdp& oldLocal, Sdp* newLocal);
+  void EnsureMsid(Sdp* remote);
   void SetupBundle(Sdp* sdp) const;
   nsresult GetRemoteIds(const Sdp& sdp,
                         const SdpMediaSection& msection,
-                        std::string* streamId,
+                        std::vector<std::string>* streamIds,
                         std::string* trackId);
-  nsresult CreateOfferMSection(SdpMediaSection::MediaType type,
-                               SdpMediaSection::Protocol proto,
-                               SdpDirectionAttribute::Direction direction,
-                               Sdp* sdp);
-  nsresult GetFreeMsectionForSend(SdpMediaSection::MediaType type,
-                                  Sdp* sdp,
-                                  SdpMediaSection** msection);
-  nsresult CreateAnswerMSection(const JsepAnswerOptions& options,
-                                size_t mlineIndex,
+  nsresult CreateOfferMsection(const JsepOfferOptions& options,
+                               JsepTransceiver& transceiver,
+                               Sdp* local);
+  nsresult CreateAnswerMsection(const JsepAnswerOptions& options,
+                                JsepTransceiver& transceiver,
                                 const SdpMediaSection& remoteMsection,
                                 Sdp* sdp);
-  nsresult SetRecvonlySsrc(SdpMediaSection* msection);
-  nsresult BindMatchingLocalTrackToAnswer(SdpMediaSection* msection);
-  nsresult BindMatchingRemoteTrackToAnswer(SdpMediaSection* msection);
   nsresult DetermineAnswererSetupRole(const SdpMediaSection& remoteMsection,
                                       SdpSetupAttribute::Role* rolep);
-  nsresult MakeNegotiatedTrackPair(const SdpMediaSection& remote,
-                                   const SdpMediaSection& local,
-                                   const RefPtr<JsepTransport>& transport,
-                                   bool usingBundle,
-                                   size_t transportLevel,
-                                   JsepTrackPair* trackPairOut);
+  nsresult MakeNegotiatedTransceiver(const SdpMediaSection& remote,
+                                     const SdpMediaSection& local,
+                                     bool usingBundle,
+                                     size_t transportLevel,
+                                     JsepTransceiver* transceiverOut);
   void InitTransport(const SdpMediaSection& msection, JsepTransport* transport);
 
   nsresult FinalizeTransport(const SdpAttributeList& remote,
                              const SdpAttributeList& answer,
                              const RefPtr<JsepTransport>& transport);
 
   nsresult GetNegotiatedBundledMids(SdpHelper::BundledMids* bundledMids);
 
   nsresult EnableOfferMsection(SdpMediaSection* msection);
 
   mozilla::Sdp* GetParsedLocalDescription(JsepDescriptionPendingOrCurrent type)
                                           const;
   mozilla::Sdp* GetParsedRemoteDescription(JsepDescriptionPendingOrCurrent type)
                                            const;
   const Sdp* GetAnswer() const;
 
-  std::vector<JsepSendingTrack> mLocalTracks;
-  std::vector<JsepReceivingTrack> mRemoteTracks;
   // By the most recent SetRemoteDescription
-  std::vector<JsepReceivingTrack> mRemoteTracksAdded;
-  std::vector<JsepReceivingTrack> mRemoteTracksRemoved;
-  std::vector<RefPtr<JsepTransport> > mTransports;
-  // So we can rollback
-  std::vector<RefPtr<JsepTransport> > mOldTransports;
-  std::vector<JsepTrackPair> mNegotiatedTrackPairs;
+  std::vector<JsepTrack> mRemoteTracksAdded;
+  std::vector<JsepTrack> mRemoteTracksRemoved;
+  // !!!NOT INDEXED BY LEVEL!!! These are in the order they were created in. The
+  // level mapping is done with JsepTransceiver::mLevel.
+  std::vector<RefPtr<JsepTransceiver>> mTransceivers;
+  // So we can rollback. Not as simple as just going back to the old, though...
+  std::vector<RefPtr<JsepTransceiver>> mOldTransceivers;
 
   bool mIsOfferer;
   bool mWasOffererLastTime;
   bool mIceControlling;
   std::string mIceUfrag;
   std::string mIcePwd;
   bool mLocalIceIsRestarting;
   bool mRemoteIsIceLite;
@@ -330,30 +273,27 @@ private:
   JsepBundlePolicy mBundlePolicy;
   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;
-  std::map<size_t, std::string> mDefaultRemoteTrackIdsByLevel;
   std::string mCNAME;
   // Used to prevent duplicate local SSRCs. Not used to prevent local/remote or
   // remote-only duplication, which will be important for EKT but not now.
   std::set<uint32_t> mSsrcs;
-  // When an m-section doesn't have a local track, it still needs an ssrc, which
-  // is stored here.
-  std::vector<uint32_t> mRecvonlySsrcs;
   UniquePtr<Sdp> mGeneratedLocalDescription; // Created but not set.
   UniquePtr<Sdp> mCurrentLocalDescription;
   UniquePtr<Sdp> mCurrentRemoteDescription;
   UniquePtr<Sdp> mPendingLocalDescription;
   UniquePtr<Sdp> mPendingRemoteDescription;
   PtrVector<JsepCodecDescription> mSupportedCodecs;
   std::string mLastError;
   SipccSdpParser mParser;
   SdpHelper mSdpHelper;
+  SsrcGenerator mSsrcGenerator;
 };
 
 } // namespace mozilla
 
 #endif
--- a/media/webrtc/signaling/src/jsep/JsepTrack.cpp
+++ b/media/webrtc/signaling/src/jsep/JsepTrack.cpp
@@ -2,21 +2,22 @@
  * 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 "signaling/src/jsep/JsepTrack.h"
 #include "signaling/src/jsep/JsepCodecDescription.h"
 #include "signaling/src/jsep/JsepTrackEncoding.h"
 
 #include <algorithm>
+#include <iostream>
 
 namespace mozilla
 {
 void
-JsepTrack::GetNegotiatedPayloadTypes(std::vector<uint16_t>* payloadTypes)
+JsepTrack::GetNegotiatedPayloadTypes(std::vector<uint16_t>* payloadTypes) const
 {
   if (!mNegotiatedDetails) {
     return;
   }
 
   for (const auto* encoding : mNegotiatedDetails->mEncodings.values) {
     GetPayloadTypes(encoding->GetCodecs(), payloadTypes);
   }
@@ -84,80 +85,113 @@ JsepTrack::EnsureNoDuplicatePayloadTypes
         codec->mDefaultPt = os.str();
         break;
       }
     }
   }
 }
 
 void
+JsepTrack::EnsureSsrcs(SsrcGenerator& ssrcGenerator)
+{
+  if (mSsrcs.empty()) {
+    uint32_t ssrc;
+    if (!ssrcGenerator.GenerateSsrc(&ssrc)) {
+      return;
+    }
+    mSsrcs.push_back(ssrc);
+  }
+}
+
+void
 JsepTrack::PopulateCodecs(const std::vector<JsepCodecDescription*>& prototype)
 {
   for (const JsepCodecDescription* prototypeCodec : prototype) {
     if (prototypeCodec->mType == mType) {
       mPrototypeCodecs.values.push_back(prototypeCodec->Clone());
       mPrototypeCodecs.values.back()->mDirection = mDirection;
     }
   }
 
   EnsureNoDuplicatePayloadTypes(&mPrototypeCodecs.values);
 }
 
 void
-JsepTrack::AddToOffer(SdpMediaSection* offer) const
+JsepTrack::AddToOffer(SsrcGenerator& ssrcGenerator, SdpMediaSection* offer)
 {
   AddToMsection(mPrototypeCodecs.values, offer);
+
   if (mDirection == sdp::kSend) {
-    AddToMsection(mJsEncodeConstraints, sdp::kSend, offer);
+    std::vector<JsConstraints> constraints;
+    if (offer->IsSending()) {
+      constraints = mJsEncodeConstraints;
+    }
+    AddToMsection(constraints, sdp::kSend, ssrcGenerator, offer);
   }
 }
 
 void
 JsepTrack::AddToAnswer(const SdpMediaSection& offer,
-                       SdpMediaSection* answer) const
+                       SsrcGenerator& ssrcGenerator,
+                       SdpMediaSection* answer)
 {
   // We do not modify mPrototypeCodecs here, since we're only creating an
   // answer. Once offer/answer concludes, we will update mPrototypeCodecs.
   PtrVector<JsepCodecDescription> codecs;
   codecs.values = GetCodecClones();
   NegotiateCodecs(offer, &codecs.values);
   if (codecs.values.empty()) {
     return;
   }
 
   AddToMsection(codecs.values, answer);
 
   if (mDirection == sdp::kSend) {
-    std::vector<JsConstraints> constraints(mJsEncodeConstraints);
-    std::vector<SdpRidAttributeList::Rid> rids;
-    GetRids(offer, sdp::kRecv, &rids);
-    NegotiateRids(rids, &constraints);
-    AddToMsection(constraints, sdp::kSend, answer);
+    std::vector<JsConstraints> constraints;
+    if (answer->IsSending()) {
+      constraints = mJsEncodeConstraints;
+      std::vector<SdpRidAttributeList::Rid> rids;
+      GetRids(offer, sdp::kRecv, &rids);
+      NegotiateRids(rids, &constraints);
+    }
+    AddToMsection(constraints, sdp::kSend, ssrcGenerator, answer);
   }
 }
 
 void
+JsepTrack::SetJsConstraints(
+    const std::vector<JsConstraints>& constraintsList)
+{
+  mJsEncodeConstraints = constraintsList;
+}
+
+void
 JsepTrack::AddToMsection(const std::vector<JsepCodecDescription*>& codecs,
-                         SdpMediaSection* msection) const
+                         SdpMediaSection* msection)
 {
   MOZ_ASSERT(msection->GetMediaType() == mType);
   MOZ_ASSERT(!codecs.empty());
 
   for (const JsepCodecDescription* codec : codecs) {
     codec->AddToMediaSection(*msection);
   }
 
-  if (mDirection == sdp::kSend) {
-    if (msection->GetMediaType() != SdpMediaSection::kApplication) {
-      msection->SetSsrcs(mSsrcs, mCNAME);
-      msection->AddMsid(mStreamId, mTrackId);
+  if ((mDirection == sdp::kSend) &&
+      (mType != SdpMediaSection::kApplication) &&
+      msection->IsSending()) {
+    if (mStreamIds.empty()) {
+      msection->AddMsid("-", mTrackId);
+    } else {
+      for (const std::string& streamId : mStreamIds) {
+        msection->AddMsid(streamId, mTrackId);
+        // TODO(bug 1402912) Interop hack; older Firefox barfs if there is more
+        // than one msid. Remove when safe.
+        break;
+      }
     }
-    msection->SetSending(true);
-  } else {
-    msection->SetReceiving(true);
   }
 }
 
 // Updates the |id| values in |constraintsList| with the rid values in |rids|,
 // where necessary.
 void
 JsepTrack::NegotiateRids(const std::vector<SdpRidAttributeList::Rid>& rids,
                          std::vector<JsConstraints>* constraintsList) const
@@ -168,20 +202,42 @@ JsepTrack::NegotiateRids(const std::vect
       JsConstraints* constraints = FindConstraints("", *constraintsList);
       if (constraints) {
         constraints->rid = rid.id;
       }
     }
   }
 }
 
-/* static */
+void
+JsepTrack::UpdateSsrcs(SsrcGenerator& ssrcGenerator, size_t encodings)
+{
+  MOZ_ASSERT(mDirection == sdp::kSend);
+  MOZ_ASSERT(mType != SdpMediaSection::kApplication);
+  size_t numSsrcs = std::max<size_t>(encodings, 1U);
+
+  // Right now, the spec does not permit changing the number of encodings after
+  // the initial creation of the sender, so we don't need to worry about things
+  // like a new encoding inserted in between two pre-existing encodings.
+  while (mSsrcs.size() < numSsrcs) {
+    uint32_t ssrc;
+    if (!ssrcGenerator.GenerateSsrc(&ssrc)) {
+      return;
+    }
+    mSsrcs.push_back(ssrc);
+  }
+
+  mSsrcs.resize(numSsrcs);
+  MOZ_ASSERT(!mSsrcs.empty());
+}
+
 void
 JsepTrack::AddToMsection(const std::vector<JsConstraints>& constraintsList,
                          sdp::Direction direction,
+                         SsrcGenerator& ssrcGenerator,
                          SdpMediaSection* msection)
 {
   UniquePtr<SdpSimulcastAttribute> simulcast(new SdpSimulcastAttribute);
   UniquePtr<SdpRidAttributeList> rids(new SdpRidAttributeList);
   for (const JsConstraints& constraints : constraintsList) {
     if (!constraints.rid.empty()) {
       SdpRidAttributeList::Rid rid;
       rid.id = constraints.rid;
@@ -193,20 +249,25 @@ JsepTrack::AddToMsection(const std::vect
       if (direction == sdp::kSend) {
         simulcast->sendVersions.push_back(version);
       } else {
         simulcast->recvVersions.push_back(version);
       }
     }
   }
 
-  if (!rids->mRids.empty()) {
+  if (rids->mRids.size() > 1) {
     msection->GetAttributeList().SetAttribute(simulcast.release());
     msection->GetAttributeList().SetAttribute(rids.release());
   }
+
+  if (mType != SdpMediaSection::kApplication && mDirection == sdp::kSend) {
+    UpdateSsrcs(ssrcGenerator, constraintsList.size());
+    msection->SetSsrcs(mSsrcs, mCNAME);
+  }
 }
 
 void
 JsepTrack::GetRids(const SdpMediaSection& msection,
                    sdp::Direction direction,
                    std::vector<SdpRidAttributeList::Rid>* rids) const
 {
   rids->clear();
@@ -276,16 +337,17 @@ JsepTrack::CreateEncodings(
   }
 
   size_t max_streams = 1;
 
   if (!mJsEncodeConstraints.empty()) {
     max_streams = std::min(rids.size(), mJsEncodeConstraints.size());
   }
   // Drop SSRCs if less RIDs were offered than we have encoding constraints
+  // Just in case.
   if (mSsrcs.size() > max_streams) {
     mSsrcs.resize(max_streams);
   }
 
   // For each stream make sure we have an encoding, and configure
   // that encoding appropriately.
   for (size_t i = 0; i < max_streams; ++i) {
     if (i == negotiatedDetails->mEncodings.values.size()) {
@@ -329,16 +391,17 @@ CompareCodec(const JsepCodecDescription*
 }
 
 void
 JsepTrack::NegotiateCodecs(
     const SdpMediaSection& remote,
     std::vector<JsepCodecDescription*>* codecs,
     std::map<std::string, std::string>* formatChanges) const
 {
+  MOZ_ASSERT(codecs->size());
   PtrVector<JsepCodecDescription> unnegotiatedCodecs;
   std::swap(unnegotiatedCodecs.values, *codecs);
 
   // Outer loop establishes the remote side's preference
   for (const std::string& fmt : remote.GetFormats()) {
     for (size_t i = 0; i < unnegotiatedCodecs.values.size(); ++i) {
       JsepCodecDescription* codec = unnegotiatedCodecs.values[i];
       if (!codec || !codec->mEnabled || !codec->Matches(fmt, remote)) {
@@ -483,41 +546,32 @@ JsepTrack::Negotiate(const SdpMediaSecti
       }
 
       if (direction & mDirection) {
         negotiatedDetails->mExtmap[extmapAttr.extensionname] = extmapAttr;
       }
     }
   }
 
-  if (mDirection == sdp::kRecv) {
-    mSsrcs.clear();
-    if (remote.GetAttributeList().HasAttribute(SdpAttribute::kSsrcAttribute)) {
-      for (auto& ssrcAttr : remote.GetAttributeList().GetSsrc().mSsrcs) {
-        AddSsrc(ssrcAttr.ssrc);
-      }
-    }
-  }
-
   mNegotiatedDetails = Move(negotiatedDetails);
 }
 
 // When doing bundle, if all else fails we can try to figure out which m-line a
 // given RTP packet belongs to by looking at the payload type field. This only
 // works, however, if that payload type appeared in only one m-section.
 // We figure that out here.
 /* static */
 void
-JsepTrack::SetUniquePayloadTypes(const std::vector<RefPtr<JsepTrack>>& tracks)
+JsepTrack::SetUniquePayloadTypes(std::vector<JsepTrack*>& tracks)
 {
   // Maps to track details if no other track contains the payload type,
   // otherwise maps to nullptr.
   std::map<uint16_t, JsepTrackNegotiatedDetails*> payloadTypeToDetailsMap;
 
-  for (const RefPtr<JsepTrack>& track : tracks) {
+  for (JsepTrack* track : tracks) {
     if (track->GetMediaType() == SdpMediaSection::kApplication) {
       continue;
     }
 
     auto* details = track->GetNegotiatedDetails();
     if (!details) {
       // Can happen if negotiation fails on a track
       continue;
--- a/media/webrtc/signaling/src/jsep/JsepTrack.h
+++ b/media/webrtc/signaling/src/jsep/JsepTrack.h
@@ -6,38 +6,45 @@
 #define _JSEPTRACK_H_
 
 #include <functional>
 #include <algorithm>
 #include <string>
 #include <map>
 #include <set>
 
-#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/jsep/JsepTrackEncoding.h"
+#include "signaling/src/jsep/SsrcGenerator.h"
 #include "signaling/src/sdp/Sdp.h"
 #include "signaling/src/sdp/SdpAttribute.h"
 #include "signaling/src/sdp/SdpMediaSection.h"
 #include "signaling/src/common/PtrVector.h"
 
 namespace mozilla {
 
 class JsepTrackNegotiatedDetails
 {
 public:
   JsepTrackNegotiatedDetails() :
     mTias(0)
   {}
 
+  JsepTrackNegotiatedDetails(const JsepTrackNegotiatedDetails& orig) :
+    mExtmap(orig.mExtmap),
+    mUniquePayloadTypes(orig.mUniquePayloadTypes),
+    mTias(orig.mTias)
+  {
+    for (const JsepTrackEncoding* encoding : orig.mEncodings.values) {
+      mEncodings.values.push_back(new JsepTrackEncoding(*encoding));
+    }
+  }
+
   size_t
   GetEncodingCount() const
   {
     return mEncodings.values.size();
   }
 
   const JsepTrackEncoding&
   GetEncoding(size_t index) const
@@ -83,56 +90,108 @@ private:
   PtrVector<JsepTrackEncoding> mEncodings;
   uint32_t mTias; // bits per second
 };
 
 class JsepTrack
 {
 public:
   JsepTrack(mozilla::SdpMediaSection::MediaType type,
-            const std::string& streamid,
-            const std::string& trackid,
-            sdp::Direction direction = sdp::kSend)
+            sdp::Direction direction)
       : mType(type),
-        mStreamId(streamid),
-        mTrackId(trackid),
         mDirection(direction),
         mActive(false)
-  {}
+  {
+  }
+
+  void UpdateTrackIds(const std::vector<std::string>& streamIds,
+                      const std::string& trackId)
+  {
+    mStreamIds = streamIds;
+    mTrackId = trackId;
+  }
+
+  void ClearTrackIds()
+  {
+    mStreamIds.clear();
+    mTrackId.clear();
+  }
+
+  void UpdateRecvTrack(const Sdp& sdp, const SdpMediaSection& msection)
+  {
+    MOZ_ASSERT(mDirection == sdp::kRecv);
+    MOZ_ASSERT(
+        msection.GetMediaType() != SdpMediaSection::MediaType::kApplication);
+    std::string error;
+    SdpHelper helper(&error);
+
+    if (msection.IsSending()) {
+      (void)helper.GetIdsFromMsid(sdp, msection, &mStreamIds, &mTrackId);
+    }
+
+    // We do this whether or not the track is active
+    SetCNAME(helper.GetCNAME(msection));
+    mSsrcs.clear();
+    if (msection.GetAttributeList().HasAttribute(
+          SdpAttribute::kSsrcAttribute)) {
+      for (auto& ssrcAttr : msection.GetAttributeList().GetSsrc().mSsrcs) {
+        mSsrcs.push_back(ssrcAttr.ssrc);
+      }
+    }
+  }
+
+  JsepTrack(const JsepTrack& orig)
+  {
+    *this = orig;
+  }
+
+  JsepTrack(JsepTrack&& orig) = default;
+  JsepTrack& operator=(JsepTrack&& rhs) = default;
+
+  JsepTrack& operator=(const JsepTrack& rhs)
+  {
+    if (this != &rhs) {
+      mType = rhs.mType;
+      mStreamIds = rhs.mStreamIds;
+      mTrackId = rhs.mTrackId;
+      mCNAME = rhs.mCNAME;
+      mDirection = rhs.mDirection;
+      mJsEncodeConstraints = rhs.mJsEncodeConstraints;
+      mSsrcs = rhs.mSsrcs;
+      mActive = rhs.mActive;
+
+      for (const JsepCodecDescription* codec : rhs.mPrototypeCodecs.values) {
+        mPrototypeCodecs.values.push_back(codec->Clone());
+      }
+      if (rhs.mNegotiatedDetails) {
+        mNegotiatedDetails.reset(
+          new JsepTrackNegotiatedDetails(*rhs.mNegotiatedDetails));
+      }
+    }
+    return *this;
+  }
 
   virtual mozilla::SdpMediaSection::MediaType
   GetMediaType() const
   {
     return mType;
   }
 
-  virtual const std::string&
-  GetStreamId() const
+  virtual const std::vector<std::string>&
+  GetStreamIds() const
   {
-    return mStreamId;
-  }
-
-  virtual void
-  SetStreamId(const std::string& id)
-  {
-    mStreamId = id;
+    return mStreamIds;
   }
 
   virtual const std::string&
   GetTrackId() const
   {
     return mTrackId;
   }
 
-  virtual void
-  SetTrackId(const std::string& id)
-  {
-    mTrackId = id;
-  }
-
   virtual const std::string&
   GetCNAME() const
   {
     return mCNAME;
   }
 
   virtual void
   SetCNAME(const std::string& cname)
@@ -147,23 +206,17 @@ public:
   }
 
   virtual const std::vector<uint32_t>&
   GetSsrcs() const
   {
     return mSsrcs;
   }
 
-  virtual void
-  AddSsrc(uint32_t ssrc)
-  {
-    if (mType != SdpMediaSection::kApplication) {
-      mSsrcs.push_back(ssrc);
-    }
-  }
+  virtual void EnsureSsrcs(SsrcGenerator& ssrcGenerator);
 
   bool
   GetActive() const
   {
     return mActive;
   }
 
   void
@@ -184,24 +237,28 @@ public:
 
   template <class BinaryPredicate>
   void SortCodecs(BinaryPredicate sorter)
   {
     std::stable_sort(mPrototypeCodecs.values.begin(),
                      mPrototypeCodecs.values.end(), sorter);
   }
 
-  virtual void AddToOffer(SdpMediaSection* offer) const;
+  // These two are non-const because this is where ssrcs are chosen.
+  virtual void AddToOffer(SsrcGenerator& ssrcGenerator,
+                          SdpMediaSection* offer);
   virtual void AddToAnswer(const SdpMediaSection& offer,
-                           SdpMediaSection* answer) const;
+                           SsrcGenerator& ssrcGenerator,
+                           SdpMediaSection* answer);
+
   virtual void Negotiate(const SdpMediaSection& answer,
                          const SdpMediaSection& remote);
-  static void SetUniquePayloadTypes(
-      const std::vector<RefPtr<JsepTrack>>& tracks);
-  virtual void GetNegotiatedPayloadTypes(std::vector<uint16_t>* payloadTypes);
+  static void SetUniquePayloadTypes(std::vector<JsepTrack*>& tracks);
+  virtual void GetNegotiatedPayloadTypes(
+      std::vector<uint16_t>* payloadTypes) const;
 
   // This will be set when negotiation is carried out.
   virtual const JsepTrackNegotiatedDetails*
   GetNegotiatedDetails() const
   {
     if (mNegotiatedDetails) {
       return mNegotiatedDetails.get();
     }
@@ -218,53 +275,47 @@ public:
   }
 
   virtual void
   ClearNegotiatedDetails()
   {
     mNegotiatedDetails.reset();
   }
 
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(JsepTrack);
-
   struct JsConstraints
   {
     std::string rid;
     EncodingConstraints constraints;
   };
 
-  void SetJsConstraints(const std::vector<JsConstraints>& constraintsList)
-  {
-    mJsEncodeConstraints = constraintsList;
-  }
+  void SetJsConstraints(const std::vector<JsConstraints>& constraintsList);
 
   void GetJsConstraints(std::vector<JsConstraints>* outConstraintsList) const
   {
     MOZ_ASSERT(outConstraintsList);
     *outConstraintsList = mJsEncodeConstraints;
   }
 
-  static void AddToMsection(const std::vector<JsConstraints>& constraintsList,
-                            sdp::Direction direction,
-                            SdpMediaSection* msection);
+  void AddToMsection(const std::vector<JsConstraints>& constraintsList,
+                     sdp::Direction direction,
+                     SsrcGenerator& ssrcGenerator,
+                     SdpMediaSection* msection);
 
-protected:
-  virtual ~JsepTrack() {}
 
 private:
   std::vector<JsepCodecDescription*> GetCodecClones() const;
   static void EnsureNoDuplicatePayloadTypes(
       std::vector<JsepCodecDescription*>* codecs);
   static void GetPayloadTypes(
       const std::vector<JsepCodecDescription*>& codecs,
       std::vector<uint16_t>* pts);
   static void EnsurePayloadTypeIsUnique(std::set<uint16_t>* uniquePayloadTypes,
                                         JsepCodecDescription* codec);
   void AddToMsection(const std::vector<JsepCodecDescription*>& codecs,
-                     SdpMediaSection* msection) const;
+                     SdpMediaSection* msection);
   void GetRids(const SdpMediaSection& msection,
                sdp::Direction direction,
                std::vector<SdpRidAttributeList::Rid>* rids) const;
   void CreateEncodings(
       const SdpMediaSection& remote,
       const std::vector<JsepCodecDescription*>& negotiatedCodecs,
       JsepTrackNegotiatedDetails* details);
 
@@ -276,53 +327,29 @@ private:
       std::vector<JsepCodecDescription*>* codecs,
       std::map<std::string, std::string>* formatChanges = nullptr) const;
 
   JsConstraints* FindConstraints(
       const std::string& rid,
       std::vector<JsConstraints>& constraintsList) const;
   void NegotiateRids(const std::vector<SdpRidAttributeList::Rid>& rids,
                      std::vector<JsConstraints>* constraints) const;
+  void UpdateSsrcs(SsrcGenerator& ssrcGenerator, size_t encodings);
 
-  const mozilla::SdpMediaSection::MediaType mType;
-  std::string mStreamId;
+  mozilla::SdpMediaSection::MediaType mType;
+  // These are the ids that everyone outside of JsepSession care about
+  std::vector<std::string> mStreamIds;
   std::string mTrackId;
   std::string mCNAME;
-  const sdp::Direction mDirection;
+  sdp::Direction mDirection;
   PtrVector<JsepCodecDescription> mPrototypeCodecs;
   // Holds encoding params/constraints from JS. Simulcast happens when there are
   // multiple of these. If there are none, we assume unconstrained unicast with
   // no rid.
   std::vector<JsConstraints> mJsEncodeConstraints;
   UniquePtr<JsepTrackNegotiatedDetails> mNegotiatedDetails;
   std::vector<uint32_t> mSsrcs;
   bool mActive;
 };
 
-// Need a better name for this.
-struct JsepTrackPair {
-  size_t mLevel;
-  // Is this track pair sharing a transport with another?
-  size_t mBundleLevel = SIZE_MAX; // SIZE_MAX if no bundle level
-  uint32_t mRecvonlySsrc;
-  RefPtr<JsepTrack> mSending;
-  RefPtr<JsepTrack> mReceiving;
-  RefPtr<JsepTransport> mRtpTransport;
-  RefPtr<JsepTransport> mRtcpTransport;
-
-  bool HasBundleLevel() const {
-    return mBundleLevel != SIZE_MAX;
-  }
-
-  size_t BundleLevel() const {
-    MOZ_ASSERT(HasBundleLevel());
-    return mBundleLevel;
-  }
-
-  void SetBundleLevel(size_t aBundleLevel) {
-    MOZ_ASSERT(aBundleLevel != SIZE_MAX);
-    mBundleLevel = aBundleLevel;
-  }
-};
-
 } // namespace mozilla
 
 #endif
--- a/media/webrtc/signaling/src/jsep/JsepTrackEncoding.h
+++ b/media/webrtc/signaling/src/jsep/JsepTrackEncoding.h
@@ -14,16 +14,26 @@
 namespace mozilla {
 // Represents a single encoding of a media track. When simulcast is used, there
 // may be multiple. Each encoding may have some constraints (imposed by JS), and
 // may be able to use any one of multiple codecs (JsepCodecDescription) at any
 // given time.
 class JsepTrackEncoding
 {
 public:
+  JsepTrackEncoding() = default;
+  JsepTrackEncoding(const JsepTrackEncoding& orig) :
+    mConstraints(orig.mConstraints),
+    mRid(orig.mRid)
+  {
+    for (const JsepCodecDescription* codec : orig.mCodecs.values) {
+      mCodecs.values.push_back(codec->Clone());
+    }
+  }
+
   const std::vector<JsepCodecDescription*>& GetCodecs() const
   {
     return mCodecs.values;
   }
 
   void AddCodec(const JsepCodecDescription& codec)
   {
     mCodecs.values.push_back(codec.Clone());
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/jsep/JsepTransceiver.h
@@ -0,0 +1,235 @@
+/* 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 _JSEPTRANSCEIVER_H_
+#define _JSEPTRANSCEIVER_H_
+
+#include <string>
+
+#include "signaling/src/sdp/SdpAttribute.h"
+#include "signaling/src/sdp/SdpMediaSection.h"
+#include "signaling/src/sdp/Sdp.h"
+#include "signaling/src/jsep/JsepTransport.h"
+#include "signaling/src/jsep/JsepTrack.h"
+
+#include <mozilla/OwningNonNull.h>
+#include "nsISupportsImpl.h"
+#include "nsError.h"
+
+namespace mozilla {
+
+class JsepTransceiver {
+  private:
+    ~JsepTransceiver() {};
+
+  public:
+    explicit JsepTransceiver(SdpMediaSection::MediaType type,
+                             SdpDirectionAttribute::Direction jsDirection =
+                                 SdpDirectionAttribute::kSendrecv) :
+      mJsDirection(jsDirection),
+      mSendTrack(type, sdp::kSend),
+      mRecvTrack(type, sdp::kRecv),
+      mTransport(*(new JsepTransport)),
+      mLevel(SIZE_MAX),
+      mBundleLevel(SIZE_MAX),
+      mAddTrackMagic(false),
+      mWasCreatedBySetRemote(false),
+      mStopped(false),
+      mRemoved(false),
+      mNegotiated(false)
+    {}
+
+    // Can't use default copy c'tor because of the refcount members. Ugh.
+    JsepTransceiver(const JsepTransceiver& orig) :
+      mJsDirection(orig.mJsDirection),
+      mSendTrack(orig.mSendTrack),
+      mRecvTrack(orig.mRecvTrack),
+      mTransport(*(new JsepTransport(orig.mTransport))),
+      mMid(orig.mMid),
+      mLevel(orig.mLevel),
+      mBundleLevel(orig.mBundleLevel),
+      mAddTrackMagic(orig.mAddTrackMagic),
+      mWasCreatedBySetRemote(orig.mWasCreatedBySetRemote),
+      mStopped(orig.mStopped),
+      mRemoved(orig.mRemoved),
+      mNegotiated(orig.mNegotiated)
+    {}
+
+    NS_INLINE_DECL_THREADSAFE_REFCOUNTING(JsepTransceiver);
+
+    void Rollback(JsepTransceiver& oldTransceiver)
+    {
+      *mTransport = *oldTransceiver.mTransport;
+      mLevel = oldTransceiver.mLevel;
+      mBundleLevel = oldTransceiver.mBundleLevel;
+      mRecvTrack = oldTransceiver.mRecvTrack;
+
+      // stop() caused by a disabled m-section in a remote offer cannot be
+      // rolled back.
+      if (!IsStopped()) {
+        mMid = oldTransceiver.mMid;
+      }
+    }
+
+    bool IsAssociated() const
+    {
+      return !mMid.empty();
+    }
+
+    const std::string& GetMid() const
+    {
+      MOZ_ASSERT(IsAssociated());
+      return mMid;
+    }
+
+    void Associate(const std::string& mid)
+    {
+      MOZ_ASSERT(HasLevel());
+      mMid = mid;
+    }
+
+    void Disassociate()
+    {
+      mMid.clear();
+    }
+
+    bool HasLevel() const
+    {
+      return mLevel != SIZE_MAX;
+    }
+
+    void SetLevel(size_t level)
+    {
+      MOZ_ASSERT(level != SIZE_MAX);
+      MOZ_ASSERT(!HasLevel());
+      MOZ_ASSERT(!IsStopped());
+
+      mLevel = level;
+    }
+
+    void ClearLevel()
+    {
+      MOZ_ASSERT(mStopped);
+      MOZ_ASSERT(!IsAssociated());
+      mLevel = SIZE_MAX;
+    }
+
+    size_t GetLevel() const
+    {
+      MOZ_ASSERT(HasLevel());
+      return mLevel;
+    }
+
+    void Stop()
+    {
+      mStopped = true;
+    }
+
+    bool IsStopped() const
+    {
+      return mStopped;
+    }
+
+    void SetRemoved()
+    {
+      mRemoved = true;
+    }
+
+    bool IsRemoved() const
+    {
+      return mRemoved;
+    }
+
+    bool HasBundleLevel() const {
+      return mBundleLevel != SIZE_MAX;
+    }
+
+    size_t BundleLevel() const {
+      MOZ_ASSERT(HasBundleLevel());
+      return mBundleLevel;
+    }
+
+    void SetBundleLevel(size_t aBundleLevel) {
+      MOZ_ASSERT(aBundleLevel != SIZE_MAX);
+      mBundleLevel = aBundleLevel;
+    }
+
+    void ClearBundleLevel()
+    {
+      mBundleLevel = SIZE_MAX;
+    }
+
+    size_t GetTransportLevel() const
+    {
+      MOZ_ASSERT(HasLevel());
+      if (HasBundleLevel()) {
+        return BundleLevel();
+      }
+      return GetLevel();
+    }
+
+    void SetAddTrackMagic()
+    {
+      mAddTrackMagic = true;
+    }
+
+    bool HasAddTrackMagic() const
+    {
+      return mAddTrackMagic;
+    }
+
+    void SetCreatedBySetRemote()
+    {
+      mWasCreatedBySetRemote = true;
+    }
+
+    bool WasCreatedBySetRemote() const
+    {
+      return mWasCreatedBySetRemote;
+    }
+
+    void SetNegotiated()
+    {
+      MOZ_ASSERT(IsAssociated());
+      MOZ_ASSERT(HasLevel());
+      mNegotiated = true;
+    }
+
+    bool IsNegotiated() const
+    {
+      return mNegotiated;
+    }
+
+    // Convenience function
+    SdpMediaSection::MediaType GetMediaType() const
+    {
+      return mRecvTrack.GetMediaType();
+    }
+
+    // This is the direction JS wants. It might not actually happen.
+    SdpDirectionAttribute::Direction mJsDirection;
+
+    JsepTrack mSendTrack;
+    JsepTrack mRecvTrack;
+    OwningNonNull<JsepTransport> mTransport;
+
+  private:
+    // Stuff that is not negotiated
+    std::string mMid;
+    size_t mLevel; // SIZE_MAX if no level
+    // Is this track pair sharing a transport with another?
+    size_t mBundleLevel; // SIZE_MAX if no bundle level
+    // The w3c and IETF specs have a lot of "magical" behavior that happens
+    // when addTrack is used. This was a deliberate design choice. Sadface.
+    bool mAddTrackMagic;
+    bool mWasCreatedBySetRemote;
+    bool mStopped;
+    bool mRemoved;
+    bool mNegotiated;
+};
+
+} // namespace mozilla
+
+#endif // _JSEPTRANSCEIVER_H_
+
--- a/media/webrtc/signaling/src/jsep/JsepTransport.h
+++ b/media/webrtc/signaling/src/jsep/JsepTransport.h
@@ -82,16 +82,32 @@ private:
 class JsepTransport
 {
 public:
   JsepTransport()
       : mComponents(0)
   {
   }
 
+  JsepTransport(const JsepTransport& orig)
+  {
+    *this = orig;
+  }
+
+  JsepTransport& operator=(const JsepTransport& orig)
+  {
+    if (this != &orig) {
+      mIce.reset(orig.mIce ? new JsepIceTransport(*orig.mIce) : nullptr);
+      mDtls.reset(orig.mDtls ? new JsepDtlsTransport(*orig.mDtls) : nullptr);
+      mTransportId = orig.mTransportId;
+      mComponents = orig.mComponents;
+    }
+    return *this;
+  }
+
   void Close()
   {
     mComponents = 0;
     mTransportId.clear();
     mIce.reset();
     mDtls.reset();
   }
 
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/jsep/SsrcGenerator.cpp
@@ -0,0 +1,24 @@
+/* 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 "signaling/src/jsep/SsrcGenerator.h"
+#include "pk11pub.h"
+
+namespace mozilla {
+
+bool
+SsrcGenerator::GenerateSsrc(uint32_t* ssrc)
+{
+  do {
+    SECStatus rv = PK11_GenerateRandom(
+        reinterpret_cast<unsigned char*>(ssrc), sizeof(uint32_t));
+    if (rv != SECSuccess) {
+      return false;
+    }
+  } while (mSsrcs.count(*ssrc));
+  mSsrcs.insert(*ssrc);
+
+  return true;
+}
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/jsep/SsrcGenerator.h
@@ -0,0 +1,20 @@
+/* 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 _SSRCGENERATOR_H_
+#define _SSRCGENERATOR_H_
+
+#include <set>
+
+namespace mozilla {
+class SsrcGenerator {
+  public:
+    bool GenerateSsrc(uint32_t* ssrc);
+  private:
+    std::set<uint32_t> mSsrcs;
+};
+} // namespace mozilla
+
+#endif // _SSRCGENERATOR_H_
+
--- a/media/webrtc/signaling/src/jsep/moz.build
+++ b/media/webrtc/signaling/src/jsep/moz.build
@@ -8,12 +8,13 @@ include('/media/webrtc/webrtc.mozbuild')
 LOCAL_INCLUDES += [
     '/media/mtransport',
     '/media/webrtc',
     '/media/webrtc/trunk',
 ]
 
 UNIFIED_SOURCES += [
     'JsepSessionImpl.cpp',
-    'JsepTrack.cpp'
+    'JsepTrack.cpp',
+    'SsrcGenerator.cpp'
 ]
 
 FINAL_LIBRARY = 'xul'
--- a/media/webrtc/signaling/src/sdp/SdpAttribute.h
+++ b/media/webrtc/signaling/src/sdp/SdpAttribute.h
@@ -245,16 +245,32 @@ operator|(SdpDirectionAttribute::Directi
 
 inline SdpDirectionAttribute::Direction
 operator&(SdpDirectionAttribute::Direction d1,
           SdpDirectionAttribute::Direction d2)
 {
   return (SdpDirectionAttribute::Direction)((unsigned)d1 & (unsigned)d2);
 }
 
+inline SdpDirectionAttribute::Direction
+operator|=(SdpDirectionAttribute::Direction& d1,
+           SdpDirectionAttribute::Direction d2)
+{
+  d1 = d1 | d2;
+  return d1;
+}
+
+inline SdpDirectionAttribute::Direction
+operator&=(SdpDirectionAttribute::Direction& d1,
+           SdpDirectionAttribute::Direction d2)
+{
+  d1 = d1 & d2;
+  return d1;
+}
+
 ///////////////////////////////////////////////////////////////////////////
 // a=dtls-message, draft-rescorla-dtls-in-sdp
 //-------------------------------------------------------------------------
 //   attribute               =/   dtls-message-attribute
 //
 //   dtls-message-attribute  =    "dtls-message" ":" role SP value
 //
 //   role                    =    "client" / "server"
--- a/media/webrtc/signaling/src/sdp/SdpHelper.cpp
+++ b/media/webrtc/signaling/src/sdp/SdpHelper.cpp
@@ -448,19 +448,19 @@ SdpHelper::SetDefaultAddresses(const std
           sdp::kInternet,
           ipVersion,
           defaultRtcpCandidateAddr));
   }
 }
 
 nsresult
 SdpHelper::GetIdsFromMsid(const Sdp& sdp,
-                                const SdpMediaSection& msection,
-                                std::string* streamId,
-                                std::string* trackId)
+                          const SdpMediaSection& msection,
+                          std::vector<std::string>* streamIds,
+                          std::string* trackId)
 {
   if (!sdp.GetAttributeList().HasAttribute(
         SdpAttribute::kMsidSemanticAttribute)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   auto& msidSemantics = sdp.GetAttributeList().GetMsidSemantic().mMsidSemantics;
   std::vector<SdpMsidAttributeList::Msid> allMsids;
@@ -488,23 +488,28 @@ SdpHelper::GetIdsFromMsid(const Sdp& sdp
   for (auto i = allMsids.begin(); i != allMsids.end(); ++i) {
     if (allMsidsAreWebrtc || webrtcMsids.count(i->identifier)) {
       if (i->appdata.empty()) {
         SDP_SET_ERROR("Invalid webrtc msid at level " << msection.GetLevel()
                        << ": Missing track id.");
         return NS_ERROR_INVALID_ARG;
       }
       if (!found) {
-        *streamId = i->identifier;
         *trackId = i->appdata;
+        streamIds->clear();
         found = true;
-      } else if ((*streamId != i->identifier) || (*trackId != i->appdata)) {
-        MOZ_MTLOG(ML_WARNING, "Found multiple different webrtc msids in "
-                       "m-section " << msection.GetLevel() << ". The "
-                       "behavior w/o transceivers is undefined.");
+      } else if ((*trackId != i->appdata)) {
+        SDP_SET_ERROR("Found multiple different webrtc track ids in m-section "
+                       << msection.GetLevel() << ". The behavior here is "
+                       "undefined.");
+        return NS_ERROR_INVALID_ARG;
+      }
+      // "-" means no stream, see draft-ietf-mmusic-msid
+      if (i->identifier != "-") {
+        streamIds->push_back(i->identifier);
       }
     }
   }
 
   if (!found) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
--- a/media/webrtc/signaling/src/sdp/SdpHelper.h
+++ b/media/webrtc/signaling/src/sdp/SdpHelper.h
@@ -58,17 +58,17 @@ class SdpHelper {
         const Sdp& sdp,
         std::vector<SdpGroupAttributeList::Group>* groups) const;
 
     nsresult GetMidFromLevel(const Sdp& sdp,
                              uint16_t level,
                              std::string* mid);
     nsresult GetIdsFromMsid(const Sdp& sdp,
                             const SdpMediaSection& msection,
-                            std::string* streamId,
+                            std::vector<std::string>* streamId,
                             std::string* trackId);
     nsresult GetMsids(const SdpMediaSection& msection,
                       std::vector<SdpMsidAttributeList::Msid>* msids);
     nsresult ParseMsid(const std::string& msidAttribute,
                        std::string* streamId,
                        std::string* trackId);
     nsresult AddCandidateToSdp(Sdp* sdp,
                                const std::string& candidate,
--- a/media/webrtc/signaling/src/sdp/SdpMediaSection.h
+++ b/media/webrtc/signaling/src/sdp/SdpMediaSection.h
@@ -103,60 +103,65 @@ public:
   GetLevel() const
   {
     return mLevel;
   }
 
   inline bool
   IsReceiving() const
   {
-    return GetDirectionAttribute().mValue & sdp::kRecv;
+    return GetDirection() & sdp::kRecv;
   }
 
   inline bool
   IsSending() const
   {
-    return GetDirectionAttribute().mValue & sdp::kSend;
+    return GetDirection() & sdp::kSend;
   }
 
   inline void
   SetReceiving(bool receiving)
   {
-    auto direction = GetDirectionAttribute().mValue;
+    auto direction = GetDirection();
     if (direction & sdp::kSend) {
       SetDirection(receiving ?
                    SdpDirectionAttribute::kSendrecv :
                    SdpDirectionAttribute::kSendonly);
     } else {
       SetDirection(receiving ?
                    SdpDirectionAttribute::kRecvonly :
                    SdpDirectionAttribute::kInactive);
     }
   }
 
   inline void
   SetSending(bool sending)
   {
-    auto direction = GetDirectionAttribute().mValue;
+    auto direction = GetDirection();
     if (direction & sdp::kRecv) {
       SetDirection(sending ?
                    SdpDirectionAttribute::kSendrecv :
                    SdpDirectionAttribute::kRecvonly);
     } else {
       SetDirection(sending ?
                    SdpDirectionAttribute::kSendonly :
                    SdpDirectionAttribute::kInactive);
     }
   }
 
   inline void SetDirection(SdpDirectionAttribute::Direction direction)
   {
     GetAttributeList().SetAttribute(new SdpDirectionAttribute(direction));
   }
 
+  inline SdpDirectionAttribute::Direction GetDirection() const
+  {
+    return GetDirectionAttribute().mValue;
+  }
+
   const SdpFmtpAttributeList::Parameters* FindFmtp(const std::string& pt) const;
   void SetFmtp(const SdpFmtpAttributeList::Fmtp& fmtp);
   void RemoveFmtp(const std::string& pt);
   const SdpRtpmapAttributeList::Rtpmap* FindRtpmap(const std::string& pt) const;
   const SdpSctpmapAttributeList::Sctpmap* GetSctpmap() const;
   uint32_t GetSctpPort() const;
   bool GetMaxMessageSize(uint32_t* size) const;
   bool HasRtcpFb(const std::string& pt,