We can use WebVTT files to define chapters. The syntax is exactly the same as for subtitles/caption .vtt files. The only difference is in the declaration of the track. Here is how we declared a chapter track in one of the previous examples (in bold in the example below):
HTML code:
<video id="myVideo" preload="metadata" controls crossOrigin="anonymous">
<source src="https://...../elephants-dream-medium.mp4"
type="video/mp4">
<source src="https://...../elephants-dream-medium.webm"
type="video/webm">
<track label="English subtitles"
kind="subtitles"
srclang="en"
src="https://...../elephants-dream-subtitles-en.vtt" >
<track label="Deutsch subtitles"
kind="subtitles"
srclang="de"
src="https://...../elephants-dream-subtitles-de.vtt"
default>
<track label="English chapters"
kind="chapters"
srclang="en"
src="https://...../elephants-dream-chapters-en.vtt">
</video>
If we try this code in an HTML document, nothing special happens. No magic menu, no extra button!
Currently, no browser takes chapter tracks into account. You could use one of the enhanced video players presented during the W3Cx HTML5 Coding Essentials course, but as you will see in this lesson: making your own chapter navigation menu is not complicated.
elephant-dream-chapters-en.vtt:
WEBVTT
chapter-1
00:00:00.000 --> 00:00:26.000
Introduction
chapter-2
00:00:28.206 --> 00:01:02.000
Watch out!
chapter-3
00:01:02.034 --> 00:03:10.000
Let's go
chapter-4
00:03:10.014 --> 00:05:40.000
The machine
chapter-5
00:05:41.208 --> 00:07:26.000
Close your eyes
chapter-6
00:07:27.125 --> 00:08:12.000
There's nothing there
chapter-7
00:08:13.000 --> 00:09:07.500
The Colossus of Rhodes
There are 7 cues (one for each chapter). Each cue id is the word "chapter-" followed by the chapter number, then we have the start and end time of the cue/chapter, and the cue content. In this case: the description of the chapter ("Introduction", "Watch out!", "Let's go", etc...).
Hmm... let's try to open this chapter track with the example we wrote earlier - the one that displayed the clickable transcript for subtitles/captions on the right of the video. We need to modify it a little bit:
Let's add a "show English chapters" button with a click event listener similar to this :
<button disabled id="buttonEnglishChapters" onclick="loadTranscript('en', 'chapters');">
Display English chapter markers
</button>
We modify the loadTranscript function from the previous example, so that it matches both the srclang and the kind attribute of the track.
Here is a new version: in bold are the source code lines we modified.
function loadTranscript(lang, kind) {
...
// Locate the track with lang and kind that match the parameters
for(var i = 0; i < tracks.length; i++) {
...
if((track.language === lang) && (track.kind === kind)) {
// display it contents...
}
}
}
This version includes the modifications we presented earlier - nothing more. Notice that we kept the existing buttons to display a clickable transcript:
HTML code:
<html lang="en">
<head>
<meta charset=utf-8>
<title>Video player with clickable transcript</title>
</head>
<body>
<section id="all">
<h1>Using the track API to extract the content of webVTT files in <code><track></code> elements</h1>
<p>Click on the buttons under the video to extract the english or german subtitles, or to display the chapter markers (english).
</p>
<p>Look at the HTML and JS code.</p>
<p>
<button disabled id="buttonEnglish" onclick="loadTranscript('en', 'subtitles');">Display English transcript</button>
<button disabled id="buttonDeutsch" onclick="loadTranscript('de', 'subtitles');">Display Deutsch transcript</button>
<button disabled id="buttonEnglishChapters" onclick="loadTranscript('en', 'chapters');">Display English chapter markers</button>
</p>
<video id="myVideo" preload="metadata" controls crossOrigin="anonymous">
<source src="https://mainline.i3s.unice.fr/mooc/elephants-dream-medium.mp4" type="video/mp4">
<source src="https://mainline.i3s.unice.fr/mooc/elephants-dream-medium.webm" type="video/webm">
<track label="English subtitles" kind="subtitles" srclang="en" src="https://mainline.i3s.unice.fr/mooc/elephants-dream-subtitles-en.vtt" >
<track label="Deutsch subtitles" kind="subtitles" srclang="de" src="https://mainline.i3s.unice.fr/mooc/elephants-dream-subtitles-de.vtt" default>
<track label="English chapters" kind="chapters" srclang="en" src="https://mainline.i3s.unice.fr/mooc/elephants-dream-chapters-en.vtt">
</video>
<div id="transcript"></div>
</section>
</body>
</html>
CSS code:
#all {
background-color: lightgrey;
border-radius:10px;
padding: 20px;
border:1px solid;
display:inline-block;
/*height:500px;*/
margin:30px;
width:90%;
}
.cues {
color:blue;
}
.cues:hover {
text-decoration: underline;
}
.cues.current {
color:black;
font-weight: bold;
}
#myVideo {
display: block;
float : left;
margin-right: 2.85714%;
width: 65.71429%;
background-color: black;
position: relative;
}
#transcript {
padding: 10px;
border:1px solid;
float: left;
max-height: 225px;
overflow: auto;
width: 25%;
margin: 0;
font-size: 14px;
list-style: none;
}
JS code:
let video, transcriptDiv;
let tracks, trackElems, tracksURLs = [];
let buttonEnglish, buttonDeutsch, buttonEnglishChapters;
window.onload = () => {
console.log("init");
// when the page is loaded
video = document.querySelector("#myVideo");
transcriptDiv = document.querySelector("#transcript");
// The tracks as HTML elements
trackElems = document.querySelectorAll("track");
for(let i = 0; i < trackElems.length; i++) {
let currentTrackElem = trackElems[i];
tracksURLs[i] = currentTrackElem.src;
}
buttonEnglish = document.querySelector("#buttonEnglish");
buttonDeutsch = document.querySelector("#buttonDeutsch");
buttonEnglishChapters = document.querySelector("#buttonEnglishChapters");
// we enable the buttons and show transcript
buttonEnglish.disabled = false;
buttonDeutsch.disabled = false;
buttonEnglishChapters.disabled = false;
// The tracks as JS objects
tracks = video.textTracks;
};
function loadTranscript(lang, kind) {
// clear current transcript
clearTranscriptDiv();
// set all track mode to disabled. We will only activate the
// one whose content will be displayed as transcript
if(kind !== 'chapters')
disableAllTracks(); // if displaying chapters, do not
// disable all tracks
// Locate the track with language = lang
for(let i = 0; i < tracks.length; i++) {
// current track
let track = tracks[i];
let trackAsHtmlElem = trackElems[i];
if((track.language === lang) && (track.kind === kind)) {
track.mode="showing";
if(trackAsHtmlElem.readyState === 2) {
// the track has already been loaded
displayCues(track);
} else {
displayCuesAfterTrackLoaded(trackAsHtmlElem, track);
}
/* track.addEventListener("cuechange", function(e) {
var cue = this.activeCues[0];
console.log("cue change");
var transcriptText = document.getElementById(cue.id);
transcriptText.classList.add("current");
});
*/
}
}
}
function displayCuesAfterTrackLoaded(trackElem, track) {
// Create a listener that will be called only when the track has
// been loaded
trackElem.addEventListener('load', (e) => {
console.log("track loaded");
displayCues(track);
});
}
function disableAllTracks() {
for(let i = 0; i < tracks.length; i++)
tracks[i].mode = "disabled";
}
function displayCues(track) {
let cues = track.cues;
//append all the subtitle texts to
for(let i=0, len = cues.length; i < len; i++) {
let cue = cues[i];
addCueListeners(cue);
let voices = getVoices(cue.text);
let transText="";
if (voices.length > 0) {
for (let j = 0; j < voices.length; j++) { // how many voices ?
transText += voices[j].voice + ': ' + removeHTML(voices[j].text);
}
} else
transText = cue.text; // not a voice text
let clickableTransText = "<li class='cues' id=" + cue.id + " onclick='jumpTo(" + cue.startTime + ");'" + ">" + transText + "</li>";
addToTranscriptDiv(clickableTransText);
}
}
function getVoices(speech) { // takes a text content and check if there are voices
let voices = []; // inside
let pos = speech.indexOf('<v'); // voices are like <v michel> ....
while (pos != -1) {
endVoice = speech.indexOf('>');
let voice = speech.substring(pos + 2, endVoice).trim();
let endSpeech = speech.indexOf('</v>');
let text = speech.substring(endVoice + 1, endSpeech);
voices.push({
'voice': voice,
'text': text
});
speech = speech.substring(endSpeech + 4);
pos = speech.indexOf('<v');
}
return voices;
}
function removeHTML(text) {
let div = document.createElement('div');
div.innerHTML = text;
return div.textContent || div.innerText || '';
}
function jumpTo(time) {
video.currentTime = time;
video.play();
}
function clearTranscriptDiv() {
transcriptDiv.innerHTML = "";
}
function addToTranscriptDiv(htmlText) {
transcriptDiv.innerHTML += htmlText;
}
function addCueListeners(cue) {
cue.onenter = (e) =>{
console.log('enter id=' + e.target.id);
let transcriptText = document.getElementById(e.target.id);
transcriptText.classList.add("current");
};
cue.onexit = (e) => {
console.log('exit id=' + e.target.id);
let transcriptText = document.getElementById(e.target.id); transcriptText.classList.remove("current");
};
}
Look at the JavaScript and HTML tabs to see the source codes. It's the same as in the clickable transcript example, except for the small changes we explained earlier.
Chapter navigation, illustrated in the screenshot of a video player below, is fairly popular.
In addition to the clickable chapter list, this one displays an enhanced progress bar created using a canvas. The small squares are drawn corresponding to the chapter cues' start and end times. You could modify the code provided, in order to add such an enhanced progress indicator.
However, we will see how we can do better by using JSON objects as cue contents. This will be the topic of the next two lessons!