Navigating to specific video timestamps. How it’s done (Part 3)

Written by Tom Miller on

Our developer, Tom, walks through how to create a feature that allows users to navigate to specifc points in a video using buttons within the content. Part 3

HeX’s Front-End Developer, Tom, talks us through how he ideated, created and finalised a solution for users to effectively navigate to specific sections of a video embedded on our website. This is part 3.

You can view part one of the blog series here.

You can view part two of the blog series here.

A neat button sits alongside a section of content that explains the section that is being navigated to, allowing the user to pinpoint the information that they would like to view, instead of having to trawl through an entire video

Knowledge required: To read, and be able to understand this blog, it is recommended that you are proficient in the following areas:

  • WordPress
  • HTML
  • Javascript
  • ACF Plugin
  • PHP

It’s worth checking out the finished video feature here.

In the final part of this video blog series, we’re going to be looking at working with the YouTube API and then creating a function to manipulate the <video> <source> URL much like we did in the second part of this blog series.


In the first blog of this series, I detailed how the custom fields were set up using the ACF Pro plugin and displayed in the WordPress CMS. The option of embedding a YouTube iframe via the API is supplied by a user selecting the YouTube radio button and entering the video ID in the field that is displayed using conditional logic. The following mark-up is then displayed:

<?php if( get_field( 'video_source' ) === 'youtube' ) : ?>
 
      <div id="videoContainer" class="responsive-embed widescreen">
        <div id="player"></div>
        <div id="jsoverlay"></div>
      </div>
 
      <script>
 
        var tag = document.createElement('script');
 
        tag.src = "https://www.youtube.com/iframe_api";
        var firstScriptTag = document.getElementsByTagName('script')[0];
        firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
 
        var player;
        function onYouTubeIframeAPIReady() {
          player = new YT.Player('player', {
            height: '390',
            width: '100%',
            videoId: '<?php echo get_field( 'youtube_video_id' ) ;?>',
            playerVars: { 'autoplay': 0, 'rel': 0, 'cc_load_policy': 1  }
          });
        }
 
      </script>
 
    <?php endif; ?>

The video ID from the custom field is supplied as a value to the videoId key in the onYouTubeIframeAPIReady() function as a string. The key/value pairs supplied to the playerVars object have been hardcoded to aid accessibility and relevance:

'autoplay': 0 –  stops the video auto-playing. Autoplay is very much an accessibility issue which this article highlights very well.

'cc_load_policy': 1 – loads closed captions automatically.

'rel': 0 – loads videos from the channel the video ID is from at the end of playing.

We then use all the same repeater fields as the HTML5 video and call another function using an on-click event on the button.

<?php elseif( $button_text && ( get_field( 'video_source' ) === 'youtube' ) ) : ?>
            <a href="#player" class="button video-switcher__button" onclick='changeYouTubeSource(<?php echo $video_start_time . "," . $video_end_time . "," . $poster  . "," . $poster_display_time; ?>)'><?php echo $button_text; ?></a>
          <?php endif; ?>

The final template-part mark-up for both video options now looks like this:

