Bug 1173599 - a=imageattr support. r=mt
authorByron Campen [:bwc] <docfaraday@gmail.com>
Thu, 30 Jul 2015 14:45:51 -0500
changeset 288157 b953b237ecadc9063066943c7be1335e5d432fcc
parent 288156 a630b1508f1bb6e6b22b75dcf90e98cb1785987d
child 288158 0599674e8edfc659ebe71ec5d03e4bfcc4c62faf
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersimageattr, mt
bugs1173599
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1173599 - a=imageattr support. r=mt
media/webrtc/signaling/src/sdp/SdpAttribute.cpp
media/webrtc/signaling/src/sdp/SdpAttribute.h
media/webrtc/signaling/src/sdp/SipccSdpAttributeList.cpp
media/webrtc/signaling/src/sdp/SipccSdpAttributeList.h
media/webrtc/signaling/src/sdp/sipcc/ccsdp.h
media/webrtc/signaling/src/sdp/sipcc/sdp_attr_access.c
media/webrtc/signaling/src/sdp/sipcc/sdp_main.c
media/webrtc/signaling/test/sdp_unittests.cpp
--- a/media/webrtc/signaling/src/sdp/SdpAttribute.cpp
+++ b/media/webrtc/signaling/src/sdp/SdpAttribute.cpp
@@ -1,16 +1,18 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "signaling/src/sdp/SdpAttribute.h"
 
+#include "plstr.h"
+
 #include <iomanip>
 
 #ifdef CRLF
 #undef CRLF
 #endif
 #define CRLF "\r\n"
 
 namespace mozilla
@@ -142,19 +144,638 @@ void SdpIdentityAttribute::Serialize(std
   for (auto i = mExtensions.begin(); i != mExtensions.end(); i++) {
     os << (i == mExtensions.begin() ? " " : ";") << (*i);
   }
   os << CRLF;
 }
 #endif
 
 void
