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']"); + } + +}