<section id="video-switcher">
  <div class="row">
    <div class="small-12 columns">
 
    <?php if( get_field( 'video_source' ) === 'media-library' ) : ?>
 
      <div id="videoContainer" class="responsive-embed widescreen">
        <video id="videoId" crossorigin="true" controls>
        <?php if( get_field( 'video_mp4_file' ) ) : ?>
          <source src="<?php the_field( 'video_mp4_file' ); ?>" class="video-switcher__source" type="video/mp4" >
        <?php endif; ?>
        <?php if( get_field( 'video_webm_file' ) ) : ?>
          <source src="<?php the_field( 'video_webm_file' ); ?>" class="video-switcher__source" type="video/webm" >
        <?php endif; ?>
        <?php if( get_field( 'video_webm_file' ) ) : ?>
          <source src="<?php the_field( 'video_webm_file' ); ?>" class="video-switcher__source" type="video/ogg" >
        <?php endif; ?>
        <?php if( get_field( 'video_caption_file' ) ) : ?>
          <track src="<?php the_field( 'video_caption_file' ); ?>" label="English" kind="captions" srclang="en" default>
        <?php endif; ?> 
        </video>
        <div id="jsoverlay"></div>
      </div>
 
    <?php endif; ?>
 
    <?php if( get_field( 'video_source' ) === 'youtube' ) : ?>
 
      <div id="videoContainer" class="responsive-embed widescreen">
        <div id="player"></div>
        <div id="jsoverlay"></div>
      </div>
 
      <script>
 
        var tag = document.createElement('script');
 
        tag.src = "https://www.youtube.com/iframe_api";
        var firstScriptTag = document.getElementsByTagName('script')[0];
        firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
 
        var player;
        function onYouTubeIframeAPIReady() {
          player = new YT.Player('player', {
            height: '390',
            width: '100%',
            videoId: '<?php echo get_field( 'youtube_video_id' ) ;?>',
            playerVars: { 'autoplay': 0, 'rel': 0, 'cc_load_policy': 1  }
          });
        }
 
      </script>
 
    <?php endif; ?>
 
    </div>
 
    <?php if( have_rows( 'video_content_repeater' ) ) : ?>
 
      <div class="small-12 columns">
 
      <?php while( have_rows( 'video_content_repeater' ) ) : the_row();
 
      $heading          = get_sub_field( 'video_repeater_heading' );
      $content          = get_sub_field( 'video_repeater_content' );
      $video_start_time = get_sub_field( 'video_start_time' );
 
      if ( get_sub_field( 'video_end_time' ) ) {
        $video_end_time = get_sub_field( 'video_end_time' );
      } else {
        $video_end_time = 0;
      }
      
      $poster               = json_encode( get_sub_field( 'video_poster' ) );
      $poster_display_time  = get_sub_field( 'video_poster_display_time' );
      $button_text          = get_sub_field( 'video_repeater_button_text' );
 
      ?>
 
        <div>
          <?php if( $heading ) : ?>
          <h2 class="video-switcher__heading"><?php echo $heading; ?></h2>
          <?php endif; ?>
 
          <?php if( $content ) : ?>
          <p class="video-switcher__content"><?php echo $content; ?></p>
          <?php endif; ?>
 
          <?php if( $button_text && ( get_field( 'video_source' ) === 'media-library' ) ) : ?>
            <a href="#videoId" class="button video-switcher__button" onclick='changeVideoSource(<?php echo $video_start_time . "," . $video_end_time . "," . $poster . "," . $poster_display_time; ?>)'><?php echo $button_text; ?></a>
          <?php elseif( $button_text && ( get_field( 'video_source' ) === 'youtube' ) ) : ?>
            <a href="#player" class="button video-switcher__button" onclick='changeYouTubeSource(<?php echo $video_start_time . "," . $video_end_time . "," . $poster  . "," . $poster_display_time; ?>)'><?php echo $button_text; ?></a>
          <?php endif; ?>
        </div>
 
      <?php endwhile; ?>
 
      </div>
 
  <?php endif; ?>
 
  </div>
</section>

The function changeYouTubeSource() is very similar to the function is the previous part of this blog series with a couple of slight differences as well are manipulating an iframe src URL.

In this function, there is only the <iframe> src attribute to change so we do not need to loop over an array. 

// Get the src attribute of the YouTube iframe.
var youtubeIframe = document.getElementById('player');
 
var youtubeIframeSrc = document.getElementById('player').getAttribute('src'); 

The YouTube URL uses parameters for start/end times e.g.

https://www.youtube.com/embed/JgA6CnndnMc?autoplay=0&start=5&end=10
So we change these by using a regular expression to check if the URL already has a &start= parameter and replace it if so, as opposed to using media fragments, as we did in part 2.