+SdpImageattrAttributeList::XYRange::Serialize(std::ostream& os) const
+{
+  if (discreteValues.size() == 0) {
+    os << "[" << min << ":";
+    if (step != 1) {
+      os << step << ":";
+    }
+    os << max << "]";
+  } else if (discreteValues.size() == 1) {
+    os << discreteValues.front();
+  } else {
+    os << "[";
+    bool first = true;
+    for (auto value : discreteValues) {
+      if (!first) {
+        os << ",";
+      }
+      first = false;
+      os << value;
+    }
+    os << "]";
+  }
+}
+
+static unsigned char
+PeekChar(std::istream& is, std::string* error)
+{
+  int next = is.peek();
+  if (next == EOF) {
+    *error = "Truncated";
+    return 0;
+  }
+
+  return next;
+}
+
+static bool
+SkipChar(std::istream& is, unsigned char c, std::string* error)
+{
+  if (PeekChar(is, error) != c) {
+    *error = "Expected \'";
+    error->push_back(c);
+    error->push_back('\'');
+    return false;
+  }
+
+  is.get();
+  return true;
+}
+
+template<typename T>
+bool
+GetUnsigned(std::istream& is, T min, T max, T* value, std::string* error)
+{
+  if (PeekChar(is, error) == '-') {
+    *error = "Value is less than 0";
+    return false;
+  }
+
+  is >> std::noskipws >> *value;
+
+  if (is.fail()) {
+    *error = "Malformed";
+    return false;
+  }
+
+  if (*value < min) {
+    *error = "Value too small";
+    return false;
+  }
+
+  if (*value > max) {
+    *error = "Value too large";
+    return false;
+  }
+
+  return true;
+}
+
+static bool
+GetXYValue(std::istream& is, uint32_t* value, std::string* error)
+{
+  return GetUnsigned<uint32_t>(is, 1, 999999, value, error);
+}
+
+bool
+SdpImageattrAttributeList::XYRange::ParseDiscreteValues(std::istream& is,
+                                                        std::string* error)
+{
+  do {
+    uint32_t value;
+    if (!GetXYValue(is, &value, error)) {
+      return false;
+    }
+    discreteValues.push_back(value);
+  } while (SkipChar(is, ',', error));
+
+  return SkipChar(is, ']', error);
+}
+
+bool
+SdpImageattrAttributeList::XYRange::ParseAfterMin(std::istream& is,
+                                                  std::string* error)
+{
+  // We have already parsed "[320:", and now expect another uint
+  uint32_t value;
+  if (!GetXYValue(is, &value, error)) {
+    return false;
+  }
+
+  if (SkipChar(is, ':', error)) {
+    // Range with step eg [320:16:640]
+    step = value;
+    // Now |value| should be the max
+    if (!GetXYValue(is, &value, error)) {
+      return false;
+    }
+  }
+
+  max = value;
+  if (min >= max) {
+    *error = "Min is not smaller than max";
+    return false;
+  }
+
+  return SkipChar(is, ']', error);
+}
+
+bool
+SdpImageattrAttributeList::XYRange::ParseAfterBracket(std::istream& is,
+                                                      std::string* error)
+{
+  // Either a range, or a list of discrete values
+  // [320:640], [320:16:640], or [320,640]
+  uint32_t value;
+  if (!GetXYValue(is, &value, error)) {
+    return false;
+  }
+
+  if (SkipChar(is, ':', error)) {
+    // Range - [640:480] or [640:16:480]
+    min = value;
+    return ParseAfterMin(is, error);
+  }
+
+  if (SkipChar(is, ',', error)) {
+    discreteValues.push_back(value);
+    return ParseDiscreteValues(is, error);
+  }
+
+  *error = "Expected \':\' or \',\'";
+  return false;
+}
+
+bool
+SdpImageattrAttributeList::XYRange::Parse(std::istream& is, std::string* error)
+{
+  if (SkipChar(is, '[', error)) {
+    return ParseAfterBracket(is, error);
+  }
+
+  // Single discrete value
+  uint32_t value;
+  if (!GetXYValue(is, &value, error)) {
+    return false;
+  }
+  discreteValues.push_back(value);
+
+  return true;
+}
+
+static bool
+GetSPValue(std::istream& is, float* value, std::string* error)
+{
+  return GetUnsigned<float>(is, 0.1f, 9.9999f, value, error);
+}
+
+static bool
+GetQValue(std::istream& is, float* value, std::string* error)
+{
+  return GetUnsigned<float>(is, 0.0f, 1.0f, value, error);
+}
+
+bool
+SdpImageattrAttributeList::SRange::ParseDiscreteValues(std::istream& is,
+                                                        std::string* error)
+{
+  do {
+    float value;
+    if (!GetSPValue(is, &value, error)) {
+      return false;
+    }
+    discreteValues.push_back(value);
+  } while (SkipChar(is, ',', error));
+
+  return SkipChar(is, ']', error);
+}
+
+bool
+SdpImageattrAttributeList::SRange::ParseAfterMin(std::istream& is,
+                                                  std::string* error)
+{
+  if (!GetSPValue(is, &max, error)) {
+    return false;
+  }
+
+  if (min >= max) {
+    *error = "Min is not smaller than max";
+    return false;
+  }
+
+  return SkipChar(is, ']', error);
+}
+
+bool
+SdpImageattrAttributeList::SRange::ParseAfterBracket(std::istream& is,
+                                                     std::string* error)
+{
+  // Either a range, or a list of discrete values
+  float value;
+  if (!GetSPValue(is, &value, error)) {
+    return false;
+  }
+
+  if (SkipChar(is, '-', error)) {
+    min = value;
+    return ParseAfterMin(is, error);
+  }
+
+  if (SkipChar(is, ',', error)) {
+    discreteValues.push_back(value);
+    return ParseDiscreteValues(is, error);
+  }
+
+  *error = "Expected either \'-\' or \',\'";
+  return false;
+}
+
+bool
+SdpImageattrAttributeList::SRange::Parse(std::istream& is, std::string* error)
+{
+  if (SkipChar(is, '[', error)) {
+    return ParseAfterBracket(is, error);
+  }
+
+  // Single discrete value
+  float value;
+  if (!GetSPValue(is, &value, error)) {
+    return false;
+  }
+  discreteValues.push_back(value);
+  return true;
+}
+
+bool
+SdpImageattrAttributeList::PRange::Parse(std::istream& is, std::string* error)
+{
+  if (!SkipChar(is, '[', error)) {
+    return false;
+  }
+
+  if (!GetSPValue(is, &min, error)) {
+    return false;
+  }
+
+  if (!SkipChar(is, '-', error)) {
+    return false;
+  }
+
+  if (!GetSPValue(is, &max, error)) {
+    return false;
+  }
+
+  if (min >= max) {
+    *error = "min must be smaller than max";
+    return false;
+  }
+
+  if (!SkipChar(is, ']', error)) {
+    return false;
+  }
+  return true;
+}
+
+void
+SdpImageattrAttributeList::SRange::Serialize(std::ostream& os) const
+{
+  os << std::setprecision(4) << std::fixed;
+  if (discreteValues.size() == 0) {
+    os << "[" << min << "-" << max << "]";
+  } else if (discreteValues.size() == 1) {
+    os << discreteValues.front();
+  } else {
+    os << "[";
+    bool first = true;
+    for (auto value : discreteValues) {
+      if (!first) {
+        os << ",";
+      }
+      first = false;
+      os << value;
+    }
+    os << "]";
+  }
+}
+
+void
+SdpImageattrAttributeList::PRange::Serialize(std::ostream& os) const
+{
+  os << std::setprecision(4) << std::fixed;
+  os << "[" << min << "-" << max << "]";
+}
+
+static std::string ParseKey(std::istream& is, std::string* error)
+{
+  is >> std::ws;
+  std::string key;
+  while (is && PeekChar(is, error) != '=') {
+    key.push_back(std::tolower(is.get()));
+  }
+
+  if (!SkipChar(is, '=', error)) {
+    return "";
+  }
+
+  return key;
+}
+
+static bool SkipBraces(std::istream& is, std::string* error)
+{
+  if (PeekChar(is, error) != '[') {
+    *error = "Expected \'[\'";
+    return false;
+  }
+
+  size_t braceCount = 0;
+  do {
+    switch (PeekChar(is, error)) {
+      case '[':
+        ++braceCount;
+        break;
+      case ']':
+        --braceCount;
+        break;
+      default:
+        break;
+    }
+    is.get();
+  } while (braceCount && is);
+
+  if (!is) {
+    *error = "Expected closing brace";
+    return false;
+  }
+
+  return true;
+}
+
+// Assumptions:
+// 1. If the value contains '[' or ']', they are balanced.
+// 2. The value contains no ',' outside of brackets.
+static bool SkipValue(std::istream& is, std::string* error)
+{
+  while (is) {
+    switch (PeekChar(is, error)) {
+      case ',':
+      case ']':
+        return true;
+      case '[':
+        if (!SkipBraces(is, error)) {
+          return false;
+        }
+        break;
+      default:
+        is.get();
+    }
+  }
+
+  *error = "No closing \']\' on set";
+  return false;
+}
+
+bool
+SdpImageattrAttributeList::Set::Parse(std::istream& is, std::string* error)
+{
+  if (!SkipChar(is, '[', error)) {
+    return false;
+  }
+
+  if (ParseKey(is, error) != "x") {
+    *error = "Expected x=";
+    return false;
+  }
+
+  if (!xRange.Parse(is, error)) {
+    return false;
+  }
+
+  if (!SkipChar(is, ',', error)) {
+    return false;
+  }
+
+  if (ParseKey(is, error) != "y") {
+    *error = "Expected y=";
+    return false;
+  }
+
+  if (!yRange.Parse(is, error)) {
+    return false;
+  }
+
+  qValue = 0.5f; // default
+
+  bool gotSar = false;
+  bool gotPar = false;
+  bool gotQ = false;
+
+  while (SkipChar(is, ',', error)) {
+    std::string key = ParseKey(is, error);
+    if (key.empty()) {
+      *error = "Expected key-value";
+      return false;
+    }
+
+    if (key == "sar") {
+      if (gotSar) {
+        *error = "Extra sar parameter";
+        return false;
+      }
+      gotSar = true;
+      if (!sRange.Parse(is, error)) {
+        return false;
+      }
+    } else if (key == "par") {
+      if (gotPar) {
+        *error = "Extra par parameter";
+        return false;
+      }
+      gotPar = true;
+      if (!pRange.Parse(is, error)) {
+        return false;
+      }
+    } else if (key == "q") {
+      if (gotQ) {
+        *error = "Extra q parameter";
+        return false;
+      }
+      gotQ = true;
+      if (!GetQValue(is, &qValue, error)) {
+        return false;
+      }
+    } else {
+      if (!SkipValue(is, error)) {
+        return false;
+      }
+    }
+  }
+
+  return SkipChar(is, ']', error);
+}
+
+void
+SdpImageattrAttributeList::Set::Serialize(std::ostream& os) const
+{
+  os << "[x=";
+  xRange.Serialize(os);
+  os << ",y=";
+  yRange.Serialize(os);
+  if (sRange.IsSet()) {
+    os << ",sar=";
+    sRange.Serialize(os);
+  }
+  if (pRange.IsSet()) {
+    os << ",par=";
+    pRange.Serialize(os);
+  }
+  if (qValue >= 0) {
+    os << std::setprecision(2) << std::fixed << ",q=" << qValue;
+  }
+  os << "]";
+}
+
+bool
+SdpImageattrAttributeList::Imageattr::ParseSets(std::istream& is,
+                                                std::string* error)
+{
+  // Either "send" or "recv"
+  static const size_t kMaxTypeLength = 4;
+  char type[kMaxTypeLength + 1] = {0};
+  is.read(type, kMaxTypeLength);
+
+  bool* isAll = nullptr;
+  std::vector<Set>* sets = nullptr;
+
+  if (!PL_strncasecmp(type, "send", kMaxTypeLength)) {
+    isAll = &sendAll;
+    sets = &sendSets;
+  } else if (!PL_strncasecmp(type, "recv", kMaxTypeLength)) {
+    isAll = &recvAll;
+    sets = &recvSets;
+  } else {
+    *error = "Unknown type, must be either send or recv";
+    return false;
+  }
+
+  if (*isAll || !sets->empty()) {
+    *error = "Multiple send or recv set lists";
+    return false;
+  }
+
+  is >> std::ws;
+  if (SkipChar(is, '*', error)) {
+    *isAll = true;
+    return true;
+  }
+
+  do {
+    Set set;
+    if (!set.Parse(is, error)) {
+      return false;
+    }
+
+    sets->push_back(set);
+    is >> std::ws;
+  } while (PeekChar(is, error) == '[');
+
+  return true;
+}
+
+bool
+SdpImageattrAttributeList::Imageattr::Parse(std::istream& is,
+                                            std::string* error)
+{
+  if (!SkipChar(is, '*', error)) {
+    uint16_t value;
+    if (!GetUnsigned<uint16_t>(is, 0, UINT16_MAX, &value, error)) {
+      return false;
+    }
+    pt = Some(value);
+  }
+
+  is >> std::ws;
+  if (!ParseSets(is, error)) {
+    return false;
+  }
+
+  // There might be a second one
+  is >> std::ws;
+  if (is.eof()) {
+    return true;
+  }
+
+  if (!ParseSets(is, error)) {
+    return false;
+  }
+
+  is >> std::ws;
+  if (!is.eof()) {
+    *error = "Trailing characters";
+    return false;
+  }
+
+  return true;
+}
+
+void
+SdpImageattrAttributeList::Imageattr::Serialize(std::ostream& os) const
+{
+  if (pt.isSome()) {
+    os << *pt;
+  } else {
+    os << "*";
+  }
+
+  if (sendAll) {
+    os << " send *";
+  } else if (!sendSets.empty()) {
+    os << " send";
+    for (auto& set : sendSets) {
+      os << " ";
+      set.Serialize(os);
+    }
+  }
+
+  if (recvAll) {
+    os << " recv *";
+  } else if (!recvSets.empty()) {
+    os << " recv";
+    for (auto& set : recvSets) {
+      os << " ";
+      set.Serialize(os);
+    }
+  }
+}
+
+void
 SdpImageattrAttributeList::Serialize(std::ostream& os) const
 {
-  MOZ_ASSERT(false, "Serializer not yet implemented");
+  for (auto& imageattr : mImageattrs) {
+    os << "a=" << mType << ":";
+    imageattr.Serialize(os);
+    os << CRLF;
+  }
+}
+
+bool
+SdpImageattrAttributeList::PushEntry(const std::string& raw,
+                                     std::string* error,
+                                     size_t* errorPos)
+{
+  std::istringstream is;
+  is.str(raw);
+
+  Imageattr imageattr;
+  if (!imageattr.Parse(is, error)) {
+    is.clear();
+    *errorPos = is.tellg();
+    return false;
+  }
+
+  mImageattrs.push_back(imageattr);
+  return true;
 }
 
 void
 SdpMsidAttributeList::Serialize(std::ostream& os) const
 {
   for (auto i = mMsids.begin(); i != mMsids.end(); ++i) {
     os << "a=" << mType << ":" << i->identifier;
     if (i->appdata.length()) {
--- a/media/webrtc/signaling/src/sdp/SdpAttribute.h
+++ b/media/webrtc/signaling/src/sdp/SdpAttribute.h
@@ -14,16 +14,17 @@
 #include <sstream>
 #include <cstring>
 #include <iomanip>
 #include <string>
 
 #include "mozilla/UniquePtr.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/Assertions.h"
+#include "mozilla/Maybe.h"
 
 #include "signaling/src/sdp/SdpEnum.h"
 
 namespace mozilla
 {
 
 /**
  * Base class for SDP attributes
@@ -600,17 +601,97 @@ public:
 //  XXX TBD -- We don't use this yet, and it's a project unto itself.
 //
 
 class SdpImageattrAttributeList : public SdpAttribute
 {
 public:
   SdpImageattrAttributeList() : SdpAttribute(kImageattrAttribute) {}
 
+  class XYRange
+  {
+    public:
+      XYRange() : min(0), max(0), step(1) {}
+      void Serialize(std::ostream& os) const;
+      bool Parse(std::istream& is, std::string* error);
+      bool ParseAfterBracket(std::istream& is, std::string* error);
+      bool ParseAfterMin(std::istream& is, std::string* error);
+      bool ParseDiscreteValues(std::istream& is, std::string* error);
+      std::vector<uint32_t> discreteValues;
+      // min/max are used iff discreteValues is empty
+      uint32_t min;
+      uint32_t max;
+      uint32_t step;
+  };
+
+  class SRange
+  {
+    public:
+      SRange() : min(0), max(0) {}
+      void Serialize(std::ostream& os) const;
+      bool Parse(std::istream& is, std::string* error);
+      bool ParseAfterBracket(std::istream& is, std::string* error);
+      bool ParseAfterMin(std::istream& is, std::string* error);
+      bool ParseDiscreteValues(std::istream& is, std::string* error);
+      bool IsSet() const
+      {
+        return !discreteValues.empty() || (min && max);
+      }
+      std::vector<float> discreteValues;
+      // min/max are used iff discreteValues is empty
+      float min;
+      float max;
+  };
+
+  class PRange
+  {
+    public:
+      PRange() : min(0), max(0) {}
+      void Serialize(std::ostream& os) const;
+      bool Parse(std::istream& is, std::string* error);
+      bool IsSet() const
+      {
+        return min && max;
+      }
+      float min;
+      float max;
+  };
+
+  class Set
+  {
+    public:
+      Set() : qValue(-1) {}
+      void Serialize(std::ostream& os) const;
+      bool Parse(std::istream& is, std::string* error);
+      XYRange xRange;
+      XYRange yRange;
+      SRange sRange;
+      PRange pRange;
+      float qValue;
+  };
+
+  class Imageattr
+  {
+    public:
+      Imageattr() : pt(), sendAll(false), recvAll(false) {}
+      void Serialize(std::ostream& os) const;
+      bool Parse(std::istream& is, std::string* error);
+      bool ParseSets(std::istream& is, std::string* error);
+      // If not set, this means all payload types
+      Maybe<uint16_t> pt;
+      bool sendAll;
+      std::vector<Set> sendSets;
+      bool recvAll;
+      std::vector<Set> recvSets;
+  };
+
   virtual void Serialize(std::ostream& os) const override;
+  bool PushEntry(const std::string& raw, std::string* error, size_t* errorPos);
+
+  std::vector<Imageattr> mImageattrs;
 };
 
 ///////////////////////////////////////////////////////////////////////////
 // a=msid, draft-ietf-mmusic-msid
 //-------------------------------------------------------------------------
 //   msid-attr = "msid:" identifier [ SP appdata ]
 //   identifier = 1*64token-char ; see RFC 4566
 //   appdata = 1*64token-char  ; see RFC 4566
--- a/media/webrtc/signaling/src/sdp/SipccSdpAttributeList.cpp
+++ b/media/webrtc/signaling/src/sdp/SipccSdpAttributeList.cpp
@@ -488,16 +488,52 @@ SipccSdpAttributeList::LoadSsrc(sdp_t* s
   }
 
   if (!ssrcs->mSsrcs.empty()) {
     SetAttribute(ssrcs.release());
   }
 }
 
 bool
+SipccSdpAttributeList::LoadImageattr(sdp_t* sdp,
+                                     uint16_t level,
+                                     SdpErrorHolder& errorHolder)
+{
+  UniquePtr<SdpImageattrAttributeList> imageattrs(
+      new SdpImageattrAttributeList);
+
+  for (uint16_t i = 1; i < UINT16_MAX; ++i) {
+    const char* imageattrRaw = sdp_attr_get_simple_string(sdp,
+                                                          SDP_ATTR_IMAGEATTR,
+                                                          level,
+                                                          0,
+                                                          i);
+    if (!imageattrRaw) {
+      break;
+    }
+
+    std::string error;
+    size_t errorPos;
+    if (!imageattrs->PushEntry(imageattrRaw, &error, &errorPos)) {
+      std::ostringstream fullError;
+      fullError << error << " at column " << errorPos;
+      errorHolder.AddParseError(
+        sdp_attr_line_number(sdp, SDP_ATTR_IMAGEATTR, level, 0, i),
+        fullError.str());
+      return false;
+    }
+  }
+
+  if (!imageattrs->mImageattrs.empty()) {
+    SetAttribute(imageattrs.release());
+  }
+  return true;
+}
+
+bool
 SipccSdpAttributeList::LoadGroups(sdp_t* sdp, uint16_t level,
                                   SdpErrorHolder& errorHolder)
 {
   uint16_t attrCount = 0;
   if (sdp_attr_num_instances(sdp, level, 0, SDP_ATTR_GROUP, &attrCount) !=
       SDP_SUCCESS) {
     MOZ_ASSERT(false, "Could not get count of group attributes");
     errorHolder.AddParseError(0, "Could not get count of group attributes");
@@ -920,16 +956,17 @@ SipccSdpAttributeList::Load(sdp_t* sdp, 
       }
     }
     LoadCandidate(sdp, level);
     LoadFmtp(sdp, level);
     LoadMsids(sdp, level, errorHolder);
     LoadRtcpFb(sdp, level, errorHolder);
     LoadRtcp(sdp, level, errorHolder);
     LoadSsrc(sdp, level);
+    LoadImageattr(sdp, level, errorHolder);
   }
 
   LoadIceAttributes(sdp, level);
   if (!LoadFingerprint(sdp, level, errorHolder)) {
     return false;
   }
   LoadSetup(sdp, level);
   LoadExtmap(sdp, level, errorHolder);
@@ -1077,17 +1114,21 @@ SipccSdpAttributeList::GetIdentity() con
   }
   const SdpAttribute* attr = GetAttribute(SdpAttribute::kIdentityAttribute);
   return static_cast<const SdpStringAttribute*>(attr)->mValue;
 }
 
 const SdpImageattrAttributeList&
 SipccSdpAttributeList::GetImageattr() const
 {
-  MOZ_CRASH("Not yet implemented.");
+  if (!HasAttribute(SdpAttribute::kImageattrAttribute)) {
+    MOZ_CRASH();
+  }
+  const SdpAttribute* attr = GetAttribute(SdpAttribute::kImageattrAttribute);
+  return *static_cast<const SdpImageattrAttributeList*>(attr);
 }
 
 const std::string&
 SipccSdpAttributeList::GetLabel() const
 {
   if (!HasAttribute(SdpAttribute::kLabelAttribute)) {
     return kEmptyString;
   }
--- a/media/webrtc/signaling/src/sdp/SipccSdpAttributeList.h
+++ b/media/webrtc/signaling/src/sdp/SipccSdpAttributeList.h
@@ -100,16 +100,17 @@ private:
   void LoadDirection(sdp_t* sdp, uint16_t level, SdpErrorHolder& errorHolder);
   bool LoadRtpmap(sdp_t* sdp, uint16_t level, SdpErrorHolder& errorHolder);
   bool LoadSctpmap(sdp_t* sdp, uint16_t level, SdpErrorHolder& errorHolder);
   void LoadIceAttributes(sdp_t* sdp, uint16_t level);
   bool LoadFingerprint(sdp_t* sdp, uint16_t level, SdpErrorHolder& errorHolder);
   void LoadCandidate(sdp_t* sdp, uint16_t level);
   void LoadSetup(sdp_t* sdp, uint16_t level);
   void LoadSsrc(sdp_t* sdp, uint16_t level);
+  bool LoadImageattr(sdp_t* sdp, uint16_t level, SdpErrorHolder& errorHolder);
   bool LoadGroups(sdp_t* sdp, uint16_t level, SdpErrorHolder& errorHolder);
   bool LoadMsidSemantics(sdp_t* sdp,
                          uint16_t level,
                          SdpErrorHolder& errorHolder);
   void LoadFmtp(sdp_t* sdp, uint16_t level);
   void LoadMsids(sdp_t* sdp, uint16_t level, SdpErrorHolder& errorHolder);
   void LoadExtmap(sdp_t* sdp, uint16_t level, SdpErrorHolder& errorHolder);
   void LoadRtcpFb(sdp_t* sdp, uint16_t level, SdpErrorHolder& errorHolder);
--- a/media/webrtc/signaling/src/sdp/sipcc/ccsdp.h
+++ b/media/webrtc/signaling/src/sdp/sipcc/ccsdp.h
@@ -179,16 +179,17 @@ typedef enum {
     SDP_ATTR_EXTMAP,  /* RFC 5285 */
     SDP_ATTR_IDENTITY,
     SDP_ATTR_MSID,
     SDP_ATTR_MSID_SEMANTIC,
     SDP_ATTR_BUNDLE_ONLY,
     SDP_ATTR_END_OF_CANDIDATES,
     SDP_ATTR_ICE_OPTIONS,
     SDP_ATTR_SSRC,
+    SDP_ATTR_IMAGEATTR,
     SDP_MAX_ATTR_TYPES,
     SDP_ATTR_INVALID
 } sdp_attr_e;
 
 typedef enum {
     SDP_SETUP_NOT_FOUND = -1,
     SDP_SETUP_ACTIVE = 0,
     SDP_SETUP_PASSIVE,
--- a/media/webrtc/signaling/src/sdp/sipcc/sdp_attr_access.c
+++ b/media/webrtc/signaling/src/sdp/sipcc/sdp_attr_access.c
@@ -677,17 +677,18 @@ static boolean sdp_attr_is_simple_string
         (attr_type != SDP_ATTR_DIALING) &&
         (attr_type != SDP_ATTR_FRAMING) &&
         (attr_type != SDP_ATTR_MID) &&
         (attr_type != SDP_ATTR_X_SIDIN) &&
         (attr_type != SDP_ATTR_X_SIDOUT)&&
         (attr_type != SDP_ATTR_X_CONFID) &&
         (attr_type != SDP_ATTR_LABEL) &&
         (attr_type != SDP_ATTR_IDENTITY) &&
-        (attr_type != SDP_ATTR_ICE_OPTIONS)) {
+        (attr_type != SDP_ATTR_ICE_OPTIONS) &&
+        (attr_type != SDP_ATTR_IMAGEATTR)) {
       return FALSE;
     }
     return TRUE;
 }
 
 /* Function:    sdp_attr_get_simple_string
  * Description: Returns a pointer to a string attribute parameter.  This
  *              routine can only be called for attributes that have just
--- a/media/webrtc/signaling/src/sdp/sipcc/sdp_main.c
+++ b/media/webrtc/signaling/src/sdp/sipcc/sdp_main.c
@@ -189,16 +189,18 @@ const sdp_attrarray_t sdp_attr[SDP_MAX_A
     {"bundle-only", sizeof("bundle-only"),
       sdp_parse_attr_simple_flag, sdp_build_attr_simple_flag},
     {"end-of-candidates", sizeof("end-of-candidates"),
       sdp_parse_attr_simple_flag, sdp_build_attr_simple_flag},
     {"ice-options", sizeof("ice-options"),
       sdp_parse_attr_complete_line, sdp_build_attr_simple_string},
     {"ssrc", sizeof("ssrc"),
       sdp_parse_attr_ssrc, sdp_build_attr_ssrc},
+    {"imageattr", sizeof("imageattr"),
+      sdp_parse_attr_complete_line, sdp_build_attr_simple_string},
 };
 
 /* Note: These *must* be in the same order as the enum types. */
 const sdp_namearray_t sdp_media[SDP_MAX_MEDIA_TYPES] =
 {
     {"audio",        sizeof("audio")},
     {"video",        sizeof("video")},
     {"application",  sizeof("application")},
--- a/media/webrtc/signaling/test/sdp_unittests.cpp
+++ b/media/webrtc/signaling/test/sdp_unittests.cpp
@@ -1188,16 +1188,18 @@ const std::string kBasicAudioVideoOffer 
 "a=candidate:6 1 UDP 16515071 162.222.183.171 64800 typ relay raddr 162.222.183.171 rport 64800" CRLF
 "a=candidate:2 1 UDP 1694236671 24.6.134.204 59530 typ srflx raddr 10.0.0.36 rport 59530" CRLF
 "a=candidate:3 1 UDP 100401151 162.222.183.171 62935 typ relay raddr 162.222.183.171 rport 62935" CRLF
 "a=candidate:3 2 UDP 100401150 162.222.183.171 61026 typ relay raddr 162.222.183.171 rport 61026" CRLF
 "a=rtcp:61026" CRLF
 "a=end-of-candidates" CRLF
 "a=ssrc:1111 foo" CRLF
 "a=ssrc:1111 foo:bar" CRLF
+"a=imageattr:120 send * recv *" CRLF
+"a=imageattr:121 send [x=640,y=480] recv [x=640,y=480]" CRLF
 "m=audio 9 RTP/SAVPF 0" CRLF
 "a=mid:third" CRLF
 "a=rtpmap:0 PCMU/8000" CRLF
 "a=ice-lite" CRLF
 "a=ice-options:foo bar" CRLF
 "a=msid:noappdata" CRLF
 "a=bundle-only" CRLF;
 
@@ -1997,16 +1999,60 @@ TEST_P(NewSdpTest, CheckRtcp) {
   ASSERT_EQ(sdp::kIPv4, rtcpAttr_0.mAddrType);
   ASSERT_EQ("162.222.183.171", rtcpAttr_0.mAddress);
 
   auto& rtcpAttr_1 = mSdp->GetMediaSection(1).GetAttributeList().GetRtcp();
   ASSERT_EQ(61026U, rtcpAttr_1.mPort);
   ASSERT_EQ("", rtcpAttr_1.mAddress);
 }
 
+TEST_P(NewSdpTest, CheckImageattr)
+{
+  ParseSdp(kBasicAudioVideoOffer);
+  ASSERT_TRUE(!!mSdp);
+  ASSERT_EQ(3U, mSdp->GetMediaSectionCount()) << "Wrong number of media sections";
+
+  ASSERT_FALSE(mSdp->GetAttributeList().HasAttribute(
+        SdpAttribute::kImageattrAttribute));
+  ASSERT_FALSE(mSdp->GetMediaSection(0).GetAttributeList().HasAttribute(
+        SdpAttribute::kImageattrAttribute));
+  ASSERT_TRUE(mSdp->GetMediaSection(1).GetAttributeList().HasAttribute(
+        SdpAttribute::kImageattrAttribute));
+  ASSERT_FALSE(mSdp->GetMediaSection(2).GetAttributeList().HasAttribute(
+        SdpAttribute::kImageattrAttribute));
+
+  const SdpImageattrAttributeList& imageattrs =
+    mSdp->GetMediaSection(1).GetAttributeList().GetImageattr();
+
+  ASSERT_EQ(2U, imageattrs.mImageattrs.size());
+  const SdpImageattrAttributeList::Imageattr& imageattr_0(
+      imageattrs.mImageattrs[0]);
+  ASSERT_TRUE(imageattr_0.pt.isSome());
+  ASSERT_EQ(120U, *imageattr_0.pt);
+  ASSERT_TRUE(imageattr_0.sendAll);
+  ASSERT_TRUE(imageattr_0.recvAll);
+
+  const SdpImageattrAttributeList::Imageattr& imageattr_1(
+      imageattrs.mImageattrs[1]);
+  ASSERT_TRUE(imageattr_1.pt.isSome());
+  ASSERT_EQ(121U, *imageattr_1.pt);
+  ASSERT_FALSE(imageattr_1.sendAll);
+  ASSERT_FALSE(imageattr_1.recvAll);
+  ASSERT_EQ(1U, imageattr_1.sendSets.size());
+  ASSERT_EQ(1U, imageattr_1.sendSets[0].xRange.discreteValues.size());
+  ASSERT_EQ(640U, imageattr_1.sendSets[0].xRange.discreteValues.front());
+  ASSERT_EQ(1U, imageattr_1.sendSets[0].yRange.discreteValues.size());
+  ASSERT_EQ(480U, imageattr_1.sendSets[0].yRange.discreteValues.front());
+  ASSERT_EQ(1U, imageattr_1.recvSets.size());
+  ASSERT_EQ(1U, imageattr_1.recvSets[0].xRange.discreteValues.size());
+  ASSERT_EQ(640U, imageattr_1.recvSets[0].xRange.discreteValues.front());
+  ASSERT_EQ(1U, imageattr_1.recvSets[0].yRange.discreteValues.size());
+  ASSERT_EQ(480U, imageattr_1.recvSets[0].yRange.discreteValues.front());
+}
+
 TEST_P(NewSdpTest, CheckSctpmap) {
   ParseSdp(kBasicAudioVideoDataOffer);
   ASSERT_TRUE(!!mSdp) << "Parse failed: " << GetParseErrors();
   ASSERT_EQ(3U, mSdp->GetMediaSectionCount())
     << "Wrong number of media sections";
 
   const SdpMediaSection& appsec = mSdp->GetMediaSection(2);
   ASSERT_TRUE(
@@ -2479,16 +2525,38 @@ TEST_P(NewSdpTest, CheckSsrcGroupInSessi
   if (mSdp) {
     ASSERT_FALSE(mSdp->GetMediaSection(0).GetAttributeList().HasAttribute(
           SdpAttribute::kSsrcGroupAttribute));
     ASSERT_FALSE(mSdp->GetAttributeList().HasAttribute(
           SdpAttribute::kSsrcGroupAttribute));
   }
 }
 
+const std::string kMalformedImageattr =
+"v=0" CRLF
+"o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF
+"s=SIP Call" CRLF
+"c=IN IP4 224.0.0.1/100/12" CRLF
+"t=0 0" CRLF
+"m=video 9 RTP/SAVPF 120" CRLF
+"c=IN IP4 0.0.0.0" CRLF
+"a=rtpmap:120 VP8/90000" CRLF
+"a=imageattr:flob" CRLF;
+
+TEST_P(NewSdpTest, CheckMalformedImageattr)
+{
+  if (GetParam()) {
+    // Don't do a parse/serialize before running this test
+    return;
+  }
+
+  ParseSdp(kMalformedImageattr, false);
+  ASSERT_NE("", GetParseErrors());
+}
+
 const std::string kNoAttributes =
 "v=0" CRLF
 "o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0" CRLF
 "s=SIP Call" CRLF
 "c=IN IP4 224.0.0.1/100/12" CRLF
 "t=0 0" CRLF
 "m=video 9 RTP/SAVPF 120" CRLF
 "c=IN IP4 0.0.0.0" CRLF
@@ -2542,16 +2610,825 @@ TEST(NewSdpTestNoFixture, CheckAttribute
     if (type != SdpAttribute::kDirectionAttribute) {
       std::ostringstream os;
       os << type;
       ASSERT_NE("", os.str());
     }
   }
 }
 
+static SdpImageattrAttributeList::XYRange
+ParseXYRange(const std::string& input)
+{
+  std::istringstream is;
+  is.str(input + ",");
+  std::string error;
+  SdpImageattrAttributeList::XYRange range;
+  EXPECT_TRUE(range.Parse(is, &error)) << error;
+  EXPECT_EQ(',', is.get());
+  EXPECT_EQ(EOF, is.get());
+  return range;
+}
+
+TEST(NewSdpTestNoFixture, CheckImageattrXYRangeParseValid)
+{
+  {
+    SdpImageattrAttributeList::XYRange range(ParseXYRange("640"));
+    ASSERT_EQ(1U, range.discreteValues.size());
+    ASSERT_EQ(640U, range.discreteValues[0]);
+  }
+
+  {
+    SdpImageattrAttributeList::XYRange range(ParseXYRange("[320,640]"));
+    ASSERT_EQ(2U, range.discreteValues.size());
+    ASSERT_EQ(320U, range.discreteValues[0]);
+    ASSERT_EQ(640U, range.discreteValues[1]);
+  }
+
+  {
+    SdpImageattrAttributeList::XYRange range(ParseXYRange("[320,640,1024]"));
+    ASSERT_EQ(3U, range.discreteValues.size());
+    ASSERT_EQ(320U, range.discreteValues[0]);
+    ASSERT_EQ(640U, range.discreteValues[1]);
+    ASSERT_EQ(1024U, range.discreteValues[2]);
+  }
+
+  {
+    SdpImageattrAttributeList::XYRange range(ParseXYRange("[320:640]"));
+    ASSERT_EQ(0U, range.discreteValues.size());
+    ASSERT_EQ(320U, range.min);
+    ASSERT_EQ(1U, range.step);
+    ASSERT_EQ(640U, range.max);
+  }
+
+  {
+    SdpImageattrAttributeList::XYRange range(ParseXYRange("[320:16:640]"));
+    ASSERT_EQ(0U, range.discreteValues.size());
+    ASSERT_EQ(320U, range.min);
+    ASSERT_EQ(16U, range.step);
+    ASSERT_EQ(640U, range.max);
+  }
+}
+
+static void
+ParseInvalidXYRange(const std::string& input, size_t last)
+{
+  std::istringstream is;
+  is.str(input);
+  SdpImageattrAttributeList::XYRange range;
+  std::string error;
+  ASSERT_FALSE(range.Parse(is, &error))
+    << "\'" << input << "\' should not have parsed successfully";
+  is.clear();
+  ASSERT_EQ(last, static_cast<size_t>(is.tellg()))
+    << "Parse failed at unexpected location:" << std::endl
+    << input << std::endl
+    << std::string(is.tellg(), ' ') << "^" << std::endl;
+  // For a human to eyeball to make sure the error strings look sane
+  std::cout << "\"" << input << "\" - " << error << std::endl; \
+}
+
+TEST(NewSdpTestNoFixture, CheckImageattrXYRangeParseInvalid)
+{
+  ParseInvalidXYRange("[-1", 1);
+  ParseInvalidXYRange("[-", 1);
+  ParseInvalidXYRange("[-x", 1);
+  ParseInvalidXYRange("[640:-1", 5);
+  ParseInvalidXYRange("[640:16:-1", 8);
+  ParseInvalidXYRange("[640,-1", 5);
+  ParseInvalidXYRange("[640,-]", 5);
+  ParseInvalidXYRange("-x", 0);
+  ParseInvalidXYRange("-1", 0);
+  ParseInvalidXYRange("", 0);
+  ParseInvalidXYRange("[", 1);
+  ParseInvalidXYRange("[x", 1);
+  ParseInvalidXYRange("[", 1);
+  ParseInvalidXYRange("[ 640", 1);
+  // It looks like the overflow detection only happens once the whole number
+  // is scanned...
+  ParseInvalidXYRange("[99999999999999999:", 18);
+  ParseInvalidXYRange("[640", 4);
+  ParseInvalidXYRange("[640:", 5);
+  ParseInvalidXYRange("[640:x", 5);
+  ParseInvalidXYRange("[640:16", 7);
+  ParseInvalidXYRange("[640:16:", 8);
+  ParseInvalidXYRange("[640:16:x", 8);
+  ParseInvalidXYRange("[640:16:320]", 11);
+  ParseInvalidXYRange("[640:16:320", 11);
+  ParseInvalidXYRange("[640:16:320x", 11);
+  ParseInvalidXYRange("[640:1024", 9);
+  ParseInvalidXYRange("[640:320]", 8);
+  ParseInvalidXYRange("[640:1024x", 9);
+  ParseInvalidXYRange("[640,", 5);
+  ParseInvalidXYRange("[640,x", 5);
+  ParseInvalidXYRange("[640]", 4);
+  ParseInvalidXYRange("[640x", 4);
+  ParseInvalidXYRange("[640,]", 5);
+  ParseInvalidXYRange(" ", 0);
+  ParseInvalidXYRange("x", 0);
+}
+
+static SdpImageattrAttributeList::SRange
+ParseSRange(const std::string& input)
+{
+  std::istringstream is;
+  is.str(input + ",");
+  std::string error;
+  SdpImageattrAttributeList::SRange range;
+  EXPECT_TRUE(range.Parse(is, &error)) << error;
+  EXPECT_EQ(',', is.get());
+  EXPECT_EQ(EOF, is.get());
+  return range;
+}
+
+TEST(NewSdpTestNoFixture, CheckImageattrSRangeParseValid)
+{
+  {
+    SdpImageattrAttributeList::SRange range(ParseSRange("0.1"));
+    ASSERT_EQ(1U, range.discreteValues.size());
+    ASSERT_FLOAT_EQ(0.1f, range.discreteValues[0]);
+  }
+
+  {
+    SdpImageattrAttributeList::SRange range(ParseSRange("[0.1,0.2]"));
+    ASSERT_EQ(2U, range.discreteValues.size());
+    ASSERT_FLOAT_EQ(0.1f, range.discreteValues[0]);
+    ASSERT_FLOAT_EQ(0.2f, range.discreteValues[1]);
+  }
+
+  {
+    SdpImageattrAttributeList::SRange range(ParseSRange("[0.1,0.2,0.3]"));
+    ASSERT_EQ(3U, range.discreteValues.size());
+    ASSERT_FLOAT_EQ(0.1f, range.discreteValues[0]);
+    ASSERT_FLOAT_EQ(0.2f, range.discreteValues[1]);
+    ASSERT_FLOAT_EQ(0.3f, range.discreteValues[2]);
+  }
+
+  {
+    SdpImageattrAttributeList::SRange range(ParseSRange("[0.1-0.2]"));
+    ASSERT_EQ(0U, range.discreteValues.size());
+    ASSERT_FLOAT_EQ(0.1f, range.min);
+    ASSERT_FLOAT_EQ(0.2f, range.max);
+  }
+}
+
+static void
+ParseInvalidSRange(const std::string& input, size_t last)
+{
+  std::istringstream is;
+  is.str(input);
+  SdpImageattrAttributeList::SRange range;
+  std::string error;
+  ASSERT_FALSE(range.Parse(is, &error))
+    << "\'" << input << "\' should not have parsed successfully";
+  is.clear();
+  ASSERT_EQ(last, static_cast<size_t>(is.tellg()))
+    << "Parse failed at unexpected location:" << std::endl
+    << input << std::endl
+    << std::string(is.tellg(), ' ') << "^" << std::endl;
+  // For a human to eyeball to make sure the error strings look sane
+  std::cout << "\"" << input << "\" - " << error << std::endl; \
+}
+
+TEST(NewSdpTestNoFixture, CheckImageattrSRangeParseInvalid)
+{
+  ParseInvalidSRange("", 0);
+  ParseInvalidSRange("[", 1);
+  ParseInvalidSRange("[x", 1);
+  ParseInvalidSRange("[-1", 1);
+  ParseInvalidSRange("[", 1);
+  ParseInvalidSRange("[-", 1);
+  ParseInvalidSRange("[x", 1);
+  ParseInvalidSRange("[ 0.2", 1);
+  ParseInvalidSRange("[10.1-", 5);
+  ParseInvalidSRange("[0.08-", 5);
+  ParseInvalidSRange("[0.2", 4);
+  ParseInvalidSRange("[0.2-", 5);
+  ParseInvalidSRange("[0.2-x", 5);
+  ParseInvalidSRange("[0.2--1", 5);
+  ParseInvalidSRange("[0.2-0.3", 8);
+  ParseInvalidSRange("[0.2-0.1]", 8);
+  ParseInvalidSRange("[0.2-0.3x", 8);
+  ParseInvalidSRange("[0.2,", 5);
+  ParseInvalidSRange("[0.2,x", 5);
+  ParseInvalidSRange("[0.2,-1", 5);
+  ParseInvalidSRange("[0.2]", 4);
+  ParseInvalidSRange("[0.2x", 4);
+  ParseInvalidSRange("[0.2,]", 5);
+  ParseInvalidSRange("[0.2,-]", 5);
+  ParseInvalidSRange(" ", 0);
+  ParseInvalidSRange("x", 0);
+  ParseInvalidSRange("-x", 0);
+  ParseInvalidSRange("-1", 0);
+}
+
+static SdpImageattrAttributeList::PRange
+ParsePRange(const std::string& input)
+{
+  std::istringstream is;
+  is.str(input + ",");
+  std::string error;
+  SdpImageattrAttributeList::PRange range;
+  EXPECT_TRUE(range.Parse(is, &error)) << error;
+  EXPECT_EQ(',', is.get());
+  EXPECT_EQ(EOF, is.get());
+  return range;
+}
+
+TEST(NewSdpTestNoFixture, CheckImageattrPRangeParseValid)
+{
+  SdpImageattrAttributeList::PRange range(ParsePRange("[0.1000-9.9999]"));
+  ASSERT_FLOAT_EQ(0.1f, range.min);
+  ASSERT_FLOAT_EQ(9.9999f, range.max);
+}
+
+static void
+ParseInvalidPRange(const std::string& input, size_t last)
+{
+  std::istringstream is;
+  is.str(input);
+  SdpImageattrAttributeList::PRange range;
+  std::string error;
+  ASSERT_FALSE(range.Parse(is, &error))
+    << "\'" << input << "\' should not have parsed successfully";
+  is.clear();
+  ASSERT_EQ(last, static_cast<size_t>(is.tellg()))
+    << "Parse failed at unexpected location:" << std::endl
+    << input << std::endl
+    << std::string(is.tellg(), ' ') << "^" << std::endl;
+  // For a human to eyeball to make sure the error strings look sane
+  std::cout << "\"" << input << "\" - " << error << std::endl; \
+}
+
+TEST(NewSdpTestNoFixture, CheckImageattrPRangeParseInvalid)
+{
+  ParseInvalidPRange("", 0);
+  ParseInvalidPRange("[", 1);
+  ParseInvalidPRange("[x", 1);
+  ParseInvalidPRange("[-1", 1);
+  ParseInvalidPRange("[", 1);
+  ParseInvalidPRange("[-", 1);
+  ParseInvalidPRange("[x", 1);
+  ParseInvalidPRange("[ 0.2", 1);
+  ParseInvalidPRange("[10.1-", 5);
+  ParseInvalidPRange("[0.08-", 5);
+  ParseInvalidPRange("[0.2", 4);
+  ParseInvalidPRange("[0.2-", 5);
+  ParseInvalidPRange("[0.2-x", 5);
+  ParseInvalidPRange("[0.2--1", 5);
+  ParseInvalidPRange("[0.2-0.3", 8);
+  ParseInvalidPRange("[0.2-0.1]", 8);
+  ParseInvalidPRange("[0.2-0.3x", 8);
+  ParseInvalidPRange("[0.2,", 4);
+  ParseInvalidPRange("[0.2:", 4);
+  ParseInvalidPRange("[0.2]", 4);
+  ParseInvalidPRange("[0.2x", 4);
+  ParseInvalidPRange(" ", 0);
+  ParseInvalidPRange("x", 0);
+  ParseInvalidPRange("-x", 0);
+  ParseInvalidPRange("-1", 0);
+}
+
+static SdpImageattrAttributeList::Set
+ParseSet(const std::string& input)
+{
+  std::istringstream is;
+  is.str(input + " ");
+  std::string error;
+  SdpImageattrAttributeList::Set set;
+  EXPECT_TRUE(set.Parse(is, &error)) << error;
+  EXPECT_EQ(' ', is.get());
+  EXPECT_EQ(EOF, is.get());
+  return set;
+}
+
+TEST(NewSdpTestNoFixture, CheckImageattrSetParseValid)
+{
+  {
+    SdpImageattrAttributeList::Set set(ParseSet("[x=320,y=240]"));
+    ASSERT_EQ(1U, set.xRange.discreteValues.size());
+    ASSERT_EQ(320U, set.xRange.discreteValues[0]);
+    ASSERT_EQ(1U, set.yRange.discreteValues.size());
+    ASSERT_EQ(240U, set.yRange.discreteValues[0]);
+    ASSERT_FALSE(set.sRange.IsSet());
+    ASSERT_FALSE(set.pRange.IsSet());
+    ASSERT_FLOAT_EQ(0.5f, set.qValue);
+  }
+
+  {
+    SdpImageattrAttributeList::Set set(ParseSet("[X=320,Y=240]"));
+    ASSERT_EQ(1U, set.xRange.discreteValues.size());
+    ASSERT_EQ(320U, set.xRange.discreteValues[0]);
+    ASSERT_EQ(1U, set.yRange.discreteValues.size());
+    ASSERT_EQ(240U, set.yRange.discreteValues[0]);
+    ASSERT_FALSE(set.sRange.IsSet());
+    ASSERT_FALSE(set.pRange.IsSet());
+    ASSERT_FLOAT_EQ(0.5f, set.qValue);
+  }
+
+  {
+    SdpImageattrAttributeList::Set set(ParseSet("[x=320,y=240,par=[0.1-0.2]]"));
+    ASSERT_EQ(1U, set.xRange.discreteValues.size());
+    ASSERT_EQ(320U, set.xRange.discreteValues[0]);
+    ASSERT_EQ(1U, set.yRange.discreteValues.size());
+    ASSERT_EQ(240U, set.yRange.discreteValues[0]);
+    ASSERT_FALSE(set.sRange.IsSet());
+    ASSERT_TRUE(set.pRange.IsSet());
+    ASSERT_FLOAT_EQ(0.1f, set.pRange.min);
+    ASSERT_FLOAT_EQ(0.2f, set.pRange.max);
+    ASSERT_FLOAT_EQ(0.5f, set.qValue);
+  }
+
+  {
+    SdpImageattrAttributeList::Set set(ParseSet("[x=320,y=240,sar=[0.1-0.2]]"));
+    ASSERT_EQ(1U, set.xRange.discreteValues.size());
+    ASSERT_EQ(320U, set.xRange.discreteValues[0]);
+    ASSERT_EQ(1U, set.yRange.discreteValues.size());
+    ASSERT_EQ(240U, set.yRange.discreteValues[0]);
+    ASSERT_TRUE(set.sRange.IsSet());
+    ASSERT_FLOAT_EQ(0.1f, set.sRange.min);
+    ASSERT_FLOAT_EQ(0.2f, set.sRange.max);
+    ASSERT_FALSE(set.pRange.IsSet());
+    ASSERT_FLOAT_EQ(0.5f, set.qValue);
+  }
+
+  {
+    SdpImageattrAttributeList::Set set(ParseSet("[x=320,y=240,q=0.1]"));
+    ASSERT_EQ(1U, set.xRange.discreteValues.size());
+    ASSERT_EQ(320U, set.xRange.discreteValues[0]);
+    ASSERT_EQ(1U, set.yRange.discreteValues.size());
+    ASSERT_EQ(240U, set.yRange.discreteValues[0]);
+    ASSERT_FALSE(set.sRange.IsSet());
+    ASSERT_FALSE(set.pRange.IsSet());
+    ASSERT_FLOAT_EQ(0.1f, set.qValue);
+  }
+
+  {
+    SdpImageattrAttributeList::Set set(
+        ParseSet("[x=320,y=240,par=[0.1-0.2],sar=[0.3-0.4],q=0.6]"));
+    ASSERT_EQ(1U, set.xRange.discreteValues.size());
+    ASSERT_EQ(320U, set.xRange.discreteValues[0]);
+    ASSERT_EQ(1U, set.yRange.discreteValues.size());
+    ASSERT_EQ(240U, set.yRange.discreteValues[0]);
+    ASSERT_TRUE(set.sRange.IsSet());
+    ASSERT_FLOAT_EQ(0.3f, set.sRange.min);
+    ASSERT_FLOAT_EQ(0.4f, set.sRange.max);
+    ASSERT_TRUE(set.pRange.IsSet());
+    ASSERT_FLOAT_EQ(0.1f, set.pRange.min);
+    ASSERT_FLOAT_EQ(0.2f, set.pRange.max);
+    ASSERT_FLOAT_EQ(0.6f, set.qValue);
+  }
+
+  {
+    SdpImageattrAttributeList::Set set(ParseSet("[x=320,y=240,foo=bar,q=0.1]"));
+    ASSERT_EQ(1U, set.xRange.discreteValues.size());
+    ASSERT_EQ(320U, set.xRange.discreteValues[0]);
+    ASSERT_EQ(1U, set.yRange.discreteValues.size());
+    ASSERT_EQ(240U, set.yRange.discreteValues[0]);
+    ASSERT_FALSE(set.sRange.IsSet());
+    ASSERT_FALSE(set.pRange.IsSet());
+    ASSERT_FLOAT_EQ(0.1f, set.qValue);
+  }
+
+  {
+    SdpImageattrAttributeList::Set set(
+        ParseSet("[x=320,y=240,foo=bar,q=0.1,bar=baz]"));
+    ASSERT_EQ(1U, set.xRange.discreteValues.size());
+    ASSERT_EQ(320U, set.xRange.discreteValues[0]);
+    ASSERT_EQ(1U, set.yRange.discreteValues.size());
+    ASSERT_EQ(240U, set.yRange.discreteValues[0]);
+    ASSERT_FALSE(set.sRange.IsSet());
+    ASSERT_FALSE(set.pRange.IsSet());
+    ASSERT_FLOAT_EQ(0.1f, set.qValue);
+  }
+
+  {
+    SdpImageattrAttributeList::Set set(
+        ParseSet("[x=320,y=240,foo=[bar],q=0.1,bar=[baz]]"));
+    ASSERT_EQ(1U, set.xRange.discreteValues.size());
+    ASSERT_EQ(320U, set.xRange.discreteValues[0]);
+    ASSERT_EQ(1U, set.yRange.discreteValues.size());
+    ASSERT_EQ(240U, set.yRange.discreteValues[0]);
+    ASSERT_FALSE(set.sRange.IsSet());
+    ASSERT_FALSE(set.pRange.IsSet());
+    ASSERT_FLOAT_EQ(0.1f, set.qValue);
+  }
+
+  {
+    SdpImageattrAttributeList::Set set(
+        ParseSet("[x=320,y=240,foo=[par=foo,sar=bar],q=0.1,bar=[baz]]"));
+    ASSERT_EQ(1U, set.xRange.discreteValues.size());
+    ASSERT_EQ(320U, set.xRange.discreteValues[0]);
+    ASSERT_EQ(1U, set.yRange.discreteValues.size());
+    ASSERT_EQ(240U, set.yRange.discreteValues[0]);
+    ASSERT_FALSE(set.sRange.IsSet());
+    ASSERT_FALSE(set.pRange.IsSet());
+    ASSERT_FLOAT_EQ(0.1f, set.qValue);
+  }
+}
+
+static void
+ParseInvalidSet(const std::string& input, size_t last)
+{
+  std::istringstream is;
+  is.str(input);
+  SdpImageattrAttributeList::Set set;
+  std::string error;
+  ASSERT_FALSE(set.Parse(is, &error))
+    << "\'" << input << "\' should not have parsed successfully";
+  is.clear();
+  ASSERT_EQ(last, static_cast<size_t>(is.tellg()))
+    << "Parse failed at unexpected location:" << std::endl
+    << input << std::endl
+    << std::string(is.tellg(), ' ') << "^" << std::endl;
+  // For a human to eyeball to make sure the error strings look sane
+  std::cout << "\"" << input << "\" - " << error << std::endl; \
+}
+
+TEST(NewSdpTestNoFixture, CheckImageattrSetParseInvalid)
+{
+  ParseInvalidSet("", 0);
+  ParseInvalidSet("x", 0);
+  ParseInvalidSet("[", 1);
+  ParseInvalidSet("[=", 2);
+  ParseInvalidSet("[x", 2);
+  ParseInvalidSet("[y=", 3);
+  ParseInvalidSet("[x=[", 4);
+  ParseInvalidSet("[x=320", 6);
+  ParseInvalidSet("[x=320x", 6);
+  ParseInvalidSet("[x=320,", 7);
+  ParseInvalidSet("[x=320,=", 8);
+  ParseInvalidSet("[x=320,x", 8);
+  ParseInvalidSet("[x=320,x=", 9);
+  ParseInvalidSet("[x=320,y=[", 10);
+  ParseInvalidSet("[x=320,y=240", 12);
+  ParseInvalidSet("[x=320,y=240x", 12);
+  ParseInvalidSet("[x=320,y=240,", 13);
+  ParseInvalidSet("[x=320,y=240,q=", 15);
+  ParseInvalidSet("[x=320,y=240,q=x", 15);
+  ParseInvalidSet("[x=320,y=240,q=0.5", 18);
+  ParseInvalidSet("[x=320,y=240,q=0.5,", 19);
+  ParseInvalidSet("[x=320,y=240,q=0.5,]", 20);
+  ParseInvalidSet("[x=320,y=240,q=0.5,=]", 20);
+  ParseInvalidSet("[x=320,y=240,q=0.5,sar=x]", 23);
+  ParseInvalidSet("[x=320,y=240,q=0.5,q=0.4", 21);
+  ParseInvalidSet("[x=320,y=240,sar=", 17);
+  ParseInvalidSet("[x=320,y=240,sar=x", 17);
+  ParseInvalidSet("[x=320,y=240,sar=[0.5-0.6],sar=[0.7-0.8]", 31);
+  ParseInvalidSet("[x=320,y=240,par=", 17);
+  ParseInvalidSet("[x=320,y=240,par=x", 17);
+  ParseInvalidSet("[x=320,y=240,par=[0.5-0.6],par=[0.7-0.8]", 31);
+  ParseInvalidSet("[x=320,y=240,foo=", 17);
+  ParseInvalidSet("[x=320,y=240,foo=x", 18);
+}
+
+static SdpImageattrAttributeList::Imageattr
+ParseImageattr(const std::string& input)
+{
+  std::istringstream is;
+  is.str(input);
+  std::string error;
+  SdpImageattrAttributeList::Imageattr imageattr;
+  EXPECT_TRUE(imageattr.Parse(is, &error)) << error;
+  EXPECT_TRUE(is.eof());
+  return imageattr;
+}
+
+TEST(NewSdpTestNoFixture, CheckImageattrParseValid)
+{
+  {
+    SdpImageattrAttributeList::Imageattr imageattr(ParseImageattr("* send *"));
+    ASSERT_FALSE(imageattr.pt.isSome());
+    ASSERT_TRUE(imageattr.sendAll);
+    ASSERT_TRUE(imageattr.sendSets.empty());
+    ASSERT_FALSE(imageattr.recvAll);
+    ASSERT_TRUE(imageattr.recvSets.empty());
+  }
+
+  {
+    SdpImageattrAttributeList::Imageattr imageattr(ParseImageattr("* SEND *"));
+    ASSERT_FALSE(imageattr.pt.isSome());
+    ASSERT_TRUE(imageattr.sendAll);
+    ASSERT_TRUE(imageattr.sendSets.empty());
+    ASSERT_FALSE(imageattr.recvAll);
+    ASSERT_TRUE(imageattr.recvSets.empty());
+  }
+
+  {
+    SdpImageattrAttributeList::Imageattr imageattr(ParseImageattr("* recv *"));
+    ASSERT_FALSE(imageattr.pt.isSome());
+    ASSERT_FALSE(imageattr.sendAll);
+    ASSERT_TRUE(imageattr.sendSets.empty());
+    ASSERT_TRUE(imageattr.recvAll);
+    ASSERT_TRUE(imageattr.recvSets.empty());
+  }
+
+  {
+    SdpImageattrAttributeList::Imageattr imageattr(ParseImageattr("* RECV *"));
+    ASSERT_FALSE(imageattr.pt.isSome());
+    ASSERT_FALSE(imageattr.sendAll);
+    ASSERT_TRUE(imageattr.sendSets.empty());
+    ASSERT_TRUE(imageattr.recvAll);
+    ASSERT_TRUE(imageattr.recvSets.empty());
+  }
+
+  {
+    SdpImageattrAttributeList::Imageattr imageattr(
+        ParseImageattr("* recv * send *"));
+    ASSERT_FALSE(imageattr.pt.isSome());
+    ASSERT_TRUE(imageattr.sendAll);
+    ASSERT_TRUE(imageattr.sendSets.empty());
+    ASSERT_TRUE(imageattr.recvAll);
+    ASSERT_TRUE(imageattr.recvSets.empty());
+  }
+
+  {
+    SdpImageattrAttributeList::Imageattr imageattr(
+        ParseImageattr("* send * recv *"));
+    ASSERT_FALSE(imageattr.pt.isSome());
+    ASSERT_TRUE(imageattr.sendAll);
+    ASSERT_TRUE(imageattr.sendSets.empty());
+    ASSERT_TRUE(imageattr.recvAll);
+    ASSERT_TRUE(imageattr.recvSets.empty());
+  }
+
+  {
+    SdpImageattrAttributeList::Imageattr imageattr(
+        ParseImageattr("8 send * recv *"));
+    ASSERT_EQ(8U, *imageattr.pt);
+    ASSERT_TRUE(imageattr.sendAll);
+    ASSERT_TRUE(imageattr.sendSets.empty());
+    ASSERT_TRUE(imageattr.recvAll);
+    ASSERT_TRUE(imageattr.recvSets.empty());
+  }
+
+  {
+    SdpImageattrAttributeList::Imageattr imageattr(
+        ParseImageattr("8 send [x=320,y=240] recv *"));
+    ASSERT_EQ(8U, *imageattr.pt);
+    ASSERT_FALSE(imageattr.sendAll);
+    ASSERT_EQ(1U, imageattr.sendSets.size());
+    ASSERT_EQ(1U, imageattr.sendSets[0].xRange.discreteValues.size());
+    ASSERT_EQ(320U, imageattr.sendSets[0].xRange.discreteValues[0]);
+    ASSERT_EQ(1U, imageattr.sendSets[0].yRange.discreteValues.size());
+    ASSERT_EQ(240U, imageattr.sendSets[0].yRange.discreteValues[0]);
+    ASSERT_TRUE(imageattr.recvAll);
+    ASSERT_TRUE(imageattr.recvSets.empty());
+  }
+
+  {
+    SdpImageattrAttributeList::Imageattr imageattr(
+        ParseImageattr("8 send [x=320,y=240] [x=640,y=480] recv *"));
+    ASSERT_EQ(8U, *imageattr.pt);
+    ASSERT_FALSE(imageattr.sendAll);
+    ASSERT_EQ(2U, imageattr.sendSets.size());
+    ASSERT_EQ(1U, imageattr.sendSets[0].xRange.discreteValues.size());
+    ASSERT_EQ(320U, imageattr.sendSets[0].xRange.discreteValues[0]);
+    ASSERT_EQ(1U, imageattr.sendSets[0].yRange.discreteValues.size());
+    ASSERT_EQ(240U, imageattr.sendSets[0].yRange.discreteValues[0]);
+    ASSERT_EQ(1U, imageattr.sendSets[1].xRange.discreteValues.size());
+    ASSERT_EQ(640U, imageattr.sendSets[1].xRange.discreteValues[0]);
+    ASSERT_EQ(1U, imageattr.sendSets[1].yRange.discreteValues.size());
+    ASSERT_EQ(480U, imageattr.sendSets[1].yRange.discreteValues[0]);
+    ASSERT_TRUE(imageattr.recvAll);
+    ASSERT_TRUE(imageattr.recvSets.empty());
+  }
+
+  {
+    SdpImageattrAttributeList::Imageattr imageattr(
+        ParseImageattr("8 send * recv [x=320,y=240]"));
+    ASSERT_EQ(8U, *imageattr.pt);
+    ASSERT_FALSE(imageattr.recvAll);
+    ASSERT_EQ(1U, imageattr.recvSets.size());
+    ASSERT_EQ(1U, imageattr.recvSets[0].xRange.discreteValues.size());
+    ASSERT_EQ(320U, imageattr.recvSets[0].xRange.discreteValues[0]);
+    ASSERT_EQ(1U, imageattr.recvSets[0].yRange.discreteValues.size());
+    ASSERT_EQ(240U, imageattr.recvSets[0].yRange.discreteValues[0]);
+    ASSERT_TRUE(imageattr.sendAll);
+    ASSERT_TRUE(imageattr.sendSets.empty());
+  }
+
+  {
+    SdpImageattrAttributeList::Imageattr imageattr(
+        ParseImageattr("8 send * recv [x=320,y=240] [x=640,y=480]"));
+    ASSERT_EQ(8U, *imageattr.pt);
+    ASSERT_FALSE(imageattr.recvAll);
+    ASSERT_EQ(2U, imageattr.recvSets.size());
+    ASSERT_EQ(1U, imageattr.recvSets[0].xRange.discreteValues.size());
+    ASSERT_EQ(320U, imageattr.recvSets[0].xRange.discreteValues[0]);
+    ASSERT_EQ(1U, imageattr.recvSets[0].yRange.discreteValues.size());
+    ASSERT_EQ(240U, imageattr.recvSets[0].yRange.discreteValues[0]);
+    ASSERT_EQ(1U, imageattr.recvSets[1].xRange.discreteValues.size());
+    ASSERT_EQ(640U, imageattr.recvSets[1].xRange.discreteValues[0]);
+    ASSERT_EQ(1U, imageattr.recvSets[1].yRange.discreteValues.size());
+    ASSERT_EQ(480U, imageattr.recvSets[1].yRange.discreteValues[0]);
+    ASSERT_TRUE(imageattr.sendAll);
+    ASSERT_TRUE(imageattr.sendSets.empty());
+  }
+}
+
+static void
+ParseInvalidImageattr(const std::string& input, size_t last)
+{
+  std::istringstream is;
+  is.str(input);
+  SdpImageattrAttributeList::Imageattr imageattr;
+  std::string error;
+  ASSERT_FALSE(imageattr.Parse(is, &error))
+    << "\'" << input << "\' should not have parsed successfully";
+  is.clear();
+  ASSERT_EQ(last, static_cast<size_t>(is.tellg()))
+    << "Parse failed at unexpected location:" << std::endl
+    << input << std::endl
+    << std::string(is.tellg(), ' ') << "^" << std::endl;
+  // For a human to eyeball to make sure the error strings look sane
+  std::cout << "\"" << input << "\" - " << error << std::endl; \
+}
+
+TEST(NewSdpTestNoFixture, CheckImageattrParseInvalid)
+{
+  ParseInvalidImageattr("", 0);
+  ParseInvalidImageattr(" ", 0);
+  ParseInvalidImageattr("-1", 0);
+  ParseInvalidImageattr("99999 ", 5);
+  ParseInvalidImageattr("*", 1);
+  ParseInvalidImageattr("* sen", 5);
+  ParseInvalidImageattr("* vcer *", 6);
+  ParseInvalidImageattr("* send x", 7);
+  ParseInvalidImageattr("* send [x=640,y=480] [", 22);
+  ParseInvalidImageattr("* send * sen", 12);
+  ParseInvalidImageattr("* send * vcer *", 13);
+  ParseInvalidImageattr("* send * send *", 13);
+  ParseInvalidImageattr("* recv * recv *", 13);
+  ParseInvalidImageattr("* send * recv x", 14);
+  ParseInvalidImageattr("* send * recv [x=640,y=480] [", 29);
+  ParseInvalidImageattr("* send * recv [x=640,y=480] *", 28);
+  ParseInvalidImageattr("* send * recv [x=640,y=480] foobajooba", 28);
+}
+
+TEST(NewSdpTestNoFixture, CheckImageattrXYRangeSerialization)
+{
+  SdpImageattrAttributeList::XYRange range;
+  std::stringstream os;
+
+  range.min = 320;
+  range.max = 640;
+  range.Serialize(os);
+  ASSERT_EQ("[320:640]", os.str());
+  os.str(""); // clear
+
+  range.step = 16;
+  range.Serialize(os);
+  ASSERT_EQ("[320:16:640]", os.str());
+  os.str(""); // clear
+
+  range.min = 0;
+  range.max = 0;
+  range.discreteValues.push_back(320);
+  range.Serialize(os);
+  ASSERT_EQ("320", os.str());
+  os.str("");
+
+  range.discreteValues.push_back(640);
+  range.Serialize(os);
+  ASSERT_EQ("[320,640]", os.str());
+}
+
+TEST(NewSdpTestNoFixture, CheckImageattrSRangeSerialization)
+{
+  SdpImageattrAttributeList::SRange range;
+  std::ostringstream os;
+
+  range.min = 0.1f;
+  range.max = 0.9999f;
+  range.Serialize(os);
+  ASSERT_EQ("[0.1000-0.9999]", os.str());
+  os.str("");
+
+  range.min = 0.0f;
+  range.max = 0.0f;
+  range.discreteValues.push_back(0.1f);
+  range.Serialize(os);
+  ASSERT_EQ("0.1000", os.str());
+  os.str("");
+
+  range.discreteValues.push_back(0.5f);
+  range.Serialize(os);
+  ASSERT_EQ("[0.1000,0.5000]", os.str());
+}
+
+TEST(NewSdpTestNoFixture, CheckImageattrPRangeSerialization)
+{
+  SdpImageattrAttributeList::PRange range;
+  std::ostringstream os;
+
+  range.min = 0.1f;
+  range.max = 0.9999f;
+  range.Serialize(os);
+  ASSERT_EQ("[0.1000-0.9999]", os.str());
+}
+
+TEST(NewSdpTestNoFixture, CheckImageattrSetSerialization)
+{
+  SdpImageattrAttributeList::Set set;
+  std::ostringstream os;
+
+  set.xRange.discreteValues.push_back(640);
+  set.yRange.discreteValues.push_back(480);
+  set.Serialize(os);
+  ASSERT_EQ("[x=640,y=480]", os.str());
+  os.str("");
+
+  set.qValue = 0.00f;
+  set.Serialize(os);
+  ASSERT_EQ("[x=640,y=480,q=0.00]", os.str());
+  os.str("");
+
+  set.qValue = 0.10f;
+  set.Serialize(os);
+  ASSERT_EQ("[x=640,y=480,q=0.10]", os.str());
+  os.str("");
+
+  set.qValue = 1.00f;
+  set.Serialize(os);
+  ASSERT_EQ("[x=640,y=480,q=1.00]", os.str());
+  os.str("");
+
+  set.sRange.discreteValues.push_back(1.1f);
+  set.Serialize(os);
+  ASSERT_EQ("[x=640,y=480,sar=1.1000,q=1.00]", os.str());
+  os.str("");
+
+  set.pRange.min = 0.9f;
+  set.pRange.max = 1.1f;
+  set.Serialize(os);
+  ASSERT_EQ("[x=640,y=480,sar=1.1000,par=[0.9000-1.1000],q=1.00]", os.str());
+  os.str("");
+}
+
+TEST(NewSdpTestNoFixture, CheckImageattrSerialization)
+{
+  SdpImageattrAttributeList::Imageattr imageattr;
+  std::ostringstream os;
+
+  imageattr.sendAll = true;
+  imageattr.pt = Some<uint16_t>(8U);
+  imageattr.Serialize(os);
+  ASSERT_EQ("8 send *", os.str());
+  os.str("");
+
+  imageattr.pt.reset();;
+  imageattr.Serialize(os);
+  ASSERT_EQ("* send *", os.str());
+  os.str("");
+
+  imageattr.sendAll = false;
+  imageattr.recvAll = true;
+  imageattr.Serialize(os);
+  ASSERT_EQ("* recv *", os.str());
+  os.str("");
+
+  imageattr.sendAll = true;
+  imageattr.Serialize(os);
+  ASSERT_EQ("* send * recv *", os.str());
+  os.str("");
+
+  imageattr.sendAll = false;
+  imageattr.sendSets.push_back(SdpImageattrAttributeList::Set());
+  imageattr.sendSets.back().xRange.discreteValues.push_back(320);
+  imageattr.sendSets.back().yRange.discreteValues.push_back(240);
+  imageattr.Serialize(os);
+  ASSERT_EQ("* send [x=320,y=240] recv *", os.str());
+  os.str("");
+
+  imageattr.sendSets.push_back(SdpImageattrAttributeList::Set());
+  imageattr.sendSets.back().xRange.discreteValues.push_back(640);
+  imageattr.sendSets.back().yRange.discreteValues.push_back(480);
+  imageattr.Serialize(os);
+  ASSERT_EQ("* send [x=320,y=240] [x=640,y=480] recv *", os.str());
+  os.str("");
+
+  imageattr.recvAll = false;
+  imageattr.recvSets.push_back(SdpImageattrAttributeList::Set());
+  imageattr.recvSets.back().xRange.discreteValues.push_back(320);
+  imageattr.recvSets.back().yRange.discreteValues.push_back(240);
+  imageattr.Serialize(os);
+  ASSERT_EQ("* send [x=320,y=240] [x=640,y=480] recv [x=320,y=240]", os.str());
+  os.str("");
+
+  imageattr.recvSets.push_back(SdpImageattrAttributeList::Set());
+  imageattr.recvSets.back().xRange.discreteValues.push_back(640);
+  imageattr.recvSets.back().yRange.discreteValues.push_back(480);
+  imageattr.Serialize(os);
+  ASSERT_EQ(
+      "* send [x=320,y=240] [x=640,y=480] recv [x=320,y=240] [x=640,y=480]",
+      os.str());
+  os.str("");
+}
+
 } // End namespace test.
 
 int main(int argc, char **argv) {
   test_utils = new MtransportTestUtils();
   NSS_NoDB_Init(nullptr);
   NSS_SetDomesticPolicy();
 
   ::testing::InitGoogleTest(&argc, argv);