summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorwebchick2017-11-17 20:07:32 (GMT)
committerwebchick2017-11-17 20:07:32 (GMT)
commit8f300f534f82f6eb80c24383273b23c69136215b (patch)
tree583ec9ae8a4e4db76b433391ac35bf4079287ecf
parentf7c9dd9352350a05667208fdd79f3eff1b86e9e8 (diff)
Issue #1174892 by chr.fritsch, travist, Dave Reid, keithm, webchick, phenaproxima, Everett Zufelt, slashrsm, Wim Leers, RobW, seanB, xjm, Gábor Hojtsy, larowlan, ericduran, webflo, Berdir, pschuelke: File field formatters for rich media display with <video> and <audio> HTML5 elements
-rw-r--r--core/modules/file/config/schema/file.schema.yml35
-rw-r--r--core/modules/file/file.module6
-rw-r--r--core/modules/file/src/Plugin/Field/FieldFormatter/FileAudioFormatter.php26
-rw-r--r--core/modules/file/src/Plugin/Field/FieldFormatter/FileMediaFormatterBase.php220
-rw-r--r--core/modules/file/src/Plugin/Field/FieldFormatter/FileMediaFormatterInterface.php26
-rw-r--r--core/modules/file/src/Plugin/Field/FieldFormatter/FileVideoFormatter.php94
-rw-r--r--core/modules/file/templates/file-audio.html.twig22
-rw-r--r--core/modules/file/templates/file-video.html.twig22
-rw-r--r--core/modules/file/tests/src/Functional/Formatter/FileAudioFormatterTest.php58
-rw-r--r--core/modules/file/tests/src/Functional/Formatter/FileMediaFormatterTestBase.php93
-rw-r--r--core/modules/file/tests/src/Functional/Formatter/FileVideoFormatterTest.php58
-rw-r--r--core/themes/classy/templates/field/file-audio.html.twig23
-rw-r--r--core/themes/classy/templates/field/file-video.html.twig23
-rw-r--r--core/themes/stable/templates/field/file-audio.html.twig22
-rw-r--r--core/themes/stable/templates/field/file-video.html.twig22
15 files changed, 750 insertions, 0 deletions
diff --git a/core/modules/file/config/schema/file.schema.yml b/core/modules/file/config/schema/file.schema.yml
index 5797ece..f94fbec 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.media:
+ type: mapping
+ label: 'Media display format settings'
+ mapping:
+ controls:
+ type: boolean
+ label: 'Show playback controls'
+ autoplay:
+ type: boolean
+ label: 'Autoplay'
+ loop:
+ type: boolean
+ label: 'Loop'
+ multiple_file_display_type:
+ type: string
+ label: 'Display of multiple files'
+
+field.formatter.settings.file_audio:
+ type: file.formatter.media
+ label: 'Audio file display format settings'
+
+field.formatter.settings.file_video:
+ type: file.formatter.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 b73dc62..60a15d6 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 0000000..970ba16
--- /dev/null
+++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileAudioFormatter.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\file\Plugin\Field\FieldFormatter;
+
+/**
+ * Plugin implementation of the 'file_audio' formatter.
+ *
+ * @FieldFormatter(
+ * id = "file_audio",
+ * label = @Translation("Audio"),
+ * description = @Translation("Display the file using an HTML5 audio tag."),
+ * field_types = {
+ * "file"
+ * }
+ * )
+ */
+class FileAudioFormatter extends FileMediaFormatterBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getMediaType() {
+ return 'audio';
+ }
+
+}
diff --git a/core/modules/file/src/Plugin/Field/FieldFormatter/FileMediaFormatterBase.php b/core/modules/file/src/Plugin/Field/FieldFormatter/FileMediaFormatterBase.php
new file mode 100644
index 0000000..8cbf88b
--- /dev/null
+++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileMediaFormatterBase.php
@@ -0,0 +1,220 @@
+<?php
+
+namespace Drupal\file\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Template\Attribute;
+
+/**
+ * Base class for media file formatter.
+ */
+abstract class FileMediaFormatterBase extends FileFormatterBase implements FileMediaFormatterInterface {
+
+ /**
+ * Gets the HTML tag for the formatter.
+ *
+ * @return string
+ * The HTML tag of this formatter.
+ */
+ protected function getHtmlTag() {
+ return static::getMediaType();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function defaultSettings() {
+ return [
+ 'controls' => TRUE,
+ 'autoplay' => FALSE,
+ 'loop' => FALSE,
+ 'multiple_file_display_type' => '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_display_type' => [
+ '#title' => $this->t('Display of multiple files'),
+ '#type' => 'radios',
+ '#options' => [
+ 'tags' => $this->t('Use multiple @tag tags, each with a single source.', ['@tag' => '<' . $this->getHtmlTag() . '>']),
+ 'sources' => $this->t('Use multiple sources within a single @tag tag.', ['@tag' => '<' . $this->getHtmlTag() . '>']),
+ ],
+ '#default_value' => $this->getSetting('multiple_file_display_type'),
+ ],
+ ];
+ }
+
+ /**
+ * {@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('Playback 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_display_type')) {
+ case 'tags':
+ $summary[] = $this->t('Multiple file display: Multiple HTML tags');
+ break;
+
+ case 'sources':
+ $summary[] = $this->t('Multiple file display: 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' => $this->getPluginId(),
+ '#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 media type 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::getMediaType();
+ }
+
+ /**
+ * 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 = [];
+ // 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_url_transform_relative(file_create_url($file->getFileUri())))
+ ->setAttribute('type', $file->getMimeType());
+ if ($this->getSetting('multiple_file_display_type') === '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 0000000..98f1de1
--- /dev/null
+++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileMediaFormatterInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\file\Plugin\Field\FieldFormatter;
+
+/**
+ * Defines getter methods for FileMediaFormatterBase.
+ *
+ * This interface is used on the FileMediaFormatterBase class to ensure that
+ * each file media formatter will be based on a media type.
+ *
+ * Abstract classes are not able to implement abstract static methods,
+ * this interface will work around that.
+ *
+ * @see \Drupal\file\Plugin\Field\FieldFormatter\FileMediaFormatterBase
+ */
+interface FileMediaFormatterInterface {
+
+ /**
+ * Gets the applicable media type for a formatter.
+ *
+ * @return string
+ * The media type of this formatter.
+ */
+ public static function getMediaType();
+
+}
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 0000000..20c1369
--- /dev/null
+++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileVideoFormatter.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Drupal\file\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Plugin implementation of the 'file_video' formatter.
+ *
+ * @FieldFormatter(
+ * id = "file_video",
+ * label = @Translation("Video"),
+ * description = @Translation("Display the file using an HTML5 video tag."),
+ * field_types = {
+ * "file"
+ * }
+ * )
+ */
+class FileVideoFormatter extends FileMediaFormatterBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getMediaType() {
+ return 'video';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function defaultSettings() {
+ return [
+ 'muted' => 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 0000000..c98725b
--- /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
+*/
+#}
+<audio {{ attributes }}>
+ {% for file in files %}
+ <source {{ file.source_attributes }} />
+ {% endfor %}
+</audio>
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 0000000..162fd49
--- /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
+*/
+#}
+<video {{ attributes }}>
+ {% for file in files %}
+ <source {{ file.source_attributes }} />
+ {% endfor %}
+</video>
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 0000000..ae75c70
--- /dev/null
+++ b/core/modules/file/tests/src/Functional/Formatter/FileAudioFormatterTest.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\Tests\file\Functional\Formatter;
+
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\file\Entity\File;
+
+/**
+ * @coversDefaultClass \Drupal\file\Plugin\Field\FieldFormatter\FileAudioFormatter
+ * @group file
+ */
+class FileAudioFormatterTest extends FileMediaFormatterTestBase {
+
+ /**
+ * @covers ::viewElements
+ *
+ * @dataProvider dataProvider
+ */
+ public function testRender($tag_count, $formatter_settings) {
+ $field_config = $this->createMediaField('file_audio', 'mp3', $formatter_settings);
+
+ file_put_contents('public://file.mp3', str_repeat('t', 10));
+ $file1 = File::create([
+ 'uri' => 'public://file.mp3',
+ 'filename' => 'file.mp3',
+ ]);
+ $file1->save();
+
+ $file2 = File::create([
+ 'uri' => 'public://file.mp3',
+ 'filename' => 'file.mp3',
+ ]);
+ $file2->save();
+
+ $entity = EntityTest::create([
+ $field_config->getName() => [
+ [
+ 'target_id' => $file1->id(),
+ ],
+ [
+ 'target_id' => $file2->id(),
+ ],
+ ],
+ ]);
+ $entity->save();
+
+ $this->drupalGet($entity->toUrl());
+
+ $file1_url = file_url_transform_relative(file_create_url($file1->getFileUri()));
+ $file2_url = file_url_transform_relative(file_create_url($file2->getFileUri()));
+
+ $assert_session = $this->assertSession();
+ $assert_session->elementsCount('css', 'audio[controls="controls"]', $tag_count);
+ $assert_session->elementExists('css', "audio > source[src='$file1_url'][type='audio/mpeg']");
+ $assert_session->elementExists('css', "audio > source[src='$file2_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 0000000..5ff99f4
--- /dev/null
+++ b/core/modules/file/tests/src/Functional/Formatter/FileMediaFormatterTestBase.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace Drupal\Tests\file\Functional\Formatter;
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Provides methods specifically for testing File module's media formatter's.
+ */
+abstract class FileMediaFormatterTestBase extends BrowserTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = [
+ 'entity_test',
+ 'field',
+ 'file',
+ 'user',
+ 'system',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+ $this->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.
+ * @param array $formatter_settings
+ * Settings for the formatter.
+ *
+ * @return \Drupal\field\Entity\FieldConfig
+ * Newly created file field.
+ */
+ protected function createMediaField($formatter, $file_extensions, array $formatter_settings = []) {
+ $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' => $formatter_settings,
+ ])->save();
+
+ return $field_config;
+ }
+
+ /**
+ * Data provider for testRender.
+ *
+ * @return array
+ * An array of data arrays.
+ * The data array contains:
+ * - The number of expected HTML tags.
+ * - An array of settings for the field formatter.
+ */
+ public function dataProvider() {
+ return [
+ [2, []],
+ [1, ['multiple_file_display_type' => 'sources']],
+ ];
+ }
+
+}
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 0000000..0cc997c
--- /dev/null
+++ b/core/modules/file/tests/src/Functional/Formatter/FileVideoFormatterTest.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\Tests\file\Functional\Formatter;
+
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\file\Entity\File;
+
+/**
+ * @coversDefaultClass \Drupal\file\Plugin\Field\FieldFormatter\FileVideoFormatter
+ * @group file
+ */
+class FileVideoFormatterTest extends FileMediaFormatterTestBase {
+
+ /**
+ * @covers ::viewElements
+ *
+ * @dataProvider dataProvider
+ */
+ public function testRender($tag_count, $formatter_settings) {
+ $field_config = $this->createMediaField('file_video', 'mp4', $formatter_settings);
+
+ file_put_contents('public://file.mp4', str_repeat('t', 10));
+ $file1 = File::create([
+ 'uri' => 'public://file.mp4',
+ 'filename' => 'file.mp4',
+ ]);
+ $file1->save();
+
+ $file2 = File::create([
+ 'uri' => 'public://file.mp4',
+ 'filename' => 'file.mp4',
+ ]);
+ $file2->save();
+
+ $entity = EntityTest::create([
+ $field_config->getName() => [
+ [
+ 'target_id' => $file1->id(),
+ ],
+ [
+ 'target_id' => $file2->id(),
+ ],
+ ],
+ ]);
+ $entity->save();
+
+ $this->drupalGet($entity->toUrl());
+
+ $file1_url = file_url_transform_relative(file_create_url($file1->getFileUri()));
+ $file2_url = file_url_transform_relative(file_create_url($file2->getFileUri()));
+
+ $assert_session = $this->assertSession();
+ $assert_session->elementsCount('css', 'video[controls="controls"]', $tag_count);
+ $assert_session->elementExists('css', "video > source[src='$file1_url'][type='video/mp4']");
+ $assert_session->elementExists('css', "video > source[src='$file2_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 0000000..f573e32
--- /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') }}
+<audio {{ attributes }}>
+ {% for file in files %}
+ <source {{ file.source_attributes }} />
+ {% endfor %}
+</audio>
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 0000000..88ab05b
--- /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') }}
+<video {{ attributes }}>
+ {% for file in files %}
+ <source {{ file.source_attributes }} />
+ {% endfor %}
+</video>
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 0000000..c98725b
--- /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
+*/
+#}
+<audio {{ attributes }}>
+ {% for file in files %}
+ <source {{ file.source_attributes }} />
+ {% endfor %}
+</audio>
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 0000000..162fd49
--- /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
+*/
+#}
+<video {{ attributes }}>
+ {% for file in files %}
+ <source {{ file.source_attributes }} />
+ {% endfor %}
+</video>