diff --git a/core/modules/file/config/schema/file.schema.yml b/core/modules/file/config/schema/file.schema.yml
index 5797ececdd..9a3fb207a8 100644
--- a/core/modules/file/config/schema/file.schema.yml
+++ b/core/modules/file/config/schema/file.schema.yml
@@ -70,6 +70,41 @@ field.field_settings.file:
type: boolean
label: 'Enable Description field'
+file.formatter.rich_media:
+ type: mapping
+ label: 'Rich media display format settings'
+ mapping:
+ controls:
+ type: boolean
+ label: 'Show controls'
+ autoplay:
+ type: boolean
+ label: 'Autoplay'
+ loop:
+ type: boolean
+ label: 'Loop'
+ multiple_file_behavior:
+ type: string
+ label: 'Display of multiple files'
+
+field.formatter.settings.file_audio:
+ type: file.formatter.rich_media
+ label: 'Audio file display format settings'
+
+field.formatter.settings.file_video:
+ type: file.formatter.rich_media
+ label: 'Video file display format settings'
+ mapping:
+ muted:
+ type: boolean
+ label: 'Muted'
+ width:
+ type: integer
+ label: 'Width'
+ height:
+ type: integer
+ label: 'Height'
+
field.formatter.settings.file_default:
type: mapping
label: 'Generic file format settings'
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index 1b55fe0e74..71ff20334b 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -571,6 +571,12 @@ function file_theme() {
'file_managed_file' => [
'render element' => 'element',
],
+ 'file_audio' => [
+ 'variables' => ['files' => [], 'attributes' => NULL],
+ ],
+ 'file_video' => [
+ 'variables' => ['files' => [], 'attributes' => NULL],
+ ],
// From file.field.inc.
'file_widget_multiple' => [
diff --git a/core/modules/file/src/Plugin/Field/FieldFormatter/FileAudioFormatter.php b/core/modules/file/src/Plugin/Field/FieldFormatter/FileAudioFormatter.php
new file mode 100644
index 0000000000..4030643115
--- /dev/null
+++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileAudioFormatter.php
@@ -0,0 +1,26 @@
+ TRUE,
+ 'autoplay' => FALSE,
+ 'loop' => FALSE,
+ 'multiple_file_behavior' => 'tags',
+ ] + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ return [
+ 'controls' => [
+ '#title' => $this->t('Show playback controls'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->getSetting('controls'),
+ ],
+ 'autoplay' => [
+ '#title' => $this->t('Autoplay'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->getSetting('autoplay'),
+ ],
+ 'loop' => [
+ '#title' => $this->t('Loop'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->getSetting('loop'),
+ ],
+ 'multiple_file_behavior' => [
+ '#title' => $this->t('Display of multiple files'),
+ '#type' => 'radios',
+ '#options' => [
+ 'tags' => $this->t('Use multiple @tag tags, each with a single source.', ['@tag' => '<' . static::getMimeType() . '>']),
+ 'sources' => $this->t('Use multiple sources within a single @tag tag.', ['@tag' => '<' . static::getMimeType() . '>']),
+ ],
+ '#default_value' => $this->getSetting('multiple_file_behavior'),
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function isApplicable(FieldDefinitionInterface $field_definition) {
+ if (!parent::isApplicable($field_definition)) {
+ return FALSE;
+ }
+ /** @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $extension_mime_type_guesser */
+ $extension_mime_type_guesser = \Drupal::service('file.mime_type.guesser.extension');
+ $extension_list = array_filter(preg_split('/\s+/', $field_definition->getSetting('file_extensions')));
+
+ foreach ($extension_list as $extension) {
+ $mime_type = $extension_mime_type_guesser->guess('fakedFile.' . $extension);
+
+ if (static::mimeTypeApplies($mime_type)) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsSummary() {
+ $summary = [];
+ $summary[] = $this->t('Controls: %controls', ['%controls' => $this->getSetting('controls') ? $this->t('visible') : $this->t('hidden')]);
+ $summary[] = $this->t('Autoplay: %autoplay', ['%autoplay' => $this->getSetting('autoplay') ? $this->t('yes') : $this->t('no')]);
+ $summary[] = $this->t('Loop: %loop', ['%loop' => $this->getSetting('loop') ? $this->t('yes') : $this->t('no')]);
+ switch ($this->getSetting('multiple_file_behavior')) {
+ case 'tags':
+ $summary[] = $this->t('Multiple files: Multiple HTML tags');
+ break;
+
+ case 'sources':
+ $summary[] = $this->t('Multiple files: One HTML tag with multiple sources');
+ break;
+ }
+ return $summary;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function viewElements(FieldItemListInterface $items, $langcode) {
+ $elements = [];
+
+ $source_files = $this->getSourceFiles($items, $langcode);
+ if (empty($source_files)) {
+ return $elements;
+ }
+
+ $attributes = $this->prepareAttributes();
+ foreach ($source_files as $delta => $files) {
+ $elements[$delta] = [
+ '#theme' => 'file_' . static::getMimeType(),
+ '#attributes' => $attributes,
+ '#files' => $files,
+ '#cache' => ['tags' => []],
+ ];
+
+ $cache_tags = [];
+ foreach ($files as $file) {
+ $cache_tags = Cache::mergeTags($cache_tags, $file['file']->getCacheTags());
+ }
+ $elements[$delta]['#cache']['tags'] = $cache_tags;
+ }
+
+ return $elements;
+ }
+
+ /**
+ * Prepare the attributes according to the settings.
+ *
+ * @param string[] $additional_attributes
+ * Additional attributes to be applied to the HTML element. Attribute names
+ * will be used as key and value in the HTML element.
+ *
+ * @return \Drupal\Core\Template\Attribute
+ * Container with all the attributes for the HTML tag.
+ */
+ protected function prepareAttributes(array $additional_attributes = []) {
+ $attributes = new Attribute();
+ foreach (['controls', 'autoplay', 'loop'] + $additional_attributes as $attribute) {
+ if ($this->getSetting($attribute)) {
+ $attributes->setAttribute($attribute, $attribute);
+ }
+ }
+ return $attributes;
+ }
+
+ /**
+ * Check if given MIME type applies to the one of the formatter.
+ *
+ * @param string $mime_type
+ * The complete MIME type.
+ *
+ * @return bool
+ * TRUE if the MIME type applies, FALSE otherwise.
+ */
+ protected static function mimeTypeApplies($mime_type) {
+ list($type) = explode('/', $mime_type, 2);
+ return ($type === static::getMimeType());
+ }
+
+ /**
+ * Gets source files with attributes.
+ *
+ * @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items
+ * The item list.
+ * @param string $langcode
+ * The language code of the referenced entities to display.
+ *
+ * @return array
+ * Numerically indexed array, which again contains an associative array with
+ * the following key/values:
+ * - file => \Drupal\file\Entity\File
+ * - source_attributes => \Drupal\Core\Template\Attribute
+ */
+ protected function getSourceFiles(EntityReferenceFieldItemListInterface $items, $langcode) {
+ $source_files = [];
+ $multiple_file_behavior = $this->getSetting('multiple_file_behavior');
+
+ // Because we can have the files grouped in a single media tag, we do a
+ // grouping in case the multiple file behavior is not 'tags'.
+ /** @var \Drupal\file\Entity\File $file */
+ foreach ($this->getEntitiesToView($items, $langcode) as $file) {
+ if (static::mimeTypeApplies($file->getMimeType())) {
+ $source_attributes = new Attribute();
+ $source_attributes
+ ->setAttribute('src', file_create_url($file->getFileUri()))
+ ->setAttribute('type', $file->getMimeType());
+ if ($multiple_file_behavior === 'tags') {
+ $source_files[] = [
+ [
+ 'file' => $file,
+ 'source_attributes' => $source_attributes,
+ ],
+ ];
+ }
+ else {
+ $source_files[0][] = [
+ 'file' => $file,
+ 'source_attributes' => $source_attributes,
+ ];
+ }
+ }
+ }
+ return $source_files;
+ }
+
+}
diff --git a/core/modules/file/src/Plugin/Field/FieldFormatter/FileMediaFormatterInterface.php b/core/modules/file/src/Plugin/Field/FieldFormatter/FileMediaFormatterInterface.php
new file mode 100644
index 0000000000..d4ac05c6d3
--- /dev/null
+++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileMediaFormatterInterface.php
@@ -0,0 +1,26 @@
+ FALSE,
+ 'width' => 640,
+ 'height' => 480,
+ ] + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ return parent::settingsForm($form, $form_state) + [
+ 'muted' => [
+ '#title' => $this->t('Muted'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->getSetting('muted'),
+ ],
+ 'width' => [
+ '#type' => 'number',
+ '#title' => $this->t('Width'),
+ '#default_value' => $this->getSetting('width'),
+ '#size' => 5,
+ '#maxlength' => 5,
+ '#field_suffix' => $this->t('pixels'),
+ '#min' => 0,
+ '#required' => TRUE,
+ ],
+ 'height' => [
+ '#type' => 'number',
+ '#title' => $this->t('Height'),
+ '#default_value' => $this->getSetting('height'),
+ '#size' => 5,
+ '#maxlength' => 5,
+ '#field_suffix' => $this->t('pixels'),
+ '#min' => 0,
+ '#required' => TRUE,
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsSummary() {
+ $summary = parent::settingsSummary();
+ $summary[] = $this->t('Muted: %muted', ['%muted' => $this->getSetting('muted') ? $this->t('yes') : $this->t('no')]);
+ $summary[] = $this->t('Size: %width x %height pixels', [
+ '%width' => $this->getSetting('width'),
+ '%height' => $this->getSetting('height'),
+ ]);
+ return $summary;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function prepareAttributes(array $additional_attributes = []) {
+ return parent::prepareAttributes(['muted'])
+ ->setAttribute('width', $this->getSetting('width'))
+ ->setAttribute('height', $this->getSetting('height'));
+ }
+
+}
diff --git a/core/modules/file/templates/file-audio.html.twig b/core/modules/file/templates/file-audio.html.twig
new file mode 100644
index 0000000000..c98725b99b
--- /dev/null
+++ b/core/modules/file/templates/file-audio.html.twig
@@ -0,0 +1,22 @@
+{#
+/**
+* @file
+* Default theme implementation to display the file entity as an audio tag.
+*
+* Available variables:
+* - attributes: An array of HTML attributes, intended to be added to the
+* audio tag.
+* - files: And array of files to be added as sources for the audio tag. Each
+* element is an array with the following elements:
+* - file: The full file object.
+* - source_attributes: An array of HTML attributes for to be added to the
+* source tag.
+*
+* @ingroup themeable
+*/
+#}
+
diff --git a/core/modules/file/templates/file-video.html.twig b/core/modules/file/templates/file-video.html.twig
new file mode 100644
index 0000000000..162fd4933e
--- /dev/null
+++ b/core/modules/file/templates/file-video.html.twig
@@ -0,0 +1,22 @@
+{#
+/**
+* @file
+* Default theme implementation to display the file entity as a video tag.
+*
+* Available variables:
+* - attributes: An array of HTML attributes, intended to be added to the
+* video tag.
+* - files: And array of files to be added as sources for the video tag. Each
+* element is an array with the following elements:
+* - file: The full file object.
+* - source_attributes: An array of HTML attributes for to be added to the
+* source tag.
+*
+* @ingroup themeable
+*/
+#}
+
diff --git a/core/modules/file/tests/src/Functional/Formatter/FileAudioFormatterTest.php b/core/modules/file/tests/src/Functional/Formatter/FileAudioFormatterTest.php
new file mode 100644
index 0000000000..a1e7b342cc
--- /dev/null
+++ b/core/modules/file/tests/src/Functional/Formatter/FileAudioFormatterTest.php
@@ -0,0 +1,43 @@
+createMediaField('file_audio', 'mp3');
+
+ file_put_contents('public://file.mp3', str_repeat('t', 10));
+ $file = File::create([
+ 'uri' => 'public://file.mp3',
+ 'filename' => 'file.mp3',
+ ]);
+ $file->save();
+
+ $entity = EntityTest::create([
+ $field_config->getName() => [
+ [
+ 'target_id' => $file->id(),
+ ],
+ ],
+ ]);
+ $entity->save();
+
+ $this->drupalGet($entity->toUrl());
+
+ $file_url = file_url_transform_relative(file_create_url($file->getFileUri()));
+ $this->assertSession()->elementExists('css', 'audio[controls="controls"]');
+ $this->assertSession()->elementExists('css', "audio > source[src*='$file_url'][type='audio/mpeg']");
+ }
+
+}
diff --git a/core/modules/file/tests/src/Functional/Formatter/FileMediaFormatterTestBase.php b/core/modules/file/tests/src/Functional/Formatter/FileMediaFormatterTestBase.php
new file mode 100644
index 0000000000..9d0511adc0
--- /dev/null
+++ b/core/modules/file/tests/src/Functional/Formatter/FileMediaFormatterTestBase.php
@@ -0,0 +1,75 @@
+drupalLogin($this->drupalCreateUser(['view test entity']));
+ }
+
+ /**
+ * Creates a file field and set's the correct formatter.
+ *
+ * @param string $formatter
+ * The formatter ID.
+ * @param string $file_extensions
+ * The file extensions of the new field.
+ *
+ * @return \Drupal\field\Entity\FieldConfig
+ * Newly created file field.
+ */
+ protected function createMediaField($formatter, $file_extensions) {
+ $entity_type = $bundle = 'entity_test';
+ $field_name = Unicode::strtolower($this->randomMachineName());
+
+ FieldStorageConfig::create([
+ 'entity_type' => $entity_type,
+ 'field_name' => $field_name,
+ 'type' => 'file',
+ 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+ ])->save();
+ $field_config = FieldConfig::create([
+ 'entity_type' => $entity_type,
+ 'field_name' => $field_name,
+ 'bundle' => $bundle,
+ 'settings' => [
+ 'file_extensions' => trim($file_extensions),
+ ],
+ ]);
+ $field_config->save();
+
+ $display = entity_get_display('entity_test', 'entity_test', 'full');
+ $display->setComponent($field_name, [
+ 'type' => $formatter,
+ 'settings' => [],
+ ])->save();
+
+ return $field_config;
+ }
+
+}
diff --git a/core/modules/file/tests/src/Functional/Formatter/FileVideoFormatterTest.php b/core/modules/file/tests/src/Functional/Formatter/FileVideoFormatterTest.php
new file mode 100644
index 0000000000..9f5b1a9dc7
--- /dev/null
+++ b/core/modules/file/tests/src/Functional/Formatter/FileVideoFormatterTest.php
@@ -0,0 +1,43 @@
+createMediaField('file_video', 'mp4');
+
+ file_put_contents('public://file.mp4', str_repeat('t', 10));
+ $file = File::create([
+ 'uri' => 'public://file.mp4',
+ 'filename' => 'file.mp4',
+ ]);
+ $file->save();
+
+ $entity = EntityTest::create([
+ $field_config->getName() => [
+ [
+ 'target_id' => $file->id(),
+ ],
+ ],
+ ]);
+ $entity->save();
+
+ $this->drupalGet($entity->toUrl());
+
+ $file_url = file_url_transform_relative(file_create_url($file->getFileUri()));
+ $this->assertSession()->elementExists('css', 'video[controls="controls"]');
+ $this->assertSession()->elementExists('css', "video > source[src*='$file_url'][type='video/mp4']");
+ }
+
+}
diff --git a/core/themes/classy/templates/field/file-audio.html.twig b/core/themes/classy/templates/field/file-audio.html.twig
new file mode 100644
index 0000000000..f573e32d46
--- /dev/null
+++ b/core/themes/classy/templates/field/file-audio.html.twig
@@ -0,0 +1,23 @@
+{#
+/**
+* @file
+* Default theme implementation to display the file entity as an audio tag.
+*
+* Available variables:
+* - attributes: An array of HTML attributes, intended to be added to the
+* audio tag.
+* - files: And array of files to be added as sources for the audio tag. Each
+* element is an array with the following elements:
+* - file: The full file object.
+* - source_attributes: An array of HTML attributes for to be added to the
+* source tag.
+*
+* @ingroup themeable
+*/
+#}
+{{ attach_library('classy/file') }}
+
diff --git a/core/themes/classy/templates/field/file-video.html.twig b/core/themes/classy/templates/field/file-video.html.twig
new file mode 100644
index 0000000000..88ab05bbc2
--- /dev/null
+++ b/core/themes/classy/templates/field/file-video.html.twig
@@ -0,0 +1,23 @@
+{#
+/**
+* @file
+* Default theme implementation to display the file entity as a video tag.
+*
+* Available variables:
+* - attributes: An array of HTML attributes, intended to be added to the
+* video tag.
+* - files: And array of files to be added as sources for the video tag. Each
+* element is an array with the following elements:
+* - file: The full file object.
+* - source_attributes: An array of HTML attributes for to be added to the
+* source tag.
+*
+* @ingroup themeable
+*/
+#}
+{{ attach_library('classy/file') }}
+
diff --git a/core/themes/stable/templates/field/file-audio.html.twig b/core/themes/stable/templates/field/file-audio.html.twig
new file mode 100644
index 0000000000..c98725b99b
--- /dev/null
+++ b/core/themes/stable/templates/field/file-audio.html.twig
@@ -0,0 +1,22 @@
+{#
+/**
+* @file
+* Default theme implementation to display the file entity as an audio tag.
+*
+* Available variables:
+* - attributes: An array of HTML attributes, intended to be added to the
+* audio tag.
+* - files: And array of files to be added as sources for the audio tag. Each
+* element is an array with the following elements:
+* - file: The full file object.
+* - source_attributes: An array of HTML attributes for to be added to the
+* source tag.
+*
+* @ingroup themeable
+*/
+#}
+
diff --git a/core/themes/stable/templates/field/file-video.html.twig b/core/themes/stable/templates/field/file-video.html.twig
new file mode 100644
index 0000000000..162fd4933e
--- /dev/null
+++ b/core/themes/stable/templates/field/file-video.html.twig
@@ -0,0 +1,22 @@
+{#
+/**
+* @file
+* Default theme implementation to display the file entity as a video tag.
+*
+* Available variables:
+* - attributes: An array of HTML attributes, intended to be added to the
+* video tag.
+* - files: And array of files to be added as sources for the video tag. Each
+* element is an array with the following elements:
+* - file: The full file object.
+* - source_attributes: An array of HTML attributes for to be added to the
+* source tag.
+*
+* @ingroup themeable
+*/
+#}
+