EmacsConf backstage: chapter markers

| emacs

Long videos are easier to navigate with chapter markers, so I've been slowly adding chapter markers to the Q&A sessions for EmacsConf 2021. I wrote an IkiWiki template and some Javascript code so that adding chapter markers to the EmacsConf wiki should be just a matter of as adding something like this:

[[!template id="chapters" vidid="mainVideo" data="""
00:00 Introduction
00:11 Upcoming Emacs 28 release
00:24 Org mode 9.5
00:57 Magit major release
01:18 Completion
01:51 Embark
02:12 tree-sitter
02:44 Collaborative editing
03:03 Graphical experiments
03:41 Community
04:00 libera.chat
"""]]

That way, updating the talk pages with chapter descriptions should be less reliant on my Emacs Lisp functions for generating HTML, so it's more likely to be something other people can do.

If you happen to be interested in Emacs and you're planning to watch the talks or Q&A sessions anyway, you can help add chapter markers to videos that don't have them yet. You can either edit the wiki yourself or e-mail me chapter timestamps at . You can also help out by cross-referencing the chapter timestamps with the discussion session on the page, so that people reading the questions can see where to find the answers. If you're feeling extra-helpful, you could even write down the answers for easy reference.

Here are a few pages that have long Q&A sessions. I've linked to the autogenerated captions in the Discussion sections.

You can call dibs by editing https://etherpad.wikimedia.org/p/emacsconf-2021-volunteers .

Little steps towards making things easier to find! =)

Behind the scenes

I used the auto-generated captions from YouTube as a starting point, since I could skim them easily. I found that the .ass format was easier to speed-read than the .vtt format, so I used ffmpeg to convert them. Then I used emacsconf-subed-mark-chapter from emacsconf-subed to capture the timestamps as a .vtt file.

This is what part of the autogenerated captions looks like:

...
Dialogue: 0,0:01:16.11,0:01:18.11,Default,,0,0,0,,First of all, in your opinion, what is
Dialogue: 0,0:01:18.11,0:01:20.11,Default,,0,0,0,,Emacs' achilles heel? it's obviously a
Dialogue: 0,0:01:20.11,0:01:22.35,Default,,0,0,0,,powerful tool but no tool is perfect
...

and this is part of the chapters file I made:

00:00:26.319 --> 00:03:09.598
In your opinion, what is Emacs' Achilles heel?

00:03:09.599 --> 00:05:06.959
What is your opinion about the documentation of Emacs in other languages?
...

I converted the timestamps to a simple text format handy for including in video descriptions and on the wiki.

[[!template id="chapters" vidid="qanda" data="""
00:00 Thanks
00:26 In your opinion, what is Emacs' Achilles heel?
03:09 What is your opinion about the documentation of Emacs in other languages?
...
]]

A number of Emacs users browse the web without Javascript, so I wanted the chapter information to be available even then. Putting all the data into a pre tag seems like the easiest way to do it with an ikiwiki template. Here's the template I used:

<pre class="chapters" data-target="<TMPL_VAR vidid>">
<TMPL_VAR data>
</pre>

I also modified the IkiWiki htmlscrubber.pm plugin to allow the attributes I wanted, like data-target and data-start.

If Javascript was enabled, I wanted people to be able to click on the chapters in order to jump to the right spot in the video. I split the content into lines, parsed out the timestamps, and replaced the pre tag with the list of links. I also added the chapters as a hidden track in the video so that I could use the cuechange event to highlight the current chapter. This is what I added to the page.tmpl:

<script>
 // @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt txt CC0-1.0
 // Copyright (c) 2021 Sacha Chua - CC0 Public Domain
 function displayChapters(elem) {
   var i;
   var chapter;
   var list = document.createElement('ol');
   list.setAttribute('class', 'chapters');
   var link;
   var target = elem.getAttribute('data-target');
   var video = document.getElementById(target);
   var track;
   if (video) {
     track = video.addTextTrack('chapters');
     track.mode = 'hidden';
   }
   var chapters = elem.textContent.split(/[ \t]*\n+[ \t]*/).forEach(function(line) {
     var m = (line.match(/^(([0-9]+:)?[0-9]+:[0-9]+)[ \t]+(.*)/));
     if (m) {
       var start = m[1];
       var text = m[3];
       chapter = document.createElement('li');
       link = document.createElement('a');
       link.setAttribute('href', '#');
       link.setAttribute('data-video', target);
       link.setAttribute('data-start', start);
       link.setAttribute('data-start-s', parseSeconds(start));
       link.appendChild(document.createTextNode(m[1] + ' ' + text));
       link.onclick = handleSubtitleClick;
       chapter.appendChild(link);
       list.appendChild(chapter);
       if (track) {
         var time = parseSeconds(start);
         if (track.cues.length > 0) {
           track.cues[track.cues.length - 1].endTime = time - 1;
         }
         track.addCue(new VTTCue(time, time, text));
       }
     }
   })
   if (track && track.cues.length > 0) {
     video.addEventListener('durationchange', function() {
       track.cues[track.cues.length - 1].endTime = video.duration;
     });
     track.addEventListener('cuechange', function() {
       if (!this.activeCues[0]) return;
       if (list.querySelector('.current')) {
         list.querySelector('.current').className = '';
       }
       var chapter;
       if (chapter = list.querySelector('a[data-start-s="' + this.activeCues[0].startTime + '"]')) {
         chapter.parentNode.className = 'current';
       }
     });
   }
   elem.parentNode.replaceChild(list, elem);
 }
  
  document.querySelectorAll('pre.chapters').forEach(displayChapters);

 // @license-end
</script>

handleSubtitleClick is also part of the JS on that page. It sets the current time of the video and scrolls so that the video is in view.

You can comment with Disqus or you can e-mail me at sacha@sachachua.com.