Update to libjingle 0.6.6

git-svn-id: http://libjingle.googlecode.com/svn/trunk@101 dd674b97-3498-5ee5-1854-bdd07cd0ff33
diff --git a/CHANGELOG b/CHANGELOG
index cacbc90..ab264a1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,13 @@
 Libjingle
 
-0.6.5 - Dec 12, 2011
+0.6.6 - Dec 14, 2011
+  - Fix support for rtcp multiplexing (aka rtcp-mux).
+  - Add more support for FreeBSD and OpenBSD.
+  - Add more unit tests to session/phone.
+  - Add session/phone/mediarecorder.cc.
+  - Fixed httpportallocator tests.
+
+0.6.5 - Dec 8, 2011
   - Add IPv6 support in SocketAddress.
   - Change PeerConnectionFactory inteface.
   - Bug fixes.
diff --git a/talk/app/webrtcv1/webrtcv1.scons b/talk/app/webrtcv1/webrtcv1.scons
new file mode 100644
index 0000000..a159290
--- /dev/null
+++ b/talk/app/webrtcv1/webrtcv1.scons
@@ -0,0 +1,60 @@
+# -*- Python -*-
+import talk
+
+Import('env')
+
+# local sources
+talk.Library(
+  env,
+  name = 'webrtcv1',
+  srcs = [
+    'peerconnectionimpl.cc',
+    'peerconnectionproxy.cc',
+    'peerconnectionfactory.cc',
+    'webrtcjson.cc',
+    'webrtcsession.cc',
+  ],
+)
+
+talk.Unittest(
+  env,
+  name = 'webrtcv1',
+  srcs = [
+    'peerconnection_unittest.cc',
+    'unittest_utilities.cc',
+    'webrtcsession_unittest.cc',
+  ],
+  libs = [
+    'base',
+    'expat',
+    'jpeg',
+    'json',
+    'p2p',
+    'phone',
+    'srtp',
+    'webrtcv1',
+    'xmpp',
+    'xmllite',
+    'yuvscaler'
+  ],
+  include_talk_media_libs = True,
+  mac_libs = [
+    'crypto',
+    'ssl',
+  ],
+  mac_FRAMEWORKS = [
+    'Foundation',
+    'IOKit',
+    'QTKit',
+  ],
+  win_link_flags = [('', '/nodefaultlib:libcmt')[env.Bit('debug')]],
+  lin_libs = [
+    'rt',
+    'dl',
+    'sound',
+    'X11',
+    'Xext',
+    'Xfixes',
+    'Xrandr'
+  ],
+)
diff --git a/talk/base/basictypes.h b/talk/base/basictypes.h
index ed993dc..f16ccf2 100644
--- a/talk/base/basictypes.h
+++ b/talk/base/basictypes.h
@@ -30,7 +30,7 @@
 
 #include <stddef.h>  // for NULL, size_t
 
-#ifndef WIN32
+#if !(defined(_MSC_VER) && (_MSC_VER < 1600))
 #include <stdint.h>  // for uintptr_t
 #endif
 
@@ -91,7 +91,7 @@
 typedef unsigned short uint16;
 typedef short int16;
 typedef unsigned char uint8;
-typedef char int8;
+typedef signed char int8;
 #endif  // INT_TYPES_DEFINED
 
 #ifdef WIN32
diff --git a/talk/base/ipaddress.cc b/talk/base/ipaddress.cc
index 264c787..9069269 100644
--- a/talk/base/ipaddress.cc
+++ b/talk/base/ipaddress.cc
@@ -29,6 +29,9 @@
 #include <sys/types.h>
 #include <sys/socket.h>
 #include <netinet/in.h>
+#ifdef OPENBSD
+#include <netinet/in_systm.h>
+#endif
 #include <netinet/ip.h>
 #include <arpa/inet.h>
 #include <netdb.h>
diff --git a/talk/base/ipaddress.h b/talk/base/ipaddress.h
index f481ed3..a421f16 100644
--- a/talk/base/ipaddress.h
+++ b/talk/base/ipaddress.h
@@ -30,6 +30,7 @@
 
 #ifdef POSIX
 #include <netinet/in.h>
+#include <sys/socket.h>
 #include <arpa/inet.h>
 #include <netdb.h>
 #endif
diff --git a/talk/base/nethelpers.cc b/talk/base/nethelpers.cc
index 52ec229..3961fc0 100644
--- a/talk/base/nethelpers.cc
+++ b/talk/base/nethelpers.cc
@@ -165,7 +165,7 @@
   result = deep_copy;
 #endif
   *herrno = 0;
-#elif defined(OSX) || defined(IOS)
+#elif defined(OSX) || defined(IOS) || defined(FREEBSD)
   // Mac OS returns an object with everything allocated.
   result = getipnodebyname(hostname, AF_INET, AI_DEFAULT, herrno);
 #elif defined(OPENBSD)
