diff --git a/core/modules/media/config/schema/media.schema.yml b/core/modules/media/config/schema/media.schema.yml
index b9156b23f8..f4b8a8c235 100644
--- a/core/modules/media/config/schema/media.schema.yml
+++ b/core/modules/media/config/schema/media.schema.yml
@@ -40,6 +40,21 @@ field.formatter.settings.media_thumbnail:
   type: field.formatter.settings.image
   label: 'Media thumbnail field display format settings'
 
+field.formatter.settings.oembed:
+  type: mapping
+  label: 'oEmbed display format settings'
+  mapping:
+    max_width:
+      type: integer
+      label: 'Maximum width'
+    max_height:
+      type: integer
+      label: 'Maximum height'
+
+field.widget.settings.oembed_textfield:
+  type: field.widget.settings.string_textfield
+  label: 'oEmbed widget format settings'
+
 media.source.*:
   type: mapping
   label: 'Media source settings'
@@ -60,6 +75,20 @@ media.source.video_file:
   type: media.source.field_aware
   label: '"Video" media source configuration'
 
+media.source.oembed:*:
+  type: media.source.field_aware
+  label: 'oEmbed media source configuration'
+  mapping:
+    thumbnails_location:
+      type: string
+      label: 'Thumbnails location'
+    allowed_providers:
+      type: sequence
+      label: 'Allowed oEmbed providers'
+      sequence:
+        type: string
+        label: 'Provider name'
+
 media.source.field_aware:
   type: mapping
   mapping:
diff --git a/core/modules/media/media.api.php b/core/modules/media/media.api.php
index 8de1c64575..579702aec6 100644
--- a/core/modules/media/media.api.php
+++ b/core/modules/media/media.api.php
@@ -20,6 +20,18 @@ function hook_media_source_info_alter(array &$sources) {
   $sources['youtube']['label'] = t('Youtube rocks!');
 }
 
+/**
+ * Alters the parsed URL before fetching the oEmbed data.
+ *
+ * @param array $parsed_url
+ *   UrlHelper::parse() provided array.
+ * @param \Drupal\media\OEmbed\Provider $provider
+ *   The current oEmbed provider.
+ */
+function hook_oembed_resource_url_alter(array &$parsed_url, \Drupal\media\OEmbed\Provider $provider) {
+  $parsed_url['query']['foo'] = 'bar';
+}
+
 /**
  * @} End of "addtogroup hooks".
  */
diff --git a/core/modules/media/media.module b/core/modules/media/media.module
index 296945973a..fc02a0072d 100644
--- a/core/modules/media/media.module
+++ b/core/modules/media/media.module
@@ -13,6 +13,7 @@
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
 use Drupal\field\FieldConfigInterface;