// If the src attribute URL already contains a media fragment, remove it.
  if (youtubeIframeSrc.match(/&start=/g) ) {
      var mediaFragmentIndex = youtubeIframeSrc.indexOf('&start=');
      trimmedIframeUrl = youtubeIframeSrc.slice(0, mediaFragmentIndex);
 
      if (endTime === 0) {
          iframeUrlTimeStamp = trimmedIframeUrl + '&start=' + startTime;
      } else {
          iframeUrlTimeStamp = trimmedIframeUrl + '&start=' + startTime + '&end=' + endTime;
      }
  }
 
  // If the src attribute URL doesn't contain a media fragment, add one.
  if (youtubeIframeSrc.match(/&start=/g) === null) {
      if (endTime === 0) {
          iframeUrlTimeStamp = youtubeIframeSrc + '&start=' + startTime;
      } else {
          iframeUrlTimeStamp = youtubeIframeSrc + '&start=' + startTime + '&end=' + endTime;
      }
  }

We then check to see if the user has uploaded a new poster to display before each chunk of video and display that using the same JS and CSS as the demonstrated in the previous blog. We also update the YouTube URL autoplay=  parameter from 0 to 1. This is because the video is playing in response to a user input (pressing the button) so we need to video to begin playing after the poster has finished displaying. The final function looks like this:

function changeYouTubeSource(startTime, endTime, videoPoster, posterDisplayTime) {
 
  // Get the src attribute of the YouTube iframe.
  var youtubeIframe = document.getElementById('player');
 
  var youtubeIframeSrc = document.getElementById('player').getAttribute('src');
 
  // Variables for assigning in the condition below.
  var trimmedIframeUrl = '';
  var iframeUrlTimeStamp = '';
 
  // If the src attribute URL already contains a media fragment, remove it.
  if (youtubeIframeSrc.match(/&start=/g) ) {
      var mediaFragmentIndex = youtubeIframeSrc.indexOf('&start=');
      trimmedIframeUrl = youtubeIframeSrc.slice(0, mediaFragmentIndex);
 
      if (endTime === 0) {
          iframeUrlTimeStamp = trimmedIframeUrl + '&start=' + startTime;
      } else {
          iframeUrlTimeStamp = trimmedIframeUrl + '&start=' + startTime + '&end=' + endTime;
      }
  }
 
  // If the src attribute URL doesn't contain a media fragment, add one.
  if (youtubeIframeSrc.match(/&start=/g) === null) {
      if (endTime === 0) {
          iframeUrlTimeStamp = youtubeIframeSrc + '&start=' + startTime;
      } else {
          iframeUrlTimeStamp = youtubeIframeSrc + '&start=' + startTime + '&end=' + endTime;
      }
  }
 
  // Apply the video poster image as a background image.
  if (videoPoster && posterDisplayTime) {
    document.getElementById('jsoverlay').setAttribute('style', "background-image: url('" + videoPoster + "'); display: block");
 
    // User inputted length of time to display poster.
    setTimeout(function() {
        document.getElementById('jsoverlay').setAttribute('style',"display: none" );
        
        // Enable autoplay on the new URL.
        var iframeAutoplayUrl = iframeUrlTimeStamp.replace('autoplay=0', 'autoplay=1' );
 
        // Set the src attribute as the original URL with the media fragment appended.
        youtubeIframe.setAttribute('src', iframeAutoplayUrl);
    }, posterDisplayTime * 1000);
  } else {
    // 1 second delay to allow for scrolling to video.
    setTimeout(function() {
        // Enable autoplay on the new URL.
        var iframeAutoplayUrl = iframeUrlTimeStamp.replace('autoplay=0', 'autoplay=1' );
 
        // Set the src attribute as the original URL with the media fragment appended.
        youtubeIframe.setAttribute('src', iframeAutoplayUrl);
    }, 1000);
  }
}

The final change we made before deploying live was to merge both functions and add a condition to check if id="videoId" or id="player" is present in the DOM which would then execute the next part of the function.

So that completes this blog series, I hope you’ve found it useful and instructive. It was an enjoyable challenge and you’ll be seeing this feature very soon across www.horlix.com and www.accessibilitynottingham.co.uk.

Skip to main content