Chapters

mattebox@0.14.0

This chapter covers chapters: named spans of the presentation that a menu or a seek bar shows.

engine.chapters

Chapters come from the manifest, from a file you load, or from data you already hold. The stage fetches manifests and files through the transport, so request hooks such as authentication headers and CMCD apply.

import chapters from 'mattebox/stages/chapters';

const engine = mattebox({ stages: [hlsCmaf(), chapters()] });

await engine.attach(video);
engine.load('https://example.com/vod/master.m3u8');
await engine.chapters.load('https://example.com/vod/chapters.vtt');

const chapter = engine.chapters.at(video.currentTime);
// { id, start, end, title, lang?, image?, data? } or null
Member Meaning
load(url) Fetches and parses a chapters file. Resolves to the chapter count
set(chapters) Sets chapters you already hold. Returns the chapter count; set([]) removes them
at(time) The chapter covering a presentation time, or null
all Every chapter, in start order
source 'none', 'app', or 'manifest'

Chapters you load or set win over the manifest’s. Load or set them after engine.load. Every load, unload, and detach empties both lists, so chapters never carry over to the next source.

Event Payload When
chapters:changed { count, source } The list is replaced or emptied
chapters:warning { url?, reason, id? } An entry was skipped, truncated, or not JSON, or the fetch failed; url is absent for set

From data you already hold

A content API often returns chapters with the media. You map them to { start, end?, title?, id?, lang?, image?, data? }, times in seconds, and pass the list to set.

engine.load(stream);
engine.chapters.set(
  apiChapters.map((c) => ({
    id: c.id,
    start: c.startMs / 1000,
    end: c.endMs / 1000,
    title: c.title,
    image: { url: c.imageUrl },
  })),
);
Rule Result
No end The chapter ends at the next chapter’s start; the last at the end of the presentation
Bad start Not finite or below 0: the entry is skipped with a chapters:warning
No id The start in milliseconds
Overlap The earlier chapter is cut at the next chapter’s start

From the manifest

An HLS multivariant playlist names a chapters document with session data. The stage fetches it once per load, with no call from you.

#EXT-X-SESSION-DATA:DATA-ID="com.apple.hls.chapters",URI="chapters.json"

The document is Apple’s JSON chapters format, the one Safari and Apple TV read. You can also pass such a file to load.

[
  {
    "chapter": 1,
    "start-time": 0,
    "duration": 24,
    "titles": [
      { "language": "en", "title": "Opening" },
      { "language": "fr", "title": "Ouverture" }
    ],
    "images": [
      { "image-category": "thumbnail", "pixel-width": 320, "pixel-height": 180, "url": "at-0.jpg" }
    ]
  }
]
Field Becomes
start-time start; an entry without it is skipped
duration end = start + duration; without it, the next chapter’s start
titles title and lang: the active subtitle language, else und, else the first
images image: the first one; image URLs resolve against the document
chapter id
all of them data, with every title, image, and metadata item

The last chapter without a duration ends where the presentation ends. Chapters that overlap are cut at the next chapter’s start, with a chapters:warning. DASH has no chapters convention; load a file instead.

You read every session-data entry, chapters or not, from engine.stats.snapshot().presentation?.sessionData.

A chapters track

A standard WebVTT chapters file. Each cue’s text is the chapter title. The browser’s <track kind="chapters"> reads the same file.

WEBVTT

00:00:00.000 --> 00:01:00.000
A morning in the meadow

00:01:00.000 --> 00:03:25.000
The rodents
  • A cue id becomes the chapter id.
  • A gap between chapters stays a gap.
  • A chapter that runs into the next is cut at the next one’s start.

A metadata track

A cue’s text may instead be a JSON object with a title, an image, and any data you need. Mattebox defines this format. Other players read it as a chapters track whose titles are raw JSON, so give it to the engine, or to the element as kind="metadata".

WEBVTT

rodents
00:01:00.000 --> 00:03:25.000
{ "title": "The rodents", "image": "images/rodents.jpg", "cast": ["Frank"] }

00:03:25.000 --> 00:05:20.000
{ "title": "The butterfly", "image": { "url": "images/butterfly.jpg", "width": 320, "height": 180 } }
Field Becomes
title title; the empty string when missing
image image: a URL string, or { url, width, height }
lang lang
anything else data, verbatim

Image URLs resolve against the chapters file. JSON cues and plain cues mix in one file. A cue whose JSON does not parse keeps its text as the title and raises chapters:warning.

Example

A chapter label that follows playback.

import { mattebox } from 'mattebox';
import hlsCmaf from 'mattebox/protocols/hls-cmaf';
import chapters from 'mattebox/stages/chapters';

const engine = mattebox({ stages: [hlsCmaf(), chapters()] });

await engine.attach(video);
engine.load('https://example.com/vod/master.m3u8');
await engine.chapters.load('https://example.com/vod/chapters.vtt');

video.addEventListener('timeupdate', () => {
  const chapter = engine.chapters.at(video.currentTime);
  label.textContent = chapter?.title ?? '';
});

Next: 16 Trick play.

Read this page's source on GitHub