diff --git a/core/misc/cspell/dictionary.txt b/core/misc/cspell/dictionary.txt
index 8fcbd4fe32..8ce92539dc 100644
--- a/core/misc/cspell/dictionary.txt
+++ b/core/misc/cspell/dictionary.txt
@@ -780,6 +780,7 @@ spreadsheetml
squaresmall
squiz
squizlabs
+srclang
srcset
ssess
stardivision
diff --git a/core/modules/file/config/schema/file.schema.yml b/core/modules/file/config/schema/file.schema.yml
index aaa74af750..e242070b1f 100644
--- a/core/modules/file/config/schema/file.schema.yml
+++ b/core/modules/file/config/schema/file.schema.yml
@@ -126,6 +126,12 @@ field.formatter.settings.file_video:
height:
type: integer
label: 'Height'
+ poster:
+ type: string
+ label: 'Poster'
+ transcript:
+ type: string
+ label: 'Transcript'
field.formatter.settings.file_default:
type: mapping
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index 997879636e..a527b912fb 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -381,7 +381,7 @@ function file_theme() {
'variables' => ['files' => [], 'attributes' => NULL],
],
'file_video' => [
- 'variables' => ['files' => [], 'attributes' => NULL],
+ 'variables' => ['files' => [], 'attributes' => NULL, 'transcript' => []],
],
'file_widget_multiple' => [
'render element' => 'element',
diff --git a/core/modules/file/src/Plugin/Field/FieldFormatter/FileVideoFormatter.php b/core/modules/file/src/Plugin/Field/FieldFormatter/FileVideoFormatter.php
index 20c1369924..dcb0caf686 100644
--- a/core/modules/file/src/Plugin/Field/FieldFormatter/FileVideoFormatter.php
+++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileVideoFormatter.php
@@ -3,6 +3,10 @@
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Language\Language;
+use Drupal\Core\Template\Attribute;
+use Drupal\Core\TypedData\TranslatableInterface;
/**
* Plugin implementation of the 'file_video' formatter.
@@ -33,6 +37,8 @@ public static function defaultSettings() {
'muted' => FALSE,
'width' => 640,
'height' => 480,
+ 'poster' => '',
+ 'transcript' => '',
] + parent::defaultSettings();
}
@@ -40,6 +46,26 @@ public static function defaultSettings() {
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
+ // Get all file fields for html5 transcript.
+ $entityFieldManager = \Drupal::service('entity_field.manager');
+ $fields = $entityFieldManager->getFieldDefinitions($form['#entity_type'], $form['#bundle']);
+ $file_fields = [];
+ foreach ($fields as $field) {
+ if ($field->getType() == 'file') {
+ $file_fields[$field->getName()] = $field->getLabel();
+ }
+ }
+
+ // Get all image fields for html5 poster.
+ $entityFieldManager = \Drupal::service('entity_field.manager');
+ $fields = $entityFieldManager->getFieldDefinitions($form['#entity_type'], $form['#bundle']);
+ $image_fields = [];
+ foreach ($fields as $field) {
+ if ($field->getType() == 'image') {
+ $image_fields[$field->getName()] = $field->getLabel();
+ }
+ }
+
return parent::settingsForm($form, $form_state) + [
'muted' => [
'#title' => $this->t('Muted'),
@@ -66,6 +92,22 @@ public function settingsForm(array $form, FormStateInterface $form_state) {
'#min' => 0,
'#required' => TRUE,
],
+ 'poster' => [
+ '#type' => 'select',
+ '#title' => $this->t('Poster field'),
+ '#description' => empty($image_fields) ? $this->t('Please add an image field to this media entity to use the poster feature.') : NULL,
+ '#default_value' => $this->getSetting('poster'),
+ '#options' => $image_fields,
+ '#empty_option' => $this->t('- None -'),
+ ],
+ 'transcript' => [
+ '#type' => 'select',
+ '#title' => $this->t('Transcript field'),
+ '#description' => empty($file_fields) ? $this->t('Please add a file field to this media entity to use the transcript feature.') : NULL,
+ '#default_value' => $this->getSetting('transcript'),
+ '#options' => $file_fields,
+ '#empty_option' => $this->t('- None -'),
+ ],
];
}
@@ -79,9 +121,77 @@ public function settingsSummary() {
'%width' => $this->getSetting('width'),
'%height' => $this->getSetting('height'),
]);
+ if (!empty($this->getSetting('poster'))) {
+ $summary[] = $this->t('Poster field: %poster', ['%poster' => $this->getSetting('poster')]);
+ }
+ if (!empty($this->getSetting('transcript'))) {
+ $summary[] = $this->t('Transcript field: %transcript', ['%transcript' => $this->getSetting('transcript')]);
+ }
return $summary;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function viewElements(FieldItemListInterface $items, $langcode) {
+ $elements = parent::viewElements($items, $langcode);
+
+ /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
+ $entity = $items->getEntity();
+ $poster_field = $this->getSetting('poster');
+ if (!empty($poster_field)) {
+ if (!$entity->get($this->getSetting('poster'))->isEmpty()) {
+ $file_item = $entity->get($this->getSetting('poster'))[0];
+ $file_entity = $file_item->entity;
+ }
+ else {
+ // Get default image if field is empty.
+ $field = $entity->getFieldDefinition($poster_field);
+ $field_storage = $field->getFieldStorageDefinition();
+ $default_image = $field_storage->getSetting('default_image');
+ if (!empty($default_image['uuid'])) {
+ // Convert the stored UUID into a file ID.
+ $file_entity = \Drupal::service('entity.repository')->loadEntityByUuid('file', $default_image['uuid']);
+ }
+ }
+ if (!empty($file_entity)) {
+ // Set the entity in the correct language for display.
+ if ($file_entity instanceof TranslatableInterface && $file_entity->hasTranslation($langcode)) {
+ $file_entity = $file_entity->getTranslation($langcode);
+ }
+
+ $poster = $file_entity->createFileUrl();
+
+ $poster_attributes = new Attribute();
+ $poster_attributes->setAttribute('poster', $poster);
+ $elements[0]['#attributes']->merge($poster_attributes);
+ }
+ }
+
+ if (!empty($this->getSetting('transcript')) && !$entity->get($this->getSetting('transcript'))->isEmpty()) {
+ $file_item = $entity->get($this->getSetting('transcript'))[0];
+ $file_entity = $file_item->entity;
+
+ // Set the entity in the correct language for display.
+ if ($file_entity instanceof TranslatableInterface && $file_entity->hasTranslation($langcode)) {
+ $file_entity = $file_entity->getTranslation($langcode);
+ }
+
+ $transcript = $file_entity->createFileUrl();
+ $languages = \Drupal::languageManager()->getNativeLanguages();
+
+ // Handle language object with and without language module enabled.
+ $label = ($languages[$langcode] instanceof Language) ? ($languages[$langcode]->getName()) : ($languages[$langcode]->label());
+ $elements[0]['#transcript'] = [
+ 'file' => $transcript,
+ 'srclang' => $langcode,
+ 'label' => $label,
+ ];
+ }
+
+ return $elements;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/core/modules/file/templates/file-video.html.twig b/core/modules/file/templates/file-video.html.twig
index 162fd4933e..78b22433f8 100644
--- a/core/modules/file/templates/file-video.html.twig
+++ b/core/modules/file/templates/file-video.html.twig
@@ -11,6 +11,7 @@
* - file: The full file object.
* - source_attributes: An array of HTML attributes for to be added to the
* source tag.
+* - transcript file: a vtt file to be added as a track
*
* @ingroup themeable
*/
@@ -19,4 +20,7 @@
{% for file in files %}
{% endfor %}
+ {% if not transcript is empty %}
+
+ {% endif %}
diff --git a/core/modules/file/tests/src/Functional/Formatter/FileVideoPosterFormatterTest.php b/core/modules/file/tests/src/Functional/Formatter/FileVideoPosterFormatterTest.php
new file mode 100644
index 0000000000..e13f9c5085
--- /dev/null
+++ b/core/modules/file/tests/src/Functional/Formatter/FileVideoPosterFormatterTest.php
@@ -0,0 +1,124 @@
+randomMachineName());
+ $this->createFileField($video_fieldname, 'node', 'article', [], ['file_extensions' => 'mp4']);
+
+ // Image field configuration.
+ $poster_fieldname = 'field_' . mb_strtolower($this->randomMachineName());
+ $this->createImageField($poster_fieldname, 'article', [], ['file_extensions' => 'jpg']);
+
+ // Transcript field configuration.
+ $transcript_fieldname = 'field_' . mb_strtolower($this->randomMachineName());
+ $this->createFileField($transcript_fieldname, 'node', 'article', [], ['file_extensions' => 'txt']);
+
+ // Configure node display.
+ $display = \Drupal::service('entity_display.repository')
+ ->getViewDisplay('node', 'article');
+ $display_options = [
+ 'type' => 'file_video',
+ 'settings' => [
+ 'poster' => $poster_fieldname,
+ 'transcript' => $transcript_fieldname,
+ ],
+ ];
+ $display->setComponent($video_fieldname, $display_options)
+ ->save();
+
+ // Create files.
+ file_put_contents('public://file.mp4', str_repeat('t', 10));
+ $video1 = File::create([
+ 'uri' => 'public://file.mp4',
+ 'filename' => 'file.mp4',
+ ]);
+ $video1->save();
+ $file_url = \Drupal::service('file_url_generator')->generate($video1->getFileUri())->toString();
+
+ file_put_contents('public://file.jpg', str_repeat('t', 10));
+ $poster1 = File::create([
+ 'uri' => 'public://file.jpg',
+ 'filename' => 'file.jpg',
+ ]);
+ $poster1->save();
+ $poster_url = \Drupal::service('file_url_generator')->generate($poster1->getFileUri())->toString();
+
+ file_put_contents('public://file.vtt', str_repeat('t', 10));
+ $transcript1 = File::create([
+ 'uri' => 'public://file.vtt',
+ 'filename' => 'file.vtt',
+ ]);
+ $transcript1->save();
+ $transcript_url = \Drupal::service('file_url_generator')->generate($transcript1->getFileUri())->toString();
+
+ // Create test node.
+ $node = $this->drupalCreateNode([
+ 'title' => 'Hello, world!',
+ 'type' => 'article',
+ $video_fieldname => [
+ [
+ 'target_id' => $video1->id(),
+ ],
+ ],
+ $poster_fieldname => [
+ [
+ 'target_id' => $poster1->id(),
+ ],
+ ],
+ $transcript_fieldname => [
+ [
+ 'target_id' => $transcript1->id(),
+ ],
+ ],
+ ]);
+ $node->save();
+
+ $this->drupalGet('/node/' . $node->id());
+ $assert_session = $this->assertSession();
+ $assert_session->elementExists('css', "video > source[src='$file_url'][type='video/mp4']");
+ $assert_session->elementExists('css', "video[poster='$poster_url']");
+ $assert_session->elementExists('css', "video > track[src='$transcript_url']");
+ $assert_session->elementExists('css', "video > track[srclang='en']");
+ $assert_session->elementExists('css', "video > track[label='English']");
+ }
+
+}