diff --git a/core/modules/media/config/schema/media.schema.yml b/core/modules/media/config/schema/media.schema.yml
index dad518985a..91303092e7 100644
--- a/core/modules/media/config/schema/media.schema.yml
+++ b/core/modules/media/config/schema/media.schema.yml
@@ -52,6 +52,10 @@ media.source.image:
   type: media.source.field_aware
   label: '"Image" media source configuration'
 
+media.source.oembed:
+  type: media.source.field_aware
+  label: '"oEmbed" media source configuration'
+
 media.source.field_aware:
   type: mapping
   mapping:
diff --git a/core/modules/media/media.services.yml b/core/modules/media/media.services.yml
index f22f90a124..3a1f3f5092 100644
--- a/core/modules/media/media.services.yml
+++ b/core/modules/media/media.services.yml
@@ -2,9 +2,11 @@ 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:
+    class: Drupal\media\OEmbed
+    arguments: ['@http_client_factory', '@cache.default', '@logger.factory']
diff --git a/core/modules/media/src/OEmbed.php b/core/modules/media/src/OEmbed.php
new file mode 100644
index 0000000000..bf2a6b4688
--- /dev/null
+++ b/core/modules/media/src/OEmbed.php
@@ -0,0 +1,286 @@
+<?php
+
+namespace Drupal\media;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Http\ClientFactory;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * OEmbed service.
+ */
+class OEmbed implements OEmbedInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * Cache key to be used to store providers info into cache.
+   *
+   * @var string
+   */
+  protected static $providersCacheKey = 'media:oembed:providers';
+
+  /**
+   * URL of the JSON with providers info.
+   *
+   * @var string
+   */
+  protected static $providersUrl = 'http://oembed.com/providers.json';
+
+  /**
+   * Cache lifetime for the providers info.
+   */
+  protected static $providersCacheLifetime = 60 * 60 * 24 * 7;
+
+  /**
+   * The HTTP client factory service.
+   *
+   * @var \Drupal\Core\Http\ClientFactory
+   */
+  protected $clientFactory;
+
+  /**
+   * OEmbed providers information.
+   *
+   * @var array
+   */
+  protected $providers;
+
+  /**
+   * Cache backend.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $cache;
+
+  /**
+   * Static cache of fetched oEmbed resources.
+   *
+   * @var array
+   */
+  protected $resources;
+
+  /**
+   * Static cache of discovered oEmbed resources.
+   *
+   * @var array
+   */
+  protected $discovered;
+
+  /**
+   * The logger channel for media.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs OEmbed class.
+   *
+   * @param \Drupal\Core\Http\ClientFactory $client_factory
+   *   The HTTP client factory service.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   The cache backend.
+   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
+   *   The logger channel for media.
+   */
+  public function __construct(ClientFactory $client_factory, CacheBackendInterface $cache, LoggerChannelFactoryInterface $logger_factory) {
+    $this->clientFactory = $client_factory;
+    $this->cache = $cache;
+    $this->logger = $logger_factory->get('media');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProviders() {
+    if (!empty($this->providers)) {
+      return $this->providers;
+    }
+    elseif ($data = $this->cache->get(static::$providersCacheKey)) {
+      $this->providers = $data->data;
+      return $this->providers;
+    }
+    else {
+      $response = $this->clientFactory->fromOptions()->get(static::$providersUrl);
+      if ($response->getStatusCode() !== 200) {
+        drupal_set_message($this->t('Could not retrieve the providers list from the remote oEmbed database.'), 'error');
+        $this->logger->error('Remote oEmbed providers database returned status code @code.', [
+          '@code' => $response->getStatusCode(),
+        ]);
+        return FALSE;
+      }
+
+      $providers = \GuzzleHttp\json_decode($response->getBody()->getContents(), TRUE);
+
+      if (!is_array($providers)) {
+        drupal_set_message($this->t('An error occurred while trying to retrieve the providers list from the remote oEmbed database.'), 'error');
+        $this->logger->error('Remote oEmbed providers database returned incorrect response content. Providers: @response', [
+          '@response' => json_encode($providers),
+        ]);
+        return FALSE;
+      }
+
+      // Some provider names may contain dot chars ("."), which are not allowed
+      // in config keys. We store them in an array where keys are hex-converted
+      // names, in order to allow an easy conversion back to their original
+      // names when necessary.
+      $keyed_providers = [];
+      foreach ($providers as $provider) {
+        $keyed_providers[bin2hex($provider['provider_name'])] = $provider;
+      }
+
+      $this->cache->set(self::$providersCacheKey, $keyed_providers, static::$providersCacheLifetime);
+      $this->providers = $keyed_providers;
+      return $this->providers;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function matchUrl($url, array $providers_limit = []) {
+    $providers = $this->getProviders();
+    if (!empty($providers_limit)) {
+      $providers = array_intersect_key($providers, $providers_limit);
+    }
+
+    foreach ($providers as $provider_name => $provider_info) {
+      // @TODO Figure out a way of making this validation more robust. Youtube,
+      // for instance, does not come with schemes...
+      if ($provider_info['provider_name'] === 'YouTube') {
+        $provider_info['endpoints'][0]['schemes'] = [
+          'http*://*youtube.com/*',
+          'http*://*youtu.be/*',
+        ];
+      }
+      if (!empty($provider_info['endpoints'])) {
+        foreach ($provider_info['endpoints'] as $endpoint) {
+          if (!empty($endpoint['schemes'])) {
+            foreach ($endpoint['schemes'] as $scheme) {
+              $regexp = str_replace(['.', '*'], ['\.', '.*'], $scheme);
+              if (preg_match("|$regexp|", $url)) {
+                return [
+                  'provider' => $provider_name,
+                  'endpoint' => $endpoint['url'],
+                ];
+              }
+            }
+          }
+        }
+      }
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function doUrlDiscovery($url) {
+    if (!empty($this->discovered[$url])) {
+      return $this->discovered[$url];
+    }
+
+    $response = $this->clientFactory->fromOptions()->get($url);
+    if ($response->getStatusCode() !== 200) {
+      drupal_set_message($this->t('Could not retrieve the remote URL correctly.'), 'error');
+      $this->logger->error('Resource for URL @url responded with status code @code.', [
+        '@url' => $url,
+        '@code' => $response->getStatusCode(),
+      ]);
+      return FALSE;
+    }
+
+    $content = $response->getBody()->getContents();
+    $dom = new \DOMDocument();
+
+    // TODO this causes annoying warning with YT videos. Fix it:
+    // Warning: DOMDocument::loadHTML(): htmlParseEntityRef: no name in Entity
+    if (!@$dom->loadHTML($content)) {
+      drupal_set_message($this->t('The remote URL returned an incorrect document structure.'), 'error');
+      return FALSE;
+    }
+
+    $xpath = new \DOMXpath($dom);
+    $result = $xpath->query("//link[@type='application/json+oembed']");
+    if ($result->length > 0) {
+      $this->discovered[$url] = $result->item(0)->getAttribute('href');
+      return $this->discovered[$url];
+    }
+
+    $result = $xpath->query("//link[@type='text/xml+oembed']");
+    if ($result->length > 0) {
+      $this->discovered[$url] = $result->item(0)->getAttribute('href');
+      return $this->discovered[$url];
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isOEmbedResource($url) {
+    if ($this->doUrlDiscovery($url)) {
+      return TRUE;
+    }
+
+    // TODO handle providers that do not support discovery.
+
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fetchResource($endpoint_url) {
+    if (!empty($this->resources[$endpoint_url])) {
+      return $this->resources[$endpoint_url];
+    }
+
+    $response = $this->clientFactory->fromOptions()->get($endpoint_url);
+    if ($response->getStatusCode() !== 200) {
+      drupal_set_message($this->t('Could not retrieve the remote URL.'), 'error');
+      $this->logger->error('Remote resource returned status code @code.', [
+        '@code' => $response->getStatusCode(),
+      ]);
+      return FALSE;
+    }
+
+    $format = $response->getHeader('Content-Type');
+    $content = $response->getBody()->getContents();
+    if ($format[0] == 'application/json') {
+      $data = \GuzzleHttp\json_decode($content, TRUE);
+
+      if (!is_array($data)) {
+        drupal_set_message($this->t('An error occurred while trying to retrieve the remote URL content.'), 'error');
+        $this->logger->error('Remote resource returned incorrect response content. Data: @response', [
+          '@response' => json_encode($data),
+        ]);
+        return FALSE;
+      }
+
+      $this->resources[$endpoint_url] = $data;
+      return $data;
+    }
+    elseif ($format[0] == 'text/xml') {
+      $data = \GuzzleHttp\json_decode(\GuzzleHttp\json_encode($content), TRUE);
+
+      if (!is_array($data)) {
+        drupal_set_message($this->t('An error occurred while trying to retrieve the remote URL content.'), 'error');
+        $this->logger->error('Remote resource returned incorrect response content. Data: @response', [
+          '@response' => json_encode($data),
+        ]);
+        return FALSE;
+      }
+
+      $this->resources[$endpoint_url] = $data;
+      return $data;
+    }
+
+    return FALSE;
+  }
+
+}
diff --git a/core/modules/media/src/OEmbedInterface.php b/core/modules/media/src/OEmbedInterface.php
new file mode 100644
index 0000000000..4a83894a9c
--- /dev/null
+++ b/core/modules/media/src/OEmbedInterface.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\media;
+
+/**
+ * Interface for oEmbed service.
+ */
+interface OEmbedInterface {
+
+  /**
+   * Gets oEmbed providers information.
+   *
+   * Returns a multi-dimensional array where each provider is represented as a
+   * single top-level array element. Each individual element is keyed by the
+   * provider's name codified with bin2hex(), and has the following values:
+   * - provider_name: Human readable name of the provider.
+   * - provider_url: Main URL of the provider.
+   * - endpoints: List of endpoints this provider exposes. Each endpoint is an
+   *   array with the following values:
+   *     - url: The endpoint's URL.
+   *     - schemes: List of URL schemes supported by the provider.
+   *     - formats: List of supported formats. Can be "json", "xml" or both.
+   *     - discovery: Whether the provider supports oEmbed discovery.
+   *
+   * @return array[]
+   *   Information about oEmbed providers.
+   */
+  public function getProviders();
+
+  /**
+   * Tries to match the URL against the database of oEmbed providers.
+   *
+   * @param string $url
+   *   The URL to be tested.
+   * @param array $allowed_providers
+   *   (optional) A list of providers to restrict to. If present, this needs to
+   *   be an array where keys are the provider names (codified with bin2hex()),
+   *   and the values are irrelevant. If ommitted, all providers will be
+   *   considered.
+   *
+   * @return array|bool
+   *   If a match was found, will return an associative array with two values:
+   *   - provider: Matching provider's name (coded with bin2hex()).
+   *   - endpoint: URL of the matching endpoint.
+   *   Will return FALSE if no match was found.
+   *
+   * @see \Drupal\media\OEmbedInterface::getProviders()
+   */
+  public function matchUrl($url, array $allowed_providers = []);
+
+  /**
+   * 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.
+   */
+  public function doUrlDiscovery($url);
+
+  /**
+   * Determines whether a given URL is an oEmbed resource.
+   *
+   * @param string $url
+   *   URL of the resource.
+   *
+   * @return bool
+   *   TRUE if the URL represents a valid oEmbed resource, or FALSE otherwise.
+   */
+  public function isOEmbedResource($url);
+
+  /**
+   * Fetches information about the oEmbed resource.
+   *
+   * @param string $endpoint_url
+   *   Resource-specific URL of the oEmbed endpoint.
+   *
+   * @return array|bool
+   *   Resource information as returned from the oEmbed endpoint, or FALSE if
+   *   the resource could not be fetched.
+   */
+  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..f542d44cb6
--- /dev/null
+++ b/core/modules/media/src/Plugin/Field/FieldFormatter/OEmbedFormatter.php
@@ -0,0 +1,103 @@
+<?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\Plugin\ContainerFactoryPluginInterface;
+use Drupal\media\OEmbedInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Plugin implementation of the 'link' formatter.
+ *
+ * @FieldFormatter(
+ *   id = "oembed",
+ *   label = @Translation("oEmbed"),
+ *   field_types = {
+ *     "link",
+ *     "string",
+ *     "string_long",
+ *   }
+ * )
+ */
+class OEmbedFormatter extends FormatterBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The oEmbed service.
+   *
+   * @var \Drupal\media\OEmbedInterface
+   */
+  protected $oEmbed;
+
+  /**
+   * Constructs a EntityReferenceEntityFormatter 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\OEmbedInterface $oembed
+   *   The oEmbed service.
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, OEmbedInterface $oembed) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
+    $this->oEmbed = $oembed;
+  }
+
+  /**
+   * {@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')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewElements(FieldItemListInterface $items, $langcode) {
+    $element = [];
+
+    foreach ($items as $delta => $item) {
+      $main_property = $item->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName();
+      if (!empty($item->{$main_property})) {
+        $resource = $this->oEmbed->fetchResource($this->oEmbed->doUrlDiscovery($item->{$main_property}));
+        $element[$delta] = [
+          '#markup' => $resource['html'],
+          '#allowed_tags' => ['iframe'],
+        ];
+      }
+    }
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function isApplicable(FieldDefinitionInterface $field_definition) {
+    // This formatter is only available for fields attached to media items.
+    return ($field_definition->getTargetEntityTypeId() === 'media');
+  }
+
+}
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..a0e1313fc0
--- /dev/null
+++ b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedProviderConstraint.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_provider",
+ *   label = @Translation("oEmbed provider", context = "Validation"),
+ *   type = {"link", "string", "string_long"}
+ * )
+ */
+class OEmbedProviderConstraint extends Constraint {
+
+  /**
+   * The default violation message.
+   *
+   * @var string
+   */
+  public $message = 'The provider used is not allowed.';
+
+}
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..7d6eb547ee
--- /dev/null
+++ b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedProviderConstraintValidator.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Drupal\media\Plugin\Validation\Constraint;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\media\MediaInterface;
+use Drupal\media\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 {
+
+  /**
+   * The oEmbed service.
+   *
+   * @var \Drupal\media\OEmbed
+   */
+  protected $oEmbed;
+
+  /**
+   * Constructs a new TweetVisibleConstraintValidator.
+   *
+   * @param \Drupal\media\OEmbedInterface $oembed
+   *   The oEmbed service.
+   */
+  public function __construct(OEmbedInterface $oembed) {
+    $this->oEmbed = $oembed;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static($container->get('media.oembed'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, Constraint $constraint) {
+    // TODO check if resource belongs to an allowed provider.
+    /** @var FieldItemListInterface $value */
+    $foo = 'ar';
+    /** @var MediaInterface $media */
+    $media = $value->getEntity();
+    $source_config = $media->getSource()->getConfiguration();
+    if (!empty($source_config['allowed_providers'])) {
+      $allowed_providers = array_map(function ($name) {
+        return hex2bin($name);
+      }, $source_config['allowed_providers']);
+      $main_property = $value->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName();
+      $url_matches = $this->oEmbed->matchUrl($value->first()->get($main_property)->getString(), $allowed_providers);
+      if (!$url_matches) {
+        $this->context->addViolation($constraint->message);
+      }
+    }
+  }
+
+}
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..bfaaa677b1
--- /dev/null
+++ b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedResourceConstraintValidator.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\media\Plugin\Validation\Constraint;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\media\OEmbedInterface;
+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 service.
+   *
+   * @var \Drupal\media\OEmbed
+   */
+  protected $oEmbed;
+
+  /**
+   * Constructs a new TweetVisibleConstraintValidator.
+   *
+   * @param \Drupal\media\OEmbedInterface $oembed
+   *   The oEmbed service.
+   */
+  public function __construct(OEmbedInterface $oembed) {
+    $this->oEmbed = $oembed;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static($container->get('media.oembed'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, Constraint $constraint) {
+    $main_property = $value->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName();
+    if (!$this->oEmbed->isOEmbedResource($value->first()->get($main_property)->getString())) {
+      $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..5b57996a25
--- /dev/null
+++ b/core/modules/media/src/Plugin/media/Source/OEmbed.php
@@ -0,0 +1,272 @@
+<?php
+
+namespace Drupal\media\Plugin\media\Source;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+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\link\LinkItemInterface;
+use Drupal\media\MediaSourceBase;
+use Drupal\media\MediaInterface;
+use Drupal\media\MediaSourceFieldConstraintsInterface;
+use Drupal\media\MediaTypeInterface;
+use Drupal\media\OEmbedInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides the media source plugin for oEmbed resources.
+ *
+ * @MediaSource(
+ *   id = "oembed",
+ *   label = @Translation("OEmbed"),
+ *   description = @Translation("Use remote URL media that implement the oEmbed protocol."),
+ *   allowed_field_types = {"link", "string", "string_long"},
+ *   default_thumbnail_filename = "no-thumbnail.png"
+ * )
+ */
+class OEmbed extends MediaSourceBase implements MediaSourceFieldConstraintsInterface {
+
+  /**
+   * The logger channel for media.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelInterface
+   */
+  protected $logger;
+
+  /**
+   * The oEmbed service.
+   *
+   * @var \Drupal\media\OEmbedInterface
+   */
+  protected $oEmbed;
+
+  /**
+   * 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\media\OEmbedInterface $oembed
+   *   The oEmbed 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, OEmbedInterface $oembed) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_field_manager, $field_type_manager, $config_factory);
+    $this->oEmbed = $oembed;
+    $this->logger = $logger;
+  }
+
+  /**
+   * {@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('media.oembed')
+    );
+  }
+
+  /**
+   * {@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_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) {
+    $main_property = $media->get($this->configuration['source_field'])->first()->mainPropertyName();
+    $resource_url = $media->get($this->configuration['source_field'])->first()->get($main_property)->getString();
+    $oembed_data = $this->oEmbed->fetchResource($this->oEmbed->doUrlDiscovery($resource_url));
+
+    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':
+        $image_url = $this->getMetadata($media, 'thumbnail_url');
+        if ($image_url) {
+          $filename = $this->getMetadata($media, 'provider_name') . '_' . substr(md5($resource_url), 0, 5);
+          return $this->getLocalImageUri($filename, $image_url);
+        }
+        return NULL;
+
+      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':
+        return parent::getMetadata($media, 'thumbnail_uri');
+
+      default:
+        if (!empty($oembed_data[$name])) {
+          return $oembed_data[$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.'),
+    ];
+
+    $provider_options = array_map(function ($provider) {
+      return $provider['provider_name'];
+    }, $this->oEmbed->getProviders());
+    $form['allowed_providers'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Allowed providers'),
+      '#multiple' => TRUE,
+      '#default_value' => $this->configuration['allowed_providers'],
+      '#options' => $provider_options,
+      '#description' => $this->t('Optionally select the allowed oEmbed providers for this media type. If left blank, all providers will be allowed.'),
+      '#attributes' => ['size' => 20],
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'thumbnails_location' => 'public://oembed_thumbnails',
+      'allowed_providers' => [],
+    ] + parent::defaultConfiguration();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getThumbnail(MediaInterface $media) {
+    if ($local_image = $this->getMetadata($media, 'thumbnail_local')) {
+      return $local_image;
+    }
+    return $this->getMetadata($media, 'thumbnail_uri');
+  }
+
+  /**
+   * Computes the destination URI for a thumbnail.
+   *
+   * @param string $filename
+   *   Filename without the extension.
+   * @param string $remote_url
+   *   The remote URL of the thumbnail.
+   *
+   * @return string
+   *   The local URI.
+   */
+  protected function getLocalImageUri($filename, $remote_url) {
+    $directory = $this->configuration['thumbnails_location'];
+    // Ensure that the destination directory is writable. If not, log a warning
+    // and return the default thumbnail.
+    $ready = file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+    if (!$ready) {
+      $this->logger->warning('Could not prepare thumbnail destination directory @dir for oEmbed media.', [
+        '@dir' => $directory,
+      ]);
+      $default_thumbnail_filename = $this->pluginDefinition['default_thumbnail_filename'];
+      return $this->configFactory->get('media.settings')->get('icon_base_uri') . '/' . $default_thumbnail_filename;
+    }
+
+    $local_uri = $this->configuration['thumbnails_location'] . '/' . $filename . '.';
+    $local_uri .= pathinfo($remote_url, PATHINFO_EXTENSION);
+
+    return $local_uri;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createSourceField(MediaTypeInterface $type) {
+    $field = parent::createSourceField($type);
+
+    $settings = $field->getSettings();
+    $settings['title'] = DRUPAL_DISABLED;
+    $settings['link_type'] = LinkItemInterface::LINK_EXTERNAL;
+
+    return $field->set('settings', $settings);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSourceFieldConstraints() {
+    return [
+      'oembed_resource' => [],
+      'oembed_provider' => [],
+    ];
+  }
+}
diff --git a/core/profiles/standard/config/optional/core.entity_form_display.media.video.default.yml b/core/profiles/standard/config/optional/core.entity_form_display.media.video.default.yml
new file mode 100644
index 0000000000..f2eed3bd02
--- /dev/null
+++ b/core/profiles/standard/config/optional/core.entity_form_display.media.video.default.yml
@@ -0,0 +1,38 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.field.media.video.field_media_oembed
+    - media.type.video
+  module:
+    - link
+id: media.video.default
+targetEntityType: media
+bundle: video
+mode: default
+content:
+  created:
+    type: datetime_timestamp
+    weight: 1
+    region: content
+    settings: {  }
+    third_party_settings: {  }
+  field_media_oembed:
+    settings:
+      placeholder_url: ''
+      placeholder_title: ''
+    third_party_settings: {  }
+    type: link_default
+    weight: 2
+    region: content
+  uid:
+    type: entity_reference_autocomplete
+    weight: 0
+    settings:
+      match_operator: CONTAINS
+      size: 60
+      placeholder: ''
+    region: content
+    third_party_settings: {  }
+hidden:
+  name: true
diff --git a/core/profiles/standard/config/optional/core.entity_view_display.media.video.default.yml b/core/profiles/standard/config/optional/core.entity_view_display.media.video.default.yml
new file mode 100644
index 0000000000..5a4466fb9b
--- /dev/null
+++ b/core/profiles/standard/config/optional/core.entity_view_display.media.video.default.yml
@@ -0,0 +1,28 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.field.media.video.field_media_oembed
+    - image.style.thumbnail
+    - media.type.video
+  module:
+    - image
+    - media
+    - user
+id: media.video.default
+targetEntityType: media
+bundle: video
+mode: default
+content:
+  field_media_oembed:
+    label: hidden
+    settings: {  }
+    third_party_settings: {  }
+    type: oembed
+    weight: 0
+    region: content
+hidden:
+  created: true
+  langcode: true
+  thumbnail: true
+  uid: true
diff --git a/core/profiles/standard/config/optional/field.field.media.video.field_media_oembed.yml b/core/profiles/standard/config/optional/field.field.media.video.field_media_oembed.yml
new file mode 100644
index 0000000000..0528e86ad4
--- /dev/null
+++ b/core/profiles/standard/config/optional/field.field.media.video.field_media_oembed.yml
@@ -0,0 +1,22 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.storage.media.field_media_oembed
+    - media.type.video
+  module:
+    - link
+id: media.video.field_media_oembed
+field_name: field_media_oembed
+entity_type: media
+bundle: video
+label: OEmbed
+description: ''
+required: false
+translatable: true
+default_value: {  }
+default_value_callback: ''
+settings:
+  title: 0
+  link_type: 16
+field_type: link
diff --git a/core/profiles/standard/config/optional/field.storage.media.field_media_oembed.yml b/core/profiles/standard/config/optional/field.storage.media.field_media_oembed.yml
new file mode 100644
index 0000000000..fab31ee29f
--- /dev/null
+++ b/core/profiles/standard/config/optional/field.storage.media.field_media_oembed.yml
@@ -0,0 +1,18 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - link
+    - media
+id: media.field_media_oembed
+field_name: field_media_oembed
+entity_type: media
+type: link
+settings: {  }
+module: link
+locked: true
+cardinality: 1
+translatable: true
+indexes: {  }
+persist_with_no_fields: false
+custom_storage: false
diff --git a/core/profiles/standard/config/optional/media.type.video.yml b/core/profiles/standard/config/optional/media.type.video.yml
new file mode 100644
index 0000000000..80ce40fd8e
--- /dev/null
+++ b/core/profiles/standard/config/optional/media.type.video.yml
@@ -0,0 +1,13 @@
+langcode: en
+status: true
+dependencies: {  }
+id: video
+label: Video
+description: "Use thie media type to store remote content such as YouTube videos."
+source: oembed
+queue_thumbnail_downloads: false
+new_revision: true
+source_configuration:
+  thumbnails_location: 'public://oembed_thumbnails'
+  source_field: field_media_oembed
+field_map: {  }
