HTML5 Video Kirby Tag for Kirby 3

A Kirby tag for embedding local videos in textareas…

2 Likes

just an info:
it seems https://getkirby.com/plugins/hashandsalt/video should link to your plugin… but it links to this one:
https://getkirby.com/plugins/schnti/video


little “bug”: in index.php line 27 and 31 are the same =)


Feature-Request:
for usage with players like plyr or video.js, it would be helpful to have the option of multiple video-sources in one player.

generating code like this:

<video controls>
 <source src="Video-480p.m4v" type="video/mp4" size="480" />
 <source src="Video-576p.m4v" type="video/mp4" size="576" />
 <source src="Video-720p.m4v" type="video/mp4" size="720" />
 <source src="Video-1080p.m4v" type="video/mp4" size="1080" />
</video>

what do you think about?

Hrmm yes that does seem to be a mistake. @texnixe could you fix that please?

I probably wont be adding multiple sources. The plugin allows using your own snippet for customisation, either by using your own snippet and specifing it on the kirby tag or by copying the one from the plugin into the usual snippets folder so that you dont have to. Probably best to find the other sizes based on filename, otherwise you would end up with a long tag.

You can also swap out the JSON schema object, I have since written a plugin for doing this in a better way:

1 Like

You could, btw, shorten your plugin, all these duplicate variable definitions are not necessary:

<?php

Kirby::plugin('hashandsalt/video', [

  'snippets' => [
    // PLAYER
    'html5video'    => __DIR__ . '/snippets/video.php'
  ],

  'tags' => [
      'vidembed' => [
        'attr' => [
          'width',
          'height',
          'poster',
          'caption',
          'title',
          'class',
          'vidclass',
          'caption',
          'preload',
          'controls',
          'snippet'
        ],
        'html' => function($tag) {

          $file         = $tag->parent()->file($tag->value);
          $alt          = $tag->alt;
          $title        = $tag->title;

          if($file) {
            if(empty($alt) and $file->alt() != '') {
              $alt = $file->alt();
            }
            if(empty($title) and $file->title() != '') {
              $title = $file->title();
            }
          }

          if(empty($alt)) $alt = pathinfo($tag->video, PATHINFO_FILENAME);

          $args = array(
            'video'     => $file,
            'videourl'  => $file ? $file->url() : '',
            'poster'    => $poster = $tag->parent()->file($tag->poster),
            'posterurl' => $poster ? $poster->url() : '',
            'width'     => $tag->width ?? 400,
            'height'    => $tag->height ?? 300,
            'class'     => $tag->class,
            'vidclass'  => $tag->vidclass,
            'preload'   => $tag->preload ?? 'preload',
            'caption'   => $tag->caption,
            'controls'  => $tag->controls ?? 'controls',
            'title'     => $title,
            'alt'       => $alt,
            'mime'      => $file ? $file->mime() : '',
            'modified'  => $file ? $file->modified('%d/%m/%Y', 'strftime') : ''

          );

          $snippet  = $tag->snippet ?? 'html5video';
          $video = snippet($snippet, $args, true);

          return $video;
        }
      ]
    ]
]);

Im not sure I follow… that was the shortest I could come up with at the time.

I posted above what I think is the shortest, just a suggestion.

Ah! thanks… i thought that was my code!

hey there…
i try to solve the multiple sources “challenge”.

what do you think is the best way to go for?
my idea is to use a “video.list” file like this:

Video-480p.m4v,480
Video-576p.m4v,576
Video-720p.m4v,720
Video-1080p.m4v,1080

and then iterating via foreach over each line to pass the comma-seperated values into arrays.

but how to generate the correct media-folder url to each file?

I would probably take the file name from the tag (In your case Video) then loop through the page files array and find all those files plus the suffix (since those are always the same). Then just check that each one phyiscally exits and get the URLs from there.

It would be unweildly and confusing for the user to set all the file names on the kirbytag itself.

thx a lot…

the idea was to upload all video files + a “video.list” text-file into the page folder…

this text-file should work as a TOC
and should be loaded like (vidembed: video.list)

not a good idea?

at what stage are media-urls generated? page->file() just shows folder-content

i’m sorry, if those questions are not related enough for this plugin but more to kirby-itself

Well lets break it down to things we know:

  1. Filename
  2. Known set of possible sizes
  3. We might not have files for all sizes
  4. We have page files that might all be videos, but might be images or other types too, so we need to filter

You could use filterby to find all the files containing a string, which in this case is the filename:

$files = $page->files()->filterBy('filename', '*=', 'filenamefromtag');

That should give you all the files called mycoolvideo-480p or mycoolvideo-1080p in an array.

Then you can loop over that, check each one physically exists and generate your source tag from that. Also check the file extensions are a valid video extension like m4v or mp4.

For extra safety, you could also check the filename also contains one of the expected sizes.

I wouldnt drive this from a video.list file as you plan, since that is an extra point of failure. For one, it relies on someone making that file and making it correctly, secondly if that file accidently gets deleted, then your player will break and its not a quick fix (you need to make a new file and upload it). Kirby also sanatises filenames on upload, so what you declare in that file might not reflect the resulting filename after upload. (files are lowercased and special charachters stripped or converted amongst other things).

It should be driven from the available files, rather then a custom playlist.

this is realy helpfull, thx a lot!
i’ll give it a try…

If you did…

$files = $page->videos()->filterBy('filename', '*=', 'filenamefromtag');

You can skip checking the file extension, since Kirby will only return files with the following extensions:

mov
movie
avi
ogg
ogv
webm
flv
swf
mp4
m4v
mpg
mpe

in my usecase this worked…
it is for sure not good code, but maybe a good start for anyone else…
i modified video.php snippet to my needs: using a videoplayer with quality-selector.

if you want to use this, you have to put js and css of plyr videoplayer into coresponding assets-folder.

This snippets searches for videofiles with same base-filename and different qualities to add them to the player…
example:

base-filename-480.m4v
base-filename-576.m4v
base-filename-720.m4v
base-filename-1080.m4v

This works also with multiple players on the same site… and you can change control-settings inside snippet

<?php
$pattern = "/(\d{3,4})$/";
$cutname = preg_replace($pattern, '', $video->name()) ;

$posterimage = Html::tag('img', null, ['src' => $posterurl, 'alt' => $alt]);

if ($page->videos()->filterBy('filename', '^=', $cutname)->count()  > 1) {
  foreach($page->videos()->filterBy('filename', '^=', $cutname) as $file) {
      preg_match($pattern, $file->name(), $vidres);
       if ( (!empty($vidres[0])) AND ($file->extension() == $video->extension()) ){
        $videsourcetag .= Html::tag('source', null, ['src' => $file->url(), 'type' => $mime, 'size' => $vidres[0]]);
      }
  }
} else {
  $videsourcetag = Html::tag('source', null, ['src' => $videourl, 'type' => $mime]);
}  

$videsourcetag .= Html::tag('a', [$posterimage], ['href' => $videourl]);
$vidtag = Html::tag('video', [$videsourcetag], ['poster' => $posterurl, 'width' => $width, 'height' => $height, 'controls' => $controls, 'preload' => $preload, 'class' => $vidclass]);

if($caption) {
  $cap = Html::tag('figcaption', [$caption]);
} else {
  $cap = null;
}

$vidtag .= $cap;
$player = Html::tag('figure', [$vidtag], ['class' => $class]);

?>

<!-- Embed plyr Player - CSS -->
<?= css(['assets/css/plyr.css']) ?>
<!-- / Embed plyr Player - CSS -->

<!-- Video Player -->
<?= $player ?>
<script type="application/ld+json">
  {
    "@context": "http://schema.org",
    "@type": "VideoObject",
    "name": "<?= $title ?>",
    "description": "<?= $caption ?>",
    "thumbnailUrl": "<?= $posterurl ?>",
    "contentUrl": "<?= $videourl ?>",
    "uploadDate": "<?= $modified ?>"
  }
</script>

<!-- / Video Player -->

<!-- Embed plyr Player - JS -->
<?= js('assets/js/plyr.min.js') ?>

<script>
  // Change the second argument to your options:
  // https://github.com/sampotts/plyr/#options
const players = Array.from(document.querySelectorAll('video')).map(p => new Plyr(p, {

controls: [
      'play-large', // The large play button in the center
//    'restart', // Restart playback
//    'rewind', // Rewind by the seek time (default 10 seconds)
      'play', // Play/pause playback
//    'fast-forward', // Fast forward by the seek time (default 10 seconds)
      'progress', // The progress bar and scrubber for playback and buffering
      'current-time', // The current time of playback
      'duration', // The full duration of the media
      'mute', // Toggle mute
      'volume', // Volume control
      'captions', // Toggle captions
      'settings', // Settings menu
//    'download', // Show a download button with a link to either the current source or a custom URL you specify in your options
      'fullscreen', // Toggle fullscreen
      ],

settings: [
 'captions', 
 'quality', 
 'loop',
 ]
}));
// Expose player
window.player = player;
</script>
<!-- / Embed plyr Player - JS -->