+use Drupal\media\Plugin\media\Source\OEmbedInterface;
 
 /**
  * Implements hook_help().
@@ -86,6 +87,7 @@ function media_entity_access(EntityInterface $entity, $operation, AccountInterfa
  */
 function media_theme_suggestions_media(array $variables) {
   $suggestions = [];
+  /** @var \Drupal\media\MediaInterface $media */
   $media = $variables['elements']['#media'];
   $sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');
 
@@ -93,6 +95,13 @@ function media_theme_suggestions_media(array $variables) {
   $suggestions[] = 'media__' . $media->bundle();
   $suggestions[] = 'media__' . $media->bundle() . '__' . $sanitized_view_mode;
 
+  $source = $media->getSource();
+  if ($source instanceof OEmbedInterface) {
+    $provider_id = $source->getMetadata($media, 'provider_id');
+    $suggestions[] = 'media__oembed';
+    $suggestions[] = 'media__oembed__' . strtolower($provider_id);
+  }
+
   return $suggestions;
 }
 
diff --git a/core/modules/media/media.services.yml b/core/modules/media/media.services.yml
index f22f90a124..395294214e 100644
--- a/core/modules/media/media.services.yml
+++ b/core/modules/media/media.services.yml
@@ -2,9 +2,17 @@ services:
   plugin.manager.media.source:
     class: Drupal\media\MediaSourceManager
     parent: default_plugin_manager
-
   access_check.media.revision:
     class: Drupal\media\Access\MediaRevisionAccessCheck
     arguments: ['@entity_type.manager']
     tags:
       - { name: access_check, applies_to: _access_media_revision }
+  media.oembed_manager:
+    class: Drupal\media\OEmbed\OEmbedManager
+    arguments: ['@media.oembed.provider_collector', '@media.oembed.resource_fetcher', '@http_client', '@module_handler', '@cache.default']
+  media.oembed.provider_collector:
+    class: Drupal\media\OEmbed\ProviderCollector
+    arguments: ['@http_client', 'https://oembed.com/providers.json', '@datetime.time', '@cache.default']
+  media.oembed.resource_fetcher:
+    class: Drupal\media\OEmbed\ResourceFetcher
+    arguments: ['@http_client', '@cache.default']
diff --git a/core/modules/media/src/MediaSourceBase.php b/core/modules/media/src/MediaSourceBase.php
index 1edc858450..3bb3c6699a 100644
--- a/core/modules/media/src/MediaSourceBase.php
+++ b/core/modules/media/src/MediaSourceBase.php
@@ -301,7 +301,9 @@ public function createSourceField(MediaTypeInterface $type) {
    *   returned. Otherwise, a new, unused one is generated.
    */
   protected function getSourceFieldName() {
-    $base_id = 'field_media_' . $this->getPluginId();
+    // Some media sources are using a deriver, so their plugin IDs are
+    // containing a ':' which is not allowed for field names.
+    $base_id = 'field_media_' . str_replace(':', '_', $this->getPluginId());
     $tries = 0;
     $storage = $this->entityTypeManager->getStorage('field_storage_config');
 
diff --git a/core/modules/media/src/OEmbed/Endpoint.php b/core/modules/media/src/OEmbed/Endpoint.php
new file mode 100644
index 0000000000..f860d51be5
--- /dev/null
+++ b/core/modules/media/src/OEmbed/Endpoint.php
@@ -0,0 +1,168 @@
+<?php
+
+namespace Drupal\media\OEmbed;
+
+use Drupal\Component\Utility\UrlHelper;
+
+/**
+ * Value object for oEmbed provider endpoints.
+ */
+class Endpoint {
+
+  /**
+   * The endpoint's URL.
+   *
+   * @var string
+   */
+  protected $url;
+
+  /**
+   * The provider this endpoint belongs to.
+   *
+   * @var \Drupal\media\OEmbed\Provider
+   */
+  protected $provider;
+
+  /**
+   * List of URL schemes supported by the provider.
+   *
+   * @var string[]
+   */
+  protected $schemes;
+
+  /**
+   * List of supported formats.
+   *
+   * @var string[]
+   *
+   * @see https://oembed.com/#section2
+   */
+  protected $formats;
+
+  /**
+   * Whether the provider supports oEmbed discovery.
+   *
+   * @var bool
+   */
+  protected $supportsDiscovery;
+
+  /**
+   * Endpoint constructor.
+   *
+   * @param string $url
+   *   The endpoint's URL.
+   * @param \Drupal\media\OEmbed\Provider $provider
+   *   The provider this endpoint belongs to.
+   * @param array $schemes
+   *   List of URL schemes supported by the provider.
+   * @param array $formats
+   *   List of supported formats. Can be "json", "xml" or both.
+   * @param bool $supports_discovery
+   *   Whether the provider supports oEmbed discovery.
+   *
+   * @throws \InvalidArgumentException
+   */
+  public function __construct($url, Provider $provider, array $schemes = [], array $formats = [], $supports_discovery = FALSE) {
+    if (!$url) {
+      throw new \InvalidArgumentException('oEmbed endpoint must have a URL');
+    }
+    $this->url = $url;
+    $this->provider = $provider;
+    $this->schemes = $schemes;
+    $this->formats = $formats;
+    $this->supportsDiscovery = $supports_discovery;
+  }
+
+  /**
+   * Returns the endpoint's URL.
+   *
+   * The URL will be built with the first defined format. If the endpoint
+   * doesn't provide a format, the JSON format will be used.
+   *
+   * @return string
+   *   Endpoint URL.
+   */
+  public function getUrl() {
+    $format = 'json';
+    if (!empty($this->formats)) {
+      // If the endpoint specifies multiple formats we use the fist one.
+      $format = reset($this->formats);
+    }
+    return str_replace('{format}', $format, $this->url);
+  }
+
+  /**
+   * Returns the provider this endpoint belongs to.
+   *
+   * @return \Drupal\media\OEmbed\Provider
+   *   The provider object.
+   */
+  public function getProvider() {
+    return $this->provider;
+  }
+
+  /**
+   * Returns list of URL schemes supported by the provider.
+   *
+   * @return string[]
+   *   List of schemes.
+   */
+  public function getSchemes() {
+    return $this->schemes;
+  }
+
+  /**
+   * Returns list of supported formats.
+   *
+   * @return string[]
+   *   List of formats.
+   */
+  public function getFormats() {
+    return $this->formats;
+  }
+
+  /**
+   * Returns whether the provider supports oEmbed discovery.
+   *
+   * @return bool
+   *   Returns TRUE if the provides discovery, otherwise FALSE.
+   */
+  public function supportsDiscovery() {
+    return (bool) $this->supportsDiscovery;
+  }
+
+  /**
+   * Tries to match a URL against the endpoint schemes.
+   *
+   * @param string $url
+   *   Media item URL.
+   *
+   * @return bool
+   *   TRUE if the URL matches against the endpoint schemes, otherwise FALSE.
+   */
+  public function matchUrl($url) {
+    foreach ($this->getSchemes() as $scheme) {
+      // Convert scheme into a valid regular expression.
+      $regexp = str_replace(['.', '*'], ['\.', '.*'], $scheme);
+      if (preg_match("|$regexp|", $url)) {
+        return TRUE;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * Builds and returns the endpoint URL.
+   *
+   * @param string $url
+   *   The canonical media URL.
+   *
+   * @return string
+   *   URL of the oEmbed endpoint.
+   */
+  public function buildResourceUrl($url) {
+    $query = ['url' => $url];
+    return $this->getUrl() . '?' . UrlHelper::buildQuery($query);
+  }
+
+}
diff --git a/core/modules/media/src/OEmbed/OEmbedManager.php b/core/modules/media/src/OEmbed/OEmbedManager.php
new file mode 100644
index 0000000000..409f0b4602
--- /dev/null
+++ b/core/modules/media/src/OEmbed/OEmbedManager.php
@@ -0,0 +1,183 @@
+<?php
+
+namespace Drupal\media\OEmbed;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\UseCacheBackendTrait;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\RequestException;
+
+/**
+ * The OEmbedManager service contains oEmbed functions.
+ */
+class OEmbedManager implements OEmbedManagerInterface {
+
+  use UseCacheBackendTrait;
+
+  /**
+   * The HTTP client.
+   *
+   * @var \GuzzleHttp\Client
+   */
+  protected $httpClient;
+
+  /**
+   * The OEmbed provider collector service.
+   *
+   * @var \Drupal\media\OEmbed\ProviderCollectorInterface
+   */
+  protected $providerCollector;
+
+  /**
+   * The OEmbed resource fetcher service.
+   *
+   * @var \Drupal\media\OEmbed\ResourceFetcherInterface
+   */
+  protected $resourceFetcher;
+
+  /**
+   * The module handler service.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * Static cache of discovered oEmbed resources.
+   *
+   * A discovered oEmbed resource is the oEmbed URL for a specific media object.
+   * This oEmbed URL is fetched from the canonical URL of the media object.
+   *
+   * @var array
+   */
+  protected $urlCache;
+
+  /**
+   * Constructs OEmbedManager class.
+   *
+   * @param \Drupal\media\OEmbed\ProviderCollectorInterface $provider_collector
+   *   The OEmbed provider collector service.
+   * @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
+   *   The OEmbed resource fetcher service.
+   * @param \GuzzleHttp\Client $http_client
+   *   The HTTP client.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler service.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   (optional) The cache backend.
+   */
+  public function __construct(ProviderCollectorInterface $provider_collector, ResourceFetcherInterface $resource_fetcher, Client $http_client, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend = NULL) {
+    $this->providerCollector = $provider_collector;
+    $this->resourceFetcher = $resource_fetcher;
+    $this->httpClient = $http_client;
+    $this->moduleHandler = $module_handler;
+    $this->cacheBackend = $cache_backend;
+    $this->useCaches = isset($cache_backend);
+  }
+
+  /**
+   * Runs oEmbed discovery and returns the endpoint URL if successful.
+   *
+   * @param string $url
+   *   The resource's URL.
+   *
+   * @return string|bool
+   *   URL of the oEmbed endpoint, or FALSE if the discovery was not successful.
+   *
+   * @throws \Drupal\media\OEmbed\ResourceException
+   */
+  protected function discoverResourceUrl($url) {
+    try {
+      $response = $this->httpClient->get($url);
+    }
+    catch (RequestException $e) {
+      throw new ResourceException('Could not fetch oEmbed resource.', $url, [], $e);
+    }
+
+    $document = Html::load((string) $response->getBody());
+    $xpath = new \DOMXpath($document);
+
+    return $this->findUrl($xpath, 'json') ?: $this->findUrl($xpath, 'xml');
+  }
+
+  /**
+   * Tries to find the oEmbed URL in a DOM.
+   *
+   * @param \DOMXPath $xpath
+   *   Page HTML as DOMXPath.
+   * @param string $format
+   *   Format of oEmbed resource. Possible values are 'json' and 'xml'.
+   *
+   * @return bool|string
+   *   A URL to an oEmbed resource or FALSE if not found.
+   */
+  protected function findUrl(\DOMXPath $xpath, $format) {
+    $result = $xpath->query("//link[@type='application/$format+oembed']");
+    return $result->length ? $result->item(0)->getAttribute('href') : FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProviderByUrl($url) {
+    // Check the URL against every scheme of every endpoint of every provider
+    // until we find a match.
+    foreach ($this->providerCollector->getAll() as $provider_name => $provider_info) {
+      foreach ($provider_info->getEndpoints() as $endpoint) {
+        if ($endpoint->matchUrl($url)) {
+          return $provider_info;
+        }
+      }
+    }
+
+    if ($resource_url = $this->discoverResourceUrl($url)) {
+      $resource = $this->resourceFetcher->fetchResource($resource_url);
+      return $this->providerCollector->get($resource['provider_name']);
+    }
+    throw new ResourceException('No matching provider found.', $url);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getResourceUrl($url, $max_width = NULL, $max_height = NULL) {
+    $cache_id = 'media:oembed_resource_url:' . hash('sha256', $url);
+
+    if ($this->urlCache === NULL) {
+      $this->urlCache = [];
+      if ($cached = $this->cacheGet($cache_id)) {
+        $this->urlCache[$url] = $cached->data;
+      }
+    }
+
+    if (isset($this->urlCache[$url])) {
+      return $this->urlCache[$url];
+    }
+
+    $provider = $this->getProviderByUrl($url);
+    $endpoints = $provider->getEndpoints();
+    $endpoint = reset($endpoints);
+    $resource_url = $endpoint->buildResourceUrl($url);
+
+    $parsed_url = UrlHelper::parse($resource_url);
+    if ($max_width) {
+      $parsed_url['query']['max_width'] = $max_width;
+    }
+    if ($max_height) {
+      $parsed_url['query']['max_height'] = $max_height;
+    }
+    // Let other modules alter the query string, because some oEmbed providers
+    // are providing extra parameters. For example, Instagram also supports the
+    // 'omitscript' parameter.
+    $this->moduleHandler->alter('oembed_resource_url', $parsed_url, $provider);
+    $resource_url = $parsed_url['path'] . '?' . UrlHelper::buildQuery($parsed_url['query']);
+
+    $this->urlCache[$url] = $resource_url;
+    $this->cacheSet($cache_id, $resource_url);
+    return $resource_url;
+  }
+
+}
diff --git a/core/modules/media/src/OEmbed/OEmbedManagerInterface.php b/core/modules/media/src/OEmbed/OEmbedManagerInterface.php
new file mode 100644
index 0000000000..4638bcb70a
--- /dev/null
+++ b/core/modules/media/src/OEmbed/OEmbedManagerInterface.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Drupal\media\OEmbed;
+
+/**
+ * Interface for OEmbedManager service.
+ */
+interface OEmbedManagerInterface {
+
+  /**
+   * Tries to get provider info from the provider list.
+   *
+   * @param string $url
+   *   The URL of a media object to match against the oEmbed providers.
+   *
+   * @return \Drupal\media\OEmbed\Provider
+   *   Returns a single provider if found, otherwise FALSE.
+   *
+   * @see \Drupal\media\ProviderCollectorInterface::get()
+   *
+   * @throws \Drupal\media\OEmbed\ResourceException
+   *   Exception will be thrown if no provider could be found for the provided
+   *   URL.
+   */
+  public function getProviderByUrl($url);
+
+  /**
+   * Builds the resource URL for a given media item URL.
+   *
+   * @param string $url
+   *   The URL to the media item.
+   * @param int|null $max_width
+   *   Max width for the oEmbed resource.
+   * @param int|null $max_height
+   *   Max height for the oEmbed resource.
+   *
+   * @return string
+   *   Returns the resource URL corresponding to the given media item URL.
+   */
+  public function getResourceUrl($url, $max_width = NULL, $max_height = NULL);
+
+}
diff --git a/core/modules/media/src/OEmbed/Provider.php b/core/modules/media/src/OEmbed/Provider.php
new file mode 100644
index 0000000000..ddbae78564
--- /dev/null
+++ b/core/modules/media/src/OEmbed/Provider.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Drupal\media\OEmbed;
+
+use Drupal\Component\Utility\UrlHelper;
+
+/**
+ * Value object for oEmbed providers.
+ */
+class Provider {
+
+  /**
+   * The provider name.
+   *
+   * @var string
+   */
+  protected $name;
+
+  /**
+   * The provider URL.
+   *
+   * @var string
+   */
+  protected $url;
+
+  /**
+   * The provider endpoints.
+   *
+   * @var \Drupal\media\OEmbed\Endpoint[]
+   */
+  protected $endpoints = [];
+
+  /**
+   * Provider constructor.
+   *
+   * @param string $name
+   *   The provider name.
+   * @param string $url
+   *   The provider $url.
+   * @param array $endpoints
+   *   List of endpoints this provider exposes.
+   *
+   * @throws \Drupal\media\OEmbed\ProviderException
+   */
+  public function __construct($name, $url, array $endpoints) {
+    if (empty($endpoints)) {
+      throw new ProviderException('Provider @name does not define any endpoints.', $this);
+    }
+
+    $this->name = $name;
+    $this->url = $url;
+    $this->endpoints = $endpoints;
+  }
+
+  /**
+   * Returns the provider name.
+   *
+   * @return string
+   *   Name of the provider.
+   */
+  public function getName() {
+    return $this->name;
+  }
+
+  /**
+   * Returns the provider URL.
+   *
+   * @return string
+   *   URL of the provider.
+   */
+  public function getUrl() {
+    return $this->url;
+  }
+
+  /**
+   * Returns the provider endpoints.
+   *
+   * @return \Drupal\media\OEmbed\Endpoint[]
+   *   List of endpoints this provider exposes.
+   */
+  public function getEndpoints() {
+    $endpoints = [];
+    foreach ($this->endpoints as $endpoint) {
+      $endpoint += ['formats' => [], 'schemes' => [], 'discovery' => FALSE];
+      $endpoints[] = new Endpoint($endpoint['url'], $this, $endpoint['schemes'], $endpoint['formats'], $endpoint['discovery']);
+    }
+    return $endpoints;
+  }
+
+}
diff --git a/core/modules/media/src/OEmbed/ProviderCollector.php b/core/modules/media/src/OEmbed/ProviderCollector.php
new file mode 100644
index 0000000000..9d764b4ee7
--- /dev/null
+++ b/core/modules/media/src/OEmbed/ProviderCollector.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Drupal\media\OEmbed;
+
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Cache\CacheBackendInterface;
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\RequestException;
+
+/**
+ * Retrieves and caches information about oEmbed providers.
+ */
+class ProviderCollector implements ProviderCollectorInterface {
+
+  /**
+   * Cache provider list for a week.
+   *
+   * @var int
+   */
+  const MAX_AGE = 604800;
+
+  /**
+   * The HTTP client.
+   *
+   * @var \GuzzleHttp\Client
+   */
+  protected $httpClient;
+
+  /**
+   * URL of the JSON with providers info.
+   *
+   * @var string
+   */
+  protected $providersUrl;
+
+  /**
+   * The time service.
+   *
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $time;
+
+  /**
+   * Cache backend instance.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $cacheBackend;
+
+  /**
+   * Constructs OEmbed class.
+   *
+   * @param \GuzzleHttp\Client $http_client
+   *   The HTTP client.
+   * @param string $providers_url
+   *   The URL of the remote database of oEmbed providers.
+   * @param \Drupal\Component\Datetime\TimeInterface $time
+   *   The time service.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   (optional) The cache backend.
+   */
+  public function __construct(Client $http_client, $providers_url, TimeInterface $time, CacheBackendInterface $cache_backend = NULL) {
+    $this->httpClient = $http_client;
+    $this->providersUrl = $providers_url;
+    $this->time = $time;
+    $this->cacheBackend = $cache_backend;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAll() {
+    $cache_id = 'media:oembed_providers';
+
+    $cached = $this->cacheBackend->get($cache_id);
+    if ($cached) {
+      return $cached->data;
+    }
+
+    try {
+      $response = $this->httpClient->request('GET', $this->providersUrl);
+    }
+    catch (RequestException $e) {
+      throw new ProviderException("Could not retrieve the oEmbed provider database from $this->providersUrl", NULL, $e);
+    }
+
+    $providers = Json::decode((string) $response->getBody());
+
+    if (!is_array($providers) || empty($providers)) {
+      throw new ProviderException('Remote oEmbed providers database returned invalid or empty list.');
+    }
+
+    $keyed_providers = [];
+    foreach ($providers as $provider) {
+      $name = $provider['provider_name'];
+      $keyed_providers[$name] = new Provider($provider['provider_name'], $provider['provider_url'], $provider['endpoints']);
+    }
+
+    $this->cacheBackend->set($cache_id, $keyed_providers, $this->time->getCurrentTime() + static::MAX_AGE);
+    return $keyed_providers;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get($provider_name) {
+    $providers = $this->getAll();
+
+    if (!isset($providers[$provider_name])) {
+      throw new \InvalidArgumentException("Unknown provider '$provider_name'");
+    }
+    return $providers[$provider_name];
+  }
+
+}
diff --git a/core/modules/media/src/OEmbed/ProviderCollectorInterface.php b/core/modules/media/src/OEmbed/ProviderCollectorInterface.php
new file mode 100644
index 0000000000..e70a11f12c
--- /dev/null
+++ b/core/modules/media/src/OEmbed/ProviderCollectorInterface.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\media\OEmbed;
+
+/**
+ * Interface for OEmbedProviderCollector service.
+ *
+ * The provider collector is only responsible to call oembed.com/providers.json
+ * and generate Provider objects out of it.
+ */
+interface ProviderCollectorInterface {
+
+  /**
+   * Gets oEmbed providers information.
+   *
+   * Returns an array of Provider objects, keyed by provider name and has
+   * the values like in Drupal\media\OEmbed\ProviderDiscoveryInterface::get().
+   *
+   * @return \Drupal\media\OEmbed\Provider[]
+   *   Returns an array of Provider objects, keyed by provider name.
+   *
+   * @see \Drupal\media\ProviderCollectorInterface::get()
+   *
+   * @throws \Drupal\media\OEmbed\ProviderException
+   *   Throws an exception if the oEmbed API is not reachable or returns
+   *   unexpected content.
+   */
+  public function getAll();
+
+  /**
+   * Gets information for a specific oEmbed provider.
+   *
+   * @param string $provider_name
+   *   The name of the provider.
+   *
+   * @return \Drupal\media\OEmbed\Provider
+   *   The provider information.
+   *
+   * @throws \InvalidArgumentException
+   *   If there is no known oEmbed provider with the specified name.
+   */
+  public function get($provider_name);
+
+}
diff --git a/core/modules/media/src/OEmbed/ProviderException.php b/core/modules/media/src/OEmbed/ProviderException.php
new file mode 100644
index 0000000000..1907e00ef6
--- /dev/null
+++ b/core/modules/media/src/OEmbed/ProviderException.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\media\OEmbed;
+
+/**
+ * Exception thrown if an oEmbed provider causes an error.
+ */
+class ProviderException extends \Exception {
+
+  /**
+   * Information about the oEmbed provider which caused the exception.
+   *
+   * @var \Drupal\media\OEmbed\Provider
+   *
+   * @see \Drupal\media\OEmbed\ProviderCollectorInterface::get()
+   */
+  protected $provider;
+
+  /**
+   * ProviderException constructor.
+   *
+   * @param string $message
+   *   The exception message. '@name' will be replaced with the provider name
+   *   if available, or '<unknown>' if not.
+   * @param \Drupal\media\OEmbed\Provider $provider
+   *   (optional) The provider information.
+   * @param \Exception $previous
+   *   (optional) The previous exception, if any.
+   */
+  public function __construct($message, Provider $provider = NULL, \Exception $previous = NULL) {
+    $this->provider = $provider;
+    $message = str_replace('@name', is_object($provider) ? $provider->getName() : '<unknown>', $message);
+    parent::__construct($message, 0, $previous);
+  }
+
+  /**
+   * Gets the provider information, if available.
+   *
+   * @return \Drupal\media\OEmbed\Provider
+   *   The information about the oEmbed provider which caused the exception.
+   */
+  public function getProvider() {
+    return $this->provider;
+  }
+
+}
diff --git a/core/modules/media/src/OEmbed/ResourceException.php b/core/modules/media/src/OEmbed/ResourceException.php
new file mode 100644
index 0000000000..f896926fac
--- /dev/null
+++ b/core/modules/media/src/OEmbed/ResourceException.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\media\OEmbed;
+
+/**
+ * Exception thrown if an oEmbed resource causes an error.
+ */
+class ResourceException extends \Exception {
+
+  /**
+   * The URL of the oEmbed resource which caused the exception.
+   *
+   * @var string
+   */
+  protected $url;
+
+  /**
+   * The oEmbed resource which caused the exception.
+   *
+   * @var array
+   */
+  protected $resource = [];
+
+  /**
+   * ResourceException constructor.
+   *
+   * @param string $message
+   *   The exception message.
+   * @param string $url
+   *   The URL of the oEmbed resource which caused the exception. Can be the
+   *   actual endpoint URL for the resource, or its canonical URL.
+   * @param array $resource
+   *   (optional) The oEmbed resource which caused the exception.
+   * @param \Exception $previous
+   *   (optional) The previous exception, if any.
+   */
+  public function __construct($message, $url, array $resource = [], \Exception $previous = NULL) {
+    $this->url = $url;
+    $this->resource = $resource;
+    parent::__construct($message, 0, $previous);
+  }
+
+  /**
+   * Gets the URL of the oEmbed resource which caused the exception.
+   *
+   * @return string
+   *   The URL of the oEmbed resource which caused the exception.
+   */
+  public function getUrl() {
+    return $this->url;
+  }
+
+  /**
+   * Gets the oEmbed resource which caused the exception, if available.
+   *
+   * @return array
+   *   The oEmbed resource which caused the exception.
+   */
+  public function getResource() {
+    return $this->resource;
+  }
+
+}
diff --git a/core/modules/media/src/OEmbed/ResourceFetcher.php b/core/modules/media/src/OEmbed/ResourceFetcher.php
new file mode 100644
index 0000000000..c89ffa5b63
--- /dev/null
+++ b/core/modules/media/src/OEmbed/ResourceFetcher.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Drupal\media\OEmbed;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Cache\UseCacheBackendTrait;
+use Drupal\Core\Url;
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\RequestException;
+
+/**
+ * Fetches and caches oEmbed resources.
+ */
+class ResourceFetcher implements ResourceFetcherInterface {
+
+  use UseCacheBackendTrait;
+
+  /**
+   * The HTTP client.
+   *
+   * @var \GuzzleHttp\Client
+   */
+  protected $httpClient;
+
+  /**
+   * Constructs ResourceFetcher class.
+   *
+   * @param \GuzzleHttp\Client $http_client
+   *   The HTTP client.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   (optional) The cache backend.
+   */
+  public function __construct(Client $http_client, CacheBackendInterface $cache_backend = NULL) {
+    $this->httpClient = $http_client;
+    $this->cacheBackend = $cache_backend;
+    $this->useCaches = isset($cache_backend);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fetchResource($url) {
+    $cache_id = 'media:oembed:' . hash('sha256', $url);
+
+    $cached = $this->cacheGet($cache_id);
+    if ($cached) {
+      return $cached->data;
+    }
+
+    try {
+      $response = $this->httpClient->get($url);
+    }
+    catch (RequestException $e) {
+      throw new ResourceException('Could not retrieve the oEmbed resource.', $url, [], $e);
+    }
+
+    $format = $response->getHeader('Content-Type');
+    $content = (string) $response->getBody();
+
+    // If the response is XML, magically convert it to JSON.
+    if (strpos($format[0], 'text/xml') !== FALSE) {
+      // Load XML into SimpleXMLElement.
+      $content = simplexml_load_string($content, "SimpleXMLElement", LIBXML_NOCDATA);
+      // Convert the SimpleXMLElement to JSON.
+      $content = Json::encode($content);
+    }
+    // If the response wasn't XML or JSON, we are in bat country.
+    elseif (strpos($format[0], 'application/json') === FALSE && strpos($format[0], 'text/javascript') === FALSE) {
+      throw new ResourceException('The fetched resource did not have a valid Content-Type header.', $url);
+    }
+
+    $resource = Json::decode($content);
+    $this->cacheSet($cache_id, $resource);
+    return $resource;
+  }
+
+}
diff --git a/core/modules/media/src/OEmbed/ResourceFetcherInterface.php b/core/modules/media/src/OEmbed/ResourceFetcherInterface.php
new file mode 100644
index 0000000000..3526b7fc88
--- /dev/null
+++ b/core/modules/media/src/OEmbed/ResourceFetcherInterface.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\media\OEmbed;
+
+/**
+ * Interface for ResourceFetcher service.
+ *
+ * The resource fetchers only responsibility is to get an oEmbed resource URL
+ * and returns the resource object.
+ */
+interface ResourceFetcherInterface {
+
+  /**
+   * Fetches information about the oEmbed resource.
+   *
+   * @param string $endpoint_url
+   *   Resource-specific URL of the oEmbed endpoint.
+   *
+   * @return array
+   *   Resource information as returned from the oEmbed endpoint, or FALSE if
+   *   the resource could not be fetched.
+   *
+   * @see https://oembed.com/#section2
+   *
+   * @throws \Drupal\media\OEmbed\ResourceException
+   *   Throws an exception if the oEmbed API is not reachable or the response
+   *   returns unexpected content type headers.
+   */
+  public function fetchResource($endpoint_url);
+
+}
diff --git a/core/modules/media/src/Plugin/Field/FieldFormatter/OEmbedFormatter.php b/core/modules/media/src/Plugin/Field/FieldFormatter/OEmbedFormatter.php
new file mode 100644
index 0000000000..d92fa1475a
--- /dev/null
+++ b/core/modules/media/src/Plugin/Field/FieldFormatter/OEmbedFormatter.php
@@ -0,0 +1,219 @@
+<?php
+
+namespace Drupal\media\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FormatterBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Url;
+use Drupal\media\OEmbed\OEmbedManagerInterface;
+use Drupal\media\OEmbed\ResourceException;
+use Drupal\media\OEmbed\ResourceFetcherInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Plugin implementation of the 'oembed' formatter.
+ *
+ * @FieldFormatter(
+ *   id = "oembed",
+ *   label = @Translation("oEmbed content"),
+ *   field_types = {
+ *     "link",
+ *     "string",
+ *     "string_long",
+ *   }
+ * )
+ */
+class OEmbedFormatter extends FormatterBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The oEmbed resource fetcher.
+   *
+   * @var \Drupal\media\OEmbed\ResourceFetcherInterface
+   */
+  protected $resourceFetcher;
+
+  /**
+   * The oEmbed manager.
+   *
+   * @var \Drupal\media\OEmbed\OEmbedManagerInterface
+   */
+  protected $oEmbedManager;
+
+  /**
+   * The logger service.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelInterface $logger
+   */
+  protected $logger;
+
+  /**
+   * Constructs a OEmbedFormatter instance.
+   *
+   * @param string $plugin_id
+   *   The plugin ID for the formatter.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The definition of the field to which the formatter is associated.
+   * @param array $settings
+   *   The formatter settings.
+   * @param string $label
+   *   The formatter label display setting.
+   * @param string $view_mode
+   *   The view mode.
+   * @param array $third_party_settings
+   *   Any third party settings settings.
+   * @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
+   *   The resource fetcher service.
+   * @param \Drupal\media\OEmbed\OEmbedManagerInterface $oembed_manager
+   *   The oEmbed manager service.
+   * @param \Drupal\Core\Logger\LoggerChannelFactory $logger_factory
+   *   The logger service.
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, ResourceFetcherInterface $resource_fetcher, OEmbedManagerInterface $oembed_manager, LoggerChannelFactoryInterface $logger_factory) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
+    $this->resourceFetcher = $resource_fetcher;
+    $this->oEmbedManager = $oembed_manager;
+    $this->logger = $logger_factory->get('media');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $plugin_id,
+      $plugin_definition,
+      $configuration['field_definition'],
+      $configuration['settings'],
+      $configuration['label'],
+      $configuration['view_mode'],
+      $configuration['third_party_settings'],
+      $container->get('media.oembed.resource_fetcher'),
+      $container->get('media.oembed_manager'),
+      $container->get('logger.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings() {
+    return [
+      'max_width' => 0,
+      'max_height' => 0,
+    ] + parent::defaultSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewElements(FieldItemListInterface $items, $langcode) {
+    $element = [];
+
+    foreach ($items as $delta => $item) {
+      $main_property = $item->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName();
+
+      if (empty($item->{$main_property})) {
+        continue;
+      }
+
+      try {
+        $resource = $this->resourceFetcher->fetchResource($this->oEmbedManager->getResourceUrl($item->{$main_property}, $this->getSetting('max_width'), $this->getSetting('max_height')));
+      }
+      catch (ResourceException $exception) {
+        $this->logger->error("Could not retrieve the remote URL (@url).", ['@url' => $item->{$main_property}]);
+        continue;
+      }
+
+      switch ($resource['type']) {
+        case 'link':
+          $element[$delta] = [
+            '#title' => $resource['title'],
+            '#type' => 'link',
+            '#url' => Url::fromUri($item->{$main_property}),
+          ];
+          break;
+
+        case 'photo':
+          $element[$delta] = [
+            '#theme' => 'image',
+            '#uri' => $resource['url'],
+            '#width' => $resource['width'],
+            '#height' => $resource['height'],
+          ];
+          break;
+
+        case 'video':
+        case 'rich':
+          $element[$delta] = [
+            '#type' => 'inline_template',
+            '#template' => (string) $resource['html'],
+          ];
+          break;
+
+        default:
+          $this->logger->error("Unknown oEmbed resource type @type", ['@type' => $resource['type']]);
+
+      }
+    }
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state) {
+    return parent::settingsForm($form, $form_state) + [
+      'max_width' => [
+        '#type' => 'number',
+        '#title' => $this->t('Maximum width'),
+        '#default_value' => $this->getSetting('max_width'),
+        '#size' => 5,
+        '#maxlength' => 5,
+        '#field_suffix' => $this->t('pixels'),
+        '#min' => 0,
+      ],
+      'max_height' => [
+        '#type' => 'number',
+        '#title' => $this->t('Maximum height'),
+        '#default_value' => $this->getSetting('max_height'),
+        '#size' => 5,
+        '#maxlength' => 5,
+        '#field_suffix' => $this->t('pixels'),
+        '#min' => 0,
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary() {
+    $summary = parent::settingsSummary();
+    if ($this->getSetting('max_width') && $this->getSetting('max_height')) {
+      $summary[] = $this->t('Maximum size: %max_width x %max_height pixels', [
+        '%max_width' => $this->getSetting('max_width'),
+        '%max_height' => $this->getSetting('max_height'),
+      ]);
+    }
+    elseif ($this->getSetting('max_width')) {
+      $summary[] = $this->t('Maximum width: %max_width pixels', [
+        '%max_width' => $this->getSetting('max_width'),
+      ]);
+    }
+    elseif ($this->getSetting('max_height')) {
+      $summary[] = $this->t('Maximum height: %max_height pixels', [
+        '%max_height' => $this->getSetting('max_height'),
+      ]);
+    }
+    return $summary;
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/Field/FieldWidget/OEmbedWidget.php b/core/modules/media/src/Plugin/Field/FieldWidget/OEmbedWidget.php
new file mode 100644
index 0000000000..c464bc97fa
--- /dev/null
+++ b/core/modules/media/src/Plugin/Field/FieldWidget/OEmbedWidget.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\media\Plugin\Field\FieldWidget;
+
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\Plugin\Field\FieldWidget\StringTextfieldWidget;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\media\Plugin\media\Source\OEmbedInterface;
+
+/**
+ * Plugin implementation of the 'oembed_textfield' widget.
+ *
+ * @FieldWidget(
+ *   id = "oembed_textfield",
+ *   label = @Translation("oEmbed Textfield"),
+ *   field_types = {
+ *     "string",
+ *   },
+ * )
+ */
+class OEmbedWidget extends StringTextfieldWidget {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+    $element = parent::formElement($items, $delta, $element, $form, $form_state);
+
+    /** @var \Drupal\media\Plugin\media\Source\OEmbedInterface $source */
+    $source = $items->getEntity()->getSource();
+    $message = t('You can link to media from the following services: @providers', ['@providers' => implode(', ', $source->getAllowedProviders())]);
+
+    $element['value']['#description'] = [
+      '#theme' => 'item_list',
+      '#items' => !empty($element['value']['#description']) ? [$element['value']['#description'], $message] : [$message],
+    ];
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function isApplicable(FieldDefinitionInterface $field_definition) {
+    if (!parent::isApplicable($field_definition) || $field_definition->getTargetEntityTypeId() !== 'media' || !$field_definition->getTargetBundle()) {
+      return FALSE;
+    }
+    /** @var \Drupal\media\MediaTypeInterface $media_type */
+    $media_type = \Drupal::entityTypeManager()->getStorage('media_type')->load($field_definition->getTargetBundle());
+    return $media_type->getSource() instanceof OEmbedInterface;
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/Validation/Constraint/OEmbedProviderConstraint.php b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedProviderConstraint.php
new file mode 100644
index 0000000000..b908468751
--- /dev/null
+++ b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedProviderConstraint.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\media\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks if a value belongs to an allowed oEmbed provider.
+ *
+ * @Constraint(
+ *   id = "oembed_provider",
+ *   label = @Translation("oEmbed provider", context = "Validation"),
+ *   type = {"link", "string", "string_long"}
+ * )
+ */
+class OEmbedProviderConstraint extends Constraint {
+
+  /**
+   * The message is used for a not allowed provider.
+   *
+   * @var string
+   */
+  public $unallowedMessage = 'The @name provider is not allowed.';
+
+  /**
+   * The message is used if an unexpected behavior occurs.
+   *
+   * @var string
+   */
+  public $invalidMessage = 'An error occurred while trying to retrieve the oEmbed provider database.';
+
+  /**
+   * The message is used for a not matching URL against the providers.
+   *
+   * @var string
+   */
+  public $emptyMessage = 'The given URL does not match any known oEmbed providers.';
+}
diff --git a/core/modules/media/src/Plugin/Validation/Constraint/OEmbedProviderConstraintValidator.php b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedProviderConstraintValidator.php
new file mode 100644
index 0000000000..408e3b2371
--- /dev/null
+++ b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedProviderConstraintValidator.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Drupal\media\Plugin\Validation\Constraint;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\media\OEmbed\OEmbedManagerInterface;
+use Drupal\media\OEmbed\ProviderException;
+use Drupal\media\OEmbed\ResourceException;
+use Drupal\media\OEmbed\ResourceFetcherInterface;
+use Drupal\media\Plugin\media\Source\OEmbedInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Validates the OEmbedProvider constraint.
+ */
+class OEmbedProviderConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The oEmbed manager service.
+   *
+   * @var \Drupal\media\OEmbed\OEmbedManagerInterface
+   */
+  protected $oEmbedManager;
+
+  /**
+   * The resource fetcher service.
+   *
+   * @var \Drupal\media\OEmbed\ResourceFetcherInterface
+   */
+  protected $resourceFetcher;
+
+  /**
+   * The logger service.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs a new OEmbedProviderConstraintValidator.
+   *
+   * @param \Drupal\media\OEmbed\OEmbedManagerInterface $oembed_manager
+   *   The oEmbed manager service.
+   * @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
+   *   The resource fetcher service.
+   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
+   *   The logger service.
+   */
+  public function __construct(OEmbedManagerInterface $oembed_manager, ResourceFetcherInterface $resource_fetcher, LoggerChannelFactoryInterface $logger_factory) {
+    $this->oEmbedManager = $oembed_manager;
+    $this->resourceFetcher = $resource_fetcher;
+    $this->logger = $logger_factory->get('media');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('media.oembed_manager'),
+      $container->get('media.oembed.resource_fetcher'),
+      $container->get('logger.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, Constraint $constraint) {
+    /** @var \Drupal\media\MediaInterface $media */
+    $media = $value->getEntity();
+    if (!($media->getSource() instanceof OEmbedInterface)) {
+      throw new \LogicException('Media source must be of type OEmbed');
+    }
+    $url = $media->getSource()->getSourceFieldValue($media);
+
+    try {
+      $provider = $this->oEmbedManager->getProviderByUrl($url);
+
+      // Verify that resource fetching works, because some URL's might match
+      // the schemes, but doesn't support oEmbed.
+      $endpoints = $provider->getEndpoints();
+      $endpoint = reset($endpoints);
+      $resource_url = $endpoint->buildResourceUrl($url);
+      $this->resourceFetcher->fetchResource($resource_url);
+    }
+    catch (ProviderException $e) {
+      $this->context->addViolation($constraint->invalidMessage);
+      $this->logger->error($e->getMessage());
+      return;
+    }
+    catch (ResourceException $e) {
+      $this->context->addViolation($constraint->emptyMessage);
+      $this->logger->error($e->getMessage());
+      return;
+    }
+
+    if (empty($provider)) {
+      $this->context->addViolation($constraint->emptyMessage);
+      return;
+    }
+
+    if (!in_array($provider->getName(), $media->getSource()->getAllowedProviders(), TRUE)) {
+      $this->context->addViolation($constraint->unallowedMessage, [
+        '@name' => $provider->getName(),
+      ]);
+    }
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/Validation/Constraint/OEmbedResourceConstraint.php b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedResourceConstraint.php
new file mode 100644
index 0000000000..2b9d75d91c
--- /dev/null
+++ b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedResourceConstraint.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\media\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Checks if a value belongs to an allowed oEmbed provider.
+ *
+ * @Constraint(
+ *   id = "oembed_resource",
+ *   label = @Translation("oEmbed resource", context = "Validation"),
+ *   type = {"link", "string", "string_long"}
+ * )
+ */
+class OEmbedResourceConstraint extends Constraint {
+
+  /**
+   * The default violation message.
+   *
+   * @var string
+   */
+  public $message = 'The provided URL does not represent a valid oEmbed resource.';
+
+}
diff --git a/core/modules/media/src/Plugin/Validation/Constraint/OEmbedResourceConstraintValidator.php b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedResourceConstraintValidator.php
new file mode 100644
index 0000000000..6154a2f23f
--- /dev/null
+++ b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedResourceConstraintValidator.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\media\Plugin\Validation\Constraint;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\media\OEmbed\OEmbedManagerInterface;
+use Drupal\media\OEmbed\ResourceException;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Validates the OEmbedResource constraint.
+ */
+class OEmbedResourceConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
+
+  /**
+   * The oEmbed resource fetcher service.
+   *
+   * @var \Drupal\media\OEmbed\OEmbedManagerInterface
+   */
+  protected $oEmbedManager;
+
+  /**
+   * Constructs a new OEmbedResourceConstraintValidator instance.
+   *
+   * @param \Drupal\media\OEmbed\OEmbedManagerInterface $oembed_manager
+   *   The oEmbed manager service.
+   */
+  public function __construct(OEmbedManagerInterface $oembed_manager) {
+    $this->oEmbedManager = $oembed_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static($container->get('media.oembed_manager'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, Constraint $constraint) {
+    /** @var \Drupal\media\MediaInterface $media */
+    $media = $value->getEntity();
+    $url = $media->getSource()->getSourceFieldValue($media);
+
+    try {
+      $this->oEmbedManager->getResourceUrl($url);
+    }
+    catch (ResourceException $e) {
+      $this->context->addViolation($constraint->message);
+    }
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/media/Source/OEmbed.php b/core/modules/media/src/Plugin/media/Source/OEmbed.php
new file mode 100644
index 0000000000..ddad7a38df
--- /dev/null
+++ b/core/modules/media/src/Plugin/media/Source/OEmbed.php
@@ -0,0 +1,349 @@
+<?php
+
+namespace Drupal\media\Plugin\media\Source;
+
+use Drupal\Component\Transliteration\TransliterationInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Logger\LoggerChannelInterface;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\media\OEmbed\OEmbedManagerInterface;
+use Drupal\media\OEmbed\ResourceException;
+use Drupal\media\MediaSourceBase;
+use Drupal\media\MediaInterface;
+use Drupal\media\MediaTypeInterface;
+use Drupal\media\OEmbed\ResourceFetcherInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides the media source plugin for oEmbed resources.
+ *
+ * @MediaSource(
+ *   id = "oembed",
+ *   label = @Translation("oEmbed source"),
+ *   description = @Translation("Use oEmbed URL for reusable media."),
+ *   allowed_field_types = {"string"},
+ *   default_thumbnail_filename = "no-thumbnail.png",
+ *   deriver = "Drupal\media\Plugin\media\Source\OEmbedDeriver"
+ * )
+ */
+class OEmbed extends MediaSourceBase implements OEmbedInterface {
+
+  /**
+   * The logger channel for media.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelInterface
+   */
+  protected $logger;
+
+  /**
+   * The messenger service.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
+  /**
+   * The transliteration service.
+   *
+   * @var \Drupal\Component\Transliteration\TransliterationInterface
+   */
+  protected $transliteration;
+
+  /**
+   * The oEmbed resource fetcher service.
+   *
+   * @var \Drupal\media\OEmbed\ResourceFetcherInterface
+   */
+  protected $resourceFetcher;
+
+  /**
+   * The OEmbed manager service.
+   *
+   * @var \Drupal\media\OEmbed\OEmbedManagerInterface
+   */
+  protected $oEmbedManager;
+
+  /**
+   * Constructs a new OEmbed instance.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   Entity type manager service.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
+   *   Entity field manager service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory service.
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
+   *   The field type plugin manager service.
+   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
+   *   The logger channel for media.
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *   The messenger service.
+   * @param \Drupal\Component\Transliteration\TransliterationInterface $transliteration
+   *   The transliteration service.
+   * @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
+   *   The oEmbed resource fetcher service.
+   * @param \Drupal\media\OEmbed\OEmbedManagerInterface $oembed_manager
+   *   The oEmbed manager service.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, ConfigFactoryInterface $config_factory, FieldTypePluginManagerInterface $field_type_manager, LoggerChannelInterface $logger, MessengerInterface $messenger, TransliterationInterface $transliteration, ResourceFetcherInterface $resource_fetcher, OEmbedManagerInterface $oembed_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_field_manager, $field_type_manager, $config_factory);
+    $this->logger = $logger;
+    $this->messenger = $messenger;
+    $this->transliteration = $transliteration;
+    $this->resourceFetcher = $resource_fetcher;
+    $this->oEmbedManager = $oembed_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_type.manager'),
+      $container->get('entity_field.manager'),
+      $container->get('config.factory'),
+      $container->get('plugin.manager.field.field_type'),
+      $container->get('logger.factory')->get('media'),
+      $container->get('messenger'),
+      $container->get('transliteration'),
+      $container->get('media.oembed.resource_fetcher'),
+      $container->get('media.oembed_manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMetadataAttributes() {
+    return [
+      'type' => $this->t('Resource type'),
+      'title' => $this->t('Resource title'),
+      'author_name' => $this->t('The name of the author/owner'),
+      'author_url' => $this->t('The URL of the author/owner'),
+      'provider_id' => $this->t("The provider's ID"),
+      'provider_name' => $this->t("The provider's name"),
+      'provider_url' => $this->t('The URL of the provider'),
+      'cache_age' => $this->t('Suggested cache lifetime'),
+      'thumbnail_url' => $this->t('The remote URL of the thumbnail'),
+      'thumbnail_local_uri' => $this->t('The local URI of the thumbnail'),
+      'thumbnail_local' => $this->t('The local URL of the thumbnail'),
+      'thumbnail_width' => $this->t('Thumbnail width'),
+      'thumbnail_height' => $this->t('Thumbnail height'),
+      'url' => $this->t('The source URL of the resource'),
+      'width' => $this->t('The width of the resource'),
+      'height' => $this->t('The height of the resource'),
+      'html' => $this->t('The HTML representation of the resource'),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMetadata(MediaInterface $media, $name) {
+    $media_url = $this->getSourceFieldValue($media);
+
+    try {
+      $resource_url = $this->oEmbedManager->getResourceUrl($media_url);
+      $resource = $this->resourceFetcher->fetchResource($resource_url);
+    }
+    catch (ResourceException $e) {
+      $this->messenger->addError($e->getMessage());
+      return NULL;
+    }
+
+    switch ($name) {
+      case 'thumbnail_local':
+        $local_uri = $this->getMetadata($media, 'thumbnail_local_uri');
+
+        if ($local_uri) {
+          if (file_exists($local_uri)) {
+            return $local_uri;
+          }
+          else {
+            $image_data = file_get_contents($this->getMetadata($media, 'thumbnail_url'));
+            if ($image_data) {
+              return file_unmanaged_save_data($image_data, $local_uri, FILE_EXISTS_REPLACE) ?: NULL;
+            }
+          }
+        }
+        return NULL;
+
+      case 'thumbnail_local_uri':
+        return $this->getLocalImageUri($media, $resource_url);
+
+      case 'default_name':
+        if ($title = $this->getMetadata($media, 'title')) {
+          return $title;
+        }
+        elseif ($url = $this->getMetadata($media, 'url')) {
+          return $url;
+        }
+        return parent::getMetadata($media, 'default_name');
+
+      case 'thumbnail_uri':
+        if ($uri = $this->getMetadata($media, 'thumbnail_local')) {
+          return $uri;
+        }
+        return parent::getMetadata($media, 'thumbnail_uri');
+
+      case 'provider_id':
+        $provider_name = $this->getMetadata($media, 'provider_name');
+        return $this->transliteration->transliterate($provider_name);
+
+      default:
+        if (!empty($resource[$name])) {
+          return $resource[$name];
+        }
+        break;
+    }
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form = parent::buildConfigurationForm($form, $form_state);
+
+    $form['thumbnails_location'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Thumbnails location'),
+      '#default_value' => $this->configuration['thumbnails_location'],
+      '#description' => $this->t('Thumbnails will be fetched from the provider for local usage. This is the location where they will be placed.'),
+    ];
+
+    $form['allowed_providers'] = [
+      '#type' => 'checkboxes',
+      '#title' => $this->t('Allowed providers'),
+      '#default_value' => $this->configuration['allowed_providers'],
+      '#options' => array_combine($this->pluginDefinition['supported_providers'], $this->pluginDefinition['supported_providers']),
+      '#description' => $this->t('Optionally select the allowed oEmbed providers for this media type. If left blank, all providers will be allowed.'),
+    ];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    parent::submitConfigurationForm($form, $form_state);
+    $this->configuration['allowed_providers'] = array_filter(array_values($this->configuration['allowed_providers']));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $thumbnails_location = $form_state->getValue('thumbnails_location');
+    if (!file_valid_uri($thumbnails_location)) {
+      $form_state->setErrorByName('thumbnails_location', $this->t('@path is not a valid path.', ['@path' => $thumbnails_location]));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'thumbnails_location' => 'public://oembed_thumbnails',
+      'allowed_providers' => [],
+    ] + parent::defaultConfiguration();
+  }
+
+  /**
+   * Computes the destination URI for a thumbnail.
+   *
+   * @param \Drupal\media\MediaInterface $media
+   *   A media item.
+   * @param string $resource_url
+   *   The resource URL of oEmbed item.
+   *
+   * @return string
+   *   The local URI.
+   */
+  protected function getLocalImageUri(MediaInterface $media, $resource_url) {
+    $remote_url = $this->getMetadata($media, 'thumbnail_url');
+    if (!$remote_url) {
+      return parent::getMetadata($media, 'thumbnail_uri');
+    }
+
+    $directory = $this->configuration['thumbnails_location'];
+    // Ensure that the destination directory is writable. If not, log a warning
+    // and return the default thumbnail.
+    if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
+      $this->logger->warning('Could not prepare thumbnail destination directory @dir for oEmbed media.', [
+        '@dir' => $directory,
+      ]);
+      return parent::getMetadata($media, 'thumbnail_uri');
+    }
+
+    $filename = $this->getMetadata($media, 'provider_name') . '_' . substr(md5($resource_url), 0, 5);
+    $local_uri = $this->configuration['thumbnails_location'] . '/' . $filename . '.';
+    $local_uri .= pathinfo($remote_url, PATHINFO_EXTENSION);
+
+    return $local_uri;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSourceFieldConstraints() {
+    return [
+      'oembed_resource' => [],
+      'oembed_provider' => [],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepareViewDisplay(MediaTypeInterface $type, EntityViewDisplayInterface $display) {
+    $display->setComponent($this->getSourceFieldDefinition($type)->getName(), [
+      'type' => 'oembed',
+    ]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepareFormDisplay(MediaTypeInterface $type, EntityFormDisplayInterface $display) {
+    parent::prepareFormDisplay($type, $display);
+    $source_field = $this->getSourceFieldDefinition($type)->getName();
+
+    $display->setComponent($source_field, [
+      'type' => 'oembed_textfield',
+      'weight' => $display->getComponent($source_field)['weight'],
+    ]);
+    $display->removeComponent('name');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAllowedProviders() {
+    return $this->configuration['allowed_providers'] ?: $this->getSupportedProviders();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSupportedProviders() {
+    return $this->pluginDefinition['supported_providers'];
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/media/Source/OEmbedDeriver.php b/core/modules/media/src/Plugin/media/Source/OEmbedDeriver.php
new file mode 100644
index 0000000000..00aa966b89
--- /dev/null
+++ b/core/modules/media/src/Plugin/media/Source/OEmbedDeriver.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Drupal\media\Plugin\media\Source;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+
+/**
+ * The oEmbed deriver generates supported media sources.
+ */
+class OEmbedDeriver extends DeriverBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    $this->derivatives = [
+      'video' => [
+        'id' => 'video',
+        'label' => t('Remote video'),
+        'description' => t('Use remote video URL for reusable media.'),
+        'supported_providers' => ['YouTube', 'Vimeo'],
+        'settings' => [],
+        'default_thumbnail_filename' => 'video.png',
+      ] + $base_plugin_definition,
+    ];
+    return parent::getDerivativeDefinitions($base_plugin_definition);
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/media/Source/OEmbedInterface.php b/core/modules/media/src/Plugin/media/Source/OEmbedInterface.php
new file mode 100644
index 0000000000..6689646bbe
--- /dev/null
+++ b/core/modules/media/src/Plugin/media/Source/OEmbedInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\media\Plugin\media\Source;
+
+use Drupal\media\MediaSourceFieldConstraintsInterface;
+
+/**
+ * OEmbedInterface adds additional functionality to the OEmbed source.
+ */
+interface OEmbedInterface extends MediaSourceFieldConstraintsInterface {
+
+  /**
+   * Returns the allowed oEmbed providers.
+   *
+   * @return string[]
+   *   List of oEmbed providers.
+   */
+  public function getAllowedProviders();
+
+  /**
+   * Returns the supported oEmbed providers.
+   *
+   * @return string[]
+   *   List of oEmbed providers.
+   */
+  public function getSupportedProviders();
+
+}
diff --git a/core/modules/media/tests/src/Functional/FieldFormatter/OEmbedFormatterTest.php b/core/modules/media/tests/src/Functional/FieldFormatter/OEmbedFormatterTest.php
new file mode 100644
index 0000000000..5c119ad3dd
--- /dev/null
+++ b/core/modules/media/tests/src/Functional/FieldFormatter/OEmbedFormatterTest.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace Drupal\Tests\media\Functional\FieldFormatter;
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\link\LinkItemInterface;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\media\Plugin\Field\FieldFormatter\OEmbedFormatter
+ * @group media
+ */
+class OEmbedFormatterTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'entity_test',
+    'field',
+    'link',
+    'media',
+    'user',
+    'system',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->drupalLogin($this->drupalCreateUser(['view test entity']));
+  }
+
+  /**
+   * @covers ::viewElements
+   */
+  public function testRender() {
+    $entity_type = $bundle = 'entity_test';
+    $field_name = Unicode::strtolower($this->randomMachineName());
+
+    FieldStorageConfig::create([
+      'entity_type' => $entity_type,
+      'field_name' => $field_name,
+      'type' => 'link',
+      'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+    ])->save();
+    $field_config = FieldConfig::create([
+      'entity_type' => $entity_type,
+      'field_name' => $field_name,
+      'bundle' => $bundle,
+      'settings' => [
+        'title' => DRUPAL_DISABLED,
+        'link_type' => LinkItemInterface::LINK_EXTERNAL,
+      ],
+    ]);
+    $field_config->save();
+
+    $display = entity_get_display('entity_test', 'entity_test', 'full');
+    $display->setComponent($field_name, ['type' => 'oembed'])->save();
+
+    $entity = EntityTest::create([
+      $field_config->getName() => [
+        [
+          'uri' => 'https://twitter.com/drupaldevdays/status/935643039741202432',
+        ],
+      ],
+    ]);
+    $entity->save();
+
+    $this->drupalGet($entity->toUrl());
+    $assert_session = $this->assertSession();
+    $assert_session->elementExists('css', "blockquote.twitter-tweet");
+    $assert_session->elementContains('css', "blockquote.twitter-tweet > p", 'Save the date for Drupal Developer Days Lisbon 2018');
+  }
+
+}
diff --git a/core/modules/media/tests/src/Functional/OEmbedProviderCollectorTest.php b/core/modules/media/tests/src/Functional/OEmbedProviderCollectorTest.php
new file mode 100644
index 0000000000..75615ccfec
--- /dev/null
+++ b/core/modules/media/tests/src/Functional/OEmbedProviderCollectorTest.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Drupal\Tests\media\Functional;
+
+use Drupal\media\OEmbed\ProviderCollector;
+use Drupal\media\OEmbed\ProviderException;
+
+/**
+ * Tests the oEmbed provider collector.
+ *
+ * @covers \Drupal\media\ProviderCollector
+ *
+ * @group media
+ */
+class OEmbedProviderCollectorTest extends MediaFunctionalTestBase {
+
+  /**
+   * Test provider collection.
+   *
+   * @dataProvider dataProviderEmptyProviderList
+   */
+  public function testEmptyProviderList($content, $providerUrl, $exceptionClass, $exceptionMessage) {
+    $cacheBackend = $this->container->get('cache.default');
+    $time = $this->container->get('datetime.time');
+
+    $body = $this->getMockBuilder('\GuzzleHttp\Psr7\Stream')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $body->method('getContents')->willReturn($content);
+
+    $response = $this->getMockBuilder('\GuzzleHttp\Psr7\Response')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $response->method('getBody')->willReturn($body);
+
+    $client = $this->getMockBuilder('\GuzzleHttp\Client')
+      ->getMock();
+    $client->method('request')->withAnyParameters()->willReturn($response);
+
+    $this->setExpectedException($exceptionClass, $exceptionMessage);
+
+    $providerCollector = new ProviderCollector($client, $providerUrl, $time, $cacheBackend);
+    $providerCollector->getAll();
+  }
+
+  /**
+   * Data provider for testEmptyProviderList.
+   *
+   * @return array
+   *   Data.
+   */
+  public function dataProviderEmptyProviderList() {
+    return [
+      [
+        json_encode([]),
+        'http://oembed.com/providers.json',
+        ProviderException::class,
+        'Remote oEmbed providers database returned invalid or empty list.',
+      ],
+      [
+        '',
+        'http://oembed.com/providers.json',
+        ProviderException::class,
+        'Remote oEmbed providers database returned invalid or empty list.',
+      ],
+    ];
+  }
+
+  /**
+   * Test provider discovery.
+   *
+   * @dataProvider dataProviderNonExistingProviderDatabase
+   */
+  public function testNonExistingProviderDatabase($providerUrl, $exceptionClass, $exceptionMessage) {
+    $cacheBackend = $this->container->get('cache.default');
+    $client = $this->container->get('http_client');
+    $time = $this->container->get('datetime.time');
+
+    $this->setExpectedException($exceptionClass, $exceptionMessage);
+
+    $providerCollector = new ProviderCollector($client, $providerUrl, $time, $cacheBackend);
+    $providerCollector->getAll();
+  }
+
+  /**
+   * Data provider for testEmptyProviderList.
+   *
+   * @return array
+   *   Data.
+   */
+  public function dataProviderNonExistingProviderDatabase() {
+    return [
+      [
+        'http://oembed1.com/providers.json',
+        ProviderException::class,
+        'Could not retrieve the oEmbed provider database from http://oembed1.com/providers.json',
+      ],
+      [
+        'http://oembed.com/providers1.json',
+        ProviderException::class,
+        'Could not retrieve the oEmbed provider database from http://oembed.com/providers1.json',
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/media/tests/src/Functional/OEmbedTest.php b/core/modules/media/tests/src/Functional/OEmbedTest.php
new file mode 100644
index 0000000000..09bd44337a
--- /dev/null
+++ b/core/modules/media/tests/src/Functional/OEmbedTest.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\Tests\media\Functional;
+
+use Drupal\media\OEmbed\OEmbedManager;
+use Drupal\media\OEmbed\ResourceFetcher;
+
+/**
+ * Tests the oEmbed service.
+ *
+ * @covers \Drupal\media\OEmbed\ResourceFetcher
+ *
+ * @group media
+ */
+class OEmbedTest extends MediaFunctionalTestBase {
+
+  /**
+   * Test urlDiscovery method.
+   */
+  public function testUrlDiscovery() {
+    $cache_backend = $this->container->get('cache.default');
+    $module_handler = $this->container->get('module_handler');
+    $client = $this->container->get('http_client');
+    $provider_collector = $this->container->get('media.oembed.provider_collector');
+    $resource_fetcher = $this->container->get('media.oembed.resource_fetcher');
+
+    $oEmbed = new OEmbedManager($provider_collector, $resource_fetcher, $client, $module_handler, $cache_backend);
+
+    // Received by discovery.
+    $this->assertSame('https://vimeo.com/api/oembed.json?url=https%3A//vimeo.com/7073899', $oEmbed->getResourceUrl('https://vimeo.com/7073899'));
+    // Received by URL building.
+    $this->assertSame('https://publish.twitter.com/oembed?url=https%3A//twitter.com/drupaldevdays/status/935643039741202432', $oEmbed->getResourceUrl('https://twitter.com/drupaldevdays/status/935643039741202432'));
+    // URL building with format replacement.
+    $this->assertSame('http://www.collegehumor.com/oembed.json?url=http%3A//www.collegehumor.com/video/40002870/lets-not-get-a-drink-sometime', $oEmbed->getResourceUrl('http://www.collegehumor.com/video/40002870/lets-not-get-a-drink-sometime'));
+    // URL building with height and width.
+    $this->assertSame('http://www.flickr.com/services/oembed/?url=https%3A//www.flickr.com/photos/amazeelabs/30530130354/in/album-72157675936437840&max_width=320&max_height=480', $oEmbed->getResourceUrl('https://www.flickr.com/photos/amazeelabs/30530130354/in/album-72157675936437840', 320, 480));
+  }
+
+  /**
+   * Test fetchResource method.
+   */
+  public function testFetchResource() {
+    $cache_backend = $this->container->get('cache.default');
+    $client = $this->container->get('http_client');
+
+    $oEmbed = new ResourceFetcher($client, $cache_backend);
+
+    // Fetch JSON resource.
+    $resource = $oEmbed->fetchResource('https://vimeo.com/api/oembed.json?url=https%3A%2F%2Fvimeo.com%2F7073899');
+    $this->assertSame('video', $resource['type']);
+    $this->assertSame('Vimeo', $resource['provider_name']);
+    $this->assertSame('Drupal Rap Video - Schipulcon09', $resource['title']);
+
+    // Fetch XML resource.
+    $resource = $oEmbed->fetchResource('http://www.collegehumor.com/oembed.xml?url=http%3A//www.collegehumor.com/video/40002870/lets-not-get-a-drink-sometime');
+    $this->assertSame('video', $resource['type']);
+    $this->assertSame('CollegeHumor', $resource['provider_name']);
+    $this->assertSame("Let's Not Get a Drink Sometime", $resource['title']);
+  }
+
+}
diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaSourceOEmbedVideoTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceOEmbedVideoTest.php
new file mode 100644
index 0000000000..121dd2f238
--- /dev/null
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceOEmbedVideoTest.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Drupal\Tests\media\FunctionalJavascript;
+
+use Drupal\media\Entity\Media;
+
+/**
+ * Tests the oembed:video media source.
+ *
+ * @group media
+ */
+class MediaSourceOEmbedVideoTest extends MediaSourceTestBase {
+
+  /**
+   * Tests the oembed media source.
+   */
+  public function testMediaOEmbedVideoSource() {
+    $media_type_id = 'test_media_oembed_type';
+    $provided_fields = [
+      'type',
+      'title',
+      'author_name',
+      'author_url',
+      'provider_name',
+      'provider_url',
+      'provider_id',
+      'cache_age',
+      'thumbnail_url',
+      'thumbnail_local_uri',
+      'thumbnail_local',
+      'thumbnail_width',
+      'thumbnail_height',
+      'url',
+      'width',
+      'height',
+      'html',
+    ];
+
+    $session = $this->getSession();
+    $page = $session->getPage();
+    $assert_session = $this->assertSession();
+
+    $this->doTestCreateMediaType($media_type_id, 'oembed:video', $provided_fields);
+
+    // Create custom fields for the media type to store metadata attributes.
+    $fields = [
+      'field_string_width' => 'string',
+      'field_string_height' => 'string',
+      'field_string_author_name' => 'string',
+    ];
+    $this->createMediaTypeFields($fields, $media_type_id);
+
+    // Hide the name field widget to test default name generation.
+    $this->hideMediaTypeFieldWidget('name', $media_type_id);
+
+    $this->drupalGet("admin/structure/media/manage/{$media_type_id}");
+    // Only accept vimeo videos.
+    $page->checkField("source_configuration[allowed_providers][Vimeo]");
+
+    $page->selectFieldOption("field_map[width]", 'field_string_width');
+    $page->selectFieldOption("field_map[height]", 'field_string_height');
+    $page->selectFieldOption("field_map[author_name]", 'field_string_author_name');
+    $page->pressButton('Save');
+
+    // Create a media item.
+    $this->drupalGet("media/add/{$media_type_id}");
+    $page->fillField("Remote video", 'https://vimeo.com/7073899');
+    $page->pressButton('Save');
+
+    $assert_session->addressEquals('media/1');
+
+    // Make sure the thumbnail is displayed from uploaded image.
+    $assert_session->elementAttributeContains('css', '.image-style-thumbnail', 'src', 'oembed_thumbnails/Vimeo_1d58b.jpg');
+
+    // Load the media and check that all fields are properly populated.
+    $media = Media::load(1);
+    $this->assertEquals('Drupal Rap Video - Schipulcon09', $media->getName());
+    $this->assertEquals('480', $media->get('field_string_width')->value);
+    $this->assertEquals('360', $media->get('field_string_height')->value);
+
+    // Try to create a Twitch item, which should not be allowed.
+    $this->drupalGet("media/add/{$media_type_id}");
+    $page->fillField("Remote video", 'https://clips.twitch.tv/SleepyBoringLapwingRuleFive');
+    $page->pressButton('Save');
+
+    $assert_session->pageTextContains('The Twitch provider is not allowed.');
+  }
+
+}
