diff --git a/chrome/VERSION b/chrome/VERSION
index 39608e4..f320c01 100644
--- a/chrome/VERSION
+++ b/chrome/VERSION
@@ -1,4 +1,4 @@
 MAJOR=60
 MINOR=0
-BUILD=3085
+BUILD=3086
 PATCH=0
diff --git a/chrome/browser/media/router/BUILD.gn b/chrome/browser/media/router/BUILD.gn
index 449a9d0..983897e 100644
--- a/chrome/browser/media/router/BUILD.gn
+++ b/chrome/browser/media/router/BUILD.gn
@@ -40,8 +40,6 @@
     "media_router_metrics.h",
     "media_routes_observer.cc",
     "media_routes_observer.h",
-    "media_sink_service.cc",
-    "media_sink_service.h",
     "media_sinks_observer.cc",
     "media_sinks_observer.h",
     "mojo/media_route_controller.cc",
diff --git a/chrome/browser/media/router/discovery/BUILD.gn b/chrome/browser/media/router/discovery/BUILD.gn
index 3115fe5..21199e12 100644
--- a/chrome/browser/media/router/discovery/BUILD.gn
+++ b/chrome/browser/media/router/discovery/BUILD.gn
@@ -18,12 +18,18 @@
   sources = [
     "dial/device_description_fetcher.cc",
     "dial/device_description_fetcher.h",
+    "dial/device_description_service.cc",
+    "dial/device_description_service.h",
     "dial/dial_device_data.cc",
     "dial/dial_device_data.h",
+    "dial/dial_media_sink_service.cc",
+    "dial/dial_media_sink_service.h",
     "dial/dial_registry.cc",
     "dial/dial_registry.h",
     "dial/dial_service.cc",
     "dial/dial_service.h",
+    "dial/parsed_dial_device_description.cc",
+    "dial/parsed_dial_device_description.h",
     "dial/safe_dial_device_description_parser.cc",
     "dial/safe_dial_device_description_parser.h",
   ]
diff --git a/chrome/browser/media/router/discovery/dial/device_description_fetcher.h b/chrome/browser/media/router/discovery/dial/device_description_fetcher.h
index 3287f72..4152d152 100644
--- a/chrome/browser/media/router/discovery/dial/device_description_fetcher.h
+++ b/chrome/browser/media/router/discovery/dial/device_description_fetcher.h
@@ -43,6 +43,8 @@
 
   ~DeviceDescriptionFetcher() override;
 
+  const GURL& device_description_url() { return device_description_url_; }
+
   void Start();
 
  private:
diff --git a/chrome/browser/media/router/discovery/dial/device_description_service.cc b/chrome/browser/media/router/discovery/dial/device_description_service.cc
new file mode 100644
index 0000000..ce271a4
--- /dev/null
+++ b/chrome/browser/media/router/discovery/dial/device_description_service.cc
@@ -0,0 +1,324 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/media/router/discovery/dial/device_description_service.h"
+
+#if DCHECK_IS_ON()
+#include <sstream>
+#endif
+
+#include "base/stl_util.h"
+#include "base/strings/string_util.h"
+#include "chrome/browser/media/router/discovery/dial/device_description_fetcher.h"
+#include "chrome/browser/media/router/discovery/dial/safe_dial_device_description_parser.h"
+#include "net/base/ip_address.h"
+#include "url/gurl.h"
+
+namespace {
+
+enum ErrorType {
+  NONE,
+  MISSING_UNIQUE_ID,
+  MISSING_FRIENDLY_NAME,
+  MISSING_APP_URL,
+  INVALID_APP_URL
+};
+
+// How long to cache a device description.
+constexpr int kDeviceDescriptionCacheTimeHours = 12;
+
+// Time interval to clean up cache entries.
+constexpr int kCacheCleanUpTimeoutMins = 30;
+
+// Maximum size on the number of cached entries.
+constexpr int kCacheMaxEntries = 256;
+
+#if DCHECK_IS_ON()
+// Replaces "<element_name>content</element_name>" with
+// "<element_name>***</element_name>"
+void Scrub(const std::string& element_name, std::string* xml_text) {
+  size_t pos = xml_text->find("<" + element_name + ">");
+  size_t end_pos = xml_text->find("</" + element_name + ">");
+
+  if (pos == std::string::npos || end_pos == std::string::npos)
+    return;
+
+  size_t start_pos = pos + element_name.length() + 2;
+  if (end_pos > start_pos)
+    xml_text->replace(start_pos, end_pos - start_pos, "***");
+}
+
+// Removes unique identifiers from the device description.
+// |xml_text|: The device description XML.
+// Returns original XML text if <UDN> or <serialNumber> field does not exist.
+std::string ScrubDeviceDescriptionXml(const std::string& xml_text) {
+  std::string scrubbed_xml(xml_text);
+  Scrub("UDN", &scrubbed_xml);
+  Scrub("serialNumber", &scrubbed_xml);
+  return scrubbed_xml;
+}
+
+std::string CachedDeviceDescriptionToString(
+    const media_router::DeviceDescriptionService::CacheEntry& cached_data) {
+  std::stringstream ss;
+  ss << "CachedDialDeviceDescription [unique_id]: "
+     << cached_data.description_data.unique_id
+     << " [friendly_name]: " << cached_data.description_data.friendly_name
+     << " [model_name]: " << cached_data.description_data.model_name
+     << " [app_url]: " << cached_data.description_data.app_url
+     << " [expire_time]: " << cached_data.expire_time
+     << " [config_id]: " << cached_data.config_id;
+
+  return ss.str();
+}
+#endif
+
+bool IsValidAppUrl(const GURL& app_url, const std::string& ip_address) {
+  return app_url.SchemeIs(url::kHttpScheme) && app_url.host() == ip_address;
+}
+
+// Checks mandatory fields. Returns ErrorType::NONE if device description is
+// valid; Otherwise returns specific error type.
+ErrorType ValidateParsedDeviceDescription(
+    const std::string& expected_ip_address,
+    const media_router::ParsedDialDeviceDescription& description_data) {
+  if (description_data.unique_id.empty()) {
+    return ErrorType::MISSING_UNIQUE_ID;
+  }
+  if (description_data.friendly_name.empty()) {
+    return ErrorType::MISSING_FRIENDLY_NAME;
+  }
+  if (!description_data.app_url.is_valid()) {
+    return ErrorType::MISSING_APP_URL;
+  }
+  if (!IsValidAppUrl(description_data.app_url, expected_ip_address)) {
+    return ErrorType::INVALID_APP_URL;
+  }
+  return ErrorType::NONE;
+}
+
+}  // namespace
+
+namespace media_router {
+
+DeviceDescriptionService::DeviceDescriptionService(
+    const DeviceDescriptionParseSuccessCallback& success_cb,
+    const DeviceDescriptionParseErrorCallback& error_cb)
+    : success_cb_(success_cb), error_cb_(error_cb) {}
+
+DeviceDescriptionService::~DeviceDescriptionService() {
+  if (!pending_device_labels_.empty()) {
+    DLOG(WARNING) << "Fail to finish parsing " << pending_device_labels_.size()
+                  << " devices.";
+  }
+}
+
+void DeviceDescriptionService::GetDeviceDescriptions(
+    const std::vector<DialDeviceData>& devices,
+    net::URLRequestContextGetter* request_context) {
+  DCHECK(thread_checker_.CalledOnValidThread());
+
+  std::map<std::string, std::unique_ptr<DeviceDescriptionFetcher>>
+      existing_fetcher_map;
+  for (auto& fetcher_it : device_description_fetcher_map_) {
+    std::string device_label = fetcher_it.first;
+    const auto& device_it =
+        std::find_if(devices.begin(), devices.end(),
+                     [&device_label](const DialDeviceData& device_data) {
+                       return device_data.label() == device_label;
+                     });
+    if (device_it == devices.end() ||
+        device_it->device_description_url() ==
+            fetcher_it.second->device_description_url()) {
+      existing_fetcher_map.insert(
+          std::make_pair(device_label, std::move(fetcher_it.second)));
+    }
+  }
+
+  // Remove all out dated fetchers.
+  device_description_fetcher_map_ = std::move(existing_fetcher_map);
+
+  for (const auto& device_data : devices) {
+    auto* cache_entry = CheckAndUpdateCache(device_data);
+    if (cache_entry) {
+      // Get device description from cache.
+      success_cb_.Run(device_data, cache_entry->description_data);
+      continue;
+    }
+
+    FetchDeviceDescription(device_data, request_context);
+  }
+
+  // Start a clean up timer.
+  if (!clean_up_timer_) {
+    clean_up_timer_.reset(new base::RepeatingTimer());
+    clean_up_timer_->Start(
+        FROM_HERE, base::TimeDelta::FromMinutes(kCacheCleanUpTimeoutMins), this,
+        &DeviceDescriptionService::CleanUpCacheEntries);
+  }
+}
+
+void DeviceDescriptionService::CleanUpCacheEntries() {
+  DCHECK(thread_checker_.CalledOnValidThread());
+  base::Time now = GetNow();
+
+  DVLOG(2) << "Before clean up, cache size: " << description_cache_.size();
+  base::EraseIf(description_cache_,
+                [&now](const std::pair<std::string, CacheEntry>& cache_pair) {
+                  return cache_pair.second.expire_time < now;
+                });
+  DVLOG(2) << "After clean up, cache size: " << description_cache_.size();
+
+  if (description_cache_.empty() && device_description_fetcher_map_.empty()) {
+    DVLOG(2) << "Cache is empty, stop clean up timer...";
+    clean_up_timer_.reset();
+  }
+}
+
+void DeviceDescriptionService::FetchDeviceDescription(
+    const DialDeviceData& device_data,
+    net::URLRequestContextGetter* request_context) {
+  DCHECK(thread_checker_.CalledOnValidThread());
+
+  // Existing Fetcher.
+  const auto& it = device_description_fetcher_map_.find(device_data.label());
+  if (it != device_description_fetcher_map_.end())
+    return;
+
+  auto device_description_fetcher =
+      base::WrapUnique(new DeviceDescriptionFetcher(
+          device_data.device_description_url(), request_context,
+          base::BindOnce(
+              &DeviceDescriptionService::OnDeviceDescriptionFetchComplete,
+              base::Unretained(this), device_data),
+          base::BindOnce(
+              &DeviceDescriptionService::OnDeviceDescriptionFetchError,
+              base::Unretained(this), device_data)));
+
+  device_description_fetcher->Start();
+  device_description_fetcher_map_.insert(std::make_pair(
+      device_data.label(), std::move(device_description_fetcher)));
+
+  pending_device_labels_.insert(device_data.label());
+}
+
+const DeviceDescriptionService::CacheEntry*
+DeviceDescriptionService::CheckAndUpdateCache(
+    const DialDeviceData& device_data) {
+  const auto& it = description_cache_.find(device_data.label());
+  if (it == description_cache_.end())
+    return nullptr;
+
+  // If the entry's config_id does not match, or it has expired, remove it.
+  if (it->second.config_id != device_data.config_id() ||
+      GetNow() >= it->second.expire_time) {
+    DVLOG(2) << "Removing invalid entry " << it->first;
+    description_cache_.erase(it);
+    return nullptr;
+  }
+
+  // Entry is valid.
+  return &it->second;
+}
+
+void DeviceDescriptionService::OnParsedDeviceDescription(
+    const DialDeviceData& device_data,
+    const GURL& app_url,
+    chrome::mojom::DialDeviceDescriptionPtr parsed_device_description) {
+  DCHECK(thread_checker_.CalledOnValidThread());
+
+  // Last callback for current utility process. Release |parser_| and
+  // SafeDialDeviceDescriptionParser object will be destroyed after this
+  // callback.
+  pending_device_labels_.erase(device_data.label());
+  if (pending_device_labels_.empty()) {
+    parser_.reset();
+  }
+
+  if (!parsed_device_description) {
+    error_cb_.Run(device_data, "Failed to parse device description XML");
+    return;
+  }
+
+  ParsedDialDeviceDescription description_data;
+  description_data.unique_id = parsed_device_description->unique_id;
+  description_data.friendly_name = parsed_device_description->friendly_name;
+  description_data.model_name = parsed_device_description->model_name;
+  description_data.app_url = app_url;
+
+  auto error = ValidateParsedDeviceDescription(
+      device_data.device_description_url().host(), description_data);
+
+  if (error != ErrorType::NONE) {
+    DLOG(WARNING) << "Device description failed to validate: " << error;
+    error_cb_.Run(device_data, "Failed to process fetch result");
+    return;
+  }
+
+  if (description_cache_.size() >= kCacheMaxEntries) {
+    success_cb_.Run(device_data, description_data);
+    return;
+  }
+
+  CacheEntry cached_description_data;
+  cached_description_data.expire_time =
+      GetNow() + base::TimeDelta::FromHours(kDeviceDescriptionCacheTimeHours);
+  cached_description_data.config_id = device_data.config_id();
+  cached_description_data.description_data = description_data;
+
+#if DCHECK_IS_ON()
+  DVLOG(2) << "Got device description for " << device_data.label()
+           << "... device description was: "
+           << CachedDeviceDescriptionToString(cached_description_data);
+#endif
+
+  DVLOG(2) << "Caching device description for " << device_data.label();
+  description_cache_.insert(
+      std::make_pair(device_data.label(), cached_description_data));
+
+  success_cb_.Run(device_data, description_data);
+}
+
+void DeviceDescriptionService::OnDeviceDescriptionFetchComplete(
+    const DialDeviceData& device_data,
+    const DialDeviceDescriptionData& description_data) {
+  DCHECK(thread_checker_.CalledOnValidThread());
+
+  if (!parser_)
+    parser_ = base::MakeUnique<SafeDialDeviceDescriptionParser>();
+
+  parser_->Start(
+      description_data.device_description,
+      base::Bind(&DeviceDescriptionService::OnParsedDeviceDescription,
+                 base::Unretained(this), device_data,
+                 description_data.app_url));
+
+#if DCHECK_IS_ON()
+  DVLOG(2) << "Device description: "
+           << ScrubDeviceDescriptionXml(description_data.device_description);
+#endif
+
+  device_description_fetcher_map_.erase(device_data.label());
+}
+
+void DeviceDescriptionService::OnDeviceDescriptionFetchError(
+    const DialDeviceData& device_data,
+    const std::string& error_message) {
+  DCHECK(thread_checker_.CalledOnValidThread());
+  DVLOG(2) << "OnDeviceDescriptionFetchError [label]: " << device_data.label();
+
+  error_cb_.Run(device_data, error_message);
+  device_description_fetcher_map_.erase(device_data.label());
+}
+
+base::Time DeviceDescriptionService::GetNow() {
+  return base::Time::Now();
+}
+
+DeviceDescriptionService::CacheEntry::CacheEntry() : config_id(-1) {}
+DeviceDescriptionService::CacheEntry::CacheEntry(const CacheEntry& other) =
+    default;
+DeviceDescriptionService::CacheEntry::~CacheEntry() = default;
+
+}  // namespace media_router
diff --git a/chrome/browser/media/router/discovery/dial/device_description_service.h b/chrome/browser/media/router/discovery/dial/device_description_service.h
new file mode 100644
index 0000000..6398d9b
--- /dev/null
+++ b/chrome/browser/media/router/discovery/dial/device_description_service.h
@@ -0,0 +1,168 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_MEDIA_ROUTER_DISCOVERY_DIAL_DEVICE_DESCRIPTION_SERVICE_H_
+#define CHROME_BROWSER_MEDIA_ROUTER_DISCOVERY_DIAL_DEVICE_DESCRIPTION_SERVICE_H_
+
+#include <memory>
+#include <set>
+#include <string>
+
+#include "base/callback.h"
+#include "base/gtest_prod_util.h"
+#include "base/threading/thread_checker.h"
+#include "base/timer/timer.h"
+#include "chrome/browser/media/router/discovery/dial/dial_device_data.h"
+#include "chrome/browser/media/router/discovery/dial/parsed_dial_device_description.h"
+#include "chrome/common/media_router/mojo/dial_device_description_parser.mojom.h"
+
+namespace net {
+class URLRequestContextGetter;
+}
+
+namespace media_router {
+
+class DeviceDescriptionFetcher;
+class SafeDialDeviceDescriptionParser;
+
+// This class fetches and parses device description XML for DIAL devices. Actual
+// parsing happens in a separate utility process via SafeDeviceDescriptionParser
+// (instead of in this class). This class lives on IO thread.
+class DeviceDescriptionService {
+ public:
+  // Represents cached device description data parsed from device description
+  // XML.
+  struct CacheEntry {
+    CacheEntry();
+    CacheEntry(const CacheEntry& other);
+    ~CacheEntry();
+
+    // The expiration time from the cache.
+    base::Time expire_time;
+
+    // The device description version number (non-negative).
+    int32_t config_id;
+
+    // Parsed device description data from XML.
+    ParsedDialDeviceDescription description_data;
+  };
+
+  // Called if parsing device description XML in utility process succeeds, and
+  // all fields are valid.
+  // |device_data|: The device to look up.
+  // |description_data|: Device description data from device description XML.
+  using DeviceDescriptionParseSuccessCallback =
+      base::Callback<void(const DialDeviceData& device_data,
+                          const ParsedDialDeviceDescription& description_data)>;
+
+  // Called if parsing device description XML in utility process fails, or some
+  // parsed fields are missing or invalid.
+  using DeviceDescriptionParseErrorCallback =
+      base::Callback<void(const DialDeviceData& device_data,
+                          const std::string& error_message)>;
+
+  DeviceDescriptionService(
+      const DeviceDescriptionParseSuccessCallback& success_cb,
+      const DeviceDescriptionParseErrorCallback& error_cb);
+  virtual ~DeviceDescriptionService();
+
+  // For each device in |devices|, if there is a valid cache entry for it, call
+  // |success_cb_| with cached device description; otherwise start fetching
+  // device description XML and parsing XML in utility process. Call
+  // |success_cb_| if both fetching and parsing succeeds; otherwise call
+  // |error_cb_|.
+  // |request_context|: Used by the background URLFetchers.
+  virtual void GetDeviceDescriptions(
+      const std::vector<DialDeviceData>& devices,
+      net::URLRequestContextGetter* request_context);
+
+ private:
+  friend class DeviceDescriptionServiceTest;
+  friend class TestDeviceDescriptionService;
+  FRIEND_TEST_ALL_PREFIXES(DeviceDescriptionServiceTest,
+                           TestGetDeviceDescriptionRemoveOutDatedFetchers);
+  FRIEND_TEST_ALL_PREFIXES(DeviceDescriptionServiceTest,
+                           TestGetDeviceDescriptionFetchURLError);
+  FRIEND_TEST_ALL_PREFIXES(DeviceDescriptionServiceTest,
+                           TestCleanUpCacheEntries);
+  FRIEND_TEST_ALL_PREFIXES(DeviceDescriptionServiceTest,
+                           TestSafeParserProperlyCreated);
+
+  // Checks the cache for a valid device description. If the entry is found but
+  // is expired, it is removed from the cache. Returns cached entry of
+  // parsed device description. Returns nullptr if cache entry does not exist or
+  // is not valid.
+  // |device_data|: The device to look up.
+  const CacheEntry* CheckAndUpdateCache(const DialDeviceData& device_data);
+
+  // Issues a HTTP GET request for the device description. No-op if there is
+  // already a pending request.
+  // |device_data|: The device to look up.
+  // |request_context|: Used by the background URLFetchers.
+  void FetchDeviceDescription(const DialDeviceData& device_data,
+                              net::URLRequestContextGetter* request_context);
+
+  // Invoked when HTTP GET request finishes.
+  // |device_data|: Device data initiating the HTTP request.
+  // |description_data|: Response from HTTP request.
+  void OnDeviceDescriptionFetchComplete(
+      const DialDeviceData& device_data,
+      const DialDeviceDescriptionData& description_data);
+
+  // Invoked when HTTP GET request fails.
+  // |device_data|: Device data initiating the HTTP request.
+  // |error_message|: Error message from HTTP request.
+  void OnDeviceDescriptionFetchError(const DialDeviceData& device_data,
+                                     const std::string& error_message);
+
+  // Invoked when SafeDialDeviceDescriptionParser finishes parsing device
+  // description XML.
+  // |device_data|: Device data initiating XML parsing in utility process.
+  // |app_url|: The app Url.
+  // |device_description_ptr|: Parsed device description from utility process,
+  // or nullptr if parsing failed.
+  void OnParsedDeviceDescription(
+      const DialDeviceData& device_data,
+      const GURL& app_url,
+      chrome::mojom::DialDeviceDescriptionPtr device_description_ptr);
+
+  // Remove expired cache entries from |description_map_|.
+  void CleanUpCacheEntries();
+
+  // Used by unit tests.
+  virtual base::Time GetNow();
+
+  // Map of current device description fetches in progress, keyed by device
+  // label.
+  std::map<std::string, std::unique_ptr<DeviceDescriptionFetcher>>
+      device_description_fetcher_map_;
+
+  // Set of device labels to be parsed in current utility process.
+  std::set<std::string> pending_device_labels_;
+
+  // Map of current cached device descriptions, keyed by device label.
+  std::map<std::string, CacheEntry> description_cache_;
+
+  // See comments for DeviceDescriptionParseSuccessCallback.
+  DeviceDescriptionParseSuccessCallback success_cb_;
+
+  // See comments for DeviceDescriptionParseErrorCallback.
+  DeviceDescriptionParseErrorCallback error_cb_;
+
+  // Timer for clean up expired cache entries.
+  std::unique_ptr<base::RepeatingTimer> clean_up_timer_;
+
+  // Safe DIAL parser associated with utility process. When this object is
+  // destroyed, associating utility process will shutdown. Keep |parser_| alive
+  // until finish parsing all device descriptions of devices passed in
+  // |GetDeviceDescriptions()|. If second |GetDeviceDescriptions()| arrives and
+  // |parser_| is still alive, reuse |parser_| instead of creating a new object.
+  std::unique_ptr<SafeDialDeviceDescriptionParser> parser_;
+
+  base::ThreadChecker thread_checker_;
+};
+
+}  // namespace media_router
+
+#endif  // CHROME_BROWSER_MEDIA_ROUTER_DISCOVERY_DIAL_DEVICE_DESCRIPTION_SERVICE_H_
diff --git a/chrome/browser/media/router/discovery/dial/device_description_service_unittest.cc b/chrome/browser/media/router/discovery/dial/device_description_service_unittest.cc
new file mode 100644
index 0000000..b58a67d
--- /dev/null
+++ b/chrome/browser/media/router/discovery/dial/device_description_service_unittest.cc
@@ -0,0 +1,328 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/media/router/discovery/dial/device_description_service.h"
+
+#include "base/strings/stringprintf.h"
+#include "base/test/mock_callback.h"
+#include "chrome/browser/media/router/discovery/dial/device_description_fetcher.h"
+#include "chrome/browser/media/router/discovery/dial/dial_device_data.h"
+#include "chrome/browser/media/router/discovery/dial/parsed_dial_device_description.h"
+#include "chrome/browser/media/router/discovery/dial/safe_dial_device_description_parser.h"
+#include "chrome/test/base/testing_profile.h"
+#include "content/public/test/test_browser_thread_bundle.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using ::testing::_;
+using ::testing::Return;
+using ::testing::SaveArg;
+
+// Create Test Data
+namespace {
+
+media_router::DialDeviceData CreateDialDeviceData(int num) {
+  media_router::DialDeviceData device_data(
+      base::StringPrintf("Device id %d", num),
+      GURL(base::StringPrintf("http://192.168.1.%d/dd.xml", num)),
+      base::Time::Now());
+  device_data.set_label(base::StringPrintf("Device label %d", num));
+  return device_data;
+}
+
+media_router::DialDeviceDescriptionData CreateDialDeviceDescriptionData(
+    int num) {
+  return media_router::DialDeviceDescriptionData(
+      "", GURL(base::StringPrintf("http://192.168.1.%d/apps", num)));
+}
+
+chrome::mojom::DialDeviceDescriptionPtr CreateDialDeviceDescriptionPtr(
+    int num) {
+  chrome::mojom::DialDeviceDescriptionPtr description_ptr =
+      chrome::mojom::DialDeviceDescription::New();
+  description_ptr->friendly_name = base::StringPrintf("Friendly name %d", num);
+  description_ptr->model_name = base::StringPrintf("Model name %d", num);
+  description_ptr->unique_id = base::StringPrintf("Unique ID %d", num);
+  return description_ptr;
+}
+
+media_router::ParsedDialDeviceDescription CreateParsedDialDeviceDescription(
+    int num) {
+  media_router::ParsedDialDeviceDescription description_data;
+  description_data.app_url =
+      GURL(base::StringPrintf("http://192.168.1.%d/apps", num));
+  description_data.friendly_name = base::StringPrintf("Friendly name %d", num);
+  description_data.model_name = base::StringPrintf("Model name %d", num);
+  description_data.unique_id = base::StringPrintf("Unique ID %d", num);
+  return description_data;
+}
+
+}  // namespace
+
+namespace media_router {
+
+class TestSafeDialDeviceDescriptionParser
+    : public SafeDialDeviceDescriptionParser {
+ public:
+  ~TestSafeDialDeviceDescriptionParser() override {}
+
+  MOCK_METHOD2(Start,
+               void(const std::string& xml_text,
+                    const DeviceDescriptionCallback& callback));
+};
+
+class TestDeviceDescriptionService : public DeviceDescriptionService {
+ public:
+  TestDeviceDescriptionService(
+      const DeviceDescriptionParseSuccessCallback& success_cb,
+      const DeviceDescriptionParseErrorCallback& error_cb)
+      : DeviceDescriptionService(success_cb, error_cb) {}
+
+  MOCK_METHOD0(CreateSafeParser, SafeDialDeviceDescriptionParser*());
+};
+
+class DeviceDescriptionServiceTest : public ::testing::Test {
+ public:
+  DeviceDescriptionServiceTest()
+      : device_description_service_(mock_success_cb_.Get(),
+                                    mock_error_cb_.Get()),
+        fetcher_map_(
+            device_description_service_.device_description_fetcher_map_),
+        description_cache_(device_description_service_.description_cache_),
+        thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP) {}
+
+  TestDeviceDescriptionService* device_description_service() {
+    return &device_description_service_;
+  }
+
+  void AddToCache(const std::string& device_label,
+                  const ParsedDialDeviceDescription& description_data,
+                  bool expired) {
+    DeviceDescriptionService::CacheEntry cache_entry;
+    cache_entry.expire_time = base::Time::Now();
+    if (!expired)
+      cache_entry.expire_time += base::TimeDelta::FromHours(12);
+    cache_entry.description_data = description_data;
+    description_cache_[device_label] = cache_entry;
+  }
+
+  void SetTestParser(
+      std::unique_ptr<TestSafeDialDeviceDescriptionParser> test_parser) {
+    if (device_description_service_.parser_)
+      return;
+    device_description_service_.parser_ = std::move(test_parser);
+  }
+
+  void OnDeviceDescriptionFetchComplete(int num) {
+    auto device_data = CreateDialDeviceData(num);
+    auto description_response_data = CreateDialDeviceDescriptionData(num);
+    auto parsed_description_data = CreateParsedDialDeviceDescription(num);
+
+    EXPECT_CALL(mock_success_cb_, Run(device_data, parsed_description_data));
+
+    device_description_service_.OnDeviceDescriptionFetchComplete(
+        device_data, description_response_data);
+    device_description_service_.OnParsedDeviceDescription(
+        device_data, description_response_data.app_url,
+        CreateDialDeviceDescriptionPtr(num));
+  }
+
+  void TestOnParsedDeviceDescription(
+      chrome::mojom::DialDeviceDescriptionPtr description_ptr,
+      const std::string& error_message) {
+    GURL app_url("http://192.168.1.1/apps");
+    auto device_data = CreateDialDeviceData(1);
+    auto description_data = CreateParsedDialDeviceDescription(1);
+
+    if (!error_message.empty()) {
+      EXPECT_CALL(mock_error_cb_, Run(device_data, error_message));
+    } else {
+      EXPECT_CALL(mock_success_cb_, Run(device_data, description_data));
+    }
+    device_description_service()->OnParsedDeviceDescription(
+        device_data, app_url, std::move(description_ptr));
+  }
+
+ protected:
+  base::MockCallback<
+      DeviceDescriptionService::DeviceDescriptionParseSuccessCallback>
+      mock_success_cb_;
+  base::MockCallback<
+      DeviceDescriptionService::DeviceDescriptionParseErrorCallback>
+      mock_error_cb_;
+
+  TestDeviceDescriptionService device_description_service_;
+  std::map<std::string, std::unique_ptr<DeviceDescriptionFetcher>>&
+      fetcher_map_;
+  std::map<std::string, DeviceDescriptionService::CacheEntry>&
+      description_cache_;
+
+  const content::TestBrowserThreadBundle thread_bundle_;
+  TestingProfile profile_;
+};
+
+TEST_F(DeviceDescriptionServiceTest, TestGetDeviceDescriptionFromCache) {
+  auto device_data = CreateDialDeviceData(1);
+  auto description_data = CreateParsedDialDeviceDescription(1);
+  EXPECT_CALL(mock_success_cb_, Run(device_data, description_data));
+
+  AddToCache(device_data.label(), description_data, false /* expired */);
+
+  std::vector<DialDeviceData> devices = {device_data};
+  device_description_service()->GetDeviceDescriptions(devices, nullptr);
+}
+
+TEST_F(DeviceDescriptionServiceTest, TestGetDeviceDescriptionFetchURL) {
+  DialDeviceData device_data = CreateDialDeviceData(1);
+  std::vector<DialDeviceData> devices = {device_data};
+
+  // Create Fetcher
+  EXPECT_TRUE(fetcher_map_.empty());
+  device_description_service()->GetDeviceDescriptions(
+      devices, profile_.GetRequestContext());
+  EXPECT_EQ(size_t(1), fetcher_map_.size());
+
+  // Remove fetcher and create safe parser
+  auto test_parser = base::MakeUnique<TestSafeDialDeviceDescriptionParser>();
+  EXPECT_CALL(*test_parser, Start(_, _));
+
+  SetTestParser(std::move(test_parser));
+  OnDeviceDescriptionFetchComplete(1);
+}
+
+TEST_F(DeviceDescriptionServiceTest, TestGetDeviceDescriptionFetchURLError) {
+  DialDeviceData device_data = CreateDialDeviceData(1);
+  std::vector<DialDeviceData> devices;
+  devices.push_back(device_data);
+
+  // Create Fetcher
+  EXPECT_TRUE(fetcher_map_.empty());
+  device_description_service()->GetDeviceDescriptions(
+      devices, profile_.GetRequestContext());
+  EXPECT_EQ(size_t(1), fetcher_map_.size());
+
+  EXPECT_CALL(mock_error_cb_, Run(device_data, ""));
+
+  device_description_service()->OnDeviceDescriptionFetchError(device_data, "");
+  EXPECT_TRUE(fetcher_map_.empty());
+}
+
+TEST_F(DeviceDescriptionServiceTest,
+       TestGetDeviceDescriptionRemoveOutDatedFetchers) {
+  DialDeviceData device_data_1 = CreateDialDeviceData(1);
+  DialDeviceData device_data_2 = CreateDialDeviceData(2);
+  DialDeviceData device_data_3 = CreateDialDeviceData(3);
+
+  std::vector<DialDeviceData> devices;
+  devices.push_back(device_data_1);
+  devices.push_back(device_data_2);
+
+  // insert fetchers
+  device_description_service()->GetDeviceDescriptions(
+      devices, profile_.GetRequestContext());
+
+  // Keep fetchers non exist in current device list and remove fetchers with
+  // different description url.
+  GURL new_url_2 = GURL("http://example.com");
+  device_data_2.set_device_description_url(new_url_2);
+
+  devices.clear();
+  devices.push_back(device_data_2);
+  devices.push_back(device_data_3);
+  device_description_service()->GetDeviceDescriptions(
+      devices, profile_.GetRequestContext());
+
+  EXPECT_EQ(size_t(3), fetcher_map_.size());
+
+  auto* description_fetcher = fetcher_map_[device_data_2.label()].get();
+  EXPECT_EQ(new_url_2, description_fetcher->device_description_url());
+
+  EXPECT_CALL(mock_error_cb_, Run(_, _)).Times(3);
+  device_description_service()->OnDeviceDescriptionFetchError(device_data_1,
+                                                              "");
+  device_description_service()->OnDeviceDescriptionFetchError(device_data_2,
+                                                              "");
+  device_description_service()->OnDeviceDescriptionFetchError(device_data_3,
+                                                              "");
+}
+
+TEST_F(DeviceDescriptionServiceTest, TestCleanUpCacheEntries) {
+  DialDeviceData device_data_1 = CreateDialDeviceData(1);
+  DialDeviceData device_data_2 = CreateDialDeviceData(2);
+  DialDeviceData device_data_3 = CreateDialDeviceData(3);
+
+  AddToCache(device_data_1.label(), ParsedDialDeviceDescription(),
+             true /* expired */);
+  AddToCache(device_data_2.label(), ParsedDialDeviceDescription(),
+             true /* expired */);
+  AddToCache(device_data_3.label(), ParsedDialDeviceDescription(),
+             false /* expired */);
+
+  device_description_service_.CleanUpCacheEntries();
+  EXPECT_EQ(size_t(1), description_cache_.size());
+  EXPECT_TRUE(base::ContainsKey(description_cache_, device_data_3.label()));
+
+  AddToCache(device_data_3.label(), ParsedDialDeviceDescription(),
+             true /* expired*/);
+  device_description_service_.CleanUpCacheEntries();
+  EXPECT_TRUE(description_cache_.empty());
+}
+
+TEST_F(DeviceDescriptionServiceTest, TestOnParsedDeviceDescription) {
+  GURL app_url("http://192.168.1.1/apps");
+  DialDeviceData device_data = CreateDialDeviceData(1);
+
+  // null_ptr
+  std::string error_message = "Failed to parse device description XML";
+  TestOnParsedDeviceDescription(nullptr, error_message);
+
+  // Empty field
+  error_message = "Failed to process fetch result";
+  TestOnParsedDeviceDescription(chrome::mojom::DialDeviceDescription::New(),
+                                error_message);
+
+  // Valid device description ptr and put in cache
+  auto description_ptr = CreateDialDeviceDescriptionPtr(1);
+  TestOnParsedDeviceDescription(std::move(description_ptr), "");
+  EXPECT_EQ(size_t(1), description_cache_.size());
+
+  // Valid device description ptr and skip cache.
+  size_t cache_num = 256;
+  for (size_t i = 0; i < cache_num; i++) {
+    AddToCache(std::to_string(i), ParsedDialDeviceDescription(),
+               false /* expired */);
+  }
+
+  EXPECT_EQ(size_t(cache_num + 1), description_cache_.size());
+  description_ptr = CreateDialDeviceDescriptionPtr(1);
+  TestOnParsedDeviceDescription(std::move(description_ptr), "");
+  EXPECT_EQ(size_t(cache_num + 1), description_cache_.size());
+}
+
+TEST_F(DeviceDescriptionServiceTest, TestSafeParserProperlyCreated) {
+  DialDeviceData device_data_1 = CreateDialDeviceData(1);
+  DialDeviceData device_data_2 = CreateDialDeviceData(2);
+  DialDeviceData device_data_3 = CreateDialDeviceData(3);
+
+  std::vector<DialDeviceData> devices = {device_data_1, device_data_2,
+                                         device_data_3};
+
+  // insert fetchers
+  device_description_service()->GetDeviceDescriptions(
+      devices, profile_.GetRequestContext());
+  auto test_parser = base::MakeUnique<TestSafeDialDeviceDescriptionParser>();
+  EXPECT_CALL(*test_parser, Start(_, _)).Times(3);
+
+  EXPECT_FALSE(device_description_service()->parser_);
+  SetTestParser(std::move(test_parser));
+  OnDeviceDescriptionFetchComplete(1);
+
+  EXPECT_TRUE(device_description_service()->parser_);
+  OnDeviceDescriptionFetchComplete(2);
+  OnDeviceDescriptionFetchComplete(3);
+
+  EXPECT_FALSE(device_description_service()->parser_);
+}
+
+}  // namespace media_router
diff --git a/chrome/browser/media/router/discovery/dial/dial_device_data.cc b/chrome/browser/media/router/discovery/dial/dial_device_data.cc
index c130b834..56ca213 100644
--- a/chrome/browser/media/router/discovery/dial/dial_device_data.cc
+++ b/chrome/browser/media/router/discovery/dial/dial_device_data.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "chrome/browser/media/router/discovery/dial/dial_device_data.h"
+#include "net/base/ip_address.h"
 
 namespace media_router {
 
@@ -31,7 +32,16 @@
 
 // static
 bool DialDeviceData::IsDeviceDescriptionUrl(const GURL& url) {
-  return url.is_valid() && !url.is_empty() && url.SchemeIsHTTPOrHTTPS();
+  if (!url.is_valid() || url.is_empty() || !url.SchemeIsHTTPOrHTTPS())
+    return false;
+
+  net::IPAddress address;
+  if (!net::ParseURLHostnameToAddress(url.host(), &address))
+    return false;
+
+  // TODO(crbug.com/679432): check that this IP address matches the address that
+  // we received the SSDP advertisement from.
+  return address.IsReserved();
 }
 
 bool DialDeviceData::UpdateFrom(const DialDeviceData& new_data) {
diff --git a/chrome/browser/media/router/discovery/dial/dial_device_data_unittest.cc b/chrome/browser/media/router/discovery/dial/dial_device_data_unittest.cc
index ac78065..edc95dfc 100644
--- a/chrome/browser/media/router/discovery/dial/dial_device_data_unittest.cc
+++ b/chrome/browser/media/router/discovery/dial/dial_device_data_unittest.cc
@@ -69,9 +69,9 @@
 }
 
 TEST(DialDeviceDataTest, TestIsDeviceDescriptionUrl) {
-  EXPECT_TRUE(DialDeviceData::IsDeviceDescriptionUrl(
+  EXPECT_FALSE(DialDeviceData::IsDeviceDescriptionUrl(
       GURL("http://some.device.com/dd.xml")));
-  EXPECT_TRUE(DialDeviceData::IsDeviceDescriptionUrl(
+  EXPECT_FALSE(DialDeviceData::IsDeviceDescriptionUrl(
       GURL("https://some.device.com/dd.xml")));
   EXPECT_TRUE(DialDeviceData::IsDeviceDescriptionUrl(
       GURL("http://192.168.1.1:1234/dd.xml")));
diff --git a/chrome/browser/media/router/discovery/dial/dial_media_sink_service.cc b/chrome/browser/media/router/discovery/dial/dial_media_sink_service.cc
new file mode 100644
index 0000000..562dcd5
--- /dev/null
+++ b/chrome/browser/media/router/discovery/dial/dial_media_sink_service.cc
@@ -0,0 +1,147 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/media/router/discovery/dial/dial_media_sink_service.h"
+
+#include "chrome/browser/media/router/discovery/dial/dial_device_data.h"
+#include "chrome/browser/profiles/profile.h"
+#include "content/public/browser/browser_thread.h"
+#include "net/url_request/url_request_context_getter.h"
+
+using content::BrowserThread;
+
+namespace {
+// Time interval when media sink service sends sinks to MRP.
+const int kFetchCompleteTimeoutSecs = 3;
+}
+
+namespace media_router {
+
+DialMediaSinkService::DialMediaSinkService(
+    const OnSinksDiscoveredCallback& callback,
+    net::URLRequestContextGetter* request_context)
+    : MediaSinkService(callback), request_context_(request_context) {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  DCHECK(request_context_);
+}
+
+DialMediaSinkService::~DialMediaSinkService() {}
+
+void DialMediaSinkService::Start() {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  dial_registry()->RegisterObserver(this);
+  dial_registry()->StartPeriodicDiscovery();
+}
+
+void DialMediaSinkService::Stop() {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  dial_registry()->UnregisterObserver(this);
+}
+
+DialRegistry* DialMediaSinkService::dial_registry() {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  return DialRegistry::GetInstance();
+}
+
+DeviceDescriptionService* DialMediaSinkService::GetDescriptionService() {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  if (!description_service_.get()) {
+    description_service_.reset(new DeviceDescriptionService(
+        base::Bind(&DialMediaSinkService::OnDeviceDescriptionAvailable,
+                   base::Unretained(this)),
+        base::Bind(&DialMediaSinkService::OnDeviceDescriptionError,
+                   base::Unretained(this))));
+  }
+  return description_service_.get();
+}
+
+void DialMediaSinkService::OnDialDeviceEvent(
+    const DialRegistry::DeviceList& devices) {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  DVLOG(2) << "DialMediaSinkService::OnDialDeviceEvent found " << devices.size()
+           << " devices";
+
+  // Add a finish timer.
+  finish_timer_.reset(new base::OneShotTimer());
+  base::TimeDelta finish_delay =
+      base::TimeDelta::FromSeconds(kFetchCompleteTimeoutSecs);
+  finish_timer_->Start(FROM_HERE, finish_delay, this,
+                       &DialMediaSinkService::OnFetchCompleted);
+
+  current_sinks_.clear();
+  current_devices_ = devices;
+
+  GetDescriptionService()->GetDeviceDescriptions(devices,
+                                                 request_context_.get());
+}
+
+void DialMediaSinkService::OnDialError(DialRegistry::DialErrorCode type) {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  DVLOG(2) << "OnDialError [DialErrorCode]: " << static_cast<int>(type);
+}
+
+void DialMediaSinkService::OnDeviceDescriptionAvailable(
+    const DialDeviceData& device_data,
+    const ParsedDialDeviceDescription& description_data) {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+
+  if (!base::ContainsValue(current_devices_, device_data)) {
+    DVLOG(2) << "Device data not found in current device data list...";
+    return;
+  }
+
+  // When use this "sink" within browser, please note it will have a different
+  // ID when it is sent to the extension, because it derives a different sink ID
+  // using the given sink ID.
+  MediaSink sink(description_data.unique_id, description_data.friendly_name,
+                 MediaSink::IconType::GENERIC);
+  DialSinkExtraData extra_data;
+  extra_data.app_url = description_data.app_url;
+  extra_data.model_name = description_data.model_name;
+  std::string ip_address = device_data.device_description_url().host();
+  if (!extra_data.ip_address.AssignFromIPLiteral(ip_address)) {
+    DVLOG(1) << "Invalid ip_address: " << ip_address;
+    return;
+  }
+
+  current_sinks_.insert(MediaSinkInternal(sink, extra_data));
+
+  if (finish_timer_)
+    return;
+
+  // Start fetch timer again if device description comes back after
+  // |finish_timer_| fires.
+  base::TimeDelta finish_delay =
+      base::TimeDelta::FromSeconds(kFetchCompleteTimeoutSecs);
+  finish_timer_.reset(new base::OneShotTimer());
+  finish_timer_->Start(FROM_HERE, finish_delay, this,
+                       &DialMediaSinkService::OnFetchCompleted);
+}
+
+void DialMediaSinkService::OnDeviceDescriptionError(
+    const DialDeviceData& device,
+    const std::string& error_message) {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  DVLOG(2) << "OnDescriptionFetchesError [message]: " << error_message;
+}
+
+void DialMediaSinkService::OnFetchCompleted() {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  DCHECK(!sink_discovery_callback_.is_null());
+
+  finish_timer_.reset();
+
+  auto sinks = current_sinks_;
+  if (sinks == mrp_sinks_) {
+    DVLOG(2) << "No update to sink list.";
+    return;
+  }
+
+  DVLOG(2) << "Send sinks to media router, [size]: " << sinks.size();
+  sink_discovery_callback_.Run(
+      std::vector<MediaSinkInternal>(sinks.begin(), sinks.end()));
+  mrp_sinks_ = std::move(sinks);
+}
+
+}  // namespace media_router
diff --git a/chrome/browser/media/router/discovery/dial/dial_media_sink_service.h b/chrome/browser/media/router/discovery/dial/dial_media_sink_service.h
new file mode 100644
index 0000000..86f72c5
--- /dev/null
+++ b/chrome/browser/media/router/discovery/dial/dial_media_sink_service.h
@@ -0,0 +1,89 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_MEDIA_ROUTER_DISCOVERY_DIAL_DIAL_MEDIA_SINK_SERVICE_H_
+#define CHROME_BROWSER_MEDIA_ROUTER_DISCOVERY_DIAL_DIAL_MEDIA_SINK_SERVICE_H_
+
+#include <memory>
+#include <set>
+
+#include "chrome/browser/media/router/discovery/dial/device_description_service.h"
+#include "chrome/browser/media/router/discovery/dial/dial_registry.h"
+#include "chrome/common/media_router/discovery/media_sink_internal.h"
+#include "chrome/common/media_router/discovery/media_sink_service.h"
+
+namespace media_router {
+
+class DeviceDescriptionService;
+class DialRegistry;
+
+// A service which can be used to start background discovery and resolution of
+// DIAL devices (Smart TVs, Game Consoles, etc.).
+// This class is not thread safe. All methods must be called from the IO thread.
+class DialMediaSinkService : public MediaSinkService,
+                             public DialRegistry::Observer {
+ public:
+  DialMediaSinkService(const OnSinksDiscoveredCallback& callback,
+                       net::URLRequestContextGetter* request_context);
+  ~DialMediaSinkService() override;
+
+  // MediaSinkService implementation
+  void Start() override;
+
+  void Stop();
+
+ protected:
+  virtual DialRegistry* dial_registry();
+
+  // Returns instance of device description service. Create a new one if none
+  // exists.
+  virtual DeviceDescriptionService* GetDescriptionService();
+
+ private:
+  friend class DialMediaSinkServiceTest;
+  FRIEND_TEST_ALL_PREFIXES(DialMediaSinkServiceTest, TestStart);
+  FRIEND_TEST_ALL_PREFIXES(DialMediaSinkServiceTest, TestFetchCompleted);
+  FRIEND_TEST_ALL_PREFIXES(DialMediaSinkServiceTest, TestIsDifferent);
+  FRIEND_TEST_ALL_PREFIXES(DialMediaSinkServiceTest,
+                           TestOnDeviceDescriptionAvailable);
+
+  // api::dial::DialRegistry::Observer implementation
+  void OnDialDeviceEvent(const DialRegistry::DeviceList& devices) override;
+  void OnDialError(DialRegistry::DialErrorCode type) override;
+
+  // Called when description service successfully fetches and parses device
+  // description XML. Restart |finish_timer_| if it is not running.
+  void OnDeviceDescriptionAvailable(
+      const DialDeviceData& device_data,
+      const ParsedDialDeviceDescription& description_data);
+
+  // Called when fails to fetch or parse device description XML.
+  void OnDeviceDescriptionError(const DialDeviceData& device,
+                                const std::string& error_message);
+
+  // Called when |finish_timer_| expires.
+  void OnFetchCompleted();
+
+  // Timer for finishing fetching. Starts in |OnDialDeviceEvent()|, and expires
+  // 3 seconds later. If |OnDeviceDescriptionAvailable()| is called after
+  // |finish_timer_| expires, |finish_timer_| is restarted.
+  std::unique_ptr<base::OneShotTimer> finish_timer_;
+
+  std::unique_ptr<DeviceDescriptionService> description_service_;
+
+  // Sorted sinks from current round of discovery.
+  std::set<MediaSinkInternal> current_sinks_;
+
+  // Sorted sinks sent to Media Router Provider in last FetchCompleted().
+  std::set<MediaSinkInternal> mrp_sinks_;
+
+  // Device data list from current round of discovery.
+  DialRegistry::DeviceList current_devices_;
+
+  scoped_refptr<net::URLRequestContextGetter> request_context_;
+};
+
+}  // namespace media_router
+
+#endif  // CHROME_BROWSER_MEDIA_ROUTER_DISCOVERY_DIAL_DIAL_MEDIA_SINK_SERVICE_H_
diff --git a/chrome/browser/media/router/discovery/dial/dial_media_sink_service_unittest.cc b/chrome/browser/media/router/discovery/dial/dial_media_sink_service_unittest.cc
new file mode 100644
index 0000000..cf542a8
--- /dev/null
+++ b/chrome/browser/media/router/discovery/dial/dial_media_sink_service_unittest.cc
@@ -0,0 +1,232 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/media/router/discovery/dial/dial_media_sink_service.h"
+#include "base/test/mock_callback.h"
+#include "chrome/browser/media/router/discovery/dial/dial_device_data.h"
+#include "chrome/browser/media/router/discovery/dial/dial_registry.h"
+#include "chrome/browser/media/router/test_helper.h"
+#include "chrome/test/base/testing_profile.h"
+#include "content/public/test/test_browser_thread_bundle.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using ::testing::_;
+using ::testing::Return;
+
+namespace {
+
+media_router::DialSinkExtraData CreateDialSinkExtraData(
+    const std::string& model_name,
+    const std::string& ip_address,
+    const std::string& app_url) {
+  media_router::DialSinkExtraData dial_extra_data;
+  EXPECT_TRUE(dial_extra_data.ip_address.AssignFromIPLiteral(ip_address));
+  dial_extra_data.model_name = model_name;
+  dial_extra_data.app_url = GURL(app_url);
+  return dial_extra_data;
+}
+
+std::vector<media_router::MediaSinkInternal> CreateDialMediaSinks() {
+  media_router::MediaSink sink1("sink1", "sink_name_1",
+                                media_router::MediaSink::IconType::CAST);
+  media_router::DialSinkExtraData extra_data1 = CreateDialSinkExtraData(
+      "model_name1", "192.168.1.1", "https://example1.com");
+
+  media_router::MediaSink sink2("sink2", "sink_name_2",
+                                media_router::MediaSink::IconType::CAST);
+  media_router::DialSinkExtraData extra_data2 = CreateDialSinkExtraData(
+      "model_name2", "192.168.1.2", "https://example2.com");
+
+  std::vector<media_router::MediaSinkInternal> sinks;
+  sinks.push_back(media_router::MediaSinkInternal(sink1, extra_data1));
+  sinks.push_back(media_router::MediaSinkInternal(sink2, extra_data2));
+  return sinks;
+}
+
+}  // namespace
+
+namespace media_router {
+
+class TestDialRegistry : public DialRegistry {
+ public:
+  TestDialRegistry() {}
+  ~TestDialRegistry() {}
+
+  MOCK_METHOD1(RegisterObserver, void(DialRegistry::Observer* observer));
+  MOCK_METHOD1(UnregisterObserver, void(DialRegistry::Observer* observer));
+};
+
+class MockDeviceDescriptionService : public DeviceDescriptionService {
+ public:
+  MockDeviceDescriptionService(DeviceDescriptionParseSuccessCallback success_cb,
+                               DeviceDescriptionParseErrorCallback error_cb)
+      : DeviceDescriptionService(success_cb, error_cb) {}
+  ~MockDeviceDescriptionService() override {}
+
+  MOCK_METHOD2(GetDeviceDescriptions,
+               void(const std::vector<DialDeviceData>& devices,
+                    net::URLRequestContextGetter* request_context));
+};
+
+class TestDialMediaSinkService : public DialMediaSinkService {
+ public:
+  TestDialMediaSinkService(
+      const MediaSinkService::OnSinksDiscoveredCallback& callback,
+      net::URLRequestContextGetter* request_context,
+      TestDialRegistry* test_dial_registry,
+      MockDeviceDescriptionService* mock_description_service)
+      : DialMediaSinkService(callback, request_context),
+        test_dial_registry_(test_dial_registry),
+        mock_description_service_(mock_description_service) {}
+
+  DialRegistry* dial_registry() override { return test_dial_registry_; }
+
+  DeviceDescriptionService* GetDescriptionService() override {
+    return mock_description_service_;
+  }
+
+ private:
+  TestDialRegistry* test_dial_registry_;
+  MockDeviceDescriptionService* mock_description_service_;
+};
+
+class DialMediaSinkServiceTest : public ::testing::Test {
+ public:
+  DialMediaSinkServiceTest()
+      : thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP),
+        mock_description_service_(mock_success_cb_.Get(), mock_error_cb_.Get()),
+        media_sink_service_(mock_sink_discovered_cb_.Get(),
+                            profile_.GetRequestContext(),
+                            &test_dial_registry_,
+                            &mock_description_service_) {}
+
+  DialMediaSinkService* media_sink_service() { return &media_sink_service_; }
+
+  void TestFetchCompleted(const std::vector<MediaSinkInternal>& old_sinks,
+                          const std::vector<MediaSinkInternal>& new_sinks) {
+    media_sink_service()->mrp_sinks_ =
+        std::set<MediaSinkInternal>(old_sinks.begin(), old_sinks.end());
+    media_sink_service()->current_sinks_ =
+        std::set<MediaSinkInternal>(new_sinks.begin(), new_sinks.end());
+    EXPECT_CALL(mock_sink_discovered_cb_, Run(new_sinks));
+    media_sink_service()->OnFetchCompleted();
+  }
+
+ protected:
+  const content::TestBrowserThreadBundle thread_bundle_;
+  TestingProfile profile_;
+
+  base::MockCallback<MediaSinkService::OnSinksDiscoveredCallback>
+      mock_sink_discovered_cb_;
+  base::MockCallback<
+      MockDeviceDescriptionService::DeviceDescriptionParseSuccessCallback>
+      mock_success_cb_;
+  base::MockCallback<
+      MockDeviceDescriptionService::DeviceDescriptionParseErrorCallback>
+      mock_error_cb_;
+
+  TestDialRegistry test_dial_registry_;
+  MockDeviceDescriptionService mock_description_service_;
+
+  TestDialMediaSinkService media_sink_service_;
+};
+
+TEST_F(DialMediaSinkServiceTest, TestStart) {
+  EXPECT_CALL(test_dial_registry_, RegisterObserver(&media_sink_service_));
+  media_sink_service()->Start();
+
+  DialRegistry::DeviceList deviceList;
+  DialDeviceData first_device("first", GURL("http://127.0.0.1/dd.xml"),
+                              base::Time::Now());
+  DialDeviceData second_device("second", GURL("http://127.0.0.2/dd.xml"),
+                               base::Time::Now());
+  DialDeviceData third_device("third", GURL("http://127.0.0.3/dd.xml"),
+                              base::Time::Now());
+  deviceList.push_back(first_device);
+  deviceList.push_back(second_device);
+  deviceList.push_back(third_device);
+
+  EXPECT_CALL(mock_description_service_, GetDeviceDescriptions(deviceList, _));
+
+  media_sink_service()->OnDialDeviceEvent(deviceList);
+  EXPECT_TRUE(media_sink_service_.finish_timer_->IsRunning());
+}
+
+TEST_F(DialMediaSinkServiceTest, TestOnDeviceDescriptionAvailable) {
+  DialDeviceData device_data("first", GURL("http://127.0.0.1/dd.xml"),
+                             base::Time::Now());
+  ParsedDialDeviceDescription device_description;
+  device_description.model_name = "model name";
+  device_description.friendly_name = "friendly name";
+  device_description.app_url = GURL("http://192.168.1.1/apps");
+  device_description.unique_id = "unique id";
+
+  media_sink_service()->OnDeviceDescriptionAvailable(device_data,
+                                                     device_description);
+  EXPECT_TRUE(media_sink_service()->current_sinks_.empty());
+
+  std::vector<DialDeviceData> deviceList{device_data};
+  EXPECT_CALL(mock_description_service_, GetDeviceDescriptions(deviceList, _));
+
+  media_sink_service()->OnDialDeviceEvent(deviceList);
+  media_sink_service()->OnDeviceDescriptionAvailable(device_data,
+                                                     device_description);
+
+  EXPECT_EQ(size_t(1), media_sink_service()->current_sinks_.size());
+}
+
+TEST_F(DialMediaSinkServiceTest, TestFetchCompleted) {
+  std::vector<MediaSinkInternal> old_sinks;
+  std::vector<MediaSinkInternal> new_sinks = CreateDialMediaSinks();
+  TestFetchCompleted(old_sinks, new_sinks);
+
+  // Same sink
+  EXPECT_CALL(mock_sink_discovered_cb_, Run(new_sinks)).Times(0);
+  media_sink_service()->OnFetchCompleted();
+}
+
+TEST_F(DialMediaSinkServiceTest, TestFetchCompleted_OneNewSink) {
+  std::vector<MediaSinkInternal> old_sinks = CreateDialMediaSinks();
+  std::vector<MediaSinkInternal> new_sinks = CreateDialMediaSinks();
+  MediaSink sink3("sink3", "sink_name_3", MediaSink::IconType::CAST);
+  DialSinkExtraData extra_data3 = CreateDialSinkExtraData(
+      "model_name3", "192.168.1.3", "https://example3.com");
+  new_sinks.push_back(MediaSinkInternal(sink3, extra_data3));
+  TestFetchCompleted(old_sinks, new_sinks);
+}
+
+TEST_F(DialMediaSinkServiceTest, TestFetchCompleted_RemovedOneSink) {
+  std::vector<MediaSinkInternal> old_sinks = CreateDialMediaSinks();
+  std::vector<MediaSinkInternal> new_sinks = CreateDialMediaSinks();
+  new_sinks.erase(new_sinks.begin());
+  TestFetchCompleted(old_sinks, new_sinks);
+}
+
+TEST_F(DialMediaSinkServiceTest, TestFetchCompleted_UpdatedOneSink) {
+  std::vector<MediaSinkInternal> old_sinks = CreateDialMediaSinks();
+  std::vector<MediaSinkInternal> new_sinks = CreateDialMediaSinks();
+  new_sinks[0].set_name("sink_name_4");
+  TestFetchCompleted(old_sinks, new_sinks);
+}
+
+TEST_F(DialMediaSinkServiceTest, TestFetchCompleted_Mixed) {
+  std::vector<MediaSinkInternal> old_sinks = CreateDialMediaSinks();
+
+  MediaSink sink1("sink1", "sink_name_1", MediaSink::IconType::CAST);
+  DialSinkExtraData extra_data2 = CreateDialSinkExtraData(
+      "model_name2", "192.168.1.2", "https://example2.com");
+
+  MediaSink sink3("sink3", "sink_name_3", MediaSink::IconType::CAST);
+  DialSinkExtraData extra_data3 = CreateDialSinkExtraData(
+      "model_name3", "192.168.1.3", "https://example3.com");
+
+  std::vector<MediaSinkInternal> new_sinks;
+  new_sinks.push_back(MediaSinkInternal(sink1, extra_data2));
+  new_sinks.push_back(MediaSinkInternal(sink3, extra_data3));
+
+  TestFetchCompleted(old_sinks, new_sinks);
+}
+
+}  // namespace media_router
diff --git a/chrome/browser/media/router/discovery/dial/dial_registry.h b/chrome/browser/media/router/discovery/dial/dial_registry.h
index 96253372..c1f885c2 100644
--- a/chrome/browser/media/router/discovery/dial/dial_registry.h
+++ b/chrome/browser/media/router/discovery/dial/dial_registry.h
@@ -63,9 +63,9 @@
   void OnListenerRemoved();
 
   // pass a reference of |observer| to allow it to notify on DIAL device events.
-  // This class does not take ownership of |observer|.
-  void RegisterObserver(Observer* observer);
-  void UnregisterObserver(Observer* observer);
+  // This class does not take ownership of observer.
+  virtual void RegisterObserver(Observer* observer);
+  virtual void UnregisterObserver(Observer* observer);
 
   // Called by the DIAL API to try to kickoff a discovery if there is not one
   // already active.
@@ -102,6 +102,7 @@
   using DeviceByLabelMap = std::map<std::string, DialDeviceData*>;
 
   friend class MockDialRegistry;
+  friend class TestDialRegistry;
   friend struct base::DefaultSingletonTraits<DialRegistry>;
 
   DialRegistry();
diff --git a/chrome/browser/media/router/discovery/dial/parsed_dial_device_description.cc b/chrome/browser/media/router/discovery/dial/parsed_dial_device_description.cc
new file mode 100644
index 0000000..a72e9863
--- /dev/null
+++ b/chrome/browser/media/router/discovery/dial/parsed_dial_device_description.cc
@@ -0,0 +1,40 @@
+// Copyright (c) 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/media/router/discovery/dial/parsed_dial_device_description.h"
+
+namespace media_router {
+
+ParsedDialDeviceDescription::ParsedDialDeviceDescription() = default;
+
+ParsedDialDeviceDescription::ParsedDialDeviceDescription(
+    const std::string& unique_id,
+    const std::string& friendly_name,
+    const GURL& app_url)
+    : unique_id(unique_id), friendly_name(friendly_name), app_url(app_url) {}
+
+ParsedDialDeviceDescription::ParsedDialDeviceDescription(
+    const ParsedDialDeviceDescription& other) = default;
+ParsedDialDeviceDescription::~ParsedDialDeviceDescription() = default;
+
+ParsedDialDeviceDescription& ParsedDialDeviceDescription::operator=(
+    const ParsedDialDeviceDescription& other) {
+  if (this == &other)
+    return *this;
+
+  this->unique_id = other.unique_id;
+  this->friendly_name = other.friendly_name;
+  this->model_name = other.model_name;
+  this->app_url = other.app_url;
+
+  return *this;
+}
+
+bool ParsedDialDeviceDescription::operator==(
+    const ParsedDialDeviceDescription& other) const {
+  return unique_id == other.unique_id && friendly_name == other.friendly_name &&
+         model_name == other.model_name && app_url == other.app_url;
+}
+
+}  // namespace media_router
diff --git a/chrome/browser/media/router/discovery/dial/parsed_dial_device_description.h b/chrome/browser/media/router/discovery/dial/parsed_dial_device_description.h
new file mode 100644
index 0000000..4c066a17
--- /dev/null
+++ b/chrome/browser/media/router/discovery/dial/parsed_dial_device_description.h
@@ -0,0 +1,41 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_MEDIA_ROUTER_DISCOVERY_DIAL_PARSED_DIAL_DEVICE_DESCRIPTION_H_
+#define CHROME_BROWSER_MEDIA_ROUTER_DISCOVERY_DIAL_PARSED_DIAL_DEVICE_DESCRIPTION_H_
+
+#include <string>
+#include "url/gurl.h"
+
+namespace media_router {
+
+struct ParsedDialDeviceDescription {
+  ParsedDialDeviceDescription();
+  ParsedDialDeviceDescription(const std::string& unique_id,
+                              const std::string& friendly_name,
+                              const GURL& app_url);
+  ParsedDialDeviceDescription(const ParsedDialDeviceDescription& other);
+  ~ParsedDialDeviceDescription();
+
+  ParsedDialDeviceDescription& operator=(
+      const ParsedDialDeviceDescription& other);
+
+  bool operator==(const ParsedDialDeviceDescription& other) const;
+
+  // UUID (UDN).
+  std::string unique_id;
+
+  // Short user-friendly device name.
+  std::string friendly_name;
+
+  // Device model name.
+  std::string model_name;
+
+  // The DIAL application URL (used to launch DIAL applications).
+  GURL app_url;
+};
+
+}  // namespace media_router
+
+#endif  // CHROME_BROWSER_MEDIA_ROUTER_DISCOVERY_DIAL_PARSED_DIAL_DEVICE_DESCRIPTION_H_
diff --git a/chrome/browser/media/router/discovery/dial/safe_dial_device_description_parser.cc b/chrome/browser/media/router/discovery/dial/safe_dial_device_description_parser.cc
index 81ea291..bb2cccc 100644
--- a/chrome/browser/media/router/discovery/dial/safe_dial_device_description_parser.cc
+++ b/chrome/browser/media/router/discovery/dial/safe_dial_device_description_parser.cc
@@ -19,51 +19,30 @@
 void SafeDialDeviceDescriptionParser::Start(
     const std::string& xml_text,
     const DeviceDescriptionCallback& callback) {
+  DVLOG(2) << "Start parsing device description...";
   DCHECK(thread_checker_.CalledOnValidThread());
-  DCHECK(!utility_process_mojo_client_);
+
   DCHECK(callback);
 
-  device_description_callback_ = callback;
+  if (!utility_process_mojo_client_) {
+    DVLOG(2) << "Start utility process in background...";
+    utility_process_mojo_client_ =
+        base::MakeUnique<content::UtilityProcessMojoClient<
+            chrome::mojom::DialDeviceDescriptionParser>>(
+            l10n_util::GetStringUTF16(
+                IDS_UTILITY_PROCESS_DIAL_DEVICE_DESCRIPTION_PARSER_NAME));
 
-  utility_process_mojo_client_ =
-      base::MakeUnique<content::UtilityProcessMojoClient<
-          chrome::mojom::DialDeviceDescriptionParser>>(
-          l10n_util::GetStringUTF16(
-              IDS_UTILITY_PROCESS_DIAL_DEVICE_DESCRIPTION_PARSER_NAME));
+    utility_process_mojo_client_->set_error_callback(
+        base::Bind(callback, nullptr));
 
-  utility_process_mojo_client_->set_error_callback(base::Bind(
-      &SafeDialDeviceDescriptionParser::OnParseDeviceDescriptionFailed,
-      base::Unretained(this)));
-
-  // This starts utility process in the background.
-  utility_process_mojo_client_->Start();
+    // This starts utility process in the background.
+    utility_process_mojo_client_->Start();
+  }
 
   // This call is queued up until the Mojo message pipe has been established to
   // the service running in the utility process.
-  utility_process_mojo_client_->service()->ParseDialDeviceDescription(
-      xml_text,
-      base::Bind(
-          &SafeDialDeviceDescriptionParser::OnParseDeviceDescriptionComplete,
-          base::Unretained(this)));
-}
-
-void SafeDialDeviceDescriptionParser::OnParseDeviceDescriptionComplete(
-    chrome::mojom::DialDeviceDescriptionPtr device_description) {
-  DCHECK(thread_checker_.CalledOnValidThread());
-
-  utility_process_mojo_client_.reset();  // Terminate the utility process.
-
-  DCHECK(device_description_callback_);
-  device_description_callback_.Run(std::move(device_description));
-}
-
-void SafeDialDeviceDescriptionParser::OnParseDeviceDescriptionFailed() {
-  DCHECK(thread_checker_.CalledOnValidThread());
-
-  utility_process_mojo_client_.reset();  // Terminate the utility process.
-
-  DCHECK(device_description_callback_);
-  device_description_callback_.Run(nullptr);
+  utility_process_mojo_client_->service()->ParseDialDeviceDescription(xml_text,
+                                                                      callback);
 }
 
 }  // namespace media_router
diff --git a/chrome/browser/media/router/discovery/dial/safe_dial_device_description_parser.h b/chrome/browser/media/router/discovery/dial/safe_dial_device_description_parser.h
index 360725b6..172d89d0 100644
--- a/chrome/browser/media/router/discovery/dial/safe_dial_device_description_parser.h
+++ b/chrome/browser/media/router/discovery/dial/safe_dial_device_description_parser.h
@@ -10,6 +10,7 @@
 
 #include "base/callback.h"
 #include "base/macros.h"
+#include "base/memory/ref_counted.h"
 #include "base/optional.h"
 #include "base/threading/thread_checker.h"
 #include "chrome/common/media_router/mojo/dial_device_description_parser.mojom.h"
@@ -29,31 +30,21 @@
       ParseDialDeviceDescriptionCallback;
 
   SafeDialDeviceDescriptionParser();
+  virtual ~SafeDialDeviceDescriptionParser();
 
   // Start parsing device description XML file in utility process.
-  void Start(const std::string& xml_text,
-             const DeviceDescriptionCallback& callback);
-
- private:
-  ~SafeDialDeviceDescriptionParser();
-
-  // See SafeDialDeviceDescriptionParser::DeviceDescriptionCallback
-  void OnParseDeviceDescriptionComplete(
-      chrome::mojom::DialDeviceDescriptionPtr device_description);
-
   // TODO(crbug.com/702766): Add an enum type describing why utility process
   // fails to parse device description xml.
-  void OnParseDeviceDescriptionFailed();
+  virtual void Start(const std::string& xml_text,
+                     const DeviceDescriptionCallback& callback);
 
+ private:
   // Utility client used to send device description parsing task to the utility
   // process.
   std::unique_ptr<content::UtilityProcessMojoClient<
       chrome::mojom::DialDeviceDescriptionParser>>
       utility_process_mojo_client_;
 
-  // Only accessed on the IO thread.
-  DeviceDescriptionCallback device_description_callback_;
-
   base::ThreadChecker thread_checker_;
 
   DISALLOW_COPY_AND_ASSIGN(SafeDialDeviceDescriptionParser);
diff --git a/chrome/browser/media/router/media_sink_service.h b/chrome/browser/media/router/media_sink_service.h
deleted file mode 100644
index 9b910c6..0000000
--- a/chrome/browser/media/router/media_sink_service.h
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright 2017 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef CHROME_BROWSER_MEDIA_ROUTER_MEDIA_SINK_SERVICE_H_
-#define CHROME_BROWSER_MEDIA_ROUTER_MEDIA_SINK_SERVICE_H_
-
-#include <memory>
-#include <vector>
-
-#include "base/callback.h"
-#include "chrome/common/media_router/media_sink.h"
-
-namespace media_router {
-
-class MediaSinksObserver;
-
-// A service which can be used to start background discovery and resolution of
-// MediaSinks. Often these are remote devices, like Chromecast. In addition, the
-// service is capable of answering MediaSink queries using the sinks that it
-// generated.
-// This class is not thread safe. All methods must be called from the IO thread.
-class MediaSinkService {
- public:
-  // Callback to be invoked when this class finishes sink discovering.
-  // Arg 0: Sinks discovered and resolved by the service.
-  using OnSinksDiscoveredCallback =
-      base::Callback<void(const std::vector<MediaSink>&)>;
-
-  explicit MediaSinkService(
-      const OnSinksDiscoveredCallback& sinks_discovered_callback);
-
-  virtual ~MediaSinkService();
-
-  // Starts sink discovery. No-ops if already started.
-  // Sinks discovered and resolved are continuously passed to
-  // |callback|.
-  virtual void Start() = 0;
-
-  // Adds a sink query to observe for MediaSink updates.
-  // Multiple observers can be added for a given MediaSource.
-  // Start() must be called first. This class does not take
-  // ownership of |observer|.
-  virtual void AddSinkQuery(MediaSinksObserver* observer) = 0;
-
-  // Removes a sink query and stops observing MediaSink updates. No-op if
-  // |observer| is not registered with this class.
-  virtual void RemoveSinkQuery(MediaSinksObserver* observer) = 0;
-
- protected:
-  OnSinksDiscoveredCallback sinks_discovered_callback_;
-
-  DISALLOW_COPY_AND_ASSIGN(MediaSinkService);
-};
-
-}  // namespace media_router
-
-#endif  // CHROME_BROWSER_MEDIA_ROUTER_MEDIA_SINK_SERVICE_H_
diff --git a/chrome/common/media_router/BUILD.gn b/chrome/common/media_router/BUILD.gn
index 5616d272..ed0f520b 100644
--- a/chrome/common/media_router/BUILD.gn
+++ b/chrome/common/media_router/BUILD.gn
@@ -15,6 +15,8 @@
   sources = [
     "discovery/media_sink_internal.cc",
     "discovery/media_sink_internal.h",
+    "discovery/media_sink_service.cc",
+    "discovery/media_sink_service.h",
     "issue.cc",
     "issue.h",
     "media_route.cc",
diff --git a/chrome/common/media_router/discovery/media_sink_internal.cc b/chrome/common/media_router/discovery/media_sink_internal.cc
index 3b9a77f..0cab52e6 100644
--- a/chrome/common/media_router/discovery/media_sink_internal.cc
+++ b/chrome/common/media_router/discovery/media_sink_internal.cc
@@ -64,6 +64,14 @@
   return false;
 }
 
+bool MediaSinkInternal::operator!=(const MediaSinkInternal& other) const {
+  return !operator==(other);
+}
+
+bool MediaSinkInternal::operator<(const MediaSinkInternal& other) const {
+  return sink_.id() < other.sink().id();
+}
+
 void MediaSinkInternal::set_sink(const MediaSink& sink) {
   sink_ = sink;
 }
diff --git a/chrome/common/media_router/discovery/media_sink_internal.h b/chrome/common/media_router/discovery/media_sink_internal.h
index 3a9c2bb6..25d7b617 100644
--- a/chrome/common/media_router/discovery/media_sink_internal.h
+++ b/chrome/common/media_router/discovery/media_sink_internal.h
@@ -73,6 +73,9 @@
 
   MediaSinkInternal& operator=(const MediaSinkInternal& other);
   bool operator==(const MediaSinkInternal& other) const;
+  bool operator!=(const MediaSinkInternal& other) const;
+  // Sorted by sink id.
+  bool operator<(const MediaSinkInternal& other) const;
 
   // Used by mojo.
   void set_sink_id(const MediaSink::Id& sink_id) { sink_.set_sink_id(sink_id); }
diff --git a/chrome/common/media_router/discovery/media_sink_internal_unittest.cc b/chrome/common/media_router/discovery/media_sink_internal_unittest.cc
index 220ecda..2267f6e 100644
--- a/chrome/common/media_router/discovery/media_sink_internal_unittest.cc
+++ b/chrome/common/media_router/discovery/media_sink_internal_unittest.cc
@@ -3,67 +3,61 @@
 // found in the LICENSE file.
 
 #include "chrome/common/media_router/discovery/media_sink_internal.h"
-
-#include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+namespace {
+constexpr char kSinkId[] = "sinkId123";
+constexpr char kSinkName[] = "The sink";
+constexpr char kIPAddress[] = "192.168.1.2";
+constexpr char kModelName[] = "model name";
+constexpr char kAppUrl[] = "https://example.com";
+
+media_router::DialSinkExtraData CreateDialSinkExtraData(
+    const std::string& model_name,
+    const std::string& ip_address,
+    const std::string& app_url) {
+  media_router::DialSinkExtraData dial_extra_data;
+  EXPECT_TRUE(dial_extra_data.ip_address.AssignFromIPLiteral(ip_address));
+  dial_extra_data.model_name = model_name;
+  dial_extra_data.app_url = GURL(app_url);
+
+  return dial_extra_data;
+}
+
+media_router::CastSinkExtraData CreateCastSinkExtraData(
+    const std::string& model_name,
+    const std::string& ip_address,
+    uint8_t capabilities,
+    int cast_channel_id) {
+  media_router::CastSinkExtraData cast_extra_data;
+  EXPECT_TRUE(cast_extra_data.ip_address.AssignFromIPLiteral(ip_address));
+  cast_extra_data.model_name = model_name;
+  cast_extra_data.capabilities = 2;
+  cast_extra_data.cast_channel_id = 3;
+  return cast_extra_data;
+}
+
+// static
+media_router::DialSinkExtraData CreateDialSinkExtraData() {
+  return CreateDialSinkExtraData(kModelName, kIPAddress, kAppUrl);
+}
+
+// static
+media_router::CastSinkExtraData CreateCastSinkExtraData() {
+  return CreateCastSinkExtraData(kModelName, kIPAddress, 2, 3);
+}
+
+}  // namespace
+
 namespace media_router {
 
-class MediaSinkInternalTest : public ::testing::Test {
- public:
-  MediaSinkInternalTest()
-      : media_sink_(sink_id_, sink_name_, MediaSink::IconType::CAST) {}
-
-  DialSinkExtraData CreateDialSinkExtraData() {
-    return CreateDialSinkExtraData(model_name_, ip_address_, app_url_);
-  }
-
-  DialSinkExtraData CreateDialSinkExtraData(const std::string& model_name,
-                                            const std::string& ip_address,
-                                            const std::string& app_url) {
-    DialSinkExtraData dial_extra_data;
-    EXPECT_TRUE(dial_extra_data.ip_address.AssignFromIPLiteral(ip_address));
-    dial_extra_data.model_name = model_name;
-    dial_extra_data.app_url = GURL(app_url);
-
-    return dial_extra_data;
-  }
-
-  CastSinkExtraData CreateCastSinkExtraData() {
-    return CreateCastSinkExtraData(model_name_, ip_address_, 2, 3);
-  }
-
-  CastSinkExtraData CreateCastSinkExtraData(const std::string& model_name,
-                                            const std::string& ip_address,
-                                            uint8_t capabilities,
-                                            int cast_channel_id) {
-    CastSinkExtraData cast_extra_data;
-    EXPECT_TRUE(cast_extra_data.ip_address.AssignFromIPLiteral(ip_address));
-    cast_extra_data.model_name = model_name;
-    cast_extra_data.capabilities = 2;
-    cast_extra_data.cast_channel_id = 3;
-    return cast_extra_data;
-  }
-
-  MediaSink media_sink() { return media_sink_; }
-
- private:
-  std::string sink_id_ = "sinkId123";
-  std::string sink_name_ = "The sink";
-  std::string ip_address_ = "192.168.1.2";
-  std::string model_name_ = "model name";
-  std::string app_url_ = "https://example.com";
-
-  MediaSink media_sink_;
-};
-
-TEST_F(MediaSinkInternalTest, TestIsValidSinkId) {
+TEST(MediaSinkInternalTest, TestIsValidSinkId) {
   EXPECT_FALSE(MediaSinkInternal::IsValidSinkId(""));
   EXPECT_TRUE(MediaSinkInternal::IsValidSinkId("rjuKv_yxhY4jg7QBIp0kbngLjR6A"));
 }
 
-TEST_F(MediaSinkInternalTest, TestConstructorAndAssignment) {
-  MediaSink sink = media_sink();
+TEST(MediaSinkInternalTest, TestConstructorAndAssignment) {
+  MediaSink sink(kSinkId, kSinkName, MediaSink::IconType::CAST);
   DialSinkExtraData dial_extra_data = CreateDialSinkExtraData();
   CastSinkExtraData cast_extra_data = CreateCastSinkExtraData();
 
@@ -99,8 +93,8 @@
   }
 }
 
-TEST_F(MediaSinkInternalTest, TestSetExtraData) {
-  MediaSink sink = media_sink();
+TEST(MediaSinkInternalTest, TestSetExtraData) {
+  MediaSink sink(kSinkId, kSinkName, MediaSink::IconType::CAST);
   DialSinkExtraData dial_extra_data = CreateDialSinkExtraData();
   CastSinkExtraData cast_extra_data = CreateCastSinkExtraData();
 
diff --git a/chrome/browser/media/router/media_sink_service.cc b/chrome/common/media_router/discovery/media_sink_service.cc
similarity index 61%
rename from chrome/browser/media/router/media_sink_service.cc
rename to chrome/common/media_router/discovery/media_sink_service.cc
index 80f15a53..ea86eea 100644
--- a/chrome/browser/media/router/media_sink_service.cc
+++ b/chrome/common/media_router/discovery/media_sink_service.cc
@@ -2,13 +2,13 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "chrome/browser/media/router/media_sink_service.h"
+#include "chrome/common/media_router/discovery/media_sink_service.h"
 
 namespace media_router {
 
 MediaSinkService::MediaSinkService(
-    const OnSinksDiscoveredCallback& sinks_discovered_callback)
-    : sinks_discovered_callback_(sinks_discovered_callback) {}
+    const OnSinksDiscoveredCallback& sink_discovery_callback)
+    : sink_discovery_callback_(sink_discovery_callback) {}
 
 MediaSinkService::~MediaSinkService() = default;
 
diff --git a/chrome/common/media_router/discovery/media_sink_service.h b/chrome/common/media_router/discovery/media_sink_service.h
new file mode 100644
index 0000000..5e2f0bc
--- /dev/null
+++ b/chrome/common/media_router/discovery/media_sink_service.h
@@ -0,0 +1,47 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_COMMON_MEDIA_ROUTER_DISCOVERY_MEDIA_SINK_SERVICE_H_
+#define CHROME_COMMON_MEDIA_ROUTER_DISCOVERY_MEDIA_SINK_SERVICE_H_
+
+#include <memory>
+#include <vector>
+
+#include "base/callback.h"
+#include "chrome/common/media_router/discovery/media_sink_internal.h"
+#include "chrome/common/media_router/media_sink.h"
+
+namespace media_router {
+
+// A service which can be used to start background discovery and resolution of
+// MediaSinks. Often these are remote devices, like Chromecast. In addition, the
+// service is capable of answering MediaSink queries using the sinks that it
+// generated.
+// This class is not thread safe. All methods must be called from the IO thread.
+class MediaSinkService {
+ public:
+  // Callback to be invoked when this class finishes sink discovering.
+  // Arg 0: Sinks discovered and resolved by the service.
+  using OnSinksDiscoveredCallback =
+      base::Callback<void(const std::vector<MediaSinkInternal>&)>;
+
+  explicit MediaSinkService(
+      const OnSinksDiscoveredCallback& sink_discovery_callback);
+
+  virtual ~MediaSinkService();
+
+  // Starts sink discovery. No-ops if already started.
+  // Sinks discovered and resolved are continuously passed to
+  // |callback|.
+  virtual void Start() = 0;
+
+ protected:
+  OnSinksDiscoveredCallback sink_discovery_callback_;
+
+  DISALLOW_COPY_AND_ASSIGN(MediaSinkService);
+};
+
+}  // namespace media_router
+
+#endif  // CHROME_COMMON_MEDIA_ROUTER_DISCOVERY_MEDIA_SINK_SERVICE_H_
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 787f95b..60e5b5a 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -3615,7 +3615,9 @@
       "../browser/download/download_dir_policy_handler_unittest.cc",
       "../browser/lifetime/keep_alive_registry_unittest.cc",
       "../browser/media/router/discovery/dial/device_description_fetcher_unittest.cc",
+      "../browser/media/router/discovery/dial/device_description_service_unittest.cc",
       "../browser/media/router/discovery/dial/dial_device_data_unittest.cc",
+      "../browser/media/router/discovery/dial/dial_media_sink_service_unittest.cc",
       "../browser/media/router/discovery/dial/dial_registry_unittest.cc",
       "../browser/media/router/discovery/dial/dial_service_unittest.cc",