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 @@
+renderer = $renderer;
+ }
+
+ /**
+ * {@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('renderer')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function defaultSettings() {
+ return [
+ 'controls' => TRUE,
+ 'autoplay' => FALSE,
+ 'loop' => FALSE,
+ 'multiple_file_behavior' => 'tags',
+ ] + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ $element['controls'] = [
+ '#title' => $this->t('Show audio controls'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->getSetting('controls'),
+ ];
+ $element['autoplay'] = [
+ '#title' => $this->t('Autoplay'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->getSetting('autoplay'),
+ ];
+ $element['loop'] = [
+ '#title' => $this->t('Loop'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->getSetting('loop'),
+ ];
+ $element['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'),
+ ];
+
+ return $element;
+ }
+
+ /**
+ * {@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')]);
+ $summary[] = $this->t('Multiple files: %multiple', ['%multiple' => $this->getSetting('multiple_file_behavior')]);
+ 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,
+ ];
+ foreach ($files as $file) {
+ $this->renderer->addCacheableDependency($elements[$delta], $file['file']);
+ }
+ }
+
+ return $elements;
+ }
+
+ /**
+ * Prepare the attributes according to the settings.
+ *
+ * @param array $additional_attributes
+ * Additional attributes for preparing the html tag attributes.
+ *
+ * @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 mimetype applies to formatter one.
+ *
+ * @param string $mimeType
+ * The full mimeType.
+ *
+ * @return bool
+ * Mimetype applies or not.
+ */
+ protected static function mimeTypeApplies($mimeType) {
+ list($type) = explode('/', $mimeType, 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
+ * List of files.
+ */
+ 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'.
+ 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()));
+ $source_attributes->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..386846c028
--- /dev/null
+++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileMediaFormatterInterface.php
@@ -0,0 +1,17 @@
+ FALSE,
+ 'width' => NULL,
+ 'height' => NULL,
+ ] + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ $element = parent::settingsForm($form, $form_state);
+
+ $element['muted'] = [
+ '#title' => $this->t('Muted'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->getSetting('muted'),
+ ];
+ $element['width'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Width'),
+ '#default_value' => $this->getSetting('width'),
+ '#size' => 5,
+ '#maxlength' => 5,
+ '#field_suffix' => $this->t('pixels'),
+ ];
+ $element['height'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Height'),
+ '#default_value' => $this->getSetting('height'),
+ '#size' => 5,
+ '#maxlength' => 5,
+ '#field_suffix' => $this->t('pixels'),
+ ];
+
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsSummary() {
+ $summary = parent::settingsSummary();
+ $summary[] = $this->t('Muted: %muted', ['%muted' => $this->getSetting('muted') ? $this->t('yes') : $this->t('no')]);
+ $width = $this->getSetting('width');
+ $height = $this->getSetting('height');
+ if ($width && $height) {
+ $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 = []) {
+ $video_attributes = parent::prepareAttributes(['muted']);
+ $width = $this->getSetting('width');
+ $height = $this->getSetting('height');
+ if ($width && $height) {
+ $video_attributes->setAttribute('width', $width);
+ $video_attributes->setAttribute('height', $height);
+ }
+
+ return $video_attributes;
+ }
+
+}
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..ce7c4059b2
--- /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([
+ $this->fieldName => [
+ [
+ 'target_id' => $file->id(),
+ ],
+ ],
+ ]);
+ $entity->save();
+
+ $this->drupalGet($entity->url());
+
+ $file_url = 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..2fbce8475d
--- /dev/null
+++ b/core/modules/file/tests/src/Functional/Formatter/FileMediaFormatterTestBase.php
@@ -0,0 +1,78 @@
+drupalLogin($this->drupalCreateUser(['view test entity']));
+ }
+
+ protected function createMediaField($formatter, $file_extionsions) {
+
+ $entityType = $bundle = 'entity_test';
+ $this->fieldName = Unicode::strtolower($this->randomMachineName());
+
+ FieldStorageConfig::create(array(
+ 'entity_type' => $entityType,
+ 'field_name' => $this->fieldName,
+ 'type' => 'file',
+ 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+ ))->save();
+ FieldConfig::create([
+ 'entity_type' => $entityType,
+ 'field_name' => $this->fieldName,
+ 'bundle' => $bundle,
+ 'settings' => [
+ 'file_extensions' => $file_extionsions,
+ ],
+ ])->save();
+
+ $this->display = entity_get_display('entity_test', 'entity_test', 'full');
+ $this->display->setComponent($this->fieldName, [
+ 'type' => $formatter,
+ 'settings' => [],
+ ]);
+ $this->display->save();
+ }
+
+}
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..cb55890019
--- /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([
+ $this->fieldName => [
+ [
+ 'target_id' => $file->id(),
+ ],
+ ],
+ ]);
+ $entity->save();
+
+ $this->drupalGet($entity->url());
+
+ $file_url = 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
+*/
+#}
+