I’ve just implemented Video timestamp navigation on videos embeded on SupeRails:

superails chapter navigation inside video

I was inspired by Chapters on Youtube:

youtube video chapters

In my app I embed videos with Vimeo.

To navigate to a timestamp in the vimeo player, you need to use the https://github.com/vimeo/player.js API.

First, install the package https://github.com/vimeo/player.js

./bin/importmap pin @vimeo/player
rails g stimulus vimeo

Stimulus controller to click play, or navigate to a timestamp:

// app/javascript/controllers/vimeo_controller.js
import { Controller } from "@hotwired/stimulus"
import Player from '@vimeo/player';

// Connects to data-controller="vimeo"
export default class extends Controller {
  static targets = ["player"]

  connect() {
    // continue only if the embedded video is from Vimeo
    try {
      this.player = new Player(this.playerTarget);
    } catch {}
  }

  play(event) {
    event.preventDefault()
    this.player.play()
  }

  setCurrentTime(event) {
    event.preventDefault()
    this.player.setCurrentTime(event.target.dataset.time)
  }
}

Embed a vimeo player, and add play and chapter navigation buttons:

<div data-controller="vimeo">
  <iframe src="https://player.vimeo.com/video/1023706220?h=7e9bb172b6"
          frameborder="0"
          data-vimeo-target="player"
          allowfullscreen>

  <button data-action="click->vimeo#play">play</button>
  <button data-action="vimeo#setCurrentTime" data-time="40">40</button>
  <button data-action="vimeo#setCurrentTime" data-time="90">90</button>
</div>

Parse timestamps from text #

To add timestamps/chapters on Youtube, you simply edit a video description and type in the timestamps.

Next, Youtube parses your video description and extracts the timestamp time and description values.

We can parse a video description with Regex, and turn timestamps into buttons:

# app/helpers/application_helper.rb
  # find all timestamps (0:00, 5:31, etc) in a text and convert them to buttons
  def timestamps_to_buttons(text)
    text.gsub(/(\d+:\d+)/) do |match|
      time = match.split(':').map(&:to_i)
      seconds = time[0] * 60 + time[1]
      "<button data-action=\"vimeo#setCurrentTime\" data-time=\"#{seconds}\" style=\"color: blue;\">#{match}</button>"
    end
  end

You can even turn the timestamps & titles into JSON:

# app/helpers/application_helper.rb
  def timestamps_array_from_text(text)
    text.scan(/(\d+:\d+)\s(.+)/).map do |match|
      time = match[0].split(':').map(&:to_i)
      seconds = (time[0] * 60) + time[1]
      { human_time: match[0], time: seconds, text: match[1] }
    end
  end

Test the parser:

  test 'timestamps_array_from_text' do
    text = "hello world 0:00 Introduction\n0:10 First section\n2:30 Second section"
    expected = [
      { human_time: '0:00', time: 0, text: 'Introduction' },
      { human_time: '0:10', time: 10, text: 'First section' },
      { human_time: '2:30', time: 150, text: 'Second section' }
    ]

    assert_equal expected, timestamps_array_from_text(text)
  end

Finally, display styled, clickable timestamps anywhere on your page (but within data-controller="vimeo"):

<ul>
  <% timestamps_array_from_text(text).each do |timestamp| %>
    <li>
      <button title="<%= timestamp[:text] %>" style="color: blue" data-action="vimeo#setCurrentTime" data-time="<%= timestamp[:time] %>"><%= timestamp[:human_time] %></button>
      <%= timestamp[:text] %>
    </li>
  <% end %>
</ul>

That’s it! Custom chapter navigation works!