@@ -184,7 +184,7 @@
 // This function should mirror the above function, and free any resources
 // allocated by the above.
 void FreeHostEnt(hostent* host) {
-#if defined(OSX) || defined(IOS)
+#if defined(OSX) || defined(IOS) || defined(FREEBSD)
   freehostent(host);
 #elif defined(WIN32) || defined(POSIX)
   free(host);
diff --git a/talk/p2p/base/fakesession.h b/talk/p2p/base/fakesession.h
new file mode 100644
index 0000000..1928613
--- /dev/null
+++ b/talk/p2p/base/fakesession.h
@@ -0,0 +1,233 @@
+// libjingle
+// Copyright 2009 Google Inc.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//  1. Redistributions of source code must retain the above copyright notice,
+//     this list of conditions and the following disclaimer.
+//  2. Redistributions in binary form must reproduce the above copyright notice,
+//     this list of conditions and the following disclaimer in the documentation
+//     and/or other materials provided with the distribution.
+//  3. The name of the author may not be used to endorse or promote products
+//     derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef TALK_SESSION_PHONE_FAKESESSION_H_
+#define TALK_SESSION_PHONE_FAKESESSION_H_
+
+#include <map>
+#include <string>
+
+#include "talk/p2p/base/session.h"
+#include "talk/p2p/base/transport.h"
+#include "talk/p2p/base/transportchannel.h"
+#include "talk/p2p/base/transportchannelimpl.h"
+
+namespace cricket {
+
+class FakeTransport;
+
+// Fake transport channel class, which can be passed to anything that needs a
+// transport channel. Can be informed of another FakeTransportChannel via
+// SetDestination.
+class FakeTransportChannel : public TransportChannelImpl {
+ public:
+  explicit FakeTransportChannel(Transport* transport,
+                                const std::string& name,
+                                const std::string& session_type)
+      : TransportChannelImpl(name, session_type),
+        transport_(transport),
+        dest_(NULL),
+        state_(STATE_INIT) {
+  }
+  ~FakeTransportChannel() {
+    Reset();
+  }
+
+  virtual Transport* GetTransport() {
+    return transport_;
+  }
+  virtual void Connect() {
+    if (state_ == STATE_INIT) {
+      state_ = STATE_CONNECTING;
+    }
+  }
+  virtual void Reset() {
+    if (state_ != STATE_INIT) {
+      state_ = STATE_INIT;
+      if (dest_) {
+        dest_->state_ = STATE_INIT;
+        dest_->dest_ = NULL;
+        dest_ = NULL;
+      }
+    }
+  }
+
+  void SetDestination(FakeTransportChannel* dest) {
+    if (state_ == STATE_CONNECTING && dest) {
+      // This simulates the delivery of candidates.
+      dest_ = dest;
+      dest_->dest_ = this;
+      state_ = STATE_CONNECTED;
+      dest_->state_ = STATE_CONNECTED;
+      set_writable(true);
+      dest_->set_writable(true);
+    } else if (state_ == STATE_CONNECTED && !dest) {
+      // Simulates loss of connectivity, by asymmetrically forgetting dest_.
+      dest_ = NULL;
+      state_ = STATE_CONNECTING;
+      set_writable(false);
+    }
+  }
+
+  virtual int SendPacket(const char *data, size_t len) {
+    if (state_ != STATE_CONNECTED) {
+      return -1;
+    }
+    dest_->SignalReadPacket(dest_, data, len);
+    return len;
+  }
+  virtual int SetOption(talk_base::Socket::Option opt, int value) {
+    return true;
+  }
+  virtual int GetError() {
+    return 0;
+  }
+
+  virtual void OnSignalingReady() {
+  }
+  virtual void OnCandidate(const Candidate& candidate) {
+  }
+
+ private:
+  enum State { STATE_INIT, STATE_CONNECTING, STATE_CONNECTED };
+  Transport* transport_;
+  FakeTransportChannel* dest_;
+  State state_;
+};
+
+// Fake transport class, which can be passed to anything that needs a Transport.
+// Can be informed of another FakeTransport via SetDestination (low-tech way
+// of doing candidates)
+class FakeTransport : public Transport {
+ public:
+  typedef std::map<std::string, FakeTransportChannel*> ChannelMap;
+  FakeTransport(talk_base::Thread* signaling_thread,
+                talk_base::Thread* worker_thread)
+      : Transport(signaling_thread, worker_thread, "test", NULL),
+        dest_(NULL) {
+  }
+  ~FakeTransport() {
+    DestroyAllChannels();
+  }
+
+  const ChannelMap& channels() const { return channels_; }
+
+  void SetDestination(FakeTransport* dest) {
+    dest_ = dest;
+    for (ChannelMap::iterator it = channels_.begin(); it != channels_.end();
+         ++it) {
+      SetChannelDestination(it->first, it->second);
+    }
+  }
+
+ protected:
+  virtual TransportChannelImpl* CreateTransportChannel(
+      const std::string& name, const std::string& session_type) {
+    if (channels_.find(name) != channels_.end()) {
+      return NULL;
+    }
+    FakeTransportChannel* channel =
+        new FakeTransportChannel(this, name, session_type);
+    SetChannelDestination(name, channel);
+    channels_[name] = channel;
+    return channel;
+  }
+  virtual void DestroyTransportChannel(TransportChannelImpl* channel) {
+    channels_.erase(channel->name());
+    delete channel;
+  }
+
+ private:
+  void SetChannelDestination(const std::string& name,
+                             FakeTransportChannel* channel) {
+    FakeTransportChannel* dest_channel = NULL;
+    if (dest_) {
+      dest_channel =
+          static_cast<FakeTransportChannel*>(dest_->GetChannel(name));
+    }
+    channel->SetDestination(dest_channel);
+  }
+
+  ChannelMap channels_;
+  FakeTransport* dest_;
+};
+
+// Fake session class, which can be passed into a BaseChannel object for
+// test purposes. Can be connected to other FakeSessions via Connect().
+class FakeSession : public BaseSession {
+ public:
+  FakeSession()
+      : BaseSession(talk_base::Thread::Current(),
+                    talk_base::Thread::Current(),
+                    NULL, "", "", true),
+        fail_create_channel_(false) {
+  }
+
+  FakeTransport* GetTransport(const std::string& content_name) {
+    return static_cast<FakeTransport*>(
+        BaseSession::GetTransport(content_name));
+  }
+
+  void Connect(FakeSession* dest) {
+    // Simulate the exchange of candidates.
+    CompleteNegotiation();
+    dest->CompleteNegotiation();
+    for (TransportMap::const_iterator it = transport_proxies().begin();
+        it != transport_proxies().end(); ++it) {
+      static_cast<FakeTransport*>(it->second->impl())->SetDestination(
+          dest->GetTransport(it->first));
+    }
+  }
+
+  virtual cricket::TransportChannel* CreateChannel(
+      const std::string& content_name, const std::string& name) {
+    if (fail_create_channel_) {
+      return NULL;
+    }
+    return BaseSession::CreateChannel(content_name, name);
+  }
+
+  void set_fail_channel_creation(bool fail_channel_creation) {
+    fail_create_channel_ = fail_channel_creation;
+  }
+
+ protected:
+  virtual Transport* CreateTransport() {
+    return new FakeTransport(signaling_thread(), worker_thread());
+  }
+  void CompleteNegotiation() {
+    for (TransportMap::const_iterator it = transport_proxies().begin();
+        it != transport_proxies().end(); ++it) {
+      it->second->CompleteNegotiation();
+    }
+  }
+
+ private:
+  bool fail_create_channel_;
+};
+
+}  // namespace cricket
+
+#endif  // TALK_SESSION_PHONE_FAKESESSION_H_
diff --git a/talk/p2p/client/httpportallocator.cc b/talk/p2p/client/httpportallocator.cc
index 827b54c..0baa07d 100644
--- a/talk/p2p/client/httpportallocator.cc
+++ b/talk/p2p/client/httpportallocator.cc
@@ -27,6 +27,7 @@
 
 #include "talk/p2p/client/httpportallocator.h"
 
+#include <algorithm>
 #include <map>
 
 #include "talk/base/asynchttprequest.h"
@@ -89,13 +90,13 @@
 
 namespace cricket {
 
-// HttpPortAllocator
+// HttpPortAllocatorBase
 
-const int HttpPortAllocator::kNumRetries = 5;
+const int HttpPortAllocatorBase::kNumRetries = 5;
 
-const char HttpPortAllocator::kCreateSessionURL[] = "/create_session";
+const char HttpPortAllocatorBase::kCreateSessionURL[] = "/create_session";
 
-HttpPortAllocator::HttpPortAllocator(
+HttpPortAllocatorBase::HttpPortAllocatorBase(
     talk_base::NetworkManager* network_manager,
     talk_base::PacketSocketFactory* socket_factory,
     const std::string &user_agent)
@@ -105,7 +106,7 @@
       talk_base::SocketAddress("stun.l.google.com", 19302));
 }
 
-HttpPortAllocator::HttpPortAllocator(
+HttpPortAllocatorBase::HttpPortAllocatorBase(
     talk_base::NetworkManager* network_manager,
     const std::string &user_agent)
     : BasicPortAllocator(network_manager), agent_(user_agent) {
@@ -114,19 +115,13 @@
       talk_base::SocketAddress("stun.l.google.com", 19302));
 }
 
-HttpPortAllocator::~HttpPortAllocator() {
+HttpPortAllocatorBase::~HttpPortAllocatorBase() {
 }
 
-PortAllocatorSession *HttpPortAllocator::CreateSession(
-    const std::string& name, const std::string& session_type) {
-  return new HttpPortAllocatorSession(this, name, session_type, stun_hosts_,
-      relay_hosts_, relay_token_, agent_);
-}
+// HttpPortAllocatorSessionBase
 
-// HttpPortAllocatorSession
-
-HttpPortAllocatorSession::HttpPortAllocatorSession(
-    HttpPortAllocator* allocator, const std::string &name,
+HttpPortAllocatorSessionBase::HttpPortAllocatorSessionBase(
+    HttpPortAllocatorBase* allocator, const std::string &name,
     const std::string& session_type,
     const std::vector<talk_base::SocketAddress>& stun_hosts,
     const std::vector<std::string>& relay_hosts,
@@ -137,7 +132,9 @@
       relay_token_(relay_token), agent_(user_agent), attempts_(0) {
 }
 
-void HttpPortAllocatorSession::GetPortConfigurations() {
+HttpPortAllocatorSessionBase::~HttpPortAllocatorSessionBase() {}
+
+void HttpPortAllocatorSessionBase::GetPortConfigurations() {
   // Creating relay sessions can take time and is done asynchronously.
   // Creating stun sessions could also take time and could be done aysnc also,
   // but for now is done here and added to the initial config.  Note any later
@@ -148,7 +145,7 @@
   TryCreateRelaySession();
 }
 
-void HttpPortAllocatorSession::TryCreateRelaySession() {
+void HttpPortAllocatorSessionBase::TryCreateRelaySession() {
   if (allocator()->flags() & PORTALLOCATOR_DISABLE_RELAY) {
     LOG(LS_VERBOSE) << "HttpPortAllocator: Relay ports disabled, skipping.";
     return;
@@ -176,50 +173,7 @@
   SendSessionRequest(host, talk_base::HTTP_SECURE_PORT);
 }
 
-void HttpPortAllocatorSession::SendSessionRequest(const std::string& host,
-                                                  int port) {
-  // Initiate an HTTP request to create a session through the chosen host.
-  talk_base::AsyncHttpRequest* request =
-      new talk_base::AsyncHttpRequest(agent_);
-  request->SignalWorkDone.connect(this,
-      &HttpPortAllocatorSession::OnRequestDone);
-
-  request->set_secure(port == talk_base::HTTP_SECURE_PORT);
-  request->set_proxy(allocator()->proxy());
-  request->response().document.reset(new talk_base::MemoryStream);
-  request->request().verb = talk_base::HV_GET;
-  request->request().path = HttpPortAllocator::kCreateSessionURL;
-  request->request().addHeader("X-Talk-Google-Relay-Auth", relay_token_, true);
-  request->request().addHeader("X-Google-Relay-Auth", relay_token_, true);
-  request->request().addHeader("X-Session-Type", session_type(), true);
-  request->request().addHeader("X-Stream-Type", name(), true);
-  request->set_host(host);
-  request->set_port(port);
-  request->Start();
-  request->Release();
-}
-
-void HttpPortAllocatorSession::OnRequestDone(talk_base::SignalThread* data) {
-  talk_base::AsyncHttpRequest* request =
-      static_cast<talk_base::AsyncHttpRequest*>(data);
-  if (request->response().scode != 200) {
-    LOG(LS_WARNING) << "HTTPPortAllocator: request "
-                    << " received error " << request->response().scode;
-    TryCreateRelaySession();
-    return;
-  }
-  LOG(LS_INFO) << "HTTPPortAllocator: request succeeded";
-
-  talk_base::MemoryStream* stream =
-      static_cast<talk_base::MemoryStream*>(request->response().document.get());
-  stream->Rewind();
-  size_t length;
-  stream->GetSize(&length);
-  std::string resp = std::string(stream->GetBuffer(), length);
-  ReceiveSessionResponse(resp);
-}
-
-void HttpPortAllocatorSession::ReceiveSessionResponse(
+void HttpPortAllocatorSessionBase::ReceiveSessionResponse(
     const std::string& response) {
 
   StringMap map;
@@ -256,4 +210,100 @@
   ConfigReady(config);
 }
 
+// HttpPortAllocator
+
+HttpPortAllocator::HttpPortAllocator(
+    talk_base::NetworkManager* network_manager,
+    talk_base::PacketSocketFactory* socket_factory,
+    const std::string &user_agent)
+    : HttpPortAllocatorBase(network_manager, socket_factory, user_agent) {
+}
+
+HttpPortAllocator::HttpPortAllocator(
+    talk_base::NetworkManager* network_manager,
+    const std::string &user_agent)
+    : HttpPortAllocatorBase(network_manager, user_agent) {
+}
+HttpPortAllocator::~HttpPortAllocator() {}
+
+PortAllocatorSession* HttpPortAllocator::CreateSession(
+    const std::string& name, const std::string& session_type) {
+  return new HttpPortAllocatorSession(this, name, session_type, stun_hosts(),
+      relay_hosts(), relay_token(), user_agent());
+}
+
+// HttpPortAllocatorSession
+
+HttpPortAllocatorSession::HttpPortAllocatorSession(
+    HttpPortAllocator* allocator,
+    const std::string& name,
+    const std::string& session_type,
+    const std::vector<talk_base::SocketAddress>& stun_hosts,
+    const std::vector<std::string>& relay_hosts,
+    const std::string& relay,
+    const std::string& agent)
+    : HttpPortAllocatorSessionBase(allocator, name, session_type, stun_hosts,
+                                   relay_hosts, relay, agent) {
+}
+
+HttpPortAllocatorSession::~HttpPortAllocatorSession() {
+  for (std::list<talk_base::AsyncHttpRequest*>::iterator it = requests_.begin();
+       it != requests_.end(); ++it) {
+    (*it)->Destroy(true);
+  }
+}
+
+void HttpPortAllocatorSession::SendSessionRequest(const std::string& host,
+                                                  int port) {
+  // Initiate an HTTP request to create a session through the chosen host.
+  talk_base::AsyncHttpRequest* request =
+      new talk_base::AsyncHttpRequest(user_agent());
+  request->SignalWorkDone.connect(this,
+      &HttpPortAllocatorSession::OnRequestDone);
+
+  request->set_secure(port == talk_base::HTTP_SECURE_PORT);
+  request->set_proxy(allocator()->proxy());
+  request->response().document.reset(new talk_base::MemoryStream);
+  request->request().verb = talk_base::HV_GET;
+  request->request().path = HttpPortAllocator::kCreateSessionURL;
+  request->request().addHeader("X-Talk-Google-Relay-Auth", relay_token(), true);
+  request->request().addHeader("X-Google-Relay-Auth", relay_token(), true);
+  request->request().addHeader("X-Session-Type", session_type(), true);
+  request->request().addHeader("X-Stream-Type", name(), true);
+  request->set_host(host);
+  request->set_port(port);
+  request->Start();
+  request->Release();
+  requests_.push_back(request);
+}
+
+void HttpPortAllocatorSession::OnRequestDone(talk_base::SignalThread* data) {
+  talk_base::AsyncHttpRequest* request =
+      static_cast<talk_base::AsyncHttpRequest*>(data);
+
+  // Remove the request from the list of active requests.
+  std::list<talk_base::AsyncHttpRequest*>::iterator it =
+      std::find(requests_.begin(), requests_.end(), request);
+  if (it != requests_.end()) {
+    requests_.erase(it);
+    return;
+  }
+
+  if (request->response().scode != 200) {
+    LOG(LS_WARNING) << "HTTPPortAllocator: request "
+                    << " received error " << request->response().scode;
+    TryCreateRelaySession();
+    return;
+  }
+  LOG(LS_INFO) << "HTTPPortAllocator: request succeeded";
+
+  talk_base::MemoryStream* stream =
+      static_cast<talk_base::MemoryStream*>(request->response().document.get());
+  stream->Rewind();
+  size_t length;
+  stream->GetSize(&length);
+  std::string resp = std::string(stream->GetBuffer(), length);
+  ReceiveSessionResponse(resp);
+}
+
 }  // namespace cricket
diff --git a/talk/p2p/client/httpportallocator.h b/talk/p2p/client/httpportallocator.h
index 53b3909..e47a69e 100644
--- a/talk/p2p/client/httpportallocator.h
+++ b/talk/p2p/client/httpportallocator.h
@@ -28,17 +28,19 @@
 #ifndef TALK_P2P_CLIENT_HTTPPORTALLOCATOR_H_
 #define TALK_P2P_CLIENT_HTTPPORTALLOCATOR_H_
 
+#include <list>
 #include <string>
 #include <vector>
 #include "talk/p2p/client/basicportallocator.h"
 
 namespace talk_base {
+class AsyncHttpRequest;
 class SignalThread;
 }
 
 namespace cricket {
 
-class HttpPortAllocator : public BasicPortAllocator {
+class HttpPortAllocatorBase : public BasicPortAllocator {
  public:
   // The number of HTTP requests we should attempt before giving up.
   static const int kNumRetries;
@@ -46,15 +48,19 @@
   // Records the URL that we will GET in order to create a session.
   static const char kCreateSessionURL[];
 
-  HttpPortAllocator(talk_base::NetworkManager* network_manager,
-                    const std::string& user_agent);
-  HttpPortAllocator(talk_base::NetworkManager* network_manager,
-                    talk_base::PacketSocketFactory* socket_factory,
-                    const std::string& user_agent);
-  virtual ~HttpPortAllocator();
+  HttpPortAllocatorBase(talk_base::NetworkManager* network_manager,
+                        const std::string& user_agent);
+  HttpPortAllocatorBase(talk_base::NetworkManager* network_manager,
+                        talk_base::PacketSocketFactory* socket_factory,
+                        const std::string& user_agent);
+  virtual ~HttpPortAllocatorBase();
 
-  virtual PortAllocatorSession* CreateSession(const std::string& name,
-                                              const std::string& session_type);
+  // CreateSession is defined in BasicPortAllocator but is
+  // redefined here as pure virtual.
+  virtual PortAllocatorSession* CreateSession(
+      const std::string& name,
+      const std::string& session_type) = 0;
+
   void SetStunHosts(const std::vector<talk_base::SocketAddress>& hosts) {
     if (!hosts.empty()) {
       stun_hosts_ = hosts;
@@ -92,7 +98,58 @@
 
 class RequestData;
 
-class HttpPortAllocatorSession : public BasicPortAllocatorSession {
+class HttpPortAllocatorSessionBase : public BasicPortAllocatorSession {
+ public:
+  HttpPortAllocatorSessionBase(
+      HttpPortAllocatorBase* allocator,
+      const std::string& name,
+      const std::string& session_type,
+      const std::vector<talk_base::SocketAddress>& stun_hosts,
+      const std::vector<std::string>& relay_hosts,
+      const std::string& relay,
+      const std::string& agent);
+  virtual ~HttpPortAllocatorSessionBase();
+
+  const std::string& relay_token() const {
+    return relay_token_;
+  }
+
+  const std::string& user_agent() const {
+      return agent_;
+  }
+
+  virtual void SendSessionRequest(const std::string& host, int port) = 0;
+  virtual void ReceiveSessionResponse(const std::string& response);
+
+ protected:
+  virtual void GetPortConfigurations();
+  void TryCreateRelaySession();
+  virtual HttpPortAllocatorBase* allocator() {
+    return static_cast<HttpPortAllocatorBase*>(
+        BasicPortAllocatorSession::allocator());
+  }
+
+ private:
+  std::vector<std::string> relay_hosts_;
+  std::vector<talk_base::SocketAddress> stun_hosts_;
+  std::string relay_token_;
+  std::string agent_;
+  int attempts_;
+};
+
+class HttpPortAllocator : public HttpPortAllocatorBase {
+ public:
+  HttpPortAllocator(talk_base::NetworkManager* network_manager,
+                    const std::string& user_agent);
+  HttpPortAllocator(talk_base::NetworkManager* network_manager,
+                    talk_base::PacketSocketFactory* socket_factory,
+                    const std::string& user_agent);
+  virtual ~HttpPortAllocator();
+  virtual PortAllocatorSession* CreateSession(const std::string& name,
+                                              const std::string& session_type);
+};
+
+class HttpPortAllocatorSession : public HttpPortAllocatorSessionBase {
  public:
   HttpPortAllocatorSession(
       HttpPortAllocator* allocator,
@@ -102,32 +159,16 @@
       const std::vector<std::string>& relay_hosts,
       const std::string& relay,
       const std::string& agent);
-  virtual ~HttpPortAllocatorSession() {}
+  virtual ~HttpPortAllocatorSession();
 
-  const std::string& relay_token() const {
-    return relay_token_;
-  }
   virtual void SendSessionRequest(const std::string& host, int port);
-  virtual void ReceiveSessionResponse(const std::string& response);
 
  protected:
   // Protected for diagnostics.
   virtual void OnRequestDone(talk_base::SignalThread* request);
 
-  virtual void GetPortConfigurations();
-  void TryCreateRelaySession();
-
  private:
-  virtual HttpPortAllocator* allocator() {
-    return static_cast<HttpPortAllocator*>(
-        BasicPortAllocatorSession::allocator());
-  }
-
-  std::vector<std::string> relay_hosts_;
-  std::vector<talk_base::SocketAddress> stun_hosts_;
-  std::string relay_token_;
-  std::string agent_;
-  int attempts_;
+  std::list<talk_base::AsyncHttpRequest*> requests_;
 };
 
 }  // namespace cricket
diff --git a/talk/session/phone/channel_unittest.cc b/talk/session/phone/channel_unittest.cc
new file mode 100644
index 0000000..86d3b6d
--- /dev/null
+++ b/talk/session/phone/channel_unittest.cc
@@ -0,0 +1,1622 @@
+// libjingle
+// Copyright 2009 Google Inc.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//  1. Redistributions of source code must retain the above copyright notice,
+//     this list of conditions and the following disclaimer.
+//  2. Redistributions in binary form must reproduce the above copyright notice,
+//     this list of conditions and the following disclaimer in the documentation
+//     and/or other materials provided with the distribution.
+//  3. The name of the author may not be used to endorse or promote products
+//     derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "talk/base/fileutils.h"
+#include "talk/base/gunit.h"
+#include "talk/base/helpers.h"
+#include "talk/base/logging.h"
+#include "talk/base/pathutils.h"
+#include "talk/base/signalthread.h"
+#include "talk/p2p/base/fakesession.h"
+#include "talk/session/phone/channel.h"
+#include "talk/session/phone/fakemediaengine.h"
+#include "talk/session/phone/fakertp.h"
+#include "talk/session/phone/mediasessionclient.h"
+#include "talk/session/phone/mediarecorder.h"
+#include "talk/session/phone/rtpdump.h"
+
+using cricket::CA_OFFER;
+using cricket::CA_ANSWER;
+using cricket::CA_UPDATE;
+
+static const cricket::AudioCodec kPcmuCodec(0, "PCMU", 64000, 8000, 1, 0);
+static const cricket::AudioCodec kPcmaCodec(8, "PCMA", 64000, 8000, 1, 0);
+static const cricket::AudioCodec kIsacCodec(103, "ISAC", 40000, 16000, 1, 0);
+static const cricket::VideoCodec kH264Codec(97, "H264", 640, 400, 30, 0);
+static const cricket::VideoCodec kH264SvcCodec(99, "H264-SVC", 320, 200, 15, 0);
+static const uint32 kSsrc1 = 0x1111;
+static const uint32 kSsrc2 = 0x2222;
+
+class VoiceTraits {
+ public:
+  typedef cricket::VoiceChannel Channel;
+  typedef cricket::FakeVoiceMediaChannel MediaChannel;
+  typedef cricket::AudioContentDescription Content;
+  typedef cricket::AudioCodec Codec;
+  typedef cricket::VoiceMediaInfo MediaInfo;
+};
+
+class VideoTraits {
+ public:
+  typedef cricket::VideoChannel Channel;
+  typedef cricket::FakeVideoMediaChannel MediaChannel;
+  typedef cricket::VideoContentDescription Content;
+  typedef cricket::VideoCodec Codec;
+  typedef cricket::VideoMediaInfo MediaInfo;
+};
+
+// Base class for Voice/VideoChannel tests
+template<class T>
+class ChannelTest : public testing::Test, public sigslot::has_slots<> {
+ public:
+  enum Flags { RTCP = 0x1, RTCP_MUX = 0x2, SECURE = 0x4, SSRC_MUX = 0x8 };
+  ChannelTest(const uint8* rtp_data, int rtp_len,
+              const uint8* rtcp_data, int rtcp_len)
+      : media_channel1_(NULL),
+        media_channel2_(NULL),
+        rtp_packet_(reinterpret_cast<const char*>(rtp_data), rtp_len),
+        rtcp_packet_(reinterpret_cast<const char*>(rtcp_data), rtcp_len),
+        media_info_callbacks1_(),
+        media_info_callbacks2_(),
+        ssrc_(0),
+        error_(T::MediaChannel::ERROR_NONE) {
+  }
+
+  void CreateChannels(int flags1, int flags2) {
+    CreateChannels(new typename T::MediaChannel(NULL),
+                   new typename T::MediaChannel(NULL),
+                   flags1, flags2, talk_base::Thread::Current());
+  }
+  void CreateChannels(int flags) {
+     CreateChannels(new typename T::MediaChannel(NULL),
+                    new typename T::MediaChannel(NULL),
+                    flags, talk_base::Thread::Current());
+  }
+  void CreateChannels(int flags1, int flags2,
+                      talk_base::Thread* thread) {
+    CreateChannels(new typename T::MediaChannel(NULL),
+                   new typename T::MediaChannel(NULL),
+                   flags1, flags2, thread);
+  }
+  void CreateChannels(int flags,
+                      talk_base::Thread* thread) {
+    CreateChannels(new typename T::MediaChannel(NULL),
+                     new typename T::MediaChannel(NULL),
+                     flags, thread);
+  }
+  void CreateChannels(
+      typename T::MediaChannel* ch1, typename T::MediaChannel* ch2,
+      int flags1, int flags2, talk_base::Thread* thread) {
+    media_channel1_ = ch1;
+    media_channel2_ = ch2;
+    channel1_.reset(CreateChannel(thread, &media_engine_, ch1, &session1_,
+                                  (flags1 & RTCP) != 0));
+    channel2_.reset(CreateChannel(thread, &media_engine_, ch2, &session2_,
+                                  (flags2 & RTCP) != 0));
+    channel1_->SignalMediaMonitor.connect(
+        this, &ChannelTest<T>::OnMediaMonitor);
+    channel2_->SignalMediaMonitor.connect(
+        this, &ChannelTest<T>::OnMediaMonitor);
+    channel1_->SignalMediaError.connect(
+        this, &ChannelTest<T>::OnMediaChannelError);
+    channel2_->SignalMediaError.connect(
+        this, &ChannelTest<T>::OnMediaChannelError);
+    CreateContent(flags1, kPcmuCodec, kH264Codec, &media_content1_);
+    CreateContent(flags2, kPcmuCodec, kH264Codec, &media_content2_);
+    AddLegacyStreamInContent(kSsrc1, flags1, &media_content1_);
+    AddLegacyStreamInContent(kSsrc2, flags2, &media_content2_);
+  }
+
+  void CreateChannels(
+      typename T::MediaChannel* ch1, typename T::MediaChannel* ch2,
+      int flags, talk_base::Thread* thread) {
+    media_channel1_ = ch1;
+    media_channel2_ = ch2;
+    channel1_.reset(CreateChannel(thread, &media_engine_, ch1, &session1_,
+                                  (flags & RTCP) != 0));
+    channel2_.reset(CreateChannel(thread, &media_engine_, ch2, &session1_,
+                                  (flags & RTCP) != 0));
+    channel1_->SignalMediaMonitor.connect(
+        this, &ChannelTest<T>::OnMediaMonitor);
+    channel2_->SignalMediaMonitor.connect(
+        this, &ChannelTest<T>::OnMediaMonitor);
+    channel2_->SignalMediaError.connect(
+        this, &ChannelTest<T>::OnMediaChannelError);
+    CreateContent(flags, kPcmuCodec, kH264Codec, &media_content1_);
+    CreateContent(flags, kPcmuCodec, kH264Codec, &media_content2_);
+    AddLegacyStreamInContent(kSsrc1, flags, &media_content1_);
+    AddLegacyStreamInContent(kSsrc2, flags, &media_content2_);
+  }
+  typename T::Channel* CreateChannel(talk_base::Thread* thread,
+                                     cricket::MediaEngineInterface* engine,
+                                     typename T::MediaChannel* ch,
+                                     cricket::BaseSession* session,
+                                     bool rtcp) {
+    typename T::Channel* channel = new typename T::Channel(
+        thread, engine, ch, session, cricket::CN_AUDIO, rtcp);
+    if (!channel->Init()) {
+      delete channel;
+      channel = NULL;
+    }
+    return channel;
+  }
+
+  bool SendInitiate() {
+    bool result = channel1_->SetLocalContent(&media_content1_, CA_OFFER);
+    if (result) {
+      channel1_->Enable(true);
+      result = channel2_->SetRemoteContent(&media_content1_, CA_OFFER);
+      if (result) {
+        result = channel2_->SetLocalContent(&media_content2_, CA_ANSWER);
+        if (result) {
+          session1_.Connect(&session2_);
+        }
+      }
+    }
+    return result;
+  }
+  bool SendAccept() {
+    channel2_->Enable(true);
+    return channel1_->SetRemoteContent(&media_content2_, CA_ANSWER);
+  }
+  bool SendTerminate() {
+    channel1_.reset();
+    channel2_.reset();
+    return true;
+  }
+
+  bool AddStream1(int id) {
+    return channel1_->AddStream(id);
+  }
+  bool RemoveStream1(int id) {
+    return channel1_->RemoveStream(id);
+  }
+
+  cricket::FakeTransport* GetTransport1() {
+    return session1_.GetTransport(channel1_->content_name());
+  }
+  cricket::FakeTransport* GetTransport2() {
+    return session2_.GetTransport(channel2_->content_name());
+  }
+
+  bool SendRtp1() {
+    return media_channel1_->SendRtp(rtp_packet_.c_str(), rtp_packet_.size());
+  }
+  bool SendRtp2() {
+    return media_channel2_->SendRtp(rtp_packet_.c_str(), rtp_packet_.size());
+  }
+  bool SendRtcp1() {
+    return media_channel1_->SendRtcp(rtcp_packet_.c_str(), rtcp_packet_.size());
+  }
+  bool SendRtcp2() {
+    return media_channel2_->SendRtcp(rtcp_packet_.c_str(), rtcp_packet_.size());
+  }
+  // Methods to send custom data.
+  bool SendCustomRtp1(uint32 ssrc) {
+    std::string data(CreateRtpData(ssrc));
+    return media_channel1_->SendRtp(data.c_str(), data.size());
+  }
+  bool SendCustomRtp2(uint32 ssrc) {
+    std::string data(CreateRtpData(ssrc));
+    return media_channel2_->SendRtp(data.c_str(), data.size());
+  }
+  bool SendCustomRtcp1(uint32 ssrc) {
+    std::string data(CreateRtcpData(ssrc));
+    return media_channel1_->SendRtcp(data.c_str(), data.size());
+  }
+  bool SendCustomRtcp2(uint32 ssrc) {
+    std::string data(CreateRtcpData(ssrc));
+    return media_channel2_->SendRtcp(data.c_str(), data.size());
+  }
+  bool CheckRtp1() {
+    return media_channel1_->CheckRtp(rtp_packet_.c_str(), rtp_packet_.size());
+  }
+  bool CheckRtp2() {
+    return media_channel2_->CheckRtp(rtp_packet_.c_str(), rtp_packet_.size());
+  }
+  bool CheckRtcp1() {
+    return media_channel1_->CheckRtcp(rtcp_packet_.c_str(),
+                                      rtcp_packet_.size());
+  }
+  bool CheckRtcp2() {
+    return media_channel2_->CheckRtcp(rtcp_packet_.c_str(),
+                                      rtcp_packet_.size());
+  }
+  // Methods to check custom data.
+  bool CheckCustomRtp1(uint32 ssrc) {
+    std::string data(CreateRtpData(ssrc));
+    return media_channel1_->CheckRtp(data.c_str(), data.size());
+  }
+  bool CheckCustomRtp2(uint32 ssrc) {
+    std::string data(CreateRtpData(ssrc));
+    return media_channel2_->CheckRtp(data.c_str(), data.size());
+  }
+  bool CheckCustomRtcp1(uint32 ssrc) {
+    std::string data(CreateRtcpData(ssrc));
+    return media_channel1_->CheckRtcp(data.c_str(), data.size());
+  }
+  bool CheckCustomRtcp2(uint32 ssrc) {
+    std::string data(CreateRtcpData(ssrc));
+    return media_channel2_->CheckRtcp(data.c_str(), data.size());
+  }
+  std::string CreateRtpData(uint32 ssrc) {
+    std::string data(rtp_packet_);
+    // Set SSRC in the rtp packet copy.
+    talk_base::SetBE32(const_cast<char*>(data.c_str()) + 8, ssrc);
+    return data;
+  }
+  std::string CreateRtcpData(uint32 ssrc) {
+    std::string data(rtcp_packet_);
+    // Set SSRC in the rtcp packet copy.
+    talk_base::SetBE32(const_cast<char*>(data.c_str()) + 4, ssrc);
+    return data;
+  }
+
+  bool CheckNoRtp1() {
+    return media_channel1_->CheckNoRtp();
+  }
+  bool CheckNoRtp2() {
+    return media_channel2_->CheckNoRtp();
+  }
+  bool CheckNoRtcp1() {
+    return media_channel1_->CheckNoRtcp();
+  }
+  bool CheckNoRtcp2() {
+    return media_channel2_->CheckNoRtcp();
+  }
+
+  void CreateContent(int flags,
+                     const cricket::AudioCodec& audio_codec,
+                     const cricket::VideoCodec& video_codec,
+                     typename T::Content* content) {
+    // overridden in specialized classes
+  }
+
+  class CallThread : public talk_base::SignalThread {
+   public:
+    typedef bool (ChannelTest<T>::*Method)();
+    CallThread(ChannelTest<T>* obj, Method method, bool* result)
+        : obj_(obj),
+          method_(method),
+          result_(result) {
+      *result = false;
+    }
+    virtual void DoWork() {
+      bool result = (*obj_.*method_)();
+      if (result_) {
+        *result_ = result;
+      }
+    }
+   private:
+    ChannelTest<T>* obj_;
+    Method method_;
+    bool* result_;
+  };
+  void CallOnThread(typename CallThread::Method method, bool* result) {
+    CallThread* thread = new CallThread(this, method, result);
+    thread->Start();
+    thread->Release();
+  }
+
+  void CallOnThreadAndWaitForDone(typename CallThread::Method method,
+                                  bool* result) {
+    CallThread* thread = new CallThread(this, method, result);
+    thread->Start();
+    thread->Destroy(true);
+  }
+
+  bool CodecMatches(const typename T::Codec& c1, const typename T::Codec& c2) {
+    return false;  // overridden in specialized classes
+  }
+
+  void OnMediaMonitor(typename T::Channel* channel,
+                      const typename T::MediaInfo& info) {
+    if (channel == channel1_.get()) {
+      media_info_callbacks1_++;
+    } else if (channel == channel2_.get()) {
+      media_info_callbacks2_++;
+    }
+  }
+
+  void OnMediaChannelError(typename T::Channel* channel,
+                           uint32 ssrc,
+                           typename T::MediaChannel::Error error) {
+    ssrc_ = ssrc;
+    error_ = error;
+  }
+
+  void AddLegacyStreamInContent(uint32 ssrc, int flags,
+                        typename T::Content* content) {
+    // Base implementation.
+  }
+
+  // Tests that can be used by derived classes.
+
+  // Basic sanity check.
+  void TestInit() {
+    CreateChannels(0, 0);
+    EXPECT_FALSE(channel1_->secure());
+    EXPECT_FALSE(media_channel1_->sending());
+    EXPECT_FALSE(media_channel1_->playout());
+    EXPECT_TRUE(media_channel1_->codecs().empty());
+    EXPECT_TRUE(media_channel1_->streams().empty());
+    EXPECT_TRUE(media_channel1_->rtp_packets().empty());
+    EXPECT_TRUE(media_channel1_->rtcp_packets().empty());
+  }
+
+  // Test that SetRtcpCName sets the RTCP CNAME successfully.
+  void TestSetRtcpCName() {
+    static const char* kTestCName = "a@b.com";
+    CreateChannels(0, 0);
+    EXPECT_TRUE(channel1_->SetRtcpCName(kTestCName));
+    EXPECT_EQ(kTestCName, media_channel1_->rtcp_cname());
+    EXPECT_TRUE(channel2_->SetRtcpCName(kTestCName));
+    EXPECT_EQ(kTestCName, media_channel2_->rtcp_cname());
+  }
+
+  // Test that SetLocalContent and SetRemoteContent properly configure
+  // the codecs.
+  void TestSetContents() {
+    CreateChannels(0, 0);
+    typename T::Content content;
+    CreateContent(0, kPcmuCodec, kH264Codec, &content);
+    EXPECT_TRUE(channel1_->SetLocalContent(&content, CA_OFFER));
+    EXPECT_EQ(0U, media_channel1_->codecs().size());
+    EXPECT_TRUE(channel1_->SetRemoteContent(&content, CA_ANSWER));
+    ASSERT_EQ(1U, media_channel1_->codecs().size());
+    EXPECT_TRUE(CodecMatches(content.codecs()[0],
+                             media_channel1_->codecs()[0]));
+  }
+
+  // Test that SetLocalContent and SetRemoteContent properly deals
+  // with an empty offer.
+  void TestSetContentsNullOffer() {
+    CreateChannels(0, 0);
+    typename T::Content content;
+    EXPECT_TRUE(channel1_->SetLocalContent(&content, CA_OFFER));
+    CreateContent(0, kPcmuCodec, kH264Codec, &content);
+    EXPECT_EQ(0U, media_channel1_->codecs().size());
+    EXPECT_TRUE(channel1_->SetRemoteContent(&content, CA_ANSWER));
+    ASSERT_EQ(1U, media_channel1_->codecs().size());
+    EXPECT_TRUE(CodecMatches(content.codecs()[0],
+                             media_channel1_->codecs()[0]));
+  }
+
+  // Test that SetLocalContent and SetRemoteContent properly set RTCP
+  // mux.
+  void TestSetContentsRtcpMux() {
+    CreateChannels(RTCP, RTCP);
+    EXPECT_TRUE(channel1_->rtcp_transport_channel() != NULL);
+    EXPECT_TRUE(channel2_->rtcp_transport_channel() != NULL);
+    typename T::Content content;
+    CreateContent(0, kPcmuCodec, kH264Codec, &content);
+    // Both sides agree on mux. Should no longer be a separate RTCP channel.
+    content.set_rtcp_mux(true);
+    EXPECT_TRUE(channel1_->SetLocalContent(&content, CA_OFFER));
+    EXPECT_TRUE(channel1_->SetRemoteContent(&content, CA_ANSWER));
+    EXPECT_TRUE(channel1_->rtcp_transport_channel() == NULL);
+    // Only initiator supports mux. Should still have a separate RTCP channel.
+    EXPECT_TRUE(channel2_->SetLocalContent(&content, CA_OFFER));
+    content.set_rtcp_mux(false);
+    EXPECT_TRUE(channel2_->SetRemoteContent(&content, CA_ANSWER));
+    EXPECT_TRUE(channel2_->rtcp_transport_channel() != NULL);
+  }
+
+  // Test that SetRemoteContent properly deals with a content update.
+  void TestSetRemoteContentUpdate() {
+    CreateChannels(0, 0);
+    typename T::Content content;
+    CreateContent(RTCP | RTCP_MUX | SECURE, kPcmuCodec, kH264Codec, &content);
+    EXPECT_EQ(0U, media_channel1_->codecs().size());
+    EXPECT_TRUE(channel1_->SetLocalContent(&content, CA_OFFER));
+    EXPECT_TRUE(channel1_->SetRemoteContent(&content, CA_ANSWER));
+    ASSERT_EQ(1U, media_channel1_->codecs().size());
+    EXPECT_TRUE(CodecMatches(content.codecs()[0],
+                             media_channel1_->codecs()[0]));
+    // Now update with other codecs.
+    typename T::Content update_content;
+    CreateContent(0, kIsacCodec, kH264SvcCodec, &update_content);
+    EXPECT_TRUE(channel1_->SetRemoteContent(&update_content, CA_UPDATE));
+    ASSERT_EQ(1U, media_channel1_->codecs().size());
+    EXPECT_TRUE(CodecMatches(update_content.codecs()[0],
+                             media_channel1_->codecs()[0]));
+
+    // Now update without any codec.
+    typename T::Content empty_content;
+    EXPECT_TRUE(channel1_->SetRemoteContent(&empty_content, CA_UPDATE));
+    ASSERT_EQ(0U, media_channel1_->codecs().size());
+  }
+
+  // Test that Add/RemoveStream properly forward to the media channel.
+  void TestStreams() {
+    CreateChannels(0, 0);
+    EXPECT_TRUE(AddStream1(1));
+    EXPECT_TRUE(AddStream1(2));
+    EXPECT_EQ(2U, media_channel1_->streams().size());
+    EXPECT_TRUE(RemoveStream1(2));
+    EXPECT_EQ(1U, media_channel1_->streams().size());
+    EXPECT_TRUE(RemoveStream1(1));
+    EXPECT_EQ(0U, media_channel1_->streams().size());
+  }
+
+  // Test that we only start playout and sending at the right times.
+  void TestPlayoutAndSendingStates() {
+    CreateChannels(0, 0);
+    EXPECT_FALSE(media_channel1_->playout());
+    EXPECT_FALSE(media_channel1_->sending());
+    EXPECT_FALSE(media_channel2_->playout());
+    EXPECT_FALSE(media_channel2_->sending());
+    EXPECT_TRUE(channel1_->Enable(true));
+    EXPECT_FALSE(media_channel1_->playout());
+    EXPECT_FALSE(media_channel1_->sending());
+    EXPECT_TRUE(channel1_->SetLocalContent(&media_content1_, CA_OFFER));
+    EXPECT_TRUE(media_channel1_->playout());
+    EXPECT_FALSE(media_channel1_->sending());
+    EXPECT_TRUE(channel2_->SetRemoteContent(&media_content1_, CA_OFFER));
+    EXPECT_FALSE(media_channel2_->playout());
+    EXPECT_FALSE(media_channel2_->sending());
+    EXPECT_TRUE(channel2_->SetLocalContent(&media_content2_, CA_ANSWER));
+    EXPECT_FALSE(media_channel2_->playout());
+    EXPECT_FALSE(media_channel2_->sending());
+    session1_.Connect(&session2_);
+    EXPECT_TRUE(media_channel1_->playout());
+    EXPECT_FALSE(media_channel1_->sending());
+    EXPECT_FALSE(media_channel2_->playout());
+    EXPECT_FALSE(media_channel2_->sending());
+    EXPECT_TRUE(channel2_->Enable(true));
+    EXPECT_TRUE(media_channel2_->playout());
+    EXPECT_TRUE(media_channel2_->sending());
+    EXPECT_TRUE(channel1_->SetRemoteContent(&media_content2_, CA_ANSWER));
+    EXPECT_TRUE(media_channel1_->playout());
+    EXPECT_TRUE(media_channel1_->sending());
+  }
+
+  // Test setting up a call.
+  void TestCallSetup() {
+    CreateChannels(0, 0);
+    EXPECT_FALSE(channel1_->secure());
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(media_channel1_->playout());
+    EXPECT_FALSE(media_channel1_->sending());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_FALSE(channel1_->secure());
+    EXPECT_TRUE(media_channel1_->sending());
+    EXPECT_EQ(1U, media_channel1_->codecs().size());
+    EXPECT_TRUE(media_channel2_->playout());
+    EXPECT_TRUE(media_channel2_->sending());
+    EXPECT_EQ(1U, media_channel2_->codecs().size());
+  }
+
+  // Test that we don't crash if packets are sent during call teardown
+  // when RTCP mux is enabled. This is a regression test against a specific
+  // race condition that would only occur when a RTCP packet was sent during
+  // teardown of a channel on which RTCP mux was enabled.
+  void TestCallTeardownRtcpMux() {
+    class LastWordMediaChannel : public T::MediaChannel {
+     public:
+      LastWordMediaChannel() : T::MediaChannel(NULL) {}
+      ~LastWordMediaChannel() {
+        T::MediaChannel::SendRtp(kPcmuFrame, sizeof(kPcmuFrame));
+        T::MediaChannel::SendRtcp(kRtcpReport, sizeof(kRtcpReport));
+      }
+    };
+    CreateChannels(new LastWordMediaChannel(), new LastWordMediaChannel(),
+                   RTCP | RTCP_MUX, RTCP | RTCP_MUX,
+                   talk_base::Thread::Current());
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_TRUE(SendTerminate());
+  }
+
+  // Send voice RTP data to the other side and ensure it gets there.
+  void SendRtpToRtp() {
+    CreateChannels(0, 0);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_EQ(1U, GetTransport1()->channels().size());
+    EXPECT_EQ(1U, GetTransport2()->channels().size());
+    EXPECT_TRUE(SendRtp1());
+    EXPECT_TRUE(SendRtp2());
+    EXPECT_TRUE(CheckRtp1());
+    EXPECT_TRUE(CheckRtp2());
+    EXPECT_TRUE(CheckNoRtp1());
+    EXPECT_TRUE(CheckNoRtp2());
+  }
+
+  // Check that RTCP is not transmitted if both sides don't support RTCP.
+  void SendNoRtcpToNoRtcp() {
+    CreateChannels(0, 0);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_EQ(1U, GetTransport1()->channels().size());
+    EXPECT_EQ(1U, GetTransport2()->channels().size());
+    EXPECT_FALSE(SendRtcp1());
+    EXPECT_FALSE(SendRtcp2());
+    EXPECT_TRUE(CheckNoRtcp1());
+    EXPECT_TRUE(CheckNoRtcp2());
+  }
+
+  // Check that RTCP is not transmitted if the callee doesn't support RTCP.
+  void SendNoRtcpToRtcp() {
+    CreateChannels(0, RTCP);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_EQ(1U, GetTransport1()->channels().size());
+    EXPECT_EQ(2U, GetTransport2()->channels().size());
+    EXPECT_FALSE(SendRtcp1());
+    EXPECT_FALSE(SendRtcp2());
+    EXPECT_TRUE(CheckNoRtcp1());
+    EXPECT_TRUE(CheckNoRtcp2());
+  }
+
+  // Check that RTCP is not transmitted if the caller doesn't support RTCP.
+  void SendRtcpToNoRtcp() {
+    CreateChannels(RTCP, 0);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_EQ(2U, GetTransport1()->channels().size());
+    EXPECT_EQ(1U, GetTransport2()->channels().size());
+    EXPECT_FALSE(SendRtcp1());
+    EXPECT_FALSE(SendRtcp2());
+    EXPECT_TRUE(CheckNoRtcp1());
+    EXPECT_TRUE(CheckNoRtcp2());
+  }
+
+  // Check that RTCP is transmitted if both sides support RTCP.
+  void SendRtcpToRtcp() {
+    CreateChannels(RTCP, RTCP);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_EQ(2U, GetTransport1()->channels().size());
+    EXPECT_EQ(2U, GetTransport2()->channels().size());
+    EXPECT_TRUE(SendRtcp1());
+    EXPECT_TRUE(SendRtcp2());
+    EXPECT_TRUE(CheckRtcp1());
+    EXPECT_TRUE(CheckRtcp2());
+    EXPECT_TRUE(CheckNoRtcp1());
+    EXPECT_TRUE(CheckNoRtcp2());
+  }
+
+  // Check that RTCP is transmitted if only the initiator supports mux.
+  void SendRtcpMuxToRtcp() {
+    CreateChannels(RTCP | RTCP_MUX, RTCP);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_EQ(2U, GetTransport1()->channels().size());
+    EXPECT_EQ(2U, GetTransport2()->channels().size());
+    EXPECT_TRUE(SendRtcp1());
+    EXPECT_TRUE(SendRtcp2());
+    EXPECT_TRUE(CheckRtcp1());
+    EXPECT_TRUE(CheckRtcp2());
+    EXPECT_TRUE(CheckNoRtcp1());
+    EXPECT_TRUE(CheckNoRtcp2());
+  }
+
+  // Check that RTP and RTCP are transmitted ok when both sides support mux.
+  void SendRtcpMuxToRtcpMux() {
+    CreateChannels(RTCP | RTCP_MUX, RTCP | RTCP_MUX);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_EQ(2U, GetTransport1()->channels().size());
+    EXPECT_EQ(1U, GetTransport2()->channels().size());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_EQ(1U, GetTransport1()->channels().size());
+    EXPECT_TRUE(SendRtp1());
+    EXPECT_TRUE(SendRtp2());
+    EXPECT_TRUE(SendRtcp1());
+    EXPECT_TRUE(SendRtcp2());
+    EXPECT_TRUE(CheckRtp1());
+    EXPECT_TRUE(CheckRtp2());
+    EXPECT_TRUE(CheckNoRtp1());
+    EXPECT_TRUE(CheckNoRtp2());
+    EXPECT_TRUE(CheckRtcp1());
+    EXPECT_TRUE(CheckRtcp2());
+    EXPECT_TRUE(CheckNoRtcp1());
+    EXPECT_TRUE(CheckNoRtcp2());
+  }
+
+  // Check that RTCP data sent by the initiator before the accept is not muxed.
+  void SendEarlyRtcpMuxToRtcp() {
+    CreateChannels(RTCP | RTCP_MUX, RTCP);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_EQ(2U, GetTransport1()->channels().size());
+    EXPECT_EQ(2U, GetTransport2()->channels().size());
+
+    // RTCP can be sent before the call is accepted, if the transport is ready.
+    // It should not be muxed though, as the remote side doesn't support mux.
+    EXPECT_TRUE(SendRtcp1());
+    EXPECT_TRUE(CheckNoRtp2());
+    EXPECT_TRUE(CheckRtcp2());
+
+    // Send RTCP packet from callee and verify that it is received.
+    EXPECT_TRUE(SendRtcp2());
+    EXPECT_TRUE(CheckNoRtp1());
+    EXPECT_TRUE(CheckRtcp1());
+
+    // Complete call setup and ensure everything is still OK.
+    EXPECT_TRUE(SendAccept());
+    EXPECT_EQ(2U, GetTransport1()->channels().size());
+    EXPECT_TRUE(SendRtcp1());
+    EXPECT_TRUE(CheckRtcp2());
+    EXPECT_TRUE(SendRtcp2());
+    EXPECT_TRUE(CheckRtcp1());
+  }
+
+
+  // Check that RTCP data is not muxed until both sides have enabled muxing,
+  // but that we properly demux before we get the accept message, since there
+  // is a race between RTP data and the jingle accept.
+  void SendEarlyRtcpMuxToRtcpMux() {
+    CreateChannels(RTCP | RTCP_MUX, RTCP | RTCP_MUX);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_EQ(2U, GetTransport1()->channels().size());
+    EXPECT_EQ(1U, GetTransport2()->channels().size());
+
+    // RTCP can't be sent yet, since the RTCP transport isn't writable, and
+    // we haven't yet received the accept that says we should mux.
+    EXPECT_FALSE(SendRtcp1());
+
+    // Send muxed RTCP packet from callee and verify that it is received.
+    EXPECT_TRUE(SendRtcp2());
+    EXPECT_TRUE(CheckNoRtp1());
+    EXPECT_TRUE(CheckRtcp1());
+
+    // Complete call setup and ensure everything is still OK.
+    EXPECT_TRUE(SendAccept());
+    EXPECT_EQ(1U, GetTransport1()->channels().size());
+    EXPECT_TRUE(SendRtcp1());
+    EXPECT_TRUE(CheckRtcp2());
+    EXPECT_TRUE(SendRtcp2());
+    EXPECT_TRUE(CheckRtcp1());
+  }
+
+  // Test that we properly send SRTP with RTCP in both directions.
+  void SendSrtpToSrtp() {
+    CreateChannels(RTCP | SECURE, RTCP | SECURE);
+    EXPECT_FALSE(channel1_->secure());
+    EXPECT_FALSE(channel2_->secure());
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_TRUE(channel1_->secure());
+    EXPECT_TRUE(channel2_->secure());
+    EXPECT_TRUE(SendRtp1());
+    EXPECT_TRUE(SendRtp2());
+    EXPECT_TRUE(SendRtcp1());
+    EXPECT_TRUE(SendRtcp2());
+    EXPECT_TRUE(CheckRtp1());
+    EXPECT_TRUE(CheckRtp2());
+    EXPECT_TRUE(CheckNoRtp1());
+    EXPECT_TRUE(CheckNoRtp2());
+    EXPECT_TRUE(CheckRtcp1());
+    EXPECT_TRUE(CheckRtcp2());
+    EXPECT_TRUE(CheckNoRtcp1());
+    EXPECT_TRUE(CheckNoRtcp2());
+  }
+
+  // Test that we properly handling SRTP negotiating down to RTP.
+  void SendSrtpToRtp() {
+    CreateChannels(RTCP | SECURE, RTCP);
+    EXPECT_FALSE(channel1_->secure());
+    EXPECT_FALSE(channel2_->secure());
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_FALSE(channel1_->secure());
+    EXPECT_FALSE(channel2_->secure());
+    EXPECT_TRUE(SendRtp1());
+    EXPECT_TRUE(SendRtp2());
+    EXPECT_TRUE(SendRtcp1());
+    EXPECT_TRUE(SendRtcp2());
+    EXPECT_TRUE(CheckRtp1());
+    EXPECT_TRUE(CheckRtp2());
+    EXPECT_TRUE(CheckNoRtp1());
+    EXPECT_TRUE(CheckNoRtp2());
+    EXPECT_TRUE(CheckRtcp1());
+    EXPECT_TRUE(CheckRtcp2());
+    EXPECT_TRUE(CheckNoRtcp1());
+    EXPECT_TRUE(CheckNoRtcp2());
+  }
+
+  // Test that we properly send SRTP with RTCP mux in both directions.
+  void SendSrtcpMux() {
+    CreateChannels(RTCP | RTCP_MUX  | SECURE, RTCP | RTCP_MUX | SECURE);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_TRUE(SendRtp1());
+    EXPECT_TRUE(SendRtp2());
+    EXPECT_TRUE(SendRtcp1());
+    EXPECT_TRUE(SendRtcp2());
+    EXPECT_TRUE(CheckRtp1());
+    EXPECT_TRUE(CheckRtp2());
+    EXPECT_TRUE(CheckNoRtp1());
+    EXPECT_TRUE(CheckNoRtp2());
+    EXPECT_TRUE(CheckRtcp1());
+    EXPECT_TRUE(CheckRtcp2());
+    EXPECT_TRUE(CheckNoRtcp1());
+    EXPECT_TRUE(CheckNoRtcp2());
+  }
+
+  // Test that we properly send RTP without SRTP from a thread.
+  void SendRtpToRtpOnThread() {
+    bool sent_rtp1, sent_rtp2, sent_rtcp1, sent_rtcp2;
+    CreateChannels(RTCP, RTCP);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    CallOnThread(&ChannelTest<T>::SendRtp1, &sent_rtp1);
+    CallOnThread(&ChannelTest<T>::SendRtp2, &sent_rtp2);
+    CallOnThread(&ChannelTest<T>::SendRtcp1, &sent_rtcp1);
+    CallOnThread(&ChannelTest<T>::SendRtcp2, &sent_rtcp2);
+    EXPECT_TRUE_WAIT(CheckRtp1(), 1000);
+    EXPECT_TRUE_WAIT(CheckRtp2(), 1000);
+    EXPECT_TRUE_WAIT(sent_rtp1, 1000);
+    EXPECT_TRUE_WAIT(sent_rtp2, 1000);
+    EXPECT_TRUE(CheckNoRtp1());
+    EXPECT_TRUE(CheckNoRtp2());
+    EXPECT_TRUE_WAIT(CheckRtcp1(), 1000);
+    EXPECT_TRUE_WAIT(CheckRtcp2(), 1000);
+    EXPECT_TRUE_WAIT(sent_rtcp1, 1000);
+    EXPECT_TRUE_WAIT(sent_rtcp2, 1000);
+    EXPECT_TRUE(CheckNoRtcp1());
+    EXPECT_TRUE(CheckNoRtcp2());
+  }
+
+  // Test that we properly send SRTP with RTCP from a thread.
+  void SendSrtpToSrtpOnThread() {
+    bool sent_rtp1, sent_rtp2, sent_rtcp1, sent_rtcp2;
+    CreateChannels(RTCP | SECURE, RTCP | SECURE);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    CallOnThread(&ChannelTest<T>::SendRtp1, &sent_rtp1);
+    CallOnThread(&ChannelTest<T>::SendRtp2, &sent_rtp2);
+    CallOnThread(&ChannelTest<T>::SendRtcp1, &sent_rtcp1);
+    CallOnThread(&ChannelTest<T>::SendRtcp2, &sent_rtcp2);
+    EXPECT_TRUE_WAIT(CheckRtp1(), 1000);
+    EXPECT_TRUE_WAIT(CheckRtp2(), 1000);
+    EXPECT_TRUE_WAIT(sent_rtp1, 1000);
+    EXPECT_TRUE_WAIT(sent_rtp2, 1000);
+    EXPECT_TRUE(CheckNoRtp1());
+    EXPECT_TRUE(CheckNoRtp2());
+    EXPECT_TRUE_WAIT(CheckRtcp1(), 1000);
+    EXPECT_TRUE_WAIT(CheckRtcp2(), 1000);
+    EXPECT_TRUE_WAIT(sent_rtcp1, 1000);
+    EXPECT_TRUE_WAIT(sent_rtcp2, 1000);
+    EXPECT_TRUE(CheckNoRtcp1());
+    EXPECT_TRUE(CheckNoRtcp2());
+  }
+
+  // Test that the mediachannel retains its sending state after the transport
+  // becomes non-writable.
+  void SendWithWritabilityLoss() {
+    CreateChannels(0, 0);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_EQ(1U, GetTransport1()->channels().size());
+    EXPECT_EQ(1U, GetTransport2()->channels().size());
+    EXPECT_TRUE(SendRtp1());
+    EXPECT_TRUE(SendRtp2());
+    EXPECT_TRUE(CheckRtp1());
+    EXPECT_TRUE(CheckRtp2());
+    EXPECT_TRUE(CheckNoRtp1());
+    EXPECT_TRUE(CheckNoRtp2());
+
+    GetTransport1()->SetDestination(NULL);
+    EXPECT_TRUE(media_channel1_->sending());
+    EXPECT_FALSE(SendRtp1());
+    EXPECT_TRUE(SendRtp2());
+    EXPECT_TRUE(CheckRtp1());
+    EXPECT_TRUE(CheckNoRtp2());
+
+    GetTransport1()->SetDestination(GetTransport2());
+    EXPECT_TRUE(media_channel1_->sending());
+    EXPECT_TRUE(SendRtp1());
+    EXPECT_TRUE(SendRtp2());
+    EXPECT_TRUE(CheckRtp1());
+    EXPECT_TRUE(CheckRtp2());
+    EXPECT_TRUE(CheckNoRtp1());
+    EXPECT_TRUE(CheckNoRtp2());
+  }
+
+  void SendSsrcMuxToSsrcMuxWithRtcpMux() {
+    CreateChannels(SSRC_MUX | RTCP | RTCP_MUX, SSRC_MUX | RTCP | RTCP_MUX);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_EQ(2U, GetTransport1()->channels().size());
+    EXPECT_EQ(1U, GetTransport2()->channels().size());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_EQ(1U, GetTransport1()->channels().size());
+    EXPECT_EQ(1U, GetTransport2()->channels().size());
+    EXPECT_TRUE(channel1_->ssrc_filter()->IsActive());
+    // channel1 - should have media_content2 as remote. i.e. kSsrc2
+    EXPECT_TRUE(channel1_->ssrc_filter()->FindStream(kSsrc2));
+    EXPECT_TRUE(channel2_->ssrc_filter()->IsActive());
+    // channel2 - should have media_content1 as remote. i.e. kSsrc1
+    EXPECT_TRUE(channel2_->ssrc_filter()->FindStream(kSsrc1));
+    EXPECT_TRUE(SendCustomRtp1(kSsrc1));
+    EXPECT_TRUE(SendCustomRtp2(kSsrc2));
+    EXPECT_TRUE(SendCustomRtcp1(kSsrc1));
+    EXPECT_TRUE(SendCustomRtcp2(kSsrc2));
+    EXPECT_TRUE(CheckCustomRtp1(kSsrc2));
+    EXPECT_TRUE(CheckNoRtp1());
+    EXPECT_TRUE(CheckCustomRtp2(kSsrc1));
+    EXPECT_TRUE(CheckNoRtp2());
+    EXPECT_TRUE(CheckCustomRtcp1(kSsrc2));
+    EXPECT_TRUE(CheckNoRtcp1());
+    EXPECT_TRUE(CheckCustomRtcp2(kSsrc1));
+    EXPECT_TRUE(CheckNoRtcp2());
+  }
+
+  void SendSsrcMuxToSsrcMux() {
+    CreateChannels(SSRC_MUX | RTCP, SSRC_MUX | RTCP);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_EQ(2U, GetTransport1()->channels().size());
+    EXPECT_EQ(2U, GetTransport2()->channels().size());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_EQ(2U, GetTransport1()->channels().size());
+    EXPECT_EQ(2U, GetTransport2()->channels().size());
+    EXPECT_TRUE(channel1_->ssrc_filter()->IsActive());
+    // channel1 - should have media_content2 as remote. i.e. kSsrc2
+    EXPECT_TRUE(channel1_->ssrc_filter()->FindStream(kSsrc2));
+    EXPECT_TRUE(channel2_->ssrc_filter()->IsActive());
+    // channel2 - should have media_content1 as remote. i.e. kSsrc1
+    EXPECT_TRUE(SendCustomRtp1(kSsrc1));
+    EXPECT_TRUE(SendCustomRtp2(kSsrc2));
+    EXPECT_TRUE(SendCustomRtcp1(kSsrc1));
+    EXPECT_TRUE(SendCustomRtcp2(kSsrc2));
+    EXPECT_TRUE(CheckCustomRtp1(kSsrc2));
+    EXPECT_FALSE(CheckCustomRtp1(kSsrc1));
+    EXPECT_TRUE(CheckCustomRtp2(kSsrc1));
+    EXPECT_FALSE(CheckCustomRtp2(kSsrc2));
+    EXPECT_TRUE(CheckCustomRtcp1(kSsrc2));
+    EXPECT_FALSE(CheckCustomRtcp1(kSsrc1));
+    EXPECT_TRUE(CheckCustomRtcp2(kSsrc1));
+    EXPECT_FALSE(CheckCustomRtcp2(kSsrc2));
+  }
+
+  // Test that the media monitor can be run and gives timely callbacks.
+  void TestMediaMonitor() {
+    static const int kTimeout = 500;
+    CreateChannels(0, 0);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    channel1_->StartMediaMonitor(100);
+    channel2_->StartMediaMonitor(100);
+    // Ensure we get callbacks and stop.
+    EXPECT_TRUE_WAIT(media_info_callbacks1_ > 0, kTimeout);
+    EXPECT_TRUE_WAIT(media_info_callbacks2_ > 0, kTimeout);
+    channel1_->StopMediaMonitor();
+    channel2_->StopMediaMonitor();
+    // Ensure a restart of a stopped monitor works.
+    channel1_->StartMediaMonitor(100);
+    EXPECT_TRUE_WAIT(media_info_callbacks1_ > 0, kTimeout);
+    channel1_->StopMediaMonitor();
+    // Ensure stopping a stopped monitor is OK.
+    channel1_->StopMediaMonitor();
+  }
+
+  void TestMediaSinks() {
+    CreateChannels(0, 0);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_FALSE(channel1_->HasSendSinks());
+    EXPECT_FALSE(channel1_->HasRecvSinks());
+
+    talk_base::Pathname path;
+    EXPECT_TRUE(talk_base::Filesystem::GetTemporaryFolder(path, true, NULL));
+    path.SetFilename("sink-test.rtpdump");
+    talk_base::scoped_ptr<cricket::RtpDumpSink> sink(
+        new cricket::RtpDumpSink(path.pathname()));
+    sink->set_packet_filter(cricket::PF_ALL);
+    EXPECT_TRUE(sink->Enable(true));
+    channel1_->RegisterSendSink(sink.get(), &cricket::RtpDumpSink::OnPacket);
+    EXPECT_TRUE(channel1_->HasSendSinks());
+    EXPECT_FALSE(channel1_->HasRecvSinks());
+    // The first packet is recorded with header + data.
+    EXPECT_TRUE(SendRtp1());
+    // The second packet is recorded with header only.
+    sink->set_packet_filter(cricket::PF_RTPHEADER);
+    EXPECT_TRUE(SendRtp1());
+    // The third packet is not recorded since sink is disabled.
+    EXPECT_TRUE(sink->Enable(false));
+    EXPECT_TRUE(SendRtp1());
+     // The fourth packet is not recorded since sink is unregistered.
+    EXPECT_TRUE(sink->Enable(true));
+    channel1_->UnregisterSendSink(sink.get());
+    EXPECT_TRUE(SendRtp1());
+    sink.reset();  // This will close the file.
+
+    // Read the recorded file and verify two packets.
+    talk_base::scoped_ptr<talk_base::StreamInterface> stream(
+        talk_base::Filesystem::OpenFile(path, "rb"));
+
+    cricket::RtpDumpReader reader(stream.get());
+    cricket::RtpDumpPacket packet;
+    EXPECT_EQ(talk_base::SR_SUCCESS, reader.ReadPacket(&packet));
+    std::string read_packet(reinterpret_cast<const char*>(&packet.data[0]),
+        packet.data.size());
+    EXPECT_EQ(rtp_packet_, read_packet);
+
+    EXPECT_EQ(talk_base::SR_SUCCESS, reader.ReadPacket(&packet));
+    size_t len = 0;
+    packet.GetRtpHeaderLen(&len);
+    EXPECT_EQ(len, packet.data.size());
+    EXPECT_EQ(0, memcmp(&packet.data[0], rtp_packet_.c_str(), len));
+
+    EXPECT_EQ(talk_base::SR_EOS, reader.ReadPacket(&packet));
+
+    // Delete the file for media recording.
+    stream.reset();
+    EXPECT_TRUE(talk_base::Filesystem::DeleteFile(path));
+  }
+
+  void TestSetContentFailure() {
+    CreateChannels(0, 0);
+    typename T::Content content;
+    cricket::SessionDescription* sdesc_loc = new cricket::SessionDescription();
+    cricket::SessionDescription* sdesc_rem = new cricket::SessionDescription();
+
+    // Set up the session description.
+    CreateContent(0, kPcmuCodec, kH264Codec, &content);
+    sdesc_loc->AddContent(cricket::CN_AUDIO, cricket::NS_JINGLE_RTP,
+                          new cricket::AudioContentDescription());
+    sdesc_loc->AddContent(cricket::CN_VIDEO, cricket::NS_JINGLE_RTP,
+                          new cricket::VideoContentDescription());
+    EXPECT_TRUE(session1_.set_local_description(sdesc_loc));
+    sdesc_rem->AddContent(cricket::CN_AUDIO, cricket::NS_JINGLE_RTP,
+                          new cricket::AudioContentDescription());
+    sdesc_rem->AddContent(cricket::CN_VIDEO, cricket::NS_JINGLE_RTP,
+                          new cricket::VideoContentDescription());
+    EXPECT_TRUE(session1_.set_remote_description(sdesc_rem));
+
+    // Test failures in SetLocalContent.
+    media_channel1_->set_fail_set_recv_codecs(true);
+    session1_.SetError(cricket::BaseSession::ERROR_NONE);
+    session1_.SignalState(&session1_, cricket::Session::STATE_SENTINITIATE);
+    EXPECT_EQ(cricket::BaseSession::ERROR_CONTENT, session1_.error());
+    media_channel1_->set_fail_set_recv_codecs(true);
+    session1_.SetError(cricket::BaseSession::ERROR_NONE);
+    session1_.SignalState(&session1_, cricket::Session::STATE_SENTACCEPT);
+    EXPECT_EQ(cricket::BaseSession::ERROR_CONTENT, session1_.error());
+
+    // Test failures in SetRemoteContent.
+    media_channel1_->set_fail_set_send_codecs(true);
+    session1_.SetError(cricket::BaseSession::ERROR_NONE);
+    session1_.SignalState(&session1_, cricket::Session::STATE_RECEIVEDINITIATE);
+    EXPECT_EQ(cricket::BaseSession::ERROR_CONTENT, session1_.error());
+    media_channel1_->set_fail_set_send_codecs(true);
+    session1_.SetError(cricket::BaseSession::ERROR_NONE);
+    session1_.SignalState(&session1_, cricket::Session::STATE_RECEIVEDACCEPT);
+    EXPECT_EQ(cricket::BaseSession::ERROR_CONTENT, session1_.error());
+  }
+
+  void TestFlushRtcp() {
+    bool send_rtcp1;
+
+    CreateChannels(RTCP, RTCP);
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_EQ(2U, GetTransport1()->channels().size());
+    EXPECT_EQ(2U, GetTransport2()->channels().size());
+
+    // Send RTCP1 from a different thread.
+    CallOnThreadAndWaitForDone(&ChannelTest<T>::SendRtcp1, &send_rtcp1);
+    EXPECT_TRUE(send_rtcp1);
+    // The sending message is only posted.  channel2_ should be empty.
+    EXPECT_TRUE(CheckNoRtcp2());
+
+    // When channel1_ is deleted, the RTCP packet should be sent out to
+    // channel2_.
+    channel1_.reset();
+    EXPECT_TRUE(CheckRtcp2());
+  }
+
+  void TestChangeStateError() {
+    CreateChannels(RTCP, RTCP);
+    EXPECT_TRUE(SendInitiate());
+    media_channel2_->set_fail_set_send(true);
+    EXPECT_TRUE(channel2_->Enable(true));
+    EXPECT_EQ(cricket::VoiceMediaChannel::ERROR_REC_DEVICE_OPEN_FAILED,
+              error_);
+  }
+
+  void TestSrtpError() {
+    static const unsigned char kBadPacket[] = {
+      0x90, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01
+    };
+    CreateChannels(RTCP | SECURE, RTCP | SECURE);
+    EXPECT_FALSE(channel1_->secure());
+    EXPECT_FALSE(channel2_->secure());
+    EXPECT_TRUE(SendInitiate());
+    EXPECT_TRUE(SendAccept());
+    EXPECT_TRUE(channel1_->secure());
+    EXPECT_TRUE(channel2_->secure());
+    channel2_->set_srtp_signal_silent_time(200);
+
+    // Testing failures in sending packets.
+    EXPECT_FALSE(media_channel2_->SendRtp(kBadPacket, sizeof(kBadPacket)));
+    // The first failure will trigger an error.
+    EXPECT_EQ_WAIT(T::MediaChannel::ERROR_REC_SRTP_ERROR, error_, 500);
+    error_ = T::MediaChannel::ERROR_NONE;
+    // The next 1 sec failures will not trigger an error.
+    EXPECT_FALSE(media_channel2_->SendRtp(kBadPacket, sizeof(kBadPacket)));
+    // Wait for a while to ensure no message comes in.
+    talk_base::Thread::Current()->ProcessMessages(210);
+    EXPECT_EQ(T::MediaChannel::ERROR_NONE, error_);
+    // The error will be triggered again.
+    EXPECT_FALSE(media_channel2_->SendRtp(kBadPacket, sizeof(kBadPacket)));
+    EXPECT_EQ_WAIT(T::MediaChannel::ERROR_REC_SRTP_ERROR, error_, 500);
+
+    // Testing failures in receiving packets.
+    error_ = T::MediaChannel::ERROR_NONE;
+    cricket::TransportChannel* transport_channel =
+        channel2_->transport_channel();
+    transport_channel->SignalReadPacket(
+        transport_channel, reinterpret_cast<const char*>(kBadPacket),
+        sizeof(kBadPacket));
+    EXPECT_EQ_WAIT(T::MediaChannel::ERROR_PLAY_SRTP_AUTH_FAILED, error_, 500);
+  }
+
+ protected:
+  cricket::FakeSession session1_;
+  cricket::FakeSession session2_;
+  cricket::FakeMediaEngine media_engine_;
+  // The media channels are owned by the voice channel objects below.
+  typename T::MediaChannel* media_channel1_;
+  typename T::MediaChannel* media_channel2_;
+  talk_base::scoped_ptr<typename T::Channel> channel1_;
+  talk_base::scoped_ptr<typename T::Channel> channel2_;
+  typename T::Content media_content1_;
+  typename T::Content media_content2_;
+  // The RTP and RTCP packets to send in the tests.
+  std::string rtp_packet_;
+  std::string rtcp_packet_;
+  int media_info_callbacks1_;
+  int media_info_callbacks2_;
+  uint32 ssrc_;
+  typename T::MediaChannel::Error error_;
+};
+
+
+template<>
+void ChannelTest<VoiceTraits>::CreateContent(
+    int flags,
+    const cricket::AudioCodec& audio_codec,
+    const cricket::VideoCodec& video_codec,
+    cricket::AudioContentDescription* audio) {
+  audio->AddCodec(audio_codec);
+  audio->set_rtcp_mux((flags & RTCP_MUX) != 0);
+  if (flags & SECURE) {
+    audio->AddCrypto(cricket::CryptoParams(
+        1, cricket::CS_AES_CM_128_HMAC_SHA1_32,
+        "inline:" + talk_base::CreateRandomString(40), ""));
+  }
+}
+
+template<>
+bool ChannelTest<VoiceTraits>::CodecMatches(const cricket::AudioCodec& c1,
+                                            const cricket::AudioCodec& c2) {
+  return c1.name == c2.name && c1.clockrate == c2.clockrate &&
+      c1.bitrate == c2.bitrate && c1.channels == c2.channels;
+}
+
+template<>
+void ChannelTest<VoiceTraits>::AddLegacyStreamInContent(
+    uint32 ssrc, int flags, cricket::AudioContentDescription* audio) {
+  if (flags & SSRC_MUX)
+    audio->AddLegacyStream(ssrc);
+}
+
+class VoiceChannelTest
+    : public ChannelTest<VoiceTraits> {
+ public:
+  typedef ChannelTest<VoiceTraits>
+  Base;
+  VoiceChannelTest() : Base(kPcmuFrame, sizeof(kPcmuFrame),
+                            kRtcpReport, sizeof(kRtcpReport)) {
+  }
+};
+
+// override to add NULL parameter
+template<>
+cricket::VideoChannel* ChannelTest<VideoTraits>::CreateChannel(
+    talk_base::Thread* thread, cricket::MediaEngineInterface* engine,
+    cricket::FakeVideoMediaChannel* ch, cricket::BaseSession* session,
+    bool rtcp) {
+  cricket::VideoChannel* channel = new cricket::VideoChannel(
+      thread, engine, ch, session, cricket::CN_VIDEO, rtcp, NULL);
+  if (!channel->Init()) {
+    delete channel;
+    channel = NULL;
+  }
+  return channel;
+}
+
+// override to add 0 parameter
+template<>
+bool ChannelTest<VideoTraits>::AddStream1(int id) {
+  return channel1_->AddStream(id, 0);
+}
+
+template<>
+void ChannelTest<VideoTraits>::CreateContent(
+    int flags,
+    const cricket::AudioCodec& audio_codec,
+    const cricket::VideoCodec& video_codec,
+    cricket::VideoContentDescription* video) {
+  video->AddCodec(video_codec);
+  video->set_rtcp_mux((flags & RTCP_MUX) != 0);
+  if (flags & SECURE) {
+    video->AddCrypto(cricket::CryptoParams(
+        1, cricket::CS_AES_CM_128_HMAC_SHA1_80,
+        "inline:" + talk_base::CreateRandomString(40), ""));
+  }
+}
+
+template<>
+bool ChannelTest<VideoTraits>::CodecMatches(const cricket::VideoCodec& c1,
+                                            const cricket::VideoCodec& c2) {
+  return c1.name == c2.name && c1.width == c2.width && c1.height == c2.height &&
+      c1.framerate == c2.framerate;
+}
+
+template<>
+void ChannelTest<VideoTraits>::AddLegacyStreamInContent(
+    uint32 ssrc, int flags, cricket::VideoContentDescription* video) {
+  if (flags & SSRC_MUX)
+    video->AddLegacyStream(ssrc);
+}
+
+class VideoChannelTest
+    : public ChannelTest<VideoTraits> {
+ public:
+  typedef ChannelTest<VideoTraits>
+  Base;
+  VideoChannelTest() : Base(kH264Packet, sizeof(kH264Packet),
+                            kRtcpReport, sizeof(kRtcpReport)) {
+  }
+};
+
+
+// VoiceChannelTest
+
+TEST_F(VoiceChannelTest, TestInit) {
+  Base::TestInit();
+  EXPECT_FALSE(media_channel1_->muted());
+  EXPECT_TRUE(media_channel1_->dtmf_queue().empty());
+}
+
+TEST_F(VoiceChannelTest, TestSetRtcpCName) {
+  Base::TestSetRtcpCName();
+}
+
+TEST_F(VoiceChannelTest, TestSetContents) {
+  Base::TestSetContents();
+}
+
+TEST_F(VoiceChannelTest, TestSetContentsNullOffer) {
+  Base::TestSetContentsNullOffer();
+}
+
+TEST_F(VoiceChannelTest, TestSetContentsRtcpMux) {
+  Base::TestSetContentsRtcpMux();
+}
+
+TEST_F(VoiceChannelTest, TestSetRemoteContentUpdate) {
+  Base::TestSetRemoteContentUpdate();
+}
+
+TEST_F(VoiceChannelTest, TestStreams) {
+  Base::TestStreams();
+}
+
+TEST_F(VoiceChannelTest, TestPlayoutAndSendingStates) {
+  Base::TestPlayoutAndSendingStates();
+}
+
+TEST_F(VoiceChannelTest, TestCallSetup) {
+  Base::TestCallSetup();
+}
+
+TEST_F(VoiceChannelTest, TestCallTeardownRtcpMux) {
+  Base::TestCallTeardownRtcpMux();
+}
+
+TEST_F(VoiceChannelTest, SendRtpToRtp) {
+  Base::SendRtpToRtp();
+}
+
+TEST_F(VoiceChannelTest, SendNoRtcpToNoRtcp) {
+  Base::SendNoRtcpToNoRtcp();
+}
+
+TEST_F(VoiceChannelTest, SendNoRtcpToRtcp) {
+  Base::SendNoRtcpToRtcp();
+}
+
+TEST_F(VoiceChannelTest, SendRtcpToNoRtcp) {
+  Base::SendRtcpToNoRtcp();
+}
+
+TEST_F(VoiceChannelTest, SendRtcpToRtcp) {
+  Base::SendRtcpToRtcp();
+}
+
+TEST_F(VoiceChannelTest, SendRtcpMuxToRtcp) {
+  Base::SendRtcpMuxToRtcp();
+}
+
+TEST_F(VoiceChannelTest, SendRtcpMuxToRtcpMux) {
+  Base::SendRtcpMuxToRtcpMux();
+}
+
+TEST_F(VoiceChannelTest, SendEarlyRtcpMuxToRtcp) {
+  Base::SendEarlyRtcpMuxToRtcp();
+}
+
+TEST_F(VoiceChannelTest, SendEarlyRtcpMuxToRtcpMux) {
+  Base::SendEarlyRtcpMuxToRtcpMux();
+}
+
+TEST_F(VoiceChannelTest, SendSrtpToSrtp) {
+  Base::SendSrtpToSrtp();
+}
+
+TEST_F(VoiceChannelTest, SendSrtpToRtp) {
+  Base::SendSrtpToSrtp();
+}
+
+TEST_F(VoiceChannelTest, SendSrtcpMux) {
+  Base::SendSrtcpMux();
+}
+
+TEST_F(VoiceChannelTest, SendRtpToRtpOnThread) {
+  Base::SendRtpToRtpOnThread();
+}
+
+TEST_F(VoiceChannelTest, SendSrtpToSrtpOnThread) {
+  Base::SendSrtpToSrtpOnThread();
+}
+
+TEST_F(VoiceChannelTest, SendWithWritabilityLoss) {
+  Base::SendWithWritabilityLoss();
+}
+
+TEST_F(VoiceChannelTest, TestMediaMonitor) {
+  Base::TestMediaMonitor();
+}
+
+// Test that Mute properly forwards to the media channel.
+TEST_F(VoiceChannelTest, TestMute) {
+  CreateChannels(0, 0);
+  EXPECT_FALSE(media_channel1_->muted());
+  EXPECT_TRUE(channel1_->Mute(true));
+  EXPECT_TRUE(media_channel1_->muted());
+  EXPECT_TRUE(channel1_->Mute(false));
+  EXPECT_FALSE(media_channel1_->muted());
+}
+
+// Test that keyboard automute works correctly.
+TEST_F(VoiceChannelTest, TestKeyboardMute) {
+  CreateChannels(0, 0);
+  EXPECT_FALSE(media_channel1_->muted());
+  EXPECT_EQ(cricket::VoiceMediaChannel::ERROR_NONE, error_);
+
+  cricket::VoiceMediaChannel::Error e =
+      cricket::VoiceMediaChannel::ERROR_REC_TYPING_NOISE_DETECTED;
+
+  // Typing doesn't mute automatically
+  media_channel1_->TriggerError(0, e);
+  talk_base::Thread::Current()->ProcessMessages(0);
+  EXPECT_EQ(e, error_);
+  EXPECT_FALSE(media_channel1_->muted());
+
+  // But it does when enabled
+  channel1_->set_mute_on_type(true, 200);
+  media_channel1_->TriggerError(0, e);
+  error_ = cricket::VoiceMediaChannel::ERROR_NONE;
+  EXPECT_TRUE_WAIT(error_ == e, 100);
+  EXPECT_TRUE(media_channel1_->muted());
+  EXPECT_TRUE_WAIT(!media_channel1_->muted(), 250);  // And resets.
+
+  // Muting manually preemts auto-unmute
+  media_channel1_->TriggerError(0, e);
+  error_ = cricket::VoiceMediaChannel::ERROR_NONE;
+  EXPECT_TRUE_WAIT(error_ == e, 100);
+  EXPECT_TRUE(media_channel1_->muted());
+  EXPECT_TRUE(channel1_->Mute(true));
+  talk_base::Thread::Current()->ProcessMessages(250);
+  EXPECT_TRUE(media_channel1_->muted());
+}
+
+// Test that PressDTMF properly forwards to the media channel.
+TEST_F(VoiceChannelTest, TestDtmf) {
+  CreateChannels(0, 0);
+  EXPECT_TRUE(SendInitiate());
+  EXPECT_TRUE(SendAccept());
+  EXPECT_EQ(0U, media_channel1_->dtmf_queue().size());
+  EXPECT_TRUE(channel1_->PressDTMF(1, true));
+  EXPECT_TRUE(channel1_->PressDTMF(8, false));
+  ASSERT_EQ(2U, media_channel1_->dtmf_queue().size());
+  EXPECT_EQ(1, media_channel1_->dtmf_queue()[0].first);
+  EXPECT_EQ(true, media_channel1_->dtmf_queue()[0].second);
+  EXPECT_EQ(8, media_channel1_->dtmf_queue()[1].first);
+  EXPECT_FALSE(media_channel1_->dtmf_queue()[1].second);
+}
+
+TEST_F(VoiceChannelTest, TestMediaSinks) {
+  Base::TestMediaSinks();
+}
+
+TEST_F(VoiceChannelTest, TestSetContentFailure) {
+  Base::TestSetContentFailure();
+}
+
+TEST_F(VoiceChannelTest, TestFlushRtcp) {
+  Base::TestFlushRtcp();
+}
+
+TEST_F(VoiceChannelTest, TestChangeStateError) {
+  Base::TestChangeStateError();
+}
+
+TEST_F(VoiceChannelTest, TestSrtpError) {
+  Base::TestSrtpError();
+}
+
+// Test that we can play a ringback tone properly.
+TEST_F(VoiceChannelTest, TestRingbackTone) {
+  CreateChannels(RTCP, RTCP);
+  EXPECT_FALSE(media_channel1_->ringback_tone_play());
+  EXPECT_TRUE(channel1_->SetRingbackTone("RIFF", 4));
+  EXPECT_TRUE(SendInitiate());
+  EXPECT_TRUE(SendAccept());
+  // Play ringback tone, no loop.
+  EXPECT_TRUE(channel1_->PlayRingbackTone(0, true, false));
+  EXPECT_EQ(0U, media_channel1_->ringback_tone_ssrc());
+  EXPECT_TRUE(media_channel1_->ringback_tone_play());
+  EXPECT_FALSE(media_channel1_->ringback_tone_loop());
+  // Stop the ringback tone.
+  EXPECT_TRUE(channel1_->PlayRingbackTone(0, false, false));
+  EXPECT_FALSE(media_channel1_->ringback_tone_play());
+  // Add a stream.
+  EXPECT_TRUE(AddStream1(1));
+  // Play ringback tone, looping, on the new stream.
+  EXPECT_TRUE(channel1_->PlayRingbackTone(1, true, true));
+  EXPECT_EQ(1U, media_channel1_->ringback_tone_ssrc());
+  EXPECT_TRUE(media_channel1_->ringback_tone_play());
+  EXPECT_TRUE(media_channel1_->ringback_tone_loop());
+  // Stop the ringback tone.
+  EXPECT_TRUE(channel1_->PlayRingbackTone(1, false, false));
+  EXPECT_FALSE(media_channel1_->ringback_tone_play());
+}
+
+// Test that we can scale the output volume properly for 1:1 calls.
+TEST_F(VoiceChannelTest, TestScaleVolume1to1Call) {
+  CreateChannels(RTCP, RTCP);
+  EXPECT_TRUE(SendInitiate());
+  EXPECT_TRUE(SendAccept());
+  double left, right;
+
+  // Default is (1.0, 1.0).
+  EXPECT_TRUE(media_channel1_->GetOutputScaling(0, &left, &right));
+  EXPECT_DOUBLE_EQ(1.0, left);
+  EXPECT_DOUBLE_EQ(1.0, right);
+  // invalid ssrc.
+  EXPECT_FALSE(media_channel1_->GetOutputScaling(3, &left, &right));
+
+  // Set scale to (1.5, 0.5).
+  EXPECT_TRUE(channel1_->SetOutputScaling(0, 1.5, 0.5));
+  EXPECT_TRUE(media_channel1_->GetOutputScaling(0, &left, &right));
+  EXPECT_DOUBLE_EQ(1.5, left);
+  EXPECT_DOUBLE_EQ(0.5, right);
+
+  // Set scale to (0, 0).
+  EXPECT_TRUE(channel1_->SetOutputScaling(0, 0.0, 0.0));
+  EXPECT_TRUE(media_channel1_->GetOutputScaling(0, &left, &right));
+  EXPECT_DOUBLE_EQ(0.0, left);
+  EXPECT_DOUBLE_EQ(0.0, right);
+}
+
+// Test that we can scale the output volume properly for multiway calls.
+TEST_F(VoiceChannelTest, TestScaleVolumeMultiwayCall) {
+  CreateChannels(RTCP, RTCP);
+  EXPECT_TRUE(SendInitiate());
+  EXPECT_TRUE(SendAccept());
+  EXPECT_TRUE(AddStream1(1));
+  EXPECT_TRUE(AddStream1(2));
+
+  double left, right;
+  // Default is (1.0, 1.0).
+  EXPECT_TRUE(media_channel1_->GetOutputScaling(0, &left, &right));
+  EXPECT_DOUBLE_EQ(1.0, left);
+  EXPECT_DOUBLE_EQ(1.0, right);
+  EXPECT_TRUE(media_channel1_->GetOutputScaling(1, &left, &right));
+  EXPECT_DOUBLE_EQ(1.0, left);
+  EXPECT_DOUBLE_EQ(1.0, right);
+  EXPECT_TRUE(media_channel1_->GetOutputScaling(2, &left, &right));
+  EXPECT_DOUBLE_EQ(1.0, left);
+  EXPECT_DOUBLE_EQ(1.0, right);
+  // invalid ssrc.
+  EXPECT_FALSE(media_channel1_->GetOutputScaling(3, &left, &right));
+
+  // Set scale to (1.5, 0.5) for ssrc = 1.
+  EXPECT_TRUE(channel1_->SetOutputScaling(1, 1.5, 0.5));
+  EXPECT_TRUE(media_channel1_->GetOutputScaling(1, &left, &right));
+  EXPECT_DOUBLE_EQ(1.5, left);
+  EXPECT_DOUBLE_EQ(0.5, right);
+  EXPECT_TRUE(media_channel1_->GetOutputScaling(2, &left, &right));
+  EXPECT_DOUBLE_EQ(1.0, left);
+  EXPECT_DOUBLE_EQ(1.0, right);
+  EXPECT_TRUE(media_channel1_->GetOutputScaling(0, &left, &right));
+  EXPECT_DOUBLE_EQ(1.0, left);
+  EXPECT_DOUBLE_EQ(1.0, right);
+
+  // Set scale to (0, 0) for all ssrcs.
+  EXPECT_TRUE(channel1_->SetOutputScaling(0,  0.0, 0.0));
+  EXPECT_TRUE(media_channel1_->GetOutputScaling(0, &left, &right));
+  EXPECT_DOUBLE_EQ(0.0, left);
+  EXPECT_DOUBLE_EQ(0.0, right);
+  EXPECT_TRUE(media_channel1_->GetOutputScaling(1, &left, &right));
+  EXPECT_DOUBLE_EQ(0.0, left);
+  EXPECT_DOUBLE_EQ(0.0, right);
+  EXPECT_TRUE(media_channel1_->GetOutputScaling(2, &left, &right));
+  EXPECT_DOUBLE_EQ(0.0, left);
+  EXPECT_DOUBLE_EQ(0.0, right);
+}
+
+TEST_F(VoiceChannelTest, SendSsrcMuxToSsrcMux) {
+  Base::SendSsrcMuxToSsrcMux();
+}
+
+TEST_F(VoiceChannelTest, SendSsrcMuxToSsrcMuxWithRtcpMux) {
+  Base::SendSsrcMuxToSsrcMuxWithRtcpMux();
+}
+
+// VideoChannelTest
+TEST_F(VideoChannelTest, TestInit) {
+  Base::TestInit();
+}
+
+TEST_F(VideoChannelTest, TestSetRtcpCName) {
+  Base::TestSetRtcpCName();
+}
+
+TEST_F(VideoChannelTest, TestSetContents) {
+  Base::TestSetContents();
+}
+
+TEST_F(VideoChannelTest, TestSetContentsNullOffer) {
+  Base::TestSetContentsNullOffer();
+}
+
+TEST_F(VideoChannelTest, TestSetContentsRtcpMux) {
+  Base::TestSetContentsRtcpMux();
+}
+
+TEST_F(VideoChannelTest, TestSetRemoteContentUpdate) {
+  Base::TestSetRemoteContentUpdate();
+}
+
+TEST_F(VideoChannelTest, TestStreams) {
+  Base::TestStreams();
+}
+
+TEST_F(VideoChannelTest, TestPlayoutAndSendingStates) {
+  Base::TestPlayoutAndSendingStates();
+}
+
+TEST_F(VideoChannelTest, TestCallSetup) {
+  Base::TestCallSetup();
+}
+
+TEST_F(VideoChannelTest, TestCallTeardownRtcpMux) {
+  Base::TestCallTeardownRtcpMux();
+}
+
+TEST_F(VideoChannelTest, SendRtpToRtp) {
+  Base::SendRtpToRtp();
+}
+
+TEST_F(VideoChannelTest, SendNoRtcpToNoRtcp) {
+  Base::SendNoRtcpToNoRtcp();
+}
+
+TEST_F(VideoChannelTest, SendNoRtcpToRtcp) {
+  Base::SendNoRtcpToRtcp();
+}
+
+TEST_F(VideoChannelTest, SendRtcpToNoRtcp) {
+  Base::SendRtcpToNoRtcp();
+}
+
+TEST_F(VideoChannelTest, SendRtcpToRtcp) {
+  Base::SendRtcpToRtcp();
+}
+
+TEST_F(VideoChannelTest, SendRtcpMuxToRtcp) {
+  Base::SendRtcpMuxToRtcp();
+}
+
+TEST_F(VideoChannelTest, SendRtcpMuxToRtcpMux) {
+  Base::SendRtcpMuxToRtcpMux();
+}
+
+TEST_F(VideoChannelTest, SendEarlyRtcpMuxToRtcp) {
+  Base::SendEarlyRtcpMuxToRtcp();
+}
+
+TEST_F(VideoChannelTest, SendEarlyRtcpMuxToRtcpMux) {
+  Base::SendEarlyRtcpMuxToRtcpMux();
+}
+
+TEST_F(VideoChannelTest, SendSrtpToSrtp) {
+  Base::SendSrtpToSrtp();
+}
+
+TEST_F(VideoChannelTest, SendSrtpToRtp) {
+  Base::SendSrtpToSrtp();
+}
+
+TEST_F(VideoChannelTest, SendSrtcpMux) {
+  Base::SendSrtcpMux();
+}
+
+TEST_F(VideoChannelTest, SendRtpToRtpOnThread) {
+  Base::SendRtpToRtpOnThread();
+}
+
+TEST_F(VideoChannelTest, SendSrtpToSrtpOnThread) {
+  Base::SendSrtpToSrtpOnThread();
+}
+
+TEST_F(VideoChannelTest, SendWithWritabilityLoss) {
+  Base::SendWithWritabilityLoss();
+}
+
+TEST_F(VideoChannelTest, TestMediaMonitor) {
+  Base::TestMediaMonitor();
+}
+
+TEST_F(VideoChannelTest, TestMediaSinks) {
+  Base::TestMediaSinks();
+}
+
+TEST_F(VideoChannelTest, TestSetContentFailure) {
+  Base::TestSetContentFailure();
+}
+
+TEST_F(VideoChannelTest, TestFlushRtcp) {
+  Base::TestFlushRtcp();
+}
+
+TEST_F(VideoChannelTest, SendSsrcMuxToSsrcMux) {
+  Base::SendSsrcMuxToSsrcMux();
+}
+
+TEST_F(VideoChannelTest, SendSsrcMuxToSsrcMuxWithRtcpMux) {
+  Base::SendSsrcMuxToSsrcMuxWithRtcpMux();
+}
+
+// TODO: Add VideoChannelTest.TestChangeStateError.
+
+TEST_F(VideoChannelTest, TestSrtpError) {
+  Base::TestSrtpError();
+}
diff --git a/talk/session/phone/channelmanager_unittest.cc b/talk/session/phone/channelmanager_unittest.cc
new file mode 100644
index 0000000..88e821f
--- /dev/null
+++ b/talk/session/phone/channelmanager_unittest.cc
@@ -0,0 +1,505 @@
+// libjingle
+// Copyright 2008 Google Inc.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//  1. Redistributions of source code must retain the above copyright notice,
+//     this list of conditions and the following disclaimer.
+//  2. Redistributions in binary form must reproduce the above copyright notice,
+//     this list of conditions and the following disclaimer in the documentation
+//     and/or other materials provided with the distribution.
+//  3. The name of the author may not be used to endorse or promote products
+//     derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "talk/base/gunit.h"
+#include "talk/base/logging.h"
+#include "talk/base/thread.h"
+#include "talk/p2p/base/fakesession.h"
+#include "talk/session/phone/channelmanager.h"
+#include "talk/session/phone/fakedevicemanager.h"
+#include "talk/session/phone/fakemediaengine.h"
+#include "talk/session/phone/fakemediaprocessor.h"
+#include "talk/session/phone/nullvideorenderer.h"
+
+class ChannelManagerTest : public testing::Test {
+ protected:
+  ChannelManagerTest() : fme_(NULL), fdm_(NULL), cm_(NULL) {
+  }
+
+  virtual void SetUp() {
+    fme_ = new cricket::FakeMediaEngine();
+    fdm_ = new cricket::FakeDeviceManager();
+    cm_ = new cricket::ChannelManager(fme_, fdm_, talk_base::Thread::Current());
+    session_ = new cricket::FakeSession();
+
+    std::vector<std::string> in_device_list, out_device_list, vid_device_list;
+    in_device_list.push_back("audio-in1");
+    in_device_list.push_back("audio-in2");
+    out_device_list.push_back("audio-out1");
+    out_device_list.push_back("audio-out2");
+    vid_device_list.push_back("video-in1");
+    vid_device_list.push_back("video-in2");
+    fdm_->SetAudioInputDevices(in_device_list);
+    fdm_->SetAudioOutputDevices(out_device_list);
+    fdm_->SetVideoCaptureDevices(vid_device_list);
+  }
+
+  virtual void TearDown() {
+    delete session_;
+    delete cm_;
+    cm_ = NULL;
+    fdm_ = NULL;
+    fme_ = NULL;
+  }
+
+  talk_base::Thread worker_;
+  cricket::FakeMediaEngine* fme_;
+  cricket::FakeDeviceManager* fdm_;
+  cricket::ChannelManager* cm_;
+  cricket::FakeSession* session_;
+};
+
+// Test that we startup/shutdown properly.
+TEST_F(ChannelManagerTest, StartupShutdown) {
+  EXPECT_FALSE(cm_->initialized());
+  EXPECT_EQ(talk_base::Thread::Current(), cm_->worker_thread());
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_TRUE(cm_->initialized());
+  cm_->Terminate();
+  EXPECT_FALSE(cm_->initialized());
+}
+
+// Test that we startup/shutdown properly with a worker thread.
+TEST_F(ChannelManagerTest, StartupShutdownOnThread) {
+  worker_.Start();
+  EXPECT_FALSE(cm_->initialized());
+  EXPECT_EQ(talk_base::Thread::Current(), cm_->worker_thread());
+  EXPECT_TRUE(cm_->set_worker_thread(&worker_));
+  EXPECT_EQ(&worker_, cm_->worker_thread());
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_TRUE(cm_->initialized());
+  // Setting the worker thread while initialized should fail.
+  EXPECT_FALSE(cm_->set_worker_thread(talk_base::Thread::Current()));
+  cm_->Terminate();
+  EXPECT_FALSE(cm_->initialized());
+}
+
+// Test that we fail to startup if we're given an unstarted thread.
+TEST_F(ChannelManagerTest, StartupShutdownOnUnstartedThread) {
+  EXPECT_TRUE(cm_->set_worker_thread(&worker_));
+  EXPECT_FALSE(cm_->Init());
+  EXPECT_FALSE(cm_->initialized());
+}
+
+// Test that we can create and destroy a voice and video channel.
+TEST_F(ChannelManagerTest, CreateDestroyChannels) {
+  EXPECT_TRUE(cm_->Init());
+  cricket::VoiceChannel* voice_channel = cm_->CreateVoiceChannel(
+      session_, cricket::CN_AUDIO, false);
+  EXPECT_TRUE(voice_channel != NULL);
+  cricket::VideoChannel* video_channel =
+      cm_->CreateVideoChannel(session_, cricket::CN_VIDEO,
+                              false, voice_channel);
+  EXPECT_TRUE(video_channel != NULL);
+  cm_->DestroyVideoChannel(video_channel);
+  cm_->DestroyVoiceChannel(voice_channel);
+  cm_->Terminate();
+}
+
+// Test that we can create and destroy a voice and video channel with a worker.
+TEST_F(ChannelManagerTest, CreateDestroyChannelsOnThread) {
+  worker_.Start();
+  EXPECT_TRUE(cm_->set_worker_thread(&worker_));
+  EXPECT_TRUE(cm_->Init());
+  cricket::VoiceChannel* voice_channel = cm_->CreateVoiceChannel(
+      session_, cricket::CN_AUDIO, false);
+  EXPECT_TRUE(voice_channel != NULL);
+  cricket::VideoChannel* video_channel =
+      cm_->CreateVideoChannel(session_, cricket::CN_VIDEO,
+                              false, voice_channel);
+  EXPECT_TRUE(video_channel != NULL);
+  cm_->DestroyVideoChannel(video_channel);
+  cm_->DestroyVoiceChannel(voice_channel);
+  cm_->Terminate();
+}
+
+// Test that we fail to create a voice/video channel if the session is unable
+// to create a cricket::TransportChannel
+TEST_F(ChannelManagerTest, NoTransportChannelTest) {
+  EXPECT_TRUE(cm_->Init());
+  session_->set_fail_channel_creation(true);
+  // The test is useless unless the session does not fail creating
+  // cricket::TransportChannel.
+  ASSERT_TRUE(session_->CreateChannel("audio", "rtp") == NULL);
+
+  cricket::VoiceChannel* voice_channel = cm_->CreateVoiceChannel(
+      session_, cricket::CN_AUDIO, false);
+  EXPECT_TRUE(voice_channel == NULL);
+  cricket::VideoChannel* video_channel =
+      cm_->CreateVideoChannel(session_, cricket::CN_VIDEO,
+                              false, voice_channel);
+  EXPECT_TRUE(video_channel == NULL);
+  cm_->Terminate();
+}
+
+// Test that SetDefaultVideoCodec passes through the right values.
+TEST_F(ChannelManagerTest, SetDefaultVideoEncoderConfig) {
+  cricket::VideoCodec codec(96, "G264", 1280, 720, 60, 0);
+  cricket::VideoEncoderConfig config(codec, 1, 2);
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_TRUE(cm_->SetDefaultVideoEncoderConfig(config));
+  EXPECT_EQ(config, fme_->default_video_encoder_config());
+}
+
+// Test that SetDefaultVideoCodec passes through the right values.
+TEST_F(ChannelManagerTest, SetDefaultVideoCodecBeforeInit) {
+  cricket::VideoCodec codec(96, "G264", 1280, 720, 60, 0);
+  cricket::VideoEncoderConfig config(codec, 1, 2);
+  EXPECT_TRUE(cm_->SetDefaultVideoEncoderConfig(config));
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_EQ(config, fme_->default_video_encoder_config());
+}
+
+TEST_F(ChannelManagerTest, SetAudioOptionsBeforeInit) {
+  // Test that values that we set before Init are applied.
+  EXPECT_TRUE(cm_->SetAudioOptions("audio-in1", "audio-out1", 0x2));
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_EQ("audio-in1", fme_->audio_in_device());
+  EXPECT_EQ("audio-out1", fme_->audio_out_device());
+  EXPECT_EQ(0x2, fme_->audio_options());
+}
+
+TEST_F(ChannelManagerTest, GetAudioOptionsBeforeInit) {
+  std::string audio_in, audio_out;
+  int opts;
+  // Test that GetAudioOptions works before Init.
+  EXPECT_TRUE(cm_->SetAudioOptions("audio-in2", "audio-out2", 0x1));
+  EXPECT_TRUE(cm_->GetAudioOptions(&audio_in, &audio_out, &opts));
+  EXPECT_EQ("audio-in2", audio_in);
+  EXPECT_EQ("audio-out2", audio_out);
+  EXPECT_EQ(0x1, opts);
+  // Test that options set before Init can be gotten after Init.
+  EXPECT_TRUE(cm_->SetAudioOptions("audio-in1", "audio-out1", 0x2));
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_TRUE(cm_->GetAudioOptions(&audio_in, &audio_out, &opts));
+  EXPECT_EQ("audio-in1", audio_in);
+  EXPECT_EQ("audio-out1", audio_out);
+  EXPECT_EQ(0x2, opts);
+}
+
+TEST_F(ChannelManagerTest, SetAudioOptions) {
+  // Test initial state.
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_EQ("", fme_->audio_in_device());
+  EXPECT_EQ("", fme_->audio_out_device());
+  EXPECT_EQ(cricket::MediaEngineInterface::DEFAULT_AUDIO_OPTIONS,
+            fme_->audio_options());
+  // Test setting defaults.
+  EXPECT_TRUE(cm_->SetAudioOptions("", "", 0x3));
+  EXPECT_EQ("", fme_->audio_in_device());
+  EXPECT_EQ("", fme_->audio_out_device());
+  EXPECT_EQ(0x3, fme_->audio_options());
+  // Test setting specific values.
+  EXPECT_TRUE(cm_->SetAudioOptions("audio-in1", "audio-out1", 0x2));
+  EXPECT_EQ("audio-in1", fme_->audio_in_device());
+  EXPECT_EQ("audio-out1", fme_->audio_out_device());
+  EXPECT_EQ(0x2, fme_->audio_options());
+  // Test setting bad values.
+  EXPECT_FALSE(cm_->SetAudioOptions("audio-in9", "audio-out2", 0x1));
+}
+
+TEST_F(ChannelManagerTest, GetAudioOptions) {
+  std::string audio_in, audio_out;
+  int opts;
+  // Test initial state.
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_TRUE(cm_->GetAudioOptions(&audio_in, &audio_out, &opts));
+  EXPECT_EQ(std::string(cricket::DeviceManagerInterface::kDefaultDeviceName),
+            audio_in);
+  EXPECT_EQ(std::string(cricket::DeviceManagerInterface::kDefaultDeviceName),
+            audio_out);
+  EXPECT_EQ(cricket::MediaEngineInterface::DEFAULT_AUDIO_OPTIONS, opts);
+  // Test that we get back specific values that we set.
+  EXPECT_TRUE(cm_->SetAudioOptions("audio-in1", "audio-out1", 0x2));
+  EXPECT_TRUE(cm_->GetAudioOptions(&audio_in, &audio_out, &opts));
+  EXPECT_EQ("audio-in1", audio_in);
+  EXPECT_EQ("audio-out1", audio_out);
+  EXPECT_EQ(0x2, opts);
+}
+
+TEST_F(ChannelManagerTest, SetVideoOptionsBeforeInit) {
+  // Test that values that we set before Init are applied.
+  EXPECT_TRUE(cm_->SetVideoOptions("video-in2"));
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_EQ("video-in2", fme_->video_in_device());
+}
+
+TEST_F(ChannelManagerTest, GetVideoOptionsBeforeInit) {
+  std::string video_in;
+  // Test that GetVideoOptions works before Init.
+  EXPECT_TRUE(cm_->SetVideoOptions("video-in1"));
+  EXPECT_TRUE(cm_->GetVideoOptions(&video_in));
+  EXPECT_EQ("video-in1", video_in);
+  // Test that options set before Init can be gotten after Init.
+  EXPECT_TRUE(cm_->SetVideoOptions("video-in2"));
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_TRUE(cm_->GetVideoOptions(&video_in));
+  EXPECT_EQ("video-in2", video_in);
+}
+
+TEST_F(ChannelManagerTest, SetVideoOptions) {
+  // Test setting defaults.
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_TRUE(cm_->SetVideoOptions(""));  // will use DeviceManager default
+  EXPECT_EQ("video-in1", fme_->video_in_device());
+  // Test setting specific values.
+  EXPECT_TRUE(cm_->SetVideoOptions("video-in2"));
+  EXPECT_EQ("video-in2", fme_->video_in_device());
+  // TODO: Add test for invalid value here.
+}
+
+// Test unplugging and plugging back the preferred devices. When the preferred
+// device is unplugged, we fall back to the default device. When the preferred
+// device is plugged back, we use it.
+TEST_F(ChannelManagerTest, SetAudioOptionsUnplugPlug) {
+  // Set preferences "audio-in1" and "audio-out1" before init.
+  EXPECT_TRUE(cm_->SetAudioOptions("audio-in1", "audio-out1", 0x2));
+  // Unplug device "audio-in1" and "audio-out1".
+  std::vector<std::string> in_device_list, out_device_list;
+  in_device_list.push_back("audio-in2");
+  out_device_list.push_back("audio-out2");
+  fdm_->SetAudioInputDevices(in_device_list);
+  fdm_->SetAudioOutputDevices(out_device_list);
+  // Init should fall back to default devices.
+  EXPECT_TRUE(cm_->Init());
+  // The media engine should use the default.
+  EXPECT_EQ("", fme_->audio_in_device());
+  EXPECT_EQ("", fme_->audio_out_device());
+  // The channel manager keeps the preferences "audio-in1" and "audio-out1".
+  std::string audio_in, audio_out;
+  int opts;
+  EXPECT_TRUE(cm_->GetAudioOptions(&audio_in, &audio_out, &opts));
+  EXPECT_EQ("audio-in1", audio_in);
+  EXPECT_EQ("audio-out1", audio_out);
+  cm_->Terminate();
+
+  // Plug devices "audio-in2" and "audio-out2" back.
+  in_device_list.push_back("audio-in1");
+  out_device_list.push_back("audio-out1");
+  fdm_->SetAudioInputDevices(in_device_list);
+  fdm_->SetAudioOutputDevices(out_device_list);
+  // Init again. The preferences, "audio-in2" and "audio-out2", are used.
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_EQ("audio-in1", fme_->audio_in_device());
+  EXPECT_EQ("audio-out1", fme_->audio_out_device());
+  EXPECT_TRUE(cm_->GetAudioOptions(&audio_in, &audio_out, &opts));
+  EXPECT_EQ("audio-in1", audio_in);
+  EXPECT_EQ("audio-out1", audio_out);
+}
+
+// We have one camera. Unplug it, fall back to no camera.
+TEST_F(ChannelManagerTest, SetVideoOptionsUnplugPlugOneCamera) {
+  // Set preferences "video-in1" before init.
+  std::vector<std::string> vid_device_list;
+  vid_device_list.push_back("video-in1");
+  fdm_->SetVideoCaptureDevices(vid_device_list);
+  EXPECT_TRUE(cm_->SetVideoOptions("video-in1"));
+
+  // Unplug "video-in1".
+  vid_device_list.clear();
+  fdm_->SetVideoCaptureDevices(vid_device_list);
+
+  // Init should fall back to avatar.
+  EXPECT_TRUE(cm_->Init());
+  // The media engine should use no camera.
+  EXPECT_EQ("", fme_->video_in_device());
+  // The channel manager keeps the user preference "video-in".
+  std::string video_in;
+  EXPECT_TRUE(cm_->GetVideoOptions(&video_in));
+  EXPECT_EQ("video-in1", video_in);
+  cm_->Terminate();
+
+  // Plug device "video-in1" back.
+  vid_device_list.push_back("video-in1");
+  fdm_->SetVideoCaptureDevices(vid_device_list);
+  // Init again. The user preferred device, "video-in1", is used.
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_EQ("video-in1", fme_->video_in_device());
+  EXPECT_TRUE(cm_->GetVideoOptions(&video_in));
+  EXPECT_EQ("video-in1", video_in);
+}
+
+// We have multiple cameras. Unplug the preferred, fall back to another camera.
+TEST_F(ChannelManagerTest, SetVideoOptionsUnplugPlugTwoDevices) {
+  // Set video device to "video-in1" before init.
+  EXPECT_TRUE(cm_->SetVideoOptions("video-in1"));
+  // Unplug device "video-in1".
+  std::vector<std::string> vid_device_list;
+  vid_device_list.push_back("video-in2");
+  fdm_->SetVideoCaptureDevices(vid_device_list);
+  // Init should fall back to default device "video-in2".
+  EXPECT_TRUE(cm_->Init());
+  // The media engine should use the default device "video-in2".
+  EXPECT_EQ("video-in2", fme_->video_in_device());
+  // The channel manager keeps the user preference "video-in".
+  std::string video_in;
+  EXPECT_TRUE(cm_->GetVideoOptions(&video_in));
+  EXPECT_EQ("video-in1", video_in);
+  cm_->Terminate();
+
+  // Plug device "video-in1" back.
+  vid_device_list.push_back("video-in1");
+  fdm_->SetVideoCaptureDevices(vid_device_list);
+  // Init again. The user preferred device, "video-in1", is used.
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_EQ("video-in1", fme_->video_in_device());
+  EXPECT_TRUE(cm_->GetVideoOptions(&video_in));
+  EXPECT_EQ("video-in1", video_in);
+}
+
+TEST_F(ChannelManagerTest, GetVideoOptions) {
+  std::string video_in;
+  // Test setting/getting defaults.
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_TRUE(cm_->SetVideoOptions(""));
+  EXPECT_TRUE(cm_->GetVideoOptions(&video_in));
+  EXPECT_EQ("video-in1", video_in);
+  // Test setting/getting specific values.
+  EXPECT_TRUE(cm_->SetVideoOptions("video-in2"));
+  EXPECT_TRUE(cm_->GetVideoOptions(&video_in));
+  EXPECT_EQ("video-in2", video_in);
+}
+
+TEST_F(ChannelManagerTest, GetSetOutputVolume) {
+  int level;
+  // Setting and getting should fail before Init.
+  EXPECT_EQ(-1, fme_->output_volume());
+  EXPECT_FALSE(cm_->GetOutputVolume(&level));
+  EXPECT_FALSE(cm_->SetOutputVolume(99));
+  EXPECT_EQ(-1, fme_->output_volume());
+  // Setting and getting should work after Init.
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_TRUE(cm_->GetOutputVolume(&level));
+  EXPECT_EQ(fme_->output_volume(), level);
+  EXPECT_TRUE(cm_->SetOutputVolume(99));
+  EXPECT_EQ(99, fme_->output_volume());
+  EXPECT_TRUE(cm_->GetOutputVolume(&level));
+  EXPECT_EQ(99, level);
+}
+
+// Test that a value set before Init is applied properly.
+TEST_F(ChannelManagerTest, SetLocalRendererBeforeInit) {
+  cricket::NullVideoRenderer renderer;
+  EXPECT_TRUE(cm_->SetLocalRenderer(&renderer));
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_EQ(&renderer, fme_->local_renderer());
+}
+
+// Test that a value set after init is passed through properly.
+TEST_F(ChannelManagerTest, SetLocalRenderer) {
+  cricket::NullVideoRenderer renderer;
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_TRUE(cm_->SetLocalRenderer(&renderer));
+  EXPECT_EQ(&renderer, fme_->local_renderer());
+}
+
+// Test that logging options set before Init are applied properly,
+// and retained even after Init.
+TEST_F(ChannelManagerTest, SetLoggingBeforeInit) {
+  cm_->SetVoiceLogging(talk_base::LS_INFO, "test-voice");
+  cm_->SetVideoLogging(talk_base::LS_VERBOSE, "test-video");
+  EXPECT_EQ(talk_base::LS_INFO, fme_->voice_loglevel());
+  EXPECT_STREQ("test-voice", fme_->voice_logfilter().c_str());
+  EXPECT_EQ(talk_base::LS_VERBOSE, fme_->video_loglevel());
+  EXPECT_STREQ("test-video", fme_->video_logfilter().c_str());
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_EQ(talk_base::LS_INFO, fme_->voice_loglevel());
+  EXPECT_STREQ("test-voice", fme_->voice_logfilter().c_str());
+  EXPECT_EQ(talk_base::LS_VERBOSE, fme_->video_loglevel());
+  EXPECT_STREQ("test-video", fme_->video_logfilter().c_str());
+}
+
+// Test that logging options set after Init are applied properly.
+TEST_F(ChannelManagerTest, SetLogging) {
+  EXPECT_TRUE(cm_->Init());
+  cm_->SetVoiceLogging(talk_base::LS_INFO, "test-voice");
+  cm_->SetVideoLogging(talk_base::LS_VERBOSE, "test-video");
+  EXPECT_EQ(talk_base::LS_INFO, fme_->voice_loglevel());
+  EXPECT_STREQ("test-voice", fme_->voice_logfilter().c_str());
+  EXPECT_EQ(talk_base::LS_VERBOSE, fme_->video_loglevel());
+  EXPECT_STREQ("test-video", fme_->video_logfilter().c_str());
+}
+
+// Test that SetVideoCapture passes through the right value.
+TEST_F(ChannelManagerTest, SetVideoCapture) {
+  // Should fail until we are initialized.
+  EXPECT_FALSE(fme_->capture());
+  EXPECT_EQ(cricket::CR_FAILURE, cm_->SetVideoCapture(true));
+  EXPECT_FALSE(fme_->capture());
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_FALSE(fme_->capture());
+  EXPECT_EQ(cricket::CR_SUCCESS, cm_->SetVideoCapture(true));
+  EXPECT_TRUE(fme_->capture());
+  EXPECT_EQ(cricket::CR_SUCCESS, cm_->SetVideoCapture(false));
+  EXPECT_FALSE(fme_->capture());
+}
+
+// Test that the Video/Voice Processors register and unregister
+TEST_F(ChannelManagerTest, RegisterProcessors) {
+  cricket::FakeMediaProcessor fmp;
+  EXPECT_TRUE(cm_->Init());
+  EXPECT_FALSE(fme_->video_processor_registered());
+  EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_TX));
+  EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_RX));
+
+  EXPECT_TRUE(cm_->RegisterVideoProcessor(1, &fmp));
+  EXPECT_TRUE(fme_->video_processor_registered());
+  EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_TX));
+  EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_RX));
+
+  EXPECT_TRUE(cm_->UnregisterVideoProcessor(1, &fmp));
+  EXPECT_FALSE(fme_->video_processor_registered());
+  EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_TX));
+  EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_RX));
+
+  EXPECT_TRUE(cm_->RegisterVoiceProcessor(1,
+                                          &fmp,
+                                          cricket::MPD_RX));
+  EXPECT_FALSE(fme_->video_processor_registered());
+  EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_TX));
+  EXPECT_TRUE(fme_->voice_processor_registered(cricket::MPD_RX));
+
+
+  EXPECT_TRUE(cm_->UnregisterVoiceProcessor(1,
+                                            &fmp,
+                                            cricket::MPD_RX));
+  EXPECT_FALSE(fme_->video_processor_registered());
+  EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_TX));
+  EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_RX));
+
+  EXPECT_TRUE(cm_->RegisterVoiceProcessor(1,
+                                          &fmp,
+                                          cricket::MPD_TX));
+  EXPECT_FALSE(fme_->video_processor_registered());
+  EXPECT_TRUE(fme_->voice_processor_registered(cricket::MPD_TX));
+  EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_RX));
+
+  EXPECT_TRUE(cm_->UnregisterVoiceProcessor(1,
+                                            &fmp,
+                                            cricket::MPD_TX));
+  EXPECT_FALSE(fme_->video_processor_registered());
+  EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_TX));
+  EXPECT_FALSE(fme_->voice_processor_registered(cricket::MPD_RX));
+}
diff --git a/talk/session/phone/mediarecorder.cc b/talk/session/phone/mediarecorder.cc
new file mode 100644
index 0000000..7b06e48
--- /dev/null
+++ b/talk/session/phone/mediarecorder.cc
@@ -0,0 +1,225 @@
+/*
+ * libjingle
+ * Copyright 2010, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "talk/session/phone/mediarecorder.h"
+
+#include <limits.h>
+
+#include <string>
+
+#include "talk/base/fileutils.h"
+#include "talk/base/logging.h"
+#include "talk/base/pathutils.h"
+#include "talk/session/phone/channel.h"
+#include "talk/session/phone/rtpdump.h"
+
+
+namespace cricket {
+
+///////////////////////////////////////////////////////////////////////////
+// Implementation of RtpDumpSink.
+///////////////////////////////////////////////////////////////////////////
+RtpDumpSink::RtpDumpSink(const std::string& filename)
+    : max_size_(INT_MAX),
+      recording_(false),
+      packet_filter_(PF_NONE),
+      filename_(filename) {
+}
+
+RtpDumpSink::~RtpDumpSink() {}
+
+void RtpDumpSink::SetMaxSize(size_t size) {
+  talk_base::CritScope cs(&critical_section_);
+  max_size_ = size;
+}
+
+bool RtpDumpSink::Enable(bool enable) {
+  talk_base::CritScope cs(&critical_section_);
+
+  recording_ = enable;
+
+  // Create a file and the RTP writer if we have not done yet.
+  if (recording_ && !writer_.get()) {
+    stream_.reset(talk_base::Filesystem::OpenFile(
+        talk_base::Pathname(filename_), "wb"));
+    if (!stream_.get()) {
+      return false;
+    }
+    writer_.reset(new RtpDumpWriter(stream_.get()));
+    writer_->set_packet_filter(packet_filter_);
+  } else if (!recording_ && stream_.get()) {
+    stream_->Flush();
+  }
+  return true;
+}
+
+void RtpDumpSink::OnPacket(const void* data, size_t size, bool rtcp) {
+  talk_base::CritScope cs(&critical_section_);
+
+  if (recording_ && writer_.get()) {
+    size_t current_size;
+    if (writer_->GetDumpSize(&current_size) &&
+        current_size + RtpDumpPacket::kHeaderLength + size <= max_size_) {
+      if (!rtcp) {
+        writer_->WriteRtpPacket(data, size);
+      } else {
+        // TODO: Enable recording RTCP.
+      }
+    }
+  }
+}
+
+void RtpDumpSink::set_packet_filter(int filter) {
+  talk_base::CritScope cs(&critical_section_);
+  packet_filter_ = filter;
+  if (writer_.get()) {
+    writer_->set_packet_filter(packet_filter_);
+  }
+}
+
+void RtpDumpSink::Flush() {
+  talk_base::CritScope cs(&critical_section_);
+  if (stream_.get()) {
+    stream_->Flush();
+  }
+}
+
+///////////////////////////////////////////////////////////////////////////
+// Implementation of MediaRecorder.
+///////////////////////////////////////////////////////////////////////////
+MediaRecorder::MediaRecorder() {}
+
+MediaRecorder::~MediaRecorder() {
+  talk_base::CritScope cs(&critical_section_);
+  std::map<BaseChannel*, SinkPair*>::iterator itr;
+  for (itr = sinks_.begin(); itr != sinks_.end(); ++itr) {
+    delete itr->second;
+  }
+}
+
+bool MediaRecorder::AddChannel(VoiceChannel* channel,
+                               const std::string& send_filename,
+                               const std::string& recv_filename,
+                               int filter) {
+  return InternalAddChannel(channel, false, send_filename, recv_filename,
+                            filter);
+}
+bool MediaRecorder::AddChannel(VideoChannel* channel,
+                               const std::string& send_filename,
+                               const std::string& recv_filename,
+                               int filter) {
+  return InternalAddChannel(channel, true, send_filename, recv_filename,
+                            filter);
+}
+
+bool MediaRecorder::InternalAddChannel(BaseChannel* channel,
+                                       bool video_channel,
+                                       const std::string& send_filename,
+                                       const std::string& recv_filename,
+                                       int filter) {
+  if (!channel) {
+    return false;
+  }
+
+  talk_base::CritScope cs(&critical_section_);
+  if (sinks_.end() != sinks_.find(channel)) {
+    return false;  // The channel was added already.
+  }
+
+  SinkPair* sink_pair = new SinkPair;
+  sink_pair->video_channel = video_channel;
+  sink_pair->filter = filter;
+  sink_pair->send_sink.reset(new RtpDumpSink(send_filename));
+  sink_pair->send_sink->set_packet_filter(filter);
+  sink_pair->recv_sink.reset(new RtpDumpSink(recv_filename));
+  sink_pair->recv_sink->set_packet_filter(filter);
+  sinks_[channel] = sink_pair;
+
+  return true;
+}
+
+void MediaRecorder::RemoveChannel(BaseChannel* channel) {
+  talk_base::CritScope cs(&critical_section_);
+  std::map<BaseChannel*, SinkPair*>::iterator itr = sinks_.find(channel);
+  if (sinks_.end() != itr) {
+    channel->UnregisterSendSink(itr->second->send_sink.get());
+    channel->UnregisterRecvSink(itr->second->recv_sink.get());
+    delete itr->second;
+    sinks_.erase(itr);
+  }
+}
+
+bool MediaRecorder::EnableChannel(
+    BaseChannel* channel, bool enable_send, bool enable_recv) {
+  talk_base::CritScope cs(&critical_section_);
+  std::map<BaseChannel*, SinkPair*>::iterator itr = sinks_.find(channel);
+  if (sinks_.end() == itr) {
+    return false;
+  }
+
+  SinkPair* sink_pair = itr->second;
+  RtpDumpSink* sink = sink_pair->send_sink.get();
+  sink->Enable(enable_send);
+  if (enable_send) {
+    channel->RegisterSendSink(sink, &RtpDumpSink::OnPacket);
+  } else {
+    channel->UnregisterSendSink(sink);
+  }
+
+  sink = sink_pair->recv_sink.get();
+  sink->Enable(enable_recv);
+  if (enable_recv) {
+    channel->RegisterRecvSink(sink, &RtpDumpSink::OnPacket);
+  } else {
+    channel->UnregisterRecvSink(sink);
+  }
+
+  if (sink_pair->video_channel &&
+      (sink_pair->filter & PF_RTPPACKET) == PF_RTPPACKET) {
+    // Request a full intra frame.
+    VideoChannel* video_channel = static_cast<VideoChannel*>(channel);
+    if (enable_send) {
+      video_channel->SendIntraFrame();
+    }
+    if (enable_recv) {
+      video_channel->RequestIntraFrame();
+    }
+  }
+
+  return true;
+}
+
+void MediaRecorder::FlushSinks() {
+  talk_base::CritScope cs(&critical_section_);
+  std::map<BaseChannel*, SinkPair*>::iterator itr;
+  for (itr = sinks_.begin(); itr != sinks_.end(); ++itr) {
+    itr->second->send_sink->Flush();
+    itr->second->recv_sink->Flush();
+  }
+}
+
+}  // namespace cricket
diff --git a/talk/session/phone/mediarecorder.h b/talk/session/phone/mediarecorder.h
new file mode 100644
index 0000000..fa77e6d
--- /dev/null
+++ b/talk/session/phone/mediarecorder.h
@@ -0,0 +1,117 @@
+/*
+ * libjingle
+ * Copyright 2010, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef TALK_SESSION_PHONE_MEDIARECORDER_H_
+#define TALK_SESSION_PHONE_MEDIARECORDER_H_
+
+#include <map>
+#include <string>
+
+#include "talk/base/criticalsection.h"
+#include "talk/base/scoped_ptr.h"
+#include "talk/base/sigslot.h"
+#include "talk/session/phone/mediasink.h"
+
+namespace talk_base {
+class Pathname;
+class FileStream;
+}
+
+namespace cricket {
+
+class BaseChannel;
+class VideoChannel;
+class VoiceChannel;
+class RtpDumpWriter;
+
+// RtpDumpSink implements MediaSinkInterface by dumping the RTP/RTCP packets to
+// a file.
+class RtpDumpSink : public MediaSinkInterface, public sigslot::has_slots<> {
+ public:
+  explicit RtpDumpSink(const std::string& filename);
+  virtual ~RtpDumpSink();
+
+  virtual void SetMaxSize(size_t size);
+  virtual bool Enable(bool enable);
+  virtual bool IsEnabled() const { return recording_; }
+  virtual void OnPacket(const void* data, size_t size, bool rtcp);
+  virtual void set_packet_filter(int filter);
+  int packet_filter() const { return packet_filter_; }
+  void Flush();
+
+ private:
+  size_t max_size_;
+  bool recording_;
+  int packet_filter_;
+  std::string filename_;
+  talk_base::scoped_ptr<talk_base::FileStream> stream_;
+  talk_base::scoped_ptr<RtpDumpWriter> writer_;
+  talk_base::CriticalSection critical_section_;
+
+  DISALLOW_COPY_AND_ASSIGN(RtpDumpSink);
+};
+
+class MediaRecorder {
+ public:
+  MediaRecorder();
+  virtual ~MediaRecorder();
+
+  bool AddChannel(VoiceChannel* channel,
+                  const std::string& send_filename,
+                  const std::string& recv_filename,
+                  int filter);
+  bool AddChannel(VideoChannel* channel,
+                  const std::string& send_filename,
+                  const std::string& recv_filename,
+                  int filter);
+  void RemoveChannel(BaseChannel* channel);
+  bool EnableChannel(BaseChannel* channel, bool enable_send, bool enable_recv);
+  void FlushSinks();
+
+ private:
+  struct SinkPair {
+    bool video_channel;
+    int filter;
+    talk_base::scoped_ptr<RtpDumpSink> send_sink;
+    talk_base::scoped_ptr<RtpDumpSink> recv_sink;
+  };
+
+  bool InternalAddChannel(BaseChannel* channel,
+                          bool video_channel,
+                          const std::string& send_filename,
+                          const std::string& recv_filename,
+                          int filter);
+
+  std::map<BaseChannel*, SinkPair*> sinks_;
+  talk_base::CriticalSection critical_section_;
+
+  DISALLOW_COPY_AND_ASSIGN(MediaRecorder);
+};
+
+}  // namespace cricket
+
+#endif  // TALK_SESSION_PHONE_MEDIARECORDER_H_
diff --git a/talk/session/phone/mediarecorder_unittest.cc b/talk/session/phone/mediarecorder_unittest.cc
new file mode 100644
index 0000000..59d1afe
--- /dev/null
+++ b/talk/session/phone/mediarecorder_unittest.cc
@@ -0,0 +1,344 @@
+// libjingle
+// Copyright 2010 Google Inc.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//  1. Redistributions of source code must retain the above copyright notice,
+//     this list of conditions and the following disclaimer.
+//  2. Redistributions in binary form must reproduce the above copyright notice,
+//     this list of conditions and the following disclaimer in the documentation
+//     and/or other materials provided with the distribution.
+//  3. The name of the author may not be used to endorse or promote products
+//     derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <string>
+
+#include "talk/base/bytebuffer.h"
+#include "talk/base/fileutils.h"
+#include "talk/base/gunit.h"
+#include "talk/base/pathutils.h"
+#include "talk/base/thread.h"
+#include "talk/p2p/base/fakesession.h"
+#include "talk/session/phone/channel.h"
+#include "talk/session/phone/fakemediaengine.h"
+#include "talk/session/phone/mediarecorder.h"
+#include "talk/session/phone/rtpdump.h"
+#include "talk/session/phone/testutils.h"
+
+namespace cricket {
+
+/////////////////////////////////////////////////////////////////////////
+// Test RtpDumpSink
+/////////////////////////////////////////////////////////////////////////
+class RtpDumpSinkTest : public testing::Test {
+ public:
+  virtual void SetUp() {
+    EXPECT_TRUE(talk_base::Filesystem::GetTemporaryFolder(path_, true, NULL));
+    path_.SetFilename("sink-test.rtpdump");
+    sink_.reset(new RtpDumpSink(path_.pathname()));
+
+    for (int i = 0; i < ARRAY_SIZE(rtp_buf_); ++i) {
+      RtpTestUtility::kTestRawRtpPackets[i].WriteToByteBuffer(
+          RtpTestUtility::kDefaultSsrc, &rtp_buf_[i]);
+    }
+  }
+
+  virtual void TearDown() {
+    stream_.reset();
+    EXPECT_TRUE(talk_base::Filesystem::DeleteFile(path_));
+  }
+
+ protected:
+  void OnRtpPacket(const RawRtpPacket& raw) {
+    talk_base::ByteBuffer buf;
+    raw.WriteToByteBuffer(RtpTestUtility::kDefaultSsrc, &buf);
+    sink_->OnPacket(buf.Data(), buf.Length(), false);
+  }
+
+  talk_base::StreamResult ReadPacket(RtpDumpPacket* packet) {
+    if (!stream_.get()) {
+      sink_.reset();  // This will close the file. So we can read it.
+      stream_.reset(talk_base::Filesystem::OpenFile(path_, "rb"));
+      reader_.reset(new RtpDumpReader(stream_.get()));
+    }
+    return reader_->ReadPacket(packet);
+  }
+
+  talk_base::Pathname path_;
+  talk_base::scoped_ptr<RtpDumpSink> sink_;
+  talk_base::ByteBuffer rtp_buf_[3];
+  talk_base::scoped_ptr<talk_base::StreamInterface> stream_;
+  talk_base::scoped_ptr<RtpDumpReader> reader_;
+};
+
+TEST_F(RtpDumpSinkTest, TestRtpDumpSink) {
+  // By default, the sink is disabled. The 1st packet is not written.
+  EXPECT_FALSE(sink_->IsEnabled());
+  sink_->set_packet_filter(PF_ALL);
+  OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[0]);
+
+  // Enable the sink. The 2nd packet is written.
+  EXPECT_TRUE(sink_->Enable(true));
+  EXPECT_TRUE(sink_->IsEnabled());
+  EXPECT_TRUE(talk_base::Filesystem::IsFile(path_.pathname()));
+  OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[1]);
+
+  // Disable the sink. The 3rd packet is not written.
+  EXPECT_TRUE(sink_->Enable(false));
+  EXPECT_FALSE(sink_->IsEnabled());
+  OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[2]);
+
+  // Read the recorded file and verify it contains only the 2nd packet.
+  RtpDumpPacket packet;
+  EXPECT_EQ(talk_base::SR_SUCCESS, ReadPacket(&packet));
+  EXPECT_TRUE(RtpTestUtility::VerifyPacket(
+      &packet, &RtpTestUtility::kTestRawRtpPackets[1], false));
+  EXPECT_EQ(talk_base::SR_EOS, ReadPacket(&packet));
+}
+
+TEST_F(RtpDumpSinkTest, TestRtpDumpSinkMaxSize) {
+  EXPECT_TRUE(sink_->Enable(true));
+  sink_->set_packet_filter(PF_ALL);
+  sink_->SetMaxSize(strlen(RtpDumpFileHeader::kFirstLine) +
+                    RtpDumpFileHeader::kHeaderLength +
+                    RtpDumpPacket::kHeaderLength +
+                    RtpTestUtility::kTestRawRtpPackets[0].size());
+  OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[0]);
+
+  // Exceed the limit size: the 2nd and 3rd packets are not written.
+  OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[1]);
+  OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[2]);
+
+  // Read the recorded file and verify that it contains only the first packet.
+  RtpDumpPacket packet;
+  EXPECT_EQ(talk_base::SR_SUCCESS, ReadPacket(&packet));
+  EXPECT_TRUE(RtpTestUtility::VerifyPacket(
+      &packet, &RtpTestUtility::kTestRawRtpPackets[0], false));
+  EXPECT_EQ(talk_base::SR_EOS, ReadPacket(&packet));
+}
+
+TEST_F(RtpDumpSinkTest, TestRtpDumpSinkFilter) {
+  // The default filter is PF_NONE.
+  EXPECT_EQ(PF_NONE, sink_->packet_filter());
+
+  // Set to PF_RTPHEADER before enable.
+  sink_->set_packet_filter(PF_RTPHEADER);
+  EXPECT_EQ(PF_RTPHEADER, sink_->packet_filter());
+  EXPECT_TRUE(sink_->Enable(true));
+  // We dump only the header of the first packet.
+  OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[0]);
+
+  // Set the filter to PF_RTPPACKET. We dump all the second packet.
+  sink_->set_packet_filter(PF_RTPPACKET);
+  EXPECT_EQ(PF_RTPPACKET, sink_->packet_filter());
+  OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[1]);
+
+  // Set the filter to PF_NONE. We do not dump the third packet.
+  sink_->set_packet_filter(PF_NONE);
+  EXPECT_EQ(PF_NONE, sink_->packet_filter());
+  OnRtpPacket(RtpTestUtility::kTestRawRtpPackets[2]);
+
+  // Read the recorded file and verify the header of the first packet and
+  // the whole packet for the second packet.
+  RtpDumpPacket packet;
+  EXPECT_EQ(talk_base::SR_SUCCESS, ReadPacket(&packet));
+  EXPECT_TRUE(RtpTestUtility::VerifyPacket(
+      &packet, &RtpTestUtility::kTestRawRtpPackets[0], true));
+  EXPECT_EQ(talk_base::SR_SUCCESS, ReadPacket(&packet));
+  EXPECT_TRUE(RtpTestUtility::VerifyPacket(
+      &packet, &RtpTestUtility::kTestRawRtpPackets[1], false));
+  EXPECT_EQ(talk_base::SR_EOS, ReadPacket(&packet));
+}
+
+/////////////////////////////////////////////////////////////////////////
+// Test MediaRecorder
+/////////////////////////////////////////////////////////////////////////
+void TestMediaRecorder(BaseChannel* channel,
+                       FakeVideoMediaChannel* video_media_channel,
+                       int filter) {
+  // Create media recorder.
+  talk_base::scoped_ptr<MediaRecorder> recorder(new MediaRecorder);
+  // Fail to EnableChannel before AddChannel.
+  EXPECT_FALSE(recorder->EnableChannel(channel, true, true));
+  EXPECT_FALSE(channel->HasSendSinks());
+  EXPECT_FALSE(channel->HasRecvSinks());
+
+  // Add the channel to the recorder.
+  talk_base::Pathname path;
+  EXPECT_TRUE(talk_base::Filesystem::GetTemporaryFolder(path, true, NULL));
+  path.SetFilename("send.rtpdump");
+  std::string send_file = path.pathname();
+  path.SetFilename("recv.rtpdump");
+  std::string recv_file = path.pathname();
+  if (video_media_channel) {
+    EXPECT_TRUE(recorder->AddChannel(static_cast<VideoChannel*>(channel),
+                                     send_file, recv_file, filter));
+  } else {
+    EXPECT_TRUE(recorder->AddChannel(static_cast<VoiceChannel*>(channel),
+                                     send_file, recv_file, filter));
+  }
+
+  // Enable recording only the sent media.
+  EXPECT_TRUE(recorder->EnableChannel(channel, true, false));
+  EXPECT_TRUE(channel->HasSendSinks());
+  EXPECT_FALSE(channel->HasRecvSinks());
+  if (video_media_channel) {
+    EXPECT_TRUE_WAIT(video_media_channel->sent_intra_frame(), 100);
+  }
+
+  // Enable recording only the received meida.
+  EXPECT_TRUE(recorder->EnableChannel(channel, false, true));
+  EXPECT_FALSE(channel->HasSendSinks());
+  EXPECT_TRUE(channel->HasRecvSinks());
+  if (video_media_channel) {
+    EXPECT_TRUE(video_media_channel->requested_intra_frame());
+  }
+
+  // Enable recording both the sent and the received video.
+  EXPECT_TRUE(recorder->EnableChannel(channel, true, true));
+  EXPECT_TRUE(channel->HasSendSinks());
+  EXPECT_TRUE(channel->HasRecvSinks());
+
+  // Enable recording only headers.
+  if (video_media_channel) {
+    video_media_channel->set_sent_intra_frame(false);
+    video_media_channel->set_requested_intra_frame(false);
+  }
+  EXPECT_TRUE(recorder->EnableChannel(channel, true, true));
+  EXPECT_TRUE(channel->HasSendSinks());
+  EXPECT_TRUE(channel->HasRecvSinks());
+  if (video_media_channel) {
+    if ((filter & PF_RTPPACKET) == PF_RTPPACKET) {
+      // If record the whole RTP packet, trigers FIR.
+      EXPECT_TRUE(video_media_channel->requested_intra_frame());
+      EXPECT_TRUE(video_media_channel->sent_intra_frame());
+    } else {
+      // If record only the RTP header, does not triger FIR.
+      EXPECT_FALSE(video_media_channel->requested_intra_frame());
+      EXPECT_FALSE(video_media_channel->sent_intra_frame());
+    }
+  }
+
+  // Remove the voice channel from the recorder.
+  recorder->RemoveChannel(channel);
+  EXPECT_FALSE(channel->HasSendSinks());
+  EXPECT_FALSE(channel->HasRecvSinks());
+
+  // Delete all files.
+  recorder.reset();
+  EXPECT_TRUE(talk_base::Filesystem::DeleteFile(send_file));
+  EXPECT_TRUE(talk_base::Filesystem::DeleteFile(recv_file));
+}
+
+// Fisrt start recording header and then start recording media. Verify that
+// differnt files are created for header and media.
+void TestRecordHeaderAndMedia(BaseChannel* channel,
+                              FakeVideoMediaChannel* video_media_channel) {
+  // Create RTP header recorder.
+  talk_base::scoped_ptr<MediaRecorder> header_recorder(new MediaRecorder);
+
+  talk_base::Pathname path;
+  EXPECT_TRUE(talk_base::Filesystem::GetTemporaryFolder(path, true, NULL));
+  path.SetFilename("send-header.rtpdump");
+  std::string send_header_file = path.pathname();
+  path.SetFilename("recv-header.rtpdump");
+  std::string recv_header_file = path.pathname();
+  if (video_media_channel) {
+    EXPECT_TRUE(header_recorder->AddChannel(
+        static_cast<VideoChannel*>(channel),
+        send_header_file, recv_header_file, PF_RTPHEADER));
+  } else {
+    EXPECT_TRUE(header_recorder->AddChannel(
+        static_cast<VoiceChannel*>(channel),
+        send_header_file, recv_header_file, PF_RTPHEADER));
+  }
+
+  // Enable recording both sent and received.
+  EXPECT_TRUE(header_recorder->EnableChannel(channel, true, true));
+  EXPECT_TRUE(channel->HasSendSinks());
+  EXPECT_TRUE(channel->HasRecvSinks());
+  if (video_media_channel) {
+    EXPECT_FALSE(video_media_channel->sent_intra_frame());
+    EXPECT_FALSE(video_media_channel->requested_intra_frame());
+  }
+
+  // Verify that header files are created.
+  EXPECT_TRUE(talk_base::Filesystem::IsFile(send_header_file));
+  EXPECT_TRUE(talk_base::Filesystem::IsFile(recv_header_file));
+
+  // Create RTP header recorder.
+  talk_base::scoped_ptr<MediaRecorder> recorder(new MediaRecorder);
+  path.SetFilename("send.rtpdump");
+  std::string send_file = path.pathname();
+  path.SetFilename("recv.rtpdump");
+  std::string recv_file = path.pathname();
+  if (video_media_channel) {
+    EXPECT_TRUE(recorder->AddChannel(
+        static_cast<VideoChannel*>(channel),
+        send_file, recv_file, PF_RTPPACKET));
+  } else {
+    EXPECT_TRUE(recorder->AddChannel(
+        static_cast<VoiceChannel*>(channel),
+        send_file, recv_file, PF_RTPPACKET));
+  }
+
+  // Enable recording both sent and received.
+  EXPECT_TRUE(recorder->EnableChannel(channel, true, true));
+  EXPECT_TRUE(channel->HasSendSinks());
+  EXPECT_TRUE(channel->HasRecvSinks());
+  if (video_media_channel) {
+    EXPECT_TRUE_WAIT(video_media_channel->sent_intra_frame(), 100);
+    EXPECT_TRUE(video_media_channel->requested_intra_frame());
+  }
+
+  // Verify that media files are created.
+  EXPECT_TRUE(talk_base::Filesystem::IsFile(send_file));
+  EXPECT_TRUE(talk_base::Filesystem::IsFile(recv_file));
+
+  // Delete all files.
+  header_recorder.reset();
+  recorder.reset();
+  EXPECT_TRUE(talk_base::Filesystem::DeleteFile(send_header_file));
+  EXPECT_TRUE(talk_base::Filesystem::DeleteFile(recv_header_file));
+  EXPECT_TRUE(talk_base::Filesystem::DeleteFile(send_file));
+  EXPECT_TRUE(talk_base::Filesystem::DeleteFile(recv_file));
+}
+
+TEST(MediaRecorderTest, TestMediaRecorderVoiceChannel) {
+  // Create the voice channel.
+  cricket::FakeSession session;
+  cricket::FakeMediaEngine media_engine;
+  VoiceChannel channel(talk_base::Thread::Current(), &media_engine,
+                       new FakeVoiceMediaChannel(NULL), &session, "", false);
+  EXPECT_TRUE(channel.Init());
+  TestMediaRecorder(&channel, NULL, PF_RTPPACKET);
+  TestMediaRecorder(&channel, NULL, PF_RTPHEADER);
+  TestRecordHeaderAndMedia(&channel, NULL);
+}
+
+TEST(MediaRecorderTest, TestMediaRecorderVideoChannel) {
+  // Create the video channel.
+  cricket::FakeSession session;
+  cricket::FakeMediaEngine media_engine;
+  FakeVideoMediaChannel* media_channel = new FakeVideoMediaChannel(NULL);
+  VideoChannel channel(talk_base::Thread::Current(), &media_engine,
+                       media_channel, &session, "", false, NULL);
+  EXPECT_TRUE(channel.Init());
+  TestMediaRecorder(&channel, media_channel, PF_RTPPACKET);
+  TestMediaRecorder(&channel, media_channel, PF_RTPHEADER);
+  TestRecordHeaderAndMedia(&channel, media_channel);
+}
+
+}  // namespace cricket
diff --git a/talk/session/phone/mediasession.cc b/talk/session/phone/mediasession.cc
index 05875b5..4e66872 100644
--- a/talk/session/phone/mediasession.cc
+++ b/talk/session/phone/mediasession.cc
@@ -286,7 +286,6 @@
 }
 
 SessionDescription* MediaSessionDescriptionFactory::CreateOffer(
-
     const MediaSessionOptions& options,
     const SessionDescription* current_description) {
   scoped_ptr<SessionDescription> offer(new SessionDescription());
@@ -310,7 +309,7 @@
       // TODO: Remove this legacy stream when all apps use StreamParams.
       audio->AddLegacyStream(talk_base::CreateRandomNonZeroId());
     }
-    audio->set_rtcp_mux(true);
+    audio->set_rtcp_mux(options.rtcp_mux_enabled);
     audio->set_lang(lang_);
 
     if (secure() != SEC_DISABLED) {
@@ -363,7 +362,7 @@
       video->AddLegacyStream(talk_base::CreateRandomNonZeroId());
     }
     video->set_bandwidth(options.video_bandwidth);
-    video->set_rtcp_mux(true);
+    video->set_rtcp_mux(options.rtcp_mux_enabled);
 
     if (secure() != SEC_DISABLED) {
       CryptoParamsVec video_cryptos;
@@ -436,7 +435,8 @@
       // TODO: Remove this legacy stream when all apps use StreamParams.
       audio_accept->AddLegacyStream(talk_base::CreateRandomNonZeroId());
     }
-    audio_accept->set_rtcp_mux(audio_offer->rtcp_mux());
+    audio_accept->set_rtcp_mux(
+        options.rtcp_mux_enabled && audio_offer->rtcp_mux());
 
     if (secure() != SEC_DISABLED) {
       CryptoParams crypto;
@@ -501,7 +501,8 @@
       video_accept->AddLegacyStream(talk_base::CreateRandomNonZeroId());
     }
     video_accept->set_bandwidth(options.video_bandwidth);
-    video_accept->set_rtcp_mux(video_offer->rtcp_mux());
+    video_accept->set_rtcp_mux(
+        options.rtcp_mux_enabled && video_offer->rtcp_mux());
     video_accept->SortCodecs();
 
     if (secure() != SEC_DISABLED) {
diff --git a/talk/session/phone/mediasession.h b/talk/session/phone/mediasession.h
index 127a7ed..49df26a 100644
--- a/talk/session/phone/mediasession.h
+++ b/talk/session/phone/mediasession.h
@@ -78,6 +78,7 @@
       has_audio(true),  // Audio enabled by default.
       has_video(false),
       is_muc(false),
+      rtcp_mux_enabled(true),
       video_bandwidth(kAutoBandwidth) {
   }
 
@@ -92,6 +93,7 @@
   bool has_audio;
   bool has_video;
   bool is_muc;
+  bool rtcp_mux_enabled;
   // bps. -1 == auto.
   int video_bandwidth;
 
diff --git a/talk/session/phone/mediasession_unittest.cc b/talk/session/phone/mediasession_unittest.cc
index 36fedc6..d2c1695 100644
--- a/talk/session/phone/mediasession_unittest.cc
+++ b/talk/session/phone/mediasession_unittest.cc
@@ -56,6 +56,7 @@
 using cricket::ContentInfo;
 using cricket::CryptoParamsVec;
 using cricket::AudioContentDescription;
+using cricket::MediaContentDescription;
 using cricket::VideoContentDescription;
 using cricket::kAutoBandwidth;
 using cricket::AudioCodec;
@@ -162,6 +163,29 @@
   }
 
  protected:
+  const MediaContentDescription*  GetMediaDescription(
+      const SessionDescription* sdesc,
+      const std::string& content_name) {
+    const ContentInfo* content = sdesc->GetContentByName(content_name);
+    if (content == NULL) {
+      return NULL;
+    }
+    return static_cast<const MediaContentDescription*>(
+        content->description);
+  }
+
+  const AudioContentDescription*  GetAudioDescription(
+      const SessionDescription* sdesc) {
+    return static_cast<const AudioContentDescription*>(
+        GetMediaDescription(sdesc, "audio"));
+  }
+
+  const VideoContentDescription*  GetVideoDescription(
+      const SessionDescription* sdesc) {
+    return static_cast<const VideoContentDescription*>(
+        GetMediaDescription(sdesc, "video"));
+  }
+
   MediaSessionDescriptionFactory f1_;
   MediaSessionDescriptionFactory f2_;
 };
@@ -276,6 +300,74 @@
   ASSERT_CRYPTO(vcd, false, 1U, CS_AES_CM_128_HMAC_SHA1_80);
 }
 
+
+// Create a typical video answer, and ensure it matches what we expect.
+TEST_F(MediaSessionDescriptionFactoryTest, TestCreateVideoAnswerRtcpMux) {
+  MediaSessionOptions offer_opts;
+  MediaSessionOptions answer_opts;
+  answer_opts.has_video = true;
+  offer_opts.has_video = true;
+
+  talk_base::scoped_ptr<SessionDescription> offer(NULL);
+  talk_base::scoped_ptr<SessionDescription> answer(NULL);
+
+  offer_opts.rtcp_mux_enabled = true;
+  answer_opts.rtcp_mux_enabled = true;
+
+  offer.reset(f1_.CreateOffer(offer_opts, NULL));
+  answer.reset(f2_.CreateAnswer(offer.get(), answer_opts, NULL));
+  ASSERT_TRUE(NULL != GetAudioDescription(offer.get()));
+  ASSERT_TRUE(NULL != GetVideoDescription(offer.get()));
+  ASSERT_TRUE(NULL != GetAudioDescription(answer.get()));
+  ASSERT_TRUE(NULL != GetVideoDescription(answer.get()));
+  EXPECT_TRUE(GetAudioDescription(offer.get())->rtcp_mux());
+  EXPECT_TRUE(GetVideoDescription(offer.get())->rtcp_mux());
+  EXPECT_TRUE(GetAudioDescription(answer.get())->rtcp_mux());
+  EXPECT_TRUE(GetVideoDescription(answer.get())->rtcp_mux());
+
+  offer_opts.rtcp_mux_enabled = true;
+  answer_opts.rtcp_mux_enabled = false;
+
+  offer.reset(f1_.CreateOffer(offer_opts, NULL));
+  answer.reset(f2_.CreateAnswer(offer.get(), answer_opts, NULL));
+  ASSERT_TRUE(NULL != GetAudioDescription(offer.get()));
+  ASSERT_TRUE(NULL != GetVideoDescription(offer.get()));
+  ASSERT_TRUE(NULL != GetAudioDescription(answer.get()));
+  ASSERT_TRUE(NULL != GetVideoDescription(answer.get()));
+  EXPECT_TRUE(GetAudioDescription(offer.get())->rtcp_mux());
+  EXPECT_TRUE(GetVideoDescription(offer.get())->rtcp_mux());
+  EXPECT_FALSE(GetAudioDescription(answer.get())->rtcp_mux());
+  EXPECT_FALSE(GetVideoDescription(answer.get())->rtcp_mux());
+
+  offer_opts.rtcp_mux_enabled = false;
+  answer_opts.rtcp_mux_enabled = true;
+
+  offer.reset(f1_.CreateOffer(offer_opts, NULL));
+  answer.reset(f2_.CreateAnswer(offer.get(), answer_opts, NULL));
+  ASSERT_TRUE(NULL != GetAudioDescription(offer.get()));
+  ASSERT_TRUE(NULL != GetVideoDescription(offer.get()));
+  ASSERT_TRUE(NULL != GetAudioDescription(answer.get()));
+  ASSERT_TRUE(NULL != GetVideoDescription(answer.get()));
+  EXPECT_FALSE(GetAudioDescription(offer.get())->rtcp_mux());
+  EXPECT_FALSE(GetVideoDescription(offer.get())->rtcp_mux());
+  EXPECT_FALSE(GetAudioDescription(answer.get())->rtcp_mux());
+  EXPECT_FALSE(GetVideoDescription(answer.get())->rtcp_mux());
+
+  offer_opts.rtcp_mux_enabled = false;
+  answer_opts.rtcp_mux_enabled = false;
+
+  offer.reset(f1_.CreateOffer(offer_opts, NULL));
+  answer.reset(f2_.CreateAnswer(offer.get(), answer_opts, NULL));
+  ASSERT_TRUE(NULL != GetAudioDescription(offer.get()));
+  ASSERT_TRUE(NULL != GetVideoDescription(offer.get()));
+  ASSERT_TRUE(NULL != GetAudioDescription(answer.get()));
+  ASSERT_TRUE(NULL != GetVideoDescription(answer.get()));
+  EXPECT_FALSE(GetAudioDescription(offer.get())->rtcp_mux());
+  EXPECT_FALSE(GetVideoDescription(offer.get())->rtcp_mux());
+  EXPECT_FALSE(GetAudioDescription(answer.get())->rtcp_mux());
+  EXPECT_FALSE(GetVideoDescription(answer.get())->rtcp_mux());
+}
+
 // Create an audio-only answer to a video offer.
 TEST_F(MediaSessionDescriptionFactoryTest, TestCreateAudioAnswerToVideo) {
   MediaSessionOptions opts;
diff --git a/talk/session/phone/mediasessionclient.cc b/talk/session/phone/mediasessionclient.cc
index c7d8c88..31bd1f7 100644
--- a/talk/session/phone/mediasessionclient.cc
+++ b/talk/session/phone/mediasessionclient.cc
@@ -500,6 +500,9 @@
   if (!ParseJingleEncryption(content_elem, audio, error)) {
     return false;
   }
+
+  audio->set_rtcp_mux(content_elem->FirstNamed(QN_JINGLE_RTCP_MUX) != NULL);
+
   *content = audio;
   return true;
 }
@@ -527,6 +530,9 @@
   if (!ParseJingleEncryption(content_elem, video, error)) {
     return false;
   }
+
+  video->set_rtcp_mux(content_elem->FirstNamed(QN_JINGLE_RTCP_MUX) != NULL);
+
   *content = video;
   return true;
 }
diff --git a/talk/session/phone/mediasessionclient_unittest.cc b/talk/session/phone/mediasessionclient_unittest.cc
new file mode 100644
index 0000000..574b052
--- /dev/null
+++ b/talk/session/phone/mediasessionclient_unittest.cc
@@ -0,0 +1,2696 @@
+/*
+ * libjingle
+ * Copyright 2004--2011, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <string>
+#include <vector>
+
+#include "talk/base/gunit.h"
+#include "talk/base/logging.h"
+#include "talk/base/scoped_ptr.h"
+#include "talk/p2p/base/constants.h"
+#include "talk/p2p/client/basicportallocator.h"
+#include "talk/session/phone/fakedevicemanager.h"
+#include "talk/session/phone/fakemediaengine.h"
+#include "talk/session/phone/mediasessionclient.h"
+#include "talk/xmllite/xmlelement.h"
+#include "talk/xmllite/xmlbuilder.h"
+#include "talk/xmllite/xmlprinter.h"
+#include "talk/xmpp/constants.h"
+
+// The codecs that our FakeMediaEngine will support. Order is important, since
+// the tests check that our messages have codecs in the correct order.
+static const cricket::AudioCodec kAudioCodecs[] = {
+  cricket::AudioCodec(103, "ISAC",   16000, -1,    1, 18),
+  cricket::AudioCodec(104, "ISAC",   32000, -1,    1, 17),
+  cricket::AudioCodec(119, "ISACLC", 16000, 40000, 1, 16),
+  cricket::AudioCodec(99,  "speex",  16000, 22000, 1, 15),
+  cricket::AudioCodec(97,  "IPCMWB", 16000, 80000, 1, 14),
+  cricket::AudioCodec(9,   "G722",   16000, 64000, 1, 13),
+  cricket::AudioCodec(102, "iLBC",   8000,  13300, 1, 12),
+  cricket::AudioCodec(98,  "speex",  8000,  11000, 1, 11),
+  cricket::AudioCodec(3,   "GSM",    8000,  13000, 1, 10),
+  cricket::AudioCodec(100, "EG711U", 8000,  64000, 1, 9),
+  cricket::AudioCodec(101, "EG711A", 8000,  64000, 1, 8),
+  cricket::AudioCodec(0,   "PCMU",   8000,  64000, 1, 7),
+  cricket::AudioCodec(8,   "PCMA",   8000,  64000, 1, 6),
+  cricket::AudioCodec(126, "CN",     32000, 0,     1, 5),
+  cricket::AudioCodec(105, "CN",     16000, 0,     1, 4),
+  cricket::AudioCodec(13,  "CN",     8000,  0,     1, 3),
+  cricket::AudioCodec(117, "red",    8000,  0,     1, 2),
+  cricket::AudioCodec(106, "telephone-event", 8000, 0, 1, 1)
+};
+
+static const cricket::VideoCodec kVideoCodecs[] = {
+  cricket::VideoCodec(96, "H264-SVC", 320, 200, 30, 1)
+};
+
+const std::string kGingleCryptoOffer = \
+    "<rtp:encryption xmlns:rtp='urn:xmpp:jingle:apps:rtp:1'>   "  \
+    "  <usage/>                                                "  \
+    "  <rtp:crypto tag='145' crypto-suite='AES_CM_128_HMAC_SHA1_32'" \
+    "  key-params='inline:hsWuSQJxx7przmb8HM+ZkeNcG3HezSNID7LmfDa9'/>" \
+    "  <rtp:crypto tag='51' crypto-suite='AES_CM_128_HMAC_SHA1_80'" \
+    "  key-params='inline:J4lfdUL8W1F7TNJKcbuygaQuA429SJy2e9JctPUy'/>" \
+    "</rtp:encryption>                                         ";
+
+// Jingle offer does not have any <usage> element.
+const std::string kJingleCryptoOffer = \
+    "<rtp:encryption xmlns:rtp='urn:xmpp:jingle:apps:rtp:1'>   "  \
+    "  <rtp:crypto tag='145' crypto-suite='AES_CM_128_HMAC_SHA1_32'" \
+    "  key-params='inline:hsWuSQJxx7przmb8HM+ZkeNcG3HezSNID7LmfDa9'/>" \
+    "  <rtp:crypto tag='51' crypto-suite='AES_CM_128_HMAC_SHA1_80'" \
+    "  key-params='inline:J4lfdUL8W1F7TNJKcbuygaQuA429SJy2e9JctPUy'/>" \
+    "</rtp:encryption>                                         ";
+
+
+const std::string kGingleRequiredCryptoOffer = \
+    "<rtp:encryption xmlns:rtp='urn:xmpp:jingle:apps:rtp:1' required='true'> "\
+    "  <usage/>                                                "  \
+    "  <rtp:crypto tag='145' crypto-suite='AES_CM_128_HMAC_SHA1_32'" \
+    "  key-params='inline:hsWuSQJxx7przmb8HM+ZkeNcG3HezSNID7LmfDa9'/>" \
+    "  <rtp:crypto tag='51' crypto-suite='AES_CM_128_HMAC_SHA1_80'" \
+    "  key-params='inline:J4lfdUL8W1F7TNJKcbuygaQuA429SJy2e9JctPUy'/>" \
+    "</rtp:encryption>                                         ";
+
+const std::string kJingleRequiredCryptoOffer = \
+    "<rtp:encryption xmlns:rtp='urn:xmpp:jingle:apps:rtp:1' required='true'> "\
+    "  <rtp:crypto tag='145' crypto-suite='AES_CM_128_HMAC_SHA1_32'" \
+    "  key-params='inline:hsWuSQJxx7przmb8HM+ZkeNcG3HezSNID7LmfDa9'/>" \
+    "  <rtp:crypto tag='51' crypto-suite='AES_CM_128_HMAC_SHA1_80'" \
+    "  key-params='inline:J4lfdUL8W1F7TNJKcbuygaQuA429SJy2e9JctPUy'/>" \
+    "</rtp:encryption>                                         ";
+
+
+const std::string kGingleUnsupportedCryptoOffer = \
+    "<rtp:encryption xmlns:rtp='urn:xmpp:jingle:apps:rtp:1'>   "  \
+    "  <usage/>                                                "  \
+    "  <rtp:crypto tag='145' crypto-suite='NOT_SUPPORTED_1'" \
+    "  key-params='inline:hsWuSQJxx7przmb8HM+ZkeNcG3HezSNID7LmfDa9'/>" \
+    "  <rtp:crypto tag='51' crypto-suite='NOT_SUPPORTED_2'" \
+    "  key-params='inline:J4lfdUL8W1F7TNJKcbuygaQuA429SJy2e9JctPUy'/>" \
+    "</rtp:encryption>                                         ";
+
+const std::string kJingleUnsupportedCryptoOffer =                 \
+    "<rtp:encryption xmlns:rtp='urn:xmpp:jingle:apps:rtp:1'>   "  \
+    "  <rtp:crypto tag='145' crypto-suite='NOT_SUPPORTED_1'" \
+    "  key-params='inline:hsWuSQJxx7przmb8HM+ZkeNcG3HezSNID7LmfDa9'/>" \
+    "  <rtp:crypto tag='51' crypto-suite='NOT_SUPPORTED_2'" \
+    "  key-params='inline:J4lfdUL8W1F7TNJKcbuygaQuA429SJy2e9JctPUy'/>" \
+    "</rtp:encryption>                                         ";
+
+
+// With unsupported but with required="true"
+const std::string kGingleRequiredUnsupportedCryptoOffer =         \
+    "<rtp:encryption xmlns:rtp='urn:xmpp:jingle:apps:rtp:1' required='true'>" \
+    "  <usage/>                                                "  \
+    "  <rtp:crypto tag='145' crypto-suite='NOT_SUPPORTED_1'"      \
+    "  key-params='inline:hsWuSQJxx7przmb8HM+ZkeNcG3HezSNID7LmfDa9'/>" \
+    "  <rtp:crypto tag='51' crypto-suite='NOT_SUPPORTED_2'" \
+    "  key-params='inline:J4lfdUL8W1F7TNJKcbuygaQuA429SJy2e9JctPUy'/>" \
+    "</rtp:encryption>                                         ";
+
+const std::string kJingleRequiredUnsupportedCryptoOffer =                     \
+    "<rtp:encryption xmlns:rtp='urn:xmpp:jingle:apps:rtp:1' required='true'>" \
+    "  <rtp:crypto tag='145' crypto-suite='NOT_SUPPORTED_1'                 " \
+    "  key-params='inline:hsWuSQJxx7przmb8HM+ZkeNcG3HezSNID7LmfDa9'/>       " \
+    "  <rtp:crypto tag='51' crypto-suite='NOT_SUPPORTED_2'                  " \
+    "  key-params='inline:J4lfdUL8W1F7TNJKcbuygaQuA429SJy2e9JctPUy'/>"        \
+    "</rtp:encryption>                                         ";
+
+const std::string kGingleInitiate(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <session xmlns='http://www.google.com/session' type='initiate'" \
+     "    id='abcdef' initiator='me@domain.com/resource'>             " \
+     "    <description xmlns='http://www.google.com/session/phone'>   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='103' name='ISAC' clockrate='16000' />               " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='104' name='ISAC' clockrate='32000' />               " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='119' name='ISACLC' clockrate='16000' bitrate='40000' />  " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='99' name='speex' clockrate='16000' bitrate='22000' />    " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='97' name='IPCMWB' clockrate='16000' bitrate='80000' />   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='9' name='G722' clockrate='16000' bitrate='64000' /> " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='102' name='iLBC' clockrate='8000' bitrate='13300' />" \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='98' name='speex' clockrate='8000' bitrate='11000' />" \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='3' name='GSM' clockrate='8000' bitrate='13000' />   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='100' name='EG711U' clockrate='8000' bitrate='64000' />   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='101' name='EG711A' clockrate='8000' bitrate='64000' />   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='0' name='PCMU' clockrate='8000' bitrate='64000' />  " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='8' name='PCMA' clockrate='8000' bitrate='64000' />  " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='126' name='CN' clockrate='32000' />                 " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='105' name='CN' clockrate='16000' />                 " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='13' name='CN' clockrate='8000' />                   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='117' name='red' clockrate='8000' />                 " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='106' name='telephone-event' clockrate='8000' />     " \
+     "    </description>                                              " \
+     "  </session>                                                    " \
+     "</iq>                                                           ");
+
+const std::string kJingleInitiate(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'           " \
+     "    to='user@domain.com/resource' type='set' id='123'>            " \
+     "  <jingle xmlns='urn:xmpp:jingle:1' action='session-initiate'     " \
+     "          sid='abcdef' initiator='me@domain.com/resource'>        " \
+     "    <content name='test audio'>                                   " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'> " \
+     "        <payload-type id='103' name='ISAC' clockrate='16000'/>    " \
+     "        <payload-type id='104' name='ISAC' clockrate='32000'/>    " \
+     "        <payload-type                                             " \
+     "          id='119' name='ISACLC' clockrate='16000'>               " \
+     "          <parameter name='bitrate' value='40000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='99' name='speex' clockrate='16000'>                 " \
+     "          <parameter name='bitrate' value='22000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='97' name='IPCMWB' clockrate='16000'>                " \
+     "          <parameter name='bitrate' value='80000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='9' name='G722' clockrate='16000'>                   " \
+     "          <parameter name='bitrate' value='64000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='102' name='iLBC' clockrate='8000'>                  " \
+     "          <parameter name='bitrate' value='13300'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='98' name='speex' clockrate='8000'>                  " \
+     "          <parameter name='bitrate' value='11000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='3' name='GSM' clockrate='8000'>                     " \
+     "          <parameter name='bitrate' value='13000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='100' name='EG711U' clockrate='8000'>                " \
+     "          <parameter name='bitrate' value='64000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='101' name='EG711A' clockrate='8000'>                " \
+     "          <parameter name='bitrate' value='64000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='0' name='PCMU' clockrate='8000'>                    " \
+     "          <parameter name='bitrate' value='64000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='8' name='PCMA' clockrate='8000'>                    " \
+     "          <parameter name='bitrate' value='64000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='126' name='CN' clockrate='32000' />                 " \
+     "        <payload-type                                             " \
+     "          id='105' name='CN' clockrate='16000' />                 " \
+     "        <payload-type                                             " \
+     "          id='13' name='CN' clockrate='8000' />                   " \
+     "        <payload-type                                             " \
+     "          id='117' name='red' clockrate='8000' />                 " \
+     "        <payload-type                                             " \
+     "          id='106' name='telephone-event' clockrate='8000' />     " \
+     "      </description>                                              " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/>   " \
+     "    </content>                                                    " \
+     "  </jingle>                                                       " \
+     "</iq>                                                             ");
+
+// Initiate string with a different order of supported codecs.
+// Should accept the supported ones, but with our desired order.
+const std::string kGingleInitiateDifferentPreference(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <session xmlns='http://www.google.com/session' type='initiate'" \
+     "    id='abcdef' initiator='me@domain.com/resource'>             " \
+     "    <description xmlns='http://www.google.com/session/phone'>   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='104' name='ISAC' clockrate='32000' />               " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='97' name='IPCMWB' clockrate='16000' bitrate='80000' />   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='9' name='G722' clockrate='16000' bitrate='64000' /> " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='119' name='ISACLC' clockrate='16000' bitrate='40000' />  " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='103' name='ISAC' clockrate='16000' />               " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='99' name='speex' clockrate='16000' bitrate='22000' />    " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='100' name='EG711U' clockrate='8000' bitrate='64000' />   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='101' name='EG711A' clockrate='8000' bitrate='64000' />   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='0' name='PCMU' clockrate='8000' bitrate='64000' />  " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='8' name='PCMA' clockrate='8000' bitrate='64000' />  " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='102' name='iLBC' clockrate='8000' bitrate='13300' />" \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='3' name='GSM' clockrate='8000' bitrate='13000' />   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='98' name='speex' clockrate='8000' bitrate='11000' />" \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='126' name='CN' clockrate='32000' />                 " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='105' name='CN' clockrate='16000' />                 " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='13' name='CN' clockrate='8000' />                   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='117' name='red' clockrate='8000' />                 " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='106' name='telephone-event' clockrate='8000' />     " \
+     "    </description>                                              " \
+     "  </session>                                                    " \
+     "</iq>                                                           ");
+
+const std::string kJingleInitiateDifferentPreference(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'           " \
+     "    to='user@domain.com/resource' type='set' id='123'>            " \
+     "  <jingle xmlns='urn:xmpp:jingle:1' action='session-initiate'     " \
+     "          sid='abcdef' initiator='me@domain.com/resource'>        " \
+     "    <content name='test audio'>                                   " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'> " \
+     "        <payload-type id='104' name='ISAC' clockrate='32000'/>    " \
+     "        <payload-type                                             " \
+     "          id='97' name='IPCMWB' clockrate='16000'>                " \
+     "          <parameter name='bitrate' value='80000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='9' name='G722' clockrate='16000'>                   " \
+     "          <parameter name='bitrate' value='64000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='119' name='ISACLC' clockrate='16000'>               " \
+     "          <parameter name='bitrate' value='40000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type id='103' name='ISAC' clockrate='16000'/>    " \
+     "        <payload-type                                             " \
+     "          id='99' name='speex' clockrate='16000'>                 " \
+     "          <parameter name='bitrate' value='22000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='100' name='EG711U' clockrate='8000'>                " \
+     "          <parameter name='bitrate' value='64000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='101' name='EG711A' clockrate='8000'>                " \
+     "          <parameter name='bitrate' value='64000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='0' name='PCMU' clockrate='8000'>                    " \
+     "          <parameter name='bitrate' value='64000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='8' name='PCMA' clockrate='8000'>                    " \
+     "          <parameter name='bitrate' value='64000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='102' name='iLBC' clockrate='8000'>                  " \
+     "          <parameter name='bitrate' value='13300'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='3' name='GSM' clockrate='8000'>                     " \
+     "          <parameter name='bitrate' value='13000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='98' name='speex' clockrate='8000'>                  " \
+     "          <parameter name='bitrate' value='11000'/>               " \
+     "        </payload-type>                                           " \
+     "        <payload-type                                             " \
+     "          id='126' name='CN' clockrate='32000' />                 " \
+     "        <payload-type                                             " \
+     "          id='105' name='CN' clockrate='16000' />                 " \
+     "        <payload-type                                             " \
+     "          id='13' name='CN' clockrate='8000' />                   " \
+     "        <payload-type                                             " \
+     "          id='117' name='red' clockrate='8000' />                 " \
+     "        <payload-type                                             " \
+     "          id='106' name='telephone-event' clockrate='8000' />     " \
+     "      </description>                                              " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/>   " \
+     "    </content>                                                    " \
+     "  </jingle>                                                       " \
+     "</iq>                                                             ");
+
+const std::string kGingleVideoInitiate(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <session xmlns='http://www.google.com/session' type='initiate'" \
+     "    id='abcdef' initiator='me@domain.com/resource'>             " \
+     "    <description xmlns='http://www.google.com/session/video'>   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='103' name='ISAC' clockrate='16000' />               " \
+     "      <payload-type xmlns='http://www.google.com/session/video' " \
+     "        id='99' name='H264-SVC' framerate='30'                  " \
+     "        height='200' width='320'/>                              " \
+     "    </description>                                              " \
+     "  </session>                                                    " \
+     "</iq>                                                           ");
+
+const std::string kJingleVideoInitiate(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'           " \
+     "    to='user@domain.com/resource' type='set' id='123'>            " \
+     "  <jingle xmlns='urn:xmpp:jingle:1' action='session-initiate'     " \
+     "          sid='abcdef' initiator='me@domain.com/resource'>        " \
+     "    <content name='test audio'>                                   " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'> " \
+     "        <payload-type id='103' name='ISAC' clockrate='16000'/>    " \
+     "      </description>                                              " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/>   " \
+     "    </content>                                                    " \
+     "    <content name='test video'>                                   " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='video'> " \
+     "        <payload-type id='99' name='H264-SVC'>                    " \
+     "          <parameter name='height' value='200'/>                  " \
+     "          <parameter name='width' value='320'/>                   " \
+     "          <parameter name='framerate' value='30'/>                " \
+     "        </payload-type>                                           " \
+     "      </description>                                              " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/>   " \
+     "    </content>                                                    " \
+     "  </jingle>                                                       " \
+     "</iq>                                                             ");
+
+const std::string kJingleVideoInitiateWithBandwidth(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'           " \
+     "    to='user@domain.com/resource' type='set' id='123'>            " \
+     "  <jingle xmlns='urn:xmpp:jingle:1' action='session-initiate'     " \
+     "         sid='abcdef' initiator='me@domain.com/resource'>         " \
+     "    <content name='test audio'>                                   " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'> " \
+     "        <payload-type id='103' name='ISAC' clockrate='16000'/>    " \
+     "      </description>                                              " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/>   " \
+     "    </content>                                                    " \
+     "    <content name='test video'>                                   " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='video'> " \
+     "        <payload-type id='99' name='H264-SVC'>                    " \
+     "          <parameter name='height' value='200'/>                  " \
+     "          <parameter name='width' value='320'/>                   " \
+     "          <parameter name='framerate' value='30'/>                " \
+     "        </payload-type>                                           " \
+     "        <bandwidth type='AS'>42</bandwidth>                       " \
+     "      </description>                                              " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/>   " \
+     "    </content>                                                    " \
+     "  </jingle>                                                       " \
+     "</iq>                                                             ");
+
+const std::string kJingleVideoInitiateWithRtcpMux(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'           " \
+     "    to='user@domain.com/resource' type='set' id='123'>            " \
+     "  <jingle xmlns='urn:xmpp:jingle:1' action='session-initiate'     " \
+     "         sid='abcdef' initiator='me@domain.com/resource'>         " \
+     "    <content name='test audio'>                                   " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'> " \
+     "        <payload-type id='103' name='ISAC' clockrate='16000'/>    " \
+     "      </description>                                              " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/>   " \
+     "    </content>                                                    " \
+     "    <content name='test video'>                                   " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='video'> " \
+     "        <payload-type id='99' name='H264-SVC'>                    " \
+     "          <parameter name='height' value='200'/>                  " \
+     "          <parameter name='width' value='320'/>                   " \
+     "          <parameter name='framerate' value='30'/>                " \
+     "        </payload-type>                                           " \
+     "        <rtcp-mux/>                                               " \
+     "      </description>                                              " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/>   " \
+     "    </content>                                                    " \
+     "  </jingle>                                                       " \
+     "</iq>                                                             ");
+
+// Initiate string with a combination of supported and unsupported codecs
+// Should accept the supported ones
+const std::string kGingleInitiateSomeUnsupported(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <session xmlns='http://www.google.com/session' type='initiate'" \
+     "    id='abcdef' initiator='me@domain.com/resource'>             " \
+     "    <description xmlns='http://www.google.com/session/phone'>   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='103' name='ISAC' clockrate='16000' />               " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='97' name='ASDFDS' />                                " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='102' name='1010' />                                 " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='107' name='DFAS' />                                 " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='100' name='EG711U' />                               " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='101' name='EG711A' />                               " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='0' name='PCMU' />                                   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='110' name=':)' />                                   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='13' name='CN' />                                    " \
+     "    </description>                                              " \
+     "  </session>                                                    " \
+     "</iq>                                                           ");
+
+const std::string kJingleInitiateSomeUnsupported(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <jingle xmlns='urn:xmpp:jingle:1' action='session-initiate'   " \
+     "          sid='abcdef' initiator='me@domain.com/resource'>      " \
+     "    <content name='test audio'>                                 " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'> " \
+     "        <payload-type                                           " \
+     "          id='103' name='ISAC' clockrate='16000' />             " \
+     "        <payload-type                                           " \
+     "          id='97' name='ASDFDS' />                              " \
+     "        <payload-type                                           " \
+     "          id='102' name='1010' />                               " \
+     "        <payload-type                                           " \
+     "          id='107' name='DFAS' />                               " \
+     "        <payload-type                                           " \
+     "          id='100' name='EG711U' />                             " \
+     "        <payload-type                                           " \
+     "          id='101' name='EG711A' />                             " \
+     "        <payload-type                                           " \
+     "          id='0' name='PCMU' />                                 " \
+     "        <payload-type                                           " \
+     "          id='110' name=':)' />                                 " \
+     "        <payload-type                                           " \
+     "          id='13' name='CN' />                                  " \
+     "      </description>                                            " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/> " \
+     "    </content>                                                  " \
+     "  </jingle>                                                     " \
+     "</iq>                                                           ");
+
+const std::string kGingleVideoInitiateWithBandwidth(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <session xmlns='http://www.google.com/session' type='initiate'" \
+     "    id='abcdef' initiator='me@domain.com/resource'>             " \
+     "    <description xmlns='http://www.google.com/session/video'>   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='103' name='ISAC' clockrate='16000' />               " \
+     "      <payload-type xmlns='http://www.google.com/session/video' " \
+     "        id='99' name='H264-SVC' framerate='30'                  " \
+     "        height='200' width='320'/>                              " \
+     "      <bandwidth type='AS'>42</bandwidth>                       " \
+     "    </description>                                              " \
+     "  </session>                                                    " \
+     "</iq>                                                           ");
+
+// Initiate string without any supported codecs. Should send a reject.
+const std::string kGingleInitiateNoSupportedAudioCodecs(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <session xmlns='http://www.google.com/session' type='initiate'" \
+     "    id='abcdef' initiator='me@domain.com/resource'>             " \
+     "    <description xmlns='http://www.google.com/session/phone'>   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='123' name='Supercodec6000' />                       " \
+     "    </description>                                              " \
+     "  </session>                                                    " \
+     "</iq>                                                           ");
+
+const std::string kJingleInitiateNoSupportedAudioCodecs(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <jingle xmlns='urn:xmpp:jingle:1' action='session-initiate'   " \
+     "          sid='abcdef' initiator='me@domain.com/resource'>      " \
+     "    <content name='test audio'>                                 " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'>" \
+     "        <payload-type                                           " \
+     "          id='123' name='Supercodec6000' />                     " \
+     "      </description>                                            " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/>  " \
+     "    </content>                                                  " \
+     "  </jingle>                                                     " \
+     "</iq>                                                           ");
+
+// Initiate string without any codecs. Assumes ancient version of Cricket
+// and tries a session with ISAC and PCMU
+const std::string kGingleInitiateNoAudioCodecs(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <session xmlns='http://www.google.com/session' type='initiate'" \
+     "    id='abcdef' initiator='me@domain.com/resource'>             " \
+     "    <description xmlns='http://www.google.com/session/phone'>   " \
+     "    </description>                                              " \
+     "  </session>                                                    " \
+     "</iq>                                                           ");
+
+const std::string kJingleInitiateNoAudioCodecs(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <jingle xmlns='urn:xmpp:jingle:1' action='session-initiate'   " \
+     "          sid='abcdef' initiator='me@domain.com/resource'>      " \
+     "    <content name='test audio'>                                 " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'>" \
+     "      </description>                                            " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/>  " \
+     "    </content>                                                  " \
+     "  </jingle>                                                     " \
+     "</iq>                                                           ");
+
+// The codecs are supported, but not at the given clockrates. Should send
+// a reject.
+const std::string kGingleInitiateWrongClockrates(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <session xmlns='http://www.google.com/session' type='initiate'" \
+     "    id='abcdef' initiator='me@domain.com/resource'>             " \
+     "    <description xmlns='http://www.google.com/session/phone'>   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='103' name='ISAC' clockrate='8000'/>                 " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='97' name='IPCMWB' clockrate='1337'/>                " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='102' name='iLBC' clockrate='1982' />                " \
+     "    </description>                                              " \
+     "  </session>                                                    " \
+     "</iq>                                                           ");
+
+const std::string kJingleInitiateWrongClockrates(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'           " \
+     "    to='user@domain.com/resource' type='set' id='123'>            " \
+     "  <jingle xmlns='urn:xmpp:jingle:1' action='session-initiate'     " \
+     "          sid='abcdef' initiator='me@domain.com/resource'>        " \
+     "    <content name='test audio'>                                   " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'>" \
+     "        <payload-type                                             " \
+     "          id='103' name='ISAC' clockrate='8000'/>                 " \
+     "        <payload-type                                             " \
+     "          id='97' name='IPCMWB' clockrate='1337'/>                " \
+     "       <payload-type                                              " \
+     "          id='102' name='iLBC' clockrate='1982' />                " \
+     "      </description>                                              " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/>  " \
+     "    </content>                                                    " \
+     "  </jingle>                                                       " \
+     "</iq>                                                             ");
+
+// The codecs are supported, but not with the given number of channels.
+// Should send a reject.
+const std::string kGingleInitiateWrongChannels(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <session xmlns='http://www.google.com/session' type='initiate'" \
+     "    id='abcdef' initiator='me@domain.com/resource'>             " \
+     "    <description xmlns='http://www.google.com/session/phone'>   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='103' name='ISAC' channels='2'/>                     " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='97' name='IPCMWB' channels='3'/>                    " \
+     "    </description>                                              " \
+     "  </session>                                                    " \
+     "</iq>                                                           ");
+
+const std::string kJingleInitiateWrongChannels(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'           " \
+     "    to='user@domain.com/resource' type='set' id='123'>            " \
+     "  <jingle xmlns='urn:xmpp:jingle:1' action='session-initiate'>    " \
+     "    <content name='test audio'>                                   " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'>" \
+     "        <payload-type                                             " \
+     "          id='103' name='ISAC' channels='2'/>                     " \
+     "        <payload-type                                             " \
+     "          id='97' name='IPCMWB' channels='3'/>                    " \
+     "      </description>                                              " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/>   " \
+     "    </content>                                                    " \
+     "  </jingle>                                                       " \
+     "</iq>                                                             ");
+
+// Initiate with a dynamic codec not using GIPS default payload id. Should
+// accept with provided payload id.
+const std::string kGingleInitiateDynamicAudioCodecs(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <session xmlns='http://www.google.com/session' type='initiate'" \
+     "    id='abcdef' initiator='me@domain.com/resource'>             " \
+     "    <description xmlns='http://www.google.com/session/phone'>   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='123' name='speex' clockrate='16000'/>               " \
+     "    </description>                                              " \
+     "  </session>                                                    " \
+     "</iq>                                                           ");
+
+const std::string kJingleInitiateDynamicAudioCodecs(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'           " \
+     "    to='user@domain.com/resource' type='set' id='123'>            " \
+     "  <jingle xmlns='urn:xmpp:jingle:1' action='session-initiate'     " \
+     "          sid='abcdef' initiator='me@domain.com/resource'>        " \
+     "    <content name='test audio'>                                   " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'>" \
+     "        <payload-type                                             " \
+     "          id='123' name='speex' clockrate='16000'/>               " \
+     "      </description>                                              " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/>   " \
+     "    </content>                                                    " \
+     "  </jingle>                                                       " \
+     "</iq>                                                             ");
+
+// Initiate string with nothing but static codec id's. Should accept.
+const std::string kGingleInitiateStaticAudioCodecs(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <session xmlns='http://www.google.com/session' type='initiate'" \
+     "    id='abcdef' initiator='me@domain.com/resource'>             " \
+     "    <description xmlns='http://www.google.com/session/phone'>   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='3' />                                               " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='0' />                                               " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='8' />                                               " \
+     "    </description>                                              " \
+     "  </session>                                                    " \
+     "</iq>                                                           ");
+
+const std::string kJingleInitiateStaticAudioCodecs(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <jingle xmlns='urn:xmpp:jingle:1' action='session-initiate'   " \
+     "          sid='abcdef' initiator='me@domain.com/resource'>      " \
+     "    <content name='test audio'>                                 " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'>" \
+     "        <payload-type id='3' />                                 " \
+     "        <payload-type id='0' />                                 " \
+     "        <payload-type id='8' />                                 " \
+     "      </description>                                            " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/> " \
+     "    </content>                                                  " \
+     "  </jingle>                                                     " \
+     "</iq>                                                           ");
+
+// Initiate with payload type-less codecs. Should reject.
+const std::string kGingleInitiateNoPayloadTypes(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <session xmlns='http://www.google.com/session' type='initiate'" \
+     "    id='abcdef' initiator='me@domain.com/resource'>             " \
+     "    <description xmlns='http://www.google.com/session/phone'>   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "       name='ISAC' clockrate='16000'/>                          " \
+     "    </description>                                              " \
+     "  </session>                                                    " \
+     "</iq>                                                           ");
+
+const std::string kJingleInitiateNoPayloadTypes(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <jingle xmlns='urn:xmpp:jingle:1' action='session-initiate'>  " \
+     "          sid='abcdef' initiator='me@domain.com/resource'>      " \
+     "    <content name='test audio'>                                 " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'>" \
+     "        <payload-type  name='ISAC' clockrate='16000'/>          " \
+     "      </description>                                            " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/> " \
+     "    </content>                                                  " \
+     "  </jingle>                                                     " \
+     "</iq>                                                           ");
+
+// Initiate with unnamed dynamic codces. Should reject.
+const std::string kGingleInitiateDynamicWithoutNames(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <session xmlns='http://www.google.com/session' type='initiate'" \
+     "    id='abcdef' initiator='me@domain.com/resource'>             " \
+     "    <description xmlns='http://www.google.com/session/phone'>   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "       id='100' clockrate='16000'/>                             " \
+     "    </description>                                              " \
+     "  </session>                                                    " \
+     "</iq>                                                           ");
+
+const std::string kJingleInitiateDynamicWithoutNames(
+     "<iq xmlns='jabber:client' from='me@domain.com/resource'         " \
+     "    to='user@domain.com/resource' type='set' id='123'>          " \
+     "  <jingle xmlns='urn:xmpp:jingle:1' action='session-initiate'>  " \
+     "          sid='abcdef' initiator='me@domain.com/resource'>      " \
+     "    <content name='test audio'>                                 " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'>" \
+     "        <payload-type id='100' clockrate='16000'/>              " \
+     "      </description>                                            " \
+     "     <transport xmlns=\"http://www.google.com/transport/p2p\"/>  " \
+     "    </content>                                                  " \
+     "  </jingle>                                                     " \
+     "</iq>                                                           ");
+
+const uint32 kAudioSsrc = 4294967295U;
+const uint32 kVideoSsrc = 87654321;
+// Note that this message does not specify a session ID. It must be populated
+// before use.
+const std::string kGingleAcceptWithSsrcs(
+     "<iq xmlns='jabber:client' from='me@mydomain.com'                " \
+     "    to='user@domain.com/resource' type='set' id='150'>          " \
+     "  <session xmlns='http://www.google.com/session' type='accept'  " \
+     "    initiator='me@domain.com/resource'>                         " \
+     "    <description xmlns='http://www.google.com/session/video'>   " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='103' name='ISAC' clockrate='16000' />               " \
+     "      <payload-type xmlns='http://www.google.com/session/phone' " \
+     "        id='104' name='ISAC' clockrate='32000' />               " \
+     "      <src-id xmlns='http://www.google.com/session/phone'>      " \
+     "        4294967295</src-id>                                       " \
+     "      <src-id>87654321</src-id>                                 " \
+     "    </description>                                              " \
+     "  </session>                                                    " \
+     "</iq>                                                           ");
+
+const std::string kJingleAcceptWithSsrcs(
+     "<iq xmlns='jabber:client' from='me@mydomain.com'                " \
+     "    to='user@domain.com/resource' type='set' id='150'>          " \
+     "  <jingle xmlns='urn:xmpp:jingle:1' action='session-accept'     " \
+     "          initiator='me@domain.com/resource'>                   " \
+     "    <content name='audio'>                                      " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1'           " \
+     "          media='audio' ssrc='4294967295'>                      " \
+     "        <payload-type id='103' name='ISAC' clockrate='16000'/>  " \
+     "        <payload-type id='104' name='ISAC' clockrate='32000'/>  " \
+     "      </description>                                            " \
+     "     <transport xmlns='http://www.google.com/transport/p2p'/>   " \
+     "    </content>                                                  " \
+     "    <content name='video'>                                      " \
+     "      <description xmlns='urn:xmpp:jingle:apps:rtp:1'           " \
+     "          media='video' ssrc='87654321'>                        " \
+     "      </description>                                            " \
+     "     <transport xmlns='http://www.google.com/transport/p2p'/>   " \
+     "    </content>                                                  " \
+     "  </jingle>                                                     " \
+     "</iq>                                                           ");
+
+std::string JingleView(const std::string& ssrc,
+                       const std::string& width,
+                       const std::string& height,
+                       const std::string& framerate) {
+  // We have some slightly weird whitespace formatting to make the
+  // actual XML generated match the expected XML here.
+  return \
+      "<cli:iq"
+      "  to='me@mydomain.com'"
+      "  type='set'"
+      "  xmlns:cli='jabber:client'>"
+        "<jingle"
+      "    xmlns='urn:xmpp:jingle:1'"
+      "    action='session-info'"
+      "    sid=''>"
+          "<view xmlns='google:jingle'"
+      "      name='video'"
+      "      type='static'"
+      "      ssrc='" + ssrc + "'>"
+            "<params"
+      "        width='" + width + "'"
+      "        height='" + height + "'"
+      "        framerate='" + framerate + "'"
+      "        preference='0'/>"
+          "</view>"
+        "</jingle>"
+      "</cli:iq>";
+}
+
+std::string JingleNotifyAdd(const std::string& content_name,
+                            const std::string& nick,
+                            const std::string& ssrc) {
+  return \
+      "<iq"
+      "  xmlns='jabber:client'"
+      "  from='me@mydomain.com'"
+      "  to='user@domain.com/resource'"
+      "  type='set'"
+      "  id='150'>"
+      "  <jingle"
+      "    xmlns='urn:xmpp:jingle:1'"
+      "    action='session-info'>"
+      "    <notify"
+      "      xmlns='google:jingle'"
+      "      name='" + content_name + "'>"
+      "      <source"
+      "        nick='" + nick + "'>"
+      "        <ssrc>"  + ssrc + "</ssrc>"
+      "      </source>"
+      "    </notify>"
+      "  </jingle>"
+      "</iq>";
+}
+
+std::string JingleNotifyImplicitRemove(const std::string& content_name,
+                                       const std::string& nick) {
+  return \
+      "<iq"
+      "  xmlns='jabber:client'"
+      "  from='me@mydomain.com'"
+      "  to='user@domain.com/resource'"
+      "  type='set'"
+      "  id='150'>"
+      "  <jingle"
+      "    xmlns='urn:xmpp:jingle:1'"
+      "    action='session-info'>"
+      "    <notify"
+      "      xmlns='google:jingle'"
+      "      name='" + content_name + "'>"
+      "      <source"
+      "        nick='" + nick + "'>"
+       "      </source>"
+      "    </notify>"
+      "  </jingle>"
+      "</iq>";
+}
+
+buzz::XmlElement* CopyElement(const buzz::XmlElement* elem) {
+  return new buzz::XmlElement(*elem);
+}
+
+static std::string AddEncryption(std::string stanza, std::string encryption) {
+  std::string::size_type pos = stanza.find("</description>");
+  while (pos != std::string::npos) {
+      stanza = stanza.insert(pos, encryption);
+      pos = stanza.find("</description>", pos + encryption.length() + 1);
+  }
+  return stanza;
+}
+
+
+int IntFromJingleCodecParameter(const buzz::XmlElement* parameter,
+                                const std::string& expected_name) {
+  if (parameter) {
+    const std::string& actual_name =
+        parameter->Attr(cricket::QN_PAYLOADTYPE_PARAMETER_NAME);
+
+    EXPECT_EQ(expected_name, actual_name)
+        << "wrong parameter name.  Expected '"
+        << expected_name << "'. Actually '"
+        << actual_name << "'.";
+
+    return atoi(parameter->Attr(
+        cricket::QN_PAYLOADTYPE_PARAMETER_VALUE).c_str());
+  }
+  return 0;
+}
+
+// Parses and extracts payload and codec info from test XML.  Since
+// that XML will be in various contents (Gingle and Jingle), we need an
+// abstract parser with one concrete implementation per XML content.
+class MediaSessionTestParser {
+ public:
+  virtual buzz::XmlElement* ActionFromStanza(buzz::XmlElement* stanza) = 0;
+  virtual buzz::XmlElement* ContentFromAction(buzz::XmlElement* action) = 0;
+  virtual buzz::XmlElement* NextContent(buzz::XmlElement* content) = 0;
+  virtual buzz::XmlElement* PayloadTypeFromContent(
+      buzz::XmlElement* content) = 0;
+  virtual buzz::XmlElement* NextFromPayloadType(
+      buzz::XmlElement* payload_type) = 0;
+  virtual cricket::AudioCodec AudioCodecFromPayloadType(
+      const buzz::XmlElement* payload_type) = 0;
+  virtual cricket::VideoCodec VideoCodecFromPayloadType(
+      const buzz::XmlElement* payload_type) = 0;
+  virtual buzz::XmlElement* EncryptionFromContent(
+      buzz::XmlElement* content) = 0;
+  virtual buzz::XmlElement* NextFromEncryption(
+      buzz::XmlElement* encryption) = 0;
+  virtual const buzz::XmlElement* BandwidthFromContent(
+      buzz::XmlElement* content) = 0;
+  virtual const buzz::XmlElement* RtcpMuxFromContent(
+      buzz::XmlElement* content) = 0;
+  virtual bool ActionIsTerminate(const buzz::XmlElement* action) = 0;
+  virtual ~MediaSessionTestParser() {}
+};
+
+class JingleSessionTestParser : public MediaSessionTestParser {
+ public:
+  JingleSessionTestParser() : action_(NULL) {}
+
+  ~JingleSessionTestParser() {
+    delete action_;
+  }
+
+  buzz::XmlElement* ActionFromStanza(buzz::XmlElement* stanza) {
+    return stanza->FirstNamed(cricket::QN_JINGLE);
+  }
+
+  buzz::XmlElement* ContentFromAction(buzz::XmlElement* action) {
+    // We need to be able to use multiple contents, but the action
+    // gets deleted before we can call NextContent, so we need to
+    // stash away a copy.
+    action_ = CopyElement(action);
+    return action_->FirstNamed(cricket::QN_JINGLE_CONTENT);
+  }
+
+  buzz::XmlElement* NextContent(buzz::XmlElement* content) {
+    // For some reason, content->NextNamed(cricket::QN_JINGLE_CONTENT)
+    // doesn't work.
+    return action_->FirstNamed(cricket::QN_JINGLE_CONTENT)
+        ->NextNamed(cricket::QN_JINGLE_CONTENT);
+  }
+
+  buzz::XmlElement* PayloadTypeFromContent(buzz::XmlElement* content) {
+    buzz::XmlElement* content_desc =
+        content->FirstNamed(cricket::QN_JINGLE_RTP_CONTENT);
+    if (!content_desc)
+      return NULL;
+
+    return content_desc->FirstNamed(cricket::QN_JINGLE_RTP_PAYLOADTYPE);
+  }
+
+  buzz::XmlElement* NextFromPayloadType(buzz::XmlElement* payload_type) {
+    return payload_type->NextNamed(cricket::QN_JINGLE_RTP_PAYLOADTYPE);
+  }
+
+  cricket::AudioCodec AudioCodecFromPayloadType(
+      const buzz::XmlElement* payload_type) {
+    int id = 0;
+    if (payload_type->HasAttr(cricket::QN_ID))
+      id = atoi(payload_type->Attr(cricket::QN_ID).c_str());
+
+    std::string name = "";
+    if (payload_type->HasAttr(cricket::QN_NAME))
+      name = payload_type->Attr(cricket::QN_NAME);
+
+    int clockrate = 0;
+    if (payload_type->HasAttr(cricket::QN_CLOCKRATE))
+      clockrate = atoi(payload_type->Attr(cricket::QN_CLOCKRATE).c_str());
+
+    int bitrate = IntFromJingleCodecParameter(
+        payload_type->FirstNamed(cricket::QN_PARAMETER), "bitrate");
+
+    int channels = 1;
+    if (payload_type->HasAttr(cricket::QN_CHANNELS))
+      channels = atoi(payload_type->Attr(
+          cricket::QN_CHANNELS).c_str());
+
+    return cricket::AudioCodec(id, name, clockrate, bitrate, channels, 0);
+  }
+
+  cricket::VideoCodec VideoCodecFromPayloadType(
+      const buzz::XmlElement* payload_type) {
+    int id = 0;
+    if (payload_type->HasAttr(cricket::QN_ID))
+      id = atoi(payload_type->Attr(cricket::QN_ID).c_str());
+
+    std::string name = "";
+    if (payload_type->HasAttr(cricket::QN_NAME))
+      name = payload_type->Attr(cricket::QN_NAME);
+
+    int width = 0;
+    int height = 0;
+    int framerate = 0;
+    const buzz::XmlElement* param =
+        payload_type->FirstNamed(cricket::QN_PARAMETER);
+    if (param) {
+      width = IntFromJingleCodecParameter(param, "width");
+      param = param->NextNamed(cricket::QN_PARAMETER);
+      if (param) {
+        height = IntFromJingleCodecParameter(param, "height");
+        param = param->NextNamed(cricket::QN_PARAMETER);
+        if (param) {
+          framerate = IntFromJingleCodecParameter(param, "framerate");
+        }
+      }
+    }
+
+    return cricket::VideoCodec(id, name, width, height, framerate, 0);
+  }
+
+  bool ActionIsTerminate(const buzz::XmlElement* action) {
+    return (action->HasAttr(cricket::QN_ACTION) &&
+            action->Attr(cricket::QN_ACTION) == "session-terminate");
+  }
+
+  buzz::XmlElement* EncryptionFromContent(buzz::XmlElement* content) {
+    buzz::XmlElement* content_desc =
+        content->FirstNamed(cricket::QN_JINGLE_RTP_CONTENT);
+    if (!content_desc)
+      return NULL;
+
+    return content_desc->FirstNamed(cricket::QN_ENCRYPTION);
+  }
+
+  buzz::XmlElement* NextFromEncryption(buzz::XmlElement* encryption) {
+    return encryption->NextNamed(cricket::QN_ENCRYPTION);
+  }
+
+  const buzz::XmlElement* BandwidthFromContent(buzz::XmlElement* content) {
+    buzz::XmlElement* content_desc =
+        content->FirstNamed(cricket::QN_JINGLE_RTP_CONTENT);
+    if (!content_desc)
+      return NULL;
+
+    return content_desc->FirstNamed(cricket::QN_JINGLE_RTP_BANDWIDTH);
+  }
+
+  const buzz::XmlElement* RtcpMuxFromContent(buzz::XmlElement* content) {
+    return content->FirstNamed(cricket::QN_JINGLE_RTCP_MUX);
+  }
+
+ private:
+  buzz::XmlElement* action_;
+};
+
+class GingleSessionTestParser : public MediaSessionTestParser {
+ public:
+  GingleSessionTestParser() : found_content_count_(0) {}
+
+  buzz::XmlElement* ActionFromStanza(buzz::XmlElement* stanza) {
+    return stanza->FirstNamed(cricket::QN_GINGLE_SESSION);
+  }
+
+  buzz::XmlElement* ContentFromAction(buzz::XmlElement* session) {
+    buzz::XmlElement* content =
+        session->FirstNamed(cricket::QN_GINGLE_AUDIO_CONTENT);
+    if (content == NULL)
+      content = session->FirstNamed(cricket::QN_GINGLE_VIDEO_CONTENT);
+    return content;
+  }
+
+  // Assumes contents are in order of audio, and then video.
+  buzz::XmlElement* NextContent(buzz::XmlElement* content) {
+    found_content_count_++;
+    return content;
+  }
+
+  buzz::XmlElement* PayloadTypeFromContent(buzz::XmlElement* content) {
+    if (found_content_count_ > 0) {
+      return content->FirstNamed(cricket::QN_GINGLE_VIDEO_PAYLOADTYPE);
+    } else {
+      return content->FirstNamed(cricket::QN_GINGLE_AUDIO_PAYLOADTYPE);
+    }
+  }
+
+  buzz::XmlElement* NextFromPayloadType(buzz::XmlElement* payload_type) {
+    if (found_content_count_ > 0) {
+      return payload_type->NextNamed(cricket::QN_GINGLE_VIDEO_PAYLOADTYPE);
+    } else {
+      return payload_type->NextNamed(cricket::QN_GINGLE_AUDIO_PAYLOADTYPE);
+    }
+  }
+
+  cricket::AudioCodec AudioCodecFromPayloadType(
+      const buzz::XmlElement* payload_type) {
+    int id = 0;
+    if (payload_type->HasAttr(cricket::QN_ID))
+      id = atoi(payload_type->Attr(cricket::QN_ID).c_str());
+
+    std::string name;
+    if (payload_type->HasAttr(cricket::QN_NAME))
+      name = payload_type->Attr(cricket::QN_NAME);
+
+    int clockrate = 0;
+    if (payload_type->HasAttr(cricket::QN_CLOCKRATE))
+      clockrate = atoi(payload_type->Attr(cricket::QN_CLOCKRATE).c_str());
+
+    int bitrate = 0;
+    if (payload_type->HasAttr(cricket::QN_BITRATE))
+      bitrate = atoi(payload_type->Attr(cricket::QN_BITRATE).c_str());
+
+    int channels = 1;
+    if (payload_type->HasAttr(cricket::QN_CHANNELS))
+      channels = atoi(payload_type->Attr(cricket::QN_CHANNELS).c_str());
+
+    return cricket::AudioCodec(id, name, clockrate, bitrate, channels, 0);
+  }
+
+  cricket::VideoCodec VideoCodecFromPayloadType(
+      const buzz::XmlElement* payload_type) {
+    int id = 0;
+    if (payload_type->HasAttr(cricket::QN_ID))
+      id = atoi(payload_type->Attr(cricket::QN_ID).c_str());
+
+    std::string name;
+    if (payload_type->HasAttr(cricket::QN_NAME))
+      name = payload_type->Attr(cricket::QN_NAME);
+
+    int width = 0;
+    if (payload_type->HasAttr(cricket::QN_WIDTH))
+      width = atoi(payload_type->Attr(cricket::QN_WIDTH).c_str());
+
+    int height = 0;
+    if (payload_type->HasAttr(cricket::QN_HEIGHT))
+      height = atoi(payload_type->Attr(cricket::QN_HEIGHT).c_str());
+
+    int framerate = 1;
+    if (payload_type->HasAttr(cricket::QN_FRAMERATE))
+      framerate = atoi(payload_type->Attr(cricket::QN_FRAMERATE).c_str());
+
+    return cricket::VideoCodec(id, name, width, height, framerate, 0);
+  }
+
+  buzz::XmlElement* EncryptionFromContent(
+      buzz::XmlElement* content) {
+    return content->FirstNamed(cricket::QN_ENCRYPTION);
+  }
+
+  buzz::XmlElement* NextFromEncryption(buzz::XmlElement* encryption) {
+    return encryption->NextNamed(cricket::QN_ENCRYPTION);
+  }
+
+  const buzz::XmlElement* BandwidthFromContent(buzz::XmlElement* content) {
+    return content->FirstNamed(cricket::QN_GINGLE_VIDEO_BANDWIDTH);
+  }
+
+  const buzz::XmlElement* RtcpMuxFromContent(buzz::XmlElement* content) {
+    return NULL;
+  }
+
+  bool ActionIsTerminate(const buzz::XmlElement* session) {
+    return (session->HasAttr(buzz::QN_TYPE) &&
+            session->Attr(buzz::QN_TYPE) == "terminate");
+  }
+
+  int found_content_count_;
+};
+
+class MediaSessionClientTest : public sigslot::has_slots<> {
+ public:
+  explicit MediaSessionClientTest(MediaSessionTestParser* parser,
+                                  cricket::SignalingProtocol initial_protocol) {
+    nm_ = new talk_base::BasicNetworkManager();
+    pa_ = new cricket::BasicPortAllocator(nm_);
+    sm_ = new cricket::SessionManager(pa_, NULL);
+    fme_ = new cricket::FakeMediaEngine();
+
+    std::vector<cricket::AudioCodec>
+        audio_codecs(kAudioCodecs, kAudioCodecs + ARRAY_SIZE(kAudioCodecs));
+    fme_->SetAudioCodecs(audio_codecs);
+    std::vector<cricket::VideoCodec>
+        video_codecs(kVideoCodecs, kVideoCodecs + ARRAY_SIZE(kVideoCodecs));
+    fme_->SetVideoCodecs(video_codecs);
+
+    client_ = new cricket::MediaSessionClient(
+        buzz::Jid("user@domain.com/resource"), sm_,
+        fme_, new cricket::FakeDeviceManager());
+    client_->session_manager()->SignalOutgoingMessage.connect(
+        this, &MediaSessionClientTest::OnSendStanza);
+    client_->session_manager()->SignalSessionCreate.connect(
+        this, &MediaSessionClientTest::OnSessionCreate);
+    client_->SignalCallCreate.connect(
+        this, &MediaSessionClientTest::OnCallCreate);
+    client_->SignalCallDestroy.connect(
+        this, &MediaSessionClientTest::OnCallDestroy);
+
+    call_ = NULL;
+    parser_ = parser;
+    initial_protocol_ = initial_protocol;
+    expect_incoming_crypto_ = false;
+    expect_outgoing_crypto_ = false;
+    expected_video_bandwidth_ = cricket::kAutoBandwidth;
+    expected_video_rtcp_mux_ = false;
+  }
+
+  ~MediaSessionClientTest() {
+    delete client_;
+    delete sm_;
+    delete pa_;
+    delete nm_;
+    delete parser_;
+  }
+
+  buzz::XmlElement* ActionFromStanza(buzz::XmlElement* stanza) {
+    return parser_->ActionFromStanza(stanza);
+  }
+
+  buzz::XmlElement* ContentFromAction(buzz::XmlElement* action) {
+    return parser_->ContentFromAction(action);
+  }
+
+  buzz::XmlElement* PayloadTypeFromContent(buzz::XmlElement* payload) {
+    return parser_->PayloadTypeFromContent(payload);
+  }
+
+  buzz::XmlElement* NextFromPayloadType(buzz::XmlElement* payload_type) {
+    return parser_->NextFromPayloadType(payload_type);
+  }
+
+  buzz::XmlElement* EncryptionFromContent(buzz::XmlElement* content) {
+    return parser_->EncryptionFromContent(content);
+  }
+
+  buzz::XmlElement* NextFromEncryption(buzz::XmlElement* encryption) {
+    return parser_->NextFromEncryption(encryption);
+  }
+
+  cricket::AudioCodec AudioCodecFromPayloadType(
+      const buzz::XmlElement* payload_type) {
+    return parser_->AudioCodecFromPayloadType(payload_type);
+  }
+
+  const cricket::AudioContentDescription* GetFirstAudioContentDescription(
+      const cricket::SessionDescription* sdesc) {
+    const cricket::ContentInfo* content =
+        cricket::GetFirstAudioContent(sdesc);
+    if (content == NULL)
+      return NULL;
+    return static_cast<const cricket::AudioContentDescription*>(
+        content->description);
+  }
+
+  const cricket::VideoContentDescription* GetFirstVideoContentDescription(
+      const cricket::SessionDescription* sdesc) {
+    const cricket::ContentInfo* content =
+        cricket::GetFirstVideoContent(sdesc);
+    if (content == NULL)
+      return NULL;
+    return static_cast<const cricket::VideoContentDescription*>(
+        content->description);
+  }
+
+  void CheckCryptoFromGoodIncomingInitiate(const cricket::Session* session) {
+    ASSERT_TRUE(session != NULL);
+    const cricket::AudioContentDescription* content =
+        GetFirstAudioContentDescription(session->remote_description());
+    ASSERT_TRUE(content != NULL);
+    ASSERT_EQ(2U, content->cryptos().size());
+    ASSERT_EQ(145, content->cryptos()[0].tag);
+    ASSERT_EQ("AES_CM_128_HMAC_SHA1_32", content->cryptos()[0].cipher_suite);
+    ASSERT_EQ("inline:hsWuSQJxx7przmb8HM+ZkeNcG3HezSNID7LmfDa9",
+              content->cryptos()[0].key_params);
+    ASSERT_EQ(51, content->cryptos()[1].tag);
+    ASSERT_EQ("AES_CM_128_HMAC_SHA1_80", content->cryptos()[1].cipher_suite);
+    ASSERT_EQ("inline:J4lfdUL8W1F7TNJKcbuygaQuA429SJy2e9JctPUy",
+              content->cryptos()[1].key_params);
+  }
+
+  void CheckCryptoForGoodOutgoingAccept(const cricket::Session* session) {
+    const cricket::AudioContentDescription* content =
+        GetFirstAudioContentDescription(session->local_description());
+    ASSERT_EQ(1U, content->cryptos().size());
+    ASSERT_EQ(145, content->cryptos()[0].tag);
+    ASSERT_EQ("AES_CM_128_HMAC_SHA1_32", content->cryptos()[0].cipher_suite);
+    ASSERT_EQ(47U, content->cryptos()[0].key_params.size());
+  }
+
+  void CheckBadCryptoFromIncomingInitiate(const cricket::Session* session) {
+    const cricket::AudioContentDescription* content =
+        GetFirstAudioContentDescription(session->remote_description());
+    ASSERT_EQ(1U, content->cryptos().size());
+    ASSERT_EQ(145, content->cryptos()[0].tag);
+    ASSERT_EQ("NOT_SUPPORTED", content->cryptos()[0].cipher_suite);
+    ASSERT_EQ("inline:hsWuSQJxx7przmb8HM+ZkeNcG3HezSNID7LmfDa9",
+              content->cryptos()[0].key_params);
+  }
+
+  void CheckNoCryptoForOutgoingAccept(const cricket::Session* session) {
+    const cricket::AudioContentDescription* content =
+        GetFirstAudioContentDescription(session->local_description());
+    ASSERT_TRUE(content->cryptos().empty());
+  }
+
+  void CheckVideoBandwidth(int expected_bandwidth,
+                           const cricket::SessionDescription* sdesc) {
+    const cricket::VideoContentDescription* video =
+        GetFirstVideoContentDescription(sdesc);
+    if (video != NULL) {
+      ASSERT_EQ(expected_bandwidth, video->bandwidth());
+    }
+  }
+
+  void CheckVideoRtcpMux(bool expected_video_rtcp_mux,
+                         const cricket::SessionDescription* sdesc) {
+    const cricket::VideoContentDescription* video =
+        GetFirstVideoContentDescription(sdesc);
+    if (video != NULL) {
+      ASSERT_EQ(expected_video_rtcp_mux, video->rtcp_mux());
+    }
+  }
+
+  void CheckAudioSsrcForIncomingAccept(const cricket::Session* session) {
+    const cricket::AudioContentDescription* audio =
+        GetFirstAudioContentDescription(session->remote_description());
+    ASSERT_TRUE(audio != NULL);
+    ASSERT_EQ(kAudioSsrc, audio->first_ssrc());
+  }
+
+  void CheckVideoSsrcForIncomingAccept(const cricket::Session* session) {
+    const cricket::VideoContentDescription* video =
+        GetFirstVideoContentDescription(session->remote_description());
+    ASSERT_TRUE(video != NULL);
+    ASSERT_EQ(kVideoSsrc, video->first_ssrc());
+  }
+
+  void TestGoodIncomingInitiate(const std::string &initiate_string,
+                                buzz::XmlElement** element) {
+    *element = NULL;
+
+    buzz::XmlElement* el = buzz::XmlElement::ForStr(initiate_string);
+    client_->session_manager()->OnIncomingMessage(el);
+    ASSERT_TRUE(call_ != NULL);
+    ASSERT_TRUE(call_->sessions()[0] != NULL);
+    ASSERT_EQ(cricket::Session::STATE_RECEIVEDINITIATE,
+              call_->sessions()[0]->state());
+    ASSERT_EQ(1U, stanzas_.size());
+    ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name());
+    ASSERT_TRUE(stanzas_[0]->HasAttr(buzz::QN_TYPE));
+    ASSERT_EQ(std::string(buzz::STR_RESULT), stanzas_[0]->Attr(buzz::QN_TYPE));
+    delete stanzas_[0];
+    stanzas_.clear();
+    CheckVideoBandwidth(expected_video_bandwidth_,
+                        call_->sessions()[0]->remote_description());
+    CheckVideoRtcpMux(expected_video_rtcp_mux_,
+                      call_->sessions()[0]->remote_description());
+    if (expect_incoming_crypto_) {
+      CheckCryptoFromGoodIncomingInitiate(call_->sessions()[0]);
+    }
+
+    // TODO: Add tests for sending <bandwidth> in accept.
+    cricket::CallOptions opts;
+    opts.has_video = (GetFirstVideoContentDescription(
+        call_->sessions()[0]->remote_description()) != NULL);
+    call_->AcceptSession(call_->sessions()[0], opts);
+    ASSERT_EQ(cricket::Session::STATE_SENTACCEPT,
+              call_->sessions()[0]->state());
+    ASSERT_EQ(1U, stanzas_.size());
+    ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name());
+    ASSERT_TRUE(stanzas_[0]->HasAttr(buzz::QN_TYPE));
+    ASSERT_EQ(std::string(buzz::STR_SET), stanzas_[0]->Attr(buzz::QN_TYPE));
+
+    buzz::XmlElement* e = ActionFromStanza(stanzas_[0]);
+    ASSERT_TRUE(e != NULL);
+    ASSERT_TRUE(ContentFromAction(e) != NULL);
+    *element = CopyElement(ContentFromAction(e));
+    ASSERT_TRUE(*element != NULL);
+    delete stanzas_[0];
+    stanzas_.clear();
+    if (expect_outgoing_crypto_) {
+      CheckCryptoForGoodOutgoingAccept(call_->sessions()[0]);
+    }
+
+    call_->Terminate();
+    ASSERT_EQ(cricket::Session::STATE_SENTTERMINATE,
+              call_->sessions()[0]->state());
+    ASSERT_EQ(1U, stanzas_.size());
+    ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name());
+    ASSERT_TRUE(stanzas_[0]->HasAttr(buzz::QN_TYPE));
+    ASSERT_EQ(std::string(buzz::STR_SET), stanzas_[0]->Attr(buzz::QN_TYPE));
+    e = ActionFromStanza(stanzas_[0]);
+    ASSERT_TRUE(e != NULL);
+    ASSERT_TRUE(parser_->ActionIsTerminate(e));
+    delete stanzas_[0];
+    stanzas_.clear();
+  }
+
+  void TestBadIncomingInitiate(const std::string& initiate_string) {
+    buzz::XmlElement* el = buzz::XmlElement::ForStr(initiate_string);
+    client_->session_manager()->OnIncomingMessage(el);
+    ASSERT_TRUE(call_ != NULL);
+    ASSERT_TRUE(call_->sessions()[0] != NULL);
+    ASSERT_EQ(cricket::Session::STATE_SENTREJECT,
+              call_->sessions()[0]->state());
+    ASSERT_EQ(2U, stanzas_.size());
+    ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name());
+    ASSERT_TRUE(stanzas_[1]->HasAttr(buzz::QN_TYPE));
+    ASSERT_EQ(std::string(buzz::STR_RESULT), stanzas_[1]->Attr(buzz::QN_TYPE));
+    delete stanzas_[0];
+    stanzas_.clear();
+  }
+
+  void TestGoodOutgoingInitiate() {
+    cricket::CallOptions options;
+    TestGoodOutgoingInitiate(options);
+  }
+
+  void TestGoodOutgoingInitiate(const cricket::CallOptions& options) {
+    client_->CreateCall();
+    ASSERT_TRUE(call_ != NULL);
+    call_->InitiateSession(buzz::Jid("me@mydomain.com"), options);
+    ASSERT_TRUE(call_->sessions()[0] != NULL);
+    ASSERT_EQ(cricket::Session::STATE_SENTINITIATE,
+              call_->sessions()[0]->state());
+    ASSERT_EQ(1U, stanzas_.size());
+    ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name());
+    ASSERT_TRUE(stanzas_[0]->HasAttr(buzz::QN_TYPE));
+    ASSERT_EQ(std::string(buzz::STR_SET), stanzas_[0]->Attr(buzz::QN_TYPE));
+    buzz::XmlElement* action = ActionFromStanza(stanzas_[0]);
+    ASSERT_TRUE(action != NULL);
+    buzz::XmlElement* content = ContentFromAction(action);
+    ASSERT_TRUE(content != NULL);
+
+    buzz::XmlElement* e = PayloadTypeFromContent(content);
+    ASSERT_TRUE(e != NULL);
+    cricket::AudioCodec codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(103, codec.id);
+    ASSERT_EQ("ISAC", codec.name);
+    ASSERT_EQ(16000, codec.clockrate);
+    ASSERT_EQ(0, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(104, codec.id);
+    ASSERT_EQ("ISAC", codec.name);
+    ASSERT_EQ(32000, codec.clockrate);
+    ASSERT_EQ(0, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(119, codec.id);
+    ASSERT_EQ("ISACLC", codec.name);
+    ASSERT_EQ(16000, codec.clockrate);
+    ASSERT_EQ(40000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(99, codec.id);
+    ASSERT_EQ("speex", codec.name);
+    ASSERT_EQ(16000, codec.clockrate);
+    ASSERT_EQ(22000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(97, codec.id);
+    ASSERT_EQ("IPCMWB", codec.name);
+    ASSERT_EQ(16000, codec.clockrate);
+    ASSERT_EQ(80000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+     e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(9, codec.id);
+    ASSERT_EQ("G722", codec.name);
+    ASSERT_EQ(16000, codec.clockrate);
+    ASSERT_EQ(64000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(102, codec.id);
+    ASSERT_EQ("iLBC", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(13300, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(98, codec.id);
+    ASSERT_EQ("speex", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(11000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(3, codec.id);
+    ASSERT_EQ("GSM", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(13000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(100, codec.id);
+    ASSERT_EQ("EG711U", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(64000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(101, codec.id);
+    ASSERT_EQ("EG711A", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(64000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(0, codec.id);
+    ASSERT_EQ("PCMU", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(64000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(8, codec.id);
+    ASSERT_EQ("PCMA", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(64000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(126, codec.id);
+    ASSERT_EQ("CN", codec.name);
+    ASSERT_EQ(32000, codec.clockrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(105, codec.id);
+    ASSERT_EQ("CN", codec.name);
+    ASSERT_EQ(16000, codec.clockrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(13, codec.id);
+    ASSERT_EQ("CN", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(117, codec.id);
+    ASSERT_EQ("red", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(106, codec.id);
+    ASSERT_EQ("telephone-event", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e == NULL);
+
+    if (expect_outgoing_crypto_) {
+      buzz::XmlElement* encryption = EncryptionFromContent(content);
+      ASSERT_TRUE(encryption != NULL);
+
+      if (client_->secure() == cricket::SEC_REQUIRED) {
+        ASSERT_TRUE(cricket::GetXmlAttr(
+            encryption, cricket::QN_ENCRYPTION_REQUIRED, false));
+      }
+
+      if (content->Name().Namespace() == cricket::NS_GINGLE_AUDIO) {
+        e = encryption->FirstNamed(cricket::QN_GINGLE_AUDIO_CRYPTO_USAGE);
+        ASSERT_TRUE(e != NULL);
+        ASSERT_TRUE(
+            e->NextNamed(cricket::QN_GINGLE_AUDIO_CRYPTO_USAGE) == NULL);
+        ASSERT_TRUE(
+            e->FirstNamed(cricket::QN_GINGLE_VIDEO_CRYPTO_USAGE) == NULL);
+      }
+
+      e = encryption->FirstNamed(cricket::QN_CRYPTO);
+      ASSERT_TRUE(e != NULL);
+      ASSERT_EQ("0", e->Attr(cricket::QN_CRYPTO_TAG));
+      ASSERT_EQ("AES_CM_128_HMAC_SHA1_32", e->Attr(cricket::QN_CRYPTO_SUITE));
+      std::string key_0 = e->Attr(cricket::QN_CRYPTO_KEY_PARAMS);
+      ASSERT_EQ(47U, key_0.length());
+      ASSERT_EQ("inline:", key_0.substr(0, 7));
+
+      e = e->NextNamed(cricket::QN_CRYPTO);
+      ASSERT_TRUE(e != NULL);
+      ASSERT_EQ("1", e->Attr(cricket::QN_CRYPTO_TAG));
+      ASSERT_EQ("AES_CM_128_HMAC_SHA1_80", e->Attr(cricket::QN_CRYPTO_SUITE));
+      std::string key_1 = e->Attr(cricket::QN_CRYPTO_KEY_PARAMS);
+      ASSERT_EQ(47U, key_1.length());
+      ASSERT_EQ("inline:", key_1.substr(0, 7));
+      ASSERT_NE(key_0, key_1);
+
+      encryption = NextFromEncryption(encryption);
+      ASSERT_TRUE(encryption == NULL);
+    }
+
+    if (options.has_video) {
+      CheckVideoBandwidth(options.video_bandwidth,
+                          call_->sessions()[0]->local_description());
+      CheckVideoRtcpMux(expected_video_rtcp_mux_,
+                        call_->sessions()[0]->remote_description());
+      content = parser_->NextContent(content);
+      const buzz::XmlElement* bandwidth =
+          parser_->BandwidthFromContent(content);
+      if (options.video_bandwidth == cricket::kAutoBandwidth) {
+        ASSERT_TRUE(bandwidth == NULL);
+      } else {
+        ASSERT_TRUE(bandwidth != NULL);
+        ASSERT_EQ("AS", bandwidth->Attr(buzz::QName("", "type")));
+        ASSERT_EQ(talk_base::ToString(options.video_bandwidth / 1000),
+                  bandwidth->BodyText());
+      }
+    }
+
+    delete stanzas_[0];
+    stanzas_.clear();
+  }
+
+  void TestHasAllSupportedAudioCodecs(buzz::XmlElement* e) {
+    ASSERT_TRUE(e != NULL);
+
+    e = PayloadTypeFromContent(e);
+    ASSERT_TRUE(e != NULL);
+    cricket::AudioCodec codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(103, codec.id);
+    ASSERT_EQ("ISAC", codec.name);
+    ASSERT_EQ(16000, codec.clockrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(104, codec.id);
+    ASSERT_EQ("ISAC", codec.name);
+    ASSERT_EQ(32000, codec.clockrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(119, codec.id);
+    ASSERT_EQ("ISACLC", codec.name);
+    ASSERT_EQ(16000, codec.clockrate);
+    ASSERT_EQ(40000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(99, codec.id);
+    ASSERT_EQ("speex", codec.name);
+    ASSERT_EQ(16000, codec.clockrate);
+    ASSERT_EQ(22000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(97, codec.id);
+    ASSERT_EQ("IPCMWB", codec.name);
+    ASSERT_EQ(16000, codec.clockrate);
+    ASSERT_EQ(80000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(9, codec.id);
+    ASSERT_EQ("G722", codec.name);
+    ASSERT_EQ(16000, codec.clockrate);
+    ASSERT_EQ(64000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(102, codec.id);
+    ASSERT_EQ("iLBC", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(13300, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(98, codec.id);
+    ASSERT_EQ("speex", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(11000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(3, codec.id);
+    ASSERT_EQ("GSM", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(13000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(100, codec.id);
+    ASSERT_EQ("EG711U", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(64000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(101, codec.id);
+    ASSERT_EQ("EG711A", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(64000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(0, codec.id);
+    ASSERT_EQ("PCMU", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(64000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(8, codec.id);
+    ASSERT_EQ("PCMA", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(64000, codec.bitrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(126, codec.id);
+    ASSERT_EQ("CN", codec.name);
+    ASSERT_EQ(32000, codec.clockrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(105, codec.id);
+    ASSERT_EQ("CN", codec.name);
+    ASSERT_EQ(16000, codec.clockrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(13, codec.id);
+    ASSERT_EQ("CN", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(117, codec.id);
+    ASSERT_EQ("red", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(106, codec.id);
+    ASSERT_EQ("telephone-event", codec.name);
+    ASSERT_EQ(8000, codec.clockrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e == NULL);
+  }
+
+  void TestCodecsOfVideoInitiate(buzz::XmlElement* content) {
+    ASSERT_TRUE(content != NULL);
+    buzz::XmlElement* payload_type = PayloadTypeFromContent(content);
+    ASSERT_TRUE(payload_type != NULL);
+    cricket::AudioCodec codec = AudioCodecFromPayloadType(payload_type);
+    ASSERT_EQ(103, codec.id);
+    ASSERT_EQ("ISAC", codec.name);
+    ASSERT_EQ(16000, codec.clockrate);
+    ASSERT_EQ(1, codec.channels);
+
+    content = parser_->NextContent(content);
+    ASSERT_TRUE(content != NULL);
+    payload_type = PayloadTypeFromContent(content);
+    ASSERT_TRUE(payload_type != NULL);
+    cricket::VideoCodec vcodec =
+        parser_->VideoCodecFromPayloadType(payload_type);
+    ASSERT_EQ(99, vcodec.id);
+    ASSERT_EQ("H264-SVC", vcodec.name);
+    ASSERT_EQ(320, vcodec.width);
+    ASSERT_EQ(200, vcodec.height);
+    ASSERT_EQ(30, vcodec.framerate);
+  }
+
+  void TestHasAudioCodecsFromInitiateSomeUnsupported(buzz::XmlElement* e) {
+    ASSERT_TRUE(e != NULL);
+    e = PayloadTypeFromContent(e);
+    ASSERT_TRUE(e != NULL);
+
+    cricket::AudioCodec codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(103, codec.id);
+    ASSERT_EQ("ISAC", codec.name);
+    ASSERT_EQ(16000, codec.clockrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(100, codec.id);
+    ASSERT_EQ("EG711U", codec.name);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(101, codec.id);
+    ASSERT_EQ("EG711A", codec.name);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(0, codec.id);
+    ASSERT_EQ("PCMU", codec.name);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(13, codec.id);
+    ASSERT_EQ("CN", codec.name);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e == NULL);
+  }
+
+  void TestHasAudioCodecsFromInitiateDynamicAudioCodecs(
+      buzz::XmlElement* e) {
+    ASSERT_TRUE(e != NULL);
+    e = PayloadTypeFromContent(e);
+    ASSERT_TRUE(e != NULL);
+
+    cricket::AudioCodec codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(123, codec.id);
+    ASSERT_EQ(16000, codec.clockrate);
+    ASSERT_EQ(1, codec.channels);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e == NULL);
+  }
+
+  void TestHasDefaultAudioCodecs(buzz::XmlElement* e) {
+    ASSERT_TRUE(e != NULL);
+    e = PayloadTypeFromContent(e);
+    ASSERT_TRUE(e != NULL);
+
+    cricket::AudioCodec codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(103, codec.id);
+    ASSERT_EQ("ISAC", codec.name);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(0, codec.id);
+    ASSERT_EQ("PCMU", codec.name);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e == NULL);
+  }
+
+  void TestHasAudioCodecsFromInitiateStaticAudioCodecs(
+      buzz::XmlElement* e) {
+    ASSERT_TRUE(e != NULL);
+    e = PayloadTypeFromContent(e);
+    ASSERT_TRUE(e != NULL);
+
+    cricket::AudioCodec codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(3, codec.id);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(0, codec.id);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e != NULL);
+    codec = AudioCodecFromPayloadType(e);
+    ASSERT_EQ(8, codec.id);
+
+    e = NextFromPayloadType(e);
+    ASSERT_TRUE(e == NULL);
+  }
+
+  void TestGingleInitiateWithUnsupportedCrypto(
+      const std::string &initiate_string,
+      buzz::XmlElement** element) {
+    *element = NULL;
+
+    buzz::XmlElement* el = buzz::XmlElement::ForStr(initiate_string);
+    client_->session_manager()->OnIncomingMessage(el);
+
+    ASSERT_EQ(cricket::Session::STATE_RECEIVEDINITIATE,
+              call_->sessions()[0]->state());
+    delete stanzas_[0];
+    stanzas_.clear();
+    CheckBadCryptoFromIncomingInitiate(call_->sessions()[0]);
+
+    call_->AcceptSession(call_->sessions()[0], cricket::CallOptions());
+    delete stanzas_[0];
+    stanzas_.clear();
+    CheckNoCryptoForOutgoingAccept(call_->sessions()[0]);
+
+    call_->Terminate();
+    ASSERT_EQ(cricket::Session::STATE_SENTTERMINATE,
+              call_->sessions()[0]->state());
+    delete stanzas_[0];
+    stanzas_.clear();
+  }
+
+  void TestIncomingAcceptWithSsrcs(const std::string& accept_string) {
+    client_->CreateCall();
+    ASSERT_TRUE(call_ != NULL);
+
+    cricket::CallOptions options;
+    options.has_video = true;
+    options.is_muc = true;
+
+    call_->InitiateSession(buzz::Jid("me@mydomain.com"), options);
+    ASSERT_TRUE(call_->sessions()[0] != NULL);
+    ASSERT_EQ(cricket::Session::STATE_SENTINITIATE,
+              call_->sessions()[0]->state());
+    ASSERT_EQ(1U, stanzas_.size());
+    ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name());
+    ASSERT_TRUE(stanzas_[0]->HasAttr(buzz::QN_TYPE));
+    ASSERT_EQ(std::string(buzz::STR_SET), stanzas_[0]->Attr(buzz::QN_TYPE));
+    buzz::XmlElement* action = ActionFromStanza(stanzas_[0]);
+    ASSERT_TRUE(action != NULL);
+    buzz::XmlElement* content = ContentFromAction(action);
+    ASSERT_TRUE(content != NULL);
+    if (initial_protocol_ == cricket::PROTOCOL_JINGLE) {
+      buzz::XmlElement* content_desc =
+          content->FirstNamed(cricket::QN_JINGLE_RTP_CONTENT);
+      ASSERT_TRUE(content_desc != NULL);
+      // TODO: Allow option for intiator to select ssrc.
+      // Right now, for MUC, we set it to a random value.
+      ASSERT_NE("", content_desc->Attr(cricket::QN_SSRC));
+    }
+    delete stanzas_[0];
+    stanzas_.clear();
+
+    // We need to insert the session ID into the session accept message.
+    buzz::XmlElement* el = buzz::XmlElement::ForStr(accept_string);
+    const std::string sid = call_->sessions()[0]->id();
+    if (initial_protocol_ == cricket::PROTOCOL_JINGLE) {
+      buzz::XmlElement* jingle = el->FirstNamed(cricket::QN_JINGLE);
+      jingle->SetAttr(cricket::QN_SID, sid);
+    } else {
+      buzz::XmlElement* session = el->FirstNamed(cricket::QN_GINGLE_SESSION);
+      session->SetAttr(cricket::QN_ID, sid);
+    }
+
+    client_->session_manager()->OnIncomingMessage(el);
+
+    ASSERT_EQ(cricket::Session::STATE_RECEIVEDACCEPT,
+              call_->sessions()[0]->state());
+    ASSERT_EQ(1U, stanzas_.size());
+    ASSERT_TRUE(buzz::QN_IQ == stanzas_[0]->Name());
+    ASSERT_TRUE(stanzas_[0]->HasAttr(buzz::QN_TYPE));
+    ASSERT_EQ(std::string(buzz::STR_RESULT), stanzas_[0]->Attr(buzz::QN_TYPE));
+    delete stanzas_[0];
+    stanzas_.clear();
+
+    CheckAudioSsrcForIncomingAccept(call_->sessions()[0]);
+    CheckVideoSsrcForIncomingAccept(call_->sessions()[0]);
+  }
+
+  size_t ClearStanzas() {
+    size_t size = stanzas_.size();
+    for (size_t i = 0; i < size; i++) {
+      delete stanzas_[i];
+    }
+    stanzas_.clear();
+    return size;
+  }
+
+  void SetJingleSid(buzz::XmlElement* stanza) {
+    buzz::XmlElement* jingle =
+        stanza->FirstNamed(cricket::QN_JINGLE);
+    jingle->SetAttr(cricket::QN_SID, call_->sessions()[0]->id());
+  }
+
+  void TestSourceNotifiesAndViewRequests() {
+    cricket::CallOptions options;
+    options.has_video = true;
+    options.is_muc = true;
+
+    client_->CreateCall();
+    call_->InitiateSession(buzz::Jid("me@mydomain.com"), options);
+    ASSERT_EQ(1U, ClearStanzas());
+    ASSERT_EQ(0U, last_sources_update_.audio().size());
+    ASSERT_EQ(0U, last_sources_update_.video().size());
+
+    talk_base::scoped_ptr<buzz::XmlElement> accept_stanza(
+        buzz::XmlElement::ForStr(kJingleAcceptWithSsrcs));
+    SetJingleSid(accept_stanza.get());
+    client_->session_manager()->OnIncomingMessage(accept_stanza.get());
+    ASSERT_EQ(cricket::Session::STATE_RECEIVEDACCEPT,
+              call_->sessions()[0]->state());
+    ASSERT_EQ(1U, stanzas_.size());
+    ASSERT_EQ(std::string(buzz::STR_RESULT), stanzas_[0]->Attr(buzz::QN_TYPE));
+    ClearStanzas();
+
+    talk_base::scoped_ptr<buzz::XmlElement> notify_stanza(
+        buzz::XmlElement::ForStr(JingleNotifyAdd("video", "Bob", "ABC")));
+    SetJingleSid(notify_stanza.get());
+    client_->session_manager()->OnIncomingMessage(notify_stanza.get());
+    // First one is ignored because of bad syntax.
+    ASSERT_EQ(1U, stanzas_.size());
+    // TODO: Figure out how to make this an ERROR rather than RESULT.
+    ASSERT_EQ(std::string(buzz::STR_RESULT), stanzas_[0]->Attr(buzz::QN_TYPE));
+    ClearStanzas();
+    ASSERT_EQ(0U, last_sources_update_.audio().size());
+    ASSERT_EQ(0U, last_sources_update_.video().size());
+
+    notify_stanza.reset(
+        buzz::XmlElement::ForStr(JingleNotifyAdd("audio", "Bob", "1234")));
+    SetJingleSid(notify_stanza.get());
+    client_->session_manager()->OnIncomingMessage(notify_stanza.get());
+    ASSERT_EQ(1U, last_sources_update_.audio().size());
+    ASSERT_EQ("Bob", last_sources_update_.audio()[0].nick);
+    ASSERT_TRUE(last_sources_update_.audio()[0].ssrc_set);
+    ASSERT_EQ(1234U, last_sources_update_.audio()[0].ssrc);
+
+    notify_stanza.reset(
+        buzz::XmlElement::ForStr(JingleNotifyAdd("audio", "Joe", "2468")));
+    SetJingleSid(notify_stanza.get());
+    client_->session_manager()->OnIncomingMessage(notify_stanza.get());
+    ASSERT_EQ(1U, last_sources_update_.audio().size());
+    ASSERT_EQ("Joe", last_sources_update_.audio()[0].nick);
+    ASSERT_TRUE(last_sources_update_.audio()[0].ssrc_set);
+    ASSERT_EQ(2468U, last_sources_update_.audio()[0].ssrc);
+
+    notify_stanza.reset(
+        buzz::XmlElement::ForStr(JingleNotifyAdd("video", "Bob", "5678")));
+    SetJingleSid(notify_stanza.get());
+    client_->session_manager()->OnIncomingMessage(notify_stanza.get());
+    ASSERT_EQ(1U, last_sources_update_.video().size());
+    ASSERT_EQ("Bob", last_sources_update_.video()[0].nick);
+    ASSERT_TRUE(last_sources_update_.video()[0].ssrc_set);
+    ASSERT_EQ(5678U, last_sources_update_.video()[0].ssrc);
+
+    // We're testing that a "duplicate" is effectively ignored.
+    last_sources_update_.mutable_video()->clear();
+    notify_stanza.reset(
+        buzz::XmlElement::ForStr(JingleNotifyAdd("video", "Bob", "5678")));
+    SetJingleSid(notify_stanza.get());
+    client_->session_manager()->OnIncomingMessage(notify_stanza.get());
+    ASSERT_EQ(0U, last_sources_update_.video().size());
+
+    cricket::FakeVoiceMediaChannel* voice_channel = fme_->GetVoiceChannel(0);
+    ASSERT_TRUE(voice_channel != NULL);
+    ASSERT_TRUE(
+        voice_channel->streams().find(1234U) != voice_channel->streams().end());
+    ASSERT_TRUE(
+        voice_channel->streams().find(2468U) != voice_channel->streams().end());
+    cricket::FakeVideoMediaChannel* video_channel = fme_->GetVideoChannel(0);
+    ASSERT_TRUE(video_channel != NULL);
+    ASSERT_TRUE(
+        video_channel->streams().find(5678U) != video_channel->streams().end());
+    ClearStanzas();
+
+    cricket::ViewRequest viewRequest;
+    cricket::StaticVideoView staticVideoView(5678U, 640, 480, 30);
+    viewRequest.static_video_views.push_back(staticVideoView);
+    talk_base::scoped_ptr<buzz::XmlElement> expected_view_elem(
+        buzz::XmlElement::ForStr(JingleView("5678", "640", "480", "30")));
+    SetJingleSid(expected_view_elem.get());
+
+    ASSERT_TRUE(
+        call_->SendViewRequest(call_->sessions()[0], viewRequest));
+    ASSERT_EQ(1U, stanzas_.size());
+    ASSERT_EQ(expected_view_elem->Str(), stanzas_[0]->Str());
+    ClearStanzas();
+
+    // Implicit removal of audio ssrc.
+    notify_stanza.reset(
+        buzz::XmlElement::ForStr(JingleNotifyImplicitRemove("audio", "Bob")));
+    SetJingleSid(notify_stanza.get());
+    client_->session_manager()->OnIncomingMessage(notify_stanza.get());
+    ASSERT_EQ(1U, last_sources_update_.audio().size());
+    ASSERT_TRUE(last_sources_update_.audio()[0].removed);
+    ASSERT_TRUE(last_sources_update_.audio()[0].ssrc_set);
+    ASSERT_EQ(1234U, last_sources_update_.audio()[0].ssrc);
+
+    // Implicit removal of video ssrc.
+    notify_stanza.reset(
+        buzz::XmlElement::ForStr(JingleNotifyImplicitRemove("video", "Bob")));
+    SetJingleSid(notify_stanza.get());
+    client_->session_manager()->OnIncomingMessage(notify_stanza.get());
+    ASSERT_EQ(1U, last_sources_update_.video().size());
+    ASSERT_TRUE(last_sources_update_.video()[0].removed);
+    ASSERT_TRUE(last_sources_update_.video()[0].ssrc_set);
+    ASSERT_EQ(5678U, last_sources_update_.video()[0].ssrc);
+
+    // Implicit removal of non-existent audio ssrc: should be ignored.
+    last_sources_update_.mutable_audio()->clear();
+    notify_stanza.reset(
+        buzz::XmlElement::ForStr(JingleNotifyImplicitRemove("audio", "Bob")));
+    SetJingleSid(notify_stanza.get());
+    client_->session_manager()->OnIncomingMessage(notify_stanza.get());
+    ASSERT_EQ(0U, last_sources_update_.audio().size());
+
+    // Implicit removal of non-existent video ssrc: should be ignored.
+    last_sources_update_.mutable_video()->clear();
+    notify_stanza.reset(
+        buzz::XmlElement::ForStr(JingleNotifyImplicitRemove("video", "Bob")));
+    SetJingleSid(notify_stanza.get());
+    client_->session_manager()->OnIncomingMessage(notify_stanza.get());
+    ASSERT_EQ(0U, last_sources_update_.video().size());
+
+    voice_channel = fme_->GetVoiceChannel(0);
+    ASSERT_TRUE(voice_channel != NULL);
+    ASSERT_FALSE(
+        voice_channel->streams().find(1234U) != voice_channel->streams().end());
+    ASSERT_TRUE(
+        voice_channel->streams().find(2468U) != voice_channel->streams().end());
+    video_channel = fme_->GetVideoChannel(0);
+    ASSERT_TRUE(video_channel != NULL);
+    ASSERT_FALSE(
+        video_channel->streams().find(5678U) != video_channel->streams().end());
+
+    // Fails because ssrc is now invalid.
+    ASSERT_FALSE(
+        call_->SendViewRequest(call_->sessions()[0], viewRequest));
+
+    ClearStanzas();
+  }
+
+  void MakeSignalingSecure(cricket::SecureMediaPolicy secure) {
+    client_->set_secure(secure);
+  }
+
+  void ExpectCrypto(cricket::SecureMediaPolicy secure) {
+    MakeSignalingSecure(secure);
+    expect_incoming_crypto_ = true;
+#ifdef HAVE_SRTP
+    expect_outgoing_crypto_ = true;
+#endif
+  }
+
+  void ExpectVideoBandwidth(int bandwidth) {
+    expected_video_bandwidth_ = bandwidth;
+  }
+
+  void ExpectVideoRtcpMux(bool rtcp_mux) {
+    expected_video_rtcp_mux_ = rtcp_mux;
+  }
+
+ private:
+  void OnSendStanza(cricket::SessionManager* manager,
+                    const buzz::XmlElement* stanza) {
+    LOG(LS_INFO) << stanza->Str();
+    stanzas_.push_back(new buzz::XmlElement(*stanza));
+  }
+
+  void OnSessionCreate(cricket::Session* session, bool initiate) {
+    session->set_current_protocol(initial_protocol_);
+  }
+
+  void OnCallCreate(cricket::Call *call) {
+    call_ = call;
+    call->SignalMediaSourcesUpdate.connect(
+        this, &MediaSessionClientTest::OnMediaSourcesUpdate);
+  }
+
+  void OnCallDestroy(cricket::Call *call) {
+    call_ = NULL;
+  }
+
+  void OnMediaSourcesUpdate(cricket::Call *call,
+                            cricket::Session *session,
+                            const cricket::MediaSources& sources) {
+    last_sources_update_.CopyFrom(sources);
+  }
+
+  talk_base::NetworkManager* nm_;
+  cricket::PortAllocator* pa_;
+  cricket::SessionManager* sm_;
+  cricket::FakeMediaEngine* fme_;
+  cricket::MediaSessionClient* client_;
+
+  cricket::Call* call_;
+  std::vector<buzz::XmlElement* > stanzas_;
+  MediaSessionTestParser* parser_;
+  cricket::SignalingProtocol initial_protocol_;
+  bool expect_incoming_crypto_;
+  bool expect_outgoing_crypto_;
+  int expected_video_bandwidth_;
+  bool expected_video_rtcp_mux_;
+  cricket::MediaSources last_sources_update_;
+};
+
+MediaSessionClientTest* GingleTest() {
+  return new MediaSessionClientTest(new GingleSessionTestParser(),
+                                    cricket::PROTOCOL_GINGLE);
+}
+
+MediaSessionClientTest* JingleTest() {
+  return new MediaSessionClientTest(new JingleSessionTestParser(),
+                                    cricket::PROTOCOL_JINGLE);
+}
+
+TEST(MediaSessionTest, JingleGoodVideoInitiate) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  test->TestGoodIncomingInitiate(kJingleVideoInitiate, elem.use());
+  test->TestCodecsOfVideoInitiate(elem.get());
+}
+
+TEST(MediaSessionTest, JingleGoodVideoInitiateWithBandwidth) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  test->ExpectVideoBandwidth(42000);
+  test->TestGoodIncomingInitiate(kJingleVideoInitiateWithBandwidth, elem.use());
+}
+
+TEST(MediaSessionTest, JingleGoodVideoInitiateWithRtcpMux) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  test->ExpectVideoRtcpMux(true);
+  test->TestGoodIncomingInitiate(kJingleVideoInitiateWithRtcpMux, elem.use());
+}
+
+TEST(MediaSessionTest, JingleGoodInitiateAllSupportedAudioCodecs) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  test->TestGoodIncomingInitiate(kJingleInitiate, elem.use());
+  test->TestHasAllSupportedAudioCodecs(elem.get());
+}
+
+TEST(MediaSessionTest, JingleGoodInitiateDifferentPreferenceAudioCodecs) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  test->TestGoodIncomingInitiate(kJingleInitiateDifferentPreference,
+      elem.use());
+  test->TestHasAllSupportedAudioCodecs(elem.get());
+}
+
+TEST(MediaSessionTest, JingleGoodInitiateSomeUnsupportedAudioCodecs) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  test->TestGoodIncomingInitiate(kJingleInitiateSomeUnsupported, elem.use());
+  test->TestHasAudioCodecsFromInitiateSomeUnsupported(elem.get());
+}
+
+TEST(MediaSessionTest, JingleGoodInitiateDynamicAudioCodecs) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  test->TestGoodIncomingInitiate(kJingleInitiateDynamicAudioCodecs, elem.use());
+  test->TestHasAudioCodecsFromInitiateDynamicAudioCodecs(elem.get());
+}
+
+TEST(MediaSessionTest, JingleGoodInitiateStaticAudioCodecs) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  test->TestGoodIncomingInitiate(kJingleInitiateStaticAudioCodecs, elem.use());
+  test->TestHasAudioCodecsFromInitiateStaticAudioCodecs(elem.get());
+}
+
+TEST(MediaSessionTest, JingleBadInitiateNoAudioCodecs) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  test->TestBadIncomingInitiate(kJingleInitiateNoAudioCodecs);
+}
+
+TEST(MediaSessionTest, JingleBadInitiateNoSupportedAudioCodecs) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  test->TestBadIncomingInitiate(kJingleInitiateNoSupportedAudioCodecs);
+}
+
+TEST(MediaSessionTest, JingleBadInitiateWrongClockrates) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  test->TestBadIncomingInitiate(kJingleInitiateWrongClockrates);
+}
+
+TEST(MediaSessionTest, JingleBadInitiateWrongChannels) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  test->TestBadIncomingInitiate(kJingleInitiateWrongChannels);
+}
+
+TEST(MediaSessionTest, JingleBadInitiateNoPayloadTypes) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  test->TestBadIncomingInitiate(kJingleInitiateNoPayloadTypes);
+}
+
+TEST(MediaSessionTest, JingleBadInitiateDynamicWithoutNames) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  test->TestBadIncomingInitiate(kJingleInitiateDynamicWithoutNames);
+}
+
+TEST(MediaSessionTest, JingleGoodOutgoingInitiate) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  test->TestGoodOutgoingInitiate();
+}
+
+TEST(MediaSessionTest, JingleGoodOutgoingInitiateWithBandwidth) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  cricket::CallOptions options;
+  options.has_video = true;
+  options.video_bandwidth = 42000;
+  test->TestGoodOutgoingInitiate(options);
+}
+
+TEST(MediaSessionTest, JingleGoodOutgoingInitiateWithRtcpMux) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  cricket::CallOptions options;
+  options.has_video = true;
+  options.rtcp_mux_enabled = true;
+  test->TestGoodOutgoingInitiate(options);
+}
+
+// Crypto related tests.
+
+// Offer has crypto but the session is not secured, just ignore it.
+TEST(MediaSessionTest, JingleInitiateWithCryptoIsIgnoredWhenNotSecured) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  test->TestGoodIncomingInitiate(AddEncryption(kJingleVideoInitiate,
+                                              kJingleCryptoOffer), elem.use());
+}
+
+// Offer has crypto required but the session is not secure, fail.
+TEST(MediaSessionTest, JingleInitiateWithCryptoRequiredWhenNotSecured) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  test->TestBadIncomingInitiate(AddEncryption(kJingleVideoInitiate,
+                                             kJingleRequiredCryptoOffer));
+}
+
+// Offer has no crypto but the session is secure required, fail.
+TEST(MediaSessionTest, JingleInitiateWithNoCryptoFailsWhenSecureRequired) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  test->ExpectCrypto(cricket::SEC_REQUIRED);
+  test->TestBadIncomingInitiate(kJingleInitiate);
+}
+
+// Offer has crypto and session is secure, expect crypto in the answer.
+TEST(MediaSessionTest, JingleInitiateWithCryptoWhenSecureEnabled) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  test->ExpectCrypto(cricket::SEC_ENABLED);
+  test->TestGoodIncomingInitiate(AddEncryption(kJingleVideoInitiate,
+                                              kJingleCryptoOffer), elem.use());
+}
+
+// Offer has crypto and session is secure required, expect crypto in
+// the answer.
+TEST(MediaSessionTest, JingleInitiateWithCryptoWhenSecureRequired) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  test->ExpectCrypto(cricket::SEC_REQUIRED);
+  test->TestGoodIncomingInitiate(AddEncryption(kJingleVideoInitiate,
+                                              kJingleCryptoOffer), elem.use());
+}
+
+// Offer has unsupported crypto and session is secure, no crypto in
+// the answer.
+TEST(MediaSessionTest, JingleInitiateWithUnsupportedCrypto) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  test->MakeSignalingSecure(cricket::SEC_ENABLED);
+  test->TestGoodIncomingInitiate(
+      AddEncryption(kJingleInitiate, kJingleUnsupportedCryptoOffer),
+      elem.use());
+}
+
+// Offer has unsupported REQUIRED crypto and session is not secure, fail.
+TEST(MediaSessionTest, JingleInitiateWithRequiredUnsupportedCrypto) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  test->TestBadIncomingInitiate(
+      AddEncryption(kJingleInitiate, kJingleRequiredUnsupportedCryptoOffer));
+}
+
+// Offer has unsupported REQUIRED crypto and session is secure, fail.
+TEST(MediaSessionTest, JingleInitiateWithRequiredUnsupportedCryptoWhenSecure) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  test->MakeSignalingSecure(cricket::SEC_ENABLED);
+  test->TestBadIncomingInitiate(
+      AddEncryption(kJingleInitiate, kJingleRequiredUnsupportedCryptoOffer));
+}
+
+// Offer has unsupported REQUIRED crypto and session is required secure, fail.
+TEST(MediaSessionTest,
+     JingleInitiateWithRequiredUnsupportedCryptoWhenSecureRequired) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  test->MakeSignalingSecure(cricket::SEC_REQUIRED);
+  test->TestBadIncomingInitiate(
+      AddEncryption(kJingleInitiate, kJingleRequiredUnsupportedCryptoOffer));
+}
+
+
+TEST(MediaSessionTest, JingleGoodOutgoingInitiateWithCrypto) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  test->ExpectCrypto(cricket::SEC_ENABLED);
+  test->TestGoodOutgoingInitiate();
+}
+
+TEST(MediaSessionTest, JingleGoodOutgoingInitiateWithCryptoRequired) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  test->ExpectCrypto(cricket::SEC_REQUIRED);
+  test->TestGoodOutgoingInitiate();
+}
+
+TEST(MediaSessionTest, JingleIncomingAcceptWithSsrcs) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  test->TestIncomingAcceptWithSsrcs(kJingleAcceptWithSsrcs);
+}
+
+TEST(MediaSessionTest, JingleNotifyAndView) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(JingleTest());
+  test->TestSourceNotifiesAndViewRequests();
+}
+
+// Gingle tests
+
+TEST(MediaSessionTest, GingleGoodVideoInitiate) {
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestGoodIncomingInitiate(kGingleVideoInitiate, elem.use());
+  test->TestCodecsOfVideoInitiate(elem.get());
+}
+
+TEST(MediaSessionTest, GingleGoodVideoInitiateWithBandwidth) {
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->ExpectVideoBandwidth(42000);
+  test->TestGoodIncomingInitiate(kGingleVideoInitiateWithBandwidth, elem.use());
+}
+
+TEST(MediaSessionTest, GingleGoodInitiateAllSupportedAudioCodecs) {
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestGoodIncomingInitiate(kGingleInitiate, elem.use());
+  test->TestHasAllSupportedAudioCodecs(elem.get());
+}
+
+TEST(MediaSessionTest, GingleGoodInitiateAllSupportedAudioCodecsWithCrypto) {
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->ExpectCrypto(cricket::SEC_ENABLED);
+  test->TestGoodIncomingInitiate(AddEncryption(kGingleInitiate,
+                                              kGingleCryptoOffer), elem.use());
+  test->TestHasAllSupportedAudioCodecs(elem.get());
+}
+
+TEST(MediaSessionTest, GingleGoodInitiateDifferentPreferenceAudioCodecs) {
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestGoodIncomingInitiate(kGingleInitiateDifferentPreference,
+      elem.use());
+  test->TestHasAllSupportedAudioCodecs(elem.get());
+}
+
+TEST(MediaSessionTest, GingleGoodInitiateSomeUnsupportedAudioCodecs) {
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestGoodIncomingInitiate(kGingleInitiateSomeUnsupported, elem.use());
+  test->TestHasAudioCodecsFromInitiateSomeUnsupported(elem.get());
+}
+
+TEST(MediaSessionTest, GingleGoodInitiateDynamicAudioCodecs) {
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestGoodIncomingInitiate(kGingleInitiateDynamicAudioCodecs, elem.use());
+  test->TestHasAudioCodecsFromInitiateDynamicAudioCodecs(elem.get());
+}
+
+TEST(MediaSessionTest, GingleGoodInitiateStaticAudioCodecs) {
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestGoodIncomingInitiate(kGingleInitiateStaticAudioCodecs, elem.use());
+  test->TestHasAudioCodecsFromInitiateStaticAudioCodecs(elem.get());
+}
+
+TEST(MediaSessionTest, GingleGoodInitiateNoAudioCodecs) {
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestGoodIncomingInitiate(kGingleInitiateNoAudioCodecs, elem.use());
+  test->TestHasDefaultAudioCodecs(elem.get());
+}
+
+TEST(MediaSessionTest, GingleBadInitiateNoSupportedAudioCodecs) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestBadIncomingInitiate(kGingleInitiateNoSupportedAudioCodecs);
+}
+
+TEST(MediaSessionTest, GingleBadInitiateWrongClockrates) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestBadIncomingInitiate(kGingleInitiateWrongClockrates);
+}
+
+TEST(MediaSessionTest, GingleBadInitiateWrongChannels) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestBadIncomingInitiate(kGingleInitiateWrongChannels);
+}
+
+
+TEST(MediaSessionTest, GingleBadInitiateNoPayloadTypes) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestBadIncomingInitiate(kGingleInitiateNoPayloadTypes);
+}
+
+TEST(MediaSessionTest, GingleBadInitiateDynamicWithoutNames) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestBadIncomingInitiate(kGingleInitiateDynamicWithoutNames);
+}
+
+TEST(MediaSessionTest, GingleGoodOutgoingInitiate) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestGoodOutgoingInitiate();
+}
+
+TEST(MediaSessionTest, GingleGoodOutgoingInitiateWithBandwidth) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  cricket::CallOptions options;
+  options.has_video = true;
+  options.video_bandwidth = 42000;
+  test->TestGoodOutgoingInitiate(options);
+}
+
+// Crypto related tests.
+
+// Offer has crypto but the session is not secured, just ignore it.
+TEST(MediaSessionTest, GingleInitiateWithCryptoIsIgnoredWhenNotSecured) {
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestGoodIncomingInitiate(AddEncryption(kGingleInitiate,
+                                              kGingleCryptoOffer), elem.use());
+}
+
+// Offer has crypto required but the session is not secure, fail.
+TEST(MediaSessionTest, GingleInitiateWithCryptoRequiredWhenNotSecured) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestBadIncomingInitiate(AddEncryption(kGingleInitiate,
+                                             kGingleRequiredCryptoOffer));
+}
+
+// Offer has no crypto but the session is secure required, fail.
+TEST(MediaSessionTest, GingleInitiateWithNoCryptoFailsWhenSecureRequired) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->ExpectCrypto(cricket::SEC_REQUIRED);
+  test->TestBadIncomingInitiate(kGingleInitiate);
+}
+
+// Offer has crypto and session is secure, expect crypto in the answer.
+TEST(MediaSessionTest, GingleInitiateWithCryptoWhenSecureEnabled) {
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->ExpectCrypto(cricket::SEC_ENABLED);
+  test->TestGoodIncomingInitiate(AddEncryption(kGingleInitiate,
+                                              kGingleCryptoOffer), elem.use());
+}
+
+// Offer has crypto and session is secure required, expect crypto in
+// the answer.
+TEST(MediaSessionTest, GingleInitiateWithCryptoWhenSecureRequired) {
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->ExpectCrypto(cricket::SEC_REQUIRED);
+  test->TestGoodIncomingInitiate(AddEncryption(kGingleInitiate,
+                                              kGingleCryptoOffer), elem.use());
+}
+
+// Offer has unsupported crypto and session is secure, no crypto in
+// the answer.
+TEST(MediaSessionTest, GingleInitiateWithUnsupportedCrypto) {
+  talk_base::scoped_ptr<buzz::XmlElement> elem;
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->MakeSignalingSecure(cricket::SEC_ENABLED);
+  test->TestGoodIncomingInitiate(
+      AddEncryption(kGingleInitiate, kGingleUnsupportedCryptoOffer),
+      elem.use());
+}
+
+// Offer has unsupported REQUIRED crypto and session is not secure, fail.
+TEST(MediaSessionTest, GingleInitiateWithRequiredUnsupportedCrypto) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestBadIncomingInitiate(
+      AddEncryption(kGingleInitiate, kGingleRequiredUnsupportedCryptoOffer));
+}
+
+// Offer has unsupported REQUIRED crypto and session is secure, fail.
+TEST(MediaSessionTest, GingleInitiateWithRequiredUnsupportedCryptoWhenSecure) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->MakeSignalingSecure(cricket::SEC_ENABLED);
+  test->TestBadIncomingInitiate(
+      AddEncryption(kGingleInitiate, kGingleRequiredUnsupportedCryptoOffer));
+}
+
+// Offer has unsupported REQUIRED crypto and session is required secure, fail.
+TEST(MediaSessionTest,
+     GingleInitiateWithRequiredUnsupportedCryptoWhenSecureRequired) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->MakeSignalingSecure(cricket::SEC_REQUIRED);
+  test->TestBadIncomingInitiate(
+      AddEncryption(kGingleInitiate, kGingleRequiredUnsupportedCryptoOffer));
+}
+
+TEST(MediaSessionTest, GingleGoodOutgoingInitiateWithCrypto) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->ExpectCrypto(cricket::SEC_ENABLED);
+  test->TestGoodOutgoingInitiate();
+}
+
+TEST(MediaSessionTest, GingleGoodOutgoingInitiateWithCryptoRequired) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->ExpectCrypto(cricket::SEC_REQUIRED);
+  test->TestGoodOutgoingInitiate();
+}
+
+TEST(MediaSessionTest, GingleIncomingAcceptWithSsrcs) {
+  talk_base::scoped_ptr<MediaSessionClientTest> test(GingleTest());
+  test->TestIncomingAcceptWithSsrcs(kGingleAcceptWithSsrcs);
+}
diff --git a/talk/session/phone/nullvideorenderer.h b/talk/session/phone/nullvideorenderer.h
new file mode 100644
index 0000000..aa48205
--- /dev/null
+++ b/talk/session/phone/nullvideorenderer.h
@@ -0,0 +1,48 @@
+/*
+ * libjingle
+ * Copyright 2004, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef TALK_SESSION_PHONE_NULLVIDEORENDERER_H_
+#define TALK_SESSION_PHONE_NULLVIDEORENDERER_H_
+
+#include "talk/session/phone/videorenderer.h"
+
+namespace cricket {
+
+// Simple implementation for use in tests.
+class NullVideoRenderer : public VideoRenderer {
+  virtual bool SetSize(int width, int height, int reserved) {
+    return true;
+  }
+  // Called when a new frame is available for display.
+  virtual bool RenderFrame(const VideoFrame *frame) {
+    return true;
+  }
+};
+
+}  // namespace cricket
+
+#endif  // TALK_SESSION_PHONE_NULLVIDEORENDERER_H_
diff --git a/talk/session/phone/rtcpmuxfilter_unittest.cc b/talk/session/phone/rtcpmuxfilter_unittest.cc
new file mode 100644
index 0000000..518330f
--- /dev/null
+++ b/talk/session/phone/rtcpmuxfilter_unittest.cc
@@ -0,0 +1,83 @@
+// libjingle
+// Copyright 2011 Google Inc.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//  1. Redistributions of source code must retain the above copyright notice,
+//     this list of conditions and the following disclaimer.
+//  2. Redistributions in binary form must reproduce the above copyright notice,
+//     this list of conditions and the following disclaimer in the documentation
+//     and/or other materials provided with the distribution.
+//  3. The name of the author may not be used to endorse or promote products
+//     derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "talk/session/phone/rtcpmuxfilter.h"
+
+#include "talk/base/gunit.h"
+#include "talk/session/phone/testutils.h"
+
+TEST(RtcpMuxFilterTest, DemuxRtcpSender) {
+  cricket::RtcpMuxFilter filter;
+  const char data[] = { 0, 73, 0, 0 };
+  const int len = 4;
+
+  // Init state - refuse to demux
+  EXPECT_FALSE(filter.DemuxRtcp(data, len));
+  // After sent offer, demux should be enabled
+  filter.SetOffer(true, cricket::CS_LOCAL);
+  EXPECT_TRUE(filter.DemuxRtcp(data, len));
+  // Remote accepted, demux should be enabled
+  filter.SetAnswer(true, cricket::CS_REMOTE);
+  EXPECT_TRUE(filter.DemuxRtcp(data, len));
+}
+
+TEST(RtcpMuxFilterTest, DemuxRtcpReceiver) {
+  cricket::RtcpMuxFilter filter;
+  const char data[] = { 0, 73, 0, 0 };
+  const int len = 4;
+
+  // Init state - refuse to demux
+  EXPECT_FALSE(filter.DemuxRtcp(data, len));
+  // After received offer, demux should not be enabled
+  filter.SetOffer(true, cricket::CS_REMOTE);
+  EXPECT_FALSE(filter.DemuxRtcp(data, len));
+  // We accept, demux is now enabled.
+  filter.SetAnswer(true, cricket::CS_LOCAL);
+  EXPECT_TRUE(filter.DemuxRtcp(data, len));
+}
+
+TEST(RtcpMuxFilterTest, IsActiveSender) {
+  cricket::RtcpMuxFilter filter;
+  // Init state - not active.
+  EXPECT_FALSE(filter.IsActive());
+  // After sent offer, demux should not be active
+  filter.SetOffer(true, cricket::CS_LOCAL);
+  EXPECT_FALSE(filter.IsActive());
+  // Remote accepted, filter is now active
+  filter.SetAnswer(true, cricket::CS_REMOTE);
+  EXPECT_TRUE(filter.IsActive());
+}
+
+TEST(RtcpMuxFilterTest, IsActiveReceiver) {
+  cricket::RtcpMuxFilter filter;
+  // Init state - not active.
+  EXPECT_FALSE(filter.IsActive());
+  // After received offer, demux should not be active
+  filter.SetOffer(true, cricket::CS_REMOTE);
+  EXPECT_FALSE(filter.IsActive());
+  // We accept, filter is now active
+  filter.SetAnswer(true, cricket::CS_LOCAL);
+  EXPECT_TRUE(filter.IsActive());
+}
diff --git a/talk/session/phone/ssrcmuxfilter_unittest.cc b/talk/session/phone/ssrcmuxfilter_unittest.cc
new file mode 100644
index 0000000..6487fac
--- /dev/null
+++ b/talk/session/phone/ssrcmuxfilter_unittest.cc
@@ -0,0 +1,184 @@
+/*
+ * libjingle
+ * Copyright 2004--2005, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+
+#include "talk/base/gunit.h"
+#include "talk/session/phone/ssrcmuxfilter.h"
+
+static const int kSsrc1 = 0x1111;
+static const int kSsrc2 = 0x2222;
+
+// SSRC = 0x1111
+static const unsigned char kRtpPacketSsrc1[] = {
+    0x80, 0x80, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x11,
+};
+
+// SSRC = 0x2222
+static const unsigned char kRtpPacketSsrc2[] = {
+    0x80, 0x80, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x22,
+};
+
+// SSRC = 0
+static const unsigned char kRtpPacketInvalidSsrc[] = {
+    0x80, 0x80, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+// invalid size
+static const unsigned char kRtpPacketTooSmall[] = {
+    0x80, 0x80, 0x00, 0x00,
+};
+
+// PT = 200 = SR, len = 28, SSRC of sender = 0x0001
+// NTP TS = 0, RTP TS = 0, packet count = 0
+static const unsigned char kRtcpPacketSrSsrc01[] = {
+    0x80, 0xC8, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x01,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00,
+};
+
+// PT = 200 = SR, len = 28, SSRC of sender = 0x2222
+// NTP TS = 0, RTP TS = 0, packet count = 0
+static const unsigned char kRtcpPacketSrSsrc2[] = {
+    0x80, 0xC8, 0x00, 0x1B, 0x00, 0x00, 0x22, 0x22,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00,
+};
+
+// First packet - SR = PT = 200, len = 0, SSRC of sender = 0x1111
+// NTP TS = 0, RTP TS = 0, packet count = 0
+// second packet - SDES = PT =  202, count = 0, SSRC = 0x1111, cname len = 0
+static const unsigned char kRtcpPacketCompoundSrSdesSsrc1[] = {
+    0x80, 0xC8, 0x00, 0x01, 0x00, 0x00, 0x11, 0x11,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00,
+    0x81, 0xCA, 0x00, 0x00, 0x00, 0x00, 0x11, 0x11, 0x01, 0x00,
+};
+
+// SDES = PT =  202, count = 0, SSRC = 0x2222, cname len = 0
+static const unsigned char kRtcpPacketSdesSsrc2[] = {
+    0x81, 0xCA, 0x00, 0x00, 0x00, 0x00, 0x22, 0x22, 0x01, 0x00,
+};
+
+// Packet has only mandatory fixed RTCP header
+static const unsigned char kRtcpPacketFixedHeaderOnly[] = {
+    0x80, 0xC8, 0x00, 0x00,
+};
+
+// Small packet for SSRC demux.
+static const unsigned char kRtcpPacketTooSmall[] = {
+    0x80, 0xC8, 0x00, 0x00, 0x00, 0x00,
+};
+
+TEST(SsrcMuxFilterTest, TestOfferSetup) {
+  cricket::SsrcMuxFilter ssrc_filter;
+  EXPECT_TRUE(ssrc_filter.SetOffer(true, cricket::CS_LOCAL));
+  EXPECT_TRUE(ssrc_filter.SetAnswer(true, cricket::CS_REMOTE));
+  EXPECT_TRUE(ssrc_filter.IsActive());
+}
+
+TEST(SsrcMuxFilterTest, TestAnswerSetup) {
+  cricket::SsrcMuxFilter ssrc_filter;
+  EXPECT_TRUE(ssrc_filter.SetOffer(true, cricket::CS_REMOTE));
+  EXPECT_TRUE(ssrc_filter.SetAnswer(true, cricket::CS_LOCAL));
+  EXPECT_TRUE(ssrc_filter.IsActive());
+}
+
+TEST(SsrcMuxFilterTest, TestIncorrectOrder) {
+  cricket::SsrcMuxFilter ssrc_filter;
+  EXPECT_TRUE(ssrc_filter.SetOffer(true, cricket::CS_REMOTE));
+  EXPECT_FALSE(ssrc_filter.SetAnswer(true, cricket::CS_REMOTE));
+  EXPECT_FALSE(ssrc_filter.IsActive());
+}
+
+TEST(SsrcMuxFilterTest, AddRemoveStreamTest) {
+  cricket::SsrcMuxFilter ssrc_filter;
+  EXPECT_TRUE(ssrc_filter.AddStream(kSsrc1));
+  EXPECT_TRUE(ssrc_filter.AddStream(kSsrc2));
+  EXPECT_TRUE(ssrc_filter.FindStream(kSsrc1));
+  EXPECT_TRUE(ssrc_filter.FindStream(kSsrc2));
+  EXPECT_TRUE(ssrc_filter.RemoveStream(kSsrc1));
+  EXPECT_FALSE(ssrc_filter.FindStream(kSsrc1));
+}
+
+TEST(SsrcMuxFilterTest, RtpPacketTest) {
+  cricket::SsrcMuxFilter ssrc_filter;
+  EXPECT_TRUE(ssrc_filter.AddStream(kSsrc1));
+  EXPECT_TRUE(ssrc_filter.DemuxPacket(
+      reinterpret_cast<const char*>(kRtpPacketSsrc1),
+      sizeof(kRtpPacketSsrc1), false));
+  EXPECT_TRUE(ssrc_filter.AddStream(kSsrc2));
+  EXPECT_TRUE(ssrc_filter.DemuxPacket(
+      reinterpret_cast<const char*>(kRtpPacketSsrc2),
+      sizeof(kRtpPacketSsrc2), false));
+  EXPECT_TRUE(ssrc_filter.RemoveStream(kSsrc2));
+  EXPECT_FALSE(ssrc_filter.DemuxPacket(
+      reinterpret_cast<const char*>(kRtpPacketSsrc2),
+      sizeof(kRtpPacketSsrc2), false));
+  EXPECT_FALSE(ssrc_filter.DemuxPacket(
+      reinterpret_cast<const char*>(kRtpPacketInvalidSsrc),
+      sizeof(kRtpPacketInvalidSsrc), false));
+  EXPECT_FALSE(ssrc_filter.DemuxPacket(
+      reinterpret_cast<const char*>(kRtpPacketTooSmall),
+      sizeof(kRtpPacketTooSmall), false));
+}
+
+TEST(SsrcMuxFilterTest, RtcpPacketTest) {
+  cricket::SsrcMuxFilter ssrc_filter;
+  EXPECT_TRUE(ssrc_filter.AddStream(kSsrc1));
+  EXPECT_TRUE(ssrc_filter.DemuxPacket(
+      reinterpret_cast<const char*>(kRtcpPacketCompoundSrSdesSsrc1),
+      sizeof(kRtcpPacketCompoundSrSdesSsrc1), true));
+  EXPECT_TRUE(ssrc_filter.AddStream(kSsrc2));
+  EXPECT_TRUE(ssrc_filter.DemuxPacket(
+      reinterpret_cast<const char*>(kRtcpPacketSrSsrc2),
+      sizeof(kRtcpPacketSrSsrc2), true));
+  EXPECT_TRUE(ssrc_filter.DemuxPacket(
+      reinterpret_cast<const char*>(kRtcpPacketSdesSsrc2),
+      sizeof(kRtcpPacketSdesSsrc2), true));
+  EXPECT_TRUE(ssrc_filter.RemoveStream(kSsrc2));
+  // RTCP Packets other than SR and RR are demuxed regardless of SSRC.
+  EXPECT_TRUE(ssrc_filter.DemuxPacket(
+      reinterpret_cast<const char*>(kRtcpPacketSdesSsrc2),
+      sizeof(kRtcpPacketSdesSsrc2), true));
+  // RTCP Packets with 'special' SSRC 0x01 are demuxed also
+  EXPECT_TRUE(ssrc_filter.DemuxPacket(
+      reinterpret_cast<const char*>(kRtcpPacketSrSsrc01),
+      sizeof(kRtcpPacketSrSsrc01), true));
+  EXPECT_FALSE(ssrc_filter.DemuxPacket(
+      reinterpret_cast<const char*>(kRtcpPacketSrSsrc2),
+      sizeof(kRtcpPacketSrSsrc2), true));
+  EXPECT_FALSE(ssrc_filter.DemuxPacket(
+      reinterpret_cast<const char*>(kRtcpPacketFixedHeaderOnly),
+      sizeof(kRtcpPacketFixedHeaderOnly), true));
+  EXPECT_FALSE(ssrc_filter.DemuxPacket(
+      reinterpret_cast<const char*>(kRtcpPacketTooSmall),
+      sizeof(kRtcpPacketTooSmall), true));
+}
+
diff --git a/talk/session/phone/videocapturer.cc b/talk/session/phone/videocapturer.cc
index 5182359..ac544eb 100644
--- a/talk/session/phone/videocapturer.cc
+++ b/talk/session/phone/videocapturer.cc
@@ -156,9 +156,31 @@
   }
 
   // Check resolution and fps.
-  int desired_height = desired.height;
   int desired_width = desired.width;
-  int64 delta_w = supported.width - desired.width;
+  int desired_height = desired.height;
+#ifdef OSX
+  // QVGA on OSX is not well supported.  For 16x10, if 320x240 is used, it has
+  // 15x11 pixel aspect ratio on logitech B910/C260 and others.  ComputeCrop
+  // in mediaengine does not crop, so we keep 320x240, which magiccam on Mac
+  // can not display.  Some other viewers can display 320x240, but do not
+  // support pixel aspect ratio and appear distorted.
+  // This code below bumps the preferred resolution to VGA, maintaining aspect
+  // ratio. ie 320x200 -> 640x400.  VGA on logitech and most cameras is 1x1
+  // pixel aspect ratio.  The camera will capture 640x480, ComputeCrop will
+  // crop to 640x400, and the adapter will scale down to QVGA due to JUP view
+  // request.
+  static const int kMinWidth = 640;
+  if (desired_width > 0 && desired_width < kMinWidth) {
+    int new_desired_height = desired_height * kMinWidth / desired_width;
+    LOG(LS_VERBOSE) << " Changed desired from "
+                    << desired_width << "x" << desired_height
+                    << " To "
+                    << kMinWidth << "x" << new_desired_height;
+    desired_width = kMinWidth;
+    desired_height = new_desired_height;
+  }
+#endif
+  int64 delta_w = supported.width - desired_width;
   int64 supported_fps = VideoFormat::IntervalToFps(supported.interval);
   int64 delta_fps = supported_fps -
       VideoFormat::IntervalToFps(desired.interval);
diff --git a/talk/session/phone/videocapturer_unittest.cc b/talk/session/phone/videocapturer_unittest.cc
index e643c47..1b73b15 100644
--- a/talk/session/phone/videocapturer_unittest.cc
+++ b/talk/session/phone/videocapturer_unittest.cc
@@ -74,10 +74,15 @@
 
   desired.width = 360;
   desired.height = 250;
-  // Ask for a litter higher than QVGA. Get QVGA.
+  // Ask for a little higher than QVGA. Get QVGA.  On OSX gets VGA
   EXPECT_TRUE(capturer.GetBestCaptureFormat(desired, &best));
+#ifdef OSX
+  EXPECT_EQ(640, best.width);
+  EXPECT_EQ(480, best.height);
+#else
   EXPECT_EQ(320, best.width);
   EXPECT_EQ(240, best.height);
+#endif
   EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval);
 
   desired.width = 480;
@@ -90,18 +95,28 @@
 
   desired.width = 320;
   desired.height = 240;
-  // Ask for QVGA. Get QVGA.
+  // Ask for QVGA. Get QVGA.  On OSX get VGA
   EXPECT_TRUE(capturer.GetBestCaptureFormat(desired, &best));
+#ifdef OSX
+  EXPECT_EQ(640, best.width);
+  EXPECT_EQ(480, best.height);
+#else
   EXPECT_EQ(320, best.width);
   EXPECT_EQ(240, best.height);
+#endif
   EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval);
 
   desired.width = 160;
   desired.height = 120;
-  // Ask for lower than QVGA. Get QVGA, which is the lowest.
+  // Ask for lower than QVGA. Get QVGA, which is the lowest.  OSX gets VGA
   EXPECT_TRUE(capturer.GetBestCaptureFormat(desired, &best));
+#ifdef OSX
+  EXPECT_EQ(640, best.width);
+  EXPECT_EQ(480, best.height);
+#else
   EXPECT_EQ(320, best.width);
   EXPECT_EQ(240, best.height);
+#endif
   EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval);
 }
 
@@ -132,10 +147,15 @@
 
   desired.width = 360;
   desired.height = 250;
-  // Ask for a litter higher than QVGA. Get QVGA.
+  // Ask for a litter higher than QVGA. Get QVGA.  OSX gets VGA
   EXPECT_TRUE(capturer.GetBestCaptureFormat(desired, &best));
+#ifdef OSX
+  EXPECT_EQ(640, best.width);
+  EXPECT_EQ(480, best.height);
+#else
   EXPECT_EQ(320, best.width);
   EXPECT_EQ(240, best.height);
+#endif
   EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval);
 
   desired.width = 480;
@@ -148,18 +168,28 @@
 
   desired.width = 320;
   desired.height = 240;
-  // Ask for QVGA. Get QVGA.
+  // Ask for QVGA. Get QVGA.  OSX gets VGA
   EXPECT_TRUE(capturer.GetBestCaptureFormat(desired, &best));
+#ifdef OSX
+  EXPECT_EQ(640, best.width);
+  EXPECT_EQ(480, best.height);
+#else
   EXPECT_EQ(320, best.width);
   EXPECT_EQ(240, best.height);
+#endif
   EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval);
 
   desired.width = 160;
   desired.height = 120;
   // Ask for lower than QVGA. Get QVGA, which is the lowest.
   EXPECT_TRUE(capturer.GetBestCaptureFormat(desired, &best));
+#ifdef OSX
+  EXPECT_EQ(640, best.width);
+  EXPECT_EQ(480, best.height);
+#else
   EXPECT_EQ(320, best.width);
   EXPECT_EQ(240, best.height);
+#endif
   EXPECT_EQ(cricket::VideoFormat::FpsToInterval(30), best.interval);
 
   desired.width = 1280;
@@ -180,6 +210,7 @@
 }
 
 // Some cameras support 320x240 and 320x640. Verify we choose 320x240.
+// On OSX we choose VGA
 TEST(VideoCapturerTest, TestStrangeFormats) {
   FakeVideoCapturer capturer;
   std::vector<cricket::VideoFormat> supported_formats;
diff --git a/talk/session/phone/webrtcvideoengine.h b/talk/session/phone/webrtcvideoengine.h
index be8c5f0..e2d1b7c 100644
--- a/talk/session/phone/webrtcvideoengine.h
+++ b/talk/session/phone/webrtcvideoengine.h
@@ -36,7 +36,7 @@
 #include "talk/session/phone/channel.h"
 #include "talk/session/phone/webrtccommon.h"
 #ifdef WEBRTC_RELATIVE_PATH
-#include "video_engine/main/interface/vie_base.h"
+#include "video_engine/include/vie_base.h"
 #else
 #include "third_party/webrtc/files/include/vie_base.h"
 #endif  // WEBRTC_RELATIVE_PATH
diff --git a/talk/session/phone/webrtcvie.h b/talk/session/phone/webrtcvie.h
index cdbfd89..bd2ba71 100644
--- a/talk/session/phone/webrtcvie.h
+++ b/talk/session/phone/webrtcvie.h
@@ -37,14 +37,14 @@
 #include "modules/interface/module_common_types.h"
 #include "modules/video_capture/main/interface/video_capture.h"
 #include "modules/video_render/main/interface/video_render.h"
-#include "video_engine/main/interface/vie_base.h"
-#include "video_engine/main/interface/vie_capture.h"
-#include "video_engine/main/interface/vie_codec.h"
-#include "video_engine/main/interface/vie_errors.h"
-#include "video_engine/main/interface/vie_image_process.h"
-#include "video_engine/main/interface/vie_network.h"
-#include "video_engine/main/interface/vie_render.h"
-#include "video_engine/main/interface/vie_rtp_rtcp.h"
+#include "video_engine/include/vie_base.h"
+#include "video_engine/include/vie_capture.h"
+#include "video_engine/include/vie_codec.h"
+#include "video_engine/include/vie_errors.h"
+#include "video_engine/include/vie_image_process.h"
+#include "video_engine/include/vie_network.h"
+#include "video_engine/include/vie_render.h"
+#include "video_engine/include/vie_rtp_rtcp.h"
 #else
 #include "third_party/webrtc/files/include/common_types.h"
 #include "third_party/webrtc/files/include/module_common_types.h"