i just added automatic subtitle detection + integration.
still not good code… but working code =)

If there is 1 or more vtt files but only 1 video without resolution in filename, then a vtt file is used for poster-url… thats weird.

for example with those two files max-interview.mp4 and max-interview-de.vtt

<?php
/*
 pattern for video files:
   base-filename-480.m4v
   base-filename-576.m4v
   base-filename-720.m4v
   base-filename-1080.m4v

 pattern for vtt subtitle files:
   base-filename-de.vtt
   base-filename-en.vtt
   base-filename-fr.vtt

*/

$afterlasthyphen = "/[^-]*$/";
$qualitypattern = "/(\d{3,4})$/";
$cutname = preg_replace($qualitypattern, '', $video->name()) ;

$posterimage = Html::tag('img', null, ['src' => $posterurl, 'alt' => $alt]);

if ($page->videos()->filterBy('filename', '^=', $cutname)->count()  > 1) {
  foreach($page->videos()->filterBy('filename', '^=', $cutname) as $file) {
      preg_match($qualitypattern, $file->name(), $vidres);
       if ( (!empty($vidres[0])) AND ($file->extension() == $video->extension()) ){
        $videsourcetag .= Html::tag('source', null, ['src' => $file->url(), 'type' => $mime, 'size' => $vidres[0]]);
      }
  }
} else {
  $videsourcetag = Html::tag('source', null, ['src' => $videourl, 'type' => $mime]);
}  

foreach($page->files()->filterBy('filename', '^=', $cutname) as $file) {
      if ( $file->extension() == "vtt" ) {
      preg_match($afterlasthyphen, $file->name(), $langsrc);
      $langcode = $langsrc[0];
        $videsourcetag .= Html::tag('track', null, ['kind' => "captions", 'label' => Locale::getDisplayLanguage($langcode, $langcode), 'srclang' => $langcode, 'src' => $file->url(), 'default' => ""]);
      }

  }

$videsourcetag .= Html::tag('a', [$posterimage], ['href' => $videourl]);
$vidtag = Html::tag('video', [$videsourcetag], ['poster' => $posterurl, 'width' => $width, 'height' => $height, 'controls' => $controls, 'preload' => $preload, 'class' => $vidclass]);

if($caption) {
  $cap = Html::tag('figcaption', [$caption]);
} else {
  $cap = null;
}

$vidtag .= $cap;
$player = Html::tag('figure', [$vidtag], ['class' => $class]);



?>

<!-- Embed plyr Player - CSS -->
<?= css(['assets/css/plyr.css']) ?>
<!-- / Embed plyr Player - CSS -->

<!-- Video Player -->
<?= $player ?>

<script type="application/ld+json">
  {
    "@context": "http://schema.org",
    "@type": "VideoObject",
    "name": "<?= $title ?>",
    "description": "<?= $caption ?>",
    "thumbnailUrl": "<?= $posterurl ?>",
    "contentUrl": "<?= $videourl ?>",
    "uploadDate": "<?= $modified ?>"
  }
</script>

<!-- / Video Player -->

<!-- Embed plyr Player - JS -->
<?= js('assets/js/plyr.min.js') ?>

<script>
  // Change the second argument to your options:
  // https://github.com/sampotts/plyr/#options
const players = Array.from(document.querySelectorAll('video')).map(p => new Plyr(p, {
  
controls: [
      'play-large', // The large play button in the center
//    'restart', // Restart playback
//    'rewind', // Rewind by the seek time (default 10 seconds)
      'play', // Play/pause playback
//    'fast-forward', // Fast forward by the seek time (default 10 seconds)
      'progress', // The progress bar and scrubber for playback and buffering
      'current-time', // The current time of playback
      'duration', // The full duration of the media
      'mute', // Toggle mute
      'volume', // Volume control
      'captions', // Toggle captions
      'settings', // Settings menu
//    'download', // Show a download button with a link to either the current source or a custom URL you specify in your options
      'fullscreen', // Toggle fullscreen
      ],

settings: [
 'captions', 
 'quality', 
 'loop',
 ]
}));
// Expose player
window.player = player;
</script>
<!-- / Embed plyr Player - JS -->

1 Like