diff --git a/core/modules/file/config/schema/file.schema.yml b/core/modules/file/config/schema/file.schema.yml
index 336550904b..1c16cf5cc2 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 audio 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 b08fad53e0..81d549ded9 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..1a957de807
--- /dev/null
+++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileAudioFormatter.php
@@ -0,0 +1,24 @@
+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'] = array(
+ '#title' => $this->t('Show audio controls'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->getSetting('controls'),
+ );
+ $element['autoplay'] = array(
+ '#title' => $this->t('Autoplay'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->getSetting('autoplay'),
+ );
+ $element['loop'] = array(
+ '#title' => $this->t('Loop'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->getSetting('loop'),
+ );
+ $element['multiple_file_behavior'] = array(
+ '#title' => $this->t('Display of multiple files'),
+ '#type' => 'radios',
+ '#options' => array(
+ 'tags' => $this->t('Use multiple @tag tags, each with a single source.', array('@tag' => '<' . static::APPLICABLE_MIME_TYPE . '>')),
+ 'sources' => $this->t('Use multiple sources within a single @tag tag.', array('@tag' => '<' . static::APPLICABLE_MIME_TYPE . '>')),
+ ),
+ '#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 MimeTypeGuesserInterface $extension_mime_type_guesser */
+ $extension_mime_type_guesser = \Drupal::service('file.mime_type.guesser.extension');
+
+ $extensionList = array_filter(preg_split('/\s+/', $field_definition->getSetting('file_extensions')));
+
+ foreach ($extensionList as $extension) {
+ $mimeType = $extension_mime_type_guesser->guess('fakedFile.' . $extension);
+
+ if (static::getMimeTypeType($mimeType) == static::APPLICABLE_MIME_TYPE) {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsSummary() {
+ $summary = [];
+ $summary[] = $this->t('Controls: %controls', array('%controls' => $this->getSetting('controls') ? $this->t('visible') : $this->t('hidden')));
+ $summary[] = $this->t('Autoplay: %autoplay', array('%autoplay' => $this->getSetting('autoplay') ? $this->t('yes') : $this->t('no')));
+ $summary[] = $this->t('Loop: %loop', array('%loop' => $this->getSetting('loop') ? $this->t('yes') : $this->t('no')));
+ $summary[] = $this->t('Multiple files: %multiple', array('%multiple' => $this->getSetting('multiple_file_behavior')));
+ return $summary;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function viewElements(FieldItemListInterface $items, $langcode) {
+ $elements = array();
+
+ $source_files = $this->getSourceFiles($items, $langcode);
+
+ if (empty($source_files)) {
+ return $elements;
+ }
+
+ $attributes = $this->prepareAttributes();
+ foreach ($source_files as $delta => $files) {
+ $elements[$delta] = array(
+ '#theme' => 'file_' . static::APPLICABLE_MIME_TYPE,
+ '#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.
+ *
+ * @return Attribute
+ */
+ protected function prepareAttributes(array $additional_attributes = []) {
+ $attributes = new Attribute();
+ foreach (array('controls', 'autoplay', 'loop') + $additional_attributes as $attribute) {
+ if ($this->getSetting($attribute)) {
+ $attributes->setAttribute($attribute, $attribute);
+ }
+ }
+ return $attributes;
+ }
+
+ /**
+ * Returns the first part of the mimetype of the file.
+ *
+ * @param string $mimeType
+ * The full mimeType.
+ *
+ * @return string
+ * The last part of a mimetype.
+ */
+ protected static function getMimeTypeType($mimeType) {
+ list($type) = explode('/', $mimeType, 2);
+ return $type;
+ }
+
+ /**
+ * 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::getMimeTypeType($file->getMimeType()) == static::APPLICABLE_MIME_TYPE) {
+ $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[] = array(
+ array(
+ 'file' => $file,
+ 'source_attributes' => $source_attributes
+ )
+ );
+ }
+ else {
+ $source_files[0][] = array(
+ 'file' => $file,
+ 'source_attributes' => $source_attributes
+ );
+ }
+ }
+ }
+
+ return $source_files;
+ }
+
+}
diff --git a/core/modules/file/src/Plugin/Field/FieldFormatter/FileVideoFormatter.php b/core/modules/file/src/Plugin/Field/FieldFormatter/FileVideoFormatter.php
new file mode 100644
index 0000000000..92bed4f31a
--- /dev/null
+++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileVideoFormatter.php
@@ -0,0 +1,103 @@
+ FALSE,
+ 'width' => NULL,
+ 'height' => NULL,
+ ] + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ $element = parent::settingsForm($form, $form_state);
+
+ $element['muted'] = array(
+ '#title' => $this->t('Muted'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->getSetting('muted'),
+ );
+ $element['width'] = array(
+ '#type' => 'textfield',
+ '#title' => $this->t('Width'),
+ '#default_value' => $this->getSetting('width'),
+ '#size' => 5,
+ '#maxlength' => 5,
+ '#field_suffix' => $this->t('pixels'),
+ );
+ $element['height'] = array(
+ '#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', array('%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', array(
+ '%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
+*/
+#}
+
+ {% for file in files %}
+
+ {% endfor %}
+
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
+*/
+#}
+
+ {% for file in files %}
+
+ {% endfor %}
+
diff --git a/core/modules/file/tests/src/Kernel/Formatter/FileAudioFormatterTest.php b/core/modules/file/tests/src/Kernel/Formatter/FileAudioFormatterTest.php
new file mode 100644
index 0000000000..c9d3aa586c
--- /dev/null
+++ b/core/modules/file/tests/src/Kernel/Formatter/FileAudioFormatterTest.php
@@ -0,0 +1,130 @@
+installConfig(['field']);
+ $this->installEntitySchema('entity_test');
+ $this->installEntitySchema('file');
+ $this->installSchema('file', array('file_usage'));
+
+ $entityType = 'entity_test';
+ $bundle = $entityType;
+ $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' => 'mp3',
+ ],
+ ])->save();
+
+ $this->display = EntityViewDisplay::create([
+ 'targetEntityType' => $entityType,
+ 'bundle' => $bundle,
+ 'mode' => 'default',
+ 'content' => [],
+ ]);
+ $this->display->setComponent($this->fieldName, [
+ 'type' => 'file_audio',
+ 'settings' => [],
+ ]);
+ $this->display->save();
+ }
+
+ /**
+ * @covers ::viewElements
+ */
+ public function testRender() {
+ 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();
+
+ $build = $this->display->build($entity);
+
+ \Drupal::service('renderer')->renderRoot($build);
+
+ $src = file_create_url($file->getFileUri());
+
+ $expected_output = <<
+ $this->fieldName
+
+
+
+EXPECTED;
+ $this->assertEquals($expected_output, $build[$this->fieldName]['#markup']);
+ }
+
+}
diff --git a/core/modules/file/tests/src/Kernel/Formatter/FileVideoFormatterTest.php b/core/modules/file/tests/src/Kernel/Formatter/FileVideoFormatterTest.php
new file mode 100644
index 0000000000..fba23f5af4
--- /dev/null
+++ b/core/modules/file/tests/src/Kernel/Formatter/FileVideoFormatterTest.php
@@ -0,0 +1,130 @@
+installConfig(['field']);
+ $this->installEntitySchema('entity_test');
+ $this->installEntitySchema('file');
+ $this->installSchema('file', array('file_usage'));
+
+ $entityType = 'entity_test';
+ $bundle = $this->entityType;
+ $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' => 'mp4',
+ ],
+ ])->save();
+
+ $this->display = EntityViewDisplay::create([
+ 'targetEntityType' => $entityType,
+ 'bundle' => $bundle,
+ 'mode' => 'default',
+ 'content' => [],
+ ]);
+ $this->display->setComponent($this->fieldName, [
+ 'type' => 'file_video',
+ 'settings' => [],
+ ]);
+ $this->display->save();
+ }
+
+ /**
+ * @covers ::viewElements
+ */
+ public function testRender() {
+ 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();
+
+ $build = $this->display->build($entity);
+
+ \Drupal::service('renderer')->renderRoot($build);
+
+ $src = file_create_url($file->getFileUri());
+
+ $expected_output = <<
+ $this->fieldName
+
+
+
+EXPECTED;
+ $this->assertEquals($expected_output, $build[$this->fieldName]['#markup']);
+ }
+
+}
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') }}
+
+ {% for file in files %}
+
+ {% endfor %}
+
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') }}
+
+ {% for file in files %}
+
+ {% endfor %}
+
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
+*/
+#}
+
+ {% for file in files %}
+
+ {% endfor %}
+
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
+*/
+#}
+
+ {% for file in files %}
+
+ {% endfor %}
+